Core Text With Asymmetric Shape on iOS
Asked Answered
D

1

4

I want to write text in a path that can take up any shape. Right now I can draw flipped text in it:

NSArray *coordinates = ...;

CGMutablePathRef pathRef = [self objectPathGivenCoordinates:coordinates];

... // Draw the shapes

// Now draw the text    
CGContextSetTextMatrix(context, CGAffineTransformIdentity);

NSAttributedString* attString = [[NSAttributedString alloc] initWithString:@"0,1,2..."];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, [attString length]), pathRef, NULL);
CTFrameDraw(frame, context);

That produces this:

Flipped text in Asymmetric Shape

I understand that iOS has a flipped coordinate system. Most solutions add these two lines before drawing:

CGContextTranslateCTM(context, 0, rect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

But that produces this:

Text is now written left-right right-side-up but out of path bounds

So why doesn't this technique work? In every example I've seen, this technique is used when text is rendered in something symmetric, or with clipping, or on OS X. If we're drawing text in a symmetric shape, our calls to CGContextTranslateCTM() and CGContextScaleCTM() make sense. A call to CGContextTranslateCTM(context, 0, rect.size.height), moves the origin of the first letter to the top. A call to CGContextScaleCTM(context, 1, -1) reverses the direction of Y. If we reverse the direction of Y on a symmetric shape, it doesn't change. Sadly, my shapes aren't symmetric.

Here is what I've considered but abandoned

  • Draw shapes and numbers on the same scale (both upside-down), then flip the UIImageView behind it somehow (1)
  • Don't make calls to change the scale or translate the CTM. Flip the text matrix with CGContextSetTextMatrix() so that text is drawn from left to right, bottom up, and is right side up. Determine how many extra characters are left in the shape after a string for the shape is determined. Reverse the words (now it will draw top to bottom, right to left, right side up). Add spaces for every extra character. Now for each CTLine, somehow reverse the words again (now it will draw top to bottom, left to right, right side up) (2)

Why I abandoned them

(1) I can't flip my images. They always need to be right side up

(2) This may be overkill for a simple solution someone else knows

Demodulator answered 9/12, 2013 at 17:21 Comment(2)
Use CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1, -1)) instead of CGAffineTransformIdentityMontelongo
See my second consideration at the end of the questionDemodulator
D
7

I determined a solution

// Flip the coordinates so that the object is drawn as if it were mirrored along the x-axis
// coordinates = @[CGPointMake(1, 2), CGPointMake(9, 17), CGPointMake(41, 3), ...]
NSArray *flippedCoordinates = [coordinates map:^id(id object) {
     CGPoint value;
     [object getValue:&value];
     CGPoint point = value;

     point.y = point.y * (-1) + rect.size.height;

     return [NSValue value:&point withObjCType:@encode(CGPoint)];
}];

// Generate a CGPathRef using the new coordinates
CGMutablePathRef pathRef = CGPathCreateMutable();
CGPathMoveToPoint(pathRef, NULL, [[flippedCoordinates objectAtIndex:0] CGPointValue].x + .5 [[flippedCoordinates objectAtIndex:0] CGPointValue].y + .5);
for(NSValue *arrayValue in flippedCoordinates) {
    CGPoint point = [arrayValue CGPointValue];
    CGPathAddLineToPoint(pathRef, NULL, point.x, point.y);
}
CGPathCloseSubpath(pathRef);

// Draw the object
// ...

// Draw the text as if it were on OS X
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
NSAttributedString* attString = [[NSAttributedString alloc] initWithString:@"0,1,2,..." attributes:@{(NSString *)kCTForegroundColorAttributeName : textColor}];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, [attString length]), pathRef, NULL);
CTFrameDraw(frame, context);

CFRelease(frame);
CFRelease(frameSetter);
CFRelease(pathRef);

// And, for the magic touch, flip the whole view AFTER everything is drawn
[self.layer setAffineTransform:CGAffineTransformMakeScale(1, -1)];

Now everything is drawn from top to bottom, left to right, right side up

Core Text correctly laid out on asymmetric shape in iOS

Demodulator answered 12/12, 2013 at 16:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.