在 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 内容htmlContentprintPageRenderer 是您的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 访问子数据和父数据

在 Swift 中从 PaintCode 访问图像

在 Swift 中从 Plist 加载 Url 图像

在 TableViewCell Swift 中从 Firebase 加载图像

在 C++ 中从纯文本生成 HTML(即 br 和 p 标签)

如何在 swift 中从 nsimage 创建灰度图像?