NSFont is full of lies

Dear Lazyweb, I need to find the bounding box of a character, using Cocoa or Quartz or whatever.

Yes, I have seen Getting Font Metrics. However, on my planet, "bounding rectangle" means "the rectangle that completely encloses all ink that will be drawn by the character." It does not mean "a rectangle that is 'advancement' wide".

Here is some code. The image to the right is what it draws. Note that none of these rectangles could even remotely be considered a bounding box. How do I find the bounding box?

- (void)drawRect:(NSRect)rect
{
  NSString *str = @"j";
  NSFont *font = [NSFont fontWithName:@"Helvetica-BoldOblique" size:180];

  NSDictionary *attr = [NSDictionary dictionaryWithObject:font
                                    forKey:NSFontAttributeName];
  NSSize bbox = [str sizeWithAttributes:attr];
  NSRect frame = [self bounds];
  NSPoint pos;
  pos.x = (frame.origin.x + ((frame.size.width  - bbox.width)  / 2));
  pos.y = (frame.origin.y + ((frame.size.height - bbox.height) / 2));

  /* I can't believe we have to go through this bullshit just to
    convert a 'char' to an NSGlyph!!
  */
  NSGlyph g;
  {
    NSTextStorage *ts = [[NSTextStorage alloc] initWithString:str];
    [ts setFont:font];
    NSLayoutManager *lm = [[NSLayoutManager alloc] init];
    NSTextContainer *tc = [[NSTextContainer alloc] init];
    [lm addTextContainer:tc];
    [tc release]; // lm retains tc
    [ts addLayoutManager:lm];
    [lm release]; // ts retains lm
    g = [lm glyphAtIndex:0];
    [ts release];
  }


  /* Clear window and draw the character.
  */
  [[NSColor whiteColor] set];
  NSRectFill([self bounds]);
  [str drawAtPoint:pos withAttributes:attr];


  /* Draw blue square marking origin.
  */
  frame.origin = pos;
  frame.origin.y -= [font descender];
  frame.size.width = frame.size.height = 10;
  [[NSColor blueColor] set];
  NSRectFill (frame);


  /* Draw blue baseline according to [NSFont descender].
  */
  frame.origin.x = 0;
  frame.origin.y = pos.y - [font descender];
  frame.size.width = [self bounds].size.width;
  frame.size.height = 1;
  [[NSColor blueColor] set];
  NSFrameRect (frame);


  /* Draw blue line according to [NSFont advancementForGlyph].
  */
  frame.origin.x = pos.x + [font advancementForGlyph:g].width;
  frame.origin.y = 0;
  frame.size.width = 1;
  frame.size.height = [self bounds].size.height;
  [[NSColor blueColor] set];
  NSFrameRect (frame);


  /* Draw red bounding box according to [NSString sizeWithAttributes].
  */
  frame.origin = pos;
  frame.size = bbox;
  [[NSColor redColor] set];
  NSFrameRect (frame);


  /* Draw green bounding box according to [NSFont boundingRectForGlyph].
  */
  NSRect bbox2 = [font boundingRectForGlyph: g];
  frame.origin.x = pos.x + bbox2.origin.x;
  frame.origin.y = pos.y - bbox2.origin.y;
  frame.size = bbox2.size;
  [[NSColor greenColor] set];
  NSFrameRect (frame);
}

(Previously.)


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

Tags: , , ,

17 Responses:

  1. strspn says:

    Make a NSBezierPath, appendBezierPathWithGlyph, then use (NSRect)bounds?

    • jwz says:

      OMG.

      Well, it works... but is it actually sane?

      • strspn says:

        I think boundingRectForGlyph is just for drawing selection regions that don't make it look like thin neighboring characters are selected in italics/oblique fonts, and not much else.

      • legolas says:

        I think it is. From what I know about font formats, a specific characters bounding box is not a part of it. A general bounding box that's as large as the largest char is, and all the advance widths and sidebearings you already have... Maybe no one needs this sort of stuff in most uses of fonts, unless they have to draw the char anyway, which will answer the question as the comment above suggested. In fact, why do you need it?

        • jwz says:

          Lots of screen savers need this info when laying out text; e.g., to know how big a box (or pixmap) around the text must be so that it doesn't clip off the serifs, or the right half of an italic character at the end of the line.

  2. violentbloom says:

    since I have "The Thames and Hudson Manual of Typography" I thought I'd look into this nonsense you speak of...
    so this book was written in 1980. It's a fascinating look at circa 1980 technology. Apparently then the Univers font was popular.

    • shigawire says:

      I was about to suggest skewing the red bounding box by 12.5 degrees, which is the 'normal' italic angle, but it's also just pure evil.

      I worked for a few years in DTP/typesetting/pre-press before I figured I could get paid for doing computer stuff, but it never ceases to amaze me how far most of the font renderers i've encountered go to make life hard for themselves.

  3. waider says:

    Out of curiosity: does it make you at all twitchy to be once more working with a seemingly less-than-sane codebase with NS- prefixes all over the place?

    • jwz says:

      No. Yes. Maybe.

      I think it must be a little like dating a girl who has the same first name as your sister.

  4. federico says:

    You can steal some code from pango_cairo_atsui_font_get_glyph_extents() in pango/pango/pangocairo-atsuifont.c, and from the lower-level _cairo_atsui_font_init_glyph_metrics() in cairo/src/cairo-atsui-font.c which the former eventually uses.

    What you want is Pango's "ink_rect" from the first function.

    The key function seems to be ATSUGlyphGetIdealMetrics(). The ink rectangle is computed in terms of the advance and the side bearings.

    • federico says:

      Also, the "Getting Font Metrics" page to which you linked mentions a boundingRectForGlyph method in NSFont. The figure on that page seems to consider "bounding box" as the ink rectangle. Maybe that method is just a wrapper for the ATSUI stuff.

  5. martling says:

    Non sequitur: have you seen this yet? It struck me as the sort of thing you'd find cool.

  6. dfrezell says:

    I saw you comment in the code, replace :

    /* I can't believe we have to go through this bullshit just to
    convert a 'char' to an NSGlyph!!
    */
    NSGlyph g;
    {
    NSTextStorage *ts = [[NSTextStorage alloc] initWithString:str];
    [ts setFont:font];
    NSLayoutManager *lm = [[NSLayoutManager alloc] init];
    NSTextContainer *tc = [[NSTextContainer alloc] init];
    [lm addTextContainer:tc];
    [tc release]; // lm retains tc
    [ts addLayoutManager:lm];
    [lm release]; // ts retains lm
    g = [lm glyphAtIndex:0];
    [ts release];
    }

    with :

    NSGlyph g = [font glyphWithName:str];

    • jwz says:

      I thought the string that glyphWithName wanted was the full Unicode nonsense like "LATIN CAPITAL LETTER A WITH ACUTE" -- will that necessarily work with an arbitrary single-character NSString?

    • jwz says:

      Yeah, that doesn't work.

      • dfrezell says:

        I'm guessing this is font specific information, some will use the full Unicode as you stated, others will use the actual character. I'm not sure what would happen when passing in a single char string with some alternate language font, like Japanese...probably fail miserably. Which font and character did it fail on?