Pdf Toolkit
PDF operations Jarvis can shell out to: FILL a user's PDF template in place (same font/size/position — never redraw it), list a template's fields, merge PDFs/images, split by page range, compress, image(s)→PDF. Use fill whenever the user has a template (invoice, quotation, 報價單, 發票, 收據, form) and wants a new one with different values: '用返個 template', '跟住呢張出多張', '幫我出張 invoice'. Also '合併', '拆開', '壓縮 PDF'.
Skill metadata
| Source | Bundled (installed by default) |
| Path | skills/document/pdf-toolkit |
| Version | 1.0.0 |
| Author | Claway Agent |
| License | MIT |
| Platforms | linux |
| Tags | pdf, document, template, fill, invoice, 報價單, 發票, merge, split, compress, image-to-pdf |
Reference: full SKILL.md
The following is the complete skill definition that Claway loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
PDF Toolkit Skill
A single Python script (scripts/pdf_toolkit.py) that wraps the PDF operations Jarvis was lacking. Each sub-command prints the output absolute path on success so callers can shell-out and pick it up with MEDIA:<path> for delivery back to WhatsApp / Telegram.
When to use
Trigger phrases (mostly Cantonese / Mandarin / English):
- merge — "合併 PDF", "combine these PDFs", "把這幾個 PDF 合成一個", "把這幾張圖整合做一個 PDF" (images get auto-converted first)
- split — "拆開呢個 PDF", "split pages 3-7 out", "把第 1-5 頁抽出嚟"
- compress — "壓縮 PDF", "shrink this PDF", "個 PDF 太大幫我細啲"
- img2pdf — "把張相轉做 PDF", "把這幾張圖做成 PDF" (merge also handles this when input mixes PDFs + images)
- fill — "用返個 template 出張 invoice", "跟住呢張 template 改個名同金額", "幫我出 11073 嗰張", any request to produce a document from a template the user uploaded
If the user only asks Jarvis to READ a PDF's content, this skill is NOT needed — the WhatsApp adapter already injects extracted text inline.
Quickstart
# Resolve for either install layout (source tenant vs binary tenant) — do not hardcode.
SKILLS=${CLAWAY_HOME:-$HOME/.claway}/skills
[ -d "$SKILLS" ] || SKILLS=$HOME/.claway/claway/skills # git install
SCRIPT=$SKILLS/document/pdf-toolkit/scripts/pdf_toolkit.py
PY=$HOME/.claway/claway/venv/bin/python
[ -x "$PY" ] || PY=python3 # binary install has no venv
# Merge: PDFs and/or images (any order; images get converted on the fly)
$PY $SCRIPT merge --out /tmp/combined.pdf /tmp/a.pdf /tmp/b.png /tmp/c.jpg /tmp/d.pdf
# Split: extract pages 3..7 (1-indexed, inclusive)
$PY $SCRIPT split --in /tmp/big.pdf --pages 3-7 --out /tmp/pages_3-7.pdf
# Multiple ranges:
$PY $SCRIPT split --in /tmp/big.pdf --pages "1-2,5,8-10" --out /tmp/excerpt.pdf
# Compress: Ghostscript /ebook (default), /screen (smallest), /printer (medium), /prepress (largest)
$PY $SCRIPT compress --in /tmp/big.pdf --out /tmp/small.pdf --quality ebook
# Image(s) → PDF (alias for `merge` when all inputs are images)
$PY $SCRIPT img2pdf --out /tmp/photos.pdf /tmp/p1.jpg /tmp/p2.png /tmp/p3.heic
Filling a template (invoice / quotation / form)
Never redraw a user's template. Rebuilding it with reportlab from a
description can only ever be "close" — wrong face, wrong sizes, wrong gaps —
and the user will see every difference. The template is the master: fill
redacts the old values and writes the new ones at the same origin, in the same
font file, at the same size. Everything else on the page is untouched
(measured: 0 pixels differ outside the edited fields).
# 1. See what the template offers (label: value pairs + every text span)
$PY $SCRIPT extract-fields --in ~/templates/invoice-11072-v4.pdf --out /tmp/inv.fields.json
# p1 '訂單編號' = '11072' p1 '訂購人' = 'Cat' p1 '電話號碼' = '' ...
# 2. Fill by field name (the label text without its colon). Empty labels are fillable too.
$PY $SCRIPT fill --template ~/templates/invoice-11072-v4.pdf --fields /tmp/inv.fields.json \
--out ~/MLI_Invoice_11073_郭富城.pdf \
--set "訂單編號=11073" --set "訂購人=郭富城" --set "訂單日期=30-Aug-26" --set "電話號碼=9123 4567"
# A value with no label (a title, an amount in a table cell): replace the exact span text
$PY $SCRIPT fill --template t.pdf --out o.pdf --replace "\$2,350=\$4,800"
# Many values at once
$PY $SCRIPT fill --template t.pdf --out o.pdf --values /tmp/values.json # {"訂單編號": "11073", ...}
Read extract-fields output first when unsure of a name — fill refuses an
unknown field and lists the known ones. It also verifies its own output: the
new values must come back from the saved file's text layer, or it exits 1.
What it will and will not do:
- Width. A value that would run into the next column is shrunk by up to
30% and reported on stderr (
shrunk 11pt → 9.5pt); one that still does not fit is refused with the measurement. Shorten it or split the field — do not work around this by editing coordinates. - Fonts. The template's own face is used when it is installed on the
host (the fleet boxes have
NotoSerifCJKtc-Regular); otherwise a built-in CJK/Latin fallback is used and a warning names the missing font. - Line items. Replacing values in existing rows works. Adding or removing rows does not (v1) — the rows that exist are the rows you can fill. For a document that needs more rows than the template has, say so rather than squeezing.
- Scanned PDFs (no text layer) cannot be filled this way;
extract-fieldsfinds nothing. That is an OCR task.
After filling, deliver with MEDIA:<path> as usual. If the user asks whether it
matches, render page 1 and look (vision_analyze accepts the PDF directly).
Failure modes & expected behaviour
- Missing input file → script prints
error: <path> not foundto stderr, exits 1. - Mixed inputs in
merge→ images convert to PDF in a temp dir, thenpypdf.PdfWriterconcatenates everything. Original images untouched. compressuses Ghostscript-dPDFSETTINGS=/ebookby default. If the user wants the smallest possible output (heavy quality loss acceptable), pass--quality screen.splitpage range is 1-indexed and inclusive —3-7means pages 3,4,5,6,7 (5 pages). Out-of-range pages print a warning to stderr but the rest still copy.- HEIC / WEBP support depends on system ImageMagick + libheif being compiled in. On the prod server
convertis GraphicsMagick-flavoured and handles JPG/PNG/HEIC; if a user attaches an unusual format and the convert fails,mergeskips it and continues with a stderr warning.
Output handling
The script writes to the --out path the caller provides. For Jarvis to actually deliver it to WhatsApp, include MEDIA:<absolute-path> in the agent's response — the gateway's WhatsApp adapter picks up MEDIA: references and routes them through send_document().
Related skills
creative/hidream-image-generation— generate fresh images (not stitching existing ones)creative/comfyui— image transforms via ControlNet