抄来的东西(文字处理)

 

Quartz 2D 参考-文本

分类: Iphone开发入门2012-09-17 21:43 171人阅读 评论(0) 收藏 举报

 

原文:苹果官方文档

本章描述Quartz对文本的基本支持,以及如何在应用程序中使用Quartz2d显示文本。 Quartz 2D 提供了基本的、低级别的绘制文字及符号的接口(使用MacRoman文本编码)。Quartz 2D提供与字体相关的API,可参考CGFont Reference 。

如果你要在iOS上开发一个基于文本的应用程序,首先参考iOS的Text,Web,andEditing编程指南,它描述了iOS对文本的支持。此外,UIKit提供了易于使用的满足一般应用的类:

 

  • UILabel类:绘制1行或多行静态文本,你可以用于标注不同的UI部分。基本的UILabel提供了对显示文本的控制,包括是否有阴影或高亮。如有必要,你可以通过子类化定制文本的显示。
  • UITextField 类是为从用户搜集少量文本和一些实时动作而设计的。包括输入搜索文本。
  • UITextView类支持特定字体、颜色和对齐方式的文本显示,也支持文本的编辑。尤其用于显示多行文本,比如大文本的显示。
  • UIFont类提供了字体选择和设置的接口。
  • UIKit扩展了NSString类用于在当前上下文计算和绘制文本。更多信息参考NSString UIKit增强指南。

 

如果你正在MacOS X上开发基于文本的应用程序,首先请参考Cocoa文本系统,见文档 Cocoa Text Architecture Guide. Cocoa 提供了完全的Unicode 支持,文本输入及编辑, 精确的文本布局及类型设置,字体管理, 以及其他高级文本处理特性。

iOS 3.2 以上及 Mac OS X 都支持Core Text, 一种优秀的显示文本和处理字体的底层技术。Core Text 是为高性能设计的,易于使用,允许你在图形上下文中直接绘制unicode字符。如果你正在编写需要精确控制文本显示的应用,请参考Core Text Programming Guide.

如果你仅仅需要使用Quartz 2D提供的最基本的特性在图形上下文中绘制MacRoman字符,请参考本章。

Quartz 2D 如何绘制文本

Quartz 2D 使用字体绘制文本。字体是与字符关联的图形形象的集合。一个字符代表一个抽象意义,如,小写字母B,数字2,“+”号等。你在显示设备上看到的并不是字符本身,而是一个符号,一个路径,是字符的可视化表现。一个符号代表一个字符,例如字母b,多个字符如”fi”;还有不可见字符,如空格。Quartz用ATS框架(Appletype services)提供的字体数据来呈现符号。

字符编码是指将一个字符以某种编码方式映射为一个byte的二进制形式。例如,用ASCII编码用数字65代表大写字母A。当你想打印或显示文本时,你一般是通过字符而不是符号进行的。

你以用户空间坐标来指定文本的位置。文本矩阵规定了由文本空间到用户空间的转换方式。文本位置在文本矩阵的tx和ty变量中存储。当你创建图像上下文时,它以指定的矩阵初始化文本矩阵,文本空间坐标被初始化为用户空间坐标。

Quartz简单地将文本矩阵和当前转换矩阵和其他图形状态参数连接起来,以产生最终的文本渲染矩阵,正是用该矩阵将文本绘制在页上。文本矩阵只存在于文本控件,并不包含字体大小。

Quartz绘制文本时,从ATS中检索对应的符号并用当前图形参数绘制文本,例如填充色(如果文字是实心的)以及描边色。绘制之后,文本位置保留在最后绘制的点处。

控制文本外观

某些图形状态既用于绘制路径也用于文本。如描边色和填充色。(影响文本显示的其他属性和操作请参考“Paths”).

有几种文本绘制属性只应用于文本。用表16-1中的函数设置这些值。注意,CGContextSelectfont函数可以设置字体以及字体大小、文字编码。“绘制文本”讨论了CGContextSelectFone和 CGContextSetFont之间的区别。

Table 16-1  文本属性和设置它们的函数

属性

函数

说明

Font

CGContextSetFont

CGContextSelectFont

Typeface.

Font size

CGContextSetFontSize

CGContextSelectFont

大小(以文本空间单位)

Character spacing

CGContextSetCharacterSpacing

字间距(文本空间单位)

Text drawing mode

CGContextSetTextDrawingMode

每个符号的绘制方式。参考 Table 16-2

Text matrix

CGContextSetTextMatrix

从文本空间到用户控件的转换矩阵

Text position

CGContextSetTextPosition

文本绘制位置

文本矩阵类似当前转换矩阵(CTM),但只用于文本。你可以用任何仿射转换函数对文本矩阵进行复杂的转换。当绘制文本时,quartz使用你指定的文本矩阵。当绘制其他东西时,Quartz使用CTM而不是文本矩阵。

 

Drawing Text

用Quartz绘制文本需要经过以下步骤:

  • 设置字体及字体大小
  • 设置绘制模式
  • 设置其他——描边色、填充色、背景区域
  • 如果需要变换,需要设置文本矩阵、旋转、缩放值
  • 绘制

 

Quartz 2D有两种方法设置字体及大小。CGContextSelectFont或CGContextSetFont+CGContextSetFontSize。CGContextSElectFont更简单一些。CGContextSetFont和CGContectSetFontSize麻烦一些,但你也可能想用Cocoa来设置字体然后进行绘制。因为Quartz在绘制文本上有一些限制。让我们再来比较一下两种方法的区别。

如果对你的应用而言,使用MacRoman文本编码已经足够的话,可以使用CGContextSelectFont函数。在绘制文本时,调用CGContextShowTextAtPoint函数。CGContextSelectFont函数有4个参数:一个图形上下文,字体的PostScript字体名,字体大小(用户空间单位),以及文本的编码。

如果想使用MacRoman以外的文本编码,应该使用CGContextSetFont和CGContextSetFontSize。必须为CGContextSetFont函数提供CGFont类型参数。调用CGFontCreateWithPlatformFont函数可以从ATS字体获得一个CGFont对象。绘制文本时,调用CGContextShowGlyphsAtPoint替代CGContextShowTextAtPoint。

原因就在于,在将文本字节映射为字体符号时,需要指定一个文本编码,默认的文本编码是kCGEncodingFontSpecific,当你调用CGContextShowTextAtPoint时,不能保证一定会获得一个文本编码。由于你调用CGContextSetFont而不是CGContextSelectFont来指定字体,在CGContextSetFont中你并没有指定文本编码,你也就不能使用CGContextShowTextAtPoint来绘制文本。

如果你要用这种方式设置字体,你必须用Cocoa或者自己实现将字符串映射为符号,这样你才能调用CGContextShowGlphsAtPoint。使用Cocoa绘制文本更简单,根本不需要任何Quartz2D函数。现在,你明白了使用Quartz绘制文本的一些限制,我们来看看使用CGContextSelectFont函数的一些例子。CGContextSelectFont函数的使用十分简单,但不建议你在MacRoman以外的编码下使用。清单16-1 显示了MyDrawText函数——用于绘制文本,如图16-1所示。代码后有详细的解释。

 

Listing16-1  Drawing text

void MyDrawText (CGContextRef myContext, CGRect contextRect) // 1

