Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Filee.Engines/Office/LibreOfficeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,10 @@ void Add(IEnumerable<string> sources, IEnumerable<string> 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);
Expand Down
4 changes: 3 additions & 1 deletion tests/Filee.Engines.Tests/LibreOfficeFormatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions tests/Filee.Engines.Tests/LibreOfficeRouteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
57 changes: 57 additions & 0 deletions tests/Filee.Engines.Tests/RtfToTextTests.cs
Original file line number Diff line number Diff line change
@@ -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<EngineFixture>
{
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
}
}
Loading