docvortex 0.2.1__py3-none-any.whl

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 (364) hide show
  1. docvortex/__init__.py +21 -0
  2. docvortex/analyzers/__init__.py +3 -0
  3. docvortex/analyzers/native/__init__.py +37 -0
  4. docvortex/analyzers/native/_shared/__init__.py +3 -0
  5. docvortex/analyzers/native/_shared/hyperlink.py +16 -0
  6. docvortex/analyzers/native/_shared/image.py +8 -0
  7. docvortex/analyzers/native/_shared/markup/__init__.py +47 -0
  8. docvortex/analyzers/native/_shared/markup/anchors.py +30 -0
  9. docvortex/analyzers/native/_shared/markup/formula.py +37 -0
  10. docvortex/analyzers/native/_shared/markup/projector.py +57 -0
  11. docvortex/analyzers/native/_shared/markup/styles.py +19 -0
  12. docvortex/analyzers/native/_shared/mathml.py +17 -0
  13. docvortex/analyzers/native/_shared/names.py +7 -0
  14. docvortex/analyzers/native/_shared/xycut.py +414 -0
  15. docvortex/analyzers/native/contracts.py +44 -0
  16. docvortex/analyzers/native/csv.py +351 -0
  17. docvortex/analyzers/native/epub/__init__.py +18 -0
  18. docvortex/analyzers/native/epub/constants.py +45 -0
  19. docvortex/analyzers/native/epub/converter.py +80 -0
  20. docvortex/analyzers/native/epub/errors.py +20 -0
  21. docvortex/analyzers/native/epub/metadata.py +31 -0
  22. docvortex/analyzers/native/epub/package.py +649 -0
  23. docvortex/analyzers/native/epub/xhtml.py +306 -0
  24. docvortex/analyzers/native/html/__init__.py +6 -0
  25. docvortex/analyzers/native/html/anchors.py +271 -0
  26. docvortex/analyzers/native/html/constants.py +25 -0
  27. docvortex/analyzers/native/html/contracts.py +7 -0
  28. docvortex/analyzers/native/html/converter.py +117 -0
  29. docvortex/analyzers/native/html/document.py +389 -0
  30. docvortex/analyzers/native/html/errors.py +12 -0
  31. docvortex/analyzers/native/html/resources.py +365 -0
  32. docvortex/analyzers/native/html/selector.py +421 -0
  33. docvortex/analyzers/native/models.py +209 -0
  34. docvortex/analyzers/native/ofd/__init__.py +14 -0
  35. docvortex/analyzers/native/ofd/constants.py +59 -0
  36. docvortex/analyzers/native/ofd/converter.py +31 -0
  37. docvortex/analyzers/native/ofd/errors.py +16 -0
  38. docvortex/analyzers/native/ofd/geometry.py +215 -0
  39. docvortex/analyzers/native/ofd/images.py +124 -0
  40. docvortex/analyzers/native/ofd/metadata.py +53 -0
  41. docvortex/analyzers/native/ofd/models.py +170 -0
  42. docvortex/analyzers/native/ofd/package.py +346 -0
  43. docvortex/analyzers/native/ofd/path.py +201 -0
  44. docvortex/analyzers/native/ofd/reading_order.py +293 -0
  45. docvortex/analyzers/native/ofd/resources.py +122 -0
  46. docvortex/analyzers/native/ofd/scene.py +347 -0
  47. docvortex/analyzers/native/ofd/table.py +240 -0
  48. docvortex/analyzers/native/ofd/text.py +521 -0
  49. docvortex/analyzers/native/office/__init__.py +3 -0
  50. docvortex/analyzers/native/office/doc/__init__.py +3 -0
  51. docvortex/analyzers/native/office/doc/bookmarks.py +81 -0
  52. docvortex/analyzers/native/office/doc/doc_converter.py +573 -0
  53. docvortex/analyzers/native/office/doc/fib.py +261 -0
  54. docvortex/analyzers/native/office/doc/fields.py +101 -0
  55. docvortex/analyzers/native/office/doc/formatting.py +171 -0
  56. docvortex/analyzers/native/office/doc/images.py +162 -0
  57. docvortex/analyzers/native/office/doc/lists.py +356 -0
  58. docvortex/analyzers/native/office/doc/models.py +164 -0
  59. docvortex/analyzers/native/office/doc/parser.py +851 -0
  60. docvortex/analyzers/native/office/doc/pieces.py +256 -0
  61. docvortex/analyzers/native/office/doc/records.py +40 -0
  62. docvortex/analyzers/native/office/doc/sprm.py +269 -0
  63. docvortex/analyzers/native/office/doc/styles.py +215 -0
  64. docvortex/analyzers/native/office/docx/__init__.py +3 -0
  65. docvortex/analyzers/native/office/docx/context.py +71 -0
  66. docvortex/analyzers/native/office/docx/docx_converter.py +614 -0
  67. docvortex/analyzers/native/office/docx/equationxml.py +119 -0
  68. docvortex/analyzers/native/office/docx/fields.py +816 -0
  69. docvortex/analyzers/native/office/docx/formatting_types.py +23 -0
  70. docvortex/analyzers/native/office/docx/main.py +50 -0
  71. docvortex/analyzers/native/office/docx/numbering.py +491 -0
  72. docvortex/analyzers/native/office/docx/office_xml.py +57 -0
  73. docvortex/analyzers/native/office/docx/package_normalizer.py +248 -0
  74. docvortex/analyzers/native/office/docx/resources.py +526 -0
  75. docvortex/analyzers/native/office/docx/styles.py +693 -0
  76. docvortex/analyzers/native/office/docx/tables.py +515 -0
  77. docvortex/analyzers/native/office/equation/__init__.py +3 -0
  78. docvortex/analyzers/native/office/equation/image.py +470 -0
  79. docvortex/analyzers/native/office/equation/latex_dict.py +324 -0
  80. docvortex/analyzers/native/office/equation/mtef.py +885 -0
  81. docvortex/analyzers/native/office/equation/mtef_v5.py +941 -0
  82. docvortex/analyzers/native/office/equation/omml.py +561 -0
  83. docvortex/analyzers/native/office/equation/ooxml.py +62 -0
  84. docvortex/analyzers/native/office/errors.py +33 -0
  85. docvortex/analyzers/native/office/image.py +307 -0
  86. docvortex/analyzers/native/office/legacy/__init__.py +3 -0
  87. docvortex/analyzers/native/office/legacy/binary.py +43 -0
  88. docvortex/analyzers/native/office/legacy/officeart.py +362 -0
  89. docvortex/analyzers/native/office/legacy/ole.py +110 -0
  90. docvortex/analyzers/native/office/limits.py +12 -0
  91. docvortex/analyzers/native/office/odf/__init__.py +3 -0
  92. docvortex/analyzers/native/office/odf/chart.py +76 -0
  93. docvortex/analyzers/native/office/odf/constants.py +70 -0
  94. docvortex/analyzers/native/office/odf/converters.py +403 -0
  95. docvortex/analyzers/native/office/odf/errors.py +18 -0
  96. docvortex/analyzers/native/office/odf/metadata.py +91 -0
  97. docvortex/analyzers/native/office/odf/models.py +176 -0
  98. docvortex/analyzers/native/office/odf/package.py +270 -0
  99. docvortex/analyzers/native/office/odf/styles.py +329 -0
  100. docvortex/analyzers/native/office/odf/table.py +469 -0
  101. docvortex/analyzers/native/office/odf/text.py +1002 -0
  102. docvortex/analyzers/native/office/ooxml_chart.py +1016 -0
  103. docvortex/analyzers/native/office/opc.py +38 -0
  104. docvortex/analyzers/native/office/ppt/__init__.py +3 -0
  105. docvortex/analyzers/native/office/ppt/models.py +118 -0
  106. docvortex/analyzers/native/office/ppt/parser.py +1895 -0
  107. docvortex/analyzers/native/office/ppt/ppt_converter.py +292 -0
  108. docvortex/analyzers/native/office/ppt/records.py +131 -0
  109. docvortex/analyzers/native/office/ppt/style_text.py +247 -0
  110. docvortex/analyzers/native/office/pptx/__init__.py +3 -0
  111. docvortex/analyzers/native/office/pptx/context.py +113 -0
  112. docvortex/analyzers/native/office/pptx/lists.py +558 -0
  113. docvortex/analyzers/native/office/pptx/main.py +20 -0
  114. docvortex/analyzers/native/office/pptx/package_normalizer.py +321 -0
  115. docvortex/analyzers/native/office/pptx/pptx_converter.py +323 -0
  116. docvortex/analyzers/native/office/pptx/resources.py +329 -0
  117. docvortex/analyzers/native/office/pptx/shapes.py +393 -0
  118. docvortex/analyzers/native/office/pptx/text_styles.py +625 -0
  119. docvortex/analyzers/native/office/pptx/titles.py +178 -0
  120. docvortex/analyzers/native/office/rich_text.py +420 -0
  121. docvortex/analyzers/native/office/rtf/__init__.py +3 -0
  122. docvortex/analyzers/native/office/rtf/converter.py +708 -0
  123. docvortex/analyzers/native/office/rtf/lexer.py +213 -0
  124. docvortex/analyzers/native/office/rtf/math.py +339 -0
  125. docvortex/analyzers/native/office/rtf/models.py +185 -0
  126. docvortex/analyzers/native/office/rtf/parser.py +1553 -0
  127. docvortex/analyzers/native/office/spreadsheet/__init__.py +3 -0
  128. docvortex/analyzers/native/office/spreadsheet/html.py +81 -0
  129. docvortex/analyzers/native/office/spreadsheet/models.py +79 -0
  130. docvortex/analyzers/native/office/spreadsheet/projector.py +928 -0
  131. docvortex/analyzers/native/office/streams.py +18 -0
  132. docvortex/analyzers/native/office/xls/__init__.py +3 -0
  133. docvortex/analyzers/native/office/xls/chart.py +132 -0
  134. docvortex/analyzers/native/office/xls/embedded_chart.py +299 -0
  135. docvortex/analyzers/native/office/xls/models.py +109 -0
  136. docvortex/analyzers/native/office/xls/number_format.py +521 -0
  137. docvortex/analyzers/native/office/xls/parser.py +1145 -0
  138. docvortex/analyzers/native/office/xls/records.py +201 -0
  139. docvortex/analyzers/native/office/xls/strings.py +205 -0
  140. docvortex/analyzers/native/office/xls/xls_converter.py +354 -0
  141. docvortex/analyzers/native/office/xlsx/__init__.py +3 -0
  142. docvortex/analyzers/native/office/xlsx/main.py +20 -0
  143. docvortex/analyzers/native/office/xlsx/ooxml_ole.py +522 -0
  144. docvortex/analyzers/native/office/xlsx/package_normalizer.py +310 -0
  145. docvortex/analyzers/native/office/xlsx/xlsx_converter.py +716 -0
  146. docvortex/analyzers/native/pdf/__init__.py +3 -0
  147. docvortex/analyzers/native/pdf/auxiliary_text.py +1671 -0
  148. docvortex/analyzers/native/pdf/char_geometry.py +1662 -0
  149. docvortex/analyzers/native/pdf/code_blocks.py +535 -0
  150. docvortex/analyzers/native/pdf/formulas.py +1985 -0
  151. docvortex/analyzers/native/pdf/geometry.py +281 -0
  152. docvortex/analyzers/native/pdf/graphics.py +1501 -0
  153. docvortex/analyzers/native/pdf/index_blocks.py +268 -0
  154. docvortex/analyzers/native/pdf/inline/__init__.py +3 -0
  155. docvortex/analyzers/native/pdf/inline/common.py +112 -0
  156. docvortex/analyzers/native/pdf/inline/detection.py +590 -0
  157. docvortex/analyzers/native/pdf/inline/matching.py +1185 -0
  158. docvortex/analyzers/native/pdf/inline/materialize.py +457 -0
  159. docvortex/analyzers/native/pdf/inline/scripts.py +975 -0
  160. docvortex/analyzers/native/pdf/inline/types.py +385 -0
  161. docvortex/analyzers/native/pdf/line_layout.py +1106 -0
  162. docvortex/analyzers/native/pdf/line_merging.py +1223 -0
  163. docvortex/analyzers/native/pdf/models.py +246 -0
  164. docvortex/analyzers/native/pdf/native_text.py +1004 -0
  165. docvortex/analyzers/native/pdf/pipeline.py +1390 -0
  166. docvortex/analyzers/native/pdf/script_geometry.py +636 -0
  167. docvortex/analyzers/native/pdf/shared.py +30 -0
  168. docvortex/analyzers/native/pdf/spatial_text.py +383 -0
  169. docvortex/analyzers/native/pdf/table_annotations.py +446 -0
  170. docvortex/analyzers/native/pdf/table_constants.py +48 -0
  171. docvortex/analyzers/native/pdf/table_detection.py +227 -0
  172. docvortex/analyzers/native/pdf/table_filled_grid.py +218 -0
  173. docvortex/analyzers/native/pdf/table_geometry.py +40 -0
  174. docvortex/analyzers/native/pdf/table_materialization.py +444 -0
  175. docvortex/analyzers/native/pdf/table_recovery/__init__.py +15 -0
  176. docvortex/analyzers/native/pdf/table_recovery/candidate.py +356 -0
  177. docvortex/analyzers/native/pdf/table_recovery/contracts.py +154 -0
  178. docvortex/analyzers/native/pdf/table_recovery/engine.py +609 -0
  179. docvortex/analyzers/native/pdf/table_recovery/geometry.py +164 -0
  180. docvortex/analyzers/native/pdf/table_recovery/sparse_common.py +85 -0
  181. docvortex/analyzers/native/pdf/table_recovery/sparse_hybrid.py +804 -0
  182. docvortex/analyzers/native/pdf/table_recovery/sparse_multiline.py +1122 -0
  183. docvortex/analyzers/native/pdf/table_recovery/text.py +414 -0
  184. docvortex/analyzers/native/pdf/table_recovery/text_grid.py +618 -0
  185. docvortex/analyzers/native/pdf/table_recovery/vector.py +1931 -0
  186. docvortex/analyzers/native/pdf/table_rows.py +34 -0
  187. docvortex/analyzers/native/pdf/table_rules.py +1129 -0
  188. docvortex/analyzers/native/pdf/table_text_styles.py +283 -0
  189. docvortex/analyzers/native/pdf/tables.py +147 -0
  190. docvortex/analyzers/native/pdf/text_assembly/__init__.py +3 -0
  191. docvortex/analyzers/native/pdf/text_assembly/annotations.py +581 -0
  192. docvortex/analyzers/native/pdf/text_assembly/assembly.py +292 -0
  193. docvortex/analyzers/native/pdf/text_assembly/common.py +477 -0
  194. docvortex/analyzers/native/pdf/text_assembly/footnotes.py +394 -0
  195. docvortex/analyzers/native/pdf/text_assembly/merging.py +1274 -0
  196. docvortex/analyzers/native/pdf/text_assembly/rows.py +692 -0
  197. docvortex/analyzers/native/pdf/text_blocks.py +82 -0
  198. docvortex/analyzers/native/pdf/text_styles.py +55 -0
  199. docvortex/analyzers/native/pdf/title_analysis/__init__.py +3 -0
  200. docvortex/analyzers/native/pdf/title_analysis/body_profile.py +215 -0
  201. docvortex/analyzers/native/pdf/title_analysis/common.py +117 -0
  202. docvortex/analyzers/native/pdf/title_analysis/document_profile.py +164 -0
  203. docvortex/analyzers/native/pdf/title_analysis/lane_titles.py +758 -0
  204. docvortex/analyzers/native/pdf/title_analysis/page_titles.py +1024 -0
  205. docvortex/analyzers/native/pdf/title_analysis/prototype.py +194 -0
  206. docvortex/analyzers/native/pdf/title_analysis/structural.py +1081 -0
  207. docvortex/analyzers/native/pdf/titles.py +75 -0
  208. docvortex/analyzers/native/pdf/typography.py +19 -0
  209. docvortex/analyzers/native/pdf/visual_annotations.py +1262 -0
  210. docvortex/api.py +180 -0
  211. docvortex/assets/__init__.py +5 -0
  212. docvortex/assets/store.py +51 -0
  213. docvortex/cli.py +54 -0
  214. docvortex/codecs/__init__.py +3 -0
  215. docvortex/codecs/html/__init__.py +22 -0
  216. docvortex/codecs/html/contracts.py +236 -0
  217. docvortex/codecs/html/materializer.py +331 -0
  218. docvortex/codecs/html/parser.py +763 -0
  219. docvortex/codecs/html/resources.py +34 -0
  220. docvortex/codecs/json.py +17 -0
  221. docvortex/content/__init__.py +5 -0
  222. docvortex/content/inline.py +248 -0
  223. docvortex/content/markup/__init__.py +44 -0
  224. docvortex/content/markup/anchors.py +188 -0
  225. docvortex/content/markup/formula.py +280 -0
  226. docvortex/content/markup/projector.py +1237 -0
  227. docvortex/content/markup/styles.py +327 -0
  228. docvortex/content/mathml.py +167 -0
  229. docvortex/content/normalization.py +188 -0
  230. docvortex/content/spans.py +183 -0
  231. docvortex/content/table/__init__.py +18 -0
  232. docvortex/content/table/blocks.py +152 -0
  233. docvortex/content/table/content.py +425 -0
  234. docvortex/content/table/document.py +104 -0
  235. docvortex/content/table/html.py +399 -0
  236. docvortex/content/table/models.py +76 -0
  237. docvortex/content/table/rules.py +42 -0
  238. docvortex/content/table/structure.py +221 -0
  239. docvortex/content/tree.py +5 -0
  240. docvortex/document/__init__.py +3 -0
  241. docvortex/document/contracts.py +22 -0
  242. docvortex/document/detection.py +389 -0
  243. docvortex/document/filetypes.py +175 -0
  244. docvortex/document/page_range.py +167 -0
  245. docvortex/document/pdf/__init__.py +19 -0
  246. docvortex/document/pdf/classify.py +1138 -0
  247. docvortex/document/pdf/constants.py +132 -0
  248. docvortex/document/pdf/diagnostics.py +350 -0
  249. docvortex/document/pdf/document.py +582 -0
  250. docvortex/document/pdf/font_runtime.py +335 -0
  251. docvortex/document/pdf/geometry.py +31 -0
  252. docvortex/document/pdf/images.py +654 -0
  253. docvortex/document/pdf/native_annotations.py +367 -0
  254. docvortex/document/pdf/native_contracts.py +169 -0
  255. docvortex/document/pdf/native_coordinates.py +216 -0
  256. docvortex/document/pdf/native_lifecycle.py +16 -0
  257. docvortex/document/pdf/native_objects.py +902 -0
  258. docvortex/document/pdf/native_text_geometry.py +315 -0
  259. docvortex/document/pdf/pdfium.py +325 -0
  260. docvortex/document/pdf/raster.py +46 -0
  261. docvortex/document/pdf/text/__init__.py +62 -0
  262. docvortex/document/pdf/text/contracts.py +211 -0
  263. docvortex/document/pdf/text/extract.py +165 -0
  264. docvortex/document/pdf/text/geometry.py +16 -0
  265. docvortex/document/pdf/text/groups.py +162 -0
  266. docvortex/document/pdf/visual_geometry.py +201 -0
  267. docvortex/document/pdf/visuals.py +343 -0
  268. docvortex/document/source.py +92 -0
  269. docvortex/errors.py +21 -0
  270. docvortex/export/__init__.py +3 -0
  271. docvortex/export/bundle.py +98 -0
  272. docvortex/export/files.py +66 -0
  273. docvortex/export/middle.py +208 -0
  274. docvortex/foundation/__init__.py +3 -0
  275. docvortex/foundation/geometry.py +125 -0
  276. docvortex/foundation/hyperlink.py +65 -0
  277. docvortex/foundation/image.py +48 -0
  278. docvortex/foundation/image_encoding.py +30 -0
  279. docvortex/foundation/image_payload.py +280 -0
  280. docvortex/foundation/language.py +92 -0
  281. docvortex/foundation/platform.py +38 -0
  282. docvortex/foundation/text.py +153 -0
  283. docvortex/foundation/type_identity.py +20 -0
  284. docvortex/foundation/xml_names.py +20 -0
  285. docvortex/options.py +30 -0
  286. docvortex/postprocess/__init__.py +3 -0
  287. docvortex/postprocess/content.py +53 -0
  288. docvortex/postprocess/document.py +19 -0
  289. docvortex/postprocess/lists.py +236 -0
  290. docvortex/postprocess/page_blocks.py +214 -0
  291. docvortex/postprocess/pages.py +95 -0
  292. docvortex/postprocess/paragraphs.py +580 -0
  293. docvortex/postprocess/visual.py +715 -0
  294. docvortex/render/__init__.py +48 -0
  295. docvortex/render/_internal/__init__.py +3 -0
  296. docvortex/render/_internal/common/__init__.py +3 -0
  297. docvortex/render/_internal/common/context.py +43 -0
  298. docvortex/render/_internal/common/html_table.py +178 -0
  299. docvortex/render/_internal/common/index.py +33 -0
  300. docvortex/render/_internal/common/list_items.py +158 -0
  301. docvortex/render/_internal/common/planner.py +140 -0
  302. docvortex/render/_internal/docx/__init__.py +3 -0
  303. docvortex/render/_internal/docx/assets.py +202 -0
  304. docvortex/render/_internal/docx/inline.py +434 -0
  305. docvortex/render/_internal/docx/math.py +220 -0
  306. docvortex/render/_internal/docx/renderer.py +905 -0
  307. docvortex/render/_internal/docx/styles.py +195 -0
  308. docvortex/render/_internal/docx/table.py +442 -0
  309. docvortex/render/_internal/epub/__init__.py +5 -0
  310. docvortex/render/_internal/epub/assets.py +173 -0
  311. docvortex/render/_internal/epub/package.py +249 -0
  312. docvortex/render/_internal/epub/renderer.py +1156 -0
  313. docvortex/render/_internal/html/__init__.py +3 -0
  314. docvortex/render/_internal/html/inline.py +346 -0
  315. docvortex/render/_internal/html/renderer.py +1041 -0
  316. docvortex/render/_internal/html/sanitizer.py +478 -0
  317. docvortex/render/_internal/html/table.py +121 -0
  318. docvortex/render/_internal/latex/__init__.py +1 -0
  319. docvortex/render/_internal/latex/assets.py +85 -0
  320. docvortex/render/_internal/latex/inline.py +145 -0
  321. docvortex/render/_internal/latex/renderer.py +506 -0
  322. docvortex/render/_internal/latex/table.py +347 -0
  323. docvortex/render/_internal/markdown/__init__.py +3 -0
  324. docvortex/render/_internal/markdown/assets.py +78 -0
  325. docvortex/render/_internal/markdown/blocks.py +635 -0
  326. docvortex/render/_internal/markdown/escaping.py +51 -0
  327. docvortex/render/_internal/markdown/inline.py +260 -0
  328. docvortex/render/_internal/markdown/renderer.py +93 -0
  329. docvortex/render/_internal/markdown/table.py +281 -0
  330. docvortex/render/_internal/pdf/__init__.py +3 -0
  331. docvortex/render/_internal/pdf/assets.py +197 -0
  332. docvortex/render/_internal/pdf/formula.py +417 -0
  333. docvortex/render/_internal/pdf/inline.py +343 -0
  334. docvortex/render/_internal/pdf/renderer.py +734 -0
  335. docvortex/render/_internal/pdf/styles.py +206 -0
  336. docvortex/render/_internal/pdf/table.py +272 -0
  337. docvortex/render/_internal/structured_content/__init__.py +3 -0
  338. docvortex/render/_internal/structured_content/renderer.py +193 -0
  339. docvortex/render/api.py +199 -0
  340. docvortex/render/contracts.py +205 -0
  341. docvortex/render/docx.py +38 -0
  342. docvortex/render/epub.py +35 -0
  343. docvortex/render/fragments.py +70 -0
  344. docvortex/render/html.py +29 -0
  345. docvortex/render/latex.py +24 -0
  346. docvortex/render/markdown.py +50 -0
  347. docvortex/render/pdf.py +25 -0
  348. docvortex/render/structured_content.py +23 -0
  349. docvortex/resources/epub/docvortex.css +91 -0
  350. docvortex/resources/fasttext-langdetect/lid.176.ftz +0 -0
  351. docvortex/resources/fonts/DroidSansFallbackFull.ttf +0 -0
  352. docvortex/resources/fonts/NOTICE +190 -0
  353. docvortex/resources/fonts/manifest.json +9 -0
  354. docvortex/resources/html/docvortex.css +601 -0
  355. docvortex/resources/html/docvortex.min.css +1 -0
  356. docvortex/result.py +91 -0
  357. docvortex/schema.py +1141 -0
  358. docvortex/version.py +3 -0
  359. docvortex-0.2.1.dist-info/METADATA +193 -0
  360. docvortex-0.2.1.dist-info/RECORD +364 -0
  361. docvortex-0.2.1.dist-info/WHEEL +5 -0
  362. docvortex-0.2.1.dist-info/entry_points.txt +2 -0
  363. docvortex-0.2.1.dist-info/licenses/LICENSE.md +21 -0
  364. docvortex-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,202 @@