{

    float w, h;

    w = contextRect.size.width;

    h = contextRect.size.height;

 

    CGAffineTransform myTextTransform; // 2

    CGContextSelectFont (myContext, // 3

                    "Helvetica-Bold",

                     h/10,

                     kCGEncodingMacRoman);

    CGContextSetCharacterSpacing (myContext, 10); // 4

    CGContextSetTextDrawingMode (myContext, kCGTextFillStroke); // 5

 

    CGContextSetRGBFillColor (myContext, 0, 1, 0, .5); // 6

    CGContextSetRGBStrokeColor (myContext, 0, 0, 1, 1); // 7

    myTextTransform =  CGAffineTransformMakeRotation  (MyRadians (45)); // 8

    CGContextSetTextMatrix (myContext, myTextTransform); // 9

    CGContextShowTextAtPoint (myContext, 40, 0, "Quartz 2D", 9); // 10

}

  1. 参数:一个图形上下文和一个用于绘制文本的矩形区域。
  2. 声明仿射渐变矩阵。
  3. 字体设置为Helvetica,大小为矩形高度/10,使用文本空间单位。在这个例子中,文字是绘制在一个大小可变的窗口中。当用户改变窗口大小,文字大小也随之改变。编码设置为kCGEndodingMacRoman,另外一个选择是kCGEncodingFontSpecific。
  4. 设置字间距为10个文字空间单位。如果你想增加字间距,请调用这个函数。
  5. 设置绘制模式:填充+描边。
  6. 填充色:绿色,alpha值0.5,以呈现部分透明效果。注意,这不是文本特有的属性,填充色会应用到图形状态。
  7. 设置描边的颜色及透明度。这也不是文本特有属性。
  8. 创建仿射变换,倾斜45度。MyRadians是一个便利函数,用于将角度转为弧度。你可以自己实现该函数,或者用弧度值替换。否则代码无法编译。
  9. 用上面创建的变换设置文本矩阵。
  10. 绘制文本。x,y坐标指定了开始位置(40,0)。此外,还要指定绘制的字符数组,以及字符数组的个数——这里,分别用一个C String字符串和9指定。

 

Figure 16-1  使用Quartz函数绘制文

 

对于iOS,必须对当前图像上下文进行转换,以便文字方向与图16-1一样。这个转换可以将y轴的原点置于屏幕底部。清单16-2显示了怎样在drawRect:方法中使用这个转换。然后调用了MyDrawText方法。

Listing 16-2  在iOS应用中绘制Quartz 2D文本

- (void)drawRect:(CGRect)rect

{

   CGContextRef theContext = UIGraphicsGetCurrentContext();

   CGRect viewBounds = self.bounds;

 

   CGContextTranslateCTM(theContext, 0, viewBounds.size.height);

   CGContextScaleCTM(theContext, 1, -1);

 

   // Draw the text using the MyDrawText function

   MyDrawText(theContext, viewBounds);

}

 

绘制之前计算文本大小

如果文本大小在你的应用中是重要的,可以用Quartz2D计算出来。计算某行文本的尺寸经过以下步骤:

  1. 调用函数CGContextGetTextPosition获得当前文本位置
  2. 调用CGContextSetTextDrawingMode函数将文本设置为kCGTextInvisible。
  3. 调用CGContextShowText绘制文本(在当前位置)。
  4. 调用CGContextGetTextPosition得到文本的结束位置。
  5. 用截止位置减去开始位置,即可得到文本的宽度。

复制 Font 变体

字体变体是包含在变体axis中的一个设置,允许你产生一系列打印风格。每个变体axis包括:

  • axis名称(如’wght’),指定了该变体axis所包含的风格。在字体变体axis的dictionary中,用关键字kCGFontVariationAxisName来检索。
  • 最大值和最小值,在字体变体axis的dictionary中,用kCGFontVariationAxisMinValue和kCGFontVariationAxisMaxValue检索。
  • axis的默认值,在dictionary中,由kCGFontVariationAxisDefaultValue指定。

例如,字重axis管理着字重的可能值——最小的值导致该字体以最淡的显示,最大的值产生最粗的字体。默认值使字体正常显示。axis是字体设计者设计的,对于有的字体,字体是可以有变体的,但不是所有字体都有变体。

关于变体,Quartz提供了3个函数:

  • CGFontCreateCopyWithVariations,创建一个字体的副本,指定一个变体设置。返回一个针对指定字体应用该变体设置进行变形后的字体。
  • CGFontCopyVariations, 返回一个dictionary,包含了该字体所包含的变体,如果该字体不包含变体,返回NULL。
  • CGFontCopyVariationAxes, 返回一个数组,包含了该字体所包含的变体axis的dictioanrys,如果字体不支持变体,返回NULL。

PostScript 字体

Quartz 提供如下函数以支持PostScript字体:

  • 获取PS字体名
  • 判断是否可用PS格式创建一个字体的子集
  • 创建字体的PS子集
  • 创建字体的PS编码

如果你想使用PS字体,请参考CGFont Reference .

 

CoreText详细用法

分类: Iphone开发入门

2012-09-18 08:25 2397人阅读 评论(0) 收藏 举报

attributesimageuiviewparsingstringclass

 

目录(?)

[+]

 






 

If you're new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!

This is a blog post by iOS Tutorial Team member Marin Todorov, a software developer with 12+ years of experience, an independant iOS developer and the creator of Touch Code Magazine.

 

Magazines, Core Text, and Brains!

Core Text is a text engine found in iOS 3.2+ and OSX 10.5+ that gives you fine-grained control over text layout and formatting.

It sits in the sweet spot between UIKit and Core Graphics/Quartz:

       In UIKit you have UILabel and you add a word or a text line on the screen by simple Drag-and-Drop in IB, but you cannot change the color of individual words.

       In Core Graphics/Quartz you can do pretty much everything the system is capable of, but you need to calculate the position of each glyph in your text and draw it on the screen.

         Core Text lies right between the two! You have complete control over position, layout, attributes like color and size, but Core Text takes care of everything else for you – from word wrap to font rendering and more.

Core Text is especially handy if you are creating a magazine or book app – which work great on the iPad!

This tutorial will get you started with Core Text by taking you through the process of creating a very simple Magazine application using Core Text – for Zombies!

You’ll learn how to:

       lay formatted text down on the screen;

       fine tune the text’s appearance;

       add images inside the text content;

       and finally create a magazine app, which loads text markup to easily control the formatting of the rendered text.

       eat brains! Ok just kidding, that’s only for the readers of this magazine.

To get the most out of this tutorial, you need to know the basics of iOS development first. If you are new to iOS development, you should check out some of the other tutorials on this site first.

Without further ado, let’s make some happy zombies by making them their very own iPad magazine!

 

Setting up a Core Text project

Start up Xcode, go to FileNewNew Project, choose the iOSApplicationView-based Application, and click Next. Name the project CoreTextMagazine, choose iPad as Device family, click Next, choose a folder to save your project in, and click Create.

Next thing you have to do is add the Core Text framework to the project:

  1. Click on the project file in the Project navigator (the strip on the left hand side)
  2. Next select your only target for this project “CoreTextMagazine” in the Targets list
  3. Click on the “Build phases” tab
  4. Expand the “Link Binary With Libraries” strip and click on the “+” button
  5. Choose “CoreText.framework” from the list and click “Add”

 

That’s all the setup you need – now it’s time to start adding some code!

Adding a Core Text view

To get on track with Core Text as fast as possible you are going to create a custom UIView, which will use Core Text in its drawRect: method.

Go to FileNewNew File, choose iOSCocoa TouchObjective-C class, and click Next. Enter UIView for Subclass of, click Next, name the new class CTView, and click Save.

In CTView.h just above @interface add the following code to include the Core Text framework:

#import <CoreText/CoreText.h>

In the next step you’re going to set this new custom view as the main view in the application.

Select in the Project navigator the XIB file “CoreTextMagazineViewController.xib”, and bring up the Utilities strip in XCode (this appears whe you select the third tab in the View section of the top toolbar). From the Utilities strip, select the third icon on the top toolbar to select the Identity tab.

Now just click in the white space in the Interface editor to select the window’s view – you should see in the Utilities strip in the field Class the text “UIView” appearing. Write in that field “CTView” and hit Enter.

 

Now your application will show your custom Core Text view when started, but we’ll do that in a moment – let’s first add the code to draw some text so we have what to test.

Open CTView.m and delete all the predefined methods. Enter the following code to draw a “Hello world” inside your view:

- (void)drawRect:(CGRect)rect

{

    [super drawRect:rect];

    CGContextRef context = UIGraphicsGetCurrentContext();

 

    CGMutablePathRef path = CGPathCreateMutable(); //1

    CGPathAddRect(path, NULL, self.bounds );

 

    NSAttributedString* attString = [[[NSAttributedString alloc]

        initWithString:@"Hello core text world!"] autorelease]; //2

 

    CTFramesetterRef framesetter =

        CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString); //3

    CTFrameRef frame =

        CTFramesetterCreateFrame(framesetter,

            CFRangeMake(0, [attString length]), path, NULL);

 

    CTFrameDraw(frame, context); //4

 

    CFRelease(frame); //5

    CFRelease(path);

    CFRelease(framesetter);

}

