Skip to content

feat(spreadsheet): preview and edit XLSX page layout before PDF export - #437

Merged
KnifeLemon merged 8 commits into
KnifeLemon:mainfrom
Perricheno:feature/spreadsheet-print-studio
Oct 23, 2026
Merged

KnifeLemon merged 8 commits into
KnifeLemon:mainfrom
Perricheno:feature/spreadsheet-print-studio

Conversation

@Perricheno

@Perricheno Perricheno commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Dependency

Please merge #428 first, then rebase this branch. That PR owns settings schema v33; this one intentionally uses v34 so existing users receive both toolbar migrations.

What this adds

XLSX → PDF gets an interactive PDF + Edit tool on the back of the spreadsheet donut.

  • left: sheets and regions in final PDF order, with include/exclude and move controls
  • center: real PDF pages rendered by LibreOffice and PDFium
  • right: print area, A5/A4/A3/Letter/Legal/Tabloid, portrait/landscape, exact scale or fit-to-pages, margins, page order, hidden rows/columns, and manual page breaks
  • duplicate one sheet into several independently configured regions, so different ranges can use different orientations or paper sizes
  • detect suspicious source layouts, such as 10–25% scale or a very wide sheet without a print area, and offer a conservative suggested layout
  • export every region separately and merge the PDFs without flattening their page sizes
  • record successful exports in Filee history

The original workbook is never modified. Preview and export both use the same disposable XLSX copy and the same LibreOffice path, so the preview is the actual output rather than an approximation.

Screenshots

The screenshots use a generated workbook with neutral sample data.

Source-layout warning and one-click suggested layout

Spreadsheet Print Studio showing a suspicious 20 percent source scale

A duplicated region with independent Letter, portrait, and fit-to-pages settings

Spreadsheet Print Studio showing a duplicated region with an independent page setup

XLSX handling

The inspector reads native print areas, repeat rows, paper/orientation, scale/fit settings, margins, hidden rows/columns, and content bounds. Styled empty cells do not inflate the detected content range.

Each temporary workbook exposes only the selected sheet. This matters for files saved with stale multi-tab selection: merely hiding the other sheets made LibreOffice export an unrelated first page. Local defined names are renumbered, the selected print area is replaced, and cached printer settings are discarded only when the user overrides layout.

This complements #432: repeated print-title rows remain intact, while the studio handles print area, scale, orientation, paper, and page composition.

Validation

  • SpreadsheetPrintPlanTests: 10/10
  • UserDataStoreTests: 74/74
  • app localization and availability tests: 83/83
  • full core suite: 457/457
  • full CLI suite: 28/28
  • manually exercised with a seven-sheet workbook whose later sheets were saved at 10% scale; a selected A4 landscape region produced six consistently landscape pages with preview matching export
  • all 12 shipped localizations contain the new UI strings

@KnifeLemon, could you also add the feature label when convenient?

@github-actions github-actions Bot added the feature New features label Oct 22, 2026
@Perricheno
Perricheno force-pushed the feature/spreadsheet-print-studio branch from 7a620c0 to fc2be75 Compare October 22, 2026 11:07
@Perricheno
Perricheno force-pushed the feature/spreadsheet-print-studio branch from fc2be75 to 632b955 Compare October 22, 2026 11:10
@KnifeLemon KnifeLemon self-assigned this Oct 22, 2026

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Perricheno, this is a big, well thought-out piece of work and we'd like to have it. The original workbook is only ever opened for reading, preview and export go through the same disposable copy, and letting one sheet become several regions with their own page setup is a neat answer to mixed layouts. All 12 languages have the strings, CI is green, and the focused tests pass here (SpreadsheetPrintPlanTests 10/10, UserDataStoreTests 74/74, availability and localization 87/87).

About #428: this branch doesn't use anything from #428. It merges cleanly into main and conflicts with #428 only where both add a tool (RadialController.ShowTool, PresetAvailability, WindowService, BuiltInData, AppSettings, UserDataStore), so there's no need to wait for it; whichever lands second gets rebased. Don't worry about the settings schema number either: several PRs claim one, and we renumber when merging.

