Laying out individual glyphs with Core Text
Asked Answered
H

2

8

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?

Hectogram answered 13/9, 2013 at 18:25 Comment(1)
BTW You are calling CTRunGetPositionsPtr but free the pointer yourself if it is non null. This is not allowed. Just a hint for everyone doing a copy and paste.Timisoara
A
3

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)
Anglim answered 5/2, 2016 at 14:4 Comment(2)
Don't forget to call CFRelease(cgFont). CTFontCopyGraphicsFont includes "Copy" so you're responsible for releasing it.Putsch
Fixed code sample. We gave this two year old question quite some activity today :)Anglim
P
3

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.

Putsch answered 5/2, 2016 at 15:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.