bd85159b36db7ff8c02b32eb18d96ed6066feb8f
braney
  Wed Aug 5 13:59:46 2026 -0700
hgTracks: measure text with the same font engine that will draw it, refs #38027

Pack mode works out how many rows a track needs by measuring its item labels,
and mgFontStringWidth answers from whichever text engine is loaded at the time.
FreeType was only loaded when an image was created, so the measuring could
happen on the bitmap engine while the drawing happened on FreeType -- and which
one you got depended on whether the ideogram image had been built first.

Load the engine in initTl(), alongside the rest of the font setup, before
anything measures a string.  maybeNewFonts() and the new initFontEngine() share
chosenFreeTypeFont(), and mgLoadFontEngine() loads an engine without attaching
it to an image.  ftInitialize() now keeps the face it already has when asked for
the same font file, so the glyph cache hanging off that face survives the extra
call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/hg/hgTracks/config.c src/hg/hgTracks/config.c
index 28257e86458..2dacb531f32 100644
--- src/hg/hgTracks/config.c
+++ src/hg/hgTracks/config.c
@@ -140,52 +140,74 @@
 char *emptyStyles[] = {
 "Normal"
 };
 
 static boolean freeTypeOn()
 {
 #ifdef USE_FREETYPE
 char *defaultState = "on";
 #else // USE_FREETYPE
 char *defaultState = "off";
 #endif // USE_FREETYPE
 
 return sameString(cfgOptionDefault("freeType", defaultState), "on");
 }
 
-void maybeNewFonts(struct hvGfx *hvg)
-/* Check to see if we want to use the alternate font engine (FreeType2). */
+static char *chosenFreeTypeFont(char **retFontName)
+/* Return the file holding the FreeType font the user has picked, and its name in
+ * retFontName.  NULL means stay on the bitmap engine. */
 {
 if (!freeTypeOn())
-    return;
+    return NULL;
 
 if (sameString(tl.textFont, "Bitmap"))
-    return;
-
-char *fontDir = cfgOptionDefault("freeTypeDir", "../htdocs/urw-fonts");
-char buffer[4096];
+    return NULL;
 
 int ii;
 for(ii=0; ii < ArraySize(freeTypeFonts); ii++)
     if (sameString(freeTypeFonts[ii].name, tl.textFont))
         break;
 if (ii == ArraySize(freeTypeFonts))
-    return;   // not a font we know about; leave the bitmap engine in place
-char *fontFile = freeTypeFonts[ii].file;
-char *fontName = freeTypeFonts[ii].name;
-safef(buffer, sizeof buffer, "%s/%s", fontDir, fontFile);
-hvGfxSetFontMethod(hvg, FONT_METHOD_FREETYPE, fontName, buffer );
+    return NULL;   // not a font we know about; leave the bitmap engine in place
+
+static char buffer[PATH_LEN];
+char *fontDir = cfgOptionDefault("freeTypeDir", "../htdocs/urw-fonts");
+safef(buffer, sizeof buffer, "%s/%s", fontDir, freeTypeFonts[ii].file);
+*retFontName = freeTypeFonts[ii].name;
+return buffer;
+}
+
+void initFontEngine()
+/* Load the text engine the user has picked, before anything measures a string.
+ * Pack mode reserves room for an item by measuring its label, and
+ * mgFontStringWidth answers from whichever engine is loaded at the time.  Doing
+ * this up front keeps the packing from depending on whether some earlier image
+ * on the page -- the ideogram -- happened to load FreeType first. */
+{
+char *fontName = NULL;
+char *fontFile = chosenFreeTypeFont(&fontName);
+if (fontFile != NULL)
+    mgLoadFontEngine(FONT_METHOD_FREETYPE, fontFile);
+}
+
+void maybeNewFonts(struct hvGfx *hvg)
+/* Check to see if we want to use the alternate font engine (FreeType2). */
+{
+char *fontName = NULL;
+char *fontFile = chosenFreeTypeFont(&fontName);
+if (fontFile != NULL)
+    hvGfxSetFontMethod(hvg, FONT_METHOD_FREETYPE, fontName, fontFile);
 }
 
 static void textFontDropDown()
 /* Create drop down for font size. */
 {
 /* get current values for font and style */
 char *currentFontName = cloneString(tl.textFont);
 char *currentStyle = strchr(currentFontName, '-');
 if (currentStyle)
     *currentStyle++ = 0;
 else
     currentStyle = "Normal";
 
 char *faceNames[ArraySize(freeTypeFonts) + 1];  // +1 for the "Bitmap" entry below
 int ii;