Last active
July 13, 2018 02:43
-
-
Save leonbreedt/ec3fc3ce76b8231dfa99 to your computer and use it in GitHub Desktop.
Render font from command line on OS X without requiring font to be installed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// FontPreview | |
// Copyright © 2018 Leon Breedt | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
// | |
import AppKit | |
import WebKit | |
import Darwin | |
let kFontSampleBoxWidth = 400 | |
let kFontSampleBoxSpacing = 5 | |
let kDefaultFontSize: Float = 14.0 | |
let kFontSampleText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." | |
class TextRendererView : NSView { | |
let primaryFontFilePath: String | |
let fontSize: Float | |
let additionalFontFilePaths: [String] | |
var pdfData: NSData? | |
private var textFontsByFilePath: [String: CTFont] | |
private var fontsByFilePath: [String: NSFont] | |
init(frame: NSRect, primaryFontFilePath: String, fontSize: Float, additionalFontFilePaths: [String]) { | |
self.primaryFontFilePath = primaryFontFilePath | |
self.fontSize = fontSize | |
self.additionalFontFilePaths = additionalFontFilePaths | |
let allFontFilePaths = [primaryFontFilePath] + additionalFontFilePaths | |
let textFontsByFilePath = Dictionary(uniqueKeysWithValues: zip(allFontFilePaths, allFontFilePaths.map { TextRendererView.loadFont($0, size: fontSize) })) | |
self.fontsByFilePath = Dictionary(uniqueKeysWithValues: textFontsByFilePath.map { ($0.key, textFontsByFilePath[$0.key]! as NSFont) }) | |
self.textFontsByFilePath = textFontsByFilePath | |
self.pdfData = nil | |
super.init(frame: frame) | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) not supported") | |
} | |
var text: String? { | |
didSet { | |
setNeedsDisplay(frame) | |
} | |
} | |
override func draw(_ dirtyRect: NSRect) { | |
let consumerData = NSMutableData() | |
let consumer = CGDataConsumer(data: consumerData)! | |
var rect = CGRect(x: dirtyRect.origin.x, | |
y: dirtyRect.origin.y, | |
width: dirtyRect.width, | |
height: dirtyRect.height) | |
guard let context = CGContext(consumer: consumer, mediaBox: &rect, nil) else { | |
fatalError("failed to create PDF context") | |
} | |
context.textMatrix = .identity | |
context.beginPDFPage(nil) | |
var boxLeft = kFontSampleBoxSpacing | |
for filePath in [primaryFontFilePath] + additionalFontFilePaths { | |
guard let font = fontsByFilePath[filePath] else { | |
fatalError("failed to load font for \(filePath)") | |
} | |
print("drawing text with font: \(font.fontName)") | |
let attributedString = NSAttributedString(string: text ?? "", attributes: [.font: font]) | |
let framesetter = CTFramesetterCreateWithAttributedString(attributedString) | |
let path = CGMutablePath() | |
let bounds = CGRect(x: boxLeft, y: 0, width: kFontSampleBoxWidth, height: 400) | |
path.addRect(bounds) | |
let frame = CTFramesetterCreateFrame(framesetter, CFRange(location: 0, length: 0), path, nil) | |
CTFrameDraw(frame, context) | |
boxLeft += kFontSampleBoxWidth + kFontSampleBoxSpacing | |
} | |
context.endPDFPage() | |
context.closePDF() | |
pdfData = consumerData | |
} | |
private static func loadFont(_ path: String, size: Float) -> CTFont { | |
print("loading font \(path) using Core Text") | |
guard let data = NSData(contentsOfFile: path) else { | |
fatalError("failed to load font from \(path)") | |
} | |
guard let provider = CGDataProvider(data: data) else { | |
fatalError("failed to create Core Graphs data provider for font data") | |
} | |
guard let font = CGFont(provider) else { | |
fatalError("failed to create Core Graphics font") | |
} | |
var error: Unmanaged<CFError>? | |
if !CTFontManagerRegisterGraphicsFont(font, &error) { | |
fatalError("failed to register font: \(error!)") | |
} | |
return CTFontCreateWithGraphicsFont(font, CGFloat(size), nil, nil) | |
} | |
} | |
// Application | |
let args = Array(CommandLine.arguments[1..<CommandLine.arguments.count]) | |
if args.count < 3 { | |
print("usage: FontPreview PDF-FILE-NAME FONT-SIZE FONT-FILE-PATH [COMPARE-FONT-FILE-PATH ...]") | |
exit(1) | |
} | |
let (pdfFilePath, fontSize, primaryFontFilePath) = | |
(args[0], Float(args[1]) ?? kDefaultFontSize, args[2]) | |
let comparisonFontFilePaths: [String] = | |
args.count > 3 | |
? Array(args[3..<args.count]) | |
: [] | |
let application = NSApplication.shared | |
let rect = NSRect(x: -1024, y: -1024, width: 1024, height: 1024) | |
let window = NSWindow(contentRect: rect, | |
styleMask: .borderless, | |
backing: .buffered, | |
defer: true) | |
window.makeKey() | |
let totalWidth = kFontSampleBoxSpacing + kFontSampleBoxWidth + | |
(kFontSampleBoxWidth * comparisonFontFilePaths.count) + | |
(kFontSampleBoxSpacing * comparisonFontFilePaths.count) + | |
kFontSampleBoxSpacing | |
let rendererFrame = NSRect(x: 0, y: 0, width: totalWidth, height: 400) | |
let rendererView = TextRendererView(frame: rendererFrame, | |
primaryFontFilePath: primaryFontFilePath, | |
fontSize: fontSize, | |
additionalFontFilePaths: comparisonFontFilePaths) | |
rendererView.text = kFontSampleText | |
window.contentView!.addSubview(rendererView) | |
rendererView.draw(rendererFrame) | |
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) | |
if let data = rendererView.pdfData { | |
data.write(toFile: pdfFilePath, atomically: true) | |
print("rendered \(data.length) byte(s) to PDF file \(pdfFilePath)") | |
} else { | |
fatalError("rendering did not occur") | |
} |
@chrissimpkins no problem, Apache 2.0. it's a complete and utter hack, just the quickest possible thing I could throw together. I'd prefer something that doesn't use WebView to render but renders using CoreText directly, but was working under tight time constraints due to upcoming Christmas Eve!
@bitserf I love complete and utter hacks. They sometimes lead to bigger complete and utter hacks. 😃
Thank you very much and happy holidays!
Updated for Swift 4.1.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@bitserf Mind adding a license to this Leon?