VectorTextLayer.m
// File: VectorTextLayer.m |
//Abstract: A subclass of CALayer that lays out CAShapeLayers containing glyph paths |
// Version: 1.0 |
// |
//Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
//Inc. ("Apple") in consideration of your agreement to the following |
//terms, and your use, installation, modification or redistribution of |
//this Apple software constitutes acceptance of these terms. If you do |
//not agree with these terms, please do not use, install, modify or |
//redistribute this Apple software. |
// |
//In consideration of your agreement to abide by the following terms, and |
//subject to these terms, Apple grants you a personal, non-exclusive |
//license, under Apple's copyrights in this original Apple software (the |
//"Apple Software"), to use, reproduce, modify and redistribute the Apple |
//Software, with or without modifications, in source and/or binary forms; |
//provided that if you redistribute the Apple Software in its entirety and |
//without modifications, you must retain this notice and the following |
//text and disclaimers in all such redistributions of the Apple Software. |
//Neither the name, trademarks, service marks or logos of Apple Inc. may |
//be used to endorse or promote products derived from the Apple Software |
//without specific prior written permission from Apple. Except as |
//expressly stated in this notice, no other rights or licenses, express or |
//implied, are granted by Apple herein, including but not limited to any |
//patent rights that may be infringed by your derivative works or by other |
//works in which the Apple Software may be incorporated. |
// |
//The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
//MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
//THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
//FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
//OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
// |
//IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
//OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
//SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
//INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
//MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
//AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
//STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
//POSSIBILITY OF SUCH DAMAGE. |
// |
//Copyright (C) 2010 Apple Inc. All Rights Reserved. |
// |
#import "VectorTextLayer.h" |
// This implements a very simple glyph cache. |
// It maps from CTFontRef to CGGlyph to CGPathRef in order to reuse glyphs. |
// It does NOT try to retain the keys that are used (CTFontRef or CGGlyph) |
// but that is not an issue with respect to how it is used by this sample. |
@interface GlyphCache : NSObject |
{ |
CFMutableDictionaryRef cache; |
} |
-(id)init; |
-(CGPathRef)pathForGlyph:(CGGlyph)glyph fromFont:(CTFontRef)font; |
@end |
@implementation GlyphCache |
-(id)init |
{ |
self = [super init]; |
if(self != nil) |
{ |
cache = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); |
} |
return self; |
} |
-(void)dealloc |
{ |
CFRelease(cache); |
[super dealloc]; |
} |
-(CGPathRef)pathForGlyph:(CGGlyph)glyph fromFont:(CTFontRef)font |
{ |
// First we lookup the font to get to its glyph dictionary |
CFMutableDictionaryRef glyphDict = (CFMutableDictionaryRef)CFDictionaryGetValue(cache, font); |
if(glyphDict == NULL) |
{ |
// And if this font hasn't been seen before, we'll create and set the dictionary for it |
glyphDict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); |
CFDictionarySetValue(cache, font, glyphDict); |
CFRelease(glyphDict); |
} |
// Next we try to get a path for the given glyph from the glyph dictionary |
CGPathRef path = (CGPathRef)CFDictionaryGetValue(glyphDict, (const void *)(uintptr_t)glyph); |
if(path == NULL) |
{ |
// If the path hasn't been seen before, then we'll create the path from the font & glyph and cache it. |
path = CTFontCreatePathForGlyph(font, glyph, NULL); |
if(path == NULL) |
{ |
// If a glyph does not have a path, then we need a placeholder to set in the dictionary |
path = (CGPathRef)kCFNull; |
} |
CFDictionarySetValue(glyphDict, (const void *)(uintptr_t)glyph, path); |
CFRelease(path); |
} |
if(path == (CGPathRef)kCFNull) |
{ |
// If we got the placeholder, then set the path to NULL |
// (this will happen either after discovering the glyph path is NULL, |
// or after looking that up in the dictionary). |
path = NULL; |
} |
return path; |
} |
@end |
#pragma mark - |
@interface VectorTextLayer() |
-(void)layout; |
-(CAShapeLayer*)createShapeLayer; |
@end |
@implementation VectorTextLayer |
-(id)init |
{ |
self = [super init]; |
if(self != nil) |
{ |
glyphLayers = [[NSMutableArray alloc] init]; |
string = nil; |
line = NULL; |
zoomToFit = YES; |
// It is likely that your text rendering will assume that 0,0 is upper left |
// proceeding downwards, so we'll make that assumption here as well. |
self.geometryFlipped = YES; |
} |
return self; |
} |
-(void)dealloc |
{ |
[glyphLayers release]; |
[string release]; |
if(line != NULL) |
{ |
CFRelease(line); |
} |
[super dealloc]; |
} |
-(BOOL)zoomToFit |
{ |
return zoomToFit; |
} |
-(void)setZoomToFit:(BOOL)ztf |
{ |
if(ztf != zoomToFit) |
{ |
zoomToFit = ztf; |
[self setNeedsLayout]; |
} |
} |
-(id)string |
{ |
return string; |
} |
-(void)setString:(id)s |
{ |
if(s == nil) |
{ |
// The rest of this code is not prepared for nil, so use a placeholder instead. |
s = @""; |
} |
// If given an NSAttributedString, then copy it and layout. |
if([s isKindOfClass:[NSAttributedString class]]) |
{ |
if(![string isEqualToAttributedString:s]) |
{ |
[string release]; |
string = [s copy]; |
[self layout]; |
} |
} |
// If given an NSString, create an NSAttributedString from it and layout. |
else if([s isKindOfClass:[NSString class]]) |
{ |
NSAttributedString *newString = [[NSAttributedString alloc] initWithString:s]; |
if(![string isEqualToAttributedString:newString]) |
{ |
[string release]; |
string = [newString retain]; |
[self layout]; |
} |
[newString release]; |
} |
} |
-(void)layoutSublayers |
{ |
if(line != NULL) |
{ |
// Grab the typographic bounds |
// Image bounds might be more appropriate here, but we don't have a context. |
CGFloat ascent, descent, leading; |
double width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading); |
double height = ascent + descent + leading; |
// Start with the identity transform, we'll modify from here. |
CATransform3D transform = CATransform3DIdentity; |
// If zoomToFit is turned on, then use the lesser of the x and y scale factors to zoom |
if(zoomToFit) |
{ |
// Grab layer geometry to figure out how to scale. |
CGRect bounds = self.bounds; |
CGPoint anchorPoint = self.anchorPoint; |
CGFloat scaleX = bounds.size.width / width; |
CGFloat scaleY = bounds.size.height / height; |
CGFloat anchorX = bounds.origin.x + bounds.size.width * anchorPoint.x; |
CGFloat anchorY = bounds.origin.y + bounds.size.height * anchorPoint.y; |
// Translate to the origin so we can scale properly |
transform = CATransform3DTranslate(transform, -anchorX, -anchorY, 0.0); |
if(scaleX > scaleY) |
{ |
transform = CATransform3DScale(transform, scaleY, scaleY, 1.0); |
} |
else |
{ |
transform = CATransform3DScale(transform, scaleX, scaleX, 1.0); |
} |
// Then translate back to the anchorPoint so the transform applies correctly. |
transform = CATransform3DTranslate(transform, anchorX, anchorY, 0.0); |
} |
// Translate to move the text down by the height of the line. |
transform = CATransform3DTranslate(transform, 0.0, ascent, 0.0); |
// And apply the transform to all sublayers. |
[CATransaction setDisableActions:YES]; |
self.sublayerTransform = transform; |
[CATransaction commit]; |
} |
} |
CGColorRef CreateCGColorFromNSColor(NSColor *color) |
{ |
NSColor *rgb = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace]; |
CGFloat rgba[4]; |
[rgb getComponents:rgba]; |
return CGColorCreateGenericRGB(rgba[0], rgba[1], rgba[2], rgba[3]); |
} |
// Since the parameters from a particular text attribute dictionary dictates the |
// appearance of multiple layers, we convert a text attribute dictionary to a style |
// dictionary in order to set multiple parameters of a layer at once. |
NSDictionary *AttributesToStyle(NSDictionary *attributes) |
{ |
NSMutableDictionary *style = [NSMutableDictionary dictionary]; |
CGColorRef fillColor; |
// First look if the Core Text attribute is available, as it is a CGColorRef that we can use directly. |
id tmp = [attributes objectForKey:(id)kCTForegroundColorAttributeName]; |
if(tmp != nil) |
{ |
// Great, use it! |
fillColor = CGColorRetain((CGColorRef)tmp); |
} |
else |
{ |
// If not, check to see if the AppKit attribute is available |
// (which in our context is usually the case) |
// A pure Core Text client should not need to do this check. |
tmp = [attributes objectForKey:NSForegroundColorAttributeName]; |
if(tmp != nil) |
{ |
// The NSForegroundColorAttributeName attribute is an NSColor, so we have to convert it. |
fillColor = CreateCGColorFromNSColor(tmp); |
} |
else |
{ |
// Otherwise there is no foreground attribute, and we use the default foreground color, black. |
fillColor = CGColorCreateGenericRGB(0.0, 0.0, 0.0, 1.0); |
} |
} |
[style setObject:(id)fillColor forKey:@"fillColor"]; |
CGColorRelease(fillColor); |
return style; |
} |
-(CAShapeLayer*)createShapeLayer |
{ |
CAShapeLayer *layer = [CAShapeLayer layer]; |
// If this layer has geometry flipped, then we want to flip the shape layer's geometry |
// to ensure that glyphs are right-side-up. |
// If not, then we want to make sure it is not flipped for the same reason. |
layer.geometryFlipped = self.geometryFlipped; |
return layer; |
} |
-(void)layout |
{ |
// Let Core Text layout the text. |
if(line != NULL) |
{ |
CFRelease(line); |
} |
line = CTLineCreateWithAttributedString((CFAttributedStringRef)string); |
// Turn off animations. |
[CATransaction setDisableActions:YES]; |
// Ensure there are enough layers for the new layout. |
CFIndex glyphCount = CTLineGetGlyphCount(line); |
for(CFIndex i = [glyphLayers count]; i < glyphCount; ++i) |
{ |
CAShapeLayer *newLayer = [self createShapeLayer]; |
[glyphLayers addObject:newLayer]; |
[self addSublayer:newLayer]; |
} |
// For those layers that we won't be using anymore, remove their paths. |
for(CFIndex i = glyphCount; i < [glyphLayers count]; ++i) |
{ |
((CAShapeLayer*)[glyphLayers objectAtIndex:i]).path = NULL; |
} |
// Create and use a glyph cache to ensure that we reuse paths when we reuse glyphs from a font |
// See comments on the GlyphCache class above. |
GlyphCache *cache = [[GlyphCache alloc] init]; |
// Now lets get down to layout. |
CFIndex glyphIndex = 0; |
CFArrayRef glyphRuns = CTLineGetGlyphRuns(line); |
CFIndex runCount = CFArrayGetCount(glyphRuns); |
for(CFIndex i = 0; i < runCount; ++i) |
{ |
// For each run, we need to get the glyphs, their font (to get the path) and their locations. |
CTRunRef run = CFArrayGetValueAtIndex(glyphRuns, i); |
CFIndex runGlyphCount = CTRunGetGlyphCount(run); |
CGPoint positions[runGlyphCount]; |
CGGlyph glyphs[runGlyphCount]; |
// Grab the glyphs, positions, and font |
CTRunGetPositions(run, CFRangeMake(0, 0), positions); |
CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); |
CFDictionaryRef attributes = CTRunGetAttributes(run); |
CTFontRef runFont = CFDictionaryGetValue(attributes, kCTFontAttributeName); |
NSDictionary *style = AttributesToStyle((NSDictionary*)attributes); |
for(CFIndex j = 0; j < runGlyphCount; ++j, ++glyphIndex) |
{ |
// Layout each of the shape layers to place them at the correct position with the correct path. |
CAShapeLayer *layer = [glyphLayers objectAtIndex:glyphIndex]; |
layer.style = style; |
// We name them for identification purposes. |
layer.name = [NSString stringWithFormat:@"Font %@ Run %i Index %i Glyph 0x%.4X", runFont, i, j, glyphs[j]]; |
layer.position = positions[j]; |
layer.path = [cache pathForGlyph:glyphs[j] fromFont:runFont]; |
} |
} |
[cache release]; |
// Commit animations, and request a relayout (ends up in -layoutSublayers). |
[CATransaction commit]; |
[self setNeedsLayout]; |
} |
@end |
Copyright © 2010 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2010-04-08