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/lib/memgfx.c src/lib/memgfx.c
index b5d159f649d..857ce69a1f1 100644
--- src/lib/memgfx.c
+++ src/lib/memgfx.c
@@ -1,1483 +1,1492 @@
 /* memgfx - routines for drawing on bitmaps in memory.
  * Currently limited to 256 color bitmaps. 
  *
  * This file is copyright 2002 Jim Kent, but license is hereby
  * granted for all use - public, private or commercial. */
 
 #include "common.h"
 #include "memgfx.h"
 #include "gemfont.h"
 #include "localmem.h"
 #include "vGfx.h"
 #include "vGfxPrivate.h"
 #include "colHash.h"
 #include "freeType.h"
 
 
 Color multiply(Color src, Color new)
 {
 unsigned char rs = (src >> 0) & 0xff;
 unsigned char gs = (src >> 8) & 0xff;
 unsigned char bs = (src >> 16) & 0xff;
 unsigned char rn = (new >> 0) & 0xff;
 unsigned char gn = (new >> 8) & 0xff;
 unsigned char bn = (new >> 16) & 0xff;
 
 unsigned char ro = ((unsigned) rn * rs) / 255;
 unsigned char go = ((unsigned) gn * gs) / 255;
 unsigned char bo = ((unsigned) bn * bs) / 255;
 return MAKECOLOR_32(ro, go, bo);
 }
 
 #ifndef min3
 #define min3(x,y,z) (min(x,min(y,z)))
 /* Return min of x,y, and z. */
 #endif
 
 #ifndef max3
 #define max3(x,y,z) (max(x,max(y,z)))
 /* Return max of x,y, and z. */
 #endif
 
 void _mgPutDotMultiply(struct memGfx *mg, int x, int y,Color color)
 {
 Color *pt = _mgPixAdr(mg,x,y);
 *pt = multiply(*pt, color);
 }
 
 
 static void mgSetDefaultColorMap(struct memGfx *mg)
 /* Set up default color map for a memGfx. */
 {
     return;
 }
 
 
 void mgSetWriteMode(struct memGfx *mg, unsigned int writeMode)
 /* Set write mode */
 {
 mg->writeMode = writeMode;
 }
 
 void mgSetClip(struct memGfx *mg, int x, int y, int width, int height)
 /* Set clipping rectangle. */
 {
 int x2, y2;
 if (x < 0)
     x = 0;
 if (y < 0)
     y = 0;
 x2 = x + width;
 if (x2 > mg->width)
     x2 = mg->width;
 y2 = y + height;
 if (y2 > mg->height)
     y2 = mg->height;
 mg->clipMinX = x;
 mg->clipMaxX = x2;
 mg->clipMinY = y;
 mg->clipMaxY = y2;
 }
 
 void mgUnclip(struct memGfx *mg)
 /* Set clipping rect cover full thing. */
 {
 mgSetClip(mg, 0,0,mg->width, mg->height);
 }
 
 struct memGfx *mgNew(int width, int height)
 /* Return new memGfx. Note new pixel memory is uninitialized */
 {
 struct memGfx *mg;
 
 mg = needMem(sizeof(*mg));
 mg->width = width;
 mg->height = height;
 mg->pixels = needLargeMem(width*height*sizeof(Color));
 mgSetDefaultColorMap(mg);
 mgUnclip(mg);
 return mg;
 }
 
 void mgClearPixels(struct memGfx *mg)
 /* Set all pixels to background. */
 {
 memset((unsigned char *)mg->pixels, 0xff, mg->width*mg->height*sizeof(unsigned int));
 }
 
 void mgClearPixelsTrans(struct memGfx *mg)
 /* Set all pixels to transparent. */
 {
 unsigned int *ptr = mg->pixels;
 unsigned int *lastPtr = &mg->pixels[mg->width * mg->height];
 for(; ptr < lastPtr; ptr++)
 #ifdef MEMGFX_BIGENDIAN
     *ptr = 0xffffff00;
 #else
     *ptr = 0x00ffffff;  // transparent white
 #endif
 }
 
 Color mgFindColor(struct memGfx *mg, unsigned char r, unsigned char g, unsigned char b)
 /* Returns closest color in color map to rgb values.  If it doesn't
  * already exist in color map and there's room, it will create
  * exact color in map. */
 {
 return MAKECOLOR_32(r,g,b);
 }
 
 Color mgFindAlphaColor(struct memGfx *mg, unsigned char r, unsigned char g, unsigned char b, unsigned char a)
 /* Returns closest color in color map to rgba values.  If it doesn't
  * already exist in color map and there's room, it will create
  * exact color in map. */
 {
 return MAKECOLOR_32_A(r,g,b,a);
 }
 
 struct rgbColor colorIxToRgb(int colorIx)
 /* Return rgb value at color index. */
 {
 struct rgbColor rgb;
 #ifdef MEMGFX_BIGENDIAN
 rgb.r = (colorIx >> 24) & 0xff;
 rgb.g = (colorIx >> 16) & 0xff;
 rgb.b = (colorIx >> 8) & 0xff;
 rgb.a = (colorIx >> 0) & 0xff;
 #else
 rgb.r = (colorIx >> 0) & 0xff;
 rgb.g = (colorIx >> 8) & 0xff;
 rgb.b = (colorIx >> 16) & 0xff;
 rgb.a = (colorIx >> 24) & 0xff;
 #endif
 return rgb;
 }
 
 struct rgbColor mgColorIxToRgb(struct memGfx *mg, int colorIx)
 /* Return rgb value at color index. */
 {
 return colorIxToRgb(colorIx);
 }
 
 Color mgClosestColor(struct memGfx *mg, unsigned char r, unsigned char g, unsigned char b)
 /* Returns closest color in color map to r,g,b */
 {
 return MAKECOLOR_32(r,g,b);
 }
 
 
 Color mgAddColor(struct memGfx *mg, unsigned char r, unsigned char g, unsigned char b)
 /* Adds color to end of color map if there's room. */
 {
 return MAKECOLOR_32(r,g,b);
 }
 
 int mgColorsFree(struct memGfx *mg)
 /* Returns # of unused colors in color map. */
 {
 return 1 << 23;
 }
 
 void mgFree(struct memGfx **pmg)
 {
 struct memGfx *mg = *pmg;
 if (mg != NULL)
     {
     if (mg->pixels != NULL)
 	freeMem(mg->pixels);
     if (mg->colorHash)
         colHashFree(&mg->colorHash);
     zeroBytes(mg, sizeof(*mg));
     freeMem(mg);
     }
 *pmg = NULL;
 }
 
 static void nonZeroCopy(Color *d, Color *s, int width)
 /* Copy non-zero colors. */
 {
 Color c;
 int i;
 for (i=0; i<width; ++i)
     {
     if ((c = s[i]) != MG_WHITE)
         {
         if (COLOR_32_ALPHA(c) == 0xff)
             d[i] = c;
         else
             mixColor(&d[i], c);
         }
     }
 }
 
 static void mgPutSegMaybeZeroClear(struct memGfx *mg, int x, int y, int width, Color *dots, boolean zeroClear)
 /* Put a series of dots starting at x, y and going to right width pixels.
  * Possibly don't put zero dots though. */
 {
 int x2;
 Color *pt;
 if (y < mg->clipMinY || y > mg->clipMaxY)
     return;
 x2 = x + width;
 if (x2 > mg->clipMaxX)
     x2 = mg->clipMaxX;
 if (x < mg->clipMinX)
     {
     dots += mg->clipMinX - x;
     x = mg->clipMinX;
     }
 width = x2 - x;
 if (width > 0)
     {
     pt = _mgPixAdr(mg, x, y);
     if (zeroClear)
         nonZeroCopy(pt, dots, width);
     else
         {
         int i;
         for (i=0; i<width; i++)
             mixColor(&pt[i], dots[i]);
         }
     }
 }
 
 void mgVerticalSmear(struct memGfx *mg,
 	int xOff, int yOff, int width, int height, 
 	Color *dots, boolean zeroClear)
 /* Put a series of one 'pixel' width vertical lines. */
 {
 while (--height >= 0)
     {
     mgPutSegMaybeZeroClear(mg, xOff, yOff, width, dots, zeroClear);
     ++yOff;
     }
 }
 
 
 void mgDrawBoxNormal(struct memGfx *mg, int x, int y, int width, int height, Color color)
 {
 int x2 = x + width;
 int y2 = y + height;
 
 if (x < mg->clipMinX)
     x = mg->clipMinX;
 if (y < mg->clipMinY)
     y = mg->clipMinY;
 if (x2 < mg->clipMinX)
     x2 = mg->clipMinX;
 if (y2 < mg->clipMinY)
     y2 = mg->clipMinY;
 
 if (x > mg->clipMaxX)
     x = mg->clipMaxX;
 if (y > mg->clipMaxY)
     y = mg->clipMaxY;
 if (x2 > mg->clipMaxX)
     x2 = mg->clipMaxX;
 if (y2 > mg->clipMaxY)
     y2 = mg->clipMaxY;
 width = x2-x;
 height = y2-y;
 
 for (int i=x; i<x2; i++)
     {
     for (int j=y; j<y2; j++)
         {
         mixDot(mg, i, j, COLOR_32_ALPHA(color)/255.0, color);
         }
     }
 
 }
 
 void mgDrawBoxMultiply(struct memGfx *mg, int x, int y, int width, int height, Color color)
 {
 int i;
 Color *pt;
 int x2 = x + width;
 int y2 = y + height;
 int wrapCount;
 
 if (x < mg->clipMinX)
     x = mg->clipMinX;
 if (y < mg->clipMinY)
     y = mg->clipMinY;
 if (x2 > mg->clipMaxX)
     x2 = mg->clipMaxX;
 if (y2 > mg->clipMaxY)
     y2 = mg->clipMaxY;
 width = x2-x;
 height = y2-y;
 if (width > 0 && height > 0)
     {
     pt = _mgPixAdr(mg,x,y);
     wrapCount = _mgBpr(mg) - width;
     while (--height >= 0)
 	{
         Color src = *pt;
 	i = width;
 	while (--i >= 0)
 	    *pt++ = multiply(src, color);
 	pt += wrapCount;
 	}
     }
 }
 
 void mgDrawBox(struct memGfx *mg, int x, int y, int width, int height, Color color)
 {
 switch(mg->writeMode)
     {
     case MG_WRITE_MODE_NORMAL:
         {
         mgDrawBoxNormal(mg,x,y, width, height, color);
         }
         break;
     case MG_WRITE_MODE_MULTIPLY:
         {
         mgDrawBoxMultiply(mg,x,y, width, height, color);
         }
         break;
     }
 }
 
 #define fraction(X) (((double)(X))-(double)(int)(X))
 #define invFraction(X) (1.0-fraction(X))
 
 void mgAliasLine( struct memGfx *mg, int x1, int y1,
   int x2, int y2, Color color)
 /* Draw an antialiased line using the Wu algorithm. */
 {
 double dx = (double)x2 - (double)x1;
 double dy = (double)y2 - (double)y1;
 
 // figure out what quadrant we're in
 if ( fabs(dx) > fabs(dy) ) 
     {
     if ( x2 < x1 ) 
 	{
 	// swap start and end points
 	int tmp = x2;
 	x2 = x1;
 	x1 = tmp;
 
 	tmp = y2;
 	y2 = y1;
 	y1 = tmp;
 	}
 
     double gradient = dy / dx;
     double xend = round(x1);
     double yend = y1 + gradient*(xend - x1);
     double xgap = invFraction(x1 + 0.5);
     int xpxl1 = xend;
     int ypxl1 = (int)yend;
     mixDot(mg, xpxl1, ypxl1, invFraction(yend)*xgap, color);
     mixDot(mg, xpxl1, ypxl1+1, fraction(yend)*xgap, color);
 
     double intery = yend + gradient;
 
     xend = round(x2);
     yend = y2 + gradient*(xend - x2);
     xgap = fraction(x2+0.5);
     int xpxl2 = xend;
     int ypxl2 = (int)yend;
     mixDot(mg, xpxl2, ypxl2, invFraction(yend) * xgap, color);
     mixDot(mg, xpxl2, ypxl2 + 1, fraction(yend) * xgap, color);
 
     int x;
     for(x=xpxl1+1; x <= (xpxl2-1); x++) 
 	{
 	mixDot(mg, x, (int)intery, invFraction(intery), color);
 	mixDot(mg, x, (int)intery + 1, fraction(intery), color);
 	intery += gradient;
 	}
     } 
 else  // ( fabs(dx) <= fabs(dy) ) 
     {    
     if ( y2 < y1 ) 
 	{
 	// swap start and end points
 	int tmp = x2;
 	x2 = x1;
 	x1 = tmp;
 
 	tmp = y2;
 	y2 = y1;
 	y1 = tmp;
 	}
 
     double gradient = dx / dy;
     double yend = rint(y1);
     double xend = x1 + gradient*(yend - y1);
     double ygap = invFraction(y1 + 0.5);
     int ypxl1 = yend;
     int xpxl1 = (int)xend;
     mixDot(mg, xpxl1, ypxl1, invFraction(xend)*ygap, color);
     mixDot(mg, xpxl1, ypxl1+1, fraction(xend)*ygap, color);
     double interx = xend + gradient;
 
     yend = rint(y2);
     xend = x2 + gradient*(yend - y2);
     ygap = fraction(y2+0.5);
     int ypxl2 = yend;
     int xpxl2 = (int)xend;
     mixDot(mg, xpxl2, ypxl2, invFraction(xend) * ygap, color);
     mixDot(mg, xpxl2, ypxl2 + 1, fraction(xend) * ygap, color);
 
     int y;
     for(y=ypxl1+1; y <= (ypxl2-1); y++) 
 	{
 	mixDot(mg, (int)interx, y, invFraction(interx), color);
 	mixDot(mg, (int)interx + 1, y, fraction(interx), color);
 	interx += gradient;
 	}
     }
 }
 #undef fraction
 #undef invFraction
 
 
 void mgBrezy(struct memGfx *mg, int x1, int y1, int x2, int y2, Color color,
 	int yBase, boolean fillFromBase)
 /* Brezenham line algorithm.  Optionally fill in under line. */
 {
 if (x1 == x2)
     {
     int y,height;
     if (y1 > y2)
 	{
 	y = y2;
 	height = y1-y2+1;
 	}
     else
         {
 	y = y1;
 	height = y2-y1+1;
 	}
     if (fillFromBase)
         {
 	if (y < yBase)
 	    mgDrawBox(mg, x1, y, 1, yBase-y, color);
 	}
     else
         mgDrawBox(mg, x1, y, 1, height, color);
     }
 else if (y1 == y2)
     {
     int x,width;
     if (x1 > x2)
         {
 	x = x2;
 	width = x1-x2+1;
 	}
     else
         {
 	x = x1;
 	width = x2-x1+1;
 	}
     if (fillFromBase)
         {
 	if (y1 < yBase)
 	    mgDrawBox(mg, x, y1, width, yBase - y1, color);
 	}
     else
         {
 	mgDrawBox(mg, x, y1, width, 1, color);
 	}
     }
 else
     {
     int duty_cycle;
     int incy;
     int delta_x, delta_y;
     int dots;
     delta_y = y2-y1;
     delta_x = x2-x1;
     if (delta_y < 0) 
 	{
 	delta_y = -delta_y;
 	incy = -1;
 	}
     else
 	{
 	incy = 1;
 	}
     if (delta_x < 0) 
 	{
 	delta_x = -delta_x;
 	incy = -incy;
 	x1 = x2;
 	y1 = y2;
 	}
     duty_cycle = (delta_x - delta_y)/2;
     if (delta_x >= delta_y)
 	{
 	dots = delta_x+1;
 	while (--dots >= 0)
 	    {
 	    if (fillFromBase)
 		{
 		if (y1 < yBase)
 		    mgDrawBox(mg,x1,y1,1,yBase-y1,color);
 		}
 	    else
 		mixDot(mg,x1,y1,COLOR_32_ALPHA(color)/255.0,color);
 	    duty_cycle -= delta_y;
 	    x1 += 1;
 	    if (duty_cycle < 0)
 		{
 		duty_cycle += delta_x;	  /* update duty cycle */
 		y1+=incy;
 		}
 	    }
 	}
     else
 	{
 	dots = delta_y+1;
 	while (--dots >= 0)
 	    {
 	    if (fillFromBase)
 		{
 		if (y1 < yBase)
 		    mgDrawBox(mg,x1,y1,1,yBase-y1,color);
 		}
 	    else
 		mixDot(mg,x1,y1,COLOR_32_ALPHA(color)/255.0,color);
 	    duty_cycle += delta_x;
 	    y1+=incy;
 	    if (duty_cycle > 0)
 		{
 		duty_cycle -= delta_y;	  /* update duty cycle */
 		x1 += 1;
 		}
 	    }
 	}
     }
 }
 
 void mgDrawLine(struct memGfx *mg, int x1, int y1, int x2, int y2, Color color)
 /* Draw a line from one point to another. Draws it antialiased if
  * it's not horizontal or vertical. */
 {
 if ((x1 == x2) || (y1 == y2))
     mgBrezy(mg, x1, y1, x2, y2, color, 0, FALSE);
 else
     mgAliasLine(mg, x1, y1, x2, y2, color);
 }
 
 void mgFillUnder(struct memGfx *mg, int x1, int y1, int x2, int y2, 
 	int bottom, Color color)
 /* Draw a 4 sided filled figure that has line x1/y1 to x2/y2 at
  * it's top, a horizontal line at bottom as it's bottom, and
  * vertical lines from the bottom to y1 on the left and bottom to
  * y2 on the right. */
 {
 mgBrezy(mg, x1, y1, x2, y2, color, bottom, TRUE);
 }
 
 void mgPutSeg(struct memGfx *mg, int x, int y, int width, Color *dots)
 /* Put a series of dots starting at x, y and going to right width pixels. */
 {
 mgPutSegMaybeZeroClear(mg, x, y, width, dots, FALSE);
 }
 
 void mgPutSegZeroClear(struct memGfx *mg, int x, int y, int width, Color *dots)
 /* Put a series of dots starting at x, y and going to right width pixels.
  * Don't put zero dots though. */
 {
 mgPutSegMaybeZeroClear(mg, x, y, width, dots, TRUE);
 }
 
 
 void mgDrawHorizontalLine(struct memGfx *mg, int y1, Color color)
 /*special case of mgDrawLine, for horizontal line across entire window 
   at y-value y1.*/
 {
 mgDrawLine( mg, mg->clipMinX, y1, mg->clipMaxX, y1, color);
 }
 
 void mgLineH(struct memGfx *mg, int y, int x1, int x2, Color color)
 /* Draw horizizontal line width pixels long starting at x/y in color */
 {
 if (y >= mg->clipMinY && y < mg->clipMaxY)
     {
     int w;
     if (x1 < mg->clipMinX)
         x1 = mg->clipMinX;
     if (x2 > mg->clipMaxX)
         x2 = mg->clipMaxX;
     w = x2 - x1;
     if (w > 0)
         {
 	Color *pt = _mgPixAdr(mg,x1,y);
 	if (mg->writeMode == MG_WRITE_MODE_MULTIPLY)
 	    {
 	    while (--w >= 0)
 		{
 		*pt = multiply(*pt, color);
 		pt += 1;
 		}
 	    }
 	else
 	    {
 	    while (--w >= 0)
                 mixColor(pt++, color);
 	    }
 	}
     }
 }
 
 
 boolean mgClipForBlit(int *w, int *h, int *sx, int *sy,
 	struct memGfx *dest, int *dx, int *dy)
 {
 /* Make sure we don't overwrite destination. */
 int over;
 
 if ((over = dest->clipMinX - *dx) > 0)
     {
     *w -= over;
     *sx += over;
     *dx = dest->clipMinX;
     }
 if ((over = dest->clipMinY - *dy) > 0)
     {
     *h -= over;
     *sy += over;
     *dy = dest->clipMinY;
     }
 if ((over = *w + *dx - dest->clipMaxX) > 0)
     *w -= over; 
 if ((over = *h + *dy - dest->clipMaxY) > 0)
     *h -= over;
 return (*h > 0 && *w > 0);
 }
 
 #ifdef SOON /* Simple but as yet untested blit function. */
 /* Note - these haven't been modified to have blit respect alpha in the blitted object - they
  * would need to start calling mixDot instead of just smashing the previous content with memcpy. */
 void mgBlit(int width, int height, 
     struct memGfx *source, int sourceX, int sourceY,
     struct memGfx *dest, int destX, int destY)
 /* Copy pixels in a rectangle from source to destination */
 {
 if (!mgClipForBlit(&width, &height, &sourceX, &sourceY, dest, &destX, &destY))
     return;
 while (--height >= 0)
     {
     Color *dLine = _mgPixAdr(dest,destX,destY++);
     Color *sLine = _mgPixAdr(source,sourceX,sourceY++);
     memcpy(dLine, sLine, width * sizeof(Color));
     }
 }
 #endif /* SOON */
 
 void mgTextBlit(int width, int height, int bitX, int bitY,
 	unsigned char *bitData, int bitDataRowBytes, 
 	struct memGfx *dest, int destX, int destY, 
 	Color color, Color backgroundColor)
 /* Copy pixels from a bit-a-pixel source to a fully colored destination
  * within rectangle */
 {
 UBYTE *inLine;
 Color *outLine;
 UBYTE inLineBit;
 
 if (!mgClipForBlit(&width, &height, &bitX, &bitY, dest, &destX, &destY))
     return;
 
 inLine = bitData + (bitX>>3) + bitY * bitDataRowBytes;
 inLineBit = (0x80 >> (bitX&7));
 outLine = _mgPixAdr(dest,destX,destY);
 while (--height >= 0)
     {
     UBYTE *in = inLine;
     Color *out = outLine;
     UBYTE inBit = inLineBit;
     UBYTE inByte = *in++;
     int i = width;
     while (--i >= 0)
 	{
 	if (inBit & inByte)
             mixColor(out, color);
 	++out;
 	if ((inBit >>= 1) == 0)
 	    {
 	    inByte = *in++;
 	    inBit = 0x80;
 	    }
 	}
     inLine += bitDataRowBytes;
     outLine += _mgBpr(dest);
     }
 }
 
 void mgTextBlitSolid(int width, int height, int bitX, int bitY,
 	unsigned char *bitData, int bitDataRowBytes, 
 	struct memGfx *dest, int destX, int destY, 
 	Color color, Color backgroundColor)
 {
 UBYTE *inLine;
 Color *outLine;
 UBYTE inLineBit;
 
 if (!mgClipForBlit(&width, &height, &bitX, &bitY, dest, &destX, &destY))
     return;
 inLine = bitData + (bitX>>3) + bitY * bitDataRowBytes;
 inLineBit = (0x80 >> (bitX&7));
 outLine = _mgPixAdr(dest,destX,destY);
 while (--height >= 0)
     {
     UBYTE *in = inLine;
     Color *out = outLine;
     UBYTE inBit = inLineBit;
     UBYTE inByte = *in++;
     int i = width;
     while (--i >= 0)
 	{
         if (inBit & inByte)
             mixColor(out, color);
         else
             mixColor(out, backgroundColor);
         out++;
 	if ((inBit >>= 1) == 0)
 	    {
 	    inByte = *in++;
 	    inBit = 0x80;
 	    }
 	}
     inLine += bitDataRowBytes;
     outLine += _mgBpr(dest);
     }
 }
 
 
 void mgText(struct memGfx *mg, int x, int y, Color color, 
 	MgFont *font, char *text)
 /* Draw a line of text with upper left corner x,y. */
 {
 switch (mg->fontMethod)
     {
     case FONT_METHOD_GEM:
         gfText(mg, font, text, x, y, color, mgTextBlit, MG_WHITE);
         break;
     case FONT_METHOD_FREETYPE:
         ftText(mg, x, y, color, font, text);
         break;
     }
 }
 
 void mgTextInBox(struct memGfx *mg, int x, int y, int width, int height, 
 	Color color, MgFont *font, char *text)
 /* Draw a line of text centered in box defined by x/y/width/height */
 {
 #ifndef USE_FREETYPE
 // should draw an X
 #else
 if (face == NULL)  // have we turned on freetype
     ;// should draw an X
 else
     ftTextInBox(mg, x, y, width, height, color, font, text);
 #endif
 }
 
 void mgTextCentered(struct memGfx *mg, int x, int y, int width, int height, 
 	Color color, MgFont *font, char *text)
 /* Draw a line of text centered in box defined by x/y/width/height */
 {
 int fWidth, fHeight;
 int xoff, yoff;
 fWidth = mgFontStringWidth(font, text);
 fHeight = mgFontPixelHeight(font);
 xoff = x + (width - fWidth)/2;
 yoff = y + (height - fHeight)/2;
 if (font == mgSmallFont())
     {
     xoff += 1;
     yoff += 1;
     }
 mgText(mg, xoff, yoff, color, font, text);
 }
 
 void mgTextRight(struct memGfx *mg, int x, int y, int width, int height, 
 	Color color, MgFont *font, char *text)
 /* Draw a line of text right justified in box defined by x/y/width/height */
 {
 int fWidth, fHeight;
 int xoff, yoff;
 fWidth = mgFontStringWidth(font, text);
 fHeight = mgFontPixelHeight(font);
 xoff = x + width - fWidth - 1;
 yoff = y + (height - fHeight)/2;
 if (font == mgSmallFont())
     {
     xoff += 1;
     yoff += 1;
     }
 mgText(mg, xoff, yoff, color, font, text);
 }
 
 int mgFontPixelHeight(MgFont *font)
 /* How high in pixels is font? */
 {
 return font_cel_height(font);
 }
 
 int mgGetFontPixelHeight(struct memGfx *mg, MgFont *font)
 /* How high in pixels is font? */
 {
 return mgFontPixelHeight(font);
 }
 
 int mgFontLineHeight(MgFont *font)
 /* How many pixels to next line ideally? */
 {
 return font_line_height(font);
 }
 
 int mgFontWidth(MgFont *font, char *chars, int charCount)
 /* How wide are a couple of letters? */
 {
 #ifndef USE_FREETYPE
 return fnstring_width(font, (unsigned char *)chars, charCount);
 #else
 if (face == NULL)  // have we turned on freetype
     return fnstring_width(font, (unsigned char *)chars, charCount);
 return ftWidth(font, (unsigned char *)chars, charCount);
 #endif
 }
 
 int mgFontStringWidth(MgFont *font, char *string)
 /* How wide is a string? */
 {
 return mgFontWidth(font, string, strlen(string));
 }
 
 int mgGetFontStringWidth(struct memGfx *mg, MgFont *font, char *string)
 /* How wide is a string? */
 {
 return mgFontStringWidth(font, string);
 }
 
