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,618 @@
1
+ """基于稀疏规则、行底纹和原生文本对齐恢复少线或无线表格。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import statistics
7
+ from collections import Counter
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+ from .candidate import GridCellSpec, build_candidate
12
+ from .contracts import NativeTableCandidate, NativeTableCandidateSource, NativeTableInput, NativeTableText, NativeTableTextRow
13
+ from .geometry import bbox_union, normalize_angle, normalize_bbox, page_bbox_to_table_local, table_local_size
14
+
15
+ MIN_COLUMN_ANCHOR_SUPPORT = 0.60
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class _LogicalRowGrouping:
20
+ """保存视觉行分组结果及不宜自动合并的稠密行对。"""
21
+
22
+ rows: tuple[NativeTableTextRow, ...]
23
+ dense_ambiguities: tuple[tuple[int, int], ...]
24
+ subset_merges: tuple[tuple[int, int], ...]
25
+
26
+
27
+ def _infer_target_column_count(text: NativeTableText) -> int | None:
28
+ """从多行文本项数量中选择有重复证据的最大叶子列数。"""
29
+
30
+ counts = [len(row.tokens) for row in text.rows if len(row.tokens) >= 2]
31
+ if not counts:
32
+ return None
33
+ occurrences = Counter(counts)
34
+ for count in sorted(occurrences, reverse=True):
35
+ if occurrences[count] >= 2:
36
+ return count
37
+ return max(counts) if len(text.rows) <= 3 else None
38
+
39
+
40
+ def _infer_column_tracks(
41
+ text: NativeTableText,
42
+ width: float,
43
+ target_cols: int,
44
+ ) -> tuple[list[float], float, float] | None:
45
+ """用最稠密视觉行的相邻文本间隙推断全局叶子列边界。"""
46
+
47
+ dense_rows = [row for row in text.rows if len(row.tokens) == target_cols]
48
+ if len(dense_rows) < 2 and len(text.rows) > 3:
49
+ return None
50
+ if not dense_rows:
51
+ return None
52
+ boundaries: list[float] = []
53
+ gap_supports: list[float] = []
54
+ minimum_gap = max(1.0, 0.15 * text.median_glyph_height)
55
+ for col_index in range(target_cols - 1):
56
+ midpoints: list[float] = []
57
+ valid_gap_count = 0
58
+ for row in dense_rows:
59
+ left_token = row.tokens[col_index]
60
+ right_token = row.tokens[col_index + 1]
61
+ gap = right_token.bbox[0] - left_token.bbox[2]
62
+ if gap >= minimum_gap:
63
+ valid_gap_count += 1
64
+ midpoints.append((left_token.bbox[2] + right_token.bbox[0]) / 2.0)
65
+ if valid_gap_count / len(dense_rows) < MIN_COLUMN_ANCHOR_SUPPORT:
66
+ return None
67
+ boundaries.append(float(statistics.median(midpoints)))
68
+ gap_supports.append(valid_gap_count / len(dense_rows))
69
+ tracks = [0.0, *boundaries, width]
70
+ if any(current <= previous for previous, current in zip(tracks, tracks[1:])):
71
+ return None
72
+
73
+ first_dense_row = min(row.row_index for row in dense_rows)
74
+ boundary_margin = max(0.5, 0.08 * text.median_glyph_height)
75
+ for row in text.rows:
76
+ if row.row_index < first_dense_row:
77
+ continue
78
+ if any(
79
+ token.bbox[0] + boundary_margin < boundary < token.bbox[2] - boundary_margin
80
+ for token in row.tokens
81
+ for boundary in tracks[1:-1]
82
+ ):
83
+ return None
84
+
85
+ supported_rows_by_col: list[set[int]] = [set() for _ in range(target_cols)]
86
+ aligned_tokens = 0
87
+ total_tokens = 0
88
+ for row in text.rows:
89
+ previous_col = -1
90
+ for token in row.tokens:
91
+ center_x = (token.bbox[0] + token.bbox[2]) / 2.0
92
+ col = next(
93
+ (index for index, (left, right) in enumerate(zip(tracks, tracks[1:])) if left <= center_x <= right),
94
+ None,
95
+ )
96
+ total_tokens += 1
97
+ if col is None or col < previous_col:
98
+ continue
99
+ supported_rows_by_col[col].add(row.row_index)
100
+ aligned_tokens += 1
101
+ previous_col = col
102
+ row_count = max(1, len(text.rows))
103
+ anchor_support = sum(len(row_indices) / row_count for row_indices in supported_rows_by_col) / target_cols
104
+ alignment_support = aligned_tokens / total_tokens if total_tokens else 0.0
105
+ if anchor_support < MIN_COLUMN_ANCHOR_SUPPORT:
106
+ return None
107
+ gap_support = float(statistics.mean(gap_supports)) if gap_supports else 1.0
108
+ return tracks, anchor_support, min(alignment_support, gap_support)
109
+
110
+
111
+ def _infer_row_tracks(
112
+ rows: tuple[NativeTableTextRow, ...],
113
+ height: float,
114
+ ) -> list[float] | None:
115
+ """用相邻逻辑行中心的中点构造完整行边界。"""
116
+
117
+ if len(rows) < 2:
118
+ return None
119
+ centers = [(row.bbox[1] + row.bbox[3]) / 2.0 for row in rows]
120
+ boundaries = [(previous + current) / 2.0 for previous, current in zip(centers, centers[1:])]
121
+ tracks = [0.0, *boundaries, height]
122
+ if any(current <= previous for previous, current in zip(tracks, tracks[1:])):
123
+ return None
124
+ return tracks
125
+
126
+
127
+ def _token_columns(
128
+ row: NativeTableTextRow,
129
+ x_tracks: list[float],
130
+ ) -> list[int] | None:
131
+ """把一行文本项按中心点单调映射到叶子列。"""
132
+
133
+ output: list[int] = []
134
+ for token in row.tokens:
135
+ center_x = (token.bbox[0] + token.bbox[2]) / 2.0
136
+ col = next(
137
+ (index for index, (left, right) in enumerate(zip(x_tracks, x_tracks[1:])) if left <= center_x <= right),
138
+ None,
139
+ )
140
+ if col is None or (output and col < output[-1]):
141
+ return None
142
+ output.append(col)
143
+ return output
144
+
145
+
146
+ def _has_horizontal_rule_between(
147
+ table_input: NativeTableInput,
148
+ previous: NativeTableTextRow,
149
+ current: NativeTableTextRow,
150
+ ) -> bool:
151
+ """判断两条视觉文本行之间是否存在贯穿多数表宽的物理横线。"""
152
+
153
+ table_bbox = normalize_bbox(table_input.table_bbox)
154
+ if table_bbox is None:
155
+ return True
156
+ angle = normalize_angle(table_input.angle)
157
+ width, _height = table_local_size(table_bbox, angle)
158
+ upper = previous.bbox[3]
159
+ lower = current.bbox[1]
160
+ for rule in table_input.drawing_lines:
161
+ rule_bbox = normalize_bbox(rule.bbox)
162
+ if rule_bbox is None:
163
+ continue
164
+ local_bbox = page_bbox_to_table_local(rule_bbox, table_bbox, angle)
165
+ if local_bbox is None:
166
+ continue
167
+ local_width = local_bbox[2] - local_bbox[0]
168
+ local_height = local_bbox[3] - local_bbox[1]
169
+ center_y = (local_bbox[1] + local_bbox[3]) / 2.0
170
+ if local_width >= 0.60 * width and local_width >= 4.0 * max(local_height, 0.1) and upper <= center_y <= lower:
171
+ return True
172
+ return False
173
+
174
+
175
+ def _group_logical_rows(
176
+ table_input: NativeTableInput,
177
+ text: NativeTableText,
178
+ x_tracks: list[float],
179
+ ) -> _LogicalRowGrouping:
180
+ """保守合并同格内紧邻 continuation,避免 baseline 直接等同逻辑行。"""
181
+
182
+ groups: list[list[NativeTableTextRow]] = []
183
+ group_occupancy: list[set[int]] = []
184
+ dense_ambiguities: list[tuple[int, int]] = []
185
+ subset_merges: list[tuple[int, int]] = []
186
+ maximum_gap = max(0.75, 0.40 * text.median_glyph_height)
187
+ target_cols = len(x_tracks) - 1
188
+ dense_column_count = max(2, math.ceil(MIN_COLUMN_ANCHOR_SUPPORT * target_cols))
189
+ for row in text.rows:
190
+ columns = _token_columns(row, x_tracks)
191
+ occupancy = set(columns or [])
192
+ if groups:
193
+ previous = groups[-1][-1]
194
+ gap = row.bbox[1] - previous.bbox[3]
195
+ has_horizontal_rule = _has_horizontal_rule_between(
196
+ table_input,
197
+ previous,
198
+ row,
199
+ )
200
+ if (
201
+ occupancy == group_occupancy[-1]
202
+ and len(occupancy) >= dense_column_count
203
+ and gap <= maximum_gap
204
+ and not has_horizontal_rule
205
+ ):
206
+ dense_ambiguities.append((previous.row_index, row.row_index))
207
+ can_continue = (
208
+ bool(occupancy) and occupancy < group_occupancy[-1] and gap <= maximum_gap and not has_horizontal_rule
209
+ )
210
+ if can_continue:
211
+ groups[-1].append(row)
212
+ subset_merges.append((previous.row_index, row.row_index))
213
+ continue
214
+ groups.append([row])
215
+ group_occupancy.append(occupancy)
216
+
217
+ logical_rows: list[NativeTableTextRow] = []
218
+ for row_index, group in enumerate(groups):
219
+ logical_rows.append(
220
+ NativeTableTextRow(
221
+ row_index=row_index,
222
+ bbox=bbox_union(row.bbox for row in group),
223
+ tokens=tuple(
224
+ sorted(
225
+ (token for row in group for token in row.tokens),
226
+ key=lambda token: (token.bbox[0], token.bbox[1]),
227
+ )
228
+ ),
229
+ glyph_ids=tuple(glyph_id for row in group for glyph_id in row.glyph_ids),
230
+ )
231
+ )
232
+ return _LogicalRowGrouping(
233
+ rows=tuple(logical_rows),
234
+ dense_ambiguities=tuple(dense_ambiguities),
235
+ subset_merges=tuple(subset_merges),
236
+ )
237
+
238
+
239
+ def _header_rows_are_representable(
240
+ rows: tuple[NativeTableTextRow, ...],
241
+ x_tracks: list[float],
242
+ first_dense_row: int,
243
+ ) -> bool:
244
+ """校验前导多层表头能否仅用当前 colspan 逻辑完整表达。"""
245
+
246
+ if first_dense_row < 1:
247
+ return True
248
+ all_columns = set(range(len(x_tracks) - 1))
249
+ for row_index in range(first_dense_row):
250
+ row = rows[row_index]
251
+ spans = _grouped_header_spans(row, rows[row_index + 1], x_tracks) if row_index + 1 < len(rows) else None
252
+ if spans is not None:
253
+ coverage = {col for start_col, end_col in spans for col in range(start_col, end_col + 1)}
254
+ else:
255
+ token_columns = _token_columns(row, x_tracks)
256
+ if token_columns is None:
257
+ return False
258
+ coverage = set(token_columns)
259
+ if coverage != all_columns:
260
+ return False
261
+ return True
262
+
263
+
264
+ def _single_header_span(
265
+ row: NativeTableTextRow,
266
+ x_tracks: list[float],
267
+ ) -> tuple[int, int] | None:
268
+ """仅在单文本项确实横跨连续叶子列时返回保守表头 colspan。"""
269
+
270
+ if len(row.tokens) != 1:
271
+ return None
272
+ token = row.tokens[0]
273
+ token_width = token.bbox[2] - token.bbox[0]
274
+ if token_width <= 0:
275
+ return None
276
+ covered_cols = []
277
+ for col, (left, right) in enumerate(zip(x_tracks, x_tracks[1:])):
278
+ overlap = max(0.0, min(token.bbox[2], right) - max(token.bbox[0], left))
279
+ if overlap / token_width >= 0.20:
280
+ covered_cols.append(col)
281
+ if len(covered_cols) < 2 or covered_cols != list(range(covered_cols[0], covered_cols[-1] + 1)):
282
+ return None
283
+ return covered_cols[0], covered_cols[-1]
284
+
285
+
286
+ def _grouped_header_spans(
287
+ row: NativeTableTextRow,
288
+ next_row: NativeTableTextRow,
289
+ x_tracks: list[float],
290
+ ) -> list[tuple[int, int]] | None:
291
+ """用下一层表头叶子列为多个分组标题推断连续 colspan。"""
292
+
293
+ if len(row.tokens) < 2:
294
+ single_span = _single_header_span(row, x_tracks)
295
+ return [single_span] if single_span is not None else None
296
+ child_cols = _token_columns(next_row, x_tracks)
297
+ if child_cols is None:
298
+ return None
299
+ unique_child_cols = sorted(set(child_cols))
300
+ if len(unique_child_cols) < len(row.tokens):
301
+ return None
302
+ header_centers = [(token.bbox[0] + token.bbox[2]) / 2.0 for token in row.tokens]
303
+ header_gaps = [current - previous for previous, current in zip(header_centers, header_centers[1:]) if current > previous]
304
+ maximum_child_distance = 1.5 * float(statistics.median(header_gaps)) if header_gaps else float("inf")
305
+ groups: list[list[int]] = [[] for _ in row.tokens]
306
+ for col in unique_child_cols:
307
+ col_center = (x_tracks[col] + x_tracks[col + 1]) / 2.0
308
+ owner = min(
309
+ range(len(header_centers)),
310
+ key=lambda index: abs(col_center - header_centers[index]),
311
+ )
312
+ if abs(col_center - header_centers[owner]) > maximum_child_distance:
313
+ continue
314
+ groups[owner].append(col)
315
+ spans: list[tuple[int, int]] = []
316
+ for token, cols in zip(row.tokens, groups, strict=True):
317
+ if not cols or cols != list(range(cols[0], cols[-1] + 1)):
318
+ return None
319
+ token_center = (token.bbox[0] + token.bbox[2]) / 2.0
320
+ if not x_tracks[cols[0]] <= token_center <= x_tracks[cols[-1] + 1]:
321
+ return None
322
+ spans.append((cols[0], cols[-1]))
323
+ return spans
324
+
325
+
326
+ def _build_text_grid_specs(
327
+ rows: tuple[NativeTableTextRow, ...],
328
+ x_tracks: list[float],
329
+ y_tracks: list[float],
330
+ first_dense_row: int,
331
+ ) -> tuple[tuple[GridCellSpec, ...], float] | None:
332
+ """为每个逻辑行构造单元格,并允许前导分组表头产生 colspan。"""
333
+
334
+ cols = len(x_tracks) - 1
335
+ specs: list[GridCellSpec] = []
336
+ stable_rows = 0
337
+ for row_index, row in enumerate(rows):
338
+ occupied: set[int] = set()
339
+ header_spans = (
340
+ _grouped_header_spans(
341
+ row,
342
+ rows[row_index + 1],
343
+ x_tracks,
344
+ )
345
+ if row_index < first_dense_row and row_index + 1 < len(rows)
346
+ else None
347
+ )
348
+ if header_spans is not None:
349
+ for start_col, end_col in header_spans:
350
+ specs.append(
351
+ GridCellSpec(
352
+ row=row_index,
353
+ col=start_col,
354
+ rowspan=1,
355
+ colspan=end_col - start_col + 1,
356
+ bbox=(
357
+ x_tracks[start_col],
358
+ y_tracks[row_index],
359
+ x_tracks[end_col + 1],
360
+ y_tracks[row_index + 1],
361
+ ),
362
+ )
363
+ )
364
+ occupied.update(range(start_col, end_col + 1))
365
+ stable_rows += 1
366
+ else:
367
+ token_cols = _token_columns(row, x_tracks)
368
+ if token_cols is None:
369
+ return None
370
+ unique_cols = set(token_cols)
371
+ occupied.update(unique_cols)
372
+ if len(unique_cols) >= 2:
373
+ stable_rows += 1
374
+ for col in range(cols):
375
+ if col in occupied:
376
+ if header_spans is not None:
377
+ continue
378
+ specs.append(
379
+ GridCellSpec(
380
+ row=row_index,
381
+ col=col,
382
+ rowspan=1,
383
+ colspan=1,
384
+ bbox=(
385
+ x_tracks[col],
386
+ y_tracks[row_index],
387
+ x_tracks[col + 1],
388
+ y_tracks[row_index + 1],
389
+ ),
390
+ )
391
+ )
392
+ row_stability = stable_rows / len(rows) if rows else 0.0
393
+ return tuple(specs), row_stability
394
+
395
+
396
+ def _physical_sparse_evidence(
397
+ table_input: NativeTableInput,
398
+ text: NativeTableText,
399
+ ) -> float:
400
+ """统计长横线和重复行底纹,为少线候选提供独立物理证据。"""
401
+
402
+ table_bbox = normalize_bbox(table_input.table_bbox)
403
+ if table_bbox is None:
404
+ return 0.0
405
+ angle = normalize_angle(table_input.angle)
406
+ width, height = table_local_size(table_bbox, angle)
407
+ long_horizontal_count = 0
408
+ for rule in table_input.drawing_lines:
409
+ bbox = normalize_bbox(rule.bbox)
410
+ if bbox is None:
411
+ continue
412
+ local_bbox = page_bbox_to_table_local(bbox, table_bbox, angle)
413
+ if local_bbox is None:
414
+ continue
415
+ local_width = local_bbox[2] - local_bbox[0]
416
+ local_height = local_bbox[3] - local_bbox[1]
417
+ if local_width >= 0.50 * width and local_width >= 4.0 * local_height:
418
+ long_horizontal_count += 1
419
+
420
+ stripe_count = 0
421
+ for rectangle in table_input.rectangles:
422
+ if rectangle.segment_count != 5 or not rectangle.fill_visible:
423
+ continue
424
+ bbox = normalize_bbox(rectangle.bbox)
425
+ if bbox is None:
426
+ continue
427
+ local_bbox = page_bbox_to_table_local(bbox, table_bbox, angle)
428
+ if local_bbox is None:
429
+ continue
430
+ local_width = local_bbox[2] - local_bbox[0]
431
+ local_height = local_bbox[3] - local_bbox[1]
432
+ if local_width >= 0.60 * width and 0.30 * text.median_glyph_height <= local_height <= min(
433
+ 3.0 * text.median_glyph_height, 0.50 * height
434
+ ):
435
+ stripe_count += 1
436
+ # 贯穿竖线只说明 vector 候选应被优先验证,不能关闭其拓扑失败后的
437
+ # 横线加文本兜底;异构候选最终由 verified 物理网格仲裁。
438
+ return min(
439
+ 1.0,
440
+ (long_horizontal_count + stripe_count) / 3.0,
441
+ )
442
+
443
+
444
+ def _build_aligned_candidate(
445
+ *,
446
+ table_input: NativeTableInput,
447
+ text: NativeTableText,
448
+ source: NativeTableCandidateSource,
449
+ require_three_rows: bool,
450
+ require_three_cols: bool,
451
+ physical_support: float,
452
+ diagnostics: dict[str, Any] | None = None,
453
+ ) -> NativeTableCandidate | None:
454
+ """按统一文本轨道构造 sparse、wireless 或 key-value 候选。"""
455
+
456
+ if diagnostics is not None:
457
+ diagnostics["source"] = source
458
+ diagnostics["raw_visual_rows"] = len(text.rows)
459
+
460
+ def reject(gate: str) -> None:
461
+ """记录文本候选的首个拒绝门。"""
462
+
463
+ if diagnostics is not None:
464
+ diagnostics["first_rejection_gate"] = gate
465
+ return None
466
+
467
+ target_cols = _infer_target_column_count(text)
468
+ if target_cols is None:
469
+ return reject("column_count")
470
+ if require_three_cols and target_cols < 3:
471
+ return reject("column_count")
472
+ if source == "key_value" and target_cols != 2:
473
+ return reject("column_count")
474
+ if require_three_rows and len(text.rows) < 3:
475
+ return reject("row_count")
476
+
477
+ table_bbox = normalize_bbox(table_input.table_bbox)
478
+ if table_bbox is None:
479
+ return reject("table_geometry")
480
+ width, height = table_local_size(table_bbox, normalize_angle(table_input.angle))
481
+ column_result = _infer_column_tracks(text, width, target_cols)
482
+ if column_result is None:
483
+ return reject("column_tracks")
484
+ x_tracks, anchor_support, alignment_support = column_result
485
+ grouping = _group_logical_rows(
486
+ table_input,
487
+ text,
488
+ x_tracks,
489
+ )
490
+ logical_rows = grouping.rows
491
+ if diagnostics is not None:
492
+ diagnostics["logical_rows"] = len(logical_rows)
493
+ diagnostics["dense_row_ambiguities"] = [list(pair) for pair in grouping.dense_ambiguities]
494
+ diagnostics["subset_continuation_merges"] = [list(pair) for pair in grouping.subset_merges]
495
+ if grouping.dense_ambiguities:
496
+ return reject("dense_row_ambiguity")
497
+ y_tracks = _infer_row_tracks(logical_rows, height)
498
+ if y_tracks is None:
499
+ return reject("row_tracks")
500
+ dense_row_indices = [row.row_index for row in logical_rows if len(set(_token_columns(row, x_tracks) or [])) == target_cols]
501
+ first_dense_row = min(dense_row_indices) if dense_row_indices else 0
502
+ header_representable = _header_rows_are_representable(
503
+ logical_rows,
504
+ x_tracks,
505
+ first_dense_row,
506
+ )
507
+ if diagnostics is not None:
508
+ diagnostics["first_dense_row"] = first_dense_row
509
+ diagnostics["header_representable"] = header_representable
510
+ if not header_representable:
511
+ return reject("header_requires_rowspan")
512
+ specs_result = _build_text_grid_specs(
513
+ logical_rows,
514
+ x_tracks,
515
+ y_tracks,
516
+ first_dense_row,
517
+ )
518
+ if specs_result is None:
519
+ return reject("grid_specs")
520
+ specs, row_stability = specs_result
521
+ if row_stability < MIN_COLUMN_ANCHOR_SUPPORT:
522
+ return reject("row_stability")
523
+ structure_support = max(anchor_support, physical_support) if source == "sparse_grid" else alignment_support
524
+ candidate = build_candidate(
525
+ source=source,
526
+ rows=len(logical_rows),
527
+ cols=target_cols,
528
+ specs=specs,
529
+ text=text,
530
+ structure_support=structure_support,
531
+ row_stability=row_stability,
532
+ column_stability=alignment_support,
533
+ require_atomic_tokens=True,
534
+ diagnostics=diagnostics,
535
+ )
536
+ if candidate is None:
537
+ if diagnostics is not None and diagnostics.get("first_rejection_gate") is None:
538
+ diagnostics["first_rejection_gate"] = diagnostics.get("candidate_rejection_gate", "candidate_hard_gate")
539
+ return None
540
+ if diagnostics is not None:
541
+ diagnostics["first_rejection_gate"] = None
542
+ diagnostics["score"] = candidate.score
543
+ return candidate
544
+
545
+
546
+ def build_text_candidates(
547
+ table_input: NativeTableInput,
548
+ text: NativeTableText,
549
+ diagnostics: list[dict[str, Any]] | None = None,
550
+ ) -> list[NativeTableCandidate]:
551
+ """同时生成少线、三列以上无线表和两列 key-value 候选。"""
552
+
553
+ candidates: list[NativeTableCandidate] = []
554
+ physical_support = _physical_sparse_evidence(table_input, text)
555
+ if physical_support > 0:
556
+ sparse_diagnostics: dict[str, Any] | None = {} if diagnostics is not None else None
557
+ sparse = _build_aligned_candidate(
558
+ table_input=table_input,
559
+ text=text,
560
+ source="sparse_grid",
561
+ require_three_rows=False,
562
+ require_three_cols=False,
563
+ physical_support=physical_support,
564
+ diagnostics=sparse_diagnostics,
565
+ )
566
+ if diagnostics is not None and sparse_diagnostics is not None:
567
+ diagnostics.append(sparse_diagnostics)
568
+ if sparse is not None:
569
+ candidates.append(sparse)
570
+
571
+ text_diagnostics: dict[str, Any] | None = {} if diagnostics is not None else None
572
+ text_grid = _build_aligned_candidate(
573
+ table_input=table_input,
574
+ text=text,
575
+ source="text_grid",
576
+ require_three_rows=True,
577
+ require_three_cols=True,
578
+ physical_support=0.0,
579
+ diagnostics=text_diagnostics,
580
+ )
581
+ if diagnostics is not None and text_diagnostics is not None:
582
+ diagnostics.append(text_diagnostics)
583
+ if text_grid is not None:
584
+ candidates.append(text_grid)
585
+
586
+ key_value_diagnostics: dict[str, Any] | None = {} if diagnostics is not None else None
587
+ key_value = _build_aligned_candidate(
588
+ table_input=table_input,
589
+ text=text,
590
+ source="key_value",
591
+ require_three_rows=True,
592
+ require_three_cols=False,
593
+ physical_support=0.0,
594
+ diagnostics=key_value_diagnostics,
595
+ )
596
+ if diagnostics is not None and key_value_diagnostics is not None:
597
+ diagnostics.append(key_value_diagnostics)
598
+ if key_value is not None:
599
+ candidates.append(key_value)
600
+ return candidates
601
+
602
+
603
+ def diagnose_text_candidate_builds(
604
+ table_input: NativeTableInput,
605
+ text: NativeTableText,
606
+ ) -> tuple[dict[str, Any], ...]:
607
+ """重放文本候选构造并返回不含单元格全文的诊断。"""
608
+
609
+ diagnostics: list[dict[str, Any]] = []
610
+ build_text_candidates(
611
+ table_input,
612
+ text,
613
+ diagnostics=diagnostics,
614
+ )
615
+ return tuple(diagnostics)
616
+
617
+
618
+ __all__ = ["build_text_candidates"]