<!-- GENERATED by tools/gen-site-operations.mjs — do not edit by hand. -->
<!-- Canonical page: https://pdf.lynxdi.com/operations/ -->

# Lynx PDF Studio — Operations Reference

**80 operations.** A workflow (`workflow.opw.yaml`) lists `inputs`, an ordered `operations:` program, and an `output`. Each operation below is one entry in that list.

## Contents

- Pages & layout
- Stamps & overlays
- Metadata, bookmarks & tables
- Text, image & Markdown extraction
- Redaction & cleanup
- Forms
- Attachments
- Encryption & permissions
- Optimize, repair & archival
- Convert to PDF
- Convert from PDF
- Document intelligence (AI)
- Digital signatures

## Pages & layout

### `merge`

Concatenates every file in the workflow's `inputs` (in order) into one PDF. List 2+ inputs; no params.

- **params:** —
- **inputs:** cover-page.pdf, chapter-01.pdf, chapter-02.pdf → **output:** `output/full-manuscript.pdf`
- **operation:** `merge: {}`
- **result:** Concatenates the cover page and both chapters, in listed order, into a single manuscript PDF.

### `split`

Splits the working PDF into several output files — by explicit `ranges` or `every` N pages.

- **params:** `ranges`, `every`
- **inputs:** annual-report-2024.pdf → **output:** `output/report-section.pdf`
- **operation:** `split: { ranges: ["1-1", "2-12", "13-40"] }`
- **result:** Splits the annual report into three files — cover (p.1), MD&A (p.2-12), and financial statements (p.13-40).

### `split_invoices`

