| 1 | import CoreText |
| 2 | import Foundation |
| 3 | |
| 4 | struct Run: Codable { |
| 5 | let text: String |
| 6 | let font: String |
| 7 | let size: Double |
| 8 | let bold: Bool |
| 9 | let italic: Bool |
| 10 | } |
| 11 | |
| 12 | struct Case: Decodable { |
| 13 | let id: String |
| 14 | let width: Double |
| 15 | let runs: [Run] |
| 16 | } |
| 17 | |
| 18 | guard CommandLine.arguments.count >= 2 else { |
| 19 | fatalError("Expected a text-case JSON file and optional font files") |
| 20 | } |
| 21 | for 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 | } |
| 27 | let data = try Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[1])) |
| 28 | let cases = try JSONDecoder().decode([Case].self, from: data) |
| 29 | var results: [[String: Any]] = [] |
| 30 | for 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 | } |
| 87 | let output: [String: Any] = [ |
| 88 | "engine": "Core Text typesetter", "os": ProcessInfo.processInfo.operatingSystemVersionString, |
| 89 | "units": "points", "cases": results, |
| 90 | ] |
| 91 | FileHandle.standardOutput.write(try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys])) |