Core Text 编程指南

8,898 阅读28分钟

介绍

原文链接: Core Text Programming Guide

重要:此文档不再更新,有关 Apple SDKs 的最新信息,访问 documentation website.

Core Text 是一种用于处理字体和文本布局的底层高级技术,自 Mac OS X v10.5 和 iOS 3.2 开始引入,你可以从所有 iOS 及 OS X 的开发环境中使用其 API。

重要:Core Text 是为一些必须处理底层字体处理和文字布局的开发者准备,如无必要,你应该使用 TextKit(Text Programming Guide for iOS)、CocoaText(Cocoa Text Architecture Guide)等框架开发你的 App 或 Mac 应用。Core Text 是以上两种文本框架的底层实现,因此它们的速度和效率是共享的。除此之外,以上两种文本框架提供了富文本编辑及页面布局引擎。如果你的 App 只使用 Core Text,则需要为其提供其他的基础实现。

省略了一些摘要和相关文档,基本后面都会提到,有需要看原文。

概述

Core Text 直接与 Core Graphics(Quartz)协调作业。Quartz 是一个图形渲染引擎,可以在 iOS 及 OS X 中处理最底层的二维图像生成。

Core Text 是为高级框架中的文本布局及字体处理功能提供支持的中间层,Quartz 则为所有的文本和字体框架提供更为底层的支持,Quartz 作用于文本的字形(CTGlyphInfo)和位置,而 Core Text 清楚如何为字符匹配字体,在调用 Quartz 绘制文本之前,它会处理文本的样式,字体规格和其他属性,Quartz 是获取基本级别绘制字形的唯一方式。因为 Core Text 直接提供了 Quartz 可用数据,因此其文本绘制性能很高。

如果客户端不改变线程之间共享的任何参数(如 attributed strings),则可以同时从多个线程调用 Core Text 函数。

Core Text 是基于 C 的跨平台 API

Core Text API 在 iOS 和 OS X 上几乎相同,不过 OS X 版本上提供了更加丰富的字体管理 API,包括可变字体集合。虽然 API 相差不多,但是 UIKit 和 AppKit 之间存在差异,如在平台之间移植代码时需要考虑到这些差异。例如,你需要一个 Quartz 图形上下文来绘制 Core Text 生成的字形,而你在每个平台上获得的图形上下文并非一样。因为 iOS 中绘制的视图是UIView,OS X 中则是NSView。你需要知道的是CGRect对象是传递给UIView drawRect:方法的,而 OS X 版本drawRect:是传递给NSRect对象的。(OS X 中使用NSRectToCGRect函数可将传入的NSRect对象转换为CGRect 作为 Core Text 函数参数所需的对象。)

UIView函数UIGraphicsGetCurrentContext返回的图形上下文相对于未经修改的 Quartz 图形上下文(UIView返回的图形上下文,原点在左上角)进行了翻转,因此在 iOS 中你需要翻转 Quartz 图形上下文,而在 OS X 中则不必如此,有关此技术的示例代码,参阅布局一个段落小节。

Core Text 使用了与 OS X 和 iOS 中其他核心框架相同的约定,并且尽可能的使用系统数据类型和服务,举例来说,Core Text 中许多入参和返回值是 Core Foundation 对象。因此你可以将它们存储在 Core Foundation 集合类中,Core Text 使用的其他对象,如CGPath对象,实际由 Core Graphics 提供支持。

Core Text 对象是 C 的不透明类型

为了速度和简洁性,iOS 和 OS X 中的许多底层库使用 C 编写。因此使用 Core Text 时,你需要使用使用 C 函数,例如CTFramesetterCreateWithAttributedStringCTFramesetterCreateFrame,而不是 OC 的类与方法。

Core Tex 不透明类型

Core Text 的布局作业通常需要由属性字符串(CFAttributedStringRef)和图形路径(CGPathRef)共同完成,CFAttributedStringRef 包含需要绘制的字符串、字符的样式属性(如颜色和字体)。Core Text 中的排版机制使用其中的信息,完成字符到字形的转换。

CGPathRef 定义了文本绘制区域的形状。在 OS X 10.7 和 iOS 3.2 及更高版本中,路径可以是非矩形的。

CFAttributedString 的引用类型 CFAttributedStringRef 可与 NSAttributedString 桥接,这意味着在函数中,你可以在 Core Foundation 类型和桥接类型间进行转换。因此,在看到参数为NSAttributedString *的方法中,可以传入CFAttributedStringRef,在看到参数为CFAttributedStringRef的函数中,可以传入NSAttributedString及其子类的实例。(你可能需要将一种类型转换为另一种类型,以消除编译器警告。)

attributes 是定义字符串中字符样式的一组键值对,你可以创建 CFDictionary 对象来保存要应用的 attributes,字符串中的字符按相同的 attributes 进行分组,称为字形组(CTRun)。在创建一个 AttributedString 时,可将 attributes 作为参数传递。或者你也可以将 attributeds 应用于已经存在的 CFMutableAttributedString 对象中,虽然 CFDictionaryRef 和 NSDictionary 可以桥接,但是存储在其中的某个对象可能不行。

CoreText 的运行时层次结构如下图所示,顶部是 CTFramesetterRef,输入 CFAttributedStringRef 和 CGPathRef,CTFramesetterRef 生成一个或多个 CTFrameRef,每个 CTFrameRef 表示一个段落。

为了生成 CTFrameRef,CTFramesetterRef 调用一个 CTTypesetterRef 对象,当 CTTypesetterRef 在CTFrameRef 中放置文本时,CTFramesetterRef 对 CTFrameRef 应用段落样式(CTParagraphStyle),包括对齐、制表符、行间距、缩进、换行等属性。CTTypesetterRef 将字符串转换成 CTRun,并将其填充到 CTLine。

每个 CTFrame 对象包含段落的 CTLine 对象。每个 CTLine 对象表示一行文本。一个 CTFrame 对象可能只包含一个 CTLine 对象,也可能是一组。CTLine 对象是在 CTFramesetterRef 创建 CTFrameRef 时被创建,CTLine 对象可以直接绘制在图形上下文中。

CTLine 包含一个 CTRun 数组,CTRun 是一组具有相同 attributes 和绘制方向的连续字形,CTTypesetterRef 在从 CFAttributedString、attributes 和 CTFont 生成 CTLine 时,同时生成 CTRuns。如果需要,CTRun 可以将自己绘制在图形上下文中,不过大多数时候,你不需要直接操作 CTRun。

字体对象

