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;
}