6

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.

Community
  • 1
  • 1
John Ramos
  • 272
  • 3
  • 15
  • 3
    See https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_pdf/dq_pdf.html#//apple_ref/doc/uid/TP30001066-CH214-CJBHHJCB, "Creating a PDF file" as a starting point. You can make a NSAttributedString out of HTML and draw that. – zneak Aug 15 '16 at 00:21
  • Not in front of my computer, but you'll want to look up how you can set a CGContextRef in a NSGraphicsContext and then you'll be able to use NSAttributedString's draw methods. – zneak Aug 15 '16 at 02:52
  • You're welcome to edit your question if you run into more specific issues. I'm on a Windows machine at work, though. – zneak Aug 15 '16 at 06:19
  • @zneak I'm having trouble converting all that Objective-C to Swift, I keep getting the error "Could not convert `CG[whatever]` to `UnsafePointer`" – John Ramos Aug 15 '16 at 12:22
  • With what method call? – zneak Aug 15 '16 at 14:59
  • @zneak A bunch of them. I think I'm doing something totally wrong; I posted the code with the errors marked up in the bottom of my question. Thank you for your help so far! – John Ramos Aug 16 '16 at 04:22
  • Where did `pselect` even come from? – jtbandes Aug 16 '16 at 04:28
  • CFDictionary, CFURL, CFString, CFArray, CFSet and friends are all "toll-free bridged" to their NS counterparts, which also means that they are often convertible from/to their Swift counterparts. [This](http://pastebin.com/yEgHDt5f) is probably *almost* what you're looking for. The next problem to solve is that `endPage` is apparently ambiguous, which I think is a bug in Swift's API importer. It might be possible to work around it by creating a tiny C function that wraps the correct one and call that one instead. (note: as this only almost builds, it's untested) – zneak Aug 16 '16 at 04:54
  • @jtbandes `pselect` is what Xcode's fix-its sub in for unrecognized identifier `pageRect` (also gives option to sub in `NSMakeRect` but that causes the same error on that line too). – John Ramos Aug 16 '16 at 16:59
  • @zneak Oh my. Isn't there some built-in function for making a PDF quickly? Maybe I can use an Objective-C pdf-making function and bridge it over to Swift? – John Ramos Aug 16 '16 at 17:00
  • Not that I know. Your C function would only be `void EndPage(CGContextRef ctx) { CGPDFContextEndRef(ctx); }` though, so I don't feel that it's asking a lot, until this gets sorted out. – zneak Aug 16 '16 at 17:05
  • @owlswipe, have you seen the link that I posted in my other comment? http://pastebin.com/yEgHDt5f This one only has two compile-time problems, one being that there's a problem with `endPage`, and the other being that the actual drawing function isn't implemented. – zneak Aug 16 '16 at 21:29
  • @zneak Apologies, I didn't see that link! Ok, I created a C file in my project (with an Objective-C bridging header) and added that function in to it. A new error popped up on top the `pageDictionary` in `pdfContext.beginPage(mediaBox: pageDictionary)`: Cannot convert value of type '[String : AnyObject]' to expected argument type 'UnsafePointer?' What should I do with that, and how do I fix my errors (which seem to follow one pattern?) in my drawing function? **EDIT:** I think my C-file creation failed :[. – John Ramos Aug 16 '16 at 21:42
  • How about one of you "why don't you check out this..." people find a computer and post an answer to the question. – quemeful Aug 17 '16 at 13:38
  • @quemeful I agree. How come this is so hard to do? – John Ramos Aug 18 '16 at 15:46
  • @zneak Just so you know, I've started a bounty on this question. – John Ramos Sep 11 '16 at 01:23
  • @jtbandes I've started a bounty on this question, if you can help! – John Ramos Sep 11 '16 at 01:25
  • @quemeful, I post answers when I think that I can solve a problem, and I post comments when I think that I can nudge people in the right direction. I have no obligation towards you. – zneak Sep 13 '16 at 05:31

1 Answers1

5

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:

  1. Put the HTML into a NSAttributedString.
  2. Render the NSAttributedString to a NSTextField.
  3. Render the NSTextField to a NSView.
  4. Create a NSPrintOperation with that NSView.
  5. Set the printing parameters to save as a PDF.
  6. Run the print operation (which actually opens a dialog to save the PDF)
  7. Everyone is happy.

This is not a perfect solution. Note the hard coded integer values.

josip04
  • 163
  • 2
  • 11
quemeful
  • 8,148
  • 4
  • 51
  • 64
  • Thank you very much for your answer, it's really very close! Unfortunately, it only generates one page, and overflow text gets cut off. How can I get the text to go onto a new page? – John Ramos Aug 23 '16 at 23:59
  • maybe we'd have to do some JavaScript on the HTML to determine page height ... idk if the pixel height scales the same for the HTML as the PDF every time or not – quemeful Aug 24 '16 at 00:13
  • Huh. There's no easy way to paginate the thing? [This code gist](https://gist.github.com/nyg/b8cd742250826cb1471f) lets me do the HTML->PDF thing on iOS with easy automatic pagination, is there a way we can adapt the pagination part of that iOS code to something that'll work on OS X? – John Ramos Aug 24 '16 at 15:35
  • quemeful, I've started a bounty on this question. If you can give me advice on how to paginate the PDF I create (instead of it getting cut off like in your code, it should go onto a new page!), I will happily award you some internet points. – John Ramos Sep 11 '16 at 01:23
  • How to save it in a specified folder and without a dialog ? thx – user3722523 Oct 16 '16 at 22:17
  • Is the dictionary key `NSPrintSaveJob` a typo? Should it be `NSPrintJobSavingURL`? – Todd Mar 22 '17 at 12:16