13. Changelog
What changed in each release, and what it may break for you.
The version is still 0.x, so a minor bump is allowed to break something. When
one does, the entry says exactly what and how to adapt. Nothing here changes
quietly.
0.19.1
Section titled “0.19.1”- A script could abort the process instead of being rejected. Nesting in
the source became nesting on the call stack, so about two thousand brackets
overflowed the stack and the run died with SIGABRT — no exit code 3, no
message, nothing catchable. Six shapes did it:
((((…)))),!!!!…,----…, nested arrays, nested blocks, and"#{"#{…}"}". The parser now counts its depth and refuses past 128 levels, which is far more than any script written by a person and far below where the stack runs out. It mattered most for the one command you would run first on a script you did not write:pdfl lintdied rather than reporting. - A package could name a file outside the folder it installs into. The
check refused
..and a leading/, but notC:/Windows/evil.dllor\\server\share\x— neither contains either, and both are absolute on Windows, where joining one to the install folder discards the folder. Every component of every path must now be a plain name, tested by rules that do not change with the platform doing the unpacking. - A package could exhaust memory while being read. Entries were read whole into memory before the manifest was consulted — including entries the manifest never mentions — so a 299 KB file of compressed zeros took 380 MB, and a larger one took the machine. Unpacking now stops at 64 MB, far above any real package of scripts and lookup tables.
--pages 1-20000000allocated the page numbers before a document was even opened — 214 MB to then say nothing. Ranges over a million pages are refused.
Worth knowing
Section titled “Worth knowing”- The fixes above put ceilings where there were none, so a few things that used
to be accepted now are not: a script nesting deeper than 128 levels, a package
expanding past 64 MB, a package naming a file with
:or\in it, and a page range over a million. Each is far outside what a real script, package or document contains — but none of them used to fail, so they are written down rather than left to be discovered.
0.19.0
Section titled “0.19.0”Breaks
Section titled “Breaks”The viewer’s modes are gone. Flip, Fade, the Blend slider and
the Differences slider, along with the space and d keys that drove
them, are no longer there.
Only the generated viewer is affected — no report, exit code or flag changes, so nothing reading
pdfloutput notices. Three panes answer what those modes were for: the two files are on screen at once rather than one at a time. If you have written instructions of your own that name a mode or a key, they need a pass.
Changed
Section titled “Changed”- The
pixelcompareviewer is three panes side by side — the original, the new file, and both with the differences painted over them — instead of one pane and a choice of modes. The two references stay put while the third wipes, so you can see what the wipe is cutting between. - Every pane carries the same pair of bars — one upright, one flat — in the same place, and all six move together. They follow the pointer: nothing to grab, nothing to aim at, the crossing is wherever the mouse is. In the difference pane the bars cut, so the new file shows to the right of the upright one and below the flat one and their crossing is the corner of what is revealed; in the other two they are rulers on the same column and the same line of the page. The flat bar starts at the top, which leaves the upright one a plain full-height wipe until the pointer enters a pane.
- The wheel zooms all three panes together, up to 8×, around the point under the pointer, and stops at the fitted page on the way out. Dragging with either mouse button moves the page itself, in all three at once and at the speed of the pointer whatever the zoom. The bars keep their weight at any zoom. Reset view puts the zoom, the pan and the bars back where they started and is disabled while there is nothing to undo; moving to another page does the same, since a zoom belongs to the page it was set on.
- The panes are sized against the window: the whole comparison is on screen without scrolling, at any window shape, and each keeps the page’s own proportions. Where the two files disagree about a page’s size, each is shown whole inside the shared box rather than stretched to fill it.
- It opens on the pages that differ, with All to put the rest back. On a long document those pages are the reason it was opened.
0.18.0
Section titled “0.18.0”Breaks
Section titled “Breaks”The identifier of a loading finding changes. It is derived from the
message, and the message no longer carries the line breaks pdfium put in it.
Only findings from a document that could not be read are affected — no check of your own produces one. If a baseline approves such an identifier, it needs to be recorded again. This is what makes the release a minor rather than a patch: an identifier is what a baseline is built on, so it does not change quietly.
pdfl packrecorded a nested file with the separator of the machine that built the package — on Windows,data\batches.csv. A package is built on one machine and installed on another, so anything but/is a package that installs and then cannot find its own files; our own verification could not find them either. Packages built on Linux and macOS were never affected.--outputand--output-filewere ignored when the PDF could not be read.run,fixandcompareprinted JSON to stdout whatever was asked for, so a pipeline that asked for JUnit in a file got no file at all and a report on a stream it was not reading — a corrupt input looked like a run that never happened, while the exit code correctly said10. The failure report now goes out through the same path as every other report.- A pdfium error reached the report as a pretty-printed Rust enum spread over three lines. A diagnostic is one field of a CSV row and one XML attribute, so it is now folded onto a single line.
0.17.0
Section titled “0.17.0”-
pdfl pixelcompareshows a progress bar for each stage — rasterising either file, comparing, writing the viewer. It is drawn only when stderr is a terminal, because the bar overwrites its own line and a log file has no cursor to move; redirected, it stays silent.--quietsilences it anywhere. -
--jobs <n>onpdfl pixelcomparecompares that many pages at a time, and defaults to one per CPU: 41 pages at 150 dpi go from 3.6s to 1.2s. Only the comparison is parallel — pdfium serialises every call behind one global lock, so rasterising cannot be, which is why the gain is threefold rather than eightfold. The report is unaffected by the value: pages are folded back in page order, so the diagnostics, their order and their fingerprints are identical, and so are the viewer’s files.This defaults to every CPU while
testandwatchdefault to--jobs 1. There a job is a child process holding its own document; here the pages are already in memory and the threads share them.
0.16.0
Section titled “0.16.0”pdfl pixelcomparecompares two PDFs by what they look like rather than by their text, page by page, and reports the share of pixels that differ. It aligns a page that only moved before comparing it, so a one-pixel offset does not bury the change that matters.--viewer <folder>onpixelcomparewrites a self-contained application — no CDN, no bundler, no server — to wipe, flip or fade between the two files with the differences painted in place: red for ink that is gone, green for ink that is new, blue for the same weight in another colour. The page strip filters to Changed only, and the arrow keys follow the filter — on a long document, paging past the pages that did not move is the slow part.
0.15.0
Section titled “0.15.0”if/else if/else, as an expression: its value is the last expression of whichever branch ran, the same rule a function already follows. So it serves both as a value (const LIMIT = if coated { 300 } else { 260 }) and as a guard around statements, with no second syntax. A branch that does not run yieldsnull, and each branch has its own scope — assigning to a variable that already exists outside still updates that one.
0.14.0
Section titled “0.14.0”--varnow reachespdfl testandpdfl watch, not justpdfl run. Neither forwarded it to the children they spawn, so a script readingvars.*could not be tested or watched at all — every case or file failed with “was not provided”, regardless of its content.
0.13.0
Section titled “0.13.0”Breaks
Section titled “Breaks”pdfl packno longer packages spreadsheets (.xlsx,.xls,.ods), and names the file it left behind. Nodata::function can open one, so a package carrying it installed cleanly and then failed at the first lookup. If you packaged a spreadsheet, export it to.csvor.jsonfirst.
--tags TAGonrunfilters which checks execute. Repeatable; a check runs when it carries any of the tags given.--jsononinspectandlint, and--output jsonondoc. Every subcommand can now be read by a program.--output sarifand--output junit, wherever a report format is chosen —run,compare,watchandfix. SARIF is what GitHub code scanning reads; JUnit is what the test panel of any CI reads.pdfl completions <shell>prints a completion script for bash, zsh, fish, elvish or powershell.--quieton every command silences progress and confirmations on stderr. Errors still appear, andprint()is untouched — that is the script’s own output, and dropping it would change what a script does.data::load_datasetanddata::lookup_valueread.jsonas well as.csv: an array of arrays, or an array of objects whose first object names the columns in the order the file writes them.pdfl test <script>runs a script against a folder of PDFs and compares each report to the one recorded beside it, so a profile that starts finding something different fails a test instead of surprising someone downstream.--updaterecords the expected reports.--jobs <n>onpdfl testruns that many cases at once, each as its own process. Eight 41-page files: 8.9s at--jobs 1, 1.1s at--jobs 8. The default stays1, since each job holds a document in memory;--jobs 0gives one per CPU.--jobs <n>onpdfl watchtoo: files are validated by child processes, so a batch pass scales the same way (9.5s to 1.2s on eight 41-page files). The report written is identical whatever--jobssays.--eventsonpdfl watchwaits on filesystem notifications instead of a timer. Opt-in, not the default: inotify on an NFS or SMB mount reports only what the local machine writes, so a network hot folder would go quiet. If the watcher cannot be created, watch says so and falls back to the timer.--journal <file>onpdfl watch: an append-only record of what was validated, one JSON object per file. Re-running with the same journal skips the files it covers — a batch interrupted at four thousand of five thousand finishes the thousand that are left — while still reporting their verdicts, so a resumed batch never claims a folder is clean.--timeout <s>onpdfl watchkills a file’s analysis past that many seconds and reports it as rejected — one finding,check_name: "timeout"— instead of leaving the batch stuck. Recursion in a.pdflscript is already depth-limited, so this is for what a script cannot cause: pdfium looping or blocking on a malformed or adversarial PDF.
Worth knowing
Section titled “Worth knowing”- A
--tagsvalue that no check carries is an error, not an empty pass. A pipeline filtering on a misspelled tag would otherwise validate nothing and report a clean file. - A
rulecarries no tags, so--tagsskips it — the same answer an untagged check gets. - The JSON report gained
checks_run, the checks and rules that ran. It does not bumpschema_version, because a reader that ignores unknown fields survives it. JUnit needs it: the diagnostics only name the checks that found something, and a clean run reported as zero tests is a run a CI thinks never happened.
pdfl watchnow wakes when the freshest file has settled, instead of up to one full interval later. With--debounce 3000, a file that arrives is reported about 3s later rather than up to 6s.
0.12.0
Section titled “0.12.0”- Scripts take values from the command line:
--var name=value, read asvars.name. A missing one names the flag that would supply it instead of resolving to nothing. - Four worked examples of comparing two documents with
visual:::proof.pdfl,reprint.pdfl,scope.pdflandintake.pdfl.
Breaks
Section titled “Breaks”Nothing. A script that never mentions vars behaves exactly as before.
0.11.0
Section titled “0.11.0”Breaks
Section titled “Breaks”Diagnostic identifiers changed shape. They were PDFL-001, a counter
within the run; they are now derived from the finding itself, like
PDFL-093751a2.
Anything matching
PDFL-\d+stops matching. In exchange, an identifier now survives a check being inserted above it, which is what makes an approved baseline possible to keep.
An unreadable input exits 10 instead of 2. A corrupt file and a rejected
file used to be indistinguishable to a pipeline.
If your CI treats
2as “this file was rejected”, it will now see10when the file was never judged at all. Findings still use0,1and2; a script syntax error still uses3.
- A check can declare the severity of its findings:
check "..." severity: warning { ... }—error(the default),warningorinfo. This is what gives--fail-on warningsomething to act on. - The JSON report opens with
schema_version, so a consumer can tell which shape it is reading. It bumps only when a reader of the previous output would break; adding a field does not bump it.
0.10.1
Section titled “0.10.1”- The PDF report was partly in Portuguese: the section header read
Diagnósticos, and every diagnostic carried(linha N). Both are English now, which is what the documentation always promised.
0.10.0
Section titled “0.10.0”Breaks
Section titled “Breaks”Release targets renamed from x64 to amd64, so every asset name changed.
Portable archives are no longer published, except one for Linux amd64.
Anything downloading
pdfl-<version>-linux-x64.tar.gz, or any portable archive other than Linux amd64, has to change. Install from the.debin CI — the recipes in this documentation were updated to do that — or use the Linux amd64 tarball where installing is not an option.
- Two documentation gaps found by auditing the docs against the source:
text::detect_personal_dataandtext::detect_piiaccept an optional string that was not documented, andfix::reorder_pageswas written two different ways across languages.
- Installers for every platform:
.debfor Linux,.dmgfor macOS,-setup.exeand.msifor Windows. - macOS Intel builds, cross-compiled from the Apple Silicon runner.
- The Windows installer was built with paths that resolved against the wrong directory, so it never produced a file.
- Release packages carried pdfium’s C headers and build files, which only matter to someone compiling against pdfium. About 550 KB per package.
- Windows x64 joins the released platforms.
The bundled
pdfium.dlllives inpdfium\bin, notpdfium\lib. If you packagepdflyourself, keep the layout as shipped: the binary looks for the library next to itself.
Breaks
Section titled “Breaks”Release assets carry the version in their name, as
pdfl-<version>-<target>.tar.gz, and the directory inside carries it too.
.../releases/latest/download/<name>no longer resolves, because that endpoint needs an exact file name. Download with a pattern instead:gh release download --pattern 'pdfl-*-linux-amd64.*'.
- The codebase, the README and the examples are in English. The documentation remains in seven languages.
v0.6.1
Section titled “v0.6.1”First public release. The language, the interpreter and ten CLI commands, with documentation in seven languages.