底层文本渲染
iPhone 上的低级别文本渲染。
引言
我需要在我的应用程序中定位单个文本字符,这比我想象的要复杂得多。所以,我将它记录下来,以便后人参考。
但我想稍微放宽视角。真正重要的是掌握处理新技术的通用方法,而不是了解如何执行某个特定技巧。
如今,当我想要在新应用中添加新功能时,我首先会从一个干净的开始,然后在上面实现它。这就像周一早晨在干净的书桌上工作,或者维护一个干净的工具棚。
我甚至制作了自己的干净启动模板 -- CreatingXcodeProject.aspx。我最终对模板进行了一些改进 -- 你可以在这里下载新版本:1prepgroundiphonedev.aspx。
我所做的修改包括
- 整理视图控制器文件,以便
iVar
与其 getter 方法不共享相同的名称(我在我的入门文章中解释了为什么这是一种糟糕的做法:1prepgroundiphonedev.aspx)。 - 改进测试图像,并将其提取为一个方法。这是一个非常实用的方法 -- 几乎每次我创建位图、在视图内定位视图,或者将图层附加到图层上,并且我使用了变换时,我都无法第一次就做到正确 -- 所以我需要知道我离正确还有多远。使用我原来的测试图像,有时图像会完全超出屏幕,这毫无帮助。所以这个图像会从 0,0 发射径向射线到某个很远的距离,比如 10,000;我使用了细窄的楔形 -- 仅仅绘制线条会更混乱,因为你必须指定线条粗细,而且你并不一定知道一个单位代表多少像素,所以你不能简单地说
CGContextSetStrokeWidth(X, 1.0)
,因为这可能太小以至于看不见,或者太大以至于填满屏幕。
总之 -- 使用这个方案,你只需查看屏幕上的内容就可以推断出原点。此外,颜色从红色变为蓝色,当我们从正 X 轴逆时针方向前进时。所以我们可以推断方向。灰底上的白圆给我们提供了边界。原点、方向、边界。搞定!
我实现了一个函数,可以转换上下文的坐标系,给出一个笛卡尔坐标系,其中 0, 0 在中心,1, 1 在右上角。就像应该的那样!
所以,我使用这个模板创建一个新项目,然后我就可以直接在视图的 drawRect
中放入代码 -- 坐标系已经按照上述方式设置好了。
将文本显示在屏幕上不是问题 -- 我可以简单地设置
[self drawTestShapeToContext: X
inRect: wholeRect
withText: YES ];
… 然后模板就会在屏幕上显示一些文本。它的实现方式是这样的
if (showText)
{
CGContextSaveGState(X);
CGAffineTransform tmX = CGContextGetTextMatrix(X);
char* text = "Hello World!";
CGContextSelectFont(X, "Helvetica Bold",
rect.size.width / 13.0, kCGEncodingMacRoman);
CGContextSetTextDrawingMode(X, kCGTextFill);
CGContextSetRGBFillColor(X, 0.1f, 0.3f, 0.8f, 1.0f);
//CGAffineTransform xform = CGAffineTransformMake(
// 1.0f, 0.0f,
// 0.0f, -1.0f,
// 0.0f, 0.0f );
//CGContextSetTextMatrix(X, xform);
CGContextShowTextAtPoint(X, centre.x, centre.y, text, strlen(text));
// Watch out! TextMatrix doesn't get restored
// when you restore the graphics context!
CGContextRestoreGState(X);
CGContextSetTextMatrix(X, tmX);
}
(注释掉的变换,因为我自己翻转了坐标系 -- 如果你不这样做,文本会显示为倒置的)。
但这对我来说毫无用处。我需要更精细地控制。我需要知道每个字形的边界。我真正想要的是每个字形的 CGPath
。如果你不了解 CGPath
,可以想象 PDF。将字形(以及所有可能的情况)表示为一系列几何移动;MoveTo(100,100)
然后绘制一个以 (50,50) 为中心,半径为 50,从 theta = pi / 2 到 pi / 2 的弧……等等,这样结果就是设备无关的。
我通常会先浏览 Apple 的帮助文档。
第一部分 -- 与 Apple 帮助文档的艰难斗争
接下来的部分是关于使用 Apple 的帮助文档。这并不愉快。如果你想直接跳到解决方案,请向下滚动。
那么,下一步是什么?嗯,如果我能直接 Alt+双击 CGContextShowTextAtPoint
,然后帮助文档告诉我“抱歉,Core Graphics 没有你想要的功能 -- 你想要的是 Core Text”。然后我点击“Core Text”并得到答案,那就太好了。但 Apple 的帮助文档并不那么智能。
对我来说,这每次都是一次糟糕的体验。即使是再次找到昨天看过的东西,也可能是一项艰巨的任务。
帮助 -> 开发者文档 -> 主页图标 -> iOS 4.0 开发者库。
如果我向下滚动,我可以看到左窗格中有“Core Text”。所以我点击它。我甚至不知道 Core Text 是什么,但听起来像是正确的。
现在我可以在右侧面板看到“Core Text 编程指南”,所以我点击它。现在我查看状态栏,它显示
iPhone OS 4.0 库 -> 数据管理 -> 字符串、文本和字体 -> Core Text 编程指南 -> 简介
所以,我双击“字符串、文本和字体”(我不想在不了解所有其他选项的情况下阅读 Core Text 指南):“未找到文档”。太棒了 -- 我的帮助文档坏了。所以我必须在线从这一点继续。
在 Google 中搜索“Core Text 编程指南”,得到相同的页面。
查看其状态栏,这与我的本地帮助文档所说的完全不一致。这里我看到
iPhone 开发中心 -> iOS 参考库 -> 框架 -> 媒体层 -> Core Text
这似乎缺少区分不同可用技术的级别,而这正是我想要的。
还有一个警告说,这些页面并未与 iOS 3.2 同步。嗯,在写作之时,iOS 的当前版本是 4.0。所以即使是告知信息过时的警告本身也已经过时了。
总之,我可以通过简介段落中的链接找到它,这会把我带到这里
太棒了!终于到了关键部分了。
我并不是愤怒地批评 Apple。我只是指出,在你光滑的金属黑色完美禅意矩形背后,有很多人正在拼命努力地团结起来。帮助文档误导性地带有相同的光泽表层,让你觉得它是某个大师的作品,而你,一个普通白痴,却因为自己的愚蠢而挣扎。
获得一些视角很重要。你需要从零开始,也就是现实。而现实是,帮助文档一团糟。也许这是好事 -- 也许这意味着他们专注于编写高质量的代码。我宁愿代码质量高但文档糟糕,也不愿相反。最有可能的是,他们最聪明的头脑没有浪费在修补帮助系统上。
总之,让我们看看那个链接。在一切变成“blah blah blah BLAH”之前,你能看到多远?
技术文档和说明手册总是会削弱我求生的意志。为什么世界会这样阴谋论!
当然,如果你已经理解了,这一切都说得通。但对于新手来说,这是一个噩梦。一切都以错误的顺序呈现。问题是,这些帮助文件是由那些对材料非常熟悉的人编写的,他们已经忘记了学习它的感觉 -- 他们已经忘记了第一个问题应该是什么。
我想深入研究代码。这一切都太含糊了。我想看到一些代码!你可能也想……
好的,长话短说,我最终通过混合使用“core-text sample”的 Google 搜索和在 IRC 上提问,找到了一些 Apple 的示例代码 -- 在这个示例中 Alt+双击各种函数,会弹出链接到其他几个示例的帮助页面。
我喜欢将此类链接保存在项目顶部
- http://cocoawithlove.com/2009/09/creating-alpha-masks-from-text-on.html
- http://developer.apple.com/library/ios/#documentation/UIKit/ Reference/NSString_UIKit_Additions/Reference/Reference.html
- http://developer.apple.com/library/mac/#samplecode/CoreTextArcCocoa/Introduction/ Intro.html
- http://developer.apple.com/library/mac/#samplecode/CoreAnimationText/Introduction/ Intro.html
这样你就可以直接从 Xcode 中打开它们。
好了,这就是互联网上提供的所有示例代码了。
我需要在 IRC 上获得一些帮助,以便将 CoreTextArcCocoa
从 OSX 转换为 iOS。虽然它能做到我需要的东西(环绕文本),但它的实现方式一点也不透明。所以我只是不断地剥离它,将辅助函数移到主体内,裁剪和修剪,直到我得到了在屏幕上显示字形所需的最低限度。
CoreAnimationText
写得非常好 -- 并且包含一句非常有趣的台词
path = CTFontCreatePathForGlyph(font, glyph, NULL);
太棒了!让我们一起加油!
第二部分:如何在屏幕上显示字形
从一个 C 风格的字符串开始,比如 'hello world'。
现在,一个富文本字符串就像 'hello world',其中 'hello' 是 Times New Roman 14 号斜体,而 ' world' 是 Courier 下划线。基本上就是富文本,还记得 .RTF 吗? Microsoft WordPad?
好的,假设你现在有了你的 attrString
(稍后我们将讨论如何从字符串创建它)。以下是如何逐个字形地渲染它
CTLineRef line = CTLineCreateWithAttributedString( attStr ) ;
CFArrayRef runArray = CTLineGetGlyphRuns(line);
// for each RUN
for (CFIndex runIndex = 0; runIndex < CFArrayGetCount(runArray); runIndex++)
{
// Get FONT for this run
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
CTFontRef runFont =
CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);
// for each GLYPH in run
for (CFIndex runGlyphIndex = 0;
runGlyphIndex < CTRunGetGlyphCount(run); runGlyphIndex++)
{
// get Glyph & Glyph-data
CFRange thisGlyphRange = CFRangeMake(runGlyphIndex, 1);
CGGlyph glyph;
CGPoint position;
CTRunGetGlyphs(run, thisGlyphRange, &glyph);
CTRunGetPositions(run, thisGlyphRange, &position);
// Render it
{
CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL);
CGAffineTransform textMatrix = CTRunGetTextMatrix(run);
CGContextSetTextMatrix(X, textMatrix);
CGContextSetFont(X, cgFont);
CGContextSetFontSize(X, CTFontGetSize(runFont));
CGContextSetRGBFillColor(X, 1.0, 1.0, 1.0, 0.5);
CGContextShowGlyphsAtPositions(X, &glyph, &position, 1);
CFRelease(cgFont);
}
// Get PATH of outline & stroke outline
{
CGPathRef path = CTFontCreatePathForGlyph(runFont, glyph, NULL);
CGMutablePathRef pT = CGPathCreateMutable();
CGAffineTransform T =
CGAffineTransformMakeTranslation(position.x, position.y);
CGPathAddPath(pT, &T, path);
CGContextAddPath(X, pT);
CGContextSetStrokeColorWithColor(X, [UIColor yellowColor].CGColor);
CGContextSetLineWidth(X, atLeastOnePixel);
CGContextStrokePath(X);
CGPathRelease(path);
CGPathRelease(pT);
}
// draw blue bounding box
{
CGRect glyphRect = CTRunGetImageBounds(run, X, thisGlyphRange);
CGContextSetLineWidth(X, atLeastOnePixel);
CGContextSetStrokeColorWithColor(X, [UIColor blueColor ].CGColor);
CGContextStrokeRect(X, glyphRect);
}
// release things
}
}
CFRelease(line);
在之前的例子中,假设 'hello world' 是我们的富文本字符串。我们将其转换为一个 CTLine
。会有一个打印 CTLine
的函数,但目前,我们继续深入。
我们发现它由两个 CTRun
组成。'hello' 和 ' world' 显然,一个 run 是一块具有相同属性的文本。
所以对于每个 run,我们逐个字形地进行。
对于每个字形,我们获取它的 CPath
。我们可以对此进行很多有趣的实验。 但现在我们保持最低限度,只将轮廓描边为黄色。
现在我们已经深入到底部,可以看到一系列更高级别的函数有了新的认识。
// Render it
CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL);
CGContextSetFont(X, cgFont);
CGContextSetFontSize(X, CTFontGetSize(runFont));
CGContextSetRGBFillColor(X, 1.0, 1.0, 1.0, 0.5);
CGContextShowGlyphsAtPositions(X, &glyph, &position, 1);
这只是在填充路径,对吧?
而这显然只是获取 CG 路径的边界框
CGRect glyphRect = CTRunGetImageBounds(run, X, thisGlyphRange);
剩下的最后一块拼图:如何首先创建富文本字符串。
#define FONT @"HelveticaNeue-Bold"
#define FONTSIZE 0.7
#define TEXT @"jkl"
:
:
NSString* string = TEXT;
// A string is something like: 'hello world!'
// An attributed string is something like 'hello world!' with 'Hello' in
// Times new Roman, 14pt and 'world' in this is the Courier Bold.
// An RTF file is basically an attributed string.
CFAttributedStringRef attStr;
// get attributed-string from string
{
UIFont* font = [UIFont fontWithName:FONT size:FONTSIZE];
CTFontRef ctFont = CTFontCreateWithName((CFStringRef)font.fontName,
font.pointSize,
NULL);
// In our example we just set the font,
// and specify ' only use ligatures when essential '.
// Ligatures: on some fonts 'fit' comes out
// as only 2 glyphs; 'fi'+'t', because if 'f' and
// 'i' are drawn separately, they will tend
// to be close together and messy. 'fi' is a ligature
NSNumber* NS_0 = [NSNumber numberWithInteger:0];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
(id) ctFont, kCTFontAttributeName, // NSFontAttributeName
(id) NS_0, kCTLigatureAttributeName, // NSLigatureAttributeName
nil];
assert(attributes != nil);
NSAttributedString* ns_attrString = [[NSAttributedString alloc] initWithString:string
attributes:attributes];
[ns_attrString autorelease];
attStr = (CFAttributedStringRef) ns_attrString;
}
由于我将所有开发者样板代码都包含在这篇文章中,我将指出我们还没有完成。下一步,一旦我们在屏幕上绘制了某些内容,就是进行测试。尝试不同的字体和大小。尝试绘制倒置的图形。尝试从非 (0,0) 的点开始绘制。踢打它,用它发泄一下 -- 让它更强壮。在此过程中,我发现恢复图形上下文不会恢复文本矩阵。这让我为代码添加了几行关键代码。
CGAffineTransform textMatrix = CTRunGetTextMatrix(run);
CGContextSetTextMatrix(X, textMatrix);
然后 -- 谁知道呢?取决于你想要什么。也许可以将功能提取到方法中,并将它们放入一个新的源文件中,这样你就可以轻松地将其拖入你的项目中。
附注:如果你在家尝试这个 -- 你需要添加 Core Text 框架和以下行到你的 .PCH 文件中
#import <CoreText/CoreText.h>
如果有人有加速研究阶段的有用技巧,请分享! 我花了几个小时才找到示例项目。我应该直接去 Apple 网站 -- 有一个很容易找到的页面列出了他们所有的代码示例:嗯……刚检查了一下,它们都是垃圾。