NSAttributedString + NSLayoutManager versus Core Text
I’m wrestling with annoying inconsistencies in the various options for text rendering.
Here’s what I want to do - I want to draw overbars above runs of characters in a text string. This is so that I can label electronic components according to the usual convention that an active low I/O port has an overbar, either above certain characters, or above a whole string.
An obvious way to do this is to use NSAttributedString with custom attributes for the overbar attribute, and then override the glyph drawing method in a subclass of NSLayoutManager to get the overbar attribute, compute where the line goes and draw it. That works really well.
However, the way NSAttributedString actually draws is strange - text is always offset to the right of the origin of the rectangle that you want to draw it into. It’s only a few pixels, but it’s more than enough to misplace the text especially with small font sizes. The offset seems to vary with font also. Because the text container has the size of the rectangle, this offset means that text is typically clipped or wrapped by the right edge when it should not be, because the whole string was drawn too far right. I can fudge the origin, but then I change the font and those numbers are wrong.
I also built a solution using Core Text. This does not exhibit the same behaviour - lines draw exactly where you tell them, not offset to the right. However, I can’t see a way to use Core Text to draw my overbars - there’s no override point for drawing additional lines after rendering the glyphs. (Unless I’m just not seeing it).
Another oddity with NSAttributedString is that when there is a paragraph style with left/centre/right alignment variations, this has no effect on the text positioning. Since for the actual glyph rendering I’m simply calling super’s implementation, it should surely work properly.
My question is why does NSAttributedString render like this, and what should I be doing about it? Or alternatively, is there a Core Text way to do this?
Here’s the code for drawing the string, in a category on NSAttributedString:
- (void) drawWithOverbarInRect:(CGRect) rect inContext:(CGContextRef) context
// to draw the string, a subclass of NSLayoutManager is required
NSTextStorage* tempStorage = [[NSTextStorage alloc] initWithAttributedString:self];
GCOverbarLayoutManager* layManager = [GCOverbarLayoutManager sharedOverbarLayoutManager];
NSTextContainer* container = [[NSTextContainer alloc] initWithSize:rect.size];
container.lineBreakMode = NSLineBreakByClipping;
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithCGContext:context flipped:YES]];
NSRange glyphRange = [layManager glyphRangeForTextContainer:container];
NSPoint textOrigin = rect.origin;
// textOrigin is the top,left of the rect - but in fact this positions glyphs too far to the right and clips off characters at the right edge
if( glyphRange.length > 0 )
[layManager drawBackgroundForGlyphRange:glyphRange atPoint:textOrigin];
[layManager drawGlyphsForGlyphRange:glyphRange atPoint:textOrigin];
And here’s the code in the NSLayoutManager subclass that actually renders the overbars:
- (void) showCGGlyphs:(const CGGlyph*) glyphs
positions:(const CGPoint*) positions
// this overrides the layout manager's standard method to add the overbar if the attribute indicates it
// attributes for overbar?
CGFloat overBarWidth = [[attributes objectForKey:GCOverbarAttributeName] doubleValue];
if( overBarWidth > 0.0 )
// there's an overbar to draw here - custom colour? If not, use text colour, or black, whatever we have info about
NSColor* overbarColour = [attributes objectForKey:GCOverbarColorAttributeName];
if( overbarColour == nil )
overbarColour = [attributes objectForKey:NSForegroundColorAttributeName];
if( overbarColour == nil )
overbarColour = [NSColor blackColor];
// need to compute the horizontal length of the bar. This can be deduced from the positions array
// the last glyph rendered also needs to be considered - we need its width
NSRect firstGlyphRect = [font boundingRectForCGGlyph:glyphs];
NSRect lastGlyphRect = [font boundingRectForCGGlyph:glyphs[glyphCount - 1]];
obPoints.x = NSMinX( firstGlyphRect ) + positions.x - 0.5;
obPoints.y = obPoints.y = positions.y - font.ascender - overBarWidth * font.xHeight;
obPoints.x = NSMaxX( lastGlyphRect ) + positions[glyphCount -1].x + 0.5;
CGContextSaveGState( CGContext );
CGContextSetTextMatrix( CGContext, textMatrix );
CGContextSetLineWidth( CGContext, overBarWidth * font.xHeight );
CGContextSetStrokeColorWithColor( CGContext, overbarColour.CGColor );
CGContextSetLineCap( CGContext, kCGLineCapButt );
CGContextStrokeLineSegments( CGContext, obPoints, 2 );
CGContextRestoreGState( CGContext );
Ignore this part - on further test this is working correctly.toggle quoted messageShow quoted text
OK, as usual after writing a lengthy question, it helps focus the mind on what’s really going on, and minutes later the solution materialises.toggle quoted messageShow quoted text
NSTextContainer has a property called lineFragmentPadding. For some inexplicable reason, this value defaults to 5, rather than 0. Why 5 would be what you want I can’t think, but setting it to 0 solves my problem.
Still, if anyone’s in the mood to offer a Core Text solution to this overall problem, I’d be happy to hear it.
On Nov 22, 2019, at 17:28 , Graham Cox <graham@...> wrote:
I don’t know exactly *which* typographic problem is intended to be solved by line fragment padding, but the most likely one is that various letters may extend outside their layout boxes. The layout box starts at the glyph origin on the left, and extends the distance of the advance width on the right. Layout proceeds by placing glyphs layout boxes side by side. Note that the layout box is only loosely related to the glyph bounding box.
That works fine if the glyph’s bounding box lies within the layout box, but some glyphs extend outside their layout box. (A typical example is “j” or “J”, which will have a similar advance width to “i” or “I”, but the hook can extend quite a way to the left.)
Within a line, that still works fine, because the glyph extension simply draws over an adjacent glyph's layout box. However, at the ends of the line, there is usually clipping to an enclosing text frame, and that results in bits of glyphs sometimes being clipped away.
“lineFragmentPadding” solves the problem by leaving space at the ends of the lines for glyphs to overflow into. Setting it to 0 isn’t a terrible thing to do, but will occasionally cause unsightly clipping. Maybe a better solution would be to fetch the value and use it in your overbar placement.
Gary L. Wade
I haven’t done this with CoreText personally, but you need to traverse the runs yourself to know where the characters are rather than rely on callbacks. What code do you use for the CoreText solution?toggle quoted messageShow quoted text