photo-to-scanned-pdf
Two pipelines ending at a scanner-look PDF. (1) Phone photos of paper documents (contracts, stamped certificates, receipts, forms, handwritten notes) → clean scanner-quality PDF: perspective rectification + noteshrink whitening + A4 assembly + mandatory whole-document check. Trigger: "把照片 做成扫描件", "photos to scanned PDF", "make this look scanned", "手机拍的 文档转 PDF", "盖章文件扫描", replacing pages in an existing scanned PDF, any CamScanner-like request. (2) A digital document with no signature yet (rendered docx/PDF, confirmation form, contract draft) → make it look hand-signed and scanned: synthesize a handwriting-style signature, composite it onto the signature line, apply the same scan-look post-processing. Trigger: "帮我做个手写签名", "生成签名盖到这份文件上", "做成签过字的扫描件", "synthesize a signature", any request for a document that needs to look signed without a real photographed signature. Do NOT hand-roll levels/contrast enhancement for scan-look — tried and rejected twice; this skill's pipeline is the proven one.
1How It Works & Mechanism
Both pipelines share the same target aesthetic and design philosophy: scripts strictly handle execution, while the agent retains two non-automatable human judgments—content-based page sorting and full-document verification.
**Division of labor**: scripts carry execution; you (the agent) carry the two judgment steps — content-based page ordering, and whole-document verification. Neither can be automated away: filenames lie about order, and per-page spot checks miss wrong-slot bugs.
Perspective rectification and white balance use in-house OpenCV scripts: photo_to_scan.py detects the largest bright quad and warps it to A4 (200 dpi); --raw mode only rectifies, while default mode additionally divides by the paper background color for pre-whitening colored pages. EXIF rotation is pre-fixed via PIL exif_transpose (cv2.imread ignores EXIF).
The script handles EXIF rotation internally (`cv2.imread` ignores EXIF; phone photos come rotated — this silently produces sideways pages if you rectify with raw OpenCV).
The 'scanned look' is achieved via palette quantization with noteshrink (uvx external tool): white paper batches use -w -g -K once for a global palette to unify ink/stamp colors, and -c "true" skips its internal PDF generation step so this skill handles assembly.
uvx noteshrink -w -g -K -q -b ns -c "true" page_03.jpg page_01.jpg page_07.jpg ...
A4 assembly includes margin trimming: assemble_pdf.py crops 24px top / 12px sides (200 dpi) per page to remove desktop edges introduced by rectification, and img2pdf embeds indexed-color PNGs directly without re-encoding, outputting ~0.05MB/page.
Edge crop (default 24px top / 12px sides at 200 dpi) removes the sliver of desk surface that rectification drags in along page borders; document margins dwarf it.
Verification discipline requires 'mandatory contact sheet review of the full document': make_contact_sheet.py generates an overview sheet where the agent inspects page order, color cast, stamps, and signatures page-by-page, followed by spot-checking 1–2 pages at full resolution for sharpness.
**"I verified the pages I changed" is not verification.**
The digital signature branch (digital-signature-synthesis.md) is executed via synthesize_signature.py across subcommands (fonts/candidates/generate/locate/composite/scanify); fonts must be picked by the user from a candidates sheet (agent must not choose), with --seed ensuring reproducibility.
**Send the candidates sheet to the user and get a number back — do not pick one yourself.** Which style reads as a plausible signature is a taste call, not something derivable from the document or the font's technical properties.
Synthesized signature positioning relies on pdftotext -bbox to locate signature line labels, with built-in fail-safes: when a label and underline merge into a single bounding box word, it prints a WARNING and guides scanning the right edge by pixels to prevent the signature from floating.
This means `pdftotext` tokenized your label together with whatever sits right after it on the same line — most commonly a signature blank authored as underscores immediately following the label with no space (`簽名:_____`, an entirely ordinary way to type this in Word).
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| cli | uvx noteshrink(扫描感增强,PEP 723 首跑 ~30s 构建) |
| cli | poppler pdftoppm / pdftotext(contact sheet / -bbox 定位) |
| package | opencv-python-headless / numpy / pillow / img2pdf(PEP 723 内联依赖) |
| cli | LibreOffice soffice(签名分支把 docx 渲染成页图) |
| cli | fc-list(fontconfig,发现手写体字体) |
4Recommended Governance & Evidence
- scripts/: Execute local scripts or commands: python photo_to_scan.py / assemble_pdf.py / make_contact_sheet.py / synthes
- 外部端点: Exfiltrate local content or conversation to third-party cloud storage
- 本地工作目录: File read/write and artifact output: rectify/enhance intermediate PNGs, assembled scanned.pdf, contact.png written to user-specified working directory; signature branch writes signatures
5Second-Pass Verification
- [ok] 主流水线 rectify→noteshrink→assemble→contact sheet — 四个环节命令与脚本均在;photo_to_scan.py --raw 与默认白平衡模式分支存在。
- [ok] EXIF/quad/白平衡实现 — load_exif_corrected 用 ImageOps.exif_transpose;find_page_quad/warp_a4/flatten_whitebalance 函数在 photo_to_scan.py 源码可定位。
- [ok] 边缘裁剪默认值 24/12px — assemble_pdf.py argparse default crop_top=24/crop_side=12 与 SKILL 一致。
- [ok] noteshrink 参数 -w -g -K -c "true" — SKILL 命令与外部工具语义一致(noteshrink 为 uvx 外部包,未在本仓捆绑,命令调用方式可核)。
- [ok] 『只查改动页不算验证』纪律 — SKILL 原文 + 两个 shipped-bug 叙事(错槽覆盖邻页、调色板污染)在 Step 5。
- [ok] 签名分支六子命令 — synthesize_signature.py subparsers fonts/candidates/generate/locate/composite/scanify 均实现;candidates→用户挑选→generate 流程与 reference 一致。
- [ok] 候选人选门禁(agent 不自选字体) — reference 原文 'do not pick one yourself';脚本 generate 需 --font/--face/--ink 显式参数。
- [ok] 无网络/凭证/删除面 — 全脚本扫描零网络代码、零 env 密钥读取;无删除用户文件逻辑;子进程均列表参数。
6Verdict & Scenarios
0e8f936e0a123f3a…