pdf

PDF files: create, read, merge, fill, OCR, edit text.

  • pdf
  • documents
  • forms
  • ocr
  • text-extraction
  • reportlab
  • pypdf
  • pdfplumber
  • pymupdf
  • marker

Declared platforms: linux · macos · windows

Install
npx skills add 'https://github.com/NousResearch/hermes-agent/tree/main/skills/productivity/pdf'
Download bundle ↓
main · 24fd22bScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md

Building Fillable Forms: spec format and workflow

The same JSON spec drives both pdf_form_layout.py (design lint) and pdf_make_form.py (AcroForm build). Coordinates are PDF points, origin at the bottom-left of the page (1 pt = 1/72 inch; A4 is 595.27 x 841.89, letter is 612 x 792).

Spec shape

{
  "title": "Example Intake Form",
  "author": "example-author",
  "page_size": "A4",
  "page_count": 1,
  "fields": [
    {"name": "surname", "type": "text", "page": 1,
     "label": "Surname", "label_box": [72, 700, 150, 714],
     "entry_box": [160, 696, 400, 716],
     "value": "", "tooltip": "Family name"},

    {"name": "agree", "type": "checkbox", "page": 1,
     "label": "I agree", "label_box": [72, 660, 150, 674],
     "entry_box": [160, 658, 176, 674], "checked": false},

    {"name": "color", "type": "radio", "page": 1,
     "label": "Color", "label_box": [72, 620, 150, 634],
     "entry_box": [160, 616, 400, 636],
     "options": ["red", "blue"], "value": "blue"},

    {"name": "size", "type": "dropdown", "page": 1,
     "label": "Size", "label_box": [72, 580, 150, 594],
     "entry_box": [160, 576, 300, 596],
     "options": ["small", "large"], "value": "small"}
  ]
}
  • page_size: "A4", "letter", or [width, height] in points.
  • page_count: optional; extended automatically to the highest field page.
  • Boxes are [x0, y0, x1, y1] with x0 < x1, y0 < y1.
  • label is drawn as static text near label_box; omit it (and label_box) for unlabeled fields.
  • radio: the buttons are laid out left-to-right inside entry_box, one slot per option, each with a small static caption. value pre-selects an option by its export name.
  • dropdown maps to an AcroForm choice (combo) field.

Field types → what pdf_read.py --fields reports

Spec type/FTvalue format after fill
text/Tx (text)the string
checkbox/Btn (button)/Yes or /Off
radio/Btn (button)/<export>, e.g. /red
dropdown/Ch (choice)the option string

When filling with pdf_fill_form.py, checkboxes accept true/false; radio values need the leading slash ("/red"); dropdown values are the plain option string.

Layout lint rules (pdf_form_layout.py)

Per field, on its declared page:

  • boxes must be well-formed and inside the page bounds;
  • entry boxes must be at least 8x8 pt (12 pt tall for text/dropdown);
  • no two entry boxes on the same page may overlap (the second and later fields of an overlapping cluster are flagged);
  • a label must sit within 150 pt of its entry box and must not overlap it.

Exit code 0 = clean, 1 = at least one problem; the JSON report lists per-field problems. Lint the spec BEFORE building — fixing numbers in JSON is cheaper than debugging a rendered PDF.

Visual review loop

python3 scripts/pdf_form_layout.py spec.json --render-overlay overlay.png [--pdf built.pdf]

Red rectangles = entry boxes (with field names), blue = label boxes. Without --pdf the overlay is drawn on a blank page (PIL-only, always works); with --pdf the real page is rasterized underneath (needs pypdfium2 or pdftoppm — otherwise the report says "rendered": false with install hints). Feed the PNG to vision_analyze and ask specifically about collisions, alignment, and stray labels.

Radio-group quirks (reportlab + pypdf)

  • reportlab requires at least two radio() calls per group; a single-option radio group produces a broken field.
  • Pre-selecting is done at build time via "value"; changing selection later via pdf_fill_form.py needs the slashed export name ("/red").
  • Some viewers render reportlab radio appearances inconsistently after a pypdf fill; verify with --fields (data truth) plus a rendered page image (visual truth) rather than either alone.
  • Flattening radio groups is the least reliable flatten case — check the output image before shipping.
Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 92.
4. **Manipulate.** `pdf_merge.py` concatenates and can add one bookmark per source file; `pdf_split.py` handles page ranges (1-based, e.g. `1-3,5,9-`), rotation in 90° steps, and `--compress`. Watermark by preparing a single-page stamp PDF (e.g. via `pdf_create.py`) and overlaying it with `pdf_watermark.py`; for one-liner stamps ("sign here", diagonal DRAFT, corner labels) use `pdf_stamp.py` with text or an image at explicit coordinates.5. **Build forms.** Write one form-spec JSON (fields with `label_box`/`entry_box` in PDF points — see `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 (`--fields`) to learn exact names and types, write a UTF-8 JSON of `{"FieldName": "value"}` with `write_file` (checkboxes accept `true`/`false`; radio/choice values must match the field's export options), then `pdf_fill_form.py`. Re-read with `--fields` to confirm values landed.
SKILL.mdView in source ↗
Source excerpt starting at line 109.
- **Rotated stamp text extraction**: pdfplumber's line grouping scrambles rotated glyphs (a 45° "DRAFT" extracts as stray letters); verify rotated stamps with `pypdf`'s `extract_text()` or a rendered image instead.- **Radio groups**: reportlab needs ≥2 `radio()` widgets per group, fills need the slashed export value (`"/red"`), and flatten fidelity is worst for radios — see `references/forms.md`.- **Metadata scope**: `pdf_meta.py` writes the classic DocInfo dictionary only; embedded XMP metadata (if any) is left untouched and may show different values in some viewers.