Draw on a PDF using Swift on macOS - swift

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.

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:
And here's the result from drawing that page into a PDF context, then drawing more text on top of it:
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()

Related

Swift PDFKit draw multiple images in a single PDFPage

I'm trying to draw multiple images into a single PDFPage.
Looking at the docs and over StackOverflow seems like the best I got is to use PDFPage with an UIImage initializer like so:
let pdfPage = PDFPage(image: image)
But it just creates a page with a full-page image. I tried to draw the images using CGContext but I don't understand how to use PDFPage within a drawing context for it to draw the images rapidly like in the example below.
let bounds = page.bounds(for: .cropBox)
// Create a `UIGraphicsImageRenderer` to use for drawing an image.
let renderer = UIGraphicsImageRenderer(bounds: bounds, format: UIGraphicsImageRendererFormat.default())
let image = renderer.image { (context) in
// How do I rapidly draw them here?
}
Any help will be highly appreciated!
The result I get with PDFPage(image: <UIImage>) vs expected result:
You probably need to improve the positioning logic for the image in the loop, but this should point you to the right direction.
import UIKit
import PDFKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let pdfView = PDFView()
pdfView.frame = CGRect(x: 0, y: 50, width: view.frame.width, height: view.frame.height - 50)
if let d = createPDFDocument() {
pdfView.document = d
}
view.addSubview(pdfView)
}
func createPDFDocument() -> PDFDocument? {
let pdfDocument = PDFDocument()
let page = PDFPage()
let bounds = page.bounds(for: .cropBox)
let imageRenderer = UIGraphicsImageRenderer(bounds: bounds, format: UIGraphicsImageRendererFormat.default())
let image = imageRenderer.image { (context) in
context.cgContext.saveGState()
context.cgContext.translateBy(x: 0, y: bounds.height)
context.cgContext.concatenate(CGAffineTransform.init(scaleX: 1, y: -1))
page.draw(with: .mediaBox, to: context.cgContext)
context.cgContext.restoreGState()
// Improve logic for image position
Range(1...4).forEach { value in
let image = UIImage(named: "YOUR_IMAGE_NAME")
let rect = CGRect(x: 50 * value, y: 0, width: 40, height: 100)
image?.draw(in: rect)
}
}
let newPage = PDFPage(image: image)!
pdfDocument.insert(newPage, at: 0)
return pdfDocument
}
}

NSFont autoscaling via NSStringDrawingContext does not work

I'm building a small test app that draws text into rectangles on images. Said text might sometimes be too long to be drawn into said rectangles, but I thought I would simply use NSStringDrawingContext and NSAttributedString.boundingRect(with:options:context:) in order to generate a scale factor that I'd use to scale the font down to make it fit.
The problem I'm now facing is that this straight up is not working, as actualScaleFactor is always 1.0, even though it should be less than that. To demonstrate, I've prepared a minimal reproducible example.
The image used as input variable image can be found here: https://i.imgur.com/VNd2y8r.png. The text target rectangle rect is marked by the black rectangle in the image.
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes: [NSAttributedString.Key: Any] = [
.font: NSFont(name: "Futura-Bold", size: 100)!,
.foregroundColor: NSColor.white,
.paragraphStyle: paragraphStyle,
]
let text = "A very long string that'll be auto-wrapped when it gets drawn"
let attrStr = NSAttributedString(string: text, attributes: textAttributes)
let drawingOptions: NSString.DrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading]
let context = NSStringDrawingContext()
// counter-intuitively, 1.0 is the "maximum" minimum scale factor
// see also https://developer.apple.com/documentation/uikit/nsstringdrawingcontext/1534020-minimumscalefactor
context.minimumScaleFactor = 1.0
let position = NSPoint(x: 80, y: 140)
let size = NSSize(width: 640, height: 200)
let rect = NSRect(origin: position, size: size)
let newImage = NSImage(size: image.size, flipped: true) { imageRect in
image.draw(in: imageRect)
// let NSStringDrawingContext do its thing
attrStr.boundingRect(with: size, options: drawingOptions, context: context)
print("Actual Scale Factor: \(context.actualScaleFactor)") // is always 1.0
attrStr.setAttributes([.font: font.withSize(font.pointSize * context.actualScaleFactor)], range: NSRangeFromString(text))
attrStr.draw(with: rect, options: drawingOptions, context: context)
return true
}
At this point, newImage should be an image that looks something like this:
(recreated in an image editing tool)
Instead, this is what I get:
This is obviously wrong.
While I was looking for a solution, I found this related SO question from 2013 with a conclusion of "it's broken", but surely this has to be possible somehow by now. iOS and macOS label controls both have this feature, SwiftUI even makes it available as a modifier entitled simply minimumScaleFactor. Is there a replacement API I'm not aware of?

