Lagrange [work/v1.8]

Media refactoring; working on FontPack management

=> 960df03c17091aca37f53eaab8fc27c669d26a5e

diff --git a/res/arabic.fontpack/fontpack.ini b/res/arabic.fontpack/fontpack.ini
index 00ad5241..305878ce 100644
--- a/res/arabic.fontpack/fontpack.ini
+++ b/res/arabic.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
+version     = 1
+
 [arabic]
 name        = "Noto Sans Arabic UI"
 auxiliary   = true
diff --git a/res/cjk.fontpack/fontpack.ini b/res/cjk.fontpack/fontpack.ini
index fbac54be..6e0d274c 100644
--- a/res/cjk.fontpack/fontpack.ini
+++ b/res/cjk.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
+version     = 1
+
 [notosansjp]
 name        = "Noto Sans JP"
 auxiliary   = true
diff --git a/res/default.fontpack/fontpack.ini b/res/default.fontpack/fontpack.ini
index 68316ef6..f8ef31ce 100644
--- a/res/default.fontpack/fontpack.ini
+++ b/res/default.fontpack/fontpack.ini
@@ -17,6 +17,8 @@
 # `glyphscale` and `voffset` can also be specified separately for the UI and
 # document domains by prefixing `ui.` or `doc.` to the key.
 
+version     = 1
+
 [default]
 name        = "Source Sans"
 regular     = "SourceSans3-Regular.ttf"
diff --git a/res/firasans.fontpack/fontpack.ini b/res/firasans.fontpack/fontpack.ini
index 4378a757..c6eb4c77 100644
--- a/res/firasans.fontpack/fontpack.ini
+++ b/res/firasans.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
+version     = 1
+
 [firasans]
 name        = "Fira Sans"
 glyphscale  = 0.85
diff --git a/res/literata.fontpack/fontpack.ini b/res/literata.fontpack/fontpack.ini
index e4e49bcb..7c29491d 100644
--- a/res/literata.fontpack/fontpack.ini
+++ b/res/literata.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
+version     = 1
+
 [literata]
 name        = "Literata"
 regular     = "Literata-Regular-opsz=14.ttf"
diff --git a/res/nunito.fontpack/fontpack.ini b/res/nunito.fontpack/fontpack.ini
index ea4a12b8..2f2471e1 100644
--- a/res/nunito.fontpack/fontpack.ini
+++ b/res/nunito.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
+version     = 1
+
 [nunito]
 name        = "Nunito"
 tweaks      = 0x1     # some hardcoded kerning changes (`Th`, etc.)
diff --git a/res/tinos.fontpack/fontpack.ini b/res/tinos.fontpack/fontpack.ini
index 8759b752..a2cf811e 100644
--- a/res/tinos.fontpack/fontpack.ini
+++ b/res/tinos.fontpack/fontpack.ini
@@ -1,3 +1,5 @@
+version     = 1
+
 [tinos]
 name        = "Tinos"
 glyphscale  = 0.850
