Repository navigation
Description
One line: Filee is one option away from "folder → one PDF I can actually print", and beyond that I would like a supported way for users to add their own actions.
What Filee already does — 2.2.0, verified
filee convert D:\project --recursive --preset merge-pdf -o out
This walks the tree, routes every leaf file through RoutePlanner, and merges everything into one PDF. On my test folder (7 mixed files — XLSX/DOCX/TXT/CSV/PNG, three levels deep) it produced a single 9-page PDF with zero failures.
So the thing I set out to ask for mostly exists already. What is left is small, specific and measurable:
What stands between that and something I can print
| # | What I measured | Effect on paper |
|---|---|---|
| 1 | Spreadsheet sources are paginated by raw column width: a 14-column sheet came out as 2 pages, the second holding a 35 pt strip of two leftover rows | 9 pages instead of 7; orphan rows |
| 2 | No fit-to-width and no centring: content landed at 46 pt left / 24 pt right, 49 pt top / 41 pt bottom | off-centre, looks wrong on the page |
| 3 | Page setup stored inside the workbook is not honoured | the scaling and area the author saved are ignored |
| 4 | Paper size follows each source: 842×596 (sheet), 612×792 (DOCX, Letter), 1200×675 for a PNG page | one print job, three paper sizes |
| 5 | In my test the merge order was breadth-first (root files → depth 1 → depth 2 …), and there are no bookmarks | people expect Explorer order; no way to jump per source file |
Proposal A — page setup for spreadsheet sources (the small one)
New options on the existing convert/merge path, not a new action:
filee convert D:\project --recursive --preset merge-pdf \
--fit-width --center-both --paper A4 --orientation auto --bookmarks
| Option | Meaning |
|---|---|
--fit-width |
scale so all columns fit one page width (Excel's fitToWidth = 1) |
--fit-page |
whole sheet on one page (fitToWidth = fitToHeight = 1) |
--center-both |
CenterHorizontally + CenterVertically |
--paper A4/A3 |
one paper size for the whole document |
--orientation auto |
pick per sheet by comparing real page counts |
--bookmarks |
one outline entry per source file, once page offsets are known |
Three details from building a workaround, in case they save you time:
- Honour the print settings already stored in the workbook when the user has not overridden them. Most such files ship with their own area and scaling; respecting it is both correct and cheaper than guessing.
- Include floating objects when deriving a print area. Charts and logos usually sit outside the used cell range (
Shape.TopLeftCell/Shape.BottomRightCell) and silently disappear from the PDF if excluded. - Keep one paper size per document — today a PNG source produces a 1200×675 page, which cannot be printed with the A4 pages around it.
On my test folder this turns 9 pages into 7, centred, on one paper size. You already have the machinery: N→1 jobs (IFileCombiner, IMediaJoiner, IImageJoiner, PDF merge via PdfSharp), IPdfFinisher running per result and over the merged file, and tool-sheet-studio / SpreadsheetPrintService already working with print regions. I read this as extending that line rather than starting a new subsystem — though I will not guess the effort from outside.
Proposal B — user-defined actions (the extensibility ask)
This is the part I care about most long term, because it generalises past my own case. Presets already persist in UserDataStore.presets.json and are exposed through ToolbarProfile / ProfileSelector / PresetAvailability, so the missing piece is mostly an execution path:
{
"id": "folder-print-pdf", "name": "Folder → one PDF",
"inputs": "all", // single | each | all (1→N vs N→1)
"accepts": ["folder", ".xlsx", ".pdf", ".docx"],
"target": "pdf",
"command": "python \"C:\\tools\\x.py\" {inputs} -o {outputDir}",
"surfaces": ["donut", "contextMenu", "watchFolder", "cli"]
}Contract: {inputs}, {outputDir}, {target}, exit code, stdout/stderr surfaced in toast and history. Nothing is downloaded, nothing runs that the user did not write themselves, the command is visible in the preset editor, each action can be disabled, and it runs out-of-process so a badly behaved tool cannot take Filee down.
That one mechanism covers the workflows people keep rebuilding around Filee: OCR post-passes, batch rename/normalise, sync to a local NAS, template printing — and, yes, the folder→PDF case above. Most requests like mine would then arrive as a preset someone shares, instead of an issue asking you to ship somebody's niche step.
Proposal C — plugin API (longer term)
If extensibility is something you want to own properly: a plugin registers converter edges into ConverterCatalog under an EngineRegistry entry (inheriting Settings → Engines priority and JobQueue's per-engine concurrency), implements IConverter (1→1) or a combiner seam for N→1, optionally IPdfFinisher for post-processing, and surfaces via Preset + ToolbarProfile + PresetAvailability. In-process assemblies vs out-of-process tools speaking a documented protocol is your call; either works from where I stand, as long as the boundary is documented.
Why an extension point is worth it
The people who chose Filee for offline conversion eventually grow a local workflow around it; today the only ways to extend it are forking or wrapping, and both duplicate plumbing Filee already has. One supported seam turns those into presets, keeps Filee focused on conversion quality, and reduces feature issues rather than adding to them.
What I am offering
- Implement A (page-setup options + tests) or B (external-command presets) as a focused PR —
dotnet format, tests intests/Filee.Core.Tests/tests/Filee.Engines.Tests, i18n keys in everysrc/Filee.App/Assets/i18n/*.json, followingCONTRIBUTING.md. - Test material: an anonymised project tree (200+ spreadsheets, three levels deep), plus the 9-page vs 7-page comparison above.
I would rather agree on the shape than hand you a surprise PR: tell me which tier you would accept (A, B, or a design you already have in mind) and I will build to that.
Maintainer note: my current workaround does the layout step through Microsoft Excel via COM. Since CONTRIBUTING.md states Filee deliberately does not use Microsoft Office, Proposal A is written as "Filee's own renderer respecting (or overriding) the print settings stored in the workbook" — not as "call Excel".
Activity
KnifeLemon commentedon Nov 1, 2026
Thanks for the careful write-up and the measurements, @pokiimon. And thanks for checking
CONTRIBUTING.mdfirst: Proposal A written as "Filee's own renderer" is exactly the right framing.Proposal A: mostly done already, not released yet
Filee now has its own spreadsheet print engine (#951, in the
builtin-enginesbranch for the next big release). XLSX, XLS and ODS go to PDF with Excel's page logic:So rows 1–3 of your table (the orphan strip, no fit or centring, the workbook's page setup ignored) are what that engine fixes. Charts and shapes aren't printed yet. Your note about floating objects outside the used range is useful for that, thanks.
The two points left from your table are tracked separately:
--recursivemerges is a fair expectation. We'll look at it with the merge work.So there's no need for a PR for A.
Proposal B: yes, let's do it
A preset that runs the user's own program is the first step we promised in #465, and you're welcome to build it. Here's the shape we'd accept. Please say if anything doesn't fit your case before you start.
The preset
{input},{inputs}(all mode),{name},{ext},{dir},{output}(the path Filee picked under the usual output rules),{outputDir}.How it runs
ProcessStartInfo.ArgumentList, one entry per argument, so a file name can never inject a command. Use the existingProcessRunner.cmd /corsh -cin the program field themselves.{output}if the program wrote it, or else new files in{outputDir}.Safety
First version: leave out
Housekeeping
Filee.Core.TestsandFilee.App.Tests.mainright before you push).main.This also covers #962 (DOCX → PDF through an installed WPS): with Run a program, that worker can be a preset instead of a built-in engine.
I've assigned this to you. Discussion of B continues here; #465 stays as the place for the wider plugin idea.
pokiimon commentedon Nov 3, 2026
Thank you — this fits my case, and I'll take B: a new kind of tool preset, strings in all 19 language files, tests in
Filee.Core.TestsandFilee.App.Tests, PR tomain. No PR for A.I read the code on
mainfirst, so my four questions each come with what I found — you can just confirm or correct:How
{inputs}encodes N paths in all-files mode.ProcessRunner.RunAsync(src/Filee.Engines/Infrastructure/ProcessRunner.cs) already takesIEnumerable<string>intoProcessStartInfo.ArgumentList, one entry per argument. So my proposal:{inputs}is the one placeholder allowed to expand into severalArgumentListentries (one per path), while every other placeholder still expands inside a single argument. No shell, no manifest file, and it matches the existing signature exactly. Right?A folder as the input. My main case is dragging one folder, not a selection of files. The CLI already walks folders (
ExpandInputsinFilee.Cli/Cli.cs, filtered byFormatRegistry.Detect), andFormatRegistryhas aFolderformat (Preset.PacksIntoOneArchiveuses it), so folders are already first-class in the queue. Proposal:acceptsmay include that Folder format, and when the input is a folder,{inputs}carries the folder path itself — my program walks it, so I keep control of recursion and depth. Or would you rather Filee expand it to the file list?Order in all-files mode.
FileSorterwithNaturalComparerandFolderOrderCompareralready exists inFilee.Core/Platform/FileSorter.cs, and the CLI exposes it as--sort name. Proposal: all-files mode passes paths inFileSorter.Sort(..., foldersTogether: true)name-ascending order, which is what Explorer shows. A per-preset sort choice could come later. Right?What the confirmation hash covers. I'd hash the program path plus the argument template — not the expanded command, or every new input re-prompts — and re-confirm when the binary at the same path changes (otherwise replacing the exe silently keeps a stale approval). If that is more than v1 needs, say so and I'll keep the first PR to hash-on-change-reprompt only.
One design note: I'd add this as a new
PresetKind(sayProgram).PresetKindJsonConverteralready reads an unknown kind as Group, so an older Filee just greys the slice out instead of breaking the user's presets file.Noted and agreed: no watch folders, no plugins / marketplaces / in-process code in v1; full command always visible; nothing shipped or downloaded. Our own workaround happens to be a watch-folder wrapper, so it will be a useful test case once the confirmation step exists.
Happy to start as soon as 1–3 are settled; 4 can wait if you would rather keep the first PR small.
KnifeLemon commentedon Nov 4, 2026
Thanks, @pokiimon, and for opening #1080 as a draft so we can look at running code. Your four questions:
1.
{inputs}→ several arguments: yes. It's the one placeholder that becomes oneArgumentListentry per path, and only when it stands as an argument on its own. Inside other text (--files={inputs}) the editor should say it can't be used that way, rather than guess a separator.One limit to handle: Windows caps a command line at 32,767 characters. A big all-files run should stop with a clear message (and suggest dropping the folder instead) rather than fail to start.
2. A folder as input: pass the folder path itself, but only when the preset's accepted formats include Folder. A preset that doesn't list it keeps today's behaviour (the folder is expanded into its files). That leaves recursion to your program without changing anything for other presets.
3. Order in all-files mode: yes,
FileSorter.Sort(..., FileSortOrder.NameAscending, foldersTogether: true). It's the same order asfilee convert --recursive --sort name. A per-preset choice can come later.4. What the confirmation covers: program path + argument template (+ per-file / all-files), not the expanded command. Agreed.
Leave the program file's contents out of v1. The prompt protects against a shared preset running something the user never saw; someone who can replace the exe at that path can already run code as the user. Re-asking when the program file changes can come later if it proves useful.
Also new since your first post: 2.3.0 (out now) has
--sort name, and with--recursiveit keeps each folder's files together in Explorer's order (sub folders first). So the breadth-first order from your table is covered:One paper size and a bookmark per source file are still open in #965.
Mark #1080 ready for review when it matches this, and I'll review it there.