2

My goal is to write text on a PDF, like an annotation.

I achieved it transforming the PDFPage to a NSImage, I drew on the NSImage then I saved the PDF formed by the images.

let image = NSImage(size: pageImage.size)        
image.lockFocus()

let rect: NSRect = NSRect(x: 50, y: 50, width: 60, height: 20)
"Write it on the page!".draw(in: rect, withAttributes: someAttributes)

image.unlockFocus()

let out = PDFPage(image: image)

The problem is obviously that out (the new page of the output PDF) is a PDFPage of images and not a regular one. So the output PDF is very big in size and you can't copy and paste anything on it. It's just a sequence of images.

My question is if there's a way to add simple text on a PDF page programmatically without using NSImage. Any idea?

Note: There's this class in iOS programming UIGraphicsBeginPDFPageWithInfo which could be very helpful in my case. But I can't find the similar class for macOS development.

jscs
  • 63,694
  • 13
  • 151
  • 195
Matt
  • 773
  • 2
  • 15
  • 32

1 Answers1

18

You can create a PDF graphics context on macOS and draw a PDFPage into it. Then you can draw more objects into the context using either Core Graphics or AppKit graphics.

Here's a test PDF I created by printing your question: input PDF

And here's the result from drawing that page into a PDF context, then drawing more text on top of it:

output PDF

Here's the code I wrote to transform the first PDF into the second PDF:

import Cocoa
import Quartz

let inUrl: URL = URL(fileURLWithPath: "/Users/mayoff/Desktop/test.pdf")
let outUrl: CFURL = URL(fileURLWithPath: "/Users/mayoff/Desktop/testout.pdf") as CFURL

let doc: PDFDocument = PDFDocument(url: inUrl)!
let page: PDFPage = doc.page(at: 0)!
var mediaBox: CGRect = page.bounds(for: .mediaBox)

let gc = CGContext(outUrl, mediaBox: &mediaBox, nil)!
let nsgc = NSGraphicsContext(cgContext: gc, flipped: false)
NSGraphicsContext.current = nsgc
gc.beginPDFPage(nil); do {
    page.draw(with: .mediaBox, to: gc)

    let style = NSMutableParagraphStyle()
    style.alignment = .center

    let richText = NSAttributedString(string: "Hello, world!", attributes: [
        NSFontAttributeName: NSFont.systemFont(ofSize: 64),
        NSForegroundColorAttributeName: NSColor.red,
        NSParagraphStyleAttributeName: style
        ])

    let richTextBounds = richText.size()
    let point = CGPoint(x: mediaBox.midX - richTextBounds.width / 2, y: mediaBox.midY - richTextBounds.height / 2)
    gc.saveGState(); do {
        gc.translateBy(x: point.x, y: point.y)
        gc.rotate(by: .pi / 5)
        richText.draw(at: .zero)
    }; gc.restoreGState()

}; gc.endPDFPage()
NSGraphicsContext.current = nil
gc.closePDF()
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Perfect answer! I understand now. – Matt Jun 20 '17 at 09:19
  • Hey Rob! What if I need to use the new drawn page? With your code, the new page gets saved at **outUrl** path but there's no way to access it programmatically. The only thing I could do is to save the PDFPage to **outUrl** and then open it again. I had a look on the documentation page of CGContext but I don't find anything to get the PDFPage which was modified. Any idea how to get it immediately after **gc.closePDF()** call? It isn't important, it was just for curiosity actually. – Matt Jun 20 '17 at 16:04
  • 2
    Use the `CGContext` initializer that takes a `CGDataConsumer` argument. After you `closePDF`, you can create a new instance of `PDFDocument` from the data without going through a file. – rob mayoff Jun 20 '17 at 16:45
  • This is a great answer, although I'm getting an error when trying to use `NSGraphicsContext.setCurrent()` in Swift 4. It says "Type 'NSGraphicsContext' has no member 'setCurrent'". It's still in the documentation so I'm not sure. – kernelpanic Jan 30 '18 at 21:13
  • `NSGraphicsContext` now has a property, `current`, instead of a setter method. I have updated my answer. – rob mayoff Jan 30 '18 at 22:12
  • Thank you very much for this answer. How did you come up with the `CGContext` and `NSGraphicsContext` stuff? I'm having trouble wrapping my mind around it. :/ Do you know a good resource? – Daan Jul 30 '18 at 12:39
  • You can read about `NSGraphicsContext` in the [*Cocoa Drawing Guide*](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaDrawingGuide/GraphicsContexts/GraphicsContexts.html#//apple_ref/doc/uid/TP40003290-CH203-BCIJFBJJ). You can read about `CGContext` in the [*Quartz 2D Programming Guide*](https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-TPXREF101). – rob mayoff Jul 30 '18 at 14:10
  • @robmayoff I want to automatically pagination the NSTextView to generate PDF. Do you have any information to help me? – Simon Aug 14 '18 at 12:50
  • Ho who you create a CGcontext with pdfData ? – user3722523 May 23 '19 at 17:42
  • I don't understand your question. – rob mayoff May 23 '19 at 18:09
  • I am getting error on let gc = CGContext(outUrl, mediaBox: &mediaBox, nil)! Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value – russell Sep 20 '20 at 13:10
  • @robmayoff It seems like you are an expert related to PDF/MACOS. I would really I appreciate if you would answer the following question. I am doing something similar. https://stackoverflow.com/questions/63941329/how-to-add-a-link-annotation-with-a-pdfactiongoto-action-type-to-a-pdfpage-using – russell Sep 20 '20 at 14:46