web-dev-qa-db-fra.com

Appel de wkhtmltopdf pour générer PDF à partir de HTML

J'essaie de créer un fichier PDF à partir d'un fichier HTML. Après avoir parcouru un peu, j'ai trouvé: wkhtmltopdf pour être parfait. J'ai besoin d'appeler cela .exe à partir du serveur ASP.NET. J'ai tenté:

    Process p = new Process();
    p.StartInfo.UseShellExecute = false;
    p.StartInfo.FileName = HttpContext.Current.Server.MapPath("wkhtmltopdf.exe");
    p.StartInfo.Arguments = "TestPDF.htm TestPDF.pdf";
    p.Start();
    p.WaitForExit();

Sans succès de fichiers créés sur le serveur. Quelqu'un peut-il me donner un pointeur dans la bonne direction? J'ai mis le fichier wkhtmltopdf.exe dans le répertoire de niveau supérieur du site. Y a-t-il un autre endroit où il devrait se tenir?


Edit: Si quelqu'un a de meilleures solutions pour créer dynamiquement des fichiers pdf à partir de html, faites-le moi savoir.

52
Sean

Mise à jour:
Ma réponse ci-dessous, crée le fichier pdf sur le disque. J'ai ensuite diffusé ce fichier dans le navigateur des utilisateurs en téléchargement. Envisagez d'utiliser quelque chose comme la réponse de Hath ci-dessous pour obtenir à la place wkhtml2pdf pour la sortie vers un flux, puis envoyez-le directement à l'utilisateur - cela évitera de nombreux problèmes avec les autorisations de fichiers, etc.

Ma réponse originale:
Assurez-vous d'avoir spécifié un chemin de sortie pour le PDF accessible en écriture par le processus ASP.NET de IIS exécuté sur votre serveur) (généralement NETWORK_SERVICE je pense).

Le mien ressemble à ça (et ça marche):

/// <summary>
/// Convert Html page at a given URL to a PDF file using open-source tool wkhtml2pdf
/// </summary>
/// <param name="Url"></param>
/// <param name="outputFilename"></param>
/// <returns></returns>
public static bool HtmlToPdf(string Url, string outputFilename)
{
    // assemble destination PDF file name
    string filename = ConfigurationManager.AppSettings["ExportFilePath"] + "\\" + outputFilename + ".pdf";

    // get proj no for header
    Project project = new Project(int.Parse(outputFilename));

    var p = new System.Diagnostics.Process();
    p.StartInfo.FileName = ConfigurationManager.AppSettings["HtmlToPdfExePath"];

    string switches = "--print-media-type ";
    switches += "--margin-top 4mm --margin-bottom 4mm --margin-right 0mm --margin-left 0mm ";
    switches += "--page-size A4 ";
    switches += "--no-background ";
    switches += "--redirect-delay 100";

    p.StartInfo.Arguments = switches + " " + Url + " " + filename;

    p.StartInfo.UseShellExecute = false; // needs to be false in order to redirect output
    p.StartInfo.RedirectStandardOutput = true;
    p.StartInfo.RedirectStandardError = true;
    p.StartInfo.RedirectStandardInput = true; // redirect all 3, as it should be all 3 or none
    p.StartInfo.WorkingDirectory = StripFilenameFromFullPath(p.StartInfo.FileName);

    p.Start();

    // read the output here...
    string output = p.StandardOutput.ReadToEnd(); 

    // ...then wait n milliseconds for exit (as after exit, it can't read the output)
    p.WaitForExit(60000); 

    // read the exit code, close process
    int returnCode = p.ExitCode;
    p.Close(); 

    // if 0 or 2, it worked (not sure about other values, I want a better way to confirm this)
    return (returnCode == 0 || returnCode == 2);
}
51
MGOwen

J'ai eu le même problème lorsque j'ai essayé d'utiliser msmq avec un service Windows mais c'était très lent pour une raison quelconque. (la partie processus).

C'est ce qui a finalement fonctionné:

