Files
logofclient/Tasks/PdfBuilder.cs
T

457 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using PdfSharp;
using PdfSharp.Drawing;
using PdfSharp.Fonts;
using PdfSharp.Pdf;
namespace Logof_Client;
public class PdfBuilder
{
private readonly PdfExportSettings _settings;
private readonly XFont _boldFont;
private readonly XFont _regularFont;
private readonly XFont _smallFont;
public PdfBuilder(PdfExportSettings? settings = null)
{
EnsureFontResolverRegistered();
_settings = settings ?? new PdfExportSettings();
// Select first font from build output fonts folder (AppContext.BaseDirectory/fonts)
var chosenFamily = "Arial";
try
{
if (Directory.Exists(Global._instance.font_path))
{
var first = Directory.EnumerateFiles(Global._instance.font_path, "*.ttf").FirstOrDefault();
if (!string.IsNullOrEmpty(first))
chosenFamily = StripStyleSuffix(Path.GetFileNameWithoutExtension(first)) ?? chosenFamily;
}
}
catch
{
chosenFamily = "Arial";
}
_boldFont = new XFont(chosenFamily, _settings.fontSize, XFontStyleEx.Bold);
_regularFont = new XFont(chosenFamily, _settings.fontSize, XFontStyleEx.Regular);
_smallFont = new XFont(chosenFamily, _settings.smallFontSize, XFontStyleEx.Regular);
}
private static void EnsureFontResolverRegistered()
{
if (GlobalFontSettings.FontResolver != null) return;
//var fontsDir = Path.Combine(AppContext.BaseDirectory, "fonts");
GlobalFontSettings.FontResolver = new StableFontResolver(Global._instance.font_path);
}
private static string StripStyleSuffix(string name)
{
if (string.IsNullOrEmpty(name)) return name;
var idx = name.IndexOf('-');
if (idx < 0) idx = name.IndexOf('_');
if (idx > 0)
return name.Substring(0, idx);
return name;
}
/// <summary>
/// Creates a PDF document with address stickers from an AddressSet with a placeholder in the first cell.
/// </summary>
/// <param name="addressSetId">The ID of the AddressSet to use</param>
/// <param name="placeholderText">Text for the first cell (top-left)</param>
/// <param name="outputPath">Path where the PDF should be saved</param>
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<string>();
// 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 = "<font6>" + owner.sender_address.Replace("\n", " ").Trim() + "</font6>\n";
}
catch
{
senderLine = null;
}
for (var i = 0; i < addressSet.KasPersons.Count; i++)
{
var addr = AddressCreator.CreateFinalMarkdownString(addressSet.KasPersons[i].id);
if (string.IsNullOrWhiteSpace(addr)) continue;
if (!string.IsNullOrEmpty(senderLine))
addresses.Add(senderLine + (addr ?? ""));
else
addresses.Add(addr);
}
if (addresses.Count == 0)
{
MessageBox.Show(MainWindow._instance, "Keine validen Adressen konnten generiert werden. Abbruch.", "Fehler");
return;
}
CreateAddressLabelPdfWithPlaceholder(addresses, placeholderText, outputPath);
if (_settings.exportRunningSheets)
{
ExportRunningSheets(addressSetId, outputPath);
}
}
/// <summary>
/// Creates a PDF document with a single placeholder cell for other information.
/// </summary>
/// <param name="addresses">Array of addresses</param>
/// <param name="placeholderText">Text for the first cell (top-left)</param>
/// <param name="outputPath">Path where the PDF should be saved</param>
public void CreateAddressLabelPdfWithPlaceholder(List<string> 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<string> 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<string> addresses, ref int addressIndex,
ref bool isFirstCell, string placeholderText)
{
for (var row = 0; row < _settings.rowsPerPage; row++)
for (var col = 0; col < _settings.columnsPerPage; col++)
{
var x = MmToPoints(_settings.pageMarginLeftMm + col * GetCellWidthMm());
var y = MmToPoints(_settings.pageMarginTopMm + row * GetCellHeightMm());
// 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);
}
}
}
private void DrawCell(XGraphics gfx, double x, double y, string? address)
{
var cellWidthPoints = MmToPoints(GetCellWidthMm());
var cellHeightPoints = MmToPoints(GetCellHeightMm());
// 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(GetCellWidthMm());
var cellHeightPoints = MmToPoints(GetCellHeightMm());
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(_settings.cellPaddingLeftMm);
var paddingRightPoints = MmToPoints(_settings.cellPaddingRightMm);
var paddingTopPoints = MmToPoints(_settings.cellPaddingTopMm);
var paddingBottomPoints = MmToPoints(_settings.cellPaddingBottomMm);
var maxWidth = Math.Max(0, cellWidth - paddingLeftPoints - paddingRightPoints);
// 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)
{
if (string.IsNullOrWhiteSpace(line)) return;
var currentX = x;
var i = 0;
while (i < line.Length)
{
if (currentX - x >= maxWidth)
break;
var remainingWidth = maxWidth - (currentX - x);
// Check for small-font tag <font6> ... </font6>
if (i <= line.Length - 7 && line.Substring(i, 7) == "<font6>")
{
var endTag = line.IndexOf("</font6>", 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 </font6>
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;
}
}
private string TruncateTextToWidth(XGraphics gfx, string text, XFont font, double maxWidth)
{
if (string.IsNullOrEmpty(text))
return text;
for (var len = text.Length; len > 0; len--)
{
var truncated = text.Substring(0, len);
var measured = gfx.MeasureString(truncated, font);
if (measured.Width <= maxWidth)
return truncated;
}
return string.Empty;
}
/// <summary>
/// Converts millimeters to points (1 mm = 2.834645669 points)
/// </summary>
private double MmToPoints(double mm)
{
return mm * 2.834645669;
}
private double GetCellWidthMm()
{
var availableWidthMm = 210d - _settings.pageMarginLeftMm - _settings.pageMarginRightMm;
return availableWidthMm / _settings.columnsPerPage;
}
private double GetCellHeightMm()
{
var availableHeightMm = 297d - _settings.pageMarginTopMm - _settings.pageMarginBottomMm;
return availableHeightMm / _settings.rowsPerPage;
}
// Configuration methods to allow customization
/// <summary>
/// Sets the cell dimensions in millimeters
/// </summary>
public void SetCellDimensions(double width, double height)
{
if (width <= 0 || height <= 0)
throw new ArgumentException("Cell dimensions must be positive");
}
/// <summary>
/// Sets the page margins in millimeters
/// </summary>
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)
{
if (path.EndsWith(".pdf"))
{
path = path.Substring(0, path.Length - 4);
path = path + "-Laufzettel.pdf";
}
else
{
path = path + "-Laufzettel.pdf";
}
CreateRunningSheets(setID, path);
}
public void CreateRunningSheets(int setID, string path)
{
KasAddressList list = Settings._instance.addressSets.GetAddressSetByID(setID);
var document = new PdfDocument();
document.Info.Title = $"Laufzettel für {list.Name}";
document.Info.Subject = "powered by logofclient";
document.Info.Author = "logofclient";
int margin = 50;
List<(int, int)> grouped_nums = GroupAddresses(setID);
var page = document.AddPage();
page.Size = PageSize.A4;
var gfx = XGraphics.FromPdfPage(page);
var width = page.Width.Point-margin;
var height = page.Height.Point-margin;
gfx.DrawLine(XPens.Black, margin, margin, margin, height);
gfx.DrawLine(XPens.Black, margin, margin, width, margin);
gfx.DrawLine(XPens.Black, width, margin, margin, height);
gfx.DrawLine(XPens.Black, margin, height, width, margin);
var font = new XFont("Times New Roman", 20, XFontStyleEx.BoldItalic);
gfx.DrawString($"Versand {list.Name}", font, XBrushes.Black,
new XRect(0, 0, page.Width.Point, page.Height.Point), XStringFormats.Center);
document.Save(path);
}
public List<(int, int)> GroupAddresses(int setID)
{
return null;
}
}