What needs another pass:

  1. Formulas that point at other sheets break. RewriteWorkbook removes every other <sheet> from the copy. I tried a workbook with Data!A1 = 21 and, on the sheet being printed, =Data!A1*3 (no cached value, as many generators write it) and =Rate*10 with a workbook-level name Rate → Data!$A$1. A plain LibreOffice PDF shows 63 and 210; the studio's copy shows #NAME? for both. Only cells that carry a cached value survive, volatile functions such as INDIRECT or TODAY can be recalculated even in Excel-saved files, and charts or pivot tables that read from other sheets are at risk too. Please keep the other sheets in the copy as state="hidden" and fix the stale multi-tab selection you ran into by clearing tabSelected in their sheet views and pointing activeTab/firstSheet at the chosen sheet, leaving localSheetIds as they are. A test with a cross-sheet formula and a workbook-level name would keep it that way.
  2. A print area the sheet never had. When a sheet has no print area, the copy gets one made from cell content (InitialRange = UsedRange), so pictures, charts and shapes outside the cells are cut off and the output differs from what the sheet prints by itself. Please write _xlnm.Print_Area only when the user changed the range (typing it, "Use content" or "Apply suggested layout").
  3. Whole-column and whole-row print areas such as 'Sheet'!$A:$H or $1:$40 are common, but IsValidRange rejects A:H and 1:40, so such a sheet opens with "Enter a valid range" and can't be exported as it is. Please accept them, and a print area whose sheet name contains a comma (the Split(',') in DefinedRange cuts it).
  4. UI thread and big workbooks.
    • The view model reads the workbook in its constructor, on the UI thread, and SpreadsheetPrintInspector goes through OpcPackage, which keeps every worksheet as an XDocument until it's disposed. A workbook with large sheets freezes the UI and takes hundreds of MB. Please show the window first and inspect on a background thread, reading sheetData/cols/mergeCells with an XmlReader (only bounds, hidden rows and print settings are needed).
    • RenderAsync calls SpreadsheetPrintPackage.Create before its first await, so the ZIP rewrite (every part recompressed with CompressionLevel.Optimal) runs on the UI thread for every preview. Please move it into Task.Run and use Fastest or NoCompression for the throw-away copy.
    • PdfPagePreviewRenderer renders every page at 112 dpi, the PNG bytes stay in SpreadsheetRenderedRegion, and a full-size Bitmap is decoded per page on the UI thread, also for the thumbnail strip. A sheet that prints to 200 A4 pages needs about 1 GB. Please render small thumbnails, the large image only for the selected page, and decode off the UI thread. Also, Conversion.ToImage(bytes, index) parses the whole PDF again for every page, the same thing we noted on #428. Both PRs add their own PDFium preview helper (PdfPageTools.Preview there, PdfPagePreviewRenderer here); one shared renderer that opens the document once would serve both.
    • ExportAsync goes through RenderAsync, so every export also renders and PNG-encodes every page only to throw it away. Please split "make the PDF" from "render previews".
  5. Export can't be cancelled. No token is passed to ExportAsync and there's no Cancel while exporting; closing the window mid-export leaves LibreOffice running, and the PDF and the history entry are still written afterwards. Please let Cancel and closing the window cancel the export (ProcessRunner already kills soffice), and don't start previews from setting changes while an export runs.
  6. Temporary folders. The filee-sheet-<guid> folders go straight into the temp folder. When deleting fails (on Windows soffice can still hold a file right after it was killed) or Filee is closed hard, nothing sweeps them. Putting them under JobQueue.DefaultWorkRoot with a name DeleteStaleWorkFolders recognizes (10 lowercase hex characters) reuses the existing clean-up.
  7. Keyboard, screen readers and text. The ↑ ↓ × buttons need AutomationProperties.Name (as in MergeOrderWindow), the check box in each row needs a name (the sheet's), and the NumericUpDowns need a label (AutomationProperties.Name or LabeledBy). Esc should close the window, as in the crop tool. The save dialog's title "Export PDF" is hard-coded English; sheet_studio.export_pdf is there for it. Exception messages ("LibreOffice did not create a PDF preview.") end up in the status line in English; a localized message with the details in the log would be better.
  8. Smaller UI things. LayerBrush and the TextBlock.h3 class don't exist in Filee's styles (Styles/Soft.axaml), so the preview pane has no background and the headings are unstyled. IsVisible="{Binding SelectedSheet}" should use ObjectConverters.IsNotNull. With several workbooks dropped, only the first opens and the rest are silently ignored; please make the slice usable for a single file only (or open them one after another). × can remove a sheet's only region, which then can't come back until the window is reopened; the check box already leaves a sheet out, so × could be limited to added regions.
  9. Screenshots in the repository. Please remove docs/media/spreadsheet-print-studio-*.png, as on #417: keep them on a separate branch for the description.
  10. Docs. Please add a short section to docs/ARCHITECTURE.md next to the crop tool (tool-sheet-studio opens SpreadsheetPrintStudioWindow, the disposable copy, one LibreOffice run per region, the PDFsharp merge), and mention in docs/ENGINES.md that the studio needs LibreOffice even when Settings → Engines doesn't use it for spreadsheets, with previews drawn by PDFium (PDFtoImage). "Preview matches export" is true inside the studio, but the donut's regular PDF slice uses the built-in engine for XLSX, so the docs should say the two can look different.

On tests: besides the XML-level tests, a LibreOffice round trip (skipped when LibreOffice is missing, like LibreOffice_warm_up_prepares_every_profile_once) for point 1, and a view-model test that cancelling or closing removes the temp folder and leaves no .filee-tmp, would cover the risky parts.

Thanks again, looking forward to the next round!

@Perricheno

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review — I addressed the full pass in 6df82df.

  1. Other worksheets now stay in the disposable workbook as hidden sheets. Their tabSelected flags are cleared, the selected sheet remains visible, activeTab/firstSheet point to its original index, and every localSheetId stays unchanged. This preserves cross-sheet formulas, workbook/local names, charts, pivots, and volatile recalculation dependencies.
  2. A workbook without a print area no longer gets one from UsedRange automatically. _xlnm.Print_Area is written only after an explicit range action (typing, Use content, Apply suggested layout, or adding a separate region); Reset restores the source behavior.
  3. Whole-column and whole-row areas (A:H, 1:40) are accepted, bounded to Excel limits, and union ranges are split without breaking quoted sheet names containing commas.
  4. Large-workbook work is off the UI thread: inspection opens after the window and streams worksheet cells/columns/merges through XmlReader; disposable XLSX copies use CompressionLevel.Fastest in Task.Run. PdfPreviewDocument caches PDF bytes and page geometry, renders the whole small thumbnail strip in one PDFium load, renders only the selected page large, and all PNG/Bitmap work happens off the UI thread. Export now uses the PDF-producing path directly and never rasterizes previews.
  5. Export owns a cancellation token. Cancel, Esc, the window close button, and closing the window cancel LibreOffice; setting changes cannot start preview jobs during export, and cancelled exports do not write history.
  6. Studio work directories now live below JobQueue.DefaultWorkRoot with 10-character lowercase hex names, so the existing stale-folder sweep recognizes crash leftovers.
  7. Added the requested automation names, localized save-dialog title/status, Esc behavior, and logging of technical exception details without exposing raw English exceptions in the UI.
  8. Replaced the nonexistent brush/style references, fixed the null visibility binding, limited the tool to one XLSX, and disabled/removes only added regions — a sheet's sole region can no longer disappear. I also separated small thumbnails from the selected-page preview and tightened the window proportions and controls.
  9. Removed both repository screenshots; the PR-description images remain external to the branch.
  10. Added the print-studio architecture and engine documentation, including the LibreOffice requirement, PDFium preview path, and why ordinary donut XLSX → PDF can differ from studio export.

Regression coverage now includes:

  • XML preservation of both direct cross-sheet formulas and workbook/local named ranges;
  • a real LibreOffice round trip, skipped when unavailable, with no cached values (Data!A1 = 21, resulting PDF contains 63 and 210);
  • no implicit print area, whole-row/whole-column areas, and comma-containing sheet names;
  • view-model close/cancel cleanup with no work folder or .filee-tmp left behind;
  • multi-workbook availability rejection.

Local results: SpreadsheetPrintPlanTests 15/15, the focused availability/localization/cancellation set 79/79, and the app build succeeds.

I also tried to make the studio feel calmer and more readable, but UI design is not my strongest area. If you have preferences for the information hierarchy, spacing/density, settings grouping (flat panel vs cards/sections), or thumbnail behavior, I would genuinely appreciate specific UI direction and can refine it to match Filee's visual language more closely.

@Perricheno

Copy link
Copy Markdown
Contributor Author

Small follow-up in 81ba641: I tightened the × behavior literally to the suggestion — original sheet regions are now permanent, and only explicitly added regions can be removed. The button is disabled on an original region even when another region for that sheet exists.

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Perricheno, this round covers the whole list, and the new tests cover the risky parts. Here, the LibreOffice round trip gives 63 and 210 too. On 81ba641 merged with today's main: SpreadsheetPrintPlanTests 15/15 (the LibreOffice round trip included), SpreadsheetPrintStudioTests, ToolbarEditorTests and LocalizationTests 66/67 (the one failure is Nepali, see the end), UserDataStoreTests 74/74.

All ten points are addressed: other sheets stay in the copy as hidden sheets with their names intact, a print area is written only after an explicit range change, A:H, 1:40 and commas in sheet names work, inspection streams through XmlReader after the window is up, the ZIP copy uses Fastest in Task.Run, thumbnails are small and only the selected page is rendered large, export never renders previews and can be cancelled (Esc, Cancel and closing the window stop LibreOffice and write no history), work folders live under JobQueue.DefaultWorkRoot with names the sweep recognizes, the automation names and localized dialog title are there, the missing styles are gone, original regions can't be removed, the screenshots are out of the tree, and both docs have their sections.

What's left is small:

  1. CI build: dotnet format stops at tests/Filee.Engines.Tests/SpreadsheetPrintPlanTests.cs (IMPORTS): using Filee.Engines.Infrastructure; has to come before using Filee.Engines.Office;. Running dotnet format Filee.slnx fixes it.
  2. Two workbooks dropped at once. The donut checks a slice against the distinct formats of the dropped files (ProfileSelector.DetectFormats ends in Distinct()), so two .xlsx files reach Evaluate as a single xlsx and the slice is enabled. Clicking it does nothing, because RadialController.ShowTool now requires files.Count == 1. The new test passes [xlsx, xlsx], which the donut never sends. Availability can't see the file count, so I'd handle it in ShowTool: open one studio per workbook (they're independent windows), or at least the first one. Either way the click shouldn't silently do nothing.
  3. Names for the remaining fields. The print-area TextBox and the four ComboBoxes (paper, orientation, scaling, page order) have a caption above them, but nothing links the caption to the control, so a screen reader announces only "edit" or "combo box". Giving each an AutomationProperties.Name with the same resource, as you did for the NumericUpDowns, is enough.
  4. PdfPagePreviewRenderer.Render is only used by a test now. Please drop it and test through PdfPreviewDocument.

About #428: both PRs now carry a cached PDF preview class that does the same job, PdfPreviewDocument here and PdfPreviewSource in #428 (the one here also renders the whole thumbnail strip in one PDFium load, so it's the better base). Since both PRs are yours, could you keep one of them in whichever PR lands first and have the other use it when it's rebased? The two branches also conflict in 8 files where both add a tool (docs/ARCHITECTURE.md, docs/ENGINES.md, PresetAvailability, RadialController, WindowService, BuiltInData, AppSettings, UserDataStore), so the second one needs a rebase anyway.

On the UI: thanks for asking. We'll try a build ourselves and send concrete notes if something stands out, so there's no need to keep polishing the layout in this PR.

Nepali (ne.json) was merged into main after your push, which is why the Linux jobs fail Every_language_has_exactly_the_english_keys(ne) on the 39 new keys. That's on us: we'll add the Nepali strings when merging, renumber the settings schema, and keep the removed screenshots out of main's history.

@Perricheno

Copy link
Copy Markdown
Contributor Author

Thanks! Addressed the remaining four points:

  1. Fixed the import order in SpreadsheetPrintPlanTests. dotnet format Filee.slnx --no-restore --verify-no-changes now passes.
  2. Multiple XLSX files now open independent print-studio windows, each with its own view model/plan. ShowTool walks the actual file list, and opening another studio no longer closes the previous one. Availability checks only supported formats and LibreOffice, rather than treating the format count as a file count. Added a window test that opens two workbooks, checks that both stay visible, closes one and confirms the other remains open.
  3. Added AutomationProperties.Name to the print-area field and all four remaining combo boxes, using their existing localized captions.
  4. Removed PdfPagePreviewRenderer; the preview-shape test now opens PdfPreviewDocument and calls its thumbnail API directly.

Validation: SpreadsheetPrintPlanTests 15/15 (including the real LibreOffice round trip), focused studio/availability/localization tests 80/80, and the full solution's formatting verification passes. These results are on this branch's current base; I understand Nepali will be supplied when merging today's main.

Agreed on the preview implementation: PdfPreviewDocument here is the base to keep, including the single-load thumbnail strip. Once the first PR lands, I will rebase the second, remove the duplicate renderer and update its callers while resolving the tool-registration conflicts. The owner-password/read-only fixes for #428 are already pushed in 85d529f.

Thanks for offering to try the UI yourselves. I will keep the layout at this scope and wait for concrete feedback from your build. Thanks as well for handling Nepali, schema numbering and the screenshot history at merge time.

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Perricheno, all four are in: the imports are in order and dotnet format is clean, several workbooks now open one studio each (ShowTool walks the files and availability accepts any number of XLSX files), the print-area box and the four combo boxes have names, and PdfPagePreviewRenderer is gone with its test going through PdfPreviewDocument.

I merged it locally onto main together with #429 (which goes in just before this one), resolved the conflicts and ran Core 508/511, SpreadsheetPrintPlanTests 15/15 with the LibreOffice round trip, and the studio, toolbar editor, availability, donut flip and localization tests 131/131. dotnet format --verify-no-changes is clean on the merged tree.

We'll merge this one first of your two PRs. What we do when merging, so you don't have to:

  • main's changes in docs/ENGINES.md, BuiltInData, AppSettings and UserDataStore: the studio goes first on the spreadsheet donut's back face next to main's new iCalendar slice (10 slices), and the migration adds the preset after "Crop" as a new library has it and skips a donut that already shows it on its front;
  • the settings schema number (40, after #429's 39) and the migration test's name;
  • Nepali and Thai strings for the 39 new keys, and three Korean strings in 해요체 like the rest of ko.json;
  • a squash merge, so the removed screenshots stay out of main's history.

#428 then gets rebased onto PdfPreviewDocument, as you planned. Approving.

To get this into the next release, we'll push the remaining changes to your branch ourselves today (maintainer edits are on). Have a look at the commits when they land, and tell us if anything looks off.

KnifeLemon and others added 4 commits October 23, 2026 20:37
Main is at settings schema 40 and 41 is taken by KnifeLemon#493, so the studio's step is
v42: "PDF + Edit" goes after "Crop" as in a new library, and first on the
spreadsheets donut's back face (next to main's iCalendar slice) when it
has room and the user hasn't put it on either face. ENGINES.md keeps
main's "PDFs without their metadata" next to the studio assembly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ne, th: the 39 print studio strings, where en.json has them.
- ko: sheet_studio.page_count, warning_scale and warning_wide in the
  polite style of the rest of ko.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/Filee.Core/Settings/AppSettings.cs
#	src/Filee.Core/Settings/UserDataStore.cs
@KnifeLemon
KnifeLemon merged commit 98d06cd into KnifeLemon:main Oct 23, 2026
10 checks passed
KnifeLemon added a commit to flexeykinDev/Filee that referenced this pull request Oct 24, 2026
Main's spreadsheet print studio (KnifeLemon#437) took settings schema 42, so Cut a
clip moves to 43. The print studio's preset has no Tool of its own, which
this branch reads as a crop: WindowService.ShowTool opens the studio before
it reaches the crop, and PresetAvailability judges it before the crop check.
A schema 36 library now gets the clip tool first on the video back face too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants