Files
elwig/Elwig/Documents/Document.cs
Lorenz Stechauner 0aefab5d63
All checks were successful
Test / Run tests (push) Successful in 2m3s
DeliveryConfirmation: Fix text alignment
2026-03-16 19:47:33 +01:00

513 lines
23 KiB
C#

using Elwig.Helpers;
using Elwig.Helpers.Printing;
using iText.IO.Font;
using iText.Kernel.Font;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Kernel.Pdf.Event;
using iText.Kernel.Pdf.Xobject;
using iText.Kernel.Utils;
using iText.Layout;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using MimeKit;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class Document : IDisposable {
public static string Name => "Dokument";
public const float PtInMM = 2.8346456693f;
public const float BorderThickness = 0.5f;
protected PdfFont NF = null!;
protected PdfFont BF = null!;
protected PdfFont IF = null!;
protected PdfFont BI = null!;
protected PdfFont SF = null!;
//protected readonly PdfFont NF = PdfFontFactory.CreateFont(StandardFonts.TIMES_ROMAN);
//protected readonly PdfFont BF = PdfFontFactory.CreateFont(StandardFonts.TIMES_BOLD);
//protected readonly PdfFont IF = PdfFontFactory.CreateFont(StandardFonts.TIMES_ITALIC);
//protected readonly PdfFont BI = PdfFontFactory.CreateFont(StandardFonts.TIMES_BOLDITALIC);
protected TempFile? _pdfFile = null;
protected string? _pdfPath;
protected string? PdfPath => _pdfPath ?? _pdfFile?.FilePath;
public int? TotalPages { get; private set; }
public int? Pages => TotalPages / (IsDoublePaged ? 2 : 1);
public bool ShowFoldMarks = App.Config.Debug;
public bool IsDoublePaged = false;
public bool IsPreview = false;
private iText.Layout.Document? _doc;
public int CurrentNextSeason;
public string? DocumentId;
public string Title;
public string Author;
public DateOnly Date;
public Document(string title) {
CurrentNextSeason = Utils.CurrentNextSeason;
Title = title;
Author = App.Client.NameFull;
Date = DateOnly.FromDateTime(Utils.Today);
}
~Document() {
Dispose();
}
public void Dispose() {
_pdfFile?.Dispose();
_pdfFile = null;
GC.SuppressFinalize(this);
}
public static Document Merge(IEnumerable<Document> docs) {
return new MergedDocument(docs);
}
public static Document FromPdf(string path) {
return new RawPdfDocument(path);
}
private class RawPdfDocument : Document {
public RawPdfDocument(string pdfPath) :
base(Path.GetFileNameWithoutExtension(pdfPath)) {
_pdfPath = pdfPath;
}
}
public int Render(string path) {
using var writer = new PdfWriter(path);
return Render(writer);
}
private int Render(PdfWriter writer) {
NF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\times.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
BF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\timesbd.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
IF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\timesi.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
BI = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\timesbi.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
SF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\seguisym.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
NF.SetSubset(true);
BF.SetSubset(true);
IF.SetSubset(true);
BI.SetSubset(true);
SF.SetSubset(true);
writer.SetCompressionLevel(CompressionConstants.BEST_COMPRESSION);
writer.SetSmartMode(true);
using var pdf = new PdfDocument(writer);
pdf.GetDocumentInfo()
.SetTitle(Title)
.SetAuthor(Author)
.SetCreator($"Elwig {App.Version}");
var handler = new EventHandler(this);
pdf.AddEventHandler(PdfDocumentEvent.START_PAGE, handler);
pdf.AddEventHandler(PdfDocumentEvent.END_PAGE, handler);
pdf.AddEventHandler(PdfDocumentEvent.START_DOCUMENT_CLOSING, handler);
_doc = new iText.Layout.Document(pdf, iText.Kernel.Geom.PageSize.A4);
try {
_doc.SetFont(NF).SetFontSize(12);
RenderHeader(_doc, pdf);
RenderBody(_doc, pdf);
var pageNum = pdf.GetNumberOfPages();
return pageNum;
} finally {
_doc.Close();
}
}
protected virtual void RenderHeader(iText.Layout.Document doc, PdfDocument pdf) { }
protected virtual void RenderBody(iText.Layout.Document doc, PdfDocument pdf) { }
protected virtual Paragraph GetFooter() {
return new KernedParagraph(App.Client.NameFull, 10);
}
public async Task Generate(CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
if (_pdfFile != null)
return;
progress?.Report(0.0);
if (this is RawPdfDocument) {
// nothing to do
} else if (this is MergedDocument m) {
using var tmpPdf = new TempFile("pdf");
var pdf = new TempFile("pdf");
try {
var pageNums = new List<int>();
using var writer = new PdfWriter(pdf.FilePath);
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 < m.Documents.Count; i++) {
if (cancelToken?.IsCancellationRequested ?? false)
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
var doc = m.Documents[i];
int p0 = mergedPdf.GetNumberOfPages();
if (letterheadPage != null && doc is Letterhead) {
if (mergedPdf.GetNumberOfPages() <= letterheadInsertIndex) {
mergedPdf.AddPage(letterheadPage);
mergedPdf.AddNewPage();
} else {
mergedPdf.AddPage(letterheadInsertIndex + 1, letterheadPage);
mergedPdf.AddNewPage(letterheadInsertIndex + 2);
}
pageNums[letterheadDocIndex] = 1;
letterheadPage = null;
}
if (doc is RawPdfDocument) {
if (IsDoublePaged && doc is Letterhead) {
using var reader = new PdfReader(doc.PdfPath);
using var src = new PdfDocument(reader);
letterheadPage = src.GetPage(1).CopyTo(mergedPdf);
letterheadInsertIndex = p0;
letterheadDocIndex = i;
} else {
using var reader = new PdfReader(doc.PdfPath);
using var src = new PdfDocument(reader);
merger.Merge(src, 1, src.GetNumberOfPages());
}
} else {
int pageNum = doc.Render(tmpPdf.FilePath);
if (IsDoublePaged && doc is Letterhead) {
using var reader = new PdfReader(tmpPdf.FilePath);
using var src = new PdfDocument(reader);
letterheadPage = src.GetPage(1).CopyTo(mergedPdf);
letterheadInsertIndex = p0;
letterheadDocIndex = i;
} else {
using var reader = new PdfReader(tmpPdf.FilePath);
using var src = new PdfDocument(reader);
merger.Merge(src, 1, pageNum);
}
}
int p1 = mergedPdf.GetNumberOfPages();
pageNums.Add(p1 - p0);
if (IsDoublePaged && doc is not Letterhead && mergedPdf.GetNumberOfPages() % 2 != 0) {
if (letterheadPage != null) {
mergedPdf.AddPage(letterheadPage);
letterheadPage = null;
} else {
mergedPdf.AddNewPage();
}
}
progress?.Report(100.0 * (i + 1) / (m.Documents.Count + 1));
}
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;
}
TotalPages = pageNums.Sum();
} catch {
pdf.Dispose();
throw;
}
_pdfFile = pdf;
} else {
if (cancelToken?.IsCancellationRequested ?? false)
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
var pdf = new TempFile("pdf");
try {
TotalPages = Render(pdf.FilePath);
} catch {
pdf.Dispose();
throw;
}
_pdfFile = pdf;
}
progress?.Report(100.0);
}
public void SaveTo(string pdfPath) {
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
File.Copy(PdfPath, pdfPath, true);
}
public async Task Print(int copies = 1) {
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
await Pdf.Print(PdfPath, copies, IsDoublePaged);
}
public void Show() {
if (_pdfFile == null) throw new InvalidOperationException("Pdf file has not been generated yet");
Pdf.Show(_pdfFile.NewReference(), Title + (this is BusinessDocument b ? $" - {b.Member.FullName}" : ""));
}
public MimePart AsEmailAttachment(string filename) {
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
return new("application", "pdf") {
Content = new MimeContent(File.OpenRead(PdfPath)),
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Base64,
FileName = filename
};
}
private class MergedDocument : Document {
public List<Document> Documents;
public MergedDocument(IEnumerable<Document> docs) :
base("Mehrere Dokumente") {
Documents = [.. docs];
IsDoublePaged = docs.Any(x => x.IsDoublePaged);
}
}
protected static Cell NewCell(Paragraph? p = null, int rowspan = 1, int colspan = 1, bool overflow = false) {
var cell = new Cell(rowspan, colspan)
.SetBorder(Border.NO_BORDER)
.SetPaddingsMM(0.5f, 1, 0.5f, 1);
if (p != null) {
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
if (!overflow) p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
cell.Add(p);
}
return cell;
}
protected Cell NewTh(string? text, float fontSize = 8, int rowspan = 1, int colspan = 1, bool left = false, bool rotated = false) {
var p = new KernedParagraph(text ?? "", fontSize);
if (rotated) p.SetRotationAngle(0.5 * Math.PI);
var cell = NewCell(p, rowspan: rowspan, colspan: colspan)
.SetTextAlignment(left ? TextAlignment.LEFT : TextAlignment.CENTER)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetFont(IF);
if (rotated || !left) cell.SetPadding(0);
return cell;
}
protected Cell NewTd(string? text = null, float fontSize = 10, int rowspan = 1, int colspan = 1, bool center = false, bool right = false, bool bold = false, bool italic = false, bool borderTop = false, bool overflow = false) {
return NewTd(new KernedParagraph(text ?? "", fontSize), rowspan: rowspan, colspan: colspan, center: center, right: right, bold: bold, italic: italic, borderTop: borderTop, overflow: overflow);
}
protected Cell NewTd(KernedParagraph p, int rowspan = 1, int colspan = 1, bool center = false, bool right = false, bool bold = false, bool italic = false, bool borderTop = false, bool overflow = false) {
return NewCell(p, rowspan: rowspan, colspan: colspan, overflow: overflow)
.SetTextAlignment(center ? TextAlignment.CENTER : right ? TextAlignment.RIGHT : TextAlignment.LEFT)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetBorderTop(borderTop ? new SolidBorder(BorderThickness) : Border.NO_BORDER)
.SetFont(bold ? (italic ? BI : BF) : (italic ? IF : NF));
}
public static float[] ColsMM(params double[] widths) {
return [.. widths.Select(w => (float)w * PtInMM)];
}
public Text Normal(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(NF);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
public Text Bold(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(BF);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
public Text Italic(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(IF);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
public Text BoldItalic(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(BI);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
private class EventHandler : AbstractPdfDocumentEventHandler {
private const float _fontSize = 10;
private const float _placeholderWidth = 50 * PtInMM;
private readonly Document _doc;
private readonly List<PdfFormXObject> _pageNumPlaceholders;
public int NumberOfPages { get; private set; }
public EventHandler(Document doc) {
_doc = doc;
_pageNumPlaceholders = [];
}
protected override void OnAcceptedEvent(AbstractPdfDocumentEvent evt) {
if (evt.GetType() == PdfDocumentEvent.START_PAGE) {
OnPageStart((PdfDocumentEvent)evt);
} else if (evt.GetType() == PdfDocumentEvent.END_PAGE) {
OnPageEnd((PdfDocumentEvent)evt);
} else if (evt.GetType() == PdfDocumentEvent.START_DOCUMENT_CLOSING) {
OnDocumentClose((PdfDocumentEvent)evt);
}
}
private void OnPageStart(PdfDocumentEvent evt) {
var pdf = evt.GetDocument();
var page = evt.GetPage();
var pageNum = pdf.GetPageNumber(page);
if (pageNum == 1) {
// first page
if (_doc is BusinessDocument) {
_doc._doc?.SetMarginsMM(90, 20, 35, 25);
} else {
_doc._doc?.SetMarginsMM(20, 20, 35, 25);
}
} else if (_doc.IsDoublePaged && (pageNum % 2) == 0) {
// left page (= swapped)
_doc._doc?.SetMarginsMM(20, 25, 25, 20);
} else {
// right page
_doc._doc?.SetMarginsMM(20, 20, 25, 25);
}
}
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 y1 = footerY + _fontSize;
float y2 = footerY - _fontSize;
var pdfCanvas = new PdfCanvas(page.NewContentStreamAfter(), page.GetResources(), pdf);
using var canvas = new Canvas(pdfCanvas, pageSize);
var placeholder = new PdfFormXObject(new iText.Kernel.Geom.Rectangle(0, 0, _placeholderWidth, _fontSize));
_pageNumPlaceholders.Add(placeholder);
var c = App.Client;
var dateP = new KernedParagraph($"{_doc.Date:dddd, d. MMMM yyyy}", _fontSize).SetFont(_doc.NF);
var centerP = new KernedParagraph(_doc.DocumentId ?? "", _fontSize).SetFont(_doc.IF);
var pageNumP = new KernedParagraph(_fontSize).Add(new Image(placeholder)).SetFont(_doc.NF);
if (pageNum == 1) {
// first page
canvas.ShowTextAligned(dateP, leftX1, y1, TextAlignment.LEFT, VerticalAlignment.BOTTOM);
canvas.ShowTextAligned(centerP, (leftX1 + rightX1) / 2, y1, TextAlignment.CENTER, VerticalAlignment.BOTTOM);
canvas.ShowTextAligned(pageNumP, rightX1, y1, TextAlignment.RIGHT, VerticalAlignment.BOTTOM);
var footer = _doc.GetFooter();
using var footerCanvas = new Canvas(page, pageSize);
footerCanvas.Add(new Table(1).AddCell(new Cell().Add(footer).SetBorder(Border.NO_BORDER).SetPaddingsMM(1, 0, 0, 0))
.SetFixedPositionMM(25, 0, 165).SetHeightMM(25)
.SetFont(_doc.NF).SetFontSize(10)
.SetTextAlignment(TextAlignment.CENTER)
.SetBorder(Border.NO_BORDER)
.SetBorderTop(new SolidBorder(BorderThickness)));
} else if (_doc.IsDoublePaged && (pageNum % 2 == 0)) {
// left page (= swapped)
canvas.ShowTextAligned(pageNumP, leftX2, y2, TextAlignment.LEFT, VerticalAlignment.TOP);
canvas.ShowTextAligned(centerP, (leftX2 + rightX2) / 2, y2, TextAlignment.CENTER, VerticalAlignment.TOP);
canvas.ShowTextAligned(dateP, rightX2, y2, TextAlignment.RIGHT, VerticalAlignment.TOP);
} else {
// right page
canvas.ShowTextAligned(dateP, leftX1, y2, TextAlignment.LEFT, VerticalAlignment.TOP);
canvas.ShowTextAligned(centerP, (leftX1 + rightX1) / 2, y2, TextAlignment.CENTER, VerticalAlignment.TOP);
canvas.ShowTextAligned(pageNumP, rightX1, y2, TextAlignment.RIGHT, VerticalAlignment.TOP);
}
if (_doc.ShowFoldMarks) {
var m1 = pageSize.GetTop() - 105 * PtInMM;
var m2 = pageSize.GetTop() - 148.5 * PtInMM;
var m3 = pageSize.GetTop() - 210 * PtInMM;
pdfCanvas.SetLineWidth(BorderThickness);
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) {
var pdf = evt.GetDocument();
var page = evt.GetPage();
var pageNum = pdf.GetPageNumber(page);
// ...
FillPlaceholders(pdf);
}
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 (_doc.IsDoublePaged && (pageNum % 2 == 0)) {
// left page (= swapped)
var p = new KernedParagraph(_fontSize).SetFont(_doc.NF);
if (_doc.IsPreview) p.Add(_doc.Bold("(vorläufig) "));
p.Add(_doc.Normal($"Seite {pageNum:N0} von {NumberOfPages:N0} "));
canvas.ShowTextAligned(p, 0, 0, TextAlignment.LEFT);
} else {
// right page
var p = new KernedParagraph(_fontSize).SetFont(_doc.NF)
.Add(_doc.Normal($"Seite {pageNum:N0} von {NumberOfPages:N0}"));
if (_doc.IsPreview) p.Add(_doc.Bold(" (vorläufig)"));
canvas.ShowTextAligned(p, _placeholderWidth, 0, TextAlignment.RIGHT);
}
}
}
}
}