65.9K
CodeProject 正在变化。 阅读更多。
Home

底层文本渲染

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2010年9月14日

CPOL

9分钟阅读

viewsIcon

78700

downloadIcon

973

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+双击各种函数,会弹出链接到其他几个示例的帮助页面。

我喜欢将此类链接保存在项目顶部

这样你就可以直接从 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 网站 -- 有一个很容易找到的页面列出了他们所有的代码示例:嗯……刚检查了一下,它们都是垃圾。

© . All rights reserved.