Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

All notable changes to UniDoc PDF Debugger are recorded here. Format follows Keep a Changelog with an added Refactored section; versions use semantic versioning.

## [Unreleased]
## [0.5.0] - 2026-10-14

Left-rail navigators for structure, pages and images, an image preview, update notices in the app and the CLI, and a CLI that says what it checked and rejects invocations it would have misread. Moves the app runtime to Wails v3 beta.

### Added

Expand All @@ -14,9 +16,17 @@ All notable changes to UniDoc PDF Debugger are recorded here. Format follows Kee
- The CLI now says when a newer release exists: a small ASCII box at the end of stderr on every interactive run while a newer release is known. It only appears when stdout and stderr are both terminals, and never under `CI`, `--json`, the other machine formats, or when `PDFDEBUG_NO_UPDATE_CHECK` or `NO_UPDATE_NOTIFIER` is set; it never changes an exit code. In a terminal `--version` checks live and says whether an update is available, the build is current, or the check failed; piped, redirected or under `CI` it prints only the version line and makes no request. The answer comes from a per-user cache directory (`pdfdebug/` under the platform cache directory) where the CLI and the desktop app each keep their own record, `updatecheck-cli.json` and `updatecheck-app.json`, so either works alone. The CLI also reads the app's record, so opening the app keeps the CLI current for 24 hours; the app's launch check skips the network only when its own check succeeded in the last 24 hours and found nothing newer, and "Check for Updates..." still goes live. An ordinary command only refreshes a stale cache in an interactive session, with one request bounded at 1.5 seconds
- Image sample interpretation: `dump image` and the GUI Image Metadata panel now report the `/Decode` array, `/ImageMask`, the image `/SMask` reference and the Adobe APP14 transform, plus one verdict joining the array and the marker into a single answer about whether the samples are read inverted. The two switches live in different layers - `/Decode` in the PDF, the Adobe record inside the JPEG - and the outcome depends on the combination, which is why there is one verdict rather than two rows. An Adobe APP14 CMYK JPEG stores its channels inverted and poppler and mupdf do not undo that in the codec, so on a four-component stream a present record with no `/Decode` reads as inverted and `/Decode [1 0 1 0 1 0 1 0]` is what compensates. The marker question is a four-component one, so below four components the verdict comes from `/Decode` alone. The marker outcome is reported either way - `absent`, `unparseable` and `not-examined` get a row of their own, not only `present` with its transform - and the marker walk reads no pixel: it hops segment to segment and stops at the Adobe record, or at SOS when there is none. `dump image` also prints the stored and decoded sizes it already knew and had been dropping
- The object tree now shows what a dictionary entry says, not just its type: `dump tree` rows read `RowSpan number = 2`, `--json` gains an additive `value` key on every scalar leaf, array elements included, and the GUI tree row renders the value between the label and the `/Key` suffix. A `valueRaw` key carries the byte-exact stored form whenever it says something `value` does not - every hex literal, and every literal carrying a PDF escape. It is omitted only where the stored form is `value` itself or `value` wrapped in `()`, so its absence means the display form is the form on disk, not that decoding was a no-op
- Image preview: a downsampled preview crosses to the GUI while the metadata still describes the full image. An image whose decode would be expensive says so and waits for consent instead of being refused, the full-resolution image can always be saved, and a large image loads without freezing the panel and can be cancelled. The metadata shows the stored and decoded sizes, so a large decode estimate next to a small saved file reads as the compressed-versus-decoded gap it is
- An in-app update notification: a quiet badge in the title bar when a newer GitHub release exists, a changelog dialog grouped by version, and a download of the right asset for the running platform, checked against the release's `SHA256SUMS.txt` before it is kept. Help > "Check for Updates..." checks on demand, and automatic checks can be turned off. The app never installs an update itself, and untagged development builds skip the check
- Cmd/Ctrl+F find in the Object and XREF tabs, scoped to the active tab, alongside the existing Plain Text find. Looking something up across the whole document stays in the Cmd/Ctrl+K palette

### Changed

- The macOS DMG mounts with the app's own volume icon and a drag-to-Applications layout
- About opens an in-app dialog with the full version string, prerelease suffix included, and a working unidoc.io link, instead of the native macOS panel. On a clean profile the main window now opens centred
- `dump stream --page N` on a page whose `/Contents` is an array decodes every stream in it, joined with newlines, as one program. It used to decode only the first and present that as the page's content
- Document metadata (`/Info` Title, Author, Subject, Keywords, Creator, Producer) and attachment names (`/UF`, `/F`) stored as UTF-16BE now read as text in the CLI and GUI instead of hex digits

