Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laying out individual glyphs with Core Text

I'm currently writing an application against the iOS 6.1 SDK. I know that some things in iOS 7 may obviate the need for a solution to my question but in the interest of learning I'm going to ask anyway.

The app will consist of a table view and custom table view cells. I'd like the only subview of the cell's contentView to be a custom view with an NSAttributedString drawn using Core Text. Since each cell's string will be different, the glyph positioning needs to be dependent on the number of glyphs (i.e. longer strings will have less visible space between glyphs). The size of the font and the physical bounds must remain the same it is only the glyph positioning that will be different.

I have the following code that for whatever reason does not do what I expect.

Here is the .h for the BMPTeamNameView - custom view (subview of contentView)

@interface BMPTeamNameView : UIView

-(id)initWithFrame:(CGRect)frame text:(NSString *)text textInset:(UIEdgeInsets)insets font:(UIFont *)font;

@property (nonatomic, copy) NSAttributedString *attributedString;
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, assign) UIEdgeInsets insets;

@end

The new designated initializer will now set the frame, the text to use for the attributed string, the insets to determine the text rect with respect to contentView rect, and the font to use.

Originally in my custom drawRect: I used a CTFramesetterRef, however a CTFramesetterRef will create an immutable frame which (may have?) restricted the laying out of individual glyphs. In this implementation I use a CTTypesetterRef to create the CTLineRef. Using a CTFrame leads to different drawing behavior when you compare CTLineDraw() and CTFrameDraw() but that is for another question. My drawRect: is as follows:

- (void)drawRect:(CGRect)rect
{        
    CGContextRef context = UIGraphicsGetCurrentContext();

    // Flips the coordinates so that drawing will be right side up
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);        
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    // Path that will hold the textRect
    CGMutablePathRef path = CGPathCreateMutable();

    // rectForTextInsets: returns a rect based on the insets with respect to cell contentView
    self.textRect = [self rectForTextWithInsets:self.insets];

    // Path adding / sets color for drawing
    CGPathAddRect(path, NULL, self.textRect);

    CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);

    CGContextAddPath(context, path);

    CGContextFillPath(context);

    // convenience method to return dictionary of attributes for string
    NSDictionary *attributes = [self attributesForAttributedString];

    // convenience method returns "Hello World" with attributes
    // typesetter property is set in the custom setAttributedString:
    self.attributedString = [self attributedStringWithAttributes:attributes];

    CTTypesetterRef typesetter = self.typesetter;

    // Creates the line for the attributed string
    CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(0, 0));

    CGPoint *positions = NULL;
    CGGlyph *glyphs = NULL;
    CGPoint *positionsBuffer = NULL;
    CGGlyph *glyphsBuffer = NULL;

    // We will only have one glyph run 
    CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
    CTRunRef glyphRun = CFArrayGetValueAtIndex(glyphRuns, 0);

    // Get the count of all the glyphs
    NSUInteger glyphCount = CTRunGetGlyphCount(glyphRun);

    // This function gets the ptr to the glyphs, may return NULL
    glyphs = (CGGlyph *)CTRunGetGlyphsPtr(glyphRun);

    if (glyphs == NULL) {

        // if glyphs is NULL allocate a buffer for them
        // store them in the buffer
        // set the glyphs ptr to the buffer
        size_t glyphsBufferSize = sizeof(CGGlyph) * glyphCount;

        CGGlyph *glyphsBuffer = malloc(glyphsBufferSize);

        CTRunGetGlyphs(glyphRun, CFRangeMake(0, 0), glyphsBuffer);

        glyphs = glyphsBuffer;

    }

    // This function gets the ptr to the positions, may return NULL
    positions = (CGPoint *)CTRunGetPositionsPtr(glyphRun);

    if (positions == NULL) {

        // if positions is NULL allocate a buffer for them
        // store them in the buffer
        // set the positions ptr to the buffer
        size_t positionsBufferSize = sizeof(CGPoint) * glyphCount;

        CGPoint *positionsBuffer = malloc(positionsBufferSize);

        CTRunGetPositions(glyphRun, CFRangeMake(0, 0), positionsBuffer);

        positions = positionsBuffer;
    }

    // Changes each x by 15 and then sets new value at array index
    for (int i = 0; i < glyphCount; i++) {

        NSLog(@"positionAtIndex: %@", NSStringFromCGPoint(positions[i]));
        CGPoint oldPosition = positions[i];
        CGPoint newPosition = CGPointZero;

        NSLog(@"oldPosition = %@", NSStringFromCGPoint(oldPosition));

        newPosition.x = oldPosition.x + 15.0f;
        newPosition.y = oldPosition.y;

        NSLog(@"newPosition = %@", NSStringFromCGPoint(newPosition));

        positions[i] = newPosition;

        NSLog(@"positionAtIndex: %@", NSStringFromCGPoint(positions[i]));
    }

    // When CTLineDraw is commented out this will not display the glyphs on the screen
    CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);

    // When CGContextShowGlyphsAtPositions is commented out...
    // This will draw the string however it aligns the text to the view's lower left corner
    // CTFrameDraw would draw the text properly in the view's upper left corner
    // This is the difference I was speaking of and I'm not sure why it is
    CTLineDraw(line, context);


    // Make sure to release any CF objects and release allocated buffers
    CFRelease(path);
    free(positionsBuffer);
    free(glyphsBuffer);
}

