Resumo pra quem tem pressa:
Criei uma lib Open Source com licença MIT para converter HTML para PDF.
https://github.com/angelobelchior/Html2Pdf
Para usar:
Instale o pacote...
dotnet add package Html2Pdf.Lib
E converta seu HTML para PDF.
using Html2Pdf.Lib;
var arguments = new Arguments().
.SetPageOrientation(PageOrientation.Landscape)
.SetPageSize(PageSize.A4);
var html = "<h1>Html to PDF</h1><p>Html to PDF converter</p>";
var pdf = Converter.FromHtml(html, arguments);
File.WriteAllBytes($"html2pdf.pdf", pdf);
É isso...
Quem nunca precisou gerar PDF a partir de um HTML?
Aliás, a gente só faz dessa forma porque é um parto de Girafa criar PDF na mão. Sendo assim, criar um HTML e converter é a forma mais simples de se criar esse tão famigerado arquivo.
Até hoje eu me questiono por que ainda usamos o PDF para compartilhamento de documentos. Já passou da hora de termos um formato aberto, e mais simples. Nada contra a Adobe, mas só quem precisa lidar com esse tipo de coisa, sabe o quanto o pâncreas dói.
Conversando com o Carlos dos Santos, a lenda do Paraná, ele me indicou esse repositório https://github.com/carloscds/HtmlToPDFCore onde ele mantém uma biblioteca que converte HTML para PDF.
Fui olhar com calma e vi que esse pacote utiliza uma outra lib chamada Wkhtmltopdf.NetCore. E além disso o target estava setado para dotnet core 3.1. Isso pra minha necessidade, era um problema. Eu precisava de algo que fosse netstandard2.1!
Caso você queira saber mais sobre netstandard recomendo a leitura desse artigo: https://learn.microsoft.com/pt-br/dotnet/standard/net-standard?tabs=net-standard-1-0
Logo de cara eu pensei em migrar o projeto do HtmlToPDFCore para netstandard2.1, porém esbarrei em um grande problema: a lib Wkhtmltopdf.NetCore não suportava essa migração!
Fui então olhar o repositório da lib Wkhtmltopdf.NetCore e logo de cara vi que o projeto foi descontinuado. Mesmo assim, naveguei entre o código para entender mais como as coisas funcionavam.
Percebi um grande uso de reflection, não que isso seja um grande problema, mas eu penso que é bom a gente evitar ao máximo isso, justamente para não termos problemas ao compilar nossas aplicações em AOT. Fiz um post que fala um pouco sobre isso e recomendo muito a leitura: https://dev.to/angelobelchior/net-8-jit-e-aot-25lk.
Mais adiante entendi o que, de fato, a lib fazia: Ela simplesmente invocava um executável chamado wkhtmltopdf passando inúmeros argumentos para efetuar a conversão.
Esse executável foi criado pelo time do QT e você pode ler mais sobre ele aqui.
Para que essa lib funcione é necessário que o executável wkhtmltopdf esteja dentro do pacote. E é aqui onde temos o maior ponto de atenção da análise. Não é apenas um executável. São três, um para cada sistema operacional, e além disso, temos mais um arquivo de suporte, totalizando seis arquivos.
Ao compilar, existem tasks configuradas no .csproj que copiam esses binários para as suas respectivas pastas juntamente com os arquivos da lib. O código utiliza reflection para obter o caminho do executável baseado no Assembly em execução. Outro ponto que me fez torcer o nariz... existem formas de se fazer isso sem utilizar reflection.
Mais adiante eu percebi que a lib Wkhtmltopdf.NetCore tinha dentro dela métodos do namespace Microsoft.AspNetCore.Mvc para renderizar uma View como string ou algo do tipo. Pra mim, tudo isso era muito confuso...
Pois bem, a minha necessidade era ter algo simples para gerar PDF a partir de um HTML. Uma lib que fosse bem enxuta, direta ao ponto.
São três cenários simples que eu precisava atender com essa lib:
- Gerar PDF a partir de um HTML.
- Gerar PDF a partir de uma Url.
- Gerar PDF a partir de um template com suporte a sintaxe do Razor.
Não era pedir muito... sendo assim, resolvi criar uma nova biblioteca, atendendo esses três cenários. (Ok, são quatro, na documentação vocês vão ver qual é o quarto cenário...).
Esse projeto segue as mesmas ideias do projeto https://github.com/carloscds/HtmlToPDFCore e Wkhtmltopdf.NetCore, porém criei uma forma mais fluente de se configurar as propriedades do PDF:
var arguments = new Arguments()
.SetImageQuality(80)
.SetPageMargins(10, 10, 10, 10)
.WithGrayScale()
.SetPageOrientation(PageOrientation.Landscape);
var html = "<h1>Html to PDF</h1><p>Html to PDF converter</p>";
var pdf = Converter.FromHtml(html, arguments);
File.WriteAllBytes($"html2pdf.pdf", pdf);
Existem muitos outros argumentos que podemos configurar e podemos, inclusive, não passar nenhum argumento, além de termos quatro métodos para a conversão:
O Converter.FromHtml
mostrado acima.
O Converter.FromUrl
que, como o nome sugere espera uma URL.
E os métodos FromRazorTemplate<T>
e FromRazorTemplateBatch<T>
. E é aqui que a brincadeira fica mais legal!
Uma das necessidades mais recorrentes ao precisar gerar PDF, é trabalhar de forma dinâmica, isto é, ter um template, com algumas marcações que, em um determinado processo, substitui essas marcações por determinados valores. Quem já precisou enviar e-mail em lote sabe bem do que eu estou falando.
Porém, essa abordagem não é tão flexível, tem cenários onde é preciso aplicar determinadas condições - ifs - ou iterar por uma lista - famigerado foreach.
E é ai que entra nosso querido Razor e sua sintaxe garbosa.
Usando o método FromRazorTemplate<T>
, é possível gerarmos um PDF a partir de um template como esse:
public record Product(string Name, decimal Price);
public record Order(string CustomerName, string CustomerAddress, string CustomerPhoneNumber, List<Product> Products);
var razorTemplate =
"""
<!DOCTYPE html>
<html>
<head>
<title>Customer Details</title>
</head>
<body>
<h1>Customer Details</h1>
<p><strong>Name:</strong> @Model.CustomerName</p>
<p><strong>Address:</strong> @Model.CustomerAddress</p>
<p><strong>Phone Number:</strong> @Model.CustomerPhoneNumber</p>
<h2>Products (@Model.Products.Count)</h2>
@if(Model.Products.Any())
{
<table>
<thead>
<tr>
<th>Product Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model.Products)
{
<tr>
<td>@product.Name</td>
<td>@product.Price.ToString("C")</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>No products found.</p>
}
</body>
</html>
""";
var order = new Order("Roberto Rivellino", "Rua São Jorge, 777", "+55 11 912345678", [
new("Product 1", 9.99m),
new("Product 2", 19.99m),
new("Product 3", 29.99m)
]);
var byteArrayRazorTemplate = Converter.FromRazorTemplate(razorTemplate, order, arguments);
Basicamente podemos utilizar o poder do csharp juntamente com o HTML, remetendo aos bons e velhos tempos do ASP 3 para gerarmos um PDF com valores dinâmicos. Não é incrível? Tirando a parte do ASP 3 é incrível sim!!!
Mas não é só isso! Muitas vezes precisamos gerar um lote de PDFs, dada uma lista de itens, por exemplo. Algo como, gerar informe de imposto de renda para um conjunto de pessoas.
E é pra isso que temos o método FromRazorTemplateBatch
:
var byteArrayRazorTemplateList = Converter.FromRazorTemplateBatch(razorTemplate, [ order1, order2, order3, order4 ], arguments);
for (int i = 0; i < byteArrayRazorTemplateList.Count; i++)
{
var pdf = byteArrayRazorTemplateList.ElementAt(i);
...
}
E você deve estar se perguntando, como que o template com sintaxe razor é "compilado". Para tal feito, utilizo a lib Open Source e MIT RazorEngineCore. Nada se cria, tudo se reaproveita! Essa é a verdadeira força da comunidade!!
Recomendo muito que você acesse esse repositório e deixe aquele like maroto. O time merece!
Por fim, dentro das minhas necessidades, o HTML2PDF.Lib atende super bem. E, pensando de forma mais ampla, creio que vá atender muitos outros cenários.
Por ser totalmente Open Source e de licença MIT, você pode fazer um fork e criar sua própria versão \o/.
Ou, pode mandar um Pull Request com correções e/ou melhorias.
Espero que o HTML2PDF.Lib sejá útil pra você. E se for, por favor, deixe aquela estrelinha no github.
Quero manter esse pacote sempre atualizado e espero que mais e mais pessoas nos ajude nessa tarefa.
É isso!
Até a próxima!!!
Top comments (0)