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 文件,请告诉我。

4

11 回答 11

51

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

我的原始答案:
确保您指定了 PDF 的输出路径,该路径可由服务器上运行的 IIS 的 ASP.NET 进程写入(我认为通常是 NETWORK_SERVICE)。

我的看起来像这样(并且有效):

/// <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);
}
于 2009-11-09T02:43:38.477 回答
41

当我尝试将 msmq 与 Windows 服务一起使用时,我遇到了同样的问题,但由于某种原因它非常慢。(过程部分)。

这就是最终奏效的方法:

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

感谢格雷厄姆·安布罗斯和其他所有人。

于 2010-09-10T10:07:56.763 回答
20

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

这是一些示例代码:

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

以下是一些关键点:

  • 没有 P/Invoke
  • 不创建新流程
  • 没有文件系统(全部在 RAM 中)
  • 带有智能感知等的本机 .NET DLL
  • 能够生成 PDF 或 PNG ( HtmlToXConverter.ConvertToPng)
于 2014-12-22T23:33:32.150 回答
7

查看 wkhtmltopdf 库的 C# 包装器库(使用 P/Invoke):https ://github.com/pruiz/WkHtmlToXSharp

于 2011-04-04T23:07:26.350 回答
5

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

于 2010-03-03T21:52:21.880 回答
5

这通常是一个坏主意有很多原因。如果发生崩溃,您将如何控制生成但最终存在于内存中的可执行文件?拒绝服务攻击怎么样,或者如果恶意软件进入 TestPDF.htm 呢?

我的理解是 ASP.NET 用户帐户将没有本地登录的权限。它还需要具有正确的文件权限才能访问可执行文件并写入文件系统。您需要编辑本地安全策略并让 ASP.NET 用户帐户(可能是 ASPNET)在本地登录(可能默认在拒绝列表中)。然后您需要编辑 NTFS 文件系统上其他文件的权限。如果您在共享主机环境中,则可能无法应用您需要的配置。

使用像这样的外部可执行文件的最佳方法是从 ASP.NET 代码中对作业进行排队,并让某种服务监视队列。如果你这样做,你将保护自己免受各种坏事的影响。在我看来,更改用户帐户的维护问题不值得付出努力,虽然设置服务或计划工作很痛苦,但它只是一个更好的设计。ASP.NET 页面应该轮询输出的结果队列,您可以向用户显示等待页面。这在大多数情况下是可以接受的。

于 2009-08-26T01:47:41.133 回答
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();
    }
}

我没有包含在其中的东西,但如果你有图像、css 或其他 wkhtmltopdf 在呈现 html 页面时必须加载的东西,它们可能会很有用:

  • 您可以使用 --cookie 传递身份验证 cookie
  • 在 html 页面的标题中,您可以设置带有 href 指向服务器的基本标记,如果需要,wkhtmltopdf 将使用它
于 2018-11-27T21:54:24.910 回答
2

感谢您的问题/回答/上面的所有评论。当我为 WKHTMLtoPDF 编写自己的 C# 包装器时,我遇到了这个问题,它回答了我遇到的几个问题。我最终在一篇博文中写到了这一点——其中还包含了我的包装(毫无疑问,你会从上面的条目中看到“灵感”渗入我的代码......)

使用 WKHTMLtoPDF 在 C# 中从 HTML 制作 PDF

再次感谢各位!

于 2012-04-05T07:52:54.893 回答
0

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

试着告诉它写信%TEMP%,看看它是否有效。

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

于 2009-08-26T01:47:13.210 回答
0

如果正确且正确地创建了 pdf 文件,则通常返回代码 =0。如果未创建,则该值在 -ve 范围内。

于 2011-12-17T06:33:35.247 回答
-1
using System;
using System.Diagnostics;
using System.Web;

public partial class pdftest : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }
    private void fn_test()
    {
        try
        {
            string url = HttpContext.Current.Request.Url.AbsoluteUri;
            Response.Write(url);
            ProcessStartInfo startInfo = new ProcessStartInfo();
            startInfo.FileName = 
                @"C:\PROGRA~1\WKHTML~1\wkhtmltopdf.exe";//"wkhtmltopdf.exe";
            startInfo.Arguments = url + @" C:\test"
                 + Guid.NewGuid().ToString() + ".pdf";
            Process.Start(startInfo);
        }
        catch (Exception ex)
        {
            string xx = ex.Message.ToString();
            Response.Write("<br>" + xx);
        }
    }
    protected void btn_test_Click(object sender, EventArgs e)
    {
        fn_test();
    }
}
于 2010-02-06T16:25:35.013 回答