A converter is one class implementing IConverter (src/Filee.Core/Conversion/IConverter.cs).
The route planner combines converters automatically, so a new edge can unlock many conversions at once
(for example, adding epub → pdf also gives epub → png through PDFium).
If the format is new, add one line to FormatRegistry.Known (src/Filee.Core/Formats/FormatRegistry.cs):
new("epub", "EPUB", FormatCategory.Document, ["epub"]),Then add its id to one grade list in FormatFields (src/Filee.Core/Formats/FormatFields.cs): everyday, general,
specialist or legacy, by how widely files in it are made today. The full criteria, tie-breaks and close calls are in
#844; what fields and grades decide is in
docs/FORMAT-RULES.md. It belongs to its category's field; if it belongs to
other fields too, or only to a field that can be turned off, give it an Overlaps row. A new output format graded
specialist or legacy must belong only to fields that can be turned off and are off by default. FormatFieldsTests fails
until the format has a grade.
Create src/Filee.Engines/<Area>/<Name>Converter.cs:
// EPUB → PDF with the (hypothetical) SuperEpub library (MIT).
using Filee.Core.Conversion;
namespace Filee.Engines.Documents;
public sealed class SuperEpubConverter : IConverter
{
public string Id => "superepub"; // stable, used in settings
public string DisplayName => "SuperEpub";
public int MaxParallelism => 0; // 0 = one per CPU core; 1 for non thread-safe engines
public IReadOnlyList<ConversionEdge> Edges { get; } =
[
new("epub", "pdf"), // cost 10 = normal; higher for lossy/slow paths
];
public EngineStatus GetStatus() => EngineStatus.Available();
public Task<IReadOnlyList<string>> ConvertAsync(ConversionStep step, IProgress<double>? progress, CancellationToken ct) =>
Task.Run<IReadOnlyList<string>>(() =>
{
var output = step.Output.Allocate("pdf"); // null = skip (file exists + "skip" policy)
if (output is null)
return [];
SuperEpub.Convert(step.InputPath, output); // honour ct in long loops
progress?.Report(1);
return [output];
}, ct);
}Rules:
- Never modify
step.InputPath. - Write outputs only to paths from
step.Output.Allocate(...). For several outputs from one input (pages), pass a suffix:Allocate("png", $"_p{page}"). - Scratch files go to
step.WorkDirectory(deleted after the job). - External programs: use
ProcessRunner.RunAsync(timeout + cancellation + no shell) and find them withEngineEnvironment.FindBundled("<folder>")only. Filee never uses programs installed on the system (nothing fromPATH, no other application's install folder): a program is either bundled with the installer (small, pinned inengines.jsonand fetched bybuild/fetch-engines.ps1) or an optional download the user installs in Settings → Engines (anEnginePackageinEngineDownloads.Packages). - Return
EngineStatus.Unavailable("engine.reason.not_installed")when a dependency is missing; the UI then greys out affected slices with a reason and names the package to download. Report a version withEngineStatus.Available(version: …)(seeEngineVersions) and add anengine.<id>.descriptiontext to the threei18n/*.jsonfiles: both are shown in Settings → Engines.
Add one line to EngineRegistry.CreateAll (src/Filee.Engines/EngineRegistry.cs).
If it should be preferred over existing engines by default, add its id to AppSettings.EnginePriority.
Add a test to tests/Filee.Engines.Tests. Generate the input files in the test (see EngineFixture) so the test
runs everywhere without sample documents, and skip when the engine is not available:
[Fact]
public async Task Epub_to_pdf()
{
var job = await fx.ConvertAsync([MakeEpub()], new Preset { TargetFormat = "pdf" });
Assert.Equal(JobState.Completed, job.State);
}Command line tools are pinned in src/Filee.Engines/Infrastructure/engines.json (URL, SHA-256, size, archive kind),
which both build/fetch-engines.ps1 and the app read. Small tools can be bundled with the installer (add the id to
the fetch-engines.ps1 -Only list in .github/workflows/release.yml); large ones become an optional download by
adding an EnginePackage to EngineDownloads.Packages plus its name and description in the three i18n/*.json
files. Either way, add a line to THIRD-PARTY-NOTICES.md.