Skip to content

Instantly share code, notes, and snippets.

@leonbreedt
Last active July 13, 2018 02:43
Show Gist options
  • Save leonbreedt/ec3fc3ce76b8231dfa99 to your computer and use it in GitHub Desktop.
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.
//
// 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")
}
@leonbreedt
Copy link
Author

Updated for Swift 4.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment