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.
Files changed (128) hide show
  1. {pdfwright-0.3.0 → pdfwright-0.4.0}/PKG-INFO +1 -1
  2. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/generate_all.py +14 -2
  3. {pdfwright-0.3.0 → pdfwright-0.4.0}/pyproject.toml +13 -3
  4. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/__init__.py +1 -1
  5. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/converters/__init__.py +2 -0
  6. pdfwright-0.4.0/src/pdfwright/converters/docx.py +123 -0
  7. pdfwright-0.4.0/src/pdfwright/converters/docx_block.py +500 -0
  8. pdfwright-0.4.0/src/pdfwright/converters/docx_common.py +198 -0
  9. pdfwright-0.4.0/src/pdfwright/converters/docx_helpers.py +533 -0
  10. pdfwright-0.4.0/src/pdfwright/converters/docx_run.py +604 -0
  11. pdfwright-0.4.0/src/pdfwright/converters/docx_state.py +97 -0
  12. pdfwright-0.4.0/src/pdfwright/converters/docx_table.py +329 -0
  13. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/converters/html.py +17 -6
  14. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/__init__.py +4 -0
  15. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/registry.py +544 -215
  16. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/scanner.py +60 -6
  17. pdfwright-0.4.0/src/pdfwright/fonts/semantic_families.py +436 -0
  18. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/__init__.py +4 -0
  19. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/constants.py +37 -23
  20. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/document.py +124 -22
  21. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/elements.py +88 -16
  22. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/style.py +73 -2
  23. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/base.py +2 -1
  24. pdfwright-0.4.0/src/pdfwright/render/codes.py +241 -0
  25. pdfwright-0.4.0/src/pdfwright/render/html/__init__.py +160 -0
  26. pdfwright-0.4.0/src/pdfwright/render/html_blocks.py +394 -0
  27. pdfwright-0.4.0/src/pdfwright/render/html_parts.py +469 -0
  28. pdfwright-0.4.0/src/pdfwright/render/html_state.py +58 -0
  29. pdfwright-0.4.0/src/pdfwright/render/html_style.py +452 -0
  30. pdfwright-0.4.0/src/pdfwright/render/html_table.py +427 -0
  31. pdfwright-0.4.0/src/pdfwright/render/kinsoku.py +299 -0
  32. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/pdf.py +210 -2157
  33. pdfwright-0.4.0/src/pdfwright/render/pdf_blocks.py +563 -0
  34. pdfwright-0.4.0/src/pdfwright/render/pdf_flowables.py +1382 -0
  35. pdfwright-0.4.0/src/pdfwright/render/pdf_media.py +492 -0
  36. pdfwright-0.4.0/src/pdfwright/render/pdf_state.py +84 -0
  37. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/table.py +11 -0
  38. pdfwright-0.4.0/src/pdfwright/render/units.py +32 -0
  39. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/utils.py +45 -2
  40. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/validate/__init__.py +215 -3
  41. pdfwright-0.4.0/tests/conftest.py +66 -0
  42. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_cell_borders.py +66 -0
  43. pdfwright-0.4.0/tests/test_divider_geometry.py +89 -0
  44. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_docx_converter.py +320 -17
  45. pdfwright-0.4.0/tests/test_fonts.py +954 -0
  46. pdfwright-0.4.0/tests/test_kinsoku.py +143 -0
  47. pdfwright-0.4.0/tests/test_line_grid.py +136 -0
  48. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_models.py +221 -0
  49. pdfwright-0.4.0/tests/test_prewrap_indent.py +272 -0
  50. pdfwright-0.4.0/tests/test_render_codes.py +166 -0
  51. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_html.py +173 -0
  52. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_pdf.py +196 -0
  53. pdfwright-0.4.0/tests/test_table_vectors.py +77 -0
  54. pdfwright-0.4.0/tests/test_validate.py +701 -0
  55. pdfwright-0.3.0/src/pdfwright/converters/docx.py +0 -1992
  56. pdfwright-0.3.0/src/pdfwright/render/codes.py +0 -152
  57. pdfwright-0.3.0/src/pdfwright/render/html/__init__.py +0 -1598
  58. pdfwright-0.3.0/tests/test_fonts.py +0 -278
  59. pdfwright-0.3.0/tests/test_validate.py +0 -284
  60. {pdfwright-0.3.0 → pdfwright-0.4.0}/.gitignore +0 -0
  61. {pdfwright-0.3.0 → pdfwright-0.4.0}/README.md +0 -0
  62. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/contract.json +0 -0
  63. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/cooperation_contract.json +0 -0
  64. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/daily_report.json +0 -0
  65. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/feature_tour.json +0 -0
  66. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/font_mixing.json +0 -0
  67. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/generate_demo.py +0 -0
  68. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/invoice.json +0 -0
  69. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/output.html +0 -0
  70. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/output.pdf +0 -0
  71. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/path_security.json +0 -0
  72. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/sample_document.json +0 -0
  73. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/service_contract.json +0 -0
  74. {pdfwright-0.3.0 → pdfwright-0.4.0}/examples/staff_form.json +0 -0
  75. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/batch.py +0 -0
  76. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/commands.py +0 -0
  77. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/converters/xlsx.py +0 -0
  78. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/errors.py +0 -0
  79. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/fallback.py +0 -0
  80. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/oblique.py +0 -0
  81. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/sfnt.py +0 -0
  82. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/fonts/system_fonts.py +0 -0
  83. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/borders.py +0 -0
  84. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/models/table_columns.py +0 -0
  85. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/__init__.py +0 -0
  86. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/fingerprint.py +0 -0
  87. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/font_scan.py +0 -0
  88. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/metadata.py +0 -0
  89. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/pdfa_verify.py +0 -0
  90. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/seal.py +0 -0
  91. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/postprocess/signed_verify.py +0 -0
  92. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/py.typed +0 -0
  93. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/__init__.py +0 -0
  94. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/colors.py +0 -0
  95. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/layout.py +0 -0
  96. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/markup.py +0 -0
  97. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/merge.py +0 -0
  98. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/options.py +0 -0
  99. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/render/pdfa.py +0 -0
  100. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/security.py +0 -0
  101. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/__init__.py +0 -0
  102. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/context.py +0 -0
  103. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/engine.py +0 -0
  104. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/filters.py +0 -0
  105. {pdfwright-0.3.0 → pdfwright-0.4.0}/src/pdfwright/template/format.py +0 -0
  106. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_colors.py +0 -0
  107. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_examples.py +0 -0
  108. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_fingerprint.py +0 -0
  109. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_font_bold.py +0 -0
  110. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_font_policy.py +0 -0
  111. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_fonts_oblique.py +0 -0
  112. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_html_converter.py +0 -0
  113. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_margins.py +0 -0
  114. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_merge.py +0 -0
  115. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_options_and_validation.py +0 -0
  116. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_markup.py +0 -0
  117. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_row.py +0 -0
  118. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_render_table.py +0 -0
  119. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_row_widths.py +0 -0
  120. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_security.py +0 -0
  121. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_shared_vectors.py +0 -0
  122. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_sign_local_stamp.py +0 -0
  123. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_signed_verify.py +0 -0
  124. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_span_expression.py +0 -0
  125. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_style_box.py +0 -0
  126. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_template.py +0 -0
  127. {pdfwright-0.3.0 → pdfwright-0.4.0}/tests/test_xlsx_converter.py +0 -0
  128. {pdfwright-0.3.0 → pdfwright-0.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pdfwright
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: 配置驱动的 PDF 生成基础库:JSON 配置 → 模板求值(变量/循环/条件)→ 高保真 PDF。纯 Python 引擎(ReportLab + fontTools + Jinja2 + pikepdf),无外部软件依赖。
5
5
  License: Proprietary
6
6
  Requires-Python: >=3.12
@@ -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
- print(f"✅ {len(files)} 个示例全部通过" + ("" if check_only else f";产物在 {OUTPUT_DIR}"))
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.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
- # 这三个解析器大量使用第三方库动态属性,规则级豁免无法表达,故整体不纳入 basedpyright;
94
- # 它们仍由 mypy 检查。
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.3.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)