Drawing Text to a CGContext for Quartz PDF not working

This is for Swift 5 on macOS
I am trying to write some text to a generated PDF.
I am able to load a background image onto the pages, but when I call my drawText method, it is not making it onto either of the pages.
I tried drawing an NSString to the context via the .draw() method and that would not work either. I hoping to get this to work so I can add more text, including text boxes, etc.
What am I doing wrong? Thanks for any pointers.
import Cocoa
import CoreText
import Quartz
extension NSImage {
/*
Converts an NSImage to a CGImage for rendering in a CGContext
Credit - Xue Yu
- https://gist.github.com/KrisYu/83d7d97cae35a0b10fd238e5c86d288f
*/
var toCGImage: CGImage {
var imageRect = NSRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
guard let image = cgImage(forProposedRect: &imageRect, context: nil, hints: nil) else {
abort()
}
return image
}
}
class PDFText {
/*
Create a non-nil CGContext
Credit - hmali - 3/15/2019
https://stackoverflow.com/questions/41100895/empty-cgcontext
*/
var pdfContext = CGContext(data: nil,
width: 0,
height: 0,
bitsPerComponent: 1,
bytesPerRow: 1,
space: CGColorSpace.init(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
let textRect = CGRect(x: 295, y: 350, width: 100, height: 100)
func createPDF() {
let filePath = "/Users/Shared/Text.pdf"
let fileURL = NSURL(fileURLWithPath: filePath)
pdfContext = CGContext(fileURL, mediaBox: &backgroundRect, nil)
pdfContext!.beginPDFPage(nil)
drawBackground()
drawText("This is page 1")
pdfContext!.endPDFPage()
pdfContext!.beginPDFPage(nil)
drawBackground()
drawText("This is page 1")
pdfContext!.endPDFPage()
pdfContext!.closePDF()
}
func drawBackground() {
let cgImage = NSImage(contentsOfFile: "/Users/Shared/background.png")?.toCGImage
pdfContext?.draw(cgImage!, in: CGRect(x: 0, y: 0, width: Int(72*8.5), height: Int(72*11)))
}
func drawText(_ text:String) {
let style = NSMutableParagraphStyle()
style.alignment = .center
let attr = [NSAttributedString.Key.font: NSFont(name: "Helvetica", size: 16.0),
NSAttributedString.Key.foregroundColor: NSColor.purple,
NSAttributedString.Key.backgroundColor: NSColor.clear,
NSAttributedString.Key.paragraphStyle: style]
let attrText = NSAttributedString(string: text, attributes: attr as [NSAttributedString.Key : Any])
pdfContext?.saveGState()
pdfContext?.translateBy(x: attrText.size().width, y: attrText.size().height)
attrText.draw(with: textRect)
pdfContext?.restoreGState()
}
}
Closing an open question that I got worked out (complete code).
Swift 5.4 on macOS
import Cocoa
import CoreText
import Quartz
var pageWidth: CGFloat = 72*8.5
var pageHeight: CGFloat = 72*11.0
var pageRect: CGRect = CGRect(x:0, y:0, width: pageWidth, height: pageHeight)
extension NSImage {
/*
Converts an NSImage to a CGImage for rendering in a CGContext
Credit - Xue Yu
- https://gist.github.com/KrisYu/83d7d97cae35a0b10fd238e5c86d288f
*/
var toCGImage: CGImage {
var imageRect = NSRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
guard let image = cgImage(forProposedRect: &imageRect, context: nil, hints: nil) else {
abort()
}
return image
}
}
class PDFText {
/*
Create a non-nil empty CGContext
Credit - hmali - 3/15/2019
https://stackoverflow.com/questions/41100895/empty-cgcontext
*/
var pdfContext = CGContext(data: nil,
width: 0,
height: 0,
bitsPerComponent: 1,
bytesPerRow: 1,
space: CGColorSpace.init(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
// Set a rectangle to be in the center of the page
let textRect = CGRect(x: pageRect.midX-50, y: pageRect.midY-50, width: 100, height: 100)
func createPDF() {
let filePath = "/Users/Shared/Text.pdf"
let fileURL = NSURL(fileURLWithPath: filePath)
pdfContext = CGContext(fileURL, mediaBox: &pageRect, nil)
// This must be called to begin a page in a PDF document
pdfContext!.beginPDFPage(nil)
drawBackground()
drawText(text: "This is page 1")
// This has to be called prior to writing another page to the PDF document
pdfContext!.endPDFPage()
pdfContext!.beginPDFPage(nil)
drawBackground()
drawText(text: "This is page 2")
// Call this or before closing the document.
pdfContext!.endPDFPage()
pdfContext!.closePDF()
}
func drawBackground() {
// Draws an image into the graphics context.
// NOTE: If the image is not sized for the specified rectangle it will be
// scaled (up/down) automatically to fit within the rectangle.
let cgImage = NSImage(contentsOfFile: "/Users/Shared/background.png")?.toCGImage
pdfContext?.draw(cgImage!, in: pageRect)
}
func drawText(text:String) {
// Credit: Nutchaphon Rewik, https://github.com/nRewik/SimplePDF
// Create a paragraph style to be used with the atributed string
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
// Set up the sttributes to be applied to the attributed text
let stringAttributes = [NSAttributedString.Key.font: NSFont(name: "Helvetica", size: 16.0),
NSAttributedString.Key.foregroundColor: NSColor.purple,
NSAttributedString.Key.backgroundColor: NSColor.clear,
NSAttributedString.Key.paragraphStyle: paragraphStyle]
// Create the attributed string
let attributedString = NSAttributedString(string: text, attributes: stringAttributes as [NSAttributedString.Key : Any])
// Set up a CoreText frame that encloses the attributed string
let frameSetter = CTFramesetterCreateWithAttributedString(attributedString)
// Get the frame size for the attributed string
let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, attributedString.string.count), nil, textRect.size, nil)
// Save the Graphics state of the context
pdfContext!.saveGState()
// Put the text matrix into a known state. This ensures that no old scaling
// factors are left in place.
pdfContext!.textMatrix = CGAffineTransform.identity
// Create a path object to enclose the text.
let framePath = CGPath(rect: CGRect(x: textRect.minX, y: textRect.midY-frameSize.height/2, width: textRect.width, height: frameSize.height), transform: nil)
// Get the frame that will do the rendering. The currentRange variable specifies
// only the starting point. The framesetter lays out as much text as will fit into
// the frame or until it runs out of text.
let frameRef = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: 0), framePath, nil)
// Draw the CoreText frame (that includes the text) into the graphics context.
CTFrameDraw(frameRef, pdfContext!)
// Restore the previous Graphics state.
pdfContext?.restoreGState()
}
}
let pdf = PDFText()
pdf.createPDF()

