summary refs log tree commit diff stats
path: root/libs/FontLabel/FontLabelStringDrawing.m
diff options
context:
space:
mode:
authorStarla Insigna <starla4444@gmail.com>2011-07-30 11:19:14 -0400
committerStarla Insigna <starla4444@gmail.com>2011-07-30 11:19:14 -0400
commit9cd57b731ab1c666d4a1cb725538fdc137763d12 (patch)
tree5bac45ae5157a1cb10c6e45500cbf72789917980 /libs/FontLabel/FontLabelStringDrawing.m
downloadcartcollect-9cd57b731ab1c666d4a1cb725538fdc137763d12.tar.gz
cartcollect-9cd57b731ab1c666d4a1cb725538fdc137763d12.tar.bz2
cartcollect-9cd57b731ab1c666d4a1cb725538fdc137763d12.zip
Initial commit (version 0.2.1)
Diffstat (limited to 'libs/FontLabel/FontLabelStringDrawing.m')
-rwxr-xr-xlibs/FontLabel/FontLabelStringDrawing.m892
1 files changed, 892 insertions, 0 deletions
diff --git a/libs/FontLabel/FontLabelStringDrawing.m b/libs/FontLabel/FontLabelStringDrawing.m new file mode 100755 index 0000000..2907372 --- /dev/null +++ b/libs/FontLabel/FontLabelStringDrawing.m
@@ -0,0 +1,892 @@
1//
2// FontLabelStringDrawing.m
3// FontLabel
4//
5// Created by Kevin Ballard on 5/5/09.
6// Copyright © 2009 Zynga Game Networks
7//
8//
9// Licensed under the Apache License, Version 2.0 (the "License");
10// you may not use this file except in compliance with the License.
11// You may obtain a copy of the License at
12//
13// http://www.apache.org/licenses/LICENSE-2.0
14//
15// Unless required by applicable law or agreed to in writing, software
16// distributed under the License is distributed on an "AS IS" BASIS,
17// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18// See the License for the specific language governing permissions and
19// limitations under the License.
20//
21
22#import "FontLabelStringDrawing.h"
23#import "ZFont.h"
24#import "ZAttributedStringPrivate.h"
25
26@interface ZFont (ZFontPrivate)
27@property (nonatomic, readonly) CGFloat ratio;
28@end
29
30#define kUnicodeHighSurrogateStart 0xD800
31#define kUnicodeHighSurrogateEnd 0xDBFF
32#define kUnicodeHighSurrogateMask kUnicodeHighSurrogateStart
33#define kUnicodeLowSurrogateStart 0xDC00
34#define kUnicodeLowSurrogateEnd 0xDFFF
35#define kUnicodeLowSurrogateMask kUnicodeLowSurrogateStart
36#define kUnicodeSurrogateTypeMask 0xFC00
37#define UnicharIsHighSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeHighSurrogateMask)
38#define UnicharIsLowSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeLowSurrogateMask)
39#define ConvertSurrogatePairToUTF32(high, low) ((UInt32)((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000))
40
41typedef enum {
42 kFontTableFormat4 = 4,
43 kFontTableFormat12 = 12,
44} FontTableFormat;
45
46typedef struct fontTable {
47 NSUInteger retainCount;
48 CFDataRef cmapTable;
49 FontTableFormat format;
50 union {
51 struct {
52 UInt16 segCountX2;
53 UInt16 *endCodes;
54 UInt16 *startCodes;
55 UInt16 *idDeltas;
56 UInt16 *idRangeOffsets;
57 } format4;
58 struct {
59 UInt32 nGroups;
60 struct {
61 UInt32 startCharCode;
62 UInt32 endCharCode;
63 UInt32 startGlyphCode;
64 } *groups;
65 } format12;
66 } cmap;
67} fontTable;
68
69static FontTableFormat supportedFormats[] = { kFontTableFormat4, kFontTableFormat12 };
70static size_t supportedFormatsCount = sizeof(supportedFormats) / sizeof(FontTableFormat);
71
72static fontTable *newFontTable(CFDataRef cmapTable, FontTableFormat format) {
73 fontTable *table = (struct fontTable *)malloc(sizeof(struct fontTable));
74 table->retainCount = 1;
75 table->cmapTable = CFRetain(cmapTable);
76 table->format = format;
77 return table;
78}
79
80static fontTable *retainFontTable(fontTable *table) {
81 if (table != NULL) {
82 table->retainCount++;
83 }
84 return table;
85}
86
87static void releaseFontTable(fontTable *table) {
88 if (table != NULL) {
89 if (table->retainCount <= 1) {
90 CFRelease(table->cmapTable);
91 free(table);
92 } else {
93 table->retainCount--;
94 }
95 }
96}
97
98static const void *fontTableRetainCallback(CFAllocatorRef allocator, const void *value) {
99 return retainFontTable((fontTable *)value);
100}
101
102static void fontTableReleaseCallback(CFAllocatorRef allocator, const void *value) {
103 releaseFontTable((fontTable *)value);
104}
105
106static const CFDictionaryValueCallBacks kFontTableDictionaryValueCallBacks = {
107 .version = 0,
108 .retain = &fontTableRetainCallback,
109 .release = &fontTableReleaseCallback,
110 .copyDescription = NULL,
111 .equal = NULL
112};
113
114// read the cmap table from the font
115// we only know how to understand some of the table formats at the moment
116static fontTable *readFontTableFromCGFont(CGFontRef font) {
117 CFDataRef cmapTable = CGFontCopyTableForTag(font, 'cmap');
118 NSCAssert1(cmapTable != NULL, @"CGFontCopyTableForTag returned NULL for 'cmap' tag in font %@",
119 (font ? [(id)CFCopyDescription(font) autorelease] : @"(null)"));
120 const UInt8 * const bytes = CFDataGetBytePtr(cmapTable);
121 NSCAssert1(OSReadBigInt16(bytes, 0) == 0, @"cmap table for font %@ has bad version number",
122 (font ? [(id)CFCopyDescription(font) autorelease] : @"(null)"));
123 UInt16 numberOfSubtables = OSReadBigInt16(bytes, 2);
124 const UInt8 *unicodeSubtable = NULL;
125 //UInt16 unicodeSubtablePlatformID;
126 UInt16 unicodeSubtablePlatformSpecificID;
127 FontTableFormat unicodeSubtableFormat;
128 const UInt8 * const encodingSubtables = &bytes[4];
129 for (UInt16 i = 0; i < numberOfSubtables; i++) {
130 const UInt8 * const encodingSubtable = &encodingSubtables[8 * i];
131 UInt16 platformID = OSReadBigInt16(encodingSubtable, 0);
132 UInt16 platformSpecificID = OSReadBigInt16(encodingSubtable, 2);
133 // find the best subtable
134 // best is defined by a combination of encoding and format
135 // At the moment we only support format 4, so ignore all other format tables
136 // We prefer platformID == 0, but we will also accept Microsoft's unicode format
137 if (platformID == 0 || (platformID == 3 && platformSpecificID == 1)) {
138 BOOL preferred = NO;
139 if (unicodeSubtable == NULL) {
140 preferred = YES;
141 } else if (platformID == 0 && platformSpecificID > unicodeSubtablePlatformSpecificID) {
142 preferred = YES;
143 }
144 if (preferred) {
145 UInt32 offset = OSReadBigInt32(encodingSubtable, 4);
146 const UInt8 *subtable = &bytes[offset];
147 UInt16 format = OSReadBigInt16(subtable, 0);
148 for (size_t i = 0; i < supportedFormatsCount; i++) {
149 if (format == supportedFormats[i]) {
150 if (format >= 8) {
151 // the version is a fixed-point
152 UInt16 formatFrac = OSReadBigInt16(subtable, 2);
153 if (formatFrac != 0) {
154 // all the current formats with a Fixed version are always *.0
155 continue;
156 }
157 }
158 unicodeSubtable = subtable;
159 //unicodeSubtablePlatformID = platformID;
160 unicodeSubtablePlatformSpecificID = platformSpecificID;
161 unicodeSubtableFormat = format;
162 break;
163 }
164 }
165 }
166 }
167 }
168 fontTable *table = NULL;
169 if (unicodeSubtable != NULL) {
170 table = newFontTable(cmapTable, unicodeSubtableFormat);
171 switch (unicodeSubtableFormat) {
172 case kFontTableFormat4:
173 // subtable format 4
174 //UInt16 length = OSReadBigInt16(unicodeSubtable, 2);
175 //UInt16 language = OSReadBigInt16(unicodeSubtable, 4);
176 table->cmap.format4.segCountX2 = OSReadBigInt16(unicodeSubtable, 6);
177 //UInt16 searchRange = OSReadBigInt16(unicodeSubtable, 8);
178 //UInt16 entrySelector = OSReadBigInt16(unicodeSubtable, 10);
179 //UInt16 rangeShift = OSReadBigInt16(unicodeSubtable, 12);
180 table->cmap.format4.endCodes = (UInt16*)&unicodeSubtable[14];
181 table->cmap.format4.startCodes = (UInt16*)&((UInt8*)table->cmap.format4.endCodes)[table->cmap.format4.segCountX2+2];
182 table->cmap.format4.idDeltas = (UInt16*)&((UInt8*)table->cmap.format4.startCodes)[table->cmap.format4.segCountX2];
183 table->cmap.format4.idRangeOffsets = (UInt16*)&((UInt8*)table->cmap.format4.idDeltas)[table->cmap.format4.segCountX2];
184 //UInt16 *glyphIndexArray = &idRangeOffsets[segCountX2];
185 break;
186 case kFontTableFormat12:
187 table->cmap.format12.nGroups = OSReadBigInt32(unicodeSubtable, 12);
188 table->cmap.format12.groups = (void *)&unicodeSubtable[16];
189 break;
190 default:
191 releaseFontTable(table);
192 table = NULL;
193 }
194 }
195 CFRelease(cmapTable);
196 return table;
197}
198
199// outGlyphs must be at least size n
200static void mapCharactersToGlyphsInFont(const fontTable *table, unichar characters[], size_t charLen, CGGlyph outGlyphs[], size_t *outGlyphLen) {
201 if (table != NULL) {
202 NSUInteger j = 0;
203 switch (table->format) {
204 case kFontTableFormat4: {
205 for (NSUInteger i = 0; i < charLen; i++, j++) {
206 unichar c = characters[i];
207 UInt16 segOffset;
208 BOOL foundSegment = NO;
209 for (segOffset = 0; segOffset < table->cmap.format4.segCountX2; segOffset += 2) {
210 UInt16 endCode = OSReadBigInt16(table->cmap.format4.endCodes, segOffset);
211 if (endCode >= c) {
212 foundSegment = YES;
213 break;
214 }
215 }
216 if (!foundSegment) {
217 // no segment
218 // this is an invalid font
219 outGlyphs[j] = 0;
220 } else {
221 UInt16 startCode = OSReadBigInt16(table->cmap.format4.startCodes, segOffset);
222 if (!(startCode <= c)) {
223 // the code falls in a hole between segments
224 outGlyphs[j] = 0;
225 } else {
226 UInt16 idRangeOffset = OSReadBigInt16(table->cmap.format4.idRangeOffsets, segOffset);
227 if (idRangeOffset == 0) {
228 UInt16 idDelta = OSReadBigInt16(table->cmap.format4.idDeltas, segOffset);
229 outGlyphs[j] = (c + idDelta) % 65536;
230 } else {
231 // use the glyphIndexArray
232 UInt16 glyphOffset = idRangeOffset + 2 * (c - startCode);
233 outGlyphs[j] = OSReadBigInt16(&((UInt8*)table->cmap.format4.idRangeOffsets)[segOffset], glyphOffset);
234 }
235 }
236 }
237 }
238 break;
239 }
240 case kFontTableFormat12: {
241 UInt32 lastSegment = UINT32_MAX;
242 for (NSUInteger i = 0; i < charLen; i++, j++) {
243 unichar c = characters[i];
244 UInt32 c32 = c;
245 if (UnicharIsHighSurrogate(c)) {
246 if (i+1 < charLen) { // do we have another character after this one?
247 unichar cc = characters[i+1];
248 if (UnicharIsLowSurrogate(cc)) {
249 c32 = ConvertSurrogatePairToUTF32(c, cc);
250 i++;
251 }
252 }
253 }
254 // Start the heuristic search
255 // If this is an ASCII char, just do a linear search
256 // Otherwise do a hinted, modified binary search
257 // Start the first pivot at the last range found
258 // And when moving the pivot, limit the movement by increasing
259 // powers of two. This should help with locality
260 __typeof__(table->cmap.format12.groups[0]) *foundGroup = NULL;
261 if (c32 <= 0x7F) {
262 // ASCII
263 for (UInt32 idx = 0; idx < table->cmap.format12.nGroups; idx++) {
264 __typeof__(table->cmap.format12.groups[idx]) *group = &table->cmap.format12.groups[idx];
265 if (c32 < OSSwapBigToHostInt32(group->startCharCode)) {
266 // we've fallen into a hole
267 break;
268 } else if (c32 <= OSSwapBigToHostInt32(group->endCharCode)) {
269 // this is the range
270 foundGroup = group;
271 break;
272 }
273 }
274 } else {
275 // heuristic search
276 UInt32 maxJump = (lastSegment == UINT32_MAX ? UINT32_MAX / 2 : 8);
277 UInt32 lowIdx = 0, highIdx = table->cmap.format12.nGroups; // highIdx is the first invalid idx
278 UInt32 pivot = (lastSegment == UINT32_MAX ? lowIdx + (highIdx - lowIdx) / 2 : lastSegment);
279 while (highIdx > lowIdx) {
280 __typeof__(table->cmap.format12.groups[pivot]) *group = &table->cmap.format12.groups[pivot];
281 if (c32 < OSSwapBigToHostInt32(group->startCharCode)) {
282 highIdx = pivot;
283 } else if (c32 > OSSwapBigToHostInt32(group->endCharCode)) {
284 lowIdx = pivot + 1;
285 } else {
286 // we've hit the range
287 foundGroup = group;
288 break;
289 }
290 if (highIdx - lowIdx > maxJump * 2) {
291 if (highIdx == pivot) {
292 pivot -= maxJump;
293 } else {
294 pivot += maxJump;
295 }
296 maxJump *= 2;
297 } else {
298 pivot = lowIdx + (highIdx - lowIdx) / 2;
299 }
300 }
301 if (foundGroup != NULL) lastSegment = pivot;
302 }
303 if (foundGroup == NULL) {
304 outGlyphs[j] = 0;
305 } else {
306 outGlyphs[j] = (CGGlyph)(OSSwapBigToHostInt32(foundGroup->startGlyphCode) +
307 (c32 - OSSwapBigToHostInt32(foundGroup->startCharCode)));
308 }
309 }
310 break;
311 }
312 }
313 if (outGlyphLen != NULL) *outGlyphLen = j;
314 } else {
315 // we have no table, so just null out the glyphs
316 bzero(outGlyphs, charLen*sizeof(CGGlyph));
317 if (outGlyphLen != NULL) *outGlyphLen = 0;
318 }
319}
320
321static BOOL mapGlyphsToAdvancesInFont(ZFont *font, size_t n, CGGlyph glyphs[], CGFloat outAdvances[]) {
322 int advances[n];
323 if (CGFontGetGlyphAdvances(font.cgFont, glyphs, n, advances)) {
324 CGFloat ratio = font.ratio;
325
326 for (size_t i = 0; i < n; i++) {
327 outAdvances[i] = advances[i]*ratio;
328 }
329 return YES;
330 } else {
331 bzero(outAdvances, n*sizeof(CGFloat));
332 }
333 return NO;
334}
335
336static id getValueOrDefaultForRun(ZAttributeRun *run, NSString *key) {
337 id value = [run.attributes objectForKey:key];
338 if (value == nil) {
339 static NSDictionary *defaultValues = nil;
340 if (defaultValues == nil) {
341 defaultValues = [[NSDictionary alloc] initWithObjectsAndKeys:
342 [ZFont fontWithUIFont:[UIFont systemFontOfSize:12]], ZFontAttributeName,
343 [UIColor blackColor], ZForegroundColorAttributeName,
344 [UIColor clearColor], ZBackgroundColorAttributeName,
345 [NSNumber numberWithInt:ZUnderlineStyleNone], ZUnderlineStyleAttributeName,
346 nil];
347 }
348 value = [defaultValues objectForKey:key];
349 }
350 return value;
351}
352
353static void readRunInformation(NSArray *attributes, NSUInteger len, CFMutableDictionaryRef fontTableMap,
354 NSUInteger index, ZAttributeRun **currentRun, NSUInteger *nextRunStart,
355 ZFont **currentFont, fontTable **currentTable) {
356 *currentRun = [attributes objectAtIndex:index];
357 *nextRunStart = ([attributes count] > index+1 ? [[attributes objectAtIndex:index+1] index] : len);
358 *currentFont = getValueOrDefaultForRun(*currentRun, ZFontAttributeName);
359 if (!CFDictionaryGetValueIfPresent(fontTableMap, (*currentFont).cgFont, (const void **)currentTable)) {
360 *currentTable = readFontTableFromCGFont((*currentFont).cgFont);
361 CFDictionarySetValue(fontTableMap, (*currentFont).cgFont, *currentTable);
362 releaseFontTable(*currentTable);
363 }
364}
365
366static CGSize drawOrSizeTextConstrainedToSize(BOOL performDraw, NSString *string, NSArray *attributes, CGSize constrainedSize, NSUInteger maxLines,
367 UILineBreakMode lineBreakMode, UITextAlignment alignment, BOOL ignoreColor) {
368 NSUInteger len = [string length];
369 NSUInteger idx = 0;
370 CGPoint drawPoint = CGPointZero;
371 CGSize retValue = CGSizeZero;
372 CGContextRef ctx = (performDraw ? UIGraphicsGetCurrentContext() : NULL);
373
374 BOOL convertNewlines = (maxLines == 1);
375
376 // Extract the characters from the string
377 // Convert newlines to spaces if necessary
378 unichar *characters = (unichar *)malloc(sizeof(unichar) * len);
379 if (convertNewlines) {
380 NSCharacterSet *charset = [NSCharacterSet newlineCharacterSet];
381 NSRange range = NSMakeRange(0, len);
382 size_t cIdx = 0;
383 while (range.length > 0) {
384 NSRange newlineRange = [string rangeOfCharacterFromSet:charset options:0 range:range];
385 if (newlineRange.location == NSNotFound) {
386 [string getCharacters:&characters[cIdx] range:range];
387 cIdx += range.length;
388 break;
389 } else {
390 NSUInteger delta = newlineRange.location - range.location;
391 if (newlineRange.location > range.location) {
392 [string getCharacters:&characters[cIdx] range:NSMakeRange(range.location, delta)];
393 }
394 cIdx += delta;
395 characters[cIdx] = (unichar)' ';
396 cIdx++;
397 delta += newlineRange.length;
398 range.location += delta, range.length -= delta;
399 if (newlineRange.length == 1 && range.length >= 1 &&
400 [string characterAtIndex:newlineRange.location] == (unichar)'\r' &&
401 [string characterAtIndex:range.location] == (unichar)'\n') {
402 // CRLF sequence, skip the LF
403 range.location += 1, range.length -= 1;
404 }
405 }
406 }
407 len = cIdx;
408 } else {
409 [string getCharacters:characters range:NSMakeRange(0, len)];
410 }
411
412 // Create storage for glyphs and advances
413 CGGlyph *glyphs;
414 CGFloat *advances;
415 {
416 NSUInteger maxRunLength = 0;
417 ZAttributeRun *a = [attributes objectAtIndex:0];
418 for (NSUInteger i = 1; i < [attributes count]; i++) {
419 ZAttributeRun *b = [attributes objectAtIndex:i];
420 maxRunLength = MAX(maxRunLength, b.index - a.index);
421 a = b;
422 }
423 maxRunLength = MAX(maxRunLength, len - a.index);
424 maxRunLength++; // for a potential ellipsis
425 glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * maxRunLength);
426 advances = (CGFloat *)malloc(sizeof(CGFloat) * maxRunLength);
427 }
428
429 // Use this table to cache all fontTable objects
430 CFMutableDictionaryRef fontTableMap = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks,
431 &kFontTableDictionaryValueCallBacks);
432
433 // Fetch initial style values
434 NSUInteger currentRunIdx = 0;
435 ZAttributeRun *currentRun;
436 NSUInteger nextRunStart;
437 ZFont *currentFont;
438 fontTable *currentTable;
439
440#define READ_RUN() readRunInformation(attributes, len, fontTableMap, \
441 currentRunIdx, &currentRun, &nextRunStart, \
442 &currentFont, &currentTable)
443
444 READ_RUN();
445
446 // fetch the glyphs for the first run
447 size_t glyphCount;
448 NSUInteger glyphIdx;
449
450#define READ_GLYPHS() do { \
451 mapCharactersToGlyphsInFont(currentTable, &characters[currentRun.index], (nextRunStart - currentRun.index), glyphs, &glyphCount); \
452 mapGlyphsToAdvancesInFont(currentFont, (nextRunStart - currentRun.index), glyphs, advances); \
453 glyphIdx = 0; \
454 } while (0)
455
456 READ_GLYPHS();
457
458 NSMutableCharacterSet *alphaCharset = [NSMutableCharacterSet alphanumericCharacterSet];
459 [alphaCharset addCharactersInString:@"([{'\"\u2019\u02BC"];
460
461 // scan left-to-right looking for newlines or until we hit the width constraint
462 // When we hit a wrapping point, calculate truncation as follows:
463 // If we have room to draw at least one more character on the next line, no truncation
464 // Otherwise apply the truncation algorithm to the current line.
465 // After calculating any truncation, draw.
466 // Each time we hit the end of an attribute run, calculate the new font and make sure
467 // it fits (vertically) within the size constraint. If not, truncate this line.
468 // When we draw, iterate over the attribute runs for this line and draw each run separately
469 BOOL lastLine = NO; // used to indicate truncation and to stop the iterating
470 NSUInteger lineCount = 1;
471 while (idx < len && !lastLine) {
472 if (maxLines > 0 && lineCount == maxLines) {
473 lastLine = YES;
474 }
475 // scan left-to-right
476 struct {
477 NSUInteger index;
478 NSUInteger glyphIndex;
479 NSUInteger currentRunIdx;
480 } indexCache = { idx, glyphIdx, currentRunIdx };
481 CGSize lineSize = CGSizeMake(0, currentFont.leading);
482 CGFloat lineAscender = currentFont.ascender;
483 struct {
484 NSUInteger index;
485 NSUInteger glyphIndex;
486 NSUInteger currentRunIdx;
487 CGSize lineSize;
488 } lastWrapCache = {0, 0, 0, CGSizeZero};
489 BOOL inAlpha = NO; // used for calculating wrap points
490
491 BOOL finishLine = NO;
492 for (;idx <= len && !finishLine;) {
493 NSUInteger skipCount = 0;
494 if (idx == len) {
495 finishLine = YES;
496 lastLine = YES;
497 } else {
498 if (idx >= nextRunStart) {
499 // cycle the font and table and grab the next set of glyphs
500 do {
501 currentRunIdx++;
502 READ_RUN();
503 } while (idx >= nextRunStart);
504 READ_GLYPHS();
505 // re-scan the characters to synchronize the glyph index
506 for (NSUInteger j = currentRun.index; j < idx; j++) {
507 if (UnicharIsHighSurrogate(characters[j]) && j+1<len && UnicharIsLowSurrogate(characters[j+1])) {
508 j++;
509 }
510 glyphIdx++;
511 }
512 if (currentFont.leading > lineSize.height) {
513 lineSize.height = currentFont.leading;
514 if (retValue.height + currentFont.ascender > constrainedSize.height) {
515 lastLine = YES;
516 finishLine = YES;
517 }
518 }
519 lineAscender = MAX(lineAscender, currentFont.ascender);
520 }
521 unichar c = characters[idx];
522 // Mark a wrap point before spaces and after any stretch of non-alpha characters
523 BOOL markWrap = NO;
524 if (c == (unichar)' ') {
525 markWrap = YES;
526 } else if ([alphaCharset characterIsMember:c]) {
527 if (!inAlpha) {
528 markWrap = YES;
529 inAlpha = YES;
530 }
531 } else {
532 inAlpha = NO;
533 }
534 if (markWrap) {
535 lastWrapCache = (__typeof__(lastWrapCache)){
536 .index = idx,
537 .glyphIndex = glyphIdx,
538 .currentRunIdx = currentRunIdx,
539 .lineSize = lineSize
540 };
541 }
542 // process the line
543 if (c == (unichar)'\n' || c == 0x0085) { // U+0085 is the NEXT_LINE unicode character
544 finishLine = YES;
545 skipCount = 1;
546 } else if (c == (unichar)'\r') {
547 finishLine = YES;
548 // check for CRLF
549 if (idx+1 < len && characters[idx+1] == (unichar)'\n') {
550 skipCount = 2;
551 } else {
552 skipCount = 1;
553 }
554 } else if (lineSize.width + advances[glyphIdx] > constrainedSize.width) {
555 finishLine = YES;
556 if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) {
557 lastLine = YES;
558 }
559 // walk backwards if wrapping is necessary
560 if (lastWrapCache.index > indexCache.index && lineBreakMode != UILineBreakModeCharacterWrap &&
561 (!lastLine || lineBreakMode != UILineBreakModeClip)) {
562 // we're doing some sort of word wrapping
563 idx = lastWrapCache.index;
564 lineSize = lastWrapCache.lineSize;
565 if (!lastLine) {
566 // re-check if this is the last line
567 if (lastWrapCache.currentRunIdx != currentRunIdx) {
568 currentRunIdx = lastWrapCache.currentRunIdx;
569 READ_RUN();
570 READ_GLYPHS();
571 }
572 if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) {
573 lastLine = YES;
574 }
575 }
576 glyphIdx = lastWrapCache.glyphIndex;
577 // skip any spaces
578 for (NSUInteger j = idx; j < len && characters[j] == (unichar)' '; j++) {
579 skipCount++;
580 }
581 }
582 }
583 }
584 if (finishLine) {
585 // TODO: support head/middle truncation
586 if (lastLine && idx < len && lineBreakMode == UILineBreakModeTailTruncation) {
587 // truncate
588 unichar ellipsis = 0x2026; // ellipsis (…)
589 CGGlyph ellipsisGlyph;
590 mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL);
591 CGFloat ellipsisWidth;
592 mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth);
593 while ((idx - indexCache.index) > 1 && lineSize.width + ellipsisWidth > constrainedSize.width) {
594 // we have more than 1 character and we're too wide, so back up
595 idx--;
596 if (UnicharIsHighSurrogate(characters[idx]) && UnicharIsLowSurrogate(characters[idx+1])) {
597 idx--;
598 }
599 if (idx < currentRun.index) {
600 ZFont *oldFont = currentFont;
601 do {
602 currentRunIdx--;
603 READ_RUN();
604 } while (idx < currentRun.index);
605 READ_GLYPHS();
606 glyphIdx = glyphCount-1;
607 if (oldFont != currentFont) {
608 mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL);
609 mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth);
610 }
611 } else {
612 glyphIdx--;
613 }
614 lineSize.width -= advances[glyphIdx];
615 }
616 // skip any spaces before truncating
617 while ((idx - indexCache.index) > 1 && characters[idx-1] == (unichar)' ') {
618 idx--;
619 if (idx < currentRun.index) {
620 currentRunIdx--;
621 READ_RUN();
622 READ_GLYPHS();
623 glyphIdx = glyphCount-1;
624 } else {
625 glyphIdx--;
626 }
627 lineSize.width -= advances[glyphIdx];
628 }
629 lineSize.width += ellipsisWidth;
630 glyphs[glyphIdx] = ellipsisGlyph;
631 idx++;
632 glyphIdx++;
633 }
634 retValue.width = MAX(retValue.width, lineSize.width);
635 retValue.height += lineSize.height;
636
637 // draw
638 if (performDraw) {
639 switch (alignment) {
640 case UITextAlignmentLeft:
641 drawPoint.x = 0;
642 break;
643 case UITextAlignmentCenter:
644 drawPoint.x = (constrainedSize.width - lineSize.width) / 2.0f;
645 break;
646 case UITextAlignmentRight:
647 drawPoint.x = constrainedSize.width - lineSize.width;
648 break;
649 }
650 NSUInteger stopGlyphIdx = glyphIdx;
651 NSUInteger lastRunIdx = currentRunIdx;
652 NSUInteger stopCharIdx = idx;
653 idx = indexCache.index;
654 if (currentRunIdx != indexCache.currentRunIdx) {
655 currentRunIdx = indexCache.currentRunIdx;
656 READ_RUN();
657 READ_GLYPHS();
658 }
659 glyphIdx = indexCache.glyphIndex;
660 for (NSUInteger drawIdx = currentRunIdx; drawIdx <= lastRunIdx; drawIdx++) {
661 if (drawIdx != currentRunIdx) {
662 currentRunIdx = drawIdx;
663 READ_RUN();
664 READ_GLYPHS();
665 }
666 NSUInteger numGlyphs;
667 if (drawIdx == lastRunIdx) {
668 numGlyphs = stopGlyphIdx - glyphIdx;
669 idx = stopCharIdx;
670 } else {
671 numGlyphs = glyphCount - glyphIdx;
672 idx = nextRunStart;
673 }
674 CGContextSetFont(ctx, currentFont.cgFont);
675 CGContextSetFontSize(ctx, currentFont.pointSize);
676 // calculate the fragment size
677 CGFloat fragmentWidth = 0;
678 for (NSUInteger g = 0; g < numGlyphs; g++) {
679 fragmentWidth += advances[glyphIdx + g];
680 }
681
682 if (!ignoreColor) {
683 UIColor *foregroundColor = getValueOrDefaultForRun(currentRun, ZForegroundColorAttributeName);
684 UIColor *backgroundColor = getValueOrDefaultForRun(currentRun, ZBackgroundColorAttributeName);
685 if (backgroundColor != nil && ![backgroundColor isEqual:[UIColor clearColor]]) {
686 [backgroundColor setFill];
687 UIRectFillUsingBlendMode((CGRect){ drawPoint, { fragmentWidth, lineSize.height } }, kCGBlendModeNormal);
688 }
689 [foregroundColor setFill];
690 }
691
692 CGContextShowGlyphsAtPoint(ctx, drawPoint.x, drawPoint.y + lineAscender, &glyphs[glyphIdx], numGlyphs);
693 NSNumber *underlineStyle = getValueOrDefaultForRun(currentRun, ZUnderlineStyleAttributeName);
694 if ([underlineStyle integerValue] & ZUnderlineStyleMask) {
695 // we only support single for the time being
696 UIRectFill(CGRectMake(drawPoint.x, drawPoint.y + lineAscender, fragmentWidth, 1));
697 }
698 drawPoint.x += fragmentWidth;
699 glyphIdx += numGlyphs;
700 }
701 drawPoint.y += lineSize.height;
702 }
703 idx += skipCount;
704 glyphIdx += skipCount;
705 lineCount++;
706 } else {
707 lineSize.width += advances[glyphIdx];
708 glyphIdx++;
709 idx++;
710 if (idx < len && UnicharIsHighSurrogate(characters[idx-1]) && UnicharIsLowSurrogate(characters[idx])) {
711 // skip the second half of the surrogate pair
712 idx++;
713 }
714 }
715 }
716 }
717 CFRelease(fontTableMap);
718 free(glyphs);
719 free(advances);
720 free(characters);
721
722#undef READ_GLYPHS
723#undef READ_RUN
724
725 return retValue;
726}
727
728static NSArray *attributeRunForFont(ZFont *font) {
729 return [NSArray arrayWithObject:[ZAttributeRun attributeRunWithIndex:0
730 attributes:[NSDictionary dictionaryWithObject:font
731 forKey:ZFontAttributeName]]];
732}
733
734static CGSize drawTextInRect(CGRect rect, NSString *text, NSArray *attributes, UILineBreakMode lineBreakMode,
735 UITextAlignment alignment, NSUInteger numberOfLines, BOOL ignoreColor) {
736 CGContextRef ctx = UIGraphicsGetCurrentContext();
737
738 CGContextSaveGState(ctx);
739
740 // flip it upside-down because our 0,0 is upper-left, whereas ttfs are for screens where 0,0 is lower-left
741 CGAffineTransform textTransform = CGAffineTransformMake(1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f);
742 CGContextSetTextMatrix(ctx, textTransform);
743
744 CGContextTranslateCTM(ctx, rect.origin.x, rect.origin.y);
745
746 CGContextSetTextDrawingMode(ctx, kCGTextFill);
747 CGSize size = drawOrSizeTextConstrainedToSize(YES, text, attributes, rect.size, numberOfLines, lineBreakMode, alignment, ignoreColor);
748
749 CGContextRestoreGState(ctx);
750
751 return size;
752}
753
754@implementation NSString (FontLabelStringDrawing)
755// CGFontRef-based methods
756- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
757 return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize]];
758}
759
760- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size {
761 return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size];
762}
763
764- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size
765 lineBreakMode:(UILineBreakMode)lineBreakMode {
766 return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size lineBreakMode:lineBreakMode];
767}
768
769- (CGSize)drawAtPoint:(CGPoint)point withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
770 return [self drawAtPoint:point withZFont:[ZFont fontWithCGFont:font size:pointSize]];
771}
772
773- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
774 return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize]];
775}
776
777- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize lineBreakMode:(UILineBreakMode)lineBreakMode {
778 return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode];
779}
780
781- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize
782 lineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment {
783 return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode alignment:alignment];
784}
785
786// ZFont-based methods
787- (CGSize)sizeWithZFont:(ZFont *)font {
788 CGSize size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1,
789 UILineBreakModeClip, UITextAlignmentLeft, YES);
790 return CGSizeMake(ceilf(size.width), ceilf(size.height));
791}
792
793- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size {
794 return [self sizeWithZFont:font constrainedToSize:size lineBreakMode:UILineBreakModeWordWrap];
795}
796
797/*
798 According to experimentation with UIStringDrawing, this can actually return a CGSize whose height is greater
799 than the one passed in. The two cases are as follows:
800 1. If the given size parameter's height is smaller than a single line, the returned value will
801 be the height of one line.
802 2. If the given size parameter's height falls between multiples of a line height, and the wrapped string
803 actually extends past the size.height, and the difference between size.height and the previous multiple
804 of a line height is >= the font's ascender, then the returned size's height is extended to the next line.
805 To put it simply, if the baseline point of a given line falls in the given size, the entire line will
806 be present in the output size.
807 */
808- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode {
809 size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, 0, lineBreakMode, UITextAlignmentLeft, YES);
810 return CGSizeMake(ceilf(size.width), ceilf(size.height));
811}
812
813- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
814 numberOfLines:(NSUInteger)numberOfLines {
815 size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, numberOfLines, lineBreakMode, UITextAlignmentLeft, YES);
816 return CGSizeMake(ceilf(size.width), ceilf(size.height));
817}
818
819- (CGSize)drawAtPoint:(CGPoint)point withZFont:(ZFont *)font {
820 return [self drawAtPoint:point forWidth:CGFLOAT_MAX withZFont:font lineBreakMode:UILineBreakModeClip];
821}
822
823- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode {
824 return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self, attributeRunForFont(font), lineBreakMode, UITextAlignmentLeft, 1, YES);
825}
826
827- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font {
828 return [self drawInRect:rect withZFont:font lineBreakMode:UILineBreakModeWordWrap];
829}
830
831- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode {
832 return [self drawInRect:rect withZFont:font lineBreakMode:lineBreakMode alignment:UITextAlignmentLeft];
833}
834
835- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
836 alignment:(UITextAlignment)alignment {
837 return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, 0, YES);
838}
839
840- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
841 alignment:(UITextAlignment)alignment numberOfLines:(NSUInteger)numberOfLines {
842 return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, numberOfLines, YES);
843}
844@end
845
846@implementation ZAttributedString (ZAttributedStringDrawing)
847- (CGSize)size {
848 CGSize size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1,
849 UILineBreakModeClip, UITextAlignmentLeft, NO);
850 return CGSizeMake(ceilf(size.width), ceilf(size.height));
851}
852
853- (CGSize)sizeConstrainedToSize:(CGSize)size {
854 return [self sizeConstrainedToSize:size lineBreakMode:UILineBreakModeWordWrap];
855}
856
857- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode {
858 size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, 0, lineBreakMode, UITextAlignmentLeft, NO);
859 return CGSizeMake(ceilf(size.width), ceilf(size.height));
860}
861
862- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
863 numberOfLines:(NSUInteger)numberOfLines {
864 size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, numberOfLines, lineBreakMode, UITextAlignmentLeft, NO);
865 return CGSizeMake(ceilf(size.width), ceilf(size.height));
866}
867
868- (CGSize)drawAtPoint:(CGPoint)point {
869 return [self drawAtPoint:point forWidth:CGFLOAT_MAX lineBreakMode:UILineBreakModeClip];
870}
871
872- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode {
873 return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self.string, self.attributes, lineBreakMode, UITextAlignmentLeft, 1, NO);
874}
875
876- (CGSize)drawInRect:(CGRect)rect {
877 return [self drawInRect:rect withLineBreakMode:UILineBreakModeWordWrap];
878}
879
880- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode {
881 return [self drawInRect:rect withLineBreakMode:lineBreakMode alignment:UITextAlignmentLeft];
882}
883
884- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment {
885 return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, 0, NO);
886}
887
888- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment
889 numberOfLines:(NSUInteger)numberOfLines {
890 return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, numberOfLines, NO);
891}
892@end