From 2b7d19199a5009a223cb6fd0488e54fa6dcb0918 Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner <lorenz.stechauner@necronda.net> Date: Wed, 6 Sep 2023 16:01:48 +0200 Subject: [PATCH] Add WeasyPrint to convert PDFs --- Elwig/App.xaml.cs | 5 - Elwig/Documents/BusinessDocument.cshtml | 4 +- Elwig/Documents/BusinessDocument.cshtml.cs | 2 +- Elwig/Documents/BusinessLetter.cshtml | 2 + Elwig/Documents/DeliveryNote.cshtml | 34 +++--- Elwig/Documents/Document.cshtml | 11 +- Elwig/Documents/Document.cshtml.cs | 30 +++-- Elwig/Documents/Pdf.cs | 73 +++++------- Elwig/Documents/style.css | 123 +++++++++++---------- Elwig/Elwig.csproj | 3 +- Elwig/fetch-resources.bat | 3 +- 11 files changed, 136 insertions(+), 154 deletions(-) diff --git a/Elwig/App.xaml.cs b/Elwig/App.xaml.cs index 3371549..f0e4812 100644 --- a/Elwig/App.xaml.cs +++ b/Elwig/App.xaml.cs @@ -137,11 +137,6 @@ namespace Elwig { base.OnStartup(evt); } - protected override void OnExit(ExitEventArgs evt) { - Utils.RunBackground("PDF Close", () => Documents.Pdf.Close()); - base.OnExit(evt); - } - private void PrintingReadyChanged() { Dispatcher.BeginInvoke(OnPrintingReadyChanged, new EventArgs()); } diff --git a/Elwig/Documents/BusinessDocument.cshtml b/Elwig/Documents/BusinessDocument.cshtml index 5633e55..981a88f 100644 --- a/Elwig/Documents/BusinessDocument.cshtml +++ b/Elwig/Documents/BusinessDocument.cshtml @@ -14,6 +14,4 @@ </div> <aside>@Raw(Model.Aside)</aside> </div> -<main> - @RenderBody() -</main> +@RenderBody() diff --git a/Elwig/Documents/BusinessDocument.cshtml.cs b/Elwig/Documents/BusinessDocument.cshtml.cs index 62cd759..b415bc3 100644 --- a/Elwig/Documents/BusinessDocument.cshtml.cs +++ b/Elwig/Documents/BusinessDocument.cshtml.cs @@ -13,7 +13,7 @@ namespace Elwig.Documents { Location = App.BranchName; IncludeSender = includeSender; var uid = (m.UstIdNr ?? "-") + (m.IsBuchführend ? "" : " <i>(pauschaliert)</i>"); - Aside = $"<table><colgroup><col span='1' style='width: 2.25cm;'/><col span='1' style='width: 100%;'/></colgroup>" + + Aside = $"<table><colgroup><col span='1' style='width: 22.5mm;'/><col span='1' style='width: 42.5mm;'/></colgroup>" + $"<thead><tr><th colspan='2'>Mitglied</th></tr></thead><tbody>" + $"<tr><th>Mitglieds-Nr.</th><td>{m.MgNr}</td></tr>" + $"<tr><th>Betriebs-Nr.</th><td>{m.LfbisNr}</td></tr>" + diff --git a/Elwig/Documents/BusinessLetter.cshtml b/Elwig/Documents/BusinessLetter.cshtml index d10323e..e6c9c67 100644 --- a/Elwig/Documents/BusinessLetter.cshtml +++ b/Elwig/Documents/BusinessLetter.cshtml @@ -2,6 +2,8 @@ @inherits TemplatePage<Elwig.Documents.BusinessLetter> @model Elwig.Documents.BusinessLetter @{ Layout = "BusinessDocument"; } +<main> <p>Sehr geehrtes Mitglied,</p> <p>nein.</p> <p>Mit freundlichen Grüßen<br/>Ihre Winzergenossenschaft</p> +</main> diff --git a/Elwig/Documents/DeliveryNote.cshtml b/Elwig/Documents/DeliveryNote.cshtml index 0627836..d89de8b 100644 --- a/Elwig/Documents/DeliveryNote.cshtml +++ b/Elwig/Documents/DeliveryNote.cshtml @@ -2,6 +2,7 @@ @inherits TemplatePage<Elwig.Documents.DeliveryNote> @model Elwig.Documents.DeliveryNote @{ Layout = "BusinessDocument"; } +<main> <div class="date">@Model.Location, am @($"{Model.Date:dd.MM.yyyy}")</div> <h1>@Model.Title</h1> @{ @@ -44,15 +45,15 @@ </script> <table class="delivery"> <colgroup> - <col style="width: 1cm;"/> - <col style="width: 25%;"/> - <col style="width: 25%;"/> - <col style="width: 25%;"/> - <col style="width: 25%;"/> - <col style="width: 3cm;"/> - <col style="width: 1.25cm;"/> - <col style="width: 1.25cm;"/> - <col style="width: 1.5cm;"/> + <col style="width: 10.00mm;"/> + <col style="width: 21.25mm;"/> + <col style="width: 21.25mm;"/> + <col style="width: 21.25mm;"/> + <col style="width: 21.25mm;"/> + <col style="width: 30.00mm;"/> + <col style="width: 12.50mm;"/> + <col style="width: 12.50mm;"/> + <col style="width: 15.00mm;"/> </colgroup> <thead> <tr> @@ -115,13 +116,13 @@ <div id="delivery-stats"> <table class="delivery-stats"> <colgroup> - <col style="width: 100%;"/> - <col style="width: 2cm;"/> - <col style="width: 2cm;"/> - <col style="width: 2cm;"/> - <col style="width: 2cm;"/> - <col style="width: 2cm;"/> - <col style="width: 2cm;"/> + <col style="width: 45mm;"/> + <col style="width: 20mm;"/> + <col style="width: 20mm;"/> + <col style="width: 20mm;"/> + <col style="width: 20mm;"/> + <col style="width: 20mm;"/> + <col style="width: 20mm;"/> </colgroup> <thead> <tr> @@ -162,6 +163,7 @@ </table> </div> } +</main> @for (int i = 0; i < 2; i++) { <div class="@(i == 0 ? "hidden" : "bottom")"> @if (Model.Text != null) { diff --git a/Elwig/Documents/Document.cshtml b/Elwig/Documents/Document.cshtml index 01fbac3..d1d31bf 100644 --- a/Elwig/Documents/Document.cshtml +++ b/Elwig/Documents/Document.cshtml @@ -5,27 +5,24 @@ <html lang="de-AT"> <head> <title>@Model.Title</title> + <meta name="author" value="@Model.Author"/> <meta charset="UTF-8"/> <script> - window.PagedConfig = { auto: false }; - if (!navigator.webdriver) { - window.addEventListener("beforeprint", async () => { await window.PagedPolyfill.preview(); }); - window.addEventListener("afterprint", () => { location.reload(); }); - } - const heightA4 = 297, widhtA4 = 210, heightFooter = 35, heightHeader = 25; const heightMain = heightA4 - heightFooter - heightHeader; function px2mm(px1, px2) { return (px2 - px1 + 1) * 2.54 / 96 * window.devicePixelRatio * 10; } </script> - <script src="file:///@Raw(Model.DataPath)\resources\paged.polyfill.js"></script> <link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\style.css"/> </head> <body> <div class="m1"></div> <div class="m2"></div> <div class="m3"></div> + <div class="m1 r"></div> + <div class="m2 r"></div> + <div class="m3 r"></div> <div class="footer-wrapper"> <div class="pre-footer"> <span class="date">@($"{Model.Date:dddd, d. MMMM yyyy}")</span> diff --git a/Elwig/Documents/Document.cshtml.cs b/Elwig/Documents/Document.cshtml.cs index e4a307e..a6fae95 100644 --- a/Elwig/Documents/Document.cshtml.cs +++ b/Elwig/Documents/Document.cshtml.cs @@ -2,17 +2,28 @@ using System; using System.Threading.Tasks; using System.IO; using Elwig.Helpers; +using System.Text; namespace Elwig.Documents { public abstract class Document : IDisposable { private TempFile? PdfFile = null; + public string DataPath; + public int CurrentNextSeason; + public string? DocumentId; + public string Title; + public string Author; + public string Header; + public string Footer; + public DateTime Date; + public Document(string title) { var c = App.Client; DataPath = App.DataPath; CurrentNextSeason = Utils.CurrentNextSeason; Title = title; + Author = App.Client.NameFull; Header = $"<h1>{c.Name}</h1>"; Footer = Utils.GenerateFooter("<br/>", " \u00b7 ") .Item(c.NameFull).NextLine() @@ -33,15 +44,7 @@ namespace Elwig.Documents { GC.SuppressFinalize(this); } - public string DataPath { get; set; } - public int CurrentNextSeason { get; set; } - public string Title { get; set; } - public string Header { get; set; } - public string Footer { get; set; } - public DateTime Date { get; set; } - public string? DocumentId { get; set; } - - private async Task<string> Render() { + private Task<string> Render() { string name; if (this is BusinessLetter) { name = "BusinessLetter"; @@ -50,16 +53,19 @@ namespace Elwig.Documents { } else { throw new InvalidOperationException("Invalid document object"); } - return await Html.CompileRenderAsync(name, this); + return Render(name); + } + + private Task<string> Render(string name) { + return Html.CompileRenderAsync(name, this); } public async Task Generate() { var pdf = new TempFile("pdf"); using (var tmpHtml = new TempFile("html")) { - await File.WriteAllTextAsync(tmpHtml.FilePath, await Render()); + await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Encoding.UTF8); await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath); } - Pdf.UpdateMetadata(pdf.FilePath, Title, App.Client.NameFull); PdfFile = pdf; } diff --git a/Elwig/Documents/Pdf.cs b/Elwig/Documents/Pdf.cs index 35d9885..bed37ff 100644 --- a/Elwig/Documents/Pdf.cs +++ b/Elwig/Documents/Pdf.cs @@ -1,64 +1,45 @@ -using System; using System.Threading.Tasks; -using System.Windows; -using PdfSharp.Pdf.IO; -using PuppeteerSharp; using Elwig.Helpers; using Elwig.Windows; using System.Diagnostics; +using Balbarak.WeasyPrint; +using System; +using System.IO; namespace Elwig.Documents { public static class Pdf { - private static readonly string Chromium = @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"; private static readonly string PdfToPrinter = App.ExePath + "PDFtoPrinter.exe"; - private static IBrowser? Browser = null; - public static bool IsReady => Browser != null; + private static readonly FilesManager WeasyPrintManager = new(); + private static string? WeasyPrintPython = null; + private static string? WeasyPrintDir => WeasyPrintManager.FolderPath; + public static bool IsReady => WeasyPrintPython != null && WeasyPrintDir != null; public static async Task Init(Action evtHandler) { - Browser = await Puppeteer.LaunchAsync(new LaunchOptions { - Headless = true, - ExecutablePath = Chromium, - // paged.js uses XHRs to load styles, so this is needed - Args = new[] { "--allow-file-access-from-files" }, - }); + if (!WeasyPrintManager.IsFilesExsited()) { + await WeasyPrintManager.InitFilesAsync(); + } + WeasyPrintPython = Path.Combine(WeasyPrintManager.FolderPath, "python.exe"); evtHandler(); } - public static async Task Close() { - if (Browser == null) return; - await Browser.CloseAsync(); - Browser = null; - } - public static async Task Convert(string htmlPath, string pdfPath) { - if (Browser == null) throw new InvalidOperationException("The puppeteer engine has not been initialized yet"); - using var page = await Browser.NewPageAsync(); - page.Console += OnConsole; - await page.GoToAsync($"file://{htmlPath}"); - await page.EvaluateFunctionAsync("async () => { await window.PagedPolyfill.preview(); }"); - await page.PdfAsync(pdfPath, new() { - PreferCSSPageSize = true, - //Format = PaperFormat.A4, - DisplayHeaderFooter = false, - MarginOptions = new() { - Top = "0mm", - Right = "0mm", - Bottom = "0mm", - Left = "0mm", - }, - }); - } - - private static void OnConsole(object? sender, ConsoleEventArgs e) { - MessageBox.Show(e.Message.Text); - } - - public static void UpdateMetadata(string path, string title, string author) { - using var doc = PdfReader.Open(path); - doc.Info.Title = title; - doc.Info.Author = author; - doc.Save(path); + var p = new Process() { StartInfo = new() { + FileName = WeasyPrintPython, + CreateNoWindow = true, + WorkingDirectory = WeasyPrintDir, + RedirectStandardError = true, + } }; + p.StartInfo.EnvironmentVariables["PATH"] = "Scripts;gtk3;" + Environment.GetEnvironmentVariable("PATH"); + p.StartInfo.ArgumentList.Add("scripts/weasyprint.exe"); + p.StartInfo.ArgumentList.Add("-e"); + p.StartInfo.ArgumentList.Add("utf8"); + p.StartInfo.ArgumentList.Add(htmlPath); + p.StartInfo.ArgumentList.Add(pdfPath); + p.Start(); + await p.WaitForExitAsync(); + var stderr = await p.StandardError.ReadToEndAsync(); + if (p.ExitCode != 0) throw new Exception(stderr); } public static void Show(TempFile file, string title) { diff --git a/Elwig/Documents/style.css b/Elwig/Documents/style.css index 66e00ad..f2626e7 100644 --- a/Elwig/Documents/style.css +++ b/Elwig/Documents/style.css @@ -14,15 +14,18 @@ body { .m1, .m2, .m3 { height: 0; - width: 1cm; + width: 10mm; position: fixed; - left: 0; - border-top: 1pt solid black; + left: -25mm; + border-top: 0.5pt solid black; } - -.m1 {top: 105mm;} -.m2 {top: 148.5mm;} -.m3 {top: 210mm;} +.m1.r, .m2.r, .m3.r { + left: initial; + right: -20mm; +} +.m1 {top: 80mm;} +.m2 {top: 123.5mm;} +.m3 {top: 185mm;} header, .address-wrapper, aside, main { overflow: hidden; @@ -40,7 +43,7 @@ header { header h1{ font-size: 18pt; - margin-top: 1cm; + margin-top: 10mm; } .spacing { @@ -50,7 +53,7 @@ header h1{ .info-wrapper { width: 100%; height: 45mm; - margin: 0 0 8.46mm 0; + margin: 0 0 2mm 0; position: relative; } @@ -60,52 +63,56 @@ header h1{ margin: 0; padding: 5mm; position: absolute; - left: 20mm; + left: -5mm; top: 0; - display: flex; - flex-direction: column; - justify-content: flex-end; } .address-wrapper .sender { - flex: 17.7mm 1 1; + height: 4em; font-size: 8pt; - display: flex; - flex-direction: column; - justify-content: flex-end; - padding-bottom: 2mm; + padding: 1em 0; } address { - flex: 27.3mm 1 1; + height: 5em; white-space: pre-line; font-size: 12pt; font-style: normal; } +table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} + table td, table th { padding: 0.5mm 1mm; } +table th { + text-align: center; +} + aside { height: 40mm; width: 75mm; margin: 0; position: absolute; - left: 125mm; + left: 100mm; top: 5mm; } aside table { border-collapse: collapse; - border: 1pt solid #808080; - width: calc(100% - 1cm); - margin-right: 1cm; + border: 0.5pt solid #808080; + width: 65mm; + margin-right: 10mm; } aside table thead:not(:first-child) tr { - border-top: 1pt solid #808080; + border-top: 0.5pt solid #808080; } aside table thead th { @@ -124,54 +131,51 @@ aside table tbody th { } main { - margin: 8.46mm 20mm 4.23mm 25mm; + margin: 2em 0 1em 0; } -main :first-child { +main > *:first-child { margin-top: 0; } -main h1, main p { +.main-wrapper h1, .main-wrapper p { font-size: 12pt; margin: 1em 0; text-align: justify; } -main p { +.main-wrapper p { widows: 3; orphans: 3; - hyphens: auto; + hyphens: manual; } -main .date { +.main-wrapper .date { margin-bottom: 2em; text-align: right; } -main h1 { +.main-wrapper h1 { margin-bottom: 2em; } -main p.comment { +.main-wrapper p.comment { font-size: 10pt; } .footer-wrapper { - padding: 0 20mm 0 25mm; position: running(page-footer); - bottom: 0; - left: 0; - right: 0; + width: 165mm; } .pre-footer { margin: 1em 0; font-size: 10pt; - display: flex; } .pre-footer > * { - flex: 5cm 1 1; + display: inline-block; + width: 33%; } .pre-footer .date { @@ -185,30 +189,29 @@ main p.comment { .pre-footer .page { text-align: right; + float: right; } -.pre-fotter .page::after { +.pre-footer .page::after { content: "Seite 1 von 1"; } footer { font-size: 10pt; - border-top: 1pt solid black; + border-top: 0.5pt solid black; height: 25mm; padding-top: 1mm; text-align: center; } -table { - width: 100%; - border-collapse: collapse; - table-layout: fixed; -} - table.delivery { margin-bottom: 5mm; } +table.delivery tr:not(.main) { + break-before: avoid; +} + table.delivery th { font-weight: normal; font-style: italic; @@ -237,12 +240,15 @@ table.delivery tr.tight.first td { padding-bottom: 0; } +/* FIXME update version of WeasyPrint table.delivery tr.tight:has(+ tr:not(.tight)) td { padding-bottom: 0.5mm !important; } +*/ table.delivery tr.sum { - border-top: 1pt solid black; + border-top: 0.5pt solid black; + break-before: avoid; } table.delivery tr.sum td { @@ -295,22 +301,22 @@ table.delivery-stats tbody th { visibility: hidden; } -main .bottom { +.main-wrapper .bottom { bottom: 0; position: absolute; - width: calc(100% - 25mm - 20mm); + width: 165mm; } -main .signatures { +.main-wrapper .signatures { width: 100%; display: flex; justify-content: space-around; margin: 20mm 0 2mm 0; } -main .signatures > * { - width: 5cm; - border-top: 1pt solid black; +.main-wrapper .signatures > * { + width: 50mm; + border-top: 0.5pt solid black; padding-top: 1mm; text-align: center; font-size: 10pt; @@ -318,7 +324,7 @@ main .signatures > * { hr { border: none; - border-top: 1pt solid black; + border-top: 0.5pt solid black; margin: 5mm 0; } @@ -333,7 +339,7 @@ tr.page-break { @page { size: A4; - margin: 25mm 0 35mm 0; + margin: 25mm 20mm 35mm 25mm; @bottom-center { content: element(page-footer); } @@ -344,13 +350,13 @@ tr.page-break { width: 210mm; } header, .address-wrapper, aside, main { - border: 1pt solid lightgray; + border: 1px solid lightgray; } .m1, .m2, .m3 {display: none;} header {top: 0;} .spacing {height: 45mm;} .main-wrapper { - margin-bottom: 40mm; + margin: 0 20mm 40mm 25mm; } .footer-wrapper { position: fixed; @@ -365,7 +371,4 @@ tr.page-break { .page::after { content: "Seite " counter(page) " von " counter(pages); } - .footer-wrapper { - display: none; - } } diff --git a/Elwig/Elwig.csproj b/Elwig/Elwig.csproj index 303244a..c2d0cc9 100644 --- a/Elwig/Elwig.csproj +++ b/Elwig/Elwig.csproj @@ -16,14 +16,13 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="Balbarak.WeasyPrint" Version="2.0.2" /> <PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.1" /> <PackageReference Include="ini-parser" Version="2.5.2" /> <PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.21" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.10" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" /> <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1938.49" /> - <PackageReference Include="PdfSharp" Version="1.50.5147" /> - <PackageReference Include="PuppeteerSharp" Version="11.0.2" /> <PackageReference Include="RazorLight" Version="2.3.1" /> <PackageReference Include="ScottPlot.WPF" Version="4.1.67" /> <PackageReference Include="System.IO.Ports" Version="7.0.0" /> diff --git a/Elwig/fetch-resources.bat b/Elwig/fetch-resources.bat index 52e4f33..9d872b0 100644 --- a/Elwig/fetch-resources.bat +++ b/Elwig/fetch-resources.bat @@ -1,6 +1,5 @@ ::mkdir "C:\Program Files\Elwig" ::curl -s "http://www.columbia.edu/~em36/PDFtoPrinter.exe" -z "C:\Program Files\Elwig\PDFtoPrinter.exe" -o "C:\Program Files\Elwig\PDFtoPrinter.exe" mkdir "C:\ProgramData\Elwig\resources" -curl -s -L "https://unpkg.com/pagedjs/dist/paged.polyfill.js" -o "C:\ProgramData\Elwig\resources\paged.polyfill.js" -copy /b /y "Documents\style.css" "C:\ProgramData\Elwig\resources\style.css" +copy /b /y Documents\*.css "C:\ProgramData\Elwig\resources" copy /b /y Documents\*.cshtml "C:\ProgramData\Elwig\resources"