Swift - Overlay Text on current PDF Document

I have been trying to simply overlay text onto a current PDF document that is essentially a timecard. I copy the file to the downloads folder and that works fine, but then when I try to use a CGContext to add text, it exports a white PDF document. Can anyone see where I'm going wrong?
do {
try fileManager.copyItem(at: pdfURL!, to: destinationURL)
} catch let error as NSError {
print("Copy failed :( with error: \(error)")
}
if let pdf: CGPDFDocument = CGPDFDocument(destinationURL as CFURL) { // Create a PDF Document
if pdf.numberOfPages == 1 {
let pdfPage: CGPDFPage = pdf.page(at: 1)!
let pageRect = pdfPage.getBoxRect(CGPDFBox.mediaBox)
//print(pageRect)
let context = CGContext.init(destinationURL as CFURL, mediaBox: nil, nil)
let font = NSFont(name: "Helvetica Bold", size: 20.0)
let textRect = CGRect(x: 250, y: 250, width: 500, height: 40)
let paragraphStyle: NSParagraphStyle = NSParagraphStyle.default
let textColor = NSColor.black
let textFontAttributes = [
NSAttributedStringKey.font: font!,
NSAttributedStringKey.foregroundColor: textColor,
NSAttributedStringKey.paragraphStyle: paragraphStyle
]
let text: NSString = "Hello world"
text.draw(in: textRect, withAttributes: textFontAttributes)
context?.addRect(textRect)
context?.closePDF()
}
}
The following code is what I used to overlay text on macOS. I've been trying to find a link to the source answer I got this from. If I find it I'll edit this answer with a link.
// Confirm there is a document there
if let doc: PDFDocument = PDFDocument(url: srcURL) {
// Create a document, get the first page, and set the size of the page
let page: PDFPage = doc.page(at: 0)!
var mediaBox: CGRect = CGRect(x: 0, y: 0, width: 792, height: 612)
// This is where the magic happens. Create the drawing context on the PDF
let context = CGContext(dstURL as CFURL, mediaBox: &mediaBox, nil)
let graphicsContext = NSGraphicsContext(cgContext: context!, flipped: false)
NSGraphicsContext.current = graphicsContext
context!.beginPDFPage(nil)
// Draws the PDF into the context
page.draw(with: .mediaBox, to: context!)
// Parse and Draw Text on the context
drawText()
context!.saveGState()
context!.restoreGState()
context!.endPDFPage()
NSGraphicsContext.current = nil
context?.closePDF()
}

