Skip to content

v0.11.0

Choose a tag to compare

@silverstein silverstein released this 15 Aug 06:24
· 159 commits to master since this release
ff761ec

Most PDF tooling guesses. A borderless table comes back as a confidently wrong grid. A font that will not link comes back as silence. An encrypted file comes back as a stack trace telling you to supply the password you just supplied. None of it announces itself, which is the part that costs you.

PDF Tools abstains instead. When it cannot prove a structure from the document, it says so and stops. That is honest, and for two years it has also been unsatisfying, because the table is sitting right there on the page.

This release adds a third path. The extractor hands over its evidence, a model proposes a structure, and the document gets the final vote.

Read this first if you already use PDF Tools

Password-protected PDFs work now, for reading and for writing.

Older versions advertised a password parameter on sixteen tools that could not possibly use it. The library underneath ships no decryption at all, so the parameter was decoration, and when it failed the error suggested supplying the password you had already supplied. We deleted that promise first, then spent this release building the thing it had been promising.

Nearly every tool that opens a document now takes a password, including read_pdf_content, read_pdf_fields, read_pdf_pages, validate_pdf, extract_to_csv and convert_pdf_to_markdown. search_pdf_text is the one holdout, so decrypt to a copy if you need it.

All twelve tools that change a document work on protected files too. A write decrypts, makes its change, and puts the original protection back: same encryption, same permissions, same owner password you never had to tell us. If a document cannot be faithfully re-protected, the operation fails rather than writing a decrypted copy. There is no "remove the password" switch, deliberately.

One rule worth knowing. For a read, either password works. For a write, if the document's permissions deny what you are about to do, only the owner password authorises it. A user password proves you may open a document. It does not prove you may overrule its author.

If you run PDF Tools under a process supervisor, one exit code changed. Shutdown by signal now reports the signal: 129, 130, 143 for SIGHUP, SIGINT, SIGTERM. It used to exit 1 with a stack trace, so every orderly shutdown looked like a crash to whatever was watching.

Verified table extraction

The hardest thing in this business is a table with no lines around it. The geometry really is ambiguous, and any tool that guesses will eventually invent a row and hand it to you with total confidence.

Where the extractor abandons a table region, convert_pdf_to_markdown can now hand out a bounded evidence packet for it: the text items, the ruled and painted evidence, header hints, the page, the bounding box, the coordinate space, and a token bound to that exact document and that exact parse. A model reads the packet and proposes a structure. The new verify_table_proposal tool checks that proposal against the source, read only, and only a proposal that survives becomes Markdown.

It is a strict check. Sixteen typed conditions will refuse a proposal, among them dropping a text item, duplicating one, introducing one that was never in the region, straddling or contradicting a ruling that is actually on the page, ordering rows or columns in a way the geometry does not support, or presenting a token that does not match the bytes it was issued against.

The model proposes. The document confirms, item by item. That is what separates a table you can check from a table that merely looks right.

Opt in with emit_table_proposals. It defaults to false, the abstention behaviour is unchanged, and default output stays byte identical, so none of your existing conversions move.

Superscripts survive the conversion

Raised smaller glyph runs used to land flat on the baseline, so Shannon's Table I came out as sin2(t/2) and t2. They now convert to sin²(t/2), t²/2, e², t³ and t⁴. On that 55 page paper it changes 63 characters across 62 runs, and every other fixture in the repository is untouched.

Worth being straight about what this is. It reproduces the typography, and it does not recover meaning. A page raises an exponent and a footnote reference with identical geometry, so the same pass writes Nyquist¹ and Hartley² as superscripts too. 18 of those 62 runs are footnote markers, and that is the correct outcome rather than a defect. The tool output says so as well.

Documents that used to come back empty

Old TeX papers. Ghostscript writes glyph names like /#0b, and the library underneath only unescaped uppercase hex, so those fonts never linked and the text came out as nothing at all. On one astrophysics corpus this took attributable text from 7,019 to 100,108 occurrences. If you work with older academic PDFs, this is the change you will feel first.

The viewer also renders on locked down hosts now. The pdf.js worker is bundled in realm rather than fetched, so a host with a strict content security policy shows you the document instead of a blank panel.

