Hermes Agent PDF Skill: Create, Read, Merge, Fill, and OCR PDFs
PDF files: create, read, merge, fill, OCR, edit text.
Written by Neura Market from the official Hermes Agent documentation for Pdf. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationThe PDF skill gives Hermes Agent a full toolkit for working with PDF files: generating documents from JSON specs, building and filling AcroForm forms, pulling out text, tables, and metadata, rearranging pages, stamping and watermarking, and encrypting or decrypting. It is the skill you reach for when a task involves a PDF as an input, an output, or both, and you want the agent to handle it end to end rather than shelling out to a dozen unrelated utilities.
What it does
At its centre the skill wraps three well-known Python libraries, pypdf, reportlab, and pdfplumber, behind a set of argparse command-line helpers that all live in scripts/. Each helper reads and writes JSON strictly as UTF-8, prints a JSON result to stdout, and exits non-zero on failure, which makes the output easy for the agent to parse and act on.
Two capabilities that used to be separate skills have been absorbed into this one and now live under references/. You read the matching reference file before attempting those tasks:
- Scanned or image-only PDFs and OCR. There is a fast path built on pymupdf and a higher-quality path built on marker-pdf, driven by
scripts/extract_pymupdf.pyandscripts/extract_marker.py. The details are inreferences/ocr-extraction.md. - Editing text inside an existing PDF through natural-language prompts, handled by the nano-pdf CLI. The details are in
references/nano-pdf-editing.md.
In practice this means a single skill covers document generation, extraction, page surgery, forms, security, and metadata, with the two specialist workflows documented separately so they do not bloat the main path.
Before you start
You need Python 3.10 or newer with pypdf, reportlab, and pdfplumber available. Install them with:
python -m pip install pypdf reportlab pdfplumber
Page rasterization, which covers pdf_page_image.py and overlay rendering, is optional and needs either pypdfium2 or poppler's pdftoppm on your PATH:
python -m pip install pypdfium2
Scripts fall back from pypdfium2 to pdftoppm and report {"rendered": false, "missing": [...]} with exit code 0 when neither is present. That exit code matters: a rasterization request that cannot be satisfied does not look like a crash, so check the JSON body rather than the status alone.
Each helper script checks its imports lazily and prints an install hint if a dependency is missing, so you generally find out what to install at the moment you need it rather than up front.
The skill is bundled with Hermes Agent, so it is installed by default. It is authored by Nous Research, carries the MIT licence, and runs on linux, macos, and windows.
How to run the helpers
Every helper is an argparse CLI in scripts/ and is meant to be run with the terminal tool. Each one supports --help. They read and write JSON strictly as UTF-8, print JSON results to stdout, and exit non-zero on failure.
python scripts/pdf_create.py spec.json -o out.pdf # build PDF from JSON spec
python scripts/pdf_make_form.py formspec.json -o form.pdf # build fillable AcroForm from JSON spec
python scripts/pdf_form_layout.py formspec.json # lint form layout BEFORE building
python scripts/pdf_form_layout.py formspec.json --render-overlay boxes.png [--pdf form.pdf]
python scripts/pdf_read.py doc.pdf --text # per-page text (JSON)
python scripts/pdf_read.py doc.pdf --tables --csv-dir t/ # tables to JSON + CSV files
python scripts/pdf_read.py doc.pdf --meta # metadata, page sizes, encrypted/scanned flags
python scripts/pdf_read.py form.pdf --fields # form fields: name, type, value
python scripts/pdf_merge.py a.pdf b.pdf -o merged.pdf [--bookmarks]
python scripts/pdf_split.py doc.pdf --pages 1-3,7 -o part.pdf [--rotate 90]
python scripts/pdf_fill_form.py form.pdf --fields-json values.json -o filled.pdf [--flatten]
python scripts/pdf_secure.py doc.pdf --encrypt -o enc.pdf --user-password your-password
python scripts/pdf_secure.py enc.pdf --decrypt -o dec.pdf --password your-password
python scripts/pdf_watermark.py doc.pdf --stamp mark.pdf -o stamped.pdf [--under]
python scripts/pdf_stamp.py doc.pdf -o out.pdf --text "DRAFT" --x 150 --y 400 \
--font-size 60 --rotation 45 --opacity 0.3 --color "#cc0000" [--pages 1-3]
python scripts/pdf_stamp.py doc.pdf -o out.pdf --image sig.png --x 400 --y 60 --width 120
python scripts/pdf_page_image.py doc.pdf --pages 1-3 --dpi 150 --out-dir imgs/
python scripts/pdf_meta.py doc.pdf --set-meta --title "T" --author "A" -o out.pdf
python scripts/pdf_meta.py doc.pdf --attach data.csv -o out.pdf
python scripts/pdf_meta.py doc.pdf --list-attachments | --extract-attachments dir/
Quick reference
The table below maps each task to the tool underneath and the command or API that drives it.
| Task | Tool | Command / API |
|---|---|---|
| Create doc (headings, tables, images) | reportlab platypus | pdf_create.py spec.json -o out.pdf |
| Build fillable form | reportlab acroForm | pdf_make_form.py formspec.json -o form.pdf |
| Lint form layout / overlay image | pure python + PIL | pdf_form_layout.py formspec.json [--render-overlay o.png] |
| Per-page text | pdfplumber | pdf_read.py f.pdf --text |
| Tables → JSON/CSV | pdfplumber | pdf_read.py f.pdf --tables |
| Metadata / sizes / encrypted / scanned | pypdf + pdfplumber | pdf_read.py f.pdf --meta |
| Merge (+ outline) | pypdf | pdf_merge.py a.pdf b.pdf -o m.pdf |
| Split / extract / rotate | pypdf | pdf_split.py f.pdf --pages 2-5 --rotate 90 |
| List / fill / flatten form | pypdf | pdf_read.py --fields, pdf_fill_form.py |
| Encrypt / decrypt (AES-256) | pypdf | pdf_secure.py --encrypt/--decrypt |
| Watermark / stamp PDF page | pypdf | pdf_watermark.py f.pdf --stamp w.pdf |
| Stamp text/image at coordinates | reportlab + pypdf | pdf_stamp.py f.pdf --text "Sign here" --x 400 --y 60 |
| Pages → PNG (review / OCR hand-off) | pypdfium2 or pdftoppm | pdf_page_image.py f.pdf --pages 1-3 --out-dir imgs/ |
| Set/clear metadata, attachments | pypdf | pdf_meta.py --set-meta / --attach / --extract-attachments |
| Compress content streams | pypdf | pdf_split.py f.pdf --pages 1-N --compress |
The working procedure
The source lays out nine steps. They are worth following in order, because several of them exist specifically to stop you from reporting a wrong result.
1. Inspect first
Run pdf_read.py file.pdf --meta. Check encrypted, and if it is true, decrypt first with pdf_secure.py --decrypt. Check likely_scanned_pages. If pages are image-only, export them with pdf_page_image.py --pages --dpi 300 --out-dir imgs/ and hand the PNGs to the references/ocr-extraction.md skill. Do not report empty text as "no content".
2. Create
Write a JSON spec with write_file. The elements are heading, paragraph, table, image, and pagebreak, with optional title and author metadata, and page numbers are added automatically. Then run pdf_create.py. If layout matters, verify visually with vision_analyze on a rendered page image.
3. Extract
--text gives a JSON list of per-page strings. --tables gives row arrays per page and can also emit CSV files. Read results with read_file. Never eyeball a binary PDF directly.
4. Manipulate
pdf_merge.py concatenates files and can add one bookmark per source file. pdf_split.py handles page ranges (1-based, for example 1-3,5,9-), rotation in 90° steps, and --compress. To watermark, prepare a single-page stamp PDF, for instance with pdf_create.py, and overlay it with pdf_watermark.py. For one-liner stamps such as "sign here", a diagonal DRAFT, or corner labels, use pdf_stamp.py with text or an image at explicit coordinates.
5. Build forms
Write one form-spec JSON with fields carrying label_box and entry_box in PDF points, as described in references/forms.md. Lint it with pdf_form_layout.py and fix every reported problem. Optionally review the --render-overlay PNG with vision_analyze. Then build with pdf_make_form.py and confirm with pdf_read.py --fields.
6. Fill forms
List fields with --fields to learn the exact names and types. Write a UTF-8 JSON of {"FieldName": "value"} with write_file. Checkboxes accept true or false, and radio or choice values must match the field's export options. Then run pdf_fill_form.py. Re-read with --fields to confirm the values landed.
7. Metadata and attachments
pdf_meta.py --set-meta writes Title, Author, Subject, and Keywords into the DocInfo dictionary. --clear-meta drops them. --attach, --list-attachments, and --extract-attachments round-trip embedded files.
8. Secure
Encrypt with distinct user and owner passwords and AES-256. To remove a password you know, --decrypt writes an unencrypted copy.
9. Verify
Run the checks in the verification section before reporting success.
When not to use it
Two exclusions are explicit. This skill is not for scanned or image-only PDFs, which belong in references/ocr-extraction.md. It is also not for pixel-perfect HTML-to-PDF rendering, where a headless browser is the right tool.
PDF/A is out of scope as well. pypdf and reportlab cannot produce or validate conformant PDF/A. If archival conformance is required, run Ghostscript through the terminal tool, for example gs -dPDFA=2 -dPDFACompatibilityPolicy=1 -sColorConversionStrategy=UseDeviceIndependentColor -sDEVICE=pdfwrite -o out.pdf in.pdf with a suitable ICC profile, and validate with veraPDF. Both are external installs, and the result still needs validation rather than assumption.
Limits and gotchas
The pitfalls below are the ones that most often produce a confidently wrong answer.
- Scanned PDFs. An empty
extract_text()combined with page images means there is no text layer. Route toreferences/ocr-extraction.mdand do not fabricate text. - Flattening limits.
pdf_fill_form.py --flattenuses pypdf's flatten support, which converts widget appearances into page content. It is reliable for plain text fields and checkboxes but can drop or misrender exotic widgets such as rich text, custom appearance streams, and some radio groups. Verify the flattened output visually withvision_analyze. For bulletproof flattening, use an external renderer such as Ghostscript orpdftoppmplus reassembly as a fallback. - NeedAppearances. After filling, viewers only render values if appearance streams exist. The fill script sets the AcroForm
NeedAppearancesflag so conforming viewers regenerate them, but some minimal viewers ignore it. Flatten if display fidelity matters. - Non-Latin form values. Values are stored correctly as UTF-16, but the field's default font may lack the glyphs, so a viewer can show blanks even though the data round-trips. Verify with
--fields, not just visually. - Compression expectations.
--compressonly deflates content streams. Typical savings are 0 to 20 percent, and it does nothing for PDFs dominated by images or already-compressed streams. It is not a substitute for image downsampling, which is Ghostscript territory. - Permission flags do not enforce. Owner-password permission bits such as no-print and no-copy are polite requests that viewers may honour. Any library, including pypdf, can read and strip them. Only the user password actually gates content through encryption. Never present permission flags as security.
- Table extraction is heuristic. pdfplumber detects tables from ruling lines and word alignment, so borderless or merged-cell tables may need
table_settingstuning or manual cleanup. - Page indexing. The helper CLIs take 1-based pages, while pypdf APIs are 0-based. The scripts convert, so do not double-convert.
- Rotated stamp text extraction. pdfplumber's line grouping scrambles rotated glyphs, so a 45° "DRAFT" extracts as stray letters. Verify rotated stamps with pypdf's
extract_text()or a rendered image instead. - Radio groups. reportlab needs at least two
radio()widgets per group, fills need the slashed export value such as"/red", and flatten fidelity is worst for radios. Seereferences/forms.md. - Metadata scope.
pdf_meta.pywrites the classic DocInfo dictionary only. Embedded XMP metadata, if any, is left untouched and may show different values in some viewers. - Rotation and encryption ordering. Rotation must be a multiple of 90, and encrypted inputs must be decrypted before any other operation.
Verification
The source gives a specific check for each kind of operation. Run the matching one before you claim success.
- After create, merge, or split:
pdf_read.py out.pdf --meta, confirmingpage_countand per-pagerotationwhen you rotated. - After extraction: check the JSON is non-empty and spot-check a known string or cell.
- Form design loop:
pdf_form_layout.py spec.jsonmust exit 0. Then run--render-overlay boxes.png --pdf form.pdfand review the PNG withvision_analyze, asking about overlaps, misalignment, and labels detached from their fields. Red marks entry boxes with field names, blue marks label boxes. Iterate spec, lint, overlay until clean. - After building a form:
pdf_read.py form.pdf --fieldslists every spec field with the right type and options. - After form fill:
pdf_read.py filled.pdf --fieldsand compare values for exact match, including non-ASCII. - After stamping: re-extract text, using pypdf for rotated stamps, or render the page with
pdf_page_image.pyand inspect withvision_analyze. - After metadata or attachment edits:
pdf_read.py --metaorpdf_meta.py --list-attachments, and re-extract an attachment to byte-compare. - After encrypt:
--metashows"encrypted": trueand opening without a password fails. After decrypt, text extraction matches the original. - For anything visual, including watermarks and flattened forms, render and inspect with
vision_analyze.
What pairs with it
The skill sits alongside the other document-format skills in the same productivity family: docx, xlsx, and powerpoint. If a workflow starts in a Word document, a spreadsheet, or a slide deck and ends as a PDF, those skills handle the earlier stages and this one takes over at the point where the output needs to be paginated, secured, or archived. The OCR reference is the natural companion when a PDF arrives as a scan rather than as text, and the nano-pdf editing reference covers the case where the change you need is inside existing prose rather than in the structure around it.