Skip to content

Turn a folder into one printable PDF — and let users add their own actions #966

Open

Description

@pokiimon

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 in tests/Filee.Core.Tests / tests/Filee.Engines.Tests, i18n keys in every src/Filee.App/Assets/i18n/*.json, following CONTRIBUTING.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

  1. KnifeLemon commented on Nov 1, 2026

    @KnifeLemon
    Owner

    Thanks for the careful write-up and the measurements, @pokiimon. And thanks for checking CONTRIBUTING.md first: 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-engines branch for the next big release). XLSX, XLS and ODS go to PDF with Excel's page logic:

    • the page setup stored in the workbook: paper, orientation, margins, scaling and fit to N × M pages;
    • print areas, print titles, manual page breaks and page order;
    • centring, gridlines and headings;
    • headers and footers.

    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:

    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

    • A new kind of tool preset, Run a program, edited in the preset editor like the others.
    • Fields:
      • the program: an absolute path, picked with a file dialog;
      • the arguments, with placeholders;
      • which input formats it accepts;
      • per file or all files at once;
      • the output extension it makes.
    • Placeholders: {input}, {inputs} (all mode), {name}, {ext}, {dir}, {output} (the path Filee picked under the usual output rules), {outputDir}.

    How it runs

    • No shell. Start the program directly with ProcessStartInfo.ArgumentList, one entry per argument, so a file name can never inject a command. Use the existing ProcessRunner.
    • Placeholders expand inside one argument and are never split. Someone who really wants a shell can put cmd /c or sh -c in the program field themselves.
    • Exit code other than 0 means failed, and stderr goes to the log and the toast.
    • The job queue handles the rest: timeout, cancelling, and the usual output-file rules, conflict names included.
    • What counts as output: the file at {output} if the program wrote it, or else new files in {outputDir}.

    Safety

    • Nothing ships with such a preset, and none is downloaded.
    • Imported or shared presets: a Run a program preset is never run until the user has confirmed its full command once, on that computer. Keep a hash of program + arguments, and ask again whenever it changes.
    • The full command is always visible in the editor.

    First version: leave out

    • Watch folders. They run without anyone looking, so they come later, once the confirmation above exists.
    • Plugins, marketplaces and in-process code (Proposal C).

    Housekeeping

    • New strings in all 19 language files.
    • Tests in Filee.Core.Tests and Filee.App.Tests.
    • A settings schema step only if you need one (check main right before you push).
    • The PR goes to 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions