严格化国标版头与全条款核验
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
|
||||
const [withFieldsPdf, withoutFieldsPdf, agencyName = "示例单位文件", copyNo = "000007"] = process.argv.slice(2);
|
||||
if (!withFieldsPdf || !withoutFieldsPdf || !fs.existsSync(withFieldsPdf) || !fs.existsSync(withoutFieldsPdf)) {
|
||||
console.error("Usage: measure-header-coordinates.mjs WITH_FIELDS.pdf WITHOUT_FIELDS.pdf [agency-name] [copy-number]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function decodeHtml(value) {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
||||
.replace(/&#(\d+);/g, (_, decimal) => String.fromCodePoint(Number(decimal)));
|
||||
}
|
||||
|
||||
function words(pdf) {
|
||||
const html = execFileSync("pdftotext", ["-f", "1", "-l", "1", "-bbox-layout", pdf, "-"], { encoding: "utf8" });
|
||||
return [...html.matchAll(/<word\s+[^>]*yMin="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({
|
||||
yMinPt: Number(match[1]),
|
||||
text: decodeHtml(match[2]),
|
||||
}));
|
||||
}
|
||||
|
||||
function findWord(pdfWords, expected) {
|
||||
const word = pdfWords.find(({ text }) => text.includes(expected));
|
||||
if (!word) throw new Error(`${expected} was not found on the first page`);
|
||||
return word;
|
||||
}
|
||||
|
||||
function mm(word) {
|
||||
return word.yMinPt * 25.4 / 72;
|
||||
}
|
||||
|
||||
const withFields = words(withFieldsPdf);
|
||||
const withoutFields = words(withoutFieldsPdf);
|
||||
const copy = findWord(withFields, copyNo);
|
||||
const agencyWithFields = findWord(withFields, agencyName);
|
||||
const agencyWithoutFields = findWord(withoutFields, agencyName);
|
||||
const copyMm = mm(copy);
|
||||
const agencyWithFieldsMm = mm(agencyWithFields);
|
||||
const agencyWithoutFieldsMm = mm(agencyWithoutFields);
|
||||
const agencyDeltaMm = Math.abs(agencyWithFieldsMm - agencyWithoutFieldsMm);
|
||||
|
||||
// PDF glyph tops sit slightly below or above the frame edge depending on the
|
||||
// installed font. These ranges still independently verify the paper positions:
|
||||
// first type-area line at 37 mm, agency mark 35 mm below it (72 mm page top).
|
||||
if (copyMm < 37 || copyMm > 45) throw new Error(`copy number glyph is at ${copyMm.toFixed(2)} mm; expected first type-area line near 37 mm`);
|
||||
if (agencyWithFieldsMm < 68 || agencyWithFieldsMm > 77) throw new Error(`agency mark glyph is at ${agencyWithFieldsMm.toFixed(2)} mm; expected 35 mm below the 37 mm type-area top`);
|
||||
if (agencyDeltaMm > 0.8) throw new Error(`optional left-corner fields moved the agency mark by ${agencyDeltaMm.toFixed(2)} mm`);
|
||||
|
||||
console.log(`Header coordinate measurement passed: copy=${copyMm.toFixed(2)}mm, agency=${agencyWithFieldsMm.toFixed(2)}mm, no-fields-agency=${agencyWithoutFieldsMm.toFixed(2)}mm, delta=${agencyDeltaMm.toFixed(2)}mm`);
|
||||
+25
-4
@@ -31,14 +31,18 @@ unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q 'w:snapToGrid w:va
|
||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q '示例单位文件'
|
||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'FF0000'
|
||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'height:0.5mm'
|
||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:framePr.*w:xAlign="center".*w:y="2126"'
|
||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:before="2015"'
|
||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:framePr.*w:hAnchor="margin".*w:vAnchor="margin".*w:xAlign="center".*w:y="1984"'
|
||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:before="0"'
|
||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:snapToGrid w:val="false"'
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/digital-fields.docx" --format formal --letterhead digital --copy-no 7 --secret "机密★3年" --urgent "特急" --org "示例单位文件" --doc-no "示例发〔2026〕3号" --title "版头字段定位测试"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/digital-fields.docx" --profile formal --letterhead digital
|
||||
unzip -p "$TMP_DIR/digital-fields.docx" word/document.xml | grep -q 'w:framePr.*w:xAlign="left".*w:y="275"'
|
||||
unzip -p "$TMP_DIR/digital-fields.docx" word/document.xml | grep -q 'w:before="2015"'
|
||||
unzip -p "$TMP_DIR/digital-fields.docx" word/document.xml | grep -q 'w:framePr.*w:hAnchor="margin".*w:vAnchor="margin".*w:xAlign="left".*w:y="0"'
|
||||
unzip -p "$TMP_DIR/digital-fields.docx" word/document.xml | grep -q 'w:framePr.*w:hAnchor="margin".*w:vAnchor="margin".*w:xAlign="left".*w:y="560"'
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/secret-only.docx" --format formal --letterhead digital --secret "机密★3年" --org "示例单位文件" --doc-no "示例发〔2026〕5号" --title "密级首行留空测试"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/secret-only.docx" --profile formal --letterhead digital
|
||||
unzip -p "$TMP_DIR/secret-only.docx" word/document.xml | grep -q 'w:framePr.*w:vAnchor="margin".*w:y="560"'
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/preprinted-fields.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例发〔2026〕4号" --title "预印字段定位测试"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/preprinted-fields.docx" --profile formal --letterhead preprinted
|
||||
@@ -70,9 +74,21 @@ 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'
|
||||
|
||||
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"
|
||||
|
||||
"$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
|
||||
|
||||
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-command.docx" --format command --org "示例机关" --doc-no "第1号" --title "命令正文" >"$TMP_DIR/bad-command.out" 2>&1; then
|
||||
echo "command without 命令/令 unexpectedly succeeded" >&2
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
|
||||
@@ -91,6 +107,11 @@ if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-date.docx" --format
|
||||
fi
|
||||
grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-date.out"
|
||||
|
||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/letter-fields.docx" --format letter --org "示例机关" --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例〔2026〕2号" --title "信函字段边界"
|
||||
"$NODE" "$VERIFY" --input "$TMP_DIR/letter-fields.docx" --profile letter
|
||||
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年'
|
||||
|
||||
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
|
||||
|
||||
@@ -22,5 +22,7 @@ for path in \
|
||||
done
|
||||
|
||||
unzip -t "$skill_dir/dist/traework-gongwen-skill.zip" >/dev/null
|
||||
unzip -l "$skill_dir/dist/traework-gongwen-skill.zip" | grep -q 'gongwen/SKILL.md'
|
||||
unzip -l "$skill_dir/dist/traework-gongwen-skill.zip" >"$test_home/traework-listing.txt"
|
||||
grep -q 'gongwen/SKILL.md' "$test_home/traework-listing.txt"
|
||||
grep -q 'gongwen/tests/visual-audit.sh' "$test_home/traework-listing.txt"
|
||||
printf '%s\n' 'cross-platform installation tests passed'
|
||||
|
||||
@@ -25,6 +25,7 @@ node "$gen" --input "$all" --output "$out_dir/docx/formal-signed.docx" --format
|
||||
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 "$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/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
|
||||
@@ -33,7 +34,7 @@ for file in "$out_dir/docx"/*.docx; do
|
||||
base=$(basename "$file" .docx)
|
||||
case "$base" in
|
||||
ordinary) profile=ordinary ;;
|
||||
letter) profile=letter ;;
|
||||
letter|letter-fields) profile=letter ;;
|
||||
command) profile=command ;;
|
||||
minutes) profile=minutes ;;
|
||||
*) profile=formal ;;
|
||||
@@ -54,6 +55,10 @@ for file in "$out_dir/pdf"/*.pdf; do
|
||||
base=$(basename "$file" .pdf)
|
||||
pdftoppm -png -r 150 "$file" "$out_dir/png/$base" >"$out_dir/logs/$base.pdftoppm.log" 2>&1
|
||||
done
|
||||
node "$skill_dir/tests/measure-header-coordinates.mjs" \
|
||||
"$out_dir/pdf/formal-digital-all.pdf" \
|
||||
"$out_dir/pdf/formal-digital-upward.pdf" \
|
||||
"示例单位文件" "000007" | tee "$out_dir/logs/header-coordinates.log"
|
||||
|
||||
{
|
||||
echo -e "profile\tdocx\tpdf\tscreenshot_prefix"
|
||||
|
||||
Reference in New Issue
Block a user