diff --git a/src/app.c b/src/app.c
index b317e7b3..cb5479e8 100644
--- a/src/app.c
+++ b/src/app.c
@@ -2669,6 +2669,7 @@ iBool handleCommand_App(const char *cmd) {
         const iBool isSplit = numRoots_Window(get_Window()) > 1;
         if (tabCount_Widget(tabs) > 1 || isSplit) {
             iWidget *closed = removeTabPage_Widget(tabs, index);
+            cancelAllRequests_DocumentWidget((iDocumentWidget *) closed);
             destroy_Widget(closed); /* released later */
             if (index == tabCount_Widget(tabs)) {
                 index--;
diff --git a/src/defs.h b/src/defs.h
index 65096389..f5479cf3 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -125,11 +125,13 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
 #define delete_Icon         "\u232b"
 #define copy_Icon           "\u2398" //"\u2bba"
 #define check_Icon          "\u2714"
-#define ballotCheck_Icon    "\U0001f5f9"
+#define ballotChecked_Icon  "\U0001f5f9"
+#define ballotUnchecked_Icon "\U0001f5f9"
 #define inbox_Icon          "\U0001f4e5"
 #define book_Icon           "\U0001f56e"
 #define bookmark_Icon       "\U0001f516"
 #define folder_Icon         "\U0001f4c1"
+#define file_Icon           "\U0001f5ce"
 #define openTab_Icon        "\u2750"
 #define openTabBg_Icon      "\u2b1a"
 #define openExt_Icon        "\u27a0"
diff --git a/src/fontpack.c b/src/fontpack.c
index ca1d1582..fb1c98ee 100644
--- a/src/fontpack.c
+++ b/src/fontpack.c
@@ -33,7 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include 
 #include 
 
-/* TODO: Clean up and/or reorder this file, it's a bit unorganized. */
+const char *mimeType_FontPack = "application/lagrange-fontpack+zip";
 
 float scale_FontSize(enum iFontSize size) {
     static const float sizes[max_FontSize] = {
@@ -57,38 +57,9 @@ float scale_FontSize(enum iFontSize size) {
     return sizes[size];
 }
 
-iDeclareType(Fonts)    
-    
-struct Impl_Fonts {
-    iString   userDir;
-    iPtrArray packs;
-    iPtrArray files;
-    iPtrArray specOrder; /* specs sorted by priority */
-};
-
-static iFonts fonts_;
-
-static void unloadFiles_Fonts_(iFonts *d) {
-    /* TODO: Mark all files in font packs as not resident. */    
-    iForEach(PtrArray, i, &d->files) {
-        delete_FontFile(i.ptr);
-    }
-    clear_PtrArray(&d->files);
-}
-
-static iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {
-    iForEach(PtrArray, i, &d->files) {
-        iFontFile *ff = i.ptr;
-        if (equal_String(&ff->id, id)) {
-            return ff;
-        }
-    }
-    return NULL;
-}
-
 /*----------------------------------------------------------------------------------------------*/
 
-iDefineTypeConstruction(FontFile)
+iDefineObjectConstruction(FontFile)
 
 void init_FontFile(iFontFile *d) {
     init_String(&d->id);
@@ -114,7 +85,7 @@ static void load_FontFile_(iFontFile *d, const iBlock *data) {
                                HB_MEMORY_MODE_READONLY, NULL, NULL);
     d->hbFace = hb_face_create(d->hbBlob, 0);
     d->hbFont = hb_font_create(d->hbFace);
-#endif    
+#endif
 }
 
 static void unload_FontFile_(iFontFile *d) {
@@ -126,12 +97,13 @@ static void unload_FontFile_(iFontFile *d) {
     d->hbFont = NULL;
     d->hbFace = NULL;
     d->hbBlob = NULL;
-#endif    
+#endif
     clear_Block(&d->sourceData);
     iZap(d->stbInfo);
 }
 
 void deinit_FontFile(iFontFile *d) {
+    printf("FontFile %p {%s} is DESTROYED\n", d, cstr_String(&d->id));
     unload_FontFile_(d);
     deinit_Block(&d->sourceData);
     deinit_String(&d->id);
@@ -156,12 +128,12 @@ void measureGlyph_FontFile(const iFontFile *d, uint32_t glyphIndex,
 
 /*----------------------------------------------------------------------------------------------*/
 
-
 iDefineTypeConstruction(FontSpec)
     
 void init_FontSpec(iFontSpec *d) {
     init_String(&d->id);
     init_String(&d->name);
+    init_String(&d->sourcePath);
     d->flags      = 0;
     d->priority   = 0;
     for (int i = 0; i < 2; ++i) {
@@ -173,52 +145,119 @@ void init_FontSpec(iFontSpec *d) {
 }
 
 void deinit_FontSpec(iFontSpec *d) {
+    /* FontFile references are held by FontSpecs. */
+    iForIndices(i, d->styles) {
+        iRelease(d->styles[i]);
+    }
+    deinit_String(&d->sourcePath);
     deinit_String(&d->name);
     deinit_String(&d->id);
 }
 
 /*----------------------------------------------------------------------------------------------*/
 
-iDeclareType(FontPack)
-iDeclareTypeConstruction(FontPack)
+iDeclareType(Fonts)
+
+struct Impl_Fonts {
+    iString   userDir;
+    iPtrArray packs;
+    iObjectList *files;
+    iPtrArray specOrder; /* specs sorted by priority */
+};
+
+static iFonts fonts_;
+
+static void unloadFiles_Fonts_(iFonts *d) {
+    /* TODO: Mark all files in font packs as not resident. */    
+    clear_ObjectList(d->files);
+}
+
+static iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {
+    iForEach(ObjectList, i, d->files) {
+        iFontFile *ff = i.object;
+        if (equal_String(&ff->id, id)) {
+            return ff;
+        }
+    }
+    return NULL;
+}
+
+static void releaseUnusedFiles_Fonts_(iFonts *d) {
+    iForEach(ObjectList, i, d->files) {
+        iFontFile *ff = i.object;
+        if (ff->object.refCount == 1) {
+            /* No specs use this. */
+            //printf("[Fonts] releasing unused font file: %p {%s}\n", ff, cstr_String(&ff->id));
+            remove_ObjectListIterator(&i);
+        }
+    }
+}
+
+/*----------------------------------------------------------------------------------------------*/
 
 struct Impl_FontPack {
-    const iArchive *archive; /* opened ZIP archive */
+    iString         id; /* lowercase filename without the .fontpack extension */
+    int             version;
+    iBool           isStandalone;
+    iBool           isReadOnly;
     iArray          fonts;   /* array of FontSpecs */
+    const iArchive *archive; /* opened ZIP archive */
     iString *       loadPath;
     iFontSpec *     loadSpec;
 };
 
+iDefineTypeConstruction(FontPack)
+
 void init_FontPack(iFontPack *d) {
-    d->archive = NULL;
+    init_String(&d->id);
+    d->version = 0;
+    d->isStandalone = iFalse;
+    d->isReadOnly = iFalse;
     init_Array(&d->fonts, sizeof(iFontSpec));
+    d->archive  = NULL;
     d->loadSpec = NULL;
     d->loadPath = NULL;
 }
 
 void deinit_FontPack(iFontPack *d) {
+    iAssert(d->archive == NULL);
+    iAssert(d->loadSpec == NULL);
     delete_String(d->loadPath);
     iForEach(Array, i, &d->fonts) {
         deinit_FontSpec(i.value);
     }
     deinit_Array(&d->fonts);
-    iAssert(d->archive == NULL);
-    iAssert(d->loadSpec == NULL);
+    deinit_String(&d->id);
+    releaseUnusedFiles_Fonts_(&fonts_);
 }
 
-iDefineTypeConstruction(FontPack)
+iFontPackId id_FontPack(const iFontPack *d) {
+    return (iFontPackId){ &d->id, d->version };
+}
+
+const iPtrArray *listSpecs_FontPack(const iFontPack *d) {
+    if (!d) return NULL;
+    iPtrArray *list = collectNew_PtrArray();
+    iConstForEach(Array, i, &d->fonts) {
+        pushBack_PtrArray(list, i.value);
+    }
+    return list;
+}
 
 void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart) {
     iFontPack *d = context;
     if (isStart) {
         iAssert(!d->loadSpec);
-        /* Each font ID must be unique. */
-        if (!findSpec_Fonts(cstr_String(table))) {
+        /* Each font ID must be unique in the non-standalone packs. */
+        if (d->isStandalone || !findSpec_Fonts(cstr_String(table))) {
             d->loadSpec = new_FontSpec();
             set_String(&d->loadSpec->id, table);
+            if (d->loadPath) {
+                set_String(&d->loadSpec->sourcePath, d->loadPath);
+            }
         }
     }
-    else {
+    else if (d->loadSpec) {
         /* Set fallback font files. */ {
             const iFontFile **styles = d->loadSpec->styles;
             if (!styles[regular_FontStyle]) {
@@ -229,11 +268,11 @@ void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart
                 return;
             }
             if (!styles[semiBold_FontStyle]) {
-                styles[semiBold_FontStyle] = styles[bold_FontStyle];
+                styles[semiBold_FontStyle] = ref_Object(styles[bold_FontStyle]);
             }
             for (size_t s = 0; s < max_FontStyle; s++) {
                 if (!styles[s]) {
-                    styles[s] = styles[regular_FontStyle];
+                    styles[s] = ref_Object(styles[regular_FontStyle]);
                 }
             }
         }
@@ -263,7 +302,15 @@ static iBlock *readFile_FontPack_(const iFontPack *d, const iString *path) {
 void handleIniKeyValue_FontPack_(void *context, const iString *table, const iString *key,
                                  const iTomlValue *value) {
     iFontPack *d = context;
-    if (!d->loadSpec) return;
+    if (isEmpty_String(table)) {
+        if (!cmp_String(key, "version")) {
+            d->version = number_TomlValue(value);
+        }
+        return;
+    }
+    if (!d->loadSpec) {
+        return;
+    }
     iUnused(table);
     if (!cmp_String(key, "name") && value->type == string_TomlType) {
         set_String(&d->loadSpec->name, value->value.string);        
@@ -322,12 +369,13 @@ void handleIniKeyValue_FontPack_(void *context, const iString *table, const iStr
                         ff = new_FontFile();
                         set_String(&ff->id, fontFileId);
                         load_FontFile_(ff, data);
-                        pushBack_PtrArray(&fonts_.files, ff); /* centralized ownership */
+                        pushBack_ObjectList(fonts_.files, ff); /* centralized ownership */
+                        iRelease(ff);
                         delete_Block(data);
 //                        printf("[FontPack] loaded file: %s\n", cstr_String(fontFileId));
                     }
                 }
-                d->loadSpec->styles[i] = ff;
+                d->loadSpec->styles[i] = ref_Object(ff);
                 delete_String(fontFileId);
                 break;
             }
@@ -348,29 +396,6 @@ static iBool load_FontPack_(iFontPack *d, const iString *ini) {
     return ok;
 }
 
-#if 0
-iBool loadIniFile_FontPack(iFontPack *d, const iString *iniPath) {
-    iBeginCollect();
-    iBool ok = iFalse;
-    iFile *f = iClob(new_File(iniPath));
-    if (open_File(f, text_FileMode | readOnly_FileMode)) {
-        d->loadPath = collect_String(newRange_String(dirName_Path(iniPath)));
-        iString *src = collect_String(readString_File(f));
-        
-        iTomlParser *ini = collect_TomlParser(new_TomlParser());
-        setHandlers_TomlParser(ini, handleIniTable_FontPack_, handleIniKeyValue_FontPack_, d);
-        if (!parse_TomlParser(ini, src)) {
-            fprintf(stderr, "[FontPack] error parsing %s\n", cstr_String(iniPath));
-        }
-        iAssert(d->loadSpec == NULL);
-        d->loadPath = NULL;
-        ok = iTrue;
-    }
-    iEndCollect();
-    return ok;
-}
-#endif
-
 iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {
     d->archive = zip;
     iBool ok = iFalse;
@@ -387,6 +412,28 @@ iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {
     return ok;
 }
 
+void setLoadPath_FontPack(iFontPack *d, const iString *path) {
+    if (!d->loadPath) {
+        d->loadPath = new_String();
+    }
+    set_String(d->loadPath, path);
+    /* Pack ID is based on the file name. */
+    setRange_String(&d->id, baseName_Path(path));
+    setRange_String(&d->id, withoutExtension_Path(&d->id));
+}
+
+void setStandalone_FontPack(iFontPack *d, iBool standalone) {
+    d->isStandalone = standalone;
+}
+
+void setReadOnly_FontPack(iFontPack *d, iBool readOnly) {
+    d->isReadOnly = readOnly;
+}
+
+iBool isReadOnly_FontPack(const iFontPack *d) {
+    return d->isReadOnly;
+}
+
 /*----------------------------------------------------------------------------------------------*/
 
 static void unloadFonts_Fonts_(iFonts *d) {
@@ -397,14 +444,23 @@ static void unloadFonts_Fonts_(iFonts *d) {
     clear_PtrArray(&d->packs);
 }
 
+static int cmpName_FontSpecPtr_(const void *a, const void *b) {
+    const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;
+    return cmpStringCase_String(&(*p1)->name, &(*p2)->name);
+}
+
 static int cmpPriority_FontSpecPtr_(const void *a, const void *b) {
     const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;
-    return -iCmp((*p1)->priority, (*p2)->priority); /* highest priority first */
+    const int cmp = -iCmp((*p1)->priority, (*p2)->priority); /* highest priority first */
+    if (cmp) return cmp;
+    return cmpName_FontSpecPtr_(a, b);
 }
 
-static int cmpName_FontSpecPtr_(const void *a, const void *b) {
+static int cmpSourceAndPriority_FontSpecPtr_(const void *a, const void *b) {
     const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;
-    return cmpStringCase_String(&(*p1)->name, &(*p2)->name);
+    const int cmp = cmpStringCase_String(&(*p1)->sourcePath, &(*p2)->sourcePath);
+    if (cmp) return cmp;
+    return cmpPriority_FontSpecPtr_(a, b);
 }
 
 static void sortSpecs_Fonts_(iFonts *d) {
@@ -422,11 +478,13 @@ void init_Fonts(const char *userDir) {
     iFonts *d = &fonts_;
     initCStr_String(&d->userDir, userDir);
     init_PtrArray(&d->packs);
-    init_PtrArray(&d->files);
+    d->files = new_ObjectList();
     init_PtrArray(&d->specOrder);
     /* Load the required fonts. */ {
         iFontPack *pack = new_FontPack();
+        setCStr_String(&pack->id, "default");
         iArchive *arch = new_Archive();
+        setReadOnly_FontPack(pack, iTrue);
         openData_Archive(arch, &fontpackDefault_Embedded);
         loadArchive_FontPack(pack, arch); /* should never fail if we've made it this far */
         iRelease(arch);
@@ -436,7 +494,7 @@ void init_Fonts(const char *userDir) {
         const char *locations[] = {
             ".",
             "./fonts",
-            "../share/lagrange",
+            "../share/lagrange", /* Note: These must match CMakeLists.txt install destination */
             "../../share/lagrange",
             concatPath_CStr(userDir, "fonts"),
             userDir,
@@ -450,7 +508,13 @@ void init_Fonts(const char *userDir) {
                     iArchive *arch = new_Archive();
                     if (openFile_Archive(arch, entryPath)) {
                         iFontPack *pack = new_FontPack();
-                        pack->loadPath = copy_String(entryPath);
+                        setLoadPath_FontPack(pack, entryPath);
+                        setReadOnly_FontPack(pack, !isWritable_FileInfo(entry.value));
+#if defined (iPlatformApple)
+                        if (startsWith_String(pack->loadPath, cstr_String(execDir))) {
+                            setReadOnly_FontPack(pack, iTrue);
+                        }
+#endif
                         if (loadArchive_FontPack(pack, arch)) {
                             pushBack_PtrArray(&d->packs, pack);
                         }
@@ -491,13 +555,18 @@ void init_Fonts(const char *userDir) {
 void deinit_Fonts(void) {
     iFonts *d = &fonts_;
     unloadFonts_Fonts_(d);
-    unloadFiles_Fonts_(d);
+    //unloadFiles_Fonts_(d);
+    iAssert(isEmpty_ObjectList(d->files));
     deinit_PtrArray(&d->specOrder);
     deinit_PtrArray(&d->packs);
-    deinit_PtrArray(&d->files);
+    iRelease(d->files);
     deinit_String(&d->userDir);
 }
 
+const iPtrArray *listPacks_Fonts(void) {
+    return &fonts_.packs;
+}
+
 const iFontSpec *findSpec_Fonts(const char *fontId) {
     iFonts *d = &fonts_;
     iConstForEach(PtrArray, i, &d->specOrder) {
@@ -524,3 +593,73 @@ const iPtrArray *listSpecs_Fonts(iBool (*filterFunc)(const iFontSpec *)) {
 const iPtrArray *listSpecsByPriority_Fonts(void) {
     return &fonts_.specOrder;
 }
+
+const iString *infoPage_Fonts(void) {
+    iFonts *d = &fonts_;
+    iString *str = collectNewCStr_String("# Fonts\n");
+    iPtrArray *specsByPack = collectNew_PtrArray();
+    setCopy_PtrArray(specsByPack, &d->specOrder);
+    sort_Array(specsByPack, cmpSourceAndPriority_FontSpecPtr_);
+    iString *currentSourcePath = collectNew_String();
+    iConstForEach(PtrArray, i, specsByPack) {
+        const iFontSpec *spec = i.ptr;
+        if (isEmpty_String(&spec->sourcePath)) {
+            continue; /* built-in font */
+        }
+        if (!equal_String(&spec->sourcePath, currentSourcePath)) {
+            appendFormat_String(str, "=> %s %s%s\n",
+                                cstrCollect_String(makeFileUrl_String(&spec->sourcePath)),
+                                endsWithCase_String(&spec->sourcePath, ".fontpack") ? "\U0001f520 " : "",
+                                cstr_Rangecc(baseName_Path(&spec->sourcePath)));
+            set_String(currentSourcePath, &spec->sourcePath);
+        }
+    }
+    return str;
+}
+
+const iFontPack *findPack_Fonts(const iString *path) {
+    iFonts *d = &fonts_;
+    iConstForEach(PtrArray, i, &d->packs) {
+        const iFontPack *pack = i.ptr;
+        if (pack->loadPath && equal_String(pack->loadPath, path)) {
+            return pack;
+        }
+    }
+    return NULL;
+}
+
+iBool preloadLocalFontpackForPreview_Fonts(iGmDocument *doc) {
+    iBool wasLoaded = iFalse;
+    for (size_t linkId = 1; ; linkId++) {
+        const iString *linkUrl = linkUrl_GmDocument(doc, linkId);
+        if (!linkUrl) {
+            break; /* ran out of links */
+        }
+        const int linkFlags = linkFlags_GmDocument(doc, linkId);
+        if (linkFlags & fontpackFileExtension_GmLinkFlag &&
+            scheme_GmLinkFlag(linkFlags) == file_GmLinkScheme) {
+            iMediaId linkMedia = findMediaForLink_Media(media_GmDocument(doc), linkId, fontpack_MediaType);
+            if (linkMedia.type) {
+                continue; /* got this one already */
+            }
+            iString *filePath = localFilePathFromUrl_String(linkUrl);
+            iFile *f = new_File(filePath);
+            if (open_File(f, readOnly_FileMode)) {
+                iBlock *fontPackArchiveData = readAll_File(f);
+                setUrl_Media(media_GmDocument(doc), linkId, fontpack_MediaType, linkUrl);
+                setData_Media(media_GmDocument(doc),
+                              linkId,
+                              collectNewCStr_String(mimeType_FontPack),
+                              fontPackArchiveData,
+                              0);
+                delete_Block(fontPackArchiveData);
+                wasLoaded = iTrue;
+            }
+            iRelease(f);
+        }
+    }
+    return wasLoaded;
+}
+
+iDefineClass(FontFile)
+
diff --git a/src/fontpack.h b/src/fontpack.h
index e59154e3..429afb5d 100644
--- a/src/fontpack.h
+++ b/src/fontpack.h
@@ -30,6 +30,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #   include 
 #endif
 
+extern const char *mimeType_FontPack;
+
 /* Fontpacks are ZIP archives that contain a configuration file and one of more font
 files. The fontpack format is used instead of plain TTF/OTF because the text renderer
 uses additional metadata about each font.
@@ -68,21 +70,13 @@ enum iFontStyle {
 
 float   scale_FontSize  (enum iFontSize size);
 
-iDeclareType(FontSpec)
-iDeclareTypeConstruction(FontSpec)
+/*----------------------------------------------------------------------------------------------*/
 
-enum iFontSpecFlags {
-    override_FontSpecFlag  = iBit(1),
-    monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */
-    auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */
-    arabic_FontSpecFlag    = iBit(4),
-    fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */
-};
-
-iDeclareType(FontFile)
-iDeclareTypeConstruction(FontFile)
+iDeclareClass(FontFile)
+iDeclareObjectConstruction(FontFile)
     
 struct Impl_FontFile {
+    iObject         object; /* reference-counted */
     iString         id; /* for detecting when the same file is used in many places */
     enum iFontStyle style;
     iBlock          sourceData;
@@ -107,9 +101,26 @@ uint8_t *   rasterizeGlyph_FontFile(const iFontFile *, float xScale, float yScal
 void        measureGlyph_FontFile  (const iFontFile *, uint32_t glyphIndex,
                                     float xScale, float yScale, float xShift,
                                     int *x0, int *y0, int *x1, int *y1);
+
+/*----------------------------------------------------------------------------------------------*/
+
+/* FontSpec describes a typeface, combining multiple fonts into a group.
+   The user will be choosing FontSpecs instead of individual font files. */
+iDeclareType(FontSpec)
+iDeclareTypeConstruction(FontSpec)
+
+enum iFontSpecFlags {
+    override_FontSpecFlag  = iBit(1),
+    monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */
+    auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */
+    arabic_FontSpecFlag    = iBit(4),
+    fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */
+};
+
 struct Impl_FontSpec {
     iString id;   /* unique ID */
     iString name; /* human-readable label */
+    iString sourcePath; /* file where the path was loaded, could be a .fontpack */
     int     flags;
     int     priority;
     float   heightScale[2];     /* overall height scaling; ui, document */
@@ -121,10 +132,38 @@ struct Impl_FontSpec {
 iLocalDef int scaleType_FontSpec(enum iFontSize sizeId) {
     return sizeId / contentRegular_FontSize;
 }
- 
+
+/*----------------------------------------------------------------------------------------------*/
+
+iDeclareType(FontPack)
+iDeclareTypeConstruction(FontPack)
+
+iDeclareType(FontPackId)
+
+struct Impl_FontPackId {
+    const iString *id;
+    int version;
+};
+
+void                setReadOnly_FontPack    (iFontPack *, iBool readOnly);
+void                setStandalone_FontPack  (iFontPack *, iBool standalone);
+void                setLoadPath_FontPack    (iFontPack *, const iString *path);
+iBool               loadArchive_FontPack    (iFontPack *, const iArchive *zip);
+
+iFontPackId         id_FontPack             (const iFontPack *);
+const iPtrArray *   listSpecs_FontPack      (const iFontPack *);
+iBool               isReadOnly_FontPack     (const iFontPack *);
+
+iDeclareType(GmDocument)
+
 void    init_Fonts      (const char *userDir);
 void    deinit_Fonts    (void);
 
+const iFontPack *   findPack_Fonts              (const iString *path);
 const iFontSpec *   findSpec_Fonts              (const char *fontId);
+const iPtrArray *   listPacks_Fonts             (void);
 const iPtrArray *   listSpecs_Fonts             (iBool (*filterFunc)(const iFontSpec *));
 const iPtrArray *   listSpecsByPriority_Fonts   (void);
+const iString *     infoPage_Fonts              (void);
+
+iBool   preloadLocalFontpackForPreview_Fonts    (iGmDocument *doc);
diff --git a/src/gempub.c b/src/gempub.c
index 23846414..952d72a1 100644
--- a/src/gempub.c
+++ b/src/gempub.c
@@ -337,7 +337,7 @@ iBool preloadCoverImage_Gempub(const iGempub *d, iGmDocument *doc) {
     for (size_t linkId = 1; ; linkId++) {
         const iString *linkUrl = linkUrl_GmDocument(doc, linkId);
         if (!linkUrl) break;
-        if (findLinkImage_Media(media_GmDocument(doc), linkId)) {
+        if (findLinkImage_Media(media_GmDocument(doc), linkId).type) {
             continue; /* got this already */
         }
         if (linkFlags_GmDocument(doc, linkId) & imageFileExtension_GmLinkFlag) {
diff --git a/src/gmdocument.c b/src/gmdocument.c
index c98b0bb8..c271ad94 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include "ui/color.h"
 #include "ui/text.h"
 #include "ui/metrics.h"
+#include "ui/mediaui.h"
 #include "ui/window.h"
 #include "visited.h"
 #include "bookmarks.h"
@@ -224,7 +225,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
                 setScheme_GmLink_(link, finger_GmLinkScheme);
             }
             else if (equalCase_Rangecc(parts.scheme, "file")) {
-                setScheme_GmLink_(link, file_GmLinkScheme);
+                setScheme_GmLink_(link, file_GmLinkScheme);                
             }
             else if (equalCase_Rangecc(parts.scheme, "data")) {
                 setScheme_GmLink_(link, data_GmLinkScheme);
@@ -251,6 +252,9 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
                          endsWithCase_String(path, ".mid") || endsWithCase_String(path, ".ogg")) {
                     link->flags |= audioFileExtension_GmLinkFlag;
                 }
+                else if (endsWithCase_String(path, ".fontpack")) {
+                    link->flags |= fontpackFileExtension_GmLinkFlag;
+                }
                 delete_String(path);
             }
             /* Check if visited. */
@@ -503,7 +507,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
     static const char *arrow           = rightArrowhead_Icon;
     static const char *envelope        = envelope_Icon;
     static const char *bullet          = "\u2022";
-    static const char *folder          = "\U0001f4c1";
+    static const char *folder          = file_Icon;
     static const char *globe           = globe_Icon;
     static const char *quote           = "\u201c";
     static const char *magnifyingGlass = "\U0001f50d";
@@ -900,77 +904,77 @@ static void doLayout_GmDocument_(iGmDocument *d) {
         ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag;
         /* Image or audio content. */
         if (type == link_GmLineType) {
-            const iMediaId imageId = findLinkImage_Media(d->media, run.linkId);
-            const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0;
-            const iMediaId downloadId = !imageId && !audioId ? findLinkDownload_Media(d->media, run.linkId) : 0;
-            if (imageId) {
-                iGmMediaInfo img;
-                imageInfo_Media(d->media, imageId, &img);
-                const iInt2 imgSize = imageSize_Media(d->media, imageId);
-                linkContentWasLaidOut_GmDocument_(d, &img, run.linkId);
-                const int margin = lineHeight_Text(paragraph_FontId) / 2;
+            /* TODO: Cleanup here? Move to a function of its own. */
+//            enum iMediaType mediaType = none_MediaType;
+            const iMediaId media = findMediaForLink_Media(d->media, run.linkId, none_MediaType);
+            iGmMediaInfo info;
+            info_Media(d->media, media, &info);
+            run.mediaType = media.type;
+            run.mediaId   = media.id;
+            run.text      = iNullRange;
+            run.font      = uiLabel_FontId;
+            run.color     = 0;
+            const int margin = lineHeight_Text(paragraph_FontId) / 2;
+            if (media.type) {
                 pos.y += margin;
-                run.bounds.pos = pos;
-                run.bounds.size.x = d->size.x;
-                const float aspect = (float) imgSize.y / (float) imgSize.x;
-                run.bounds.size.y = d->size.x * aspect;
-                /* Extend the image to full width, including outside margin, if the viewport
-                   is narrow enough. */
-                if (isFullWidthImages) {
-                    run.bounds.size.x += d->outsideMargin * 2;
-                    run.bounds.size.y += d->outsideMargin * 2 * aspect;
-                    run.bounds.pos.x  -= d->outsideMargin;
-                }                
-                run.visBounds = run.bounds;
-                const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);
-                if (width_Rect(run.visBounds) > maxSize.x) {
-                    /* Don't scale the image up. */
-                    run.visBounds.size.y =
-                        run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);
-                    run.visBounds.size.x = maxSize.x;
-                    run.visBounds.pos.x  = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;
-                    run.bounds.size.y    = run.visBounds.size.y;
-                }
-                run.text      = iNullRange;
-                run.font      = 0;
-                run.color     = 0;
-                run.mediaType = image_GmRunMediaType;
-                run.mediaId   = imageId;
-                pushBack_Array(&d->layout, &run);
-                pos.y += run.bounds.size.y + margin;
-            }
-            else if (audioId) {
-                iGmMediaInfo info;
-                audioInfo_Media(d->media, audioId, &info);
+                run.bounds.size.y = 0;
                 linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);
-                const int margin = lineHeight_Text(paragraph_FontId) / 2;
-                pos.y += margin;
-                run.bounds.pos    = pos;
-                run.bounds.size.x = d->size.x;
-                run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;
-                run.visBounds     = run.bounds;
-                run.text          = iNullRange;
-                run.color         = 0;
-                run.mediaType     = audio_GmRunMediaType;
-                run.mediaId       = audioId;
-                pushBack_Array(&d->layout, &run);
-                pos.y += run.bounds.size.y + margin;
             }
-            else if (downloadId) {
-                iGmMediaInfo info;
-                downloadInfo_Media(d->media, downloadId, &info);
-                linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);
-                const int margin = lineHeight_Text(paragraph_FontId) / 2;
-                pos.y += margin;
-                run.bounds.pos    = pos;
-                run.bounds.size.x = d->size.x;
-                run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;
-                run.visBounds     = run.bounds;
-                run.text          = iNullRange;
-                run.color         = 0;
-                run.mediaType     = download_GmRunMediaType;
-                run.mediaId       = downloadId;
-                pushBack_Array(&d->layout, &run);
+            switch (media.type) {
+                case image_MediaType: {
+                    const iInt2 imgSize = imageSize_Media(d->media, media);
+                    run.bounds.pos = pos;
+                    run.bounds.size.x = d->size.x;
+                    const float aspect = (float) imgSize.y / (float) imgSize.x;
+                    run.bounds.size.y = d->size.x * aspect;
+                    /* Extend the image to full width, including outside margin, if the viewport
+                       is narrow enough. */
+                    if (isFullWidthImages) {
+                        run.bounds.size.x += d->outsideMargin * 2;
+                        run.bounds.size.y += d->outsideMargin * 2 * aspect;
+                        run.bounds.pos.x  -= d->outsideMargin;
+                    }
+                    run.visBounds = run.bounds;
+                    const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);
+                    if (width_Rect(run.visBounds) > maxSize.x) {
+                        /* Don't scale the image up. */
+                        run.visBounds.size.y =
+                            run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);
+                        run.visBounds.size.x = maxSize.x;
+                        run.visBounds.pos.x  = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;
+                        run.bounds.size.y    = run.visBounds.size.y;
+                    }
+                    pushBack_Array(&d->layout, &run);
+                    break;
+                }
+                case audio_MediaType: {
+                    run.bounds.pos    = pos;
+                    run.bounds.size.x = d->size.x;
+                    run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;
+                    run.visBounds     = run.bounds;
+                    pushBack_Array(&d->layout, &run);
+                    break;
+                }
+                case download_MediaType: {
+                    run.bounds.pos    = pos;
+                    run.bounds.size.x = d->size.x;
+                    run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;
+                    run.visBounds     = run.bounds;
+                    pushBack_Array(&d->layout, &run);
+                    break;
+                }
+                case fontpack_MediaType: {
+                    run.bounds.pos    = pos;
+                    run.bounds.size.x = d->size.x;
+                    run.bounds.size.y = height_FontpackUI(d->media, media.id, d->size.x);
+                    run.visBounds     = run.bounds;
+                    pushBack_Array(&d->layout, &run);
+                    break;
+                }
+                default:
+                    break;
+            }
+            if (media.type && run.bounds.size.y) {
                 pos.y += run.bounds.size.y + margin;
             }
         }
@@ -1057,21 +1061,6 @@ const iString *url_GmDocument(const iGmDocument *d) {
     return &d->url;
 }
 
-#if 0
-void reset_GmDocument(iGmDocument *d) {
-    clear_Media(d->media);
-    clearLinks_GmDocument_(d);
-    clear_Array(&d->layout);
-    clear_Array(&d->headings);
-    clear_Array(&d->preMeta);
-    clear_String(&d->url);
-    clear_String(&d->localHost);
-    clear_String(&d->source);
-    clear_String(&d->unormSource);
-    d->themeSeed = 0;
-}
-#endif
-
 static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {
     set_Color(tmQuoteIcon_ColorId,
               mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f));
diff --git a/src/gmdocument.h b/src/gmdocument.h
index b2c6d9b7..20bc9890 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -90,6 +90,7 @@ enum iGmLinkFlag {
     query_GmLinkFlag              = iBit(14), /* Gopher query link */
     iconFromLabel_GmLinkFlag      = iBit(15), /* use an Emoji/special character from label */
     isOpen_GmLinkFlag             = iBit(16), /* currently open in a tab */
+    fontpackFileExtension_GmLinkFlag = iBit(17),
 };
 
 iLocalDef enum iGmLinkScheme scheme_GmLinkFlag(int flags) {
@@ -126,13 +127,6 @@ enum iGmRunFlags {
     altText_GmRunFlag     = iBit(8),
 };
 
-enum iGmRunMediaType {
-    none_GmRunMediaType,
-    image_GmRunMediaType,
-    audio_GmRunMediaType,
-    download_GmRunMediaType,
-};
-
 /* This structure is tightly packed because GmDocuments are mostly composed of
    a large number of GmRuns. */
 struct Impl_GmRun {
@@ -146,12 +140,16 @@ struct Impl_GmRun {
         uint32_t color     : 7; /* see max_ColorId */
 
         uint32_t font      : 10;
-        uint32_t mediaType : 2;
-        uint32_t mediaId   : 10; /* zero if not an image */
+        uint32_t mediaType : 3;
+        uint32_t mediaId   : 9; /* zero if not an image */
         uint32_t preId     : 10; /* preformatted block ID (sequential); merge with mediaId? */
     };
 };
 
+iLocalDef iMediaId mediaId_GmRun(const iGmRun *d) {
+    return (iMediaId){ .type = d->mediaType, .id = d->mediaId };
+}
+
 iDeclareType(GmRunRange)
 
 struct Impl_GmRunRange {
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 1a9e83a9..f7a22e0a 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -361,6 +361,9 @@ static const iBlock *aboutPageSource_(iRangecc path, iRangecc query) {
     if (equalCase_Rangecc(path, "debug")) {
         return utf8_String(debugInfo_App());
     }
+    if (equalCase_Rangecc(path, "fonts")) {
+        return utf8_String(infoPage_Fonts());
+    }
     if (equalCase_Rangecc(path, "feeds")) {
         return utf8_String(entryListPage_Feeds());
     }
@@ -710,8 +713,9 @@ void submit_GmRequest(iGmRequest *d) {
             sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_);
             iForEach(PtrArray, s, sortedInfo) {
                 const iFileInfo *entry = s.ptr;
-                appendFormat_String(page, "=> %s %s%s\n",
+                appendFormat_String(page, "=> %s %s%s%s\n",
                                     cstrCollect_String(makeFileUrl_String(path_FileInfo(entry))),
+                                    isDirectory_FileInfo(entry) ? folder_Icon " " : "",
                                     cstr_Rangecc(baseName_Path(path_FileInfo(entry))),
                                     isDirectory_FileInfo(entry) ? iPathSeparator : "");
                 iRelease(entry);
@@ -808,9 +812,10 @@ void submit_GmRequest(iGmRequest *d) {
                             const iString *subPath = e.value;
                             iRangecc relSub = range_String(subPath);
                             relSub.start += size_String(entryPath);
-                            appendFormat_String(page, "=> %s/%s %s\n",
+                            appendFormat_String(page, "=> %s/%s %s%s\n",
                                                 cstr_String(&d->url),
                                                 cstr_String(withSpacesEncoded_String(collectNewRange_String(relSub))),
+                                                endsWith_Rangecc(relSub, "/") ? folder_Icon " " : "",
                                                 cstr_Rangecc(relSub));
                         }
                         resp->statusCode = success_GmStatusCode;
diff --git a/src/gmutil.c b/src/gmutil.c
index 971747d4..5be7e198 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -21,6 +21,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 
 #include "gmutil.h"
+#include "fontpack.h"
 
 #include 
 #include 
@@ -511,7 +512,8 @@ const iString *findContainerArchive_Path(const iString *path) {
     while (!isEmpty_String(path) && cmp_String(path, ".")) {
         iString *dir = newRange_String(dirName_Path(path));
         if (endsWithCase_String(dir, ".zip") ||
-            endsWithCase_String(dir, ".gpub")) {
+            endsWithCase_String(dir, ".gpub") ||
+            endsWithCase_String(dir, ".fontpack")) {
             iEndCollect();
             return collect_String(dir);
         }
@@ -534,6 +536,9 @@ const char *mediaTypeFromFileExtension_String(const iString *d) {
     else if (endsWithCase_String(d, ".gpub")) {
         return "application/gpub+zip";
     }
+    else if (endsWithCase_String(d, ".fontpack")) {
+        return mimeType_FontPack;
+    }
     else if (endsWithCase_String(d, ".xml")) {
         return "text/xml";
     }
@@ -562,6 +567,7 @@ const char *mediaTypeFromFileExtension_String(const iString *d) {
         return "audio/midi";
     }
     else if (endsWithCase_String(d, ".txt") ||
+             endsWithCase_String(d, ".ini") ||
              endsWithCase_String(d, ".md") ||
              endsWithCase_String(d, ".c") ||
              endsWithCase_String(d, ".h") ||
diff --git a/src/media.c b/src/media.c
index 26f0af4b..0ce2ac5c 100644
--- a/src/media.c
+++ b/src/media.c
@@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -287,45 +288,112 @@ iDefineTypeConstruction(GmDownload)
 
 /*----------------------------------------------------------------------------------------------*/
 
+iDeclareType(GmFontpack)
+
+struct Impl_GmFontpack {
+    iGmMediaProps props;
+    iString packId;
+    iFontpackMediaInfo info;
+    /* TODO: Font preview images? */
+};
+
+void init_GmFontpack(iGmFontpack *d) {
+    init_GmMediaProps_(&d->props);
+    init_String(&d->packId);
+    iZap(d->info);
+    d->info.names = new_StringList();
+}
+
+void deinit_GmFontpack(iGmFontpack *d) {
+    iRelease(d->info.names);
+    deinit_String(&d->packId);
+    deinit_GmMediaProps_(&d->props);
+}
+
+static void loadData_GmFontpack_(iGmFontpack *d, const iBlock *data) {
+    const iString *loadPath = collect_String(localFilePathFromUrl_String(&d->props.url));
+    const iFontPack *pack = findPack_Fonts(loadPath);
+    d->info.isValid = d->info.isInstalled = pack != NULL;
+    d->info.isReadOnly = iFalse;
+    if (!pack) {
+        /* Let's load it now temporarily and see what's inside. */
+        iArchive *zip = new_Archive();
+        if (openData_Archive(zip, data)) {
+            iFontPack *fp = collect_FontPack(new_FontPack());
+            setLoadPath_FontPack(fp, loadPath);
+            setStandalone_FontPack(fp, iTrue);
+            if (loadArchive_FontPack(fp, zip)) {
+                d->info.isValid = iTrue;
+                pack = fp;
+            }
+        }
+        iRelease(zip);
+    }
+    if (pack) {
+        set_String(&d->packId, id_FontPack(pack).id);
+        d->info.packId.id = &d->packId; /* we own this String */
+        d->info.packId.version = id_FontPack(pack).version;
+        d->info.isReadOnly = isReadOnly_FontPack(pack);
+    }
+    iPtrSet *unique = new_PtrSet();
+    iConstForEach(PtrArray, i, listSpecs_FontPack(pack)) {
+        const iFontSpec *spec = i.ptr;
+        pushBack_StringList(d->info.names, &spec->name);
+        iForIndices(j, spec->styles) {
+            insert_PtrSet(unique, spec->styles[j]);
+        }
+    }
+    iConstForEach(PtrSet, j, unique) {
+        d->info.sizeInBytes += size_Block(&((const iFontFile *) *j.value)->sourceData);
+    }
+    delete_PtrSet(unique);
+}
+
+iDefineTypeConstruction(GmFontpack)
+
+/*----------------------------------------------------------------------------------------------*/
+
 struct Impl_Media {
-    iPtrArray images;
-    iPtrArray audio;
-    iPtrArray downloads;
+    iPtrArray items[max_MediaType];
+    /* TODO: Add a hash to quickly look up a link's media. */
 };
 
 iDefineTypeConstruction(Media)
 
 void init_Media(iMedia *d) {
-    init_PtrArray(&d->images);
-    init_PtrArray(&d->audio);
-    init_PtrArray(&d->downloads);
+    iForIndices(i, d->items) {
+        init_PtrArray(&d->items[i]);
+    }
 }
 
 void deinit_Media(iMedia *d) {
     clear_Media(d);
-    deinit_PtrArray(&d->downloads);
-    deinit_PtrArray(&d->audio);
-    deinit_PtrArray(&d->images);
+    iForIndices(i, d->items) {
+        deinit_PtrArray(&d->items[i]);
+    }
 }
 
 void clear_Media(iMedia *d) {
-    iForEach(PtrArray, i, &d->images) {
+    iForEach(PtrArray, i, &d->items[image_MediaType]) {
         deinit_GmImage(i.ptr);
     }
-    clear_PtrArray(&d->images);
-    iForEach(PtrArray, a, &d->audio) {
+    iForEach(PtrArray, a, &d->items[audio_MediaType]) {
         deinit_GmAudio(a.ptr);
     }
-    clear_PtrArray(&d->audio);
-    iForEach(PtrArray, n, &d->downloads) {
+    iForEach(PtrArray, n, &d->items[download_MediaType]) {
         deinit_GmDownload(n.ptr);
     }
-    clear_PtrArray(&d->downloads);
+    iForEach(PtrArray, f, &d->items[fontpack_MediaType]) {
+        deinit_GmFontpack(f.ptr);
+    }
+    iForIndices(type, d->items) {
+        clear_PtrArray(&d->items[type]);
+    }
 }
 
 size_t memorySize_Media(const iMedia *d) {
     size_t memSize = 0;
-    iConstForEach(PtrArray, i, &d->images) {
+    iConstForEach(PtrArray, i, &d->items[image_MediaType]) {
         const iGmImage *img = i.ptr;
         if (img->texture) {
             const iInt2 texSize = size_SDLTexture(img->texture);
@@ -335,34 +403,49 @@ size_t memorySize_Media(const iMedia *d) {
             memSize += size_Block(&img->partialData);
         }
     }
-    iConstForEach(PtrArray, a, &d->audio) {
+    iConstForEach(PtrArray, a, &d->items[audio_MediaType]) {
         const iGmAudio *audio = a.ptr;
         if (audio->player) {
             memSize += sourceDataSize_Player(audio->player);
         }
     }
-    iConstForEach(PtrArray, n, &d->downloads) {
+    iConstForEach(PtrArray, n, &d->items[download_MediaType]) {
         const iGmDownload *down = n.ptr;
         memSize += down->numBytes;
     }
     return memSize; 
 }
 
-iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) {
-    iGmDownload *dl       = NULL;
-    iMediaId     existing = findLinkDownload_Media(d, linkId);
-    iBool        isNew    = iFalse;
-    if (!existing) {
-        isNew = iTrue;
-        dl = new_GmDownload();
-        dl->props.linkId = linkId;
-        dl->props.isPermanent = iTrue;
-        set_String(&dl->props.url, url);
-        pushBack_PtrArray(&d->downloads, dl);
+iBool setUrl_Media(iMedia *d, iGmLinkId linkId, enum iMediaType mediaType, const iString *url) {
+    iMediaId existing = findMediaForLink_Media(d, linkId, mediaType);
+    const iBool isNew = !existing.id;
+    iGmMediaProps *props = NULL;
+    if (mediaType == download_MediaType) {
+        iGmDownload *dl = NULL;
+        if (isNew) {
+            dl = new_GmDownload();
+            pushBack_PtrArray(&d->items[download_MediaType], dl);
+        }
+        else {
+            dl = at_PtrArray(&d->items[download_MediaType], index_MediaId(existing));
+        }
+        props = &dl->props;
     }
-    else {
-        iGmDownload *dl = at_PtrArray(&d->downloads, existing - 1);
-        set_String(&dl->props.url, url);
+    else if (mediaType == fontpack_MediaType) {
+        iGmFontpack *fp = NULL;
+        if (isNew) {
+            fp = new_GmFontpack();
+            pushBack_PtrArray(&d->items[fontpack_MediaType], fp);
+        }
+        else {
+            fp = at_PtrArray(&d->items[fontpack_MediaType], index_MediaId(existing));
+        }
+        props = &fp->props;
+    }
+    if (props) {
+        props->linkId = linkId;
+        props->isPermanent = iTrue;
+        set_String(&props->url, url);
     }
     return isNew;
 }
@@ -372,16 +455,17 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
     const iBool isPartial  = (flags & partialData_MediaFlag) != 0;
     const iBool allowHide  = (flags & allowHide_MediaFlag) != 0;
     const iBool isDeleting = (!mime || !data);
-    iMediaId    existing   = findLinkImage_Media(d, linkId);
+    iMediaId    existing   = findMediaForLink_Media(d, linkId, none_MediaType);// findLinkImage_Media(d, linkId);
+    const size_t existingIndex = index_MediaId(existing);
     iBool       isNew      = iFalse;
-    if (existing) {
+    if (existing.type == image_MediaType) {
         iGmImage *img;
         if (isDeleting) {
-            take_PtrArray(&d->images, existing - 1, (void **) &img);
+            take_PtrArray(&d->items[image_MediaType], existingIndex, (void **) &img);
             delete_GmImage(img);
         }
         else {
-            img = at_PtrArray(&d->images, existing - 1);
+            img = at_PtrArray(&d->items[image_MediaType], existingIndex);
             iAssert(equal_String(&img->props.mime, mime)); /* MIME cannot change */
             set_Block(&img->partialData, data);
             if (!isPartial) {
@@ -389,14 +473,14 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
             }
         }
     }
-    else if ((existing = findLinkAudio_Media(d, linkId)) != 0) {
+    else if (existing.type == audio_MediaType) {
         iGmAudio *audio;
         if (isDeleting) {
-            take_PtrArray(&d->audio, existing - 1, (void **) &audio);
+            take_PtrArray(&d->items[audio_MediaType], existingIndex, (void **) &audio);
             delete_GmAudio(audio);
         }
         else {
-            audio = at_PtrArray(&d->audio, existing - 1);
+            audio = at_PtrArray(&d->items[audio_MediaType], existingIndex);
             iAssert(equal_String(&audio->props.mime, mime)); /* MIME cannot change */
             updateSourceData_Player(audio->player, mime, data, append_PlayerUpdate);
             if (!isPartial) {
@@ -408,14 +492,14 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
             }
         }
     }
-    else if ((existing = findLinkDownload_Media(d, linkId)) != 0) {
+    else if (existing.type == download_MediaType) {
         iGmDownload *dl;
         if (isDeleting) {
-            take_PtrArray(&d->downloads, existing - 1, (void **) &dl);
+            take_PtrArray(&d->items[download_MediaType], existingIndex, (void **) &dl);
             delete_GmDownload(dl);
         }
         else {
-            dl = at_PtrArray(&d->downloads, existing - 1);
+            dl = at_PtrArray(&d->items[download_MediaType], existingIndex);
             if (isEmpty_String(&dl->props.mime)) {
                 set_String(&dl->props.mime, mime);
             }
@@ -428,6 +512,21 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
             }
         }
     }
+    else if (existing.type == fontpack_MediaType) {
+        iGmFontpack *fp;
+        if (isDeleting) {
+            take_PtrArray(&d->items[fontpack_MediaType], existingIndex, (void **) &fp);
+            delete_GmFontpack(fp);
+        }
+        else {
+            iAssert(!isPartial);
+            fp = at_PtrArray(&d->items[fontpack_MediaType], existingIndex);
+            if (isEmpty_String(&fp->props.mime)) {
+                set_String(&fp->props.mime, mime);
+            }
+            loadData_GmFontpack_(fp, data);
+        }
+    }
     else if (!isDeleting) {
         if (startsWith_String(mime, "image/")) {
             /* Copy the image to a texture. */
@@ -435,7 +534,7 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
             img->props.linkId = linkId; /* TODO: use a hash? */
             img->props.isPermanent = !allowHide;
             set_String(&img->props.mime, mime);
-            pushBack_PtrArray(&d->images, img);
+            pushBack_PtrArray(&d->items[image_MediaType], img);
             if (!isPartial) {
                 makeTexture_GmImage(img);
             }
@@ -450,7 +549,7 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
             if (!isPartial) {
                 updateSourceData_Player(audio->player, NULL, NULL, complete_PlayerUpdate);
             }
-            pushBack_PtrArray(&d->audio, audio);
+            pushBack_PtrArray(&d->items[audio_MediaType], audio);
             /* Start playing right away. */
             start_Player(audio->player);
             postCommandf_App("media.player.started player:%p", audio->player);
@@ -460,125 +559,135 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
     return isNew;
 }
 
-iMediaId findLinkImage_Media(const iMedia *d, iGmLinkId linkId) {
-    /* TODO: use a hash */
-    iConstForEach(PtrArray, i, &d->images) {
-        const iGmImage *img = i.ptr;
-        if (img->props.linkId == linkId) {
-            return index_PtrArrayConstIterator(&i) + 1;
+static iMediaId findMediaPtr_Media_(const iPtrArray *items, enum iMediaType mediaType, iGmLinkId linkId) {
+    iConstForEach(PtrArray, i, items) {
+        const iGmMediaProps *props = i.ptr;
+        if (props->linkId == linkId) {
+            return (iMediaId){
+                .type = mediaType,
+                .id = index_PtrArrayConstIterator(&i) + 1
+            };
         }
     }
-    return 0;
-}
-
-size_t numAudio_Media(const iMedia *d) {
-    return size_PtrArray(&d->audio);
+    return iInvalidMediaId;
 }
 
-iMediaId findLinkAudio_Media(const iMedia *d, iGmLinkId linkId) {
-    /* TODO: use a hash */
-    iConstForEach(PtrArray, i, &d->audio) {
-        const iGmAudio *audio = i.ptr;
-        if (audio->props.linkId == linkId) {
-            return index_PtrArrayConstIterator(&i) + 1;
+iMediaId findMediaForLink_Media(const iMedia *d, iGmLinkId linkId, enum iMediaType mediaType) {
+    /* TODO: Use hashes, this will get very slow if there is a large number of media items. */
+    iMediaId mid;
+    for (int i = 0; i < max_MediaType; i++) {
+        if (mediaType == i || !mediaType) {
+            mid = findMediaPtr_Media_(&d->items[i], i, linkId);
+            if (mid.type) {
+                return mid;
+            }
         }
     }
-    return 0;
+    return iInvalidMediaId;
 }
 
-iMediaId findLinkDownload_Media(const iMedia *d, uint16_t linkId) {
-    iConstForEach(PtrArray, i, &d->downloads) {
-        const iGmDownload *dl = i.ptr;
-        if (dl->props.linkId == linkId) {
-            return index_PtrArrayConstIterator(&i) + 1;
-        }
-    }
-    return 0;
+size_t numAudio_Media(const iMedia *d) {
+    return size_PtrArray(&d->items[audio_MediaType]);
 }
 
 iInt2 imageSize_Media(const iMedia *d, iMediaId imageId) {
-    if (imageId > 0 && imageId <= size_PtrArray(&d->images)) {
-        const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1);
+    iAssert(imageId.type == image_MediaType);
+    const size_t index = index_MediaId(imageId);
+    if (index < size_PtrArray(&d->items[image_MediaType])) {
+        const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);
         return img->size;
     }
     return zero_I2();
 }
 
-SDL_Texture *imageTexture_Media(const iMedia *d, uint16_t imageId) {
-    if (imageId > 0 && imageId <= size_PtrArray(&d->images)) {
-        const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1);
+SDL_Texture *imageTexture_Media(const iMedia *d, iMediaId imageId) {
+    iAssert(imageId.type == image_MediaType);
+    const size_t index = index_MediaId(imageId);
+    if (index < size_PtrArray(&d->items[image_MediaType])) {
+        const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);
         return img->texture;
     }
     return NULL;
 }
 
-iBool imageInfo_Media(const iMedia *d, iMediaId imageId, iGmMediaInfo *info_out) {
-    if (imageId > 0 && imageId <= size_PtrArray(&d->images)) {
-        const iGmImage *img   = constAt_PtrArray(&d->images, imageId - 1);
-        info_out->numBytes    = img->numBytes;
-        info_out->type        = cstr_String(&img->props.mime);
-        info_out->isPermanent = img->props.isPermanent;
-        return iTrue;
+iBool info_Media(const iMedia *d, iMediaId mediaId, iGmMediaInfo *info_out) {
+    /* TODO: Use a hash. */
+    const size_t index = index_MediaId(mediaId);
+    switch (mediaId.type) {
+        case image_MediaType:
+            if (index < size_PtrArray(&d->items[image_MediaType])) {
+                const iGmImage *img   = constAt_PtrArray(&d->items[image_MediaType], index);
+                info_out->numBytes    = img->numBytes;
+                info_out->type        = cstr_String(&img->props.mime);
+                info_out->isPermanent = img->props.isPermanent;
+                return iTrue;
+            }
+            break;
+        case audio_MediaType:
+            if (index < size_PtrArray(&d->items[audio_MediaType])) {
+                const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);
+                info_out->type        = cstr_String(&audio->props.mime);
+                info_out->isPermanent = audio->props.isPermanent;
+                return iTrue;
+            }
+            break;
+        case download_MediaType:
+            if (index < size_PtrArray(&d->items[download_MediaType])) {
+                const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);
+                info_out->type = cstr_String(&dl->props.mime);
+                info_out->isPermanent = dl->props.isPermanent;
+                info_out->numBytes = dl->numBytes;
+                return iTrue;
+            }
+            break;
+        case fontpack_MediaType:
+            /* TODO */
+            break;
+        default:
+            break;
     }
     iZap(*info_out);
     return iFalse;
 }
 
 iPlayer *audioData_Media(const iMedia *d, iMediaId audioId) {
-    if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) {
-        const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1);
+    iAssert(audioId.type == audio_MediaType);
+    const size_t index = index_MediaId(audioId);
+    if (index < size_PtrArray(&d->items[audio_MediaType])) {
+        const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);
         return audio->player;
     }
     return NULL;
 }
 
-iBool audioInfo_Media(const iMedia *d, iMediaId audioId, iGmMediaInfo *info_out) {
-    if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) {
-        const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1);
-        info_out->type        = cstr_String(&audio->props.mime);
-        info_out->isPermanent = audio->props.isPermanent;
-        return iTrue;
-    }
-    iZap(*info_out);
-    return iFalse;
-}
-
 iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) {
-    if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) {
-        const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1);
+    iAssert(audioId.type == audio_MediaType);
+    const size_t index = index_MediaId(audioId);
+    if (index < size_PtrArray(&d->items[audio_MediaType])) {
+        const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);
         return audio->player;
     }
     return NULL;
 }
 
 void pauseAllPlayers_Media(const iMedia *d, iBool setPaused) {
-    for (size_t i = 0; i < size_PtrArray(&d->audio); ++i) {
-        const iGmAudio *audio = constAt_PtrArray(&d->audio, i);
+    for (size_t i = 0; i < size_PtrArray(&d->items[audio_MediaType]); ++i) {
+        const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], i);
         if (audio->player) {
             setPaused_Player(audio->player, setPaused);
         }
     }
 }
 
-iBool downloadInfo_Media(const iMedia *d, iMediaId downloadId, iGmMediaInfo *info_out) {
-    if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {
-        const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);
-        info_out->type = cstr_String(&dl->props.mime);
-        info_out->isPermanent = dl->props.isPermanent;
-        info_out->numBytes = dl->numBytes;
-        return iTrue;
-    }
-    iZap(*info_out);
-    return iFalse;
-}
-
 void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **path_out,
                          float *bytesPerSecond_out, iBool *isFinished_out) {
-    *path_out = NULL;
+    iAssert(downloadId.type == download_MediaType);
+    *path_out           = NULL;
     *bytesPerSecond_out = 0.0f;
-    *isFinished_out = iFalse;
-    if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {
-        const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);
+    *isFinished_out     = iFalse;
+    const size_t index  = index_MediaId(downloadId);
+    if (index < size_PtrArray(&d->items[download_MediaType])) {
+        const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);
         if (dl->path) {
             *path_out = dl->path;
         }
@@ -587,6 +696,16 @@ void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **p
     }
 }
 
+void fontpackInfo_Media(const iMedia *d, iMediaId fontpackId, iFontpackMediaInfo *info_out) {
+    iAssert(fontpackId.type == fontpack_MediaType);
+    iZap(*info_out);
+    const size_t index = index_MediaId(fontpackId);
+    if (index < size_PtrArray(&d->items[fontpack_MediaType])) {
+        const iGmFontpack *fp = constAt_PtrArray(&d->items[fontpack_MediaType], index);
+        *info_out = fp->info;
+    }
+}
+
 /*----------------------------------------------------------------------------------------------*/
 
 static void updated_MediaRequest_(iAnyObject *obj) {
diff --git a/src/media.h b/src/media.h
index f7ad6efd..47a4da93 100644
--- a/src/media.h
+++ b/src/media.h
@@ -22,13 +22,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 
 #pragma once
 
+#include "fontpack.h"
+
 #include 
 #include 
 #include 
 #include 
 
-typedef uint16_t iMediaId;
-
 iDeclareType(Player)
 iDeclareType(GmMediaInfo)
 
@@ -38,6 +38,7 @@ struct Impl_GmMediaInfo {
     iBool       isPermanent;
 };
 
+iDeclareType(MediaId)
 iDeclareType(Media)
 iDeclareTypeConstruction(Media)
 
@@ -46,28 +47,81 @@ enum iMediaFlags {
     partialData_MediaFlag = iBit(2),
 };
 
-void    clear_Media             (iMedia *);
-iBool   setDownloadUrl_Media    (iMedia *, uint16_t linkId, const iString *url);
-iBool   setData_Media           (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);
-
-size_t  memorySize_Media        (const iMedia *);
+enum iMediaType { /* Note: There is a limited number of bits for these; see GmRun below. */
+    none_MediaType,
+    image_MediaType,
+    //animatedImage_MediaType, /* TODO */
+    audio_MediaType,
+    download_MediaType,
+    fontpack_MediaType,
+    max_MediaType
+};
 
-iMediaId        findLinkImage_Media (const iMedia *, uint16_t linkId);
-iBool           imageInfo_Media     (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out);
-iInt2           imageSize_Media     (const iMedia *, iMediaId imageId);
-SDL_Texture *   imageTexture_Media  (const iMedia *, iMediaId imageId);
+struct Impl_MediaId {
+    enum iMediaType type;
+    uint16_t id; /* see GmRun for actually used number of bits */
+};
 
-size_t          numAudio_Media      (const iMedia *);
-iMediaId        findLinkAudio_Media (const iMedia *, uint16_t linkId);
-iBool           audioInfo_Media     (const iMedia *, iMediaId audioId, iGmMediaInfo *info_out);
-iPlayer *       audioPlayer_Media   (const iMedia *, iMediaId audioId);
-void            pauseAllPlayers_Media(const iMedia *, iBool setPaused);
+iLocalDef size_t index_MediaId(const iMediaId mediaId) {
+    return (size_t) mediaId.id - 1;
+}
+
+#define iInvalidMediaId     (iMediaId){ none_MediaType, 0 }
+
+void            clear_Media             (iMedia *);
+iBool           setUrl_Media            (iMedia *, uint16_t linkId, enum iMediaType mediaType, const iString *url);
+iBool           setData_Media           (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);
+
+size_t          memorySize_Media        (const iMedia *);
+iMediaId        findMediaForLink_Media  (const iMedia *, uint16_t linkId, enum iMediaType mediaType);
+
+iMediaId        id_Media        (const iMedia *, uint16_t linkId, enum iMediaType type);
+iBool           info_Media      (const iMedia *, iMediaId mediaId, iGmMediaInfo *info_out);
+
+iLocalDef iMediaId findLinkImage_Media(const iMedia *d, uint16_t linkId) {
+    return findMediaForLink_Media(d, linkId, image_MediaType);
+}
+iLocalDef iMediaId findLinkAudio_Media (const iMedia *d, uint16_t linkId) {
+    return findMediaForLink_Media(d, linkId, audio_MediaType);
+}
+iLocalDef iMediaId findLinkDownload_Media(const iMedia *d, uint16_t linkId) {
+    return findMediaForLink_Media(d, linkId, download_MediaType);
+}
+
+iLocalDef iBool imageInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {
+    return info_Media(d, (iMediaId){ image_MediaType, mediaId }, info_out);
+}
+iLocalDef iBool audioInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {
+    return info_Media(d, (iMediaId){ audio_MediaType, mediaId }, info_out);
+}
+iLocalDef iBool downloadInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {
+    return info_Media(d, (iMediaId){ download_MediaType, mediaId }, info_out);
+}
+
+iInt2           imageSize_Media         (const iMedia *, iMediaId imageId);
+SDL_Texture *   imageTexture_Media      (const iMedia *, iMediaId imageId);
+
+size_t          numAudio_Media          (const iMedia *);
+iPlayer *       audioPlayer_Media       (const iMedia *, iMediaId audioId);
+void            pauseAllPlayers_Media   (const iMedia *, iBool setPaused);
 
-iMediaId        findLinkDownload_Media  (const iMedia *, uint16_t linkId);
-iBool           downloadInfo_Media      (const iMedia *, iMediaId downloadId, iGmMediaInfo *info_out);
 void            downloadStats_Media     (const iMedia *, iMediaId downloadId, const iString **path_out,
                                          float *bytesPerSecond_out, iBool *isFinished_out);
 
+iDeclareType(FontpackMediaInfo)
+
+struct Impl_FontpackMediaInfo {
+    iFontPackId packId;
+    iBool isValid;
+    iBool isInstalled;
+    iBool isReadOnly;
+    size_t sizeInBytes;
+    iStringList *names;
+};
+
+void            fontpackInfo_Media      (const iMedia *, iMediaId fontpackId,
+                                         iFontpackMediaInfo *info_out);
+
 /*----------------------------------------------------------------------------------------------*/
 
 iDeclareType(GmRequest)
@@ -78,7 +132,7 @@ iDeclareClass(MediaRequest)
 struct Impl_MediaRequest {
     iObject          object;
     iDocumentWidget *doc;
-    unsigned int     linkId;
+    unsigned int     linkId;    
     iGmRequest *     req;
 };
 
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 45a8cf2d..44db3e5b 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -400,7 +400,18 @@ void init_DocumentWidget(iDocumentWidget *d) {
     addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");
 }
 
+void cancelAllRequests_DocumentWidget(iDocumentWidget *d) {
+    iForEach(ObjectList, i, d->media) {
+        iMediaRequest *mr = i.object;
+        cancel_GmRequest(mr->req);
+    }
+    if (d->request) {
+        cancel_GmRequest(d->request);
+    }
+}
+
 void deinit_DocumentWidget(iDocumentWidget *d) {
+    cancelAllRequests_DocumentWidget(d);
     pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);
     removeTicker_App(animate_DocumentWidget_, d);
     removeTicker_App(prerender_DocumentWidget_, d);
@@ -564,7 +575,8 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
             pushBack_PtrArray(&d->visibleWideRuns, run);
         }
     }
-    if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) {
+    /* Image runs are static so they're drawn as part of the content. */
+    if (run->mediaType && run->mediaType != image_MediaType) {
         iAssert(run->mediaId);
         pushBack_PtrArray(&d->visibleMedia, run);
     }
@@ -758,14 +770,14 @@ static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
     uint32_t interval = invalidInterval_;
     iConstForEach(PtrArray, i, &d->visibleMedia) {
         const iGmRun *run = i.ptr;
-        if (run->mediaType == audio_GmRunMediaType) {
-            iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);
+        if (run->mediaType == audio_MediaType) {
+            iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
             if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
                 (isStarted_Player(plr) && !isPaused_Player(plr))) {
                 interval = iMin(interval, 1000 / 15);
             }
         }
-        else if (run->mediaType == download_GmRunMediaType) {
+        else if (run->mediaType == download_MediaType) {
             interval = iMin(interval, 1000);
         }
     }
@@ -784,8 +796,8 @@ static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
         refresh_Widget(d);
         iConstForEach(PtrArray, i, &d->visibleMedia) {
             const iGmRun *run = i.ptr;
-            if (run->mediaType == audio_GmRunMediaType) {
-                iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);
+            if (run->mediaType == audio_MediaType) {
+                iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));
                 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
                     flags_Player(plr) & adjustingVolume_PlayerFlag) {
                     setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
@@ -1244,6 +1256,9 @@ static const char *zipPageHeading_(const iRangecc mime) {
     if (equalCase_Rangecc(mime, "application/gpub+zip")) {
         return book_Icon " Gempub";
     }
+    else if (equalCase_Rangecc(mime, mimeType_FontPack)) {
+        return "\U0001f520 Fontpack";
+    }
     iRangecc type = iNullRange;
     nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */
     nextSplit_Rangecc(mime, "/", &type);
@@ -1258,165 +1273,175 @@ static const char *zipPageHeading_(const iRangecc mime) {
 
 static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) {
     iWidget *w = as_Widget(d);
-    delete_Gempub(d->sourceGempub);
-    d->sourceGempub = NULL;
-    if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||
-        !cmpCase_String(&d->sourceMime, mimeType_Gempub) ||
-        endsWithCase_String(d->mod.url, ".gpub")) {
-        iGempub *gempub = new_Gempub();
-        if (open_Gempub(gempub, &d->sourceContent)) {
-            setBaseUrl_Gempub(gempub, d->mod.url);
-            setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
-            setCStr_String(&d->sourceMime, mimeType_Gempub);
-            d->sourceGempub = gempub;
-        }
-        else {
-            delete_Gempub(gempub);
-        }
-    }
-    if (!d->sourceGempub) {
-        const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));
-        iBool isInside = iFalse;
-        if (localPath && !fileExists_FileInfo(localPath)) {
-            /* This URL may refer to a file inside the archive. */
-            localPath = findContainerArchive_Path(localPath);
-            isInside = iTrue;
-        }
-        if (localPath && equal_CStr(mediaType_Path(localPath), "application/gpub+zip")) {
+    /* Gempub page behavior and footer actions. */ {
+        /* TODO: move this to gempub.c */
+        delete_Gempub(d->sourceGempub);
+        d->sourceGempub = NULL;
+        if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||
+            !cmpCase_String(&d->sourceMime, mimeType_Gempub) ||
+            endsWithCase_String(d->mod.url, ".gpub")) {
             iGempub *gempub = new_Gempub();
-            if (openFile_Gempub(gempub, localPath)) {
-                setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));
-                if (!isInside) {
-                    setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
-                    setCStr_String(&d->sourceMime, mimeType_Gempub);
-                }
+            if (open_Gempub(gempub, &d->sourceContent)) {
+                setBaseUrl_Gempub(gempub, d->mod.url);
+                setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
+                setCStr_String(&d->sourceMime, mimeType_Gempub);
                 d->sourceGempub = gempub;
             }
             else {
                 delete_Gempub(gempub);
             }
         }
-    }
-    if (d->sourceGempub) {
-        if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {
-            if (!isRemote_Gempub(d->sourceGempub)) {
-                iArray *items = collectNew_Array(sizeof(iMenuItem));
-                pushBack_Array(
-                    items,
-                    &(iMenuItem){ book_Icon " ${gempub.cover.view}",
-                                  0,
-                                  0,
-                                  format_CStr("!open url:%s",
-                                              cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
-                if (navSize_Gempub(d->sourceGempub) > 0) {
-                    pushBack_Array(
-                        items,
-                        &(iMenuItem){
-                            format_CStr(forwardArrow_Icon " %s",
-                                        cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
-                            SDLK_RIGHT,
-                            0,
-                            format_CStr("!open url:%s",
-                                        cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
-                }
-                makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
+        if (!d->sourceGempub) {
+            const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));
+            iBool isInside = iFalse;
+            if (localPath && !fileExists_FileInfo(localPath)) {
+                /* This URL may refer to a file inside the archive. */
+                localPath = findContainerArchive_Path(localPath);
+                isInside = iTrue;
             }
-            else {
-                makeFooterButtons_DocumentWidget_(
-                    d,
-                    (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",
-                                     SDLK_s,
-                                     KMOD_PRIMARY | KMOD_SHIFT,
-                                     "document.save open:1" },
-                                   { download_Icon " " saveToDownloads_Label,
-                                     SDLK_s,
-                                     KMOD_PRIMARY,
-                                     "document.save" } },
-                    2);
-            }
-            if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
-                redoLayout_GmDocument(d->doc);
-                updateVisible_DocumentWidget_(d);
-                invalidate_DocumentWidget_(d);
+            if (localPath && equal_CStr(mediaType_Path(localPath), mimeType_Gempub)) {
+                iGempub *gempub = new_Gempub();
+                if (openFile_Gempub(gempub, localPath)) {
+                    setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));
+                    if (!isInside) {
+                        setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
+                        setCStr_String(&d->sourceMime, mimeType_Gempub);
+                    }
+                    d->sourceGempub = gempub;
+                }
+                else {
+                    delete_Gempub(gempub);
+                }
             }
         }
-        else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
-            makeFooterButtons_DocumentWidget_(
-                d,
-                (iMenuItem[]){ { format_CStr(book_Icon " %s",
-                                             cstr_String(property_Gempub(d->sourceGempub,
-                                                                         title_GempubProperty))),
-                                 SDLK_LEFT,
-                                 0,
-                                 format_CStr("!open url:%s",
-                                             cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
-                1);
-        }
-        else {
-            /* Navigation buttons. */
-            iArray *items = collectNew_Array(sizeof(iMenuItem));
-            const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
-            if (navIndex != iInvalidPos) {
-                if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
+        if (d->sourceGempub) {
+            if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {
+                if (!isRemote_Gempub(d->sourceGempub)) {
+                    iArray *items = collectNew_Array(sizeof(iMenuItem));
                     pushBack_Array(
                         items,
-                        &(iMenuItem){
-                            format_CStr(forwardArrow_Icon " %s",
-                                        cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),
-                            SDLK_RIGHT,
-                            0,
-                            format_CStr("!open url:%s",
-                                        cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });
+                        &(iMenuItem){ book_Icon " ${gempub.cover.view}",
+                                      0,
+                                      0,
+                                      format_CStr("!open url:%s",
+                                                  cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
+                    if (navSize_Gempub(d->sourceGempub) > 0) {
+                        pushBack_Array(
+                            items,
+                            &(iMenuItem){
+                                format_CStr(forwardArrow_Icon " %s",
+                                            cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
+                                SDLK_RIGHT,
+                                0,
+                                format_CStr("!open url:%s",
+                                            cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
+                    }
+                    makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
                 }
-                if (navIndex > 0) {
-                    pushBack_Array(
-                        items,
-                        &(iMenuItem){
-                            format_CStr(backArrow_Icon " %s",
-                                        cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),
-                            SDLK_LEFT,
-                            0,
-                            format_CStr("!open url:%s",
-                                        cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });
+                else {
+                    makeFooterButtons_DocumentWidget_(
+                        d,
+                        (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",
+                                         SDLK_s,
+                                         KMOD_PRIMARY | KMOD_SHIFT,
+                                         "document.save open:1" },
+                                       { download_Icon " " saveToDownloads_Label,
+                                         SDLK_s,
+                                         KMOD_PRIMARY,
+                                         "document.save" } },
+                        2);
                 }
-                else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
-                    pushBack_Array(
-                        items,
-                        &(iMenuItem){
-                            format_CStr(book_Icon " %s",
-                                        cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
-                            SDLK_LEFT,
-                            0,
-                            format_CStr("!open url:%s",
-                                        cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
+                if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
+                    redoLayout_GmDocument(d->doc);
+                    updateVisible_DocumentWidget_(d);
+                    invalidate_DocumentWidget_(d);
                 }
             }
-            if (!isEmpty_Array(items)) {
-                makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));                
+            else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
+                makeFooterButtons_DocumentWidget_(
+                    d,
+                    (iMenuItem[]){ { format_CStr(book_Icon " %s",
+                                                 cstr_String(property_Gempub(d->sourceGempub,
+                                                                             title_GempubProperty))),
+                                     SDLK_LEFT,
+                                     0,
+                                     format_CStr("!open url:%s",
+                                                 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
+                    1);
             }
-        }
-        if (!isCached && prefs_App()->pinSplit &&
-            equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
-            const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub);
-            if (navStart) {
-                iWindow *win = get_Window();
-                /* Auto-split to show index and the first navigation link. */
-                if (numRoots_Window(win) == 2) {
-                    /* This document is showing the index page. */
-                    iRoot *other = otherRoot_Window(win, w->root);
-                    postCommandf_Root(other, "open url:%s", cstr_String(navStart));
-                    if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) {
-                        /* On the wrong side. */

(truncated output; full size was 131.02 KB)

Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.8/cdiff/960df03c17091aca37f53eaab8fc27c669d26a5e
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
92.023164 milliseconds
Gemini-to-HTML Time
3.461807 milliseconds

This content has been proxied by September (ba2dc).