Security and robustness

Decompression bombs are bounded. A compressed object stream gets size checked before the parser expands it, rather than after.

Hostile cross-reference widths are refused. A 1 KiB file declaring /W [1 1000000000 1] used to burn 198 seconds of CPU at flat memory, because those numbers went into loop bounds unchecked.

Decryption moved off the server thread. A lawful 14.3 MiB document spent 5.1 seconds inside uninterruptible WebAssembly on the same thread that answers everything else. It runs on its own worker now, with a deadline that can actually fire.

Permission checks stopped protecting nothing. read_pdf_fields would refuse an owner-locked document while read_pdf_content returned 33,173 characters of the same file. Permissions govern writes now, consistently, and the read tools behave the same way as each other.

Rasterization inside embedded hosts

Still off by default on every platform, unchanged.

What changed is that opting in with PDF_TOOLS_EMBEDDED_NATIVE_CANVAS=1 is now backed by a crash latch. If a load starts and the host never comes back, the next start refuses instead of crashing again, and tells you why and how to recover (=force). We verified this on a real Windows machine by hard killing the process mid render: the marker survived, and the restart refused with the recovery instructions.

Refusals that tell you what to do

Three tools reported user-correctable situations as internal faults. compare_pdfs was the worst offender, blaming itself for something you could have fixed in seconds if it had said so.

The accessibility inspector now says AcroForm when what it measured was AcroForm. "This PDF has no form fields" and "this PDF has no AcroForm fields" are different sentences, and only one of them was true.

Distribution

PDF Tools installs as an Agent Plugin, and the published plugin is checked against the source automatically and republished when anything that ships inside it changes. A stale published plugin used to sit there unnoticed.

The plugin now starts under NVM. When a host launches it from a GUI session it gets a minimal PATH and no shell startup files, so an NVM-managed Node was invisible and the server never started. The plugin ships its own launcher that looks on PATH first, then asks NVM for your default version, checking NVM_BIN, NVM_DIR, ~/.nvm and ~/.config/nvm. It sources nvm.sh alone, never your shell config, so nothing in bashrc or zshrc can reach the server. With no usable Node it exits 127 and prints what to do, rather than dying without output.

Hosts that display an icon and description for a plugin now get real ones.

Every component in the shipped bill of materials states its licence, including the native code, which the old bill omitted entirely because it was generated from the npm lockfile and the native binaries are not in the lockfile.

If you are building an agent on this

A few contracts, because they are the reason this is pleasant to build against.

Refusals are typed and specific. A refused table proposal names which of sixteen conditions it violated. A refused write names the permission bit that denied it. You can branch on these.

Where evidence runs out, the output says so with a typed reason such as TABLE_TOPOLOGY_UNKNOWN, TEXT_INTEGRITY_SUSPECT or PDF_RENDERER_UNAVAILABLE. A gap you can see is worth more to an agent than a guess you cannot detect.

Observations are bound to the bytes they came from. Results carry the source SHA-256, and a table proposal token binds a proposal to one document and one parse, so it cannot be replayed against a different file.

URLs, destinations and actions found inside a PDF are reported and never opened.

Everything runs locally. Filesystem work and rasterization happen on your machine, and there is no PDF service behind any of it.

The filesystem boundary is the one you configure. There are no fallback directories, and a read-only tool will tell you which folders are reachable and which configuration layer supplied them.

Install the .mcpb for Claude Desktop, the .zip for MCP clients like Cursor, or the Agent Plugin for hosts that speak that standard. Issues and pull requests are welcome. The test suite is the interesting part of the repository, and adversarial review of it has found more real defects than review of the code has.

Verifying what you downloaded

pdf-toolkit-mcp.mcpb  sha256  24179fb68790014407d3af34bb1da68b15cfe31907f7ff733df45d013f3e2dd4
pdf-toolkit-mcp.zip   sha256  bd3e0392769cc204a0dc1b2e5a80745d956d4f9b515e4d315dedc431da4f6b72

Install the .mcpb in Claude Desktop by dragging it onto Settings, Extensions.