@deepwhale-cn/dsh-cn-doc-formatter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +155 -0
- package/cordis.patch.yml +10 -0
- package/fonts/PUT-FONTS-HERE.txt +27 -0
- package/lib/index.js +88 -0
- package/package.json +42 -0
- package/skill/chinese-doc-formatter.md +328 -0
- package/skill/scripts/docx_formatter.py +500 -0
- package/skill/scripts/md_adapter.py +199 -0
- package/skill/scripts/pdf_formatter.py +405 -0
- package/skill/scripts/txt_formatter.py +112 -0
- package/skill/scripts/verify_output.py +233 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
verify_output.py — 输出自检
|
|
5
|
+
|
|
6
|
+
生成 docx / pdf 后核对是否真的符合本 skill 的排版规范。
|
|
7
|
+
原先没有这一步,字体名写错、页边距没生效这类问题不会被发现。
|
|
8
|
+
|
|
9
|
+
用法
|
|
10
|
+
----
|
|
11
|
+
python verify_output.py --dir <输出目录> [--title <标题>]
|
|
12
|
+
|
|
13
|
+
退出码 0 = 全部通过,1 = 有不符合项。
|
|
14
|
+
"""
|
|
15
|
+
import argparse
|
|
16
|
+
import glob
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
# 规范值(与 SKILL.md 保持一致)
|
|
21
|
+
SPEC = {
|
|
22
|
+
'margin_top_cm': 3.7,
|
|
23
|
+
'margin_bottom_cm': 3.5,
|
|
24
|
+
'margin_left_cm': 2.8,
|
|
25
|
+
'margin_right_cm': 2.6,
|
|
26
|
+
'title_pt': 22, # 二号
|
|
27
|
+
'h1_pt': 16, # 三号
|
|
28
|
+
'body_pt': 14, # 四号
|
|
29
|
+
'title_leading_pt': (33, 36),
|
|
30
|
+
'body_leading_pt': 28,
|
|
31
|
+
'tol': 0.15, # 容差
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
PASS, FAIL, WARN = '✅', '❌', '⚠️ '
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_docx(path):
|
|
38
|
+
"""核对 docx 的页面设置与字号、行距、字体"""
|
|
39
|
+
issues, notes = [], []
|
|
40
|
+
try:
|
|
41
|
+
from docx import Document
|
|
42
|
+
from docx.oxml.ns import qn
|
|
43
|
+
except ImportError:
|
|
44
|
+
return ['未安装 python-docx,跳过 docx 检查'], []
|
|
45
|
+
|
|
46
|
+
d = Document(path)
|
|
47
|
+
s = d.sections[0]
|
|
48
|
+
|
|
49
|
+
# 页边距
|
|
50
|
+
for name, got, want in (
|
|
51
|
+
('上边距', s.top_margin.cm, SPEC['margin_top_cm']),
|
|
52
|
+
('下边距', s.bottom_margin.cm, SPEC['margin_bottom_cm']),
|
|
53
|
+
('左边距', s.left_margin.cm, SPEC['margin_left_cm']),
|
|
54
|
+
('右边距', s.right_margin.cm, SPEC['margin_right_cm']),
|
|
55
|
+
):
|
|
56
|
+
if abs(got - want) > SPEC['tol']:
|
|
57
|
+
issues.append(f'{name} {got:.2f}cm,应为 {want}cm')
|
|
58
|
+
else:
|
|
59
|
+
notes.append(f'{name} {got:.2f}cm ✓')
|
|
60
|
+
|
|
61
|
+
# 段落:字号 / 行距 / 字体
|
|
62
|
+
kinds = {}
|
|
63
|
+
fonts = set()
|
|
64
|
+
for p in d.paragraphs:
|
|
65
|
+
t = p.text.strip()
|
|
66
|
+
if not t or not p.runs:
|
|
67
|
+
continue
|
|
68
|
+
r = p.runs[0]
|
|
69
|
+
sz = r.font.size.pt if r.font.size else None
|
|
70
|
+
bold = bool(r.font.bold)
|
|
71
|
+
leading = None
|
|
72
|
+
pPr = p._element.find(qn('w:pPr'))
|
|
73
|
+
if pPr is not None:
|
|
74
|
+
sp = pPr.find(qn('w:spacing'))
|
|
75
|
+
if sp is not None and sp.get(qn('w:line')):
|
|
76
|
+
leading = int(sp.get(qn('w:line'))) / 20.0 # twip → pt
|
|
77
|
+
rPr = r._element.find(qn('w:rPr'))
|
|
78
|
+
if rPr is not None:
|
|
79
|
+
rf = rPr.find(qn('w:rFonts'))
|
|
80
|
+
if rf is not None:
|
|
81
|
+
ea = rf.get(qn('w:eastAsia'))
|
|
82
|
+
if ea:
|
|
83
|
+
fonts.add(ea)
|
|
84
|
+
kinds.setdefault((sz, bold), []).append((t[:22], leading))
|
|
85
|
+
|
|
86
|
+
# 标题:22pt 加粗
|
|
87
|
+
if not any(sz == SPEC['title_pt'] and b for (sz, b) in kinds):
|
|
88
|
+
issues.append(f'未找到 {SPEC["title_pt"]}pt 加粗的标题(二号)')
|
|
89
|
+
else:
|
|
90
|
+
notes.append(f'标题 {SPEC["title_pt"]}pt 加粗 ✓')
|
|
91
|
+
|
|
92
|
+
# 一级标题:16pt 加粗
|
|
93
|
+
if not any(sz == SPEC['h1_pt'] and b for (sz, b) in kinds):
|
|
94
|
+
issues.append(f'未找到 {SPEC["h1_pt"]}pt 加粗的一级标题(三号)')
|
|
95
|
+
else:
|
|
96
|
+
notes.append(f'一级标题 {SPEC["h1_pt"]}pt 加粗 ✓')
|
|
97
|
+
|
|
98
|
+
# 正文:14pt 不加粗,行距 28pt
|
|
99
|
+
body_ok = False
|
|
100
|
+
for (sz, b), items in kinds.items():
|
|
101
|
+
if sz == SPEC['body_pt'] and not b:
|
|
102
|
+
leads = [l for _t, l in items if l is not None]
|
|
103
|
+
if leads:
|
|
104
|
+
avg = sum(leads) / len(leads)
|
|
105
|
+
if abs(avg - SPEC['body_leading_pt']) > 1.5:
|
|
106
|
+
issues.append(f'正文行距 {avg:.1f}pt,应为 {SPEC["body_leading_pt"]}pt')
|
|
107
|
+
else:
|
|
108
|
+
notes.append(f'正文 {sz}pt / 行距 {avg:.0f}pt ✓')
|
|
109
|
+
body_ok = True
|
|
110
|
+
else:
|
|
111
|
+
issues.append('正文段落未设置固定行距')
|
|
112
|
+
break
|
|
113
|
+
if not body_ok and not any(i.startswith('正文') for i in issues):
|
|
114
|
+
issues.append(f'未找到 {SPEC["body_pt"]}pt 不加粗的正文段落(四号)')
|
|
115
|
+
|
|
116
|
+
# 字体一致性
|
|
117
|
+
if len(fonts) > 1:
|
|
118
|
+
issues.append(f'全文出现多种字体:{sorted(fonts)},规范要求统一仿宋')
|
|
119
|
+
elif fonts:
|
|
120
|
+
notes.append(f'全文统一字体:{sorted(fonts)[0]} ✓')
|
|
121
|
+
else:
|
|
122
|
+
warnings_only = True
|
|
123
|
+
|
|
124
|
+
# 字体是否本机真实存在(避免 Word 静默替换)
|
|
125
|
+
try:
|
|
126
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
127
|
+
from docx_formatter import FOUND_FONTS
|
|
128
|
+
for f in fonts:
|
|
129
|
+
if f and f not in FOUND_FONTS:
|
|
130
|
+
issues.append(f'字体「{f}」本机不存在,Word 会替换为其他字体')
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
return issues, notes
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def check_pdf(path):
|
|
138
|
+
"""核对 PDF 是否嵌入并使用仿宋"""
|
|
139
|
+
issues, notes = [], []
|
|
140
|
+
try:
|
|
141
|
+
from pypdf import PdfReader
|
|
142
|
+
except ImportError:
|
|
143
|
+
return ['未安装 pypdf,跳过 pdf 检查'], []
|
|
144
|
+
|
|
145
|
+
r = PdfReader(path)
|
|
146
|
+
notes.append(f'页数 {len(r.pages)}')
|
|
147
|
+
embedded = set()
|
|
148
|
+
for pg in r.pages:
|
|
149
|
+
try:
|
|
150
|
+
res = pg.get('/Resources')
|
|
151
|
+
fonts = res.get('/Font') if res else None
|
|
152
|
+
if fonts:
|
|
153
|
+
for _k, v in fonts.items():
|
|
154
|
+
o = v.get_object()
|
|
155
|
+
base = str(o.get('/BaseFont', ''))
|
|
156
|
+
if base:
|
|
157
|
+
embedded.add(base)
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
if embedded:
|
|
161
|
+
names = ', '.join(sorted(embedded)[:6])
|
|
162
|
+
notes.append(f'嵌入字体 {len(embedded)} 种:{names}')
|
|
163
|
+
if not any('Fang' in x or '仿宋' in x or 'Song' in x for x in embedded):
|
|
164
|
+
issues.append(f'PDF 未使用仿宋字体,实际:{sorted(embedded)}')
|
|
165
|
+
else:
|
|
166
|
+
issues.append('PDF 未检出嵌入字体')
|
|
167
|
+
|
|
168
|
+
# 尺寸应为 A4
|
|
169
|
+
try:
|
|
170
|
+
mb = r.pages[0].mediabox
|
|
171
|
+
w_cm, h_cm = float(mb.width) / 28.3465, float(mb.height) / 28.3465
|
|
172
|
+
if abs(w_cm - 21.0) > 0.3 or abs(h_cm - 29.7) > 0.3:
|
|
173
|
+
issues.append(f'页面尺寸 {w_cm:.1f}×{h_cm:.1f}cm,应为 A4 21.0×29.7cm')
|
|
174
|
+
else:
|
|
175
|
+
notes.append(f'页面 A4 {w_cm:.1f}×{h_cm:.1f}cm ✓')
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
return issues, notes
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def main():
|
|
183
|
+
ap = argparse.ArgumentParser(description='排版输出自检')
|
|
184
|
+
ap.add_argument('--dir', required=True, help='输出目录')
|
|
185
|
+
a = ap.parse_args()
|
|
186
|
+
|
|
187
|
+
d = a.dir
|
|
188
|
+
docx = glob.glob(os.path.join(d, '*.docx'))
|
|
189
|
+
pdf = glob.glob(os.path.join(d, '*.pdf'))
|
|
190
|
+
txt = glob.glob(os.path.join(d, '*.txt'))
|
|
191
|
+
|
|
192
|
+
all_issues = []
|
|
193
|
+
print('════ 排版输出自检 ════')
|
|
194
|
+
print(f' 目录:{d}')
|
|
195
|
+
print(f' docx {len(docx)} / pdf {len(pdf)} / txt {len(txt)}')
|
|
196
|
+
print()
|
|
197
|
+
|
|
198
|
+
for f in docx:
|
|
199
|
+
print(f' ── {os.path.basename(f)}')
|
|
200
|
+
iss, notes = check_docx(f)
|
|
201
|
+
for n in notes:
|
|
202
|
+
print(f' {PASS} {n}')
|
|
203
|
+
for i in iss:
|
|
204
|
+
print(f' {FAIL} {i}')
|
|
205
|
+
all_issues += iss
|
|
206
|
+
print()
|
|
207
|
+
|
|
208
|
+
for f in pdf:
|
|
209
|
+
print(f' ── {os.path.basename(f)}')
|
|
210
|
+
iss, notes = check_pdf(f)
|
|
211
|
+
for n in notes:
|
|
212
|
+
print(f' {PASS} {n}')
|
|
213
|
+
for i in iss:
|
|
214
|
+
print(f' {FAIL} {i}')
|
|
215
|
+
all_issues += iss
|
|
216
|
+
print()
|
|
217
|
+
|
|
218
|
+
if not docx and not pdf:
|
|
219
|
+
print(' ⚠️ 目录下没有 docx / pdf,无法自检')
|
|
220
|
+
return 1
|
|
221
|
+
|
|
222
|
+
print('════ 结果 ════')
|
|
223
|
+
if all_issues:
|
|
224
|
+
print(f' {FAIL} {len(all_issues)} 项不符合规范:')
|
|
225
|
+
for i in all_issues:
|
|
226
|
+
print(f' · {i}')
|
|
227
|
+
return 1
|
|
228
|
+
print(f' {PASS} 全部符合排版规范')
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == '__main__':
|
|
233
|
+
sys.exit(main())
|