using System.Collections.Concurrent; using PdfSharp.Fonts; namespace IBKRTrader.Modules.Accounting.Logic; /// /// Schriftauflösung für den PDF-Export. /// /// Warum das nötig ist: Der Export gab MigraDoc bisher „Segoe UI" vor – eine /// Windows-Schrift. PDFsharp 6 löst Schriften auf Nicht-Windows-Plattformen nicht von selbst auf; /// ohne einen eigenen scheitert der Export auf Linux zur Laufzeit. /// /// Vorgehen: Unter Windows übernimmt weiterhin die Plattform (unveränderte Optik) – /// wir geben null zurück und setzen . /// Auf Linux wird eine der üblichen freien Schriften aus dem System genommen. Bewusst keine /// Schrift im Repository: das erspart uns eine Lizenzfrage und hält das Paket klein – die /// Abhängigkeit ist dafür ausdrücklich dokumentiert und die Fehlermeldung nennt das Paket. /// public sealed class DocumentFontResolver : IFontResolver { /// Logischer Familienname, den der Export verwendet. public const string FamilyName = "IBKRTrader Sans"; /// Windows-Entsprechung – hält die Optik der bisherigen Exporte. private const string WindowsFamily = "Segoe UI"; /// /// Freie Sans-Serif-Schriften in Vorzugsreihenfolge, je (regular, bold). DejaVu ist auf /// praktisch jeder Distribution verfügbar, Liberation und Noto sind die üblichen Alternativen. /// private static readonly (string Regular, string Bold)[] Candidates = [ ("DejaVuSans.ttf", "DejaVuSans-Bold.ttf"), ("LiberationSans-Regular.ttf", "LiberationSans-Bold.ttf"), ("NotoSans-Regular.ttf", "NotoSans-Bold.ttf"), ("FreeSans.ttf", "FreeSansBold.ttf"), ]; private static readonly string[] SearchDirs = [ "/usr/share/fonts", "/usr/local/share/fonts", "/run/host/usr/share/fonts", // Flatpak/Toolbox ]; private static readonly ConcurrentDictionary Cache = new(); private static readonly Lazy<(string Regular, string Bold)?> Found = new(Locate); /// /// Richtet die Schriftauflösung einmalig ein. Mehrfachaufrufe sind unschädlich – /// PDFsharp lässt den Resolver nur einmal setzen. /// public static void EnsureConfigured() { if (GlobalFontSettings.FontResolver is not null) return; GlobalFontSettings.UseWindowsFontsUnderWindows = true; GlobalFontSettings.FontResolver = new DocumentFontResolver(); } public FontResolverInfo? ResolveTypeface(string familyName, bool bold, bool italic) { // Unter Windows die Plattform machen lassen: null bedeutet „nicht zuständig". if (OperatingSystem.IsWindows()) return null; if (Found.Value is not { } files) throw new InvalidOperationException( "Für den PDF-Export wurde keine Schriftart gefunden. Auf dem System muss eine freie " + "Sans-Serif-Schrift installiert sein – unter Debian/Ubuntu z. B. per " + "\"apt-get install fonts-dejavu-core\", unter Alpine \"apk add font-dejavu\". " + $"Gesucht wurde in: {string.Join(", ", SearchDirs)}."); // Kursiv wird von PDFsharp simuliert; wir liefern nur normal und fett. var path = bold ? files.Bold : files.Regular; return new FontResolverInfo(path, mustSimulateBold: false, mustSimulateItalic: italic); } public byte[]? GetFont(string faceName) => Cache.GetOrAdd(faceName, File.ReadAllBytes); /// Sucht das erste vollständige Paar (regular + bold) in den üblichen Verzeichnissen. private static (string Regular, string Bold)? Locate() { var existing = SearchDirs.Where(Directory.Exists).ToArray(); if (existing.Length == 0) return null; foreach (var (regular, bold) in Candidates) { var regularPath = FindFile(existing, regular); var boldPath = FindFile(existing, bold); // Ohne Fettschnitt bleibt der Bericht lesbar – dann eben beides normal. if (regularPath is not null) return (regularPath, boldPath ?? regularPath); } return null; } private static string? FindFile(string[] dirs, string fileName) { foreach (var dir in dirs) { try { var hit = Directory.EnumerateFiles(dir, fileName, SearchOption.AllDirectories).FirstOrDefault(); if (hit is not null) return hit; } catch (UnauthorizedAccessException) { /* Verzeichnis nicht lesbar – nächstes */ } catch (DirectoryNotFoundException) { /* zwischenzeitlich weg – nächstes */ } } return null; } /// Der Familienname, den das Dokument setzen soll. public static string DocumentFamily => OperatingSystem.IsWindows() ? WindowsFamily : FamilyName; }