Quartz font metrics

Dear Lazyweb,

I'm lost in a twisty maze of font handling APIs, all alike.

I've got a bunch of Quartz code, and I need to do fonty things. Right now I'm doing this kind of stuff:

<LJ-CUT text=" --More--(16%) ">

    CGContextSelectFont (cgc, "Helvetica Bold", 12, kCGEncodingMacRoman);
    CGContextSetTextMatrix (cgc, CGAffineTransformIdentity);
    CGContextSetTextDrawingMode (cgc, kCGTextFill);
    CGContextSetShouldAntialias (cgc, YES);
    CGContextShowTextAtPoint (cgc, x, y, string, length);

That's all well and good, but now I need per-char metrics: I need to know the ascent, descent, lbearing, rbearing, and bounding box of each character I'm drawing. As far as I can tell, the only thing Quartz will give you is rbearing (aka horizontal cursor motion):

    CGContextSetTextDrawingMode (cgc, kCGTextInvisible);
    CGContextShowTextAtPoint (cgc, 0, 0, &c, 1);
    CGPoint p = CGContextGetTextPosition (cgc);

So should I be using NSFont? ATSU? Something else? I can't tell... I tried to use NSFont, but I can't see how to attach an NSFont to an arbitrary CGContextRef (which might be either a window or bitmap context). I thought it might be:

    NSGraphicsContext *nsctx =
      [NSGraphicsContext graphicsContextWithGraphicsPort:cgc flipped:NO];
    [font setInContext:nsctx];
but that doesn't seem to set the font used by CGContextShowTextAtPoint(). Or is there some other text-drawing routine I should be using with NSFont?

If the answer is ATSU, please point me at some sample code that involves ATSU and Quartz but not Carbon/QuickDraw, because I haven't found any.

Second question:

How does aglUseFont() relate to these other three font APIs (CG, ATSU, and NSFont)? How do I turn "Helvetica Bold" or "Courier" into the kind of "fontID" that it wants?

Update: I did solve this eventually; if you want to see the working code, see the query_font() and draw_string() functions in xscreensaver/OSX/jwxyz.m.

Tags: , , ,