How to create a PDF in Swift with Cocoa (Mac)

Xcode 7.3.2, Swift 2, Cocoa (Mac).
My app involves the user entering in some text, which can be exported to a PDF.
In the iOS version of my app, I can create the PDF relatively easily with the CoreText framework:
let html = "<font face=\'Futura\' color=\"SlateGray\"><h2>\(title)</h2></font><font face=\"Avenir\" color=\"SlateGray\"><h4>\(string)</h4></font>"
let fmt = UIMarkupTextPrintFormatter(markupText: html)
// 2. Assign print formatter to UIPrintPageRenderer
let render = UIPrintPageRenderer()
render.addPrintFormatter(fmt, startingAtPageAt: 0)
// 3. Assign paperRect and printableRect
let page = CGRect(x: 10, y: 10, width: 595.2, height: 841.8) // A4, 72 dpi, margin of 10 from top and left.
let printable = page.insetBy(dx: 0, dy: 0)
render.setValue(NSValue(cgRect: page), forKey: "paperRect")
render.setValue(NSValue(cgRect: printable), forKey: "printableRect")
// 4. Create PDF context and draw
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, CGRect.zero, nil)
for i in 1...render.numberOfPages {
UIGraphicsBeginPDFPage();
let bounds = UIGraphicsGetPDFContextBounds()
render.drawPage(at: i - 1, in: bounds)
}
UIGraphicsEndPDFContext();
// 5. Save PDF file
path = "\(NSTemporaryDirectory())\(title).pdf"
pdfData.write(toFile: path, atomically: true)
However, UIMarkupTextPrintFormatter, UIPrintPageRenderer, UIGraphicsBeginPDFContextToData, and UIGraphicsEndPDFContext all do not exist on OS X. How can I do the exact same thing as I am doing with this iOS code (create a basic PDF from some HTML and write it to a certain file path as a paginated PDF) with Mac and Cocoa?
EDIT: The answer to this question is here: Create a paginated PDF—Mac OS X.
Here is a function that will generate a PDF from pure HTML.
func makePDF(markup: String) {
let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
let printOpts: [NSPrintInfo.AttributeKey: Any] = [NSPrintInfo.AttributeKey.jobDisposition: NSPrintInfo.JobDisposition.save, NSPrintInfo.AttributeKey.jobSavingURL: directoryURL]
let printInfo = NSPrintInfo(dictionary: printOpts)
printInfo.horizontalPagination = NSPrintingPaginationMode.AutoPagination
printInfo.verticalPagination = NSPrintingPaginationMode.AutoPagination
printInfo.topMargin = 20.0
printInfo.leftMargin = 20.0
printInfo.rightMargin = 20.0
printInfo.bottomMargin = 20.0
let view = NSView(frame: NSRect(x: 0, y: 0, width: 570, height: 740))
if let htmlData = markup.dataUsingEncoding(NSUTF8StringEncoding) {
if let attrStr = NSAttributedString(HTML: htmlData, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil) {
let frameRect = NSRect(x: 0, y: 0, width: 570, height: 740)
let textField = NSTextField(frame: frameRect)
textField.attributedStringValue = attrStr
view.addSubview(textField)
let printOperation = NSPrintOperation(view: view, printInfo: printInfo)
printOperation.showsPrintPanel = false
printOperation.showsProgressPanel = false
printOperation.run()
}
}
}
What is happening:
Put the HTML into a NSAttributedString.
Render the NSAttributedString to a NSTextField.
Render the NSTextField to a NSView.
Create a NSPrintOperation with that NSView.
Set the printing parameters to save as a PDF.
Run the print operation (which actually opens a dialog to save the PDF)
Everyone is happy.
This is not a perfect solution. Note the hard coded integer values.