在 Swift 中从 HTML 生成带有图像的 PDF 而不显示打印界面
Posted
技术标签:
【中文标题】在 Swift 中从 HTML 生成带有图像的 PDF 而不显示打印界面【英文标题】:Generate PDF with images from HTML in Swift without displaying print interface 【发布时间】:2017-03-15 01:39:02 【问题描述】:我在我的Swift
应用程序中从一些html 生成了一个PDF。我使用UIMarkupTextPrintFormatter
并有类似于this gist 的代码。我将 PDF 作为NSData
获取并将其附加到电子邮件中。该应用在附加 PDF 之前不会向用户显示它。
我现在想添加一些图片。使用我当前的 PDF 生成策略在 HTML 中添加他们的NSURL
不起作用。如何获得与我的 HTML 相对应的 PDF 的NSData
并添加了图像?以下是我尝试过的一些事情:
This answer 建议在 HTML 中嵌入 base64 图像并使用UIPrintInteractionController
。这确实为我提供了带有正确嵌入图像的打印预览,但我如何从那里转到与 PDF 输出相对应的NSData
?
我通过UIWebView
看到了一些类似的建议,但这些建议导致了同样的问题——我不想向用户显示预览。
【问题讨论】:
我有几个问题。图片是本地的还是 html 文档中的 url 引用?您想将图像添加到您创建的 pdf 文档中吗?你可以发布/模拟你当前的 pdf 输出与你想要的吗? @fragilecat 图片是手机上的文件。我尝试使用 【参考方案1】:使用来自 FRICKCAT 的部分他的回答,我在 Swift 5 中组合了一个包含三个视图控制器的示例项目:
第一个使用 WKWebView 用一个图像呈现本地 HTML,然后 导出为 PDF 第二个使用另一个 WKWebView 呈现 PDF 第三个显示打印对话框https://github.com/bvankuik/TestMakeAndPrintPDF
【讨论】:
我在应用 css 时遇到了类似的问题。你的例子终于帮我解决了。非常感谢! 运行示例项目应用程序时图像不显示。 @fivewood 我无法可靠地重现该问题,但我添加了一个小延迟以允许加载图像。 请说明您如何添加延迟以允许加载图像。 @fivewood 查看 Github 上的更新项目:github.com/bvankuik/TestMakeAndPrintPDF/blob/master/…【参考方案2】:像这里的许多人一样,我浪费了几个小时来解决这个问题。我使用 base-64 图像,而不是外部图像。以下内容适用于我在<img src="data:image/jpeg;base64,..." />
html 标记中加载的图像以及使用background-image: "url('data:image/jpeg;base64,...)"
的css。
这是我最终得到的结果:
原始问题要求在生成 PDF 之前不必显示 web 视图。我不需要那个,但如果你这样做,只需按照@Pete Hornsby (accepted answer) 的建议让它不可见。 As @fivewood mentioned 不要使用UIMarkupTextPrintFormatter
而是使用 webPreview.viewPrintFormatter()
,我不知道为什么,但它就是这样工作的。
必需:在生成 PDF (pointed here) 之前等待 webview 加载。
let renderer = UIPrintPageRenderer()
// A4 in portrait mode
let pageFrame = CGRect(x: 0.0, y: 0.0, width: 595.2, height: 841.8)
renderer.setValue(NSValue(cgRect: pageFrame), forKey: "paperRect")
// webview here is a WKWebView instance in which we previously loaded the page
renderer.addPrintFormatter(webview.viewPrintFormatter(), startingAtPageAt: 0)
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, CGRect.zero, nil)
for i in 0..<renderer.numberOfPages
UIGraphicsBeginPDFPage()
renderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
UIGraphicsEndPDFContext()
// DONE: pdfData contains the pdf data with images.
// The following tutorial (mentioned several times in this thread) shows
// how to store it in a local file or send it by mail:
// https://www.appcoda.com/pdf-generation-ios/
【讨论】:
【参考方案3】:首先,创建 UIWebView 实例
var webView:UIWebView?
然后创建初始化webView的函数并在控制器中添加UIWebViewDelegate,
func initWebView(htmlString:String)
self.webView = UIWebView.init(frame:self.view.frame)
self.webView?.delegate = self
self.webView?.loadHTMLString(htmlString, baseURL: nil)
然后编写PDF转换代码webViewDidFinishLoad方法
func webViewDidFinishLoad(_ webView: UIWebView)
if(webView.isLoading)
return
let render = UIPrintPageRenderer()
render.addPrintFormatter((self.webView?.viewPrintFormatter())!, startingAtPageAt: 0)
//Give your needed size
let page = CGRect(x: 0, y: 0, width: 384, height: 192)
render.setValue(NSValue(cgRect:page),forKey:"paperRect")
render.setValue(NSValue(cgRect:page), forKey: "printableRect")
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData,page, nil)
for i in 1...render.numberOfPages-1
UIGraphicsBeginPDFPage();
let bounds = UIGraphicsGetPDFContextBounds()
render.drawPage(at: i - 1, in: bounds)
UIGraphicsEndPDFContext();
//For locally view page
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentsDirectory.appendingPathComponent("file.pdf");
if !FileManager.default.fileExists(atPath:fileURL.path)
do
try pdfData.write(to: fileURL)
print("file saved")
catch
print("error saving file:", error);
【讨论】:
【参考方案4】:替换
let printFormatter = UIMarkupTextPrintFormatter(markupText: htmlContent)
printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0)
与
let printFormatter = wkWebView.viewPrintFormatter()
printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0)
其中wkWebView
是您的WKWebView
实例,您之前已在其中加载了包含图像的HTML 内容htmlContent
,printPageRenderer
是您的UIPrintPageRenderer
实例。
【讨论】:
在浪费了无数时间之后,这是唯一适合我的解决方案。 这是最好的答案。不仅仅是 UIMarkupTextPrintFormatter 有图像问题,它还有所有链接(包括指向外部 CSS 的链接)的问题。因此,除非您可以预测您的 HTML 将只有某些类型的链接,否则这是最可靠的方法。【参考方案5】:UIMarkupTextPrintFormatter
似乎不支持 html img
标签。 Apple 的文档在这里没有提供太多信息,它只是说明初始化参数是“打印格式化程序的 HTML 标记文本”。没有具体说明打印格式化程序支持哪些标签。
经过多次测试,我唯一能得出的结论是UIMarkupTextPrintFormatter
确实不支持显示图像。
那么,对于那些想要方便地从 HTML 内容创建 PDF 的人来说,这会给他们带来什么影响呢?
所以我发现让这项工作的唯一方法是使用一个隐藏的 web 视图,你可以在其中加载你的 html 内容,然后使用 web 视图的UIViewPrintFormatter
。这行得通,但真的感觉像一个黑客。
它确实有效,它会在您的 PDF 文档中嵌入图像,但如果是我,我会倾向于使用 CoreText 和 Quartz 2D,因为您可以更好地控制 pdf 生成过程,我说过我理解这可能是矫枉过正,我不知道您的 html 内容的大小或复杂性。
接下来是一个工作示例...
设置
定义一个基本 url 很有用,这样我就可以传入我想要使用的图像的文件名。映射到应用程序包中图像所在目录的基本 url。您也可以定义自己的位置。
Bundle.main.resourceURL + "www/"
然后我创建了一个协议来处理与文档相关的功能。默认实现由扩展提供,您可以在下面的代码中看到。
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
然后我让视图控制器采用了这个协议。完成这项工作的关键就在这里,您的视图控制器需要采用UIWebViewDelegate
并在
func webViewDidFinishLoad(_ webView: UIWebView)
你可以看到pdf被创建了。
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)")
【讨论】:
希望有一种方法可以使用 HTML 而不会成为黑客,但我认为您找到了最佳策略! 本教程中的示例项目使用了img标签http://www.appcoda.com/pdf-generation-ios/
,在我的项目中使用相同的代码,它不起作用。
您的代码几乎可以完美运行,但对我来说,生成的第一个没有图像的 PDF 和第二个正确。有什么想法吗?
问题已解决。从 UIWebView 切换到 WKWebView(检查了 Bart van Kuik 的答案),现在一切正常,感谢您的帮助
我正在使用UIMarkupTextPrintFormatter
将 HTML 转换为 PDF,并且在 Internet 连接可用时工作正常,但在没有 Internet 连接时无法正常工作。有什么想法吗?【参考方案6】:
我在这个问题上浪费了很多时间告诉我UIMarkupTextPrintFormatter
确实支持图片。有两个原因:
-
如您所说,
UIPrintInteractionController
显示的 PDF 和 UIMarkupTextPrintFormatter
正确显示了图像。
以下tutorial(在 cmets 中也提到过)实际上设法使用UIMarkupTextPrintFormatter
创建带有图像的 PDF。我调查并发现其原因是 HTML 代码是预先加载到 UIWebView
中的。看起来 UIMarkupTextPrintFormatter
依赖于一些 WebKit
组件来呈现其图像。
我知道我没有提供任何解决方案,但对我来说这显然是一个 iOS 错误。我没有 iOS 11,所以可能在这个即将发布的版本中已经解决了。
我已经详细描述了该错误here 以及一个示例应用程序,该应用程序允许使用可用的不同打印格式化程序创建 PDF。
注意:我只设法让 Base64 和“外部”(即http://example.com/my-image.png)图像工作。
【讨论】:
非常有趣的分析!非常感谢分享。看看我们在 iOS 11 中发现了什么会很有趣,我也没有尝试过。 @pete-hornsby 的解决方案解决了我的短期问题,但感觉应该有更直接的方法。 @HélèneMartin 谢谢!好吧,我安装了 iOS 11,bug 还在!我已经向 Apple 提交了错误报告...openradar.me/radar?id=6067400759836672 太糟糕了,还没有解决,感谢提交错误报告! 看起来我也遇到了这个问题,我设法让它在我的测试项目中工作,但是当我将代码移动到我的生产应用程序时它不起作用以上是关于在 Swift 中从 HTML 生成带有图像的 PDF 而不显示打印界面的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Swift 中从 Core Data 访问子数据和父数据
在 TableViewCell Swift 中从 Firebase 加载图像