13 Responses:

  1. duskwuff says:

    For aglUseFont, take a look at Apple's "AGL String" sample. The fontID is a short returned by the Carbon routine GetFNum(pascal char * fontName, short *return).

  2. sheilagh says:

    ball sucker/furniture? http://www.animicausa.com/

  3. ejones2 says:

    I've used ATSU to get this information, and do this type of drawing using Quartz. I *believe* that it is the lowest level text API on Mac OS X. It is also quite complicated. The ">Rendering Unicode with ATSUI documentation shows how to draw into a Quartz context. Just ignore the parts that talk about using QuickDraw to get a CGContext, since you will already have one. The TypeServicesForUnicode sample code draws using ATSUI and Quartz. Look at the "draw" event handler in that example. If you want to get at the actual glyph data, you want to look at the ATSUIDirectAccessDemo.

    • jwz says:

      Well, this graph suggests that Quartz is the lowest level.

      I can't believe how complicated this shit is! Just to draw a string, apparently I'm expected to go through this: and this doesn't even work yet, I'm getting error -8802 (which is ungoogleable) from ATSUDrawText.

      CGContextScaleCTM (d->cgc, 1, -1);
      CGContextTranslateCTM (d->cgc, 0, -wr.size.height);

      int status;

      UniChar *unistr;
      ByteCount unilen;

      TextToUnicodeInfo info = 0;
      status = CreateTextToUnicodeInfoByEncoding (
      CreateTextEncoding (kTextEncodingMacRoman,
      if (status) abort();
      if (!info) abort();

      unistr = (UniChar *) malloc (len * sizeof(*unistr));
      ByteCount converted = -1;
      status = ConvertFromTextToUnicode (info, len, str,
      0, 0, 0, 0, 0, // flags, offsets
      len * sizeof(*unistr),
      if (status) abort();
      if (unilen < len) abort();
      DisposeTextToUnicodeInfo (&info);

      // #### cache and reuse this in the Font?
      ATSUTextLayout atsui_layout = 0;
      status = ATSUCreateTextLayoutWithTextPtr (unistr, 0,
      unilen, unilen,
      0, 0, /* nruns, runs */
      0, /* styles */
      if (status) abort();

      // status = atsuSetLayoutOptions (atsui_layout, kATSUNoJustification);
      // if (status) abort();

      status = ATSUSetTransientFontMatching (atsui_layout, TRUE);
      if (status) abort();

      ATSUAttributeTag tags[] = { kATSUCGContextTag };
      ByteCount sizes[] = { sizeof(CGContextRef) };
      ATSUAttributeValuePtr values[] = { &d->cgc };
      status = ATSUSetLayoutControls (atsui_layout, 1, tags, sizes, values);
      if (status) abort();

      ATSUTextMeasurement xx = wr.origin.x + x;
      ATSUTextMeasurement yy = wr.origin.y + wr.size.height - y;

      status = ATSUDrawText (atsui_layout, 0, unilen, xx, yy);
      if (status) abort();

      ATSUDisposeTextLayout (atsui_layout);
      free (unistr);
      • chanson says:

        I don't know enough about the low-level font handling to be of much help, but I can explain a little about why it's so complicated: The glyphs that are actually drawn for a string don't necessarily have a 1:1 correspondence with the characters. Not only do combining marks need to be considered, but languages that have rules for the behavior of multi-character sequences (like Arabic and Sanskrit) need to be supported as well. This same support also enables features like ligatures (fi, fl, ffi, ffl, etc.) and context-sensitive behavior (beginning of line, end of line, swashes, etc.).

        It may seem complicated for drawing ASCII text, but the same system works for rendering strings in virtually any horizontally-written language - and even in multiple languages. (I don't think vertical text layout is supported right now, but I'm not certain about that.)

      • ejones2 says:

        Well, my understanding of it is that Quartz just draws paths (bezier splines and lines, etc), and ATSUI combines the Unicode string with the font and style information to output those splines. But again, I could be wrong.

        I think your problem may be that ATSUDrawText wants Unicode character counts, and not byte counts. Just try the following which should render the entire string. Also, the x and y positions are ATSUFontMeasurements, which are Fixed point types, so you probably need to do conversions. Try the following:

        ATSUTextMeasurement xx = FloatToFixed(wr.origin.x + x);
        ATSUTextMeasurement yy = FloatToFixed(wr.origin.y + wr.size.height - y);

        status = ATSUDrawText (atsui_layout, kATSUFromTextBeginning, kATSUToTextEnd, xx, yy);

        If that doesn't solve it, I'll copy my text rendering code into an example program that renders to an image using Quartz.

        • jwz says:

          I got it working, but I just scrapped all that ATSU code. Basically what I'm doing is mixing and matching font systems and hoping they match: I draw my text with CGContextSelectFont / CGContextShowTextAtPoint, but to get the metrics, I do [NSFont fontWithName:size], and then query that with getAdvancements, etc. So, assuming that passing the same postscript-like font-name-string in to both Quartz and Cocoa actually yields the same font, I guess I'm ok...

          The metrics I'm getting back seem slightly off, though; it seems like lbearing and rbearing are too small. So I just scaled up the claimed bounding box of each character a bit, and now it seems to work ok so far.

          I'm still totally baffled as to how to turn an NSFont into something that aglUseFont will take, though, so the metrics are wrong on the text that I'm printing on GL windows.

  4. structurefall says:

    i don't know of these "APIs" of which you speak, but generally when i'm stuck in a maze of twisty little anything all alike, i figure the best course of action is to drop a useless item in each room, rob a skeleton, and then climb up in to the forest throuh the grating in the ceiling. try that.

  5. legolas says:

    I may not be understanding your exact needs, but I see this help for ATSUGlyphGetIdealMetrics and it says: "oIdealMetrics A pointer to memory you have allocated for an array of ATSGlyphIdealMetrics structures. On return, each structure contains advance and side-bearing values for a glyph. "
    ATSGlyphIdealMetrics contains:
    advance The amount by which the pen is advanced after drawing the glyph.
    sideBearing The offset from the glyph origin to the beginning of the glyph image.
    otherSideBearing The offset from the end of the glyph image to the end of the glyph advance.

    There's also ATSUGlyphGetScreenMetrics.

    But I see you have already scrapped your ATSU code, so I guess this is too late even if it would be what you need. Oh well, for a rewrite maybe ;-)

  6. sambushell says:

    FWIW, there's a new Programming with Quartz book out -- some more info here. I browsed a friend's copy a few weeks ago and concluded that it didn't suck.