I'm not sure exactly why CGContextShowGlyphsAtPositions() is not displaying the glyphs properly or why CTLineDraw() will not make use of the new glyph positions. Am I handling the allocation of those positions and glyphs incorrectly? Caveman debugging shows that the glyphs are as expected and the positions are being changed. I know that my code did not satisfy exactly what I was looking for (I was changing glyph position by 15.0f rather than based on string) however, where am I going wrong in laying out these glyphs?

like image 969
Brian Palma Avatar asked Sep 13 '13 18:09

Brian Palma


People also ask

What are glyphs?

Glyphs are units of visual rendering. A set of glyphs makes up a font . Glyphs can be construed as the basic units of organization of the visual rendering of text, just as characters are the basic unit of organization of encoded text.

What can you do with core text?

Create text layouts, optimize font handling, and access font metrics and glyph data. Core Text provides a low-level programming interface for laying out text and handling fonts. The Core Text layout engine is designed for high performance, ease of use, and close integration with Core Foundation.

What is core text on Mac OS?

Core Text is an advanced, low-level technology for laying out text and handling fonts. The Core Text API, introduced in Mac OS X v10.5 and iOS 3.2, is accessible from all OS X and iOS environments. Important: Core Text is intended for developers who must do text layout and font handling at a low level, such as developers of layout engines.

What is core text in Core Foundation?

Core Text provides a low-level programming interface for laying out text and handling fonts. The Core Text layout engine is designed for high performance, ease of use, and close integration with Core Foundation.


Video Answer


2 Answers

First, if you want to just want to tighten or loosen character spacing, you don't need Core Text for that. Just attach NSKernAttributeName to the section of your attributed string that you want to adjust. Positive to loosen, negative to tighten. (Zero means "no kerning," which is different from "default kerning." To get default kerning, don't set this attribute.) You can use size on NSAttributedString to try different spacings until you get the size you want.

"Positions" don't work the way you think they do. Positions are not screen coordinates. They're relative to the current text origin, which is the lower-left corner of the current line. (Remember that CoreText coordinates are upside down from UIKit coordinates.) You need to call CGContextSetTextPosition() before calling CGContextShowGlyphsAtPositions(). CTLineDraw() is a wrapper around CGContextShowGlyphsAtPositions(). That's why CTLineDraw() is drawing in the lower left (0,0). CTFrameDraw is drawing in the upper left because it adjusts the text origin correctly.

When you call CGContextShowGlyphsAtPositions() directly, it looks like nothing draws. catlan's answer addresses this. You need to set the context's font and color before drawing.

Your reposition code doesn't really do anything useful. It moves all the text 15 points to the right, but it doesn't actually change the spacing between them. (If that's all you wanted then you could just draw the string 15 points to the right.)

Your current code leaks memory. You allocate positionsBuffer and glyphsBuffer, but these shadow the previously declared versions. So you're always calling free(NULL) at the end.

For a full example of doing this kind of text adjustment by hand, see PinchTextLayer. But it can almost certainly be solved better by adjusting the kerning in the attributed string.

like image 133
Rob Napier Avatar answered Oct 09 '22 04:10

Rob Napier


CTLineDraw will use the font and color information from the CFAttributedString.

CGContextShowGlyphsAtPositions on the other hand needs these to be set on the CGContext:

CGFontRef cgFont = CTFontCopyGraphicsFont(font, NULL);
CGContextSetFont(context, cgFont);
CGContextSetFontSize(context, CTFontGetSize(font));
CGContextSetFillColorWithColor(context, fillColor);

CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);

CFRelease(cgFont)
like image 20
catlan Avatar answered Oct 09 '22 04:10

catlan