From e76b60cc98ec3cdb6b3a1686df4678bf36176161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yavuz=20Mete=20Af=C5=9Far?= Date: Sun, 8 Nov 2026 11:37:45 +0300 Subject: [PATCH 1/2] Convert RTF to TXT through LibreOffice (#1204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The built-in routes read RTF through Pandoc: through Markdown every run in a monospace font became a code span, so code copied from an IDE came out full of backticks; through the HWPX writer the line breaks are lost. LibreOffice's RTF → TXT now costs as much as a chosen engine, as a direct route only. --- .../Office/LibreOfficeConverter.cs | 4 ++ .../LibreOfficeRouteTests.cs | 11 ++++ tests/Filee.Engines.Tests/RtfToTextTests.cs | 57 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/Filee.Engines.Tests/RtfToTextTests.cs diff --git a/src/Filee.Engines/Office/LibreOfficeConverter.cs b/src/Filee.Engines/Office/LibreOfficeConverter.cs index b5afba72e..25c60d3ce 100644 --- a/src/Filee.Engines/Office/LibreOfficeConverter.cs +++ b/src/Filee.Engines/Office/LibreOfficeConverter.cs @@ -453,6 +453,10 @@ void Add(IEnumerable sources, IEnumerable targets, int cost) // turn into DOCX (Excel → DOCX → LibreOffice would cost 17 + 19). const int Chosen = 10; Add(Writer, [.. Writer, "txt", "html"], Fallback); + // RTF → TXT (#1204): the built-in routes read RTF through Pandoc, which makes a code span of every run in a + // monospace font (through Markdown, about 29) or loses the line breaks (through the HWPX writer, about 32). + // Direct only, so it stays the fallback as a step towards other formats. + edges.Add(new ConversionEdge("rtf", "txt", Chosen, DirectOnly: true)); Add(Writer, ["pdf"], documents ? Chosen : Fallback); Add(Calc, [.. Calc, "html"], Fallback); Add(Calc, ["pdf"], sheets ? Chosen : Fallback); diff --git a/tests/Filee.Engines.Tests/LibreOfficeRouteTests.cs b/tests/Filee.Engines.Tests/LibreOfficeRouteTests.cs index 99c4eccbc..34791c71b 100644 --- a/tests/Filee.Engines.Tests/LibreOfficeRouteTests.cs +++ b/tests/Filee.Engines.Tests/LibreOfficeRouteTests.cs @@ -58,6 +58,17 @@ public void Word_and_excel_go_the_built_in_way_unless_libreoffice_is_chosen(stri Assert.Equal("libreoffice", Assert.Single(chosen.Plan(from, "pdf")!.Steps).Converter.Id); } + [Fact] + public void Rtf_goes_to_txt_through_libreoffice() + { + // Issue #1204: through Markdown, code copied from an IDE came out full of backticks. + var planner = Planner(slides: false, hwpToPdf: false); + + Assert.Equal("libreoffice", Assert.Single(planner.Plan("rtf", "txt")!.Steps).Converter.Id); + Assert.Equal("pandoc", Assert.Single(planner.Plan("rtf", "md")!.Steps).Converter.Id); + Assert.DoesNotContain(planner.Plan("rtf", "epub")!.Steps, s => s.From == "rtf" && s.To == "txt"); // not a hop + } + [Theory] [InlineData("docx")] [InlineData("txt")] diff --git a/tests/Filee.Engines.Tests/RtfToTextTests.cs b/tests/Filee.Engines.Tests/RtfToTextTests.cs new file mode 100644 index 000000000..8578cb01c --- /dev/null +++ b/tests/Filee.Engines.Tests/RtfToTextTests.cs @@ -0,0 +1,57 @@ +// RTF → TXT through LibreOffice (#1204): code copied from an IDE (a colour run per token and an italic run here and +// there) keeps its text, its indentation and its blank lines. The route itself is in LibreOfficeRouteTests. + +using Filee.Core.Conversion; +using Filee.Core.Presets; + +namespace Filee.Engines.Tests; + +public class RtfToTextTests(EngineFixture fx) : IClassFixture +{ + private const string IdeCode = """ + {\rtf1\ansi\ansicpg1252\cocoartf2907 + \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fmodern\fcharset0 Courier;\f1\fmodern\fcharset0 Courier-Oblique;} + {\colortbl;\red255\green255\blue255;\red195\green122\blue88;\red175\green177\blue183;\red186\green102\blue173; + \red73\green153\blue245;\red39\green159\blue171;\red88\green157\blue94;} + \pard\tx560\tx1120\tx1680\tx2240\pardirnatural\partightenfactor0 + + \f0\fs26 \cf2 import \cf3 java.io.BufferedReader;\ + \ + \cf2 class \cf3 FileTask \cf2 implements \cf3 Runnable \{\ + \cf2 private \cf3 String \cf4 fileName\cf3 ;\ + \ + \cf2 public void \cf5 run\cf3 () \{\ + \cf2 int \cf3 count = \cf6 0\cf3 ;\ + content.append(line).append(\cf7 "\cf2 \\n\cf7 "\cf3 );\ + \cf2 for \cf3 (String word : line.split(\cf7 "\cf2 \\\\\cf7 W+"\cf3 )) \{\ + count++;\ + \}\ + System. + \f1\i \cf4 out + \f0\i0 \cf3 .println(\cf7 "Java word is found " \cf3 + count);\ + \}\ + \}\ + } + """; + + [Fact] + public async Task Code_copied_from_an_IDE_keeps_its_text() + { + Assert.SkipUnless(fx.Catalog.StatusOf(fx.Converters.Single(c => c.Id == "libreoffice")).IsAvailable, "needs LibreOffice"); + var rtf = Path.Combine(fx.NewFolder(), "FileTask.rtf"); + await File.WriteAllTextAsync(rtf, IdeCode, TestContext.Current.CancellationToken); + + var job = await fx.ConvertAsync([rtf], new Preset { TargetFormat = "txt" }); + Assert.True(job.State == JobState.Completed, string.Join("; ", job.Files.Select(f => $"{f.ErrorKey} {f.ErrorDetail}"))); + var lines = (await File.ReadAllTextAsync(job.Outputs.Single(), TestContext.Current.CancellationToken)) + .TrimStart('\uFEFF').ReplaceLineEndings("\n").Split('\n'); + + Assert.DoesNotContain(lines, l => l.Contains('`')); + Assert.Equal("import java.io.BufferedReader;", lines[0]); + Assert.Equal("", lines[1]); // blank lines stay + Assert.Contains(" private String fileName;", lines); // and so does the indentation + Assert.Contains(" content.append(line).append(\"\\n\");", lines); + Assert.Contains(" for (String word : line.split(\"\\\\W+\")) {", lines); + Assert.Contains(" System.out.println(\"Java word is found \" + count);", lines); // the italic run + } +} From f0f43a85e046c72dd3f570e60c9b2200ca41837d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yavuz=20Mete=20Af=C5=9Far?= Date: Thu, 12 Nov 2026 00:33:32 +0300 Subject: [PATCH 2/2] =?UTF-8?q?Allow=20LibreOffice's=20RTF=20=E2=86=92=20T?= =?UTF-8?q?XT=20edge=20below=20the=20fallback=20cost=20in=20the=20test=20(?= =?UTF-8?q?#1204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RTF has no built-in reader: every other route reads it through Pandoc, so this is the one LibreOffice edge besides presentations → PDF that may cost less than 25. --- tests/Filee.Engines.Tests/LibreOfficeFormatTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Filee.Engines.Tests/LibreOfficeFormatTests.cs b/tests/Filee.Engines.Tests/LibreOfficeFormatTests.cs index 9bbb3b8bb..98436529f 100644 --- a/tests/Filee.Engines.Tests/LibreOfficeFormatTests.cs +++ b/tests/Filee.Engines.Tests/LibreOfficeFormatTests.cs @@ -74,7 +74,9 @@ public void Every_edge_has_a_filter_of_its_application() } // Built-in engines stay first: LibreOffice's edges cost more than a normal direct conversion, except // presentations → PDF, which it lays out like PowerPoint (Settings → Engines, on by default). - Assert.All(LibreOffice.Edges.Where(e => !(impress.Contains(e.From) && e.To == "pdf")), e => Assert.True(e.Cost >= 25)); + // RTF → TXT: RTF has no built-in reader, and every other route reads it through Pandoc (#1204). + Assert.All(LibreOffice.Edges.Where(e => !(impress.Contains(e.From) && e.To == "pdf") && !(e.From == "rtf" && e.To == "txt")), + e => Assert.True(e.Cost >= 25)); foreach (var from in new[] { "dot", "wps", "wpd", "lwp", "abw", "pages", "sdw", "cwk", "mcw", "602", "psw", "fodt", "ott", "et", "numbers", "sdc", "fods", "ots", "xlt", "key", "dps", "sda", "fodp", "otp", "pub", "cdr", "cgm", "vsd", "odg", "fodg" }) Assert.Contains(LibreOffice.Edges, e => e.From == from && e.To == "pdf"); // Read-only formats are never targets.