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,347 @@
1
+ """把 OFD 文档、模板、图层与资源组合成逐页场景。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import replace
6
+
7
+ from loguru import logger
8
+ from lxml import etree # type: ignore[reportMissingImports]
9
+
10
+ from ....schema import BBox
11
+ from .constants import MAX_OBJECT_RECURSION, MAX_PAGE_COUNT, OFD_NAMESPACES
12
+ from .errors import OfdParseError, OfdResourceLimitError
13
+ from .geometry import Affine, bbox_intersection, parse_affine, parse_st_box, transform_bbox
14
+ from .images import build_image_item
15
+ from .models import OfdPageScene, PageBuildContext, PageRef, ResourceRegistry, TemplateRef
16
+ from .package import OfdPackage, element_text, first_child, first_descendant, local_name, namespace_name, parse_int
17
+ from .path import OfdPathBudget, build_axis_lines
18
+ from .resources import merge_registries, parse_resource_part, resolve_draw_param
19
+ from .text import FontMetricResolver, OfdTextBudget, build_text_lines
20
+
21
+
22
+ class OfdSceneBuilder:
23
+ """按文档页树构造 OFD 页面场景。"""
24
+
25
+ def __init__(self, package: OfdPackage) -> None:
26
+ """绑定当前包和跨页共享预算。"""
27
+ self.package = package
28
+ self.text_budget = OfdTextBudget()
29
+ self.path_budget = OfdPathBudget()
30
+ self.font_metrics = FontMetricResolver(package)
31
+ self._paint_order = 0
32
+ self._page_count = 0
33
+
34
+ def build(self) -> list[OfdPageScene]:
35
+ """按 DocBody 和 Pages 声明顺序构造全部页面。"""
36
+ scenes: list[OfdPageScene] = []
37
+ try:
38
+ for document_ref in self.package.document_refs():
39
+ document_root = self._required_ofd_xml(document_ref.document_part)
40
+ scenes.extend(self._build_document(document_ref.document_part, document_root, len(scenes)))
41
+ return scenes
42
+ finally:
43
+ self.font_metrics.close()
44
+
45
+ def _required_ofd_xml(self, part_name: str) -> etree._Element:
46
+ """读取必需核心 XML 并限制为已知 OFD 命名空间。"""
47
+ root = self.package.xml_part(part_name, required=True)
48
+ assert root is not None
49
+ namespace = namespace_name(root.tag)
50
+ if namespace not in OFD_NAMESPACES:
51
+ raise OfdParseError(f"Malformed OFD package: unsupported namespace {namespace!r} in {part_name!r}")
52
+ return root
53
+
54
+ def _resolve_part(self, base_part: str, element: etree._Element | None, *, required: bool = False) -> str | None:
55
+ """解析一个 ST_Loc 元素并按需要求结果存在。"""
56
+ part = self.package.resolve_reference(base_part, element_text(element)) if element is not None else None
57
+ if required and (part is None or not self.package.has_part(part)):
58
+ raise OfdParseError(f"Malformed OFD package: invalid required location from {base_part!r}")
59
+ return part
60
+
61
+ def _document_resources(self, document_part: str, document_root: etree._Element) -> ResourceRegistry:
62
+ """解析文档级 PublicRes 与 DocumentRes。"""
63
+ common_data = first_descendant(document_root, "CommonData")
64
+ public_element = first_child(common_data, "PublicRes")
65
+ document_resource_element = first_child(common_data, "DocumentRes")
66
+ public_part = self._resolve_part(document_part, public_element, required=public_element is not None)
67
+ document_resource_part = self._resolve_part(
68
+ document_part,
69
+ document_resource_element,
70
+ required=document_resource_element is not None,
71
+ )
72
+ return merge_registries(
73
+ parse_resource_part(self.package, public_part),
74
+ parse_resource_part(self.package, document_resource_part),
75
+ )
76
+
77
+ def _page_area(self, root: etree._Element, fallback: BBox | None, name: str) -> BBox | None:
78
+ """读取页面区域,缺失或非法时返回文档级回退。"""
79
+ area = first_descendant(root, "Area")
80
+ if area is None:
81
+ area = first_descendant(root, "PageArea")
82
+ value = parse_st_box(element_text(first_descendant(area, name))) if area is not None else None
83
+ return value or fallback
84
+
85
+ def _template_refs(self, document_part: str, document_root: etree._Element) -> dict[int, TemplateRef]:
86
+ """读取 CommonData 中的模板页面映射。"""
87
+ common_data = first_descendant(document_root, "CommonData")
88
+ result: dict[int, TemplateRef] = {}
89
+ if common_data is None:
90
+ return result
91
+ for element in common_data.iter():
92
+ if local_name(element.tag) != "TemplatePage":
93
+ continue
94
+ template_id = parse_int(element.get("ID"))
95
+ page_part = self.package.resolve_reference(document_part, element.get("BaseLoc"))
96
+ if template_id is not None and page_part is not None:
97
+ result[template_id] = TemplateRef(template_id=template_id, page_part=page_part)
98
+ return result
99
+
100
+ def _page_refs(self, document_part: str, document_root: etree._Element) -> list[PageRef]:
101
+ """按 Document.xml 的 Pages 子节点顺序读取页面引用。"""
102
+ pages = first_descendant(document_root, "Pages")
103
+ if pages is None:
104
+ raise OfdParseError(f"Malformed OFD package: {document_part!r} has no Pages")
105
+ result: list[PageRef] = []
106
+ for element in pages:
107
+ if local_name(element.tag) != "Page":
108
+ continue
109
+ self._page_count += 1
110
+ if self._page_count > MAX_PAGE_COUNT:
111
+ raise OfdResourceLimitError(f"OFD resource limit exceeded: max_page_count={MAX_PAGE_COUNT}")
112
+ page_part = self.package.resolve_reference(document_part, element.get("BaseLoc"))
113
+ if page_part is None or not self.package.has_part(page_part):
114
+ raise OfdParseError(f"Malformed OFD package: invalid page BaseLoc in {document_part!r}")
115
+ result.append(PageRef(page_id=parse_int(element.get("ID")), page_part=page_part))
116
+ return result
117
+
118
+ def _build_document(
119
+ self,
120
+ document_part: str,
121
+ document_root: etree._Element,
122
+ page_index_offset: int,
123
+ ) -> list[OfdPageScene]:
124
+ """构造单个 Document.xml 声明的全部页面。"""
125
+ common_data = first_descendant(document_root, "CommonData")
126
+ default_physical = self._page_area(common_data, None, "PhysicalBox") if common_data is not None else None
127
+ default_content = self._page_area(common_data, None, "ContentBox") if common_data is not None else None
128
+ document_resources = self._document_resources(document_part, document_root)
129
+ templates = self._template_refs(document_part, document_root)
130
+ scenes: list[OfdPageScene] = []
131
+ for local_page_idx, page_ref in enumerate(self._page_refs(document_part, document_root)):
132
+ page_root = self._required_ofd_xml(page_ref.page_part)
133
+ physical_box = self._page_area(page_root, default_physical, "PhysicalBox")
134
+ if physical_box is None:
135
+ raise OfdParseError(f"Malformed OFD package: page {page_ref.page_part!r} has no PhysicalBox")
136
+ content_box = self._page_area(page_root, default_content, "ContentBox")
137
+ page_resource_element = first_descendant(page_root, "PageRes")
138
+ page_resource_part = self._resolve_part(
139
+ page_ref.page_part,
140
+ page_resource_element,
141
+ required=page_resource_element is not None,
142
+ )
143
+ resources = merge_registries(document_resources, parse_resource_part(self.package, page_resource_part))
144
+ scene = OfdPageScene(
145
+ page_idx=page_index_offset + local_page_idx,
146
+ physical_box=physical_box,
147
+ content_box=content_box,
148
+ )
149
+ base_context = PageBuildContext(
150
+ transform=Affine(),
151
+ clip_bbox=physical_box,
152
+ layer_type="Body",
153
+ template_id=None,
154
+ draw_style={},
155
+ )
156
+ template_uses = [element for element in page_root.iter() if local_name(element.tag) == "Template"]
157
+ for template_use in template_uses:
158
+ if (template_use.get("ZOrder") or "Background").casefold() == "foreground":
159
+ continue
160
+ self._append_template(template_use, templates, resources, scene, base_context)
161
+ content = first_descendant(page_root, "Content")
162
+ if content is not None:
163
+ self._walk(content, base_context, resources, scene, depth=0, composite_stack=frozenset())
164
+ for template_use in template_uses:
165
+ if (template_use.get("ZOrder") or "Background").casefold() != "foreground":
166
+ continue
167
+ self._append_template(template_use, templates, resources, scene, base_context)
168
+ scenes.append(scene)
169
+ return scenes
170
+
171
+ def _append_template(
172
+ self,
173
+ template_use: etree._Element,
174
+ templates: dict[int, TemplateRef],
175
+ resources: ResourceRegistry,
176
+ scene: OfdPageScene,
177
+ base_context: PageBuildContext,
178
+ ) -> None:
179
+ """把一个页面引用的模板内容合并到当前场景。"""
180
+ template_id = parse_int(template_use.get("TemplateID"))
181
+ template_ref = templates.get(template_id) if template_id is not None else None
182
+ if template_ref is None:
183
+ logger.warning(f"OFD_TEMPLATE_MISSING: template_id={template_id}")
184
+ return
185
+ template_root = self._required_ofd_xml(template_ref.page_part)
186
+ template_resource_element = first_descendant(template_root, "PageRes")
187
+ template_resource_part = self._resolve_part(
188
+ template_ref.page_part,
189
+ template_resource_element,
190
+ required=template_resource_element is not None,
191
+ )
192
+ template_resources = merge_registries(resources, parse_resource_part(self.package, template_resource_part))
193
+ content = first_descendant(template_root, "Content")
194
+ if content is None:
195
+ return
196
+ context = replace(base_context, template_id=template_id)
197
+ self._walk(content, context, template_resources, scene, depth=0, composite_stack=frozenset())
198
+
199
+ def _child_context(self, element: etree._Element, context: PageBuildContext) -> PageBuildContext | None:
200
+ """为 PageBlock 或 CompositeObject 计算子级变换和裁剪。"""
201
+ boundary = parse_st_box(element.get("Boundary"))
202
+ if boundary is None:
203
+ return context
204
+ boundary_page = transform_bbox(boundary, context.transform)
205
+ if boundary_page is None:
206
+ return None
207
+ clip = bbox_intersection(context.clip_bbox, boundary_page)
208
+ if clip is None:
209
+ return None
210
+ transform = context.transform.compose(Affine.translation(boundary[0], boundary[1])).compose(
211
+ parse_affine(element.get("CTM"))
212
+ )
213
+ return replace(context, transform=transform, clip_bbox=clip)
214
+
215
+ def _resolved_style(
216
+ self,
217
+ element: etree._Element,
218
+ context: PageBuildContext,
219
+ resources: ResourceRegistry,
220
+ ) -> dict[str, str]:
221
+ """按父上下文、DrawParam 和对象直接属性解析最终样式。"""
222
+ style = dict(context.draw_style)
223
+ try:
224
+ style.update(resolve_draw_param(resources, parse_int(element.get("DrawParam"))))
225
+ except OfdResourceLimitError:
226
+ raise
227
+ except ValueError as exc:
228
+ raise OfdParseError(str(exc)) from exc
229
+ style.update({str(key): str(value) for key, value in element.attrib.items()})
230
+ return style
231
+
232
+ def _walk(
233
+ self,
234
+ element: etree._Element,
235
+ context: PageBuildContext,
236
+ resources: ResourceRegistry,
237
+ scene: OfdPageScene,
238
+ *,
239
+ depth: int,
240
+ composite_stack: frozenset[int],
241
+ ) -> None:
242
+ """按绘制顺序递归展开页面、模板和复合图元对象。"""
243
+ if depth > MAX_OBJECT_RECURSION:
244
+ raise OfdResourceLimitError(f"OFD resource limit exceeded: max_object_recursion={MAX_OBJECT_RECURSION}")
245
+ for child in element:
246
+ if not isinstance(child.tag, str):
247
+ continue
248
+ name = local_name(child.tag)
249
+ child_context = context
250
+ if name == "Layer":
251
+ child_context = replace(
252
+ context,
253
+ layer_type=(child.get("Type") or "Body"),
254
+ draw_style=self._resolved_style(child, context, resources),
255
+ )
256
+ elif name == "PageBlock":
257
+ resolved_context = self._child_context(child, context)
258
+ if resolved_context is None:
259
+ continue
260
+ child_context = replace(
261
+ resolved_context,
262
+ draw_style=self._resolved_style(child, context, resources),
263
+ )
264
+ if name == "TextObject":
265
+ lines = build_text_lines(
266
+ child,
267
+ parent_transform=context.transform,
268
+ parent_clip=context.clip_bbox,
269
+ resources=resources,
270
+ package=self.package,
271
+ font_metrics=self.font_metrics,
272
+ budget=self.text_budget,
273
+ paint_order=self._paint_order,
274
+ layer_type=context.layer_type,
275
+ template_id=context.template_id,
276
+ resolved_style=self._resolved_style(child, context, resources),
277
+ )
278
+ scene.text_lines.extend(lines)
279
+ self._paint_order += max(1, len(lines))
280
+ continue
281
+ if name == "PathObject":
282
+ scene.axis_lines.extend(
283
+ build_axis_lines(
284
+ child,
285
+ parent_transform=context.transform,
286
+ parent_clip=context.clip_bbox,
287
+ paint_order=self._paint_order,
288
+ template_id=context.template_id,
289
+ budget=self.path_budget,
290
+ resolved_style=self._resolved_style(child, context, resources),
291
+ )
292
+ )
293
+ self._paint_order += 1
294
+ continue
295
+ if name == "ImageObject":
296
+ image = build_image_item(
297
+ child,
298
+ parent_transform=context.transform,
299
+ parent_clip=context.clip_bbox,
300
+ resources=resources,
301
+ package=self.package,
302
+ paint_order=self._paint_order,
303
+ layer_type=context.layer_type,
304
+ template_id=context.template_id,
305
+ resolved_style=self._resolved_style(child, context, resources),
306
+ )
307
+ if image is not None:
308
+ scene.images.append(image)
309
+ self._paint_order += 1
310
+ continue
311
+ if name == "CompositeObject":
312
+ resource_id = parse_int(child.get("ResourceID"))
313
+ composite = resources.composites.get(resource_id) if resource_id is not None else None
314
+ if composite is None:
315
+ logger.warning(f"OFD_COMPOSITE_MISSING: resource_id={resource_id}")
316
+ continue
317
+ if resource_id in composite_stack:
318
+ raise OfdParseError(f"OFD CompositeObject cycle detected at id={resource_id}")
319
+ resolved_context = self._child_context(child, context)
320
+ if resolved_context is None:
321
+ continue
322
+ resolved_context = replace(
323
+ resolved_context,
324
+ draw_style=self._resolved_style(child, context, resources),
325
+ )
326
+ content = first_descendant(composite.element, "Content")
327
+ if content is not None:
328
+ self._walk(
329
+ content,
330
+ resolved_context,
331
+ resources,
332
+ scene,
333
+ depth=depth + 1,
334
+ composite_stack=composite_stack | {resource_id},
335
+ )
336
+ continue
337
+ self._walk(
338
+ child,
339
+ child_context,
340
+ resources,
341
+ scene,
342
+ depth=depth + 1,
343
+ composite_stack=composite_stack,
344
+ )
345
+
346
+
347
+ __all__ = ["OfdSceneBuilder"]
@@ -0,0 +1,240 @@
1
+ """基于 OFD 原生横竖线与文字框恢复高置信全线表。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from ....schema import BBox
8
+ from .constants import MAX_TABLE_INTERSECTION_CHECKS
9
+ from .errors import OfdResourceLimitError
10
+ from .geometry import bbox_area, bbox_center, bbox_intersection
11
+ from .models import AxisLine, TextLine
12
+ from .text import format_line_html
13
+
14
+ _COORD_TOLERANCE = 0.6
15
+ _INTERSECTION_TOLERANCE = 0.8
16
+ _MAX_GRID_CELLS = 10_000
17
+ _MAX_TABLE_AXIS_LINES = 2_000
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class OfdTableRegion:
22
+ """保存一个已物化 OFD 表格及其消耗的文字行。"""
23
+
24
+ bbox: BBox
25
+ html: str
26
+ paint_order: int
27
+ consumed_line_ids: frozenset[int]
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class OfdTableBudget:
32
+ """累计限制整份 OFD 的表格线段相交比较次数。"""
33
+
34
+ intersection_check_count: int = 0
35
+
36
+ def charge_intersection_check(self) -> None:
37
+ """累计一次线段相交比较并在超限时失败。"""
38
+ self.intersection_check_count += 1
39
+ if self.intersection_check_count > MAX_TABLE_INTERSECTION_CHECKS:
40
+ raise OfdResourceLimitError(
41
+ f"OFD resource limit exceeded: max_table_intersection_checks={MAX_TABLE_INTERSECTION_CHECKS}"
42
+ )
43
+
44
+
45
+ def _line_coord(line: AxisLine) -> float:
46
+ """返回轴向线段在垂直方向上的中心坐标。"""
47
+ if line.orientation == "horizontal":
48
+ return (line.bbox[1] + line.bbox[3]) / 2.0
49
+ return (line.bbox[0] + line.bbox[2]) / 2.0
50
+
51
+
52
+ def _line_interval(line: AxisLine) -> tuple[float, float]:
53
+ """返回轴向线段沿自身方向的区间。"""
54
+ return (line.bbox[0], line.bbox[2]) if line.orientation == "horizontal" else (line.bbox[1], line.bbox[3])
55
+
56
+
57
+ def _touches(first: AxisLine, second: AxisLine) -> bool:
58
+ """判断两条轴向线段是否相交或共线连接。"""
59
+ if first.orientation == second.orientation:
60
+ if abs(_line_coord(first) - _line_coord(second)) > _COORD_TOLERANCE:
61
+ return False
62
+ first_interval = _line_interval(first)
63
+ second_interval = _line_interval(second)
64
+ return min(first_interval[1], second_interval[1]) + _INTERSECTION_TOLERANCE >= max(
65
+ first_interval[0], second_interval[0]
66
+ )
67
+ horizontal = first if first.orientation == "horizontal" else second
68
+ vertical = second if first.orientation == "horizontal" else first
69
+ x = _line_coord(vertical)
70
+ y = _line_coord(horizontal)
71
+ return (
72
+ horizontal.bbox[0] - _INTERSECTION_TOLERANCE <= x <= horizontal.bbox[2] + _INTERSECTION_TOLERANCE
73
+ and vertical.bbox[1] - _INTERSECTION_TOLERANCE <= y <= vertical.bbox[3] + _INTERSECTION_TOLERANCE
74
+ )
75
+
76
+
77
+ def _components(lines: list[AxisLine], budget: OfdTableBudget) -> list[list[AxisLine]]:
78
+ """按线段相交关系构造确定性的连通分量。"""
79
+ parents = list(range(len(lines)))
80
+
81
+ def find(index: int) -> int:
82
+ """查找并压缩一个并查集根节点。"""
83
+ while parents[index] != index:
84
+ parents[index] = parents[parents[index]]
85
+ index = parents[index]
86
+ return index
87
+
88
+ def union(first: int, second: int) -> None:
89
+ """合并两个线段分量。"""
90
+ first_root = find(first)
91
+ second_root = find(second)
92
+ if first_root != second_root:
93
+ parents[max(first_root, second_root)] = min(first_root, second_root)
94
+
95
+ for first_index, first in enumerate(lines):
96
+ for second_index in range(first_index + 1, len(lines)):
97
+ budget.charge_intersection_check()
98
+ if _touches(first, lines[second_index]):
99
+ union(first_index, second_index)
100
+ grouped: dict[int, list[AxisLine]] = {}
101
+ for index, line in enumerate(lines):
102
+ grouped.setdefault(find(index), []).append(line)
103
+ return list(grouped.values())
104
+
105
+
106
+ def _cluster_coordinates(values: list[float]) -> list[float]:
107
+ """把近邻线坐标聚合为稳定网格轨道。"""
108
+ clusters: list[list[float]] = []
109
+ for value in sorted(values):
110
+ if not clusters or value - clusters[-1][-1] > _COORD_TOLERANCE:
111
+ clusters.append([value])
112
+ else:
113
+ clusters[-1].append(value)
114
+ return [sum(cluster) / len(cluster) for cluster in clusters]
115
+
116
+
117
+ def _covers_interval(lines: list[AxisLine], start: float, end: float) -> bool:
118
+ """判断共线片段是否基本覆盖给定区间。"""
119
+ intervals = sorted(_line_interval(line) for line in lines)
120
+ cursor = start
121
+ for left, right in intervals:
122
+ if right < cursor - _INTERSECTION_TOLERANCE:
123
+ continue
124
+ if left > cursor + _INTERSECTION_TOLERANCE:
125
+ return False
126
+ cursor = max(cursor, right)
127
+ if cursor >= end - _INTERSECTION_TOLERANCE:
128
+ return True
129
+ return cursor >= end - _INTERSECTION_TOLERANCE
130
+
131
+
132
+ def _component_grid(component: list[AxisLine]) -> tuple[list[float], list[float], BBox] | None:
133
+ """从线段分量恢复有完整外框的网格轨道。"""
134
+ horizontal = [line for line in component if line.orientation == "horizontal"]
135
+ vertical = [line for line in component if line.orientation == "vertical"]
136
+ if len(horizontal) < 2 or len(vertical) < 2:
137
+ return None
138
+ xs = _cluster_coordinates([_line_coord(line) for line in vertical])
139
+ ys = _cluster_coordinates([_line_coord(line) for line in horizontal])
140
+ if len(xs) < 2 or len(ys) < 2 or (len(xs) - 1) * (len(ys) - 1) > _MAX_GRID_CELLS:
141
+ return None
142
+ left, right = xs[0], xs[-1]
143
+ top, bottom = ys[0], ys[-1]
144
+ outer_horizontal = [line for line in horizontal if abs(_line_coord(line) - top) <= _COORD_TOLERANCE]
145
+ outer_horizontal += [line for line in horizontal if abs(_line_coord(line) - bottom) <= _COORD_TOLERANCE]
146
+ outer_vertical = [line for line in vertical if abs(_line_coord(line) - left) <= _COORD_TOLERANCE]
147
+ outer_vertical += [line for line in vertical if abs(_line_coord(line) - right) <= _COORD_TOLERANCE]
148
+ top_ok = _covers_interval(
149
+ [line for line in outer_horizontal if abs(_line_coord(line) - top) <= _COORD_TOLERANCE],
150
+ left,
151
+ right,
152
+ )
153
+ bottom_ok = _covers_interval(
154
+ [line for line in outer_horizontal if abs(_line_coord(line) - bottom) <= _COORD_TOLERANCE], left, right
155
+ )
156
+ left_ok = _covers_interval(
157
+ [line for line in outer_vertical if abs(_line_coord(line) - left) <= _COORD_TOLERANCE],
158
+ top,
159
+ bottom,
160
+ )
161
+ right_ok = _covers_interval(
162
+ [line for line in outer_vertical if abs(_line_coord(line) - right) <= _COORD_TOLERANCE], top, bottom
163
+ )
164
+ if not (top_ok and bottom_ok and left_ok and right_ok):
165
+ return None
166
+ return xs, ys, (left, top, right, bottom)
167
+
168
+
169
+ def _cell_index(values: list[float], coordinate: float) -> int | None:
170
+ """返回坐标所在的相邻轨道区间索引。"""
171
+ for index, (start, end) in enumerate(zip(values, values[1:], strict=True)):
172
+ if start - _COORD_TOLERANCE <= coordinate <= end + _COORD_TOLERANCE:
173
+ return index
174
+ return None
175
+
176
+
177
+ def _serialize_grid(xs: list[float], ys: list[float], lines: list[TextLine]) -> str:
178
+ """把网格内文字按单元格序列化为安全 HTML。"""
179
+ cells: dict[tuple[int, int], list[TextLine]] = {}
180
+ for line in lines:
181
+ center_x, center_y = bbox_center(line.bbox)
182
+ column = _cell_index(xs, center_x)
183
+ row = _cell_index(ys, center_y)
184
+ if row is None or column is None:
185
+ continue
186
+ cells.setdefault((row, column), []).append(line)
187
+ output = ["<table>"]
188
+ for row in range(len(ys) - 1):
189
+ output.append("<tr>")
190
+ for column in range(len(xs) - 1):
191
+ items = sorted(cells.get((row, column), []), key=lambda item: (item.bbox[1], item.bbox[0], item.paint_order))
192
+ content = "<br>".join(format_line_html(item.text, item.styles) for item in items if item.text.strip())
193
+ output.append(f"<td>{content}</td>")
194
+ output.append("</tr>")
195
+ output.append("</table>")
196
+ return "".join(output)
197
+
198
+
199
+ def recover_tables(
200
+ axis_lines: list[AxisLine],
201
+ text_lines: list[TextLine],
202
+ budget: OfdTableBudget,
203
+ ) -> list[OfdTableRegion]:
204
+ """从页面轴向线段中恢复互不重叠的高置信表格。"""
205
+ if len(axis_lines) < 4 or len(text_lines) < 2 or len(axis_lines) > _MAX_TABLE_AXIS_LINES:
206
+ return []
207
+ candidates: list[OfdTableRegion] = []
208
+ for component in _components(axis_lines, budget):
209
+ grid = _component_grid(component)
210
+ if grid is None:
211
+ continue
212
+ xs, ys, bbox = grid
213
+ contained: list[TextLine] = []
214
+ for line in text_lines:
215
+ center_x, center_y = bbox_center(line.bbox)
216
+ if (
217
+ bbox_intersection(line.bbox, bbox) is not None
218
+ and bbox[0] <= center_x <= bbox[2]
219
+ and bbox[1] <= center_y <= bbox[3]
220
+ ):
221
+ contained.append(line)
222
+ if len(contained) < 2:
223
+ continue
224
+ candidates.append(
225
+ OfdTableRegion(
226
+ bbox=bbox,
227
+ html=_serialize_grid(xs, ys, contained),
228
+ paint_order=min((line.paint_order for line in component), default=0),
229
+ consumed_line_ids=frozenset(id(line) for line in contained),
230
+ )
231
+ )
232
+ selected: list[OfdTableRegion] = []
233
+ for candidate in sorted(candidates, key=lambda item: (-bbox_area(item.bbox), item.bbox[1], item.bbox[0])):
234
+ if any(bbox_intersection(candidate.bbox, existing.bbox) is not None for existing in selected):
235
+ continue
236
+ selected.append(candidate)
237
+ return sorted(selected, key=lambda item: (item.bbox[1], item.bbox[0], item.paint_order))
238
+
239
+
240
+ __all__ = ["OfdTableBudget", "OfdTableRegion", "recover_tables"]