pdfwright 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {pdfwright-0.3.0 → pdfwright-0.4.0}/PKG-INFO +1 -1
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/generate_all.py +14 -2
- {pdfwright-0.3.0 → pdfwright-0.4.0}/pyproject.toml +13 -3
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/__init__.py +1 -1
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/converters/__init__.py +2 -0
- pdfwright-0.4.0/src/pdfwright/converters/docx.py +123 -0
- pdfwright-0.4.0/src/pdfwright/converters/docx_block.py +500 -0
- pdfwright-0.4.0/src/pdfwright/converters/docx_common.py +198 -0
- pdfwright-0.4.0/src/pdfwright/converters/docx_helpers.py +533 -0
- pdfwright-0.4.0/src/pdfwright/converters/docx_run.py +604 -0
- pdfwright-0.4.0/src/pdfwright/converters/docx_state.py +97 -0
- pdfwright-0.4.0/src/pdfwright/converters/docx_table.py +329 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/converters/html.py +17 -6
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/__init__.py +4 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/registry.py +544 -215
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/scanner.py +60 -6
- pdfwright-0.4.0/src/pdfwright/fonts/semantic_families.py +436 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/__init__.py +4 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/constants.py +37 -23
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/document.py +124 -22
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/elements.py +88 -16
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/style.py +73 -2
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/base.py +2 -1
- pdfwright-0.4.0/src/pdfwright/render/codes.py +241 -0
- pdfwright-0.4.0/src/pdfwright/render/html/__init__.py +160 -0
- pdfwright-0.4.0/src/pdfwright/render/html_blocks.py +394 -0
- pdfwright-0.4.0/src/pdfwright/render/html_parts.py +469 -0
- pdfwright-0.4.0/src/pdfwright/render/html_state.py +58 -0
- pdfwright-0.4.0/src/pdfwright/render/html_style.py +452 -0
- pdfwright-0.4.0/src/pdfwright/render/html_table.py +427 -0
- pdfwright-0.4.0/src/pdfwright/render/kinsoku.py +299 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/pdf.py +210 -2157
- pdfwright-0.4.0/src/pdfwright/render/pdf_blocks.py +563 -0
- pdfwright-0.4.0/src/pdfwright/render/pdf_flowables.py +1382 -0
- pdfwright-0.4.0/src/pdfwright/render/pdf_media.py +492 -0
- pdfwright-0.4.0/src/pdfwright/render/pdf_state.py +84 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/table.py +11 -0
- pdfwright-0.4.0/src/pdfwright/render/units.py +32 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/utils.py +45 -2
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/validate/__init__.py +215 -3
- pdfwright-0.4.0/tests/conftest.py +66 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_cell_borders.py +66 -0
- pdfwright-0.4.0/tests/test_divider_geometry.py +89 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_docx_converter.py +320 -17
- pdfwright-0.4.0/tests/test_fonts.py +954 -0
- pdfwright-0.4.0/tests/test_kinsoku.py +143 -0
- pdfwright-0.4.0/tests/test_line_grid.py +136 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_models.py +221 -0
- pdfwright-0.4.0/tests/test_prewrap_indent.py +272 -0
- pdfwright-0.4.0/tests/test_render_codes.py +166 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_html.py +173 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_pdf.py +196 -0
- pdfwright-0.4.0/tests/test_table_vectors.py +77 -0
- pdfwright-0.4.0/tests/test_validate.py +701 -0
- pdfwright-0.3.0/src/pdfwright/converters/docx.py +0 -1992
- pdfwright-0.3.0/src/pdfwright/render/codes.py +0 -152
- pdfwright-0.3.0/src/pdfwright/render/html/__init__.py +0 -1598
- pdfwright-0.3.0/tests/test_fonts.py +0 -278
- pdfwright-0.3.0/tests/test_validate.py +0 -284
- {pdfwright-0.3.0 → pdfwright-0.4.0}/.gitignore +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/README.md +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/contract.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/cooperation_contract.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/daily_report.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/feature_tour.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/font_mixing.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/generate_demo.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/invoice.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/output.html +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/output.pdf +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/path_security.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/sample_document.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/service_contract.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/staff_form.json +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/batch.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/commands.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/converters/xlsx.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/errors.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/fallback.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/oblique.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/sfnt.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/system_fonts.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/borders.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/table_columns.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/__init__.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/fingerprint.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/font_scan.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/metadata.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/pdfa_verify.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/seal.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/signed_verify.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/py.typed +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/__init__.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/colors.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/layout.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/markup.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/merge.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/options.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/pdfa.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/security.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/__init__.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/context.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/engine.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/filters.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/format.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_colors.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_examples.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_fingerprint.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_font_bold.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_font_policy.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_fonts_oblique.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_html_converter.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_margins.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_merge.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_options_and_validation.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_markup.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_row.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_table.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_row_widths.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_security.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_shared_vectors.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_sign_local_stamp.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_signed_verify.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_span_expression.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_style_box.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_template.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_xlsx_converter.py +0 -0
- {pdfwright-0.3.0 → pdfwright-0.4.0}/uv.lock +0 -0
|
@@ -13,6 +13,7 @@ import sys
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
from pdfwright import DocumentConfig
|
|
16
|
+
from pdfwright.fonts.registry import has_embeddable_cjk_font
|
|
16
17
|
from pdfwright.render.html import render_html
|
|
17
18
|
from pdfwright.render.pdf import render_pdf
|
|
18
19
|
from pdfwright.validate import validate_document
|
|
@@ -26,6 +27,12 @@ def main() -> int:
|
|
|
26
27
|
if not check_only:
|
|
27
28
|
OUTPUT_DIR.mkdir(exist_ok=True)
|
|
28
29
|
|
|
30
|
+
# 渲染中文需要可嵌入的中文字体;缺字体时只跳过渲染,校验照跑 ——
|
|
31
|
+
# validate_document 不碰字体,整个脚本提前 return 会让这道关卡空转。
|
|
32
|
+
can_render = has_embeddable_cjk_font()
|
|
33
|
+
if not can_render:
|
|
34
|
+
print("⚠️ 本机没有可嵌入的中文字体(ReportLab 只支持 TrueType/glyf):只校验、不渲染")
|
|
35
|
+
|
|
29
36
|
files = sorted(EXAMPLES_DIR.glob("*.json"))
|
|
30
37
|
files = [f for f in files if f.parent == EXAMPLES_DIR]
|
|
31
38
|
if not files:
|
|
@@ -45,6 +52,10 @@ def main() -> int:
|
|
|
45
52
|
print(f" [{issue.path}] {issue.message}")
|
|
46
53
|
continue
|
|
47
54
|
|
|
55
|
+
if not can_render:
|
|
56
|
+
print(f"{path.stem:<22}{'✓':<8}{'—':<6}{'(无字体,未渲染)':<20}")
|
|
57
|
+
continue
|
|
58
|
+
|
|
48
59
|
document = DocumentConfig.from_dict(config)
|
|
49
60
|
data = config.get("data")
|
|
50
61
|
try:
|
|
@@ -67,7 +78,8 @@ def main() -> int:
|
|
|
67
78
|
if failures:
|
|
68
79
|
print(f"❌ {failures} 个示例未通过")
|
|
69
80
|
return 1
|
|
70
|
-
|
|
81
|
+
suffix = "" if can_render else "(无字体,仅校验)"
|
|
82
|
+
print(f"✅ {len(files)} 个示例全部通过{suffix}" + ("" if check_only else f";产物在 {OUTPUT_DIR}"))
|
|
71
83
|
return 0
|
|
72
84
|
|
|
73
85
|
|
|
@@ -78,4 +90,4 @@ def _page_count(pdf_bytes: bytes) -> int:
|
|
|
78
90
|
|
|
79
91
|
|
|
80
92
|
if __name__ == "__main__":
|
|
81
|
-
raise SystemExit(main())
|
|
93
|
+
raise SystemExit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pdfwright"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "配置驱动的 PDF 生成基础库:JSON 配置 → 模板求值(变量/循环/条件)→ 高保真 PDF。纯 Python 引擎(ReportLab + fontTools + Jinja2 + pikepdf),无外部软件依赖。"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -37,6 +37,10 @@ exclude = ["examples/output"]
|
|
|
37
37
|
addopts = "-ra --strict-markers"
|
|
38
38
|
testpaths = ["tests"]
|
|
39
39
|
pythonpath = ["src"]
|
|
40
|
+
markers = [
|
|
41
|
+
# 需要本机装**可嵌入中文字体**的用例(无字体的容器里自动跳过,见 tests/conftest.py)
|
|
42
|
+
"needs_cjk: 用例前提是本机存在可嵌入的中文字面(ReportLab 只支持 TrueType/glyf)",
|
|
43
|
+
]
|
|
40
44
|
|
|
41
45
|
[tool.ruff]
|
|
42
46
|
line-length = 100
|
|
@@ -90,10 +94,16 @@ venvPath = ".."
|
|
|
90
94
|
venv = ".venv"
|
|
91
95
|
# basedpyright 的 ``ignore`` 只接受「文件/目录 glob」,不支持「每模块忽略哪些规则」
|
|
92
96
|
# (旧写法 [{module, codes}] 会让 basedpyright 报配置错并以退出码 3 结束,等于从未生效)。
|
|
93
|
-
#
|
|
94
|
-
#
|
|
97
|
+
# 下列模块大量使用第三方库动态属性(python-docx / reportlab / fontTools 的属性在
|
|
98
|
+
# 类型存根里查不到),规则级豁免无法表达,故整体不纳入 basedpyright;仍由 mypy 检查。
|
|
99
|
+
# 逐个列「真实报错」的模块,不是「docx 目录全列」—— docx_helpers / docx_state 目前
|
|
100
|
+
# 干净,未列;新增模块请先单独跑一次 basedpyright 再决定要不要加进来。
|
|
95
101
|
ignore = [
|
|
96
102
|
"src/pdfwright/converters/docx.py",
|
|
103
|
+
"src/pdfwright/converters/docx_block.py",
|
|
104
|
+
"src/pdfwright/converters/docx_common.py",
|
|
105
|
+
"src/pdfwright/converters/docx_run.py",
|
|
106
|
+
"src/pdfwright/converters/docx_table.py",
|
|
97
107
|
"src/pdfwright/converters/xlsx.py",
|
|
98
108
|
"src/pdfwright/fonts/scanner.py",
|
|
99
109
|
]
|
|
@@ -26,7 +26,7 @@ from pdfwright.postprocess import verify_pdfa
|
|
|
26
26
|
from pdfwright.render.html import render_html
|
|
27
27
|
from pdfwright.render.pdf import PdfRenderer, render_pdf, render_pdf_to_file
|
|
28
28
|
|
|
29
|
-
__version__ = "0.
|
|
29
|
+
__version__ = "0.4.0"
|
|
30
30
|
|
|
31
31
|
__all__ = [
|
|
32
32
|
"BatchItem",
|
|
@@ -5,6 +5,7 @@ from pdfwright.converters.docx import (
|
|
|
5
5
|
DocxField,
|
|
6
6
|
DocxParseResult,
|
|
7
7
|
docx_to_schema,
|
|
8
|
+
semantic_family,
|
|
8
9
|
)
|
|
9
10
|
from pdfwright.converters.html import (
|
|
10
11
|
HtmlField,
|
|
@@ -23,6 +24,7 @@ __all__ = [
|
|
|
23
24
|
"XlsxConverter",
|
|
24
25
|
"docx_to_schema",
|
|
25
26
|
"html_to_schema",
|
|
27
|
+
"semantic_family",
|
|
26
28
|
"strip_html_tags",
|
|
27
29
|
"xlsx_to_schema",
|
|
28
30
|
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""DOCX 转换器:python-docx 解析 Word 文档 → pdfwright schema(高保真 + 模板标签 + 自动字段 key)。
|
|
2
|
+
|
|
3
|
+
保真原则:**只翻译 docx 里真实存在的信息,不发明样式**。
|
|
4
|
+
|
|
5
|
+
- 段落 = **一个元素**,`text` 是**富文本标记**(``<b>`` / ``<i>`` / ``<u>`` /
|
|
6
|
+
``<font color= size= face=>``),``{{ 变量 }}`` **保持内联**——不再把
|
|
7
|
+
「``{{project_name}}日报``」拆成 input + 段落两个区域;
|
|
8
|
+
- **逐 run** 取字体/字号/颜色/粗斜体(Word 的混排段落不会丢格式),但**样式已经表达过的
|
|
9
|
+
格式不重复写进 `text`**:整段同格式 → `text` 是纯文本(字体/字号/粗细在元素 `style`
|
|
10
|
+
上);混排段落也只有与样式不同的片段才带标记。样式没有的位(下划线/高亮/上下标/字符间距)
|
|
11
|
+
留在内联标记里 —— 见 ``_baseline_fmt`` / ``_trim_fmt``;
|
|
12
|
+
- 字体名**原样保留**(文档写 ``宋体`` 就存 ``宋体``、写 ``SimSun`` 就存 ``SimSun``、
|
|
13
|
+
写 ``华文中宋`` 就存 ``华文中宋``)——导入不替用户挑语义族,跨平台解析交给
|
|
14
|
+
fonts.registry(本机有该字面就精确命中真身,没有才按名字里的风格回退);
|
|
15
|
+
- 表格边框读 ``w:tblBorders`` / ``w:tcBorders``,底纹读 ``w:shd`` ——
|
|
16
|
+
**docx 没有底纹就不画底纹**(早前版本硬编码灰色表头,属于发明);
|
|
17
|
+
- 列宽读 ``w:gridCol`` / ``w:tcW``,垂直对齐读 ``w:vAlign``;
|
|
18
|
+
- 图片提取为 image 元素(base64 data URL);分页符 → pageBreak;
|
|
19
|
+
- 字段识别:Word 内容控件(w:sdt)与**循环外**的 ``{{ 变量 }}`` → 可填字段;
|
|
20
|
+
**循环内**的 ``{{ 变量 }}`` → 保留为模板插值(渲染时按数据求值);
|
|
21
|
+
- 模板标签:``{% for x in y %}`` / ``{% endfor %}`` 可出现在段落或表格单元格中,
|
|
22
|
+
按出现顺序配对,循环体可跨多行(如「人员明细行 + 合计行」共享一个外层循环)。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import io
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from docx import Document
|
|
31
|
+
|
|
32
|
+
from pdfwright.converters.docx_block import _DocxBlockMixin
|
|
33
|
+
|
|
34
|
+
# ``DocxField`` 只在这里再导出(公开 API:``converters/__init__.py`` 从本模块取),
|
|
35
|
+
# 本模块自己不用它 —— 故显式标注 F401。(noqa 只能跟规则码,说明写在独立注释行。)
|
|
36
|
+
from pdfwright.converters.docx_common import (
|
|
37
|
+
DocxField, # noqa: F401
|
|
38
|
+
DocxParseResult,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# 拆模块后本文件只剩「组合 + 入口」。`font_name` / `scan_jinja_tags` /
|
|
42
|
+
# `strip_jinja_tags` 曾在这里带 F401 抑制注释再导出,但兄弟模块一律**直接**从
|
|
43
|
+
# ``docx_common`` 导入,全仓(含测试与 pdfwright-web)无人从 ``converters.docx``
|
|
44
|
+
# 取这三个名字 —— 那是纯死代码,还会让人误以为这里是它们的公开入口,故移除。
|
|
45
|
+
# ``_format_number`` / ``semantic_family`` 相反:确实有人从这里导入,作为**向后兼容的
|
|
46
|
+
# 公开入口**保留(本模块自己不用),并显式标注 F401。
|
|
47
|
+
# (noqa 指令只能跟规则码,中文说明必须写在独立注释行。)
|
|
48
|
+
from pdfwright.converters.docx_helpers import ( # noqa: F401
|
|
49
|
+
_format_number, # 既有测试从这里导入
|
|
50
|
+
semantic_family, # deprecated shim,见 docx_helpers.semantic_family
|
|
51
|
+
)
|
|
52
|
+
from pdfwright.converters.docx_run import _DocxRunMixin
|
|
53
|
+
from pdfwright.converters.docx_table import _DocxTableMixin
|
|
54
|
+
from pdfwright.errors import ConversionError
|
|
55
|
+
from pdfwright.models import (
|
|
56
|
+
Element,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# 方法按职责拆到这些 mixin(共享状态在 ``docx_state.DocxConverterState`` 声明):
|
|
61
|
+
# 段落与 run → ``docx_run``,表格 → ``docx_table``,块结构与文档级信息 → ``docx_block``。
|
|
62
|
+
class DocxConverter(_DocxRunMixin, _DocxTableMixin, _DocxBlockMixin):
|
|
63
|
+
"""DOCX → DocumentConfig 转换器。"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, auto_field: bool = True, inline_commands: bool = False) -> None:
|
|
66
|
+
self.auto_field = auto_field
|
|
67
|
+
#: 同一段落里的「文字 + 内联命令标签」是否**排在同一行**(见
|
|
68
|
+
#: ``_append_paragraph_content``)。默认 ``False`` = **块级**:文字与图片/签章位
|
|
69
|
+
#: 各自独立成块(导入结果与旧版逐字节一致,可自由拖拽、重排);
|
|
70
|
+
#: 置 ``True`` 才包一层按内容的 ``row`` 保留 Word 的同行排版。
|
|
71
|
+
self.inline_commands = inline_commands
|
|
72
|
+
self._used_keys: set[str] = set()
|
|
73
|
+
self._part: Any = None
|
|
74
|
+
self._doc_defaults: Any = None
|
|
75
|
+
self._doc_sections: list[Any] = []
|
|
76
|
+
self._section_index = 0
|
|
77
|
+
self._table_margins: Any = None
|
|
78
|
+
# 列表编号:numId → {ilvl: (numFmt, start, lvlText)};计数器随文档顺序累加
|
|
79
|
+
self._numbering: dict[str, dict[int, tuple[str, int, str]]] = {}
|
|
80
|
+
self._number_counters: dict[tuple[str, int], int] = {}
|
|
81
|
+
# 块栈:栈顶是当前打开的容器(``for`` / ``if``),其 children(if 在 else 时为
|
|
82
|
+
# else_children)收后续元素。元素为 ``(kind, node, in_else, chain_start)``;
|
|
83
|
+
# ``chain_start`` 标记 if 链起点(``{% elif %}`` 会在 else 分支里再开一个嵌套 if,
|
|
84
|
+
# 因此 ``{% endif %}`` 要把整条链一起关掉)。
|
|
85
|
+
self._block_stack: list[tuple[str, Element, bool, bool]] = []
|
|
86
|
+
# 内容插入目标覆盖:表格单元格必须写入自己的 td,而不是当前循环体
|
|
87
|
+
self._target_override: list[Element] | None = None
|
|
88
|
+
# 文档水印(浮动图片,``wp:anchor/@behindDoc=1``)
|
|
89
|
+
self._watermark: dict[str, Any] | None = None
|
|
90
|
+
# 文档默认字体(→ page.fontFamily;仅当文档自己声明了才下发,不按语言猜)
|
|
91
|
+
self._document_font_family: str | None = None
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------ 入口
|
|
94
|
+
def convert(self, source: str | bytes | io.BytesIO) -> DocxParseResult:
|
|
95
|
+
try:
|
|
96
|
+
if isinstance(source, bytes):
|
|
97
|
+
document = Document(io.BytesIO(source)) # pyright: ignore[reportCallIssue]
|
|
98
|
+
elif isinstance(source, io.BytesIO):
|
|
99
|
+
document = Document(source) # pyright: ignore[reportCallIssue]
|
|
100
|
+
else:
|
|
101
|
+
document = Document(source) # pyright: ignore[reportCallIssue]
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
# python-docx 只认 OOXML(.docx/.docm);旧版二进制 .doc 会报
|
|
104
|
+
# PackageNotFoundError → 这里统一转成 ConversionError(HTTP 400 + 可读提示),
|
|
105
|
+
# 否则会以未处理异常冒到 500。
|
|
106
|
+
raise ConversionError(
|
|
107
|
+
message="不是有效的 .docx 文件(旧版二进制 .doc 需先转换为 .docx)",
|
|
108
|
+
detail={"reason": type(exc).__name__},
|
|
109
|
+
cause=exc,
|
|
110
|
+
) from exc
|
|
111
|
+
self._part = document.part
|
|
112
|
+
return self._convert_doc(document)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def docx_to_schema(
|
|
116
|
+
source: str | bytes | io.BytesIO, auto_field: bool = True, inline_commands: bool = False
|
|
117
|
+
) -> DocxParseResult:
|
|
118
|
+
"""一行调用:DOCX → DocumentConfig + 字段列表。
|
|
119
|
+
|
|
120
|
+
``inline_commands``:同一段落里的「文字 + 内联命令标签」(``记录人:{{sign:…}}``)
|
|
121
|
+
是否排在**同一行**(默认 ``False`` = 块级,各自独立成块)。
|
|
122
|
+
"""
|
|
123
|
+
return DocxConverter(auto_field=auto_field, inline_commands=inline_commands).convert(source)
|