修正联合行文布局并刷新审计证据

This commit is contained in:
mizzlelover
2026-09-11 08:14:31 +08:00
parent 9f3598d60d
commit 4bf5d00346
34 changed files with 77 additions and 20 deletions
BIN
View File
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 158 KiB

@@ -1,19 +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 digital 4 references/assets/visual-audit/all-rules/formal-attachments-detached-3.png
formal-attachments digital 4 references/assets/visual-audit/all-rules/formal-attachments-3.png, references/assets/visual-audit/all-rules/formal-attachments-4.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 digital 3 references/assets/visual-audit/all-rules/formal-long-2.png, references/assets/visual-audit/all-rules/formal-long-3.png
formal-preprinted digital 2 references/assets/visual-audit/all-rules/formal-preprinted-1.png, references/assets/visual-audit/all-rules/formal-preprinted-2.png
formal-seal digital 2 references/assets/visual-audit/all-rules/formal-seal-2.png
formal-signed digital 2 references/assets/visual-audit/all-rules/formal-signed-2.png
formal-unsigned digital 2 references/assets/visual-audit/all-rules/formal-unsigned-1.png, references/assets/visual-audit/all-rules/formal-unsigned-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 digital 2 references/assets/visual-audit/all-rules/minutes-1.png, references/assets/visual-audit/all-rules/minutes-2.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 ordinary 1 references/assets/visual-audit/all-rules/table-1.png
table formal 1 references/assets/visual-audit/all-rules/table-1.png
1 scenario profile pdf_pages committed_screenshot_examples
2 command command 2 references/assets/visual-audit/all-rules/command-1.png, references/assets/visual-audit/all-rules/command-2.png
3 formal-attachments-detached digital preprinted 4 references/assets/visual-audit/all-rules/formal-attachments-detached-3.png
4 formal-attachments digital preprinted 4 references/assets/visual-audit/all-rules/formal-attachments-3.png, references/assets/visual-audit/all-rules/formal-attachments-4.png
5 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
6 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
7 formal-joint digital 2 references/assets/visual-audit/all-rules/formal-joint-1.png, references/assets/visual-audit/all-rules/formal-joint-2.png
8 formal-long digital preprinted 3 references/assets/visual-audit/all-rules/formal-long-2.png, references/assets/visual-audit/all-rules/formal-long-3.png
9 formal-preprinted digital preprinted 2 references/assets/visual-audit/all-rules/formal-preprinted-1.png, references/assets/visual-audit/all-rules/formal-preprinted-2.png
10 formal-seal digital preprinted 2 references/assets/visual-audit/all-rules/formal-seal-2.png
11 formal-signed digital preprinted 2 references/assets/visual-audit/all-rules/formal-signed-2.png
12 formal-unsigned digital preprinted 2 references/assets/visual-audit/all-rules/formal-unsigned-1.png, references/assets/visual-audit/all-rules/formal-unsigned-2.png
13 horizontal-table horizontal-table 1 references/assets/visual-audit/all-rules/horizontal-table-1.png
14 letter-fields letter 2 references/assets/visual-audit/all-rules/letter-fields-1.png
15 letter letter 1 references/assets/visual-audit/all-rules/letter-1.png
16 minutes digital minutes 2 references/assets/visual-audit/all-rules/minutes-1.png, references/assets/visual-audit/all-rules/minutes-2.png
17 ordinary-calibration ordinary 2 references/assets/visual-audit/all-rules/ordinary-calibration-1.png
18 ordinary ordinary 1 references/assets/visual-audit/all-rules/ordinary-1.png
19 table ordinary formal 1 references/assets/visual-audit/all-rules/table-1.png
Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 47 KiB

+19 -6
View File
@@ -240,6 +240,22 @@ function floatingAgencyMarkParagraph(text, lineCount = 1, size = "56") {
});
}
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",
@@ -797,12 +813,9 @@ function buildDocument(blocks, args) {
const jointNames = jointOrganizations.length && host.endsWith("文件")
? [host.slice(0, -2), ...jointOrganizations]
: agencyNames;
const middleIndex = Math.floor((jointNames.length - 1) / 2);
const jointText = jointOrganizations.length && host.endsWith("文件")
? jointNames.map((name, index) => index === middleIndex ? `${name} 文件` : name).join("\n")
: agencyNames.join(" ");
const agencyMarkSize = agencyNames.length > 1 ? (jointText.length > 16 ? "44" : "48") : "56";
body.push(floatingAgencyMarkParagraph(jointText, jointOrganizations.length ? jointNames.length : 1, agencyMarkSize));
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));
}
+14 -2
View File
@@ -133,17 +133,29 @@ if (profile === "formal") {
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|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 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");
+34 -2
View File
@@ -130,10 +130,42 @@ unzip -p "$TMP_DIR/horizontal-table.docx" word/document.xml | grep -q 'w:tblW w:
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:t><w:br/>'
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '协办机关</w:t>'
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 "示例发〔20264号" --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 "示例发〔20265号" --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 "示例发〔20264号" --title "缺少联署机关名称" >"$TMP_DIR/missing-joint-name.out" 2>&1; then
echo "missing joint agency name unexpectedly succeeded" >&2
exit 1
+1 -1
View File
@@ -27,7 +27,7 @@ node "$gen" --input "$all" --output "$out_dir/docx/formal-unsigned.docx" --forma
node "$gen" --input "$all" --output "$out_dir/docx/formal-attachments.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔202610号" --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 "示例发〔202615号" --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 "示例发〔202611号" --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 "协办机关二" --doc-no "示例发〔202616号" --title "联合行文实测" --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 "示例发〔202616号" --title "联合行文实测" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/letter.docx" --format letter --org "示例机关" --doc-no "示例〔202612号" --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 "示例〔202614号" --title "信函版头字段实测" --page-number none
node "$gen" --input "$all" --output "$out_dir/docx/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令格式实测" --page-number standard