diff --git a/Tasks/PdfBuilder.cs b/Tasks/PdfBuilder.cs index afcc455..8541511 100644 --- a/Tasks/PdfBuilder.cs +++ b/Tasks/PdfBuilder.cs @@ -124,7 +124,7 @@ public class PdfBuilder { 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"; + senderLine = "" + owner.sender_address.Replace("\n", " ").Trim() + "\\n"; } catch { @@ -134,11 +134,16 @@ public class PdfBuilder // national addresses for (var i = 0; i < addressSet.KasPersons.Count; i++) { + string senderLineID = senderLine; if (!addressSet.KasPersons[i].IsGermany()) continue; var addr = AddressCreator.CreateFinalMarkdownString(addressSet.KasPersons[i].id); if (string.IsNullOrWhiteSpace(addr)) continue; - if (!string.IsNullOrEmpty(senderLine)) - addresses_german.Add(senderLine + (addr ?? "")); + if (addressSet.KasPersons[i].refsid != null || addressSet.KasPersons[i].refsid != 0) + senderLineID += $"ID: {addressSet.KasPersons[i].refsid}\n"; + else + senderLineID += "\n"; + if (!string.IsNullOrEmpty(senderLineID)) + addresses_german.Add(senderLineID + (addr ?? "")); else addresses_german.Add(addr); } @@ -146,11 +151,16 @@ public class PdfBuilder // international addresses for (var i = 0; i < addressSet.KasPersons.Count; i++) { + string senderLineID = senderLine; if (addressSet.KasPersons[i].IsGermany()) continue; var addr = AddressCreator.CreateFinalMarkdownString(addressSet.KasPersons[i].id); if (string.IsNullOrWhiteSpace(addr)) continue; - if (!string.IsNullOrEmpty(senderLine)) - addresses_inter.Add(senderLine + (addr ?? "")); + if (addressSet.KasPersons[i].refsid != null || addressSet.KasPersons[i].refsid != 0) + senderLineID += $"ID: {addressSet.KasPersons[i].refsid}\n"; + else + senderLineID += "\n"; + if (!string.IsNullOrEmpty(senderLineID)) + addresses_inter.Add(senderLineID + (addr ?? "")); else addresses_inter.Add(addr); } @@ -223,24 +233,6 @@ public class PdfBuilder } - private void DrawPage(XGraphics gfx, List addresses, ref int addressIndex) - { - for (var row = 0; row < _settings.rowsPerPage; row++) - { - for (var col = 0; col < _settings.columnsPerPage; col++) - { - if (addressIndex >= addresses.Count) break; - - var x = MmToPoints(_settings.pageMarginLeftMm + col * GetCellWidthMm()); - var y = MmToPoints(_settings.pageMarginTopMm + row * GetCellHeightMm()); - - DrawCell(gfx, x, y, addresses[addressIndex]); - addressIndex++; - } - - if (addressIndex >= addresses.Count) break; - } - } private void DrawPageWithPlaceholder(XGraphics gfx, List addresses, ref int addressIndex, ref bool isFirstCell, string placeholderText) @@ -315,6 +307,34 @@ public class PdfBuilder } + private enum TextStyle + { + Regular, + Bold, + Small + } + + private sealed class TextToken + { + public string Text { get; } + public TextStyle Style { get; } + public bool LineBreak { get; } + + public TextToken(string text, TextStyle style, bool lineBreak = false) + { + Text = text; + Style = style; + LineBreak = lineBreak; + } + } + + private sealed class TextRun + { + public string Text { get; set; } = ""; + public XFont Font { get; set; } = null!; + public bool LineBreakAfter { get; set; } + } + private void DrawMarkdownText(XGraphics gfx, string text, double x, double y, double cellWidth, double cellHeight) { try @@ -325,139 +345,249 @@ public class PdfBuilder var paddingBottomPoints = MmToPoints(_settings.cellPaddingBottomMm); var maxWidth = Math.Max(0, cellWidth - paddingLeftPoints - paddingRightPoints); + var maxHeight = Math.Max(0, cellHeight - paddingTopPoints - paddingBottomPoints); - // 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(); + var runs = ParseStyledText(text); + var lines = WrapRunsToLines(gfx, runs, maxWidth); - // 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; + var currentY = y + paddingTopPoints; + var bottomLimit = y + paddingTopPoints + maxHeight; foreach (var line in lines) { - // Stop if we've reached the top boundary - if (currentY + lineHeight > y + cellHeight - paddingBottomPoints + 0.001) + var lineHeight = GetLineHeightForRuns(line); + + if (currentY + lineHeight > bottomLimit + 0.001) break; - // Parse and draw the line with markdown support - DrawLineWithMarkdown(gfx, line, x + paddingLeftPoints, currentY, maxWidth); + DrawLineRuns(gfx, line, x + paddingLeftPoints, currentY); currentY += lineHeight; } } catch (Exception ex) { - Logger.Log($"Error while drawing markdown text: {ex.Message}",Logger.LogType.Error); + Logger.Log($"Error while drawing markdown text: {ex.Message}", Logger.LogType.Error); } - } - private void DrawLineWithMarkdown(XGraphics gfx, string line, double x, double y, double maxWidth) + private List ParseStyledText(string text) + { + var tokens = new List(); + if (string.IsNullOrEmpty(text)) + return tokens; + + var i = 0; + var style = TextStyle.Regular; + var buffer = new System.Text.StringBuilder(); + + void FlushBuffer() + { + if (buffer.Length > 0) + { + tokens.Add(new TextToken(buffer.ToString(), style)); + buffer.Clear(); + } + } + + while (i < text.Length) + { + if (text[i] == '\r') + { + i++; + continue; + } + + if (text[i] == '\n') + { + FlushBuffer(); + tokens.Add(new TextToken("", style, lineBreak: true)); + i++; + continue; + } + + if (i <= text.Length - 7 && text.Substring(i, 7) == "") + { + FlushBuffer(); + style = TextStyle.Small; + i += 7; + continue; + } + + if (i <= text.Length - 8 && text.Substring(i, 8) == "") + { + FlushBuffer(); + style = TextStyle.Regular; + i += 8; + continue; + } + + if (i < text.Length - 1 && text[i] == '*' && text[i + 1] == '*') + { + FlushBuffer(); + style = style == TextStyle.Bold ? TextStyle.Regular : TextStyle.Bold; + i += 2; + continue; + } + + if (i < text.Length - 1 && text[i] == '\\' && text[i + 1] == 'n') + { + FlushBuffer(); + tokens.Add(new TextToken("", style, lineBreak: true)); + i += 2; + continue; + } + + buffer.Append(text[i]); + i++; + } + + FlushBuffer(); + return tokens; + } + + private double GetLineHeightForRuns(IEnumerable line) + { + double max = 0; + + foreach (var run in line) + { + if (run.Font != null) + { + var h = run.Font.GetHeight(); + if (h > max) max = h; + } + } + + return max > 0 ? max * 0.8 : _regularFont.GetHeight(); + } + + private List> WrapRunsToLines(XGraphics gfx, List tokens, double maxWidth) + { + var lines = new List>(); + var currentLine = new List(); + double currentWidth = 0; + + void PushLine() + { + lines.Add(currentLine); + currentLine = new List(); + currentWidth = 0; + } + + foreach (var token in tokens) + { + if (token.LineBreak) + { + PushLine(); + continue; + } + + var font = token.Style switch + { + TextStyle.Bold => _boldFont, + TextStyle.Small => _smallFont, + _ => _regularFont + }; + + var parts = token.Text.Split('\n'); + + for (int p = 0; p < parts.Length; p++) + { + var part = parts[p]; + if (part.Length > 0) + { + var remaining = part; + + while (!string.IsNullOrEmpty(remaining)) + { + var available = maxWidth - currentWidth; + if (available <= 0.001) + { + PushLine(); + available = maxWidth; + } + + var fit = FitTextToWidth(gfx, remaining, font, available); + if (string.IsNullOrEmpty(fit)) + { + PushLine(); + continue; + } + + currentLine.Add(new TextRun + { + Text = fit, + Font = font + }); + + currentWidth += gfx.MeasureString(fit, font).Width; + remaining = remaining.Substring(fit.Length); + + if (!string.IsNullOrEmpty(remaining)) + PushLine(); + } + } + + if (p < parts.Length - 1) + PushLine(); + } + } + + if (currentLine.Count > 0 || lines.Count == 0) + lines.Add(currentLine); + + return lines; + } + + private void DrawLineRuns(XGraphics gfx, List line, double x, double y) { try { - if (string.IsNullOrWhiteSpace(line)) return; var currentX = x; - var i = 0; - while (i < line.Length) + foreach (var run in line) { - if (currentX - x >= maxWidth) - break; + if (string.IsNullOrEmpty(run.Text)) + continue; - var remainingWidth = maxWidth - (currentX - x); + gfx.DrawString( + run.Text, + run.Font, + XBrushes.Black, + new XRect(currentX, y, 10000, run.Font.Size * 1.2), + XStringFormats.TopLeft); - // 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)) - { - var measuredSmall = gfx.MeasureString(inner, _smallFont); - - if (measuredSmall.Width > remainingWidth) - { - inner = TruncateTextToWidth(gfx, inner, _smallFont, remainingWidth); - measuredSmall = gfx.MeasureString(inner, _smallFont); - } - - gfx.DrawString(inner, _smallFont, XBrushes.Black, - new XRect(currentX, y, remainingWidth, _smallFont.Size * 1.2), - XStringFormats.TopLeft); - 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)); - var measured = gfx.MeasureString(boldText, _boldFont); - - if (measured.Width > remainingWidth) - { - boldText = TruncateTextToWidth(gfx, boldText, _boldFont, remainingWidth); - measured = gfx.MeasureString(boldText, _boldFont); - } - - // Draw bold text and measure width accurately - gfx.DrawString(boldText, _boldFont, XBrushes.Black, - new XRect(currentX, y, remainingWidth, _boldFont.Size * 1.2), - XStringFormats.TopLeft); - 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)) - { - var measured = gfx.MeasureString(regularText, _regularFont); - - if (measured.Width > remainingWidth) - { - regularText = TruncateTextToWidth(gfx, regularText, _regularFont, remainingWidth); - measured = gfx.MeasureString(regularText, _regularFont); - } - - gfx.DrawString(regularText, _regularFont, XBrushes.Black, - new XRect(currentX, y, remainingWidth, _regularFont.Size * 1.2), XStringFormats.TopLeft); - currentX += measured.Width; - } - - i = textEnd; + currentX += gfx.MeasureString(run.Text, run.Font).Width; } } catch (Exception ex) { - Logger.Log($"Error while drawing markdown line: {ex.Message}",Logger.LogType.Error); + Logger.Log($"Error while drawing markdown line: {ex.Message}", Logger.LogType.Error); } - } + private string FitTextToWidth(XGraphics gfx, string text, XFont font, double maxWidth) + { + if (string.IsNullOrEmpty(text)) + return ""; + + if (gfx.MeasureString(text, font).Width <= maxWidth) + return text; + + var lo = 0; + var hi = text.Length; + while (lo < hi) + { + var mid = (lo + hi + 1) / 2; + var candidate = text.Substring(0, mid); + if (gfx.MeasureString(candidate, font).Width <= maxWidth) + lo = mid; + else + hi = mid - 1; + } + + return lo > 0 ? text.Substring(0, lo) : ""; + } private string TruncateTextToWidth(XGraphics gfx, string text, XFont font, double maxWidth) { try @@ -502,26 +632,7 @@ public class PdfBuilder var availableHeightMm = 297d - _settings.pageMarginTopMm - _settings.pageMarginBottomMm; return availableHeightMm / _settings.rowsPerPage; } - - // 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"); - } + public void ExportRunningSheets(int setID, string path) {