Text Wrap with Core Text
I’m writing a text renderer using Core Text, and I discovered I’ll need to wrap text around objects (such as is done in any DTP program). I couldn’t find any easy answers as to how to do this in the documentation, so having finally got it working I’ll share what I did.
To lay out text in a custom shape in Core Text, you can pass a CGPath in when you create your CTFramesetterRef. Originally this only supported rectangular paths, but now it supports fully custom paths. My first thought was to see if I could subtract the region to wrap around from the path for my frame’s border, and pass the result in to Core Text as a path.
It turns out firstly that subtracting one path from another in Core Graphics is not trivial. However, if you simply add two shapes to the same path, Core Graphics can use a winding rule to work out which areas to draw. Core Text, at least as of iOS 4.2, can also use this kind of algorithm. This will work for many cases: if you can guarantee that your object to be wrapped will be fully inside the frame (and not overlapping the edge), just go ahead and add its border to the same path as your frame, and Core Text will do the rest.
However, this solution doesn’t work if the two regions overlap:
At this point, I started looking around for algorithms to do path intersections, and considered porting one to use Core Graphics. But then, I found a very interesting line in the CTFrame header file:
extern const CFStringRef kCTFrameClippingPathsAttributeName CT_AVAILABLE_STARTING( __MAC_10_7, __IPHONE_4_3);
The description said merely “Specifies array of paths to clip frame.”.
It turns out that Core Text can take an array of paths to wrap text around, by means of the options dictionary passed in as part of CTFramesetterCreateFrame(). I’ve posted some code below that shows how to use it. It seems that this finally provides the wrapping behaviour that we want:
Here is my code. It goes inside a drawRect: method on a UIView subclass. (About the only change needed to run it on the Mac would be the first line).
CGContextRef context = UIGraphicsGetCurrentContext(); // Flip the coordinate system CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); // Create a path to render text in CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, self.bounds ); // An attributed string containing the text to render NSAttributedString* attString = [[NSAttributedString alloc] initWithString:...]; // Create a path to wrap around CGMutablePathRef clipPath = CGPathCreateMutable(); CGPathAddEllipseInRect(clipPath, NULL, CGRectMake(200, 200, 300, 300) ); // A CFDictionary containing the clipping path CFStringRef keys[] = { kCTFramePathClippingPathAttributeName }; CFTypeRef values[] = { clipPath }; CFDictionaryRef clippingPathDict = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); // An array of clipping paths -- you can use more than one if needed! NSArray *clippingPaths = [NSArray arrayWithObject:(NSDictionary*)clippingPathDict]; // Create an options dictionary, to pass in to CTFramesetter NSDictionary *optionsDict = [NSDictionary dictionaryWithObject:clippingPaths forKey:(NSString*)kCTFrameClippingPathsAttributeName]; // Finally create the framesetter and render text CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString); //3 CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]), path, optionsDict); CTFrameDraw(frame, context); // Clean up CFRelease(frame); CFRelease(path); CFRelease(framesetter);