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,197 @@
1
+ """PDF 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
+ MAX_IMAGE_PAYLOAD_BYTES,
17
+ extract_generated_svg_fallback,
18
+ parse_image_data_uri_strict,
19
+ validate_decoded_raster_size,
20
+ validate_image_sidecar_path,
21
+ )
22
+
23
+ _PIL_FORMAT_EXTENSIONS = {
24
+ "BMP": "bmp",
25
+ "GIF": "gif",
26
+ "JPEG": "jpg",
27
+ "PNG": "png",
28
+ "TIFF": "tiff",
29
+ "WEBP": "webp",
30
+ }
31
+ _EXTENSION_ALIASES = {"jpeg": "jpg", "jpe": "jpg", "tif": "tiff"}
32
+ _SVG_START_RE = re.compile(rb"^(?:\xef\xbb\xbf)?\s*(?:<\?xml\b.*?\?>\s*)?<svg\b", re.IGNORECASE | re.DOTALL)
33
+
34
+
35
+ class PdfAssetError(ValueError):
36
+ """表示 PDF renderer 无法离线安全加载或识别图片素材。"""
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class PreparedImage:
41
+ """保存可直接交给 ReportLab 的图片字节与像素尺寸。"""
42
+
43
+ data: bytes
44
+ extension: str
45
+ width_px: int
46
+ height_px: int
47
+
48
+
49
+ def prepare_block_image(
50
+ block: ImagePayloadBlock,
51
+ asset_resolver: AssetResolver | None = None,
52
+ ) -> PreparedImage:
53
+ """按 sidecar、data URI 的固定优先级加载一个图片载荷 block。"""
54
+ if not isinstance(block, ImagePayloadBlock):
55
+ raise TypeError("block must be an ImagePayloadBlock")
56
+ if block.image_path is not None:
57
+ return _prepare_relative_asset(block.image_path, asset_resolver)
58
+ if block.image_base64 is not None:
59
+ return _prepare_data_uri(block.image_base64)
60
+ if block.image_url is not None:
61
+ raise PdfAssetError(f"Remote image is not downloaded: {block.image_url}")
62
+ raise PdfAssetError("Image block does not contain an image source")
63
+
64
+
65
+ def prepare_html_image(
66
+ source: str,
67
+ asset_resolver: AssetResolver | None = None,
68
+ ) -> PreparedImage:
69
+ """从 HTML img 的 data URI 或安全相对 sidecar 加载图片。"""
70
+ if not isinstance(source, str):
71
+ raise TypeError("HTML image source must be a string")
72
+ normalized = source.strip()
73
+ if not normalized:
74
+ raise PdfAssetError("HTML image source must not be empty")
75
+ if normalized.lower().startswith("data:"):
76
+ if not normalized.lower().startswith("data:image/"):
77
+ raise PdfAssetError("Only image data URIs are supported")
78
+ return _prepare_data_uri(normalized)
79
+ parsed = urlsplit(normalized)
80
+ if parsed.scheme or parsed.netloc:
81
+ raise PdfAssetError(f"Remote HTML image is not downloaded: {normalized}")
82
+ return _prepare_relative_asset(normalized, asset_resolver)
83
+
84
+
85
+ def prepare_image_bytes(data: bytes, *, declared_extension: str | None = None) -> PreparedImage:
86
+ """严格解码图片字节,并把 WebP 与安全 SVG fallback 转成 PNG。"""
87
+ if not isinstance(data, bytes):
88
+ raise PdfAssetError("Image resolver must return bytes")
89
+ if not data:
90
+ raise PdfAssetError("Image payload must not be empty")
91
+ if len(data) > MAX_IMAGE_PAYLOAD_BYTES:
92
+ raise PdfAssetError("Image payload exceeds its byte limit")
93
+ expected_extension = _normalize_extension(declared_extension)
94
+ if expected_extension == "svg" or _looks_like_svg(data):
95
+ try:
96
+ fallback, logical_width, logical_height = extract_generated_svg_fallback(data)
97
+ except ValueError as exc:
98
+ raise PdfAssetError("SVG images require a validated DocVortex PNG fallback") from exc
99
+ prepared = prepare_image_bytes(fallback, declared_extension="png")
100
+ return PreparedImage(
101
+ data=prepared.data,
102
+ extension="png",
103
+ width_px=logical_width,
104
+ height_px=logical_height,
105
+ )
106
+ try:
107
+ with Image.open(BytesIO(data)) as image:
108
+ detected_extension = _PIL_FORMAT_EXTENSIONS.get((image.format or "").upper())
109
+ if detected_extension is None:
110
+ raise PdfAssetError(f"Unsupported image format: {image.format or 'unknown'}")
111
+ width_px, height_px = image.size
112
+ try:
113
+ validate_decoded_raster_size(width_px, height_px)
114
+ except ValueError as exc:
115
+ raise PdfAssetError(str(exc)) from exc
116
+ image.load()
117
+ if expected_extension is not None and expected_extension != detected_extension:
118
+ raise PdfAssetError(
119
+ f"Image bytes do not match declared format: expected {expected_extension}, detected {detected_extension}"
120
+ )
121
+ if detected_extension == "webp":
122
+ return _convert_webp_to_png(image, width_px, height_px)
123
+ except PdfAssetError:
124
+ raise
125
+ except (Image.DecompressionBombError, UnidentifiedImageError, OSError, ValueError) as exc:
126
+ raise PdfAssetError("Invalid or corrupted image payload") from exc
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 PdfAssetError(str(exc)) from exc
141
+ return prepare_image_bytes(data, declared_extension=extension)
142
+
143
+
144
+ def _prepare_relative_asset(image_path: str, asset_resolver: AssetResolver | None) -> PreparedImage:
145
+ """校验相对 sidecar 路径,并仅通过显式 resolver 获取字节。"""
146
+ parsed = urlsplit(image_path)
147
+ if parsed.scheme or parsed.netloc:
148
+ raise PdfAssetError(f"Remote or scheme-based image source is not supported: {image_path}")
149
+ try:
150
+ safe_path = validate_image_sidecar_path(image_path)
151
+ except ValueError as exc:
152
+ raise PdfAssetError(str(exc)) from exc
153
+ if asset_resolver is None:
154
+ raise PdfAssetError(f"asset_resolver is required for image_path: {safe_path}")
155
+ try:
156
+ data = asset_resolver(safe_path)
157
+ except Exception as exc:
158
+ raise PdfAssetError(f"Failed to resolve image asset: {safe_path}") from exc
159
+ extension = PurePosixPath(safe_path).suffix.removeprefix(".") or None
160
+ return prepare_image_bytes(data, declared_extension=extension)
161
+
162
+
163
+ def _normalize_extension(extension: str | None) -> str | None:
164
+ """规范化可识别扩展名,未知扩展不作为签名声明。"""
165
+ if extension is None:
166
+ return None
167
+ normalized = extension.lower().lstrip(".").split("+", 1)[0]
168
+ normalized = _EXTENSION_ALIASES.get(normalized, normalized)
169
+ if normalized == "svg":
170
+ return normalized
171
+ return normalized if normalized in set(_PIL_FORMAT_EXTENSIONS.values()) else None
172
+
173
+
174
+ def _looks_like_svg(data: bytes) -> bool:
175
+ """识别带可选 BOM 与 XML 声明的 SVG 字节。"""
176
+ return _SVG_START_RE.match(data[:4096]) is not None
177
+
178
+
179
+ def _convert_webp_to_png(image: Image.Image, width_px: int, height_px: int) -> PreparedImage:
180
+ """在内存中把已解码 WebP 转成 ReportLab 可稳定读取的 PNG。"""
181
+ output = BytesIO()
182
+ image.save(output, format="PNG")
183
+ return PreparedImage(
184
+ data=output.getvalue(),
185
+ extension="png",
186
+ width_px=width_px,
187
+ height_px=height_px,
188
+ )
189
+
190
+
191
+ __all__ = [
192
+ "PdfAssetError",
193
+ "PreparedImage",
194
+ "prepare_block_image",
195
+ "prepare_html_image",
196
+ "prepare_image_bytes",
197
+ ]
@@ -0,0 +1,417 @@
1
+ """ZiaMath LaTeX 到受控 ReportLab 矢量路径的转换。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import re
7
+ from threading import RLock
8
+ from typing import Any
9
+ from xml.etree import ElementTree
10
+
11
+ from fontTools.pens.basePen import BasePen
12
+ from fontTools.svgLib.path import parse_path
13
+ from reportlab.graphics.shapes import Drawing, Group, Path, Rect
14
+ from reportlab.lib.colors import Color, toColor
15
+ from reportlab.platypus import Flowable
16
+ import ziamath
17
+
18
+ MAX_FORMULA_CHARACTERS = 20_000
19
+ MAX_CACHED_FORMULAS = 512
20
+ _MAX_SVG_NODES = 20_000
21
+ _MAX_SVG_PATH_CHARACTERS = 20_000_000
22
+ _SVG_NAMESPACE = "http://www.w3.org/2000/svg"
23
+ _ZIAMATH_LOCK = RLock()
24
+ _SVG_PATH_COMMAND_RE = re.compile(r"[A-DF-Za-df-z]")
25
+
26
+
27
+ class PdfFormulaError(ValueError):
28
+ """表示 LaTeX 或 ZiaMath SVG 无法安全转换为 PDF 矢量对象。"""
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class FormulaVector:
33
+ """保存一个公式的 ReportLab Drawing 与基线几何。"""
34
+
35
+ drawing: Drawing
36
+ width: float
37
+ height: float
38
+ ascent: float
39
+ descent: float
40
+
41
+ def scaled(self, factor: float) -> FormulaVector:
42
+ """返回仅调整展示几何、不复制 Drawing 的等比公式对象。"""
43
+ return FormulaVector(
44
+ drawing=self.drawing,
45
+ width=self.width * factor,
46
+ height=self.height * factor,
47
+ ascent=self.ascent * factor,
48
+ descent=self.descent * factor,
49
+ )
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class InlineFormulaImage:
54
+ """作为 ReportLab Paragraph 行内图片占位的矢量公式代理。"""
55
+
56
+ vector: FormulaVector
57
+
58
+
59
+ class FormulaRenderer:
60
+ """维护单份 PDF 文档内有界且无跨文档状态的公式缓存。"""
61
+
62
+ def __init__(self) -> None:
63
+ """初始化最多缓存 512 个唯一公式的文档级缓存。"""
64
+ self._cache: dict[tuple[str, bool, float, str], FormulaVector] = {}
65
+
66
+ def render(
67
+ self,
68
+ latex: str,
69
+ *,
70
+ inline: bool,
71
+ font_size: float,
72
+ color: str = "#1f2937",
73
+ ) -> FormulaVector:
74
+ """把一个裸 LaTeX 公式转换为行内或行间矢量对象。"""
75
+ if not isinstance(latex, str) or not latex.strip():
76
+ raise PdfFormulaError("formula must contain non-blank LaTeX")
77
+ if not isinstance(font_size, (int, float)) or isinstance(font_size, bool) or font_size <= 0:
78
+ raise PdfFormulaError("font_size must be a positive number")
79
+ if len(latex) > MAX_FORMULA_CHARACTERS:
80
+ raise PdfFormulaError(f"formula exceeds max_formula_characters={MAX_FORMULA_CHARACTERS}")
81
+
82
+ key = (latex, inline, float(font_size), color)
83
+ if key in self._cache:
84
+ return self._cache[key]
85
+
86
+ vector = _render_ziamath_formula(latex, inline=inline, font_size=float(font_size), color=color)
87
+ if len(self._cache) < MAX_CACHED_FORMULAS:
88
+ self._cache[key] = vector
89
+ return vector
90
+
91
+
92
+ class DisplayFormulaFlowable(Flowable):
93
+ """在可用行宽内居中绘制公式,并把可选编号贴到右边界。"""
94
+
95
+ def __init__(self, formula: FormulaVector, tag: FormulaVector | None = None) -> None:
96
+ """保存公式、可选编号以及延迟到 wrap 阶段计算的缩放参数。"""
97
+ super().__init__()
98
+ self.formula = formula
99
+ self.tag = tag
100
+ self._available_width = formula.width
101
+ self._formula_scale = 1.0
102
+ self._tag_scale = 1.0
103
+ self.width = formula.width
104
+ self.height = formula.height
105
+
106
+ def wrap(self, avail_width: float, _avail_height: float) -> tuple[float, float]:
107
+ """按正文宽度缩放主公式和编号,避免二者重叠或水平裁切。"""
108
+ self._available_width = max(1.0, avail_width)
109
+ tag_width = self.tag.width if self.tag is not None else 0.0
110
+ tag_gap = 10.0 if self.tag is not None else 0.0
111
+ main_limit = self._available_width if self.tag is None else max(1.0, self._available_width - tag_width - tag_gap)
112
+ self._formula_scale = min(1.0, main_limit / max(self.formula.width, 1.0))
113
+ if self.tag is not None and tag_width > self._available_width * 0.25:
114
+ self._tag_scale = min(1.0, self._available_width * 0.25 / max(tag_width, 1.0))
115
+ main_limit = max(1.0, self._available_width - tag_width * self._tag_scale - tag_gap)
116
+ self._formula_scale = min(self._formula_scale, main_limit / max(self.formula.width, 1.0))
117
+ formula_height = self.formula.height * self._formula_scale
118
+ tag_height = self.tag.height * self._tag_scale if self.tag is not None else 0.0
119
+ self.width = self._available_width
120
+ self.height = max(formula_height, tag_height, 1.0)
121
+ return self.width, self.height
122
+
123
+ def draw(self) -> None:
124
+ """把缩放后的主公式居中,并在同一垂直中心绘制右侧编号。"""
125
+ formula_width = self.formula.width * self._formula_scale
126
+ formula_height = self.formula.height * self._formula_scale
127
+ formula_x = max(0.0, (self._available_width - formula_width) / 2)
128
+ formula_y = max(0.0, (self.height - formula_height) / 2)
129
+ _draw_vector(self.canv, self.formula, formula_x, formula_y, self._formula_scale)
130
+ if self.tag is not None:
131
+ tag_width = self.tag.width * self._tag_scale
132
+ tag_height = self.tag.height * self._tag_scale
133
+ tag_x = max(0.0, self._available_width - tag_width)
134
+ tag_y = max(0.0, (self.height - tag_height) / 2)
135
+ _draw_vector(self.canv, self.tag, tag_x, tag_y, self._tag_scale)
136
+
137
+
138
+ class _ReportLabPathPen(BasePen):
139
+ """把 FontTools SVG path 回调写入 ReportLab Path。"""
140
+
141
+ def __init__(self) -> None:
142
+ """创建不依赖 glyphSet 的空 ReportLab 路径。"""
143
+ super().__init__(None)
144
+ self.path = Path()
145
+
146
+ def _moveTo(self, point: tuple[float, float]) -> None:
147
+ """把 SVG move 命令写入目标路径。"""
148
+ self.path.moveTo(*point)
149
+
150
+ def _lineTo(self, point: tuple[float, float]) -> None:
151
+ """把 SVG line 命令写入目标路径。"""
152
+ self.path.lineTo(*point)
153
+
154
+ def _curveToOne(
155
+ self,
156
+ point1: tuple[float, float],
157
+ point2: tuple[float, float],
158
+ point3: tuple[float, float],
159
+ ) -> None:
160
+ """把三次曲线写入目标路径,二次曲线由 BasePen 自动转换。"""
161
+ self.path.curveTo(*point1, *point2, *point3)
162
+
163
+ def _closePath(self) -> None:
164
+ """闭合当前 ReportLab 子路径。"""
165
+ self.path.closePath()
166
+
167
+ def _endPath(self) -> None:
168
+ """结束不闭合的 SVG 子路径。"""
169
+
170
+
171
+ def split_formula_tag(content: str) -> tuple[str, str | None]:
172
+ """剥离公式末尾括号平衡的 ``\\tag{...}``,并返回正文与编号。"""
173
+ stripped_end = len(content.rstrip())
174
+ if stripped_end == 0 or content[stripped_end - 1] != "}":
175
+ return content, None
176
+ search_end = stripped_end
177
+ while (tag_start := content.rfind(r"\tag", 0, search_end)) >= 0:
178
+ if not _is_escaped_character(content, tag_start):
179
+ opening_brace = _find_tag_opening_brace(content, tag_start, stripped_end)
180
+ if opening_brace is not None:
181
+ closing_brace = _find_balanced_closing_brace(content, opening_brace, stripped_end)
182
+ if closing_brace == stripped_end - 1:
183
+ return content[:tag_start].rstrip(), content[opening_brace + 1 : closing_brace].strip()
184
+ search_end = tag_start
185
+ return content, None
186
+
187
+
188
+ def draw_inline_formula(
189
+ canvas: Any,
190
+ image: InlineFormulaImage,
191
+ x: float,
192
+ y: float,
193
+ width: float,
194
+ height: float,
195
+ ) -> tuple[float, float]:
196
+ """由自定义 Canvas 在 Paragraph 计算的位置绘制一个行内矢量公式。"""
197
+ vector = image.vector
198
+ scale = min(width / max(vector.width, 1.0), height / max(vector.height, 1.0))
199
+ _draw_vector(canvas, vector, x, y, scale)
200
+ return width, height
201
+
202
+
203
+ def _render_ziamath_formula(latex: str, *, inline: bool, font_size: float, color: str) -> FormulaVector:
204
+ """在全局锁内临时关闭 SVG2 symbols,并转换单个 ZiaMath 结果。"""
205
+ try:
206
+ with _ZIAMATH_LOCK:
207
+ previous_svg2 = ziamath.config.svg2
208
+ ziamath.config.svg2 = False
209
+ try:
210
+ formula = ziamath.Latex(latex, inline=inline, size=font_size, color=color, margin=0)
211
+ root = formula.svgxml()
212
+ finally:
213
+ ziamath.config.svg2 = previous_svg2
214
+ return _svg_root_to_vector(root)
215
+ except PdfFormulaError:
216
+ raise
217
+ except Exception as exc:
218
+ raise PdfFormulaError(f"LaTeX formula cannot be rendered: {_formula_preview(latex)!r}") from exc
219
+
220
+
221
+ def _svg_root_to_vector(root: ElementTree.Element) -> FormulaVector:
222
+ """把 ZiaMath 的固定 SVG 子集转换为坐标已翻转的 ReportLab Drawing。"""
223
+ namespace = root.get("xmlns") if root.tag == "svg" else root.tag.removeprefix("{").split("}", 1)[0]
224
+ if _local_name(root.tag) != "svg" or namespace != _SVG_NAMESPACE:
225
+ raise PdfFormulaError("ZiaMath output must contain an SVG root")
226
+ view_box = _parse_number_list(root.get("viewBox"), count=4, field="viewBox")
227
+ min_x, min_y, width, height = view_box
228
+ if width <= 0 or height <= 0:
229
+ raise PdfFormulaError("ZiaMath SVG dimensions must be positive")
230
+ drawing = Drawing(width, height)
231
+ root_group = Group()
232
+ root_group.transform = (1, 0, 0, -1, -min_x, min_y + height)
233
+ node_counter = [0]
234
+ path_budget = [0]
235
+ for child in root:
236
+ _append_svg_element(
237
+ root_group,
238
+ child,
239
+ inherited={},
240
+ node_counter=node_counter,
241
+ path_budget=path_budget,
242
+ )
243
+ drawing.add(root_group)
244
+ ascent = max(0.0, -min_y)
245
+ descent = max(0.0, min_y + height)
246
+ return FormulaVector(drawing=drawing, width=width, height=height, ascent=ascent, descent=descent)
247
+
248
+
249
+ def _append_svg_element(
250
+ parent: Group,
251
+ element: ElementTree.Element,
252
+ *,
253
+ inherited: dict[str, str],
254
+ node_counter: list[int],
255
+ path_budget: list[int],
256
+ ) -> None:
257
+ """递归转换 ZiaMath 允许的 group、path 与 rect 节点。"""
258
+ node_counter[0] += 1
259
+ if node_counter[0] > _MAX_SVG_NODES:
260
+ raise PdfFormulaError("ZiaMath SVG exceeds its node limit")
261
+ tag = _local_name(element.tag)
262
+ if tag == "g":
263
+ _reject_unknown_attributes(element, {"fill", "stroke", "stroke-width"})
264
+ group_style = {**inherited, **element.attrib}
265
+ group = Group()
266
+ for child in element:
267
+ _append_svg_element(
268
+ group,
269
+ child,
270
+ inherited=group_style,
271
+ node_counter=node_counter,
272
+ path_budget=path_budget,
273
+ )
274
+ parent.add(group)
275
+ return
276
+ if tag == "path":
277
+ _reject_unknown_attributes(element, {"d", "fill", "stroke", "stroke-width"})
278
+ path_data = element.get("d", "")
279
+ path_budget[0] += len(path_data)
280
+ if not path_data or path_budget[0] > _MAX_SVG_PATH_CHARACTERS:
281
+ raise PdfFormulaError("ZiaMath SVG path data is empty or exceeds its limit")
282
+ commands = set(_SVG_PATH_COMMAND_RE.findall(path_data))
283
+ if not commands.issubset({"M", "L", "Q", "Z"}):
284
+ raise PdfFormulaError(f"Unsupported ZiaMath SVG path commands: {', '.join(sorted(commands))}")
285
+ pen = _ReportLabPathPen()
286
+ try:
287
+ parse_path(path_data, pen)
288
+ except Exception as exc:
289
+ raise PdfFormulaError("ZiaMath SVG path data is invalid") from exc
290
+ _apply_paint(pen.path, element.attrib, inherited)
291
+ parent.add(pen.path)
292
+ return
293
+ if tag == "rect":
294
+ _reject_unknown_attributes(element, {"x", "y", "width", "height", "fill", "stroke", "stroke-width"})
295
+ x, y, width, height = (_parse_number(element.get(name, "0"), field=name) for name in ("x", "y", "width", "height"))
296
+ if width < 0 or height < 0:
297
+ raise PdfFormulaError("ZiaMath SVG rect dimensions must not be negative")
298
+ rectangle = Rect(x, y, width, height)
299
+ _apply_paint(rectangle, element.attrib, inherited)
300
+ parent.add(rectangle)
301
+ return
302
+ raise PdfFormulaError(f"Unsupported ZiaMath SVG element: {tag}")
303
+
304
+
305
+ def _apply_paint(shape: Any, attributes: dict[str, str], inherited: dict[str, str]) -> None:
306
+ """把受控 SVG fill、stroke 与 stroke-width 映射到 ReportLab shape。"""
307
+ fill = attributes.get("fill", inherited.get("fill", "black"))
308
+ stroke = attributes.get("stroke", inherited.get("stroke", "none"))
309
+ shape.fillColor = _parse_color(fill)
310
+ shape.strokeColor = _parse_color(stroke)
311
+ stroke_width = attributes.get("stroke-width", inherited.get("stroke-width", "1"))
312
+ shape.strokeWidth = _parse_number(stroke_width, field="stroke-width")
313
+
314
+
315
+ def _parse_color(value: str) -> Color | None:
316
+ """解析 ZiaMath 生成的静态颜色,none 映射为透明。"""
317
+ if value.strip().casefold() == "none":
318
+ return None
319
+ try:
320
+ return toColor(value)
321
+ except Exception as exc:
322
+ raise PdfFormulaError(f"Unsupported ZiaMath SVG color: {value!r}") from exc
323
+
324
+
325
+ def _parse_number(value: str, *, field: str) -> float:
326
+ """严格读取不含 CSS 单位的有限 SVG 数值。"""
327
+ try:
328
+ number = float(value)
329
+ except (TypeError, ValueError) as exc:
330
+ raise PdfFormulaError(f"Invalid ZiaMath SVG {field}: {value!r}") from exc
331
+ if number != number or number in (float("inf"), float("-inf")):
332
+ raise PdfFormulaError(f"Invalid ZiaMath SVG {field}: {value!r}")
333
+ return number
334
+
335
+
336
+ def _parse_number_list(value: str | None, *, count: int, field: str) -> tuple[float, ...]:
337
+ """读取固定长度的空白或逗号分隔 SVG 数值列表。"""
338
+ if value is None:
339
+ raise PdfFormulaError(f"ZiaMath SVG is missing {field}")
340
+ values = tuple(_parse_number(item, field=field) for item in value.replace(",", " ").split())
341
+ if len(values) != count:
342
+ raise PdfFormulaError(f"ZiaMath SVG {field} must contain {count} numbers")
343
+ return values
344
+
345
+
346
+ def _reject_unknown_attributes(element: ElementTree.Element, allowed: set[str]) -> None:
347
+ """拒绝 ZiaMath 固定子集之外的 SVG 属性。"""
348
+ unexpected = set(element.attrib) - allowed
349
+ if unexpected:
350
+ raise PdfFormulaError(f"Unsupported ZiaMath SVG attributes: {', '.join(sorted(unexpected))}")
351
+
352
+
353
+ def _local_name(tag: str) -> str:
354
+ """返回可带 XML namespace 的元素本地名称。"""
355
+ return tag.rsplit("}", 1)[-1]
356
+
357
+
358
+ def _formula_preview(latex: str) -> str:
359
+ """为告警生成有界 LaTeX 摘要,避免超长公式污染日志。"""
360
+ return latex if len(latex) <= 200 else f"{latex[:197]}..."
361
+
362
+
363
+ def _draw_vector(canvas: Any, vector: FormulaVector, x: float, y: float, scale: float) -> None:
364
+ """在 canvas 上按给定位置和比例绘制公式 Drawing。"""
365
+ canvas.saveState()
366
+ try:
367
+ canvas.translate(x, y)
368
+ canvas.scale(scale, scale)
369
+ vector.drawing.drawOn(canvas, 0, 0)
370
+ finally:
371
+ canvas.restoreState()
372
+
373
+
374
+ def _find_tag_opening_brace(content: str, tag_start: int, content_end: int) -> int | None:
375
+ """查找 tag 命令允许空白后的左花括号。"""
376
+ cursor = tag_start + len(r"\tag")
377
+ while cursor < content_end and content[cursor].isspace():
378
+ cursor += 1
379
+ return cursor if cursor < content_end and content[cursor] == "{" else None
380
+
381
+
382
+ def _find_balanced_closing_brace(content: str, opening_brace: int, content_end: int) -> int | None:
383
+ """查找与 tag 左花括号配对的右花括号,并忽略转义花括号。"""
384
+ depth = 0
385
+ for cursor in range(opening_brace, content_end):
386
+ character = content[cursor]
387
+ if character not in "{}" or _is_escaped_character(content, cursor):
388
+ continue
389
+ depth += 1 if character == "{" else -1
390
+ if depth == 0:
391
+ return cursor
392
+ if depth < 0:
393
+ return None
394
+ return None
395
+
396
+
397
+ def _is_escaped_character(content: str, position: int) -> bool:
398
+ """判断指定字符前是否存在奇数个连续反斜杠。"""
399
+ preceding_backslashes = 0
400
+ cursor = position - 1
401
+ while cursor >= 0 and content[cursor] == "\\":
402
+ preceding_backslashes += 1
403
+ cursor -= 1
404
+ return preceding_backslashes % 2 == 1
405
+
406
+
407
+ __all__ = [
408
+ "DisplayFormulaFlowable",
409
+ "FormulaRenderer",
410
+ "FormulaVector",
411
+ "InlineFormulaImage",
412
+ "MAX_CACHED_FORMULAS",
413
+ "MAX_FORMULA_CHARACTERS",
414
+ "PdfFormulaError",
415
+ "draw_inline_formula",
416
+ "split_formula_tag",
417
+ ]