Repository navigation
feat(spreadsheet): preview and edit XLSX page layout before PDF export - #437
Conversation
7a620c0 to
fc2be75
Compare
fc2be75 to
632b955
Compare
KnifeLemon
left a comment
There was a problem hiding this comment.
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:
- Formulas that point at other sheets break.
RewriteWorkbookremoves every other<sheet>from the copy. I tried a workbook withData!A1 = 21and, on the sheet being printed,=Data!A1*3(no cached value, as many generators write it) and=Rate*10with a workbook-level nameRate→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 asstate="hidden"and fix the stale multi-tab selection you ran into by clearingtabSelectedin their sheet views and pointingactiveTab/firstSheetat the chosen sheet, leavinglocalSheetIds as they are. A test with a cross-sheet formula and a workbook-level name would keep it that way. - 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_Areaonly when the user changed the range (typing it, "Use content" or "Apply suggested layout"). - Whole-column and whole-row print areas such as
'Sheet'!$A:$Hor$1:$40are common, butIsValidRangerejectsA:Hand1: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 (theSplit(',')inDefinedRangecuts it). - UI thread and big workbooks.
- The view model reads the workbook in its constructor, on the UI thread, and
SpreadsheetPrintInspectorgoes throughOpcPackage, which keeps every worksheet as anXDocumentuntil 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, readingsheetData/cols/mergeCellswith anXmlReader(only bounds, hidden rows and print settings are needed). RenderAsynccallsSpreadsheetPrintPackage.Createbefore its firstawait, so the ZIP rewrite (every part recompressed withCompressionLevel.Optimal) runs on the UI thread for every preview. Please move it intoTask.Runand useFastestorNoCompressionfor the throw-away copy.PdfPagePreviewRendererrenders every page at 112 dpi, the PNG bytes stay inSpreadsheetRenderedRegion, and a full-sizeBitmapis 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.Previewthere,PdfPagePreviewRendererhere); one shared renderer that opens the document once would serve both.ExportAsyncgoes throughRenderAsync, so every export also renders and PNG-encodes every page only to throw it away. Please split "make the PDF" from "render previews".
- The view model reads the workbook in its constructor, on the UI thread, and
- Export can't be cancelled. No token is passed to
ExportAsyncand 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 (ProcessRunneralready kills soffice), and don't start previews from setting changes while an export runs. - 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 underJobQueue.DefaultWorkRootwith a nameDeleteStaleWorkFoldersrecognizes (10 lowercase hex characters) reuses the existing clean-up. - Keyboard, screen readers and text. The ↑ ↓ × buttons need
AutomationProperties.Name(as inMergeOrderWindow), the check box in each row needs a name (the sheet's), and theNumericUpDowns need a label (AutomationProperties.NameorLabeledBy). Esc should close the window, as in the crop tool. The save dialog's title "Export PDF" is hard-coded English;sheet_studio.export_pdfis 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. - Smaller UI things.
LayerBrushand theTextBlock.h3class 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 useObjectConverters.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. - Screenshots in the repository. Please remove
docs/media/spreadsheet-print-studio-*.png, as on #417: keep them on a separate branch for the description. - Docs. Please add a short section to
docs/ARCHITECTURE.mdnext to the crop tool (tool-sheet-studioopensSpreadsheetPrintStudioWindow, the disposable copy, one LibreOffice run per region, the PDFsharp merge), and mention indocs/ENGINES.mdthat 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!
|
Thanks for the detailed review — I addressed the full pass in 6df82df.
Regression coverage now includes:
Local results: 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. |
|
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
left a comment
There was a problem hiding this comment.
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:
- CI build:
dotnet formatstops attests/Filee.Engines.Tests/SpreadsheetPrintPlanTests.cs(IMPORTS):using Filee.Engines.Infrastructure;has to come beforeusing Filee.Engines.Office;. Runningdotnet format Filee.slnxfixes it. - Two workbooks dropped at once. The donut checks a slice against the distinct formats of the dropped files (
ProfileSelector.DetectFormatsends inDistinct()), so two.xlsxfiles reachEvaluateas a singlexlsxand the slice is enabled. Clicking it does nothing, becauseRadialController.ShowToolnow requiresfiles.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 inShowTool: open one studio per workbook (they're independent windows), or at least the first one. Either way the click shouldn't silently do nothing. - Names for the remaining fields. The print-area
TextBoxand the fourComboBoxes (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 anAutomationProperties.Namewith the same resource, as you did for theNumericUpDowns, is enough. PdfPagePreviewRenderer.Renderis only used by a test now. Please drop it and test throughPdfPreviewDocument.
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.
|
Thanks! Addressed the remaining four points:
Validation: Agreed on the preview implementation: 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
left a comment
There was a problem hiding this comment.
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,AppSettingsandUserDataStore: 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.
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
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>
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.
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
A duplicated region with independent Letter, portrait, and fit-to-pages settings
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/10UserDataStoreTests: 74/74@KnifeLemon, could you also add the
featurelabel when convenient?