Ad

Our DNA is written in Swift
Jump

Getting Glyph Paths with DTCoreText

A client wanted to have a method for producing text that has a “cut out” effect, aka Text with “Inner Shadow”. Sort of like if you take a sheet of paper and then cut out the letters, then have light coming from up and slightly to the left so that it throws a shadow into the cut out letters.

For such a scenario you have to get a CGPath that is comprised of the glyphs that make up the text. Those are called glyphs because in some languages they are letters, but in some others they are not. Glyphs are the atomic element that any written language consists of.

Because it reasonably fits with the other work I have already done in DTCoreText I added such a method to both the classes for glyph runs as well as lines. These new methods will be released in the upcoming DTCoreText 1.5 release.

Many thanks go to Zach Waldowski whose method of constructing the glyph and line paths in AZAppearanceKit I used as a starting point. Zach has quite a few smart block-based enumerators for CTRuns and what not. But since I have a distinct class to represent a glyph run – called DTCoreTextGlyphRun – I opted to instead put the method to construct the glyph path in there.

There are some other decisions that I had to do differently. For example I am supporting synthetic italics for fonts that don’t have an italic typeface. This is done by supplying a transformation matrix when creating the font. Zach was ignoring this matrix and used it instead to position the individual glyphs. Nevertheless I would have had to do much lengthier research without Zach.

This is the final result which I will now proceed to demonstrate how to achieve. Note that this is as simple an approach as was justified to show the basic concepts. I challenge the reader to experiment with Quartz to get an even nicer looking shadow effect.

Text with inner shadow

The steps to produce this effect are:

  1. Create a DTCoreTextLayoutFrame for your text
  2. For drawing, iterate over all DTCoreTextLayoutLine elements in the layout frame
  3. Retrieve the path for all glyphs in the line
  4. Do your fancy drawing to a view or bitmap context

To keep this example as easy as possible, let’s create a method that creates a bitmap with some arbitrary text.

For the sake of this demonstration I am using the latest develop branch version of DTCoreText and I’m putting all the demo code into the CoreTextDemoAppDelegate.

For the first step I’m creating an attributed string via HTML, create an image via a new method and then just – brutally – slap it into an image view and add it to the main window, just to see what’s going on.

- (void)_demo
{
	// create attributed text
	NSString *html = @"<div style=\"font-family:Helvetica;font-size:60px\"><b>Testing</b></div>";
	NSData *data = [html dataUsingEncoding:NSUTF8StringEncoding];
	NSAttributedString *text = [[NSAttributedString alloc] initWithHTMLData:data documentAttributes:NULL];
 
	// create image with inner shadow from it
	UIImage *image = [self imageWithInnerShadowForText:text];
 
	// to show it, just put it into an image view
	UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
	imageView.layer.borderWidth = 1;
	imageView.layer.borderColor = [UIColor redColor].CGColor;
 
	[_window addSubview:imageView];
}

The imageWithInnerShadowForText does the rest. Read the inline comments to learn what is happening here.

- (UIImage *)imageWithInnerShadowForText:(NSAttributedString *)text
{
	// create a layouter, this owns the attributed string
	DTCoreTextLayouter *layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:text];
 
	// create a wide enough frame with open height
	NSRange fullRange = NSMakeRange(0, [text length]);
	CGRect rect = CGRectMake(10, 10, 10000, CGFLOAT_OPEN_HEIGHT); // height unknown
	DTCoreTextLayoutFrame *layoutFrame = [layouter layoutFrameWithRect:rect range:fullRange];
 
	// get actually used space
	CGRect rectCovered = layoutFrame.intrinsicContentFrame;
	CGRect bounds = CGRectMake(0, 10, rectCovered.size.width, rectCovered.size.height+10);
 
	UIGraphicsBeginImageContextWithOptions(bounds.size, YES, 0);
	CGContextRef context = UIGraphicsGetCurrentContext();
 
	// background
	CGContextSetGrayFillColor(context, 1, 1);
	CGContextFillRect(context, bounds);
 
	for (DTCoreTextLayoutLine *line in layoutFrame.lines)
	{
		CGContextSaveGState(context);
 
		// get the glyph path
		CGPathRef linePath = [line newPathWithGlyphs];
 
		// clip to that
		CGContextAddPath(context, linePath);
		CGContextClip(context);
 
		// draw "lower level slightly darker"
		CGContextSetGrayFillColor(context, 0.95, 1);
		CGContextFillRect(context, bounds);
 
		// add the path for shadow
		CGContextAddPath(context, linePath);
 
		UIColor *shadowColor = [UIColor blackColor];
		CGContextSetShadowWithColor(context, CGSizeMake(0.5, 2), 2, shadowColor.CGColor);
		CGContextSetLineWidth(context, 1);
		CGContextStrokePath(context);
 
		// remove clipping
		CGContextRestoreGState(context);
 
		// draw the outline a bit thicker to remove it
		CGContextAddPath(context, linePath);
		CGContextSetLineWidth(context, 2);
		CGContextSetGrayStrokeColor(context, 1, 1);
		CGContextDrawPath(context, kCGPathStroke);
	}
 
	UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
	UIGraphicsEndImageContext();
 
	return image;
}

The result that this will produce looks like the following picture. I took the liberty of drawing the outline a bit thicker a second time after having removed the glyph clipping. The 10s you see in the code are for moving the text 10 pixels away from the edges of the drawing context.

How do you like it?

Demo Result

I also made the shading inside the characters a slightly bit darker. The combination of all these parameters gives it a nice three-dimensional effect.

Conclusion

With the new methods for retrieving glyph paths in DTCoreText 1.5 you can let your creativity run wild with what you want to do with the shapes of these characters. The method to produce an “Inner Shadow” effect is just a starting point, you should experiment with the parameters to get the effect that you are looking for.

A word of caution: when drawing text CoreText does some nifty sub-pixel-smoothing. This effect is not possible when rendering into bitmap contexts because don’t have any sub-pixels. Instead Quartz antialiases edges by making them slightly blurry. It is not easy to find a sweet spot where the outlines of this custom drawn text remain visibly sharp but you still get a nice effect.

If you have any tips for us as to how to improve the visual fidelity further, please let us know in the comments.


Categories: Recipes

5 Comments »