Let’s discuss this bit by bit, using the comment markers above to designate each section:

  1. Here you need to create a path which bounds the area where you will be drawing text. Core Text on the Mac supports different shapes like rectangles and circles, but for the moment iOS supports only rectangular shape for drawing with Core Text. In this simple example, you’ll use the entire view bounds as the rectangle where you will be drawing by creating a CGPath reference from self.bounds.
  2. In Core Text you won’t be using NSString, but rather NSAttributedString, as shown here. NSAttributedString is a very powerful NSString derivate class, which allows you apply formatting attributes to text. For the moment we won’t be using formatting – this just creates a string holding plain text.
  3. CTFramesetter is the most important class to use when drawing with Core Text. It manages your font references and your text drawing frames. For the moment what you need to know is that CTFramesetterCreateWithAttributedString creates a CTFramesetter for you, retains it and initializes it with the supplied attributed string. In this section, after you have the framesetter you create a frame, you give the CTFramesetterCreateFrame a range of the string to render (we choose the entire string here) and the rectangle where the text will appear when drawn.
  4. Here CTFrameDraw draws the supplied frame in the given context.
  5. Finally, all the used objects are released.

Note that when working with Core Text classes you use a set of functions like CTFramesetterCreateWithAttributedString and CTFramesetterCreateFrame instead of directly using Objective-C objects.

You might think to yourself “Why would I ever want to use C again, I thought I was done with that since we have Objective-C?!”

Well, many of the low level libraries on iOS are written in plain C for speed and simplicity. Don’t worry though, you’ll find the Core Text functions pretty easy to work with.

Just one important thing to remember though: don’t forget to always use CFRelease on the references you get from functions which have “Create” in their name.

Believe it or not, that’s all you need to draw some simple text using Core Text! Hit Run and see the result.

 

Well that does not seem right, does it? Like many of the low level APIs, Core Text uses a Y-flipped coordinate system. To make it even worse, the content is also rendered flipped downwards! Because of this, keep in mind that if you mix UIKit drawing and Core Text drawing, you might get weird results.

Let’s fix the content orientation! Add the following code just after this line “CGContextRef context = UIGraphicsGetCurrentContext();”:

// Flip the coordinate system

CGContextSetTextMatrix(context, CGAffineTransformIdentity);

CGContextTranslateCTM(context, 0, self.bounds.size.height);

CGContextScaleCTM(context, 1.0, -1.0);

This is very simple code, which just flips the content by applying a transformation to the view’s context. Just copy/paste it each time you do drawing with CT.

Now hit Run again – congrats on your first Core Text app!

 

The Core Text Object Model

If you are a bit confused about the CTFramesetter and the CTFrame – that’s OK. Here I’ll make a short detour to explain how Core Text renders text content.

Here’s what the Core Text object model looks like:

 

You create a CTFramesetter reference and you provide it with NSAttributedString. At this point, an instance of CTTypesetter is automatically created for you, a class that manages your fonts. Next you use the CTFramesetter to create one or more frames in which you will be rendering text.

When you create a frame you tell it the subrange of text that will be rendered inside its rectangle. Core Text then automatically creates a CTLine for each line of text and (pay attention here) a CTRun for each piece of text with the same formatting.

As an example, Core Text would create a CTRun if you had several words in a row colored red, then another CTRun for the following plain text, then another CTRun for a bold sentence, etc. Again: very important – you don’t create CTRun instances, Core Text creates them for you based on the attributes of the supplied NSAttributedString.

Each of these CTRun objects can adopt different attributes, so you have fine control over kerning, ligatures, width, height and more.

Onto the Magazine App!

To create this magazine app, we need the capability to mark some of the text as having different attributes. We could do this by directly using methods on NSAttributedString such as setAttributes:range, but this is unwieldy to deal with in practice (unless you like to painstakingly write a ton of code!)

So to make thing simpler to work with, we’ll create a simple text markup parser which will allow us to use simple tags to set formatting in the magazine content.

Go to FileNewNew File, choose iOSCocoa TouchObjective-C class, and click Next. Enter NSObject for Subclass of, click Next, name the new class MarkupParser.m, and click Save.

Inside MarkupParser.h delete all the text and paste this code – it defines few properties and the method to do the parsing:

#import <Foundation/Foundation.h>

#import <CoreText/CoreText.h>

 

@interface MarkupParser : NSObject {

 

    NSString* font;

    UIColor* color;

    UIColor* strokeColor;

    float strokeWidth;

 

    NSMutableArray* images;

}

 

@property (retain, nonatomic) NSString* font;

@property (retain, nonatomic) UIColor* color;

@property (retain, nonatomic) UIColor* strokeColor;

@property (assign, readwrite) float strokeWidth;

 

@property (retain, nonatomic) NSMutableArray* images;

 

-(NSAttributedString*)attrStringFromMarkup:(NSString*)html;

 

@end

Next open MarkupParser.m and replace the contents with the following:

#import "MarkupParser.h"

 

@implementation MarkupParser

 

@synthesize font, color, strokeColor, strokeWidth;

@synthesize images;

 

-(id)init

{

    self = [super init];

    if (self) {

        self.font = @"Arial";

        self.color = [UIColor blackColor];

        self.strokeColor = [UIColor whiteColor];

        self.strokeWidth = 0.0;

        self.images = [NSMutableArray array];

    }

    return self;

}

 

-(NSAttributedString*)attrStringFromMarkup:(NSString*)markup

{

 

}

 

-(void)dealloc

{

    self.font = nil;

    self.color = nil;

    self.strokeColor = nil;

    self.images = nil;

 

    [super dealloc];

}

 

@end

As you see you start pretty easy with the parser code – it just contains properties to hold the font, text color, stroke width and stroke color. Later on we’ll be adding images inside the text, so you need an array where you’re going to keep the list of images in the text.

Writing a parser is usually pretty hard work, so I’m going to show you how to build a very very simple one using regular expressions. This tutorial’s parser will be very simple and will support only opening tags – i.e. a tag will set the style of the text after the tag, the style will be applied until a new tag is found. The text markup will look like this:

These are <font color="red">red<font color="black"> and

<font color="blue">blue <font color="black">words.

and will produce output like this:

These are red and blue words.

For the purpose of the tutorial such markup will be quite sufficient. For your projects you can develop it further if you'd like to.

Let's get parsin'!

Inside the attrStringFromMarkup: method add the following:

NSMutableAttributedString* aString =

    [[NSMutableAttributedString alloc] initWithString:@""]; //1

 

NSRegularExpression* regex = [[NSRegularExpression alloc]

    initWithPattern:@"(.*?)(<[^>]+>|\Z)"

    options:NSRegularExpressionCaseInsensitive|NSRegularExpressionDotMatchesLineSeparators

    error:nil]; //2

NSArray* chunks = [regex matchesInString:markup options:0

    range:NSMakeRange(0, [markup length])];

[regex release];

There are two sections to cover here:

  1. First, you set the empty result string to which you'll be adding text as it's found.
  2. Next, you create a regex to match chunks of text and tags. This regex will basically match a string of text and a following tag. The regular expression basically says "Look for any number of characters, until you come across an opening bracket. Then match any number of characters until you hit a closing bracket. Or - stop processing when you hit the end of the string."

Why are we creating this regular expression? We're going to use it to search the string for every place it matches, and then 1) render the text chunk found; then 2) change the current styles according to what's found in the tag. This will be repeated until the text is over.

Very simple parser indeed, eh?

Now that you have the chunks of text and all the formatting tags (like the font tag you see a bit above) in the "chunks" array, you'll need to loop trough them and build the attributed string from the text and tags.

Add this to the method body:

for (NSTextCheckingResult* b in chunks) {

    NSArray* parts = [[markup substringWithRange:b.range]

componentsSeparatedByString:@"<"]; //1

 

    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)self.font,

