pdfops
Fast PDF operations for AI agents. A single Rust binary with no native PDF libraries to install, JSON in and out.
- CLI: every command prints a single JSON document.
- MCP server:
pdfops mcpexposes every command as a tool over stdio. - Function calling:
pdfops toolsprints tool definitions with JSON Schemas. - Library: the same commands as Rust functions.
Install
Download a prebuilt binary from the latest release
and put pdfops on your PATH. Archives are named by platform:
| Platform | Archive |
|---|---|
| Linux x86-64 | pdfops-<version>-x86_64-unknown-linux-gnu.tar.gz |
| Linux ARM64 | pdfops-<version>-aarch64-unknown-linux-gnu.tar.gz |
| macOS Apple silicon | pdfops-<version>-aarch64-apple-darwin.tar.gz |
| macOS Intel | pdfops-<version>-x86_64-apple-darwin.tar.gz |
| Windows x86-64 | pdfops-<version>-x86_64-pc-windows-msvc.zip |
Or build from source with Rust 1.92 or newer:
OCR additionally needs the tesseract program; nothing else does.
Language data is fetched on request, without administrator rights:
Commands
| Command | What it does |
|---|---|
info |
Page count, page size, metadata, encryption, outline and form summary |
text |
Extract text page by page, with a character budget and optional OCR fallback |
search |
Find text or a regex, returns pages and snippets |
layout |
Text with positions: bounding box, font and size of every line or word |
tables |
Tables as rows of cells, Markdown or CSV |
ocr |
Recognise text on scanned pages |
ocr-langs / ocr-install |
Check the OCR setup, download language data, install tesseract |
outline |
Bookmarks with target pages |
render |
Pages to PNG, for charts and layout |
images |
Extract the images drawn on pages |
create |
New PDF from Markdown |
merge |
Concatenate PDFs |
pages |
Keep, reorder, duplicate or delete pages |
split |
Split by page count or by ranges |
rotate |
Rotate pages by multiples of 90 degrees |
stamp |
Text watermark, header, footer or page numbers; an image such as a signature; a QR code |
redact |
Remove text, images and drawings in given areas or matching given text, then verify |
replace |
Replace text in place, in the document's own font where possible |
set-meta |
Set title, author, subject, keywords, creator |
compress |
Shrink: lossless by default, optionally re-encoding and downscaling images |
encrypt / decrypt |
AES-256 passwords and permissions |
forms / fill |
List and fill form fields |
Run pdfops <command> --help for the options of each.
Usage
}
}
}
}
}
Positions are in points with the origin at the top-left corner of the page as displayed, y growing
downwards. layout reports them, stamp --x/--y and redact --rect accept them, and a pixel of
render --dpi 72 is exactly one point.
Page specs are 1-based and comma separated: 3, 2-5, 7- (to the end), -4 (from the start),
5-2 (descending), last, odd, even, all.
Commands that write a file take -o; it may be the input file, since output goes through a
temporary file. Errors are printed to stderr as {"error": "..."} with exit status 1. Add
--pretty to indent the JSON.
MCP server
For Claude Code: claude mcp add pdfops -- pdfops mcp.
Tools are named pdf_info, pdf_text, pdf_set_meta and so on, and take the same arguments as
the CLI. Relative paths resolve against the server's working directory.
Function calling without MCP
pdfops tools prints [{"name", "description", "inputSchema"}] for every command. Pass these to
any function calling API, then run the call through the CLI or the library.
Library
let result = call?;
Typed entry points live in pdfops::ops, for example pdfops::ops::read::text(TextArgs { .. }).
Performance
Best of 5 runs on a 357 page, 1.4 MB manual, 4 core Intel i5-4460, against poppler 26 and qpdf 12:
| Task | pdfops | Reference tool |
|---|---|---|
| Info | 7 ms | pdfinfo 16 ms |
| Text, all pages | 466 ms | pdftotext 565 ms |
| Keep 10 pages | 32 ms | qpdf 164 ms |
| Split into single pages | 64 ms | pdfseparate over 40 s |
| Merge three copies | 164 ms | qpdf 314 ms |
| Render 20 pages at 150 dpi | 214 ms | pdftoppm 6125 ms |
info, render, images and ocr read objects on demand, so opening a file costs a few
milliseconds whatever its size. Text extraction, rendering, splitting, image export and OCR run on
all cores. Page operations copy only the objects the selected pages reach, so output size and time
follow the selection, not the source.
Behaviour worth knowing
- Text outside Latin-1.
stamp,fill,replaceandcreateembed a subset of a font that has the glyphs: the one given withstamp --font, otherwise one found on the system. Latin-1 text uses the built-in Helvetica and embeds nothing. Text is placed glyph by glyph: scripts that need shaping or right-to-left layout (Arabic, Hebrew, Indic) will not come out right. - Redaction deletes what is under the areas from the page content: glyphs, image pixels (including scanned pages), drawings lying wholly inside an area, and annotations with the form values they show. Text around it does not move. The result is read back by a second, independent interpreter, and nothing is written unless every area is empty. When a page is built in a way that cannot be rewritten with certainty (inline images, JPEG 2000 or CMYK JPEG under an area), the command fails instead of guessing. It does not rewrite document metadata, bookmarks or attached files, and it removes the accessibility structure tree, which can repeat page text.
- Replace writes the new text with the codes the document's own font already uses for those
characters on that page, so style is kept exactly. If the font (usually a subset) lacks a needed
glyph, another font writes just those words, at the same size, position and colour, and the result
says so. Lines are not re-flowed: a longer replacement runs into what follows, and
overflow_ptreports by how much. Text inside form fields and annotations is not edited. - Tables drawn with ruling lines are read cell by cell and are reliable. Tables without lines
are inferred from column alignment (
detected_by: alignment) and deserve a look before trusting. - Create understands headings, emphasis, links, nested lists, quotes, code blocks, tables, rules and local images. HTML inside the Markdown is ignored; there is no HTML or CSS engine.
- Bookmarks.
merge,pagesandsplitkeep the bookmarks whose target page is in the output. They land at the top of that page. Named destinations used by links are not carried over. - Forms when merging. Fields of the second and later inputs are renamed
doc2.<name>,doc3.<name>, so equal names do not share a value. - Lossy compression is opt-in through
--image-qualityand--max-image-edge. It re-encodes 8-bit grey and RGB images as JPEG. Masks, palette images, CMYK and 1-bit scans are left untouched, and the output is never larger than the input. - Images are the ones a page actually draws, including inline images and images inside forms. JPEG and JPEG 2000 are written as stored; everything else (Flate, LZW, CCITT fax, JBIG2, palette, masks) is decoded to PNG.
- OCR quality and languages are tesseract's. Recognised text is returned, not written into the
PDF. When a language is missing, the error names the
ocr-installcall that fixes it, so an agent can recover on its own. Data comes from thetessdata_fastrepository (--bestfor the larger models) and is kept in~/.local/share/pdfops/tessdata, orPDFOPS_TESSDATAif set. Installing tesseract itself uses the system package manager; where that needs a password, the command to run is returned instead. - Signatures. Any change to a signed PDF invalidates its digital signatures.
Development
Built on lopdf (object model), pdf-extract (text), hayro (rendering, positions and image decoding), subsetter (font embedding) and pulldown-cmark (Markdown).
License
MIT