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,1016 @@
1
+ """Flash Office 文档中的 OOXML 图表解析与表格化渲染。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass, field
7
+ from datetime import date, datetime, time
8
+ from html import escape
9
+ from io import BytesIO
10
+ from typing import Any, Final
11
+
12
+ from lxml import etree
13
+ from openpyxl import load_workbook
14
+ from openpyxl.utils.cell import range_to_tuple
15
+ from openpyxl.utils.datetime import MAC_EPOCH, WINDOWS_EPOCH, from_excel
16
+ from openpyxl.workbook.workbook import Workbook
17
+ from openpyxl.worksheet.worksheet import Worksheet
18
+
19
+ _CHART_NS: Final = "http://schemas.openxmlformats.org/drawingml/2006/chart"
20
+ _DRAWING_NS: Final = "http://schemas.openxmlformats.org/drawingml/2006/main"
21
+ _NS: Final = {"c": _CHART_NS, "a": _DRAWING_NS}
22
+ _MAX_CACHE_INDEX_SPAN: Final = 100_000
23
+ _PLOT_TAGS: Final = (
24
+ "areaChart",
25
+ "area3DChart",
26
+ "barChart",
27
+ "bar3DChart",
28
+ "bubbleChart",
29
+ "doughnutChart",
30
+ "lineChart",
31
+ "line3DChart",
32
+ "ofPieChart",
33
+ "pieChart",
34
+ "pie3DChart",
35
+ "radarChart",
36
+ "scatterChart",
37
+ "stockChart",
38
+ "surfaceChart",
39
+ "surface3DChart",
40
+ )
41
+
42
+
43
+ @dataclass
44
+ class SeriesSpec:
45
+ """保存单个 OOXML 图表序列的公式引用、名称和缓存数据。"""
46
+
47
+ name_formula: str | None = None
48
+ literal_name: str | None = None
49
+ cat_formula: str | None = None
50
+ x_formula: str | None = None
51
+ val_formula: str | None = None
52
+ y_formula: str | None = None
53
+ bubble_size_formula: str | None = None
54
+ cached_categories: list[str] = field(default_factory=list)
55
+ cached_x_values: list[str] = field(default_factory=list)
56
+ cached_values: list[str] = field(default_factory=list)
57
+ cached_bubble_sizes: list[str] = field(default_factory=list)
58
+
59
+
60
+ @dataclass
61
+ class ChartSpec:
62
+ """保存图表类型、坐标轴信息和全部序列的规范化描述。"""
63
+
64
+ chart_type: str
65
+ plot_kind: str
66
+ title: str = ""
67
+ category_axis_title: str = ""
68
+ value_axis_title: str = ""
69
+ x_axis_title: str = ""
70
+ has_date_axis: bool = False
71
+ date_1904: bool = False
72
+ series: list[SeriesSpec] = field(default_factory=list)
73
+
74
+
75
+ def html_table_from_excel_bytes(excel_bytes: bytes) -> str:
76
+ """将嵌入工作簿中第一个非空工作表转换为 HTML 表格。"""
77
+ if not excel_bytes:
78
+ return ""
79
+
80
+ try:
81
+ workbook = load_workbook(
82
+ filename=BytesIO(excel_bytes),
83
+ data_only=True,
84
+ read_only=True,
85
+ )
86
+ except Exception:
87
+ return ""
88
+
89
+ try:
90
+ for worksheet in workbook.worksheets:
91
+ rows = _extract_non_empty_worksheet_rows(worksheet)
92
+ if rows:
93
+ return _render_embedded_workbook_table(rows)
94
+ finally:
95
+ workbook.close()
96
+
97
+ return ""
98
+
99
+
100
+ def _extract_non_empty_worksheet_rows(worksheet: Worksheet) -> list[list[str]]:
101
+ """提取工作表中首尾有内容的行,避免空 sheet 或尾部空列撑大兜底表格。"""
102
+ raw_rows: list[list[str]] = []
103
+ for row in worksheet.iter_rows(values_only=True):
104
+ stringified_row = [_stringify_cell_value(value) for value in row]
105
+ raw_rows.append(_trim_trailing_empty_values(stringified_row))
106
+
107
+ first_row_idx = _find_first_non_empty_row(raw_rows)
108
+ if first_row_idx is None:
109
+ return []
110
+
111
+ last_row_idx = _find_last_non_empty_row(raw_rows)
112
+ if last_row_idx is None:
113
+ return []
114
+
115
+ last_row_exclusive = last_row_idx + 1
116
+ rows = raw_rows[first_row_idx:last_row_exclusive]
117
+ width = max(len(row) for row in rows)
118
+ return [row + [""] * (width - len(row)) for row in rows]
119
+
120
+
121
+ def _trim_trailing_empty_values(values: list[str]) -> list[str]:
122
+ """移除行尾空值,保留中间空单元格的位置。"""
123
+ end = len(values)
124
+ while end > 0 and values[end - 1] == "":
125
+ end -= 1
126
+ return values[:end]
127
+
128
+
129
+ def _find_first_non_empty_row(rows: list[list[str]]) -> int | None:
130
+ """返回第一行非空行的索引,未找到时返回 None。"""
131
+ for idx, row in enumerate(rows):
132
+ if any(value != "" for value in row):
133
+ return idx
134
+ return None
135
+
136
+
137
+ def _find_last_non_empty_row(rows: list[list[str]]) -> int | None:
138
+ """返回最后一行非空行的索引。"""
139
+ for idx in range(len(rows) - 1, -1, -1):
140
+ if any(value != "" for value in rows[idx]):
141
+ return idx
142
+ return None
143
+
144
+
145
+ def _render_embedded_workbook_table(rows: list[list[str]]) -> str:
146
+ """将嵌入 workbook 的二维数据渲染为紧凑 HTML 表格,首行作为表头。"""
147
+ if not rows:
148
+ return ""
149
+
150
+ headers = rows[0]
151
+ data_rows = rows[1:]
152
+ html_parts = ["<table><thead><tr>"]
153
+ for header in headers:
154
+ html_parts.append(f"<th>{escape(header)}</th>")
155
+ html_parts.append("</tr></thead><tbody>")
156
+
157
+ for row in data_rows:
158
+ html_parts.append("<tr>")
159
+ for value in row:
160
+ html_parts.append(f"<td>{escape(value)}</td>")
161
+ html_parts.append("</tr>")
162
+
163
+ html_parts.append("</tbody></table>")
164
+ return "".join(html_parts)
165
+
166
+
167
+ def extract_chart_html_from_ooxml(chart_xml: bytes, workbook_bytes: bytes | None) -> str:
168
+ """解析 OOXML 图表并按工作簿、缓存和普通表格的优先级生成 HTML。"""
169
+ spec = parse_chart_spec_from_ooxml(chart_xml)
170
+ if spec is None or not spec.series:
171
+ if workbook_bytes:
172
+ return html_table_from_excel_bytes(workbook_bytes)
173
+ return ""
174
+
175
+ chart_cache_html = render_chart_html_from_cache(spec)
176
+
177
+ if workbook_bytes:
178
+ chart_html = render_chart_html_from_workbook(spec, workbook_bytes)
179
+ if chart_html:
180
+ return chart_html
181
+ if chart_cache_html:
182
+ return chart_cache_html
183
+ workbook_table_html = html_table_from_excel_bytes(workbook_bytes)
184
+ if workbook_table_html:
185
+ return workbook_table_html
186
+
187
+ return chart_cache_html
188
+
189
+
190
+ def parse_chart_spec_from_ooxml(chart_xml: bytes) -> ChartSpec | None:
191
+ """解析图表 XML,生成统一的图表类型、坐标轴和序列描述。"""
192
+ try:
193
+ root = etree.fromstring(
194
+ chart_xml,
195
+ parser=etree.XMLParser(
196
+ load_dtd=False,
197
+ resolve_entities=False,
198
+ no_network=True,
199
+ ),
200
+ )
201
+ except (etree.XMLSyntaxError, TypeError, ValueError):
202
+ return None
203
+
204
+ plot_area = root.find(".//c:plotArea", namespaces=_NS)
205
+ if plot_area is None:
206
+ return None
207
+
208
+ plot_elements = _collect_plot_elements(plot_area)
209
+ if not plot_elements:
210
+ return None
211
+
212
+ has_date_axis = plot_area.find("c:dateAx", namespaces=_NS) is not None
213
+ plot_kinds = {_plot_kind_from_tag_name(tag_name, has_date_axis) for tag_name, _ in plot_elements}
214
+ if plot_kinds == {"scatter"}:
215
+ plot_kind = "scatter"
216
+ elif plot_kinds == {"bubble"}:
217
+ plot_kind = "bubble"
218
+ elif plot_kinds <= {"category", "date"}:
219
+ plot_kind = "date" if "date" in plot_kinds else "category"
220
+ else:
221
+ return None
222
+
223
+ category_axis_title = ""
224
+ axis = plot_area.find("c:dateAx", namespaces=_NS)
225
+ if axis is None:
226
+ axis = plot_area.find("c:catAx", namespaces=_NS)
227
+ if axis is not None:
228
+ category_axis_title = _extract_title_text(axis.find("c:title", namespaces=_NS)) # type: ignore
229
+
230
+ x_axis_title = ""
231
+ value_axis_title = ""
232
+ if plot_kind in {"scatter", "bubble"}:
233
+ for axis in plot_area.findall("c:valAx", namespaces=_NS):
234
+ axis_pos = axis.find("c:axPos", namespaces=_NS)
235
+ axis_position = axis_pos.get("val") if axis_pos is not None else ""
236
+ title = _extract_title_text(axis.find("c:title", namespaces=_NS)) # type: ignore
237
+ if axis_position == "b" and not x_axis_title:
238
+ x_axis_title = title
239
+ elif axis_position == "l" and not value_axis_title:
240
+ value_axis_title = title
241
+ if not x_axis_title:
242
+ x_axis_title = category_axis_title
243
+ else:
244
+ axis = plot_area.find("c:valAx", namespaces=_NS)
245
+ if axis is not None:
246
+ value_axis_title = _extract_title_text(axis.find("c:title", namespaces=_NS)) # type: ignore
247
+
248
+ series_specs = []
249
+ for _, plot_element in plot_elements:
250
+ for series_element in plot_element.findall("c:ser", namespaces=_NS):
251
+ series_specs.append(
252
+ SeriesSpec(
253
+ name_formula=_extract_tx_formula(series_element.find("c:tx", namespaces=_NS)), # type: ignore
254
+ literal_name=_extract_tx_text(series_element.find("c:tx", namespaces=_NS)), # type: ignore
255
+ cat_formula=_extract_reference_formula(series_element.find("c:cat", namespaces=_NS)), # type: ignore
256
+ x_formula=_extract_reference_formula(series_element.find("c:xVal", namespaces=_NS)), # type: ignore
257
+ val_formula=_extract_reference_formula(series_element.find("c:val", namespaces=_NS)), # type: ignore
258
+ y_formula=_extract_reference_formula(series_element.find("c:yVal", namespaces=_NS)), # type: ignore
259
+ bubble_size_formula=_extract_reference_formula(series_element.find("c:bubbleSize", namespaces=_NS)), # type: ignore
260
+ cached_categories=_extract_reference_cache(
261
+ series_element.find("c:cat", namespaces=_NS), # type: ignore
262
+ date_hint=has_date_axis,
263
+ date_1904=_chart_uses_date_1904(root),
264
+ ),
265
+ cached_x_values=_extract_reference_cache(series_element.find("c:xVal", namespaces=_NS)), # type: ignore
266
+ cached_values=_extract_reference_cache(
267
+ _first_non_none(
268
+ series_element.find("c:val", namespaces=_NS),
269
+ series_element.find("c:yVal", namespaces=_NS),
270
+ ) # type: ignore
271
+ ),
272
+ cached_bubble_sizes=_extract_reference_cache(series_element.find("c:bubbleSize", namespaces=_NS)), # type: ignore
273
+ )
274
+ )
275
+
276
+ return ChartSpec(
277
+ chart_type=(plot_elements[0][0] if len(plot_elements) == 1 else "comboChart"),
278
+ plot_kind=plot_kind,
279
+ title=_extract_title_text(root.find(".//c:chart/c:title", namespaces=_NS)), # type: ignore
280
+ category_axis_title=category_axis_title,
281
+ value_axis_title=value_axis_title,
282
+ x_axis_title=x_axis_title,
283
+ has_date_axis=has_date_axis,
284
+ date_1904=_chart_uses_date_1904(root),
285
+ series=series_specs,
286
+ )
287
+
288
+
289
+ def render_chart_html_from_workbook(spec: ChartSpec, workbook_bytes: bytes) -> str:
290
+ """根据图表公式从嵌入工作簿读取数据并渲染 HTML 表格。"""
291
+ try:
292
+ workbook = load_workbook(
293
+ filename=BytesIO(workbook_bytes),
294
+ data_only=True,
295
+ read_only=True,
296
+ )
297
+ except Exception:
298
+ return ""
299
+
300
+ try:
301
+ if spec.plot_kind in {"category", "date"}:
302
+ return _render_category_like_chart_from_workbook(spec, workbook)
303
+ if spec.plot_kind == "scatter":
304
+ return _render_scatter_like_chart_from_workbook(spec, workbook)
305
+ if spec.plot_kind == "bubble":
306
+ return _render_bubble_chart_from_workbook(spec, workbook)
307
+ return ""
308
+ finally:
309
+ workbook.close()
310
+
311
+
312
+ def render_chart_html_from_cache(spec: ChartSpec) -> str:
313
+ """在工作簿不可用时使用 OOXML 图表缓存数据渲染 HTML 表格。"""
314
+ if spec.plot_kind in {"category", "date"}:
315
+ categories = []
316
+ for series in spec.series:
317
+ if series.cached_categories:
318
+ categories = series.cached_categories
319
+ break
320
+
321
+ series_names = []
322
+ series_values = []
323
+ for idx, series in enumerate(spec.series, start=1):
324
+ series_names.append(_resolve_series_name(series, idx))
325
+ series_values.append(series.cached_values)
326
+
327
+ row_count = max(
328
+ len(categories),
329
+ max((len(values) for values in series_values), default=0),
330
+ )
331
+ if not series_names or row_count == 0:
332
+ return ""
333
+
334
+ headers = [spec.category_axis_title or ""] + series_names
335
+ columns = [categories] + series_values
336
+ return _render_html_table(headers, columns, row_count)
337
+
338
+ if spec.plot_kind == "scatter":
339
+ return _render_scatter_like_chart_from_cache(spec)
340
+
341
+ if spec.plot_kind == "bubble":
342
+ return _render_bubble_chart_from_cache(spec)
343
+
344
+ return ""
345
+
346
+
347
+ def _render_category_like_chart_from_workbook(spec: ChartSpec, workbook: Workbook) -> str:
348
+ """从工作簿渲染分类轴或日期轴图表的二维 HTML 表格。"""
349
+ categories = []
350
+
351
+ for series in spec.series:
352
+ if not series.cat_formula:
353
+ continue
354
+ read_result = _read_formula_vector(workbook, series.cat_formula)
355
+ if read_result is None:
356
+ return ""
357
+ _, values = read_result
358
+ categories = values
359
+ break
360
+
361
+ series_names = []
362
+ series_values = []
363
+ for idx, series in enumerate(spec.series, start=1):
364
+ if not series.val_formula:
365
+ return ""
366
+ read_result = _read_formula_vector(workbook, series.val_formula)
367
+ if read_result is None:
368
+ return ""
369
+ _, values = read_result
370
+ series_names.append(_resolve_series_name(series, idx, workbook))
371
+ series_values.append(values)
372
+
373
+ row_count = max(
374
+ len(categories),
375
+ max((len(values) for values in series_values), default=0),
376
+ )
377
+ if not series_names or row_count == 0:
378
+ return ""
379
+
380
+ headers = [spec.category_axis_title or ""] + series_names
381
+ columns = [
382
+ _stringify_series_values(
383
+ categories,
384
+ date_hint=spec.has_date_axis,
385
+ date_1904=spec.date_1904,
386
+ )
387
+ ]
388
+ columns.extend(_stringify_series_values(values) for values in series_values)
389
+ return _render_html_table(headers, columns, row_count)
390
+
391
+
392
+ def _render_scatter_like_chart_from_workbook(spec: ChartSpec, workbook: Workbook) -> str:
393
+ """读取工作簿中的散点图横纵轴数据并渲染 HTML 表格。"""
394
+ x_sequences, series_names, series_y_values = _read_scatter_axes_from_workbook(
395
+ spec,
396
+ workbook,
397
+ )
398
+ return _render_scatter_like_chart_table(
399
+ x_sequences,
400
+ series_names,
401
+ series_y_values,
402
+ x_axis_title=spec.x_axis_title,
403
+ )
404
+
405
+
406
+ def _render_bubble_chart_from_workbook(spec: ChartSpec, workbook: Workbook) -> str:
407
+ """读取工作簿中的气泡图横纵轴和尺寸数据并渲染 HTML 表格。"""
408
+ x_sequences, series_names, series_y_values, series_sizes = _read_bubble_axes_from_workbook(
409
+ spec,
410
+ workbook,
411
+ )
412
+ return _render_bubble_chart_table(
413
+ x_sequences,
414
+ series_names,
415
+ series_y_values,
416
+ series_sizes,
417
+ x_axis_title=spec.x_axis_title,
418
+ )
419
+
420
+
421
+ def _render_scatter_like_chart_from_cache(spec: ChartSpec) -> str:
422
+ """使用 OOXML 缓存的横纵轴数据渲染散点图 HTML 表格。"""
423
+ x_sequences = []
424
+ series_names = []
425
+ series_y_values = []
426
+ for idx, series in enumerate(spec.series, start=1):
427
+ if not series.cached_x_values or not series.cached_values:
428
+ return ""
429
+ x_sequences.append(series.cached_x_values)
430
+ series_names.append(_resolve_series_name(series, idx))
431
+ series_y_values.append(series.cached_values)
432
+
433
+ return _render_scatter_like_chart_table(
434
+ x_sequences,
435
+ series_names,
436
+ series_y_values,
437
+ x_axis_title=spec.x_axis_title,
438
+ )
439
+
440
+
441
+ def _render_bubble_chart_from_cache(spec: ChartSpec) -> str:
442
+ """使用 OOXML 缓存的横纵轴和尺寸数据渲染气泡图 HTML 表格。"""
443
+ x_sequences = []
444
+ series_names = []
445
+ series_y_values = []
446
+ series_sizes = []
447
+ for idx, series in enumerate(spec.series, start=1):
448
+ if not series.cached_x_values or not series.cached_values or not series.cached_bubble_sizes:
449
+ return ""
450
+ x_sequences.append(series.cached_x_values)
451
+ series_names.append(_resolve_series_name(series, idx))
452
+ series_y_values.append(series.cached_values)
453
+ series_sizes.append(series.cached_bubble_sizes)
454
+
455
+ return _render_bubble_chart_table(
456
+ x_sequences,
457
+ series_names,
458
+ series_y_values,
459
+ series_sizes,
460
+ x_axis_title=spec.x_axis_title,
461
+ )
462
+
463
+
464
+ def _read_scatter_axes_from_workbook(
465
+ spec: ChartSpec, workbook: Workbook
466
+ ) -> tuple[list[list[float]] | None, list[str], list[list[float]]]:
467
+ """按序列公式读取散点图的 X 轴、序列名称和 Y 轴数据。"""
468
+ x_sequences = []
469
+ series_names = []
470
+ series_y_values = []
471
+
472
+ for idx, series in enumerate(spec.series, start=1):
473
+ if not series.x_formula or not series.y_formula:
474
+ return None, [], []
475
+
476
+ x_read = _read_formula_vector(workbook, series.x_formula)
477
+ if x_read is None:
478
+ return None, [], []
479
+ _, x_values = x_read
480
+ x_sequences.append(x_values)
481
+
482
+ y_read = _read_formula_vector(workbook, series.y_formula)
483
+ if y_read is None:
484
+ return None, [], []
485
+ _, y_values = y_read
486
+ series_names.append(_resolve_series_name(series, idx, workbook))
487
+ series_y_values.append(y_values)
488
+
489
+ return x_sequences, series_names, series_y_values
490
+
491
+
492
+ def _read_bubble_axes_from_workbook(
493
+ spec: ChartSpec, workbook: Workbook
494
+ ) -> tuple[list[list[float]] | None, list[str], list[list[float]], list[list[float]]]:
495
+ """按序列公式读取气泡图的 X/Y 轴、名称和气泡尺寸数据。"""
496
+ x_sequences = []
497
+ series_names = []
498
+ series_y_values = []
499
+ series_sizes = []
500
+
501
+ for idx, series in enumerate(spec.series, start=1):
502
+ if not series.x_formula or not series.y_formula or not series.bubble_size_formula:
503
+ return None, [], [], []
504
+
505
+ x_read = _read_formula_vector(workbook, series.x_formula)
506
+ if x_read is None:
507
+ return None, [], [], []
508
+ _, x_values = x_read
509
+ x_sequences.append(x_values)
510
+
511
+ y_read = _read_formula_vector(workbook, series.y_formula)
512
+ bubble_size_read = _read_formula_vector(workbook, series.bubble_size_formula)
513
+ if y_read is None or bubble_size_read is None:
514
+ return None, [], [], []
515
+
516
+ series_names.append(_resolve_series_name(series, idx, workbook))
517
+ series_y_values.append(y_read[1])
518
+ series_sizes.append(bubble_size_read[1])
519
+
520
+ return x_sequences, series_names, series_y_values, series_sizes
521
+
522
+
523
+ def _read_formula_vector(workbook: Workbook, formula: str) -> tuple[str, list[Any]] | None:
524
+ """解析单行或单列单元格公式,并从工作簿读取对应的一维数据。"""
525
+ parsed = _parse_formula(formula)
526
+ if parsed is None:
527
+ return None
528
+
529
+ sheet_name, min_col, min_row, max_col, max_row = parsed
530
+
531
+ try:
532
+ worksheet = workbook[sheet_name]
533
+ except KeyError:
534
+ return None
535
+
536
+ if min_col != max_col and min_row != max_row:
537
+ return None
538
+
539
+ values = []
540
+ if min_col == max_col:
541
+ for row_idx in range(min_row, max_row + 1):
542
+ values.append(worksheet.cell(row=row_idx, column=min_col).value)
543
+ else:
544
+ for col_idx in range(min_col, max_col + 1):
545
+ values.append(worksheet.cell(row=min_row, column=col_idx).value)
546
+
547
+ return sheet_name, values
548
+
549
+
550
+ def _read_formula_scalar(workbook: Workbook, formula: str) -> str | None:
551
+ """读取公式引用区域的首个有效值并转换为字符串。"""
552
+ read_result = _read_formula_vector(workbook, formula)
553
+ if read_result is None:
554
+ return None
555
+
556
+ _, values = read_result
557
+ if not values:
558
+ return None
559
+
560
+ value = values[0]
561
+ if value in (None, ""):
562
+ return None
563
+ return _stringify_cell_value(value)
564
+
565
+
566
+ def _parse_formula(formula: str) -> tuple[str, int, int, int, int] | None:
567
+ """将工作表区域公式解析为工作表名称和规范化单元格边界。"""
568
+ formula = formula.strip()
569
+ if not formula:
570
+ return None
571
+ if formula.startswith("="):
572
+ formula = formula[1:]
573
+
574
+ try:
575
+ sheet_name, bounds = range_to_tuple(formula)
576
+ except ValueError:
577
+ return None
578
+
579
+ if not all(isinstance(bound, int) for bound in bounds):
580
+ return None
581
+
582
+ min_col, min_row, max_col, max_row = bounds
583
+ return _unescape_formula_sheet_name(sheet_name), min_col, min_row, max_col, max_row
584
+
585
+
586
+ def _unescape_formula_sheet_name(sheet_name: str) -> str:
587
+ """还原 OOXML 公式中以双单引号转义的工作表名称。"""
588
+ return sheet_name.replace("''", "'")
589
+
590
+
591
+ def _extract_reference_formula(container: etree._Element) -> str | None:
592
+ """从字符串、数字或多级字符串引用容器中提取公式文本。"""
593
+ ref_element = _find_reference_element(container)
594
+ if ref_element is None:
595
+ return None
596
+ formula_element = ref_element.find("c:f", namespaces=_NS)
597
+ if formula_element is None or formula_element.text is None:
598
+ return None
599
+ return formula_element.text.strip()
600
+
601
+
602
+ def _extract_reference_cache(
603
+ container: etree._Element,
604
+ *,
605
+ date_hint: bool = False,
606
+ date_1904: bool = False,
607
+ ) -> list[str]:
608
+ """从图表引用容器提取缓存数据,并按日期提示规范化值。"""
609
+ ref_element = _find_reference_element(container)
610
+ if ref_element is None:
611
+ return []
612
+
613
+ tag_name = etree.QName(ref_element).localname
614
+ if tag_name == "multiLvlStrRef":
615
+ return _extract_multilevel_string_cache(ref_element)
616
+
617
+ cache_element = ref_element.find("c:strCache", namespaces=_NS)
618
+ if cache_element is None:
619
+ cache_element = ref_element.find("c:numCache", namespaces=_NS)
620
+ if cache_element is None:
621
+ return []
622
+
623
+ return _extract_cache_points(
624
+ cache_element,
625
+ date_hint=date_hint,
626
+ date_1904=date_1904,
627
+ )
628
+
629
+
630
+ def _extract_cache_points(
631
+ cache_element: etree._Element,
632
+ *,
633
+ date_hint: bool = False,
634
+ date_1904: bool = False,
635
+ ) -> list[str]:
636
+ """按缓存点索引还原连续序列,并拒绝异常大的稀疏索引范围。"""
637
+ points = {}
638
+ for point in cache_element.findall("c:pt", namespaces=_NS):
639
+ raw_index = point.get("idx")
640
+ if raw_index is None:
641
+ continue
642
+ try:
643
+ point_index = int(raw_index)
644
+ except ValueError:
645
+ continue
646
+
647
+ value_element = point.find("c:v", namespaces=_NS)
648
+ raw_value = value_element.text if value_element is not None else ""
649
+ points[point_index] = _stringify_cache_value(
650
+ raw_value,
651
+ date_hint=date_hint,
652
+ date_1904=date_1904,
653
+ )
654
+
655
+ if not points:
656
+ return []
657
+
658
+ max_index = max(points.keys())
659
+ if max_index + 1 > _MAX_CACHE_INDEX_SPAN:
660
+ return []
661
+
662
+ return [points.get(index, "") for index in range(max_index + 1)]
663
+
664
+
665
+ def _extract_multilevel_string_cache(ref_element: etree._Element) -> list[str]:
666
+ """合并多级分类缓存的同索引文本,生成扁平分类标签序列。"""
667
+ level_maps = []
668
+ max_index = -1
669
+ for level in ref_element.findall("c:multiLvlStrCache/c:lvl", namespaces=_NS):
670
+ values = {}
671
+ for point in level.findall("c:pt", namespaces=_NS):
672
+ raw_index = point.get("idx")
673
+ if raw_index is None:
674
+ continue
675
+ try:
676
+ point_index = int(raw_index)
677
+ except ValueError:
678
+ continue
679
+ value_element = point.find("c:v", namespaces=_NS)
680
+ values[point_index] = value_element.text if value_element is not None else ""
681
+ max_index = max(max_index, point_index)
682
+ level_maps.append(values)
683
+
684
+ if max_index < 0:
685
+ return []
686
+
687
+ if max_index + 1 > _MAX_CACHE_INDEX_SPAN:
688
+ return []
689
+
690
+ rows = []
691
+ for point_index in range(max_index + 1):
692
+ parts = [value_map[point_index] for value_map in level_maps if value_map.get(point_index)]
693
+ rows.append(" / ".join(parts))
694
+ return rows
695
+
696
+
697
+ def _extract_tx_formula(tx_element: etree._Element) -> str | None:
698
+ """从图表序列名称节点提取字符串引用公式。"""
699
+ if tx_element is None:
700
+ return None
701
+ str_ref = tx_element.find("c:strRef", namespaces=_NS)
702
+ if str_ref is None:
703
+ return None
704
+ formula_element = str_ref.find("c:f", namespaces=_NS)
705
+ if formula_element is None or formula_element.text is None:
706
+ return None
707
+ return formula_element.text.strip()
708
+
709
+
710
+ def _extract_tx_text(tx_element: etree._Element) -> str | None:
711
+ """从图表序列名称节点提取缓存文本或直接文本。"""
712
+ if tx_element is None:
713
+ return None
714
+
715
+ str_cache = tx_element.find("c:strRef/c:strCache", namespaces=_NS)
716
+ if str_cache is not None:
717
+ values = _extract_cache_points(str_cache)
718
+ return values[0] if values else None
719
+
720
+ value_element = tx_element.find("c:v", namespaces=_NS)
721
+ if value_element is not None and value_element.text:
722
+ return value_element.text.strip()
723
+
724
+ return None
725
+
726
+
727
+ def _extract_title_text(title_element: etree._Element) -> str:
728
+ """拼接图表标题节点中的全部富文本片段。"""
729
+ if title_element is None:
730
+ return ""
731
+ texts = title_element.findall(".//a:t", namespaces=_NS)
732
+ return "".join(text.text or "" for text in texts).strip()
733
+
734
+
735
+ def _find_reference_element(container: etree._Element) -> object | None:
736
+ """在容器中查找受支持的字符串、数字或多级字符串引用节点。"""
737
+ if container is None:
738
+ return None
739
+ for tag_name in ("strRef", "numRef", "multiLvlStrRef"):
740
+ ref_element = container.find(f"c:{tag_name}", namespaces=_NS)
741
+ if ref_element is not None:
742
+ return ref_element
743
+ return None
744
+
745
+
746
+ def _first_non_none(*values: Any) -> object | None:
747
+ """返回参数序列中的第一个非空对象。"""
748
+ for value in values:
749
+ if value is not None:
750
+ return value
751
+ return None
752
+
753
+
754
+ def _collect_plot_elements(plot_area: etree._Element) -> list[tuple[str, etree._Element]]:
755
+ """收集绘图区中受支持的图表节点及其局部标签名。"""
756
+ plot_elements = []
757
+ for child in plot_area:
758
+ if not isinstance(child.tag, str):
759
+ continue
760
+ tag_name = etree.QName(child).localname
761
+ if tag_name in _PLOT_TAGS:
762
+ plot_elements.append((tag_name, child))
763
+ return plot_elements
764
+
765
+
766
+ def _plot_kind_from_tag_name(tag_name: str, has_date_axis: bool) -> str:
767
+ """根据 OOXML 图表标签和日期轴信息归一化绘图类别。"""
768
+ if tag_name == "scatterChart":
769
+ return "scatter"
770
+ if tag_name == "bubbleChart":
771
+ return "bubble"
772
+ if has_date_axis:
773
+ return "date"
774
+ return "category"
775
+
776
+
777
+ def _chart_uses_date_1904(root: etree._Element) -> bool:
778
+ """判断图表是否使用以 1904 年为起点的 Excel 日期系统。"""
779
+ date_1904 = root.find("c:date1904", namespaces=_NS)
780
+ if date_1904 is None:
781
+ return False
782
+ return date_1904.get("val") == "1"
783
+
784
+
785
+ def _resolve_series_name(series: SeriesSpec, index: int, workbook: Workbook | None = None) -> str:
786
+ """按工作簿引用、缓存名称和默认序号依次解析序列名称。"""
787
+ if workbook is not None and series.name_formula:
788
+ workbook_name = _read_formula_scalar(workbook, series.name_formula)
789
+ if workbook_name:
790
+ return workbook_name
791
+ if series.literal_name:
792
+ return series.literal_name
793
+ return f"Series{index}"
794
+
795
+
796
+ def _get_shared_axis_values(sequences: list[list[Any]]) -> list[Any] | None:
797
+ """判断多个序列是否共享相同轴值,并在一致时返回首个序列。"""
798
+ if not sequences:
799
+ return None
800
+
801
+ normalized = [_normalize_sequence(sequence) for sequence in sequences]
802
+ first = normalized[0]
803
+ if any(sequence != first for sequence in normalized[1:]):
804
+ return None
805
+ return sequences[0]
806
+
807
+
808
+ def _normalize_sequence(sequence: list[Any]) -> list[str]:
809
+ """将轴值序列统一转换为可比较的字符串列表。"""
810
+ return [_stringify_cell_value(value) for value in sequence]
811
+
812
+
813
+ def _render_scatter_like_chart_table(
814
+ x_sequences: list[list[Any]] | None,
815
+ series_names: list[str],
816
+ series_y_values: list[list[Any]],
817
+ *,
818
+ x_axis_title: str,
819
+ ) -> str:
820
+ """按共享或独立 X 轴布局,将散点图序列渲染为 HTML 表格。"""
821
+ if not x_sequences or not series_names or len(x_sequences) != len(series_names):
822
+ return ""
823
+
824
+ shared_x_values = _get_shared_axis_values(x_sequences)
825
+ if shared_x_values is not None:
826
+ row_count = max(
827
+ len(shared_x_values),
828
+ max((len(values) for values in series_y_values), default=0),
829
+ )
830
+ if row_count == 0:
831
+ return ""
832
+
833
+ headers = [x_axis_title or ""] + series_names
834
+ columns = [_stringify_series_values(shared_x_values)]
835
+ columns.extend(_stringify_series_values(values) for values in series_y_values)
836
+ return _render_html_table(headers, columns, row_count)
837
+
838
+ headers = []
839
+ columns = []
840
+ row_count = 0
841
+ for name, x_values, y_values in zip(series_names, x_sequences, series_y_values):
842
+ headers.extend((f"{name} X", f"{name} Y"))
843
+ columns.append(_stringify_series_values(x_values))
844
+ columns.append(_stringify_series_values(y_values))
845
+ row_count = max(row_count, len(x_values), len(y_values))
846
+
847
+ if row_count == 0:
848
+ return ""
849
+
850
+ return _render_html_table(headers, columns, row_count)
851
+
852
+
853
+ def _render_bubble_chart_table(
854
+ x_sequences: list[list[Any]] | None,
855
+ series_names: list[str],
856
+ series_y_values: list[list[Any]],
857
+ series_sizes: list[list[Any]],
858
+ *,
859
+ x_axis_title: str,
860
+ ) -> str:
861
+ """按共享或独立 X 轴布局,将气泡图三维序列渲染为 HTML 表格。"""
862
+ if (
863
+ not x_sequences
864
+ or not series_names
865
+ or len(x_sequences) != len(series_names)
866
+ or len(series_y_values) != len(series_names)
867
+ or len(series_sizes) != len(series_names)
868
+ ):
869
+ return ""
870
+
871
+ shared_x_values = _get_shared_axis_values(x_sequences)
872
+ if shared_x_values is not None:
873
+ row_count = max(
874
+ len(shared_x_values),
875
+ max((len(values) for values in series_y_values), default=0),
876
+ max((len(values) for values in series_sizes), default=0),
877
+ )
878
+ if row_count == 0:
879
+ return ""
880
+
881
+ headers = [x_axis_title or ""]
882
+ columns = [_stringify_series_values(shared_x_values)]
883
+ for name, y_values, bubble_sizes in zip(series_names, series_y_values, series_sizes):
884
+ headers.extend((name, f"{name} size"))
885
+ columns.append(_stringify_series_values(y_values))
886
+ columns.append(_stringify_series_values(bubble_sizes))
887
+ return _render_html_table(headers, columns, row_count)
888
+
889
+ headers = []
890
+ columns = []
891
+ row_count = 0
892
+ for name, x_values, y_values, bubble_sizes in zip(
893
+ series_names,
894
+ x_sequences,
895
+ series_y_values,
896
+ series_sizes,
897
+ ):
898
+ headers.extend((f"{name} X", f"{name} Y", f"{name} size"))
899
+ columns.append(_stringify_series_values(x_values))
900
+ columns.append(_stringify_series_values(y_values))
901
+ columns.append(_stringify_series_values(bubble_sizes))
902
+ row_count = max(row_count, len(x_values), len(y_values), len(bubble_sizes))
903
+
904
+ if row_count == 0:
905
+ return ""
906
+
907
+ return _render_html_table(headers, columns, row_count)
908
+
909
+
910
+ def _stringify_series_values(
911
+ values: list[Any],
912
+ *,
913
+ date_hint: bool = False,
914
+ date_1904: bool = False,
915
+ ) -> list[str]:
916
+ """将图表序列值批量转换为适合 HTML 输出的文本。"""
917
+ return [
918
+ _stringify_cell_value(
919
+ value,
920
+ date_hint=date_hint,
921
+ date_1904=date_1904,
922
+ )
923
+ for value in values
924
+ ]
925
+
926
+
927
+ def _stringify_cache_value(
928
+ value: str | None,
929
+ *,
930
+ date_hint: bool = False,
931
+ date_1904: bool = False,
932
+ ) -> str:
933
+ """规范化 OOXML 缓存值,并在需要时转换 Excel 日期序列号。"""
934
+ if value in (None, ""):
935
+ return ""
936
+
937
+ if date_hint:
938
+ try:
939
+ serial = float(value)
940
+ except (TypeError, ValueError):
941
+ return value
942
+ return _excel_serial_to_iso(serial, date_1904=date_1904) or value
943
+
944
+ return value
945
+
946
+
947
+ def _stringify_cell_value(
948
+ value: Any,
949
+ *,
950
+ date_hint: bool = False,
951
+ date_1904: bool = False,
952
+ ) -> str:
953
+ """将工作簿单元格值转换为稳定文本,并保留日期时间语义。"""
954
+ if value in (None, ""):
955
+ return ""
956
+
957
+ if isinstance(value, datetime):
958
+ if date_hint and value.time() == time():
959
+ return value.date().isoformat()
960
+ return value.isoformat(sep=" ")
961
+ if isinstance(value, date):
962
+ return value.isoformat()
963
+ if isinstance(value, time):
964
+ return value.isoformat()
965
+
966
+ if date_hint and isinstance(value, (int, float)):
967
+ return _excel_serial_to_iso(float(value), date_1904=date_1904) or _stringify_non_date_value(value)
968
+
969
+ return _stringify_non_date_value(value)
970
+
971
+
972
+ def _excel_serial_to_iso(serial: float, *, date_1904: bool = False) -> str | None:
973
+ """把 Excel 日期序列号转换为 ISO 文本,无效数值返回空结果。"""
974
+ if not math.isfinite(serial):
975
+ return None
976
+ try:
977
+ excel_value = from_excel(serial, MAC_EPOCH if date_1904 else WINDOWS_EPOCH)
978
+ except (TypeError, ValueError, OverflowError):
979
+ return None
980
+ if isinstance(excel_value, datetime):
981
+ if excel_value.time() == time():
982
+ return excel_value.date().isoformat()
983
+ return excel_value.isoformat(sep=" ")
984
+ if isinstance(excel_value, date):
985
+ return excel_value.isoformat()
986
+ if isinstance(excel_value, time):
987
+ return excel_value.isoformat()
988
+ return str(excel_value)
989
+
990
+
991
+ def _stringify_non_date_value(value: Any) -> str:
992
+ """将非日期值转换为文本,并去除整数浮点值末尾的小数部分。"""
993
+ if isinstance(value, float) and value.is_integer():
994
+ return str(int(value))
995
+ return str(value)
996
+
997
+
998
+ def _render_html_table(headers: list[str], columns: list[list[str]], row_count: int) -> str:
999
+ """按表头、列数据和行数生成已转义的紧凑 HTML 表格。"""
1000
+ if row_count <= 0 or len(headers) != len(columns):
1001
+ return ""
1002
+
1003
+ html_parts = ["<table><thead><tr>"]
1004
+ for header in headers:
1005
+ html_parts.append(f"<th>{escape(header)}</th>")
1006
+ html_parts.append("</tr></thead><tbody>")
1007
+
1008
+ for row_idx in range(row_count):
1009
+ html_parts.append("<tr>")
1010
+ for column in columns:
1011
+ value = column[row_idx] if row_idx < len(column) else ""
1012
+ html_parts.append(f"<td>{escape(value)}</td>")
1013
+ html_parts.append("</tr>")
1014
+
1015
+ html_parts.append("</tbody></table>")
1016
+ return "".join(html_parts)