using System; using System.Collections.Generic; 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); private readonly XFont _smallFont = new("Arial", 6, 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]; var addresses = new List(); // find customer (owner) to include sender_address string senderLine = null; try { var owner = Settings._instance.customers.customers.FirstOrDefault(c => c.ID == addressSet.owner_id); if (owner != null && !string.IsNullOrWhiteSpace(owner.sender_address)) // ensure single line and wrap in a small-font tag senderLine = "" + owner.sender_address.Replace("\n", " ").Trim() + "\n"; } catch { senderLine = null; } for (var i = 0; i < addressSet.KasPersons.Count; i++) { var addr = AddressCreator.CreateFinalMarkdownString(addressSet.KasPersons[i].refsid); if (string.IsNullOrWhiteSpace(addr.Trim())) continue; if (!string.IsNullOrEmpty(senderLine)) addresses.Add(senderLine + addr); else addresses.Add(addr); } 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]; var addresses = new List(); // find customer (owner) to include sender_address string senderLine = null; try { var owner = Settings._instance.customers.customers.FirstOrDefault(c => c.ID == addressSet.owner_id); if (owner != null && !string.IsNullOrWhiteSpace(owner.sender_address)) senderLine = "" + owner.sender_address.Replace("\n", " ").Trim() + "\n"; } catch { senderLine = null; } for (var i = 0; i < addressSet.KasPersons.Count; i++) { var addr = AddressCreator.CreateFinalMarkdownString(addressSet.KasPersons[i].refsid); if (string.IsNullOrWhiteSpace(addr.Trim())) continue; if (!string.IsNullOrEmpty(senderLine)) addresses.Add(senderLine + (addr ?? "")); else addresses.Add(addr); } 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(List addresses, string outputPath) { if (addresses == null || addresses.Count == 0) throw new ArgumentException("Addresses array cannot be null or empty"); var document = new PdfDocument(); var addressIndex = 0; while (addressIndex < addresses.Count) { 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(List addresses, string placeholderText, string outputPath) { if (addresses == null || addresses.Count == 0) throw new ArgumentException("Addresses array cannot be null or empty"); var document = new PdfDocument(); var addressIndex = 0; var isFirstCell = true; while (addressIndex < addresses.Count || 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, List addresses, ref int addressIndex) { var cellCount = 0; for (var row = 0; row < 7; row++) { for (var col = 0; col < 3; col++) { if (addressIndex >= addresses.Count) break; var x = MmToPoints(_marginLeft + col * _cellWidth); var y = MmToPoints(_marginTop + row * _cellHeight); DrawCell(gfx, x, y, addresses[addressIndex]); addressIndex++; cellCount++; } if (addressIndex >= addresses.Count) break; } } private void DrawPageWithPlaceholder(XGraphics gfx, List 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.Count) { 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 top of the cell (align addresses to top) var startY = y + paddingTopPoints; 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 small-font tag ... if (i <= line.Length - 7 && line.Substring(i, 7) == "") { var endTag = line.IndexOf("", i + 7, StringComparison.Ordinal); if (endTag != -1) { var inner = line.Substring(i + 7, endTag - (i + 7)); if (!string.IsNullOrEmpty(inner)) { gfx.DrawString(inner, _smallFont, XBrushes.Black, new XRect(currentX, y, maxWidth - (currentX - x), _smallFont.Size * 1.2), XStringFormats.TopLeft); var measuredSmall = gfx.MeasureString(inner, _smallFont); currentX += measuredSmall.Width; } i = endTag + 8; // move past continue; } } // 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"); } }