diff --git a/Elwig/Documents/BusinessDocument.cs b/Elwig/Documents/BusinessDocument.cs index 4fb57ed..b7d74e0 100644 --- a/Elwig/Documents/BusinessDocument.cs +++ b/Elwig/Documents/BusinessDocument.cs @@ -21,14 +21,6 @@ namespace Elwig.Documents { IncludeSender = includeSender; var c = App.Client; Header = $"
{c.Name}
{c.NameSuffix}
{c.NameTypeFull}
"; - Footer = Utils.GenerateFooter("
", " \u00b7 ") - .Item(c.NameFull).NextLine() - .Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextLine() - .Item(c.EmailAddress != null ? $"\">{c.EmailAddress}" : null) - .Item(c.Website != null ? $"{c.Website}" : null) - .Item("Betriebs-Nr.", c.LfbisNr).Item("Bio-KSt.", c.OrganicAuthority).NextLine() - .Item("UID", c.UstIdNr).Item("BIC", c.Bic).Item("IBAN", c.Iban) - .ToString(); var uid = (m.UstIdNr ?? "-") + (m.IsBuchführend ? "" : " (pauschaliert)"); Aside = $"" + $"" + diff --git a/Elwig/Documents/Document.Page.css b/Elwig/Documents/Document.Page.css index 4bece0f..758fb29 100644 --- a/Elwig/Documents/Document.Page.css +++ b/Elwig/Documents/Document.Page.css @@ -1,19 +1,4 @@ -.m1, .m2, .m3 { - height: 0; - width: 10mm; - position: fixed; - left: -25mm; - border-top: var(--border-thickness) solid black; -} -.m1.r, .m2.r, .m3.r { - left: initial; - right: -20mm; -} -.m1 {top: 80mm;} -.m2 {top: 123.5mm;} -.m3 {top: 185mm;} - .page-break { break-before: page; } @@ -23,15 +8,15 @@ hr.page-break { @page { size: A4; - margin: 25mm 20mm 35mm 25mm; + margin: 25mm 20mm 25mm 25mm; +} - @bottom-center { - content: element(page-footer); - } +@page :first { + margin: 25mm 20mm 35mm 25mm; } @media screen { - body, header, .footer-wrapper { + body, header { width: 210mm; } @@ -39,10 +24,6 @@ hr.page-break { border: 1px solid lightgray; } - .m1, .m2, .m3 { - display: none; - } - header { top: 0; } @@ -54,23 +35,9 @@ hr.page-break { .main-wrapper { margin: 0 20mm 40mm 25mm; } - - .footer-wrapper { - position: fixed; - bottom: 0; - left: 0; - right: 0; - background: white; - } } -@media print { - .page::after { - content: "Seite " counter(page) " von " counter(pages) !important; - } - - a { - text-decoration: inherit; - color: inherit; - } +a { + text-decoration: inherit; + color: inherit; } diff --git a/Elwig/Documents/Document.Table.css b/Elwig/Documents/Document.Table.css index 7f7f4a8..a8633f1 100644 --- a/Elwig/Documents/Document.Table.css +++ b/Elwig/Documents/Document.Table.css @@ -1,13 +1,13 @@ main table { border-collapse: collapse; - margin-bottom: 10mm; + margin-bottom: 0mm; font-size: 10pt; } main table.large {font-size: 12pt;} main table.tiny { font-size: 8pt; - margin-bottom: 5mm; + margin-bottom: 0mm; } main table.border { @@ -30,7 +30,7 @@ main table.small th, main table.small td, main table.tiny th, main table.tiny td { - padding: 0.125mm 0.125mm; + padding: 0mm 0.125mm; } main table td[rowspan] { diff --git a/Elwig/Documents/Document.cs b/Elwig/Documents/Document.cs index 2f5364f..a15511a 100644 --- a/Elwig/Documents/Document.cs +++ b/Elwig/Documents/Document.cs @@ -31,7 +31,6 @@ namespace Elwig.Documents { public string Title; public string Author; public string Header; - public string Footer; public DateOnly Date; public Document(string title) { @@ -41,7 +40,6 @@ namespace Elwig.Documents { Title = title; Author = c.NameFull; Header = ""; - Footer = Utils.GenerateFooter("
", " \u00b7 ").Item(c.NameFull).ToString(); Date = DateOnly.FromDateTime(Utils.Today); } @@ -122,12 +120,12 @@ namespace Elwig.Documents { var tmpHtml = new TempFile("html"); await File.WriteAllTextAsync(tmpHtml.FilePath, await doc.Render(), Utils.UTF8); tmpHtmls.Add(tmpHtml); - tmpFiles.Add((doc is Letterhead ? "#" : "") + tmpHtml.FileName); + tmpFiles.Add((doc is Letterhead ? "#" : "") + tmpHtml.FilePath); i++; progress?.Report(GenerationProportion * 100 * i / n); } progress?.Report(GenerationProportion * 100); - var pages = await Pdf.Convert(tmpFiles, pdf.FileName, IsDoublePaged, cancelToken, new Progress(v => progress?.Report(GenerationProportion * 100 + v * (1 - GenerationProportion)))); + var pages = Pdf.Convert(tmpFiles, pdf.FilePath, IsDoublePaged, m.Documents, cancelToken, new Progress(v => progress?.Report(GenerationProportion * 100 + v * (1 - GenerationProportion)))); TotalPages = pages.Pages; _pdfFile = pdf; } catch { @@ -146,7 +144,7 @@ namespace Elwig.Documents { using var tmpHtml = new TempFile("html"); await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Utils.UTF8); progress?.Report(50.0); - var pages = await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath, IsDoublePaged, cancelToken); + var pages = Pdf.Convert(tmpHtml.FilePath, pdf.FilePath, this); TotalPages = pages.Pages; _pdfFile = pdf; } catch { diff --git a/Elwig/Documents/Document.cshtml b/Elwig/Documents/Document.cshtml index bc48103..814b4ee 100644 --- a/Elwig/Documents/Document.cshtml +++ b/Elwig/Documents/Document.cshtml @@ -13,41 +13,12 @@ @if (Model.IsDoublePaged) { } - @if (Model.ShowFoldMarks) { -
-
-
-
-
-
- } - - @if (Model.IsDoublePaged) { - - }
@Raw(Model.Header)
diff --git a/Elwig/Documents/Document.css b/Elwig/Documents/Document.css index cc52c29..55933fa 100644 --- a/Elwig/Documents/Document.css +++ b/Elwig/Documents/Document.css @@ -55,53 +55,6 @@ header .type { font-weight: normal; } -.footer-wrapper { - position: running(page-footer); - width: 165mm; - /* for some reason the position without the following statement changes on the second page */ - border: var(--border-thickness) solid #00000000; -} - -.footer-wrapper.left { - position: running(page-footer-left); -} - -.pre-footer { - margin: 1em 0; - font-size: 10pt; -} - -.pre-footer > * { - display: inline-block; - width: 33%; -} - -.pre-footer > *:first-child { - text-align: left; -} - -.pre-footer > *:nth-child(2) { - text-align: center; - font-style: italic; -} - -.pre-footer > *:last-child { - text-align: right; - float: right; -} - -.pre-footer .page::after { - content: "Seite 1 von 1"; -} - -footer { - font-size: 10pt; - border-top: var(--border-thickness) solid black; - height: 25mm; - padding-top: 1mm; - text-align: center; -} - .hidden { visibility: hidden; } diff --git a/Elwig/Documents/MemberDataSheet.cshtml b/Elwig/Documents/MemberDataSheet.cshtml index 3a35e82..3adcb63 100644 --- a/Elwig/Documents/MemberDataSheet.cshtml +++ b/Elwig/Documents/MemberDataSheet.cshtml @@ -166,7 +166,6 @@ } @if (areaComs.Count != 0) { -

Flächenbindungen per @($"{Model.Date:dd.MM.yyyy}")

Mitglied
diff --git a/Elwig/Documents/MemberDataSheet.css b/Elwig/Documents/MemberDataSheet.css index 75d31c4..59f21b5 100644 --- a/Elwig/Documents/MemberDataSheet.css +++ b/Elwig/Documents/MemberDataSheet.css @@ -1,5 +1,6 @@ h2 { + margin-top: 0; margin-bottom: 0.5em !important; } @@ -22,9 +23,3 @@ table.area-commitements td.text { table.area-commitements tr.sum { font-size: 12pt; } - -@page :not(:first) { - br.area-commitements { - display: none; - } -} diff --git a/Elwig/Elwig.csproj b/Elwig/Elwig.csproj index cf6c5f5..6022c58 100644 --- a/Elwig/Elwig.csproj +++ b/Elwig/Elwig.csproj @@ -25,6 +25,9 @@ + + + diff --git a/Elwig/Helpers/Printing/FooterEventHandler.cs b/Elwig/Helpers/Printing/FooterEventHandler.cs new file mode 100644 index 0000000..5514684 --- /dev/null +++ b/Elwig/Helpers/Printing/FooterEventHandler.cs @@ -0,0 +1,176 @@ +using Elwig.Documents; +using iText.IO.Font.Constants; +using iText.Kernel.Font; +using iText.Kernel.Geom; +using iText.Kernel.Pdf; +using iText.Kernel.Pdf.Action; +using iText.Kernel.Pdf.Canvas; +using iText.Kernel.Pdf.Event; +using iText.Kernel.Pdf.Xobject; +using iText.Layout; +using iText.Layout.Borders; +using iText.Layout.Element; +using iText.Layout.Properties; +using System; +using System.Collections.Generic; + +namespace Elwig.Helpers.Printing { + public class FooterEventHandler : AbstractPdfDocumentEventHandler { + + private const float _fontSize = 10; + private const float _ptInMm = 2.8346456693f; + private const float _placeholderWidth = 50 * _ptInMm; + + private readonly string _date; + private readonly string? _center; + private readonly bool _doublePaged; + private readonly bool _isPreview; + private readonly bool _isBusiness; + private readonly bool _showFoldMarks; + private readonly PdfFont _font; + private readonly PdfFont _fontBold; + private readonly PdfFont _fontItalic; + + private readonly List _pageNumPlaceholders; + + public int NumberOfPages { get; private set; } + + public FooterEventHandler(Documents.Document? doc = null) { + _date = $"{doc?.Date ?? DateOnly.FromDateTime(Utils.Today):dddd, d. MMMM yyyy}"; + _center = doc?.DocumentId; + _doublePaged = doc?.IsDoublePaged ?? false; + _isPreview = doc?.IsPreview ?? false; + _isBusiness = doc is BusinessDocument; + _showFoldMarks = doc?.ShowFoldMarks ?? false; + _font = PdfFontFactory.CreateFont(StandardFonts.TIMES_ROMAN); + _fontBold = PdfFontFactory.CreateFont(StandardFonts.TIMES_BOLD); + _fontItalic = PdfFontFactory.CreateFont(StandardFonts.TIMES_ITALIC); + _pageNumPlaceholders = []; + } + + protected override void OnAcceptedEvent(AbstractPdfDocumentEvent evt) { + if (evt.GetType() == PdfDocumentEvent.END_PAGE) { + OnPageEnd((PdfDocumentEvent)evt); + } else if (evt.GetType() == PdfDocumentEvent.START_DOCUMENT_CLOSING) { + OnDocumentClose((PdfDocumentEvent)evt); + } + } + + private void OnPageEnd(PdfDocumentEvent evt) { + var pdf = evt.GetDocument(); + var page = evt.GetPage(); + var pageNum = pdf.GetPageNumber(page); + + var pageSize = page.GetPageSize(); + float leftX1 = pageSize.GetLeft() + 25 * _ptInMm; + float leftX2 = pageSize.GetLeft() + 20 * _ptInMm; + float rightX1 = pageSize.GetRight() - 20 * _ptInMm; + float rightX2 = pageSize.GetRight() - 25 * _ptInMm; + float footerY = pageSize.GetBottom() + 25 * _ptInMm; + float y = footerY + _fontSize; + float footerWidth = 165 * _ptInMm; + float footerHeight = 25 * _ptInMm; + + var pdfCanvas = new PdfCanvas(page.NewContentStreamAfter(), page.GetResources(), pdf); + using var canvas = new Canvas(pdfCanvas, pageSize); + + var placeholder = new PdfFormXObject(new Rectangle(0, 0, _placeholderWidth, _fontSize)); + _pageNumPlaceholders.Add(placeholder); + + var c = App.Client; + var dateP = new Paragraph(_date).SetFont(_font).SetFontSize(_fontSize); + var centerP = new Paragraph(_center ?? "").SetFont(_fontItalic).SetFontSize(_fontSize); + var pageNumP = new Paragraph().Add(new Image(placeholder)).SetFont(_font).SetFontSize(_fontSize); + + if (pageNum == 1) { + // First page + canvas.ShowTextAligned(dateP, leftX1, y, TextAlignment.LEFT); + canvas.ShowTextAligned(centerP, (leftX1 + rightX1) / 2, y, TextAlignment.CENTER); + canvas.ShowTextAligned(pageNumP, rightX1, y, TextAlignment.RIGHT); + + var footerP = new Paragraph().SetFont(_font).SetFontSize(_fontSize) + .SetTextAlignment(TextAlignment.CENTER).SetVerticalAlignment(VerticalAlignment.TOP) + .SetMargin(0).SetMultipliedLeading(1) + .SetWidth(footerWidth).SetHeight(footerHeight).SetPaddingTop(1 * _ptInMm).SetBorderTop(new SolidBorder(0.5f)) + .Add(c.NameFull); + if (_isBusiness) { + footerP.Add("\n"); + footerP.AddAll(Utils.GenerateFooter("\n", " \u00b7 ") + .Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextLine() + .Item(c.EmailAddress != null ? new Link(c.EmailAddress, PdfAction.CreateURI($"mailto:{Uri.EscapeDataString(c.Name)}%20<{c.EmailAddress}>")) : null) + .Item(c.Website != null ? new Link(c.Website, PdfAction.CreateURI($"http://{c.Website}/")) : null) + .Item("Betriebs-Nr.", c.LfbisNr).Item("Bio-KSt.", c.OrganicAuthority).NextLine() + .Item("UID", c.UstIdNr).Item("BIC", c.Bic).Item("IBAN", c.Iban) + .ToLeafElements()); + } + // FIXME links are drawn on next page - move footer into "normal" document creation + canvas.ShowTextAligned(footerP, (leftX1 + rightX1) / 2, footerY, TextAlignment.CENTER, VerticalAlignment.TOP); + } else if (_doublePaged && (pageNum % 2 == 0)) { + // Swap side + canvas.ShowTextAligned(pageNumP, leftX2, footerY, TextAlignment.LEFT, VerticalAlignment.TOP); + canvas.ShowTextAligned(centerP, (leftX2 + rightX2) / 2, footerY, TextAlignment.CENTER, VerticalAlignment.TOP); + canvas.ShowTextAligned(dateP, rightX2, footerY, TextAlignment.RIGHT, VerticalAlignment.TOP); + } else { + canvas.ShowTextAligned(dateP, leftX1, footerY, TextAlignment.LEFT, VerticalAlignment.TOP); + canvas.ShowTextAligned(centerP, (leftX1 + rightX1) / 2, footerY, TextAlignment.CENTER, VerticalAlignment.TOP); + canvas.ShowTextAligned(pageNumP, rightX1, footerY, TextAlignment.RIGHT, VerticalAlignment.TOP); + } + + if (_showFoldMarks) { + var m1 = pageSize.GetTop() - 105 * _ptInMm; + var m2 = pageSize.GetTop() - 148.5 * _ptInMm; + var m3 = pageSize.GetTop() - 210 * _ptInMm; + pdfCanvas.SetLineWidth(0.5f); + + pdfCanvas.MoveTo(0, m1); + pdfCanvas.LineTo(10 * _ptInMm, m1); + pdfCanvas.MoveTo(pageSize.GetRight(), m1); + pdfCanvas.LineTo(pageSize.GetRight() - 10 * _ptInMm, m1); + + pdfCanvas.MoveTo(0, m2); + pdfCanvas.LineTo(7 * _ptInMm, m2); + pdfCanvas.MoveTo(pageSize.GetRight(), m2); + pdfCanvas.LineTo(pageSize.GetRight() - 7 * _ptInMm, m2); + + pdfCanvas.MoveTo(0, m3); + pdfCanvas.LineTo(10 * _ptInMm, m3); + pdfCanvas.MoveTo(pageSize.GetRight(), m3); + pdfCanvas.LineTo(pageSize.GetRight() - 10 * _ptInMm, m3); + + pdfCanvas.ClosePathStroke(); + } + + if (NumberOfPages > 0) { + // FillPlaceholders() was already called + FillPlaceholder(pdf, pageNum); + } + } + + private void OnDocumentClose(PdfDocumentEvent evt) { + FillPlaceholders(evt.GetDocument()); + } + + private void FillPlaceholders(PdfDocument pdf) { + NumberOfPages = pdf.GetNumberOfPages(); + for (int i = 0; i < _pageNumPlaceholders.Count; i++) + FillPlaceholder(pdf, i + 1); + } + + private void FillPlaceholder(PdfDocument pdf, int pageNum) { + var placeholder = _pageNumPlaceholders[pageNum - 1]; + using var canvas = new Canvas(placeholder, pdf); + if (_doublePaged && (pageNum % 2 == 0)) { + // swap + var p = new Paragraph().SetFont(_font).SetFontSize(_fontSize); + if (_isPreview) p.Add(new Text("(vorläufig) ").SetFont(_fontBold)); + p.Add($"Seite {pageNum:N0} von {NumberOfPages:N0} "); + canvas.ShowTextAligned(p, 0, 0, TextAlignment.LEFT); + } else { + var p = new Paragraph().SetFont(_font).SetFontSize(_fontSize) + .Add($"Seite {pageNum:N0} von {NumberOfPages:N0}"); + if (_isPreview) p.Add(new Text(" (vorläufig)").SetFont(_fontBold)); + canvas.ShowTextAligned(p, _placeholderWidth, 0, TextAlignment.RIGHT); + } + } + } +} diff --git a/Elwig/Helpers/Printing/Pdf.cs b/Elwig/Helpers/Printing/Pdf.cs index cd253be..28a1d60 100644 --- a/Elwig/Helpers/Printing/Pdf.cs +++ b/Elwig/Helpers/Printing/Pdf.cs @@ -1,103 +1,148 @@ using Elwig.Windows; +using iText.Html2pdf; +using iText.Kernel.Pdf; +using iText.Kernel.Pdf.Event; +using iText.Kernel.Utils; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Drawing.Printing; using System.IO; using System.Linq; -using System.Net.Sockets; -using System.Reflection; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; namespace Elwig.Helpers.Printing { public static class Pdf { - - private static readonly string WinziPrint = (Assembly.GetEntryAssembly()?.Location.Contains(@"\bin\") ?? false) ? - Path.Combine(Assembly.GetEntryAssembly()!.Location.Split(@"\bin\")[0], "../Installer/Files/WinziPrint.exe") : - new string[] { App.InstallPath } - .Union(Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? []) - .Select(x => Path.Combine(x, "WinziPrint.exe")) - .Where(File.Exists) - .FirstOrDefault() ?? throw new FileNotFoundException("WiniPrint executable not found"); - private static Process? WinziPrintProc; - public static bool IsReady => WinziPrintProc != null; - - public static async Task Init(Action? evtHandler = null) { + public static Task Init(Action? evtHandler = null) { PdfiumNative.FPDF_InitLibrary(); - // NOTE: If the WinziPrint daemon is already running this will succeed, but the process will fail. - // Should be no problem, as long as the daemon is not closed - var p = new Process() { StartInfo = new() { - FileName = WinziPrint, - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardOutput = true, - } }; - p.StartInfo.ArgumentList.Add("-D"); - p.StartInfo.ArgumentList.Add("-d"); - p.StartInfo.ArgumentList.Add(App.TempPath); - p.Start(); - await p.StandardOutput.ReadLineAsync(); - WinziPrintProc = p; evtHandler?.Invoke(); + return Task.CompletedTask; } public static Task Cleanup() { - WinziPrintProc?.Kill(true); - WinziPrintProc?.Close(); PdfiumNative.FPDF_DestroyLibrary(); return Task.CompletedTask; } - public static async Task<(int Pages, IEnumerable PerDoc)> Convert(string htmlPath, string pdfPath, bool doublePaged = false, CancellationToken? cancelToken = null, IProgress? progress = null) { - return await Convert([htmlPath], pdfPath, doublePaged, cancelToken, progress); + public static (int Pages, IEnumerable PerDoc) Convert(string htmlPath, string pdfPath, Documents.Document? doc = null) { + int nPages; + using var html = File.Open(htmlPath, FileMode.Open); + using var writer = new PdfWriter(pdfPath); + using var pdf = new PdfDocument(writer); + var footerHandler = new FooterEventHandler(doc); + pdf.AddEventHandler(PdfDocumentEvent.END_PAGE, footerHandler); + pdf.AddEventHandler(PdfDocumentEvent.START_DOCUMENT_CLOSING, footerHandler); + // TODO embed fonts? + //pdf.AddFont(PdfFontFactory.CreateFont(StandardFonts.TIMES_ROMAN)); + HtmlConverter.ConvertToPdf(html, pdf); + nPages = footerHandler.NumberOfPages; + return (nPages, [nPages]); } - public static async Task<(int Pages, IEnumerable PerDoc)> Convert(IEnumerable htmlPath, string pdfPath, bool doublePaged = false, CancellationToken? cancelToken = null, IProgress? progress = null) { - if (WinziPrintProc == null) throw new InvalidOperationException("The WinziPrint process has not been initialized yet"); - progress?.Report(0.0); - using var client = new TcpClient("127.0.0.1", 30983); - using var stream = client.GetStream(); - string cnxId; - using var reader = new StreamReader(stream); - var first = await reader.ReadLineAsync() ?? throw new IOException("Invalid response from WinziPrint"); - if (first.StartsWith("id:")) { - cnxId = first[3..].Trim(); - } else { - throw new IOException("Invalid response from WinziPrint"); - } - await stream.WriteAsync(Utils.UTF8.GetBytes( - "-e utf-8;-p;" + (doublePaged ? "-2;" : "") + - $"{string.Join(';', htmlPath)};{pdfPath}" + - "\r\n")); - bool cancelled = false; - while (true) { - if (!cancelled && (cancelToken?.IsCancellationRequested ?? false)) { - try { - using var cancelClient = new TcpClient("127.0.0.1", 30983); - using var cancelStream = cancelClient.GetStream(); - using var cancelReader = new StreamReader(cancelStream); - await cancelReader.ReadLineAsync(); - await cancelStream.WriteAsync(Utils.UTF8.GetBytes($"cancel;{cnxId}\r\n")); - } catch { } - cancelled = true; + public static (int Pages, IEnumerable PerDoc) Convert(IEnumerable inputFiles, string pdfPath, bool doublePaged = false, IEnumerable? docs = null, CancellationToken? cancelToken = null, IProgress? progress = null) { + var tmpFileNames = new List(); + var pageNums = new List(); + var tmpPageNums = new List(); + + var htmlFiles = inputFiles + .Where(f => !f.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) + .Select(f => f.TrimStart('!', '#')) + .ToList(); + + try { + for (int i = 0; i < htmlFiles.Count; i++) { + string tmpFile = $"{pdfPath}.{i:0000}.part"; + tmpFileNames.Add(tmpFile); + var (pages, _) = Convert(htmlFiles[i], tmpFile, docs?.ElementAt(i)); + tmpPageNums.Add(pages); } - var line = await reader.ReadLineAsync() ?? throw new IOException("Invalid response from WinziPrint"); - if (line.StartsWith("error:")) { - var msg = line[6..].Trim(); - if (msg == "aborted") - throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!"); - throw new IOException($"WinziPrint: {msg}"); - } else if (line.StartsWith("progress:")) { - var parts = line[9..].Trim().Split('/').Select(int.Parse).ToArray(); - progress?.Report(100.0 * parts[0] / parts[1]); - } else if (line.StartsWith("success:")) { - var m = Regex.Match(line, @"([0-9]+) pages \(([0-9, ]+)\)"); - return (int.Parse(m.Groups[1].Value), m.Groups[2].Value.Split(", ").Select(int.Parse).ToList()); + + using var writer = new PdfWriter(pdfPath); + using var mergedPdf = new PdfDocument(writer); + var merger = new PdfMerger(mergedPdf); + + PdfPage? letterheadPage = null; + int letterheadInsertIndex = 0; + int letterheadDocIndex = 0; + + for (int i = 0; i < inputFiles.Count(); i++) { + var fileName = inputFiles.ElementAt(i); + int p0 = mergedPdf.GetNumberOfPages(); + + if (letterheadPage != null && fileName.StartsWith('#')) { + if (mergedPdf.GetNumberOfPages() <= letterheadInsertIndex) { + mergedPdf.AddPage(letterheadPage.CopyTo(mergedPdf)); + mergedPdf.AddNewPage(); + } else { + mergedPdf.AddPage(letterheadInsertIndex + 1, letterheadPage.CopyTo(mergedPdf)); + mergedPdf.AddNewPage(letterheadInsertIndex + 2); + } + + pageNums[letterheadDocIndex] = 1; + letterheadPage = null; + } + + if (fileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) { + var cleanName = fileName.TrimStart('!', '#'); + + if (doublePaged && fileName.StartsWith('#')) { + using var reader = new PdfReader(cleanName); + using var src = new PdfDocument(reader); + letterheadPage = src.GetPage(1).CopyTo(mergedPdf); + letterheadInsertIndex = p0; + letterheadDocIndex = i; + } else { + using var reader = new PdfReader(cleanName); + using var src = new PdfDocument(reader); + merger.Merge(src, 1, src.GetNumberOfPages()); + } + } else { + string tmpFile = tmpFileNames[i]; + if (doublePaged && fileName.StartsWith('#')) { + using var reader = new PdfReader(tmpFile); + using var src = new PdfDocument(reader); + letterheadPage = src.GetPage(1).CopyTo(mergedPdf); + letterheadInsertIndex = p0; + letterheadDocIndex = i; + } else { + using var reader = new PdfReader(tmpFile); + using var src = new PdfDocument(reader); + merger.Merge(src, 1, tmpPageNums[i]); + } + } + + int p1 = mergedPdf.GetNumberOfPages(); + pageNums.Add(p1 - p0); + + if (doublePaged && fileName[0] != '!' && fileName[0] != '#' && mergedPdf.GetNumberOfPages() % 2 != 0) { + if (letterheadPage != null) { + mergedPdf.AddPage(letterheadPage.CopyTo(mergedPdf)); + letterheadPage = null; + } else { + mergedPdf.AddNewPage(); + } + } + } + + if (letterheadPage != null) { + if (mergedPdf.GetNumberOfPages() <= letterheadInsertIndex) { + mergedPdf.AddPage(letterheadPage.CopyTo(mergedPdf)); + mergedPdf.AddNewPage(); + } else { + mergedPdf.AddPage(letterheadInsertIndex + 1, letterheadPage.CopyTo(mergedPdf)); + mergedPdf.AddNewPage(letterheadInsertIndex + 2); + } + + pageNums[letterheadDocIndex] = 1; + } + } finally { + foreach (var tmp in tmpFileNames) { + if (File.Exists(tmp)) File.Delete(tmp); } } + + return (pageNums.Sum(), pageNums); } public static void Show(TempFile file, string title) { diff --git a/Elwig/Helpers/Utils.cs b/Elwig/Helpers/Utils.cs index ff9b91d..eb187ad 100644 --- a/Elwig/Helpers/Utils.cs +++ b/Elwig/Helpers/Utils.cs @@ -3,6 +3,7 @@ using Elwig.Documents; using Elwig.Helpers.Billing; using Elwig.Models; using Elwig.Models.Entities; +using iText.Layout.Element; using LinqKit; using MailKit.Net.Smtp; using MailKit.Security; @@ -304,23 +305,18 @@ namespace Elwig.Helpers { } public class Footer { - private string Text = ""; + private readonly List> Items = [[]]; private readonly string LineBreak; - private readonly string Seperator; - private bool FirstLine = true; - private bool FirstItemInLine = true; + private readonly string Separator; - public Footer(string lineBreak, string seperator) { + public Footer(string lineBreak, string separator) { LineBreak = lineBreak; - Seperator = seperator; + Separator = separator; } - public Footer Item(string? text) { + public Footer Item(object? text) { if (text == null) return this; - Text += FirstItemInLine ? (FirstLine ? "" : LineBreak) : Seperator; - Text += text; - FirstLine = false; - FirstItemInLine = false; + Items[^1].Add(text); return this; } @@ -329,12 +325,28 @@ namespace Elwig.Helpers { } public Footer NextLine() { - FirstItemInLine = true; + Items.Add([]); return this; } public override string ToString() { - return Text; + return string.Join(LineBreak, Items.Select(l => string.Join(Separator, l.ToString()))); + } + + public IList ToLeafElements() { + var l = new List(); + var first1 = true; + foreach (var line in Items) { + if (!first1) l.Add(new Text(LineBreak)); + var first2 = true; + foreach (var item in line) { + if (!first2) l.Add(new Text(Separator)); + l.Add(item as ILeafElement ?? new Text(item.ToString())); + first2 = false; + } + first1 = false; + } + return l; } } diff --git a/Elwig/Windows/AboutWindow.xaml b/Elwig/Windows/AboutWindow.xaml index edb7c7e..161cb9d 100644 --- a/Elwig/Windows/AboutWindow.xaml +++ b/Elwig/Windows/AboutWindow.xaml @@ -22,7 +22,7 @@ Programmiersprache: C# Framework: Windows Presentation Framework (WPF) Datenbank: SQLite - PDF-Erstellung: WeasyPrint, RazorLight, Pdfium + PDF-Erstellung: iText, RazorLight, Pdfium Paketierung: WiX Toolset diff --git a/Installer/Files/WinziPrint.exe b/Installer/Files/WinziPrint.exe deleted file mode 100644 index eeba62a..0000000 Binary files a/Installer/Files/WinziPrint.exe and /dev/null differ diff --git a/Installer/Installer.wixproj b/Installer/Installer.wixproj index 94511f1..d1d24f9 100644 --- a/Installer/Installer.wixproj +++ b/Installer/Installer.wixproj @@ -35,6 +35,5 @@ - diff --git a/Installer/MainComponents.wxs b/Installer/MainComponents.wxs index 8322ba4..acf9adb 100644 --- a/Installer/MainComponents.wxs +++ b/Installer/MainComponents.wxs @@ -7,9 +7,6 @@ - - - diff --git a/README.md b/README.md index e7a2d0e..5be4471 100644 --- a/README.md +++ b/README.md @@ -43,5 +43,5 @@ Packaging: [WiX Toolset](https://www.firegiant.com/wixtoolset/) Programmiersprache: C# Framework: Windows Presentation Framework (WPF) Datenbank: [SQLite](https://sqlite.org/) -PDF-Erstellung: [WeasyPrint](https://weasyprint.org/), [RazorLight](https://github.com/toddams/RazorLight), [Pdfium](https://github.com/bblanchon/pdfium-binaries) +PDF-Erstellung: [iText](https://itextpdf.com/), [RazorLight](https://github.com/toddams/RazorLight), [Pdfium](https://github.com/bblanchon/pdfium-binaries) Paketierung: [WiX Toolset](https://www.firegiant.com/wixtoolset/)