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