# PDF

> Operates on PDF files: inspect/fill forms, merge/split, watermark, encrypt/decrypt, strip metadata, extract tables or images, compress, validate, render pages to images (so the agent can read scanned PDFs with vision), or reconstruct a text-based PDF as editable Word; render an existing .md/.html/.tex source file to PDF; or create new branded PDFs from MDX (reports, briefings, whitepapers). Do not use for: reading/summarizing/analyzing PDF content (use parse_file); editing existing Word/PPT/spreadsheets; unauthorized files.

- Skill: `ahang1598/pdf-5` (Agent Skill, multi-file: 53 files)
- Install (CLI): `npx skillmds@latest add ahang1598/pdf-5`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ahang1598/pdf-5/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: Proprietary. LICENSE has complete terms.
- Author: ahang1598 (https://skillmd.com/u/ahang1598)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ahang1598/pdf-5

---


# PDF — Professional PDF Processing

## What this skill is for

This skill operates on **PDF files themselves** — structural processing, not content authoring.

Use it when the user wants to:
- merge, split, rotate, reorder, watermark, or crop PDF pages
- encrypt, decrypt, strip metadata from, or redact a PDF
- optimize, compress, or validate a PDF
- remove or manipulate pages, images, or sections within a PDF
- extract tables from a PDF to Excel (preserving table structure)
- extract images from a PDF
- render PDF pages to images so the agent can read scanned pages with vision
- inspect or fill a PDF form (AcroForm or flat/scanned via overlay)
- reconstruct a text-based PDF as an editable Word document
- convert an existing Office document (DOCX/PPTX/XLSX and legacy variants) to PDF
- generate a PDF from an **existing** `.md` / `.html` / `.tex` source file in the workspace

## Do not trigger this skill for

Do **not** use this skill when:
- the user only wants to **read, summarize, analyze, or ask questions** about PDF content — no PDF structural operation is needed
- the user wants to **write/generate content** (report, article, meeting minutes, analysis) and output as PDF — the core task is content authoring, not PDF manipulation
- the user says "export to PDF / output PDF / make this a PDF" but has **no existing `.md`/`.html`/`.tex` source file** — that is content creation
- the user’s real task is to edit **Word / slides / spreadsheet** content
- the task is mainly **image editing, restoration, or stylization**
- the user asks for **legal-grade permanent redaction guarantees** beyond what this toolchain verifies
- the user refers to files that are **not provided, not accessible, or not authorized**

**Key principle**: if the user’s core need is “create/write content” and PDF is just the output format, do NOT trigger this skill. Only trigger when the PDF file itself (or a structured source file like `.md`) is the direct subject of work.

---

## PDF → Word

Choose by available capability, not operating system. Start with the product and
runtime routes below because they are usually direct, but prefer completing the
task over minimizing route discovery. When needed, detect compatible
pre-existing system or workspace tools through targeted checks such as PATH,
tool registries, or standard application locations.

1. Use typed `document.convert` once when it explicitly supports PDF input and
   DOCX output.
2. Otherwise prefer a ready Node.js route: extract text and coordinates with
   `pdfjs-dist`, then generate the DOCX with `docx`. Load the PDF as
   `Uint8Array`; do not flatten text before reconstructing paragraphs or tables.
3. If Node.js, its modules, or npm are unavailable, use an existing Python
   interpreter with `pdf2docx`.
4. If these routes are unavailable or fail, use another compatible pre-existing
   tool that can produce DOCX in the current environment. Verify its actual
   PDF-to-DOCX capability instead of assuming support from the operating system.

For the selected local route only, recover missing packages once. With a ready
Node.js and npm, install:

```bash
npm install --no-save --no-package-lock --prefer-offline --fetch-retries=0 --fetch-timeout=30000 "pdfjs-dist@3.11.174" "docx@9.7.1"
```

With a ready Python and pip, install `pdf2docx` at user scope using the same
interpreter (`<python> -m pip install --user pdf2docx`). Retry a failed route
only when dependency recovery or new evidence changes its conditions; otherwise
continue with another plausible route and do not revisit it. Stop only after the
available and authorized paths are exhausted, then report the concrete evidence.

Validate that the DOCX opens. Treat layout as best-effort and disclose that
images, stamps, and pixel-perfect positioning are not preserved unless verified.
For the PDF.js Node API, read
[references/advanced-libraries.md](references/advanced-libraries.md#pdfjs-dist--javascript-pdf-rendering-browser--node).

---

## Creating a new PDF — route selection

| Route | When |
|-------|------|
| **[mdx2pdf.md](mdx2pdf.md) — DEFAULT semantic entry** | Invoke `scripts/generate_mdx_pdf.py` for Markdown/MDX. It prefers the typed cloud capability when that backend supports the requested source and features, and otherwise preserves the local `md2pdf` / ReportLab paths needed to finish the task. |
| `markdown_to_pdf.py` (below) | The local ReportLab implementation for ordinary Markdown. Use it directly when the user requests raw layout controls or when a plain local render is the best fit. |

All other PDF work — reading, extracting, merging, splitting, OCR, forms, watermarking, encryption — uses the scripts below; mdx2pdf is creation-only.

---

## Routing rules (non-creation operations)

### Route map

| User request | Route | Required entry step | Primary scripts / route |
|---|---|---|---|
| Convert a text-based PDF to editable Word | CONVERT | capability probe in PDF → Word | compatible `document.convert` → Node.js → Python → pre-existing tool |
| Extract text from PDF | EXTRACT | `analyze_pdf.py` | `extract_content.py` |
| Extract tables from PDF | EXTRACT | `analyze_pdf.py` | `extract_tables.py` |
| Extract images from PDF | EXTRACT | `analyze_pdf.py` | `extract_images.py` |
| Render PDF pages to images (for vision / scanned PDFs) | RENDER | input check | `convert_pdf_to_images.py` |
| Convert an existing Office document to PDF | CONVERT | input check | `office_to_pdf.py` |
| Merge / split / rotate / reorder / watermark / text-watermark PDF pages | EDIT | input + action check | `batch_ops.py` |
| Crop pages | CROP | input + action check | `crop_compose.py` (auto-detect content bbox, or manual `--crop`) |
| Compose / N-up (multiple pages → one page) | CROP | input + action check | `crop_compose.py` (`--layout vertical\|horizontal\|grid`) |
| Fill or inspect a PDF form | FORM | `inspect_form.py` | AcroForm → `fill_form.py` → optional `flatten_form.py` · flat/scan → `extract_form_structure.py` → `check_bounding_boxes.py` → `fill_form_overlay.py` |
| Encrypt / decrypt / redact / strip metadata | SECURITY | input + risk check | `secure_pdf.py` |
| Compress / optimize PDF | OPTIMIZE | input check | `optimize_pdf.py` |
| Validate PDF structure / PDF/A hints | VALIDATE | input check | `validate_pdf.py` |
| Create a branded PDF from Markdown/MDX | GENERATE | source type check | `generate_mdx_pdf.py` |
| Create PDF from HTML | GENERATE | source type check | `html_to_pdf.py` (thin shim → markdown_to_pdf) |
| Create PDF from LaTeX | GENERATE | source type check | `latex_to_pdf.py` |
| Create PDF processing report | REPORT | output inventory check | `create_report.py` |
| Batch process many PDFs | BATCH | enumerate inputs + per-file `analyze_pdf.py` | `batch_ops.py` + refs |

### Start here when the input is already a PDF

For declared-script routes whose input is already a PDF, run `analyze_pdf.py` to confirm `pdf_type`, `page_count`, and file size. The downstream text/render and form decisions key off this inspection. PDF → Word follows its capability order above and does not require `analyze_pdf.py` before runtime selection. Do not run it for an Office input being converted to PDF or for an MD/MDX source being generated as a new PDF.

If `pdf_type = scanned` (or text comes back empty/garbled), default to the RENDER route: render the relevant pages with `convert_pdf_to_images.py` and read them with vision. Reach for `ocr_pipeline.py` only when the user explicitly asks for a searchable PDF or a plain-text OCR transcript.

---

## Working norms

A few non-obvious norms that this skill relies on:

**Choose the operation semantics first; prefer cloud when it fits, and preserve
task completion when it does not.** The agent decides pages, format, output
path, layout, quality, and other user-visible parameters. For routes with a
documented semantic script, start with it; PDF → Word follows its capability
order above. In `auto` mode a semantic script prefers a compatible typed
QwenWork capability and may switch once to a validated local implementation.
Do not preflight packages or install dependencies before the selected route's
first invocation.

Cloud authentication/authorization failures, missing capability routes, source
limits, and unsupported feature combinations make that cloud path unsuitable;
they do not end a normal user task when a trusted local or workspace-provided
route can still complete it. Do not repeat a known-ineligible cloud call or loop
between engines. If the semantic entry genuinely cannot express the request,
use a compatible existing tool or a bounded local implementation, preserve the
requested output semantics, and validate the result. An explicit
`cloud_required` request remains terminal because it is an acceptance test of
that backend rather than a normal document task.

Backend policy is controlled uniformly by
`QWENWORK_DOCUMENT_EXECUTION_MODE=auto|local_required|cloud_required` (`auto`
is the default). `QWENWORK_DOCUMENT_RUNTIME_REQUIRED=true` remains a legacy
alias for `cloud_required`. Desktop and cloud sandboxes inject endpoint/token;
the Skill scripts never read or persist credentials.

**Write to a new output file by default.** Why: the original is the only fallback if a step goes wrong, and most users don't realize they're asking for an in-place edit. Overwrite only when the user explicitly says so.

**Don't claim the output is sanitized, redacted, or PDF/A-compliant beyond what the scripts actually verified.** Why: `secure_pdf.py --action redact` draws annotations over text — the underlying text stream is still there. `validate_pdf.py` checks structural integrity, not standards certification. Describe what was done, not what it sounds like.

**Stop on missing inputs, parse failures, or wrong-password encryption.** Don't paper over them. Why: silently skipping an unreadable page or fabricating an empty result wastes more user time than an early stop.

**Use the declared scripts before reaching for an alternative.** Why: every script in `scripts/` handles edge cases the obvious one-liner skips — CJK font registration, atomic writes, coordinate-system flips, FlateDecode image reconstruction. If a script lacks a required feature, choose a compatible existing tool or a bounded implementation and apply the same validation and non-destructive-write rules; do not stop merely because the preferred path is unavailable.

**Label OCR output as OCR-derived when it materially affects the answer.** Recognition errors happen; the user needs to know which numbers are scanned vs. native text.

---

## Large-file strategy

When `analyze_pdf.py` reports **page_count > 100** or file size > **10 MB**, work in chunks to avoid timeouts and context overflow.

### Chunk strategy by route

| Route | Approach |
|---|---|
| EXTRACT (text) | `extract_content.py --pages 1-50`, then `51-100`, etc. Merge afterward. |
| EXTRACT (tables) | `extract_tables.py --pages 1-30 --output tables_p1_30/`, then next range. For long reports, use the TOC / bookmarks from `analyze_pdf.py` to target only the pages with tables. |
| RENDER | Render only the pages the user asked about with `--pages <spec>`. |
| OCR | `ocr_pipeline.py` already streams page-by-page (O(1) memory). For files > 200 pages, always pass `--pages` to limit scope. |
| EDIT (split-by-chapter) | `batch_ops.py --action split-by-bookmarks` uses PDF bookmarks in a single pass. Manual chapter detection only if bookmarks are absent. |

### Chunk sizes

| pdf_type | Pages per chunk | Rationale |
|---|---|---|
| text | 50–100 | pdfplumber ~0.1 s/page |
| scanned | 20–30 | OCR at DPI 200 ~2–4 s/page |
| mixed | 30–50 | balance |

### Target-then-extract

For "find X in this long document": extract the TOC or first 5 pages first to locate the target pages, then extract only those. Almost always faster and more reliable than processing the whole file.

```bash
# Find the financial statements
python scripts/extract_content.py report.pdf --pages 1-5 --output toc.txt
# (toc says: 资产负债表 starts on page 120, 利润表 page 135 …)

# Extract only those pages
python scripts/extract_tables.py report.pdf --pages 118-145 --output financials/ --format xlsx
```

---

## Per-route notes

The route map says what script to call; these notes capture the non-obvious bits per route.

### EXTRACT
- Required: `analyze_pdf.py` first (even for images — `pdf_type` decides routing).
- Scripts: text → `extract_content.py` · tables → `extract_tables.py` · images → `extract_images.py`.

### RENDER
- Default DPI 150; bump to 200–250 only if text on the 150-DPI render is unreadable.
- Render only the pages the user asked about — never preemptively rasterize a whole long PDF.
- Prefer `convert_pdf_to_images.py`; it uses the typed `pdf.render_pages`
  capability when compatible and otherwise local PDFium. If neither route can
  satisfy an unusual rendering request, use another trusted renderer and keep
  the same page, quality, and output-validation requirements.
- The cloud renderer returns PNG. Explicit JPEG output remains local-only. In
  `cloud_required` mode, an unsupported format or remote failure is terminal.

### CONVERT
- Start with `python3 scripts/office_to_pdf.py input.docx output.pdf`.
- `office_to_pdf.py` uses local LibreOffice when ready and otherwise the typed
  `document.convert` capability. QwenWork validates/uploads the input and
  downloads the result atomically. In `cloud_required` mode a remote failure is
  terminal.

### FORM
- `inspect_form.py` reports `form_type`. Branch on it:
  - `acroform` → edit `fill_value` in fields.json → `fill_form.py`.
  - `layout_based` (flat / scanned) → `extract_form_structure.py` → compose `fields.json` (see `references/forms-guide.md` for schema; use `pdf_width`/`pdf_height` for structure path, `image_width`/`image_height` if you fell back to visual estimation) → `check_bounding_boxes.py` → `fill_form_overlay.py`.
- After filling, render the result with `convert_pdf_to_images.py` and eyeball it. CJK is auto-handled via `_fonts.py`; you don't need to register anything.
- When the user requests a non-editable result, run `flatten_form.py` after
  filling; do not replace this with an ad-hoc render/rebuild loop.
- Use `debug_annotations.py` only when coordinates feel uncertain.

### GENERATE
- For Markdown/MDX, follow [mdx2pdf.md](mdx2pdf.md) and start with
  `python3 scripts/generate_mdx_pdf.py SOURCE --output OUTPUT.pdf`. It uses an
  eligible typed cloud route first, then a compatible local `md2pdf` or
  ReportLab implementation. Version 0.4.7+ receives
  `--brand qwenwork-cn`; an older detected release receives `--no-header` so
  its legacy MuleRun mark cannot leak into the output. Local-only features such
  as `--base-dir`, `--components`, sources above the cloud upload limit, or an
  explicit plain ReportLab profile bypass the incompatible cloud route without
  reducing the requested result. It never installs Node, React, Playwright,
  Chromium, or md2pdf automatically.
- When the command runs through Workspace Bash in the secure VM, set the Bash
  tool timeout to at least 900 seconds. The cloud operation itself is bounded
  to 10 minutes; the default 120-second VM shell timeout is too short.
- `generate_mdx_pdf_cloud.py` is retained only as an explicit forced-cloud
  adapter for acceptance tests; normal Skill workflows do not call it.
- For LaTeX invoke `latex_to_pdf.py`. It uses an existing local Tectonic or the
  typed `pdf.generate_latex` capability. Cloud generation accepts only a
  bounded self-contained `.tex` source (no `input`, `include`, external images,
  or bibliography files); do not install a TeX distribution in the client.
- Validate the output with `validate_pdf.py` for HTML / Markdown / LaTeX generation — these pipelines can produce broken PDFs in edge cases.

### SECURITY
- Before destructive actions (redact, decrypt, strip-metadata), summarize the scope (page range, pattern set) in your response so the user can spot a wrong target.
- `--action redact` draws annotations over text; the underlying stream is still there. Say so if the user implies they need court-grade redaction — point them at `qpdf --qdf` + manual stream editing, or Acrobat.

### OPTIMIZE
- `optimize_pdf.py` covers image re-encoding → adaptive quality search → controlled rasterization. Don't reinvent it.
- With `--target-size`, the script iterates to find the best quality that fits. If image compression alone can't hit the target, ask the user before passing `--rasterize-fallback` — it turns text into images and breaks search/select.

```bash
# Standard
python scripts/optimize_pdf.py input.pdf output.pdf --target-size 10240

# After user accepts quality loss
python scripts/optimize_pdf.py input.pdf output.pdf --target-size 10240 \
    --rasterize-fallback
```

### CROP
- `crop_compose.py` auto-detects content bbox via PyMuPDF. Pass `--crop x0,y0,x1,y1` to override, `--layout vertical|horizontal|grid` for composition, `--per-page N` for N-up.

### BATCH
- Enumerate inputs explicitly; drop missing / unauthorized files and list them in the final report.
- Run `analyze_pdf.py` per file, then dispatch each to its single-file route. Apply the Large-file strategy per file (not globally).
- Distinct output path per file. Aggregate into `{input, route, output, status, warnings}` rows.

### EDIT / VALIDATE / REPORT
- Single-script routes — `batch_ops.py` / `validate_pdf.py` / `create_report.py`. Pick action, give explicit output path, summarize the change.

---

## Stop conditions

Stop and surface the problem (don't paper over):

- Missing / inaccessible / corrupted input.
- Encrypted PDF without a working password.
- Required inspection step failed.
- Ambiguous target for a destructive write.
- The bounded dependency recovery below is ineligible, unavailable, or has
  failed after a backend error.
- No safe available route can preserve the requested semantics after bounded
  exploration and validation. Report the concrete missing capability and any
  usable partial result.

---

## Dependency policy

Do not pre-install packages speculatively. For routes with a documented
semantic script, invoke it first and let it use an already-ready local backend
or the typed cloud route. PDF → Word follows its capability probe and bounded
recovery above.

### One-time local dependency recovery

For a normal `auto` or `local_required` task, use this recovery only after the
semantic script reports `LOCAL_DEPENDENCY_MISSING`,
`NO_EXECUTION_BACKEND_AVAILABLE`, or a cloud infrastructure failure. Do not
recover from bad arguments, missing/corrupted/encrypted inputs, wrong passwords,
or `PDF_VALIDATION_FAILED`.

Install only the requested operation's declared packages at user scope, using
the same interpreter that launches the script. Use
`python -m pip install --user <packages>` (or the equivalent `python3` / `py -3`
command).

- Analysis/text/validation/form inspection: `pypdf pdfplumber`; tables to XLSX:
  `pdfplumber pandas openpyxl`; embedded images: `pypdf Pillow`; rendering:
  `pypdfium2 Pillow`.
- Form filling: `pypdf PyMuPDF`; flatten/crop/compose: `PyMuPDF`; edit/security:
  `pypdf`; text watermark/form overlay: `pypdf reportlab`; optimization:
  `pypdf Pillow` (raster fallback also needs `pypdfium2 reportlab`).
- Ordinary Markdown: `markdown-it-py reportlab pygments Pillow`; OCR:
  `numpy PyMuPDF rapidocr-onnxruntime reportlab Pillow`.

For a declared Python semantic script, after installation rerun the same script
and arguments under
`QWENWORK_DOCUMENT_EXECUTION_MODE=local_required` once before considering a
different trusted local route. On POSIX,
prefix that environment assignment to the command; in PowerShell, set
`$env:QWENWORK_DOCUMENT_EXECUTION_MODE = "local_required"` first. If Python,
`pip`, installation, or forced-local execution is unavailable, stop and report
the concrete error; never loop or retry the cloud route. PDF → Word uses the
operation-specific retry order above instead.

Do not turn this default recovery into a hard path restriction. When the
declared script cannot express the request, a compatible pre-existing tool may
still be used if its output is validated and the task does not enter a retry
loop.

Do not use this recovery path for a cloud_required acceptance test. That mode
must expose the cloud result without masking it with local execution.

Python package recovery does not authorize installing native programs such as
LibreOffice, Tectonic, qpdf, browser runtimes, or `md2pdf`. Ask the user before
installing a system-level dependency, and only when the requested operation has
no Python-only route. For a Python-script route, if Python or `pip` itself is
absent, surface that boundary instead of bootstrapping a runtime.

Some legacy, local-only scripts still print historical install hints. Treat
them as diagnostics and apply only the bounded policy above.

---

## Output contract

When you finish a run, return: route used · input file(s) · output file(s) · key settings that mattered · validation result (if applicable) · any warnings or uncertainty. Skip fields that don't apply — don't pad. A run is complete when the user has the artifact and the summary reflects any limitations honestly.

---

## Quick workflows

Common copy-paste starting points. For everything else, see the per-route notes above or run `python scripts/<script>.py --help`.

### Fill a PDF form

**AcroForm path:**
```bash
python scripts/inspect_form.py form.pdf --output fields.json --check-overlaps
# edit fields.json to set fill_value
python scripts/fill_form.py form.pdf fields.json filled.pdf
```

**Overlay path (flat / scanned):**
```bash
python scripts/inspect_form.py form.pdf --output detect.json    # confirm form_type
python scripts/extract_form_structure.py form.pdf structure.json
# agent composes fields.json from structure.json (schema in references/forms-guide.md)
python scripts/check_bounding_boxes.py fields.json
python scripts/fill_form_overlay.py form.pdf fields.json filled.pdf
python scripts/convert_pdf_to_images.py filled.pdf --output verify/ --dpi 150    # eyeball
```

### Render to images / extract / generate

```bash
# Render for vision reading
python scripts/convert_pdf_to_images.py scan.pdf --output images/ --pages 1-5 --dpi 150

# Extract tables to Excel
python scripts/extract_tables.py report.pdf --output tables/ --format xlsx

# Markdown/MDX → PDF (cloud when compatible, otherwise a validated local path)
python3 scripts/generate_mdx_pdf.py report.mdx --output result.pdf --page-size A4

# HTML → PDF
python scripts/html_to_pdf.py page.html --output result.pdf --page-size a4 --margin 20mm
```

> Do not write your own `markdown_to_pdf.py` / `html_to_pdf.py` / `create_*_pdf.py`. If the skill script errors out, re-read its `--help` — every CLI prints a usage example. Ad-hoc scripts miss CJK font registration, aspect-ratio handling, atomic writes, and splittable code blocks.

### Split / merge / watermark / crop

```bash
# Split specific pages (contiguous or not)
python scripts/batch_ops.py --action split --input book.pdf --pages 1,3,7-9 --output selected.pdf

# Split by bookmarks (one file per chapter)
python scripts/batch_ops.py --action split-by-bookmarks --input book.pdf --output-dir chapters/

# Merge
python scripts/batch_ops.py --action merge --inputs a.pdf b.pdf c.pdf --output merged.pdf

# Text watermark (CJK works)
python scripts/batch_ops.py --action text-watermark --input doc.pdf --text "机密" --output wm.pdf

# Crop / N-up compose — see python scripts/crop_compose.py --help
```

### Encrypt

```bash
python scripts/secure_pdf.py --action encrypt --input doc.pdf --output secured.pdf \
    --user-password "read123" --owner-password "admin456"
```

---

## References

Read when you need deeper context — not by default.

- `references/extraction-guide.md` — multi-column text, table edge cases
- `references/forms-guide.md` — AcroForm vs layout-based, coordinate systems, fields.json schema
- `references/generation-guide.md` — HTML / Markdown / LaTeX generation
- `references/security-guide.md` — encryption, redaction, metadata
- `references/advanced-libraries.md` — pypdfium2 / pdf-lib / qpdf recipes
- `references/troubleshooting.md` — common failure modes

---

## State handling

Stateless. Treat intermediate JSON / debug PNGs / OCR outputs as temporary unless the user asks to keep them. Re-run inspection for each new or materially changed file.

