Generate PDF with images from HTML in Swift without displaying print interface
Replace
let printFormatter = UIMarkupTextPrintFormatter(markupText: htmlContent)
printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0)
with
let printFormatter = wkWebView.viewPrintFormatter()
printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0)
where wkWebView
is your instance of WKWebView
in which you have previously loaded the HTML content htmlContent
that contains an image, and printPageRenderer
is your instance of UIPrintPageRenderer
.
Using parts from fragilecat his answer, I've put together a sample project with three viewcontrollers in Swift 5:
- The first one renders local HTML with one image using WKWebView, then exports to PDF
- The second one renders the PDF with another WKWebView
- The third one shows the print dialog
https://github.com/bvankuik/TestMakeAndPrintPDF
My many hours wasted on this issue tells me UIMarkupTextPrintFormatter
does support images. Two reasons for this:
- As you say, the PDF shown by
UIPrintInteractionController
with aUIMarkupTextPrintFormatter
shows the images correctly. - The following tutorial (also mentioned in the comments) actually manages to create a PDF with images using
UIMarkupTextPrintFormatter
. I've investigated and found the reason for this was that the HTML code was loaded beforehand in aUIWebView
. It looks likeUIMarkupTextPrintFormatter
relies on someWebKit
component to render its images.
I'm aware I'm not providing any solution but to me this is clearly an iOS bug. I don't have iOS 11 so maybe it has been solved in this upcoming version.
I've described the bug in detail here along with a sample app that allows creation of PDFs using the different print formatters available.
NB: I've only managed to get Base64 and "external" (i.e. http://example.com/my-image.png) images working.
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/"
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 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)
}
}
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)")
}
}
}