This is for macOS & Swift 5.3
I am generating a PDF with Quartz through use of off screen CALayers and converting the combined layers to an NSImage via a CALayer extension I found here.
However, the screen quality of the cgpath lines differ quite a bit from the PDF quality.
As you can see, the image (both zoomed at 100%) on the left (screen output) has narrow lines for my cgpaths, where the image on the right (pdf output) the paths are larger and fuzzy. Also, the text is fuzzy in both cases. What would cause this to happen, and how could I fix it so the cgpath lines are crisp (and small like on screen) and the text is sharp? If I reduce the lineWidth of the paths it doesn't make them smaller in the PDF, it makes them more opaque. I am using the system font Helvetica for the text.
The backing layer is a single CALayer that I add some CAShapeLayers, CATextLayers, and CALayers to make a single view, then convert the combined layers to an NSImage as per above, and save it to a PDF document.
Here's the code that I am using to generate the PDF:
NSBezierPath Extension:
extension NSBezierPath {
// Credit - Henrick - 9/18/2016
// https://stackoverflow.com/questions/1815568/how-can-i-convert-nsbezierpath-to-cgpath
public var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< self.elementCount {
let type = self.element(at: i, associatedPoints: &points)
switch type {
case .moveTo:
path.move(to: points[0])
case .lineTo:
path.addLine(to: points[0])
case .curveTo:
path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath:
path.closeSubpath()
@unknown default:
print("Error occurred in NSBezierPath extension.")
}
}
return path
}
}
CALayer Extension:
extension CALayer {
/*
Credit - Robert Myran - 12/29/2016
https://stackoverflow.com/questions/41386423/get-image-from-calayer-or-nsview-swift-3
*/
/// Get `NSImage` representation of the layer.
///
/// - Returns: `NSImage` of the layer.
func image() -> NSImage {
let width = 612
let height = 792
let imageRepresentation = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: width, pixelsHigh: height, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSColorSpaceName.deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)!
imageRepresentation.size = CGSize(width: width, height: height)
let context = NSGraphicsContext(bitmapImageRep: imageRepresentation)!
render(in: context.cgContext)
return NSImage(cgImage: imageRepresentation.cgImage!, size: bounds.size)
}
}
NSImage Extension:
extension NSImage {
/*
Credit - Xue Yu - 7/9/2017
https://gist.github.com/KrisYu/83d7d97cae35a0b10fd238e5c86d288f
*/
var toCGImage: CGImage {
var imageRect = NSRect(x: 0, y: 0, width: size.width, height: size.height)
guard let image = cgImage(forProposedRect: &imageRect, context: nil, hints: nil) else {
abort()
}
return image
}
}
Grid drawing method:
func insertGrid() -> CALayer {
/*
Draws a single table grid of 25 boxes (5 high by 5 wide)
centered on a letter sized page
*/
// Create a new shape layer for the grid
let gridLayer = CAShapeLayer()
// Assign the grid fill and stroke colors
gridLayer.strokeColor = NSColor.black.cgColor
gridLayer.fillColor = NSColor.clear.cgColor
// Create the path
let gridPath = NSBezierPath()
gridPath.lineWidth = 0.25
// Draw the paths for the grid
// Create the outside box
gridPath.move(to: CGPoint(x: col1X, y: bottomY)) // Bottom left corner
gridPath.line(to: CGPoint(x: col1X, y: topY)) // Column 1, left line
gridPath.line(to: CGPoint(x: rightX, y: topY)) // Top line of row
gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 3 right line
gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Bottom line of row
// Add in column lines
gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 1 & 2
gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 1 & 2
gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 2 & 3
gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 2 & 3
// Close the path
gridPath.close()
// Add grid to layer (note the use of the cgPath extension)
gridLayer.path = gridPath.cgPath
return gridLayer
}
Create PDF Method:
func createPDF(image: NSImage) -> NSData {
/*
Credit - Xue Yu - 7/9/2017
https://gist.github.com/KrisYu/83d7d97cae35a0b10fd238e5c86d288f
*/
let pdfData = NSMutableData()
let pdfConsumer = CGDataConsumer(data: pdfData as CFMutableData)!
var mediaBox = NSRect.init(x: 0, y: 0, width: image.size.width, height: image.size.height)
let pdfContext = CGContext(consumer: pdfConsumer, mediaBox: &mediaBox, nil)!
pdfContext.beginPage(mediaBox: &mediaBox)
pdfContext.draw(image.toCGImage , in: mediaBox)
pdfContext.endPage()
return pdfData
}
And I call it like this:
createPDF(image: pdfBaseLayer.image())?.write(to: pdfFileURL, atomically: true)
Is there a way to build this as a vector image, or is that just not necessary?
This is what I do:
- Create the pdfBaseLayer as a CALayer
- Create the gridLayer as a CAShapeLayer, then perform pdfBaseLayer.addSublayer(gridLayer)
- Create the textLayer as three separate CATextLayer sublayers and then pdfBaseLayer.addSubLayer(textLayer).
If you need more code, let me know.