字体对象(CTFont)可以帮助确定字形之间的相对位置,并提供在图形上下文绘制时使用的字体。Core Text 不透明类型 CTFont 是一个封装了很多信息的具体字体实例。它的引用类型 CTFontRef 可以桥接 UIFont 和 NSFont。当你创建一个 CTFont 对象,你通常需要指定(或使用默认)字号和转换模型(transformation matrix),以提供 CTFont 具体的特征。然后,你可以在 CTFont 对象中查询关于此字号下字体的许多信息,例如character-to-glyph mapping, encodings, font metric data, glyph data等。font metric data包括ascent, descent, leading, cap height, x-height等参数。glyph data包括bounding rectangle、glyph advance等参数。

以上参数无法理解的可参考下图,与原文无关。

font-metric-data

CTFont 对象是不可变的,因此它们可以在多个操作,队列或线程中同时使用。创建 CTFont 对象有许多方法。首选方法是使用字体描述CTFontCreateWithFontDescriptor。当然你也可以使用其他备选的API,这取决于你现在有什么样的数据。例如,你可以使用字体的 PostScript 名称(CTFontCreateWithName)或 CGFont(CTFontCreateWithGraphicsFont)。还有CTFontCreateUIFontForLanguage,它将引用你本地化应用中交互界面中的字体。

CTFont 引用提供了一种称为字体级联(font cascading)的复杂的自动替换字体机制,该机制会在考虑字体特征的同时,选择适当的字体来替代缺失的字体。字体级联基于级联列表(cascading list),级联列表是一个有序的字体描述(Font Descriptors)数组。有一个系统默认级联列表(多态的,基于用户的语言设置和当前字体的)和在字体创建时指定的字体级联列表。使用字体描述中的信息,级联机制可以根据样式和匹配字符匹配字体。CTFontCreateForString函数使用级联列表来选择合适的字体来编码给定的字符串。要指定和检索字体级联列表,请使用kCTFontCascadeListAttribute属性。

字体描述

字体描述(CTFontDescriptor)不透明类型提供了一种完全由属性字典描述字体的机制,以及一种易于使用的,用于构建新字体的字体匹配工具,你可以从一个 CTFontDescriptor 中创建一个 CTFont,也可以从一个 CTFont 中获得一个 CTFontDescriptor,你还可以更改 CTFontDescriptor 并使用它来创建新的字体对象,或者创建 CTFontDescriptor 并指定部分属性,例如family name、weight,就可以找到系统中与之匹配的所有字体。CTFontDescriptorRef类型可以和UIFontDescriptorNSFontDescriptor桥接。

使用 CTFontDescriptor,就无需处理复杂的转换模型(transformation matrix),你可以创建一个字体属性字典,属性包括PostScript name、font family、style,以及特征(traits)(例如,粗体或斜体),用它创建 CTFontDescriptor 对象。而后使用 CTFontDescriptor 创建 CTFont 对象。CTFontDescriptor 可以序列化并存储,我们可以借此持久化字体。下图演示了字体系统使用 CTFontDescriptor 创建 CTFont 。

你可以将 CTFontDescriptor 视为对字体系统的查询条件。创建具有不完整描述的 CTFontDescriptor,即在属性字典中使用一个或几个值,字体系统将从可用的字体中选择最合适的字体。例如,如果你指定使用family查询,而不指定standard faces(normal, bold, italic, bold italic),则会匹配family中所有 standard faces,但是如果指定kCTFontTraitsAttributekCTFontTraitBold,结果将收缩到符合bold trait的字体。系统通过CTFontDescriptorCreateMatchingFontDescriptors提供与查询匹配的字体描述的完整列表。

在iOS 6.0及更高版本中,应用程序可以按需使用CTFontDescriptorMatchFontDescriptorsWithProgressHandler下载安装未安装的可用字体。以这种方式下载的字体不会永久安装,系统可能会在特定情况下将其删除。可供下载的字体在iOS 6:字体列表iOS 7:字体列表中作为附加信息。DownloadFont 示例(在 iOS Developer Library 中)演示了这项技术。OS X 中不需要按需下载字体,因为所有可用字体已随系统一起安装。

字体集合

字体集合是由一组 CTFontDescriptor 组成的单个对象。字体集合由 CTFontCollection 不透明类型表示。字体集合提供了字体枚举、全局和自定义 CTFontCollection 访问,以及访问该 CTFontCollection 中 CTFontDescriptors 的功能。例如,你可以通过CTFontCollectionCreateFromAvailableFonts创建系统中所有可用字体的CTFontCollection,并可以使用该 CTFontCollection 获取所有 CTFontDescriptors。

常见的文本布局操作

本章介绍了一些常规文本布局操作,以及如何使用 Core Text 编码实现。

布局一个段落

排版中最常见的操作之一是在任意大小的矩形区域内布局多行的段落。Core Text 使此操作变得简单,只需要几行特定的 Core Text 的代码。如要布局段落,你需要获得绘制图形上下文(CGContext),文本布局路径(CGPath)以及属性字符串(CFAttributedString),文本布局路径则需要一个矩形路径(CGReact)。这个例子中大多数代码都需要创建和初始化上下文,路径和字符串。完成此操作后,Core Text 只需要三行代码即可完成布局。

以下代码显示了段落是如何布局的。此代码可以在UIViewNSView)的drawRect:中执行。

// Initialize a graphics context in iOS.
CGContextRef context = UIGraphicsGetCurrentContext();
 
// Flip the context coordinates, in iOS only.
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
 
// Initializing a graphic context in OS X is different:
// CGContextRef context =
//     (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
 
// Set the text matrix.
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
 
// Create a path which bounds the area where you will be drawing text.
// The path need not be rectangular.
CGMutablePathRef path = CGPathCreateMutable();
 
// In this simple example, initialize a rectangular path.
CGRect bounds = CGRectMake(10.0, 10.0, 200.0, 200.0);
CGPathAddRect(path, NULL, bounds );
 
// Initialize a string.
CFStringRef textString = CFSTR("Hello, World! I know nothing in the world that has as much power as a word. Sometimes I write one, and I look at it, until it begins to shine.");
 
// Create a mutable attributed string with a max length of 0.
// The max length is a hint as to how much internal storage to reserve.
// 0 means no hint.
CFMutableAttributedStringRef attrString =
         CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
 
// Copy the textString into the newly created attrString
CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0),
         textString);
 
// Create a color that will be added as an attribute to the attrString.
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat components[] = { 1.0, 0.0, 0.0, 0.8 };
CGColorRef red = CGColorCreate(rgbColorSpace, components);
CGColorSpaceRelease(rgbColorSpace);
 
// Set the color of the first 12 chars to red.
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 12),
         kCTForegroundColorAttributeName, red);
 
// Create the framesetter with the attributed string.
CTFramesetterRef framesetter =
         CTFramesetterCreateWithAttributedString(attrString);
CFRelease(attrString);
 
