Files
IBKRTrader/src/IBKRTrader.Modules.Accounting/Logic/DocumentFontResolver.cs
T
RichardandClaude Opus 5 8b9b993d1d L1b/2: Kultur und Dateisystem plattformunabhaengig machen
Kultur - Ausgaben und Parsen haengen nicht mehr am Host:

- PdfExporter formatierte Betraege mit ToString("N2") ohne Formatanbieter,
  also CurrentCulture. Auf dem deutschen Desktop "1.234,56", in einem
  Container mit LANG=C "1,234.56" - dieselbe Zahl, fuer einen Leser eine
  andere. Fuer ein ausdruecklich pruefbares Dokument jetzt fest de-DE.
- CapitolTradesScraper.ParseDate nutzte DateOnly.TryParse ohne
  Formatanbieter. Das ist nicht theoretisch: gemessen wurde aus
  "2026-08-04" unter th-TH das Jahr 1483 (buddhistischer Kalender), unter
  fa-IR das Jahr 2647 (persischer Kalender), unter ar-SA schlug das Parsen
  ganz fehl. de-DE und en-US kommen mit ISO klar - genau deshalb faellt so
  etwas auf dem Entwicklungsrechner nie auf. Jetzt TryParseExact mit
  InvariantCulture; ein Formatwechsel der Quelle faellt damit auf, statt
  still ein falsches Datum zu erzeugen. Regressionstest ueber vier Kulturen.
- IBKRGatewayService baute den Query-Parameter mit .ToString().ToLower()
  (Tuerkisch-I) - jetzt fest "true"/"false".

PDF-Schriften: PDFsharp 6 loest auf Nicht-Windows-Plattformen nichts von
selbst auf, "Segoe UI" gibt es dort nicht - der Export waere zur Laufzeit
gescheitert. Neuer DocumentFontResolver: unter Windows bleibt die Plattform
zustaendig (unveraenderte Optik), auf Linux wird eine freie Systemschrift
gesucht (DejaVu/Liberation/Noto/FreeSans). Bewusst keine Schrift im Repo -
das erspart eine Lizenzfrage; fehlt sie, nennt die Fehlermeldung das zu
installierende Paket.

BackupWorker:
- Suchte "mysqldump.exe" in C:\Program Files\... und splittete PATH mit ';'.
  Auf Linux ist das Trennzeichen ':' - der gesamte PATH waere als ein
  Eintrag gelesen worden. Jetzt Path.PathSeparator, plattformabhaengige
  Suchpfade und zusaetzlich "mariadb-dump" (MariaDB hat mysqldump ab 10.5
  umbenannt).
- Das DB-Passwort stand als Kommandozeilenargument im Prozessbaum. Unter
  Linux ist /proc/<pid>/cmdline fuer jeden lokalen Nutzer lesbar - das waere
  eine neue Offenlegung gewesen, die es unter Windows so nicht gab. Jetzt
  ueber MYSQL_PWD, nur an den Kindprozess vererbt. Argumente einzeln statt
  als Zeichenkette (kein Quoting-Problem bei Pfaden mit Leerzeichen).

Verifiziert: 188 Tests gruen (+5), Build 0 Fehler/0 Warnungen, Core + 3
Module + Tests bauen fuer linux-x64, --smoke-ui konstruiert alle 7 Fenster.

Offen aus L1b und nach L2 verschoben: IAppPaths (Logs/Backups/settings.json/
master.key liegen neben der Binaerdatei; unter /opt ist das nicht schreibbar).
Gehoert zum Daemon, wo die Pfade tatsaechlich gebraucht werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:58:10 +02:00

117 lines
5.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Concurrent;
using PdfSharp.Fonts;
namespace IBKRTrader.Modules.Accounting.Logic;
/// <summary>
/// Schriftauflösung für den PDF-Export.
///
/// <para><b>Warum das nötig ist:</b> 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 <see cref="IFontResolver"/> scheitert der Export auf Linux zur Laufzeit.</para>
///
/// <para><b>Vorgehen:</b> Unter Windows übernimmt weiterhin die Plattform (unveränderte Optik)
/// wir geben <c>null</c> zurück und setzen <see cref="GlobalFontSettings.UseWindowsFontsUnderWindows"/>.
/// Auf Linux wird eine der üblichen freien Schriften aus dem System genommen. Bewusst <b>keine</b>
/// 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.</para>
/// </summary>
public sealed class DocumentFontResolver : IFontResolver
{
/// <summary>Logischer Familienname, den der Export verwendet.</summary>
public const string FamilyName = "IBKRTrader Sans";
/// <summary>Windows-Entsprechung hält die Optik der bisherigen Exporte.</summary>
private const string WindowsFamily = "Segoe UI";
/// <summary>
/// Freie Sans-Serif-Schriften in Vorzugsreihenfolge, je (regular, bold). DejaVu ist auf
/// praktisch jeder Distribution verfügbar, Liberation und Noto sind die üblichen Alternativen.
/// </summary>
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<string, byte[]> Cache = new();
private static readonly Lazy<(string Regular, string Bold)?> Found = new(Locate);
/// <summary>
/// Richtet die Schriftauflösung einmalig ein. Mehrfachaufrufe sind unschädlich
/// PDFsharp lässt den Resolver nur einmal setzen.
/// </summary>
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);
/// <summary>Sucht das erste vollständige Paar (regular + bold) in den üblichen Verzeichnissen.</summary>
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;
}
/// <summary>Der Familienname, den das Dokument setzen soll.</summary>
public static string DocumentFamily => OperatingSystem.IsWindows() ? WindowsFamily : FamilyName;
}