read

To create PDF, one of the most popular way is to create the HTML, then convert it to a PDF.

This is in essence using UIMarkupTextPrintFormatter, one of the provided formatters to generate PDF.

Pitfall: It does NOT work for images

Unfortunately, UIMarkupTextPrintFormatter can interpret all of HTML markups, but not the images.

Numerous approaches yield no results, including:

  • img tag with local file path
  • img with base64 encoding

It is in no fault to do with the HTML, since that can be displayed correctly in web views.

Solution: Use WebKit

It turns out a possible solution is to use WKWebView to load the html, then access it’s viewPrintFormatter (a UIView property), then generate the PDF.

An extra step needs to be taken to wait for the web view to finish loading the HTML.

Step 1: Load the web view

typealias ExportManagerCompletion = (Result<NSData, Error>) -> Void

class ExportManager: NSObject {
  var webView: WKWebView? = nil
  var completion: ExportManagerCompletion!

  func exportPDF(html: String, completion: @escaping ExportManagerCompletion) throws {
      self.completion = completion

      let webView = WKWebView()
      webView.navigationDelegate = self
      webView.loadHTMLString(html, baseURL: nil)
      self.webView = webView
  }
}

We created an ExportManager here to make things clearer, largely it needs to:

  • hold an instance of the web view, though not displaying it
  • provide a completion handler (ExportManagerCompletion) to return the PDF data
  • be a delegate to the web view

Step 2. Handle when finish loading

extension ExportManager: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
          let formatter = webView.viewPrintFormatter()
          createPDF(formatter)
        }
    }
}

Our class extend WKNavigationDelegate to handle when the HTML has finished loading.

We access the web view’s viewPrintFormatter, which works correctly with images!

Step 3. Render the PDF

Finally, we can render the PDF with a working formatter.

The manager class will use UIPrintPageRenderer to render the PDF and then call the completion handler successfully.

func createPDF(_ formatter: UIViewPrintFormatter) {
  let render = UIPrintPageRenderer()
  render.addPrintFormatter(formatter, startingAtPageAt: 0)

  // Assign paperRect and printableRect
  // A4, 72 dpi
  let paperRect = CGRect(x: 0, y: 0, width: 595.2, height: 841.8)
  render.setValue(paperRect, forKey: "paperRect")
  let padding: CGFloat = 24
  let printableRect = paperRect.insetBy(dx: padding, dy: padding)
  render.setValue(printableRect, forKey: "printableRect")

  // 4. Create PDF context and draw
  let pdfData = NSMutableData()
  UIGraphicsBeginPDFContextToData(pdfData, .zero, nil)
  for i in 0..<render.numberOfPages {
      UIGraphicsBeginPDFPage();
      render.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
  }
  UIGraphicsEndPDFContext();

  self.completion?(.success(pdfData))
}

Extra: HTML and Base64 encode the image

As mentioned earlier, you can encode an image with base64, and then include it in the HTML. The <img> tag can be generated like this.

func imageBase64Tag(_ image: UIImage) -> String {
    let jpegData = image.jpeg
    let base64EncodedString = (jpegData as NSData).base64EncodedString()
    let src = "data:image/jpeg;base64,\(base64EncodedString)"
    let tag = "<img src=\"\(src)\"/>"
    return tag
}

Image

@samwize

¯\_(ツ)_/¯

Back to Home