// Create a frame.
CTFrameRef frame = CTFramesetterCreateFrame(framesetter,
          CFRangeMake(0, 0), path, NULL);
 
// Draw the specified frame in the given context.
CTFrameDraw(frame, context);
 
// Release the objects we used.
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);
// 获取图形上下文。
guard let context = UIGraphicsGetCurrentContext() else { return }

// 翻转上下文坐标,仅iOS。
context.translateBy(x: 0, y: self.bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)

// 设置文本绘制的矩形
context.textMatrix = .identity

// 创建一个绘制文本的区域的路径 不必是矩形
let path = CGMutablePath()

// 初始化一个矩形路径。
let bounds = CGRect(x: 10.0, y: 10.0, width: 200.0, height: 200.0)
path.addRect(bounds, transform: .identity)

// 初始化一个字符串
let textString = "Hello, World! I know nothing in the world that has as much power as a word. Sometimes I write one, and I look at it, until it begins to shine." as CFString

// 创建一个最大长度为0的可变属性字符串
// kCFAllocatorDefault表示要一个内存分配器
// 0表示最大长度
guard let attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0) else { return }

// 将textString复制到新创建的attrString中
CFAttributedStringReplaceString(attrString, CFRangeMake(0, 0), textString)

// 创建一个将添加到attrString的颜色属性。
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
var components: [CGFloat] = [1.0, 0.0, 0.0, 0.8]
let red = CGColor(colorSpace: rgbColorSpace, components: &components)

// 将前12个字符的颜色设置为红色.
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 12), kCTForegroundColorAttributeName, red)

// 使用属性字符串创建framesetter。
let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)

// 创建一个frame
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)

// 在给定的frame绘制上下文
CTFrameDraw(frame, context)

简单的文字标签

还有一种常见的排版操作是绘制单行文本以用作于用户界面元素的标签(label)。在 Core Text 中,这只需要两行代码:一行创建具有 CFAttributedString 的 CTLine 对象,另一行将 CTLine 绘制到图形上下文中。

以下代码展示了如何在UIViewNSViewdrawRect:方法中完成此操作。代码中省略了plain text string、font、graphics context等已在本文档其他小节中展示过的操作,展示了如何创建属性字典并用其创建attributed string。(字体创建展示在创建字体描述和根据字体描述创建字体小节。)

CFStringRef string; CTFontRef font; CGContextRef context;
// Initialize the string, font, and context
 
CFStringRef keys[] = { kCTFontAttributeName };
CFTypeRef values[] = { font };
 
CFDictionaryRef attributes =
    CFDictionaryCreate(kCFAllocatorDefault, (const void**)&keys,
        (const void**)&values, sizeof(keys) / sizeof(keys[0]),
        &kCFTypeDictionaryKeyCallBacks,
        &kCFTypeDictionaryValueCallBacks);
 
CFAttributedStringRef attrString =
    CFAttributedStringCreate(kCFAllocatorDefault, string, attributes);
CFRelease(string);
CFRelease(attributes);
 
CTLineRef line = CTLineCreateWithAttributedString(attrString);
 
// Set text position and draw the line into the graphics context
CGContextSetTextPosition(context, 10.0, 10.0);
CTLineDraw(line, context);
CFRelease(line);
// 初始化 string, font, and context
let string: CFString = "Hello, World! I know nothing in the world that has as much power as a word" as CFString
let font: CTFont = CTFontCreateUIFontForLanguage(.label, 28, nil)!
let context: CGContext = UIGraphicsGetCurrentContext()!

// 1
// let attributes = [kCTFontAttributeName : font] as CFDictionary

// 2
let key = UnsafeMutablePointer<CFString>.allocate(capacity: 1)
key.initialize(to: kCTFontAttributeName)
let keyPointer = unsafeBitCast(key, to: UnsafeMutablePointer<UnsafeRawPointer?>.self)
defer {
    keyPointer.deinitialize(count: 1)
    keyPointer.deallocate()
}

let value = UnsafeMutablePointer<CTFont>.allocate(capacity: 1)
value.initialize(to: font)
let valuePointer = unsafeBitCast(value, to: UnsafeMutablePointer<UnsafeRawPointer?>.self)
defer {
    valuePointer.deinitialize(count: 1)
    valuePointer.deallocate()
}

guard let attributes = CFDictionaryCreate(kCFAllocatorDefault, keyPointer, valuePointer, 1, nil, nil) else {
    debugPrint("attributes create fail")
    return
}

guard let attributeString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes) else {
    debugPrint("attributeString create fail")
    return
}

let line = CTLineCreateWithAttributedString(attributeString)
context.textPosition = CGPoint(x: 100, y: 100)
CTLineDraw(line, context)

多列布局

还有一种常见的排版操作是在多个列中布局文本。严格的讲,Core Text 本身一次只显示一列,并不计算列的尺寸或位置。不过在调用 Core Text 布局文本之前,计算列的路径区域,可以使文本在列中绘制,以此达到多列的文本,在这个示例中,Core Text 除了布局每一列文本外,还为每一列提供了字符串的子范围。

以下代码中的createColumnsWithColumnCount:方法接受列数作为参数,并返回一个路径数组,每个路径代表一列。

- (CFArrayRef)createColumnsWithColumnCount:(int)columnCount
{
    int column;
 
    CGRect* columnRects = (CGRect*)calloc(columnCount, sizeof(*columnRects));
    // Set the first column to cover the entire view.
    columnRects[0] = self.bounds;
 
    // Divide the columns equally across the frame's width.
    CGFloat columnWidth = CGRectGetWidth(self.bounds) / columnCount;
    for (column = 0; column < columnCount - 1; column++) {
        CGRectDivide(columnRects[column], &columnRects[column],
                     &columnRects[column + 1], columnWidth, CGRectMinXEdge);
    }
 
   // Inset all columns by a few pixels of margin.
    for (column = 0; column < columnCount; column++) {
        columnRects[column] = CGRectInset(columnRects[column], 8.0, 15.0);
    }
 
    // Create an array of layout paths, one for each column.
    CFMutableArrayRef array =
                     CFArrayCreateMutable(kCFAllocatorDefault,
                                  columnCount, &kCFTypeArrayCallBacks);
 
    for (column = 0; column < columnCount; column++) {
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, columnRects[column]);
        CFArrayInsertValueAtIndex(array, column, path);
        CFRelease(path);
    }
    free(columnRects);
    return array;
}
func createColumns(withColumnCount columnCount: Int) -> CFArray? {
    let columnWidth = CGFloat(bounds.width / CGFloat(columnCount))
    var remainder = bounds

    // 桥接转换
//        var paths = [CGMutablePath]()
//        for _ in 0 ..< columnCount {
//            let (slice, remainded) = remainder.divided(atDistance: columnWidth, from: .minXEdge)
//            let columnReact = slice.insetBy(dx: 8, dy: 15)
//            remainder = remainded
//            let path = CGMutablePath()
//            path.addRect(columnReact, transform: .identity)
//            paths.append(path)
//        }
//        return paths as CFArray

    // 指针创建
    let allocator = CFAllocatorGetDefault()?.takeUnretainedValue()
    let array = CFArrayCreateMutable(allocator, columnCount, nil)

    for _ in 0 ..< columnCount {
        let (slice, remainded) = remainder.divided(atDistance: columnWidth, from: .minXEdge)
        let columnReact = slice.insetBy(dx: 8, dy: 15)
        remainder = remainded
        let path = CGMutablePath()
        path.addRect(columnReact, transform: .identity)
        let manager = Unmanaged.passRetained(path)
        CFArrayAppendValue(array, manager.toOpaque())
    }
    return array
}

