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.
- docvortex/__init__.py +21 -0
- docvortex/analyzers/__init__.py +3 -0
- docvortex/analyzers/native/__init__.py +37 -0
- docvortex/analyzers/native/_shared/__init__.py +3 -0
- docvortex/analyzers/native/_shared/hyperlink.py +16 -0
- docvortex/analyzers/native/_shared/image.py +8 -0
- docvortex/analyzers/native/_shared/markup/__init__.py +47 -0
- docvortex/analyzers/native/_shared/markup/anchors.py +30 -0
- docvortex/analyzers/native/_shared/markup/formula.py +37 -0
- docvortex/analyzers/native/_shared/markup/projector.py +57 -0
- docvortex/analyzers/native/_shared/markup/styles.py +19 -0
- docvortex/analyzers/native/_shared/mathml.py +17 -0
- docvortex/analyzers/native/_shared/names.py +7 -0
- docvortex/analyzers/native/_shared/xycut.py +414 -0
- docvortex/analyzers/native/contracts.py +44 -0
- docvortex/analyzers/native/csv.py +351 -0
- docvortex/analyzers/native/epub/__init__.py +18 -0
- docvortex/analyzers/native/epub/constants.py +45 -0
- docvortex/analyzers/native/epub/converter.py +80 -0
- docvortex/analyzers/native/epub/errors.py +20 -0
- docvortex/analyzers/native/epub/metadata.py +31 -0
- docvortex/analyzers/native/epub/package.py +649 -0
- docvortex/analyzers/native/epub/xhtml.py +306 -0
- docvortex/analyzers/native/html/__init__.py +6 -0
- docvortex/analyzers/native/html/anchors.py +271 -0
- docvortex/analyzers/native/html/constants.py +25 -0
- docvortex/analyzers/native/html/contracts.py +7 -0
- docvortex/analyzers/native/html/converter.py +117 -0
- docvortex/analyzers/native/html/document.py +389 -0
- docvortex/analyzers/native/html/errors.py +12 -0
- docvortex/analyzers/native/html/resources.py +365 -0
- docvortex/analyzers/native/html/selector.py +421 -0
- docvortex/analyzers/native/models.py +209 -0
- docvortex/analyzers/native/ofd/__init__.py +14 -0
- docvortex/analyzers/native/ofd/constants.py +59 -0
- docvortex/analyzers/native/ofd/converter.py +31 -0
- docvortex/analyzers/native/ofd/errors.py +16 -0
- docvortex/analyzers/native/ofd/geometry.py +215 -0
- docvortex/analyzers/native/ofd/images.py +124 -0
- docvortex/analyzers/native/ofd/metadata.py +53 -0
- docvortex/analyzers/native/ofd/models.py +170 -0
- docvortex/analyzers/native/ofd/package.py +346 -0
- docvortex/analyzers/native/ofd/path.py +201 -0
- docvortex/analyzers/native/ofd/reading_order.py +293 -0
- docvortex/analyzers/native/ofd/resources.py +122 -0
- docvortex/analyzers/native/ofd/scene.py +347 -0
- docvortex/analyzers/native/ofd/table.py +240 -0
- docvortex/analyzers/native/ofd/text.py +521 -0
- docvortex/analyzers/native/office/__init__.py +3 -0
- docvortex/analyzers/native/office/doc/__init__.py +3 -0
- docvortex/analyzers/native/office/doc/bookmarks.py +81 -0
- docvortex/analyzers/native/office/doc/doc_converter.py +573 -0
- docvortex/analyzers/native/office/doc/fib.py +261 -0
- docvortex/analyzers/native/office/doc/fields.py +101 -0
- docvortex/analyzers/native/office/doc/formatting.py +171 -0
- docvortex/analyzers/native/office/doc/images.py +162 -0
- docvortex/analyzers/native/office/doc/lists.py +356 -0
- docvortex/analyzers/native/office/doc/models.py +164 -0
- docvortex/analyzers/native/office/doc/parser.py +851 -0
- docvortex/analyzers/native/office/doc/pieces.py +256 -0
- docvortex/analyzers/native/office/doc/records.py +40 -0
- docvortex/analyzers/native/office/doc/sprm.py +269 -0
- docvortex/analyzers/native/office/doc/styles.py +215 -0
- docvortex/analyzers/native/office/docx/__init__.py +3 -0
- docvortex/analyzers/native/office/docx/context.py +71 -0
- docvortex/analyzers/native/office/docx/docx_converter.py +614 -0
- docvortex/analyzers/native/office/docx/equationxml.py +119 -0
- docvortex/analyzers/native/office/docx/fields.py +816 -0
- docvortex/analyzers/native/office/docx/formatting_types.py +23 -0
- docvortex/analyzers/native/office/docx/main.py +50 -0
- docvortex/analyzers/native/office/docx/numbering.py +491 -0
- docvortex/analyzers/native/office/docx/office_xml.py +57 -0
- docvortex/analyzers/native/office/docx/package_normalizer.py +248 -0
- docvortex/analyzers/native/office/docx/resources.py +526 -0
- docvortex/analyzers/native/office/docx/styles.py +693 -0
- docvortex/analyzers/native/office/docx/tables.py +515 -0
- docvortex/analyzers/native/office/equation/__init__.py +3 -0
- docvortex/analyzers/native/office/equation/image.py +470 -0
- docvortex/analyzers/native/office/equation/latex_dict.py +324 -0
- docvortex/analyzers/native/office/equation/mtef.py +885 -0
- docvortex/analyzers/native/office/equation/mtef_v5.py +941 -0
- docvortex/analyzers/native/office/equation/omml.py +561 -0
- docvortex/analyzers/native/office/equation/ooxml.py +62 -0
- docvortex/analyzers/native/office/errors.py +33 -0
- docvortex/analyzers/native/office/image.py +307 -0
- docvortex/analyzers/native/office/legacy/__init__.py +3 -0
- docvortex/analyzers/native/office/legacy/binary.py +43 -0
- docvortex/analyzers/native/office/legacy/officeart.py +362 -0
- docvortex/analyzers/native/office/legacy/ole.py +110 -0
- docvortex/analyzers/native/office/limits.py +12 -0
- docvortex/analyzers/native/office/odf/__init__.py +3 -0
- docvortex/analyzers/native/office/odf/chart.py +76 -0
- docvortex/analyzers/native/office/odf/constants.py +70 -0
- docvortex/analyzers/native/office/odf/converters.py +403 -0
- docvortex/analyzers/native/office/odf/errors.py +18 -0
- docvortex/analyzers/native/office/odf/metadata.py +91 -0
- docvortex/analyzers/native/office/odf/models.py +176 -0
- docvortex/analyzers/native/office/odf/package.py +270 -0
- docvortex/analyzers/native/office/odf/styles.py +329 -0
- docvortex/analyzers/native/office/odf/table.py +469 -0
- docvortex/analyzers/native/office/odf/text.py +1002 -0
- docvortex/analyzers/native/office/ooxml_chart.py +1016 -0
- docvortex/analyzers/native/office/opc.py +38 -0
- docvortex/analyzers/native/office/ppt/__init__.py +3 -0
- docvortex/analyzers/native/office/ppt/models.py +118 -0
- docvortex/analyzers/native/office/ppt/parser.py +1895 -0
- docvortex/analyzers/native/office/ppt/ppt_converter.py +292 -0
- docvortex/analyzers/native/office/ppt/records.py +131 -0
- docvortex/analyzers/native/office/ppt/style_text.py +247 -0
- docvortex/analyzers/native/office/pptx/__init__.py +3 -0
- docvortex/analyzers/native/office/pptx/context.py +113 -0
- docvortex/analyzers/native/office/pptx/lists.py +558 -0
- docvortex/analyzers/native/office/pptx/main.py +20 -0
- docvortex/analyzers/native/office/pptx/package_normalizer.py +321 -0
- docvortex/analyzers/native/office/pptx/pptx_converter.py +323 -0
- docvortex/analyzers/native/office/pptx/resources.py +329 -0
- docvortex/analyzers/native/office/pptx/shapes.py +393 -0
- docvortex/analyzers/native/office/pptx/text_styles.py +625 -0
- docvortex/analyzers/native/office/pptx/titles.py +178 -0
- docvortex/analyzers/native/office/rich_text.py +420 -0
- docvortex/analyzers/native/office/rtf/__init__.py +3 -0
- docvortex/analyzers/native/office/rtf/converter.py +708 -0
- docvortex/analyzers/native/office/rtf/lexer.py +213 -0
- docvortex/analyzers/native/office/rtf/math.py +339 -0
- docvortex/analyzers/native/office/rtf/models.py +185 -0
- docvortex/analyzers/native/office/rtf/parser.py +1553 -0
- docvortex/analyzers/native/office/spreadsheet/__init__.py +3 -0
- docvortex/analyzers/native/office/spreadsheet/html.py +81 -0
- docvortex/analyzers/native/office/spreadsheet/models.py +79 -0
- docvortex/analyzers/native/office/spreadsheet/projector.py +928 -0
- docvortex/analyzers/native/office/streams.py +18 -0
- docvortex/analyzers/native/office/xls/__init__.py +3 -0
- docvortex/analyzers/native/office/xls/chart.py +132 -0
- docvortex/analyzers/native/office/xls/embedded_chart.py +299 -0
- docvortex/analyzers/native/office/xls/models.py +109 -0
- docvortex/analyzers/native/office/xls/number_format.py +521 -0
- docvortex/analyzers/native/office/xls/parser.py +1145 -0
- docvortex/analyzers/native/office/xls/records.py +201 -0
- docvortex/analyzers/native/office/xls/strings.py +205 -0
- docvortex/analyzers/native/office/xls/xls_converter.py +354 -0
- docvortex/analyzers/native/office/xlsx/__init__.py +3 -0
- docvortex/analyzers/native/office/xlsx/main.py +20 -0
- docvortex/analyzers/native/office/xlsx/ooxml_ole.py +522 -0
- docvortex/analyzers/native/office/xlsx/package_normalizer.py +310 -0
- docvortex/analyzers/native/office/xlsx/xlsx_converter.py +716 -0
- docvortex/analyzers/native/pdf/__init__.py +3 -0
- docvortex/analyzers/native/pdf/auxiliary_text.py +1671 -0
- docvortex/analyzers/native/pdf/char_geometry.py +1662 -0
- docvortex/analyzers/native/pdf/code_blocks.py +535 -0
- docvortex/analyzers/native/pdf/formulas.py +1985 -0
- docvortex/analyzers/native/pdf/geometry.py +281 -0
- docvortex/analyzers/native/pdf/graphics.py +1501 -0
- docvortex/analyzers/native/pdf/index_blocks.py +268 -0
- docvortex/analyzers/native/pdf/inline/__init__.py +3 -0
- docvortex/analyzers/native/pdf/inline/common.py +112 -0
- docvortex/analyzers/native/pdf/inline/detection.py +590 -0
- docvortex/analyzers/native/pdf/inline/matching.py +1185 -0
- docvortex/analyzers/native/pdf/inline/materialize.py +457 -0
- docvortex/analyzers/native/pdf/inline/scripts.py +975 -0
- docvortex/analyzers/native/pdf/inline/types.py +385 -0
- docvortex/analyzers/native/pdf/line_layout.py +1106 -0
- docvortex/analyzers/native/pdf/line_merging.py +1223 -0
- docvortex/analyzers/native/pdf/models.py +246 -0
- docvortex/analyzers/native/pdf/native_text.py +1004 -0
- docvortex/analyzers/native/pdf/pipeline.py +1390 -0
- docvortex/analyzers/native/pdf/script_geometry.py +636 -0
- docvortex/analyzers/native/pdf/shared.py +30 -0
- docvortex/analyzers/native/pdf/spatial_text.py +383 -0
- docvortex/analyzers/native/pdf/table_annotations.py +446 -0
- docvortex/analyzers/native/pdf/table_constants.py +48 -0
- docvortex/analyzers/native/pdf/table_detection.py +227 -0
- docvortex/analyzers/native/pdf/table_filled_grid.py +218 -0
- docvortex/analyzers/native/pdf/table_geometry.py +40 -0
- docvortex/analyzers/native/pdf/table_materialization.py +444 -0
- docvortex/analyzers/native/pdf/table_recovery/__init__.py +15 -0
- docvortex/analyzers/native/pdf/table_recovery/candidate.py +356 -0
- docvortex/analyzers/native/pdf/table_recovery/contracts.py +154 -0
- docvortex/analyzers/native/pdf/table_recovery/engine.py +609 -0
- docvortex/analyzers/native/pdf/table_recovery/geometry.py +164 -0
- docvortex/analyzers/native/pdf/table_recovery/sparse_common.py +85 -0
- docvortex/analyzers/native/pdf/table_recovery/sparse_hybrid.py +804 -0
- docvortex/analyzers/native/pdf/table_recovery/sparse_multiline.py +1122 -0
- docvortex/analyzers/native/pdf/table_recovery/text.py +414 -0
- docvortex/analyzers/native/pdf/table_recovery/text_grid.py +618 -0
- docvortex/analyzers/native/pdf/table_recovery/vector.py +1931 -0
- docvortex/analyzers/native/pdf/table_rows.py +34 -0
- docvortex/analyzers/native/pdf/table_rules.py +1129 -0
- docvortex/analyzers/native/pdf/table_text_styles.py +283 -0
- docvortex/analyzers/native/pdf/tables.py +147 -0
- docvortex/analyzers/native/pdf/text_assembly/__init__.py +3 -0
- docvortex/analyzers/native/pdf/text_assembly/annotations.py +581 -0
- docvortex/analyzers/native/pdf/text_assembly/assembly.py +292 -0
- docvortex/analyzers/native/pdf/text_assembly/common.py +477 -0
- docvortex/analyzers/native/pdf/text_assembly/footnotes.py +394 -0
- docvortex/analyzers/native/pdf/text_assembly/merging.py +1274 -0
- docvortex/analyzers/native/pdf/text_assembly/rows.py +692 -0
- docvortex/analyzers/native/pdf/text_blocks.py +82 -0
- docvortex/analyzers/native/pdf/text_styles.py +55 -0
- docvortex/analyzers/native/pdf/title_analysis/__init__.py +3 -0
- docvortex/analyzers/native/pdf/title_analysis/body_profile.py +215 -0
- docvortex/analyzers/native/pdf/title_analysis/common.py +117 -0
- docvortex/analyzers/native/pdf/title_analysis/document_profile.py +164 -0
- docvortex/analyzers/native/pdf/title_analysis/lane_titles.py +758 -0
- docvortex/analyzers/native/pdf/title_analysis/page_titles.py +1024 -0
- docvortex/analyzers/native/pdf/title_analysis/prototype.py +194 -0
- docvortex/analyzers/native/pdf/title_analysis/structural.py +1081 -0
- docvortex/analyzers/native/pdf/titles.py +75 -0
- docvortex/analyzers/native/pdf/typography.py +19 -0
- docvortex/analyzers/native/pdf/visual_annotations.py +1262 -0
- docvortex/api.py +180 -0
- docvortex/assets/__init__.py +5 -0
- docvortex/assets/store.py +51 -0
- docvortex/cli.py +54 -0
- docvortex/codecs/__init__.py +3 -0
- docvortex/codecs/html/__init__.py +22 -0
- docvortex/codecs/html/contracts.py +236 -0
- docvortex/codecs/html/materializer.py +331 -0
- docvortex/codecs/html/parser.py +763 -0
- docvortex/codecs/html/resources.py +34 -0
- docvortex/codecs/json.py +17 -0
- docvortex/content/__init__.py +5 -0
- docvortex/content/inline.py +248 -0
- docvortex/content/markup/__init__.py +44 -0
- docvortex/content/markup/anchors.py +188 -0
- docvortex/content/markup/formula.py +280 -0
- docvortex/content/markup/projector.py +1237 -0
- docvortex/content/markup/styles.py +327 -0
- docvortex/content/mathml.py +167 -0
- docvortex/content/normalization.py +188 -0
- docvortex/content/spans.py +183 -0
- docvortex/content/table/__init__.py +18 -0
- docvortex/content/table/blocks.py +152 -0
- docvortex/content/table/content.py +425 -0
- docvortex/content/table/document.py +104 -0
- docvortex/content/table/html.py +399 -0
- docvortex/content/table/models.py +76 -0
- docvortex/content/table/rules.py +42 -0
- docvortex/content/table/structure.py +221 -0
- docvortex/content/tree.py +5 -0
- docvortex/document/__init__.py +3 -0
- docvortex/document/contracts.py +22 -0
- docvortex/document/detection.py +389 -0
- docvortex/document/filetypes.py +175 -0
- docvortex/document/page_range.py +167 -0
- docvortex/document/pdf/__init__.py +19 -0
- docvortex/document/pdf/classify.py +1138 -0
- docvortex/document/pdf/constants.py +132 -0
- docvortex/document/pdf/diagnostics.py +350 -0
- docvortex/document/pdf/document.py +582 -0
- docvortex/document/pdf/font_runtime.py +335 -0
- docvortex/document/pdf/geometry.py +31 -0
- docvortex/document/pdf/images.py +654 -0
- docvortex/document/pdf/native_annotations.py +367 -0
- docvortex/document/pdf/native_contracts.py +169 -0
- docvortex/document/pdf/native_coordinates.py +216 -0
- docvortex/document/pdf/native_lifecycle.py +16 -0
- docvortex/document/pdf/native_objects.py +902 -0
- docvortex/document/pdf/native_text_geometry.py +315 -0
- docvortex/document/pdf/pdfium.py +325 -0
- docvortex/document/pdf/raster.py +46 -0
- docvortex/document/pdf/text/__init__.py +62 -0
- docvortex/document/pdf/text/contracts.py +211 -0
- docvortex/document/pdf/text/extract.py +165 -0
- docvortex/document/pdf/text/geometry.py +16 -0
- docvortex/document/pdf/text/groups.py +162 -0
- docvortex/document/pdf/visual_geometry.py +201 -0
- docvortex/document/pdf/visuals.py +343 -0
- docvortex/document/source.py +92 -0
- docvortex/errors.py +21 -0
- docvortex/export/__init__.py +3 -0
- docvortex/export/bundle.py +98 -0
- docvortex/export/files.py +66 -0
- docvortex/export/middle.py +208 -0
- docvortex/foundation/__init__.py +3 -0
- docvortex/foundation/geometry.py +125 -0
- docvortex/foundation/hyperlink.py +65 -0
- docvortex/foundation/image.py +48 -0
- docvortex/foundation/image_encoding.py +30 -0
- docvortex/foundation/image_payload.py +280 -0
- docvortex/foundation/language.py +92 -0
- docvortex/foundation/platform.py +38 -0
- docvortex/foundation/text.py +153 -0
- docvortex/foundation/type_identity.py +20 -0
- docvortex/foundation/xml_names.py +20 -0
- docvortex/options.py +30 -0
- docvortex/postprocess/__init__.py +3 -0
- docvortex/postprocess/content.py +53 -0
- docvortex/postprocess/document.py +19 -0
- docvortex/postprocess/lists.py +236 -0
- docvortex/postprocess/page_blocks.py +214 -0
- docvortex/postprocess/pages.py +95 -0
- docvortex/postprocess/paragraphs.py +580 -0
- docvortex/postprocess/visual.py +715 -0
- docvortex/render/__init__.py +48 -0
- docvortex/render/_internal/__init__.py +3 -0
- docvortex/render/_internal/common/__init__.py +3 -0
- docvortex/render/_internal/common/context.py +43 -0
- docvortex/render/_internal/common/html_table.py +178 -0
- docvortex/render/_internal/common/index.py +33 -0
- docvortex/render/_internal/common/list_items.py +158 -0
- docvortex/render/_internal/common/planner.py +140 -0
- docvortex/render/_internal/docx/__init__.py +3 -0
- docvortex/render/_internal/docx/assets.py +202 -0
- docvortex/render/_internal/docx/inline.py +434 -0
- docvortex/render/_internal/docx/math.py +220 -0
- docvortex/render/_internal/docx/renderer.py +905 -0
- docvortex/render/_internal/docx/styles.py +195 -0
- docvortex/render/_internal/docx/table.py +442 -0
- docvortex/render/_internal/epub/__init__.py +5 -0
- docvortex/render/_internal/epub/assets.py +173 -0
- docvortex/render/_internal/epub/package.py +249 -0
- docvortex/render/_internal/epub/renderer.py +1156 -0
- docvortex/render/_internal/html/__init__.py +3 -0
- docvortex/render/_internal/html/inline.py +346 -0
- docvortex/render/_internal/html/renderer.py +1041 -0
- docvortex/render/_internal/html/sanitizer.py +478 -0
- docvortex/render/_internal/html/table.py +121 -0
- docvortex/render/_internal/latex/__init__.py +1 -0
- docvortex/render/_internal/latex/assets.py +85 -0
- docvortex/render/_internal/latex/inline.py +145 -0
- docvortex/render/_internal/latex/renderer.py +506 -0
- docvortex/render/_internal/latex/table.py +347 -0
- docvortex/render/_internal/markdown/__init__.py +3 -0
- docvortex/render/_internal/markdown/assets.py +78 -0
- docvortex/render/_internal/markdown/blocks.py +635 -0
- docvortex/render/_internal/markdown/escaping.py +51 -0
- docvortex/render/_internal/markdown/inline.py +260 -0
- docvortex/render/_internal/markdown/renderer.py +93 -0
- docvortex/render/_internal/markdown/table.py +281 -0
- docvortex/render/_internal/pdf/__init__.py +3 -0
- docvortex/render/_internal/pdf/assets.py +197 -0
- docvortex/render/_internal/pdf/formula.py +417 -0
- docvortex/render/_internal/pdf/inline.py +343 -0
- docvortex/render/_internal/pdf/renderer.py +734 -0
- docvortex/render/_internal/pdf/styles.py +206 -0
- docvortex/render/_internal/pdf/table.py +272 -0
- docvortex/render/_internal/structured_content/__init__.py +3 -0
- docvortex/render/_internal/structured_content/renderer.py +193 -0
- docvortex/render/api.py +199 -0
- docvortex/render/contracts.py +205 -0
- docvortex/render/docx.py +38 -0
- docvortex/render/epub.py +35 -0
- docvortex/render/fragments.py +70 -0
- docvortex/render/html.py +29 -0
- docvortex/render/latex.py +24 -0
- docvortex/render/markdown.py +50 -0
- docvortex/render/pdf.py +25 -0
- docvortex/render/structured_content.py +23 -0
- docvortex/resources/epub/docvortex.css +91 -0
- docvortex/resources/fasttext-langdetect/lid.176.ftz +0 -0
- docvortex/resources/fonts/DroidSansFallbackFull.ttf +0 -0
- docvortex/resources/fonts/NOTICE +190 -0
- docvortex/resources/fonts/manifest.json +9 -0
- docvortex/resources/html/docvortex.css +601 -0
- docvortex/resources/html/docvortex.min.css +1 -0
- docvortex/result.py +91 -0
- docvortex/schema.py +1141 -0
- docvortex/version.py +3 -0
- docvortex-0.2.1.dist-info/METADATA +193 -0
- docvortex-0.2.1.dist-info/RECORD +364 -0
- docvortex-0.2.1.dist-info/WHEEL +5 -0
- docvortex-0.2.1.dist-info/entry_points.txt +2 -0
- docvortex-0.2.1.dist-info/licenses/LICENSE.md +21 -0
- 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"]
|