+void mgLoadFontEngine(unsigned int method, char *fontFile)
+/* Load a text engine without attaching it to an image.  Text measurement
+ * (mgFontStringWidth) answers from whichever engine is loaded, so a caller that
+ * measures text before it has an image to draw on needs to load the engine
+ * first, or it will measure with one engine and draw with another. */
+{
+if (method == FONT_METHOD_FREETYPE)
+    ftInitialize(fontFile);
+}
+
 void mgSetFontMethod(struct memGfx *mg, unsigned int method, char *fontName, char *fontFile)
 /* Which font drawing method shoud we use. */
 {
 mg->fontMethod = method;
 
-if (method == FONT_METHOD_FREETYPE)
-    ftInitialize(fontFile);
+mgLoadFontEngine(method, fontFile);
 }
 
 int mgFontCharWidth(MgFont *font, char c)
 /* How wide is a character? */
 {
 return mgFontWidth(font, &c, 1);
 }
 
 char *mgFontSizeBackwardsCompatible(char *size)
 /* Given "size" argument that may be in old tiny/small/medium/big/huge format,
  * return it in new numerical string format. Do NOT free the return string*/
 {
 if (isdigit(size[0]))
     return size;
 else if (sameWord(size, "tiny"))
     return "6";
 else if (sameWord(size, "small"))
     return "8";
 else if (sameWord(size, "medium"))
     return "14";
 else if (sameWord(size, "large"))
     return "18";
 else if (sameWord(size, "huge"))
     return "34";
 else
     {
     errAbort("unknown font size %s", size);
     return NULL;
     }
 }
 
 MgFont *mgFontForSizeAndStyle(char *textSize, char *fontType)
 /* Get a font of given size and style.  Abort with error message if not found.
  * The textSize should be 6,8,10,12,14,18,24 or 34.  For backwards compatibility
  * textSizes of "tiny" "small", "medium", "large" and "huge" are also ok.
  * The fontType should be "medium", "bold", or "fixed" */
 {
 textSize = mgFontSizeBackwardsCompatible(textSize);
 MgFont *font = NULL;
 if (sameString(fontType,"bold"))
     {
     if (sameString(textSize, "6"))
 	 font = mgTinyBoldFont();
     else if (sameString(textSize, "8"))
 	 font = mgHelveticaBold8Font();
     else if (sameString(textSize, "10"))
 	 font = mgHelveticaBold10Font();
     else if (sameString(textSize, "12"))
 	 font = mgHelveticaBold12Font();
     else if (sameString(textSize, "14"))
 	 font = mgHelveticaBold14Font();
     else if (sameString(textSize, "18"))
 	 font = mgHelveticaBold18Font();
     else if (sameString(textSize, "24"))
 	 font = mgHelveticaBold24Font();
     else if (sameString(textSize, "34"))
 	 font = mgHelveticaBold34Font();
     else
 	 errAbort("unknown textSize %s", textSize);
     }
 else if (sameString(fontType,"fixed"))
     {
     if (sameString(textSize, "6"))
 	 font = mgTinyFixedFont();
     else if (sameString(textSize, "8"))
 	 font = mgCourier8Font();
     else if (sameString(textSize, "10"))
 	 font = mgCourier10Font();
     else if (sameString(textSize, "12"))
 	 font = mgCourier12Font();
     else if (sameString(textSize, "14"))
 	 font = mgCourier14Font();
     else if (sameString(textSize, "18"))
 	 font = mgCourier18Font();
     else if (sameString(textSize, "24"))
 	 font = mgCourier24Font();
     else if (sameString(textSize, "34"))
 	 font = mgCourier34Font();
     else
 	 errAbort("unknown textSize %s", textSize);
     }
 else
     {
     if (sameString(textSize, "6"))
 	 font = mgTinyFont();
     else if (sameString(textSize, "8"))
 	 font = mgSmallFont();
     else if (sameString(textSize, "10"))
 	 font = mgHelvetica10Font();
     else if (sameString(textSize, "12"))
 	 font = mgHelvetica12Font();
     else if (sameString(textSize, "14"))
 	 font = mgHelvetica14Font();
     else if (sameString(textSize, "18"))
 	 font = mgHelvetica18Font();
     else if (sameString(textSize, "24"))
 	 font = mgHelvetica24Font();
     else if (sameString(textSize, "34"))
 	 font = mgHelvetica34Font();
     else
 	 errAbort("unknown textSize %s", textSize);
     }
 return font;
 }
 
 MgFont *mgFontForSize(char *textSize)
 /* Get a font of given size and style.  Abort with error message if not found.
  * The textSize should be 6,8,10,12,14,18,24 or 34.  For backwards compatibility
  * textSizes of "tiny" "small", "medium", "large" and "huge" are also ok. */
 {
 return mgFontForSizeAndStyle(textSize, "medium");
 }
 
 MgFont *mgFontForCellHeight(int cellHeight)
 /* Return a font whose reported cell height is cellHeight, cloned from the small
  * font.  Built-in fonts only come in fixed sizes, but the FreeType engine
  * derives its render size from the font's cell height (see getFontCorrection in
  * freeType.c), so this yields an in-between size that no fixed font offers (e.g.
  * cellHeight 10 renders ~9px).  VALID ONLY UNDER FREETYPE: the GEM bitmap engine
  * indexes fixed bitmap rows by this height, so the glyphs would be misread.
  * Callers must fall back to a real fixed-size font when FreeType is off.  Not
  * thread-safe; it hands back one static header re-stamped on each call. */
 {
 static struct font_hdr clone;
 clone = *mgSmallFont();        // copy the header; FreeType ignores the glyph data
 clone.frm_hgt = cellHeight;    // this is what mgFontPixelHeight() returns
 return &clone;
 }
 
 
 void mgSlowDot(struct memGfx *mg, int x, int y, int colorIx)
 /* Draw a dot when a macro won't do. */
 {
 mixDot(mg, x, y, COLOR_32_ALPHA(colorIx)/255.0, colorIx);
 }
 
 int mgSlowGetDot(struct memGfx *mg, int x, int y)
 /* Fetch a dot when a macro won't do. */
 {
 return mgGetDot(mg, x, y);
 }
 
 struct memGfx *mgRotate90(struct memGfx *in)
 /* Create a copy of input that is rotated 90 degrees clockwise. */
 {
 int iWidth = in->width, iHeight = in->height;
 struct memGfx *out = mgNew(iHeight, iWidth);
 Color *inCol, *outRow, *outRowStart;
 int i,j;
 
 memcpy(out->colorMap, in->colorMap, sizeof(out->colorMap));
 outRowStart = out->pixels;
 for (i=0; i<iWidth; ++i)
     {
     inCol = in->pixels + i;
     outRow = outRowStart;
     outRowStart += _mgBpr(out);
     j = iHeight;
     while (--j >= 0)
         {
 	outRow[j] = *inCol;
 	inCol += _mgBpr(in);
 	}
     }
 return out;
 }
 
 void mgSetHint(char *hint)
 /* dummy function */
 {
 return;
 }
 
 char *mgGetHint(char *hint)
 /* dummy function */
 {
 return "";
 }
 
 void vgMgMethods(struct vGfx *vg)
 /* Fill in virtual graphics methods for memory based drawing. */
 {
 vg->pixelBased = TRUE;
 vg->close = (vg_close)mgFree;
 vg->dot = (vg_dot)mgSlowDot;
 vg->getDot = (vg_getDot)mgSlowGetDot;
 vg->box = (vg_box)mgDrawBox;
 vg->line = (vg_line)mgDrawLine;
 vg->text = (vg_text)mgText;
 vg->textRight = (vg_textRight)mgTextRight;
 vg->textCentered = (vg_textCentered)mgTextCentered;
 vg->textInBox = (vg_textInBox)mgTextInBox;
 vg->findColorIx = (vg_findColorIx)mgFindColor;
 vg->findAlphaColorIx = (vg_findAlphaColorIx)mgFindAlphaColor;
 vg->colorIxToRgb = (vg_colorIxToRgb)mgColorIxToRgb;
 vg->setWriteMode = (vg_setWriteMode)mgSetWriteMode;
 vg->setClip = (vg_setClip)mgSetClip;
 vg->unclip = (vg_unclip)mgUnclip;
 vg->verticalSmear = (vg_verticalSmear)mgVerticalSmear;
 vg->fillUnder = (vg_fillUnder)mgFillUnder;
 vg->drawPoly = (vg_drawPoly)mgDrawPoly;
 vg->circle = (vg_circle)mgCircle;
 vg->ellipse = (vg_ellipse)mgEllipse;
 vg->curve = (vg_curve)mgCurve;
 vg->setHint = (vg_setHint)mgSetHint;
 vg->getHint = (vg_getHint)mgGetHint;
 vg->getFontPixelHeight = (vg_getFontPixelHeight)mgGetFontPixelHeight;
 vg->getFontStringWidth = (vg_getFontStringWidth)mgGetFontStringWidth;
 vg->setFontMethod = (vg_setFontMethod)mgSetFontMethod;
 }
 
 
 struct hslColor mgRgbToHsl(struct rgbColor rgb)
 /* Convert RGB to HSL colorspace (see http://en.wikipedia.org/wiki/HSL_and_HSV)
  * In HSL, Hue is the color in the range [0,360) with 0=red 120=green 240=blue,
  * Saturation goes from a shade of grey (0) to fully saturated color (1000), and
  * Lightness goes from black (0) through the hue (500) to white (1000). */
 {
 unsigned char rgbMax = max3(rgb.r, rgb.g, rgb.b);
 unsigned char rgbMin = min3(rgb.r, rgb.g, rgb.b);
 unsigned char delta = rgbMax - rgbMin;
 unsigned short minMax = rgbMax + rgbMin;
 int divisor;
 struct hslColor hsl = { 0.0, 0, (1000*minMax+255)/(2*255), rgb.a }; // round up
 
 // if max=min then no saturation, and this is gray
 if (rgbMax == rgbMin)
     return hsl;
 else if (hsl.l <= 500)
     divisor = minMax;
 else
     divisor = (2*255-minMax);
 hsl.s = (1000*delta + divisor/2)/divisor; // round up
 
 // Saturation so compute hue 0..360 degrees (same as for HSV)
 if (rgbMax == rgb.r) // red is 0 +/- offset in blue or green direction
     {
     hsl.h = 0 + 60.0*(rgb.g - rgb.b)/delta;
     }
 else if (rgbMax == rgb.g) // green is 120 +/- offset in blue or red direction
     {
     hsl.h = 120 + 60.0*(rgb.b - rgb.r)/delta;
     }
 else // rgb_max == rgb.b // blue is 240 +/- offset in red or green direction
     {
     hsl.h = 240 + 60.0*(rgb.r - rgb.g)/delta;
     }
 // normalize to [0,360)
 if (hsl.h < 0.0)
     hsl.h += 360.0;
 else if (hsl.h >= 360.0)
     hsl.h -= 360.0;
 return hsl;
 }
 
 
 struct hsvColor mgRgbToHsv(struct rgbColor rgb) 
 /* Convert RGB to HSV colorspace (see http://en.wikipedia.org/wiki/HSL_and_HSV)
  * In HSV, Hue is the color in the range [0,360) with 0=red 120=green 240=blue,
  * Saturation goes from white (0) to fully saturated color (1000), and
  * Value goes from black (0) through to the hue (1000). */
 {
 unsigned char rgbMax = max3(rgb.r, rgb.g, rgb.b);
 unsigned char rgbMin = min3(rgb.r, rgb.g, rgb.b);
 unsigned char delta = rgbMax - rgbMin;
 struct hsvColor hsv = {0.0, 0, 1000*rgbMax/255, rgb.a};
 
 if (hsv.v == 0) 
     return hsv;
 hsv.s = 1000*delta/rgbMax;
 // if no saturation, then this is gray
 if (hsv.s == 0) 
     return hsv;
 // Saturation so compute hue 0..360 degrees (same as for HSL)
 if (rgbMax == rgb.r) // red is 0 +/- offset in blue or green direction
     {
     hsv.h = 0 + 60.0*(rgb.g - rgb.b)/delta;
     } 
 else if (rgbMax == rgb.g) // green is 120 +/- offset in blue or red direction
     {
     hsv.h = 120 + 60.0*(rgb.b - rgb.r)/delta;
     } 
 else // rgb_max == rgb.b // blue is 240 +/- offset in red or green direction
     {
     hsv.h = 240 + 60.0*(rgb.r - rgb.g)/delta;
     }
 // normalize to [0,360)
 if (hsv.h < 0.0)
     hsv.h += 360.0;
 else if (hsv.h >= 360.0)
     hsv.h -= 360.0;
 return hsv;
 }
 
 
 struct rgbColor mgHslToRgb(struct hslColor hsl)
 /* Convert HSL to RGB colorspace http://en.wikipedia.org/wiki/HSL_and_HSV */
 {
 int p, q;
 double r, g, b;
 //unsigned short p, q, r, g, b;
 double tR, tG, tB;
 
 if( hsl.s == 0 ) // achromatic (grey)
     return (struct rgbColor) {(255*hsl.l+500)/1000, (255*hsl.l+500)/1000, (255*hsl.l+500)/1000, hsl.alpha};
 if (hsl.l <= 500)
     q = hsl.l + (hsl.l*hsl.s+500)/1000;
 else
     q = hsl.l + hsl.s - (hsl.l*hsl.s+500)/1000;
 p = 2 * hsl.l - q;
 hsl.h /= 360.0; // normalize h to 0..1
 tR = hsl.h + 1/3.0;
 tG = hsl.h;
 tB = hsl.h - 1/3.0;
 if (tR < 0.0)
     tR += 1.0;
 else if (tR > 1.0)
     tR -= 1.0;
 if (tG < 0.0)
     tG += 1.0;
 else if (tG > 1.0)
     tG -= 1.0;
 if (tB < 0.0)
     tB += 1.0;
 else if (tB > 1.0)
     tB -= 1.0;
 // Red
 if (tR < 1/6.0)
     r = p + (q-p)*6*tR;
 else if (tR < 0.5)
     r = q;
 else if (tR < 2/3.0)
     r = p + (q-p)*6*(2/3.0 - tR);
 else
     r = p;
 // Green
 if (tG < 1/6.0)
     g = p + (q-p)*6*tG;
 else if (tG < 0.5)
     g = q;
 else if (tG < 2/3.0)
     g = p + (q-p)*6*(2/3.0 - tG);
 else
     g = p;
 // Blue
 if (tB < 1/6.0)
     b = p + (q-p)*6*tB;
 else if (tB < 0.5)
     b = q;
 else if (tB < 2/3.0)
     b = p + (q-p)*6*(2/3.0 - tB);
 else
     b = p;
 return (struct rgbColor) {(255*r+500)/1000, (255*g+500)/1000, (255*b+500)/1000, hsl.alpha}; // round up
 }
 
 
 struct rgbColor mgHsvToRgb(struct hsvColor hsv)
 /* Convert HSV to RGB colorspace http://en.wikipedia.org/wiki/HSL_and_HSV */
 {
 int i;
 double f;
 unsigned short low, q, t, r, g, b;
 
 if( hsv.s == 0 ) // achromatic (grey)
     return (struct rgbColor) {(255*hsv.v+500)/1000, (255*hsv.v+500)/1000, (255*hsv.v+500)/1000, hsv.alpha};
 hsv.h /= 60.0; 
 i = (int) hsv.h;                     // floor the floating point value, sector 0 to 5
 f = hsv.h - i;                       // fractional part (distance) from hue 
 // hsv.v is highest r,g, or b value
 // low value is related to saturation
 low = hsv.v - (hsv.v * hsv.s / 1000); // lowest r,g, or b value
 t = low + (hsv.v - low) * f;         // scaled value from low..high
 q = low + (hsv.v - low) * (1.0-f);   // scaled value from low..high
 
 switch( i )
     {
     case 0:
 	r = hsv.v;
         g = t;
         b = low;
         break;
     case 1:
         r = q;
         g = hsv.v;
         b = low;
         break;
     case 2:
         r = low;
         g = hsv.v;
         b = t;
         break;
     case 3:
         r = low;
         g = q;
         b = hsv.v;
         break;
     case 4:
         r = t;
         g = low;
         b = hsv.v;
         break;
     default:                // case 5:
         r = hsv.v;
         g = low;
         b = q;
         break;
     }
 return (struct rgbColor) {(255*r+500)/1000, (255*g+500)/1000, (255*b+500)/1000, hsv.alpha};
 }
 
 
 struct rgbColor mgRgbTransformHsl(struct rgbColor in, double h, double s, double l)
 /* Transform rgb 'in' value using
  *   hue shift 'h' (0..360 degrees), 
  *   saturation scale 's', and 
  *   lightness scale 'l'
  * Returns the transformed rgb value 
  * Use H=0, S=L=1 for identity transformation
  */
 {
 struct hslColor hsl = mgRgbToHsl(in);
 hsl.h += h;
 while (hsl.h < 0.0)
     hsl.h += 360.0;
 while (hsl.h > 360.0)
     hsl.h -= 360.0;
 hsl.s = min(max(hsl.s * s, 0), 1000);
 hsl.l = min(max(hsl.l * l, 0), 1000);
 return mgHslToRgb(hsl);
 }
 
 
 struct rgbColor mgRgbTransformHsv(struct rgbColor in, double h, double s, double v)
 /* Transform rgb 'in' value using
  *   hue shift 'h' (0..360 degrees), 
  *   saturation scale 's', and 
  *   value scale 'v'
  * Returns the transformed rgb value 
  * Use H=0, S=V=1 for identity transformation
  */
 {
 struct hsvColor hsv = mgRgbToHsv(in);
 hsv.h += h;
 while (hsv.h < 0.0)
     hsv.h += 360.0;
 while (hsv.h > 360.0)
     hsv.h -= 360.0;
 hsv.s = min(max(hsv.s * s, 0), 1000);
 hsv.v = min(max(hsv.v * v, 0), 1000);
 return mgHsvToRgb(hsv);
 }
 
 void mgEllipse(struct memGfx *mg, int x0, int y0, int x1, int y1, Color color,
                         int mode, boolean isDashed)
 /* Draw an ellipse (or limit to top or bottom) specified by rectangle, using Bresenham algorithm.
  * Optionally, alternate dots.
  * Point 0 is left, point 1 is top of rectangle
  * Adapted trivially from code posted at http://members.chello.at/~easyfilter/bresenham.html
  * Author: Zingl Alois, 8/22/2016
  */
 {
    int a = abs(x1-x0), b = abs(y1-y0), b1 = b&1; /* values of diameter */
    long dx = 4*(1-a)*b*b, dy = 4*(b1+1)*a*a; /* error increment */
    long err = dx+dy+b1*a*a, e2; /* error of 1.step */
 
    if (x0 > x1) { x0 = x1; x1 += a; } /* if called with swapped points */
    if (y0 > y1) y0 = y1; /* .. exchange them */
    y0 += (b+1)/2; y1 = y0-b1;   /* starting pixel */
    a *= 8*a; b1 = 8*b*b;
 
    int dots = 0;
    do {
        if (!isDashed || (++dots % 3))
            {
            if (mode == ELLIPSE_BOTTOM || mode == ELLIPSE_FULL) 
                {
                mixDot(mg, x1, y0, COLOR_32_ALPHA(color)/255.0, color); /*   I. Quadrant */
                mixDot(mg, x0, y0, COLOR_32_ALPHA(color)/255.0, color); /*  II. Quadrant */
                }
            if (mode == ELLIPSE_TOP || mode == ELLIPSE_FULL) 
                {
                mixDot(mg, x0, y1, COLOR_32_ALPHA(color)/255.0, color); /* III. Quadrant */
                mixDot(mg, x1, y1, COLOR_32_ALPHA(color)/255.0, color); /*  IV. Quadrant */
                }
            }
        e2 = 2*err;
        if (e2 <= dy) { y0++; y1--; err += dy += a; }  /* y step */
        if (e2 >= dx || 2*err > dy) { x0++; x1--; err += dx += b1; } /* x step */
    } while (x0 <= x1);
 
    while (y0-y1 < b) {  /* too early stop of flat ellipses a=1 */
        if (!isDashed && (++dots % 3))
            {
            mixDot(mg, x0-1, y0, COLOR_32_ALPHA(color)/255.0, color); /* -> finish tip of ellipse */
            mixDot(mg, x1+1, y0++, COLOR_32_ALPHA(color)/255.0, color);
            mixDot(mg, x0-1, y1, COLOR_32_ALPHA(color)/255.0, color);
            mixDot(mg, x1+1, y1--, COLOR_32_ALPHA(color)/255.0, color);
            }
    }
 }
 
 static int mgCurveSegAA(struct memGfx *mg, int x0, int y0, int x1, int y1, int x2, int y2, 
                         Color color, boolean isDashed)
 /* Draw a segment of an anti-aliased curve within 3 points (quadratic Bezier)
  * Return max y value. Optionally alternate dots.
  * Adapted trivially from code posted on github and at http://members.chello.at/~easyfilter/bresenham.html */
  /* Thanks to author  * @author Zingl Alois
  * @date 22.08.2016 */
 {
    int yMax = 0;
    int sx = x2-x1, sy = y2-y1;
    long xx = x0-x1, yy = y0-y1, xy;             /* relative values for checks */
    double dx, dy, err, ed, cur = xx*sy-yy*sx;                    /* curvature */
 
    assert(xx*sx <= 0 && yy*sy <= 0);      /* sign of gradient must not change */
 
    if (sx*(long)sx+sy*(long)sy > xx*xx+yy*yy) {     /* begin with longer part */
       x2 = x0; x0 = sx+x1; y2 = y0; y0 = sy+y1; cur = -cur;     /* swap P0 P2 */
    }
    if (cur != 0)
    {                                                      /* no straight line */
       xx += sx; xx *= sx = x0 < x2 ? 1 : -1;              /* x step direction */
       yy += sy; yy *= sy = y0 < y2 ? 1 : -1;              /* y step direction */
       xy = 2*xx*yy; xx *= xx; yy *= yy;             /* differences 2nd degree */
       if (cur*sx*sy < 0) {                              /* negated curvature? */
          xx = -xx; yy = -yy; xy = -xy; cur = -cur;
       }
       dx = 4.0*sy*(x1-x0)*cur+xx-xy;                /* differences 1st degree */
       dy = 4.0*sx*(y0-y1)*cur+yy-xy;
       xx += xx; yy += yy; err = dx+dy+xy;                   /* error 1st step */
       int dots = 0;
       do {
          cur = fmin(dx+xy,-xy-dy);
          ed = fmax(dx+xy,-xy-dy);               /* approximate error distance */
          ed += 2*ed*cur*cur/(4*ed*ed+cur*cur);
          if (!isDashed || (++dots % 3))
             {
             mixDot(mg, x0,y0, 1-fabs(err-dx-dy-xy)/ed, color);          /* plot curve */
             if (y0 > yMax)
                 yMax = y0;
             }
          if (x0 == x2 || y0 == y2) break;     /* last pixel -> curve finished */
          x1 = x0; cur = dx-err; y1 = 2*err+dy < 0;
          if (2*err+dx > 0) {                                        /* x step */
             if (err-dy < ed) 
                 {
                 mixDot(mg, x0,y0+sy, 1-fabs(err-dy)/ed, color);
                 if (y0 > yMax)
                     yMax = y0;
                 }
             x0 += sx; dx -= xy; err += dy += yy;
          }
          if (y1) {                                                  /* y step */
             if (cur < ed) 
                 {
                 mixDot(mg, x1+sx,y0, 1-fabs(cur)/ed, color);
                 if (y0 > yMax)
                     yMax = y0;
                 }
             y0 += sy; dy -= xy; err += dx += xx;
          }
       } while (dy < dx);                  /* gradient negates -> close curves */
    }
    mgDrawLine(mg, x0,y0, x2,y2, color);                  /* plot remaining needle to end */
    if (y0 > yMax)
        yMax = y0;
    return yMax;
 }
 
 int mgCurve(struct memGfx *mg, int x0, int y0, int x1, int y1, int x2, int y2, Color color,
                         boolean isDashed)
 /* Draw a segment of an anti-aliased curve within 3 points (quadratic Bezier)
  * Return max y value. Optionally draw curve as dashed line.
  * Adapted trivially from code posted at http://members.chello.at/~easyfilter/bresenham.html
  * Author: Zingl Alois, 8/22/2016
  */
 /* TODO: allow specifying a third point on the line
  *  P(t) = (1-t)^2 * p0 + 2 * (1-t) * t * p1 + t^2 * p2
  */
 {
    int x = x0-x1, y = y0-y1;
    double t = x0-2*x1+x2, r;
    int yMax = 0, yMaxRet = 0;
    if ((long)x*(x2-x1) > 0) {                        /* horizontal cut at P4? */
       if ((long)y*(y2-y1) > 0)                     /* vertical cut at P6 too? */
          if (fabs((y0-2*y1+y2)/t*x) > abs(y)) {               /* which first? */
             x0 = x2; x2 = x+x1; y0 = y2; y2 = y+y1;            /* swap points */
          }                            /* now horizontal cut at P4 comes first */
       t = (x0-x1)/t;
       r = (1-t)*((1-t)*y0+2.0*t*y1)+t*t*y2;                       /* By(t=P4) */
       t = (x0*x2-x1*x1)*t/(x0-x1);                       /* gradient dP4/dx=0 */
       x = floor(t+0.5); y = floor(r+0.5);
       r = (y1-y0)*(t-x0)/(x1-x0)+y0;                  /* intersect P3 | P0 P1 */
       yMax = mgCurveSegAA(mg,x0,y0, x,floor(r+0.5), x,y, color, isDashed);
       r = (y1-y2)*(t-x2)/(x1-x2)+y2;                  /* intersect P4 | P1 P2 */
       x0 = x1 = x; y0 = y; y1 = floor(r+0.5);             /* P0 = P4, P1 = P8 */
    }
    if ((long)(y0-y1)*(y2-y1) > 0) {                    /* vertical cut at P6? */
       t = y0-2*y1+y2; t = (y0-y1)/t;
       r = (1-t)*((1-t)*x0+2.0*t*x1)+t*t*x2;                       /* Bx(t=P6) */
       t = (y0*y2-y1*y1)*t/(y0-y1);                       /* gradient dP6/dy=0 */
       x = floor(r+0.5); y = floor(t+0.5);
       r = (x1-x0)*(t-y0)/(y1-y0)+x0;                  /* intersect P6 | P0 P1 */
       yMaxRet = mgCurveSegAA(mg,x0,y0, floor(r+0.5),y, x,y, color, isDashed);
       if (yMaxRet > yMax)
         yMax = yMaxRet;
       r = (x1-x2)*(t-y2)/(y1-y2)+x2;                  /* intersect P7 | P1 P2 */
       x0 = x; x1 = floor(r+0.5); y0 = y1 = y;             /* P0 = P6, P1 = P7 */
    }
    yMaxRet = mgCurveSegAA(mg,x0,y0, x1,y1, x2,y2, color, isDashed); /* remaining part */
    if (yMaxRet > yMax)
      yMax = yMaxRet;
    return yMax;
 }