web-dev-qa-db-fra.com

Produire PDF avec des images HTML dans Swift sans afficher l'interface d'impression

Je génère un PDF dans mon application Swift à partir de HTML. J'utilise un UIMarkupTextPrintFormatter et ai un code similaire à this Gist . Je reçois le PDF en tant que NSData et le joint à un courrier électronique. L'application ne montre pas le PDF à l'utilisateur avant de le joindre.

J'aimerais maintenant inclure quelques images. Ajouter leur NSURL en HTML avec ma stratégie de génération PDF actuelle ne fonctionne pas. Comment puis-je obtenir NSData d'un PDF correspondant à mon code HTML avec des images ajoutées? Voici quelques choses que j'ai essayées:

  1. Cette réponse suggère d’incorporer l’image base64 dans le code HTML et d’utiliser UIPrintInteractionController . Cela me donne un aperçu avant impression avec des images correctement intégrées, mais comment puis-je y aller à NSData correspondant à la sortie PDF?

  2. J'ai vu des suggestions similaires passer par UIWebView mais celles-ci mènent au même problème - je ne veux pas montrer un aperçu à l'utilisateur.

27
Hélène Martin

Le UIMarkupTextPrintFormatter ne semble pas supporter la balise html img. La documentation d'Apple est peu informative ici, elle indique simplement que le paramètre d'initialisation est "Le texte de balisage HTML pour le formateur d'impression". Rien n’indique exactement quelles étiquettes sont supportées par le formateur d’impression. 

Après de nombreux tests, la seule conclusion que je puisse tirer est que UIMarkupTextPrintFormatter ne N'EST PAS supporte l'affichage d'images.

Alors, où cela laisse-t-il les gens qui veulent avoir la possibilité de créer des PDF à partir de contenu HTML? 

Ainsi, le seul moyen que j’ai trouvé pour que cela fonctionne est d’utiliser une vue Web masquée dans laquelle vous chargez votre contenu HTML, puis utilisez le UIViewPrintFormatter de la vue Web. Cela fonctionne mais ressemble vraiment à un hack. 

Cela fonctionne et incorporera des images dans votre document PDF. Cependant, si c'était moi, je préférerais utiliser CoreText et Quartz 2D car vous auriez beaucoup plus de contrôle sur la génération de pdf. processus, après avoir dit que je comprenais que c’était peut-être excessif, je ne connaissais pas la taille ni la complexité de votre contenu HTML. 

Passons à un exemple de travail ...

Installer

Il était utile de définir une URL de base afin que je puisse simplement passer dans les noms de fichiers des images que je voulais utiliser. L'URL de base est mappée sur un répertoire du groupe d'applications où se trouvent les images. Vous pouvez également définir votre propre emplacement.

Bundle.main.resourceURL + "www/"

 Copy Files Build Phase

Ensuite, j'ai créé un protocole pour gérer les fonctionnalités liées aux documents. Les implémentations par défaut sont fournies par une extension comme vous pouvez le voir dans le code ci-dessous.

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
    }

}

Ensuite, j'ai fait adopter ce protocole par un contrôleur de vue. La clé pour que cela fonctionne est ici, votre contrôleur de vue doit adopter la UIWebViewDelegate et dans le func webViewDidFinishLoad(_ webView: UIWebView) vous pouvez voir que le pdf est créé.

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)")
        }


    }


}
25
Pete Hornsby

En utilisant des éléments de fragilecat pour y répondre, j'ai créé un exemple de projet avec trois contrôleurs de vue:

  • Le premier rend le code HTML local avec une image à l'aide de WKWebView, puis Exporte au format PDF
  • Le second rend le PDF avec un autre WKWebView
  • Le troisième montre la boîte de dialogue d'impression

https://github.com/bvankuik/TestMakeAndPrintPDF

7
Bart van Kuik

Mes nombreuses heures perdues sur ce problème me disent que UIMarkupTextPrintFormatterne prend en charge les images. Deux raisons à cela:

  1. Comme vous le dites, le PDF affiché par UIPrintInteractionController avec une UIMarkupTextPrintFormatter montre les images correctement.
  2. Le tutoriel } _ suivant (également mentionné dans les commentaires) parvient à créer un PDF avec des images utilisant UIMarkupTextPrintFormatter. J'ai enquêté et trouvé que la raison en était que le code HTML avait été chargé auparavant dans une UIWebView. Il semble que UIMarkupTextPrintFormatter s'appuie sur un composant WebKit pour restituer ses images.

Je suis conscient que je ne fournis aucune solution mais pour moi, il s'agit clairement d'un bogue iOS. Je n'ai pas iOS 11, alors peut-être que cela a été résolu dans cette version à venir.

J'ai décrit le bogue en détail ici avec un exemple d'application permettant de créer des PDF à l'aide des différents formateurs d'impression disponibles.

NB: Je n'ai réussi à obtenir que des images Base64 et "externes" (c'est-à-dire http://example.com/my-image.png ).

7
nyg

Remplacer

let printFormatter = UIMarkupTextPrintFormatter(markupText: htmlContent)
printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0)

avec

let printFormatter = wkWebView.viewPrintFormatter()
printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0)

wkWebView est votre instance de WKWebView dans laquelle vous avez précédemment chargé le contenu HTML htmlContent qui contient une image, et printPageRenderer est votre instance de UIPrintPageRenderer.

3
fivewood

Comme beaucoup de gens ici, j'ai perdu des heures à résoudre ce problème. J'ai utilisé des images en base 64, pas des images externes. Ce qui suit fonctionne bien pour moi avec les images chargées dans la balise html <img src="data:image/jpeg;base64,..." /> et également avec css en utilisant background-image: "url('data:image/jpeg;base64,...)".

Voici ce que j'ai fini avec:

  • La question initiale demandait de ne pas avoir à afficher la vue Web avant de générer le PDF. Je n'en avais pas besoin, mais si vous le rendez invisible comme @Pete Hornsby (réponse acceptée) suggéré.
  • Comme @fivewood l'a mentionné n'utilise pas UIMarkupTextPrintFormatter mais webPreview.viewPrintFormatter() à la place, je ne sais pas pourquoi mais cela fonctionne comme ça.
  • Obligatoire: attendez que la vue Web soit chargée avant de générer le PDF ( indiqué ici ).
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/

0
Cinn

Tout d'abord, créez une instance UIWebView

var webView:UIWebView?

Après cela, créez la fonction pour init webView et ajoutez UIWebViewDelegate dans le contrôleur,

func initWebView(htmlString:String) {
  self.webView = UIWebView.init(frame:self.view.frame)
  self.webView?.delegate = self
  self.webView?.loadHTMLString(htmlString, baseURL: nil)
}

Après cela, écrivez PDF le code de conversion méthode 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);
       }
     }
}
0