private void DoDownload()
{
    var url = Request.Url.GetLeftPart(UriPartial.Authority) + "/CPCDownload.aspx?IsPDF=False?UserID=" + this.CurrentUser.UserID.ToString();
    var file = WKHtmlToPdf(url);
    if (file != null)
    {
        Response.ContentType = "Application/pdf";
        Response.BinaryWrite(file);
        Response.End();
    }
}

public byte[] WKHtmlToPdf(string url)
{
    var fileName = " - ";
    var wkhtmlDir = "C:\\Program Files\\wkhtmltopdf\\";
    var wkhtml = "C:\\Program Files\\wkhtmltopdf\\wkhtmltopdf.exe";
    var p = new Process();

    p.StartInfo.CreateNoWindow = true;
    p.StartInfo.RedirectStandardOutput = true;
    p.StartInfo.RedirectStandardError = true;
    p.StartInfo.RedirectStandardInput = true;
    p.StartInfo.UseShellExecute = false;
    p.StartInfo.FileName = wkhtml;
    p.StartInfo.WorkingDirectory = wkhtmlDir;

    string switches = "";
    switches += "--print-media-type ";
    switches += "--margin-top 10mm --margin-bottom 10mm --margin-right 10mm --margin-left 10mm ";
    switches += "--page-size Letter ";
    p.StartInfo.Arguments = switches + " " + url + " " + fileName;
    p.Start();

    //read output
    byte[] buffer = new byte[32768];
    byte[] file;
    using(var ms = new MemoryStream())
    {
        while(true)
        {
            int read =  p.StandardOutput.BaseStream.Read(buffer, 0,buffer.Length);

            if(read <=0)
            {
                break;
            }
            ms.Write(buffer, 0, read);
        }
        file = ms.ToArray();
    }

    // wait or exit
    p.WaitForExit(60000);

    // read the exit code, close process
    int returnCode = p.ExitCode;
    p.Close();

    return returnCode == 0 ? file : null;
}

Merci Graham Ambrose et tout le monde.

41
Hath

OK, c'est donc une vieille question, mais une excellente. Et comme je n'ai pas trouvé de bonne réponse, j'ai fait la mienne :) De plus, j'ai posté ce projet super simple sur GitHub.

Voici un exemple de code:

var pdfData = HtmlToXConverter.ConvertToPdf("<h1>SOO COOL!</h1>");

Voici quelques points clés:

  • Pas de P/Invoke
  • Pas de création d'un nouveau processus
  • Pas de système de fichiers (tout en RAM)
  • Native .NET DLL avec intellisense, etc.
  • Possibilité de générer un PDF ou PNG (HtmlToXConverter.ConvertToPng)
16
Timothy Khouri

Consultez la bibliothèque d'encapsuleur C # (à l'aide de P/Invoke) pour la bibliothèque wkhtmltopdf: https://github.com/pruiz/WkHtmlToXSharp

7
Jason S

Vous pouvez dire à wkhtmltopdf d'envoyer sa sortie à sout en spécifiant "-" comme fichier de sortie. Vous pouvez ensuite lire la sortie du processus dans le flux de réponse et éviter les problèmes d'autorisations lors de l'écriture dans le système de fichiers.

5
Graham Ambrose

Il y a plusieurs raisons pour lesquelles c'est généralement une mauvaise idée. Comment allez-vous contrôler les exécutables qui sont générés mais qui finissent par vivre en mémoire en cas de plantage? Qu'en est-il des attaques par déni de service ou si quelque chose de malveillant pénètre dans TestPDF.htm?

Ma compréhension est que le compte d'utilisateur ASP.Net n'aura pas les droits de connexion locale. Il doit également disposer des autorisations de fichier appropriées pour accéder à l'exécutable et pour écrire dans le système de fichiers. Vous devez modifier la stratégie de sécurité locale et laisser le compte d'utilisateur ASP.Net (peut-être ASPNET) se connecter localement (il peut être dans la liste de refus par défaut). Ensuite, vous devez modifier les autorisations sur le système de fichiers NTFS pour les autres fichiers. Si vous êtes dans un environnement d'hébergement partagé, il peut être impossible d'appliquer la configuration dont vous avez besoin.

