Printing: Replace WinziPrint with iText
Some checks failed
Test / Run tests (push) Failing after 2m18s
Some checks failed
Test / Run tests (push) Failing after 2m18s
This commit is contained in:
176
Elwig/Helpers/Printing/FooterEventHandler.cs
Normal file
176
Elwig/Helpers/Printing/FooterEventHandler.cs
Normal file
@@ -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<PdfFormXObject> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<int> PerDoc)> Convert(string htmlPath, string pdfPath, bool doublePaged = false, CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
|
||||
return await Convert([htmlPath], pdfPath, doublePaged, cancelToken, progress);
|
||||
public static (int Pages, IEnumerable<int> 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<int> PerDoc)> Convert(IEnumerable<string> htmlPath, string pdfPath, bool doublePaged = false, CancellationToken? cancelToken = null, IProgress<double>? 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<int> PerDoc) Convert(IEnumerable<string> inputFiles, string pdfPath, bool doublePaged = false, IEnumerable<Documents.Document>? docs = null, CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
|
||||
var tmpFileNames = new List<string>();
|
||||
var pageNums = new List<int>();
|
||||
var tmpPageNums = new List<int>();
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user