diff --git a/Tasks/PdfBuilder.cs b/Tasks/PdfBuilder.cs new file mode 100644 index 0000000..c20b328 --- /dev/null +++ b/Tasks/PdfBuilder.cs @@ -0,0 +1,325 @@ +using System; +using System.Linq; +using PdfSharp; +using PdfSharp.Drawing; +using PdfSharp.Pdf; + +namespace Logof_Client; + +public class PdfBuilder +{ + // Table layout + private const int CellsPerRow = 3; + private const int CellsPerPage = 21; // 3 columns × 7 rows + private readonly XFont _boldFont = new("Arial", 9, XFontStyleEx.Bold); + private readonly double _cellHeight = 42.43; // mm + private readonly double _cellPaddingBottom = 5; // mm + + // Padding inside cells + private readonly double _cellPaddingLeft = 5; // mm + private readonly double _cellPaddingTop = 5; // mm + + // Cell dimensions (in mm) + private readonly double _cellWidth = 70; // mm + + // Font settings + private readonly double _fontSize = 9; + + private readonly double _marginBottom = 1; // mm + + // Paper and layout settings + private readonly double _marginLeft = 0; // mm + private readonly double _marginRight = 0; // mm + private readonly double _marginTop = 0; // mm + private readonly XFont _regularFont = new("Arial", 9, XFontStyleEx.Regular); + + /// + /// Creates a PDF document with address stickers from an AddressSet in a 3×7 grid layout on A4 pages. + /// + /// The ID of the AddressSet to use + /// Path where the PDF should be saved + public void CreateAddressLabelPdfFromAddressSet(int addressSetId, string outputPath) + { + // Find the AddressSet by ID + var addressSet = Settings._instance.addressSets.GetAddressSetByID(addressSetId); + if (addressSet == null) + throw new ArgumentException($"AddressSet with ID {addressSetId} not found"); + + if (addressSet.KasPersons == null || addressSet.KasPersons.Count == 0) + throw new ArgumentException($"AddressSet with ID {addressSetId} contains no addresses"); + + // Generate markdown addresses from all KasPersons in the set + var addresses = new string?[addressSet.KasPersons.Count]; + for (var i = 0; i < addressSet.KasPersons.Count; i++) + addresses[i] = AddressCreator.CreateFinalMarkdownString(addressSet.KasPersons[i].refsid); + + CreateAddressLabelPdf(addresses, outputPath); + } + + /// + /// Creates a PDF document with address stickers from an AddressSet with a placeholder in the first cell. + /// + /// The ID of the AddressSet to use + /// Text for the first cell (top-left) + /// Path where the PDF should be saved + public void CreateAddressLabelPdfFromAddressSetWithPlaceholder(int addressSetId, string placeholderText, + string outputPath) + { + // Find the AddressSet by ID + var addressSet = Settings._instance.addressSets.GetAddressSetByID(addressSetId); + if (addressSet == null) + throw new ArgumentException($"AddressSet with ID {addressSetId} not found"); + + if (addressSet.KasPersons == null || addressSet.KasPersons.Count == 0) + throw new ArgumentException($"AddressSet with ID {addressSetId} contains no addresses"); + + // Generate markdown addresses from all KasPersons in the set + var addresses = new string?[addressSet.KasPersons.Count]; + for (var i = 0; i < addressSet.KasPersons.Count; i++) + addresses[i] = AddressCreator.CreateFinalMarkdownString(addressSet.KasPersons[i].refsid); + + CreateAddressLabelPdfWithPlaceholder(addresses, placeholderText, outputPath); + } + + /// + /// Creates a PDF document with address stickers in a 3×7 grid layout on A4 pages. + /// + /// Array of addresses (from CreateFinalMarkdownString) + /// Path where the PDF should be saved + public void CreateAddressLabelPdf(string?[] addresses, string outputPath) + { + if (addresses == null || addresses.Length == 0) + throw new ArgumentException("Addresses array cannot be null or empty"); + + var document = new PdfDocument(); + + var addressIndex = 0; + while (addressIndex < addresses.Length) + { + var page = document.AddPage(); + page.Size = PageSize.A4; + + using (var gfx = XGraphics.FromPdfPage(page)) + { + // Draw the grid and fill cells + DrawPage(gfx, addresses, ref addressIndex); + } + } + + // Save the document + document.Save(outputPath); + } + + /// + /// Creates a PDF document with a single placeholder cell for other information. + /// + /// Array of addresses + /// Text for the first cell (top-left) + /// Path where the PDF should be saved + public void CreateAddressLabelPdfWithPlaceholder(string?[] addresses, string placeholderText, string outputPath) + { + if (addresses == null || addresses.Length == 0) + throw new ArgumentException("Addresses array cannot be null or empty"); + + var document = new PdfDocument(); + + var addressIndex = 0; + var isFirstCell = true; + + while (addressIndex < addresses.Length || isFirstCell) + { + var page = document.AddPage(); + page.Size = PageSize.A4; + + using (var gfx = XGraphics.FromPdfPage(page)) + { + DrawPageWithPlaceholder(gfx, addresses, ref addressIndex, ref isFirstCell, placeholderText); + } + } + + document.Save(outputPath); + } + + private void DrawPage(XGraphics gfx, string?[] addresses, ref int addressIndex) + { + var cellCount = 0; + + for (var row = 0; row < 7; row++) + { + for (var col = 0; col < 3; col++) + { + if (addressIndex >= addresses.Length) break; + + var x = MmToPoints(_marginLeft + col * _cellWidth); + var y = MmToPoints(_marginTop + row * _cellHeight); + + DrawCell(gfx, x, y, addresses[addressIndex]); + addressIndex++; + cellCount++; + } + + if (addressIndex >= addresses.Length) break; + } + } + + private void DrawPageWithPlaceholder(XGraphics gfx, string?[] addresses, ref int addressIndex, + ref bool isFirstCell, string placeholderText) + { + var cellCount = 0; + + for (var row = 0; row < 7; row++) + for (var col = 0; col < 3; col++) + { + var x = MmToPoints(_marginLeft + col * _cellWidth); + var y = MmToPoints(_marginTop + row * _cellHeight); + + // First cell: placeholder + if (isFirstCell) + { + DrawCell(gfx, x, y, placeholderText); + isFirstCell = false; + } + else if (addressIndex < addresses.Length) + { + DrawCell(gfx, x, y, addresses[addressIndex]); + addressIndex++; + } + else + { + DrawEmptyCell(gfx, x, y); + } + + cellCount++; + } + } + + private void DrawCell(XGraphics gfx, double x, double y, string? address) + { + var cellWidthPoints = MmToPoints(_cellWidth); + var cellHeightPoints = MmToPoints(_cellHeight); + + // Draw cell border + var rect = new XRect(x, y, cellWidthPoints, cellHeightPoints); + gfx.DrawRectangle(XPens.Black, rect); + + // Draw address content if available + if (!string.IsNullOrEmpty(address)) DrawMarkdownText(gfx, address, x, y, cellWidthPoints, cellHeightPoints); + } + + private void DrawEmptyCell(XGraphics gfx, double x, double y) + { + var cellWidthPoints = MmToPoints(_cellWidth); + var cellHeightPoints = MmToPoints(_cellHeight); + + var rect = new XRect(x, y, cellWidthPoints, cellHeightPoints); + gfx.DrawRectangle(XPens.Black, rect); + } + + private void DrawMarkdownText(XGraphics gfx, string text, double x, double y, double cellWidth, double cellHeight) + { + var paddingLeftPoints = MmToPoints(_cellPaddingLeft); + var paddingTopPoints = MmToPoints(_cellPaddingTop); + var paddingBottomPoints = MmToPoints(_cellPaddingBottom); + + var maxWidth = cellWidth - paddingLeftPoints * 2; + + // Split text by newlines and remove empty trailing lines + var rawLines = text.Split(new[] { "\n" }, StringSplitOptions.None); + var lines = rawLines.Where(l => l != null).ToArray(); + + // Use a conservative line height in points + var lineHeight = _regularFont.Size * 1.2; + + // Calculate total height of the text block + var totalHeight = lines.Length * lineHeight; + + // Start drawing from the lower-left corner of the cell + var startY = y + cellHeight - paddingBottomPoints - totalHeight; + if (startY < y + paddingTopPoints) + startY = y + paddingTopPoints; // don't overflow top padding + + var currentY = startY; + + foreach (var line in lines) + { + // Stop if we've reached the top boundary + if (currentY + lineHeight > y + cellHeight - paddingBottomPoints + 0.001) + break; + + // Parse and draw the line with markdown support + DrawLineWithMarkdown(gfx, line, x + paddingLeftPoints, currentY, maxWidth); + currentY += lineHeight; + } + } + + private void DrawLineWithMarkdown(XGraphics gfx, string line, double x, double y, double maxWidth) + { + var currentX = x; + var i = 0; + + while (i < line.Length) + { + // Check for bold marker (**) + if (i < line.Length - 1 && line[i] == '*' && line[i + 1] == '*') + { + // Find closing ** + var endIndex = line.IndexOf("**", i + 2, StringComparison.Ordinal); + if (endIndex != -1) + { + var boldText = line.Substring(i + 2, endIndex - (i + 2)); + // Draw bold text and measure width accurately + gfx.DrawString(boldText, _boldFont, XBrushes.Black, + new XRect(currentX, y, maxWidth - (currentX - x), _boldFont.Size * 1.2), + XStringFormats.TopLeft); + var measured = gfx.MeasureString(boldText, _boldFont); + currentX += measured.Width; + i = endIndex + 2; + continue; + } + } + + // Regular text until next ** or end of line + var nextBoldIndex = line.IndexOf("**", i, StringComparison.Ordinal); + var textEnd = nextBoldIndex == -1 ? line.Length : nextBoldIndex; + var regularText = line.Substring(i, textEnd - i); + + if (!string.IsNullOrEmpty(regularText)) + { + gfx.DrawString(regularText, _regularFont, XBrushes.Black, + new XRect(currentX, y, maxWidth - (currentX - x), _regularFont.Size * 1.2), XStringFormats.TopLeft); + var measured = gfx.MeasureString(regularText, _regularFont); + currentX += measured.Width; + } + + i = textEnd; + } + } + + /// + /// Converts millimeters to points (1 mm = 2.834645669 points) + /// + private double MmToPoints(double mm) + { + return mm * 2.834645669; + } + + // Configuration methods to allow customization + + /// + /// Sets the cell dimensions in millimeters + /// + public void SetCellDimensions(double width, double height) + { + if (width <= 0 || height <= 0) + throw new ArgumentException("Cell dimensions must be positive"); + } + + /// + /// Sets the page margins in millimeters + /// + public void SetMargins(double left, double top, double right, double bottom) + { + if (left < 0 || top < 0 || right < 0 || bottom < 0) + throw new ArgumentException("Margins cannot be negative"); + } +} \ No newline at end of file