La meilleure façon d'utiliser un exécutable externe comme celui-ci est de mettre en file d'attente des travaux à partir du code ASP.NET et de disposer d'une sorte de service pour surveiller la file d'attente. Si vous faites cela, vous vous protégerez de toutes sortes de mauvaises choses. À mon avis, les problèmes de maintenance liés au changement de compte utilisateur ne valent pas l'effort, et bien que la mise en place d'un service ou d'un travail planifié soit pénible, c'est juste une meilleure conception. La page ASP.NET doit interroger une file d'attente de résultats pour la sortie et vous pouvez présenter à l'utilisateur une page d'attente. Ceci est acceptable dans la plupart des cas.

5
Brian Lyttle

Mon point de vue sur cela avec des trucs de 2018.

J'utilise async. Je diffuse vers et depuis wkhtmltopdf. J'ai créé un nouveau StreamWriter parce que wkhtmltopdf attend utf-8 par défaut mais il est défini sur autre chose lorsque le processus démarre.

Je n'ai pas inclus beaucoup d'arguments car ceux-ci varient d'un utilisateur à l'autre. Vous pouvez ajouter ce dont vous avez besoin à l'aide d'arguments supplémentaires.

J'ai supprimé p.WaitForExit (...) car je ne gérais pas s'il échouait et il se bloquerait quand même sur await tStandardOutput. Si un délai d'attente est nécessaire, vous devrez alors appeler Wait(...) sur les différentes tâches avec un jeton d'annulation ou un délai d'expiration et gérer en conséquence.

public async Task<byte[]> GeneratePdf(string html, string additionalArgs)
{
    ProcessStartInfo psi = new ProcessStartInfo
    {
        FileName = @"C:\Program Files\wkhtmltopdf\wkhtmltopdf.exe",
        UseShellExecute = false,
        CreateNoWindow = true,
        RedirectStandardInput = true,
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        Arguments = "-q -n " + additionalArgs + " - -";
    };

    using (var p = Process.Start(psi))
    using (var pdfSream = new MemoryStream())
    using (var utf8Writer = new StreamWriter(p.StandardInput.BaseStream, 
                                             Encoding.UTF8))
    {
        await utf8Writer.WriteAsync(html);
        utf8Writer.Close();
        var tStdOut = p.StandardOutput.BaseStream.CopyToAsync(pdfSream);
        var tStdError = p.StandardError.ReadToEndAsync();

        await tStandardOutput;
        string errors = await tStandardError;

        if (!string.IsNullOrEmpty(errors)) { /* deal/log with errors */ }

        return pdfSream.ToArray();
    }
}

Choses que je n'ai pas incluses ici mais qui pourraient être utiles si vous avez des images, des CSS ou d'autres choses que wkhtmltopdf devra charger lors du rendu de la page html:

  • vous pouvez passer le cookie d'authentification en utilisant --cookie
  • dans l'en-tête de la page html, vous pouvez définir la balise de base avec href pointant vers le serveur et wkhtmltopdf l'utilisera si besoin est
2
Yepeekai

Merci pour la question/réponse/tous les commentaires ci-dessus. Je suis tombé sur cela lorsque j'écrivais mon propre wrapper C # pour WKHTMLtoPDF et cela a répondu à quelques problèmes que j'avais. J'ai fini par écrire à ce sujet dans un article de blog - qui contient également mon wrapper (vous verrez sans aucun doute "l'inspiration" des entrées ci-dessus s'infiltrer dans mon code ...)

http://icanmakethiswork.blogspot.de/2012/04/making-pdfs-from-html-in-c-using.html

Merci encore les gars!

2
John Reilly

Généralement, le code retour = 0 arrive si le fichier pdf est créé correctement et correctement.Si il n'est pas créé, la valeur est dans la plage -ve.

0
Sukanya

Le processus ASP .Net n'a probablement pas accès en écriture au répertoire.

Essayez de lui dire d'écrire à %TEMP%, et voyez si cela fonctionne.

En outre, faites en sorte que votre page ASP .Net fasse écho à stdout et stderr du processus et vérifiez les messages d'erreur.

0
SLaks