扫描件复原转 Word
目的:把"只有图像的扫描件"变成"可编辑、可填写、且与原件等效的文档"。
本质困难:反推是欠定推断——扫描件是文档的有损投影,往回推一定有错,而且错误在成品上看不出来。
(实证:验收数据"均差 0.15pt / IoU 0.994",实际 21 项里错 17 项,见 references/像素反推误差档案.md。)
所以本技能要交付的是"一份带可信度标注的文档",不是"一份看起来像的文档"。
"像"是手段,"能改能填、不被退回"才是目的。
一、执行卡
进门三问(不答不动手)
| 问 | 为什么 |
|---|---|
| 要「能填能改的附件本身」,还是「整页视觉复刻」? | 两者验收标准不同,混了必错(默认前者) |
| 这份文件的 Word 原件能拿到吗? | 拿到就用原件,本技能全部推断作废 |
| 扫描件多少 dpi? | 决定能承诺到什么程度——承诺之前先分级 |
「先定交付目标」的差别(默认按左列做):
| ① 还原文档(默认) | ② 复刻页面画像(用户明说才做) | |
|---|---|---|
| 页眉页脚 | 只保留属于该文档的;发文通知的版记/页码一律不要 | 照页面所见全部保留 |
| 页边距 | 照原件声明值 | 照页面实测值 |
⚠️ 动手前先分清:这一页上有几个文档。 从公文里抽出的一页,页脚的版记(抄送/印发)与
页码属于发文通知,不属于附件本身。判别法:原件 Word 里有没有 footer/header 文件、
附件标识是否另起一行顶格。我曾把版记 + 3 条版记线 + 页码一起复刻进附件,多出 5 个元素。
输入质量分级(先分级,再承诺)
| 分辨率 | 能承诺 | 不能承诺 |
|---|---|---|
| ≥300dpi、字体齐全 | 保真复刻:几何 ≤0.5%、字号 ±0.2pt、字体到"款" | — |
| 200~300dpi | 版式复刻 + 字体只到"族"(宋/黑/楷) | 具体字体款名 |
| 150~200dpi | 内容+版式近似 | 任何"复刻"措辞 |
| <150dpi | 只能给内容(OCR 级) | 保真 |
五条不许破的线
- 先要原件——有原件则一切推断作废
- 先验只缩范围,不下结论
- 每一处与原件的不同,都要能回答"原件是不是本来就这样"——答不出就是我自己加的
- 工具不能倒逼内容——手段不好用就换手段,别改被验收的文档
- 交付时按来源分层声明(原件 / 实证 / 反推),不把推断说成事实
误差预算(力气按容差分,不按"哪里容易测")
结构性属性容差为 0(错了没法用别的参数补);度量性属性有容差。
| 参数 | 容差 | 超了会怎样 |
|---|---|---|
| 元素归属(哪些属于本文件) | 0 | 交付物性质变了(多/少整个元素) |
docGrid / 行距 |
0 | 整块位移,无法用间距补回(实测 36.5pt) |
| 段落数 / 空段 | 0 | 版面整体位移 |
| 全/半角、数字形态 | 0 | 字形与字宽都不同,换行随之变 |
| 字号 | ±0.2pt | 换行漂 1 字;填空线断点错位 |
| 表格列宽 | ±0.5pt/列 | 单元格内折行数变化(直接影响可用性) |
| 页边距 / 版心 | ±0.5pt | 整块水平位移 |
| 表格总宽 | ±1pt | 与文本列的溢出量变化(观感) |
| 下划线纵向位置 | ±1.5pt | 观感 |
| 边框粗细 | 0.5~0.75pt 皆可 | 观感(黑白打印更明显) |
⚠️ 三个数字别混用:±0.2pt = 版面敏感阈值(判断值不值得死磕);≤0.5pt = 取整档闸门;
±0.5pt = 对外声明的不确定度。
能力清单(未实测的不许说成"支持")
| 场景 | 状态 |
|---|---|
| 段落式填空表单(无框线) | ✅ 已实测 |
| 带框线表格、无合并单元格 | ✅ 已实测 |
| 带框线表格含合并单元格 | ⚠️ 方法可行("缺线"反推 gridSpan/vMerge),未实测 |
| 无框线、靠空白对齐的表 | ⚠️ 未做,易多列/少列 |
| 跨页表(表头行重复) | ⚠️ 未做,需 w:tblHeader |
| 印章/手写 | ❌ 不复刻;留白或标「(此处盖章)」「(签名)」 |
| 已有文本层的 PDF | ❌ 不适用(走文本抽取) |
拿不到原件时:可证伪五件套(至少做前四条)
- 内部一致性:列宽合计 = 表格宽 = 版心(或已知溢出量);行高合计 + 上下留白 = 页高
- 多分辨率交叉:同一元素在 200/300/600dpi 各测一次,结果应收敛
- ≥3 个独立样本:字体与字号由 3 处不同位置交叉得出,彼此差异 <0.1pt
- 误差声明:写明这是反推件,并给范围(几何 ≤0.5% / 字号 ±0.5pt / 字体族级确定、款级存疑)
- 可用性抽检:填入示例数据看折行与串行(见第 5 步)
二、流程
第 0 步 先要原件(最高优先)
问一句:"这份文件是哪个部门发的?他们那边有没有 Word 原版?能拿到我就不用从扫描件反推了,改出来零误差。"
拿到原件后只剩一件事:核对原件与手上的扫描件是否同版(机关可能事后改过)。判据:
| 判据 | 同版特征 |
|---|---|
| 横向 | 表格线总跨距比值≈1(允许 0.5% 打印/扫描缩差) |
| 纵向 | 行高总和比值≈1(纵向通常不被缩放) |
| 平移 | 只允许整体位移;个别元素错位就要怀疑不同版 |
不同版时以扫描件为准(用户手上那份才是他要的结果),原件值当强候选逐一确认,并把差异写进交付说明。
第 0.1 步 判断是不是扫描件 + 有没有整页缩放
import fitz
pg = fitz.open(path)[0]
print(len(pg.get_text()), pg.get_images(full=True)) # 文本 0 + 有图 = 扫描件
再算 图片像素宽 ÷ 页面宽(pt) × 72:等于 300 说明 1:1 扫描,测得尺寸即原件真实尺寸;
不是整数说明被缩放过,此时只能复刻比例、不能照搬绝对值。
第 0.5 步 问发布机关 → 查先验(只缩范围)
问:"这份文件是哪个机关发布的?知道机关我就能先把候选字体缩小,测出来和惯例冲突时也能立刻警觉。
不知道就说'不清楚',我按纯实测走。"
然后查 references/发文机关先验.md(含来源分级:【原件】/【实证】/【反推】/【待验】)。
⚠️ 读条文找「限定词」(这条能救你):带"一般 / 推荐 / 如无特殊说明"的是可调整项,必须实测;
带"应当 / 必须 / 用…字"(无限定词)的才是硬规定,可直接照用。
例:GB/T 9704 附件标识「用3号黑体字」是硬的(实测吻合);正文「一般用3号仿宋」是软的
(实测为楷体,不违反国标——同条明文允许"特定情况作适当调整")。
别拿"公文正文=仿宋三号"去套表单——国标管"公文版式",不管"表单内容"。
先验只用来缩范围:用 identify_font.py --filter 跑完,必须再跑一次全量对照——
全量第一名若落在候选集之外,说明先验失准,信实测,并把这次实测回填进先验表。
> 先验按机关存,但同一机关内部不同元素字体也会不同,每个元素都要单独实测。
第 1 步 字体 + 字号双定标(scripts/identify_font.py)——不可跳过
字体靠目视=赌博,用掩膜 IoU 网格搜索给数据。
python3 scripts/identify_font.py --pdf scan.pdf --probe # ① 探行带坐标
python3 scripts/identify_font.py --pdf scan.pdf --text "授权委托事项:" --rect 76 466 173 484
python3 scripts/identify_font.py --pdf scan.pdf --text "…" --rect … --filter 楷体 # ③ 先验候选集
- 判定规则:第一名字形分 ≥ 次名 1.15 倍(差 ≥0.15)可下结论;<1.08 倍必须换行交叉验证。
同设计变体(简体/繁体/_GBK)脚本已归并,别被"次名是自己"骗了。 - 两侧必须同等膨胀(默认
--dilate 1):只膨胀原件会系统性偏袒粗笔字体。 - 短串要连全/半角一起判:把
附件1(全角)与附件1(半角)各跑一次比分数。
⚠️ 全/半角必须每案单独判,不要沿用上一个案子。 - 字号三口径互校:墨迹宽法(脚本自带)/列投影字宽法(
(末-首)/(n-1))/反证法(换字号渲染比几何)。
⚠️ 列投影在短串(≤4 字)上不可信,必须用 ≥10 字的串。
⚠️ 反证法用逐行墨迹投影相关,别用"墨迹带条数"(下划线与文字并带,条数天然不等)。 - 小字号 + 长串 IoU 会失效(200dpi、12pt:各候选 IoU 都 0.30~0.37)。两条兜底:
① 降到族级结论(写"宋体/黑体/楷体");
② 改用墨迹密度(墨迹像素 ÷ 墨迹包围盒面积,同口径)——
楷体≈15~17 / 书宋≈17~20 / 黑体≈27~28 / 宋体≈30~34(数据见references/像素反推误差档案.md)。
三条限制:只能排除不能定案(样本量 2、与字重强相关);看量级与排序别抠绝对值;
候选必须用本机真实字体,先fc-match <字体名>查映射(本机 macOS:黑体→Noto Sans SC、
宋体→Songti SC,不是 SimHei/SimSun,密度不可跨机照抄)。 - 字号优先取整档(22/16/14/12/11pt)。要动字符缩放
w:w必须三条闸门全过:
① 实测与整档差 ≤0.5pt 就直接写整档;② 该值由 ≥3 个独立样本交叉得出、彼此差 <0.1pt;
③ 有原件时一律照抄声明值。写法见references/构建规范.md。
> 反例:原件标题本是干净的整档值,我测出小数后写「下调 0.5pt + 缩放 102%」去凑——把测量噪声写进了文档。
> (具体数值见references/发文机关先验.md案例 #2)
第 2 步 量几何(页面 / 表格)
- 页面:页宽高、四边边距、页脚距离、
w:docGrid(照抄)、版心宽。 - 表格网格:先用
cv2.HoughLinesP测倾角(歪的先转正),再形态学开运算分离长横线/长竖线,
横线数−1 = 行数、竖线数−1 = 列数,线中心距即列宽/行高。
所有竖线纵贯全表 ⇒ 无纵向合并;某行横线缺段 ⇒ 该处有gridSpan。
表内文字先cv2.subtract(bw, bitwise_or(横线, 竖线))去线再取范围。 - 短串(页码级)再加"分段拟合":比分段模式(段宽+段间距),只比总宽会选错。
第 3 步 构建
# 表格式:参数驱动,脚本不猜任何值
python3 scripts/build_table_docx.py spec.json -o out.docx
# 段落式:脚本内含案例 #1 的参数(改成你自己的实测值)
python3 scripts/build_form_docx.py
- spec 结构见
references/table-spec-模板.json;经原件核对的样例见references/table-spec-律协附件3.json。
spec 必填_sources(每个参数的来源等级)。 - 构建要点清单(细节与原理见
references/构建规范.md):
w:rFonts三属性齐设(ascii/hAnsi/eastAsia);ASCII 字体必须是 Times New Roman(决定_下沉);
关autoSpaceDE/DN;行距用lineRule="exact";长填空线手动<w:br/>断行并每行留 8pt 余量;
w:docGrid照抄原件(或全段显式行距,绝不能删网格还留几段没有行距);
w:tblPr子元素顺序是强制的(乱序会被静默忽略);单元格边距用原件值(别乱压tblCellMar);
表格可以比文本列宽(jc=center居中溢出,别改页边距迁就);边框w:sz单位 1/8pt。 - ⚠️ 已知不对称:
spec + _sources + 原件核对样本这套机制目前只有表格分支有;
段落式只有build_form_docx.py(参数写死在脚本里)。下次做段落式案件、且拿到原件时,顺手把参数导成 spec。
第 4 步 校对(渲染器优先 Microsoft Word)
python3 ~/.workbuddy/skills/word-to-pdf-macos/scripts/batch_word_to_pdf.py <out.docx>
- LibreOffice 的纵向位置与折行数在两种情况下不可信:① 文档带
w:docGrid;② 目标字体被回退。
文档无网格且字体齐全时 LO 也可信。根因与实测见references/校对与验证.md。
我曾被 LO 的假失配骗着改了不该改的地方(压单元格边距、把表格改左对齐)——验收工具选错比不验收更危险。 - Word 不可用时(TCC 权限被拒等):换「同引擎独立实现对照」验构建保真度;绝对禁止据 LO 渲染改文档。
- 同引擎独立实现对照(验"构建保真度",不依赖 Word):把原件参数导出成 spec → 跑构建器 →
两份用同一引擎渲染,引擎偏差相互抵消。本案 8 横线 + 9 竖线逐条 Δ=0.00pt ⇒ 构建环节零缺陷。 - 验收线与基准定义见
references/校对与验证.md(有原件 / 无原件两套基准,别混)。 - ⚠️ 从扫描件反推的绝对尺寸带 ~0.5% 系统性偏差,列宽会整体略窄于原件;别声称"0.1pt 级吻合"。
第 5 步 交付前自检(脚本化,不通过不交付)
python3 scripts/audit_docx.py out.docx # 参数全表 + 计数 + 一致性
python3 scripts/audit_docx.py out.docx --spec spec.json # 再与 spec 逐项比对(理想:零差异)
- 数量校验(双向条目比对)——脚本列出段落数/表格行列数/下划线字符数/页眉脚元素/页面元素清单;
与"原件有什么"的集合必须一一对上,多出的要能说出理由。别用眼睛数表格行。 - 逐段核对
w:eastAsia/w:ascii/w:sz/w:w/行距/缩进/w:docGrid/表格列宽行高 —— 读脚本打印的表。 - 每一视觉行宽度 ≤ 可用宽度(CJK 1em、
_0.5em 累加,含缩放)。 - 原文关键句 substring 全在(比对前先去掉
<w:br/>渲染出的\n)。 - 无占位符/模板残留(
[\(\)\[\]\{\}]|\{\{|XX|待填|TODO|示例)。 - 元素归属复核:逐个页眉/页脚/线框元素反问"它属于这份文档,还是属于页面上的其他文档?"
- 无"未经质询的改动"清单:把与原件的每一处不同列出来,逐条回答"原件是不是本来就这样?"——答不出就回退。
- 可用性抽检("像"之外的验收):填一遍——数据格填示例、填空线打字,看
① 折行位置是否变化 ② 是否串行/行高跳变 ③ 是否仍可编辑。精度是手段,"填得进去"才是目的。 - 可信度分层声明落笔(见下)。
三、交付说明要写什么
- 有没有拿到原件:拿到了就说"与原件逐项一致";没拿到必须写明这是反推件并给误差范围。
- 参数来源分层:哪些来自原件、哪些实测、哪些推断(对应 spec 的
_sources)。 - 发布机关与惯例,以及本次实测是否与惯例一致(不一致要点名)。
- 用到的字体名(含 ASCII 字体),方便换机核对。字号若用了字符缩放要说明为什么。
- 哪些元素是我判定"属于/不属于"本文件的,以及依据(附件标识留、版记页码去)。
- 残留差异要点名(哪些没复刻、为什么)。
四、遇到问题去哪查
| 文件 | 管什么 |
|---|---|
references/发文机关先验.md |
机关 → 字体/字号(含来源分级与回填模板);两个案例的逐元素值 |
references/构建规范.md |
OOXML 细节:rFonts/行距/docGrid/字符缩放/下划线字体度量/表格属性与顺序 |
references/校对与验证.md |
渲染器选择与根因/同引擎对照/验收线/阈值调参/三个工具的分工 |
references/像素反推误差档案.md |
「我错在哪」的完整清单、三类归因、反证实验、墨迹密度判别器数据 |
references/坑速查.md |
症状 → 根因 → 解法(通用坑 + 表格专有坑) |
references/table-spec-*.json |
spec 模板 + 唯一经原件核对的样例 |
改完技能本体,跑 python3 scripts/self_check.py(frontmatter/引用完整性/
摘要表与先验表同值/编号不重复/计数口径/脚本冒烟/关键规则在位),不通过不发布。
> 法律科技实务工具 · 维护者陆凌燕律师(北京德恒·无锡)· 关注公众号「鹿鸣于野 UMU」获取更多内容