24.0f, NULL);

 

    //apply the current text style //2

    NSDictionary* attrs = [NSDictionary dictionaryWithObjectsAndKeys:

                           (id)self.color.CGColor, kCTForegroundColorAttributeName,

                           (id)fontRef, kCTFontAttributeName,

                           (id)self.strokeColor.CGColor, (NSString *) kCTStrokeColorAttributeName,

                           (id)[NSNumber numberWithFloat: self.strokeWidth], (NSString *)kCTStrokeWidthAttributeName,

                           nil];

    [aString appendAttributedString:[[[NSAttributedString alloc] initWithString:[parts objectAtIndex:0] attributes:attrs] autorelease]];

 

    CFRelease(fontRef);

 

    //handle new formatting tag //3

    if ([parts count]>1) {

        NSString* tag = (NSString*)[parts objectAtIndex:1];

        if ([tag hasPrefix:@"font"]) {

            //stroke color

            NSRegularExpression* scolorRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=strokeColor=")\w+" options:0 error:NULL] autorelease];

            [scolorRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){

                if ([[tag substringWithRange:match.range] isEqualToString:@"none"]) {

                    self.strokeWidth = 0.0;

                } else {

                    self.strokeWidth = -3.0;

                    SEL colorSel = NSSelectorFromString([NSString stringWithFormat: @"%@Color", [tag substringWithRange:match.range]]);

                    self.strokeColor = [UIColor performSelector:colorSel];

                }

            }];

 

            //color

            NSRegularExpression* colorRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=color=")\w+" options:0 error:NULL] autorelease];

            [colorRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){

                SEL colorSel = NSSelectorFromString([NSString stringWithFormat: @"%@Color", [tag substringWithRange:match.range]]);

                self.color = [UIColor performSelector:colorSel];

            }];

 

            //face

            NSRegularExpression* faceRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=face=")[^"]+" options:0 error:NULL] autorelease];

            [faceRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){

                self.font = [tag substringWithRange:match.range];

            }];

        } //end of font parsing

    }

}

 

return (NSAttributedString*)aString;

Phew, this is a lot of code! But don't worry, we'll go over it here section by section.

  1. You iterate over the chunks matched by the prior regular expression, and in this section you split the chunks by the "<" character (the tag opener). As a result, in parts[0] you have the text to add to the result and in parts[1] you have the content of the tag that changes the formatting for the text that follows.
  2. Next you create a dictionary holding a number of formatting options - this is the way you can pass formatting attributes to a NSAttributedString. Have a look at the key names - they are Apple defined constants which are pretty self-explanatory (of you can check out Apple's Core Text String Attributes Reference for full details). By calling appendAttributedString: the new text chunk with applied formatting is added to the result string.
  3. Finally, you check if there's a tag found after the text; if it starts with "font" a regex is ran for every possible tag attribute. For the "face" attribute the name of the font is saved in self.font, for "color" I and you did a little trickery: for <font color="red"> the text value "red" is taken by the colorRegex and then a selector "redColor" is created and performed on the UIColor
class - this (hehe) returns a UIColor instance of a red color. Note this trick works only for the predefined colors of UIColor (and can even cause your code to crash if you pass in a selector that does not exist!) but this is sufficient for this tutorial. The stroke color attribute works much like the color attribute, but if the value of strokecolor is "none" just sets the stroke widht to 0.0, so no stroke will be applied to the text.

Note: If you're unsatiably curious how the regular expressions in this section work, they are basically saying ("Use the look-behind assertion to look for any text that is preceded by color=". Then match any normal word character (which does not include a quote), which basically keeps matching until the close quote is found. For more details, check out Apple's NSRegularExpression class reference.

Right! Half the work of rendering formatted text is done - now attrStringFromMarkup: can take markup in and spit a NSAttributedString out ready to be fed to Core Text.

So let's pass in a string to render, and try it out!

Open CTView.m and add this just before @implementation:

#import "MarkupParser.h"

Find the line where attString is defined - replace it with the following code:

MarkupParser* p = [[[MarkupParser alloc] init] autorelease];

NSAttributedString* attString = [p attrStringFromMarkup: @"Hello <font color="red">core text <font color="blue">world!"];

Above you make a new parser, feed it a piece of markup and it gives you back formatted text.

That's it - hit Run and try it for yourself!

 

Ain't that just awesome? Thanks to 50 lines of parsing we don't have to deal with text ranges and code heavy text formatting, we can just use now a simple text file to hold the contents of our magazine app. Also the simple parser you just wrote can be extended infinitely to support everything you'd need in your magazine app.

A Basic Magazine Layout

So far we have text showing up, which is a good first step. But for a magazine we'd like to have columns - and this is where Core Text becomes particularly handy.

Before proceeding with the layout code, let's first load a longer string into the app so we have something long enough to wrap across multiple lines.

Go to FileNewNew File, choose iOSOtherEmpty, and click Next. Name the new file test.txt, and click Save.

Then add the text from this file into test.txt and save.

Open CTView.m and find the 2 lines where you create MarkupParser and NSAttributedString and delete them. We're taking the loading of the text file out of the drawRect: method, because that sort of code doesn't really belong there. It's the job of a UIView to display content given to it - not load content. We'll move the attString variable to an instance variable and property in this class later.

Next open CoreTextMagazineViewController.m, delete all the existing content, and add the following instead:

#import "CoreTextMagazineViewController.h"

#import "CTView.h"

#import "MarkupParser.h"

 

@implementation CoreTextMagazineViewController

 

- (void)viewDidLoad

{

    [super viewDidLoad];

 

    NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"txt"];

    NSString* text = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];

    MarkupParser* p = [[[MarkupParser alloc] init] autorelease];

    NSAttributedString* attString = [p attrStringFromMarkup: text];

    [(CTView*)self.view setAttString: attString];

}

 

@end

When the view of the application is loaded, the app reads the text from test.txt, converts it to an attributed string and then sets the attString property on the window's view. We haven't added that property to CTView yet though, so let's add that next!

In CTView.h define these 3 instance variables:

float frameXOffset;

float frameYOffset;

 

NSAttributedString* attString;

Then add the corresponding code in CTView.h and CTView.m to define a property for attString:

//CTView.h

@property (retain, nonatomic) NSAttributedString* attString;

 

//CTView.m

//just below @implementation ...

@synthesize attString;

 

//at the bottom of the file

-(void)dealloc

{

    self.attString = nil;

    [super dealloc];

}

Now you can hit Run again to see the view showing the contents of the text file. Cool!

 

How to make columns out of this text? Luckily Core Text provides a handy function - CTFrameGetVisibleStringRange. This function tells you how much text will fit into a given frame. So the idea is - create column, check how much text fits inside, if there's more - create another column, etc. etc. (columns here will be CTFrame instances, since columns are just taller rectangles)

First of all - we are going to have columns, then pages, then a whole magazine, so... let's make our CTView subclass UIScrollView to get free paging and scrolling!

Open up CTView.h and change the @interface line to:

@interface CTView : UIScrollView<UIScrollViewDelegate> {

OK! We've got free scrolling and paging now available. We're going to enable the paging in a minute.

Up to now we were creating our framesetter and frame inside the drawRect: method. When you have columns and different formatting it's better to do all those calculations only once. So what we are going to do is have a new class "CTColumnView" which will only render CT content passed to it, and in our CTView class we're going to only once create instances of CTColumnView and add them as subviews.

So to summarize: CTView is going to take care of scrolling, paging and building the columns; CTColumnView will actually render the content on the screen.

Go to FileNewNew File, choose iOSCocoa TouchObjective-C class, and click Next. Enter UIView for "Subclass of", click Next, name the new class CTColumnView.m, and click Save. Here's the initial code for the CTColumnView class:

//inside CTColumnView.h

 

#import <UIKit/UIKit.h>

#import <CoreText/CoreText.h>

 

@interface CTColumnView : UIView {

    id ctFrame;

}

 

-(void)setCTFrame:(id)f;

@end

 

//inside CTColumnView.m

#import "CTColumnView.h"

 

@implementation CTColumnView

-(void)setCTFrame: (id) f

{

    ctFrame = f;

}

 

-(void)drawRect:(CGRect)rect

{

    CGContextRef context = UIGraphicsGetCurrentContext();

 

    // Flip the coordinate system

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);

