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,580 @@
|
|
|
1
|
+
"""PDF 段落延续关系的 raw model-list 后处理。"""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Any, TypeAlias
|
|
5
|
+
|
|
6
|
+
from ..content.spans import inline_span_plain_text
|
|
7
|
+
from ..schema import MERGE_TRANSPARENT_BLOCK_TYPES, BlockType
|
|
8
|
+
|
|
9
|
+
LINE_STOP_FLAG = (".", "!", "?", "。", "!", "?", ")", ")", '"', "”", ":", ":", ";", ";")
|
|
10
|
+
SECTION_MERGE_BARRIER_TYPES = {
|
|
11
|
+
BlockType.DOC_TITLE,
|
|
12
|
+
BlockType.PARAGRAPH_TITLE,
|
|
13
|
+
BlockType.EQUATION,
|
|
14
|
+
}
|
|
15
|
+
TEXT_MERGE_BARRIER_TYPES = {
|
|
16
|
+
*SECTION_MERGE_BARRIER_TYPES,
|
|
17
|
+
BlockType.LIST,
|
|
18
|
+
}
|
|
19
|
+
# 文本段落合并允许跨过视觉根块、页面脚注和页面装饰块,其他语义块仍会阻断候选查找。
|
|
20
|
+
TEXT_MERGE_TRANSPARENT_TYPES = {
|
|
21
|
+
BlockType.IMAGE,
|
|
22
|
+
BlockType.TABLE,
|
|
23
|
+
BlockType.CHART,
|
|
24
|
+
BlockType.CODE,
|
|
25
|
+
*MERGE_TRANSPARENT_BLOCK_TYPES,
|
|
26
|
+
}
|
|
27
|
+
CONTINUABLE_TEXT_BLOCK_TYPES = {BlockType.TEXT, BlockType.REF_TEXT}
|
|
28
|
+
VERTICAL_LINE_HEIGHT_TO_WIDTH_RATIO_THRESHOLD = 2
|
|
29
|
+
VERTICAL_LINE_IN_BLOCK_THRESHOLD = 0.8
|
|
30
|
+
SINGLE_LINE_LOOKAHEAD_LIMIT = 5
|
|
31
|
+
SINGLE_LINE_MIN_ALIGNED_LOOKAHEAD = 3
|
|
32
|
+
SINGLE_LINE_THICKNESS_RATIO_MAX = 1.5
|
|
33
|
+
BlockDict: TypeAlias = dict[str, Any]
|
|
34
|
+
CalculationBBox: TypeAlias = tuple[int, int, int, int]
|
|
35
|
+
OrderedBlock: TypeAlias = tuple[int, int, BlockDict]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def merge_para_text_blocks(pages: list[dict[str, Any]]) -> None:
|
|
39
|
+
"""按页面阅读顺序给可延续的文本或参考文献列表写入 continues_prev 标记。"""
|
|
40
|
+
ordered_blocks: list[OrderedBlock] = []
|
|
41
|
+
for page_info in pages:
|
|
42
|
+
blocks = page_info.get("blocks")
|
|
43
|
+
if not isinstance(blocks, list):
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
for block in blocks:
|
|
47
|
+
if isinstance(block, dict):
|
|
48
|
+
_clear_nested_continues_prev(block)
|
|
49
|
+
if block.get("type") not in CONTINUABLE_TEXT_BLOCK_TYPES:
|
|
50
|
+
block.pop("continues_prev", None)
|
|
51
|
+
|
|
52
|
+
page_idx = page_info.get("page_idx")
|
|
53
|
+
if not isinstance(page_idx, int):
|
|
54
|
+
continue
|
|
55
|
+
for order_idx, block in enumerate(blocks):
|
|
56
|
+
if isinstance(block, dict):
|
|
57
|
+
ordered_blocks.append((page_idx, order_idx, block))
|
|
58
|
+
|
|
59
|
+
for current_index in range(len(ordered_blocks) - 1, -1, -1):
|
|
60
|
+
current_page_idx, _, current_block = ordered_blocks[current_index]
|
|
61
|
+
current_type = current_block.get("type")
|
|
62
|
+
if current_type in CONTINUABLE_TEXT_BLOCK_TYPES:
|
|
63
|
+
# 已清理过 lines 的结果视为 finalize 完成,保留其既有标记以支持幂等调用。
|
|
64
|
+
if "lines" not in current_block:
|
|
65
|
+
continue
|
|
66
|
+
current_block.pop("continues_prev", None)
|
|
67
|
+
is_ref_text = current_type == BlockType.REF_TEXT
|
|
68
|
+
previous_block = (
|
|
69
|
+
_find_previous_ref_text_block(ordered_blocks, current_index)
|
|
70
|
+
if is_ref_text
|
|
71
|
+
else _find_previous_text_block(ordered_blocks, current_index)
|
|
72
|
+
)
|
|
73
|
+
if previous_block is None:
|
|
74
|
+
continue
|
|
75
|
+
previous_page_idx, _, previous_text_block = previous_block
|
|
76
|
+
if not _is_same_or_consecutive_page(current_page_idx, previous_page_idx):
|
|
77
|
+
continue
|
|
78
|
+
can_merge = (
|
|
79
|
+
can_auto_merge_ref_text_blocks(current_block, previous_text_block)
|
|
80
|
+
if is_ref_text
|
|
81
|
+
else can_auto_merge_text_blocks(current_block, previous_text_block)
|
|
82
|
+
)
|
|
83
|
+
if can_merge or _can_auto_merge_multiline_to_single_line(
|
|
84
|
+
current_block,
|
|
85
|
+
previous_text_block,
|
|
86
|
+
ordered_blocks=ordered_blocks,
|
|
87
|
+
current_index=current_index,
|
|
88
|
+
current_page_idx=current_page_idx,
|
|
89
|
+
current_type=current_type,
|
|
90
|
+
require_current_leading_edge=not is_ref_text,
|
|
91
|
+
reject_digit_or_uppercase_start=not is_ref_text,
|
|
92
|
+
):
|
|
93
|
+
current_block["continues_prev"] = True
|
|
94
|
+
elif current_type == BlockType.LIST:
|
|
95
|
+
previous_block = _find_previous_ref_text_list_block(
|
|
96
|
+
ordered_blocks,
|
|
97
|
+
current_index,
|
|
98
|
+
current_block,
|
|
99
|
+
)
|
|
100
|
+
if previous_block is None:
|
|
101
|
+
continue
|
|
102
|
+
previous_page_idx, _, _ = previous_block
|
|
103
|
+
if _is_same_or_consecutive_page(current_page_idx, previous_page_idx):
|
|
104
|
+
current_block["continues_prev"] = True
|
|
105
|
+
|
|
106
|
+
for page_info in pages:
|
|
107
|
+
blocks = page_info.get("blocks")
|
|
108
|
+
if not isinstance(blocks, list):
|
|
109
|
+
continue
|
|
110
|
+
for block in blocks:
|
|
111
|
+
if isinstance(block, dict):
|
|
112
|
+
_remove_line_metadata(block)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def can_auto_merge_text_blocks(current_block: BlockDict, previous_block: BlockDict) -> bool:
|
|
116
|
+
"""按文本首尾、行方向和几何关系判断两个 dict text block 是否可连续。"""
|
|
117
|
+
return _can_auto_merge_continuable_text_blocks(
|
|
118
|
+
current_block,
|
|
119
|
+
previous_block,
|
|
120
|
+
require_current_leading_edge=True,
|
|
121
|
+
reject_digit_or_uppercase_start=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def can_auto_merge_ref_text_blocks(current_block: BlockDict, previous_block: BlockDict) -> bool:
|
|
126
|
+
"""按正文规则判断 ref_text,放宽当前起始边界及数字或大写字符开头限制。"""
|
|
127
|
+
return _can_auto_merge_continuable_text_blocks(
|
|
128
|
+
current_block,
|
|
129
|
+
previous_block,
|
|
130
|
+
require_current_leading_edge=False,
|
|
131
|
+
reject_digit_or_uppercase_start=False,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _can_auto_merge_continuable_text_blocks(
|
|
136
|
+
current_block: BlockDict,
|
|
137
|
+
previous_block: BlockDict,
|
|
138
|
+
*,
|
|
139
|
+
require_current_leading_edge: bool,
|
|
140
|
+
reject_digit_or_uppercase_start: bool,
|
|
141
|
+
) -> bool:
|
|
142
|
+
"""复用正文与参考文献的公共文本边界、方向及几何续接规则。"""
|
|
143
|
+
current_metric_lines = _metric_line_bboxes(current_block)
|
|
144
|
+
previous_metric_lines = _metric_line_bboxes(previous_block)
|
|
145
|
+
if not current_metric_lines or not previous_metric_lines:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
current_bbox = _bbox_for_calculation(current_block.get("bbox"))
|
|
149
|
+
previous_bbox = _bbox_for_calculation(previous_block.get("bbox"))
|
|
150
|
+
if current_bbox is None or previous_bbox is None:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
current_content = _normalized_text_content(current_block)
|
|
154
|
+
previous_content = _normalized_text_content(previous_block)
|
|
155
|
+
if not current_content or not previous_content:
|
|
156
|
+
return False
|
|
157
|
+
if not _has_mergeable_text_boundary(
|
|
158
|
+
current_content,
|
|
159
|
+
previous_content,
|
|
160
|
+
reject_digit_or_uppercase_start=reject_digit_or_uppercase_start,
|
|
161
|
+
):
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
current_is_vertical = _is_vertical_text_block_by_lines(current_metric_lines)
|
|
165
|
+
previous_is_vertical = _is_vertical_text_block_by_lines(previous_metric_lines)
|
|
166
|
+
if current_is_vertical != previous_is_vertical:
|
|
167
|
+
return False
|
|
168
|
+
if current_is_vertical:
|
|
169
|
+
return _can_auto_merge_vertical_text_blocks(
|
|
170
|
+
current_content,
|
|
171
|
+
previous_content,
|
|
172
|
+
current_bbox,
|
|
173
|
+
previous_bbox,
|
|
174
|
+
current_metric_lines,
|
|
175
|
+
previous_metric_lines,
|
|
176
|
+
require_current_leading_edge=require_current_leading_edge,
|
|
177
|
+
)
|
|
178
|
+
return _can_auto_merge_horizontal_text_blocks(
|
|
179
|
+
current_content,
|
|
180
|
+
previous_content,
|
|
181
|
+
current_bbox,
|
|
182
|
+
previous_bbox,
|
|
183
|
+
current_metric_lines,
|
|
184
|
+
previous_metric_lines,
|
|
185
|
+
require_current_leading_edge=require_current_leading_edge,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _find_previous_text_block(
|
|
190
|
+
ordered_blocks: list[OrderedBlock],
|
|
191
|
+
current_index: int,
|
|
192
|
+
) -> OrderedBlock | None:
|
|
193
|
+
"""向前查找 text,视觉根块和合并透明块可跨过,其他语义块会阻断查找。"""
|
|
194
|
+
for previous_index in range(current_index - 1, -1, -1):
|
|
195
|
+
previous_block = ordered_blocks[previous_index][2]
|
|
196
|
+
previous_type = previous_block.get("type")
|
|
197
|
+
if previous_type in TEXT_MERGE_BARRIER_TYPES:
|
|
198
|
+
return None
|
|
199
|
+
if previous_type != BlockType.TEXT:
|
|
200
|
+
if previous_type not in TEXT_MERGE_TRANSPARENT_TYPES:
|
|
201
|
+
return None
|
|
202
|
+
continue
|
|
203
|
+
return ordered_blocks[previous_index]
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _find_previous_ref_text_block(
|
|
208
|
+
ordered_blocks: list[OrderedBlock],
|
|
209
|
+
current_index: int,
|
|
210
|
+
) -> OrderedBlock | None:
|
|
211
|
+
"""跳过页面脚注与辅助块查找前一个 ref_text,其他语义块保持阻断。"""
|
|
212
|
+
for previous_index in range(current_index - 1, -1, -1):
|
|
213
|
+
previous_block = ordered_blocks[previous_index][2]
|
|
214
|
+
previous_type = previous_block.get("type")
|
|
215
|
+
if previous_type in MERGE_TRANSPARENT_BLOCK_TYPES:
|
|
216
|
+
continue
|
|
217
|
+
if previous_type == BlockType.REF_TEXT:
|
|
218
|
+
return ordered_blocks[previous_index]
|
|
219
|
+
return None
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _find_previous_ref_text_list_block(
|
|
224
|
+
ordered_blocks: list[OrderedBlock],
|
|
225
|
+
current_index: int,
|
|
226
|
+
current_block: BlockDict,
|
|
227
|
+
) -> OrderedBlock | None:
|
|
228
|
+
"""跳过页面脚注与辅助块查找前一个 ref_text list,其他语义块保持阻断。"""
|
|
229
|
+
if not _is_ref_text_list_block(current_block):
|
|
230
|
+
return None
|
|
231
|
+
for previous_index in range(current_index - 1, -1, -1):
|
|
232
|
+
previous_block = ordered_blocks[previous_index][2]
|
|
233
|
+
if previous_block.get("type") in MERGE_TRANSPARENT_BLOCK_TYPES:
|
|
234
|
+
continue
|
|
235
|
+
if _is_ref_text_list_block(previous_block):
|
|
236
|
+
return ordered_blocks[previous_index]
|
|
237
|
+
return None
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _is_ref_text_list_block(block: BlockDict) -> bool:
|
|
242
|
+
"""判断当前 dict block 是否为参考文献列表。"""
|
|
243
|
+
return block.get("type") == BlockType.LIST and block.get("sub_type") == BlockType.REF_TEXT
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _is_same_or_consecutive_page(current_page_idx: int, previous_page_idx: int) -> bool:
|
|
247
|
+
"""只允许同页或页码严格连续的前后页建立延续关系。"""
|
|
248
|
+
return current_page_idx == previous_page_idx or current_page_idx == previous_page_idx + 1
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _positive_values_have_max_ratio(first: float, second: float, max_ratio: float) -> bool:
|
|
252
|
+
"""判断两个正值的较大较小比是否不超过给定上限。"""
|
|
253
|
+
if first <= 0 or second <= 0:
|
|
254
|
+
return False
|
|
255
|
+
return max(first, second) / min(first, second) <= max_ratio
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _bbox_for_calculation(bbox: Any) -> CalculationBBox | None:
|
|
259
|
+
"""复制并将 0~1 bbox 放大为千分位整数,原始 bbox 保持不变。"""
|
|
260
|
+
if not isinstance(bbox, (list, tuple)) or len(bbox) != 4:
|
|
261
|
+
return None
|
|
262
|
+
try:
|
|
263
|
+
values = tuple(float(value) for value in bbox)
|
|
264
|
+
except (TypeError, ValueError):
|
|
265
|
+
return None
|
|
266
|
+
if not all(math.isfinite(value) and 0 <= value <= 1 for value in values):
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
x0, y0, x1, y1 = (int(round(value * 1000)) for value in values)
|
|
270
|
+
if x1 <= x0 or y1 <= y0:
|
|
271
|
+
return None
|
|
272
|
+
return x0, y0, x1, y1
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _metric_line_bboxes(block: BlockDict) -> list[CalculationBBox]:
|
|
276
|
+
"""读取 block.lines 的全部合法行框,任一行非法时整块按不可合并处理。"""
|
|
277
|
+
lines = block.get("lines")
|
|
278
|
+
if not isinstance(lines, list) or not lines:
|
|
279
|
+
return []
|
|
280
|
+
|
|
281
|
+
line_bboxes: list[CalculationBBox] = []
|
|
282
|
+
for line in lines:
|
|
283
|
+
if not isinstance(line, dict):
|
|
284
|
+
return []
|
|
285
|
+
line_bbox = _bbox_for_calculation(line.get("bbox"))
|
|
286
|
+
if line_bbox is None:
|
|
287
|
+
return []
|
|
288
|
+
line_bboxes.append(line_bbox)
|
|
289
|
+
return line_bboxes
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _normalized_text_content(block: BlockDict) -> str:
|
|
293
|
+
"""读取 text block 的结构化 Span 可见文本。"""
|
|
294
|
+
|
|
295
|
+
content = block.get("content")
|
|
296
|
+
if not isinstance(content, list):
|
|
297
|
+
return ""
|
|
298
|
+
return inline_span_plain_text(item for item in content if isinstance(item, dict)).strip()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _bbox_union(line_bboxes: list[CalculationBBox]) -> CalculationBBox:
|
|
302
|
+
"""聚合全部行框,得到只用于几何判断的文本覆盖范围。"""
|
|
303
|
+
return (
|
|
304
|
+
min(bbox[0] for bbox in line_bboxes),
|
|
305
|
+
min(bbox[1] for bbox in line_bboxes),
|
|
306
|
+
max(bbox[2] for bbox in line_bboxes),
|
|
307
|
+
max(bbox[3] for bbox in line_bboxes),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _line_height(line_bbox: CalculationBBox) -> int:
|
|
312
|
+
"""计算千分位行框高度。"""
|
|
313
|
+
return line_bbox[3] - line_bbox[1]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _line_width(line_bbox: CalculationBBox) -> int:
|
|
317
|
+
"""计算千分位行框宽度。"""
|
|
318
|
+
return line_bbox[2] - line_bbox[0]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _is_vertical_text_block_by_lines(line_bboxes: list[CalculationBBox]) -> bool:
|
|
322
|
+
"""按行框高宽比判断 block 是否为竖排文本。"""
|
|
323
|
+
vertical_line_count = sum(
|
|
324
|
+
_line_height(line_bbox) / _line_width(line_bbox) > VERTICAL_LINE_HEIGHT_TO_WIDTH_RATIO_THRESHOLD
|
|
325
|
+
for line_bbox in line_bboxes
|
|
326
|
+
)
|
|
327
|
+
return vertical_line_count / len(line_bboxes) > VERTICAL_LINE_IN_BLOCK_THRESHOLD
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _has_mergeable_text_boundary(
|
|
331
|
+
current_content: str,
|
|
332
|
+
previous_content: str,
|
|
333
|
+
*,
|
|
334
|
+
reject_digit_or_uppercase_start: bool,
|
|
335
|
+
) -> bool:
|
|
336
|
+
"""使用前块结尾和后块开头字符排除明显的新段落边界。"""
|
|
337
|
+
if previous_content.endswith(LINE_STOP_FLAG):
|
|
338
|
+
return False
|
|
339
|
+
if not reject_digit_or_uppercase_start:
|
|
340
|
+
return True
|
|
341
|
+
first_char = current_content[0]
|
|
342
|
+
return not first_char.isdigit() and not first_char.isupper()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _collect_following_same_orientation_lines(
|
|
346
|
+
ordered_blocks: list[OrderedBlock],
|
|
347
|
+
current_index: int,
|
|
348
|
+
*,
|
|
349
|
+
current_page_idx: int,
|
|
350
|
+
current_type: str,
|
|
351
|
+
is_vertical: bool,
|
|
352
|
+
) -> list[CalculationBBox]:
|
|
353
|
+
"""在当前页向后读取至多五条同类型同方向行,语义屏障或非法文本会终止读取。"""
|
|
354
|
+
following_lines: list[CalculationBBox] = []
|
|
355
|
+
transparent_types = TEXT_MERGE_TRANSPARENT_TYPES if current_type == BlockType.TEXT else MERGE_TRANSPARENT_BLOCK_TYPES
|
|
356
|
+
for page_idx, _, block in ordered_blocks[current_index + 1 :]:
|
|
357
|
+
if page_idx != current_page_idx:
|
|
358
|
+
break
|
|
359
|
+
block_type = block.get("type")
|
|
360
|
+
if block_type != current_type:
|
|
361
|
+
if block_type in transparent_types:
|
|
362
|
+
continue
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
block_lines = _metric_line_bboxes(block)
|
|
366
|
+
if not block_lines or _is_vertical_text_block_by_lines(block_lines) != is_vertical:
|
|
367
|
+
break
|
|
368
|
+
for line_bbox in block_lines:
|
|
369
|
+
following_lines.append(line_bbox)
|
|
370
|
+
if len(following_lines) >= SINGLE_LINE_LOOKAHEAD_LIMIT:
|
|
371
|
+
return following_lines
|
|
372
|
+
return following_lines
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _aligned_following_lines(
|
|
376
|
+
current_line: CalculationBBox,
|
|
377
|
+
following_lines: list[CalculationBBox],
|
|
378
|
+
*,
|
|
379
|
+
is_vertical: bool,
|
|
380
|
+
) -> list[CalculationBBox]:
|
|
381
|
+
"""按横排左边界或竖排上边界筛选同一虚拟栏内的后续行列。"""
|
|
382
|
+
current_start = current_line[1] if is_vertical else current_line[0]
|
|
383
|
+
current_thickness = _line_width(current_line) if is_vertical else _line_height(current_line)
|
|
384
|
+
aligned_lines: list[CalculationBBox] = []
|
|
385
|
+
for line_bbox in following_lines:
|
|
386
|
+
line_start = line_bbox[1] if is_vertical else line_bbox[0]
|
|
387
|
+
line_thickness = _line_width(line_bbox) if is_vertical else _line_height(line_bbox)
|
|
388
|
+
if not _positive_values_have_max_ratio(
|
|
389
|
+
current_thickness,
|
|
390
|
+
line_thickness,
|
|
391
|
+
SINGLE_LINE_THICKNESS_RATIO_MAX,
|
|
392
|
+
):
|
|
393
|
+
continue
|
|
394
|
+
if abs(line_start - current_start) <= max(current_thickness, line_thickness):
|
|
395
|
+
aligned_lines.append(line_bbox)
|
|
396
|
+
return aligned_lines
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _virtual_single_line_bbox(
|
|
400
|
+
current_line: CalculationBBox,
|
|
401
|
+
aligned_lines: list[CalculationBBox],
|
|
402
|
+
*,
|
|
403
|
+
is_vertical: bool,
|
|
404
|
+
) -> CalculationBBox:
|
|
405
|
+
"""仅沿文本主轴扩展单行计算框,原始 line 和 block bbox 保持不变。"""
|
|
406
|
+
if is_vertical:
|
|
407
|
+
return (
|
|
408
|
+
current_line[0],
|
|
409
|
+
current_line[1],
|
|
410
|
+
current_line[2],
|
|
411
|
+
max(current_line[3], *(line_bbox[3] for line_bbox in aligned_lines)),
|
|
412
|
+
)
|
|
413
|
+
return (
|
|
414
|
+
current_line[0],
|
|
415
|
+
current_line[1],
|
|
416
|
+
max(current_line[2], *(line_bbox[2] for line_bbox in aligned_lines)),
|
|
417
|
+
current_line[3],
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _can_auto_merge_multiline_to_single_line(
|
|
422
|
+
current_block: BlockDict,
|
|
423
|
+
previous_block: BlockDict,
|
|
424
|
+
*,
|
|
425
|
+
ordered_blocks: list[OrderedBlock],
|
|
426
|
+
current_index: int,
|
|
427
|
+
current_page_idx: int,
|
|
428
|
+
current_type: str,
|
|
429
|
+
require_current_leading_edge: bool,
|
|
430
|
+
reject_digit_or_uppercase_start: bool,
|
|
431
|
+
) -> bool:
|
|
432
|
+
"""用后续五行或列补足单行主轴尺寸,再复用原横排或竖排连接规则。"""
|
|
433
|
+
current_lines = _metric_line_bboxes(current_block)
|
|
434
|
+
previous_lines = _metric_line_bboxes(previous_block)
|
|
435
|
+
if len(current_lines) != 1 or len(previous_lines) <= 1:
|
|
436
|
+
return False
|
|
437
|
+
|
|
438
|
+
current_is_vertical = _is_vertical_text_block_by_lines(current_lines)
|
|
439
|
+
if _is_vertical_text_block_by_lines(previous_lines) != current_is_vertical:
|
|
440
|
+
return False
|
|
441
|
+
following_lines = _collect_following_same_orientation_lines(
|
|
442
|
+
ordered_blocks,
|
|
443
|
+
current_index,
|
|
444
|
+
current_page_idx=current_page_idx,
|
|
445
|
+
current_type=current_type,
|
|
446
|
+
is_vertical=current_is_vertical,
|
|
447
|
+
)
|
|
448
|
+
aligned_lines = _aligned_following_lines(
|
|
449
|
+
current_lines[0],
|
|
450
|
+
following_lines,
|
|
451
|
+
is_vertical=current_is_vertical,
|
|
452
|
+
)
|
|
453
|
+
if len(aligned_lines) < SINGLE_LINE_MIN_ALIGNED_LOOKAHEAD:
|
|
454
|
+
return False
|
|
455
|
+
|
|
456
|
+
current_bbox = _bbox_for_calculation(current_block.get("bbox"))
|
|
457
|
+
previous_bbox = _bbox_for_calculation(previous_block.get("bbox"))
|
|
458
|
+
current_content = _normalized_text_content(current_block)
|
|
459
|
+
previous_content = _normalized_text_content(previous_block)
|
|
460
|
+
if current_bbox is None or previous_bbox is None or not current_content or not previous_content:
|
|
461
|
+
return False
|
|
462
|
+
if not _has_mergeable_text_boundary(
|
|
463
|
+
current_content,
|
|
464
|
+
previous_content,
|
|
465
|
+
reject_digit_or_uppercase_start=reject_digit_or_uppercase_start,
|
|
466
|
+
):
|
|
467
|
+
return False
|
|
468
|
+
|
|
469
|
+
virtual_current_line = _virtual_single_line_bbox(
|
|
470
|
+
current_lines[0],
|
|
471
|
+
aligned_lines,
|
|
472
|
+
is_vertical=current_is_vertical,
|
|
473
|
+
)
|
|
474
|
+
if current_is_vertical:
|
|
475
|
+
return _can_auto_merge_vertical_text_blocks(
|
|
476
|
+
current_content,
|
|
477
|
+
previous_content,
|
|
478
|
+
current_bbox,
|
|
479
|
+
previous_bbox,
|
|
480
|
+
[virtual_current_line],
|
|
481
|
+
previous_lines,
|
|
482
|
+
require_current_leading_edge=require_current_leading_edge,
|
|
483
|
+
)
|
|
484
|
+
return _can_auto_merge_horizontal_text_blocks(
|
|
485
|
+
current_content,
|
|
486
|
+
previous_content,
|
|
487
|
+
current_bbox,
|
|
488
|
+
previous_bbox,
|
|
489
|
+
[virtual_current_line],
|
|
490
|
+
previous_lines,
|
|
491
|
+
require_current_leading_edge=require_current_leading_edge,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _can_auto_merge_horizontal_text_blocks(
|
|
496
|
+
current_content: str,
|
|
497
|
+
previous_content: str,
|
|
498
|
+
current_bbox: CalculationBBox,
|
|
499
|
+
previous_bbox: CalculationBBox,
|
|
500
|
+
current_lines: list[CalculationBBox],
|
|
501
|
+
previous_lines: list[CalculationBBox],
|
|
502
|
+
*,
|
|
503
|
+
require_current_leading_edge: bool,
|
|
504
|
+
) -> bool:
|
|
505
|
+
"""使用横排段落的首行、末行、宽度和 block 相交规则判断是否连续。"""
|
|
506
|
+
first_line = current_lines[0]
|
|
507
|
+
last_line = previous_lines[-1]
|
|
508
|
+
first_line_height = _line_height(first_line)
|
|
509
|
+
last_line_height = _line_height(last_line)
|
|
510
|
+
if first_line_height <= 0 or last_line_height <= 0:
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
current_lines_bbox = _bbox_union(current_lines)
|
|
514
|
+
previous_lines_bbox = _bbox_union(previous_lines)
|
|
515
|
+
if require_current_leading_edge and abs(current_lines_bbox[0] - first_line[0]) >= first_line_height / 2:
|
|
516
|
+
return False
|
|
517
|
+
if abs(previous_lines_bbox[2] - last_line[2]) >= last_line_height:
|
|
518
|
+
return False
|
|
519
|
+
current_width = current_lines_bbox[2] - current_lines_bbox[0]
|
|
520
|
+
previous_width = previous_lines_bbox[2] - previous_lines_bbox[0]
|
|
521
|
+
min_width = min(current_width, previous_width)
|
|
522
|
+
if min_width <= 0 or abs(current_width - previous_width) >= min_width:
|
|
523
|
+
return False
|
|
524
|
+
if len(current_lines) <= 1 and len(previous_lines) <= 1:
|
|
525
|
+
return False
|
|
526
|
+
return current_bbox[1] < previous_bbox[3]
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _can_auto_merge_vertical_text_blocks(
|
|
530
|
+
current_content: str,
|
|
531
|
+
previous_content: str,
|
|
532
|
+
current_bbox: CalculationBBox,
|
|
533
|
+
previous_bbox: CalculationBBox,
|
|
534
|
+
current_lines: list[CalculationBBox],
|
|
535
|
+
previous_lines: list[CalculationBBox],
|
|
536
|
+
*,
|
|
537
|
+
require_current_leading_edge: bool,
|
|
538
|
+
) -> bool:
|
|
539
|
+
"""使用竖排段落的首列、末列、高度和 block 相交规则判断是否连续。"""
|
|
540
|
+
first_line = current_lines[0]
|
|
541
|
+
last_line = previous_lines[-1]
|
|
542
|
+
first_line_width = _line_width(first_line)
|
|
543
|
+
last_line_width = _line_width(last_line)
|
|
544
|
+
if first_line_width <= 0 or last_line_width <= 0:
|
|
545
|
+
return False
|
|
546
|
+
|
|
547
|
+
current_lines_bbox = _bbox_union(current_lines)
|
|
548
|
+
previous_lines_bbox = _bbox_union(previous_lines)
|
|
549
|
+
if require_current_leading_edge and abs(current_lines_bbox[1] - first_line[1]) >= first_line_width / 2:
|
|
550
|
+
return False
|
|
551
|
+
if abs(previous_lines_bbox[3] - last_line[3]) >= last_line_width:
|
|
552
|
+
return False
|
|
553
|
+
current_height = current_lines_bbox[3] - current_lines_bbox[1]
|
|
554
|
+
previous_height = previous_lines_bbox[3] - previous_lines_bbox[1]
|
|
555
|
+
min_height = min(current_height, previous_height)
|
|
556
|
+
if min_height <= 0 or abs(current_height - previous_height) >= min_height:
|
|
557
|
+
return False
|
|
558
|
+
return current_bbox[2] > previous_bbox[0]
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _clear_nested_continues_prev(block: BlockDict) -> None:
|
|
562
|
+
"""递归清理子块旧标记,顶层 text/ref_text 标记由是否仍有 lines 决定是否重算。"""
|
|
563
|
+
content = block.get("content")
|
|
564
|
+
if not isinstance(content, list):
|
|
565
|
+
return
|
|
566
|
+
for child_block in content:
|
|
567
|
+
if isinstance(child_block, dict):
|
|
568
|
+
child_block.pop("continues_prev", None)
|
|
569
|
+
_clear_nested_continues_prev(child_block)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _remove_line_metadata(block: BlockDict) -> None:
|
|
573
|
+
"""递归删除顶层及嵌套 block 的临时 lines 字段。"""
|
|
574
|
+
block.pop("lines", None)
|
|
575
|
+
content = block.get("content")
|
|
576
|
+
if not isinstance(content, list):
|
|
577
|
+
return
|
|
578
|
+
for child_block in content:
|
|
579
|
+
if isinstance(child_block, dict):
|
|
580
|
+
_remove_line_metadata(child_block)
|