关于 photo-to-scanned-pdf
将已签字或盖章纸质文件的手机照片处理为扫描风格 A4 PDF 的技能。它结合图像处理脚本、按内容排序页面,以及必须进行的整份文档目视检查。
主要功能
- 01透视校正与旋转
- 02按内容排列页面
- 03白纸和彩纸分别处理
- 04组装 A4 PDF
- 05联系表整份核验
适用场景
- 01将纸质文件照片转为 PDF
- 02处理含彩纸页面的文件
- 03替换现有扫描 PDF 中的页面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ daymade/ claude-code-skills --skill photo-to-scanned-pdf安装后在 Claude Code 中输入/photo-to-scanned-pdf确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/digital-signature-synthesis.mdscripts/assemble_pdf.pymake_contact_sheet.pyphoto_to_scan.pysynthesize_signature.py详细介绍
照片 → 扫描版 PDF
两个相关流程,最终呈现相同,起点不同: 纸质文档的手机照片,或需要添加合成签名、让它看起来像已签署的数字文档。以下介绍有效的流程,以及跳过后会导致 PDF 出错的失败模式。
你需要哪一种?
| 输入是…… | 使用 |
|---|---|
| 已签名/盖章纸质文档的手机照片 | 本文件,见下方主流程 |
| 尚无签名、需要制作成手写签署效果的数字文档(docx/PDF) | references/digital-signature-synthesis.md |
photos ──► rectify (photo_to_scan.py --raw)
──► ORDER BY CONTENT, detect colored paper ← agent eyes, not filenames
──► enhance: noteshrink (white batch with -g │ colored pages separately,
after white-balance pre-pass)
──► assemble_pdf.py → A4 PDF
──► make_contact_sheet.py → READ IT, verify EVERY page ← mandatory
分工:脚本负责执行;你(Agent)负责两个判断步骤——基于内容排列页面顺序,以及核验整份文档。这两步都无法自动化:文件名会误导顺序,而逐页抽查会漏掉位置错误。数字签名分支也遵循相同理念,其中有自己的两个判断环节——参见参考文件。
步骤 0 — 依赖项
which pdftoppm || brew install poppler # contact sheet + any PDF rendering
uvx noteshrink --help | head -3 # first run builds it (~30 s)
脚本是通过 uv run 运行的单文件脚本(PEP 723);首次运行时会自动解析 OpenCV/PIL/img2pdf。
步骤 1 — 校正
uv run <skill>/scripts/photo_to_scan.py --raw --out-dir work --prefix page \
photo1.jpg photo2.jpg ...
预期结果:每张照片生成一个 page_NN.jpg,并标记为 [quad]。标记为 [FULLFRAME-fallback] 表示未找到纸张轮廓(背景杂乱、页面被截断)——查看该照片并决定:重新拍摄,还是接受未裁剪的画面。
脚本会在内部处理 EXIF 旋转(cv2.imread 会忽略 EXIF;手机照片带有旋转信息——如果直接用原始 OpenCV 图像进行校正,页面会悄无声息地变成横着的)。
步骤 2 — 按内容排序并检测彩色纸张(Agent 判断)
阅读每张校正后的图像(每条消息批量处理约 6 张),并记录两项信息:
- 页面标识——日期、标题、页码,以及任何能区分页面的信息。批量导出的照片(微信、AirDrop)会带有导出时的时间戳,通常所有照片的时间都只相差一秒以内——文件名顺序毫无意义。实际案例:17 张照片的顺序恰好与文档顺序完全相反;只有阅读内容才能发现这一点。
- 纸张颜色——白色还是彩色(蓝色/黄色/粉色纸张)?彩色页面在步骤 3 中要采用不同流程。如果不确定,可通过程序采样:取空白区域的 RGB 平均值;
B > R + 25⇒ 纸张偏蓝。
继续之前,先将最终页面顺序整理成明确列表。如果页面需要与外部登记表(发票清单、场次表等)对应,现在就核对页面标识——此时发现缺页/重复页只需几秒;交付后才发现则需要返工。
步骤 3 — 增强(noteshrink,按纸张颜色拆分)
白纸页面——统一批处理,使用全局调色板:
uvx noteshrink -w -g -K -q -b ns -c "true" page_03.jpg page_01.jpg page_07.jpg ...
# inputs IN FINAL PAGE ORDER → outputs ns0000.png, ns0001.png, ... in that order
-w设置白色背景,-g使用一个全局调色板(确保所有页面上的墨迹/印章颜色一致),-K保持给定顺序,-c "true"跳过其内部 PDF 步骤(由我们自行组装)。- 显式传入文件名。 zsh 不会对
$VAR进行按空格拆分——变量中的文件列表会被当成一个巨大的“文件名”传入,noteshrink 会在不输出任何结果的情况下退出,而-q会让它保持静默。请确认输出文件确实存在(ls ns0*.png),不要只相信标准输出。
彩色纸张页面——单独处理,并先进行白平衡预处理:
uv run <skill>/scripts/photo_to_scan.py --out-dir work --prefix wb colored_photo.jpg # no --raw
uvx noteshrink -w -g -K -q -b nc -c "true" work/wb_01.jpg
以下两种不同的失败模式要求将两者分开处理(规则制定前,这两种情况都曾作为 bug 交付):
- 彩色页面混入
-g批次会污染整份文档——纸张颜色会进入全局调色板,导致白纸页面的阴影/瑕疵带有色调。 - 仅用 noteshrink 处理彩色纸张时,背景会变白,但前景色偏不会消除——在蓝色纸张上拍摄的黑色墨迹会呈蓝紫色,红色印章会呈暗红色。
photo_to_scan.py的默认(非--raw)模式会先除去纸张颜色,使墨迹恢复黑色,印章恢复红色。
步骤 4 — 组装
uv run <skill>/scripts/assemble_pdf.py --out scanned.pdf \
ns0000.png ns0001.png nc0000.png ns0002.png ... # FINAL page order
预期结果:OK scanned.pdf (N pages, ~0.05 MB/page)。边缘裁剪(默认在 200 dpi 下裁掉顶部 24px、两侧各 12px)会去除透视校正沿页面边缘带入的一小条桌面;相比之下,文档边距宽得多。
步骤 5 — 验证整个文档(必须执行,不可跳过)
uv run <skill>/scripts/make_contact_sheet.py scanned.pdf --out contact.png
读取 contact.png 并检查每一页:身份顺序完整且正确(每个日期/标题都在应在的位置,没有重复,也没有遗漏),没有偏色页面,印章/签名均在。然后以全分辨率抽查 1–2 页的文字清晰度。
为什么每次都要检查整个文档的每一页:这个技能源自本次会话,期间出现过两个已交付内容中的错误案例——
- 一项页面替换任务把新页面写入了错误的槽位(复制命令中有一个差一错误),悄无声息地覆盖了相邻页面。被替换槽位的逐页检查通过了;被覆盖的邻页只有用户发现了。
- 调色板污染错误(步骤 3 #1)给未编辑的页面也染了色。只检查编辑过的页面没能发现问题。
成本差异是绝对的:联系表 = 一次 Read;交付的 PDF 中有错误页面 = 重做 + 失去信任。“我验证了修改过的页面”不等于验证。
替换现有扫描 PDF 中的页面
将每页增强后的 PNG(ns*/nc*)保留为工作文件集。要替换第 k 页:将新照片按步骤 1–3 处理,覆盖该页的 PNG,然后重新运行步骤 4–5。复制到编号槽位时要注意映射关系——照片顺序被反转时,槽位编号也会变化;应根据页面的内容身份确定槽位,绝不能根据它在照片批次中的位置来确定。之后,步骤 5 的完整检查才是真正的保障。
故障排除
| 现象 | 原因/解决方法 |
|---|---|
| 输出“看起来不像扫描件”——灰蒙蒙的,文字发虚 | 你手动调了色阶/曲线/除法增强。不要这样做——切换到 noteshrink 之前,真实用户拒绝了两次尝试(背景采样 + 调色板量化才是生成纯白扫描效果的关键)。 |
| 白色页面的阴影带有色调 | -g 批次中包含了彩色纸张页面。重新运行仅处理白色页面的批次(步骤 3)。 |
| 彩色页面上的墨迹呈蓝色/紫色,印章呈栗色 | noteshrink 直接处理了彩色页面。插入白平衡预处理步骤(运行 photo_to_scan.py,不带 --raw)。 |
| noteshrink 没有输出,也没有报错 | 在 zsh 下通过未加引号的 shell 变量传入文件列表(不会进行单词拆分),或者路径中包含空格。传入明确的文件名;检查 ls ns0*.png。 |
| 页面横置/倒置 | 上游某处忽略了 EXIF,或者检测到的四边形是横向的。photo_to_scan.py 会校正 EXIF 并旋转为纵向;它无法判断页面是否倒置——应在步骤 2 中发现并旋转源照片。 |
照片上出现 [FULLFRAME-fallback] | 未检测到纸张轮廓(与桌面的对比度低,或页面被截断)。在深色背景下重新拍摄,或者接受完整画面并依靠边缘裁剪。 |
| PDF 页面边缘有细窄的深色条带 | 透视校正带入了桌面。调高 assemble_pdf.py 中的 --crop-top/--crop-side。 |
| PDF 中页面顺序错误 | 假定文件名顺序。顺序根据步骤 2 的内容读取结果确定,并明确传递给 noteshrink(-K)和 assemble_pdf.py。 |