    CGContextTranslateCTM(context, 0, self.bounds.size.height);

    CGContextScaleCTM(context, 1.0, -1.0);

 

    CTFrameDraw((CTFrameRef)ctFrame, context);

}

@end

This class does pretty much what we've been doing up to now - it just renders a CTFrame. We're going to create an instance of it for each text column in the magazine.

Let's first add a property to hold our CTView's text frames and declare the buildFrames method, which will do the columns setup:

//CTView.h - at the top

#import "CTColumnView.h"

 

//CTView.h - as an ivar

NSMutableArray* frames;

 

//CTView.h - declare property

@property (retain, nonatomic) NSMutableArray* frames;

 

//CTView.h - in method declarations

- (void)buildFrames;

 

//CTView.m - just below @implementation

@synthesize frames;

 

//CTView.m - inside dealloc

self.frames = nil;

Now buildFrames can create the text frames once and store them in the "frames" array. Let's add the code to do so.

- (void)buildFrames

{

    frameXOffset = 20; //1

    frameYOffset = 20;

    self.pagingEnabled = YES;

    self.delegate = self;

    self.frames = [NSMutableArray array];

 

    CGMutablePathRef path = CGPathCreateMutable(); //2

    CGRect textFrame = CGRectInset(self.bounds, frameXOffset, frameYOffset);

    CGPathAddRect(path, NULL, textFrame );

 

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);

 

    int textPos = 0; //3

    int columnIndex = 0;

 

    while (textPos < [attString length]) { //4

        CGPoint colOffset = CGPointMake( (columnIndex+1)*frameXOffset + columnIndex*(textFrame.size.width/2), 20 );

        CGRect colRect = CGRectMake(0, 0 , textFrame.size.width/2-10, textFrame.size.height-40);

 

        CGMutablePathRef path = CGPathCreateMutable();

        CGPathAddRect(path, NULL, colRect);

 

        //use the column path

        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, NULL);

        CFRange frameRange = CTFrameGetVisibleStringRange(frame); //5

 

        //create an empty column view

        CTColumnView* content = [[[CTColumnView alloc] initWithFrame: CGRectMake(0, 0, self.contentSize.width, self.contentSize.height)] autorelease];

        content.backgroundColor = [UIColor clearColor];

        content.frame = CGRectMake(colOffset.x, colOffset.y, colRect.size.width, colRect.size.height) ;

 

          //set the column view contents and add it as subview

        [content setCTFrame:(id)frame];  //6  

        [self.frames addObject: (id)frame];

        [self addSubview: content];

 

        //prepare for next frame

        textPos += frameRange.length;

 

        //CFRelease(frame);

        CFRelease(path);

 

        columnIndex++;

    }

 

    //set the total width of the scroll view

    int totalPages = (columnIndex+1) / 2; //7

    self.contentSize = CGSizeMake(totalPages*self.bounds.size.width, textFrame.size.height);

}

Let's look at the code.

  1. here we do some setup - define the x & y offsets, enable paging and create an empty frames array
  2. buildFrames continues by creating a path and a frame for the view's bounds (offset slightly so we have a margin).
  3. This section declares textPos, which will hold the current position in the text. It also declares columnIndex, which will count how many columns are already created.
  4. The while loop here runs until we've reached the end of the text. Inside the loop we create a column bounds: colRect is a CGRect which depending on columnIndex holds the origin and size of the current column. Note that we are building columns continuously to the right (not across and then down).
  5. This makes use of CTFrameGetVisibleStringRange function to figure out what portion of the string can fit in the frame (a text column in this case). textPos is incremented by the length of this range, and so the building of the next column can begin on the next loop (if there's more text remaining).
  6. Here, instead of drawing the frame like before, we pass it to the newly created CTColumnView, we store it in the self.frames array for later usage and we add it as subview to the scrollview
  7. Finally, totalPages holds the total number of pages generated, and the contentSize property of the CTView is set so when there's more than one page of content we get scrolling for free!

Now let's also call buildFrames when all the CT setup is done. Inside CoreTextMagazineViewController.m add at the end of viewDidLoad :

[(CTView *)[self view] buildFrames];

One more thing to do before giving the new code a try: in the file CTView.m find the method drawRect: and remove it. We do now all the rendering in the CTColumnView class, so we'll leave the CTView drawRect: method to be the standard UIScrollView implementation.

Alright... hit Run and you'll see text floating in columns! Drag right and left to go between pages ... awesome!

 

We have columns, formatted text, but we miss images. Turns out drawing images with Core Text is not that easy - it's a text framework after all.

But thanks to the fact we already have a little markup parser we're going to get images inside the text pretty quick!

Drawing Images in Core Text

Basically Core Text does not have possibility to draw images. However, since it's a layout engine, what it can do is leave an empty space where you want to draw a picture. And since your code is already inside a drawRect: method, drawing an image yourself is easy.

Let's look at how leaving an empty space in the text works. Remember all the text chunks are CTRun instances? You simply set a delegate for a given CTRun and the delegate object is responsible to let know Core Text what is the CTRun ascent space, descent space and width. Like so:

 

When Core Text "reaches" a CTRun which has a CTRunDelegate it asks the delegate - how much width should I leave for this chunk of data, how high should it be? This way you build a hole in the text - then you draw your image in that very spot.

Let's start by adding support for an "img" tag in our little markup parser! Open up MarkupParser.m and find "} //end of font parsing"; add the following code immediately after this line to add support for "img" tag:

if ([tag hasPrefix:@"img"]) {

 

    __block NSNumber* width = [NSNumber numberWithInt:0];

    __block NSNumber* height = [NSNumber numberWithInt:0];

    __block NSString* fileName = @"";

 

    //width

    NSRegularExpression* widthRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=width=")[^"]+" options:0 error:NULL] autorelease];

    [widthRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){

        width = [NSNumber numberWithInt: [[tag substringWithRange: match.range] intValue] ];

    }];

 

    //height

    NSRegularExpression* faceRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=height=")[^"]+" options:0 error:NULL] autorelease];

    [faceRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){

        height = [NSNumber numberWithInt: [[tag substringWithRange:match.range] intValue]];

    }];

 

    //image

    NSRegularExpression* srcRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=src=")[^"]+" options:0 error:NULL] autorelease];

    [srcRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){

        fileName = [tag substringWithRange: match.range];

    }];

 

    //add the image for drawing

    [self.images addObject:

     [NSDictionary dictionaryWithObjectsAndKeys:

      width, @"width",

      height, @"height",

      fileName, @"fileName",

      [NSNumber numberWithInt: [aString length]], @"location",

      nil]

     ];

 

    //render empty space for drawing the image in the text //1

    CTRunDelegateCallbacks callbacks;

    callbacks.version = kCTRunDelegateVersion1;

    callbacks.getAscent = ascentCallback;

    callbacks.getDescent = descentCallback;

    callbacks.getWidth = widthCallback;

    callbacks.dealloc = deallocCallback;

 

    NSDictionary* imgAttr = [[NSDictionary dictionaryWithObjectsAndKeys: //2

                              width, @"width",

                              height, @"height",

                              nil] retain];

 

    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, imgAttr); //3

    NSDictionary *attrDictionaryDelegate = [NSDictionary dictionaryWithObjectsAndKeys:

                                            //set the delegate

                                            (id)delegate, (NSString*)kCTRunDelegateAttributeName,

                                            nil];

 

    //add a space to the text so that it can call the delegate

    [aString appendAttributedString:[[[NSAttributedString alloc] initWithString:@" " attributes:attrDictionaryDelegate] autorelease]];

}

Let's get a look at all that new code - actually parsing the "img" tag does pretty much the same as what you did already for the font tag. By using 3 regexes you effectively read the width, height and src attributes of the img tag. When done - you add a new NSDictionary to self.images holding the information you just parsed out plus the location of the image in the text.

Now look at section 1 - CTRunDelegateCallbacks is a C struct which holds references to functions. This struct provides the information you want to pass to the CTRunDelegate. As you can guess already getWidth is called to provide a width for the CTRun, getAscent provides the height of the CTRun, etc. In the code above you provide function names for those handlers; in a minute we're gonna add the function bodies too.

