Вызов wkhtmltopdf для создания PDF из HTML
Я пытаюсь создать файл PDF из файла HTML. Осмотрев немного, я нашел: wkhtmltopdf, чтобы быть идеальным. Мне нужно вызвать этот .exe с сервера ASP.NET. Я попытался:
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 в каталог верхнего уровня сайта. Есть ли где-нибудь еще это должно быть проведено?
Изменить: Если у кого-то есть лучшие решения для динамического создания PDF файлов из html, пожалуйста, дайте мне знать.
Ответы
Ответ 1
Update:
Мой ответ ниже, создает файл pdf на диске. Затем я передал этот файл в браузер пользователей в качестве загрузки. Подумайте, используя что-то вроде Hath ниже, чтобы получить wkhtml2pdf для вывода в поток вместо этого, а затем отправить его непосредственно пользователю - это обходит множество проблем с разрешениями на файлы и т.д.
Мой оригинальный ответ:
Убедитесь, что вы указали путь вывода для PDF, который можно записать процессом ASP.NET IIS, запущенным на вашем сервере (обычно это 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);
}
Ответ 2
У меня была та же проблема, когда я пытался использовать 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;
}
Спасибо Грэм Амброуз и всем остальным.
Ответ 3
Хорошо, так что это старый вопрос, но отличный. И поскольку я не нашел хорошего ответа, я сделал свой собственный. Кроме того, я опубликовал этот супер простой проект для GitHub.
Вот пример кода:
var pdfData = HtmlToXConverter.ConvertToPdf("<h1>SOO COOL!</h1>");
Вот несколько ключевых моментов:
- Нет P/Invoke
- Нет создания нового процесса.
- Нет файловой системы (все в ОЗУ)
- Встроенная .NET DLL с intellisense и т.д.
- Возможность генерации PDF или PNG (
HtmlToXConverter.ConvertToPng
)
Ответ 4
Проверьте библиотеку оболочки С# (используя P/Invoke) для библиотеки wkhtmltopdf: https://github.com/pruiz/WkHtmlToXSharp
Ответ 5
Есть много причин, почему это, как правило, плохая идея. Как вы собираетесь контролировать исполняемые файлы, которые генерируются, но в конечном итоге живут в памяти, если есть сбой? Как насчет атак типа "отказ в обслуживании" или если что-то вредоносное попадает в TestPDF.htm?
Я понимаю, что учетная запись пользователя ASP.NET не будет иметь права на локальный вход в систему. Он также должен иметь правильные разрешения для доступа к исполняемому файлу и записи в файловую систему. Вам нужно отредактировать локальную политику безопасности и позволить учетной записи пользователя ASP.NET(возможно, ASPNET) локально (она может быть в списке запретов по умолчанию). Затем вам нужно отредактировать разрешения для файловой системы NTFS для других файлов. Если вы находитесь в среде общедоступного хостинга, вам может быть невозможно применить требуемую конфигурацию.
Лучший способ использовать внешний исполняемый файл, как это, - это заказывать задания из кода ASP.NET и иметь некоторую службу мониторинга очереди. Если вы сделаете это, вы защитите себя от всякого рода плохих событий. По моему мнению, проблемы обслуживания с изменением учетной записи пользователя не стоят усилий, и в то время как настройка службы или запланированной работы - это боль, ее просто лучший дизайн. Страница ASP.NET должна опросить очередь результатов для вывода, и вы можете представить пользователю страницу ожидания. Это приемлемо в большинстве случаев.
Ответ 6
Вы можете сообщить wkhtmltopdf, чтобы отправить его вывод в sout, указав в качестве выходного файла "-".
Затем вы можете прочитать результат процесса в поток ответов и избежать проблем с правами на запись в файловую систему.
Ответ 7
Спасибо за вопрос/ответ/все комментарии выше. Я столкнулся с этим, когда писал свою собственную оболочку на С# для WKHTMLtoPDF, и это помогло мне решить пару проблем. Я закончил тем, что написал об этом в блоге, который также содержит мою обертку (вы, несомненно, увидите "вдохновение" из приведенных выше записей, просачивающихся в мой код...)
Создание PDF файлов из HTML в С# с использованием WKHTMLtoPDF
Еще раз спасибо, ребята!
Ответ 8
Мой взгляд на вещи с 2018 года.
Я использую асинхронный. Я транслирую в и из wkhtmltopdf. Я создал новый StreamWriter, потому что wkhtmltopdf ожидает utf-8 по умолчанию, но при запуске процесса он настроен на что-то другое.
Я не включил много аргументов, так как они варьируются от пользователя к пользователю. Вы можете добавить то, что вам нужно, используя дополнительные Args.
Я удалил 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
- в заголовке html-страницы вы можете установить базовый тег с помощью href, указывающего на сервер, и wkhtmltopdf будет использовать его в случае необходимости
Ответ 9
Процесс ASP. Net, вероятно, не имеет права на запись в каталог.
Попробуйте записать его в %TEMP%
и посмотрите, работает ли он.
Кроме того, сделайте страницу ASP.Net повторите процесс stdout и stderr и проверьте наличие сообщений об ошибках.
Ответ 10
Как правило, возвращается код = 0, если файл pdf создается правильно и правильно. Если он не создан, значение находится в диапазоне -ve.
Ответ 11
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();
}
}