1import CoreText
2import Foundation
3
4struct Run: Codable {
5 let text: String
6 let font: String
7 let size: Double
8 let bold: Bool
9 let italic: Bool
10}
11
12struct Case: Decodable {
13 let id: String
14 let width: Double
15 let runs: [Run]
16}
17
18guard CommandLine.arguments.count >= 2 else {
19 fatalError("Expected a text-case JSON file and optional font files")
20}
21for path in CommandLine.arguments.dropFirst(2) {
22 var error: Unmanaged<CFError>?
23 guard CTFontManagerRegisterFontsForURL(URL(fileURLWithPath: path) as CFURL, .process, &error) else {
24 throw error!.takeRetainedValue()
25 }
26}
27let data = try Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[1]))
28let cases = try JSONDecoder().decode([Case].self, from: data)
29var results: [[String: Any]] = []
30for item in cases {
31 precondition(item.width.isFinite && item.width > 0 && !item.runs.isEmpty)
32 let text = NSMutableAttributedString(string: "")
33 var emptyFont: CTFont?
34 var resolvedFonts: [String] = []
35 for run in item.runs {
36 precondition(run.size.isFinite && run.size > 0)
37 let base = CTFontCreateWithName(run.font as CFString, run.size, nil)
38 var traits: CTFontSymbolicTraits = []
39 if run.bold { traits.insert(.boldTrait) }
40 if run.italic { traits.insert(.italicTrait) }
41 let font = CTFontCreateCopyWithSymbolicTraits(base, run.size, nil, traits, [.boldTrait, .italicTrait]) ?? base
42 emptyFont = emptyFont ?? font
43 resolvedFonts.append(CTFontCopyPostScriptName(font) as String)
44 text.append(NSAttributedString(string: run.text, attributes: [
45 NSAttributedString.Key(kCTFontAttributeName as String): font,
46 ]))
47 }
48 let typesetter = CTTypesetterCreateWithAttributedString(text)
49 var start = 0
50 var lines: [[String: Any]] = []
51 repeat {
52 let count = text.length == 0 ? 0 : CTTypesetterSuggestLineBreak(typesetter, start, item.width)
53 precondition(count > 0 || text.length == 0, "Typesetter made no progress")
54 let line = CTTypesetterCreateLine(typesetter, CFRange(location: start, length: count))
55 var ascent: CGFloat = 0
56 var descent: CGFloat = 0
57 var leading: CGFloat = 0
58 let advance = CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
59 if text.length == 0 {
60 ascent = CTFontGetAscent(emptyFont!)
61 descent = CTFontGetDescent(emptyFont!)
62 leading = CTFontGetLeading(emptyFont!)
63 }
64 let faces = (CTLineGetGlyphRuns(line) as! [CTRun]).map { run -> [String: Any] in
65 let attributes = CTRunGetAttributes(run) as NSDictionary
66 let font = attributes[kCTFontAttributeName] as! CTFont
67 var result: [String: Any] = ["face": CTFontCopyPostScriptName(font) as String,
68 "size": CTFontGetSize(font)]
69 if let version = CTFontCopyName(font, kCTFontVersionNameKey) {
70 result["version"] = version as String
71 }
72 return result
73 }
74 lines.append([
75 "start_utf16": start, "end_utf16": start + count,
76 "text": (text.string as NSString).substring(with: NSRange(location: start, length: count)),
77 "advance": advance, "trailing_whitespace": CTLineGetTrailingWhitespaceWidth(line),
78 "ascent": ascent, "descent": descent, "leading": leading,
79 "runs": faces,
80 ])
81 start += count
82 } while start < text.length
83 results.append(["id": item.id, "width": item.width,
84 "requested_runs": try JSONSerialization.jsonObject(with: JSONEncoder().encode(item.runs)),
85 "requested_faces_resolved": resolvedFonts, "lines": lines])
86}
87let output: [String: Any] = [
88 "engine": "Core Text typesetter", "os": ProcessInfo.processInfo.operatingSystemVersionString,
89 "units": "points", "cases": results,
90]
91FileHandle.standardOutput.write(try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys]))