Splits a PDF that concatenates many invoices/receipts into one PDF per invoice. Set `output.folder` (or the op's `to` param) as the destination. Detects boundaries from the page text ("Page 1 of N" resets, invoice-number changes, header keywords) and names each file from the detected invoice number/date/vendor via `name`. Scans: add `ocr_first: true`. Always writes a `_manifest.csv` (page ranges + metadata) so you can verify the split. Escape hatch: pass explicit `starts` (1-based page numbers).

- **params:** `to`, `name`, `starts`, `keywords`, `drop_blank`, `ocr_first`, `lang`, `detect`
- **inputs:** invoices-batch.pdf → **output:** `output/invoices`
- **operation:** `split_invoices: { name: "{invoice_number}" }`
- **result:** Detects each invoice in the batch and writes one PDF per invoice to output/invoices/ (named by invoice number), plus output/invoices/_manifest.csv.

### `delete_pages`

Removes the listed 1-based pages from the working PDF.

- **params:** `pages*`
- **inputs:** scanned-contract.pdf → **output:** `output/contract-clean.pdf`
- **operation:** `delete_pages: { pages: [2, 7, 12] }`
- **result:** Removes the three blank separator pages (2, 7, and 12) from the scanned contract.

### `reorder_pages`

Reorders pages to the given 1-based permutation (every page appears once).

- **params:** `order*`
- **inputs:** board-deck.pdf → **output:** `output/board-deck-reordered.pdf`
- **operation:** `reorder_pages: { order: [1, 4, 2, 3, 5] }`
- **result:** Reorders the 5-page board deck so the summary slide (page 4) sits right after the title.

### `rotate_pages`

Rotates pages clockwise by a multiple of 90°. Omit `pages` to rotate all.

- **params:** `pages`, `degrees*`
- **inputs:** scanned-survey.pdf → **output:** `output/survey-upright.pdf`
- **operation:** `rotate_pages: { pages: [3, 4], degrees: 90 }`
- **result:** Rotates the two landscape scan pages (3 and 4) 90° clockwise so they read upright.

### `flip_pages`

Mirrors pages — a true flip, not a rotation. `direction: horizontal` (default) mirrors left↔right; `vertical` mirrors top↕bottom. Omit `pages` to flip the whole document. Useful for scans fed through a duplex feeder backwards, or for iron-on/transfer prints.

- **params:** `pages`, `direction`
- **inputs:** scanned-booklet.pdf → **output:** `output/scanned-booklet.pdf`
- **operation:** `flip_pages: { direction: horizontal, pages: [2, 4] }`
- **result:** Mirrors pages 2 and 4 left-to-right, leaving the other pages untouched.

### `insert_blank`

Inserts a blank page before the 1-based position `at`. `size` copies a neighbor by default.

- **params:** `at*`, `size`
- **inputs:** employee-handbook.pdf → **output:** `output/handbook-print-ready.pdf`
- **operation:** `insert_blank: { at: 2, size: Letter }`
- **result:** Inserts a Letter-size blank page before page 2 so chapter 1 opens on a right-hand page for duplex printing.

### `extract_pages`

Keeps only the listed pages (drops the rest), in the order given.

- **params:** `pages*`
- **inputs:** lease-agreement.pdf → **output:** `output/lease-signature-pages.pdf`
- **operation:** `extract_pages: { pages: [1, 8, 9] }`
- **result:** Keeps only the cover and the two signature pages (1, 8, 9) and drops everything else.

### `crop`

Crops pages to a rectangle [x, y, width, height] in points (origin bottom-left). Omit `pages` for all.

- **params:** `box*`, `pages`
- **inputs:** quarterly-report.pdf → **output:** `output/report-chart-crop.pdf`
- **operation:** `crop: { box: [72, 500, 468, 240], pages: [4] }`
- **result:** Crops page 4 down to the revenue-chart region ([72,500,468,240] pt), dropping the surrounding text.

### `scale_pages`

Resizes pages to a named `size` (A4/Letter/Legal) or by a `factor`, scaling the content to fit.

- **params:** `size`, `factor`
- **inputs:** mixed-size-scans.pdf → **output:** `output/normalized-a4.pdf`
- **operation:** `scale_pages: { size: A4 }`
- **result:** Resizes every page to A4, scaling the content to fit a uniform page size.

### `n_up`

Places `cols`×`rows` source pages onto each output page (e.g. 2×1 = two-up handouts).

- **params:** `cols*`, `rows*`
- **inputs:** conference-slides.pdf → **output:** `output/slides-4up-handout.pdf`
- **operation:** `n_up: { cols: 2, rows: 2 }`
- **result:** Places four slides per sheet in a 2×2 grid to produce a printable handout.

### `booklet`

Imposes pages 2-up in saddle-stitch order for folded printing (pads to a multiple of 4). No params.

- **params:** —
- **inputs:** event-program.pdf → **output:** `output/event-program-booklet.pdf`
- **operation:** `booklet: {}`
- **result:** Imposes the program 2-up in saddle-stitch order (padded to a multiple of 4) for folded booklet printing.

### `poster`

Splits each page into a `rows`×`cols` grid of separate pages (in reading order). Use a large grid for poster printing, or **`rows: 1, cols: 2`** to halve a 2-up book scan back into single pages.

- **params:** `rows`, `cols`
- **inputs:** book-2up-scan.pdf → **output:** `output/book-single-pages.pdf`
- **operation:** `poster: { rows: 1, cols: 2 }`
- **result:** Splits each two-page spread down the middle into separate left/right pages (halving). Use rows: 3, cols: 3 to tile a page across nine sheets for a poster instead.

### `single_page`

Combines every page into one tall single page, stacked top-to-bottom. Optional `gap` adds blank space (points) between pages.

- **params:** `gap`
- **inputs:** receipt-scans.pdf → **output:** `output/receipts-continuous.pdf`
- **operation:** `single_page: { gap: 12 }`
- **result:** Stacks all receipt pages into a single continuous page with a 12 pt gap between them — handy for scrolling or web embedding.

## Stamps & overlays

### `watermark`

Stamps a diagonal `text` (or `image`) watermark across pages. Image comes from a declared asset.

- **params:** `text`, `image`, `opacity`, `pages`, `rotate`
- **inputs:** contracts/master-services-agreement.pdf → **output:** `output/msa-confidential.pdf`
- **operation:** `watermark: { text: CONFIDENTIAL, opacity: 0.12, rotate: 45 }`
- **result:** Stamps a translucent diagonal "CONFIDENTIAL" watermark across every page of the agreement at 12% opacity.

### `stamp`

Places positioned `text` or an `image` at (`x`, `y`) in points from bottom-left (not diagonal — that's watermark).

- **params:** `text`, `image`, `x`, `y`, `size`, `width`, `opacity`, `pages`
- **inputs:** invoices/invoice-2024-0417.pdf → **output:** `output/invoice-0417-paid.pdf`
- **operation:** `stamp: { image: assets/paid-stamp.png, x: 400, y: 640, width: 120, opacity: 0.9, pages: [1] }`
- **result:** Stamps a 120 pt-wide "PAID" graphic near the top-right of the invoice's first page.

### `annotate`

Adds a list of `annotations` to the PDF — free `text` boxes, text `highlight`s (by `find` text or `rect`), sticky `note`s, shapes (`rect`/`line`/`ellipse`) and `image`s. Each item names a `type`, a 1-based `page`, and a `rect [x,y,w,h]` or point `at [x,y]` in points, origin BOTTOM-LEFT (like crop/stamp). Colors are "#RRGGBB" or [r,g,b]. Invalid items are skipped.

- **params:** `annotations*`, `origin`, `flatten`
- **inputs:** proposal.pdf → **output:** `output/proposal-annotated.pdf`
- **operation:** `annotate: { annotations: [{ type: highlight, page: 1, find: "DRAFT", color: "#ffcc00" }] }`
- **result:** Highlights every "DRAFT" on page 1 in yellow and writes output/proposal-annotated.pdf.

### `add_page_numbers`

Stamps page numbers on every page. `format` uses {n} and {total}; `position` places them.

- **params:** `format`, `position`, `start`, `size`
- **inputs:** reports/annual-report-2024.pdf → **output:** `output/annual-report-2024-numbered.pdf`
- **operation:** `add_page_numbers: { format: "Page {n} of {total}", position: bottom-right, start: 1, size: 9 }`
- **result:** Adds "Page {n} of {total}" numbering to the bottom-right corner of every page in 9 pt type.

### `overlay`

Overlays another PDF's pages on top of the current document. `over` is a declared asset PDF.

- **params:** `over*`, `opacity`
- **inputs:** statements/bank-statement-june.pdf → **output:** `output/bank-statement-june-specimen.pdf`
- **operation:** `overlay: { over: assets/void-specimen-overlay.pdf, opacity: 0.35 }`
- **result:** Overlays a translucent "SPECIMEN" template on top of every page at 35% opacity, keeping the underlying figures readable.

## Metadata, bookmarks & tables

### `set_metadata`

Sets document metadata. Provide any of title/author/subject/keywords/creator/producer.

- **params:** `title`, `author`, `subject`, `keywords`, `creator`, `producer`
- **inputs:** annual-report-2024.pdf → **output:** `output/annual-report-2024-tagged.pdf`
- **operation:** `set_metadata: { title: "2024 Annual Report", author: "Acme Corporation", subject: "Fiscal Year 2024 Results", keywords: ["annual report", "finance", "FY2024"] }`
- **result:** Writes the title, author, subject, and keyword fields into the report's document properties.

### `set_bookmarks`

Replaces the outline with a given list of { level, title, page } entries (page is 1-based).

- **params:** `bookmarks*`
- **inputs:** user-manual.pdf → **output:** `output/user-manual-bookmarked.pdf`
- **operation:** `set_bookmarks: { bookmarks: [{ level: 1, title: Introduction, page: 1 }, { level: 1, title: Installation, page: 4 }, { level: 2, title: "System Requirements", page: 5 }, { level: 1, title: Troubleshooting, page: 12 }] }`
- **result:** Replaces the outline with a nested table of contents that jumps to the right pages.

### `extract_bookmarks`

Exports the outline/bookmarks to a JSON file (`to`). Does not change the PDF.

- **params:** `to`
- **inputs:** organic-chemistry-textbook.pdf → **output:** `output/organic-chemistry-textbook.pdf`
- **operation:** `extract_bookmarks: { to: output/textbook-outline.json }`
- **result:** Exports the textbook's chapter/section outline to a JSON file, leaving the PDF unchanged.

### `extract_fields`

Exports form fields (page/name/type/value) to a CSV file (`to`). Does not change the PDF.

- **params:** `to`
- **inputs:** tax-form-w9.pdf → **output:** `output/tax-form-w9.pdf`
- **operation:** `extract_fields: { to: output/w9-fields.csv }`
- **result:** Writes every form field (page, name, type, value) from the W-9 to a CSV for review.

### `extract_tables`

Detects tables and writes each to a CSV under the `to` directory. Does not change the PDF.

- **params:** `to`
- **inputs:** financial-statement-q4.pdf → **output:** `output/financial-statement-q4.pdf`
- **operation:** `extract_tables: { to: output/statement-tables }`
- **result:** Detects each table in the statement and writes it to its own CSV under output/statement-tables.

### `pdf_info`

Writes a read-only report of the PDF — page count, per-page size/rotation, metadata, encryption, fonts, image/field/annotation counts — as JSON to `to`. Leaves the PDF unchanged.

- **params:** `to`
- **inputs:** quarterly-report.pdf → **output:** `output/quarterly-report.pdf`
- **operation:** `pdf_info: { to: output/report-info.json }`
- **result:** Writes a structured JSON report of the quarterly report (pages, dimensions, metadata, fonts, security) for inspection or CI; the PDF passes through unchanged.

### `compare_pdfs`

Compares the working document with a second PDF (`against`, a workflow-relative path) and writes a diff report + highlighted images to `output.folder`. Pages are ALIGNED first (by text), so an inserted/deleted page is reported as added/removed instead of shifting every later page. Writes `_report.md` (per-page unified text diff), `_summary.json` (the change set as data), and one `change_*.png` per changed/added/removed page. `mode: text` skips images; `dpi` sets image resolution. Add `side_by_side: true` for one shareable `diff.pdf` (each changed page shown base-vs-against with the changed side highlighted). `tolerance` (0-255) ignores anti-aliasing noise in the visual fallback.

- **params:** `against*`, `to`, `mode`, `dpi`, `only_changed`, `tolerance`, `side_by_side`
- **inputs:** contract-v1.pdf → **output:** `output/diff`
- **operation:** `compare_pdfs: { against: input/contract-v2.pdf, side_by_side: true }`
- **result:** Aligns the two contract revisions, writes output/diff/_report.md + _summary.json, a highlighted change_*.png per changed page, and a side-by-side output/diff/diff.pdf.

### `set_view_preferences`

Controls how the PDF OPENS in a reader (not its content): `page_layout` (e.g. TwoPageLeft for a book spread), `page_mode` (FullScreen for a kiosk/presentation, UseOutlines to reveal bookmarks), `open_page` + `zoom` ("fit"/"fit-width"/a percent), and window chrome (hide_toolbar/menubar, fit_window, center_window, display_doc_title).

- **params:** `page_layout`, `page_mode`, `open_page`, `zoom`, `hide_toolbar`, `hide_menubar`, `hide_window_ui`, `fit_window`, `center_window`, `display_doc_title`
- **inputs:** slides.pdf → **output:** `output/slides.pdf`
- **operation:** `set_view_preferences: { page_mode: FullScreen, page_layout: SinglePage, open_page: 1, zoom: fit }`
- **result:** Saves a copy that opens full-screen at page 1, fit to the window — ready to present.

## Text, image & Markdown extraction

### `extract_text`

Extracts the PDF's text to a sidecar file (`to`). Does not change the PDF. Add `clean: true` to tidy the text: drop running headers/footers, rejoin hyphen-split words, and reflow hard-wrapped lines back into paragraphs (each pass can also be toggled on its own). `page_markers: true` labels each page.

- **params:** `to`, `clean`, `remove_headers_footers`, `fix_hyphenation`, `reflow`, `page_markers`
- **inputs:** signed-lease-agreement.pdf → **output:** `output/signed-lease-agreement.pdf`
- **operation:** `extract_text: { to: output/lease-text.txt, clean: true }`
- **result:** Writes the full, cleaned-up text of the lease to output/lease-text.txt while leaving the PDF unchanged.

### `extract_markdown`

Extracts page content as Markdown (tables included) to `to`. `engine`: auto/pymupdf4llm/markitdown, or `marker` (Surya OCR + layout) for scanned books — best quality, but slow and needs the marker-pdf backend. `remote` ("user@host") offloads Marker to a GPU box over SSH — minutes on a 4090 vs hours on a CPU; requires the pdfStudio.allowRemoteRender setting.

- **params:** `to`, `engine`, `ocr_first`, `margins`, `lang`, `remote`
- **inputs:** annual-report-2024.pdf → **output:** `output/annual-report-2024.pdf`
- **operation:** `extract_markdown: { to: output/annual-report.md, engine: pymupdf4llm }`
- **result:** Converts the report to clean Markdown (tables included) at output/annual-report.md, ready for LLM ingestion.

### `extract_images`

Extracts every embedded image to files under the `to` directory. Does not change the PDF.

- **params:** `to`
- **inputs:** spring-catalog.pdf → **output:** `output/spring-catalog.pdf`
- **operation:** `extract_images: { to: output/catalog-images }`
- **result:** Saves every embedded product photo from the catalog into the output/catalog-images/ directory.

### `render_pages`

Renders each page to a raster image (PNG/JPG) under `to` at `dpi`. Different from extract_images (embedded images). Set `format: svg` for vector output (infinite zoom, tiny files, no DPI needed).

- **params:** `to`, `dpi`, `format`, `text_as_path`
- **inputs:** investor-deck.pdf → **output:** `output/investor-deck.pdf`
- **operation:** `render_pages: { to: output/deck-thumbnails, dpi: 200, format: png }`
- **result:** Rasterizes each slide to a 200-DPI PNG in output/deck-thumbnails/ for previews or thumbnails.

### `replace_image`

Replaces an embedded image. `selector` picks the target ({ page, object_name }); `image` is the replacement path.

- **params:** `selector*`, `image*`
- **inputs:** company-brochure.pdf → **output:** `output/brochure-rebranded.pdf`
- **operation:** `replace_image: { selector: { page: 1, object_name: Im0 }, image: assets/new-logo.png }`
- **result:** Swaps the old logo (image Im0 on page 1) for assets/new-logo.png, producing a rebranded brochure.

### `replace_text`

Finds literal text and replaces it IN PLACE — the workflow-shaped answer to "edit the PDF": re-date a template, fix a recurring typo, swap an entity name across a whole folder (put a glob in `inputs` and it batches). The matched text is truly deleted, and the replacement lands on the original baseline in the original size and color. Matching is per LINE (text that wraps won't match) and literal (not regex); `ignore_case` / `whole_word` refine it, `pages` restricts it, and `replace: ""` deletes the match. **Font honesty:** an embedded, subsetted original font can't render NEW glyphs, so the replacement uses the closest base-14 font (serif→Times, mono→Courier, else Helvetica, keeping bold/italic) — use it for utility edits, not typography-preserving ones. Run `preview: true` first for a report of every match with nothing changed.

- **params:** `find*`, `replace`, `ignore_case`, `whole_word`, `pages`, `font`, `preview`, `to`
- **inputs:** contracts/master-services-agreement.pdf → **output:** `output/msa-renamed.pdf`
- **operation:** `replace_text: { find: "ACME Corp", replace: "Initech LLC" }`
- **result:** Replaces every in-line occurrence of "ACME Corp" with "Initech LLC", keeping each match's position, size and color; the render note reports the count and warns if a replacement is wider than what it replaced.

## Redaction & cleanup

### `redact`

Permanently removes content in rectangles on a page. `rects` are [x, y, w, h] in points. Add `rasterize: true` to also flatten the WHOLE document to an image-only PDF afterward, so nothing hidden (text layer, metadata, off-page content) can survive — the safest way to share.

- **params:** `page*`, `rects*`, `rasterize`, `dpi`
- **inputs:** bank-statement.pdf → **output:** `output/bank-statement-redacted.pdf`
- **operation:** `redact: { page: 2, rects: [[72, 688, 250, 26], [72, 650, 210, 26]], rasterize: true }`
- **result:** Permanently deletes two rectangular regions on page 2 (the customer name and SSN block) and flattens the file to an image-only PDF, so nothing can be recovered.

### `auto_redact`

Finds and permanently redacts matches across all pages. Match by literal `text` (a list), by named PII `patterns` (ssn, email, phone, credit_card, ein, ipv4, iban), and/or by custom `regex`. Literal matching is case-sensitive by default — add `ignore_case: true`, or `whole_word: true` so "Ann" doesn't hit "Anniversary". The run note reports any rule that found nothing, so verify. Add `rasterize: true` to also flatten the document to an image-only PDF so nothing hidden (text layer, metadata, off-page content) survives — the safest way to share.

**Preview first (recommended for redaction):** set `preview: true` for a dry run — it writes `output/redaction-preview.md` listing every match (page + matched text + which rule) and applies NOTHING. Review it, then remove `preview` to apply for real.

**Prompt example** — you don't have to write the YAML; just tell your coding agent in plain English, e.g.:
> "Create a workflow to redact my name (Jane Whitfield), account number 123456789, my email and any SSNs from statement.pdf, and give me a version that's safe to share."

The agent then creates a `.opw.yaml` with `auto_redact` — your EXACT values in `text`, `patterns: [email, ssn]` for the shape-based ones, `rasterize: true` (because "safe to share" = flatten to an image), `output.file` at a new path — runs `opw_validate`, and renders it.

- **params:** `text`, `patterns`, `regex`, `ignore_case`, `whole_word`, `preview`, `to`, `rasterize`, `dpi`
- **inputs:** statement.pdf → **output:** `output/statement-redacted.pdf`
- **operation:** `auto_redact: { text: ["Jane Whitfield", "123456789"], patterns: ["email", "ssn"], ignore_case: true, rasterize: true }`
- **result:** Permanently blacks out the name and account number, every email and SSN by pattern, then flattens to an image-only PDF that's safe to share (nothing hidden can be recovered).

### `sanitize`

Strips JavaScript, embedded files, metadata, and links from the PDF. No params.

- **params:** —
- **inputs:** downloaded-form.pdf → **output:** `output/downloaded-form-sanitized.pdf`
- **operation:** `sanitize: {}`
- **result:** Strips embedded JavaScript, attached files, metadata, and links, producing a safe-to-share PDF.

### `remove_annotations`

Removes all annotations (comments, highlights, link markup). No params.

- **params:** —
- **inputs:** reviewed-manuscript.pdf → **output:** `output/manuscript-clean.pdf`
- **operation:** `remove_annotations: {}`
- **result:** Removes all reviewer comments, highlights, and link markup, leaving only the underlying page content.

### `remove_images`

Removes all images from the document. No params.

- **params:** —
- **inputs:** photo-heavy-report.pdf → **output:** `output/report-text-only.pdf`
- **operation:** `remove_images: {}`
- **result:** Drops every embedded image, yielding a lightweight text-only version of the report.

### `remove_blank_pages`

Detects and deletes blank pages (no text, no images, near-white). No params.

- **params:** —
- **inputs:** scanned-batch.pdf → **output:** `output/scanned-batch-trimmed.pdf`
- **operation:** `remove_blank_pages: {}`
- **result:** Detects and deletes the empty separator pages left by the scanner, keeping only pages with real content.

### `ocr`

Adds a searchable text layer to scanned pages via OCR. `language` is a Tesseract code (default eng).

- **params:** `language`
- **inputs:** scanned-contract.pdf → **output:** `output/searchable-contract.pdf`
- **operation:** `ocr: { language: "eng+fra" }`
- **result:** Adds a searchable, selectable text layer over the scanned pages, recognizing both English and French text.

### `extract_js`

Surfaces any JavaScript embedded in the PDF (document-level Names tree and the open action) to a Markdown report at `to`. Read-only — it does not change the PDF. To strip JavaScript, use `sanitize`.

- **params:** `to`
- **inputs:** suspicious.pdf → **output:** `output/suspicious.pdf`
- **operation:** `extract_js: { to: output/javascript.md }`
- **result:** Writes every embedded JavaScript block to output/javascript.md for inspection, leaving the PDF unchanged.

## Forms

### `create_form`

Turns a template into a FILLABLE PDF. Type a marker where each field belongs, run a conversion op (`office_to_pdf` / `markdown_to_pdf` / `html_to_pdf`) to render the layout, and create_form finds each marker, DELETES it, and puts a real AcroForm field in its place — so it works with any source that leaves a text layer. **The easy way: use the TYPE as the tag** — `[[text]]`, `[[check]]`, `[[date]]`, `[[sign]]` — as many times as you like; they're numbered in reading order (text_01, checkbox_01, checkbox_02…), so there's nothing to name and no config file. Fields fill their table cell automatically and get a visible border. Use a NAMED tag (`[[employee_name]]`) when you want a meaningful field name; add a `fields_file` YAML keyed by tag for types/tooltips/choices/widths. Either way the tag key BECOMES the PDF field name, so you can fill it with `fill_form: { fields: { text_01: "Jane" } }` and read it back with `extract_form` — no form pack needed. **Keep tags short:** a marker too long for its column wraps or gets clipped by the renderer, and create_form then FAILS the run rather than ship a form with a missing field and a mangled `[[emplo` on the page (in a narrow cell, set the marker's font small in Word — it's deleted anyway). `preview: true` dry-runs it; `debug: true` writes a copy with every field outlined.

- **params:** `fields_file`, `from`, `fields`, `style`, `tag`, `strict`, `preview`, `to`, `debug`, `debug_to`, `keep_tags`
- **inputs:** onboarding.docx → **output:** `output/onboarding-fillable.pdf`
- **operation:** `create_form: { debug: true }`
- **result:** Converts the Word template and turns every [[check]]/[[text]] marker into a real form field sized to its table cell, writing output/form-map.json (each field with its page and rect) plus an outlined debug copy.

### `fill_form`

Fills a known PDF form from your records — no need to know the raw field names. `form` is a supported id (ds11, f1040, … — see the `form_list` MCP tool or the PDF Fill catalog); `people` points at your people.yaml and `person` chooses whose info fills the primary role. The form pack maps friendly keys → the form's real fields, handling checkboxes, radio groups, split SSN/date boxes, and dropdowns. `roles` binds relatives (spouse/parents) for forms that need them; `values` sets form-specific fields (e.g. applying_for). `signature: { image, field }` stamps a signature and `flatten: true` bakes a locked, print-ready final. **Run once with `preview: true`** for a dry-run report of exactly what will be filled. (Advanced: omit `form` and pass raw `fields: { RealFieldName: value }` to fill any AcroForm directly.)

- **params:** `form`, `records`, `people`, `person`, `roles`, `values`, `fields`, `signature`, `flatten`, `preview`, `to`
- **inputs:** ds11.pdf → **output:** `output/ds11-filled.pdf`
- **operation:** `fill_form: { form: ds11, people: people.yaml, person: me }`
- **result:** Fills the DS-11 passport application from the 'me' record — name, date of birth, SSN split across the three boxes, sex, and the parents pulled in via `relations` — leaving it editable for review.

### `extract_form`

Reads filled forms back OUT to data — the inverse of `fill_form`. Because a form pack maps friendly keys to real fields, the same pack that fills a form reads one back, so every supported form is extractable with no extra setup. Point `inputs` at one PDF or a whole folder (`forms/*.pdf` — all matches fold into ONE table, they aren't batched). Each PDF is auto-identified by its field signature (pass `form:` to force one). Writes per-form `<stem>.json`, a combined `forms.json`, and **one CSV per form type** (`w9.csv`, `f1040.csv` — different forms have unrelated schemas, so they never share a table) into `to`; `format: json`/`csv` narrows it. A CSV's columns come from the form pack, in the form's own field order, so the header is stable across runs no matter which files are present or what a form left blank. **Re-runs are incremental**: `forms.json` doubles as a ledger keyed by content hash, so running again after dropping new files in extracts only what's new or changed and merges it into the table (`resume: false` re-reads everything). A PDF that matches NO pack — anything you built with `create_form` — is read **raw**: field name → value, into `raw.csv`. That's what closes the loop (template → fillable → filled → CSV) with nothing to author. Note a flattened PDF has no fields left to read.

- **params:** `form`, `to`, `format`, `resume`, `include_empty`
- **inputs:** forms/*.pdf → **output:** `output/result.pdf`
- **operation:** `extract_form: { to: output/extracted }`
- **result:** Identifies each filled form, maps its fields back to friendly keys, and writes one JSON per form, a combined forms.json, and one CSV per form type for a downstream system to ingest. Run it again after adding more forms and only the new ones are read.

### `fill_field`

Sets an AcroForm field's value. `field` is the field name, `value` the text to set.

- **params:** `field*`, `value*`
- **inputs:** auto-claim-form.pdf → **output:** `output/auto-claim-filled.pdf`
- **operation:** `fill_field: { field: "policyholder_name", value: "Marcus Webb" }`
- **result:** Sets the AcroForm field 'policyholder_name' to 'Marcus Webb', producing a completed auto-insurance claim form.

### `flatten`

Bakes form fields + annotations into static page content (no longer editable). No params.

- **params:** —
- **inputs:** lease-agreement-filled.pdf → **output:** `output/lease-agreement-final.pdf`
- **operation:** `flatten: {}`
- **result:** Bakes the filled form fields and annotations into static page content so the signed lease can no longer be edited.

### `unlock_forms`

Clears the read-only flag on form fields so they can be filled. No params.

- **params:** —
- **inputs:** irs-w9-locked.pdf → **output:** `output/irs-w9-fillable.pdf`
- **operation:** `unlock_forms: {}`
- **result:** Clears the read-only flag on every form field so the locked W-9 can be filled in.

## Attachments

### `extract_attachments`

Extracts embedded file attachments to the `to` directory. Does not change the PDF.

- **params:** `to`
- **inputs:** zugferd-invoice-2024.pdf → **output:** `output/zugferd-invoice-2024.pdf`
- **operation:** `extract_attachments: { to: output/invoice-attachments }`
- **result:** Pulls every embedded file (e.g. the ZUGFeRD line-item XML) out of zugferd-invoice-2024.pdf into output/invoice-attachments/, leaving the PDF itself unchanged.

### `add_attachments`

Embeds a file as an attachment. `file` is a workflow-relative path; `name` defaults to its basename.

- **params:** `file*`, `name`
- **inputs:** invoice-2024.pdf → **output:** `output/invoice-2024-with-source.pdf`
- **operation:** `add_attachments: { file: assets/line-items-2024.xlsx, name: line-items-2024.xlsx }`
- **result:** Embeds the source spreadsheet assets/line-items-2024.xlsx into invoice-2024.pdf as an attachment named line-items-2024.xlsx so recipients get the raw data alongside the invoice.

## Encryption & permissions

### `encrypt`

Password-protects the PDF (AES-256). Set `user_password` (to open) and/or `owner_password`. Put it last. Use ${ENV} to keep secrets out of the file.

- **params:** `user_password`, `owner_password`
- **inputs:** board-minutes-2024-q4.pdf → **output:** `output/board-minutes-2024-q4-encrypted.pdf`
- **operation:** `encrypt: { user_password: "${PDF_OPEN_PASSWORD}", owner_password: "${PDF_OWNER_PASSWORD}" }`
- **result:** Locks the Q4 board minutes with AES-256, requiring an open password to view and a separate owner password to change permissions.

### `decrypt`

Removes password protection — supply the current `password`. Put it first so later ops can read the PDF.

- **params:** `password`
- **inputs:** locked-bank-statement.pdf → **output:** `output/bank-statement-unlocked.pdf`
- **operation:** `decrypt: { password: "${PDF_PASSWORD}" }`
- **result:** Removes the open-password protection from the bank statement so readers and downstream operations can access it.

### `set_permissions`

Restricts what viewers can do via an `owner_password`. `allow` toggles print/copy/modify/annotate/fill_form/assemble.

- **params:** `owner_password`, `allow`
- **inputs:** employee-handbook.pdf → **output:** `output/employee-handbook-protected.pdf`
- **operation:** `set_permissions: { owner_password: "${PDF_OWNER_PASSWORD}", allow: { print: true, copy: false, modify: false, annotate: false } }`
- **result:** Applies an owner-password policy to the handbook so viewers may print it but cannot copy text, edit, or annotate.

## Optimize, repair & archival

### `compress`

Reduces file size; deep image compression kicks in when Ghostscript/qpdf are available. `quality` 1-100.

- **params:** `quality`
- **inputs:** scanned-brochure.pdf → **output:** `output/brochure-compressed.pdf`
- **operation:** `compress: { quality: 60 }`
- **result:** Shrinks a large scanned brochure by recompressing its embedded images at 60% quality, producing a much smaller PDF.

### `linearize`

"Fast web view" — reorders the PDF for progressive loading. Put it last. No params.

- **params:** —
- **inputs:** annual-report-2024.pdf → **output:** `output/annual-report-web.pdf`
- **operation:** `linearize: {}`
- **result:** Reorders the annual report for "fast web view" so a browser can render the first page before the whole file finishes downloading.

### `repair`

Repairs / rewrites a structurally-messy PDF. No params.

- **params:** —
- **inputs:** corrupted-scan.pdf → **output:** `output/repaired-scan.pdf`
- **operation:** `repair: {}`
- **result:** Rewrites a structurally-broken scan with a rebuilt cross-reference table into a clean, openable PDF.

### `decompress`

Uncompresses content streams to produce a readable/debuggable PDF. No params.

- **params:** —
- **inputs:** invoice-2024.pdf → **output:** `output/invoice-uncompressed.pdf`
- **operation:** `decompress: {}`
- **result:** Uncompresses the invoice's content streams so the raw PDF operators are human-readable for inspection and debugging.

### `rasterize`

Flattens every page to a raster image (non-editable, non-selectable output) at `dpi`.

- **params:** `dpi`
- **inputs:** signed-contract.pdf → **output:** `output/contract-flattened.pdf`
- **operation:** `rasterize: { dpi: 200 }`
- **result:** Flattens every page of the signed contract to a 200-DPI image so its text can no longer be selected, copied, or edited.

### `recolor`

Recolors every page for comfortable reading. **`mode: dark`** (default) is a smart **dark mode** — it flips text and background to light-on-dark, but leaves embedded photos and logos looking normal (not a negative). `mode: invert` is a raw negative of everything; `mode: grayscale` drops color. Rasterizes at `dpi`, so the text becomes an image.

- **params:** `mode`, `dpi`
- **inputs:** report.pdf → **output:** `output/report-dark.pdf`
- **operation:** `recolor: { mode: dark }`
- **result:** Turns the whole document into an easy-on-the-eyes dark mode — white-on-black text with the report's photos still in full color.

### `scanner_effect`

Makes a clean, born-digital PDF look like it was scanned — a slight page skew (alternating per page), softening, and grain. Rasterizes each page. Tune `skew`, `noise`, `grayscale`, `dpi`.

- **params:** `dpi`, `skew`, `noise`, `grayscale`
- **inputs:** contract.pdf → **output:** `output/contract-scanned.pdf`
- **operation:** `scanner_effect: { grayscale: true, skew: 0.8 }`
- **result:** Produces a copy that looks photocopied/scanned (skewed, softened, grainy, black-and-white).

### `pdf_to_pdfa`

Converts to PDF/A for long-term archival. No params.

- **params:** —
- **inputs:** board-minutes.pdf → **output:** `output/board-minutes-pdfa.pdf`
- **operation:** `pdf_to_pdfa: {}`
- **result:** Converts the board minutes to PDF/A, embedding fonts and color profiles for compliant long-term archival.

## Convert to PDF

### `images_to_pdf`

Builds a PDF from image inputs — set the workflow's `inputs` to image files (one page each). PNG/JPEG work with the bundled engine; PNG/JPEG/WEBP/TIFF/GIF/BMP/SVG (and HEIC/HEIF with the `pillow-heif` package) work when the Python backend is installed. No params.

- **params:** —
- **inputs:** scans/receipt-front.jpg, photos/warranty.heic, logo.svg → **output:** `output/expense-receipts.pdf`
- **operation:** `images_to_pdf: {}`
- **result:** Bundles the images into a single PDF, one image per page in listed order.

### `html_to_pdf`

Renders an HTML file to PDF — set the workflow's input to a .html file. `engine` auto-picks a system Chrome/Edge (best fidelity), then WeasyPrint, then the built-in engine.

- **params:** `engine`
- **inputs:** invoice-2024-041.html → **output:** `output/invoice-2024-041.pdf`
- **operation:** `html_to_pdf: { engine: chrome }`
- **result:** Renders the styled HTML invoice to a pixel-faithful PDF using the system Chrome/Edge engine.

### `markdown_to_pdf`

Renders a Markdown file to a styled PDF — set the workflow's input to a .md file. `engine` auto-picks a system Chrome/Edge for the best result (no bundled browser); force `story` for a zero-dependency render.

- **params:** `engine`
- **inputs:** quarterly-report.md → **output:** `output/quarterly-report.pdf`
- **operation:** `markdown_to_pdf: { engine: chrome }`
- **result:** Turns the Markdown report (headings, tables, code blocks) into a themed PDF via system Chrome.

### `url_to_pdf`

Fetches a web page and renders it to PDF using a system Chrome/Edge when present. Needs no input file: use `inputs: []` and pass the `url`.

- **params:** `url*`, `engine`
- **inputs:** (none — sourced from params) → **output:** `output/pdf-wikipedia-snapshot.pdf`
- **operation:** `url_to_pdf: { url: "https://en.wikipedia.org/wiki/Portable_Document_Format", engine: chrome }`
- **result:** Fetches the live Wikipedia article and renders a full-page PDF snapshot with the Chrome engine.

### `eml_to_pdf`

Renders an .eml email (headers + body) to a styled PDF — set the workflow's input to the .eml file.

- **params:** `engine`, `load_remote_images`
- **inputs:** mailbox/customer-complaint.eml → **output:** `output/customer-complaint.pdf`
- **operation:** `eml_to_pdf: { engine: auto }`
- **result:** Renders the saved email — From/To/Subject/Date headers plus the message body — to a tidy styled PDF.

### `epub_to_pdf`

Converts an EPUB ebook to PDF. Make it the first operation with a `.epub` input. The bundled PyMuPDF engine renders it by default; `engine: calibre` (with `paper_size`) paginates better when Calibre is installed.

- **params:** `engine`, `paper_size`
- **inputs:** novel.epub → **output:** `output/novel.pdf`
- **operation:** `epub_to_pdf: { engine: auto }`
- **result:** Renders the EPUB's reflowed content to a paged PDF.

### `office_to_pdf`

Converts an Office document to PDF — set the input to a docx/xlsx/pptx/odt file and set `from` to its format.

- **params:** `from`
- **inputs:** decks/investor-pitch.pptx → **output:** `output/investor-pitch.pdf`
- **operation:** `office_to_pdf: { from: pptx }`
- **result:** Converts the PowerPoint deck to a PDF, one slide per page, preserving layout and fonts.

## Convert from PDF

### `pdf_to_docx`

Converts the PDF to a Word document written to `to`. Fidelity varies (LibreOffice).

- **params:** `to`
- **inputs:** board-minutes-2024.pdf → **output:** `output/board-minutes-2024.docx`
- **operation:** `pdf_to_docx: { to: output/board-minutes-2024.docx }`
- **result:** Produces an editable Word version of the board minutes so text and formatting can be revised.

### `pdf_to_pptx`

Converts the PDF to a PowerPoint file written to `to`. Fidelity varies (LibreOffice).

- **params:** `to`
- **inputs:** investor-deck.pdf → **output:** `output/investor-deck.pptx`
- **operation:** `pdf_to_pptx: { to: output/investor-deck.pptx }`
- **result:** Turns each PDF page of the investor deck into an editable PowerPoint slide.

### `pdf_to_xlsx`

Converts the PDF to an Excel file written to `to`. Fidelity varies (LibreOffice).

- **params:** `to`
- **inputs:** expense-report-2024.pdf → **output:** `output/expense-report-2024.xlsx`
- **operation:** `pdf_to_xlsx: { to: output/expense-report-2024.xlsx }`
- **result:** Extracts the expense report's tabular data into an editable Excel workbook.

### `pdf_to_html`

Converts the PDF to an HTML file written to `to`. Fidelity varies (LibreOffice).

- **params:** `to`
- **inputs:** brochure.pdf → **output:** `output/brochure.html`
- **operation:** `pdf_to_html: { to: output/converted.html }`
- **result:** Converts the brochure to an HTML document via LibreOffice for web publishing or content reuse.

### `pdf_to_markdown`

Converts the PDF to Markdown. Set the workflow's `output.file` to a `.md` path and the Markdown is written THERE directly (no stray PDF) — or pass `to` for a side artifact while the PDF passes through. `engine` auto-picks pymupdf4llm → markitdown → PyMuPDF; use `marker` (Surya OCR + layout, needs marker-pdf) for scanned books. `remote` ("user@host") runs Marker on a GPU box over SSH — minutes on a 4090 vs hours on a CPU; requires the pdfStudio.allowRemoteRender setting.

- **params:** `to`, `engine`, `ocr_first`, `margins`, `lang`, `remote`
- **inputs:** research-paper.pdf → **output:** `output/research-paper.md`
- **operation:** `pdf_to_markdown: { engine: auto }`
- **result:** Writes the paper as clean Markdown (tables included) to output/research-paper.md — ready for docs or LLM ingestion.

### `pdf_to_epub`

Converts the PDF to a **reflowable EPUB** ebook written to `to` — ideal for reading on Kindle and other e-readers (text reflows to the screen, unlike a fixed PDF). `engine`: `auto` uses Calibre's `ebook-convert` when it's installed (best reflow + formatting) and otherwise falls back to a bundled text-to-EPUB builder (always available; text-focused). Chapters come from the PDF outline when present, else every `chapter_pages` pages. `title`/`author` default to the PDF metadata. Best for prose/text books; complex layouts reflow imperfectly.

- **params:** `to`, `title`, `author`, `engine`, `chapter_pages`
- **inputs:** novel.pdf → **output:** `output/novel.pdf`
- **operation:** `pdf_to_epub: { to: output/book.epub }`
- **result:** Writes a reflowable EPUB to output/book.epub (chaptered from the outline) that reads cleanly on a Kindle, while the PDF passes through unchanged.

## Document intelligence (AI)

### `summarize`

Summarizes the PDF's text with an LLM and writes a Markdown summary to `to` (or a `.md` output.file). `style`: bullets (default), abstract, or outline; `focus` narrows the topic. Runs a LOCAL model via Ollama by default; set ANTHROPIC_API_KEY for Claude. Requires the pdfStudio.allowAiRequests setting.

- **params:** `to`, `style`, `focus`, `model`, `max_chars`, `max_tokens`
- **inputs:** annual-report-2024.pdf → **output:** `output/summary.md`
- **operation:** `summarize: { style: bullets, to: output/summary.md }`
- **result:** Writes a concise bulleted Markdown summary of the annual report to output/summary.md (using a local LLM by default).

### `translate`

Translates the PDF's text to `lang` with an LLM. By default writes the translation as Markdown to `to`. Set `layout: true` to instead render a translated PDF that keeps the original page geometry, images and tables — each text block is translated in place (best for Latin/CJK scripts; point output.file at the PDF). Chunks long documents automatically. Runs a LOCAL model via Ollama by default; set ANTHROPIC_API_KEY for Claude. Requires the pdfStudio.allowAiRequests setting.

- **params:** `lang*`, `layout`, `to`, `model`, `max_chars`, `chunk`, `max_tokens`
- **inputs:** user-guide.pdf → **output:** `output/user-guide-es.pdf`
- **operation:** `translate: { lang: Spanish, layout: true }`
- **result:** Renders a Spanish user guide that keeps the original layout (images/tables in place), each text block translated where it sits.

### `semantic_search`

Finds passages by MEANING, not keywords — ask in plain language and get the matching passages back, ranked, with their page numbers. Embeds the document with a LOCAL model (nomic-embed-text via Ollama by default: `ollama pull nomic-embed-text`), so nothing leaves your machine. Needs the pdfStudio.allowAiRequests setting. Scanned PDF? Run `ocr` first.

- **params:** `query*`, `top_k`, `min_score`, `to`, `model`, `chunk_words`, `overlap_words`, `no_cache`
- **inputs:** terms-and-conditions.pdf → **output:** `output/terms-and-conditions.pdf`
- **operation:** `semantic_search: { query: "the refund policy for late cancellations", top_k: 5, to: output/search-results.md }`
- **result:** Writes the 5 passages closest in meaning to the query — each with its page number and score — to output/search-results.md, leaving the PDF unchanged.

## Digital signatures

### `sign`

Digitally signs the PDF with a PKCS#12 certificate. `cert` is a .p12/.pfx path; use ${ENV} for the `password`.

- **params:** `cert*`, `password`, `field`, `reason`
- **inputs:** contracts/master-service-agreement.pdf → **output:** `output/signed-master-service-agreement.pdf`
- **operation:** `sign: { cert: assets/acme-legal.p12, password: "${CERT_PASSWORD}", field: Signature1, reason: "Approved by Legal" }`
- **result:** Digitally signs the agreement with the ACME PKCS#12 certificate, embedding a verifiable signature in the Signature1 field with the reason "Approved by Legal".

### `validate_signature`

Validates the PDF's digital signatures and writes a JSON report to `to`. Does not change the PDF.

- **params:** `to`
- **inputs:** signed-master-service-agreement.pdf → **output:** `output/signed-master-service-agreement.pdf`
- **operation:** `validate_signature: { to: output/signature-report.json }`
- **result:** Checks every digital signature in the signed agreement and writes a JSON report of each signer, validity status, and signing time to signature-report.json (the PDF passes through unchanged).

### `timestamp`

Adds an RFC-3161 trusted timestamp from a TSA. `tsa_url` is the timestamp server (needs network).

- **params:** `tsa_url*`
- **inputs:** signed-invoice-2024.pdf → **output:** `output/timestamped-invoice-2024.pdf`
- **operation:** `timestamp: { tsa_url: "http://timestamp.digicert.com" }`
- **result:** Adds an RFC-3161 trusted timestamp from DigiCert's TSA, cryptographically proving the signed invoice existed and was unaltered as of that moment.

_* = required parameter._