- `validate` now says which rules ran. Every run prints the profile's scope sentence and a `Rules checked (N)` block with each rule's id, spec clause, outcome (`0 found`, `N found` or `not evaluated`) and what it checks, including how far the check reaches. A clean run used to print `no structural problems found`, which on `testdata/tagged.pdf` under `pdfua-1-structural` sat next to a veraPDF UA-1 FAIL; it now reads `none of the N rules checked found a problem`, N being the number of rules the profile ran, and the scope sentence says the profile is a structural subset of PDF/UA-1 limited to its catalog-level rules, and that marked content, the structure tree's contents, alternate text, fonts and the rest of the catalog are not examined. `--json` gains two keys after `disclaimer`: `scope` and `rules` (`ruleId`, `specRef`, `severity`, `checks`, `evaluated`, `findings`); the existing keys are unchanged. An encrypted file under `pdfa-1b` lists only `no-encryption`, since the other rules never ran. `pdfdebug --help` and `pdfdebug validate --help` list each profile's rules, and the Validate tab shows the same scope and rules list, with its profiles labelled "(structural subset)". A rule that cannot read the catalog or a stream it needs now reports `not evaluated` instead of passing. Under `pdfa-1b` a file whose page content stream or form XObject cannot be decoded therefore exits 1, with `output-intent` not evaluated, where it used to exit 0, and the plain-text output says why after the summary line; `pdfdebug --help` and the CLI guide now list this as a cause of exit 1. All other exit codes are unchanged
- The left rail now remembers its destination per document tab: picking Images on one tab leaves the other tabs on their own panel, and switching tabs brings back each tab's panel. New tabs start on Structure; collapsing the left panel still applies to all tabs
- Opening a file that is already open in a tab now reloads it into that tab instead of only switching to it. The tab keeps its place and its left-rail panel; the tree, selection, Back/Forward history and panel state reset to the file as it is on disk
Expand All @@ -34,6 +44,11 @@ All notable changes to UniDoc PDF Debugger are recorded here. Format follows Kee

### Fixed

- Opening a large PDF no longer freezes the window. The XREF table was fetched and every row rendered on each open, even with another tab showing; it now loads when its tab is first shown and renders only the visible rows. The tree no longer rebuilds its node model on every render
- `diff` no longer reports two files as structurally identical when they differ below its 32-level depth limit. A node cut off at the limit is marked truncated, the summary counts those subtrees, and the GUI diff view says the comparison is incomplete instead of showing "identical"
- A small compressed stream can no longer inflate past the extraction ceiling in `dump embedded` and `dump image`: the stream is measured before anything is allocated. Filter pipelines that cannot be measured (JBIG2, four-component DCT) are refused for embedded files rather than decoded without a limit, and CCITTFax output is now measured. Three malformed shapes that used to crash or hang (a pipeline led by a filter pdfcpu stops at, predictor parameters giving a zero or absurd row width, and a RunLength run promising more bytes than remain) are refused, and one unreadable image colour space no longer ends the whole command
- `dump stream --json` and `--ops`: an operand with a leading `+` (`+5`, `+.5`) is a number rather than an operator, `--ops` no longer emits empty `{"op":""}` records for comment lines, and an inline image's data no longer keeps the `\r` from a CRLF before `EI`
- The content stream and object source views no longer hide their last characters under the macOS overlay scrollbar
- Closing the comparison document, or switching documents, while a structural diff is still computing now stops the diff. The walk held both documents' locks to the end, so the tree and detail panel of the open document stayed blocked until a large diff finished. Switching away from the Diff tab and back while a diff is computing no longer starts a second diff behind the first; the running one finishes and its result is shown
- `dump tree --page N`, `dump stream --page N` and `dump page --info N` now number pages the way `dump pages` and viewers do. A `/Type /Page` that also carries `/Kids` was unreachable (`page 1 not found`) or lost its number to its kid; it now resolves as the page. `validate` scans that page's content for device colour too. `dump tree --page` past the last page reports `page N not found` instead of `malformed PDF: pdfcpu: page not found`; `dump page --info` and `dump stream --page` keep their `page N out of range: document has M pages` check. `dump page --info` on a page whose `/MediaBox`, `/CropBox`, `/Rotate` or `/Resources` cannot be read names the attribute instead of calling the page not found, and a real-number `/Rotate` reads the same rounded value in `dump pages` as in `dump page`
- `pdfdebug --help` and each command's usage line now list the same flags, every flag the command accepts. Both now come from one synopsis per command. They were written separately and had drifted: most help lines left out `--pretty`, the `dump tree` help line had no `--page` and its usage line no `--resolve`, the `dump font` help line had no `--glyphs`, the `dump image`, `dump source`, `dump reverserefs` and `dump page` usage lines had no `--json`, and `dump object`, `dump stream` and `dump metadata` listed `--pretty` in neither place. The validate and diff exit-code headers now say `dump` exits 0, 1 or 2, with 1 for a usage error, and their exit 2 lines say usage errors exit 2 too, as they already did. No flag or exit code changed
Expand Down Expand Up @@ -206,6 +221,7 @@ Initial public release. GUI and CLI PDF debugger for macOS arm64, Windows amd64,
- `LICENSE` (Apache 2.0) and `NOTICE` bundled in every archive on every platform
- `SHA256SUMS.txt` published alongside each release

[0.5.0]: https://github.com/unidoc/pdfdebug/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/unidoc/pdfdebug/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/unidoc/pdfdebug/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/unidoc/pdfdebug/compare/v0.2.0...v0.3.0
Expand Down
Loading