Core Text 程式設計指南

薛定諤發表於2019-01-30

介紹

原文連結: 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。

Core Text 程式設計指南

字型物件

字型物件(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 。

Core Text 程式設計指南

你可以將 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)")
}
複製程式碼

相關文章