1
+ """DOCX renderer 的安全图片素材解析与格式准备。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from io import BytesIO
7
+ from pathlib import PurePosixPath
8
+ import re
9
+ from urllib.parse import urlsplit
10
+
11
+ from PIL import Image, UnidentifiedImageError
12
+
13
+ from ...contracts import AssetResolver
14
+ from ....schema import ImagePayloadBlock
15
+ from ....foundation.image_payload import (
16
+ extract_generated_svg_fallback,
17
+ parse_image_data_uri_strict,
18
+ validate_image_sidecar_path,
19
+ )
20
+
21
+ _PIL_FORMAT_EXTENSIONS = {
22
+ "BMP": "bmp",
23
+ "GIF": "gif",
24
+ "JPEG": "jpg",
25
+ "PNG": "png",
26
+ "TIFF": "tiff",
27
+ "WEBP": "webp",
28
+ }
29
+ _EXTENSION_ALIASES = {
30
+ "jpeg": "jpg",
31
+ "jpe": "jpg",
32
+ "tif": "tiff",
33
+ }
34
+ _SVG_START_RE = re.compile(rb"^(?:\xef\xbb\xbf)?\s*(?:<\?xml\b.*?\?>\s*)?<svg\b", re.IGNORECASE | re.DOTALL)
35
+
36
+
37
+ class DocxAssetError(ValueError):
38
+ """表示 DOCX renderer 无法安全加载或识别图片素材。"""
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class PreparedImage:
43
+ """保存可直接交给 DOCX 的图片字节、格式与像素尺寸。"""
44
+
45
+ data: bytes
46
+ extension: str
47
+ width_px: int
48
+ height_px: int
49
+ svg_data: bytes | None = None
50
+
51
+
52
+ def prepare_block_image(
53
+ block: ImagePayloadBlock,
54
+ asset_resolver: AssetResolver | None = None,
55
+ ) -> PreparedImage:
56
+ """从图片载荷 block 加载图片,按 sidecar、data URI 的公共优先级选择。"""
57
+ if not isinstance(block, ImagePayloadBlock):
58
+ raise TypeError("block must be an ImagePayloadBlock")
59
+ if block.image_path is not None:
60
+ return _prepare_relative_asset(block.image_path, asset_resolver)
61
+ if block.image_base64 is not None:
62
+ return _prepare_data_uri(block.image_base64)
63
+ raise DocxAssetError("Image block does not contain image_base64 or image_path")
64
+
65
+
66
+ def prepare_html_image(
67
+ source: str,
68
+ asset_resolver: AssetResolver | None = None,
69
+ ) -> PreparedImage:
70
+ """从 HTML ``img`` 的 src 加载 data URI 或安全的相对 sidecar 图片。"""
71
+ if not isinstance(source, str):
72
+ raise TypeError("HTML image source must be a string")
73
+ normalized_source = source.strip()
74
+ if not normalized_source:
75
+ raise DocxAssetError("HTML image source must not be empty")
76
+ if normalized_source.lower().startswith("data:"):
77
+ if not normalized_source.lower().startswith("data:image/"):
78
+ raise DocxAssetError("Only image data URIs are supported")
79
+ return _prepare_data_uri(normalized_source)
80
+ return _prepare_relative_asset(normalized_source, asset_resolver)
81
+
82
+
83
+ def prepare_image_bytes(
84
+ data: bytes,
85
+ *,
86
+ declared_extension: str | None = None,
87
+ ) -> PreparedImage:
88
+ """严格解码图片字节,并将 Word 不直接支持的 WebP 在内存中转为 PNG。"""
89
+ if not isinstance(data, bytes):
90
+ raise DocxAssetError("Image resolver must return bytes")
91
+ if not data:
92
+ raise DocxAssetError("Image payload must not be empty")
93
+
94
+ expected_extension = _normalize_extension(declared_extension)
95
+ if expected_extension == "svg" or _looks_like_svg(data):
96
+ try:
97
+ fallback, logical_width, logical_height = extract_generated_svg_fallback(data)
98
+ except ValueError as exc:
99
+ raise DocxAssetError("SVG images are not supported unless they are safely generated by DocVortex") from exc
100
+ prepared = prepare_image_bytes(fallback, declared_extension="png")
101
+ return PreparedImage(
102
+ data=prepared.data,
103
+ extension=prepared.extension,
104
+ width_px=logical_width,
105
+ height_px=logical_height,
106
+ svg_data=data,
107
+ )
108
+
109
+ try:
110
+ with Image.open(BytesIO(data)) as image:
111
+ detected_extension = _PIL_FORMAT_EXTENSIONS.get((image.format or "").upper())
112
+ if detected_extension is None:
113
+ raise DocxAssetError(f"Unsupported image format: {image.format or 'unknown'}")
114
+ width_px, height_px = image.size
115
+ image.load()
116
+ if expected_extension is not None and expected_extension != detected_extension:
117
+ raise DocxAssetError(
118
+ f"Image bytes do not match declared format: expected {expected_extension}, detected {detected_extension}"
119
+ )
120
+ if detected_extension == "webp":
121
+ return _convert_webp_to_png(image, width_px, height_px)
122
+ except DocxAssetError:
123
+ raise
124
+ except (UnidentifiedImageError, OSError, ValueError) as exc:
125
+ raise DocxAssetError("Invalid or corrupted image payload") from exc
126
+
127
+ return PreparedImage(
128
+ data=data,
129
+ extension=detected_extension,
130
+ width_px=width_px,
131
+ height_px=height_px,
132
+ )
133
+
134
+
135
+ def _prepare_data_uri(data_uri: str) -> PreparedImage:
136
+ """严格解析 data URI,并校验 MIME、签名和完整图片数据。"""
137
+ try:
138
+ data, extension = parse_image_data_uri_strict(data_uri)
139
+ except ValueError as exc:
140
+ raise DocxAssetError(str(exc)) from exc
141
+ return prepare_image_bytes(data, declared_extension=extension)
142
+
143
+
144
+ def _prepare_relative_asset(
145
+ image_path: str,
146
+ asset_resolver: AssetResolver | None,
147
+ ) -> PreparedImage:
148
+ """校验相对 sidecar 路径,通过调用方 resolver 读取字节且不访问文件系统或网络。"""
149
+ parsed_source = urlsplit(image_path)
150
+ if parsed_source.scheme or parsed_source.netloc:
151
+ raise DocxAssetError(f"Remote or scheme-based image source is not supported: {image_path}")
152
+ try:
153
+ safe_path = validate_image_sidecar_path(image_path)
154
+ except ValueError as exc:
155
+ raise DocxAssetError(str(exc)) from exc
156
+ if asset_resolver is None:
157
+ raise DocxAssetError(f"asset_resolver is required for image_path: {safe_path}")
158
+ try:
159
+ data = asset_resolver(safe_path)
160
+ except Exception as exc:
161
+ raise DocxAssetError(f"Failed to resolve image asset: {safe_path}") from exc
162
+
163
+ extension = PurePosixPath(safe_path).suffix.removeprefix(".") or None
164
+ return prepare_image_bytes(data, declared_extension=extension)
165
+
166
+
167
+ def _normalize_extension(extension: str | None) -> str | None:
168
+ """规范化可识别的扩展名;未知扩展名不作为图片格式声明。"""
169
+ if extension is None:
170
+ return None
171
+ normalized = extension.lower().lstrip(".").split("+", 1)[0]
172
+ normalized = _EXTENSION_ALIASES.get(normalized, normalized)
173
+ if normalized == "svg":
174
+ return normalized
175
+ return normalized if normalized in set(_PIL_FORMAT_EXTENSIONS.values()) else None
176
+
177
+
178
+ def _looks_like_svg(data: bytes) -> bool:
179
+ """识别带可选 BOM/XML 声明的 SVG 字节,以便给出明确的不支持错误。"""
180
+ return _SVG_START_RE.match(data[:4096]) is not None
181
+
182
+
183
+ def _convert_webp_to_png(image: Image.Image, width_px: int, height_px: int) -> PreparedImage:
184
+ """在内存中把已解码 WebP 转换为 PNG,保留透明通道与像素尺寸。"""
185
+ output = BytesIO()
186
+ image.save(output, format="PNG")
187
+ return PreparedImage(
188
+ data=output.getvalue(),
189
+ extension="png",
190
+ width_px=width_px,
191
+ height_px=height_px,
192
+ )
193
+
194
+
195
+ __all__ = [
196
+ "AssetResolver",
197
+ "DocxAssetError",
198
+ "PreparedImage",
199
+ "prepare_block_image",
200
+ "prepare_html_image",
201
+ "prepare_image_bytes",
202
+ ]
@@ -0,0 +1,434 @@
1
+ """Middle JSON 2.0 行内 Span 到 Word run、超链接、书签和 OMML 的写入。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass
7
+ from hashlib import sha1
8
+ import re
9
+
10
+ from docx.opc.constants import RELATIONSHIP_TYPE
11
+ from docx.oxml import OxmlElement
12
+ from docx.oxml.ns import qn
13
+ from docx.text.paragraph import Paragraph
14
+ from docx.text.run import Run
15
+ from docx.shared import Pt, RGBColor
16
+ from lxml import etree
17
+ from loguru import logger
18
+
19
+ from ....content.inline import inline_plain_text, join_inline_spans
20
+ from ....schema import CodeInlineSpan, EquationInlineSpan, HyperlinkSpan, InlineSpan, TextSpan
21
+ from .math import DocxFormulaError, latex_to_omml
22
+
23
+ _BOOKMARK_SAFE_RE = re.compile(r"[^A-Za-z0-9_]+")
24
+ _BOOKMARK_MAX_LENGTH = 40
25
+ _INVALID_XML_TEXT_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\ud800-\udfff\ufffe\uffff]")
26
+ _BARE_SCRIPT_RE = re.compile(r"^\s*(?P<marker>[\^_])\s*\{(?P<content>[^{}]+)\}\s*$")
27
+ _VISIBLE_SPACE_STYLES = frozenset({"underline", "strikethrough", "emphasis"})
28
+ _NONBREAKING_SPACE = "\u00a0"
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class InlineRenderContext:
33
+ """保存行内渲染告警所需的 block 定位与书签表。"""
34
+
35
+ bookmarks: BookmarkRegistry
36
+ page_idx: int
37
+ block_index: int | None
38
+ block_type: str
39
+
40
+ def location(self) -> str:
41
+ """返回稳定、可读的 page/block 定位文本。"""
42
+ return f"page_idx={self.page_idx}, block_index={self.block_index}, block_type={self.block_type}"
43
+
44
+
45
+ class BookmarkRegistry:
46
+ """把 MiddleJson anchor 映射为合法且唯一的 Word bookmark 名称。"""
47
+
48
+ def __init__(self, anchors: Iterable[str]) -> None:
49
+ """预注册全部 anchor,保证标题、目录和脚注前向引用可提前解析。"""
50
+ self._names: dict[str, str] = {}
51
+ self._attached: set[str] = set()
52
+ self._used_names: set[str] = set()
53
+ self._next_id = 0
54
+ for anchor in anchors:
55
+ normalized = anchor.strip()
56
+ if normalized and normalized not in self._names:
57
+ self._names[normalized] = self._allocate_name(normalized)
58
+
59
+ def _allocate_name(self, anchor: str) -> str:
60
+ """为一个原始 anchor 分配满足 Word 限制的确定性名称。"""
61
+ base = _BOOKMARK_SAFE_RE.sub("_", anchor).strip("_")
62
+ if not base or not base[0].isalpha():
63
+ base = f"b_{base}"
64
+ candidate = base[:_BOOKMARK_MAX_LENGTH]
65
+ if candidate not in self._used_names:
66
+ self._used_names.add(candidate)
67
+ return candidate
68
+
69
+ digest = sha1(anchor.encode("utf-8")).hexdigest()[:8]
70
+ prefix_length = _BOOKMARK_MAX_LENGTH - len(digest) - 1
71
+ candidate = f"{base[:prefix_length]}_{digest}"
72
+ ordinal = 1
73
+ while candidate in self._used_names:
74
+ suffix = f"_{ordinal}"
75
+ candidate = f"{base[: _BOOKMARK_MAX_LENGTH - len(suffix)]}{suffix}"
76
+ ordinal += 1
77
+ self._used_names.add(candidate)
78
+ return candidate
79
+
80
+ def resolve(self, anchor: str | None) -> str | None:
81
+ """解析已注册 anchor;空值或未知 anchor 返回 None。"""
82
+ if not anchor:
83
+ return None
84
+ return self._names.get(anchor.strip())
85
+
86
+ def attach(self, paragraph: Paragraph, anchor: str | None) -> bool:
87
+ """把 anchor bookmark 包围当前段落内容;重复正文 anchor 只保留首个。"""
88
+ normalized = (anchor or "").strip()
89
+ name = self.resolve(normalized)
90
+ if name is None:
91
+ return False
92
+ if normalized in self._attached:
93
+ logger.warning("Duplicate DOCX bookmark anchor ignored: {}", normalized)
94
+ return False
95
+
96
+ bookmark_id = str(self._next_id)
97
+ self._next_id += 1
98
+ start = OxmlElement("w:bookmarkStart")
99
+ start.set(qn("w:id"), bookmark_id)
100
+ start.set(qn("w:name"), name)
101
+ end = OxmlElement("w:bookmarkEnd")
102
+ end.set(qn("w:id"), bookmark_id)
103
+
104
+ paragraph_xml = paragraph._p
105
+ insert_at = 1 if paragraph_xml.pPr is not None else 0
106
+ paragraph_xml.insert(insert_at, start)
107
+ paragraph_xml.append(end)
108
+ self._attached.add(normalized)
109
+ return True
110
+
111
+
112
+ def append_inline_content(
113
+ paragraph: Paragraph,
114
+ content: list[InlineSpan],
115
+ *,
116
+ context: InlineRenderContext,
117
+ ) -> None:
118
+ """把一段 MiddleJson 行内 Span 追加到 Word 段落。"""
119
+ append_inline_spans(paragraph, content, context=context)
120
+
121
+
122
+ def append_joined_inline_contents(
123
+ paragraph: Paragraph,
124
+ contents: list[list[InlineSpan]],
125
+ *,
126
+ context: InlineRenderContext,
127
+ ) -> None:
128
+ """按共享边界规则合并续段,并把结构化行内 Span 写入 Word。"""
129
+ append_inline_spans(paragraph, join_inline_spans(contents), context=context)
130
+
131
+
132
+ def append_inline_spans(
133
+ paragraph: Paragraph,
134
+ spans: list[InlineSpan],
135
+ *,
136
+ context: InlineRenderContext,
137
+ inherited_styles: tuple[str, ...] = (),
138
+ ) -> None:
139
+ """把行内 Span 写入段落,同时保留样式、链接与公式语义。"""
140
+ _append_spans_to_container(
141
+ paragraph._p,
142
+ paragraph,
143
+ spans,
144
+ context=context,
145
+ inherited_styles=inherited_styles,
146
+ hyperlink=False,
147
+ )
148
+
149
+
150
+ def append_internal_link(
151
+ paragraph: Paragraph,
152
+ label_spans: list[InlineSpan],
153
+ *,
154
+ anchor: str | None,
155
+ context: InlineRenderContext,
156
+ ) -> bool:
157
+ """把目录标签写为内部链接;目标缺失时退回普通行内内容。"""
158
+ bookmark_name = context.bookmarks.resolve(anchor)
159
+ if bookmark_name is None:
160
+ append_inline_spans(paragraph, label_spans, context=context)
161
+ if anchor:
162
+ logger.warning("Unmatched DOCX index anchor: {} ({})", anchor, context.location())
163
+ return False
164
+
165
+ hyperlink = OxmlElement("w:hyperlink")
166
+ hyperlink.set(qn("w:anchor"), bookmark_name)
167
+ hyperlink.set(qn("w:history"), "1")
168
+ paragraph._p.append(hyperlink)
169
+ _append_spans_to_container(
170
+ hyperlink,
171
+ paragraph,
172
+ label_spans,
173
+ context=context,
174
+ inherited_styles=(),
175
+ hyperlink=True,
176
+ )
177
+ return True
178
+
179
+
180
+ def _append_spans_to_container(
181
+ container: etree._Element,
182
+ paragraph: Paragraph,
183
+ spans: list[InlineSpan],
184
+ *,
185
+ context: InlineRenderContext,
186
+ inherited_styles: tuple[str, ...],
187
+ hyperlink: bool,
188
+ ) -> None:
189
+ """递归写入一个段落或 hyperlink XML 容器。"""
190
+ for span in spans:
191
+ if isinstance(span, TextSpan):
192
+ styles = tuple(dict.fromkeys((*inherited_styles, *span.styles)))
193
+ _append_text_run(
194
+ container,
195
+ paragraph,
196
+ span.content,
197
+ styles=styles,
198
+ hyperlink=hyperlink,
199
+ context=context,
200
+ )
201
+ continue
202
+ if isinstance(span, CodeInlineSpan):
203
+ run = _append_text_run(
204
+ container,
205
+ paragraph,
206
+ span.content,
207
+ styles=inherited_styles,
208
+ hyperlink=hyperlink,
209
+ context=context,
210
+ )
211
+ run.font.name = "Courier New"
212
+ run.font.size = Pt(9)
213
+ continue
214
+ if isinstance(span, EquationInlineSpan):
215
+ bare_script = _BARE_SCRIPT_RE.fullmatch(span.content)
216
+ if bare_script is not None:
217
+ script_style = "superscript" if bare_script.group("marker") == "^" else "subscript"
218
+ styles = tuple(dict.fromkeys((*inherited_styles, script_style)))
219
+ _append_text_run(
220
+ container,
221
+ paragraph,
222
+ bare_script.group("content"),
223
+ styles=styles,
224
+ hyperlink=hyperlink,
225
+ context=context,
226
+ )
227
+ continue
228
+ if hyperlink:
229
+ _append_text_run(
230
+ container,
231
+ paragraph,
232
+ span.content,
233
+ styles=inherited_styles,
234
+ hyperlink=True,
235
+ context=context,
236
+ )
237
+ continue
238
+ try:
239
+ container.append(latex_to_omml(span.content, display=False))
240
+ except DocxFormulaError as exc:
241
+ logger.warning("DOCX inline formula fallback: {} ({})", exc, context.location())
242
+ _append_text_run(
243
+ container,
244
+ paragraph,
245
+ span.content,
246
+ styles=inherited_styles,
247
+ hyperlink=False,
248
+ formula_fallback=True,
249
+ context=context,
250
+ )
251
+ continue
252
+ if isinstance(span, HyperlinkSpan):
253
+ if hyperlink or not span.url or span.url == ".":
254
+ _append_spans_to_container(
255
+ container,
256
+ paragraph,
257
+ list(span.content),
258
+ context=context,
259
+ inherited_styles=inherited_styles,
260
+ hyperlink=hyperlink,
261
+ )
262
+ continue
263
+ if span.url.startswith("#"):
264
+ _append_inline_internal_link(
265
+ container,
266
+ paragraph,
267
+ span,
268
+ context=context,
269
+ inherited_styles=inherited_styles,
270
+ )
271
+ continue
272
+ _append_external_link(
273
+ paragraph,
274
+ span,
275
+ context=context,
276
+ inherited_styles=inherited_styles,
277
+ )
278
+ continue
279
+ raise TypeError(f"Unsupported inline span: {type(span).__name__}")
280
+
281
+
282
+ def _append_inline_internal_link(
283
+ container: etree._Element,
284
+ paragraph: Paragraph,
285
+ span: HyperlinkSpan,
286
+ *,
287
+ context: InlineRenderContext,
288
+ inherited_styles: tuple[str, ...],
289
+ ) -> None:
290
+ """把 #anchor 行内链接写为 Word bookmark 跳转,未知目标退化为普通文本。"""
291
+ anchor = span.url[1:].strip()
292
+ bookmark_name = context.bookmarks.resolve(anchor)
293
+ if bookmark_name is None:
294
+ _append_spans_to_container(
295
+ container,
296
+ paragraph,
297
+ list(span.content),
298
+ context=context,
299
+ inherited_styles=inherited_styles,
300
+ hyperlink=False,
301
+ )
302
+ return
303
+
304
+ internal_link = OxmlElement("w:hyperlink")
305
+ internal_link.set(qn("w:anchor"), bookmark_name)
306
+ internal_link.set(qn("w:history"), "1")
307
+ container.append(internal_link)
308
+ _append_spans_to_container(
309
+ internal_link,
310
+ paragraph,
311
+ list(span.content),
312
+ context=context,
313
+ inherited_styles=inherited_styles,
314
+ hyperlink=True,
315
+ )
316
+
317
+
318
+ def _append_external_link(
319
+ paragraph: Paragraph,
320
+ span: HyperlinkSpan,
321
+ *,
322
+ context: InlineRenderContext,
323
+ inherited_styles: tuple[str, ...],
324
+ ) -> None:
325
+ """创建外部 hyperlink relationship,并写入完整标签内容。"""
326
+ relation_id = paragraph.part.relate_to(
327
+ sanitize_xml_text(span.url, context=context),
328
+ RELATIONSHIP_TYPE.HYPERLINK,
329
+ is_external=True,
330
+ )
331
+ hyperlink = OxmlElement("w:hyperlink")
332
+ hyperlink.set(qn("r:id"), relation_id)
333
+ paragraph._p.append(hyperlink)
334
+ _append_spans_to_container(
335
+ hyperlink,
336
+ paragraph,
337
+ list(span.content),
338
+ context=context,
339
+ inherited_styles=inherited_styles,
340
+ hyperlink=True,
341
+ )
342
+
343
+
344
+ def _append_text_run(
345
+ container: etree._Element,
346
+ paragraph: Paragraph,
347
+ content: str,
348
+ *,
349
+ styles: tuple[str, ...],
350
+ hyperlink: bool,
351
+ context: InlineRenderContext,
352
+ formula_fallback: bool = False,
353
+ ) -> Run:
354
+ """向 XML 容器追加一个 run,并应用 MiddleJson 行内样式。"""
355
+ run_xml = OxmlElement("w:r")
356
+ container.append(run_xml)
357
+ run = Run(run_xml, paragraph)
358
+ sanitized = sanitize_xml_text(content, context=context)
359
+ run.text = _make_visible_style_spaces(sanitized, styles)
360
+ _apply_run_styles(run, styles)
361
+ if hyperlink:
362
+ run.font.color.rgb = _rgb_color("0563C1")
363
+ run.underline = True
364
+ if formula_fallback:
365
+ run.font.name = "Courier New"
366
+ run.font.size = Pt(9)
367
+ run.font.color.rgb = _rgb_color("555555")
368
+ return run
369
+
370
+
371
+ def _make_visible_style_spaces(content: str, styles: tuple[str, ...]) -> str:
372
+ """把可见样式 run 的边界 ASCII 空格等量转为 NBSP,避免 Word 隐藏装饰线。"""
373
+ if not content or not _VISIBLE_SPACE_STYLES.intersection(styles):
374
+ return content
375
+
376
+ leading_count = len(content) - len(content.lstrip(" "))
377
+ trailing_count = len(content) - len(content.rstrip(" "))
378
+ if leading_count == 0 and trailing_count == 0:
379
+ return content
380
+ if leading_count + trailing_count >= len(content):
381
+ return _NONBREAKING_SPACE * len(content)
382
+
383
+ content_end = len(content) - trailing_count if trailing_count else len(content)
384
+ return _NONBREAKING_SPACE * leading_count + content[leading_count:content_end] + _NONBREAKING_SPACE * trailing_count
385
+
386
+
387
+ def sanitize_xml_text(content: str, *, context: InlineRenderContext) -> str:
388
+ """把 XML 1.0 禁止字符替换为 U+FFFD,并记录可定位的 renderer 告警。"""
389
+ sanitized, replacement_count = _INVALID_XML_TEXT_RE.subn("\ufffd", content)
390
+ if replacement_count:
391
+ logger.warning(
392
+ "DOCX replaced {} XML-incompatible character(s) with U+FFFD ({})",
393
+ replacement_count,
394
+ context.location(),
395
+ )
396
+ return sanitized
397
+
398
+
399
+ def _apply_run_styles(run: Run, styles: tuple[str, ...]) -> None:
400
+ """把已解析的行内样式应用到 Word run。"""
401
+ style_set = set(styles)
402
+ run.bold = "bold" in style_set
403
+ run.italic = "italic" in style_set
404
+ run.underline = "underline" in style_set
405
+ run.font.strike = "strikethrough" in style_set
406
+ run.font.superscript = "superscript" in style_set
407
+ run.font.subscript = "subscript" in style_set and "superscript" not in style_set
408
+ if "emphasis" in style_set:
409
+ run_properties = run._element.get_or_add_rPr()
410
+ emphasis = OxmlElement("w:em")
411
+ emphasis.set(qn("w:val"), "underDot")
412
+ run_properties.append(emphasis)
413
+
414
+
415
+ def _rgb_color(value: str) -> RGBColor:
416
+ """延迟构造 RGBColor,避免在模块常量阶段加载额外对象。"""
417
+ return RGBColor.from_string(value)
418
+
419
+
420
+ def plain_inline_text(content: list[InlineSpan]) -> str:
421
+ """提取一段 MiddleJson 行内内容的可见文本。"""
422
+ return inline_plain_text(content)
423
+
424
+
425
+ __all__ = [
426
+ "BookmarkRegistry",
427
+ "InlineRenderContext",
428
+ "append_inline_content",
429
+ "append_inline_spans",
430
+ "append_internal_link",
431
+ "append_joined_inline_contents",
432
+ "plain_inline_text",
433
+ "sanitize_xml_text",
434
+ ]