Hélène Martin Hélène Martin - 4 years ago 188
iOS Question

Generate PDF with images from HTML in Swift without displaying print interface

I generate a PDF in my

Swift
application from some HTML. I use a
UIMarkupTextPrintFormatter
and have code similar to this gist. I get the PDF as
NSData
and attach it to an email. The app does not show the PDF to the user before attaching it.

I'd now like to include some images. Adding their
NSURL
in HTML with my current PDF generating strategy doesn't work. How can I get
NSData
of a PDF corresponding to my HTML with images added? Here are some things I've tried:


  1. This answer suggests embedding the base64 image in the HTML and using
    UIPrintInteractionController
    . This does give me a print preview with correctly-embedded images but how do I go from there to
    NSData
    corresponding to the PDF output?

  2. I've seen some similar suggestions going through
    UIWebView
    but those lead to the same issue -- I don't want to show a preview to the user.


Answer Source

The UIMarkupTextPrintFormatter does not seem to support the html img tag. Apple's documentation is not very informative here, it simply states that the initialization parameter is "The HTML markup text for the print formatter". There is no indication of exactly what tags are supported by the print formatter.

After many tests the only conclusion I can draw is that UIMarkupTextPrintFormatter does NOT support displaying images.

So where does that leave people who want the convenience of creating PDF's from HTML content?

So the only way I have found to make this work is to use a hidden web view that you load your html content in and then use the web view's UIViewPrintFormatter. This works but really feels like a hack.

It does work and it will embed images in your PDF document, however if it was me I would lean towards using CoreText and Quartz 2D as you would have much more control of the pdf generation process, having said that I understand it might be overkill, I don't know the size or complexity of your html content.

So on to a working example...

Setup

It was useful to define a base url so that I could just pass in the filenames of the images I wanted to use. The base url mapped to a directory in the app bundle where the images are located. You can define your own location too.

Bundle.main.resourceURL + "www/"

Copy Files Build Phase

Then I created a protocol to handle document related functionality. Default implementations are provide by an extension as you can see in the code below.

protocol DocumentOperations {

    // Takes your image tags and the base url and generates a html string
    func generateHTMLString(imageTags: [String], baseURL: String) -> String

    // Uses UIViewPrintFormatter to generate pdf and returns pdf location
    func createPDF(html: String, formmatter: UIViewPrintFormatter, filename: String) -> String


    // Wraps your image filename in a HTML img tag
    func imageTags(filenames: [String]) -> [String]
}


extension DocumentOperations  {


    func imageTags(filenames: [String]) -> [String] {

        let tags = filenames.map { "<img src=\"\($0)\">" }

        return tags
    }


    func generateHTMLString(imageTags: [String], baseURL: String) -> String {

        // Example: just using the first element in the array
        var string = "<!DOCTYPE html><head><base href=\"\(baseURL)\"></head>\n<html>\n<body>\n"
        string = string + "\t<h2>PDF Document With Image</h2>\n"
        string = string + "\t\(imageTags[0])\n"
        string = string + "</body>\n</html>\n"

        return string
    }


    func createPDF(html: String, formmatter: UIViewPrintFormatter, filename: String) -> String {
        // From: https://gist.github.com/nyg/b8cd742250826cb1471f

        print("createPDF: \(html)")

        // 2. Assign print formatter to UIPrintPageRenderer
        let render = UIPrintPageRenderer()
        render.addPrintFormatter(formmatter, startingAtPageAt: 0)

        // 3. Assign paperRect and printableRect
        let page = CGRect(x: 0, y: 0, width: 595.2, height: 841.8) // A4, 72 dpi
        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
        let path = "\(NSTemporaryDirectory())\(filename).pdf"
        pdfData.write(toFile: path, atomically: true)
        print("open \(path)")

        return path
    }

}

Then I had this protocol adopted by a view controller. The key to making this work is here, your view controller needs to adopt the UIWebViewDelegate and in the func webViewDidFinishLoad(_ webView: UIWebView) you can see the pdf is created.

class ViewController: UIViewController, DocumentOperations {

    @IBOutlet private var webView: UIWebView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }


    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        webView.delegate = self
        webView.alpha = 0

        if let html = prepareHTML() {
            print("html document:\(html)")
            webView.loadHTMLString(html, baseURL: nil)

        }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    fileprivate func prepareHTML() -> String? {

        // Create Your Image tags here
        let tags = imageTags(filenames: ["PJH_144.png"])
        var html: String?

        // html
        if let url = Bundle.main.resourceURL {

            // Images are stored in the app bundle under the 'www' directory
            html = generateHTMLString(imageTags: tags, baseURL: url.absoluteString + "www/")
        }

        return html
    }
}


extension ViewController: UIWebViewDelegate {

    func webViewDidFinishLoad(_ webView: UIWebView) {
        if let content = prepareHTML() {
            let path = createPDF(html: content, formmatter: webView.viewPrintFormatter(), filename: "MyPDFDocument")
            print("PDF location: \(path)")
        }


    }


}
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download