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,609 @@
1
+ """编排 Native PDF 表格多候选生成、欠分割诊断和高置信仲裁。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Iterable
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from .candidate import serialize_candidate_html
11
+ from .contracts import (
12
+ NativeTableCandidate,
13
+ NativeTableInput,
14
+ NativeTableRectangle,
15
+ NativeTableResult,
16
+ NativeTableRule,
17
+ NativeTableText,
18
+ )
19
+ from .geometry import bbox_intersection, normalize_bbox
20
+ from .sparse_hybrid import build_sparse_hybrid_candidates, diagnose_sparse_hybrid_candidate_builds
21
+ from .sparse_multiline import build_sparse_multiline_candidates, diagnose_sparse_multiline_candidate_builds
22
+ from .text import build_native_table_text
23
+ from .text_grid import build_text_candidates, diagnose_text_candidate_builds
24
+ from .vector import MAX_PRIMITIVES_PER_TABLE, build_vector_candidates, diagnose_vector_candidate_builds
25
+
26
+ MIN_TOPOLOGY_SCORE_GAP = 0.05
27
+ _SOURCE_PRIORITY = {
28
+ "vector_grid": 6,
29
+ "sparse_hybrid": 5,
30
+ "sparse_grid": 4,
31
+ "key_value": 3,
32
+ "text_grid": 2,
33
+ "sparse_multiline": 1,
34
+ }
35
+ _VERIFIED_SCORE_BY_SOURCE = {
36
+ "vector_grid": 0.95,
37
+ "sparse_hybrid": 0.98,
38
+ "sparse_multiline": 0.98,
39
+ "sparse_grid": 0.95,
40
+ "key_value": 0.95,
41
+ "text_grid": 0.95,
42
+ }
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class _CandidateEvaluation:
47
+ """保存一次生产决策及调试工具需要的中间候选。"""
48
+
49
+ primitive_count: int
50
+ text: NativeTableText | None
51
+ generated_candidates: tuple[NativeTableCandidate, ...]
52
+ candidates: tuple[NativeTableCandidate, ...]
53
+ selected: NativeTableCandidate | None
54
+ physical_topology_conflict: bool
55
+ first_rejection_gate: str | None
56
+
57
+
58
+ def _is_verified_line_candidate(candidate: NativeTableCandidate) -> bool:
59
+ """判断候选是否由无歧义 drawing 网格独立验证。"""
60
+
61
+ return (
62
+ candidate.source == "vector_grid"
63
+ and candidate.score >= 0.95
64
+ and "evidence=line_grid" in candidate.issues
65
+ and "ambiguous_separator_ratio=0.0000" in candidate.issues
66
+ )
67
+
68
+
69
+ def _is_verified_rect_candidate(candidate: NativeTableCandidate) -> bool:
70
+ """判断候选是否由无歧义矩形晶格独立验证。"""
71
+
72
+ return (
73
+ candidate.source == "vector_grid"
74
+ and candidate.score >= 0.95
75
+ and "evidence=rect_grid" in candidate.issues
76
+ and "ambiguous_separator_ratio=0.0000" in candidate.issues
77
+ )
78
+
79
+
80
+ def _has_line_rect_topology_conflict(
81
+ candidates: Iterable[NativeTableCandidate],
82
+ ) -> bool:
83
+ """判断两类独立物理证据是否给出不同拓扑。"""
84
+
85
+ materialized = list(candidates)
86
+ line_candidates = [candidate for candidate in materialized if _is_verified_line_candidate(candidate)]
87
+ rect_candidates = [candidate for candidate in materialized if _is_verified_rect_candidate(candidate)]
88
+ return any(line.topology != rect.topology for line in line_candidates for rect in rect_candidates)
89
+
90
+
91
+ def _has_attempted_line_rect_grid_conflict(
92
+ attempts: Iterable[dict[str, Any]],
93
+ ) -> bool:
94
+ """判断已恢复轨道的 line/rect 物理假设是否在行列数上冲突。"""
95
+
96
+ materialized = list(attempts)
97
+ line_grids = [attempt.get("grid") for attempt in materialized if attempt.get("evidence") == "line_grid"]
98
+ rect_grids = [attempt.get("grid") for attempt in materialized if attempt.get("evidence") == "rect_grid"]
99
+ return any(
100
+ isinstance(line, dict)
101
+ and isinstance(rect, dict)
102
+ and (line.get("rows"), line.get("cols")) != (rect.get("rows"), rect.get("cols"))
103
+ for line in line_grids
104
+ for rect in rect_grids
105
+ )
106
+
107
+
108
+ def _resolved_rect_undercount_candidate(
109
+ candidates: Iterable[NativeTableCandidate],
110
+ attempts: Iterable[dict[str, Any]],
111
+ text: NativeTableText,
112
+ ) -> NativeTableCandidate | None:
113
+ """在线网格明确漏行且矩形晶格逐行吻合时允许 rect 独立胜出。"""
114
+
115
+ rect_candidates = [candidate for candidate in candidates if _is_verified_rect_candidate(candidate)]
116
+ if len(rect_candidates) != 1:
117
+ return None
118
+ rect_candidate = rect_candidates[0]
119
+ line_attempts = [attempt for attempt in attempts if attempt.get("evidence") == "line_grid"]
120
+ has_matching_undercount = any(
121
+ attempt.get("first_rejection_gate") == "physical_row_undercount"
122
+ and isinstance(attempt.get("grid"), dict)
123
+ and attempt["grid"].get("cols") == rect_candidate.cols
124
+ and attempt["grid"].get("rows", 0) < rect_candidate.rows
125
+ for attempt in line_attempts
126
+ )
127
+ if not has_matching_undercount:
128
+ return None
129
+ if rect_candidate.rows != len(text.rows) or rect_candidate.text_capture < 1.0 or rect_candidate.order_consistency < 1.0:
130
+ return None
131
+ return rect_candidate
132
+
133
+
134
+ def _passes_verified_threshold(candidate: NativeTableCandidate) -> bool:
135
+ """按候选来源应用独立校准的 verified 可靠度门槛。"""
136
+
137
+ return candidate.score >= _VERIFIED_SCORE_BY_SOURCE[candidate.source]
138
+
139
+
140
+ def _read_value(item: object, name: str, default: Any = None) -> Any:
141
+ """同时读取普通对象属性和字典字段,供页面原语适配使用。"""
142
+
143
+ if isinstance(item, dict):
144
+ return item.get(name, default)
145
+ return getattr(item, name, default)
146
+
147
+
148
+ def coerce_native_table_rules(
149
+ drawing_lines: Iterable[object],
150
+ ) -> tuple[NativeTableRule, ...]:
151
+ """把 PDFDocument 或 Flash drawing 结果转换成共享横竖线契约。"""
152
+
153
+ rules: list[NativeTableRule] = []
154
+ for drawing_line in drawing_lines:
155
+ bbox = normalize_bbox(_read_value(drawing_line, "bbox"))
156
+ orientation = str(_read_value(drawing_line, "orientation", ""))
157
+ if bbox is None or orientation not in {"horizontal", "vertical"}:
158
+ continue
159
+ try:
160
+ width = max(0.0, float(_read_value(drawing_line, "width", 0.0) or 0.0))
161
+ except (TypeError, ValueError):
162
+ width = 0.0
163
+ rules.append(
164
+ NativeTableRule(
165
+ bbox=bbox,
166
+ width=width,
167
+ orientation=orientation, # type: ignore[arg-type]
168
+ )
169
+ )
170
+ return tuple(rules)
171
+
172
+
173
+ def coerce_native_table_rectangles(
174
+ path_infos: Iterable[object],
175
+ ) -> tuple[NativeTableRectangle, ...]:
176
+ """把 PDF Path 摘要转换成共享矩形路径契约。"""
177
+
178
+ rectangles: list[NativeTableRectangle] = []
179
+ for path_info in path_infos:
180
+ bbox = normalize_bbox(_read_value(path_info, "bbox"))
181
+ if bbox is None:
182
+ continue
183
+ try:
184
+ segment_count = int(_read_value(path_info, "segment_count", 0) or 0)
185
+ form_depth = int(_read_value(path_info, "form_depth", 0) or 0)
186
+ except (TypeError, ValueError):
187
+ continue
188
+ rectangles.append(
189
+ NativeTableRectangle(
190
+ bbox=bbox,
191
+ segment_count=segment_count,
192
+ fill_visible=bool(_read_value(path_info, "fill_visible", False)),
193
+ stroke_visible=bool(_read_value(path_info, "stroke_visible", False)),
194
+ form_depth=form_depth,
195
+ )
196
+ )
197
+ return tuple(rectangles)
198
+
199
+
200
+ def _remove_undercounted_vector_candidates(
201
+ candidates: list[NativeTableCandidate],
202
+ ) -> list[NativeTableCandidate]:
203
+ """当稳定文本候选显著多出行列时,剔除欠分割矢量候选。"""
204
+
205
+ text_candidates = [
206
+ candidate
207
+ for candidate in candidates
208
+ if candidate.source != "vector_grid" and candidate.row_stability >= 0.80 and candidate.column_stability >= 0.80
209
+ ]
210
+ if not text_candidates:
211
+ return candidates
212
+ output: list[NativeTableCandidate] = []
213
+ for candidate in candidates:
214
+ if candidate.source != "vector_grid":
215
+ output.append(candidate)
216
+ continue
217
+ if _is_verified_line_candidate(candidate):
218
+ output.append(candidate)
219
+ continue
220
+ undercounted = any(
221
+ text_candidate.rows >= math.ceil(1.5 * candidate.rows) or text_candidate.cols >= math.ceil(1.3 * candidate.cols)
222
+ for text_candidate in text_candidates
223
+ )
224
+ if not undercounted:
225
+ output.append(candidate)
226
+ return output
227
+
228
+
229
+ def _has_alias_affected_physical_blank_row(
230
+ vector_attempts: tuple[dict[str, Any], ...],
231
+ ) -> bool:
232
+ """判断强线框空白行是否因自身 alias 风险而禁止文本候选绕过。"""
233
+
234
+ for attempt in vector_attempts:
235
+ if attempt.get("evidence") != "line_grid":
236
+ continue
237
+ hypotheses = [attempt, *attempt.get("track_hypotheses", [])]
238
+ for hypothesis in hypotheses:
239
+ if hypothesis.get("first_rejection_gate") != "empty_row":
240
+ continue
241
+ empty_rows = set(hypothesis.get("empty_rows", []))
242
+ affected_rows = set(hypothesis.get("alias_affected_rows", []))
243
+ if empty_rows.intersection(affected_rows):
244
+ return True
245
+ return False
246
+
247
+
248
+ def _has_physical_row_undercount(
249
+ vector_attempts: tuple[dict[str, Any], ...],
250
+ ) -> bool:
251
+ """判断 line-grid 及其有限轨道假设是否已发现物理行欠分割。"""
252
+
253
+ for attempt in vector_attempts:
254
+ if attempt.get("evidence") != "line_grid":
255
+ continue
256
+ hypotheses = [attempt, *attempt.get("track_hypotheses", [])]
257
+ if any(hypothesis.get("first_rejection_gate") == "physical_row_undercount" for hypothesis in hypotheses):
258
+ return True
259
+ return False
260
+
261
+
262
+ def _table_primitive_count(table_input: NativeTableInput) -> int:
263
+ """统计实际与目标表格相交的 drawing 和矩形数量。"""
264
+
265
+ table_bbox = normalize_bbox(table_input.table_bbox)
266
+ if table_bbox is None:
267
+ return 0
268
+ count = 0
269
+ for primitive in (*table_input.drawing_lines, *table_input.rectangles):
270
+ primitive_bbox = normalize_bbox(primitive.bbox)
271
+ if primitive_bbox is not None and bbox_intersection(primitive_bbox, table_bbox) is not None:
272
+ count += 1
273
+ return count
274
+
275
+
276
+ def _select_candidate(
277
+ candidates: list[NativeTableCandidate],
278
+ ) -> NativeTableCandidate | None:
279
+ """选择达到生产门槛且未与近分异构候选冲突的最佳结果。"""
280
+
281
+ accepted = [candidate for candidate in candidates if _passes_verified_threshold(candidate)]
282
+ if not accepted:
283
+ return None
284
+ if _has_line_rect_topology_conflict(accepted):
285
+ return None
286
+ verified_line_candidates = [candidate for candidate in accepted if _is_verified_line_candidate(candidate)]
287
+ if len(verified_line_candidates) == 1:
288
+ return verified_line_candidates[0]
289
+ accepted.sort(
290
+ key=lambda candidate: (
291
+ candidate.score,
292
+ _SOURCE_PRIORITY[candidate.source],
293
+ ),
294
+ reverse=True,
295
+ )
296
+ best = accepted[0]
297
+ for competitor in accepted[1:]:
298
+ if competitor.topology == best.topology:
299
+ continue
300
+ if best.score - competitor.score < MIN_TOPOLOGY_SCORE_GAP:
301
+ return None
302
+ return best
303
+
304
+
305
+ def _evaluate_native_pdf_table(
306
+ table_input: NativeTableInput,
307
+ ) -> _CandidateEvaluation:
308
+ """执行共享生产判定,并保留候选生成到仲裁的完整阶段结果。"""
309
+
310
+ table_bbox = normalize_bbox(table_input.table_bbox)
311
+ page_width, page_height = table_input.page_size
312
+ primitive_count = _table_primitive_count(table_input)
313
+ if (
314
+ table_bbox is None
315
+ or page_width <= 0
316
+ or page_height <= 0
317
+ or table_bbox[0] < 0
318
+ or table_bbox[1] < 0
319
+ or table_bbox[2] > page_width
320
+ or table_bbox[3] > page_height
321
+ ):
322
+ return _CandidateEvaluation(
323
+ primitive_count,
324
+ None,
325
+ (),
326
+ (),
327
+ None,
328
+ False,
329
+ "input_geometry",
330
+ )
331
+ if primitive_count > MAX_PRIMITIVES_PER_TABLE:
332
+ return _CandidateEvaluation(
333
+ primitive_count,
334
+ None,
335
+ (),
336
+ (),
337
+ None,
338
+ False,
339
+ "primitive_limit",
340
+ )
341
+ text = build_native_table_text(table_input)
342
+ if text is None:
343
+ return _CandidateEvaluation(
344
+ primitive_count,
345
+ None,
346
+ (),
347
+ (),
348
+ None,
349
+ False,
350
+ "native_text",
351
+ )
352
+ vector_attempt_records: list[dict[str, Any]] = []
353
+ vector_candidates = build_vector_candidates(
354
+ table_input,
355
+ text,
356
+ diagnostics=vector_attempt_records,
357
+ )
358
+ vector_attempts: tuple[dict[str, Any], ...] = tuple(vector_attempt_records)
359
+ physical_topology_conflict = False
360
+ if any(_is_verified_rect_candidate(candidate) for candidate in vector_candidates):
361
+ physical_topology_conflict = _has_attempted_line_rect_grid_conflict(vector_attempts)
362
+ if (
363
+ physical_topology_conflict
364
+ and _resolved_rect_undercount_candidate(
365
+ vector_candidates,
366
+ vector_attempts,
367
+ text,
368
+ )
369
+ is not None
370
+ ):
371
+ physical_topology_conflict = False
372
+ vector_selection = None if physical_topology_conflict else _select_candidate(vector_candidates)
373
+ sparse_hybrid_allowed = len(text.rows) >= 2 and vector_selection is None and not physical_topology_conflict
374
+ if sparse_hybrid_allowed:
375
+ if not vector_attempts:
376
+ vector_attempts = diagnose_vector_candidate_builds(
377
+ table_input,
378
+ text,
379
+ )
380
+ if _has_alias_affected_physical_blank_row(vector_attempts):
381
+ sparse_hybrid_allowed = False
382
+ sparse_hybrid_candidates = build_sparse_hybrid_candidates(table_input, text) if sparse_hybrid_allowed else []
383
+ sparse_hybrid_selection = _select_candidate(sparse_hybrid_candidates)
384
+ text_candidates = (
385
+ build_text_candidates(table_input, text)
386
+ if len(text.rows) >= 2 and vector_selection is None and sparse_hybrid_selection is None
387
+ else []
388
+ )
389
+ if text_candidates and (not vector_candidates or physical_topology_conflict):
390
+ if not vector_attempts:
391
+ vector_attempts = diagnose_vector_candidate_builds(
392
+ table_input,
393
+ text,
394
+ )
395
+ if _has_alias_affected_physical_blank_row(vector_attempts) or _has_physical_row_undercount(vector_attempts):
396
+ text_candidates = []
397
+ existing_generated_candidates = [
398
+ *vector_candidates,
399
+ *sparse_hybrid_candidates,
400
+ *text_candidates,
401
+ ]
402
+ existing_candidates = _remove_undercounted_vector_candidates(existing_generated_candidates)
403
+ existing_verified = any(_passes_verified_threshold(candidate) for candidate in existing_candidates)
404
+ sparse_multiline_allowed = len(text.rows) >= 2 and not physical_topology_conflict and not existing_verified
405
+ if sparse_multiline_allowed:
406
+ if not vector_attempts:
407
+ vector_attempts = diagnose_vector_candidate_builds(
408
+ table_input,
409
+ text,
410
+ )
411
+ if _has_alias_affected_physical_blank_row(vector_attempts):
412
+ sparse_multiline_allowed = False
413
+ sparse_multiline_candidates = build_sparse_multiline_candidates(table_input, text) if sparse_multiline_allowed else []
414
+ generated_candidates = [
415
+ *existing_generated_candidates,
416
+ *sparse_multiline_candidates,
417
+ ]
418
+ candidates = _remove_undercounted_vector_candidates(generated_candidates)
419
+ selected = None if physical_topology_conflict else _select_candidate(candidates)
420
+ if selected is not None:
421
+ first_rejection_gate = None
422
+ elif not generated_candidates:
423
+ first_rejection_gate = "candidate_generation"
424
+ elif not candidates:
425
+ first_rejection_gate = "undercount_guard"
426
+ elif not any(_passes_verified_threshold(candidate) for candidate in candidates):
427
+ first_rejection_gate = "verified_threshold"
428
+ else:
429
+ first_rejection_gate = "topology_conflict"
430
+ return _CandidateEvaluation(
431
+ primitive_count,
432
+ text,
433
+ tuple(generated_candidates),
434
+ tuple(candidates),
435
+ selected,
436
+ physical_topology_conflict,
437
+ first_rejection_gate,
438
+ )
439
+
440
+
441
+ def diagnose_native_pdf_table(table_input: NativeTableInput) -> dict[str, Any]:
442
+ """返回仅供测试评测使用、不会进入用户结果的候选诊断。"""
443
+
444
+ evaluation = _evaluate_native_pdf_table(table_input)
445
+ vector_attempts = (
446
+ diagnose_vector_candidate_builds(
447
+ table_input,
448
+ evaluation.text,
449
+ )
450
+ if evaluation.text is not None
451
+ else ()
452
+ )
453
+ text_attempts = (
454
+ diagnose_text_candidate_builds(
455
+ table_input,
456
+ evaluation.text,
457
+ )
458
+ if evaluation.text is not None
459
+ else ()
460
+ )
461
+ sparse_hybrid_attempts = (
462
+ diagnose_sparse_hybrid_candidate_builds(
463
+ table_input,
464
+ evaluation.text,
465
+ )
466
+ if evaluation.text is not None
467
+ else ()
468
+ )
469
+ sparse_multiline_attempts = (
470
+ diagnose_sparse_multiline_candidate_builds(
471
+ table_input,
472
+ evaluation.text,
473
+ )
474
+ if evaluation.text is not None
475
+ else ()
476
+ )
477
+ first_rejection_gate = evaluation.first_rejection_gate
478
+ if first_rejection_gate == "candidate_generation":
479
+ text_attempt = next(
480
+ (
481
+ attempt
482
+ for attempt in text_attempts
483
+ if attempt.get("first_rejection_gate")
484
+ in {
485
+ "dense_row_ambiguity",
486
+ "header_requires_rowspan",
487
+ "token_split",
488
+ }
489
+ ),
490
+ None,
491
+ )
492
+ line_attempt = next(
493
+ (attempt for attempt in vector_attempts if attempt.get("evidence") == "line_grid"),
494
+ None,
495
+ )
496
+ if text_attempt is not None:
497
+ first_rejection_gate = "text_" + str(text_attempt["first_rejection_gate"])
498
+ elif sparse_attempt := next(
499
+ (attempt for attempt in sparse_hybrid_attempts if attempt.get("first_rejection_gate")),
500
+ None,
501
+ ):
502
+ first_rejection_gate = "sparse_hybrid_" + str(sparse_attempt["first_rejection_gate"])
503
+ elif line_attempt is not None and line_attempt.get("first_rejection_gate"):
504
+ first_rejection_gate = "vector_" + str(line_attempt["first_rejection_gate"])
505
+ elif multiline_attempt := next(
506
+ (attempt for attempt in sparse_multiline_attempts if attempt.get("first_rejection_gate")),
507
+ None,
508
+ ):
509
+ first_rejection_gate = "sparse_multiline_" + str(multiline_attempt["first_rejection_gate"])
510
+
511
+ def candidate_record(
512
+ candidate: NativeTableCandidate,
513
+ ) -> dict[str, Any]:
514
+ """把内部候选转换成稳定且不包含单元格全文的诊断记录。"""
515
+
516
+ return {
517
+ "source": candidate.source,
518
+ "rows": candidate.rows,
519
+ "cols": candidate.cols,
520
+ "tracks": {
521
+ "x": candidate.cols + 1,
522
+ "y": candidate.rows + 1,
523
+ },
524
+ "score": candidate.score,
525
+ "verified": _passes_verified_threshold(candidate),
526
+ "score_components": {
527
+ "text_capture": candidate.text_capture,
528
+ "structure_support": candidate.structure_support,
529
+ "row_stability": candidate.row_stability,
530
+ "column_stability": candidate.column_stability,
531
+ "order_consistency": candidate.order_consistency,
532
+ },
533
+ "span_signature": [
534
+ [cell.row, cell.col, cell.rowspan, cell.colspan]
535
+ for cell in candidate.cells
536
+ if cell.rowspan > 1 or cell.colspan > 1
537
+ ],
538
+ "issues": list(candidate.issues),
539
+ }
540
+
541
+ retained_ids = {id(candidate) for candidate in evaluation.candidates}
542
+ removed_candidates = [
543
+ candidate_record(candidate) for candidate in evaluation.generated_candidates if id(candidate) not in retained_ids
544
+ ]
545
+ counterfactual_best = max(
546
+ evaluation.generated_candidates,
547
+ key=lambda candidate: (
548
+ candidate.score,
549
+ _SOURCE_PRIORITY[candidate.source],
550
+ ),
551
+ default=None,
552
+ )
553
+ return {
554
+ "primitive_count": evaluation.primitive_count,
555
+ "glyph_count": len(evaluation.text.glyphs) if evaluation.text else 0,
556
+ "visual_text_rows": len(evaluation.text.rows) if evaluation.text else 0,
557
+ "first_rejection_gate": first_rejection_gate,
558
+ "line_rect_topology_conflict": (
559
+ evaluation.physical_topology_conflict or _has_line_rect_topology_conflict(evaluation.candidates)
560
+ ),
561
+ "vector_attempts": list(vector_attempts),
562
+ "sparse_hybrid_attempts": list(sparse_hybrid_attempts),
563
+ "sparse_multiline_attempts": list(sparse_multiline_attempts),
564
+ "text_attempts": list(text_attempts),
565
+ "generated_candidates": [candidate_record(candidate) for candidate in evaluation.generated_candidates],
566
+ "removed_by_undercount": removed_candidates,
567
+ "counterfactual_best": (candidate_record(counterfactual_best) if counterfactual_best is not None else None),
568
+ "adopted": (candidate_record(evaluation.selected) if evaluation.selected is not None else None),
569
+ }
570
+
571
+
572
+ def recover_native_pdf_table(
573
+ table_input: NativeTableInput,
574
+ ) -> NativeTableResult | None:
575
+ """对一个已知表格区域运行全部候选并返回高置信 HTML 结果。"""
576
+
577
+ evaluation = _evaluate_native_pdf_table(table_input)
578
+ selected = evaluation.selected
579
+ if selected is None or evaluation.text is None:
580
+ return None
581
+ diagnostics = tuple(
582
+ [
583
+ f"candidate={selected.source}",
584
+ f"score={selected.score:.4f}",
585
+ f"text_capture={selected.text_capture:.4f}",
586
+ f"structure_support={selected.structure_support:.4f}",
587
+ f"row_stability={selected.row_stability:.4f}",
588
+ f"column_stability={selected.column_stability:.4f}",
589
+ f"order_consistency={selected.order_consistency:.4f}",
590
+ ]
591
+ + list(selected.issues)
592
+ )
593
+ return NativeTableResult(
594
+ html=serialize_candidate_html(selected),
595
+ rows=selected.rows,
596
+ cols=selected.cols,
597
+ cells=selected.cells,
598
+ text=evaluation.text,
599
+ source=selected.source,
600
+ confidence=selected.score,
601
+ diagnostics=diagnostics,
602
+ )
603
+
604
+
605
+ __all__ = [
606
+ "coerce_native_table_rectangles",
607
+ "coerce_native_table_rules",
608
+ "recover_native_pdf_table",
609
+ ]