Compare commits
10 Commits
8e00cef24e
...
9493e7e2fd
| Author | SHA1 | Date | |
|---|---|---|---|
| 9493e7e2fd | |||
| 486ccc202a | |||
| e2c2e7148d | |||
| 9f41d72aab | |||
| 2cd937a22e | |||
| 6b9d19698f | |||
| 4bf5d00346 | |||
| 9f3598d60d | |||
| de1fd6574d | |||
| ac994d4881 |
@@ -58,7 +58,7 @@ https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p
|
||||
|
||||
## 最新更新
|
||||
|
||||
新版将普通材料、预印红头纸套打和完整电子红头分开处理,并补入 Word/WPS 的四级标题样式。完整的更新说明、实测范围和配图见[这次更新说明](marketing/x-v2-release.md)。
|
||||
本次重大更新重新核验了红头场景、首页预留、红线下标题间距、Word/WPS 标题样式、目录引用、特殊格式和跨平台安装,并完成 18 份 DOCX、37 个 PNG 页面和版头坐标量测。完整的更新说明、过程配图和剩余边界见[重大更新说明](marketing/x-major-update.md);上一版记录仍保留在[红头规则更新说明](marketing/x-v2-release.md)。
|
||||
|
||||
## 作者与同名内容 IP
|
||||
|
||||
@@ -86,7 +86,7 @@ https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p
|
||||
| 标题与目录 | 四级标题写入 Word/WPS 标题样式及大纲级别,可插入自动目录 |
|
||||
| 页码 | 居中、单双页或不显示 |
|
||||
| 印章、签名章 | 在生成的 DOCX 中按实际需要插入图片或完成实体盖章 |
|
||||
| 特定格式 | `letter`、`command`、`minutes` 使用对应生成分支 |
|
||||
| 特定格式 | `letter`、`command`、`minutes`、`horizontal-table` 使用对应生成分支;联合行文用 `--joint-org`,分离装订附件用 `--attachment-detached` |
|
||||
|
||||
GB/T 9704-2012 当前为现行标准,2025-05-30 复审继续有效。标准适用于党政机关制发公文,其他机关和单位可以参照执行。[全国标准信息公共服务平台](https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p)
|
||||
|
||||
@@ -97,7 +97,7 @@ node scripts/generate_gongwen_docx.mjs --input tests/fixture.md --output /tmp/go
|
||||
node scripts/verify_gongwen_docx.mjs --input /tmp/gongwen.docx --profile ordinary
|
||||
```
|
||||
|
||||
需要正式发文时,必须明确选择版式。下面的命令生成预印红头纸套打稿;首页只留出纸上已有红头和红线区域,DOCX 不会重绘它们。`72` 是默认预留高度,应按本单位实际红头纸改为 `37—130` 毫米。
|
||||
需要正式发文时,必须明确选择版式。下面的命令生成预印红头纸套打稿;首页只留出纸上已有红头和红线区域,DOCX 不会重绘它们。`--letterhead-reserve-mm` 表示纸张上边缘到预印机关标志区域起点的高度,生成器随后保留两行 28 磅版心空行再排变量文号,标题前再保留红线下两行空行。`72` 是默认值,应按本单位实际红头纸改为 `37—130` 毫米。
|
||||
|
||||
```bash
|
||||
node scripts/generate_gongwen_docx.mjs --input tests/fixture.md --output /tmp/formal.docx --format formal --letterhead preprinted --letterhead-reserve-mm 72 --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "公文格式回归测试"
|
||||
@@ -106,11 +106,11 @@ node scripts/verify_gongwen_docx.mjs --input /tmp/formal.docx --profile formal -
|
||||
|
||||
只有需要完整电子红头文件时,才加 `--letterhead digital`。红头和标题要使用目标电脑中实际安装的小标宋体;生成器缺少小标宋体或仿宋体时会在终端明确警告并写出替代字体,要求严格阻止替代输出时加 `--require-standard-fonts`。红线下标题会写入两个显式的28磅版心空行,不会依赖渲染器自行解释段前距;发文机关标志到文号也按相同方式留出两行。上行文的文号和签发人会在同一行编排。打开文档后,Word/WPS 的“引用→目录”可以按“标题1—标题4”生成并更新目录;是否显示第四级,在目录设置中选择。
|
||||
|
||||
信函、命令(令)和纪要走专用分支:信函生成170mm上粗下细、下页边20mm处上细下粗的两条红色双线,底线放在页脚并关闭页码;命令(令)落实机关标志下空二行、令号下空二行;纪要把“出席”“请假”“列席”标签设为黑体、人员名单设为仿宋。附件可用重复的 `--attachment-file` 另页生成“附件”标签、第三行标题和正文,并保持正式页码连续。带抄送或印发信息时,版记浮动锚定在最后一页版心底部,首末粗线约0.35mm、中间细线约0.25mm。
|
||||
信函、命令(令)、纪要和横排表格走专用分支:信函生成170mm上粗下细、下页边20mm处上细下粗的两条红色双线,底线放在页脚并关闭页码;命令(令)落实机关标志距版心上边缘20mm、令号下空二行、正文下空二行;纪要把“出席”“请假”“列席”标签设为黑体、人员名单设为仿宋,并放在正文或附件说明下一行;横排表格生成横向A4和225mm表格,单双页表头方向仍需目标Word/WPS模板核对。联合行文可重复传入 `--joint-org`,生成主办机关在前、联署机关名称分行排列;输入含“文件”时将“文件”置右并上下居中的标志;不能与正文一起装订的附件加 `--attachment-detached` 会在首行补排文号和附件序号。附件可用重复的 `--attachment-file` 另页生成“附件”标签、第三行标题和正文,并保持正式页码连续。带抄送或印发信息时,版记浮动锚定在最后一页版心底部,首末粗线约0.35mm、中间细线约0.25mm。
|
||||
|
||||
## 跨平台安装
|
||||
|
||||
本项目使用开放的 `SKILL.md` 目录结构,可在 Codex、Claude Code、OpenCode、Trae Code、Trae CLI、Kimi Code CLI、Kimi Code、WorkBuddy 和 ZCode 使用。
|
||||
本项目使用开放的 `SKILL.md` 目录结构,可在 Codex、Claude Code、OpenCode、Trae Code、Trae CLI、Kimi Code CLI、Kimi Code、TraeWork、WorkBuddy 和 ZCode 使用。
|
||||
|
||||
macOS / Linux:
|
||||
|
||||
@@ -135,7 +135,8 @@ Windows:
|
||||
- [生成器](scripts/generate_gongwen_docx.mjs):根据输入生成 DOCX 版式。
|
||||
- [校验器](scripts/verify_gongwen_docx.mjs):检查生成器可承诺的 DOCX 版式要素。
|
||||
- [回归测试](tests/run_tests.sh):运行居中与单双页页码生成、DOCX 包结构与 PDF 转换验证。
|
||||
- [全条款视觉核验](references/gbt9704-visual-audit.md):实际生成 13 份 DOCX、渲染 24 张 PNG,并逐条记录截图证据与人工边界。
|
||||
- [全条款视觉核验](references/gbt9704-visual-audit.md):实际生成 18 份 DOCX、37 个 PDF 页面和 PNG,并逐条记录截图证据与人工边界。
|
||||
- [全条款截图证据矩阵](references/gbt9704-screenshot-evidence.md):把 GB/T 9704-2012 每个条款绑定到实际截图,明确哪些项目截图不能证明。
|
||||
- [版头坐标量测](tests/measure-header-coordinates.mjs):用 PDF 字形坐标独立核对份号首行、机关标志 35 mm 定位及可选字段不移位。
|
||||
- [跨平台安装测试](tests/test-install.sh):隔离环境中验证九个本地目录和 TraeWork 导入包。
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ GB/T 9704-2012《党政机关公文格式》现行,2025-05-30 复审继续有
|
||||
|
||||
本工具只处理排版,不判断单位身份、授权状态或后续盖章方式。它会根据用户明确选择的版式模式应用对应的排版规则,不因“正式”“国企”“报告”或仅提供机构名称而擅自使用红头。
|
||||
|
||||
生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。上行文的文号和签发人在同一行编排,左右各空一字;信函、命令(令)、纪要使用各自专用版式;重复传入 `--attachment-file` 时必须同时提供与附件顺序、标题一致的 `--attachment-note`,附件会另页生成并保持页码连续。版记会锚定最后一页版心底部,正式页码按版心下边缘下7mm的位置生成。正式版头的份号、密级、紧急程度和电子机关标志以版心上边缘为垂直锚点固定定位,不会因为左上字段而把机关标志或预印红头留白向下推移。只有传入密级而未传入份号时,首行会保留空位,密级仍从第二行开始。
|
||||
生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。上行文的文号和签发人在同一行编排,左右各空一字;信函、命令(令)、纪要和横排表格使用各自专用版式;电子联合行文可重复传入 `--joint-org`;主办机关在前,联署机关名称分行排列;输入含“文件”时将“文件”置右并按联署名称上下居中;分离装订附件可加 `--attachment-detached`;重复传入 `--attachment-file` 时必须同时提供与附件顺序、标题一致的 `--attachment-note`,附件会另页生成并保持页码连续。版记会锚定最后一页版心底部,正式页码按版心下边缘下7mm的位置生成。正式版头的份号、密级、紧急程度和电子机关标志以版心上边缘为垂直锚点固定定位,不会因为左上字段而把机关标志或预印红头留白向下推移。只有传入密级而未传入份号时,首行会保留空位,密级仍从第二行开始。
|
||||
|
||||
`references/formal-checklist.md` 是可选的版式复核清单。涉及印章或签名章时,使用者可在 Word/WPS 中插入已取得的图片或完成实体盖章;本生成器不内置印章图像。
|
||||
|
||||
@@ -28,14 +28,14 @@ node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --
|
||||
普通材料使用 `--format ordinary`,默认居中页码,不绘制红头。正式发文须显式使用 `--format formal`;其默认 `--letterhead preprinted`,即首面预留红头纸区域、不在 DOCX 中重绘纸上已有的红色机关标志和红线。`--doc-no` 等本次需打印的黑色变量仍可保留。只有明确要求完整电子版时使用 `--letterhead digital`。完整电子红头的机关标志按标准推荐使用小标宋体,标题按标准使用小标宋体;生成器会在缺失时明确警告并记录实际替代字体,需要硬性阻止替代输出时加 `--require-standard-fonts`。红线下标题按28磅版心网格空二行。
|
||||
|
||||
```bash
|
||||
# 预印红头纸套打;72mm 是默认机关标志区域高度,可按实际纸样调为 37—130mm
|
||||
# 预印红头纸套打;72mm 是默认机关标志区域起点,可按实际纸样调为 37—130mm
|
||||
node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format formal --letterhead preprinted --letterhead-reserve-mm 72 --org "发文机关名称" --doc-no "单位发〔2026〕1号" --title "文档标题" --sender "落款单位" --date "2026年9月9日"
|
||||
|
||||
# 完整电子红头版,仅在明确要求时使用
|
||||
node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format formal --letterhead digital --org "发文机关名称" --doc-no "单位发〔2026〕1号" --title "文档标题"
|
||||
```
|
||||
|
||||
`letter`、`command`、`minutes` 分别对应信函、命令(令)、纪要格式;需要时显式传入 `--format` 和相应字段。`command` 会要求机关标志以“命令”或“令”结尾;信函中的份号、密级和紧急程度会在第一条双线下按标准顺序编排,联合行文和机关专用信函模板仍需目标软件核对。正式发文默认单双页页码;`standard` 为单页右、双页左,`center` 与 `none` 供普通材料选择。
|
||||
`letter`、`command`、`minutes`、`horizontal-table` 分别对应信函、命令(令)、纪要和横排表格格式;需要时显式传入 `--format` 和相应字段。横排表格会生成横向A4页面和225mm表格,单双页表头在订口/切口的朝向仍需目标 Word/WPS 模板核对。联合行文使用 `--joint-org`,主办机关在前、联署机关名称分行排列;输入含“文件”时将“文件”置右并按联署名称上下居中;附件不能与正文一起装订时使用 `--attachment-detached`,附件首行会补排文号和附件序号。`command` 会要求机关标志以“命令”或“令”结尾;信函中的份号、密级和紧急程度会在第一条双线下按标准顺序编排,联合行文和机关专用信函模板仍需目标软件核对。正式发文默认单双页页码;`standard` 为单页右、双页左,`center` 与 `none` 供普通材料选择。
|
||||
|
||||
附件可重复传入 `--attachment-file attachment.md`。附件 Markdown 的首个一级标题会作为附件标题,第一页第一行写“附件1”(后续文件递增),第三行写标题,随后排正文;附件说明通过 `--attachment-note` 提供,工具会统一为“附件:”并去除末尾标点,同时拒绝顺序或标题与附件文件不一致的输入。复杂横排附件、不能与正文一起装订的附件和机关专用模板仍需在目标 Word/WPS 中核对。
|
||||
|
||||
@@ -55,9 +55,9 @@ node scripts/verify_gongwen_docx.mjs --input output.docx --profile formal --lett
|
||||
bash tests/visual-audit.sh /tmp/gongwen-visual-audit
|
||||
```
|
||||
|
||||
全条款视觉证据和未自动化边界见[GB/T 9704-2012 全条款视觉核验记录](references/gbt9704-visual-audit.md)。横排表格、联合行文、实体印章、纸张和装订不能只由 DOCX 生成器证明。
|
||||
全条款视觉证据和未自动化边界见[GB/T 9704-2012 全条款视觉核验记录](references/gbt9704-visual-audit.md)和[全条款截图证据矩阵](references/gbt9704-screenshot-evidence.md)。横排表格的单双页表头方向、实体印章、纸张和装订不能只由 DOCX 生成器证明。
|
||||
|
||||
校验器核验生成器可检查的版式要素,并按 `ordinary`、`formal`、`letter`、`command`、`minutes` 选择校验档案。输出后仍须在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码、红头纸套打和装订。
|
||||
校验器核验生成器可检查的版式要素,并按 `ordinary`、`formal`、`letter`、`command`、`minutes`、`horizontal-table` 选择校验档案。输出后仍须在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码、红头纸套打和装订。
|
||||
|
||||
## 输出说明
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,101 @@
|
||||
# 一条贴着标题的红线,让我把公文 Skill 重做了一遍
|
||||
|
||||

|
||||
|
||||
上一篇刚把 Gongwen Skill 介绍出去,我自己用着用着,又发现了问题。
|
||||
|
||||
这次出问题的是红头。
|
||||
|
||||
打开生成的文件,机关名称是红的,分隔线也有,可下面的标题几乎贴在线上。再看抬头的字体,也不对。
|
||||
|
||||
做公文的人应该能理解这种感觉。内容还没开始读,光看首页,就知道这份文件还得改。
|
||||
|
||||
我得先把一件事交代清楚。
|
||||
|
||||
上一版的红头模式,我自己没有完整实测过。此前我的习惯是先把正文排好,手动换行,在首页上方留出空白,再用单位已经印好红头的纸打印。机关名称和红线都在纸上,电脑里的文件留好位置就行。
|
||||
|
||||
我一直用自己的办法处理这一段,也就一直没碰到 Skill 里的这条分支。
|
||||
|
||||
直到这次触发,它直接在文档里生成了电子抬头和红线,连间距问题一起暴露出来。
|
||||
|
||||
一个自己天天在用的工具,也可能藏着一段自己从没认真用过的功能。发布时,我对它的把握显然超过了实际验证的范围。
|
||||
|
||||

|
||||
|
||||
最初做这个 Skill,只是想少花一点时间调 Word。
|
||||
|
||||
我经常要处理正式材料,字体、缩进、页边距、页码,每份都从头交代一遍实在麻烦。好不容易在一台电脑上排顺了,换到 WPS 或另一台装着不同字体的电脑,又可能变样。
|
||||
|
||||
所以我把常用规则做进 Skill,慢慢补,自己也一直用。
|
||||
|
||||
但红头这一页让我开始怀疑,之前那些“已经核对过”的地方,到底核到了哪一步。
|
||||
|
||||
规则写进说明了吗?生成的文件用上了吗?打开以后,页面上的效果还对吗?
|
||||
|
||||
这几件事之间,确实隔着不少工作。
|
||||
|
||||
于是这次,我把 GB/T 9704-2012 又拿出来,逐条对照生成器和校验器,再去看最终页面。查到哪里,就拿对应的场景生成一份文件。普通材料之外,上行文、联合行文、附件、信函、命令、纪要,也都要各自跑到。
|
||||
|
||||
最后这一批生成了 **18 份 Word 文件,转换成 18 份 PDF,共 37 页**。
|
||||
|
||||
下面是仓库里保留的实际渲染总览。它们用于核对版式,当前环境仍有字体替代,不能当作标准字体的印刷样张。
|
||||
|
||||

|
||||
|
||||
这一轮里,有些错误看起来很小,原因却藏得很深。
|
||||
|
||||
比如左上角加上份号、密级和紧急程度以后,机关标志会被下面的段落一起往下推。字段都在,顺序也对,整页的位置已经变了。
|
||||
|
||||
修完后,又专门量了一次 PDF 里的字形坐标。这批样本中,有这些字段和没有这些字段,机关标志的位置差是 0.00 毫米。至少这次,补几个字段不会再把抬头挤走。
|
||||
|
||||
还有那条贴着标题的红线。原先设置了段前距离,最终渲染却没有按预期留出来。现在改成明确的空行结构,再看渲染后的页面,确认间距确实出现。
|
||||
|
||||
我对这种问题印象特别深。代码里明明写着,纸面上偏偏没有。只让 AI 再读一次代码,很容易又得到一句“已符合要求”。
|
||||
|
||||

|
||||
|
||||
红头的使用场景也重新拆开了。
|
||||
|
||||
普通方案、报告和汇报材料,默认按普通材料排。出现单位名称,不会自动加红头。
|
||||
|
||||
需要正式发文时,再选择对应格式。如果用预印红头纸套打,文档给纸上的红头区域留白,具体位置按实际纸样核对。需要完整电子红头文件,就明确选电子红头模式。
|
||||
|
||||
这两种使用方式,现在有各自的处理规则。
|
||||
|
||||
另外一个补上的地方,是标题样式。
|
||||
|
||||
以前多级标题已经有了不同字体和字号,可 Word 并不知道这些段落是标题。等你想插入目录,才发现前面排得再像,引用功能也找不到文章结构。
|
||||
|
||||
这对长方案尤其烦。章节改了,目录还得跟着手改。
|
||||
|
||||
现在标题一到标题四都写入了对应样式和大纲级别,可以供 Word、WPS 的目录功能引用。具体文件打开后,仍要更新目录、核对分页,但至少不用再从正文里把标题一个个重新挑出来。
|
||||
|
||||

|
||||
|
||||
附件、末页版记、单双页页码,以及几种特殊公文格式,这次也补查了不少地方。相关条款、实际截图和仍需人工处理的项目,都放进了仓库,方便以后接着核。
|
||||
|
||||
做到这里,我才觉得,这次可以叫重大更新。
|
||||
|
||||
第一版到现在,改动已经远远超过了我最初想补的那几个小问题。连验证的方法都换了一遍。对我个人来说,确实有种脱胎换骨的感觉。
|
||||
|
||||
不过,剩下的问题也要一起讲。
|
||||
|
||||
这轮验证用的机器缺少所需的小标宋、仿宋字体,部分页面使用了替代字体。现在工具会明确提示缺什么、换成了什么,也提供严格模式,缺字体就停止生成。字号相同,字体换了,字宽和换行仍可能跟着变,这一点躲不开。
|
||||
|
||||
纸张和实际打印也还有工作。预印纸的位置、打印缩放、双面套正、装订,都得拿纸样检查。长标题、复杂表格和部分溢出情况,还没有全部解决。横排表格的单双页表头朝向,目前也需要在目标 Word 或 WPS 模板里处理。
|
||||
|
||||
这些都是后续要继续做的具体事情。
|
||||
|
||||

|
||||
|
||||
跨平台的安装检查也重新跑过了。Claude Code、OpenCode、Trae Code、Kimi、TraeWork、WorkBuddy、ZCode,以及我自己用的 Codex,都有对应的安装或导入方式。各平台共用同一套规则,后续继续修,也能一起更新。
|
||||
|
||||
新版已经放到 GitHub。
|
||||
|
||||
如果你从上一篇过来,建议更新一下。这次改动值得重新安装、拿自己的材料试一遍。
|
||||
|
||||
也欢迎把问题直接反馈给我。最好附上脱敏后的页面截图,告诉我用的是 Word 还是 WPS、什么系统、装了哪些字体。如果是套打偏移,纸样和打印设置也很有帮助。
|
||||
|
||||
我会继续用它处理自己的材料。下次再碰到一条不该贴着标题的红线,希望我们能顺着已有的记录,很快找到它为什么又跑偏了。
|
||||
|
||||
[GitHub · Gongwen 公文排版 Skill](https://github.com/mizzlelover/gongwen-gbt9704-skill)
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,22 @@
|
||||
# 超大标题图文穿插 X cover
|
||||
You are an editorial art director. Create ONE finished opaque image, X Article cover, landscape aspect ratio 5:2.
|
||||
## Input and meaning
|
||||
Article title: 一条贴着标题的红线,让我把公文 Skill 重做了一遍.
|
||||
Main visual title, exact Chinese: 一条红线
|
||||
Supporting line, exact Chinese: 让我把公文 Skill 重做了一遍
|
||||
Small author label: 谁是专家
|
||||
The personal article describes a real formatting defect prompting a substantial rebuild and visual verification. It does not claim perfect compliance. Audience: Chinese professionals using Word/WPS and AI tools. Mood: candid, rigorous, arresting, human.
|
||||
## Selected visual language
|
||||
Use only interleaved-title-editorial-poster: enormous bold geometric sans-serif Chinese typography interwoven with ONE recognizable document-paper subject. Maintain the restrained Swiss editorial character of the series through asymmetric alignment, confident scale and abundant whitespace. This is conceptual cover art, never a supposed screenshot or compliance certificate.
|
||||
## Composition and subject
|
||||
Warm white opaque background. Huge black 一条红线 occupies the left and center of a very wide composition. On the right, a single sculpturally bent white A4 sheet rises diagonally, carrying ONE razor-sharp vermilion separator line. That line visually continues horizontally into the central typography, connecting the document defect to the title. The sheet contains only a handful of faint gray abstract paragraph bars, no invented readable official text. Create a physically credible folded-paper silhouette, subtle soft shadow and tactile fiber. The whole subject is one sheet, no stacks, printers or rulers.
|
||||
## Image-text relationship
|
||||
Explicit back-title / paper / front-title layers: paper corner covers a tiny outer edge of 条 while a clean stroke of 红 is in front of the paper edge, giving at least two depth-order switches. Keep all four Chinese characters instantly legible, whole and undistorted. Red line may cross negative spaces but must never obscure character recognition. Preserve breathing room around the dominant title. Supporting line below the title, smaller yet easily readable, deliberately aligned. 谁是专家 in a small restrained lower-left author credit.
|
||||
## Typography
|
||||
Precisely spelled Chinese 一条红线, subordinate 让我把公文 Skill 重做了一遍. Bold clean geometric sans-serif main title, lighter contemporary sans-serif subtitle. No random English labels, no excessive text, no full article title duplicated. Three levels maximum.
|
||||
## Color/material
|
||||
Only warm white, deep black, vermilion red and faint gray shadows. Matte real paper fibers, clean solid type edges, high-end independent magazine/exhibition design. Fully opaque background, no alpha/transparency. Red is an actual compositional line, not general red decoration.
|
||||
## Negative constraints
|
||||
No generic office stock photo, no pile of paper, ruler, robot, chrome, fake official agency, seals, standard certification, infographic cards, dashboard, PPT, ad template, tiny nonsense text, gradients, distressed/grunge edges, borders, watermarks, or illegible cropped title.
|
||||
## Final standard
|
||||
One independently composed X banner at 5:2. Viewers first read 一条红线 and then recognize the paper/line metaphor and the rebuild subtitle. Beautiful, editorially specific, restrained, finished.
|
||||
@@ -0,0 +1,15 @@
|
||||
# 超大标题图文穿插 X Article 插图
|
||||
|
||||
生成一张 X Article 文中插图,横向 5:2,使用且只使用“超大标题图文穿插 / interleaved-title-editorial-poster”视觉语言。
|
||||
|
||||
主题是四级标题写入 Word 和 WPS 的标题样式与大纲级别,目录可以引用和更新。主标题必须准确写成“目录可引用”,副标题准确写成“标题一到标题四写入文档结构”,署名小字为“谁是专家”。
|
||||
|
||||
全图只有一个核心主体,一张白色 A4 文档纸。纸面被折成四个由大到小的阶梯层级,每层只有一个深黑标题短条和少量浅灰正文线。四个层级通过一根细朱红线连接到右侧的四组目录点线和页码圆点。它看起来像真实纸张上的结构设计,不是软件界面截图。
|
||||
|
||||
主标题使用超大黑色几何无衬线字,横跨左侧和中央,与四级纸张阶梯发生至少两次前后穿插。朱红色只用于连接线和一个标题字的局部强调。副标题位于主标题下方,署名位于左下角。所有中文必须准确、清楚、完整。
|
||||
|
||||
背景为不透明暖白色。配色只用暖白、深黑、朱红、浅灰。纸张纤维真实,阴影轻,网格精确,留白充足,具有瑞士编辑海报和独立杂志质感。
|
||||
|
||||
禁止真实 Word 或 WPS 界面、菜单截图、代码、乱码、随机英文、复杂流程图、多人、机器人、PPT、电商广告、蓝色科技风和多个视觉中心。
|
||||
|
||||
只生成这一张独立插图,不生成选项、拼图或解释。
|
||||
@@ -0,0 +1,15 @@
|
||||
# 超大标题图文穿插 X Article 插图
|
||||
|
||||
生成一张 X Article 文中插图,横向 5:2,使用且只使用“超大标题图文穿插 / interleaved-title-editorial-poster”视觉语言。
|
||||
|
||||
主题是预印红头纸套打。主标题必须准确写成“预印纸套打”,副标题准确写成“首页上方留出红头区域”,署名小字为“谁是专家”。读者一眼看懂红头和红线已经印在纸上,电脑只打印下面的黑色内容。
|
||||
|
||||
全图只有一个核心主体,一张微微弯曲的白色 A4 纸。纸张右侧进入一台极简抽象的黑色打印机边缘,纸张上部已有一条朱红色分隔线,线以上只保留红色几何标志区域,线以下留出大片干净空间。不要出现机关名称、印章或可被误认成真实发文的内容。
|
||||
|
||||
主标题使用超大黑色几何无衬线字,位于左侧和中部,与纸张边缘发生两次清晰的前后穿插。朱红色分隔线从纸面延伸进入标题区域,成为画面的横向骨架。副标题位于主标题下方,字号明显更小。署名放在左下角。所有文字必须完整、准确、清楚。
|
||||
|
||||
背景为不透明暖白色。配色只用暖白、深黑、朱红和少量浅灰。纸张有细微纤维,阴影柔和,网格克制,留白充足,像高端独立杂志或瑞士编辑海报。
|
||||
|
||||
禁止库存办公照片、真实机关名称、电子红头完整页面、印章、乱码、随机英文、机器人、仪表盘、PPT、电商广告、渐变、多个主体和复杂说明文字。
|
||||
|
||||
只生成这一张独立插图,不生成选项、拼图或解释。
|
||||
@@ -0,0 +1,15 @@
|
||||
# 超大标题图文穿插 X Article 插图
|
||||
|
||||
生成一张 X Article 文中插图,横向 5:2,使用且只使用“超大标题图文穿插 / interleaved-title-editorial-poster”视觉语言。
|
||||
|
||||
主题是结构验证完成以后,字体、纸张、分页和套打仍需在真实环境检查。主标题必须准确写成“还要实测”,副标题准确写成“字体、纸张、分页与套打留给真实环境”,署名小字为“谁是专家”。
|
||||
|
||||
全图只有一个核心主体,一张从左侧数字网格过渡到右侧真实纸张纤维的白色 A4 页面。页面左半部分有精确的浅灰排版网格和黑色正文线,右半部分出现纸张纤维、轻微印刷套准标记与自然卷边。中间一条朱红竖线像检查边界,表达屏幕结构和实体印品之间还隔着一次现场验证。
|
||||
|
||||
主标题使用超大黑色几何无衬线字,占据左侧和中部,与纸张卷边和朱红边界线发生至少两次清晰的前后穿插。副标题位于标题下方,署名位于左下角。标题必须完整可读,不得变形或缺字。
|
||||
|
||||
背景为不透明暖白色。配色只用暖白、深黑、朱红和浅灰。纸张材质克制真实,阴影柔和,画面严格对齐、留白充足,延续新头图的瑞士编辑气质。
|
||||
|
||||
禁止认证章、奖章、100% 合规字样、字体样张卡、放大镜、英文批注、随机英文、多个物件、机器人、软件界面、PPT、广告、复杂信息图和乱码。
|
||||
|
||||
只生成这一张独立插图,不生成选项、拼图或解释。
|
||||
@@ -0,0 +1,15 @@
|
||||
# 超大标题图文穿插 X Article 插图
|
||||
|
||||
生成一张 X Article 文中插图,横向 5:2,使用且只使用“超大标题图文穿插 / interleaved-title-editorial-poster”视觉语言。
|
||||
|
||||
主题是红色分隔线与公文标题之间必须留出两行版心空白。主标题必须准确写成“两行留白”,副标题准确写成“红线和标题之间,要在实际页面看见”,署名小字为“谁是专家”。
|
||||
|
||||
全图只有一个核心主体,一张放大的白色 A4 公文局部。纸面上方横贯一条清晰朱红线,红线下方有两条淡灰虚线形成可见的两行空白,再往下才出现一个深黑标题块和少量浅灰正文线。加入一个极简红色双向量测箭头,准确跨过两行空白,不使用真实尺子。
|
||||
|
||||
主标题使用超大黑色和朱红色几何无衬线字,占据左侧和中部,与纸张和红线形成至少两次前后穿插。标题字不能被遮到难认。副标题放在左下部,署名位于最下方。视觉路径先读“两行留白”,再看到红线与标题块的实际距离。
|
||||
|
||||
背景为不透明暖白色。配色只用暖白、深黑、朱红和浅灰。纸张有轻微纤维和柔和阴影,画面严格对齐、留白大、编辑感强。
|
||||
|
||||
禁止红线贴住标题、真实机关文字、乱码、随机英文、长段落、真实尺子、打勾清单、仪表盘、机器人、PPT、广告、复杂信息图和多个主体。
|
||||
|
||||
只生成这一张独立插图,不生成选项、拼图或解释。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 封面提示词
|
||||
|
||||
用途:X Article 头图,画幅 5:2,Swiss-inspired editorial poster,现代杂志与展览海报质感。
|
||||
|
||||
主题:Gongwen Skill 重大更新,核心不是新增几个参数,而是经历真实验证后重新建立公文排版规则。画面只保留一个主视觉焦点:一叠正在被校准的 A4 公文纸,纸面带有克制的红色分隔线和清晰的黑色正文网格,一把透明刻度尺与纸张形成真实的校准关系,旁边只出现少量纸张边缘和留白。画面要让人感到从“看起来像”走到“逐条核验”。
|
||||
|
||||
主标题:重大更新
|
||||
副标题:GONGWEN SKILL / VERIFIED IN PRACTICE
|
||||
|
||||
构图:5:2 横幅;米白底、深墨黑、朱红强调色、少量冷灰。单一主体位于画面右侧偏中,左侧留出大面积呼吸空间。巨大几何无衬线标题“重大更新”跨越画面,但与主体形成真实前后穿插:标题部分笔画位于纸张后方,部分笔画压在纸张和刻度尺前方,至少两次明显换层;字形边缘清晰、无乱码。标题必须可读,不能用难辨的艺术字。辅助信息只保留一行小字,不堆英文碎句。整体像瑞士风格的编辑海报,网格严谨,留白充足,颜色节制。
|
||||
|
||||
禁止:机器人、金属人像、未来城市、科技仪表盘、电商广告、PPT模板、复杂图表、多人、廉价渐变、乱码中文、错误红头、把电子红头画成完整机关公文首页、标题贴在图片上没有穿插、过度装饰、过多辅助文字。
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,9 @@
|
||||
# 文中插图二:逐条核验
|
||||
|
||||
用途:X Article 文中横向插图,画幅 5:2,Swiss-inspired editorial visual。
|
||||
|
||||
主题:把 GB/T 9704-2012 的条款逐项落回 DOCX 和最终页面。画面只保留一个核心主体:打开的公文样张与一把透明刻度尺,红色分隔线下方留出清晰的两行版心空白,黑色标题与正文在下方整齐出现;刻度尺指向红线和标题之间的间距,旁边只有少量方形校验标记。画面传达“可量、可看、可追溯”的验证过程。
|
||||
|
||||
构图:米白纸张、深墨黑、朱红、冷灰;横向 5:2;主体在右侧,左侧留白;严谨网格、强对比、编辑海报质感。不要放可读长文字,避免生成乱码。刻度、红线、标题块与正文块必须清晰分层。
|
||||
|
||||
禁止:泛泛的代码屏幕、科技仪表盘、机器人、复杂表格、PPT、廉价办公插画、乱码字、把红线和标题画成贴合。
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,9 @@
|
||||
# 文中插图四:验证与边界
|
||||
|
||||
用途:X Article 文中横向插图,画幅 5:2,Swiss-inspired editorial visual。
|
||||
|
||||
主题:真实验证后的诚实边界。画面只保留一个核心主体:一叠已经打印的 A4 公文样张,旁边放着字体样张卡、纸张样本和一只小型放大镜;纸张上能看出整齐的黑色正文、红色版头区域和连续页码,但字体样张与纸张样本仍保留“待在目标环境复核”的感觉。画面表达“结构和链路已验证,字体、纸张、溢出仍需现场验证”。
|
||||
|
||||
构图:米白、墨黑、朱红、暖灰;5:2 横幅;主体在中右部,左侧留白;编辑杂志、瑞士网格、克制而有温度。不要出现可读长文字,不能伪造标准认证印章或“100%合规”字样。
|
||||
|
||||
禁止:奖章、认证徽章、机器人、科技屏幕、复杂信息图、广告风、PPT、乱码。
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,9 @@
|
||||
# 文中插图一:红头规则
|
||||
|
||||
用途:X Article 文中横向插图,画幅 5:2,Swiss-inspired editorial visual。
|
||||
|
||||
主题:红头公文规则重新核验。画面只保留一个核心主体:一张放在打印机进纸位置的预印红头纸,纸张上方是已经印好的红色机关标志区域和红线,下方是留给电脑正文的干净空白区域;旁边只有一枚小型刻度标记,表达首页预留高度。重点表现“打印机使用预印纸,文档首页只留空白区域”,不要把电子红头完整绘制在屏幕里。
|
||||
|
||||
构图:米白、墨黑、朱红、冷灰四色;主体居中偏右,左侧留白;可加入一条细网格线与小圆点作为编辑设计元素。不要出现可读长文字,只保留几何化的红色区域和空白区域。真实纸张和打印机结构,克制、精致、像编辑杂志插图。
|
||||
|
||||
禁止:完整电子公文首页、虚构机关名称、乱码文字、红线贴着标题、机器人、3D科技风、信息图堆叠、PPT、广告风。
|
||||
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,9 @@
|
||||
# 文中插图三:标题样式与目录
|
||||
|
||||
用途:X Article 文中横向插图,画幅 5:2,Swiss-inspired editorial visual。
|
||||
|
||||
主题:多级标题真正写入 Word/WPS 的标题样式与大纲级别,后续可以自动生成目录。画面只保留一个核心主体:一份打开的层级化公文目录页,四个清晰的层级块从大到小排列,右侧有一条整齐的目录线与页码点线;主体像纸张与编辑网格,不要做成软件界面截图。表达“文档结构可以被引用和更新”。
|
||||
|
||||
构图:米白底、深墨黑、朱红小面积强调、冷灰辅助;横向 5:2;标题层级块与目录线形成单一焦点,留白明显;现代瑞士编辑设计,几何无衬线,图形清楚。不要出现可读中文长句,只用抽象短横线、数字级别和页码点线表示结构。
|
||||
|
||||
禁止:代码、复杂 UI、多个主体、乱码、过度信息密度、PPT、蓝色科技风、卡通办公人物。
|
||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 331 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 359 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1 @@
|
||||
Header coordinate measurement passed: copy=39.88mm, agency=71.41mm, no-fields-agency=71.41mm, delta=0.00mm
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 177 KiB |
@@ -0,0 +1,19 @@
|
||||
scenario profile pdf_pages committed_screenshot_examples
|
||||
command command 2 references/assets/visual-audit/all-rules/command-1.png, references/assets/visual-audit/all-rules/command-2.png
|
||||
formal-attachments-detached preprinted 4 references/assets/visual-audit/all-rules/formal-attachments-detached-3.png
|
||||
formal-attachments preprinted 4 references/assets/visual-audit/all-rules/formal-attachments-3.png, references/assets/visual-audit/all-rules/formal-attachments-4.png
|
||||
formal-digital-all digital 2 references/assets/visual-audit/all-rules/formal-digital-all-1.png, references/assets/visual-audit/all-rules/formal-digital-all-2.png
|
||||
formal-digital-upward digital 2 references/assets/visual-audit/all-rules/formal-digital-upward-1.png, references/assets/visual-audit/all-rules/formal-digital-upward-2.png
|
||||
formal-joint digital 2 references/assets/visual-audit/all-rules/formal-joint-1.png, references/assets/visual-audit/all-rules/formal-joint-2.png
|
||||
formal-long preprinted 3 references/assets/visual-audit/all-rules/formal-long-2.png, references/assets/visual-audit/all-rules/formal-long-3.png
|
||||
formal-preprinted preprinted 2 references/assets/visual-audit/all-rules/formal-preprinted-1.png, references/assets/visual-audit/all-rules/formal-preprinted-2.png
|
||||
formal-seal preprinted 2 references/assets/visual-audit/all-rules/formal-seal-2.png
|
||||
formal-signed preprinted 2 references/assets/visual-audit/all-rules/formal-signed-2.png
|
||||
formal-unsigned preprinted 2 references/assets/visual-audit/all-rules/formal-unsigned-1.png, references/assets/visual-audit/all-rules/formal-unsigned-2.png
|
||||
horizontal-table horizontal-table 1 references/assets/visual-audit/all-rules/horizontal-table-1.png
|
||||
letter-fields letter 2 references/assets/visual-audit/all-rules/letter-fields-1.png
|
||||
letter letter 1 references/assets/visual-audit/all-rules/letter-1.png
|
||||
minutes minutes 2 references/assets/visual-audit/all-rules/minutes-1.png, references/assets/visual-audit/all-rules/minutes-2.png
|
||||
ordinary-calibration ordinary 2 references/assets/visual-audit/all-rules/ordinary-calibration-1.png
|
||||
ordinary ordinary 1 references/assets/visual-audit/all-rules/ordinary-1.png
|
||||
table formal 1 references/assets/visual-audit/all-rules/table-1.png
|
||||
|
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -12,7 +12,7 @@
|
||||
|
||||
## 1. 前提与制作
|
||||
|
||||
- [ ] 已明确选择 `ordinary`、`formal`、`letter`、`command` 或 `minutes`;报告、方案、汇报材料不因出现机构名称而自动采用红头。
|
||||
- [ ] 已明确选择 `ordinary`、`formal`、`letter`、`command`、`minutes` 或 `horizontal-table`;报告、方案、汇报材料不因出现机构名称而自动采用红头。
|
||||
- [ ] 正式发文已确认采用预印红头纸套打或完整电子红头。套打稿以实际纸样核定首页预留高度,不重复打印纸上既有的红色机关标志、红线。
|
||||
- [ ] 已确认发文机关、文种、行文关系、主送范围、发文字号、签发权限及机关现行模板。
|
||||
- [ ] A4 成品幅面为 210mm×297mm;天头 37mm±1mm、订口 28mm±1mm、版心 156mm×225mm。
|
||||
@@ -43,7 +43,7 @@
|
||||
- [ ] 署名、成文日期和印章已按“加盖印章/不加盖印章/签发人签名章/联合行文”之一完整处理:签发人签名章模式正文下空二行、职务左空二字,签名章下空一行、日期右空四字;成文日期年月日用阿拉伯数字全称且月日不补零;没有空白印章或伪造印章。空白不足时记录行距、字距调整。
|
||||
- [ ] 附注适用时在成文日期下一行左空二字,以圆括号标注。
|
||||
- [ ] 附件另面编排、置于版记之前;左上第一行为“附件”及序号黑体三号,标题在第三行居中;附件序号和标题与正文附件说明逐项一致;与正文一起装订的附件页码连续。不能一起装订时,左上补排发文字号及“附件”序号。
|
||||
- [ ] 使用 `--attachment-file` 生成的附件已逐个另页,附件标签在第一行、标题在第三行;无法一起装订时已人工补排发文字号和“附件”标识。
|
||||
- [ ] 使用 `--attachment-file` 生成的附件已逐个另页,附件标签在第一行、标题在第三行;使用 `--attachment-detached` 时已确认附件首行补排发文字号和“附件”标识;复杂附件仍需人工核对。
|
||||
|
||||
## 4. 版记、页码与特殊格式
|
||||
|
||||
@@ -51,8 +51,11 @@
|
||||
- [ ] 抄送机关适用时为四号仿宋、左右各空一字、回行与冒号后首字对齐、末尾句号;主送移入版记时按“主送”规则处理。
|
||||
- [ ] 印发机关和日期为四号仿宋,左空一字与右空一字;日期完整且不补零,后加“印发”。
|
||||
- [ ] 页码为四号半角宋体阿拉伯数字,两侧各一字线,线距版心下边缘7mm;单页右空一字、双页左空一字;空白页、特定版记页和附件连续页码已按规则处理。
|
||||
- [ ] 横排表格、信函、命令(令)、纪要、联合行文已使用相应专用版式;不能用普通打印稿替代。
|
||||
- [ ] 横排表格使用 `--format horizontal-table` 或目标 Word/WPS 横向模板;本项目已实际渲染横向页面和225mm表格,但单双页表头在订口/切口的方向仍需目标软件核对。
|
||||
- [ ] 信函、命令(令)、纪要、联合行文已使用相应专用版式;`formal --joint-org` 的截图已确认主办机关在前、联署机关分行排列;输入含“文件”时将“文件”置右并上下居中;复杂机关标志仍按现行模板核对;不能用普通打印稿替代。
|
||||
- [ ] 信函格式已核对上边缘30mm、上粗下细双线、下页边20mm处上细下粗双线,双线长度均170mm;如有份号、密级、紧急程度,均置于第一条双线下并距线3号字高7/8;文号顶格居右;首页无页码且版记不画分隔线。当前生成器对这些可选信函版头字段要求使用专用模板。
|
||||
- [ ] 命令(令)格式已核对发文机关全称加“命令”或“令”、标志上边缘距版心20mm、令号下空二行、正文下空二行;签发章和日期按7.3.5.3核对。
|
||||
- [ ] 纪要格式已核对标志上边缘距版心35mm,出席/请假/列席标签为黑体,人员为仿宋。
|
||||
- [ ] 计量单位、标点符号、数字分别按 GB 3100、GB 3101、GB 3102、GB/T 15834、GB/T 15835 复核。
|
||||
|
||||
- [ ] 已保存本次实际渲染的[全条款截图证据矩阵](gbt9704-screenshot-evidence.md),并逐项记录截图通过或截图不能证明的边界。
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
### 3. 预印红头纸套打与完整电子版
|
||||
|
||||
预印红头纸套打是常见制作流程:DOCX 首页预留纸上已印制的机关标志、红线等区域,打印时放入对应红头纸;不在文档中重复绘制这些红色元素。标准规定最终版式,不规定只能使用预印纸。完整电子红头仅在用户明确要求时生成。套打区域高度以实际纸样为准,不能将“35mm”误作全部预留高度。电子红头的发文机关标志与标题必须在目标环境中使用可核对的小标宋体;若字体不存在,生成器会明确警告并显示替代字体,`--require-standard-fonts` 可阻止继续生成。红色分隔线后的标题按版心网格空二行,不以视觉贴近为准。
|
||||
预印红头纸套打是常见制作流程:DOCX 首页预留纸上已印制的机关标志、红线等区域,打印时放入对应红头纸;不在文档中重复绘制这些红色元素。标准规定最终版式,不规定只能使用预印纸。完整电子红头仅在用户明确要求时生成。套打起点高度以实际纸样为准,不能将“35mm”误作全部预留高度;生成器在预印机关标志区域后保留两行28磅版心空行,再排变量文号,标题前再保留红线下两行空行。电子红头的发文机关标志与标题必须在目标环境中使用可核对的小标宋体;若字体不存在,生成器会明确警告并显示替代字体,`--require-standard-fonts` 可阻止继续生成。红色分隔线后的标题按版心网格空二行,不以视觉贴近为准。
|
||||
|
||||
## 三、纸张、版面和印制
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
## 八、横排表格和特定格式
|
||||
|
||||
- A4横排表格的页码位置与其他页一致;单双页表头方向按标准处理。
|
||||
- A4横排表格的页码位置与其他页一致;生成器的 `--format horizontal-table` 会生成横向A4和225mm表格,单双页表头在订口/切口的朝向仍需目标 Word/WPS 模板处理。
|
||||
- 信函、命令(令)、纪要均有专门格式,不能套用普通公文首页。
|
||||
- 信函首页不显示页码,版记规则也不同。上边缘距纸边30mm;发文机关标志下4mm处为170mm上粗下细红色双线,距下页边20mm处为170mm上细下粗红色双线;标题与其上最后一个要素相距二行。
|
||||
- 命令(令)发文机关标志由机关全称加“命令”或“令”组成,上边缘距版心20mm;标志下空二行编排令号,令号下空二行编排正文;签发人职务、签名章和日期按7.3.5.3处理。
|
||||
@@ -155,14 +155,15 @@
|
||||
| 四级正文标题 | 是 | 特殊机关模板调整 |
|
||||
| 报告居中页码 | 是 | 无 |
|
||||
| 正式单双页页码 | 是 | `--page-number standard`;页脚按版心下边缘下7mm的位置生成 |
|
||||
| 普通表格 | 是 | 横排表格方向和复杂跨页表格复核 |
|
||||
| 横排表格 | 部分通过 | `--format horizontal-table` 生成横向页面和225mm表格;单双页表头方向、复杂跨页表格复核 |
|
||||
| 普通材料 | 是 | `--format ordinary`,不绘制红头 |
|
||||
| 预印红头纸套打 | 是 | `--format formal --letterhead preprinted`,按纸样设置预留高度 |
|
||||
| 完整电子红头 | 是 | `--format formal --letterhead digital` |
|
||||
| 印章、签名章 | 文字版式之外 | 在 Word/WPS 中插入图片或实体盖章 |
|
||||
| 附件 | 通过 `--attachment-file` 另页生成标签、标题和正文 | 无法一起装订时的发文字号标注、复杂附件版式仍需人工复核 |
|
||||
| 联合行文 | 通过 `--joint-org` 分行生成,主办机关在前;输入含“文件”时将“文件”置右并按联署名称上下居中 | 复杂机关专用标志和联署模板复核 |
|
||||
| 附件 | 通过 `--attachment-file` 另页生成标签、标题和正文;`--attachment-detached` 补排文号和附件序号 | 复杂附件版式仍需人工复核 |
|
||||
| 版记 | 是 | `--cc`、`--print-org`、`--print-date` 适用时生成 |
|
||||
| 信函、命令、纪要 | 是 | `--format letter|command|minutes`,按实际字段生成 |
|
||||
| 信函、命令、纪要、横排表格 | 是/部分通过 | `--format letter|command|minutes|horizontal-table`,按实际字段生成;横排表头方向需模板复核 |
|
||||
|
||||
## 十、建议检查
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
| 7.2.1 | 份号六位、3 号、左上第一行 | `--copy-no` 只接受 1—6 位阿拉伯数字并补足六位 | 结构检查;实际是否需要份号由发文机关确认 |
|
||||
| 7.2.2 | 密级和保密期限第二行、3 号黑体 | `--secret` 按黑体3号写入;没有份号时自动保留第一行空位,使密级仍在第二行 | 结构检查和视觉审计;文字内容和期限写法人工复核 |
|
||||
| 7.2.3 | 紧急程度及排列顺序 | `--urgent` 按黑体 3 号写入,并按份号、密级、紧急程度顺序下移 | 适用性和紧急级别人工确认 |
|
||||
| 7.2.4 | 发文机关标志居中、红色、版心上边缘下 35mm、推荐小标宋 | `formal --letterhead digital` 才绘制;预印模式只留纸样区域;首面字段以版心上边缘为垂直锚点,机关标志固定在其下35mm,不推动机关标志或预留区;联合行文仍需模板 | `verify_gongwen_docx.mjs`、`tests/measure-header-coordinates.mjs` 和视觉核验记录;当前机缺少推荐字体时严格模式失败 |
|
||||
| 7.2.5 | 文号下空二行、六角括号、全年份、顺序号不加“第”不补零 | `formal`/`letter` 对 `--doc-no` 做格式校验,并写入两个显式 28 磅空行 | 文号语义由机关编号规则确认 |
|
||||
| 7.2.4 | 发文机关标志居中、红色、版心上边缘下 35mm、推荐小标宋 | `formal --letterhead digital` 才绘制;预印模式只留纸样区域;首面字段以版心上边缘为垂直锚点,机关标志固定在其下35mm,不推动机关标志或预留区;联合行文通过 `--joint-org` 分行生成,主办机关在前;输入含“文件”时将“文件”置右并按联署名称上下居中;名称过长时自适应标志字号 | `verify_gongwen_docx.mjs`、`tests/measure-header-coordinates.mjs` 和[`gbt9704-screenshot-evidence.md`](gbt9704-screenshot-evidence.md);当前机缺少推荐字体时严格模式失败,复杂机关专用标志仍需模板核对 |
|
||||
| 7.2.5 | 机关标志下空二行、六角括号、全年份、顺序号不加“第”不补零 | `formal`/`letter` 对 `--doc-no` 做格式校验,并写入两个显式 28 磅空行 | 文号语义由机关编号规则确认 |
|
||||
| 7.2.6 | 上行文文号左空一字、签发人右空一字并同一行;标签仿宋、姓名楷体 | `--upward true --signer` 同行生成单一或两个签发人,左右各空一字;多签发人、联合行文转人工模板 | 结构校验和 PDF;复杂联合行文需按机关顺序人工排布 |
|
||||
| 7.2.7 | 文号下 4mm、版心等宽红线 | `redRule()` 生成 156mm 红线,文号段后 4mm;预印模式不重绘 | PDF 量测;红色油墨和实际纸样人工核对 |
|
||||
| 7.3.1 | 标题 2 号小标宋,红线下空二行、居中 | 标题使用 `GongwenTitle`,红线/预印线后插入两个显式 28 磅空段落 | XML + PDF 视觉门;小标宋缺失时严格模式失败 |
|
||||
@@ -30,16 +30,16 @@
|
||||
| 7.3.4 | 附件说明正文下空一行、左空二字、名称末无标点 | `--attachment-note` 统一为“附件:”并去除末尾标点,表格后不重复空行 | 长名称回行和多附件编号仍需 PDF/模板复核 |
|
||||
| 7.3.5 | 署名、成文日期、印章/签名章 | 分别记录加盖印章、不加盖印章、签发人签名章、日期数字和特殊空白处理;生成器排文字占位并补足签名章模式的两行/一行间距,不伪造印章 | `--seal-mode` 仍需把实体章或已取得的图片放入 Word/WPS;单/联合机关印章、红色签名章和空白不足时调整人工完成 |
|
||||
| 7.3.6 | 附注在日期下一行左空二字、圆括号 | `--note` 规范化为全角圆括号 | 日期与附注的最终分页人工核对 |
|
||||
| 7.3.7 | 附件另面、第一行附件标签黑体、第三行标题、版记前、页码连续且与附件说明一致 | `--attachment-file` 用分页符、显式空行和标题样式生成,并校验附件顺序和标题与附件说明一致,附件共用连续页脚 | 不能一起装订时的文号+附件标识、复杂附件版式人工处理 |
|
||||
| 7.3.7 | 附件另面、第一行附件标签黑体、第三行标题、版记前、页码连续且与附件说明一致 | `--attachment-file` 用分页符、显式空行和标题样式生成,并校验附件顺序和标题与附件说明一致,附件共用连续页脚 | `--attachment-detached` 会在附件首行补排文号和“附件”序号;复杂附件版式仍需人工处理 |
|
||||
| 7.4.1 | 版记首末粗线、中间细线,末线贴最后一页版心下边缘 | 版记改为浮动表,锚定最后一页版心底部;粗线 0.35mm、细线 0.25mm | 已用长文 PDF 截图实测;Word/WPS 和极端分页仍需抽检 |
|
||||
| 7.4.2 | 抄送 4 号仿宋、左右一字、回行对齐、末尾句号 | `--cc` 生成 4 号、左右一字、全角冒号和句号 | 多行抄送回行和主送移版记人工复核 |
|
||||
| 7.4.3 | 印发机关/日期 4 号仿宋,左右一字,日期后“印发”,细线隔开 | `--print-org`、`--print-date` 生成右制表位、日期校验和末条粗线 | 实际机关名称长度和目标软件分页人工复核 |
|
||||
| 7.5 | 4 号半角宋体页码、两侧一字线、距版心下边缘 7mm、单双页位置、附件连续 | 正式页脚用半角宋体、单双页字段,footer 距离调整为 7mm 位置;信函关闭页码 | XML + PDF bbox;空白页/特定版记页的例外仍需人工复核 |
|
||||
| 8 | 横排表格页码与表头方向 | 普通 Markdown 表格按纵向版心生成;视觉审计保留 `table-1.png` 作为未自动化证据,不把纵向表格冒充横排 | 横排表格须在 Word/WPS 采用专用横向模板并人工检查 |
|
||||
| 8 | 横排表格页码与表头方向 | 新增 `--format horizontal-table`,实际生成横向 A4 和225mm表格,页码保持标准单双页位置;单双页表头在订口/切口方向尚未自动化 | [`horizontal-table-1.png`](gbt9704-screenshot-evidence.md);表头方向须在 Word/WPS 专用横向模板中人工检查 |
|
||||
| 9 | 计量单位、标点、数字按引用标准 | 结构字段(文号、日期、附件冒号、句号)做边界校验 | 正文语义、单位、标点和数字用法需人工或专门文本审校 |
|
||||
| 10.1 | 信函上下双红线、30mm/20mm、170mm、可选版头字段、标题二行、无首页页码、无版记线 | `--format letter` 生成双线、专用边距、文号距离、标题空行、底部双线页脚和无页码,并支持份号、密级、紧急程度在第一条双线下按顺序编排 | PDF 实测;联合行文及模板字段人工复核 |
|
||||
| 10.2 | 命令(令)标志由机关全称加“命令”或“令”、上边缘20mm、令号下空二行、正文下空二行 | `--format command` 要求 `--org` 以“命令/令”结尾、`--doc-no`,两个间隔均为显式28磅空行 | 令号文字和签名章/日期按7.3.5.3人工复核 |
|
||||
| 10.3 | 纪要标志 35mm;出席/请假/列席标签黑体,人员仿宋 | `--format minutes` 要求机关标志以“纪要”结尾,三类人员分 run 写入对应字体,并按两字起排、悬挂缩进处理回行 | `references/gbt9704-visual-audit.md` 的 `minutes-1.png`/`minutes-2.png`;纪要具体版式可按机关制度调整 |
|
||||
| 10.2 | 命令(令)标志由机关全称加“命令”或“令”、上边缘20mm、令号下空二行、正文下空二行 | `--format command` 要求 `--org` 以“命令/令”结尾、`--doc-no`,机关标志从版心上边缘下20mm开始,令号和正文均为空二行 | [`command-1.png`](gbt9704-screenshot-evidence.md);签名章/日期按7.3.5.3人工复核 |
|
||||
| 10.3 | 纪要标志 35mm;出席/请假/列席标签黑体,人员仿宋 | `--format minutes` 要求机关标志以“纪要”结尾,三类人员分 run 写入对应字体,紧跟正文或附件说明下一行,并按两字起排、悬挂缩进处理回行 | [`minutes-1.png`](gbt9704-screenshot-evidence.md)/[`minutes-2.png`](gbt9704-screenshot-evidence.md);纪要具体版式可按机关制度调整 |
|
||||
| 11 | 标准式样图 | 各分支按条款参数实现,不把截图当作唯一依据 | 最终仍需与机关现行式样、Word/WPS 和打印样张逐页比对 |
|
||||
|
||||
## 本轮硬性修正
|
||||
@@ -50,4 +50,4 @@
|
||||
- 文号、成文日期、附件说明的结构边界现在会拒绝明显不符合标准的输入;缺失小标宋或仿宋时仍会明确告警,严格模式会停止生成。
|
||||
|
||||
|
||||
本矩阵的实际渲染证据、截图文件名和逐条人工边界见[GB/T 9704-2012 全条款视觉核验记录](gbt9704-visual-audit.md)。
|
||||
本矩阵的实际渲染证据、截图文件名和逐条人工边界见[GB/T 9704-2012 全条款视觉核验记录](gbt9704-visual-audit.md)和[全条款截图证据矩阵](gbt9704-screenshot-evidence.md)。
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# GB/T 9704-2012 全条款截图证据矩阵
|
||||
|
||||
本文件是本次修订的逐条复核记录。它把国标条款、生成场景、实际渲染 PNG 和可证明边界逐一对应起来。截图只证明截图中可观察到的版式结果;纸张指标、油墨、套印、装订、机关语义和目标 Word/WPS 的专用模板行为,明确标为“截图不能证明”,不以 DOCX ZIP 属性替代。
|
||||
|
||||
标准依据:
|
||||
|
||||
- [全国标准信息公共服务平台现行状态页](https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p):GB/T 9704-2012,2025-05-30 复审结论为继续有效。
|
||||
- [GB/T 9704-2012 条文全文转载页](https://dzb.cumtb.edu.cn/info/1040/1184.htm):本次逐条对照的公开全文。
|
||||
|
||||
## 本次实际渲染批次
|
||||
|
||||
运行命令:
|
||||
|
||||
```bash
|
||||
bash tests/visual-audit.sh /tmp/gongwen-visual-audit-all-rules
|
||||
```
|
||||
|
||||
结果:18 份 DOCX、18 份 PDF、37 个 PDF 页面;每份 DOCX 先由校验器检查,再由 LibreOffice 转 PDF,最后用 `pdftoppm` 生成 PNG。`tests/measure-header-coordinates.mjs` 还用 `pdftotext -bbox-layout` 独立量测了电子红头首面:份号 39.88mm,机关标志 71.41mm,带字段和不带字段的位置差 0.00mm。
|
||||
|
||||
提交到仓库的证据目录为 [`references/assets/visual-audit/all-rules/`](assets/visual-audit/all-rules/),其中:
|
||||
|
||||
- [`manifest.tsv`](assets/visual-audit/all-rules/manifest.tsv) 记录 18 个场景、页数和已提交截图。
|
||||
- [`contact-sheet.png`](assets/visual-audit/all-rules/contact-sheet.png) 是场景总览;单页 PNG 保留原始截图尺寸。
|
||||
- [`header-coordinates.log`](assets/visual-audit/all-rules/header-coordinates.log) 是独立坐标量测结果。
|
||||
|
||||
## 场景与截图索引
|
||||
|
||||
| 场景 | 用途 | 截图 |
|
||||
| --- | --- | --- |
|
||||
| `ordinary` | 普通材料,不因机构名称变红头 | [`ordinary-1.png`](assets/visual-audit/all-rules/ordinary-1.png) |
|
||||
| `ordinary-calibration` | 22 行、28 字网格校准页 | [`ordinary-calibration-1.png`](assets/visual-audit/all-rules/ordinary-calibration-1.png) |
|
||||
| `formal-digital-all` | 电子红头全要素、附件说明、附注、版记 | [`formal-digital-all-1.png`](assets/visual-audit/all-rules/formal-digital-all-1.png)、[`formal-digital-all-2.png`](assets/visual-audit/all-rules/formal-digital-all-2.png) |
|
||||
| `formal-digital-upward` | 上行文文号、签发人、单双页页码 | [`formal-digital-upward-1.png`](assets/visual-audit/all-rules/formal-digital-upward-1.png)、[`formal-digital-upward-2.png`](assets/visual-audit/all-rules/formal-digital-upward-2.png) |
|
||||
| `formal-preprinted` | 预印红头纸套打留白 | [`formal-preprinted-1.png`](assets/visual-audit/all-rules/formal-preprinted-1.png)、[`formal-preprinted-2.png`](assets/visual-audit/all-rules/formal-preprinted-2.png) |
|
||||
| `formal-joint` | 联合行文机关标志 | [`formal-joint-1.png`](assets/visual-audit/all-rules/formal-joint-1.png)、[`formal-joint-2.png`](assets/visual-audit/all-rules/formal-joint-2.png) |
|
||||
| `formal-unsigned` | 不加盖印章文字位置 | [`formal-unsigned-1.png`](assets/visual-audit/all-rules/formal-unsigned-1.png)、[`formal-unsigned-2.png`](assets/visual-audit/all-rules/formal-unsigned-2.png) |
|
||||
| `formal-seal` / `formal-signed` | 印章、签名章文字占位 | [`formal-seal-2.png`](assets/visual-audit/all-rules/formal-seal-2.png)、[`formal-signed-2.png`](assets/visual-audit/all-rules/formal-signed-2.png) |
|
||||
| `formal-attachments` / `formal-attachments-detached` | 附件另面、分离装订附件 | [`formal-attachments-3.png`](assets/visual-audit/all-rules/formal-attachments-3.png)、[`formal-attachments-4.png`](assets/visual-audit/all-rules/formal-attachments-4.png)、[`formal-attachments-detached-3.png`](assets/visual-audit/all-rules/formal-attachments-detached-3.png) |
|
||||
| `formal-long` | 版记底部锚定、单双页页码 | [`formal-long-2.png`](assets/visual-audit/all-rules/formal-long-2.png)、[`formal-long-3.png`](assets/visual-audit/all-rules/formal-long-3.png) |
|
||||
| `letter` / `letter-fields` | 信函双线和可选字段 | [`letter-1.png`](assets/visual-audit/all-rules/letter-1.png)、[`letter-fields-1.png`](assets/visual-audit/all-rules/letter-fields-1.png) |
|
||||
| `command` | 命令(令)标志、令号、正文间距 | [`command-1.png`](assets/visual-audit/all-rules/command-1.png) |
|
||||
| `minutes` | 纪要标志和出席、请假、列席人员 | [`minutes-1.png`](assets/visual-audit/all-rules/minutes-1.png)、[`minutes-2.png`](assets/visual-audit/all-rules/minutes-2.png) |
|
||||
| `horizontal-table` / `table` | 横排表格和纵向表格对照 | [`horizontal-table-1.png`](assets/visual-audit/all-rules/horizontal-table-1.png)、[`table-1.png`](assets/visual-audit/all-rules/table-1.png) |
|
||||
|
||||
## 逐条核对
|
||||
|
||||
状态只使用以下三种含义:
|
||||
|
||||
- **截图通过**:本次 PNG 中已经看到条款要求的可视结果,并有结构或坐标检查辅助。
|
||||
- **截图通过,字体/目标软件仍需核对**:位置或结构可见,但本机缺标准字体,或 Word/WPS 的专用行为还要在目标环境复核。
|
||||
- **截图不能证明**:标准要求的是纸张、印刷、装订、语义或实体印章等外部条件;工具没有伪造通过结论。
|
||||
|
||||
| 条款 | 国标要求 | 本批截图与结果 | 状态及边界 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 规定党政机关公文的纸张、版面、印制装订、格式要素和式样;其他单位可参照。 | `ordinary-1.png`、`formal-digital-all-1.png`、`letter-1.png`、`command-1.png`、`minutes-1.png` 展示了不同格式分支。 | 截图通过;具体文种、行文关系和机关模板仍由使用者确认。 |
|
||||
| 2 | 引用 GB/T 148、GB 3100/3101/3102、GB/T 15834、GB/T 15835。 | 本仓库摘要、矩阵和本证据文件均列出引用文件;截图不能验证引用标准的语义执行。 | 截图不能证明正文单位、标点和数字语义。 |
|
||||
| 3.1 | 一字为一个汉字宽度。 | `ordinary-calibration-1.png` 显示固定字宽网格样例。 | 截图通过,实际字体字面宽度随目标字体核对。 |
|
||||
| 3.2 | 一行为一个汉字高度加三号字高度 7/8。 | `ordinary-calibration-1.png` 显示 28 磅实现网格;XML 校验确认 `w:linePitch=560`。 | 截图通过,术语本身不能只靠截图定义。 |
|
||||
| 4 | 纸张 60—80g/m²、白度80%—90%、横向耐折度≥15、不透明度≥85%、pH7.5—9.5。 | 生成器没有把这些物性写成 DOCX 属性;截图只显示页面。 | 截图不能证明;须以纸张规格和成品检测为准。 |
|
||||
| 5.1 | A4 成品 210mm×297mm。 | 18 个 PDF 场景均由脚本检查 A4;`horizontal-table-1.png` 为 A4 横向页面。 | 截图通过;裁切误差仍属成品核验。 |
|
||||
| 5.2.1 | 天头37±1mm、订口28±1mm、版心156×225mm。 | `formal-digital-all-1.png`、`formal-preprinted-1.png`、`ordinary-calibration-1.png`;校验器检查页边,标题和正文落在版心内。 | 截图通过;打印缩放关闭后需再量纸样。 |
|
||||
| 5.2.2 | 默认三号仿宋,特殊情况可调整。 | 全部截图可见三号网格;本机实际使用 `STFangsong` 替代并输出警告,严格模式会失败。 | 截图通过,字体仍需目标机安装方正小标宋/仿宋后复核。 |
|
||||
| 5.2.3 | 一般每面22行、每行28字并撑满版心。 | `ordinary-calibration-1.png` 是 22 行×28 字校准页;普通长文和特定格式按内容自然分页。 | 截图通过;长标题、表格和目标字体导致的特殊调整需逐文档看图。 |
|
||||
| 5.2.4 | 未特别说明时文字为黑色。 | `ordinary-1.png`、`formal-digital-all-2.png` 的正文、标题、版记文字为黑色;红色仅用于规定的红头和线。 | 截图通过。 |
|
||||
| 6.1 | 版面干净无底灰、字迹清楚无断划、尺寸标准、版心不斜,误差≤1mm。 | `contact-sheet.png` 中所有 PDF 页面均无生成的底灰或倾斜;屏幕截图不能证明实体制版误差。 | 截图部分通过;实体误差和印品质量不能由本工具证明。 |
|
||||
| 6.2 | 双面印刷、套正≤2mm、黑墨BL100%、红墨Y80%/M80%,着墨均匀无缺陷。 | `formal-long-2.png`、`formal-long-3.png` 展示双页页码位置;无实体双面印刷样。 | 截图不能证明油墨、套正和印品质量。 |
|
||||
| 6.3 | 左侧装订、页码误差≤4mm、成品±2mm、两钉距上下边缘70±4mm、平订距书脊3—5mm等。 | `formal-long-3.png` 只显示页面顺序和版记位置。 | 截图不能证明装订、裁切、钉位和封皮。 |
|
||||
| 7.1 | 版头、主体、版记三区;页码位于版心外。 | `formal-digital-all-1.png` 展示版头与主体,`formal-long-3.png` 展示末页版记和页码。 | 截图通过。 |
|
||||
| 7.2.1 | 份号六位三号阿拉伯数字,版心左上第一行。 | `formal-digital-all-1.png`、`formal-preprinted-1.png` 显示 `000007`;独立坐标日志显示 39.88mm 首行位置。 | 截图通过。 |
|
||||
| 7.2.2 | 密级和保密期限第二行三号黑体,数字用阿拉伯数字。 | `formal-digital-all-1.png` 显示 `机密★3年` 位于第二行。 | 截图通过;密级文字及期限是否适用需机关确认。 |
|
||||
| 7.2.3 | 紧急程度三号黑体;与份号、密级同时出现时按份号、密级、紧急程度自上而下。 | `formal-digital-all-1.png` 显示三项顺序;`letter-fields-1.png` 显示信函双线下同顺序。 | 截图通过。 |
|
||||
| 7.2.4 | 机关全称/规范简称可加“文件”,居中红色;上边缘距版心上边缘35mm;联合行文主办机关在前,联署名称上下居中。 | `formal-digital-all-1.png`、`formal-joint-1.png`;坐标日志显示机关标志 71.41mm 页面坐标,字段差0.00mm;联合行文以主办机关在前、联署机关名称分行排列,输入含“文件”时将“文件”置右并按联署名称上下居中;名称过长时自适应标志字号。 | 截图通过,推荐小标宋字体需目标机复核;复杂机关专用标志仍需模板核对。 |
|
||||
| 7.2.5 | 机关标志下空二行;年份全称、六角括号、顺序号不加“第”不补零、末尾“号”;上行文文号左空一字。 | `formal-digital-all-1.png`、`formal-digital-upward-1.png`;文号输入边界和显式空行均经校验。 | 截图通过。 |
|
||||
| 7.2.6 | “签发人:”三号仿宋、姓名三号楷体,右空一字;多签发人按顺序、一般每行两名、回行对齐。 | `formal-digital-upward-1.png` 显示同一行双签发人,姓名字体和标签分 run。 | 截图通过;超过两名或联合行文复杂签发人仍须专用模板。 |
|
||||
| 7.2.7 | 文号下4mm处、版心等宽红色分隔线。 | `formal-digital-all-1.png`、`formal-joint-1.png`;线宽156mm,红线下标题空两行。 | 截图通过;预印模式明确不重绘红线。 |
|
||||
| 7.3.1 | 标题二号小标宋,红线下空二行,居中;多行应词意完整、对称、梯形或菱形。 | `formal-digital-all-1.png`、`formal-preprinted-1.png`;标题样式为 `GongwenTitle`,空行在 PNG 中可见。 | 截图通过;小标宋缺失时严格模式失败;长标题断行需逐稿人工调整。 |
|
||||
| 7.3.2 | 标题下空一行,主送机关顶格,回行顶格,末尾全角冒号;过多可移版记。 | `formal-digital-all-1.png`、`formal-joint-1.png`。 | 截图通过;自动移版记的决定由发文机关确认。 |
|
||||
| 7.3.3 | 首页有正文;正文三号仿宋、自然段左空二字回行顶格;层次序数依次“一、”“(一)”“1.”“(1)”,字体黑体/楷体/仿宋。 | `formal-digital-all-1.png`、`ordinary-1.png` 显示首页正文和四级层次;校验器确认 Heading1—Heading4、大纲级别。 | 截图通过,标准字体和复杂分页仍需目标软件复核。 |
|
||||
| 7.3.4 | 正文下空一行左空二字写“附件:”;多附件编号;附件名称末不加标点,回行与首字对齐。 | `formal-digital-all-2.png` 显示附件说明;生成器校验说明与附件标题、顺序一致。 | 截图通过;极长名称需看最终换行。 |
|
||||
| 7.3.5.1 | 加盖印章:日期右空四字,红色印章压署名和日期,印章顶端距正文/附件说明一行内,不得空白印章;联合章须排列整齐。 | `formal-seal-2.png` 展示署名和日期占位;生成器不写入虚假印章。 | 截图不能证明实体章和联合章几何关系;需 Word/WPS 或打印盖章。 |
|
||||
| 7.3.5.2 | 不盖章:署名正文下空一行右空二字,日期下一行首字右移二字;联合署名自上而下。 | `formal-unsigned-2.png` 显示署名日期两行和右侧定位。 | 截图通过;多机关联合署名需目标模板复核。 |
|
||||
| 7.3.5.3 | 签发人签名章:正文下空二行,职务左空二字,签名章右空四字;日期下空一行右空四字;联合逐行对齐。 | `formal-signed-2.png` 展示职务/签名章文字占位和日期行。 | 截图部分通过;实体签名章颜色、尺寸、联合对齐不能由本工具证明。 |
|
||||
| 7.3.5.4 | 年月日使用阿拉伯数字,年份全称,月日不补零。 | `formal-digital-all-2.png`、`formal-unsigned-2.png` 显示 `2026年9月10日`;输入校验拒绝 `09月`。 | 截图通过。 |
|
||||
| 7.3.5.5 | 空白不足时可调行距、字距容纳印章或签名章、日期。 | `formal-seal-2.png`、`formal-signed-2.png` 仅覆盖正常空间。 | 截图不能证明极端内容自动调节;需按稿件人工调节。 |
|
||||
| 7.3.6 | 附注在日期下一行左空二字,用圆括号。 | `formal-digital-all-2.png` 显示 `(此件公开)`。 | 截图通过。 |
|
||||
| 7.3.7 | 附件另面、版记前;第一行附件标签三号黑体顶格,标题第三行居中;与附件说明一致;不能一起装订时左上补发文字号和附件序号。 | `formal-attachments-3.png`、`formal-attachments-4.png`、`formal-attachments-detached-3.png`;分离装订截图显示文号+附件序号。 | 截图通过;复杂附件内容和实体装订仍需复核。 |
|
||||
| 7.4.1 | 版记线与版心等宽,首末粗线约0.35mm,中间细线约0.25mm,末线与末页版心下边缘重合。 | `formal-long-3.png` 显示粗—细—粗顺序;版记表锚定末页版心底部。 | 截图通过;极端内容在 Word/WPS 需抽检。 |
|
||||
| 7.4.2 | 抄送四号仿宋,印发机关日期上一行,左右各空一字,回行对齐,末尾句号;主送移版记时改“主送”。 | `formal-digital-all-2.png`、`formal-long-3.png` 显示抄送、回行和句号。 | 截图通过;主送自动移版记仍是人工决定。 |
|
||||
| 7.4.3 | 印发机关日期四号仿宋,末条线之上,左一字右一字,日期后“印发”;其他要素用细线隔开。 | `formal-digital-all-2.png`、`formal-long-3.png`。 | 截图通过。 |
|
||||
| 7.5 | 四号半角宋体页码,两侧一字线,线上距版心下边缘7mm;单页右、双页左;空白/版记例外;附件连续。 | `formal-long-2.png`、`formal-long-3.png`、`formal-attachments-4.png`;校验器和 PDF 坐标检查页脚。 | 截图通过;空白页例外和实体套印仍需人工。 |
|
||||
| 8 | 横排 A4 表格页码位置不变,单页表头在订口边、双页表头在切口边。 | `horizontal-table-1.png` 已实际渲染 A4 横向页面、225mm 表格和标准页码;截图同时暴露当前表头仍在横向页面上边缘,尚未实现单双页订口/切口方向转换。 | 部分通过;表头朝向必须在 Word/WPS 横向模板中处理,不能宣称本生成器已完全覆盖8。 |
|
||||
| 9 | 计量单位、标点、数字分别符合引用标准。 | `formal-digital-all-2.png` 有 `10 kg、5 m、3.14` 观察样例;工具只校验字段边界。 | 截图不能证明全文语义;需文本审校。 |
|
||||
| 10.1 | 信函:标志距上页边30mm;上粗下细双线、下页边20mm处上细下粗,均170mm;可选字段顺序;标题空二行;首页无页码;版记无印发/分隔线。 | `letter-1.png`、`letter-fields-1.png`;校验器检查双线宽度、方向、底线页脚和无页码。 | 截图通过;联合行文信函和机关专用模板仍需复核。 |
|
||||
| 10.2 | 命令(令):机关全称加“命令/令”,标志上边缘距版心上边缘20mm;令号下空二行,正文下空二行;签章日期按7.3.5.3。 | `command-1.png`;独立截图中标志约57mm页面坐标,令号和正文间隔可见。 | 截图通过;签章和日期实体环节仍需人工。 |
|
||||
| 10.3 | 纪要标志上边缘距版心上边缘35mm;出席、请假、列席标签三号黑体,人员三号仿宋,正文/附件说明下空一行左空二字,回行对齐。 | `minutes-1.png`、`minutes-2.png`;出席信息已移动到正文/附件说明之后,而不是错误地追加到版记之后。 | 截图通过;纪要可按机关实际制度调整。 |
|
||||
| 11 | 给出 A4 页边、普通首页、联合行文、末页、附件、信函、命令等式样。 | `contact-sheet.png` 覆盖普通、联合、末页、附件、信函、命令和纪要实际渲染场景。 | 截图通过;正式发文仍应对照本机关现行式样图。 |
|
||||
|
||||
## 本次发现并修正的实现问题
|
||||
|
||||
1. 命令(令)分支曾把20mm误当成纸张上边距,导致机关标志距离纸顶约20mm;现在恢复标准37mm天头,并在版心上边缘下20mm处排标志,实际截图已复核。
|
||||
2. 联合行文曾把联署机关名称错误压成同一横行;现在主办机关在前、联署机关名称分行排列,输入含“文件”时将“文件”置右并按联署名称上下居中;名称过长时自适应标志字号。
|
||||
3. 纪要出席、请假、列席名单曾追加在附件和版记之后;现在紧跟正文或附件说明下一行生成,符合10.3的编排位置。
|
||||
4. PDF 字形量测曾按 `yMin` 粗略分组,遇到高字形会把同一视觉行拆开;现在按字形垂直重叠合并后再量测,独立坐标结果稳定。
|
||||
5. 视觉审计补入了横排表格、22×28校准页、联合行文、分离装订附件、不盖章公文和命令(令)等场景,不再只依赖电子红头样例。
|
||||
|
||||
## 不能由生成器或截图单独保证的边界
|
||||
|
||||
- 当前机器没有方正小标宋和标准仿宋字体;生成器会明确报警,严格模式会停止。要交付标准字体稿,应在目标机器安装标准字体后重新生成并截图。
|
||||
- 纸张克重、白度、耐折度、不透明度、pH、黑红油墨、双面套正、裁切、装订、钉位和实体印章属于制作环节。
|
||||
- 正文单位、标点、数字语义,以及文种、行文关系、主送范围、机关名称规范化和发文权限属于内容或机关审校。
|
||||
- 横排表格的单双页表头朝向、复杂联合机关标志、超过两名签发人、复杂联合印章和目标机关专用模板仍需在 Word/WPS 或打印样张中完成。
|
||||
@@ -4,19 +4,17 @@
|
||||
|
||||
## 核验方法
|
||||
|
||||
2026 年 9 月 10 日运行:
|
||||
2026 年 9 月 11 日运行:
|
||||
|
||||
```bash
|
||||
bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
|
||||
bash tests/visual-audit.sh /tmp/gongwen-visual-audit-all-rules
|
||||
```
|
||||
|
||||
脚本实际生成 13 份 DOCX,逐份通过校验器,使用 LibreOffice 转 PDF,再用 `pdftoppm` 输出 24 张 PNG。人工检查以 PNG 为准,XML 和校验器只作为辅助证据。脚本还用 `pdftotext -bbox-layout` 独立量测电子红头首页:份号字形落在 37 mm 版心首行附近,机关标志字形落在 35 mm 版心上边缘下方附近,并把带版头字段与不带字段的机关标志位置差控制在 0.8 mm 内;结果写入运行目录的 `logs/header-coordinates.log`。当前批次的文件清单保存在运行目录的 `manifest.tsv`,每份文件的校验日志保存在 `logs/`。
|
||||
脚本实际生成 18 份 DOCX,逐份通过校验器,使用 LibreOffice 转 PDF,再用 `pdftoppm` 输出 37 个 PDF 页面和 PNG。人工检查以 PNG 为准,XML 和校验器只作为辅助证据。脚本还用 `pdftotext -bbox-layout` 独立量测电子红头首页:份号字形落在 37 mm 版心首行附近,机关标志字形落在 35 mm 版心上边缘下方附近;本批次带版头字段与不带字段的机关标志位置差为 0.00 mm,校验阈值为 0.8 mm;结果写入运行目录的 `logs/header-coordinates.log`。当前批次的文件清单保存在提交的 [`assets/visual-audit/all-rules/manifest.tsv`](assets/visual-audit/all-rules/manifest.tsv),单页证据和总览图保存在同目录;每份文件的校验日志保存在运行目录的 `logs/`。完整逐条矩阵见 [`gbt9704-screenshot-evidence.md`](gbt9704-screenshot-evidence.md)。
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
本批次的原始单页截图、场景清单和坐标日志见 [`assets/visual-audit/all-rules/`](assets/visual-audit/all-rules)。
|
||||
|
||||
## 场景证据
|
||||
|
||||
@@ -30,9 +28,9 @@ bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
|
||||
| 长文版记 | `formal-long-1.png`、`formal-long-2.png`、`formal-long-3.png` | 单页右、双页左页码连续;末页版记贴近版心底部,粗细线顺序可见。 |
|
||||
| 盖章/签名章位置 | `formal-seal-2.png`、`formal-signed-2.png` | 文字落款和日期位置已生成;实体印章或签名章图片没有被伪造,需在 Word/WPS 或打印环节完成。 |
|
||||
| 信函 | `letter-1.png`、`letter-fields-1.png` | 上粗下细双红线、文号、标题和页末上细下粗双红线可见;带份号、密级、紧急程度的回归页也按第一条双线下顺序出现;不显示页码。 |
|
||||
| 命令(令) | `command-1.png` | 机关标志、令号下空二行、正文前再空二行可见。 |
|
||||
| 纪要 | `minutes-1.png`、`minutes-2.png` | “出席/请假/列席”标签为黑体,人员为仿宋;人员标签左空二字,回行对齐。 |
|
||||
| 横排表格 | `table-1.png` | 当前示例仍是纵向表格。这一张图是故意保留的缺口证据,不能把普通纵向表格宣称为标准横排表格。 |
|
||||
| 命令(令) | `command-1.png` | 机关标志距版心上边缘20mm,令号下空二行、正文前再空二行可见。 |
|
||||
| 纪要 | `minutes-1.png`、`minutes-2.png` | “出席/请假/列席”标签为黑体,人员为仿宋;人员标签左空二字,回行对齐,且已移动到正文/附件说明下一行。 |
|
||||
| 横排表格 | `horizontal-table-1.png`、`table-1.png` | `horizontal-table-1.png` 已实际渲染 A4 横向页面和225mm表格;`table-1.png` 保留纵向对照。单双页表头朝向仍需目标 Word/WPS 模板处理。 |
|
||||
|
||||
## 条款逐项对照
|
||||
|
||||
@@ -60,8 +58,8 @@ bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
|
||||
| 7.2.1 | 份号六位、左上第一行 | 截图通过 | `formal-digital-all-1.png` 和 `formal-preprinted-1.png` 显示 `000007`;首面字段采用固定定位。 |
|
||||
| 7.2.2 | 密级和保密期限第二行、3 号黑体 | 截图通过 | `formal-digital-all-1.png` 显示左上第二行;保密期限具体写法仍需机关确认。 |
|
||||
| 7.2.3 | 紧急程度及顺序 | 截图通过 | `formal-digital-all-1.png` 显示份号、密级、紧急程度顺序;是否适用由发文机关决定。 |
|
||||
| 7.2.4 | 机关标志居中、红色、距版心上边缘 35 mm | 版式结构通过,标准字体需复核 | `formal-digital-all-1.png` 加 `references/assets/visual-audit/header-coordinates.log` 的 PDF 字形量测;左上字段不再推动机关标志。机关标志字体按标准为推荐小标宋,本机缺少该字体,严格模式失败。联合行文标志仍需专用模板。 |
|
||||
| 7.2.5 | 文号下空二行、年份、六角括号、序号规则 | 截图通过 | `formal-digital-all-1.png`、`formal-digital-upward-1.png`;输入验证拒绝“第”和前导零。 |
|
||||
| 7.2.4 | 机关标志居中、红色、距版心上边缘 35 mm | 版式结构通过,标准字体需复核 | `formal-digital-all-1.png` 加 `references/assets/visual-audit/header-coordinates.log` 的 PDF 字形量测;左上字段不再推动机关标志。机关标志字体按标准为推荐小标宋,本机缺少该字体,严格模式失败;`formal-joint-1.png` 已观察主办机关在前、联署机关分行排列且输入含“文件”时将“文件”置右并按联署名称上下居中,复杂机关专用标志仍需模板。 |
|
||||
| 7.2.5 | 机关标志下空二行、文号年份、六角括号、序号规则 | 截图通过 | `formal-digital-all-1.png`、`formal-digital-upward-1.png`;输入验证拒绝“第”和前导零。 |
|
||||
| 7.2.6 | 上行文签发人及多签发人 | 结构通过,仍需人工 | `formal-digital-upward-1.png` 已见同一行和两个签发人;超过两个签发人或联合行文转机关模板。 |
|
||||
| 7.2.7 | 文号下 4 mm、版心等宽红线 | 截图通过 | `formal-digital-all-1.png` 红线与版心同宽;预印模式不重绘,改由纸样提供。 |
|
||||
| 7.3.1 | 标题二号小标宋、红线下空二行、居中 | 版式结构通过,标准字体需复核 | `formal-digital-all-1.png`、`formal-preprinted-1.png`;本机缺小标宋时使用替代并报警,不能把替代字体称为标准字体。 |
|
||||
@@ -75,19 +73,22 @@ bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
|
||||
| 7.4.2 | 抄送机关字号、缩进、句号 | 截图通过 | `formal-long-3.png`;多行和主送移版记仍需结合机关模板。 |
|
||||
| 7.4.3 | 印发机关、日期、印发和细线 | 截图通过 | `formal-long-3.png`;实际名称过长时仍需在目标软件确认。 |
|
||||
| 7.5 | 页码字号、单双页位置、7 mm、附件连续 | 截图通过 | `formal-long-2.png`、`formal-long-3.png`、`formal-attachments-4.png`;空白页及特殊版记例外需人工复核。 |
|
||||
| 8 | 横排表格 | 当前未自动化 | `table-1.png` 明确显示当前生成器输出纵向表格;横排 A4 表格、表头方向和页码方向必须使用 Word/WPS 专用横向模板。 |
|
||||
| 8 | 横排表格 | 部分通过,表头方向未自动化 | `horizontal-table-1.png` 显示横向 A4 表格和标准页码;`table-1.png` 是纵向对照。单双页表头在订口/切口的方向切换尚未自动化,必须使用 Word/WPS 专用横向模板。 |
|
||||
| 9 | 计量单位、标点、数字 | 结构通过,仍需人工 | 字段边界已校验;正文语义必须按引用标准审校。 |
|
||||
| 10.1 | 信函格式 | 结构通过,仍需人工 | `letter-1.png` 显示 170 mm 上粗下细、下粗上细双线、文号、标题、无首页页码和无版记分隔线;可选份号、密级、紧急程度已在回归场景中按第一条双线下的顺序生成,联合行文仍需专用模板。 |
|
||||
| 10.2 | 命令(令)格式 | 结构通过,仍需人工 | `command-1.png` 显示机关全称加“命令”、令号下空二行和正文下空二行;签名章和日期按7.3.5.3仍需人工。 |
|
||||
| 10.3 | 纪要格式 | 截图通过 | `minutes-1.png`、`minutes-2.png`;机关标志 35 mm、人员标签字体和两字起排已观察。 |
|
||||
| 10.2 | 命令(令)格式 | 结构通过,仍需人工 | `command-1.png` 显示机关全称加“命令”、标志距版心上边缘20mm、令号下空二行和正文下空二行;签名章和日期按7.3.5.3仍需人工。 |
|
||||
| 10.3 | 纪要格式 | 截图通过 | `minutes-1.png`、`minutes-2.png`;机关标志35mm、人员标签字体、两字起排和人员名单在正文/附件说明下一行均已观察。 |
|
||||
| 11 | 标准式样图 | 结构通过,仍需人工 | 本记录使用标准条文和实际截图;最终发文仍须对照本单位现行式样图或授权模板。 |
|
||||
|
||||
## 发现并修正的实现问题
|
||||
|
||||
1. 版头字段曾以普通段落排列,份号、密级、紧急程度会把电子红头机关标志向下推移,预印红头套打的留白也会被挤开。现在这些字段改为首面固定定位,版心中保留等高占位;`formal-digital-all-1.png` 和 `formal-preprinted-1.png` 已重新观察。
|
||||
2. 红线下标题曾只依赖段前距,部分渲染器会把标题贴在线上。现在红线或纸上红线之后写入两个显式 28 磅空段落;截图能看到标题与红线之间的实际距离。
|
||||
3. 主送机关带不带末尾冒号时曾可能重复,现在归一化为一行全角冒号。
|
||||
4. 纪要出席名单曾从版心左边缘开始,现改为左空二字并用悬挂缩进让回行对齐。
|
||||
2. 命令(令)分支曾把20mm误当成纸顶边距;现在恢复37mm天头,并从版心上边缘下20mm量测。
|
||||
3. 联合行文曾把联署机关错误压成同一横行;现在主办机关在前、联署名称分行排列,输入含“文件”时将“文件”置右并按联署名称上下居中,名称过长时自适应标志字号。
|
||||
4. 纪要人员曾追加在附件和版记之后;现在紧跟正文或附件说明下一行。
|
||||
5. 红线下标题曾只依赖段前距,部分渲染器会把标题贴在线上。现在红线或纸上红线之后写入两个显式 28 磅空段落;截图能看到标题与红线之间的实际距离。
|
||||
6. 主送机关带不带末尾冒号时曾可能重复,现在归一化为一行全角冒号。
|
||||
7. 纪要出席名单曾从版心左边缘开始,现改为左空二字并用悬挂缩进让回行对齐。
|
||||
|
||||
## 仍然不能由本工具单独保证的事项
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ const MARGIN = { top: 2098, bottom: 1984, left: 1588, right: 1474 };
|
||||
const CONTENT_W = PAGE_W - MARGIN.left - MARGIN.right;
|
||||
const FIRST_LINE_INDENT = 640;
|
||||
const ONE_CHAR_INDENT = FIRST_LINE_INDENT / 2;
|
||||
// Four 3号 Chinese characters equal 1280 dxa; GB/T 9704-2012 7.3.5.1/7.3.5.3
|
||||
// uses this right indent for the signed/sealed date position.
|
||||
const FOUR_CHAR_RIGHT_INDENT = FIRST_LINE_INDENT * 2;
|
||||
// First-page red-head frames use the type-area top as their vertical anchor.
|
||||
// GB/T 9704-2012 7.2.1-7.2.4 then become direct coordinates: the first
|
||||
// header line is y=0 and the agency mark begins 35 mm below it. Keeping the
|
||||
@@ -108,7 +111,7 @@ function reportFontStatus(args) {
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage:
|
||||
node generate_gongwen_docx.mjs --input source.md --output out.docx [--format ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--org 发文机关] [--doc-no 发文字号] [--title 标题] [--subtitle 副标题] [--to 主送机关] [--sender 落款] [--date 日期] [--attachment-note 附件说明] [--attachment-file attachment.md ...] [--page-number center|standard|none] [--require-standard-fonts]
|
||||
node generate_gongwen_docx.mjs --input source.md --output out.docx [--format ordinary|formal|letter|command|minutes|horizontal-table] [--letterhead preprinted|digital] [--org 发文机关] [--joint-org 联署机关 ...] [--doc-no 发文字号] [--title 标题] [--subtitle 副标题] [--to 主送机关] [--sender 落款] [--date 日期] [--attachment-note 附件说明] [--attachment-file attachment.md ...] [--attachment-detached] [--page-number center|standard|none] [--require-standard-fonts]
|
||||
|
||||
Notes:
|
||||
- Converts Markdown to a GB/T 9704-2012 page-layout DOCX.
|
||||
@@ -124,8 +127,8 @@ Notes:
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
const flags = new Set(["help", "no-page-number", "no-page-numbers", "require-standard-fonts"]);
|
||||
const repeatable = new Set(["attachment-file"]);
|
||||
const flags = new Set(["help", "no-page-number", "no-page-numbers", "require-standard-fonts", "attachment-detached"]);
|
||||
const repeatable = new Set(["attachment-file", "joint-org"]);
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
const key = argv[i];
|
||||
if (!key.startsWith("--")) continue;
|
||||
@@ -134,10 +137,14 @@ function parseArgs(argv) {
|
||||
args[name] = true;
|
||||
continue;
|
||||
}
|
||||
const value = argv[i + 1];
|
||||
if (value === undefined || value.startsWith("--")) {
|
||||
throw new Error(`--${name} requires a value`);
|
||||
}
|
||||
if (repeatable.has(name)) {
|
||||
args[name] = [...(args[name] ?? []), argv[i + 1]];
|
||||
args[name] = [...(args[name] ?? []), value];
|
||||
} else {
|
||||
args[name] = argv[i + 1];
|
||||
args[name] = value;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
@@ -175,7 +182,8 @@ function run(text, opts = {}) {
|
||||
const size = opts.size ?? "32";
|
||||
const bold = opts.bold ? "<w:b/>" : "";
|
||||
const color = opts.color ?? "000000";
|
||||
return `<w:r><w:rPr><w:rFonts ${fontAttrs(opts)}/>${bold}<w:color w:val="${color}"/><w:sz w:val="${size}"/><w:szCs w:val="${size}"/><w:lang w:val="en-US" w:eastAsia="zh-CN"/></w:rPr><w:t xml:space="preserve">${esc(text)}</w:t></w:r>`;
|
||||
const textXml = String(text ?? "").split("\n").map((line, index) => `${index ? "<w:br/>" : ""}<w:t xml:space="preserve">${esc(line)}</w:t>`).join("");
|
||||
return `<w:r><w:rPr><w:rFonts ${fontAttrs(opts)}/>${bold}<w:color w:val="${color}"/><w:sz w:val="${size}"/><w:szCs w:val="${size}"/><w:lang w:val="en-US" w:eastAsia="zh-CN"/></w:rPr>${textXml}</w:r>`;
|
||||
}
|
||||
|
||||
function paragraph(text, opts = {}) {
|
||||
@@ -223,19 +231,35 @@ function floatingHeaderFieldParagraph(text, fontPreset, index) {
|
||||
});
|
||||
}
|
||||
|
||||
function floatingAgencyMarkParagraph(text) {
|
||||
function floatingAgencyMarkParagraph(text, lineCount = 1, size = "56") {
|
||||
return paragraph(text, {
|
||||
align: "center",
|
||||
indent: false,
|
||||
fontPreset: "title",
|
||||
size: "56",
|
||||
size,
|
||||
color: "FF0000",
|
||||
before: AGENCY_FRAME_BEFORE,
|
||||
frame: { hAnchor: "margin", vAnchor: "margin", xAlign: "center", y: AGENCY_FRAME_Y, w: CONTENT_W, h: 800 },
|
||||
frame: { hAnchor: "margin", vAnchor: "margin", xAlign: "center", y: AGENCY_FRAME_Y, w: CONTENT_W, h: 800 * Math.max(1, lineCount) },
|
||||
});
|
||||
}
|
||||
|
||||
function agencyMarkFlowSpacer() {
|
||||
function floatingJointAgencyTable(names, size = "48", includeFile = true) {
|
||||
const maxNameLength = Math.max(...names.map((name) => [...name].length), 1);
|
||||
const rightWidth = includeFile ? 1600 : 0;
|
||||
const leftWidth = Math.min(CONTENT_W - rightWidth, Math.max(3600, maxNameLength * 520 + 400));
|
||||
const tableWidth = leftWidth + rightWidth;
|
||||
const cellMargins = '<w:tcMar><w:top w:w="0" w:type="dxa"/><w:left w:w="0" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="0" w:type="dxa"/></w:tcMar>';
|
||||
const lineParagraph = (text) => `<w:p><w:pPr><w:jc w:val="center"/><w:spacing w:before="0" w:after="0" w:line="560" w:lineRule="exact"/><w:snapToGrid w:val="true"/><w:kinsoku w:val="true"/></w:pPr>${run(text, { fontPreset: "title", size, color: "FF0000" })}</w:p>`;
|
||||
const leftCell = `<w:tc><w:tcPr><w:tcW w:w="${leftWidth}" w:type="dxa"/><w:vAlign w:val="center"/>${cellMargins}</w:tcPr>${names.map(lineParagraph).join("")}</w:tc>`;
|
||||
const rightCell = includeFile
|
||||
? `<w:tc><w:tcPr><w:tcW w:w="${rightWidth}" w:type="dxa"/><w:vAlign w:val="center"/>${cellMargins}</w:tcPr>${lineParagraph("文件")}</w:tc>`
|
||||
: "";
|
||||
const tablePr = `<w:tblPr><w:tblpPr w:leftFromText="0" w:rightFromText="0" w:topFromText="0" w:bottomFromText="0" w:vertAnchor="margin" w:horzAnchor="margin" w:tblpXSpec="center" w:tblpY="${AGENCY_FRAME_Y}"/><w:tblOverlap w:val="never"/><w:tblW w:w="${tableWidth}" w:type="dxa"/><w:tblLayout w:type="fixed"/><w:tblBorders><w:top w:val="nil"/><w:left w:val="nil"/><w:bottom w:val="nil"/><w:right w:val="nil"/><w:insideH w:val="nil"/><w:insideV w:val="nil"/></w:tblBorders><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="0" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="0" w:type="dxa"/></w:tblCellMar></w:tblPr>`;
|
||||
const grid = `<w:gridCol w:w="${leftWidth}"/>${includeFile ? `<w:gridCol w:w="${rightWidth}"/>` : ""}`;
|
||||
return `<w:tbl>${tablePr}<w:tblGrid>${grid}</w:tblGrid><w:tr><w:trPr><w:cantSplit/></w:trPr>${leftCell}${rightCell}</w:tr></w:tbl>`;
|
||||
}
|
||||
|
||||
function agencyMarkFlowSpacer(extraDxa = 0) {
|
||||
return paragraph("\u00a0", {
|
||||
align: "left",
|
||||
indent: false,
|
||||
@@ -243,7 +267,7 @@ function agencyMarkFlowSpacer() {
|
||||
size: "2",
|
||||
line: "560",
|
||||
snapToGrid: false,
|
||||
before: AGENCY_FLOW_BEFORE,
|
||||
before: AGENCY_FLOW_BEFORE + extraDxa,
|
||||
after: "0",
|
||||
});
|
||||
}
|
||||
@@ -473,15 +497,15 @@ function estimatedTextWidthDxa(text) {
|
||||
function signatureParagraphs(args) {
|
||||
if (args["seal-mode"] === "signed") {
|
||||
if (!args.signer || !args["signer-title"]) throw new Error("signed seal mode requires --signer and --signer-title");
|
||||
const lines = [rightWithIndent(`${args["signer-title"]} ${args.signer}`, FIRST_LINE_INDENT * 2)];
|
||||
if (args.date) lines.push(...blankGridLines(1), rightWithIndent(args.date, FIRST_LINE_INDENT * 2));
|
||||
const lines = [rightWithIndent(`${args["signer-title"]} ${args.signer}`, FOUR_CHAR_RIGHT_INDENT)];
|
||||
if (args.date) lines.push(...blankGridLines(1), rightWithIndent(args.date, FOUR_CHAR_RIGHT_INDENT));
|
||||
return lines;
|
||||
}
|
||||
if (args["seal-mode"] === "seal") {
|
||||
if (!args.sender && !args.date) return [];
|
||||
const lines = [];
|
||||
if (args.sender) lines.push(rightWithIndent(args.sender, FIRST_LINE_INDENT * 2));
|
||||
if (args.date) lines.push(rightWithIndent(args.date, FIRST_LINE_INDENT * 2));
|
||||
if (args.sender) lines.push(rightWithIndent(args.sender, FOUR_CHAR_RIGHT_INDENT));
|
||||
if (args.date) lines.push(rightWithIndent(args.date, FOUR_CHAR_RIGHT_INDENT));
|
||||
return lines;
|
||||
}
|
||||
if (!args.sender && !args.date) return [];
|
||||
@@ -547,15 +571,15 @@ function blankGridLines(count = 2) {
|
||||
return Array.from({ length: count }, () => blankGridLine());
|
||||
}
|
||||
|
||||
function tableXml(headers, rows) {
|
||||
function tableXml(headers, rows, width = CONTENT_W) {
|
||||
const colCount = headers.length;
|
||||
const colWidth = Math.floor(CONTENT_W / colCount);
|
||||
const colWidth = Math.floor(width / colCount);
|
||||
const grid = Array.from({ length: colCount }, () => `<w:gridCol w:w="${colWidth}"/>`).join("");
|
||||
const tr = (cells, header = false) => {
|
||||
const trPr = header ? "<w:trPr><w:tblHeader/></w:trPr>" : "";
|
||||
return `<w:tr>${trPr}${cells.map((cell) => tableCell(cell, colWidth, header)).join("")}</w:tr>`;
|
||||
};
|
||||
return `<w:tbl><w:tblPr><w:tblW w:w="${CONTENT_W}" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/></w:tblBorders><w:tblLook w:firstRow="1" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/></w:tblPr><w:tblGrid>${grid}</w:tblGrid>${tr(headers, true)}${rows.map((row) => tr(row)).join("")}</w:tbl>`;
|
||||
return `<w:tbl><w:tblPr><w:tblW w:w="${width}" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/></w:tblBorders><w:tblLook w:firstRow="1" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/></w:tblPr><w:tblGrid>${grid}</w:tblGrid>${tr(headers, true)}${rows.map((row) => tr(row)).join("")}</w:tbl>`;
|
||||
}
|
||||
|
||||
function tableCell(text, width, header) {
|
||||
@@ -715,14 +739,14 @@ function renderParagraph(block) {
|
||||
return paragraph(block.text);
|
||||
}
|
||||
|
||||
function attachmentPage(blocks, index, filePath) {
|
||||
function attachmentPage(blocks, index, filePath, detachedDocNo = "") {
|
||||
const attachmentBlocks = [...blocks];
|
||||
let attachmentTitle = path.basename(filePath, path.extname(filePath));
|
||||
if (attachmentBlocks[0]?.type === "heading") {
|
||||
attachmentTitle = attachmentBlocks.shift().text;
|
||||
}
|
||||
const body = [
|
||||
paragraph(`附件${index}`, { align: "left", indent: false, fontPreset: "h1", size: "32", pageBreakBefore: true, keepNext: true }),
|
||||
paragraph(detachedDocNo ? `${detachedDocNo} 附件${index}` : `附件${index}`, { align: "left", indent: false, fontPreset: "h1", size: "32", pageBreakBefore: true, keepNext: true }),
|
||||
emptyLine(),
|
||||
titleParagraph(attachmentTitle),
|
||||
];
|
||||
@@ -738,18 +762,22 @@ function resolvePageNumberMode(args, format) {
|
||||
if (args["no-page-number"] || args["no-page-numbers"]) return "none";
|
||||
if (["center", "standard", "none"].includes(args["page-number"])) return args["page-number"];
|
||||
if (format === "letter") return "none";
|
||||
if (format === "formal" || format === "command" || format === "minutes") return "standard";
|
||||
if (format === "formal" || format === "command" || format === "minutes" || format === "horizontal-table") return "standard";
|
||||
return "center";
|
||||
}
|
||||
|
||||
function buildDocument(blocks, args) {
|
||||
let title = args.title;
|
||||
const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary";
|
||||
const format = ["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"].includes(args.format) ? args.format : "ordinary";
|
||||
const letterhead = args.letterhead === "digital" ? "digital" : "preprinted";
|
||||
const pageNumberMode = resolvePageNumberMode(args, format);
|
||||
const horizontalTable = format === "horizontal-table" || args["horizontal-table"] === true || args["horizontal-table"] === "true";
|
||||
const horizontalContentWidth = mmToDxa(225);
|
||||
const body = [];
|
||||
let titleGapLines = 0;
|
||||
const pageMarginTop = format === "letter" ? 1701 : format === "command" ? 1134 : 2098;
|
||||
// All formats keep the standard 37 mm top white margin. Special formats
|
||||
// offset their marks inside the type area, measured from that boundary.
|
||||
const pageMarginTop = format === "letter" ? 1701 : 2098;
|
||||
const sourceBlocks = [...blocks];
|
||||
if (!title && sourceBlocks[0]?.type === "heading") {
|
||||
title = sourceBlocks.shift().text;
|
||||
@@ -779,8 +807,19 @@ function buildDocument(blocks, args) {
|
||||
// GB/T 9704-2012 7.2.4 fixes the agency mark at 35 mm below the
|
||||
// type-area top. Left-corner fields occupy their own column and must
|
||||
// not move the centered mark upward.
|
||||
body.push(floatingAgencyMarkParagraph(args.org));
|
||||
body.push(agencyMarkFlowSpacer());
|
||||
const jointOrganizations = Array.isArray(args["joint-org"]) ? args["joint-org"].map((name) => String(name).trim()).filter(Boolean) : [];
|
||||
const agencyNames = [String(args.org).trim(), ...jointOrganizations];
|
||||
// GB/T 9704-2012 7.2.4 keeps joint agency names in order with the
|
||||
// host first. When “文件” is present it sits to the right of the
|
||||
// stacked names and is vertically centred against that name block.
|
||||
const host = String(args.org).trim();
|
||||
const jointNames = jointOrganizations.length && host.endsWith("文件")
|
||||
? [host.slice(0, -2), ...jointOrganizations]
|
||||
: agencyNames;
|
||||
const agencyMarkSize = agencyNames.length > 1 ? (Math.max(...jointNames.map((name) => [...name].length)) > 10 ? "44" : "48") : "56";
|
||||
if (jointOrganizations.length) body.push(floatingJointAgencyTable(jointNames, agencyMarkSize, host.endsWith("文件")));
|
||||
else body.push(floatingAgencyMarkParagraph(agencyNames.join(" "), 1, agencyMarkSize));
|
||||
body.push(agencyMarkFlowSpacer(jointOrganizations.length ? Math.max(0, jointNames.length - 1) * 560 : 0));
|
||||
if (args["doc-no"]) body.push(...blankGridLines(2));
|
||||
}
|
||||
if (letterhead === "preprinted") {
|
||||
@@ -790,6 +829,10 @@ function buildDocument(blocks, args) {
|
||||
// separator. Its reserve is a paper-coordinate boundary, so optional
|
||||
// copy/secret/urgency fields must not shorten it.
|
||||
body.push(letterheadReserveParagraph(reserve));
|
||||
// The preprinted paper supplies the agency mark and separator. Keep the
|
||||
// variable document number below the reserved mark block by preserving
|
||||
// the same two 28-point grid lines used under a digital agency mark.
|
||||
if (args["doc-no"]) body.push(...blankGridLines(2));
|
||||
}
|
||||
if (args["doc-no"]) args["doc-no"] = normalizeDocumentNumber(args["doc-no"]);
|
||||
if (upward && args["doc-no"] && args.signer) body.push(upwardHeaderParagraph(args["doc-no"], args.signer));
|
||||
@@ -819,7 +862,9 @@ function buildDocument(blocks, args) {
|
||||
if (!args.org) throw new Error("command format requires --org");
|
||||
if (!/(?:命令|令)$/.test(String(args.org).trim())) throw new Error("command --org must end with“命令”or“令”");
|
||||
if (!args["doc-no"]) throw new Error("command format requires --doc-no for the 令号");
|
||||
body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "0" }));
|
||||
// GB/T 9704-2012 10.2 measures the command mark from the top edge of
|
||||
// the type area, not from the paper edge: 37 mm + 20 mm from the page.
|
||||
body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: String(mmToDxa(20)), after: "0" }));
|
||||
if (args["doc-no"]) {
|
||||
body.push(...blankGridLines(2));
|
||||
body.push(paragraph(args["doc-no"], { align: "center", indent: false, fontPreset: "body", size: "32", after: "0" }));
|
||||
@@ -845,7 +890,7 @@ function buildDocument(blocks, args) {
|
||||
continue;
|
||||
}
|
||||
if (block.type === "table") {
|
||||
body.push(tableXml(block.headers, block.rows));
|
||||
body.push(tableXml(block.headers, block.rows, horizontalTable ? horizontalContentWidth : CONTENT_W));
|
||||
body.push(emptyLine());
|
||||
continue;
|
||||
}
|
||||
@@ -855,6 +900,13 @@ function buildDocument(blocks, args) {
|
||||
if (body.at(-1) === emptyLine()) body.pop();
|
||||
body.push(attachmentNote(args["attachment-note"]));
|
||||
}
|
||||
// GB/T 9704-2012 10.3 places attendance information one line below the
|
||||
// minutes body (or its attachment note), before any trailing signature or
|
||||
// note material. Keeping it here also makes the screenshot evidence match
|
||||
// the standard's “正文或附件说明下空一行” rule.
|
||||
if (format === "minutes" && args.attendees) body.push(minutesPersonParagraph("出席", args.attendees, "560"));
|
||||
if (format === "minutes" && args.absent) body.push(minutesPersonParagraph("请假", args.absent));
|
||||
if (format === "minutes" && args.observers) body.push(minutesPersonParagraph("列席", args.observers));
|
||||
if (args.sender || args.date) {
|
||||
body.push(...(args["seal-mode"] === "signed" ? blankGridLines(2) : [emptyLine()]));
|
||||
body.push(...signatureParagraphs(args));
|
||||
@@ -867,13 +919,11 @@ function buildDocument(blocks, args) {
|
||||
return absolutePath;
|
||||
});
|
||||
validateAttachmentConsistency(args["attachment-note"], absoluteAttachmentFiles);
|
||||
if (args["attachment-detached"] && !args["doc-no"]) throw new Error("--attachment-detached requires --doc-no so the detached attachment can carry the document number");
|
||||
absoluteAttachmentFiles.forEach((absolutePath, index) => {
|
||||
const attachmentBlocks = parseMarkdown(fs.readFileSync(absolutePath, "utf8"));
|
||||
body.push(attachmentPage(attachmentBlocks, index + 1, absolutePath));
|
||||
body.push(attachmentPage(attachmentBlocks, index + 1, absolutePath, args["attachment-detached"] ? args["doc-no"] : ""));
|
||||
});
|
||||
if (format === "minutes" && args.attendees) body.push(minutesPersonParagraph("出席", args.attendees, "560"));
|
||||
if (format === "minutes" && args.absent) body.push(minutesPersonParagraph("请假", args.absent));
|
||||
if (format === "minutes" && args.observers) body.push(minutesPersonParagraph("列席", args.observers));
|
||||
if (format !== "letter") body.push(...colophon(args));
|
||||
const letterFooter = format === "letter" ? '<w:footerReference w:type="default" r:id="rIdLetterFooter"/>' : "";
|
||||
const footerRefs = letterFooter || (pageNumberMode === "standard"
|
||||
@@ -882,7 +932,10 @@ function buildDocument(blocks, args) {
|
||||
? '<w:footerReference w:type="default" r:id="rIdFooterCenter"/>'
|
||||
: "");
|
||||
const footerDistance = format === "letter" || pageNumberMode === "standard" ? "1134" : "1588";
|
||||
body.push(`<w:sectPr>${footerRefs}<w:pgSz w:w="${PAGE_W}" w:h="${PAGE_H}"/><w:pgMar w:top="${pageMarginTop}" w:right="${MARGIN.right}" w:bottom="${MARGIN.bottom}" w:left="${MARGIN.left}" w:header="720" w:footer="${footerDistance}" w:gutter="0"/><w:docGrid w:type="lines" w:linePitch="560"/></w:sectPr>`);
|
||||
const pageSize = horizontalTable
|
||||
? `<w:pgSz w:w="${PAGE_H}" w:h="${PAGE_W}" w:orient="landscape"/>`
|
||||
: `<w:pgSz w:w="${PAGE_W}" w:h="${PAGE_H}"/>`;
|
||||
body.push(`<w:sectPr>${footerRefs}${pageSize}<w:pgMar w:top="${pageMarginTop}" w:right="${MARGIN.right}" w:bottom="${MARGIN.bottom}" w:left="${MARGIN.left}" w:header="720" w:footer="${footerDistance}" w:gutter="0"/><w:docGrid w:type="lines" w:linePitch="560"/></w:sectPr>`);
|
||||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml"><w:body>${body.join("")}</w:body></w:document>`;
|
||||
}
|
||||
|
||||
@@ -988,11 +1041,26 @@ function writeDocx(output, documentXml, pageNumberMode, format) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv);
|
||||
let args;
|
||||
try {
|
||||
args = parseArgs(process.argv);
|
||||
} catch (error) {
|
||||
console.error(`GENERATOR ERROR: ${error.message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
if (args.help || !args.input || !args.output) {
|
||||
usage();
|
||||
process.exit(args.help ? 0 : 1);
|
||||
}
|
||||
const supportedFormats = ["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"];
|
||||
try {
|
||||
if (args.format !== undefined && !supportedFormats.includes(args.format)) throw new Error(`--format must be one of ${supportedFormats.join(", ")}`);
|
||||
if (args.letterhead !== undefined && !["preprinted", "digital"].includes(args.letterhead)) throw new Error("--letterhead must be preprinted or digital");
|
||||
if (args["page-number"] !== undefined && !["center", "standard", "none"].includes(args["page-number"])) throw new Error("--page-number must be center, standard, or none");
|
||||
} catch (error) {
|
||||
console.error(`GENERATOR ERROR: ${error.message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
try {
|
||||
reportFontStatus(args);
|
||||
} catch (error) {
|
||||
@@ -1003,7 +1071,7 @@ const md = fs.readFileSync(args.input, "utf8");
|
||||
try {
|
||||
const blocks = parseMarkdown(md);
|
||||
const documentXml = buildDocument(blocks, args);
|
||||
const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary";
|
||||
const format = ["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"].includes(args.format) ? args.format : "ordinary";
|
||||
const pageNumberMode = resolvePageNumberMode(args, format);
|
||||
writeDocx(path.resolve(args.output), documentXml, pageNumberMode, format);
|
||||
console.log(`Generated: ${path.resolve(args.output)}`);
|
||||
|
||||
@@ -6,7 +6,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
function usage() {
|
||||
console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--require-standard-fonts]");
|
||||
console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes|horizontal-table] [--letterhead preprinted|digital] [--require-standard-fonts]");
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
@@ -35,7 +35,7 @@ function paragraphs(xml) { return xml.match(/<w:p(?:\s[^>]*)?>[\s\S]*?<\/w:p>/g)
|
||||
const args = parseArgs(process.argv);
|
||||
if (!args.input || !fs.existsSync(args.input)) { usage(); process.exit(2); }
|
||||
const profile = args.profile ?? "ordinary";
|
||||
if (!["ordinary", "formal", "letter", "command", "minutes"].includes(profile)) { console.error("Unsupported profile."); process.exit(2); }
|
||||
if (!["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"].includes(profile)) { console.error("Unsupported profile."); process.exit(2); }
|
||||
|
||||
const doc = readPart(args.input, "word/document.xml");
|
||||
const styles = readPart(args.input, "word/styles.xml");
|
||||
@@ -71,9 +71,10 @@ const installedFonts = (() => {
|
||||
const checks = [];
|
||||
function check(name, ok, detail) { checks.push({ name, ok, detail }); }
|
||||
|
||||
check("A4 page size", /<w:pgSz[^>]*w:w="11906"[^>]*w:h="16838"/.test(doc), "210 mm x 297 mm");
|
||||
const expectedTopMargin = profile === "letter" ? "1701" : profile === "command" ? "1134" : "2098";
|
||||
check("Page margins", new RegExp(`<w:pgMar[^>]*w:top="${expectedTopMargin}"[^>]*w:right="1474"[^>]*w:bottom="1984"[^>]*w:left="1588"`).test(doc), `top ${expectedTopMargin === "2098" ? 37 : expectedTopMargin === "1701" ? 30 : 20} mm, bottom 35, left 28, right 26 mm`);
|
||||
const horizontal = profile === "horizontal-table";
|
||||
check("A4 page size", horizontal ? /<w:pgSz[^>]*w:w="16838"[^>]*w:h="11906"[^>]*w:orient="landscape"/.test(doc) : /<w:pgSz[^>]*w:w="11906"[^>]*w:h="16838"/.test(doc), horizontal ? "297 mm x 210 mm landscape table page" : "210 mm x 297 mm");
|
||||
const expectedTopMargin = profile === "letter" ? "1701" : "2098";
|
||||
check("Page margins", new RegExp(`<w:pgMar[^>]*w:top="${expectedTopMargin}"[^>]*w:right="1474"[^>]*w:bottom="1984"[^>]*w:left="1588"`).test(doc), `top ${expectedTopMargin === "2098" ? 37 : 30} mm, bottom 35, left 28, right 26 mm`);
|
||||
check("Document grid", /<w:docGrid[^>]*w:linePitch="560"/.test(doc), "28 pt implementation grid");
|
||||
const bodyFontStandard = /w:eastAsia="(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312)"/.test(styles);
|
||||
const bodyFontAvailable = bodyFontStandard || (!requireStandardFonts && /w:eastAsia="STFangsong"/.test(styles));
|
||||
@@ -122,25 +123,40 @@ if (profile === "ordinary") {
|
||||
check("Odd/even page positions", /w:jc w:val="right"/.test(footerOdd) && /w:jc w:val="left"/.test(footerEven), "odd right and even left");
|
||||
check("Formal page-number distance", /w:footer="1134"/.test(doc), "page-number footer is 7 mm below the 225 mm type area");
|
||||
}
|
||||
if (horizontal) {
|
||||
check("Horizontal table width", /<w:tblW w:w="12756" w:type="dxa"/.test(doc), "table uses the 225 mm landscape writing width");
|
||||
}
|
||||
if (profile === "formal") {
|
||||
if (args.letterhead === "digital") {
|
||||
check("Digital red head", /w:color w:val="FF0000"/.test(doc), "digital formal output contains requested red elements");
|
||||
check("Red rule thickness", /height:0\.5mm/.test(doc), "header separator uses the recommended 0.35-0.5 mm range");
|
||||
check("Red rule width", /style="width:156mm;height:0\.5mm"/.test(doc), "header separator spans the 156 mm type area");
|
||||
const docParagraphs = paragraphs(doc);
|
||||
const agencyIndex = docParagraphs.findIndex((p) => /w:sz w:val="56"/.test(p) && /w:color w:val="FF0000"/.test(p));
|
||||
const agencyIndex = docParagraphs.findIndex((p) => /w:sz w:val="(?:56|48|44)"/.test(p) && /w:color w:val="FF0000"/.test(p));
|
||||
const jointAgencyTable = doc.match(/<w:tbl>[\s\S]*?<w:tblpPr[^>]*w:vertAnchor="margin"[^>]*w:horzAnchor="margin"[^>]*w:tblpXSpec="center"[^>]*w:tblpY="1984"[\s\S]*?<\/w:tbl>/)?.[0] ?? "";
|
||||
// Keep this expected value independent from the generator's calibration
|
||||
// constants. It is 35 mm in twentieths of a point, as required by 7.2.4.
|
||||
const agencyOffsetFromTypeAreaTop = Math.round(35 * 56.692913);
|
||||
check("Digital red-head agency position", agencyIndex >= 0 && new RegExp(`w:framePr[^>]*w:hAnchor="margin"[^>]*w:vAnchor="margin"[^>]*w:xAlign="center"[^>]*w:y="${agencyOffsetFromTypeAreaTop}"`).test(docParagraphs[agencyIndex]) && /w:before="0"/.test(docParagraphs[agencyIndex]), "agency mark is independently fixed 35 mm below the type-area top");
|
||||
const agencyFrameOk = agencyIndex >= 0
|
||||
&& new RegExp(`w:framePr[^>]*w:hAnchor="margin"[^>]*w:vAnchor="margin"[^>]*w:xAlign="center"[^>]*w:y="${agencyOffsetFromTypeAreaTop}"`).test(docParagraphs[agencyIndex])
|
||||
&& /w:before="0"/.test(docParagraphs[agencyIndex]);
|
||||
const jointAgencyFrameOk = Boolean(jointAgencyTable) && new RegExp(`w:tblpY="${agencyOffsetFromTypeAreaTop}"`).test(jointAgencyTable);
|
||||
check("Digital red-head agency position", agencyFrameOk || jointAgencyFrameOk, "agency mark is independently fixed 35 mm below the type-area top");
|
||||
if (jointAgencyTable) {
|
||||
const jointRows = (jointAgencyTable.match(/<w:p>/g) ?? []).length;
|
||||
const hasFileCell = /<w:t[^>]*>文件<\/w:t>/.test(jointAgencyTable);
|
||||
const centeredCells = (jointAgencyTable.match(/w:vAlign w:val="center"/g) ?? []).length;
|
||||
check("Joint agency vertical layout", jointRows >= 2 && centeredCells >= (hasFileCell ? 2 : 1) && /w:color w:val="FF0000"/.test(jointAgencyTable), hasFileCell ? "joint names are stacked and 文件 is vertically centered in a right-side cell" : "joint names are stacked and vertically centered");
|
||||
}
|
||||
const fixedHeaderFrames = docParagraphs
|
||||
.filter((p) => /w:framePr[^>]*w:hAnchor="margin"[^>]*w:vAnchor="margin"/.test(p))
|
||||
.map((p) => Number((p.match(/w:y="(\d+)"/) ?? [])[1]))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
check("Digital red-head field frames", fixedHeaderFrames.every((value, index) => value === index * 560 || value === agencyOffsetFromTypeAreaTop), "left-corner fields use first-line grid positions without changing the agency mark anchor");
|
||||
const docNoIndex = docParagraphs.findIndex((p) => /〔\d{4}〕[1-9]\d*号/.test(textOf(p)));
|
||||
const agencyToDocNo = agencyIndex >= 0 && docNoIndex > agencyIndex ? docParagraphs.slice(agencyIndex + 1, docNoIndex) : [];
|
||||
const agencyFlowSpacer = agencyToDocNo.filter((p) => /w:before="1880"/.test(p));
|
||||
const agencyBoundary = agencyIndex >= 0 ? agencyIndex + 1 : jointAgencyTable ? 0 : -1;
|
||||
const agencyToDocNo = agencyBoundary >= 0 && docNoIndex > agencyBoundary ? docParagraphs.slice(agencyBoundary, docNoIndex) : [];
|
||||
const agencyFlowSpacer = agencyToDocNo.filter((p) => /w:before="[1-9]\d*"/.test(p) && /w:snapToGrid w:val="false"/.test(p));
|
||||
const agencyGapLines = agencyToDocNo.slice(-2);
|
||||
check("Agency mark to document number two blank lines", docNoIndex < 0 || (agencyFlowSpacer.length === 1 && agencyGapLines.length === 2 && agencyGapLines.every(isExplicitBlankGridLine)), "document number follows the fixed agency mark with two exact 28-point grid lines");
|
||||
check("Two blank lines below red rule", hasExplicitTwoLineTitleGap, "title is preceded by two exact 28-point blank paragraphs below the red rule");
|
||||
@@ -149,11 +165,29 @@ if (profile === "formal") {
|
||||
check("Upward document number and signer share one row", paragraphs(doc).some((p) => /签发人:/.test(textOf(p)) && /〔|\[|文号|发/.test(textOf(p))), "signer is in the same paragraph row as the document number");
|
||||
}
|
||||
} else {
|
||||
check("Preprinted letterhead reserve", /w:before="\d{4,}"/.test(doc), "first-page top reserve is present");
|
||||
const preprintedParagraphs = paragraphs(doc);
|
||||
const reserveCandidate = (p) => /w:jc w:val="left"/.test(p)
|
||||
&& /w:line="1"/.test(p)
|
||||
&& /w:lineRule="exact"/.test(p)
|
||||
&& /w:snapToGrid w:val="false"/.test(p)
|
||||
&& /w:sz w:val="2"/.test(p)
|
||||
&& /<w:t[^>]*> <\/w:t>/.test(p);
|
||||
const reserveIndex = preprintedParagraphs.findIndex(reserveCandidate);
|
||||
const reserveParagraph = reserveIndex >= 0 ? preprintedParagraphs[reserveIndex] : "";
|
||||
const reserveSpacing = reserveParagraph.match(/<w:spacing\b[^>]*>/)?.[0] ?? "";
|
||||
const reserveBefore = Number((reserveSpacing.match(/w:before="(\d+)"/) ?? [])[1]);
|
||||
const reserveBeforeInRange = Number.isFinite(reserveBefore) && reserveBefore >= 0 && reserveBefore <= Math.round((130 - 37) * 56.692913);
|
||||
const priorParagraphsAreFloating = reserveIndex >= 0 && preprintedParagraphs.slice(0, reserveIndex).every((p) => /w:framePr\b/.test(p));
|
||||
const preprintedReserveParagraph = reserveIndex >= 0
|
||||
&& priorParagraphsAreFloating
|
||||
&& reserveBeforeInRange;
|
||||
check("Preprinted letterhead reserve", preprintedReserveParagraph, "first non-floating body paragraph reserves 37-130 mm paper-top agency-mark space");
|
||||
const titleXmlStart = firstTitleIndex < 0 ? doc.length : doc.indexOf(preprintedParagraphs[firstTitleIndex]);
|
||||
const preprintedHeaderXml = titleXmlStart < 0 ? doc : doc.slice(0, titleXmlStart);
|
||||
check("No red drawing for preprinted letterhead", !/w:color w:val="FF0000"/.test(preprintedHeaderXml), "preprinted mode does not redraw red letterhead or rule");
|
||||
const preprintedDocNoIndex = preprintedParagraphs.findIndex((p) => /〔\d{4}〕[1-9]\d*号/.test(textOf(p)));
|
||||
const preprintedDocNoGap = preprintedDocNoIndex >= 0 ? preprintedParagraphs.slice(0, preprintedDocNoIndex).slice(-2) : [];
|
||||
check("Preprinted agency-to-document number two blank lines", preprintedDocNoIndex < 0 || (preprintedDocNoGap.length === 2 && preprintedDocNoGap.every(isExplicitBlankGridLine)), "document number starts below the reserved physical agency mark block");
|
||||
check("Title spacing below physical red rule", hasExplicitTwoLineTitleGap, "title is preceded by two exact 28-point blank paragraphs below the printed red rule");
|
||||
}
|
||||
}
|
||||
@@ -165,6 +199,7 @@ if (profile === "letter") {
|
||||
if (profile === "command") {
|
||||
const commandParagraphs = paragraphs(doc);
|
||||
const commandAgencyIndex = commandParagraphs.findIndex((p) => /w:color w:val="FF0000"/.test(p));
|
||||
check("Command agency position", commandAgencyIndex >= 0 && /w:before="1134"/.test(commandParagraphs[commandAgencyIndex]), "command mark starts 20 mm below the 37 mm type-area top");
|
||||
const commandOrderIndex = commandParagraphs.findIndex((p, index) => index > commandAgencyIndex && /w:jc w:val="center"/.test(p) && /w:sz w:val="32"/.test(p) && !/w:color w:val="FF0000"/.test(p));
|
||||
const agencyToOrder = commandAgencyIndex >= 0 && commandOrderIndex > commandAgencyIndex ? commandParagraphs.slice(commandAgencyIndex + 1, commandOrderIndex) : [];
|
||||
check("Command agency-to-order spacing", commandOrderIndex < 0 || (agencyToOrder.length === 2 && agencyToOrder.every(isExplicitBlankGridLine)), "command agency mark is followed by two exact 28-point grid lines");
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
|
||||
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
|
||||
@@ -20,11 +20,12 @@ function decodeHtml(value) {
|
||||
|
||||
function words(pdf) {
|
||||
const html = execFileSync("pdftotext", ["-f", "1", "-l", "1", "-bbox-layout", pdf, "-"], { encoding: "utf8" });
|
||||
return [...html.matchAll(/<word\s+[^>]*xMin="([0-9.]+)"[^>]*yMin="([0-9.]+)"[^>]*xMax="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({
|
||||
return [...html.matchAll(/<word\s+[^>]*xMin="([0-9.]+)"[^>]*yMin="([0-9.]+)"[^>]*xMax="([0-9.]+)"[^>]*yMax="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({
|
||||
xMinPt: Number(match[1]),
|
||||
yMinPt: Number(match[2]),
|
||||
xMaxPt: Number(match[3]),
|
||||
text: decodeHtml(match[4]),
|
||||
yMaxPt: Number(match[4]),
|
||||
text: decodeHtml(match[5]),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -34,9 +35,12 @@ function findText(pdfWords, expected) {
|
||||
// measurement is independent of that tokenization choice.
|
||||
const rows = [];
|
||||
for (const word of [...pdfWords].sort((left, right) => left.yMinPt - right.yMinPt || left.xMinPt - right.xMinPt)) {
|
||||
const row = rows.find(({ yMinPt }) => Math.abs(yMinPt - word.yMinPt) <= 1.5);
|
||||
if (row) row.words.push(word);
|
||||
else rows.push({ yMinPt: word.yMinPt, words: [word] });
|
||||
const row = rows.find(({ yMinPt, yMaxPt }) => word.yMinPt <= yMaxPt + 1.5 && word.yMaxPt >= yMinPt - 1.5);
|
||||
if (row) {
|
||||
row.words.push(word);
|
||||
row.yMinPt = Math.min(row.yMinPt, word.yMinPt);
|
||||
row.yMaxPt = Math.max(row.yMaxPt, word.yMaxPt);
|
||||
} else rows.push({ yMinPt: word.yMinPt, yMaxPt: word.yMaxPt, words: [word] });
|
||||
}
|
||||
for (const row of rows) {
|
||||
const ordered = row.words.sort((left, right) => left.xMinPt - right.xMinPt);
|
||||
|
||||
@@ -12,6 +12,7 @@ FIXTURE="$SKILL_DIR/tests/fixture.md"
|
||||
|
||||
"$NODE" --check "$GEN"
|
||||
"$NODE" --check "$VERIFY"
|
||||
"$NODE" --check "$SKILL_DIR/tests/measure-header-coordinates.mjs"
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/layout.docx" --format ordinary --org "不应变红头的单位" --title "公文格式回归测试" --page-number center
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/layout.docx" --profile ordinary
|
||||
@@ -25,6 +26,41 @@ unzip -p "$TMP_DIR/layout.docx" word/document.xml | grep -q 'w:pStyle w:val="Hea
|
||||
! unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q '示例单位文件'
|
||||
unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q '示例发〔2026〕1号'
|
||||
unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q 'w:snapToGrid w:val="false"'
|
||||
python3 - "$TMP_DIR/standard.docx" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
with zipfile.ZipFile(sys.argv[1]) as archive:
|
||||
xml = archive.read("word/document.xml").decode()
|
||||
paragraphs = re.findall(r'<w:p>[\s\S]*?</w:p>', xml)
|
||||
doc_no_index = next(i for i, paragraph in enumerate(paragraphs) if '示例发〔2026〕1号' in paragraph)
|
||||
gap = paragraphs[doc_no_index - 2:doc_no_index]
|
||||
assert len(gap) == 2
|
||||
assert all('w:line="560"' in paragraph and 'w:lineRule="exact"' in paragraph and 'w:snapToGrid w:val="false"' in paragraph for paragraph in gap)
|
||||
PY
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/preprinted-minimum.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 37 --doc-no "示例发〔2026〕7号" --title "最小预印留白测试"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/preprinted-minimum.docx" --profile formal --letterhead preprinted
|
||||
python3 - "$TMP_DIR/preprinted-minimum.docx" "$TMP_DIR/preprinted-malformed.docx" <<'PY'
|
||||
import sys, zipfile
|
||||
source, target = sys.argv[1:]
|
||||
with zipfile.ZipFile(source) as src, zipfile.ZipFile(target, 'w', zipfile.ZIP_DEFLATED) as dst:
|
||||
for item in src.infolist():
|
||||
data = src.read(item.filename)
|
||||
if item.filename == 'word/document.xml':
|
||||
text = data.decode()
|
||||
old = '<w:spacing w:before="0" w:after="0" w:line="1" w:lineRule="exact"/>'
|
||||
assert text.count(old) == 1
|
||||
text = text.replace(old, '<w:spacing w:before="0" w:after="0" w:line="560" w:lineRule="exact"/>', 1)
|
||||
data = text.encode()
|
||||
dst.writestr(item, data)
|
||||
PY
|
||||
if "$NODE" "$VERIFY" --input "$TMP_DIR/preprinted-malformed.docx" --profile formal >"$TMP_DIR/preprinted-malformed.out" 2>&1; then
|
||||
echo "malformed preprinted reserve unexpectedly passed" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'Preprinted letterhead reserve' "$TMP_DIR/preprinted-malformed.out"
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/digital.docx" --format formal --letterhead digital --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "电子红头测试" --page-number standard
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/digital.docx" --profile formal --letterhead digital
|
||||
@@ -74,12 +110,24 @@ unzip -p "$TMP_DIR/letter.docx" word/footerLetter.xml | grep -q 'w:fill="FF0000"
|
||||
unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q '附件:1. 测试附件'
|
||||
unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q 'w:pageBreakBefore'
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/detached-attachment.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕1号" --title "分离附件测试" --attachment-note "1. 测试附件" --attachment-file "$TMP_DIR/attachment.md" --attachment-detached
|
||||
unzip -p "$TMP_DIR/detached-attachment.docx" word/document.xml | grep -q '示例发〔2026〕1号 附件1'
|
||||
|
||||
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-attachment.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕6号" --title "附件一致性测试" --attachment-note "1. 另一份附件" --attachment-file "$TMP_DIR/attachment.md" >"$TMP_DIR/bad-attachment.out" 2>&1; then
|
||||
echo "mismatched attachment note unexpectedly succeeded" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-attachment.out"
|
||||
|
||||
for bad_option in format letterhead page-number; do
|
||||
output="$TMP_DIR/invalid-$bad_option.out"
|
||||
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/invalid-$bad_option.docx" --format ordinary --"$bad_option" "invalid-value" >"$output" 2>&1; then
|
||||
echo "invalid --$bad_option unexpectedly succeeded" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q -- "--$bad_option" "$output"
|
||||
done
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令正文"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/command.docx" --profile command
|
||||
|
||||
@@ -91,9 +139,40 @@ grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-command.out"
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/minutes.docx" --format minutes --org "示例机关纪要" --title "纪要正文" --attendees "张三、李四" --absent "王五"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/minutes.docx" --profile minutes
|
||||
minutes_xml=$(unzip -p "$TMP_DIR/minutes.docx" word/document.xml)
|
||||
test "${minutes_xml%%出席:*}" != "$minutes_xml"
|
||||
test "${minutes_xml%%w:sectPr*}" != "$minutes_xml"
|
||||
test "${minutes_xml%%出席:*}" != "${minutes_xml%%w:sectPr*}"
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/seal-layout.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "签章位置测试" --sender "示例机关" --date "2026年9月10日" --seal-mode seal
|
||||
unzip -p "$TMP_DIR/seal-layout.docx" word/document.xml | grep -q 'w:right="1280"'
|
||||
python3 - "$TMP_DIR/seal-layout.docx" <<'PY'
|
||||
import sys, zipfile
|
||||
from xml.etree import ElementTree as ET
|
||||
ns={'w':'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
|
||||
with zipfile.ZipFile(sys.argv[1]) as z:
|
||||
root=ET.fromstring(z.read('word/document.xml'))
|
||||
for p in root.findall('.//w:p', ns):
|
||||
if ''.join(p.itertext()).strip() == '2026年9月10日':
|
||||
assert p.find('./w:pPr/w:ind', ns).attrib['{http://schemas.openxmlformats.org/wordprocessingml/2006/main}right'] == '1280'
|
||||
break
|
||||
else:
|
||||
raise SystemExit('seal date paragraph with four-character right indent not found')
|
||||
PY
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/signed-layout.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "签名章位置测试" --date "2026年9月10日" --seal-mode signed --signer-title "主要负责人" --signer "张三"
|
||||
python3 - "$TMP_DIR/signed-layout.docx" <<'PY'
|
||||
import sys, zipfile
|
||||
from xml.etree import ElementTree as ET
|
||||
ns={'w':'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
|
||||
with zipfile.ZipFile(sys.argv[1]) as z:
|
||||
root=ET.fromstring(z.read('word/document.xml'))
|
||||
for p in root.findall('.//w:p', ns):
|
||||
if ''.join(p.itertext()).strip() == '2026年9月10日':
|
||||
assert p.find('./w:pPr/w:ind', ns).attrib['{http://schemas.openxmlformats.org/wordprocessingml/2006/main}right'] == '1280'
|
||||
break
|
||||
else:
|
||||
raise SystemExit('signed date paragraph with four-character right indent not found')
|
||||
PY
|
||||
|
||||
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-doc-no.docx" --format formal --letterhead digital --org "示例单位文件" --doc-no "示例发〔2026〕01号" --title "非法文号" >"$TMP_DIR/bad-doc-no.out" 2>&1; then
|
||||
echo "invalid document number unexpectedly succeeded" >&2
|
||||
@@ -112,6 +191,58 @@ grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-date.out"
|
||||
unzip -p "$TMP_DIR/letter-fields.docx" word/document.xml | grep -q '000007'
|
||||
unzip -p "$TMP_DIR/letter-fields.docx" word/document.xml | grep -q '机密★3年'
|
||||
|
||||
"$NODE" "$GEN" --input "$SKILL_DIR/tests/fixtures/visual-horizontal.md" --output "$TMP_DIR/horizontal-table.docx" --format horizontal-table --title "横排表格测试" --page-number standard
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/horizontal-table.docx" --profile horizontal-table
|
||||
unzip -p "$TMP_DIR/horizontal-table.docx" word/document.xml | grep -q 'w:orient="landscape"'
|
||||
unzip -p "$TMP_DIR/horizontal-table.docx" word/document.xml | grep -q 'w:tblW w:w="12756"'
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/joint.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org "协办机关" --doc-no "示例发〔2026〕3号" --title "联合行文测试"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/joint.docx" --profile formal --letterhead digital
|
||||
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '主办机关'
|
||||
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '协办机关'
|
||||
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '文件'
|
||||
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q 'w:sz w:val="48"'
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/joint-four-agencies.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org "协办机关一" --joint-org "协办机关二" --joint-org "协办机关三" --doc-no "示例发〔2026〕4号" --title "四机关联合行文测试"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/joint-four-agencies.docx" --profile formal --letterhead digital
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/joint-without-file-suffix.docx" --format formal --letterhead digital --org "主办机关" --joint-org "协办机关" --doc-no "示例发〔2026〕5号" --title "无文件后缀联合行文测试"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/joint-without-file-suffix.docx" --profile formal --letterhead digital
|
||||
python3 - "$TMP_DIR/joint.docx" "$TMP_DIR/joint-four-agencies.docx" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
for filename, expected_count in ((sys.argv[1], 2), (sys.argv[2], 4)):
|
||||
with zipfile.ZipFile(filename) as archive:
|
||||
xml = archive.read("word/document.xml").decode()
|
||||
tables = re.findall(r'<w:tbl>[\s\S]*?<w:tblpPr[^>]*w:tblpY="1984"[\s\S]*?</w:tbl>', xml)
|
||||
assert len(tables) == 1, filename
|
||||
table = tables[0]
|
||||
assert table.count('<w:vAlign w:val="center"/>') >= 2, filename
|
||||
assert table.count('<w:tc>') == 2, filename
|
||||
assert table.count('<w:p>') == expected_count + 1, filename
|
||||
assert '<w:t xml:space="preserve">文件</w:t>' in table, filename
|
||||
PY
|
||||
python3 - "$TMP_DIR/joint-without-file-suffix.docx" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
with zipfile.ZipFile(sys.argv[1]) as archive:
|
||||
xml = archive.read("word/document.xml").decode()
|
||||
tables = re.findall(r'<w:tbl>[\s\S]*?<w:tblpPr[^>]*w:tblpY="1984"[\s\S]*?</w:tbl>', xml)
|
||||
assert len(tables) == 1
|
||||
table = tables[0]
|
||||
assert table.count('<w:vAlign w:val="center"/>') >= 1
|
||||
assert '<w:t xml:space="preserve">文件</w:t>' not in table
|
||||
PY
|
||||
|
||||
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/missing-joint-name.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org --doc-no "示例发〔2026〕4号" --title "缺少联署机关名称" >"$TMP_DIR/missing-joint-name.out" 2>&1; then
|
||||
echo "missing joint agency name unexpectedly succeeded" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'GENERATOR ERROR: --joint-org requires a value' "$TMP_DIR/missing-joint-name.out"
|
||||
|
||||
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/strict.docx" --format formal --letterhead digital --org "示例单位文件" --title "严格字体测试" --require-standard-fonts >"$TMP_DIR/strict.out" 2>&1; then
|
||||
if ! fc-list -f '%{family}\n' | grep -Eq '方正小标宋简体|方正小标宋_GBK|FZXiaoBiaoSong'; then
|
||||
echo "strict font mode unexpectedly succeeded without a small-standard-title font" >&2
|
||||
|
||||
@@ -15,6 +15,7 @@ long="$skill_dir/tests/fixtures/visual-long.md"
|
||||
attach_a="$skill_dir/tests/fixtures/visual-attachment-a.md"
|
||||
attach_b="$skill_dir/tests/fixtures/visual-attachment-b.md"
|
||||
horizontal="$skill_dir/tests/fixtures/visual-horizontal.md"
|
||||
calibration="$skill_dir/tests/fixtures/visual-calibration.md"
|
||||
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/ordinary.docx" --format ordinary --title "普通材料全要素实测" --page-number center
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/formal-digital-all.docx" --format formal --letterhead digital --copy-no 7 --secret "机密★3年" --urgent "特急" --org "示例单位文件" --doc-no "示例发〔2026〕7号" --title "正式公文全要素实测" --to "各主送机关" --sender "示例单位" --date "2026年9月10日" --note "此件公开" --attachment-note "1. 数据清单;2. 实施计划。" --cc "办公室、财务部" --print-org "示例印发机关" --print-date "2026年9月10日" --page-number standard
|
||||
@@ -22,25 +23,31 @@ node "$gen" --input "$all" --output "$out_dir/docx/formal-digital-upward.docx" -
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/formal-preprinted.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例发〔2026〕9号" --title "预印红头套打实测" --page-number standard
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/formal-seal.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "盖章公文实测" --sender "示例单位" --date "2026年9月10日" --seal-mode seal --page-number standard
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/formal-signed.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "签名章公文实测" --sender "示例单位" --date "2026年9月10日" --seal-mode signed --signer-title "主要负责人" --signer "张三" --page-number standard
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/formal-unsigned.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "不盖章公文实测" --sender "示例单位" --date "2026年9月10日" --page-number standard
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/formal-attachments.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕10号" --title "多附件实测" --attachment-note "1. 数据清单;2. 实施计划。" --attachment-file "$attach_a" --attachment-file "$attach_b" --page-number standard
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/formal-attachments-detached.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕15号" --title "分离装订附件实测" --attachment-note "1. 数据清单;2. 实施计划。" --attachment-file "$attach_a" --attachment-file "$attach_b" --attachment-detached --page-number standard
|
||||
node "$gen" --input "$long" --output "$out_dir/docx/formal-long.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕11号" --title "长文版记与单双页实测" --cc "办公室、财务部、审计部、采购部、法务部、信息中心" --print-org "示例印发机关" --print-date "2026年9月10日" --page-number standard
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/formal-joint.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org "协办机关一" --joint-org "协办机关二" --joint-org "协办机关三" --doc-no "示例发〔2026〕16号" --title "联合行文实测" --page-number standard
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/letter.docx" --format letter --org "示例机关" --doc-no "示例〔2026〕12号" --title "信函格式实测" --page-number none
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/letter-fields.docx" --format letter --org "示例机关" --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例〔2026〕14号" --title "信函版头字段实测" --page-number none
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令格式实测" --page-number standard
|
||||
node "$gen" --input "$all" --output "$out_dir/docx/minutes.docx" --format minutes --org "示例机关纪要" --title "纪要格式实测" --attendees "张三(办公室)、李四(财务部)" --absent "王五(审计部)" --observers "赵六(采购部)" --page-number standard
|
||||
node "$gen" --input "$horizontal" --output "$out_dir/docx/table.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕13号" --title "表格版式实测" --page-number standard
|
||||
node "$gen" --input "$horizontal" --output "$out_dir/docx/horizontal-table.docx" --format horizontal-table --title "横排表格实测" --page-number standard
|
||||
node "$gen" --input "$calibration" --output "$out_dir/docx/ordinary-calibration.docx" --format ordinary --page-number center
|
||||
|
||||
for file in "$out_dir/docx"/*.docx; do
|
||||
base=$(basename "$file" .docx)
|
||||
case "$base" in
|
||||
ordinary) profile=ordinary ;;
|
||||
ordinary|ordinary-calibration) profile=ordinary ;;
|
||||
letter|letter-fields) profile=letter ;;
|
||||
horizontal-table) profile=horizontal-table ;;
|
||||
command) profile=command ;;
|
||||
minutes) profile=minutes ;;
|
||||
*) profile=formal ;;
|
||||
esac
|
||||
case "$base" in
|
||||
formal-digital-*) letterhead=digital ;;
|
||||
formal-digital-*|formal-joint) letterhead=digital ;;
|
||||
*) letterhead=preprinted ;;
|
||||
esac
|
||||
node "$verify" --input "$file" --profile "$profile" --letterhead "$letterhead" >"$out_dir/logs/$base.verify.log"
|
||||
|
||||