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>
117 lines
5.0 KiB
C#
117 lines
5.0 KiB
C#
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;
|
||
}
|