0

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()
    }
}
SouthernYankee65
  • 1,129
  • 10
  • 22
  • Have you tried Core Text? – apodidae Aug 28 '20 at 06:30
  • I looked at it, but another example I was following didn't use it. However, due to your comment researched that. I think I may have found something at https://github.com/nRewik/SimplePDF and hope I can convert it to Swift 5, macOS. Thanks for the suggestion! – SouthernYankee65 Aug 28 '20 at 14:53

1 Answers1

5

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()
SouthernYankee65
  • 1,129
  • 10
  • 22
  • 1
    Thank you, thank you, thank you. For months I was looking for this solution. :-) Do you coincidentally know, how to make automatic pagebreaks with longer text? – mihema Aug 23 '22 at 16:57
  • You’re welcome. You might be able to use something like intrinsic content size to check that the text does not exceed your space requirements. If it does, remove some text until it fits and then continue to another page. That would be my first approach. – SouthernYankee65 Aug 24 '22 at 17:47
  • Thanks again for your hint. That's what I was thinking about, too. I hoped, there's an easier way. – mihema Aug 24 '22 at 18:54
  • For all of us, who need paginated pdf - look here: https://stackoverflow.com/questions/58483933/create-pdf-with-multiple-pages/65767854#65767854. In combination with this post obove you get all you need, to create pdf with swift. – mihema Aug 30 '22 at 04:39
  • Is there a way to just modify CGPDFDictionary ? Because I do not need to create whole new pdf, I just need to remove background from it. I already modified the "Contents" stream, but I have no luck saving it back. Or should I copy other keys from the page to the new page? I need to modify each page separately – Jason Krowl Nov 14 '22 at 13:38
  • I am not sure. I only create new PDFs or read the XMP metadata from them. I have not run into a need to modify a PDF and then save it. – SouthernYankee65 Nov 14 '22 at 14:53