以下代码在 UIView(NSView)的drawRect:中,该方法调用了createColumnsWithColumnCount,这个类包含一个 attributedString 属性,需要你自己定义。

// Override drawRect: to draw the attributed string into columns.
// (In OS X, the drawRect: method of NSView takes an NSRect parameter,
//  but that parameter is not used in this listing.)
- (void)drawRect:(CGRect)rect
{
    // Initialize a graphics context in iOS.
    CGContextRef context = UIGraphicsGetCurrentContext();
 
    // Flip the context coordinates in iOS only.
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
 
    // Initializing a graphic context in OS X is different:
    // CGContextRef context =
    //     (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
 
    // Set the text matrix.
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
 
    // Create the framesetter with the attributed string.
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(
                                      (CFAttributedStringRef)self.attributedString);
 
    // Call createColumnsWithColumnCount function to create an array of
    // three paths (columns).
    CFArrayRef columnPaths = [self createColumnsWithColumnCount:3];
 
    CFIndex pathCount = CFArrayGetCount(columnPaths);
    CFIndex startIndex = 0;
    int column;
 
    // Create a frame for each column (path).
    for (column = 0; column < pathCount; column++) {
        // Get the path for this column.
        CGPathRef path = (CGPathRef)CFArrayGetValueAtIndex(columnPaths, column);
 
        // Create a frame for this column and draw it.
        CTFrameRef frame = CTFramesetterCreateFrame(
                             framesetter, CFRangeMake(startIndex, 0), path, NULL);
        CTFrameDraw(frame, context);
 
        // Start the next frame at the first character not visible in this frame.
        CFRange frameRange = CTFrameGetVisibleStringRange(frame);
        startIndex += frameRange.length;
        CFRelease(frame);
 
    }
    CFRelease(columnPaths);
    CFRelease(framesetter);
 
}
guard let context = UIGraphicsGetCurrentContext() else { return }
let text = "replace that, use long long long text" as CFString
guard let attributedString = CFAttributedStringCreate(kCFAllocatorDefault, text, nil) else {
    debugPrint("create attributedString fail")
    return
}
context.translateBy(x: 0, y: self.bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
context.textMatrix = .identity
let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
guard let columPaths = self.createColumns(withColumnCount: 3) else {
    debugPrint("columPaths create fail")
    return
}
let pathCount = CFArrayGetCount(columPaths)
var startIndex = 0
for i in 0 ..< pathCount {
    guard let pointer = CFArrayGetValueAtIndex(columPaths, i) else {
        debugPrint("columPaths index \(i) load fail")
        continue
    }
    let path = Unmanaged<CGMutablePath>.fromOpaque(pointer)
    let frame = CTFramesetterCreateFrame(framesetter, .init(location: startIndex, length: 0), path.takeUnretainedValue(), nil)
    CTFrameDraw(frame, context)
    startIndex += CTFrameGetVisibleStringRange(frame).length
    path.release()
}

手动换行

在Core Text中,除非你有特殊的断字过程或类似的需求,否则不需要手动换行。framesetter可以自动换行。不过Core Text 也可以让你准确的指定在哪里中断一行文本。以下代码展示了如何创建 typesetter 以及直接使用 typesetter 查找换行符并手动创建 typeset line。这个示例还展示了如何在绘制之前使行居中。

此代码可以在UIViewNSView)的drawRect:方法中。未显示代码中使用的变量的初始化。

double width; CGContextRef context; CGPoint textPosition; CFAttributedStringRef attrString;
// Initialize those variables.
 
// Create a typesetter using the attributed string.
CTTypesetterRef typesetter = CTTypesetterCreateWithAttributedString(attrString);
 
// Find a break for line from the beginning of the string to the given width.
CFIndex start = 0;
CFIndex count = CTTypesetterSuggestLineBreak(typesetter, start, width);
 
// Use the returned character count (to the break) to create the line.
CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(start, count));
 
// Get the offset needed to center the line.
float flush = 0.5; // centered
double penOffset = CTLineGetPenOffsetForFlush(line, flush, width);
 
// Move the given text drawing position by the calculated offset and draw the line.
CGContextSetTextPosition(context, textPosition.x + penOffset, textPosition.y);
CTLineDraw(line, context);
 
