0

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.

 ca

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:

  1. Create the pdfBaseLayer as a CALayer
  2. Create the gridLayer as a CAShapeLayer, then perform pdfBaseLayer.addSublayer(gridLayer)
  3. Create the textLayer as three separate CATextLayer sublayers and then pdfBaseLayer.addSubLayer(textLayer).

If you need more code, let me know.

SouthernYankee65
  • 1,129
  • 10
  • 22
  • Thinking a little more about this, I know how many pages the PDF will be before I start generating it. In order to get the crisp lines and sharper text, should I just create a PDF document that has ```count``` blank pages long, and then draw the paths and text directly to the PDF? – SouthernYankee65 Aug 24 '20 at 17:33

1 Answers1

0

The issue is resolved. I was inserting a CALayer into the pdf as a CGContext. Essentially, from what I can tell, it was converting the CGContext to an image and then placing it into the pdf. I have since changed my methods to forgo creating a CALayer to writing directly into the CGContext, which is then written to the pdf. This way makes everything super crisp and clear.

SouthernYankee65
  • 1,129
  • 10
  • 22