Section 2 is very important - imgAttr dictionary holds the dimentions of the image; this object is also retained as it is going to be passed to the function handlers - so, when getAscent handler triggers it'll get as a parameter imgAttr and will just read out the height of the image and provide the value to CT. Neat! (we'll get to this in a moment)

CTRunDelegateCreate in section 3 creates a delegate instance reference and binds the callbacks and the data parameter together.

In the next step you need to create the attributes dictionary (the same way as for the font formatting above), but instead of formatting attributes you pass the delegate instance. In the end you add a single space which will trigger the delegate and create the hole in the text where the image will be rendered later on.

Next step, which you already anticipated, is to provide the callback functions for the delegate:

//inside MarkupParser.m, just above @implementation

 

/* Callbacks */

static void deallocCallback( void* ref ){

    [(id)ref release];

}

static CGFloat ascentCallback( void *ref ){

    return [(NSString*)[(NSDictionary*)ref objectForKey:@"height"] floatValue];

}

static CGFloat descentCallback( void *ref ){

    return [(NSString*)[(NSDictionary*)ref objectForKey:@"descent"] floatValue];

}

static CGFloat widthCallback( void* ref ){

    return [(NSString*)[(NSDictionary*)ref objectForKey:@"width"] floatValue];

}

ascentCallback, descentCallback and widthCallback only read the respective properties from the passed NSDictionary and provide them to CT. What deallocCallback does is that it releases the dictionary holding the image information - it's called when the CTRunDelegate gets deallocated, so you have chance to do your memory management there.

Now that your parser is handling "img" tags, let's adjust also the CTView to render them. We need a method to send the images array to the view, let's combine setting the attributed string and the images into one method. Add the code:

//CTView.h - inside @interface declaration as an ivar

NSArray* images;

 

//CTView.h - declare property for images

@property (retain, nonatomic) NSArray* images;

 

//CTView.h - add a method declaration

-(void)setAttString:(NSAttributedString *)attString withImages:(NSArray*)imgs;

 

//CTView.m - just below @implementation

@synthesize images;

 

//CTView.m - inside the dealloc method

self.images = nil;

 

//CTView.m - anywhere inside the implementation

-(void)setAttString:(NSAttributedString *)string withImages:(NSArray*)imgs

{

    self.attString = string;

    self.images = imgs;

}

Now that CTView is prepared to accept an array with images, let's pass those from the parser to the view and render them!

Go to CoreTextMagazineViewController.m and find the line "[(CTView*)self.view setAttString: attString];" - replace it with the following:

[(CTView *)[self view] setAttString:attString withImages: p.images];

If you lookup attrStringFromMarkup: in the MarkupParser class you'll see it saves all the image tags data into self.images. This is what you are passing now directly to the CTView.

To render images we'll have to know the exact frame of the run where the image should appear. To find the origin of that spot we need to take in account several values:

       contentOffset when the content is scrolled

       the offset of CTView's frame (frameXOffset,frameYOffset)

       the CTLine origin coordinates (CTLine might have offset in the beginning of paragraphs for example)

       finally the distance between CTRun's origin and the origin of the CTLine

 

Let's render those images! First of all we need to update the CTColumnView class:

//inside CTColumnView.h

//as an ivar

NSMutableArray* images;

 

//as a property

@property (retain, nonatomic) NSMutableArray* images;

 

//inside CTColumnView.m

//after @implementation...

@synthesize images;

 

-(id)initWithFrame:(CGRect)frame

{

    if ([super initWithFrame:frame]!=nil) {

        self.images = [NSMutableArray array];

    }

    return self;

}

 

-(void)dealloc

{

    self.images= nil;

    [super dealloc];

}

 

//at the end of drawRect:

for (NSArray* imageData in self.images) {

    UIImage* img = [imageData objectAtIndex:0];

    CGRect imgBounds = CGRectFromString([imageData objectAtIndex:1]);

    CGContextDrawImage(context, imgBounds, img.CGImage);

}

So with this code update we add an ivar and a property called images where we will keep a list of the images appearing in each text column. To avoid declaring yet another new class to hold the image data insideimages we're going to store NSArray objects which will hold:

  1. a UIImage instance
  2. the image bounds - i.e. origin inside the text and size of the image

And now the code that calculates the images' position and attaches them to the respective text columns:

//inside CTView.h

-(void)attachImagesWithFrame:(CTFrameRef)f inColumnView:(CTColumnView*)col;

 

//inside CTView.m

-(void)attachImagesWithFrame:(CTFrameRef)f inColumnView:(CTColumnView*)col

{

    //drawing images

    NSArray *lines = (NSArray *)CTFrameGetLines(f); //1

 

    CGPoint origins[[lines count]];

    CTFrameGetLineOrigins(f, CFRangeMake(0, 0), origins); //2

 

    int imgIndex = 0; //3

    NSDictionary* nextImage = [self.images objectAtIndex:imgIndex];

    int imgLocation = [[nextImage objectForKey:@"location"] intValue];

 

    //find images for the current column

    CFRange frameRange = CTFrameGetVisibleStringRange(f); //4

    while ( imgLocation < frameRange.location ) {

        imgIndex++;

        if (imgIndex>=[self.images count]) return; //quit if no images for this column

        nextImage = [self.images objectAtIndex:imgIndex];

        imgLocation = [[nextImage objectForKey:@"location"] intValue];

    }

 

    NSUInteger lineIndex = 0;

    for (id lineObj in lines) { //5

        CTLineRef line = (CTLineRef)lineObj;

 

        for (id runObj in (NSArray *)CTLineGetGlyphRuns(line)) { //6

            CTRunRef run = (CTRunRef)runObj;

            CFRange runRange = CTRunGetStringRange(run);

 

            if ( runRange.location <= imgLocation && runRange.location+runRange.length > imgLocation ) { //7

                 CGRect runBounds;

                 CGFloat ascent;//height above the baseline

                 CGFloat descent;//height below the baseline

                 runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL); //8

                 runBounds.size.height = ascent + descent;

 

                 CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL); //9

                 runBounds.origin.x = origins[lineIndex].x + self.frame.origin.x + xOffset + frameXOffset;

                 runBounds.origin.y = origins[lineIndex].y + self.frame.origin.y + frameYOffset;

                 runBounds.origin.y -= descent;

 

                UIImage *img = [UIImage imageNamed: [nextImage objectForKey:@"fileName"] ];

                CGPathRef pathRef = CTFrameGetPath(f); //10

                CGRect colRect = CGPathGetBoundingBox(pathRef);

 

                CGRect imgBounds = CGRectOffset(runBounds, colRect.origin.x - frameXOffset - self.contentOffset.x, colRect.origin.y - frameYOffset - self.frame.origin.y);

                [col.images addObject: //11

                    [NSArray arrayWithObjects:img, NSStringFromCGRect(imgBounds) , nil]

                 ];

                //load the next image //12

                imgIndex++;

                if (imgIndex < [self.images count]) {

                    nextImage = [self.images objectAtIndex: imgIndex];

                    imgLocation = [[nextImage objectForKey: @"location"] intValue];

                }

 

            }

        }

        lineIndex++;

    }

}

Note: Ultimate credit goes to David Beck for his example on looping over runs, on which this code builds upon.
I know at first this code looks really hard-core, but bear with me - we're at the end of the tutorial; this is the final push for glory!

Let's go section by section:

  1. CTFrameGetLines gives you back an array of CTLine objects.
  2. You get the origins of all the current frame's lines - in short: you get a list of the top-left coordinates of all the text lines.
  3. You load nextImage with the attribute data of the first image in the text and imgLocation holds the string position of that same image.
  4. CTFrameGetVisibleStringRange gives you the visible text range for the frame you're rendering - i.e. you get which part of the text you render at present, then you loop trough the images array until you find the first image, which is in the frame you render at present. In other words - you fast-forward to the images relevant to the piece of text you are rendering at the moment.
  5. You loop trough the text's lines and loads each line into the "line" variable.
  6. You loop trough all the runs of that line (you get them by calling CTLineGetGlyphRuns).
  7. You check whether the next image is located inside the range of the present run - if so then you have to go on and proceed to rendering the image at this precise spot.
  8. You figure out the width and height of the run, by using the CTRunGetTypographicBounds method.
  9. You figure out the origin of the run, by using the CTLineGetOffsetForStringIndex and other offsets.
  10. You load the image with the given name in the variable "img" and get the rectangle of the current column and eventually the rectangle where the image needs to be rendered.
  11. You create an NSArray with the UIImage and the calculated frame for it, then you add it to the CTColumnView's image list
  12. In the end the next image in the list is loaded if one exists and the loop goes on over the text's lines.

OK! Great! Almost there - one tiny final step: find in CTView.m the line "[content setCTFrame:(id)frame];" and add below it:

[self attachImagesWithFrame:frame inColumnView: content];

Now you have all the code working, but you don't have great content to visualize...

No fear, I've prepared the next issue of Zombie Monthly - a montly popular magazine for zombies - for you; just do the following to import the content:

  1. In the project navigator delete test.txt that you created earlier
  2. Download and unarchive this file Zombie mag materials.
  3. Drag all the files over your Xcode project to import them. Make sure that "Copy items into destination group's folder (if needed)" is selected, and click Finish.

Then switch to CoreTextMagazineViewController.m and switch the line that gets the path to the file to use the new zombies.txt instead:

NSString *path = [[NSBundle mainBundle] pathForResource:@"zombies" ofType:@"txt"];

That's it - compile and run, and enjoy the latest issue of Zombie Monthly! :)

 

Just one final touch. Say we want to make the text in the columns justified so they fill the entire width of the column. Add the following code to make this happen:

//inside CTView.m

//at the end of the setAttString:withImages: method

 

CTTextAlignment alignment = kCTJustifiedTextAlignment;

 

CTParagraphStyleSetting settings[] = {

    {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment},

};

CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));

NSDictionary *attrDictionary = [NSDictionary dictionaryWithObjectsAndKeys:

                                (id)paragraphStyle, (NSString*)kCTParagraphStyleAttributeName,

                                nil];

 

NSMutableAttributedString* stringCopy = [[[NSMutableAttributedString alloc] initWithAttributedString:self.attString] autorelease];

[stringCopy addAttributes:attrDictionary range:NSMakeRange(0, [attString length])];

self.attString = (NSAttributedString*)stringCopy;

This should get you started with paragraph styling; lookup kCTParagraphStyleSpecifierAlignment in Apple's Core Text documentation to get to the list of all paragraph styles you can control.

When to use Core Text and why?

Now that your Magazine App with Core Text is finished you might be asking yourself: "Why would I use Core Text over UIWebView?"

Well both CT and UIWebView are useful in their own context.

Don't forget UIWebView is a full-blown web browser and using it to visualize a single multicolored line of text is a huge overkill.

Imagine you have 10 multicolor labels in your UI- this means you're going to consume the memory for 10 Safaris (ok, almost, but you get the point).

So, keep in mind: UIWebView is a great web browser for when you need one, while Core Text is an efficient text rendering engine.

Where To Go From Here?

Here's the complete Core Text example project we developed in the above tutorial.

If you want to keep playing around with this project to learn more about Core Text, read up in Apple's Core Text Reference Collection and see if you can add some of the following features to the app:

       Add more tags to the magazine engine parser

       Add support for more run styles

       Add more paragraph styles

       Add the ability to automatically apply styles to word bounds, paragraphs, sentences

       Enable ligatures and kerning - it's actually cool to enable ligatures and have a look at "if" and "fi" ;)

Since I know you are already thinking how to expand the parser engine beyond what I included in this short tutorial I have two recommendations for you:

       the fast lane to HTML parsing- use and extend Ben Reeves' Obj-C HTML Parser;

       or create your own grammar parser to do any wonder you come up with by using the Obj-C ParseKit

If you have any questions, comments, or suggestions, please join in the forum discussion below!

 

This is a blog post by iOS Tutorial Team member Marin Todorov, a software developer with 12+ years of experience, an independant iOS developer and the creator of Touch Code Magazine.

 

 

iPadCategory:

Tags: Core TextiOSiPadsample codetutorial

I'd love to hear your thoughts!

  

44 Comments!

 

 

http://www.raywenderlich.com/4147/how-to-create-a-simple-magazine-app-with-core-text

 

 

 

CoreText绘制基本知识

分类: Iphone开发入门2012-09-18 20:22 415人阅读 评论(0) 收藏 举报

stringalignmentquartzuiviewstylesnull

首先,进行创建一个UIView的子类,并实现如下代码:

- (void)drawRect:(CGRect)rect {
// Drawing code.

//创建要输出的字符串

[cpp] view plaincopy

  1. NSString *longText = @”袁唯来来 Lorem ipsum dolor sit amet, Before the iPad was released you had basically two ways how to get text on screen. Either you would stick with UILabel or UITextView provided by UIKit or if you felt hard-core you would draw the text yourself on the Quartz level incurring all the headaches induced by having to mentally switch between Objective-C and C API functions.  
  2. As of iOS 3.2 we gained a third alternative in Core Text promising full control over styles, thread safety and performance. However for most of my apps I did not want to break 3.x compatibility and so I procrastinated looking at this powerful new API. Apps running only on iPads could have made use of Core Text from day 1, but to me it made more sense supporting iPad via hybrid apps where the iPhone part would still be backwards compatible.  
  3. Now as the year has turned the adoption of 4.x on all iOS platforms is ever more accelerating. Many new iPads where found under the Christmas tree and by now even the most stubborn people (read needing 3.x for jailbreaking and sim-unlocking) have little reason to stick with 3.x. Thus we have almost no incentive left to stick with 3.x compatibility. Yay!”;  

 

//创建AttributeString
NSMutableAttributedString *string = [[NSMutableAttributedString alloc]
initWithString:longText];

//创建字体以及字体大小
CTFontRef helvetica = CTFontCreateWithName(CFSTR(”Helvetica”), 14.0, NULL);
CTFontRef helveticaBold = CTFontCreateWithName(CFSTR(”Helvetica-Bold”), 14.0, NULL);

//添加字体 目标字符串从下标0开始到字符串结尾
[string addAttribute:(id)kCTFontAttributeName
value:(id)helvetica
range:NSMakeRange(0, [string length])];

//添加字体 目标字符串从下标0开始,截止到4个单位的长度
[string addAttribute:(id)kCTFontAttributeName
value:(id)helveticaBold
range:NSMakeRange(0, 4)];

//添加字体 目标字符串从下标6开始,截止到5个单位长度
[string addAttribute:(id)kCTFontAttributeName
value:(id)helveticaBold
range:NSMakeRange(6, 5)];

//添加字体 目标字符串从下标109开始,截止到9个单位长度
[string addAttribute:(id)kCTFontAttributeName
value:(id)helveticaBold
range:NSMakeRange(109, 9)];

//添加字体 目标字符串从下标223开始,截止到6个单位长度
[string addAttribute:(id)kCTFontAttributeName
value:(id)helveticaBold
range:NSMakeRange(223, 6)];

//添加颜色,目标字符串从下标0开始,截止到4个单位长度
[string addAttribute:(id)kCTForegroundColorAttributeName
value:(id)[UIColor blueColor].CGColor
range:NSMakeRange(0, 4)];

//添加过程同上
[string addAttribute:(id)kCTForegroundColorAttributeName
value:(id)[UIColor redColor].CGColor
range:NSMakeRange(18, 3)];

[string addAttribute:(id)kCTForegroundColorAttributeName
value:(id)[UIColor greenColor].CGColor
range:NSMakeRange(657, 6)];

[string addAttribute:(id)kCTForegroundColorAttributeName
value:(id)[UIColor blueColor].CGColor
range:NSMakeRange(153, 6)];

//创建文本对齐方式
CTTextAlignment alignment = kCTLeftTextAlignment;//左对齐 kCTRightTextAlignment为右对齐
CTParagraphStyleSetting alignmentStyle;
alignmentStyle.spec=kCTParagraphStyleSpecifierAlignment;//指定为对齐属性
alignmentStyle.valueSize=sizeof(alignment);
alignmentStyle.value=&alignment;

//创建文本行间距
CGFloat lineSpace=5.0f;//间距数据
CTParagraphStyleSetting lineSpaceStyle;
lineSpaceStyle.spec=kCTParagraphStyleSpecifierLineSpacing;//指定为行间距属性
lineSpaceStyle.valueSize=sizeof(lineSpace);
lineSpaceStyle.value=&lineSpace;

//创建样式数组
CTParagraphStyleSetting settings[]={
alignmentStyle,lineSpaceStyle
};

//设置样式
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings));

//给字符串添加样式attribute
[string addAttribute:(id)kCTParagraphStyleAttributeName
value:(id)paragraphStyle
range:NSMakeRange(0, [string length])];

// layout master
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(
(CFAttributedStringRef)string);

CGMutablePathRef leftColumnPath = CGPathCreateMutable();
CGPathAddRect(leftColumnPath, NULL,
CGRectMake(0, 0,
self.bounds.size.width,
self.bounds.size.height));

CTFrameRef leftFrame = CTFramesetterCreateFrame(framesetter,
CFRangeMake(0, 0),
leftColumnPath, NULL);

// flip the coordinate system
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

// draw
CTFrameDraw(leftFrame, context);

// cleanup

[cpp] view plaincopy

  1. CGPathRelease(leftColumnPath);  
  2. CFRelease(framesetter);  
  3. CFRelease(helvetica);  
  4. CFRelease(helveticaBold);  
  5. [string release];  
  6. UIGraphicsPushContext(context);  

 

}

iOS 定制应用程序字体

分类: Iphone开发入门2012-09-19 12:29 127人阅读 评论(0) 收藏 举报

iosquartzuiview文档ttfapplication

使用Quartz Core绘制文字非常简单,苹果的Quartz 2D参考中演示了如何使用CGContextShowTextAtPoint函数绘制文本。不幸的是,这个函数不支持Unicode字符的绘制(这个函数只支持MacRoman一种编码)。如果你使用中文、日文等亚洲字体,那么就不得不悲催了。

许多童鞋肯定会被文档中的这句话所吸引:

“如果想使用MacRoman以外的文本编码,⋯⋯调用CGContextShowGlyphsAtPoint替代CGContextShowTextAtPoint。”

如果你采用这种办法,那么另一种悲剧就产生了。

一、CGContextShowGlyphsAtPoint的挑战

CGContextShowGlyphsAtPoint 函数的第4个参数要使用到CGGlyph数组。意思是你需要自己把unichar字符自已映射成字体文件中的Glyph(字符图形,有称字模、字符点阵)索引。如果你不使用私有框架,那么这是一个几乎不可能完成的任务。

实际上,大部分程序员会建议你用UIKit框架或者[NSString drawAtPoint:]方法而不是NSStringCGContextShowGlyphsAtPoint来绘制unicode字符。

如果你真是只是想在UIView上绘制几句文本,那么drawAtPoint能够满足你的需要,下面的内容你也不必要看了。

但是,CGContextShowGlyphsAtPoint就真的没有任何用处了吗?

实际上,苹果最初设计这个API的目的,是为了提供给Application使用定制字体的能力。基本上iOS提供的系统字体非常少,我们在一些app中为了实现一些效果(比如让字体更加清晰)不得不使用自己的字体文件,这个时候这些API就显得非常必要。

然而,关于使用自定字体,苹果的文档描述得非常少。我们知道的仅仅是CGContextSetFont和CGContextSetFontSize、CGContextShowGlyphsAtPoint等3个Quartz函数。所有的google文档都语焉不详。显然,真正要想使用自定字体,根本没有这么简单。

那么真正的挑战在哪里?

StackOverFlow上有人发了一个帖子,帖子问到如何用绘制日文字符(中文也是一样)。其中 Brad Larson 的回复描述得很精确:

You may also be able to print custom characters withCGContextShowGlyphsAtPoint(), but the challenge is generating the glyphs on theiPhone. This post has an example: gogo-robot.com/devblog/?p=5 – Brad Larson

真正的挑战是unicharàglyph,也就是 CGContextShowGlyphsAtPoint的第4个参数如何获得。Brad提到的例子实际上描述了一种不可能的情况,他假设字体文件中的glyph编码是在unicode编码的基础上加一个固定的偏移量(他称之为Magic Number)构成。实际上对于某个.ttf/.otf字体文件,我们无法获得这个MagicNumber,我们不能靠猜测来编写程序代码。

那么我们只有自己解析字体文件的数据结构了。万幸的是,Zynga Game Networks的 Kevin Ballard实现了一个unichar到glyph的映射。项目地址:https://github.com/zynga/FontLabel

我们可以利用这个实现来解析cmap为format4和format12的字体文件。

关于cmap的内容,请参考True Type 字体文件相关文档(微软和苹果的网站)。

二、实现过程

1、在程序中加载ttf文件

首先要解决的问题,是在程序中加载.ttf/otf文件。我们在示例程序的资源束中加入了一个“方正大黑简体.ttf”的文件。这个文件是我从Mac系统中搜索到的,应该是MicrosoftOffice中提供的字体文件。我们用以下代码来加载它:

NSString *fontPath = [[NSBundlemainBundle] pathForResource:@"方正大黑简体" ofType:@"ttf"];

    CGDataProviderReffontDataProvider = CGDataProviderCreateWithFilename([fontPath UTF8String]);

    font_ref =CGFontCreateWithDataProvider(fontDataProvider);

    currentTable=readFontTableFromCGFont(font_ref);

    CGDataProviderRelease(fontDataProvider);

提示:在iOS3.2以后,还可以在plist文件中添加 UIAppFonts键的方式添加自定字体。见苹果文档“Custom Font Support”主题。

2、从字体文件中获取Glyph

这部分的实现来自Ballard的FontLabel,非常感谢!整个实现主要包含2个函数定义:

staticfontTable*readFontTableFromCGFont(CGFontRef font);

staticvoidmapCharactersToGlyphsInFont(constfontTable *table, unichar characters[], size_t charLen,CGGlyph outGlyphs[], size_t *outGlyphLen) ;

第一个函数用于从CGFont中读取cmap表,第二个函数用于将unichar数组映射为Glyphs数组(实现cmap format4和format12子表)。

3、绘制字符

Quartz绘图是在UIView的drawRect方法中进行,字符也不例外。下面是字符绘制代码:

CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextClearRect(context, rect);

    CGContextSetFont(context, font_ref);

    CGContextSetTextDrawingMode(context, kCGTextFillStroke);

    CGContextSetFillColorWithColor(context, fontColor.CGColor);

    UIColor * strokeColor = [UIColorblackColor];

    CGContextSetStrokeColorWithColor(context, strokeColor.CGColor);

    CGContextSetFontSize(context, 24.0f);

   

    CGGlyph glyphs[[self.textlength]];

    size_t glyphCount;

    unichar textChars[[textlength]];

    [textgetCharacters:textChars range:NSMakeRange(0, text.length)];

    mapCharactersToGlyphsInFont(currentTable, textChars, text.length, glyphs, &glyphCount);

    CGAffineTransform textTransform = CGAffineTransformMake(1.0, 0.0, 0.0, -1.0,0.0, 0.0);

    CGContextSetTextMatrix(context,textTransform);

    CGContextShowGlyphsAtPoint(context, 20, 30, glyphs, [self.textlength]);

代码中先使用Ballard实现的mapCharactersToGlyphsInFont函数将unicode字符转换为Glyphs数组,然后用CGContextShowGlyphsAtPoint进行绘制。

注意:由于Quartz空间和用户空间的坐标系统不同(Quartz空间坐标原点位于屏幕左下角,用户空间坐标原点位于屏幕左上角,二者坐标转换只需y坐标取反即可),所以我们使用了一个渐变反射来进行坐标转换并应用于文字矩阵。

4、运行效果

图片

5、源代码下载

整个示例工程见资源下载:http://download.csdn.net/detail/kmyhy/4359481

http://blog.csdn.net/kmyhy/article/details/7643568

原文地址:https://www.cnblogs.com/liyufeng2013/p/3332625.html