// // FontLabelStringDrawing.m // FontLabel // // Created by Kevin Ballard on 5/5/09. // Copyright © 2009 Zynga Game Networks // // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // #import "FontLabelStringDrawing.h" #import "ZFont.h" #import "ZAttributedStringPrivate.h" @interface ZFont (ZFontPrivate) @property (nonatomic, readonly) CGFloat ratio; @end #define kUnicodeHighSurrogateStart 0xD800 #define kUnicodeHighSurrogateEnd 0xDBFF #define kUnicodeHighSurrogateMask kUnicodeHighSurrogateStart #define kUnicodeLowSurrogateStart 0xDC00 #define kUnicodeLowSurrogateEnd 0xDFFF #define kUnicodeLowSurrogateMask kUnicodeLowSurrogateStart #define kUnicodeSurrogateTypeMask 0xFC00 #define UnicharIsHighSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeHighSurrogateMask) #define UnicharIsLowSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeLowSurrogateMask) #define ConvertSurrogatePairToUTF32(high, low) ((UInt32)((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)) typedef enum { kFontTableFormat4 = 4, kFontTableFormat12 = 12, } FontTableFormat; typedef struct fontTable { NSUInteger retainCount; CFDataRef cmapTable; FontTableFormat format; union { struct { UInt16 segCountX2; UInt16 *endCodes; UInt16 *startCodes; UInt16 *idDeltas; UInt16 *idRangeOffsets; } format4; struct { UInt32 nGroups; struct { UInt32 startCharCode; UInt32 endCharCode; UInt32 startGlyphCode; } *groups; } format12; } cmap; } fontTable; static FontTableFormat supportedFormats[] = { kFontTableFormat4, kFontTableFormat12 }; static size_t supportedFormatsCount = sizeof(supportedFormats) / sizeof(FontTableFormat); static fontTable *newFontTable(CFDataRef cmapTable, FontTableFormat format) { fontTable *table = (struct fontTable *)malloc(sizeof(struct fontTable)); table->retainCount = 1; table->cmapTable = CFRetain(cmapTable); table->format = format; return table; } static fontTable *retainFontTable(fontTable *table) { if (table != NULL) { table->retainCount++; } return table; } static void releaseFontTable(fontTable *table) { if (table != NULL) { if (table->retainCount <= 1) { CFRelease(table->cmapTable); free(table); } else { table->retainCount--; } } } static const void *fontTableRetainCallback(CFAllocatorRef allocator, const void *value) { return retainFontTable((fontTable *)value); } static void fontTableReleaseCallback(CFAllocatorRef allocator, const void *value) { releaseFontTable((fontTable *)value); } static const CFDictionaryValueCallBacks kFontTableDictionaryValueCallBacks = { .version = 0, .retain = &fontTableRetainCallback, .release = &fontTableReleaseCallback, .copyDescription = NULL, .equal = NULL }; // read the cmap table from the font // we only know how to understand some of the table formats at the moment static fontTable *readFontTableFromCGFont(CGFontRef font) { CFDataRef cmapTable = CGFontCopyTableForTag(font, 'cmap'); NSCAssert1(cmapTable != NULL, @"CGFontCopyTableForTag returned NULL for 'cmap' tag in font %@", (font ? [(id)CFCopyDescription(font) autorelease] : @"(null)")); const UInt8 * const bytes = CFDataGetBytePtr(cmapTable); NSCAssert1(OSReadBigInt16(bytes, 0) == 0, @"cmap table for font %@ has bad version number", (font ? [(id)CFCopyDescription(font) autorelease] : @"(null)")); UInt16 numberOfSubtables = OSReadBigInt16(bytes, 2); const UInt8 *unicodeSubtable = NULL; //UInt16 unicodeSubtablePlatformID; UInt16 unicodeSubtablePlatformSpecificID; FontTableFormat unicodeSubtableFormat; const UInt8 * const encodingSubtables = &bytes[4]; for (UInt16 i = 0; i < numberOfSubtables; i++) { const UInt8 * const encodingSubtable = &encodingSubtables[8 * i]; UInt16 platformID = OSReadBigInt16(encodingSubtable, 0); UInt16 platformSpecificID = OSReadBigInt16(encodingSubtable, 2); // find the best subtable // best is defined by a combination of encoding and format // At the moment we only support format 4, so ignore all other format tables // We prefer platformID == 0, but we will also accept Microsoft's unicode format if (platformID == 0 || (platformID == 3 && platformSpecificID == 1)) { BOOL preferred = NO; if (unicodeSubtable == NULL) { preferred = YES; } else if (platformID == 0 && platformSpecificID > unicodeSubtablePlatformSpecificID) { preferred = YES; } if (preferred) { UInt32 offset = OSReadBigInt32(encodingSubtable, 4); const UInt8 *subtable = &bytes[offset]; UInt16 format = OSReadBigInt16(subtable, 0); for (size_t i = 0; i < supportedFormatsCount; i++) { if (format == supportedFormats[i]) { if (format >= 8) { // the version is a fixed-point UInt16 formatFrac = OSReadBigInt16(subtable, 2); if (formatFrac != 0) { // all the current formats with a Fixed version are always *.0 continue; } } unicodeSubtable = subtable; //unicodeSubtablePlatformID = platformID; unicodeSubtablePlatformSpecificID = platformSpecificID; unicodeSubtableFormat = format; break; } } } } } fontTable *table = NULL; if (unicodeSubtable != NULL) { table = newFontTable(cmapTable, unicodeSubtableFormat); switch (unicodeSubtableFormat) { case kFontTableFormat4: // subtable format 4 //UInt16 length = OSReadBigInt16(unicodeSubtable, 2); //UInt16 language = OSReadBigInt16(unicodeSubtable, 4); table->cmap.format4.segCountX2 = OSReadBigInt16(unicodeSubtable, 6); //UInt16 searchRange = OSReadBigInt16(unicodeSubtable, 8); //UInt16 entrySelector = OSReadBigInt16(unicodeSubtable, 10); //UInt16 rangeShift = OSReadBigInt16(unicodeSubtable, 12); table->cmap.format4.endCodes = (UInt16*)&unicodeSubtable[14]; table->cmap.format4.startCodes = (UInt16*)&((UInt8*)table->cmap.format4.endCodes)[table->cmap.format4.segCountX2+2]; table->cmap.format4.idDeltas = (UInt16*)&((UInt8*)table->cmap.format4.startCodes)[table->cmap.format4.segCountX2]; table->cmap.format4.idRangeOffsets = (UInt16*)&((UInt8*)table->cmap.format4.idDeltas)[table->cmap.format4.segCountX2]; //UInt16 *glyphIndexArray = &idRangeOffsets[segCountX2]; break; case kFontTableFormat12: table->cmap.format12.nGroups = OSReadBigInt32(unicodeSubtable, 12); table->cmap.format12.groups = (void *)&unicodeSubtable[16]; break; default: releaseFontTable(table); table = NULL; } } CFRelease(cmapTable); return table; } // outGlyphs must be at least size n static void mapCharactersToGlyphsInFont(const fontTable *table, unichar characters[], size_t charLen, CGGlyph outGlyphs[], size_t *outGlyphLen) { if (table != NULL) { NSUInteger j = 0; switch (table->format) { case kFontTableFormat4: { for (NSUInteger i = 0; i < charLen; i++, j++) { unichar c = characters[i]; UInt16 segOffset; BOOL foundSegment = NO; for (segOffset = 0; segOffset < table->cmap.format4.segCountX2; segOffset += 2) { UInt16 endCode = OSReadBigInt16(table->cmap.format4.endCodes, segOffset); if (endCode >= c) { foundSegment = YES; break; } } if (!foundSegment) { // no segment // this is an invalid font outGlyphs[j] = 0; } else { UInt16 startCode = OSReadBigInt16(table->cmap.format4.startCodes, segOffset); if (!(startCode <= c)) { // the code falls in a hole between segments outGlyphs[j] = 0; } else { UInt16 idRangeOffset = OSReadBigInt16(table->cmap.format4.idRangeOffsets, segOffset); if (idRangeOffset == 0) { UInt16 idDelta = OSReadBigInt16(table->cmap.format4.idDeltas, segOffset); outGlyphs[j] = (c + idDelta) % 65536; } else { // use the glyphIndexArray UInt16 glyphOffset = idRangeOffset + 2 * (c - startCode); outGlyphs[j] = OSReadBigInt16(&((UInt8*)table->cmap.format4.idRangeOffsets)[segOffset], glyphOffset); } } } } break; } case kFontTableFormat12: { UInt32 lastSegment = UINT32_MAX; for (NSUInteger i = 0; i < charLen; i++, j++) { unichar c = characters[i]; UInt32 c32 = c; if (UnicharIsHighSurrogate(c)) { if (i+1 < charLen) { // do we have another character after this one? unichar cc = characters[i+1]; if (UnicharIsLowSurrogate(cc)) { c32 = ConvertSurrogatePairToUTF32(c, cc); i++; } } } // Start the heuristic search // If this is an ASCII char, just do a linear search // Otherwise do a hinted, modified binary search // Start the first pivot at the last range found // And when moving the pivot, limit the movement by increasing // powers of two. This should help with locality __typeof__(table->cmap.format12.groups[0]) *foundGroup = NULL; if (c32 <= 0x7F) { // ASCII for (UInt32 idx = 0; idx < table->cmap.format12.nGroups; idx++) { __typeof__(table->cmap.format12.groups[idx]) *group = &table->cmap.format12.groups[idx]; if (c32 < OSSwapBigToHostInt32(group->startCharCode)) { // we've fallen into a hole break; } else if (c32 <= OSSwapBigToHostInt32(group->endCharCode)) { // this is the range foundGroup = group; break; } } } else { // heuristic search UInt32 maxJump = (lastSegment == UINT32_MAX ? UINT32_MAX / 2 : 8); UInt32 lowIdx = 0, highIdx = table->cmap.format12.nGroups; // highIdx is the first invalid idx UInt32 pivot = (lastSegment == UINT32_MAX ? lowIdx + (highIdx - lowIdx) / 2 : lastSegment); while (highIdx > lowIdx) { __typeof__(table->cmap.format12.groups[pivot]) *group = &table->cmap.format12.groups[pivot]; if (c32 < OSSwapBigToHostInt32(group->startCharCode)) { highIdx = pivot; } else if (c32 > OSSwapBigToHostInt32(group->endCharCode)) { lowIdx = pivot + 1; } else { // we've hit the range foundGroup = group; break; } if (highIdx - lowIdx > maxJump * 2) { if (highIdx == pivot) { pivot -= maxJump; } else { pivot += maxJump; } maxJump *= 2; } else { pivot = lowIdx + (highIdx - lowIdx) / 2; } } if (foundGroup != NULL) lastSegment = pivot; } if (foundGroup == NULL) { outGlyphs[j] = 0; } else { outGlyphs[j] = (CGGlyph)(OSSwapBigToHostInt32(foundGroup->startGlyphCode) + (c32 - OSSwapBigToHostInt32(foundGroup->startCharCode))); } } break; } } if (outGlyphLen != NULL) *outGlyphLen = j; } else { // we have no table, so just null out the glyphs bzero(outGlyphs, charLen*sizeof(CGGlyph)); if (outGlyphLen != NULL) *outGlyphLen = 0; } } static BOOL mapGlyphsToAdvancesInFont(ZFont *font, size_t n, CGGlyph glyphs[], CGFloat outAdvances[]) { int advances[n]; if (CGFontGetGlyphAdvances(font.cgFont, glyphs, n, advances)) { CGFloat ratio = font.ratio; for (size_t i = 0; i < n; i++) { outAdvances[i] = advances[i]*ratio; } return YES; } else { bzero(outAdvances, n*sizeof(CGFloat)); } return NO; } static id getValueOrDefaultForRun(ZAttributeRun *run, NSString *key) { id value = [run.attributes objectForKey:key]; if (value == nil) { static NSDictionary *defaultValues = nil; if (defaultValues == nil) { defaultValues = [[NSDictionary alloc] initWithObjectsAndKeys: [ZFont fontWithUIFont:[UIFont systemFontOfSize:12]], ZFontAttributeName, [UIColor blackColor], ZForegroundColorAttributeName, [UIColor clearColor], ZBackgroundColorAttributeName, [NSNumber numberWithInt:ZUnderlineStyleNone], ZUnderlineStyleAttributeName, nil]; } value = [defaultValues objectForKey:key]; } return value; } static void readRunInformation(NSArray *attributes, NSUInteger len, CFMutableDictionaryRef fontTableMap, NSUInteger index, ZAttributeRun **currentRun, NSUInteger *nextRunStart, ZFont **currentFont, fontTable **currentTable) { *currentRun = [attributes objectAtIndex:index]; *nextRunStart = ([attributes count] > index+1 ? [[attributes objectAtIndex:index+1] index] : len); *currentFont = getValueOrDefaultForRun(*currentRun, ZFontAttributeName); if (!CFDictionaryGetValueIfPresent(fontTableMap, (*currentFont).cgFont, (const void **)currentTable)) { *currentTable = readFontTableFromCGFont((*currentFont).cgFont); CFDictionarySetValue(fontTableMap, (*currentFont).cgFont, *currentTable); releaseFontTable(*currentTable); } } static CGSize drawOrSizeTextConstrainedToSize(BOOL performDraw, NSString *string, NSArray *attributes, CGSize constrainedSize, NSUInteger maxLines, UILineBreakMode lineBreakMode, UITextAlignment alignment, BOOL ignoreColor) { NSUInteger len = [string length]; NSUInteger idx = 0; CGPoint drawPoint = CGPointZero; CGSize retValue = CGSizeZero; CGContextRef ctx = (performDraw ? UIGraphicsGetCurrentContext() : NULL); BOOL convertNewlines = (maxLines == 1); // Extract the characters from the string // Convert newlines to spaces if necessary unichar *characters = (unichar *)malloc(sizeof(unichar) * len); if (convertNewlines) { NSCharacterSet *charset = [NSCharacterSet newlineCharacterSet]; NSRange range = NSMakeRange(0, len); size_t cIdx = 0; while (range.length > 0) { NSRange newlineRange = [string rangeOfCharacterFromSet:charset options:0 range:range]; if (newlineRange.location == NSNotFound) { [string getCharacters:&characters[cIdx] range:range]; cIdx += range.length; break; } else { NSUInteger delta = newlineRange.location - range.location; if (newlineRange.location > range.location) { [string getCharacters:&characters[cIdx] range:NSMakeRange(range.location, delta)]; } cIdx += delta; characters[cIdx] = (unichar)' '; cIdx++; delta += newlineRange.length; range.location += delta, range.length -= delta; if (newlineRange.length == 1 && range.length >= 1 && [string characterAtIndex:newlineRange.location] == (unichar)'\r' && [string characterAtIndex:range.location] == (unichar)'\n') { // CRLF sequence, skip the LF range.location += 1, range.length -= 1; } } } len = cIdx; } else { [string getCharacters:characters range:NSMakeRange(0, len)]; } // Create storage for glyphs and advances CGGlyph *glyphs; CGFloat *advances; { NSUInteger maxRunLength = 0; ZAttributeRun *a = [attributes objectAtIndex:0]; for (NSUInteger i = 1; i < [attributes count]; i++) { ZAttributeRun *b = [attributes objectAtIndex:i]; maxRunLength = MAX(maxRunLength, b.index - a.index); a = b; } maxRunLength = MAX(maxRunLength, len - a.index); maxRunLength++; // for a potential ellipsis glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * maxRunLength); advances = (CGFloat *)malloc(sizeof(CGFloat) * maxRunLength); } // Use this table to cache all fontTable objects CFMutableDictionaryRef fontTableMap = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kFontTableDictionaryValueCallBacks); // Fetch initial style values NSUInteger currentRunIdx = 0; ZAttributeRun *currentRun; NSUInteger nextRunStart; ZFont *currentFont; fontTable *currentTable; #define READ_RUN() readRunInformation(attributes, len, fontTableMap, \ currentRunIdx, ¤tRun, &nextRunStart, \ ¤tFont, ¤tTable) READ_RUN(); // fetch the glyphs for the first run size_t glyphCount; NSUInteger glyphIdx; #define READ_GLYPHS() do { \ mapCharactersToGlyphsInFont(currentTable, &characters[currentRun.index], (nextRunStart - currentRun.index), glyphs, &glyphCount); \ mapGlyphsToAdvancesInFont(currentFont, (nextRunStart - currentRun.index), glyphs, advances); \ glyphIdx = 0; \ } while (0) READ_GLYPHS(); NSMutableCharacterSet *alphaCharset = [NSMutableCharacterSet alphanumericCharacterSet]; [alphaCharset addCharactersInString:@"([{'\"\u2019\u02BC"]; // scan left-to-right looking for newlines or until we hit the width constraint // When we hit a wrapping point, calculate truncation as follows: // If we have room to draw at least one more character on the next line, no truncation // Otherwise apply the truncation algorithm to the current line. // After calculating any truncation, draw. // Each time we hit the end of an attribute run, calculate the new font and make sure // it fits (vertically) within the size constraint. If not, truncate this line. // When we draw, iterate over the attribute runs for this line and draw each run separately BOOL lastLine = NO; // used to indicate truncation and to stop the iterating NSUInteger lineCount = 1; while (idx < len && !lastLine) { if (maxLines > 0 && lineCount == maxLines) { lastLine = YES; } // scan left-to-right struct { NSUInteger index; NSUInteger glyphIndex; NSUInteger currentRunIdx; } indexCache = { idx, glyphIdx, currentRunIdx }; CGSize lineSize = CGSizeMake(0, currentFont.leading); CGFloat lineAscender = currentFont.ascender; struct { NSUInteger index; NSUInteger glyphIndex; NSUInteger currentRunIdx; CGSize lineSize; } lastWrapCache = {0, 0, 0, CGSizeZero}; BOOL inAlpha = NO; // used for calculating wrap points BOOL finishLine = NO; for (;idx <= len && !finishLine;) { NSUInteger skipCount = 0; if (idx == len) { finishLine = YES; lastLine = YES; } else { if (idx >= nextRunStart) { // cycle the font and table and grab the next set of glyphs do { currentRunIdx++; READ_RUN(); } while (idx >= nextRunStart); READ_GLYPHS(); // re-scan the characters to synchronize the glyph index for (NSUInteger j = currentRun.index; j < idx; j++) { if (UnicharIsHighSurrogate(characters[j]) && j+1 lineSize.height) { lineSize.height = currentFont.leading; if (retValue.height + currentFont.ascender > constrainedSize.height) { lastLine = YES; finishLine = YES; } } lineAscender = MAX(lineAscender, currentFont.ascender); } unichar c = characters[idx]; // Mark a wrap point before spaces and after any stretch of non-alpha characters BOOL markWrap = NO; if (c == (unichar)' ') { markWrap = YES; } else if ([alphaCharset characterIsMember:c]) { if (!inAlpha) { markWrap = YES; inAlpha = YES; } } else { inAlpha = NO; } if (markWrap) { lastWrapCache = (__typeof__(lastWrapCache)){ .index = idx, .glyphIndex = glyphIdx, .currentRunIdx = currentRunIdx, .lineSize = lineSize }; } // process the line if (c == (unichar)'\n' || c == 0x0085) { // U+0085 is the NEXT_LINE unicode character finishLine = YES; skipCount = 1; } else if (c == (unichar)'\r') { finishLine = YES; // check for CRLF if (idx+1 < len && characters[idx+1] == (unichar)'\n') { skipCount = 2; } else { skipCount = 1; } } else if (lineSize.width + advances[glyphIdx] > constrainedSize.width) { finishLine = YES; if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) { lastLine = YES; } // walk backwards if wrapping is necessary if (lastWrapCache.index > indexCache.index && lineBreakMode != UILineBreakModeCharacterWrap && (!lastLine || lineBreakMode != UILineBreakModeClip)) { // we're doing some sort of word wrapping idx = lastWrapCache.index; lineSize = lastWrapCache.lineSize; if (!lastLine) { // re-check if this is the last line if (lastWrapCache.currentRunIdx != currentRunIdx) { currentRunIdx = lastWrapCache.currentRunIdx; READ_RUN(); READ_GLYPHS(); } if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) { lastLine = YES; } } glyphIdx = lastWrapCache.glyphIndex; // skip any spaces for (NSUInteger j = idx; j < len && characters[j] == (unichar)' '; j++) { skipCount++; } } } } if (finishLine) { // TODO: support head/middle truncation if (lastLine && idx < len && lineBreakMode == UILineBreakModeTailTruncation) { // truncate unichar ellipsis = 0x2026; // ellipsis (…) CGGlyph ellipsisGlyph; mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL); CGFloat ellipsisWidth; mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth); while ((idx - indexCache.index) > 1 && lineSize.width + ellipsisWidth > constrainedSize.width) { // we have more than 1 character and we're too wide, so back up idx--; if (UnicharIsHighSurrogate(characters[idx]) && UnicharIsLowSurrogate(characters[idx+1])) { idx--; } if (idx < currentRun.index) { ZFont *oldFont = currentFont; do { currentRunIdx--; READ_RUN(); } while (idx < currentRun.index); READ_GLYPHS(); glyphIdx = glyphCount-1; if (oldFont != currentFont) { mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL); mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth); } } else { glyphIdx--; } lineSize.width -= advances[glyphIdx]; } // skip any spaces before truncating while ((idx - indexCache.index) > 1 && characters[idx-1] == (unichar)' ') { idx--; if (idx < currentRun.index) { currentRunIdx--; READ_RUN(); READ_GLYPHS(); glyphIdx = glyphCount-1; } else { glyphIdx--; } lineSize.width -= advances[glyphIdx]; } lineSize.width += ellipsisWidth; glyphs[glyphIdx] = ellipsisGlyph; idx++; glyphIdx++; } retValue.width = MAX(retValue.width, lineSize.width); retValue.height += lineSize.height; // draw if (performDraw) { switch (alignment) { case UITextAlignmentLeft: drawPoint.x = 0; break; case UITextAlignmentCenter: drawPoint.x = (constrainedSize.width - lineSize.width) / 2.0f; break; case UITextAlignmentRight: drawPoint.x = constrainedSize.width - lineSize.width; break; } NSUInteger stopGlyphIdx = glyphIdx; NSUInteger lastRunIdx = currentRunIdx; NSUInteger stopCharIdx = idx; idx = indexCache.index; if (currentRunIdx != indexCache.currentRunIdx) { currentRunIdx = indexCache.currentRunIdx; READ_RUN(); READ_GLYPHS(); } glyphIdx = indexCache.glyphIndex; for (NSUInteger drawIdx = currentRunIdx; drawIdx <= lastRunIdx; drawIdx++) { if (drawIdx != currentRunIdx) { currentRunIdx = drawIdx; READ_RUN(); READ_GLYPHS(); } NSUInteger numGlyphs; if (drawIdx == lastRunIdx) { numGlyphs = stopGlyphIdx - glyphIdx; idx = stopCharIdx; } else { numGlyphs = glyphCount - glyphIdx; idx = nextRunStart; } CGContextSetFont(ctx, currentFont.cgFont); CGContextSetFontSize(ctx, currentFont.pointSize); // calculate the fragment size CGFloat fragmentWidth = 0; for (NSUInteger g = 0; g < numGlyphs; g++) { fragmentWidth += advances[glyphIdx + g]; } if (!ignoreColor) { UIColor *foregroundColor = getValueOrDefaultForRun(currentRun, ZForegroundColorAttributeName); UIColor *backgroundColor = getValueOrDefaultForRun(currentRun, ZBackgroundColorAttributeName); if (backgroundColor != nil && ![backgroundColor isEqual:[UIColor clearColor]]) { [backgroundColor setFill]; UIRectFillUsingBlendMode((CGRect){ drawPoint, { fragmentWidth, lineSize.height } }, kCGBlendModeNormal); } [foregroundColor setFill]; } CGContextShowGlyphsAtPoint(ctx, drawPoint.x, drawPoint.y + lineAscender, &glyphs[glyphIdx], numGlyphs); NSNumber *underlineStyle = getValueOrDefaultForRun(currentRun, ZUnderlineStyleAttributeName); if ([underlineStyle integerValue] & ZUnderlineStyleMask) { // we only support single for the time being UIRectFill(CGRectMake(drawPoint.x, drawPoint.y + lineAscender, fragmentWidth, 1)); } drawPoint.x += fragmentWidth; glyphIdx += numGlyphs; } drawPoint.y += lineSize.height; } idx += skipCount; glyphIdx += skipCount; lineCount++; } else { lineSize.width += advances[glyphIdx]; glyphIdx++; idx++; if (idx < len && UnicharIsHighSurrogate(characters[idx-1]) && UnicharIsLowSurrogate(characters[idx])) { // skip the second half of the surrogate pair idx++; } } } } CFRelease(fontTableMap); free(glyphs); free(advances); free(characters); #undef READ_GLYPHS #undef READ_RUN return retValue; } static NSArray *attributeRunForFont(ZFont *font) { return [NSArray arrayWithObject:[ZAttributeRun attributeRunWithIndex:0 attributes:[NSDictionary dictionaryWithObject:font forKey:ZFontAttributeName]]]; } static CGSize drawTextInRect(CGRect rect, NSString *text, NSArray *attributes, UILineBreakMode lineBreakMode, UITextAlignment alignment, NSUInteger numberOfLines, BOOL ignoreColor) { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSaveGState(ctx); // flip it upside-down because our 0,0 is upper-left, whereas ttfs are for screens where 0,0 is lower-left CGAffineTransform textTransform = CGAffineTransformMake(1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f); CGContextSetTextMatrix(ctx, textTransform); CGContextTranslateCTM(ctx, rect.origin.x, rect.origin.y); CGContextSetTextDrawingMode(ctx, kCGTextFill); CGSize size = drawOrSizeTextConstrainedToSize(YES, text, attributes, rect.size, numberOfLines, lineBreakMode, alignment, ignoreColor); CGContextRestoreGState(ctx); return size; } @implementation NSString (FontLabelStringDrawing) // CGFontRef-based methods - (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize { return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize]]; } - (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size { return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size]; } - (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode { return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size lineBreakMode:lineBreakMode]; } - (CGSize)drawAtPoint:(CGPoint)point withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize { return [self drawAtPoint:point withZFont:[ZFont fontWithCGFont:font size:pointSize]]; } - (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize { return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize]]; } - (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize lineBreakMode:(UILineBreakMode)lineBreakMode { return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode]; } - (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize lineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment { return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode alignment:alignment]; } // ZFont-based methods - (CGSize)sizeWithZFont:(ZFont *)font { CGSize size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1, UILineBreakModeClip, UITextAlignmentLeft, YES); return CGSizeMake(ceilf(size.width), ceilf(size.height)); } - (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size { return [self sizeWithZFont:font constrainedToSize:size lineBreakMode:UILineBreakModeWordWrap]; } /* According to experimentation with UIStringDrawing, this can actually return a CGSize whose height is greater than the one passed in. The two cases are as follows: 1. If the given size parameter's height is smaller than a single line, the returned value will be the height of one line. 2. If the given size parameter's height falls between multiples of a line height, and the wrapped string actually extends past the size.height, and the difference between size.height and the previous multiple of a line height is >= the font's ascender, then the returned size's height is extended to the next line. To put it simply, if the baseline point of a given line falls in the given size, the entire line will be present in the output size. */ - (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode { size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, 0, lineBreakMode, UITextAlignmentLeft, YES); return CGSizeMake(ceilf(size.width), ceilf(size.height)); } - (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode numberOfLines:(NSUInteger)numberOfLines { size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, numberOfLines, lineBreakMode, UITextAlignmentLeft, YES); return CGSizeMake(ceilf(size.width), ceilf(size.height)); } - (CGSize)drawAtPoint:(CGPoint)point withZFont:(ZFont *)font { return [self drawAtPoint:point forWidth:CGFLOAT_MAX withZFont:font lineBreakMode:UILineBreakModeClip]; } - (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode { return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self, attributeRunForFont(font), lineBreakMode, UITextAlignmentLeft, 1, YES); } - (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font { return [self drawInRect:rect withZFont:font lineBreakMode:UILineBreakModeWordWrap]; } - (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode { return [self drawInRect:rect withZFont:font lineBreakMode:lineBreakMode alignment:UITextAlignmentLeft]; } - (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment { return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, 0, YES); } - (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment numberOfLines:(NSUInteger)numberOfLines { return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, numberOfLines, YES); } @end @implementation ZAttributedString (ZAttributedStringDrawing) - (CGSize)size { CGSize size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1, UILineBreakModeClip, UITextAlignmentLeft, NO); return CGSizeMake(ceilf(size.width), ceilf(size.height)); } - (CGSize)sizeConstrainedToSize:(CGSize)size { return [self sizeConstrainedToSize:size lineBreakMode:UILineBreakModeWordWrap]; } - (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode { size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, 0, lineBreakMode, UITextAlignmentLeft, NO); return CGSizeMake(ceilf(size.width), ceilf(size.height)); } - (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode numberOfLines:(NSUInteger)numberOfLines { size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, numberOfLines, lineBreakMode, UITextAlignmentLeft, NO); return CGSizeMake(ceilf(size.width), ceilf(size.height)); } - (CGSize)drawAtPoint:(CGPoint)point { return [self drawAtPoint:point forWidth:CGFLOAT_MAX lineBreakMode:UILineBreakModeClip]; } - (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode { return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self.string, self.attributes, lineBreakMode, UITextAlignmentLeft, 1, NO); } - (CGSize)drawInRect:(CGRect)rect { return [self drawInRect:rect withLineBreakMode:UILineBreakModeWordWrap]; } - (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode { return [self drawInRect:rect withLineBreakMode:lineBreakMode alignment:UITextAlignmentLeft]; } - (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment { return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, 0, NO); } - (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment numberOfLines:(NSUInteger)numberOfLines { return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, numberOfLines, NO); } @end