You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[US-1905] Images navigator in the left rail and a dump images command - #26
There was no way to see the images in a document without expanding pages one at a time in the Structure tree. A logo on every page shows up once per page there, so you could not tell whether a file shares one image object or carries 300 copies of it. The CLI had no command that lists images at all.
This adds an Images entry to the left rail, next to Structure and Pages. It lists every image XObject in the document once per object, with how many pages use it, and a By page view for "what is on page 47". The same list is available from the CLI as dump images.
internal/pdfcore/imageindex.go adds GetImageIndex, GetImagePages and GetImagePageGroups. The index is built once per open document from the cached page walk the Pages navigator already uses, so inherited /Resources are resolved in one place. Entries are sorted by first page used, then object number, and deduplicated by object number. The getters return copies, so a caller cannot change the cache.
The walk follows /XObject entries into Form XObjects with a visited set, a depth cap of 32 (the existing maxFormWalkDepth) and a budget of 1,000,000 examined resource entries. A shared resources dictionary or form is walked and charged once, not once per page that uses it. Hitting the cap or the budget, or an entry that cannot be read, writes an error row and marks the affected pages incomplete. Nothing is dropped silently.
Entries are read from the image dictionary only and never decoded. readImageDictMetadata (size, colour space, filters, components, size estimate, warnings) and readImageDictFacts (adds /SMask, /Decode, the Adobe APP14 marker and the sample-interpretation verdict) are shared with DescribeImage and renderImage, so the list, the preview and dump image read the dictionary one way. That changes the warning text: every prefix now uses the PDF key name (Width, Height, BitsPerComponent, ImageMask, ColorSpace metadata: ...; the preview used lower-case width, height, bitsPerComponent, and DescribeImage used imageMask, colorSpace), and the BitsPerComponent warning now comes before the ImageMask one. The CHANGELOG lists this under Changed. A DCT preview still resolves its colour space once: the DCT lookup runs before the shared read, which reuses its result. A NaN or infinite /Decode entry rejects the array, because JSON cannot carry those values.
Three new service wrappers with the usual panic recovery, and regenerated bindings.
frontend/src/components/ImagesPanel.tsx is the third left-rail destination (Cmd/Ctrl+3), registered as one entry in leftRailDestinations.ts. It has text rows only: reference, size, filters, colour space, page count, and badges for stencil mask, SMask, non-default Decode, APP14 and read warnings. It has two views, Flat and By page, and each view keeps its own expansion. Rows whose image is used on more than 16 pages fetch the full page list on expand. Clicking a row selects the image with iconHint: 'image', so the existing preview and its 64MB consent prompt run unchanged.
The "Show node in tree" context menu is moved out of PagesPanel into RowContextMenu.tsx, and the trigger logic (right-click, Shift+F10, the ContextMenu key) and container sizing into navigatorHooks.ts. Both panels use them and keep their own row lookup and menu label.
dump images [--json] <file> in cmd/cli/cmd_images.go, with the same facts as the panel. Plain output is a table with FLAGS, PAGES and ERROR columns. The help text and docs/cli-usage.md say inline images (BI/ID/EI) are not listed.
The left-rail destination is now per document tab. leftView moved from app state onto TabState, read through selectActiveLeftView. Picking Images on one tab leaves the other tabs where they were, and switching tabs brings back that tab's panel. A new tab starts on Structure. Cmd+G and "Show node in tree" change only their own tab. Collapsing the left pane stays app-wide and is still the only thing persisted. The Images view (Flat / By page) and sort were already kept per tab.
Opening a file that is already open now replaces that tab's content with the fresh parse instead of discarding it. The tab keeps its position and its rail destination. Selection, history and panel data reset because the tab id is new. App.jsx now closes the old tab id in the backend and keeps the new one.
PagesPanel showed "Loading pages..." next to the error banner when the index fetch failed with an empty message. The loading line now uses the same check as the banner. ImagesPanel had the same code and gets the same fix.
Considerations
Thumbnails in the rows were left out. The preview path renders up to 2048px, and shrinking the output saves the re-encode but not the decode: a 4000x3000 JPEG is about 36MB of RGBA before scaling, so 200 rows would be 200 full decodes. That needs its own render path and consent wording.
"Used on N pages" counts pages whose resources declare the image, not pages whose content stream actually draws it. Answering the second means parsing every content stream, which is far more work than reading the resources.
Images reached only through annotation appearance streams, pattern resources or Type 3 glyphs are not listed, and an image's own /SMask target shows as a badge on its parent instead of a separate row.
Deduplication is by object number alone. A page that references 5 1 R after another page used 5 0 R lands on the same entry, which matches what the tree resolves.
How was this tested?
go vet ./... clean, golangci-lint run 0 issues, go test -race ./... green
tests/images-navigator is a new Go module that builds the CLI and runs dump images against ten PDFs generated at test time. They cover one image shared by 20 pages, inherited resources, forms two levels deep, a form cycle, a 40-form chain past the depth cap, entries that must be skipped, and every badge. It also checks each listed image against dump image --json --metadata for parity.
scripts/test-all.sh all suites green
npm run typecheck and npm run lint clean; npx vitest run 1306 passed across 82 files
New regression tests were run against the code with their fix removed and confirmed to fail
Manual: not done yet on a production build. Still to check: Cmd+3, a document with a logo on every page, the 64MB consent prompt on a large image, "Show node in tree" for an image two forms deep, and scrolling a document with 1000+ images
What could go wrong?
Re-opening an already-open file used to keep the old parse and land on Structure. It now shows the file as it is on disk and keeps the tab's panel. If a file changed on disk, the tab switches to the new content, and the selection is cleared.
The first open of the Images panel on a very large document walks every page's resources while holding the document lock. The budget bounds it, but the tree and detail panel wait for that walk to finish.
pdfcpu panics in its palette lookup on an Indexed image whose samples exceed hival. The index never decodes, so it is not affected, but clicking such an image reports a malformed-PDF error in the preview, and dump image exits 2 with "internal error". That is the same as before this change.
Two existing left-rail tests changed their expected values because the rail now has three entries.
Screenshots/videos (if appropriate)
To follow from a production build: the Flat view on a document with a shared logo, the By page view, and a row with badges and a warning tooltip.
Preserve resource errors in image index completeness
internal/pdfcore/imageindex.go:213
A page's own /Resources can fail to resolve before the image walk sees it. In pageindex.go, effectiveAttrs uses derefIn, which discards returned errors and supplies nil or inherited resources. fillLeaf records the diagnostic only in the numbered PageIndexEntry.Err; leaf.err stays nil. This loop skips that diagnostic, so the image index can report an empty, complete page after a failed resource read. Preserve actual resource-resolution errors in the cached page attributes, emit an image error row, and mark affected groups incomplete while still walking available resources. Keep successful null/dangling resolutions distinct from errors.
Avoid caching cycle-dependent resource walks
internal/pdfcore/imageindex.go:482
When childLow == pos, this caches a walk that encountered a cycle. For F -> G -> F with an image in G, walking X -> F on page 1 then G on page 2 reuses F at the same depth and reports G > Back > G > Im instead of G > Im. A depth-capped branch can likewise carry a cached incomplete flag onto a page whose traversal fits the cap. Do not memoise cycle-dependent walks, and propagate the dependency through their parent walks so an enclosing memo cannot reuse it either.
The reason will be displayed to describe this comment to others. Learn more.
Thanks, the backend walker held up well: I traced memo reuse across depths, cycle low propagation, the budget/use-limit stops and page tracking for error rows, and found no correctness issue.
Requesting changes mainly for the first three inline comments:
Re-opening an already-open file now replaces the tab and resets selection/history/caches, and a duplicate cold-start delivery triggers it too. This also seems out of scope for this PR.
The first image-index build holds pdfMu for the whole walk, which freezes the tab on large documents.
Page-list reads wait on pdfMu behind image decodes even after the index is built.
The rest are cleanups: duplicated metadata reads with mismatched warning text, a dead ref, stored derived state, an unpruned session map, an out-of-date comment, and a memo-reuse edge case.
The reason will be displayed to describe this comment to others. Learn more.
Re-opening an already-open path now re-parses it and swaps the tab's id. Before this PR the existing tab was just activated. The comment above says this is deliberate, but the cost is high:
Selection, Back/Forward history, the Pages/Images caches and tree expansion are all reset.
dispatchOpenedDocument sends CloseDocument for the old id before the reducer runs, so requests still in flight on that id fail with ErrDocumentNotFound.
The cold-start drain and the document:opened event can deliver the same path twice (the Wails goroutine race mentioned in this reducer). Each duplicate delivery re-parses the file and wipes the user's state.
This also looks outside the Images-navigator scope. Could the re-open change be dropped from this PR (keep activate-existing-tab) and handled separately, with a decision on whether re-open should mean "reload"?
The reason will be displayed to describe this comment to others. Learn more.
Keeping this one in the PR on purpose. It came out of manual testing of the per-tab rail: once each tab has its own rail view, re-opening a file that is already open should give you what is on disk now, not the parse from an hour ago, while the tab keeps its position and panel. The reload story builds on exactly this path (reload = re-open the same path, same tab slot), so it lands here rather than later.
On the duplicate delivery: I checked main.go. Each OS open goes through routeOpenPath once. Before the frontend is ready it is queued and only the drain delivers it, after that it opens right away and only document:opened delivers it, and every parse gets a fresh tab id. So one OS open never shows up on both paths. As a safeguard anyway, e931faf makes a repeated delivery of a tab id we already have only activate that tab: no reset, nothing closed. Requests still in flight on a replaced id are dropped by the live-tab guards in useTabCache / useLazyLoad, with no error shown (there is a test for the not-found case now).
The reason will be displayed to describe this comment to others. Learn more.
This comment is out of date: App.jsx no longer releases the replaced document (this PR removed the CloseDocument import there). The release now happens in lib/openedDocuments.ts (dispatchOpenedDocument), which EmptyState also calls. Someone following this pointer could add a second release and close the document twice.
The reason will be displayed to describe this comment to others. Learn more.
The first GetImageIndex call runs the whole resource walk (up to 1M entries / 4M uses, reading every image dict and APP14 marker) while holding pdfMu. That blocks every other pdfcpu-backed call on the tab. On a large document (e.g. 10k pages), pressing Cmd+3 freezes the Structure tree, the detail panel and the object source until the walk finishes.
Can the build run without pdfMu, take it in smaller pieces, or at least not hold it across the whole traversal?
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 6723190. The build now takes pdfMu one page at a time and lets go between pages, so tree expands, detail loads and previews on the same tab run during a long first build. Callers that arrive mid-build wait on that one build instead of starting another, and closing the document stops it before the next page with nothing cached. Lock order is now image cache mutex, then pdfMu, then the page cache mutex; documented in inspector.go and lazycache.go. Tests cover another call completing mid-build and close during the build.
The reason will be displayed to describe this comment to others. Learn more.
Once the image index is built, GetImagePages / GetImagePageGroups only read the cached walk, which never changes after the build. They still take pdfMu, though.
renderImage holds pdfMu for a whole decode (image.go:113, which can be a large JPEG). So if the user clicks an image row and then expands its capped page list, the expansion stalls until the decode finishes. These reads could skip pdfMu when imageIndex.isBuilt() is true.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 6723190. Once built, GetImageIndex, GetImagePages and GetImagePageGroups read the cached walk without pdfMu. There is a test that holds pdfMu and checks all three still return.
The reason will be displayed to describe this comment to others. Learn more.
A memoised walk marked cyclic is never reused at depth > 0, even when the cycle stays inside this form's own subtree (childLow >= pos). Because cyclic spreads to every ancestor, one self-referencing form turns off reuse for every form above it.
Pathological case: a form ladder where each level has two parents and the bottom form lists itself in its own /Resources (some producers do this). Every path is walked again, so the work doubles per level. Around 20 levels exhausts the 1M-entry budget, and the remaining pages show as not walked.
Edge case, but reuse looks safe whenever the cycle closes within the subtree.
The reason will be displayed to describe this comment to others. Learn more.
Good catch, confirmed and fixed in 6723190. The cyclic flag is replaced by a guard list: forms whose walk the depth cap cut, plus members of a cycle that closes at the memoised form. A memo is reused whenever none of its guard forms is on the current stack, so a cycle closing inside the subtree no longer turns reuse off for every ancestor. Your ladder at 14 levels now examines 59 entries instead of 98,301. While in there I also found a capped memo could be reused on a page whose walk fits the cap and mark it incomplete; that is fixed and tested too.
The reason will be displayed to describe this comment to others. Learn more.
This PR adds readImageDictMetadata / readImageDictFacts, but renderImage (~L170-200) still has its own copy of the Width/Height/BitsPerComponent/ImageMask/ColorSpace reads, and the two copies word their warnings differently. The same unreadable /BitsPerComponent shows as BitsPerComponent metadata: ... in the Images row tooltip and the dump images ERROR column, but as bitsPerComponent metadata: ... in the preview and dump image (likewise Width vs width). Could renderImage call readImageDictFacts so there's one source?
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 85d5219. renderImage now goes through readImageDictMetadata, so there is one set of reads and one wording (PDF key names: Width, BitsPerComponent, ...) in the preview, dump image, the Images rows and dump images.
The reason will be displayed to describe this comment to others. Learn more.
tabsRef is dead code. EmptyState is only mounted while activeTabId is null (App.jsx:353) and unmounts after the first OPEN_DOCUMENT, so tabsRef.current is always [] when dispatchOpenedDocument reads it. Only sessionOpenTabs in openedDocuments.ts does the dedup. The ref, its effect and the tabs read can go; pass [] instead.
The reason will be displayed to describe this comment to others. Learn more.
flatData / groupData are fully derived from entries, sort, fullPages and pagesErrors, but they're stored in the cache and rebuilt by hand in four places (loadPages success, loadPages error, handleToggle, setSort). Any future mutation that misses buildFlat leaves the rows out of date. A useMemo over those inputs would drop the stored state and the repeated rebuild calls.
The reason will be displayed to describe this comment to others. Learn more.
sessionOpenTabs is never pruned when a tab closes. Example: open A (tab1), close it (TabBar already sends CloseDocument(tab1)), open A again. This loop then sends CloseDocument(tab1) a second time, and the backend error is swallowed by .catch(() => {}). Harmless today, but it hides real close failures, and the map grows with every path opened in the session. Delete the entry on close?
The reason will be displayed to describe this comment to others. Learn more.
Fixed in e931faf. New closeDocumentTab in openedDocuments.ts dispatches the close, forgets the path entry and closes the backend document once; TabBar uses it for the close button and Cmd/Ctrl+W. A not-found close stays quiet, anything else is logged with console.warn.
Also handled the two items from the second Copilot pass that had no inline thread:
A page-tree /Resources entry that fails to resolve was dropped by the page walk, so the image list showed that page as complete with no images. The page walk now keeps that error on the cached page (Pages navigator rows unchanged), and the image index writes one error row naming the affected pages, marks them incomplete and still walks inherited resources (6723190).
The cyclic-memo wrong-path case (F -> G -> F) did not reproduce on the previous head, which already refused those memos below page level. It is kept as a regression test against the new reuse rule in 6723190.
The reason will be displayed to describe this comment to others. Learn more.
Requesting changes for the description/changelog mismatches, the extra colour-space lookup and the open-flow question below. Details are inline.
One more nit outside the diff hunks: frontend/src/components/LeftRail.tsx:52-53 still says Cmd/Ctrl+1..N works "with no document open". The rail view now lives on the active tab, so SELECT_LEFT_VIEW with no active tab changes no view. The comment describes behaviour that's gone.
The reason will be displayed to describe this comment to others. Learn more.
The description says DescribeImage output is unchanged, but its warning prefixes change here: imageMask metadata: becomes ImageMask metadata:, and on line 909 colorSpace metadata: becomes ColorSpace metadata:.
The preview (renderImage) changes more: width, height and bitsPerComponent are now capitalised, and the ImageMask warning now comes after BitsPerComponent instead of before. Anyone matching the warning field of dump image --json gets different strings.
If the shared wording is intended, please update the description and add a changelog line. Otherwise keep the old prefixes.
The reason will be displayed to describe this comment to others. Learn more.
The shared wording is intended: it is what makes the preview, dump image, the Images rows and dump images agree. Description updated (item 3 now lists the prefix changes and the new BitsPerComponent-before-ImageMask order), and there is a line under Changed in the CHANGELOG (29ae8d3).
The reason will be displayed to describe this comment to others. Learn more.
This drops the old guarantee that the colour space is looked up only once. readImageDictMetadata (line 171) now calls declaredComponents before the DCT block sets sd.CSComponents. So the reuse described in declaredComponents' doc comment never happens on this path, and every DCTDecode preview runs ColorSpaceComponents twice.
A colour space that faults now faults twice. Both are absorbed, so nothing crashes, but it's double the work and the invariant the removed comment stated is gone. Running the shared read after the DCT block, or passing the DCT result in, restores the single lookup.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in e42e160. The DCT lookup now runs before the shared metadata read, so the read picks up sd.CSComponents through declaredComponents and a DCT preview resolves the colour space once again.
The reason will be displayed to describe this comment to others. Learn more.
repeated only recognises the newest tab id for each path. Sequence:
Path P opens as tab B.
P is re-opened as C. The map now says P → C, and B's backend document is closed.
A duplicate document:opened for B arrives. B is not in tabs, and the map says C, so repeated is false.
This then releases C (the live document), records P → B and dispatches B. The reducer replaces C's tab with B, whose backend document is already closed, so every panel on that tab fails with "document not found".
Can a repeated delivery arrive after a newer open of the same path? If yes, an id that was already released should also count as repeated and be ignored. If no, please add a comment saying where repeated deliveries come from.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 3ee4f45. Every id released by a re-open or a tab close goes into a releasedTabIds set, and a delivery for one of those ids is dropped before anything is closed or dispatched. Your B/C sequence is a test now, plus the same case after a tab close. On where repeats come from: each backend open mints a new id and is delivered once (cold-start drain or document:opened, never both), so this only guards against an event delivered twice or out of order. The doc comment on dispatchOpenedDocument says that.
- A Close diff button in the Diff panel, in the toolbar and while a diff is computing or has failed. It returns the panel to "Compare with another PDF..." and moves focus there, so a document can be compared against a different file without switching away first. Rows in the right-hand diff pane are now clickable: a click selects that path in both panes. Each diff pane is a single tab stop; Up/Down, Home and End move the selection, Enter or Space selects, and Right/Left expand and collapse a row in the left pane. Any change of selection, by click, key or Prev/Next change, scrolls both panes to the selected row
- A Pages navigator on a new left rail. The rail sits at the window's left edge with labelled Structure and Pages items (Cmd/Ctrl+1 and Cmd/Ctrl+2, arrow keys, Home/End); clicking the active item collapses the left panel and clicking it again brings it back at its stored width. Pages lists every page as `1: Page [3 0 R]`, in document order: a row is the `/Page` dictionary and expands to its entries, and selecting it drives the object source and detail panels exactly as a tree click does. Right-click offers "Show node in tree". A jump field at the top takes a page number, and the header says when the page tree's leaf count disagrees with the root `/Count`
- An Images navigator, the third item on the left rail (Cmd/Ctrl+3). It lists every image XObject referenced from page resources once per object, however many pages use it: reference, size, filters, colour space, how many pages use it, and text badges for `mask`, `SMask`, a non-default `Decode`, `APP14 t=N` and dictionary warnings. Rows expand to the pages that use the image, and "By page" switches to a per-page grouping that shows the resource path (`Fm1 > Im0`) for each use. Clicking a row opens the existing image preview; right-click offers "Show node in tree". The list is built from the dictionaries alone, so no image is decoded to show it. Inline images (`BI`/`ID`/`EI`) are not listed
The reason will be displayed to describe this comment to others. Learn more.
Two user-visible behaviour changes aren't in the changelog:
Re-opening a file that is already open now replaces the tab's content (selection, history and panel state are cleared) instead of switching to the existing tab.
The left-rail view is now kept per tab.
Both are in the PR description (items 8–9 and "What could go wrong"). They belong under "Changed" too.
The reason will be displayed to describe this comment to others. Learn more.
Nit: this list of files is stale after this change. TabBar.tsx no longer imports pdfservice.js; it uses closeDocumentTab from lib/openedDocuments.ts. And lib/openedDocuments.ts is missing, though it's now where the open/close flow calls CloseDocument.
The reason will be displayed to describe this comment to others. Learn more.
Updated both consumer lists in 29ae8d3: TabBar.tsx dropped, lib/openedDocuments.ts (CloseDocument) and treeRows.tsx (GetChildren) added, App.jsx and PagesPanel.tsx trimmed to what they still import.
Also fixed the LeftRail.tsx:52-53 comment from the review summary (29ae8d3): Cmd/Ctrl+1..N now reads as selecting the destination on the active tab, and with no document open it only reopens a collapsed panel.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description and links
There was no way to see the images in a document without expanding pages one at a time in the Structure tree. A logo on every page shows up once per page there, so you could not tell whether a file shares one image object or carries 300 copies of it. The CLI had no command that lists images at all.
This adds an Images entry to the left rail, next to Structure and Pages. It lists every image XObject in the document once per object, with how many pages use it, and a By page view for "what is on page 47". The same list is available from the CLI as
dump images.Jira: US-1905
Technical changes
internal/pdfcore/imageindex.goaddsGetImageIndex,GetImagePagesandGetImagePageGroups. The index is built once per open document from the cached page walk the Pages navigator already uses, so inherited/Resourcesare resolved in one place. Entries are sorted by first page used, then object number, and deduplicated by object number. The getters return copies, so a caller cannot change the cache./XObjectentries into Form XObjects with a visited set, a depth cap of 32 (the existingmaxFormWalkDepth) and a budget of 1,000,000 examined resource entries. A shared resources dictionary or form is walked and charged once, not once per page that uses it. Hitting the cap or the budget, or an entry that cannot be read, writes an error row and marks the affected pagesincomplete. Nothing is dropped silently.readImageDictMetadata(size, colour space, filters, components, size estimate, warnings) andreadImageDictFacts(adds/SMask,/Decode, the Adobe APP14 marker and the sample-interpretation verdict) are shared withDescribeImageandrenderImage, so the list, the preview anddump imageread the dictionary one way. That changes the warning text: every prefix now uses the PDF key name (Width,Height,BitsPerComponent,ImageMask,ColorSpace metadata: ...; the preview used lower-casewidth,height,bitsPerComponent, andDescribeImageusedimageMask,colorSpace), and the BitsPerComponent warning now comes before the ImageMask one. The CHANGELOG lists this under Changed. A DCT preview still resolves its colour space once: the DCT lookup runs before the shared read, which reuses its result. A NaN or infinite/Decodeentry rejects the array, because JSON cannot carry those values.frontend/src/components/ImagesPanel.tsxis the third left-rail destination (Cmd/Ctrl+3), registered as one entry inleftRailDestinations.ts. It has text rows only: reference, size, filters, colour space, page count, and badges for stencil mask, SMask, non-default Decode, APP14 and read warnings. It has two views, Flat and By page, and each view keeps its own expansion. Rows whose image is used on more than 16 pages fetch the full page list on expand. Clicking a row selects the image withiconHint: 'image', so the existing preview and its 64MB consent prompt run unchanged.PagesPanelintoRowContextMenu.tsx, and the trigger logic (right-click, Shift+F10, the ContextMenu key) and container sizing intonavigatorHooks.ts. Both panels use them and keep their own row lookup and menu label.dump images [--json] <file>incmd/cli/cmd_images.go, with the same facts as the panel. Plain output is a table with FLAGS, PAGES and ERROR columns. The help text anddocs/cli-usage.mdsay inline images (BI/ID/EI) are not listed.leftViewmoved from app state ontoTabState, read throughselectActiveLeftView. Picking Images on one tab leaves the other tabs where they were, and switching tabs brings back that tab's panel. A new tab starts on Structure. Cmd+G and "Show node in tree" change only their own tab. Collapsing the left pane stays app-wide and is still the only thing persisted. The Images view (Flat / By page) and sort were already kept per tab.App.jsxnow closes the old tab id in the backend and keeps the new one.Considerations
Thumbnails in the rows were left out. The preview path renders up to 2048px, and shrinking the output saves the re-encode but not the decode: a 4000x3000 JPEG is about 36MB of RGBA before scaling, so 200 rows would be 200 full decodes. That needs its own render path and consent wording.
"Used on N pages" counts pages whose resources declare the image, not pages whose content stream actually draws it. Answering the second means parsing every content stream, which is far more work than reading the resources.
Images reached only through annotation appearance streams, pattern resources or Type 3 glyphs are not listed, and an image's own
/SMasktarget shows as a badge on its parent instead of a separate row.Deduplication is by object number alone. A page that references
5 1 Rafter another page used5 0 Rlands on the same entry, which matches what the tree resolves.How was this tested?
go vet ./...clean,golangci-lint run0 issues,go test -race ./...greentests/images-navigatoris a new Go module that builds the CLI and runsdump imagesagainst ten PDFs generated at test time. They cover one image shared by 20 pages, inherited resources, forms two levels deep, a form cycle, a 40-form chain past the depth cap, entries that must be skipped, and every badge. It also checks each listed image againstdump image --json --metadatafor parity.scripts/test-all.shall suites greennpm run typecheckandnpm run lintclean;npx vitest run1306 passed across 82 filesWhat could go wrong?
Re-opening an already-open file used to keep the old parse and land on Structure. It now shows the file as it is on disk and keeps the tab's panel. If a file changed on disk, the tab switches to the new content, and the selection is cleared.
The first open of the Images panel on a very large document walks every page's resources while holding the document lock. The budget bounds it, but the tree and detail panel wait for that walk to finish.
pdfcpu panics in its palette lookup on an Indexed image whose samples exceed
hival. The index never decodes, so it is not affected, but clicking such an image reports a malformed-PDF error in the preview, anddump imageexits 2 with "internal error". That is the same as before this change.Two existing left-rail tests changed their expected values because the rail now has three entries.
Screenshots/videos (if appropriate)
To follow from a production build: the Flat view on a document with a shared logo, the By page view, and a row with badges and a warning tooltip.
Checklist
devbranch, notmaster