// Move the index beyond the line break.
start += count;
guard let context = UIGraphicsGetCurrentContext() else { return }
let text = "Hello, World!Hello, World!Hello, World!" as CFString
let attributeds = [kCTFontAttributeName: CTFontCreateUIFontForLanguage(.label, 28, nil)!, kCTBackgroundColorAttributeName: UIColor.red.cgColor] as CFDictionary
guard let attributedString = CFAttributedStringCreate(kCFAllocatorDefault, text, attributeds) else { return }
context.translateBy(x: 0, y: self.bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
let width: CGFloat = bounds.size.width / 1.5
let textPosition: CGPoint = .init(x: 0, y: 100)
// 使用CFAttributedString创建CTTypesetter
let typesetter = CTTypesetterCreateWithAttributedString(attributedString)
// 寻找从字符串开头到给定宽度的换行符(这个宽度可以绘制多少字)
var start = 0
let count = CTTypesetterSuggestLineBreak(typesetter, start, width.native)
// 根据返回的字数创建行
let line = CTTypesetterCreateLine(typesetter, .init(location: start, length: count))
let flush: CGFloat = 0.5
// 计算使行居中所需的偏移量
let penOffset = CTLineGetPenOffsetForFlush(line, flush, bounds.size.width.native)
// 根据计算的偏移量移动 textPosition 并绘制行。
context.textPosition = .init(x: textPosition.x.native + penOffset, y: textPosition.y.native)
CTLineDraw(line, context)
// 将索引移动到换行处
start += count

应用段落样式

applyParaStyle方法实现了一个将段落样式应用于attributed string。该方法接收字体,字号和行间距等参数,行间距会增加或减少line之间的距离。

NSAttributedString* applyParaStyle(
                CFStringRef fontName , CGFloat pointSize,
                NSString *plainText, CGFloat lineSpaceInc){
 
    // Create the font so we can determine its height.
    CTFontRef font = CTFontCreateWithName(fontName, pointSize, NULL);
 
    // Set the lineSpacing.
    CGFloat lineSpacing = (CTFontGetLeading(font) + lineSpaceInc) * 2;
 
    // Create the paragraph style settings.
    CTParagraphStyleSetting setting;
 
    setting.spec = kCTParagraphStyleSpecifierLineSpacing;
    setting.valueSize = sizeof(CGFloat);
    setting.value = &lineSpacing;
 
    CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(&setting, 1);
 
    // Add the paragraph style to the dictionary.
    NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
                               (__bridge id)font, (id)kCTFontNameAttribute,
                               (__bridge id)paragraphStyle,
                               (id)kCTParagraphStyleAttributeName, nil];
    CFRelease(font);
    CFRelease(paragraphStyle);
 
    // Apply the paragraph style to the string to created the attributed string.
    NSAttributedString* attrString = [[NSAttributedString alloc]
                               initWithString:(NSString*)plainText
                               attributes:attributes];
 
    return attrString;
}
func applyParaStyle(fontName: CFString, pointSize: CGFloat, plainText: String, lineSpaceInc: CGFloat) -> NSAttributedString {
    // 创建字体以确定高度
    let font = CTFontCreateWithName(fontName, pointSize, nil)
    // 计算lineSpacing
    var lineSpace = (CTFontGetLeading(font) + lineSpaceInc) * 2
    // 创建段落样式
    let valueSize = MemoryLayout<CGFloat>.stride
    var setting =  CTParagraphStyleSetting(spec: .lineSpacingAdjustment, valueSize: valueSize, value: &lineSpace)
    let paragtaphStyle = CTParagraphStyleCreate(&setting, 1)
    // 创建attributed string。设置字体和段落样式。
    let attrString = NSAttributedString(string: plainText, attributes: [
        .font: font,
        .paragraphStyle: paragtaphStyle,
        ])
    return attrString
}

以下代码调用applyParaStyle,该方法创建纯文本字符串,使用applyParaStyle方法创建具有段落属性的attributed string,然后创建 framesetter 和 frame,并绘制 frame。

- (void)drawRect:(CGRect)rect {
    // Initialize a graphics context in iOS.
    CGContextRef context = UIGraphicsGetCurrentContext();
 
    // Flip the context coordinates in iOS only.
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
 
    // Set the text matrix.
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
 
    CFStringRef fontName = CFSTR("Didot Italic");
    CGFloat pointSize = 24.0;
 
    CFStringRef string = CFSTR("Hello, World! I know nothing in the world that has
                                   as much power as a word. Sometimes I write one,
                                   and I look at it, until it begins to shine.");
 
    // Apply the paragraph style.
    NSAttributedString* attrString = applyParaStyle(fontName, pointSize, string, 50.0);
 
    // Put the attributed string with applied paragraph style into a framesetter.
    CTFramesetterRef framesetter =
             CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
 
    // Create a path to fill the View.
    CGPathRef path = CGPathCreateWithRect(rect, NULL);
 
    // Create a frame in which to draw.
    CTFrameRef frame = CTFramesetterCreateFrame(
                                    framesetter, CFRangeMake(0, 0), path, NULL);
 
    // Draw the frame.
    CTFrameDraw(frame, context);
    CFRelease(frame);
    CGPathRelease(path);
    CFRelease(framesetter);
}
// 在iOS中初始化上下文。
guard let context = UIGraphicsGetCurrentContext() else { return }
// 仅在iOS中翻转上下文坐标
context.translateBy(x: 0, y: self.bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
// 设置文本矩阵(就是设置字符绘制的方向,以免字符上下或左右翻转,因为在iOS上Core Text和Core Graphicsz坐标系不同)
context.textMatrix = .identity
// 创建有段落样式的attributed string
let string = "Hello, World! I know nothing in the world that has as much power as a word. Sometimes I write one, and I look at it, until it begins to shine."
let attString = applyParaStyle(fontName: "Didot-Italic" as CFString, pointSize: 24, plainText: string, lineSpaceInc: 50)
// 根据attributed string创建framesetter
let framesetter = CTFramesetterCreateWithAttributedString(attString as CFAttributedString)
// 创建要填充的区域的路径
let path = CGPath(rect: rect, transform: nil)
// 创建绘制区域
let frame = CTFramesetterCreateFrame(framesetter, .init(location: 0, length: 0), path, nil)
// 绘制
CTFrameDraw(frame, context)

在 OS X中,NSViewdrawRect:方法接收一个 NSRec 对象,但是CGPathCreateWithRect方法需要一个 CGRect 对象。因此,必须使用下面的方法将 NSRect 对象转换为 CGRect 对象:

CGRect myRect = NSRectToCGRect([self bounds]);

此外,在 OS X 中,获取图形上下文的方式不同,不需要翻转坐标。

在非矩形区域绘制文本

在非矩形区域中绘制文本的难点在于描述非矩形路径。以下代码的AddSquashedDonutPath方法返回一个环形路径。有了路径后,只需调用常用的 Core Text 函数即可应用属性并绘制。

// Create a path in the shape of a donut.
static void AddSquashedDonutPath(CGMutablePathRef path,
              const CGAffineTransform *m, CGRect rect)
{
    CGFloat width = CGRectGetWidth(rect);
    CGFloat height = CGRectGetHeight(rect);
 
    CGFloat radiusH = width / 3.0;
    CGFloat radiusV = height / 3.0;
 
    CGPathMoveToPoint( path, m, rect.origin.x, rect.origin.y + height - radiusV);
    CGPathAddQuadCurveToPoint( path, m, rect.origin.x, rect.origin.y + height,
                               rect.origin.x + radiusH, rect.origin.y + height);
    CGPathAddLineToPoint( path, m, rect.origin.x + width - radiusH,
                               rect.origin.y + height);
    CGPathAddQuadCurveToPoint( path, m, rect.origin.x + width,
                               rect.origin.y + height,
                               rect.origin.x + width,
                               rect.origin.y + height - radiusV);
    CGPathAddLineToPoint( path, m, rect.origin.x + width,
                               rect.origin.y + radiusV);
    CGPathAddQuadCurveToPoint( path, m, rect.origin.x + width, rect.origin.y,
                               rect.origin.x + width - radiusH, rect.origin.y);
    CGPathAddLineToPoint( path, m, rect.origin.x + radiusH, rect.origin.y);
    CGPathAddQuadCurveToPoint( path, m, rect.origin.x, rect.origin.y,
                               rect.origin.x, rect.origin.y + radiusV);
    CGPathCloseSubpath( path);
 
    CGPathAddEllipseInRect( path, m,
                            CGRectMake( rect.origin.x + width / 2.0 - width / 5.0,
                            rect.origin.y + height / 2.0 - height / 5.0,
                            width / 5.0 * 2.0, height / 5.0 * 2.0));
}
 
// Generate the path outside of the drawRect call so the path is calculated only once.
- (NSArray *)paths
{
    CGMutablePathRef path = CGPathCreateMutable();
    CGRect bounds = self.bounds;
    bounds = CGRectInset(bounds, 10.0, 10.0);
    AddSquashedDonutPath(path, NULL, bounds);
 
    NSMutableArray *result =
              [NSMutableArray arrayWithObject:CFBridgingRelease(path)];
    return result;
}
 
- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
 
    // Initialize a graphics context in iOS.
    CGContextRef context = UIGraphicsGetCurrentContext();
 
    // Flip the context coordinates in iOS only.
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
 
    // Set the text matrix.
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
 
    // Initialize an attributed string.
    CFStringRef textString = CFSTR("Hello, World! I know nothing in the world that
    has as much power as a word. Sometimes I write one, and I look at it,
    until it begins to shine.");
 
    // Create a mutable attributed string.
     CFMutableAttributedStringRef attrString =
                CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
 
    // Copy the textString into the newly created attrString.
    CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), textString);
 
    // Create a color that will be added as an attribute to the attrString.
    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat components[] = { 1.0, 0.0, 0.0, 0.8 };
    CGColorRef red = CGColorCreate(rgbColorSpace, components);
    CGColorSpaceRelease(rgbColorSpace);
 
    // Set the color of the first 13 chars to red.
    CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 13),
                                     kCTForegroundColorAttributeName, red);
 
    // Create the framesetter with the attributed string.
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
 
    // Create the array of paths in which to draw the text.
    NSArray *paths = [self paths];
 
    CFIndex startIndex = 0;
 
    // In OS X, use NSColor instead of UIColor.
    #define GREEN_COLOR [UIColor greenColor]
    #define YELLOW_COLOR [UIColor yellowColor]
    #define BLACK_COLOR [UIColor blackColor]
 
    // For each path in the array of paths...
    for (id object in paths) {
        CGPathRef path = (__bridge CGPathRef)object;
 
        // Set the background of the path to yellow.
        CGContextSetFillColorWithColor(context, [YELLOW_COLOR CGColor]);
 
        CGContextAddPath(context, path);
        CGContextFillPath(context);
 
        CGContextDrawPath(context, kCGPathStroke);
 
        // Create a frame for this path and draw the text.
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter,
                                         CFRangeMake(startIndex, 0), path, NULL);
        CTFrameDraw(frame, context);
 
        // Start the next frame at the first character not visible in this frame.
        CFRange frameRange = CTFrameGetVisibleStringRange(frame);
        startIndex += frameRange.length;
        CFRelease(frame);
}
 
CFRelease(attrString);
CFRelease(framesetter);
}
// 创建一个环形路径
func addSquashedDonut(path: CGMutablePath, transform: CGAffineTransform, rect: CGRect) {
    let width = rect.size.width
    let height = rect.size.height
    let radiusH: CGFloat = width / 3.0
    let radiusV: CGFloat = height / 3.0
    path.move(to: .init(x: rect.origin.x, y: rect.origin.y + height - radiusV), transform: transform)
    path.addQuadCurve(to: .init(x: rect.origin.x + radiusH, y: rect.origin.y + height), control: .init(x: rect.origin.x, y: rect.origin.y + height), transform: transform)
    path.addLine(to: .init(x: rect.origin.x + width - radiusH, y: rect.origin.y + height), transform: transform)
    path.addQuadCurve(to: .init(x: rect.origin.x + width, y: rect.origin.y + height - radiusV), control: .init(x: rect.origin.x + width, y: rect.origin.y + height), transform: transform)
    path.addLine(to: .init(x: rect.origin.x + width, y: rect.origin.y + radiusV), transform: transform)
    path.addQuadCurve(to: .init(x: rect.origin.x + width - radiusH, y: rect.origin.y), control: .init(x: rect.origin.x + width, y: rect.origin.y), transform: transform)
    path.addLine(to: .init(x: rect.origin.x + radiusH, y: rect.origin.y), transform: transform)
    path.addQuadCurve(to: .init(x: rect.origin.x, y: rect.origin.y + radiusV), control: .init(x: rect.origin.x, y: rect.origin.y), transform: transform)
    path.closeSubpath()
    path.addEllipse(in: .init(x: rect.origin.x + width / 2.0 - width / 5.0, y: rect.origin.y + height / 2.0 - height / 5.0, width: width / 5.0 * 2.0, height: height / 5.0 * 2.0), transform: transform)
}

func paths() -> [CGMutablePath] {
    let path = CGMutablePath()
    var bounds = self.bounds
    bounds = bounds.insetBy(dx: 10, dy: 10)
    addSquashedDonut(path: path, transform: .identity, rect: bounds)
    return [path]
}

func f() {
    // 在iOS中初始化上下文。
    guard let context = UIGraphicsGetCurrentContext() else { return }
    // 仅在iOS中翻转上下文坐标
    context.translateBy(x: 0, y: self.bounds.size.height)
    context.scaleBy(x: 1.0, y: -1.0)
    // 设置文本矩阵(就是设置字符绘制的方向,以免字符上下或左右翻转,因为在iOS上Core Text和Core Graphicsz坐标系不同)
    context.textMatrix = .identity
    let string = "Hello, World! I know nothing in the world that has as much power as a word. Sometimes I write one, and I look at it, until it begins to shine."
    guard let attString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0) else { return }
    CFAttributedStringReplaceString(attString, .init(location: 0, length: 0), string as CFString)
    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
    var components: [CGFloat] = [1.0, 0.0, 0.0, 0.8]
    guard let red = CGColor(colorSpace: rgbColorSpace, components: &components) else { return }
    // 将前13个字符设置为红色
    CFAttributedStringSetAttribute(attString, .init(location: 0, length: 13), kCTForegroundColorAttributeName, red)
    let framesetter = CTFramesetterCreateWithAttributedString(attString)
    let paths = self.paths()
    var startIndex = 0
    for path in paths {
        // 路径的背景色设置为黄色
        context.setFillColor(UIColor.yellow.cgColor)
        context.addPath(path)
        context.fillPath()
        context.drawPath(using: .stroke)
        let frame = CTFramesetterCreateFrame(framesetter, .init(location: startIndex, length: 0), path, nil)
        CTFrameDraw(frame, context)
        let frameRange = CTFrameGetVisibleStringRange(frame)
        startIndex += frameRange.length
    }
}

常见字体操作

本章介绍了一些常见的字体处理操作,并展示了如何使用 Core Text 编码实现。这些操作在 iOS 和 OS X 上是相同的。

创建字体描述

下面的示例函数根据字体名称和字号创建字体描述。

CTFontDescriptorRef CreateFontDescriptorFromName(CFStringRef postScriptName,
                                                 CGFloat size)
{
   return CTFontDescriptorCreateWithNameAndSize(postScriptName, size);
}
func CreateFontDescriptorFromName(postScriptName: CFString, size: CGFloat) -> CTFontDescriptor {
    return CTFontDescriptorCreateWithNameAndSize(postScriptName, size)
}

下面的示例函数根据 font family 和字体特征创建字体描述。

NSString* familyName = @"Papyrus";
CTFontSymbolicTraits symbolicTraits = kCTFontTraitCondensed;
CGFloat size = 24.0;
 
NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
[attributes setObject:familyName forKey:(id)kCTFontFamilyNameAttribute];
 
// The attributes dictionary contains another dictionary, the traits dictionary,
// which in this example specifies only the symbolic traits.
NSMutableDictionary* traits = [NSMutableDictionary dictionary];
[traits setObject:[NSNumber numberWithUnsignedInt:symbolicTraits]
                                           forKey:(id)kCTFontSymbolicTrait];
 
[attributes setObject:traits forKey:(id)kCTFontTraitsAttribute];
[attributes setObject:[NSNumber numberWithFloat:size]
                                         forKey:(id)kCTFontSizeAttribute];
 
CTFontDescriptorRef descriptor =
             CTFontDescriptorCreateWithAttributes((CFDictionaryRef)attributes);
CFRelease(descriptor);
let familyName = "Papyrus"
let symbolicTraits: CTFontSymbolicTraits = .traitCondensed
let size: CGFloat = 24.0

var attributes: [AnyHashable : Any] = [:]
attributes[kCTFontFamilyNameAttribute] = familyName
// attributes字典中包含traits字典
// 本例中只指定字体特征。
var traits: [AnyHashable : Any] = [:]
traits[kCTFontSymbolicTrait] = NSNumber(value: symbolicTraits.rawValue)
attributes[kCTFontTraitsAttribute] = traits
attributes[kCTFontSizeAttribute] = NSNumber(value: Float(size))
let descriptor = CTFontDescriptorCreateWithAttributes(attributes as CFDictionary)
print(descriptor)

根据字体描述创建字体

以下代码展示了如何创建字体描述并使用它创建字体。当调用CTFontCreateWithFontDescriptor时,通常会传给 matrix 参数NULL,以指定默认(identity)矩阵。CTFontCreateWithFontDescriptor的 size 和 matrix 会覆盖字体描述中的值,除非字体描述未指定 size 和 matrix。

NSDictionary *fontAttributes =
                  [NSDictionary dictionaryWithObjectsAndKeys:
                          @"Courier", (NSString *)kCTFontFamilyNameAttribute,
                          @"Bold", (NSString *)kCTFontStyleNameAttribute,
                          [NSNumber numberWithFloat:16.0],
                          (NSString *)kCTFontSizeAttribute,
                          nil];
// Create a descriptor.
CTFontDescriptorRef descriptor =
          CTFontDescriptorCreateWithAttributes((CFDictionaryRef)fontAttributes);
 
// Create a font using the descriptor.
CTFontRef font = CTFontCreateWithFontDescriptor(descriptor, 0.0, NULL);
CFRelease(descriptor);
let fontAttributes = [
    kCTFontFamilyNameAttribute: "Courier",
    kCTFontStyleNameAttribute: "Bold",
    kCTFontSizeAttribute: NSNumber(value: 16.0)
] as CFDictionary
// 创建字体描述
let descriptor = CTFontDescriptorCreateWithAttributes(fontAttributes)
// 根据字体描述创建字体
let font = CTFontCreateWithFontDescriptor(descriptor, 0.0, nil)
print(font)

创建类似字体

将一个已经存在的字体转换为相关或类似字体非常实用。以下代码中的示例函数展示了如何利用函数调用传入Boolean值使字体加粗或取消加粗。如果当前 font family 没有要求的 font,函数返回NULL

CTFontRef CreateBoldFont(CTFontRef font, Boolean makeBold)
{
    CTFontSymbolicTraits desiredTrait = 0;
    CTFontSymbolicTraits traitMask;
 
    // If requesting that the font be bold, set the desired trait
    // to be bold.
    if (makeBold) desiredTrait = kCTFontBoldTrait;
 
    // Mask off the bold trait to indicate that it is the only trait
    // to be modified. As CTFontSymbolicTraits is a bit field,
    // could change multiple traits if desired.
    traitMask = kCTFontBoldTrait;
 
    // Create a copy of the original font with the masked trait set to the
    // desired value. If the font family does not have the appropriate style,
    // returns NULL.
 
    return CTFontCreateCopyWithSymbolicTraits(font, 0.0, NULL, desiredTrait, traitMask);
}
func CreateBoldFont(font: CTFont, makeBold: Bool) -> CTFont? {
    // CTFontSymbolicTraits是一个OptionSet(选择集合),如果需要,可以指定多个特征

    // 需要修改的trait集合(相当于keys)
    let traitMask: CTFontSymbolicTraits = [.boldTrait]
    // traitMask中trait的值,两者结合可以增加或去除trait(相当于values)
    var desiredTrait = CTFontSymbolicTraits.init(rawValue: 0)

    // 如果要求字体加粗,设置trait为bold
    if makeBold {
        desiredTrait = .boldTrait
    }

    // 创建原始字体的副本,如无匹配trait的字体,返回nil
    return CTFontCreateCopyWithSymbolicTraits(font, 0.0, nil, desiredTrait, traitMask)
}

以下代码中的示例函数将传入一个给定的字体,返回另一个 font family 中相似的字体,如果可能,保留原字体的 trait。这个函数可能返回NULL。将 size 传入0.0,matrix 传入NULL,可以使返回的字体 size 等同于原字体。

CTFontRef CreateFontConvertedToFamily(CTFontRef font, CFStringRef family)
{
    // Create a copy of the original font with the new family. This call
    // attempts to preserve traits, and may return NULL if that is not possible.
    // Pass in 0.0 and NULL for size and matrix to preserve the values from
    // the original font.
 
    return CTFontCreateCopyWithFamily(font, 0.0, NULL, family);
}
func CreateFontConvertedToFamily(font: CTFont, family: CFString) -> CTFont? {
    return CTFontCreateCopyWithFamily(font, 0, nil, family)
}

字体序列化

以下代码中的示例函数展示了如何创建一个 XML,并使用其序列化一个可以在文档中使用的字体。或者你也可以使用NSArchiver完成同样的效果。这只是将创建一个确切字体所需要的数据进行存储的一种方法。

CFDataRef CreateFlattenedFontData(CTFontRef font)
{
    CFDataRef           result = NULL;
    CTFontDescriptorRef descriptor;
    CFDictionaryRef     attributes;
 
    // Get the font descriptor for the font.
    descriptor = CTFontCopyFontDescriptor(font);
 
    if (descriptor != NULL) {
        // Get the font attributes from the descriptor. This should be enough
        // information to recreate the descriptor and the font later.
        attributes = CTFontDescriptorCopyAttributes(descriptor);
 
        if (attributes != NULL) {
            // If attributes are a valid property list, directly flatten
            // the property list. Otherwise we may need to analyze the attributes
            // and remove or manually convert them to serializable forms.
            // This is left as an exercise for the reader.
           if (CFPropertyListIsValid(attributes, kCFPropertyListXMLFormat_v1_0)) {
                result = CFPropertyListCreateXMLData(kCFAllocatorDefault, attributes);
            }
        }
    }
    return result;
}
func CreateFlattenedFontData(font: CTFont) -> Unmanaged<CFData>? {
    // 根据字体获取字体描述
    let descriptor = CTFontCopyFontDescriptor(font)
    // 根据字体描述获取属性字典
    let attributes = CTFontDescriptorCopyAttributes(descriptor)

    if CFPropertyListIsValid(attributes, .xmlFormat_v1_0) {
        // 如果属性列表有效,可直接将其序列化
        return CFPropertyListCreateXMLData(kCFAllocatorDefault, attributes)
    }
    else {
        // 否则可能需要分析其中某个属性,将其丢弃或转为可序列化的数据类型
    }
    return nil
}

字体反序列化

以下代码中的示例函数展示了如何从 XML 数据中反序列出字体的属性字典,并利用属性字典创建一个字体引用。

CTFontRef CreateFontFromFlattenedFontData(CFDataRef iData)
{
    CTFontRef           font = NULL;
    CFDictionaryRef     attributes;
    CTFontDescriptorRef descriptor;
 
    // Create our font attributes from the property list.
    // For simplicity, this example creates an immutable object.
    // If you needed to massage or convert certain attributes
    // from their serializable form to the Core Text usable form,
    // do it here.
    attributes =
          (CFDictionaryRef)CFPropertyListCreateFromXMLData(
                               kCFAllocatorDefault,
                               iData, kCFPropertyListImmutable, NULL);
    if (attributes != NULL) {
        // Create the font descriptor from the attributes.
        descriptor = CTFontDescriptorCreateWithAttributes(attributes);
        if (descriptor != NULL) {
            // Create the font from the font descriptor. This sample uses
            // 0.0 and NULL for the size and matrix parameters. This
            // causes the font to be created with the size and/or matrix
            // that exist in the descriptor, if present. Otherwise default
            // values are used.
            font = CTFontCreateWithFontDescriptor(descriptor, 0.0, NULL);
        }
    }
    return font;
}
func CreateFontFromFlattenedFontData(data: CFData) -> CTFont? {
    let immutable = CFPropertyListMutabilityOptions.mutableContainers.rawValue
    guard let attributesUnm = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, data, immutable, nil) else {
        return nil
    }
    let attributes = attributesUnm.takeRetainedValue() as! CFDictionary
//        defer {
//            attributesUnm.release()
//        }
    let descriptor = CTFontDescriptorCreateWithAttributes(attributes)
    let font = CTFontCreateWithFontDescriptor(descriptor, 0, nil)
    return font
}

修改字距

连字(Ligatures)和字距默认是开启的,可通过将kCTKernAttributeName设置为 0 禁用,以下代码为绘制的前几个字符设置了较大的字距。

 // Set the color of the first 13 characters to red
 // using a previously defined red CGColor object.
 CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 13),
                                      kCTForegroundColorAttributeName, red);
 
 // Set kerning between the first 18 chars to be 20
 CGFloat otherNum = 20;
 CFNumberRef otherCFNum = CFNumberCreate(NULL, kCFNumberCGFloatType, &otherNum);
 CFAttributedStringSetAttribute(attrString, CFRangeMake(0,18),
                                           kCTKernAttributeName, otherCFNum);	
let attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0)
CFAttributedStringReplaceString(attributedString, .init(location: 0, length: 0), "Hello, World! I know nothing in the world that has as much power as a word." as CFString)
CFAttributedStringSetAttribute(attributedString, .init(location: 0, length: 0), kCTForegroundColorAttributeName, UIColor.red.cgColor)
var num: CGFloat = 20
let cfNum = CFNumberCreate(kCFAllocatorNull, .cgFloatType, &num)
CFAttributedStringSetAttribute(attributedString, .init(location: 0, length: 18), kCTKernAttributeName, cfNum)

从字符获取字形

以下代码展示了如何从一个只有一个字体的stringcharacters中获取字形(glyphs),大部分情况下,你应该从 CTLine 中获取这些信息,因为string中可能包含不止一种字体。此外,对于比较复杂的文本绘制而言,简单的character to glyphs不能得到预期的外观,如果你希望使用一种字体,显示特定的Unicode字符(Characters),这种字符到字形的映射是适合的。

void GetGlyphsForCharacters(CTFontRef font, CFStringRef string)
{
    // Get the string length.
    CFIndex count = CFStringGetLength(string);
 
    // Allocate our buffers for characters and glyphs.
    UniChar *characters = (UniChar *)malloc(sizeof(UniChar) * count);
    CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * count);
 
    // Get the characters from the string.
    CFStringGetCharacters(string, CFRangeMake(0, count), characters);
 
    // Get the glyphs for the characters.
    CTFontGetGlyphsForCharacters(font, characters, glyphs, count);
 
    // Do something with the glyphs here. Characters not mapped by this font will be zero.
    // ...
 
    // Free the buffers
    free(characters);
    free(glyphs);
}
func GetGlyphsForCharacters(font: CTFont, string: CFString) {
    let count = CFStringGetLength(string)
    let characters = UnsafeMutablePointer<UniChar>.allocate(capacity: count)
    defer {
        characters.deinitialize(count: count)
        characters.deallocate()
    }
    let glyphs = UnsafeMutablePointer<CGGlyph>.allocate(capacity: count)
    defer {
        glyphs.deinitialize(count: count)
        glyphs.deallocate()
    }
    CFStringGetCharacters(string, .init(location: 0, length: count), characters)
    CTFontGetGlyphsForCharacters(font, characters, glyphs, count)
    print("characters: \(characters.pointee)")
    print("glyphs: \(glyphs.pointee)")
}