diff --git a/tests/Filee.App.Tests/RenderTests.cs b/tests/Filee.App.Tests/RenderTests.cs index e871baf2a..52879e0c9 100644 --- a/tests/Filee.App.Tests/RenderTests.cs +++ b/tests/Filee.App.Tests/RenderTests.cs @@ -1,6 +1,7 @@ // Renders real windows headlessly (Skia) to catch XAML / binding errors and to produce screenshots // in bin/.../screenshots for visual review. +using System.Globalization; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Headless; @@ -1608,6 +1609,7 @@ public void Preset_editor_keeps_rotation_visible_for_pdf_and_drops_it_for_target public void Preset_editor_saves_every_frame_only_for_png_and_jpg() { TestServices.EnsureInitialized("en"); + using var culture = new CultureScope("en-US"); // "100,000": numbers follow the regional format var loc = AppHost.Get(); var preset = new Filee.Core.Presets.Preset { TargetFormat = "jpg" }; @@ -1635,6 +1637,7 @@ public void Preset_editor_saves_every_frame_only_for_png_and_jpg() public void Preset_editor_shows_playback_speed_only_for_targets_that_keep_animations() { TestServices.EnsureInitialized("ko"); + using var culture = new CultureScope("ko-KR"); // "0.25×": numbers follow the regional format var loc = AppHost.Get(); var preset = new Filee.Core.Presets.Preset { TargetFormat = "gif", Name = "Slow GIF" }; var vm = new PresetEditorViewModel(preset, loc); @@ -2788,6 +2791,16 @@ internal static void Pump() } } + /// The regional format until disposed: numbers in labels follow it, not the UI language. + private sealed class CultureScope : IDisposable + { + private readonly CultureInfo _before = CultureInfo.CurrentCulture; + + public CultureScope(string name) => CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(name); + + public void Dispose() => CultureInfo.CurrentCulture = _before; + } + internal static void Save(TopLevel window, string name) { // Disposed at the end, so the frame stays alive while Skia encodes it: a collection in between (the idle memory diff --git a/tests/Filee.Engines.Tests/OfdTests.cs b/tests/Filee.Engines.Tests/OfdTests.cs index 59963eedb..b778653f0 100644 --- a/tests/Filee.Engines.Tests/OfdTests.cs +++ b/tests/Filee.Engines.Tests/OfdTests.cs @@ -3,6 +3,7 @@ // with the frame and labels, values on the page, QR code and seal pictures), a WPS document (page size only in // Document.xml, decimal colours, wrapped body text over two pages), and hostile packages. +using System.Globalization; using System.IO.Compression; using System.Text; using System.Xml.Linq; @@ -65,10 +66,10 @@ private static string MakeInvoice(string folder) var template = new StringBuilder(); void Label(string text, double x, double y, double size = 3) { - template.Append($"""{text}"""); + template.Append(string.Create(CultureInfo.InvariantCulture, $"""{text}""")); } void Rule(double x0, double y0, double x1, double y1) => - template.Append($"""M 0 0 L {x1 - x0} {y1 - y0}"""); + template.Append(string.Create(CultureInfo.InvariantCulture, $"""M 0 0 L {x1 - x0} {y1 - y0}""")); Label("电子发票(普通发票)", 75, 12, 6); Label("发票号码:", 140, 18); Label("开票日期:", 140, 24); @@ -112,7 +113,7 @@ void Rule(double x0, double y0, double x1, double y1) => var page = new StringBuilder(); void Value(string text, double x, double y, int font = 4, double size = 3.5, string extra = "") => - page.Append($"""{text}"""); + page.Append(string.Create(CultureInfo.InvariantCulture, $"""{text}""")); page.Append(""""""); Value("25442000000123456789", 155.5, 18, 5, extra: " DeltaX=\"g 19 2.1\""); Value("2025年03月18日", 155.5, 24); @@ -260,7 +261,7 @@ private static string MakeWps(string folder) Text(zip, "Doc_0/Res.xml", $""""""); string Line(string text, double x, double y, int font = 3, double size = 5.6, string extra = "") => - $"""{text}"""; + string.Create(CultureInfo.InvariantCulture, $"""{text}"""); var first = new StringBuilder(); first.Append(Line("医院信息化建设方案", 67.15, 40, 4, 8.5, " Weight=\"700\"")); // A paragraph of three lines on a 156.8 mm measure (28 characters): the first indented by two characters. @@ -334,7 +335,7 @@ private static string MakeOfd(string folder, string page, string template = "", } private static string TextObject(string text, double x, double y, double size = 3.175, int font = 3, string code = "", double width = 60) => - $"""{text}"""; + string.Create(CultureInfo.InvariantCulture, $"""{text}"""); [Fact] public void A_block_drawn_by_one_text_code_keeps_its_lines_and_a_short_delta_list_goes_on() @@ -509,7 +510,7 @@ public async Task Columns_of_a_list_keep_their_rows_level_and_the_page_its_heigh { var template = new StringBuilder(); void Rule(double x0, double y0, double x1, double y1) => - template.Append($"""M 0 0 L {x1 - x0} {y1 - y0}"""); + template.Append(string.Create(CultureInfo.InvariantCulture, $"""M 0 0 L {x1 - x0} {y1 - y0}""")); Rule(5, 30, 205, 30); Rule(5, 37, 205, 37); Rule(5, 110, 205, 110); diff --git a/tests/Filee.Engines.Tests/SpreadsheetFormatTests.cs b/tests/Filee.Engines.Tests/SpreadsheetFormatTests.cs index 738fce0d6..9dd0e4301 100644 --- a/tests/Filee.Engines.Tests/SpreadsheetFormatTests.cs +++ b/tests/Filee.Engines.Tests/SpreadsheetFormatTests.cs @@ -5,6 +5,7 @@ using System.IO.Compression; using System.Text; +using System.Text.RegularExpressions; using System.Xml.Linq; using Filee.Core.Conversion; using Filee.Core.Presets; @@ -598,22 +599,29 @@ public async Task LibreOffice_reads_our_ods_and_we_read_its_ods_and_xls() foreach (var (name, book) in books) { var sales = book.Sheets[0]; + // An ODS by LibreOffice keeps the text Calc showed (text:p), with the separators of Calc's locale + // ("1.350.000원", "12,8%" on a German system): compare those without the separators. + Func shown = name == "ODS by LibreOffice" ? WithoutSeparators : text => text; Assert.True(new[] { "매출", "메모", "넓은 표" }.SequenceEqual(book.Sheets.Select(s => s.Name)), name); - Assert.True(sales.Get(2, 1)!.Text == "1,350,000원", $"{name}: {sales.Get(2, 1)!.Text}"); - Assert.True(sales.Get(2, 3)!.Text == "12.8%", $"{name}: {sales.Get(2, 3)!.Text}"); + Assert.True(shown(sales.Get(2, 1)!.Text) == shown("1,350,000원"), $"{name}: {sales.Get(2, 1)!.Text}"); + Assert.True(shown(sales.Get(2, 3)!.Text) == shown("12.8%"), $"{name}: {sales.Get(2, 3)!.Text}"); // LibreOffice shows built-in dates in its own locale's order: compare the value. Assert.True(sales.Get(2, 2)!.Value == October1, $"{name}: {sales.Get(2, 2)!.Value}"); - Assert.True(sales.Get(4, 1)!.Text == "2,220,000원", $"{name}: {sales.Get(4, 1)!.Text}"); + Assert.True(shown(sales.Get(4, 1)!.Text) == shown("2,220,000원"), $"{name}: {sales.Get(4, 1)!.Text}"); Assert.True(new CellRange(0, 0, 0, 3) == Assert.Single(sales.Merges), name); Assert.True(sales.Get(1, 0)!.Style.Align == CellAlign.Center, name); - // LibreOffice measures character widths with its own fonts, so widths through ODS move by up to a fifth; - // XLS keeps Excel's own unit. - var tolerance = name.StartsWith("XLS", StringComparison.Ordinal) ? 0.01 : 0.25; - Assert.True(Math.Abs(sales.ColumnWidth(0) - 14) <= 14 * tolerance, $"{name}: {sales.ColumnWidth(0)}"); + // LibreOffice measures character widths with its own fonts, so widths it writes move by up to a fifth. XLS + // keeps Excel's unit, but for Korean text LibreOffice measures with its locale's default East Asian font + // (Microsoft YaHei on a German system: 15.27 for 14). The ratio of the columns stays. + Assert.True(Math.Abs(sales.ColumnWidth(0) - 14) <= 14 * 0.25, $"{name}: {sales.ColumnWidth(0)}"); Assert.True(Math.Abs(sales.ColumnWidth(0) / sales.ColumnWidth(1) - 14.0 / 12) < 0.02, $"{name}: {sales.ColumnWidth(0)} / {sales.ColumnWidth(1)}"); } } + /// "1.350.000원" and "1,350,000원" as "1350000원", "12,8 %" and "12.8%" as "12.8%". + private static string WithoutSeparators(string text) => + Regex.Replace(Regex.Replace(text, @"(?<=\d)[.,\u00A0\u202F' ](?=\d{3}(?!\d))", ""), @"(?<=\d),(?=\d)", ".").Replace(" %", "%"); + private async Task LibreOfficeAsync(IConverter libreOffice, string input, string from, string to) { var output = Path.Combine(Path.GetDirectoryName(input)!, $"{Path.GetFileNameWithoutExtension(input)}.lo-{from}.{to}");