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,758 @@
|
|
|
1
|
+
"""在既有正文统计和结构证据下分类栏内标题。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import statistics
|
|
6
|
+
from typing import Literal
|
|
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 (
|
|
11
|
+
_effective_text_row_gap,
|
|
12
|
+
_font_signatures_share_family,
|
|
13
|
+
_font_weights_conflict,
|
|
14
|
+
_line_effective_height,
|
|
15
|
+
_should_connect_semantic_rows,
|
|
16
|
+
_title_fonts_compatible,
|
|
17
|
+
)
|
|
18
|
+
from ..models import _DocumentBodyProfile, _DocumentTitleProfile, _LaneBodyProfile, _LineItem, _TextLane
|
|
19
|
+
from .body_profile import _line_uses_document_regular_font
|
|
20
|
+
from .common import _line_inside_visual_container, _line_near_visual_container
|
|
21
|
+
from .prototype import _line_conflicts_document_title_profile, _matching_document_title_prototype
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _classify_paragraph_titles_in_lane(
|
|
25
|
+
lane: _TextLane,
|
|
26
|
+
profile: _LaneBodyProfile,
|
|
27
|
+
local_page_width: float,
|
|
28
|
+
local_page_height: float,
|
|
29
|
+
container_bboxes: list[BBox],
|
|
30
|
+
*,
|
|
31
|
+
document_title_bottom: float | None,
|
|
32
|
+
preserve_front_matter_boundaries: bool,
|
|
33
|
+
physical_gaps: dict[int, tuple[float | None, float | None]],
|
|
34
|
+
grid_title_suppressions: set[int],
|
|
35
|
+
document_body_profile: _DocumentBodyProfile | None,
|
|
36
|
+
document_title_profile: _DocumentTitleProfile | None,
|
|
37
|
+
page_index: int,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""以字号、样式、留白、对齐、栏宽和容器邻接判定段落标题。"""
|
|
40
|
+
|
|
41
|
+
lane_width = max(0.1, lane.right - lane.left)
|
|
42
|
+
rows = lane.lines
|
|
43
|
+
selected_indices: set[int] = set()
|
|
44
|
+
front_matter_boundary = _infer_front_matter_boundary(
|
|
45
|
+
lane,
|
|
46
|
+
profile,
|
|
47
|
+
local_page_height,
|
|
48
|
+
document_title_bottom=document_title_bottom,
|
|
49
|
+
)
|
|
50
|
+
for index, (line, bbox) in enumerate(rows):
|
|
51
|
+
if line.semantic_type is not None:
|
|
52
|
+
continue
|
|
53
|
+
if line.source_index in grid_title_suppressions:
|
|
54
|
+
continue
|
|
55
|
+
if not 0.07 * local_page_height <= _bbox_center_y(bbox) <= 0.93 * local_page_height:
|
|
56
|
+
continue
|
|
57
|
+
inside_front_matter = front_matter_boundary is not None and _bbox_center_y(bbox) <= front_matter_boundary
|
|
58
|
+
if inside_front_matter and not preserve_front_matter_boundaries:
|
|
59
|
+
continue
|
|
60
|
+
if (
|
|
61
|
+
document_title_bottom is not None
|
|
62
|
+
and document_title_bottom <= 0.6 * local_page_height
|
|
63
|
+
and _bbox_center_y(bbox) >= 0.84 * local_page_height
|
|
64
|
+
):
|
|
65
|
+
# 首页底部短行通常是版本、日期等封面元数据,不属于正文标题层级。
|
|
66
|
+
continue
|
|
67
|
+
line_height = _line_effective_height(line, bbox)
|
|
68
|
+
title_prototype = _matching_document_title_prototype(
|
|
69
|
+
line,
|
|
70
|
+
bbox,
|
|
71
|
+
lane,
|
|
72
|
+
document_body_profile,
|
|
73
|
+
document_title_profile,
|
|
74
|
+
)
|
|
75
|
+
title_profile_size_conflict = title_prototype is None and _line_conflicts_document_title_profile(
|
|
76
|
+
line,
|
|
77
|
+
bbox,
|
|
78
|
+
lane,
|
|
79
|
+
document_body_profile,
|
|
80
|
+
document_title_profile,
|
|
81
|
+
)
|
|
82
|
+
inside_visual_container = _line_inside_visual_container(
|
|
83
|
+
bbox,
|
|
84
|
+
container_bboxes,
|
|
85
|
+
)
|
|
86
|
+
near_visual_container = _line_near_visual_container(
|
|
87
|
+
bbox,
|
|
88
|
+
container_bboxes,
|
|
89
|
+
profile.body_height,
|
|
90
|
+
)
|
|
91
|
+
if inside_visual_container or (near_visual_container and title_prototype is None):
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
recurrent_regular_font = _line_uses_document_regular_font(
|
|
95
|
+
line,
|
|
96
|
+
document_body_profile,
|
|
97
|
+
)
|
|
98
|
+
document_regular_body_candidate = (
|
|
99
|
+
recurrent_regular_font
|
|
100
|
+
and document_body_profile is not None
|
|
101
|
+
and line_height >= 0.9 * document_body_profile.body_height
|
|
102
|
+
)
|
|
103
|
+
reference_body_height = profile.body_height
|
|
104
|
+
if profile.body_row_count < 3 and document_body_profile is not None:
|
|
105
|
+
reference_body_height = document_body_profile.body_height
|
|
106
|
+
if document_regular_body_candidate and document_body_profile is not None:
|
|
107
|
+
reference_body_height = max(
|
|
108
|
+
reference_body_height,
|
|
109
|
+
document_body_profile.body_height,
|
|
110
|
+
)
|
|
111
|
+
height_ratio = line_height / reference_body_height
|
|
112
|
+
width_ratio = (bbox[2] - bbox[0]) / lane_width
|
|
113
|
+
centered = abs(_bbox_center_x(bbox) - (lane.left + lane.right) / 2.0) <= 0.12 * lane_width
|
|
114
|
+
left_aligned = abs(bbox[0] - lane.left) <= 0.65 * profile.body_height
|
|
115
|
+
if line.median_glyph_width is not None and bbox[2] - bbox[0] <= 1.25 * line.median_glyph_width and height_ratio < 1.18:
|
|
116
|
+
continue
|
|
117
|
+
style_differs = (
|
|
118
|
+
not recurrent_regular_font
|
|
119
|
+
and profile.body_font is not None
|
|
120
|
+
and line.font_signature is not None
|
|
121
|
+
and line.font_coverage >= 0.75
|
|
122
|
+
and not _font_signatures_share_family(
|
|
123
|
+
line.font_signature,
|
|
124
|
+
profile.body_font,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
low_coverage_style_differs = (
|
|
128
|
+
not recurrent_regular_font
|
|
129
|
+
and profile.body_font is not None
|
|
130
|
+
and line.font_signature is not None
|
|
131
|
+
and 0.5 <= line.font_coverage < 0.75
|
|
132
|
+
and not _font_signatures_share_family(
|
|
133
|
+
line.font_signature,
|
|
134
|
+
profile.body_font,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
style_support = profile.style_support.get(line.font_signature, 1.0) if line.font_signature is not None else 1.0
|
|
138
|
+
weight_emphasized = (
|
|
139
|
+
profile.body_weight is not None
|
|
140
|
+
and line.dominant_font_weight is not None
|
|
141
|
+
and line.dominant_font_weight >= max(profile.body_weight + 100.0, 1.15 * profile.body_weight)
|
|
142
|
+
)
|
|
143
|
+
gap_above = _normalized_title_gap(
|
|
144
|
+
rows,
|
|
145
|
+
index,
|
|
146
|
+
direction=-1,
|
|
147
|
+
body_height=profile.body_height,
|
|
148
|
+
physical_gaps=physical_gaps,
|
|
149
|
+
)
|
|
150
|
+
gap_below = _normalized_title_gap(
|
|
151
|
+
rows,
|
|
152
|
+
index,
|
|
153
|
+
direction=1,
|
|
154
|
+
body_height=profile.body_height,
|
|
155
|
+
physical_gaps=physical_gaps,
|
|
156
|
+
)
|
|
157
|
+
regular_gap_ratio = profile.regular_gap / max(0.1, profile.body_height)
|
|
158
|
+
gap_above_excess = max(0.0, gap_above - regular_gap_ratio)
|
|
159
|
+
gap_below_excess = max(0.0, gap_below - regular_gap_ratio)
|
|
160
|
+
has_spacing_signal = gap_above >= 0.35
|
|
161
|
+
|
|
162
|
+
if _visual_row_has_body_style_sibling(rows, index):
|
|
163
|
+
continue
|
|
164
|
+
if _is_near_full_mixed_inline_row(
|
|
165
|
+
rows,
|
|
166
|
+
index,
|
|
167
|
+
lane_width,
|
|
168
|
+
profile,
|
|
169
|
+
):
|
|
170
|
+
continue
|
|
171
|
+
if (
|
|
172
|
+
index > 0
|
|
173
|
+
and rows[index - 1][0].semantic_type == "paragraph_title"
|
|
174
|
+
and width_ratio >= 0.85
|
|
175
|
+
and gap_above < 1.0
|
|
176
|
+
and not _title_fonts_compatible(rows[index - 1][0], line)
|
|
177
|
+
):
|
|
178
|
+
continue
|
|
179
|
+
if _continues_local_body_row(
|
|
180
|
+
rows,
|
|
181
|
+
index,
|
|
182
|
+
lane_width,
|
|
183
|
+
profile,
|
|
184
|
+
):
|
|
185
|
+
continue
|
|
186
|
+
has_following_body_row = _has_following_body_row(
|
|
187
|
+
rows,
|
|
188
|
+
index,
|
|
189
|
+
lane_width,
|
|
190
|
+
lane.left,
|
|
191
|
+
profile,
|
|
192
|
+
)
|
|
193
|
+
compact_local_transition = (
|
|
194
|
+
low_coverage_style_differs
|
|
195
|
+
and left_aligned
|
|
196
|
+
and width_ratio <= 0.65
|
|
197
|
+
and gap_above >= 0.1
|
|
198
|
+
and gap_below >= 0.2
|
|
199
|
+
and has_following_body_row
|
|
200
|
+
)
|
|
201
|
+
compact_text_section = (
|
|
202
|
+
height_ratio < 0.9
|
|
203
|
+
and centered
|
|
204
|
+
and width_ratio <= 0.7
|
|
205
|
+
and gap_above >= 0.35
|
|
206
|
+
and gap_below >= 0.2
|
|
207
|
+
and (page_index != 0 or style_differs or weight_emphasized or title_prototype is not None)
|
|
208
|
+
and _has_following_compact_text_section(
|
|
209
|
+
rows,
|
|
210
|
+
index,
|
|
211
|
+
lane_width,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
weak_regular_pitch_body_candidate = (
|
|
215
|
+
document_body_profile is not None
|
|
216
|
+
and 0.9 <= line_height / document_body_profile.body_height <= 1.1
|
|
217
|
+
and title_prototype is None
|
|
218
|
+
and (not centered or width_ratio >= 0.75)
|
|
219
|
+
and not weight_emphasized
|
|
220
|
+
and gap_above_excess < 0.1
|
|
221
|
+
and gap_below_excess < 0.1
|
|
222
|
+
)
|
|
223
|
+
if weak_regular_pitch_body_candidate:
|
|
224
|
+
continue
|
|
225
|
+
if (
|
|
226
|
+
title_profile_size_conflict
|
|
227
|
+
and document_body_profile is not None
|
|
228
|
+
and line_height <= 1.18 * document_body_profile.body_height
|
|
229
|
+
and not weight_emphasized
|
|
230
|
+
):
|
|
231
|
+
continue
|
|
232
|
+
prototype_promotion = (
|
|
233
|
+
title_prototype is not None
|
|
234
|
+
and (width_ratio <= 0.9 or (bbox[2] - bbox[0]) / max(0.1, local_page_width) <= 0.8)
|
|
235
|
+
and (centered or left_aligned)
|
|
236
|
+
and (
|
|
237
|
+
(document_body_profile is not None and line_height >= 1.18 * document_body_profile.body_height)
|
|
238
|
+
or near_visual_container
|
|
239
|
+
or (has_following_body_row and gap_above_excess >= 0.1)
|
|
240
|
+
or gap_above_excess >= 0.35
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
prototype_inline_heading = (
|
|
244
|
+
document_body_profile is not None
|
|
245
|
+
and line_height <= 1.15 * document_body_profile.body_height
|
|
246
|
+
and _is_full_width_inline_heading(
|
|
247
|
+
rows,
|
|
248
|
+
index,
|
|
249
|
+
lane_width,
|
|
250
|
+
profile,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
if prototype_promotion and not prototype_inline_heading:
|
|
254
|
+
line.semantic_type = "paragraph_title"
|
|
255
|
+
selected_indices.add(index)
|
|
256
|
+
continue
|
|
257
|
+
if (
|
|
258
|
+
document_body_profile is not None
|
|
259
|
+
and 0.9 <= line_height / document_body_profile.body_height <= 1.1
|
|
260
|
+
and title_prototype is None
|
|
261
|
+
and centered
|
|
262
|
+
and not has_following_body_row
|
|
263
|
+
and not compact_text_section
|
|
264
|
+
):
|
|
265
|
+
continue
|
|
266
|
+
if height_ratio < 0.9 and not inside_front_matter and not has_following_body_row and not compact_text_section:
|
|
267
|
+
continue
|
|
268
|
+
if width_ratio >= 0.9 and height_ratio < 1.15 and not (style_differs and gap_above >= 0.65):
|
|
269
|
+
continue
|
|
270
|
+
score = 0.0
|
|
271
|
+
if style_differs or compact_local_transition:
|
|
272
|
+
score += 2.0
|
|
273
|
+
if compact_local_transition or style_support <= 0.2:
|
|
274
|
+
score += 0.75
|
|
275
|
+
if weight_emphasized:
|
|
276
|
+
score += 1.0
|
|
277
|
+
if height_ratio >= 1.18:
|
|
278
|
+
score += 2.0
|
|
279
|
+
elif height_ratio <= 0.9 and centered:
|
|
280
|
+
score += 0.75
|
|
281
|
+
if width_ratio <= 0.78:
|
|
282
|
+
score += 0.75
|
|
283
|
+
if centered and width_ratio <= 0.85:
|
|
284
|
+
score += 1.25
|
|
285
|
+
if gap_above >= 0.65:
|
|
286
|
+
score += 1.25
|
|
287
|
+
elif gap_above >= 0.35:
|
|
288
|
+
score += 0.5
|
|
289
|
+
if gap_below >= 0.35:
|
|
290
|
+
score += 0.75
|
|
291
|
+
if left_aligned:
|
|
292
|
+
score += 0.25
|
|
293
|
+
|
|
294
|
+
precisely_centered = abs(_bbox_center_x(bbox) - (lane.left + lane.right) / 2.0) <= 0.05 * lane_width
|
|
295
|
+
centered_structural_fallback = (
|
|
296
|
+
0.75 <= height_ratio <= 1.15
|
|
297
|
+
and precisely_centered
|
|
298
|
+
and width_ratio <= 0.7
|
|
299
|
+
and not inside_front_matter
|
|
300
|
+
and not (page_index == 0 and _bbox_center_y(bbox) <= 0.2 * local_page_height)
|
|
301
|
+
and 0.35 <= gap_above <= 1.5
|
|
302
|
+
and 0.2 <= gap_below <= 1.5
|
|
303
|
+
and has_following_body_row
|
|
304
|
+
and not _is_continuous_field_row(
|
|
305
|
+
rows,
|
|
306
|
+
index,
|
|
307
|
+
lane_width,
|
|
308
|
+
profile,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
strong_layout_signal = (
|
|
312
|
+
height_ratio >= 1.18
|
|
313
|
+
or style_differs
|
|
314
|
+
or compact_local_transition
|
|
315
|
+
or weight_emphasized
|
|
316
|
+
or centered_structural_fallback
|
|
317
|
+
or compact_text_section
|
|
318
|
+
)
|
|
319
|
+
if score >= 4.0 and (has_spacing_signal or compact_local_transition) and strong_layout_signal:
|
|
320
|
+
if _is_full_width_inline_heading(
|
|
321
|
+
rows,
|
|
322
|
+
index,
|
|
323
|
+
lane_width,
|
|
324
|
+
profile,
|
|
325
|
+
):
|
|
326
|
+
continue
|
|
327
|
+
line.semantic_type = "paragraph_title"
|
|
328
|
+
selected_indices.add(index)
|
|
329
|
+
|
|
330
|
+
_unify_visual_row_title_types(
|
|
331
|
+
lane,
|
|
332
|
+
selected_indices,
|
|
333
|
+
)
|
|
334
|
+
_expand_paragraph_title_neighbors(
|
|
335
|
+
lane,
|
|
336
|
+
selected_indices,
|
|
337
|
+
profile,
|
|
338
|
+
)
|
|
339
|
+
if front_matter_boundary is not None and preserve_front_matter_boundaries:
|
|
340
|
+
_protect_front_matter_title_types(
|
|
341
|
+
lane,
|
|
342
|
+
profile,
|
|
343
|
+
front_matter_boundary,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _protect_front_matter_title_types(
|
|
348
|
+
lane: _TextLane,
|
|
349
|
+
profile: _LaneBodyProfile,
|
|
350
|
+
front_matter_boundary: float,
|
|
351
|
+
) -> None:
|
|
352
|
+
"""把作者区误命中的标题降为正文,并仅保留原本需要跨行聚合的内部标记。"""
|
|
353
|
+
|
|
354
|
+
front_title_indices = [
|
|
355
|
+
index
|
|
356
|
+
for index, (line, bbox) in enumerate(lane.lines)
|
|
357
|
+
if line.semantic_type == "paragraph_title" and _bbox_center_y(bbox) <= front_matter_boundary
|
|
358
|
+
]
|
|
359
|
+
connected_indices: set[int] = set()
|
|
360
|
+
for position, first_index in enumerate(front_title_indices):
|
|
361
|
+
for second_index in front_title_indices[position + 1 :]:
|
|
362
|
+
if _should_connect_semantic_rows(
|
|
363
|
+
lane.lines[first_index],
|
|
364
|
+
lane.lines[second_index],
|
|
365
|
+
lane,
|
|
366
|
+
profile.regular_gap,
|
|
367
|
+
[],
|
|
368
|
+
[],
|
|
369
|
+
):
|
|
370
|
+
connected_indices.update((first_index, second_index))
|
|
371
|
+
for index in front_title_indices:
|
|
372
|
+
lane.lines[index][0].semantic_type = "text" if index in connected_indices else None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _visual_row_has_body_style_sibling(
|
|
376
|
+
rows: list[tuple[_LineItem, BBox]],
|
|
377
|
+
index: int,
|
|
378
|
+
) -> bool:
|
|
379
|
+
"""检查同一完整视觉行是否同时包含标题样式与正文样式 run。"""
|
|
380
|
+
|
|
381
|
+
line, _bbox = rows[index]
|
|
382
|
+
if line.visual_row_id is None:
|
|
383
|
+
return False
|
|
384
|
+
siblings = [
|
|
385
|
+
sibling for sibling, _sibling_bbox in rows if sibling is not line and sibling.visual_row_id == line.visual_row_id
|
|
386
|
+
]
|
|
387
|
+
return any(not _title_fonts_compatible(line, sibling) for sibling in siblings)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _is_near_full_mixed_inline_row(
|
|
391
|
+
rows: list[tuple[_LineItem, BBox]],
|
|
392
|
+
index: int,
|
|
393
|
+
lane_width: float,
|
|
394
|
+
profile: _LaneBodyProfile,
|
|
395
|
+
) -> bool:
|
|
396
|
+
"""识别近满栏混合字体行内强调,避免把粗体条目头单独标成标题。"""
|
|
397
|
+
|
|
398
|
+
line, bbox = rows[index]
|
|
399
|
+
line_height = _line_effective_height(line, bbox)
|
|
400
|
+
if (
|
|
401
|
+
line.font_coverage >= 0.95
|
|
402
|
+
or (bbox[2] - bbox[0]) < 0.85 * lane_width
|
|
403
|
+
or not 0.85 <= line_height / profile.body_height <= 1.15
|
|
404
|
+
or index + 1 >= len(rows)
|
|
405
|
+
):
|
|
406
|
+
return False
|
|
407
|
+
next_line, next_bbox = rows[index + 1]
|
|
408
|
+
next_height = _line_effective_height(next_line, next_bbox)
|
|
409
|
+
gap_below = _effective_text_row_gap(rows[index], rows[index + 1])
|
|
410
|
+
return (
|
|
411
|
+
next_line.semantic_type is None
|
|
412
|
+
and 0.8 <= next_height / profile.body_height <= 1.2
|
|
413
|
+
and gap_below <= profile.regular_gap + 0.25 * profile.body_height
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _continues_local_body_row(
|
|
418
|
+
rows: list[tuple[_LineItem, BBox]],
|
|
419
|
+
index: int,
|
|
420
|
+
lane_width: float,
|
|
421
|
+
profile: _LaneBodyProfile,
|
|
422
|
+
) -> bool:
|
|
423
|
+
"""识别紧随同字体满行的正文尾行,阻止全局小字号基线造成标题误判。"""
|
|
424
|
+
|
|
425
|
+
if index <= 0:
|
|
426
|
+
return False
|
|
427
|
+
previous_line, previous_bbox = rows[index - 1]
|
|
428
|
+
line, bbox = rows[index]
|
|
429
|
+
if previous_line.semantic_type is not None:
|
|
430
|
+
return False
|
|
431
|
+
previous_visual_row = (
|
|
432
|
+
[item for item in rows[:index] if item[0].visual_row_id == previous_line.visual_row_id]
|
|
433
|
+
if previous_line.visual_row_id is not None
|
|
434
|
+
else [rows[index - 1]]
|
|
435
|
+
)
|
|
436
|
+
previous_style_members = [
|
|
437
|
+
member
|
|
438
|
+
for member, _member_bbox in previous_visual_row
|
|
439
|
+
if member.font_signature is not None and member.font_coverage >= 0.5
|
|
440
|
+
]
|
|
441
|
+
if (
|
|
442
|
+
line.font_signature is not None
|
|
443
|
+
and previous_style_members
|
|
444
|
+
and not any(
|
|
445
|
+
_font_signatures_share_family(member.font_signature, line.font_signature)
|
|
446
|
+
and not _font_weights_conflict(member, line)
|
|
447
|
+
for member in previous_style_members
|
|
448
|
+
)
|
|
449
|
+
):
|
|
450
|
+
return False
|
|
451
|
+
previous_bbox = _bbox_union_many([member_bbox for _member, member_bbox in previous_visual_row])
|
|
452
|
+
previous_height = statistics.median(
|
|
453
|
+
_line_effective_height(member, member_bbox) for member, member_bbox in previous_visual_row
|
|
454
|
+
)
|
|
455
|
+
line_height = _line_effective_height(line, bbox)
|
|
456
|
+
pair_height = max(previous_height, line_height)
|
|
457
|
+
gap = _effective_text_row_gap(
|
|
458
|
+
(previous_line, previous_bbox),
|
|
459
|
+
rows[index],
|
|
460
|
+
)
|
|
461
|
+
return (
|
|
462
|
+
0.8 <= line_height / previous_height <= 1.25
|
|
463
|
+
and previous_bbox[2] - previous_bbox[0] >= 0.75 * lane_width
|
|
464
|
+
and abs(bbox[0] - previous_bbox[0]) <= 2.0 * pair_height
|
|
465
|
+
and -0.25 * pair_height <= gap <= profile.regular_gap + pair_height
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _is_continuous_field_row(
|
|
470
|
+
rows: list[tuple[_LineItem, BBox]],
|
|
471
|
+
index: int,
|
|
472
|
+
lane_width: float,
|
|
473
|
+
profile: _LaneBodyProfile,
|
|
474
|
+
) -> bool:
|
|
475
|
+
"""识别同缩进、同尺度且紧邻的连续字段行,阻止其借居中误差成为标题。"""
|
|
476
|
+
|
|
477
|
+
_line, bbox = rows[index]
|
|
478
|
+
line_height = _line_effective_height(*rows[index])
|
|
479
|
+
if not 0.85 <= line_height / profile.body_height <= 1.15:
|
|
480
|
+
return False
|
|
481
|
+
for neighbor_index in (index - 1, index + 1):
|
|
482
|
+
if not 0 <= neighbor_index < len(rows):
|
|
483
|
+
continue
|
|
484
|
+
neighbor_line, neighbor_bbox = rows[neighbor_index]
|
|
485
|
+
neighbor_height = _line_effective_height(neighbor_line, neighbor_bbox)
|
|
486
|
+
if (
|
|
487
|
+
neighbor_line.semantic_type is None
|
|
488
|
+
and 0.85 <= neighbor_height / profile.body_height <= 1.15
|
|
489
|
+
and neighbor_bbox[2] - neighbor_bbox[0] >= 0.35 * lane_width
|
|
490
|
+
and abs(neighbor_bbox[0] - bbox[0]) <= 0.75 * profile.body_height
|
|
491
|
+
and max(
|
|
492
|
+
_effective_text_row_gap(rows[neighbor_index], rows[index]),
|
|
493
|
+
_effective_text_row_gap(rows[index], rows[neighbor_index]),
|
|
494
|
+
)
|
|
495
|
+
<= 1.5 * profile.body_height
|
|
496
|
+
):
|
|
497
|
+
return True
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _is_full_width_inline_heading(
|
|
502
|
+
rows: list[tuple[_LineItem, BBox]],
|
|
503
|
+
index: int,
|
|
504
|
+
lane_width: float,
|
|
505
|
+
profile: _LaneBodyProfile,
|
|
506
|
+
) -> bool:
|
|
507
|
+
"""识别后接常规正文续行的满栏正常字号行内标题。"""
|
|
508
|
+
|
|
509
|
+
line, bbox = rows[index]
|
|
510
|
+
line_height = _line_effective_height(line, bbox)
|
|
511
|
+
if (
|
|
512
|
+
not 0.85 <= line_height / profile.body_height <= 1.15
|
|
513
|
+
or (bbox[2] - bbox[0]) < 0.9 * lane_width
|
|
514
|
+
or index + 1 >= len(rows)
|
|
515
|
+
):
|
|
516
|
+
return False
|
|
517
|
+
|
|
518
|
+
next_line, next_bbox = rows[index + 1]
|
|
519
|
+
next_height = _line_effective_height(next_line, next_bbox)
|
|
520
|
+
next_uses_body_font = (
|
|
521
|
+
profile.body_font is None
|
|
522
|
+
or next_line.font_signature is None
|
|
523
|
+
or next_line.font_coverage < 0.75
|
|
524
|
+
or _font_signatures_share_family(
|
|
525
|
+
next_line.font_signature,
|
|
526
|
+
profile.body_font,
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
return (
|
|
530
|
+
next_line.semantic_type is None
|
|
531
|
+
and next_uses_body_font
|
|
532
|
+
and 0.75 <= next_height / profile.body_height <= 1.15
|
|
533
|
+
and (next_bbox[2] - next_bbox[0]) >= 0.75 * lane_width
|
|
534
|
+
and _effective_text_row_gap(rows[index], rows[index + 1]) <= profile.regular_gap + 0.25 * profile.body_height
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _has_following_body_row(
|
|
539
|
+
rows: list[tuple[_LineItem, BBox]],
|
|
540
|
+
index: int,
|
|
541
|
+
lane_width: float,
|
|
542
|
+
lane_left: float,
|
|
543
|
+
profile: _LaneBodyProfile,
|
|
544
|
+
) -> bool:
|
|
545
|
+
"""只检查紧邻且空间相关的下一行,禁止用远处正文支撑小字号标题。"""
|
|
546
|
+
|
|
547
|
+
if index + 1 >= len(rows):
|
|
548
|
+
return False
|
|
549
|
+
current = rows[index]
|
|
550
|
+
line, bbox = rows[index + 1]
|
|
551
|
+
gap = _effective_text_row_gap(current, (line, bbox))
|
|
552
|
+
horizontally_related = (
|
|
553
|
+
_bbox_axis_overlap_ratio(current[1], bbox, axis="x") >= 0.35
|
|
554
|
+
or abs(current[1][0] - bbox[0]) <= 0.75 * profile.body_height
|
|
555
|
+
)
|
|
556
|
+
current_centered = abs(_bbox_center_x(current[1]) - (lane_left + lane_width / 2.0)) <= 0.12 * lane_width
|
|
557
|
+
compact_left_aligned = (
|
|
558
|
+
current[1][2] - current[1][0] <= 0.35 * lane_width and abs(current[1][0] - lane_left) <= 0.65 * profile.body_height
|
|
559
|
+
)
|
|
560
|
+
maximum_gap = (1.5 if current_centered else 1.25 if compact_left_aligned else 0.75) * profile.body_height
|
|
561
|
+
height_ratio = _line_effective_height(line, bbox) / profile.body_height
|
|
562
|
+
width_ratio = (bbox[2] - bbox[0]) / lane_width
|
|
563
|
+
return (
|
|
564
|
+
line.semantic_type is None
|
|
565
|
+
and horizontally_related
|
|
566
|
+
and -0.25 * profile.body_height <= gap <= maximum_gap
|
|
567
|
+
and 0.9 <= height_ratio <= 1.2
|
|
568
|
+
and width_ratio >= 0.45
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _has_following_compact_text_section(
|
|
573
|
+
rows: list[tuple[_LineItem, BBox]],
|
|
574
|
+
index: int,
|
|
575
|
+
lane_width: float,
|
|
576
|
+
) -> bool:
|
|
577
|
+
"""检查小字号候选后是否紧接三行同尺度的宽文本区段。"""
|
|
578
|
+
|
|
579
|
+
candidate = rows[index]
|
|
580
|
+
candidate_height = _line_effective_height(*candidate)
|
|
581
|
+
following_rows = rows[index + 1 : index + 4]
|
|
582
|
+
if len(following_rows) < 3:
|
|
583
|
+
return False
|
|
584
|
+
|
|
585
|
+
previous = candidate
|
|
586
|
+
for current in following_rows:
|
|
587
|
+
line, bbox = current
|
|
588
|
+
height_ratio = _line_effective_height(line, bbox) / candidate_height
|
|
589
|
+
width_ratio = (bbox[2] - bbox[0]) / lane_width
|
|
590
|
+
gap = _effective_text_row_gap(previous, current)
|
|
591
|
+
if (
|
|
592
|
+
line.semantic_type is not None
|
|
593
|
+
or not 0.75 <= height_ratio <= 1.25
|
|
594
|
+
or width_ratio < 0.45
|
|
595
|
+
or not -0.25 * candidate_height <= gap <= 0.75 * candidate_height
|
|
596
|
+
):
|
|
597
|
+
return False
|
|
598
|
+
previous = current
|
|
599
|
+
return True
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _unify_visual_row_title_types(
|
|
603
|
+
lane: _TextLane,
|
|
604
|
+
selected_indices: set[int],
|
|
605
|
+
) -> None:
|
|
606
|
+
"""按完整 visual_row_id 统一标题类型:同样式整行晋升,混合样式整行降级。"""
|
|
607
|
+
|
|
608
|
+
row_indices: dict[int, list[int]] = {}
|
|
609
|
+
for index, (line, _bbox) in enumerate(lane.lines):
|
|
610
|
+
if line.visual_row_id is not None:
|
|
611
|
+
row_indices.setdefault(line.visual_row_id, []).append(index)
|
|
612
|
+
for indices in row_indices.values():
|
|
613
|
+
title_indices = [index for index in indices if lane.lines[index][0].semantic_type == "paragraph_title"]
|
|
614
|
+
if not title_indices:
|
|
615
|
+
continue
|
|
616
|
+
anchor_line, anchor_bbox = lane.lines[title_indices[0]]
|
|
617
|
+
anchor_height = _line_effective_height(anchor_line, anchor_bbox)
|
|
618
|
+
compatible = all(
|
|
619
|
+
0.75 <= _line_effective_height(lane.lines[index][0], lane.lines[index][1]) / anchor_height <= 1.25
|
|
620
|
+
and _title_fonts_compatible(anchor_line, lane.lines[index][0])
|
|
621
|
+
for index in indices
|
|
622
|
+
)
|
|
623
|
+
if compatible:
|
|
624
|
+
for index in indices:
|
|
625
|
+
lane.lines[index][0].semantic_type = "paragraph_title"
|
|
626
|
+
selected_indices.add(index)
|
|
627
|
+
continue
|
|
628
|
+
for index in title_indices:
|
|
629
|
+
lane.lines[index][0].semantic_type = None
|
|
630
|
+
selected_indices.discard(index)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _infer_front_matter_boundary(
|
|
634
|
+
lane: _TextLane,
|
|
635
|
+
profile: _LaneBodyProfile,
|
|
636
|
+
local_page_height: float,
|
|
637
|
+
*,
|
|
638
|
+
document_title_bottom: float | None,
|
|
639
|
+
) -> float | None:
|
|
640
|
+
"""用短行后衔接满栏正文的几何转折点界定首页作者等前置信息。"""
|
|
641
|
+
|
|
642
|
+
if document_title_bottom is None:
|
|
643
|
+
return None
|
|
644
|
+
default_boundary = max(
|
|
645
|
+
document_title_bottom + 2.5 * profile.body_height,
|
|
646
|
+
0.39 * local_page_height,
|
|
647
|
+
)
|
|
648
|
+
lane_width = max(0.1, lane.right - lane.left)
|
|
649
|
+
for index, (line, bbox) in enumerate(lane.lines[:-1]):
|
|
650
|
+
if bbox[1] <= document_title_bottom:
|
|
651
|
+
continue
|
|
652
|
+
next_line, next_bbox = lane.lines[index + 1]
|
|
653
|
+
width_ratio = (bbox[2] - bbox[0]) / lane_width
|
|
654
|
+
next_width_ratio = (next_bbox[2] - next_bbox[0]) / lane_width
|
|
655
|
+
left_aligned = abs(bbox[0] - lane.left) <= 0.65 * profile.body_height
|
|
656
|
+
centered = abs(_bbox_center_x(bbox) - (lane.left + lane.right) / 2.0) <= 0.12 * lane_width
|
|
657
|
+
next_height_ratio = _line_effective_height(next_line, next_bbox) / profile.body_height
|
|
658
|
+
gap_below = max(0.0, _effective_text_row_gap((line, bbox), (next_line, next_bbox)))
|
|
659
|
+
if (
|
|
660
|
+
width_ratio <= 0.35
|
|
661
|
+
and next_width_ratio >= 0.75
|
|
662
|
+
and (left_aligned or centered)
|
|
663
|
+
and 0.75 <= next_height_ratio <= 1.35
|
|
664
|
+
and gap_below <= 1.5 * profile.body_height
|
|
665
|
+
):
|
|
666
|
+
return min(default_boundary, bbox[1] - 0.1)
|
|
667
|
+
return default_boundary
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _normalized_title_gap(
|
|
671
|
+
rows: list[tuple[_LineItem, BBox]],
|
|
672
|
+
index: int,
|
|
673
|
+
*,
|
|
674
|
+
direction: Literal[-1, 1],
|
|
675
|
+
body_height: float,
|
|
676
|
+
physical_gaps: dict[int, tuple[float | None, float | None]],
|
|
677
|
+
) -> float:
|
|
678
|
+
"""返回栏内邻行和页面最近物理邻行中较小的归一化净空。"""
|
|
679
|
+
|
|
680
|
+
neighbor_index = index + direction
|
|
681
|
+
if 0 <= neighbor_index < len(rows):
|
|
682
|
+
if direction < 0:
|
|
683
|
+
lane_gap = _effective_text_row_gap(rows[neighbor_index], rows[index])
|
|
684
|
+
else:
|
|
685
|
+
lane_gap = _effective_text_row_gap(rows[index], rows[neighbor_index])
|
|
686
|
+
else:
|
|
687
|
+
lane_gap = 0.5 * body_height
|
|
688
|
+
physical_pair = physical_gaps.get(rows[index][0].source_index, (None, None))
|
|
689
|
+
physical_gap = physical_pair[0 if direction < 0 else 1]
|
|
690
|
+
gap = lane_gap if physical_gap is None else min(lane_gap, physical_gap)
|
|
691
|
+
return max(0.0, gap) / max(0.1, body_height)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _expand_paragraph_title_neighbors(
|
|
695
|
+
lane: _TextLane,
|
|
696
|
+
selected_indices: set[int],
|
|
697
|
+
profile: _LaneBodyProfile,
|
|
698
|
+
) -> None:
|
|
699
|
+
"""用相邻行的字体、尺寸、对齐和紧凑净空补齐折行段落标题。"""
|
|
700
|
+
|
|
701
|
+
if not selected_indices:
|
|
702
|
+
return
|
|
703
|
+
rows = lane.lines
|
|
704
|
+
lane_width = max(0.1, lane.right - lane.left)
|
|
705
|
+
pending = list(selected_indices)
|
|
706
|
+
while pending:
|
|
707
|
+
selected_index = pending.pop()
|
|
708
|
+
selected_line, selected_bbox = rows[selected_index]
|
|
709
|
+
selected_height = _line_effective_height(selected_line, selected_bbox)
|
|
710
|
+
for candidate_index in (selected_index - 1, selected_index + 1):
|
|
711
|
+
if not 0 <= candidate_index < len(rows) or candidate_index in selected_indices:
|
|
712
|
+
continue
|
|
713
|
+
candidate_line, candidate_bbox = rows[candidate_index]
|
|
714
|
+
if candidate_line.semantic_type is not None:
|
|
715
|
+
continue
|
|
716
|
+
candidate_height = _line_effective_height(candidate_line, candidate_bbox)
|
|
717
|
+
if not 0.75 <= candidate_height / selected_height <= 1.25:
|
|
718
|
+
continue
|
|
719
|
+
if candidate_bbox[2] - candidate_bbox[0] > 0.8 * lane_width:
|
|
720
|
+
continue
|
|
721
|
+
if (
|
|
722
|
+
selected_line.font_signature is not None
|
|
723
|
+
and candidate_line.font_signature is not None
|
|
724
|
+
and selected_line.font_signature != candidate_line.font_signature
|
|
725
|
+
):
|
|
726
|
+
continue
|
|
727
|
+
if not _title_fonts_compatible(selected_line, candidate_line):
|
|
728
|
+
continue
|
|
729
|
+
vertical_gap = max(
|
|
730
|
+
candidate_bbox[1] - selected_bbox[3],
|
|
731
|
+
selected_bbox[1] - candidate_bbox[3],
|
|
732
|
+
0.0,
|
|
733
|
+
)
|
|
734
|
+
centers_align = abs(_bbox_center_x(candidate_bbox) - _bbox_center_x(selected_bbox)) <= 0.15 * lane_width
|
|
735
|
+
left_edges_align = abs(candidate_bbox[0] - selected_bbox[0]) <= 0.75 * profile.body_height
|
|
736
|
+
wrapped_indent = abs(candidate_bbox[0] - selected_bbox[0]) <= 2.0 * profile.body_height
|
|
737
|
+
if vertical_gap > 0.65 * profile.body_height or not (centers_align or left_edges_align or wrapped_indent):
|
|
738
|
+
continue
|
|
739
|
+
candidate_line.semantic_type = "paragraph_title"
|
|
740
|
+
selected_indices.add(candidate_index)
|
|
741
|
+
pending.append(candidate_index)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
__all__ = [
|
|
745
|
+
"_classify_paragraph_titles_in_lane",
|
|
746
|
+
"_protect_front_matter_title_types",
|
|
747
|
+
"_visual_row_has_body_style_sibling",
|
|
748
|
+
"_is_near_full_mixed_inline_row",
|
|
749
|
+
"_continues_local_body_row",
|
|
750
|
+
"_is_continuous_field_row",
|
|
751
|
+
"_is_full_width_inline_heading",
|
|
752
|
+
"_has_following_body_row",
|
|
753
|
+
"_has_following_compact_text_section",
|
|
754
|
+
"_unify_visual_row_title_types",
|
|
755
|
+
"_infer_front_matter_boundary",
|
|
756
|
+
"_normalized_title_gap",
|
|
757
|
+
"_expand_paragraph_title_neighbors",
|
|
758
|
+
]
|