使用wkhtmltopdf从HTML生成PDF

54

我正在尝试从 HTML 文件创建 PDF 文件。在看了一些资料后,我发现 wkhtmltopdf 是完美的解决方案。我需要从 ASP.NET 服务器调用该 .exe 文件。我已经尝试过:

    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();

我尝试在服务器上生成文件但没有成功。有人能给我指点一下吗?我将wkhtmltopdf.exe文件放在站点的顶级目录中,它还需要放在其他地方吗?


编辑: 如果有更好的解决方案可以从HTML动态创建PDF文件,请告诉我。


你的应用程序是否因此操作产生任何异常?命令行操作是否产生任何异常或错误? - Nathan Taylor
不,它没有产生任何异常。我实际上看到命令提示符非常快地出现。如果我不使用HttpContext.Current.Server.MapPath(),我会得到一个文件未找到的异常。 - Sean
你可以尝试使用FileMon或其他Sysinternals工具来查看未找到的文件。你也尝试过指定绝对路径吗? - Brian Lyttle
请参阅http://stackoverflow.com/questions/tagged/pdf-generation。 - John Saunders
11个回答

51

更新:
我的下面的答案会在磁盘上创建pdf文件。然后我将该文件作为下载流式传输到用户的浏览器中。考虑使用像下面Hath的答案一样让wkhtml2pdf输出到一个流,然后直接将其发送给用户-这将绕过许多与文件权限等相关的问题。

我的原始回答:
确保你已经为PDF指定了一个可被运行在你的服务器上的IIS进程(通常是NETWORK_SERVICE I think)写入的输出路径。

我的路径看起来像这样(并且它可以工作):

/// <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);
}

3
"return (returnCode <= 2)" 应该改为 "return (returnCode == 0 || returnCode == 2)",因为如果输出文件已经存在,你将会收到 '1' 的返回值,所以在执行进程之前要先进行检查。 - bob
我不认为这段代码能在IIS上运行。你会遇到访问被拒绝的问题,因为默认的IIS用户账户不允许执行exe文件。 - Tomas
+1 非常有帮助。感谢您发布这段代码。我不完全确定您是否需要WaitForExit()调用。Start对我来说从未立即返回...您是否在等待输出读出? - JasonCoder
我收到了“1”作为返回代码,但目录是空的,所以我猜@bob并不完全正确。 - marquito
在调试模式下,当它进入p.StandardOutput.ReadToEnd()时,我得到了太多这样的行: 线程'<No Name>'(0x1e40)已退出,代码为0(0x0)... 然后它继续... 没有尽头。 - Pnctovski
显示剩余3条评论

41

当我尝试在Windows服务中使用MSMQ时,我也遇到了同样的问题,但是它出现了某些原因非常慢(进程部分)。

这是最终解决的方法:

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;
}

感谢Graham Ambrose和其他所有人。


我正在尝试测试您的解决方案,如果它能够正常工作,那将是对我极大的帮助。但是我想把我的.aspx文件转换成PDF而不是URL,这种方式可行吗?因此,我已经用以下代码替换了您的变量:var url = HttpContext.Current.Server.MapPath("~/wkhtmltopdf/chartImage.aspx"); 但是它没有起作用。 - Armance
2
@astrocybernaute aspx 需要一个服务器才能从中生成 HTML,因此您需要使用服务器调用它而不是直接调用 :) - Joel Peltonen

20

好的,这是一个旧问题,但是很好。由于我没有找到一个好的答案,所以我自己做了一个 :) 此外,我已经将这个超级简单的项目发布到GitHub上。

下面是一些示例代码:

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

以下是一些关键点:

  • 无需 P/Invoke
  • 无需创建新进程
  • 无需文件系统(全部在 RAM 中)
  • 本地 .NET DLL,具有智能感知等功能
  • 可生成 PDF 或 PNG (HtmlToXConverter.ConvertToPng)

