@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,405 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
党政机关公文排版格式生成器 — PDF 文档
|
|
4
|
+
严格遵循 GB/T 9704-2012 标准
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import glob
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from reportlab.pdfbase import pdfmetrics
|
|
12
|
+
from reportlab.pdfbase.ttfonts import TTFont
|
|
13
|
+
from reportlab.lib.pagesizes import A4
|
|
14
|
+
from reportlab.lib.units import cm
|
|
15
|
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, KeepTogether
|
|
16
|
+
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
17
|
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# 字号(磅)
|
|
21
|
+
FONT_SIZES = {
|
|
22
|
+
'二号': 22,
|
|
23
|
+
'三号': 16,
|
|
24
|
+
'四号': 14,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# 行距(磅)
|
|
28
|
+
LINE_SPACING = {
|
|
29
|
+
'title': 36,
|
|
30
|
+
'body': 28,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# 页边距(cm)
|
|
34
|
+
MARGINS = {
|
|
35
|
+
'top': 3.7,
|
|
36
|
+
'bottom': 3.5,
|
|
37
|
+
'left': 2.8,
|
|
38
|
+
'right': 2.6,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 三号字(16pt)字符宽度(磅),用于缩进换算
|
|
42
|
+
CHAR_PT = 16
|
|
43
|
+
|
|
44
|
+
# 各排版元素使用的字体(注册成功后按可用性调整)
|
|
45
|
+
FONTS = {
|
|
46
|
+
'title': 'FangBold', # 标题:仿宋加粗(二号)
|
|
47
|
+
'body': 'FangSC', # 正文:仿宋(四号)
|
|
48
|
+
'h1': 'FangBold', # 一级标题:仿宋加粗(三号)
|
|
49
|
+
'h2': 'FangBold', # 二级标题:仿宋加粗(三号)
|
|
50
|
+
'h3': 'FangBold', # 三级标题:仿宋加粗(三号)
|
|
51
|
+
'h4': 'FangBold', # 四级标题:仿宋加粗(三号)
|
|
52
|
+
'mi_ji': 'FangSC', # 版头:仿宋(与全文一致)
|
|
53
|
+
'page': 'FangSC', # 页码:仿宋
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _first_file(patterns):
|
|
58
|
+
for pat in patterns:
|
|
59
|
+
hits = sorted(glob.glob(pat, recursive=True))
|
|
60
|
+
if hits:
|
|
61
|
+
return hits[0]
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _font_search_dirs():
|
|
69
|
+
"""用户自备字体的查找目录(按优先级,跨平台,只返回真实存在的目录)。"""
|
|
70
|
+
home = os.path.expanduser('~')
|
|
71
|
+
local = os.environ.get('LOCALAPPDATA', '')
|
|
72
|
+
candidates = [
|
|
73
|
+
os.environ.get('DSH_CN_FONT_DIR', ''), # 1) 显式指定目录
|
|
74
|
+
os.path.join(_HERE, '..', 'fonts'), # 2) 用户放进插件自己的 fonts/
|
|
75
|
+
os.path.join(home, 'Library', 'Fonts'), # 3) macOS 用户字体
|
|
76
|
+
os.path.join(home, '.fonts'), # Linux 用户字体
|
|
77
|
+
os.path.join(home, '.local', 'share', 'fonts'), # Linux 用户字体
|
|
78
|
+
os.path.join(local, 'Microsoft', 'Windows', 'Fonts') if local else '',
|
|
79
|
+
'C:/Windows/Fonts', # Windows 系统字体
|
|
80
|
+
'/usr/share/fonts', # Linux 系统字体
|
|
81
|
+
'/usr/local/share/fonts',
|
|
82
|
+
]
|
|
83
|
+
return [d for d in candidates if d and os.path.isdir(d)]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# 仿宋在各平台的常见文件名(不同来源命名不一)
|
|
87
|
+
FANGSONG_NAMES = [
|
|
88
|
+
'FangS-SC.ttf', # WPS 自带(方正仿宋)
|
|
89
|
+
'simfang.ttf', 'SIMFANG.TTF', # Windows 仿宋
|
|
90
|
+
'FangSong.ttf', 'FangSong_GB2312.ttf',
|
|
91
|
+
'STFangsong.ttf', # macOS 华文仿宋
|
|
92
|
+
'FandolFang-Regular.otf', # TeX Live 开源仿宋
|
|
93
|
+
'仿宋.ttf', '仿宋_GB2312.ttf',
|
|
94
|
+
]
|
|
95
|
+
FANGSONG_BOLD_NAMES = [
|
|
96
|
+
'FangS-SC-Bold.ttf',
|
|
97
|
+
'simfangb.ttf', 'SIMFANGB.TTF',
|
|
98
|
+
'FangSong-Bold.ttf',
|
|
99
|
+
'FandolFang-Bold.otf',
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _find_font(names):
|
|
104
|
+
"""在用户字体目录里按文件名找字体,返回第一个命中的绝对路径。"""
|
|
105
|
+
dirs = _font_search_dirs()
|
|
106
|
+
for d in dirs:
|
|
107
|
+
for name in names:
|
|
108
|
+
hit = _first_file([os.path.join(d, name), os.path.join(d, '**', name)])
|
|
109
|
+
if hit:
|
|
110
|
+
return hit
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# WPS Office 自带方正仿宋(常规体),作为最后的外部兜底
|
|
115
|
+
_WPS_FANGSONG = [
|
|
116
|
+
'/Applications/wpsoffice.app/Contents/Resources/office6/fonts/FangS-SC.ttf',
|
|
117
|
+
'/Applications/WPS Office.app/Contents/Resources/office6/fonts/FangS-SC.ttf',
|
|
118
|
+
'/Applications/WPS*.app/Contents/Resources/office6/fonts/FangS-SC.ttf',
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def register_fonts():
|
|
123
|
+
"""注册字体(TTC 用 subfontIndex 指定字面)。
|
|
124
|
+
|
|
125
|
+
⚠ 仿宋字体**不随本插件分发**。方正仿宋的授权允许在文档中使用,但不允许再分发
|
|
126
|
+
字体文件本身,因此改为查找**用户自备**的字体,顺序如下:
|
|
127
|
+
|
|
128
|
+
1. 环境变量 DSH_CN_FANGSONG / DSH_CN_FANGSONG_BOLD 直接指定文件(最高优先)
|
|
129
|
+
2. 环境变量 DSH_CN_FONT_DIR 指定的目录
|
|
130
|
+
3. 插件自己 fonts/ 目录(用户自行放入)
|
|
131
|
+
4. 系统字体目录(macOS / Windows / Linux)
|
|
132
|
+
5. WPS Office 安装目录(自带方正仿宋)
|
|
133
|
+
|
|
134
|
+
全部找不到时退回系统宋体,并在 stderr 给出可操作的获取指引。
|
|
135
|
+
"""
|
|
136
|
+
songti = _first_file([
|
|
137
|
+
'/System/Library/Fonts/Supplemental/Songti.ttc',
|
|
138
|
+
'/System/Library/Fonts/STHeiti Light.ttc',
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
# ① 显式指定优先
|
|
142
|
+
explicit = os.environ.get('DSH_CN_FANGSONG', '').strip()
|
|
143
|
+
fangsong = _first_file([explicit]) if explicit else None
|
|
144
|
+
# ②③④ 按目录与文件名查找
|
|
145
|
+
if not fangsong:
|
|
146
|
+
fangsong = _find_font(FANGSONG_NAMES)
|
|
147
|
+
# ⑤ WPS 兜底
|
|
148
|
+
if not fangsong:
|
|
149
|
+
fangsong = _first_file(_WPS_FANGSONG)
|
|
150
|
+
|
|
151
|
+
explicit_bold = os.environ.get('DSH_CN_FANGSONG_BOLD', '').strip()
|
|
152
|
+
bold_fang = _first_file([explicit_bold]) if explicit_bold else None
|
|
153
|
+
if not bold_fang:
|
|
154
|
+
bold_fang = _find_font(FANGSONG_BOLD_NAMES)
|
|
155
|
+
|
|
156
|
+
ok = {}
|
|
157
|
+
|
|
158
|
+
def reg(name, path, sub=None):
|
|
159
|
+
try:
|
|
160
|
+
pdfmetrics.registerFont(
|
|
161
|
+
TTFont(name, path, subfontIndex=sub) if sub is not None
|
|
162
|
+
else TTFont(name, path))
|
|
163
|
+
ok[name] = True
|
|
164
|
+
except Exception:
|
|
165
|
+
ok[name] = False
|
|
166
|
+
|
|
167
|
+
reg('SongSC', songti, 6) # 宋体-简 常规体(兜底)
|
|
168
|
+
reg('SongSCBold', songti, 1) # 宋体-简 粗体(兜底)
|
|
169
|
+
if fangsong:
|
|
170
|
+
reg('FangSC', fangsong) # 仿宋(用户自备,TrueType)
|
|
171
|
+
if bold_fang:
|
|
172
|
+
reg('FangBold', bold_fang) # 仿宋加粗(用户自备)
|
|
173
|
+
|
|
174
|
+
# 仿宋缺失时给出可操作的提示,避免静默退宋体导致排版不达标
|
|
175
|
+
if not ok.get('FangSC'):
|
|
176
|
+
import sys as _sys
|
|
177
|
+
print('[提示] 未找到仿宋字体,PDF 将退回宋体,与「全文仿宋」规范不符。\n'
|
|
178
|
+
' 本插件不随包分发字体(方正仿宋授权不允许再分发字体文件),\n'
|
|
179
|
+
' 请自备仿宋字体,任选一种方式:\n'
|
|
180
|
+
' 1) 环境变量直接指定:export DSH_CN_FANGSONG=/path/to/仿宋.ttf\n'
|
|
181
|
+
' 2) 指定查找目录: export DSH_CN_FONT_DIR=~/my-fonts\n'
|
|
182
|
+
' 3) 放进插件自己的: <插件目录>/fonts/\n'
|
|
183
|
+
' 4) 装到系统字体目录:macOS ~/Library/Fonts/ / '
|
|
184
|
+
'Windows C:\\Windows\\Fonts\\ / Linux ~/.fonts/\n'
|
|
185
|
+
' 5) 安装 WPS Office(自带方正仿宋)\n'
|
|
186
|
+
' 可用文件名举例:FangS-SC.ttf、simfang.ttf、STFangsong.ttf、\n'
|
|
187
|
+
' FandolFang-Regular.otf(TeX Live 开源仿宋)',
|
|
188
|
+
file=_sys.stderr)
|
|
189
|
+
|
|
190
|
+
# 字体缺失时回退
|
|
191
|
+
if not ok.get('FangSC'):
|
|
192
|
+
for key in ('body', 'mi_ji', 'page'):
|
|
193
|
+
FONTS[key] = 'SongSC'
|
|
194
|
+
if not ok.get('FangBold'):
|
|
195
|
+
if ok.get('FangSC'):
|
|
196
|
+
# 有仿宋但没有仿宋粗体(用户自备字体时的常态):
|
|
197
|
+
# 标题继续用仿宋,保住「全文统一仿宋」这条硬规范。
|
|
198
|
+
# reportlab 不做合成粗体,这里宁可不粗也不换字体族。
|
|
199
|
+
for key in ('title', 'h1', 'h2', 'h3', 'h4'):
|
|
200
|
+
FONTS[key] = 'FangSC'
|
|
201
|
+
import sys as _sys
|
|
202
|
+
print('[提示] 未提供仿宋加粗字体,标题将使用常规仿宋(不加粗)。\n'
|
|
203
|
+
' 如需加粗,用 DSH_CN_FANGSONG_BOLD 指定加粗字体文件。',
|
|
204
|
+
file=_sys.stderr)
|
|
205
|
+
else:
|
|
206
|
+
# 仿宋整体缺失 → 标题退宋体粗体
|
|
207
|
+
for key in ('title', 'h1', 'h2', 'h3', 'h4'):
|
|
208
|
+
FONTS[key] = 'SongSCBold' if ok.get('SongSCBold') else 'SongSC'
|
|
209
|
+
if not ok.get('SongSC'):
|
|
210
|
+
raise RuntimeError('系统缺少宋体字体,无法生成 PDF')
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def make_style(font_name, font_size, alignment=TA_LEFT, leading=None,
|
|
214
|
+
first_line_indent=0, left_indent=0, right_indent=0):
|
|
215
|
+
"""创建段落样式(wordWrap='CJK' 保证中文逐字换行)"""
|
|
216
|
+
if leading is None:
|
|
217
|
+
leading = font_size * 1.75
|
|
218
|
+
return ParagraphStyle(
|
|
219
|
+
f'style_{font_name}_{font_size}_{alignment}_{first_line_indent}_{left_indent}_{right_indent}',
|
|
220
|
+
parent=getSampleStyleSheet()['Normal'],
|
|
221
|
+
fontName=font_name,
|
|
222
|
+
fontSize=font_size,
|
|
223
|
+
alignment=alignment,
|
|
224
|
+
firstLineIndent=first_line_indent,
|
|
225
|
+
leftIndent=left_indent,
|
|
226
|
+
rightIndent=right_indent,
|
|
227
|
+
leading=leading,
|
|
228
|
+
spaceBefore=0,
|
|
229
|
+
spaceAfter=0,
|
|
230
|
+
wordWrap='CJK',
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def markup_text(text):
|
|
235
|
+
"""转义 XML(全文统一仿宋,不再单独换西文字体)"""
|
|
236
|
+
return text.replace('&', '&').replace('<', '<').replace('>', '>')
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def text_width_chars(text):
|
|
240
|
+
"""估算文本宽度(三号字为单位:汉字/全角=1,半角数字字母=0.5)"""
|
|
241
|
+
return sum(0.5 if ord(ch) < 128 else 1.0 for ch in text)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def is_level1_title(line):
|
|
245
|
+
"""一级标题:一、二、三……"""
|
|
246
|
+
return bool(line and line[0] in '一二三四五六七八九十'
|
|
247
|
+
and len(line) > 1 and line[1] == '、')
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def is_level2_title(line):
|
|
251
|
+
"""二级标题:(一)(二)(三)……"""
|
|
252
|
+
return bool(line and line.startswith('(') and len(line) >= 4
|
|
253
|
+
and line[1] in '一二三四五六七八九十' and line[2] == ')')
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def is_level3_title(line):
|
|
257
|
+
"""三级标题:1. 2. 3. ……(半角或全角点)"""
|
|
258
|
+
return bool(line and line[:1].isdigit() and len(line) >= 2
|
|
259
|
+
and line[1] in '..')
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def is_level4_title(line):
|
|
263
|
+
"""四级标题:(1)(2)(3)……"""
|
|
264
|
+
return bool(line and line.startswith('(') and len(line) >= 4
|
|
265
|
+
and line[1].isdigit() and line[2] == ')')
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def on_page(canvas, doc):
|
|
269
|
+
"""每页页脚页码:- n -,宋体四号,单页码居右空一字,双页码居左空一字"""
|
|
270
|
+
canvas.saveState()
|
|
271
|
+
canvas.setFont(FONTS['page'], FONT_SIZES['四号'])
|
|
272
|
+
text = f'- {canvas.getPageNumber()} -'
|
|
273
|
+
y = doc.bottomMargin * 0.55
|
|
274
|
+
if canvas.getPageNumber() % 2 == 1:
|
|
275
|
+
x = doc.pagesize[0] - doc.rightMargin - CHAR_PT
|
|
276
|
+
canvas.drawRightString(x, y, text)
|
|
277
|
+
else:
|
|
278
|
+
x = doc.leftMargin + CHAR_PT
|
|
279
|
+
canvas.drawString(x, y, text)
|
|
280
|
+
canvas.restoreState()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def generate_pdf(args):
|
|
284
|
+
"""生成 PDF 文档"""
|
|
285
|
+
register_fonts()
|
|
286
|
+
|
|
287
|
+
output_path = args.output
|
|
288
|
+
if not os.path.exists(output_path):
|
|
289
|
+
os.makedirs(output_path)
|
|
290
|
+
filepath = os.path.join(output_path, args.filename or '公文.pdf')
|
|
291
|
+
|
|
292
|
+
doc = SimpleDocTemplate(
|
|
293
|
+
filepath,
|
|
294
|
+
pagesize=A4,
|
|
295
|
+
topMargin=MARGINS['top'] * cm,
|
|
296
|
+
bottomMargin=MARGINS['bottom'] * cm,
|
|
297
|
+
leftMargin=MARGINS['left'] * cm,
|
|
298
|
+
rightMargin=MARGINS['right'] * cm,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
story = []
|
|
302
|
+
body_size = FONT_SIZES['四号']
|
|
303
|
+
heading_size = FONT_SIZES['三号']
|
|
304
|
+
body_leading = LINE_SPACING['body']
|
|
305
|
+
body_indent = int(2 * body_size) # 正文首行缩进 2 字
|
|
306
|
+
heading_indent = int(2 * heading_size) # 小标题左空 2 字
|
|
307
|
+
right4 = int(4 * body_size) # 右空 4 字(按四号字宽)
|
|
308
|
+
|
|
309
|
+
# === 版头信息 ===
|
|
310
|
+
for text in (args.fen_hao, args.mi_ji, args.jin_ji_chengdu):
|
|
311
|
+
if text:
|
|
312
|
+
style = make_style(FONTS['mi_ji'], heading_size, TA_LEFT,
|
|
313
|
+
leading=body_leading)
|
|
314
|
+
story.append(Paragraph(markup_text(text), style))
|
|
315
|
+
|
|
316
|
+
if args.fa_wen_zi_hao:
|
|
317
|
+
if args.shang_xing_wen:
|
|
318
|
+
style = make_style(FONTS['body'], body_size, TA_LEFT,
|
|
319
|
+
leading=body_leading, left_indent=CHAR_PT)
|
|
320
|
+
else:
|
|
321
|
+
style = make_style(FONTS['body'], body_size, TA_CENTER,
|
|
322
|
+
leading=body_leading)
|
|
323
|
+
story.append(Paragraph(markup_text(args.fa_wen_zi_hao), style))
|
|
324
|
+
|
|
325
|
+
# === 标题 ===
|
|
326
|
+
if args.title:
|
|
327
|
+
title_style = make_style(FONTS['title'], FONT_SIZES['二号'], TA_CENTER,
|
|
328
|
+
leading=LINE_SPACING['title'])
|
|
329
|
+
story.append(Paragraph(markup_text(args.title), title_style))
|
|
330
|
+
|
|
331
|
+
# === 正文 ===
|
|
332
|
+
if args.body:
|
|
333
|
+
for line in args.body.strip().split('\n'):
|
|
334
|
+
line = line.strip()
|
|
335
|
+
if not line:
|
|
336
|
+
continue
|
|
337
|
+
if is_level1_title(line):
|
|
338
|
+
font, size = FONTS['h1'], heading_size
|
|
339
|
+
elif is_level2_title(line):
|
|
340
|
+
font, size = FONTS['h2'], heading_size
|
|
341
|
+
elif is_level3_title(line):
|
|
342
|
+
font, size = FONTS['h3'], heading_size
|
|
343
|
+
elif is_level4_title(line):
|
|
344
|
+
font, size = FONTS['h4'], heading_size
|
|
345
|
+
else:
|
|
346
|
+
font, size = FONTS['body'], body_size
|
|
347
|
+
line_indent = heading_indent if size == heading_size else body_indent
|
|
348
|
+
style = make_style(font, size, TA_JUSTIFY,
|
|
349
|
+
first_line_indent=line_indent,
|
|
350
|
+
leading=body_leading)
|
|
351
|
+
story.append(Paragraph(markup_text(line), style))
|
|
352
|
+
|
|
353
|
+
# === 附件说明 ===
|
|
354
|
+
if args.attachment:
|
|
355
|
+
story.append(Spacer(1, body_leading))
|
|
356
|
+
style = make_style(FONTS['body'], body_size, TA_JUSTIFY,
|
|
357
|
+
first_line_indent=body_indent, leading=body_leading)
|
|
358
|
+
story.append(Paragraph(markup_text(args.attachment), style))
|
|
359
|
+
|
|
360
|
+
# === 发文机关署名和日期 ===
|
|
361
|
+
if args.sender or args.date:
|
|
362
|
+
sign_block = [Spacer(1, body_leading * 3)]
|
|
363
|
+
if args.sender and args.date:
|
|
364
|
+
sender_indent = max(0, round((4 + (text_width_chars(args.date)
|
|
365
|
+
- text_width_chars(args.sender)) / 2)
|
|
366
|
+
* body_size))
|
|
367
|
+
else:
|
|
368
|
+
sender_indent = right4
|
|
369
|
+
if args.sender:
|
|
370
|
+
style = make_style(FONTS['body'], body_size, TA_RIGHT,
|
|
371
|
+
leading=body_leading, right_indent=sender_indent)
|
|
372
|
+
sign_block.append(Paragraph(markup_text(args.sender), style))
|
|
373
|
+
if args.date:
|
|
374
|
+
style = make_style(FONTS['body'], body_size, TA_RIGHT,
|
|
375
|
+
leading=body_leading, right_indent=right4)
|
|
376
|
+
sign_block.append(Paragraph(markup_text(args.date), style))
|
|
377
|
+
story.append(KeepTogether(sign_block))
|
|
378
|
+
|
|
379
|
+
doc.build(story, onFirstPage=on_page, onLaterPages=on_page)
|
|
380
|
+
print(f'PDF 文档已生成: {filepath}')
|
|
381
|
+
return filepath
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def main():
|
|
385
|
+
parser = argparse.ArgumentParser(description='党政机关公文排版格式生成器 (.pdf)')
|
|
386
|
+
parser.add_argument('--output', required=True, help='输出目录')
|
|
387
|
+
parser.add_argument('--filename', default='公文.pdf', help='文件名')
|
|
388
|
+
parser.add_argument('--title', default='', help='标题')
|
|
389
|
+
parser.add_argument('--body', default='', help='正文内容(多行用换行分隔)')
|
|
390
|
+
parser.add_argument('--sender', default='', help='发文机关署名')
|
|
391
|
+
parser.add_argument('--date', default='', help='成文日期')
|
|
392
|
+
parser.add_argument('--attachment', default='', help='附件说明')
|
|
393
|
+
parser.add_argument('--fen_hao', default='', help='份号')
|
|
394
|
+
parser.add_argument('--mi_ji', default='', help='密级')
|
|
395
|
+
parser.add_argument('--jin_ji_chengdu', default='', help='紧急程度')
|
|
396
|
+
parser.add_argument('--fa_wen_zi_hao', default='', help='发文字号')
|
|
397
|
+
parser.add_argument('--shang_xing_wen', action='store_true',
|
|
398
|
+
help='上行文(发文字号左空一字)')
|
|
399
|
+
|
|
400
|
+
args = parser.parse_args()
|
|
401
|
+
generate_pdf(args)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
if __name__ == '__main__':
|
|
405
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
党政机关公文排版格式生成器 — 纯文本文件 (.txt)
|
|
4
|
+
保留标题层级结构,无格式标记
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_txt(args):
|
|
13
|
+
"""生成纯文本文件"""
|
|
14
|
+
output_path = args.output
|
|
15
|
+
if not os.path.exists(output_path):
|
|
16
|
+
os.makedirs(output_path)
|
|
17
|
+
|
|
18
|
+
filename = args.filename or '公文.txt'
|
|
19
|
+
filepath = os.path.join(output_path, filename)
|
|
20
|
+
|
|
21
|
+
lines = []
|
|
22
|
+
|
|
23
|
+
# 版头信息
|
|
24
|
+
if args.fen_hao:
|
|
25
|
+
lines.append(args.fen_hao)
|
|
26
|
+
if args.mi_ji:
|
|
27
|
+
lines.append(args.mi_ji)
|
|
28
|
+
if args.jin_ji_chengdu:
|
|
29
|
+
lines.append(args.jin_ji_chengdu)
|
|
30
|
+
if args.fa_wen_zi_hao:
|
|
31
|
+
lines.append(args.fa_wen_zi_hao)
|
|
32
|
+
# 标题
|
|
33
|
+
if args.title:
|
|
34
|
+
lines.append('')
|
|
35
|
+
lines.append(args.title)
|
|
36
|
+
lines.append('')
|
|
37
|
+
|
|
38
|
+
# 正文
|
|
39
|
+
if args.body:
|
|
40
|
+
body_lines = args.body.strip().split('\n')
|
|
41
|
+
for line in body_lines:
|
|
42
|
+
line = line.strip()
|
|
43
|
+
if not line:
|
|
44
|
+
continue
|
|
45
|
+
# 检测标题级别(与 docx/pdf 脚本保持一致的严格规则)
|
|
46
|
+
if is_heading(line):
|
|
47
|
+
lines.append(line)
|
|
48
|
+
lines.append('')
|
|
49
|
+
else:
|
|
50
|
+
lines.append(line)
|
|
51
|
+
lines.append('')
|
|
52
|
+
|
|
53
|
+
# 附件说明
|
|
54
|
+
if args.attachment:
|
|
55
|
+
lines.append('')
|
|
56
|
+
lines.append(args.attachment)
|
|
57
|
+
lines.append('')
|
|
58
|
+
|
|
59
|
+
# 发文机关署名和日期
|
|
60
|
+
if args.sender or args.date:
|
|
61
|
+
lines.append('')
|
|
62
|
+
lines.append('')
|
|
63
|
+
if args.sender:
|
|
64
|
+
lines.append(args.sender)
|
|
65
|
+
if args.date:
|
|
66
|
+
lines.append(args.date)
|
|
67
|
+
|
|
68
|
+
# 写入文件
|
|
69
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
70
|
+
f.write('\n'.join(lines))
|
|
71
|
+
|
|
72
|
+
print(f'纯文本文件已生成: {filepath}')
|
|
73
|
+
return filepath
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_heading(line):
|
|
77
|
+
"""严格检测各类标题:一、/(一)/1. /(1)"""
|
|
78
|
+
if line and line[0] in '一二三四五六七八九十' and len(line) > 1 and line[1] == '、':
|
|
79
|
+
return True
|
|
80
|
+
if (line and line.startswith('(') and len(line) >= 4
|
|
81
|
+
and line[1] in '一二三四五六七八九十' and line[2] == ')'):
|
|
82
|
+
return True
|
|
83
|
+
if line and line[:1].isdigit() and len(line) >= 2 and line[1] in '..':
|
|
84
|
+
return True
|
|
85
|
+
if (line and line.startswith('(') and len(line) >= 4
|
|
86
|
+
and line[1].isdigit() and line[2] == ')'):
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
parser = argparse.ArgumentParser(description='党政机关公文排版格式生成器 (.txt)')
|
|
93
|
+
parser.add_argument('--output', required=True, help='输出目录')
|
|
94
|
+
parser.add_argument('--filename', default='公文.txt', help='文件名')
|
|
95
|
+
parser.add_argument('--title', default='', help='标题')
|
|
96
|
+
parser.add_argument('--body', default='', help='正文内容(多行用换行分隔)')
|
|
97
|
+
parser.add_argument('--sender', default='', help='发文机关署名')
|
|
98
|
+
parser.add_argument('--date', default='', help='成文日期')
|
|
99
|
+
parser.add_argument('--attachment', default='', help='附件说明')
|
|
100
|
+
parser.add_argument('--fen_hao', default='', help='份号')
|
|
101
|
+
parser.add_argument('--mi_ji', default='', help='密级')
|
|
102
|
+
parser.add_argument('--jin_ji_chengdu', default='', help='紧急程度')
|
|
103
|
+
parser.add_argument('--fa_wen_zi_hao', default='', help='发文字号')
|
|
104
|
+
parser.add_argument('--shang_xing_wen', action='store_true',
|
|
105
|
+
help='上行文(发文字号左空一字)')
|
|
106
|
+
|
|
107
|
+
args = parser.parse_args()
|
|
108
|
+
generate_txt(args)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
main()
|