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,581 @@
|
|
|
1
|
+
"""组装跨行标题、图片注释、页眉及首页信息块。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import statistics
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .....schema import BBox
|
|
9
|
+
from ..geometry import _bbox_axis_overlap_ratio, _bbox_center_x, _bbox_center_y, _bbox_union_many
|
|
10
|
+
from ..line_layout import _is_structural_typography_gap
|
|
11
|
+
from .common import _FIGURE_CAPTION_MARKER_RE, _components_share_lane_role, _merge_internal_text_block_group
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _merge_image_caption_text_blocks(
|
|
15
|
+
blocks: list[dict[str, Any]],
|
|
16
|
+
image_bboxes: list[BBox],
|
|
17
|
+
) -> list[dict[str, Any]]:
|
|
18
|
+
"""在图像邻接已成立后,用通用图注标记确认锚点并吸收同字体续行。"""
|
|
19
|
+
|
|
20
|
+
if not image_bboxes:
|
|
21
|
+
return blocks
|
|
22
|
+
text_indices = [
|
|
23
|
+
index
|
|
24
|
+
for index, block in enumerate(blocks)
|
|
25
|
+
if block.get("type") == "text"
|
|
26
|
+
and isinstance(block.get("content"), str)
|
|
27
|
+
and isinstance(block.get("bbox"), (list, tuple))
|
|
28
|
+
]
|
|
29
|
+
all_heights = [
|
|
30
|
+
float(height)
|
|
31
|
+
for index in text_indices
|
|
32
|
+
for height in blocks[index].get("_line_heights", [])
|
|
33
|
+
if isinstance(height, (int, float)) and height > 0
|
|
34
|
+
]
|
|
35
|
+
median_height = statistics.median(all_heights) if all_heights else 1.0
|
|
36
|
+
caption_image_bboxes = _caption_image_group_bboxes(
|
|
37
|
+
image_bboxes,
|
|
38
|
+
median_height,
|
|
39
|
+
)
|
|
40
|
+
seed_indices = {
|
|
41
|
+
index
|
|
42
|
+
for index in text_indices
|
|
43
|
+
if _FIGURE_CAPTION_MARKER_RE.match(str(blocks[index]["content"]).strip())
|
|
44
|
+
and any(
|
|
45
|
+
_caption_seed_matches_image(
|
|
46
|
+
blocks[index],
|
|
47
|
+
image_bbox,
|
|
48
|
+
median_height,
|
|
49
|
+
)
|
|
50
|
+
for image_bbox in caption_image_bboxes
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
if not seed_indices:
|
|
54
|
+
return blocks
|
|
55
|
+
|
|
56
|
+
assignments: dict[int, list[int]] = {index: [] for index in seed_indices}
|
|
57
|
+
for candidate_index in text_indices:
|
|
58
|
+
if candidate_index in seed_indices:
|
|
59
|
+
continue
|
|
60
|
+
candidate = blocks[candidate_index]
|
|
61
|
+
matches: list[tuple[float, float, int]] = []
|
|
62
|
+
for seed_index in seed_indices:
|
|
63
|
+
seed = blocks[seed_index]
|
|
64
|
+
if not _caption_tail_matches_seed(
|
|
65
|
+
seed,
|
|
66
|
+
candidate,
|
|
67
|
+
median_height,
|
|
68
|
+
):
|
|
69
|
+
continue
|
|
70
|
+
seed_bbox = seed["bbox"]
|
|
71
|
+
candidate_bbox = candidate["bbox"]
|
|
72
|
+
matches.append(
|
|
73
|
+
(
|
|
74
|
+
_bbox_center_y(candidate_bbox) - _bbox_center_y(seed_bbox),
|
|
75
|
+
abs(_bbox_center_x(candidate_bbox) - _bbox_center_x(seed_bbox)),
|
|
76
|
+
seed_index,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
if matches:
|
|
80
|
+
assignments[min(matches)[2]].append(candidate_index)
|
|
81
|
+
|
|
82
|
+
merged_indices: set[int] = set()
|
|
83
|
+
replacements: dict[int, dict[str, Any]] = {}
|
|
84
|
+
for seed_index, tail_indices in assignments.items():
|
|
85
|
+
if not tail_indices:
|
|
86
|
+
continue
|
|
87
|
+
group_indices = [seed_index, *tail_indices]
|
|
88
|
+
replacements[seed_index] = _merge_internal_text_block_group(
|
|
89
|
+
blocks,
|
|
90
|
+
group_indices,
|
|
91
|
+
)
|
|
92
|
+
merged_indices.update(tail_indices)
|
|
93
|
+
return [replacements.get(index, block) for index, block in enumerate(blocks) if index not in merged_indices]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _caption_image_group_bboxes(
|
|
97
|
+
image_bboxes: list[BBox],
|
|
98
|
+
median_height: float,
|
|
99
|
+
) -> list[BBox]:
|
|
100
|
+
"""合并同一视觉行的并排图片 bbox,使跨多图的统一图注也能建立邻接。"""
|
|
101
|
+
|
|
102
|
+
remaining = list(image_bboxes)
|
|
103
|
+
grouped_bboxes = list(image_bboxes)
|
|
104
|
+
while remaining:
|
|
105
|
+
group = [remaining.pop(0)]
|
|
106
|
+
changed = True
|
|
107
|
+
while changed:
|
|
108
|
+
changed = False
|
|
109
|
+
for candidate in list(remaining):
|
|
110
|
+
aligned = False
|
|
111
|
+
for member in group:
|
|
112
|
+
overlap = max(
|
|
113
|
+
0.0,
|
|
114
|
+
min(candidate[3], member[3]) - max(candidate[1], member[1]),
|
|
115
|
+
)
|
|
116
|
+
minimum_height = max(
|
|
117
|
+
0.1,
|
|
118
|
+
min(
|
|
119
|
+
candidate[3] - candidate[1],
|
|
120
|
+
member[3] - member[1],
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
horizontal_gap = max(
|
|
124
|
+
0.0,
|
|
125
|
+
max(candidate[0], member[0]) - min(candidate[2], member[2]),
|
|
126
|
+
)
|
|
127
|
+
if overlap / minimum_height >= 0.7 and horizontal_gap <= 2.0 * median_height:
|
|
128
|
+
aligned = True
|
|
129
|
+
break
|
|
130
|
+
if aligned:
|
|
131
|
+
group.append(candidate)
|
|
132
|
+
remaining.remove(candidate)
|
|
133
|
+
changed = True
|
|
134
|
+
if len(group) >= 2:
|
|
135
|
+
grouped_bboxes.append(_bbox_union_many(group))
|
|
136
|
+
return grouped_bboxes
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _caption_seed_matches_image(
|
|
140
|
+
block: dict[str, Any],
|
|
141
|
+
image_bbox: BBox,
|
|
142
|
+
median_height: float,
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""用上下位置、水平投影和居中关系确认图像下方的图注空间候选。"""
|
|
145
|
+
|
|
146
|
+
bbox = block["bbox"]
|
|
147
|
+
image_width = max(0.1, image_bbox[2] - image_bbox[0])
|
|
148
|
+
block_width = max(0.1, bbox[2] - bbox[0])
|
|
149
|
+
vertical_gap = max(0.0, bbox[1] - image_bbox[3])
|
|
150
|
+
return (
|
|
151
|
+
_bbox_center_y(bbox) >= image_bbox[3] - 0.25 * median_height
|
|
152
|
+
and vertical_gap <= 2.5 * median_height
|
|
153
|
+
and _bbox_axis_overlap_ratio(bbox, image_bbox, axis="x") >= 0.35
|
|
154
|
+
and abs(_bbox_center_x(bbox) - _bbox_center_x(image_bbox)) <= 0.35 * max(image_width, block_width)
|
|
155
|
+
and block_width <= 1.75 * image_width
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _caption_body_has_structural_gap(
|
|
160
|
+
seed: dict[str, Any],
|
|
161
|
+
candidate: dict[str, Any],
|
|
162
|
+
) -> bool:
|
|
163
|
+
"""用图注末行、候选首行和图注内部行距阻止跨排版层级回并。"""
|
|
164
|
+
|
|
165
|
+
seed_bboxes = seed.get("_local_line_bboxes")
|
|
166
|
+
seed_heights = seed.get("_line_heights")
|
|
167
|
+
candidate_bboxes = candidate.get("_local_line_bboxes")
|
|
168
|
+
candidate_heights = candidate.get("_line_heights")
|
|
169
|
+
if not (
|
|
170
|
+
isinstance(seed_bboxes, list)
|
|
171
|
+
and isinstance(seed_heights, list)
|
|
172
|
+
and len(seed_bboxes) == len(seed_heights)
|
|
173
|
+
and seed_bboxes
|
|
174
|
+
and isinstance(candidate_bboxes, list)
|
|
175
|
+
and isinstance(candidate_heights, list)
|
|
176
|
+
and len(candidate_bboxes) == len(candidate_heights)
|
|
177
|
+
and candidate_bboxes
|
|
178
|
+
):
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
seed_rows = sorted(
|
|
182
|
+
zip(seed_bboxes, seed_heights, strict=True),
|
|
183
|
+
key=lambda item: (item[0][1], item[0][0]),
|
|
184
|
+
)
|
|
185
|
+
candidate_rows = sorted(
|
|
186
|
+
zip(candidate_bboxes, candidate_heights, strict=True),
|
|
187
|
+
key=lambda item: (item[0][1], item[0][0]),
|
|
188
|
+
)
|
|
189
|
+
previous_bbox, previous_height = seed_rows[-1]
|
|
190
|
+
current_bbox, current_height = candidate_rows[0]
|
|
191
|
+
internal_gaps = [
|
|
192
|
+
max(0.0, current[0][1] - (previous[0][1] + float(previous[1]))) for previous, current in zip(seed_rows, seed_rows[1:])
|
|
193
|
+
]
|
|
194
|
+
regular_gap = statistics.median(internal_gaps) if internal_gaps else 0.0
|
|
195
|
+
gap_mad = statistics.median(abs(gap - regular_gap) for gap in internal_gaps) if internal_gaps else 0.0
|
|
196
|
+
seed_fonts = seed.get("_font_signatures")
|
|
197
|
+
candidate_fonts = candidate.get("_font_signatures")
|
|
198
|
+
reliable_style_change = (
|
|
199
|
+
isinstance(seed_fonts, set)
|
|
200
|
+
and bool(seed_fonts)
|
|
201
|
+
and isinstance(candidate_fonts, set)
|
|
202
|
+
and bool(candidate_fonts)
|
|
203
|
+
and seed_fonts.isdisjoint(candidate_fonts)
|
|
204
|
+
)
|
|
205
|
+
return _is_structural_typography_gap(
|
|
206
|
+
float(previous_height),
|
|
207
|
+
float(current_height),
|
|
208
|
+
current_bbox[1] - (previous_bbox[1] + float(previous_height)),
|
|
209
|
+
regular_gap,
|
|
210
|
+
gap_mad,
|
|
211
|
+
reliable_style_change=reliable_style_change,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _caption_tail_matches_seed(
|
|
216
|
+
seed: dict[str, Any],
|
|
217
|
+
candidate: dict[str, Any],
|
|
218
|
+
median_height: float,
|
|
219
|
+
) -> bool:
|
|
220
|
+
"""只用同栏角色、字体、邻接和投影把无标记的图注续行接回锚点。"""
|
|
221
|
+
|
|
222
|
+
seed_bbox = seed["bbox"]
|
|
223
|
+
candidate_bbox = candidate["bbox"]
|
|
224
|
+
if not _components_share_lane_role(seed, candidate, median_height) and (
|
|
225
|
+
_bbox_axis_overlap_ratio(seed_bbox, candidate_bbox, axis="x") < 0.75
|
|
226
|
+
or abs(seed_bbox[0] - candidate_bbox[0]) > median_height
|
|
227
|
+
):
|
|
228
|
+
return False
|
|
229
|
+
if _bbox_center_y(candidate_bbox) <= _bbox_center_y(seed_bbox):
|
|
230
|
+
return False
|
|
231
|
+
if _caption_body_has_structural_gap(seed, candidate):
|
|
232
|
+
return False
|
|
233
|
+
vertical_gap = max(0.0, candidate_bbox[1] - seed_bbox[3])
|
|
234
|
+
if vertical_gap > 0.5 * median_height or _bbox_axis_overlap_ratio(seed_bbox, candidate_bbox, axis="x") < 0.35:
|
|
235
|
+
return False
|
|
236
|
+
seed_fonts = seed.get("_font_signatures")
|
|
237
|
+
candidate_fonts = candidate.get("_font_signatures")
|
|
238
|
+
return not (
|
|
239
|
+
isinstance(seed_fonts, set)
|
|
240
|
+
and seed_fonts
|
|
241
|
+
and isinstance(candidate_fonts, set)
|
|
242
|
+
and candidate_fonts
|
|
243
|
+
and seed_fonts.isdisjoint(candidate_fonts)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _merge_multiline_title_blocks(
|
|
248
|
+
blocks: list[dict[str, Any]],
|
|
249
|
+
) -> list[dict[str, Any]]:
|
|
250
|
+
"""跨错误栏带合并紧贴且字体兼容的多行文档标题和段落标题。"""
|
|
251
|
+
|
|
252
|
+
replacements: dict[int, dict[str, Any]] = {}
|
|
253
|
+
consumed: set[int] = set()
|
|
254
|
+
for block_type in ("doc_title", "paragraph_title"):
|
|
255
|
+
indices = [
|
|
256
|
+
index
|
|
257
|
+
for index, block in enumerate(blocks)
|
|
258
|
+
if block.get("type") == block_type and isinstance(block.get("bbox"), (list, tuple))
|
|
259
|
+
]
|
|
260
|
+
indices.sort(
|
|
261
|
+
key=lambda index: (
|
|
262
|
+
blocks[index]["bbox"][1],
|
|
263
|
+
blocks[index]["bbox"][0],
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
groups: list[list[int]] = []
|
|
267
|
+
for index in indices:
|
|
268
|
+
if not groups:
|
|
269
|
+
groups.append([index])
|
|
270
|
+
continue
|
|
271
|
+
previous_index = groups[-1][-1]
|
|
272
|
+
previous = blocks[previous_index]
|
|
273
|
+
current = blocks[index]
|
|
274
|
+
previous_bbox = previous["bbox"]
|
|
275
|
+
current_bbox = current["bbox"]
|
|
276
|
+
previous_heights = previous.get("_line_heights", [])
|
|
277
|
+
current_heights = current.get("_line_heights", [])
|
|
278
|
+
previous_height = (
|
|
279
|
+
statistics.median(previous_heights)
|
|
280
|
+
if isinstance(previous_heights, list) and previous_heights
|
|
281
|
+
else previous_bbox[3] - previous_bbox[1]
|
|
282
|
+
)
|
|
283
|
+
current_height = (
|
|
284
|
+
statistics.median(current_heights)
|
|
285
|
+
if isinstance(current_heights, list) and current_heights
|
|
286
|
+
else current_bbox[3] - current_bbox[1]
|
|
287
|
+
)
|
|
288
|
+
vertical_gap = current_bbox[1] - previous_bbox[3]
|
|
289
|
+
previous_fonts = previous.get("_font_signatures")
|
|
290
|
+
current_fonts = current.get("_font_signatures")
|
|
291
|
+
fonts_conflict = (
|
|
292
|
+
isinstance(previous_fonts, set)
|
|
293
|
+
and previous_fonts
|
|
294
|
+
and isinstance(current_fonts, set)
|
|
295
|
+
and current_fonts
|
|
296
|
+
and previous_fonts.isdisjoint(current_fonts)
|
|
297
|
+
)
|
|
298
|
+
if (
|
|
299
|
+
-0.2 * max(previous_height, current_height) <= vertical_gap <= 0.4 * max(previous_height, current_height)
|
|
300
|
+
and _bbox_axis_overlap_ratio(
|
|
301
|
+
previous_bbox,
|
|
302
|
+
current_bbox,
|
|
303
|
+
axis="x",
|
|
304
|
+
)
|
|
305
|
+
>= 0.2
|
|
306
|
+
and not fonts_conflict
|
|
307
|
+
):
|
|
308
|
+
groups[-1].append(index)
|
|
309
|
+
else:
|
|
310
|
+
groups.append([index])
|
|
311
|
+
for group in groups:
|
|
312
|
+
if len(group) < 2:
|
|
313
|
+
continue
|
|
314
|
+
replacements[group[0]] = _merge_internal_text_block_group(
|
|
315
|
+
blocks,
|
|
316
|
+
group,
|
|
317
|
+
)
|
|
318
|
+
consumed.update(group[1:])
|
|
319
|
+
return [replacements.get(index, block) for index, block in enumerate(blocks) if index not in consumed]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _merge_fragmented_header_blocks(
|
|
323
|
+
blocks: list[dict[str, Any]],
|
|
324
|
+
) -> list[dict[str, Any]]:
|
|
325
|
+
"""聚合同一视觉行中等距分散的页眉页脚片段。"""
|
|
326
|
+
|
|
327
|
+
grouped: dict[tuple[int, int], list[int]] = {}
|
|
328
|
+
for index, block in enumerate(blocks):
|
|
329
|
+
row_id = block.get("_single_run_row_id")
|
|
330
|
+
angle = int(block.get("angle", 0) or 0) % 360
|
|
331
|
+
if block.get("type") in {"header", "footer"} and isinstance(row_id, int):
|
|
332
|
+
grouped.setdefault((angle, row_id), []).append(index)
|
|
333
|
+
|
|
334
|
+
replacements: dict[int, dict[str, Any]] = {}
|
|
335
|
+
consumed: set[int] = set()
|
|
336
|
+
for indices in grouped.values():
|
|
337
|
+
ordered = sorted(indices, key=lambda index: blocks[index]["bbox"][0])
|
|
338
|
+
components: list[list[int]] = []
|
|
339
|
+
for index in ordered:
|
|
340
|
+
bbox = blocks[index]["bbox"]
|
|
341
|
+
heights = blocks[index].get("_line_heights", [])
|
|
342
|
+
effective_height = (
|
|
343
|
+
statistics.median(heights) if isinstance(heights, list) and heights else max(0.1, bbox[3] - bbox[1])
|
|
344
|
+
)
|
|
345
|
+
if blocks[index].get("type") == "header" and bbox[2] - bbox[0] > 1.25 * effective_height:
|
|
346
|
+
continue
|
|
347
|
+
if not components:
|
|
348
|
+
components.append([index])
|
|
349
|
+
continue
|
|
350
|
+
previous_index = components[-1][-1]
|
|
351
|
+
previous_bbox = blocks[previous_index]["bbox"]
|
|
352
|
+
previous_heights = blocks[previous_index].get("_line_heights", [])
|
|
353
|
+
previous_height = (
|
|
354
|
+
statistics.median(previous_heights)
|
|
355
|
+
if isinstance(previous_heights, list) and previous_heights
|
|
356
|
+
else max(0.1, previous_bbox[3] - previous_bbox[1])
|
|
357
|
+
)
|
|
358
|
+
if (
|
|
359
|
+
bbox[0] - previous_bbox[2] <= 4.0 * max(effective_height, previous_height)
|
|
360
|
+
and _bbox_axis_overlap_ratio(previous_bbox, bbox, axis="y") >= 0.5
|
|
361
|
+
):
|
|
362
|
+
components[-1].append(index)
|
|
363
|
+
else:
|
|
364
|
+
components.append([index])
|
|
365
|
+
for component in components:
|
|
366
|
+
if len(component) < 2:
|
|
367
|
+
continue
|
|
368
|
+
replacement = _merge_internal_text_block_group(
|
|
369
|
+
blocks,
|
|
370
|
+
component,
|
|
371
|
+
preserve_visual_spaces=True,
|
|
372
|
+
)
|
|
373
|
+
replacement["_single_run_row_id"] = None
|
|
374
|
+
replacements[component[0]] = replacement
|
|
375
|
+
consumed.update(component[1:])
|
|
376
|
+
return [replacements.get(index, block) for index, block in enumerate(blocks) if index not in consumed]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _merge_front_matter_column_blocks(
|
|
380
|
+
blocks: list[dict[str, Any]],
|
|
381
|
+
page_size: tuple[float, float],
|
|
382
|
+
*,
|
|
383
|
+
page_index: int,
|
|
384
|
+
) -> list[dict[str, Any]]:
|
|
385
|
+
"""把首页标题下方规则排列的多列作者信息按列聚合。"""
|
|
386
|
+
|
|
387
|
+
if page_index != 0:
|
|
388
|
+
return blocks
|
|
389
|
+
page_width, page_height = page_size
|
|
390
|
+
if page_width <= 0 or page_height <= 0:
|
|
391
|
+
return blocks
|
|
392
|
+
title_blocks = [
|
|
393
|
+
block for block in blocks if block.get("type") == "doc_title" and isinstance(block.get("bbox"), (list, tuple))
|
|
394
|
+
]
|
|
395
|
+
if not title_blocks:
|
|
396
|
+
return blocks
|
|
397
|
+
title_bottom = max(block["bbox"][3] for block in title_blocks)
|
|
398
|
+
candidates = [
|
|
399
|
+
index
|
|
400
|
+
for index, block in enumerate(blocks)
|
|
401
|
+
if block.get("type") == "text"
|
|
402
|
+
and isinstance(block.get("bbox"), (list, tuple))
|
|
403
|
+
and title_bottom < block["bbox"][1]
|
|
404
|
+
and block["bbox"][3]
|
|
405
|
+
<= min(
|
|
406
|
+
0.4 * page_height,
|
|
407
|
+
title_bottom + 0.22 * page_height,
|
|
408
|
+
)
|
|
409
|
+
and block["bbox"][2] - block["bbox"][0] <= 0.32 * page_width
|
|
410
|
+
and block["bbox"][3] - block["bbox"][1] <= 0.035 * page_height
|
|
411
|
+
]
|
|
412
|
+
if len(candidates) < 9:
|
|
413
|
+
return blocks
|
|
414
|
+
median_height = statistics.median(blocks[index]["bbox"][3] - blocks[index]["bbox"][1] for index in candidates)
|
|
415
|
+
row_groups: list[list[int]] = []
|
|
416
|
+
for index in sorted(
|
|
417
|
+
candidates,
|
|
418
|
+
key=lambda item: (
|
|
419
|
+
_bbox_center_y(blocks[item]["bbox"]),
|
|
420
|
+
blocks[item]["bbox"][0],
|
|
421
|
+
),
|
|
422
|
+
):
|
|
423
|
+
center_y = _bbox_center_y(blocks[index]["bbox"])
|
|
424
|
+
target = next(
|
|
425
|
+
(
|
|
426
|
+
row
|
|
427
|
+
for row in row_groups
|
|
428
|
+
if abs(center_y - statistics.median(_bbox_center_y(blocks[member]["bbox"]) for member in row))
|
|
429
|
+
<= 0.6 * median_height
|
|
430
|
+
),
|
|
431
|
+
None,
|
|
432
|
+
)
|
|
433
|
+
if target is None:
|
|
434
|
+
row_groups.append([index])
|
|
435
|
+
else:
|
|
436
|
+
target.append(index)
|
|
437
|
+
dense_rows = [
|
|
438
|
+
row
|
|
439
|
+
for row in row_groups
|
|
440
|
+
if 3 <= len(row) <= 6
|
|
441
|
+
and (
|
|
442
|
+
max(blocks[index]["bbox"][2] for index in row) - min(blocks[index]["bbox"][0] for index in row) >= 0.55 * page_width
|
|
443
|
+
)
|
|
444
|
+
]
|
|
445
|
+
if len(dense_rows) < 2:
|
|
446
|
+
return blocks
|
|
447
|
+
anchor_row = min(
|
|
448
|
+
dense_rows,
|
|
449
|
+
key=lambda row: (
|
|
450
|
+
-len(row),
|
|
451
|
+
statistics.median(_bbox_center_y(blocks[index]["bbox"]) for index in row),
|
|
452
|
+
),
|
|
453
|
+
)
|
|
454
|
+
anchor_centers = sorted(_bbox_center_x(blocks[index]["bbox"]) for index in anchor_row)
|
|
455
|
+
if len(anchor_centers) != 4:
|
|
456
|
+
return blocks
|
|
457
|
+
boundaries = [0.5 * (left + right) for left, right in zip(anchor_centers, anchor_centers[1:])]
|
|
458
|
+
band_top = min(min(blocks[index]["bbox"][1] for index in row) for row in dense_rows) - median_height
|
|
459
|
+
band_bottom = max(max(blocks[index]["bbox"][3] for index in row) for row in dense_rows) + median_height
|
|
460
|
+
column_groups: list[list[int]] = [[] for _center in anchor_centers]
|
|
461
|
+
for index in candidates:
|
|
462
|
+
bbox = blocks[index]["bbox"]
|
|
463
|
+
if not band_top <= _bbox_center_y(bbox) <= band_bottom:
|
|
464
|
+
continue
|
|
465
|
+
center_x = _bbox_center_x(bbox)
|
|
466
|
+
column_index = sum(center_x > boundary for boundary in boundaries)
|
|
467
|
+
if column_index >= len(column_groups):
|
|
468
|
+
continue
|
|
469
|
+
column_groups[column_index].append(index)
|
|
470
|
+
if any(len(group) < 3 for group in column_groups):
|
|
471
|
+
return blocks
|
|
472
|
+
|
|
473
|
+
replacements: dict[int, dict[str, Any]] = {}
|
|
474
|
+
consumed: set[int] = set()
|
|
475
|
+
for group in column_groups:
|
|
476
|
+
ordered = sorted(
|
|
477
|
+
group,
|
|
478
|
+
key=lambda index: (
|
|
479
|
+
blocks[index]["bbox"][1],
|
|
480
|
+
blocks[index]["bbox"][0],
|
|
481
|
+
),
|
|
482
|
+
)
|
|
483
|
+
replacements[ordered[0]] = _merge_internal_text_block_group(
|
|
484
|
+
blocks,
|
|
485
|
+
ordered,
|
|
486
|
+
)
|
|
487
|
+
consumed.update(ordered[1:])
|
|
488
|
+
return [replacements.get(index, block) for index, block in enumerate(blocks) if index not in consumed]
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _merge_repeated_compact_title_continuations(
|
|
492
|
+
blocks: list[dict[str, Any]],
|
|
493
|
+
page_size: tuple[float, float],
|
|
494
|
+
) -> list[dict[str, Any]]:
|
|
495
|
+
"""把重复出现的两行弱标题与紧邻异字体续行恢复为普通文本块。"""
|
|
496
|
+
|
|
497
|
+
candidate_pairs: list[tuple[int, int, float]] = []
|
|
498
|
+
for title_index, title in enumerate(blocks):
|
|
499
|
+
title_lines = title.get("_local_line_bboxes")
|
|
500
|
+
title_fonts = title.get("_font_signatures")
|
|
501
|
+
title_bbox = title.get("bbox")
|
|
502
|
+
if (
|
|
503
|
+
title.get("type") != "paragraph_title"
|
|
504
|
+
or not isinstance(title_bbox, (list, tuple))
|
|
505
|
+
or not isinstance(title_lines, list)
|
|
506
|
+
or len(title_lines) < 2
|
|
507
|
+
or not isinstance(title_fonts, set)
|
|
508
|
+
or not title_fonts
|
|
509
|
+
):
|
|
510
|
+
continue
|
|
511
|
+
angle = int(title.get("angle", 0) or 0) % 360
|
|
512
|
+
local_page_width = page_size[1] if angle in {90, 270} else page_size[0]
|
|
513
|
+
line_heights = [
|
|
514
|
+
float(height) for height in title.get("_line_heights", []) if isinstance(height, (int, float)) and height > 0
|
|
515
|
+
]
|
|
516
|
+
title_height = statistics.median(line_heights) if line_heights else 0.0
|
|
517
|
+
if title_height <= 0 or title_bbox[2] - title_bbox[0] > 0.55 * local_page_width:
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
continuations: list[tuple[float, int]] = []
|
|
521
|
+
for text_index, text_block in enumerate(blocks):
|
|
522
|
+
text_bbox = text_block.get("bbox")
|
|
523
|
+
text_fonts = text_block.get("_font_signatures")
|
|
524
|
+
if (
|
|
525
|
+
text_block.get("type") != "text"
|
|
526
|
+
or int(text_block.get("angle", 0) or 0) % 360 != angle
|
|
527
|
+
or not isinstance(text_bbox, (list, tuple))
|
|
528
|
+
or not isinstance(text_fonts, set)
|
|
529
|
+
or not text_fonts
|
|
530
|
+
or not title_fonts.isdisjoint(text_fonts)
|
|
531
|
+
or text_bbox[2] - text_bbox[0] > 0.6 * local_page_width
|
|
532
|
+
):
|
|
533
|
+
continue
|
|
534
|
+
gap = text_bbox[1] - title_bbox[3]
|
|
535
|
+
if -0.25 * title_height <= gap <= 0.6 * title_height and abs(text_bbox[0] - title_bbox[0]) <= 0.75 * title_height:
|
|
536
|
+
continuations.append((max(0.0, gap), text_index))
|
|
537
|
+
if continuations:
|
|
538
|
+
_gap, text_index = min(continuations)
|
|
539
|
+
candidate_pairs.append((title_index, text_index, title_height))
|
|
540
|
+
|
|
541
|
+
supported_pairs: list[tuple[int, int]] = []
|
|
542
|
+
for title_index, text_index, title_height in candidate_pairs:
|
|
543
|
+
title_bbox = blocks[title_index]["bbox"]
|
|
544
|
+
support_count = sum(
|
|
545
|
+
abs(blocks[other_title]["bbox"][0] - title_bbox[0]) <= max(title_height, other_height)
|
|
546
|
+
and 0.75 <= other_height / title_height <= 1.25
|
|
547
|
+
for other_title, _other_text, other_height in candidate_pairs
|
|
548
|
+
)
|
|
549
|
+
if support_count >= 2:
|
|
550
|
+
supported_pairs.append((title_index, text_index))
|
|
551
|
+
if not supported_pairs:
|
|
552
|
+
return blocks
|
|
553
|
+
|
|
554
|
+
replacements: dict[int, dict[str, Any]] = {}
|
|
555
|
+
consumed: set[int] = set()
|
|
556
|
+
for title_index, text_index in supported_pairs:
|
|
557
|
+
if title_index in consumed or text_index in consumed:
|
|
558
|
+
continue
|
|
559
|
+
merged = _merge_internal_text_block_group(
|
|
560
|
+
blocks,
|
|
561
|
+
[title_index, text_index],
|
|
562
|
+
)
|
|
563
|
+
merged["type"] = "text"
|
|
564
|
+
replacements[min(title_index, text_index)] = merged
|
|
565
|
+
consumed.update({title_index, text_index})
|
|
566
|
+
return [
|
|
567
|
+
replacements.get(index, block) for index, block in enumerate(blocks) if index not in consumed or index in replacements
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
__all__ = [
|
|
572
|
+
"_merge_image_caption_text_blocks",
|
|
573
|
+
"_caption_image_group_bboxes",
|
|
574
|
+
"_caption_seed_matches_image",
|
|
575
|
+
"_caption_body_has_structural_gap",
|
|
576
|
+
"_caption_tail_matches_seed",
|
|
577
|
+
"_merge_multiline_title_blocks",
|
|
578
|
+
"_merge_fragmented_header_blocks",
|
|
579
|
+
"_merge_front_matter_column_blocks",
|
|
580
|
+
"_merge_repeated_compact_title_continuations",
|
|
581
|
+
]
|