2
我不确定为什么每个人都没有给你的解决方案三倍的星,这正是每个人所寻找的。将原始的C应用程序转换为在内存中运行并返回字节数组。工作做得非常好! - Dave
1
Nuget包安装总是失败,编译后的DLL始终提示缺少程序集或引用错误。 - SMUsamaShah
@LifeH2O 哪个 NuGet 包?我在这个项目中找不到。 - Ergwun
@LifeH2O 谢谢。我安装也出现了同样的失败 :( - Ergwun
@LifeH2O Nuget不起作用。我在尝试安装时遇到以下错误:http://pastebin.com/9RVxTeB3 - slayernoah

7

5
您可以通过将输出文件指定为“ -”来告诉wkhtmltopdf将其输出发送到sout。 然后,您可以从进程中读取输出到响应流并避免写入文件系统的权限问题。

5
这通常是一个不好的想法,原因有很多。如果发生崩溃,你如何控制被产生的可执行文件在内存中继续存在?如果出现拒绝服务攻击,或者某些恶意内容进入TestPDF.htm怎么办?
我理解ASP.NET用户帐户将没有本地登录权限。它还需要具有访问可执行文件和写入文件系统的正确文件权限。您需要编辑本地安全策略,并允许ASP.NET用户帐户(可能是ASPNET)本地登录(默认情况下可能在拒绝列表中)。然后,您需要编辑其他文件的NTFS文件系统上的权限。如果您处于共享托管环境中,则可能无法应用所需的配置。
使用外部可执行文件的最佳方法是从ASP.NET代码中排队作业,并让某种类型的服务监视队列。如果这样做,您将保护自己免受各种糟糕事情的影响。根据我的经验,更改用户帐户的维护问题不值得付出努力,虽然设置服务或计划任务很麻烦,但这只是更好的设计。ASP.NET页面应轮询输出队列以获取输出,并且您可以向用户呈现等待页面。在大多数情况下,这是可以接受的。

嗯,我明白了。你能提供一个更好的方法吗? - Sean
1
MSMQ + Windows Services 是一种常见的方法。 - Noon Silk
为了跟进这个问题,你可以搜索一下,或者我在这里简要地描述一下:https://dev59.com/kXM_5IYBdhLWcg3wlELO - Noon Silk
MSMQ + Windows Services 是一种特定的方法。如果你不知道如何使用 MSMQ 或者不想依赖它,通常可以使用 SQL Server 实现某些功能。要寻找的一般性东西是排队系统,其中 MSMQ 只是其中之一。 - Brian Lyttle
你可能不应该给ASP.NET用户帐户任何额外的权限,否则可能会出现安全问题。如果可能的话,你应该为这个操作模拟一个特殊的帐户,并赋予非常有限的权限。 - Yuriy Faktorovich

3

我对2018年的情况有所了解。

我正在使用异步。我正在向wkhtmltopdf流式传输数据。我创建了一个新的StreamWriter,因为wkhtmltopdf默认期望utf-8,但在进程开始时设置为其他编码。

我没有包含很多参数,因为这些参数因用户而异。您可以使用additionalArgs添加所需内容。

我删除了p.WaitForExit(...),因为我没有处理它失败的情况,而且在await tStandardOutput上也会挂起。如果需要超时,则必须在不同的任务上调用Wait(...),并使用取消令牌或超时进行处理。

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();
    }
}

下面是一些可能有用的内容,如果你的html页面中有图片、css或其他需要wkhtmltopdf加载的资源:

  • 你可以使用--cookie参数传递身份验证cookie
  • 在html页面头部,你可以设置base标签并将href指向服务器,wkhtmltopdf会在必要时使用它

2
感谢提问/回答/上面的所有评论。我在编写自己的C#包装器WKHTMLtoPDF时遇到了一些问题,这篇文章解决了我的几个问题。最终,我在博客文章中写了关于此的内容 - 该文章还包含我的包装器(您无疑会看到上述条目中的“灵感”渗入到我的代码中...) 使用WKHTMLtoPDF在C#中从HTML制作PDF 再次感谢大家!

0
通常情况下,如果 PDF 文件被正确创建,则返回代码 = 0。如果未创建,则值在负范围内。

0

ASP .Net 进程可能没有对该目录的写入权限。

尝试让它写入 %TEMP%,看看是否可以正常工作。

此外,让你的 ASP .Net 页面回显进程的标准输出和标准错误,并检查错误消息。


不确定,不是我干的。谢谢提供信息,不过我会测试一下的。看来我应该采用其他方式从HTML创建PDF文件。 - Sean
有.NET的包装器,http://csharp-source.net/open-source/pdf-libraries来源于一个快速的谷歌搜索。 - Yuriy Faktorovich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接