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 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? 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(); 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 Documents; public MergedDocument(IEnumerable 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(rotated ? 0.5 * Math.PI : 0); 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 _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); } } } } }