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,975 @@
|
|
|
1
|
+
"""按公式区域和字符几何识别上下标证据。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import statistics
|
|
6
|
+
import unicodedata
|
|
7
|
+
from typing import Any, Literal, Sequence
|
|
8
|
+
|
|
9
|
+
from .....schema import BBox
|
|
10
|
+
from ..geometry import _rotate_bbox_to_upright
|
|
11
|
+
from ..script_geometry import ScriptRole, classify_char_script_roles
|
|
12
|
+
from .common import _coerce_bbox, _normalize_match_fragment, _ordered_line_chars
|
|
13
|
+
from .types import (
|
|
14
|
+
_PDF_SCRIPT_AUTHOR_MARKS,
|
|
15
|
+
_PDF_SCRIPT_CITATION_BRACKETS,
|
|
16
|
+
_PDF_SCRIPT_COMPACT_JOINERS,
|
|
17
|
+
_PDF_SCRIPT_MATH_BASE_CHARS,
|
|
18
|
+
_PDF_SCRIPT_SIGN_CHARS,
|
|
19
|
+
_PDF_SCRIPT_SPACED_OPERATORS,
|
|
20
|
+
_PDF_SCRIPT_TOKEN_CONNECTORS,
|
|
21
|
+
_PDF_SCRIPT_TRAILING_MARKS,
|
|
22
|
+
PDFTextScriptLine,
|
|
23
|
+
PDFTextScriptRange,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _rotate_origin_to_upright(
|
|
28
|
+
origin: tuple[float, float],
|
|
29
|
+
page_size: tuple[float, float],
|
|
30
|
+
angle: int,
|
|
31
|
+
) -> tuple[float, float]:
|
|
32
|
+
"""把页面字符 origin 旋到当前 Flash 行的局部正向坐标。"""
|
|
33
|
+
x, y = origin
|
|
34
|
+
page_width, page_height = page_size
|
|
35
|
+
if angle == 270:
|
|
36
|
+
return page_height - y, x
|
|
37
|
+
if angle == 90:
|
|
38
|
+
return y, page_width - x
|
|
39
|
+
if angle == 180:
|
|
40
|
+
return page_width - x, page_height - y
|
|
41
|
+
return origin
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _bbox_center_inside_region(bbox: BBox, region: BBox) -> bool:
|
|
45
|
+
"""判断字符 tight bbox 中心是否落入公式区域。"""
|
|
46
|
+
center_x = (bbox[0] + bbox[2]) / 2
|
|
47
|
+
center_y = (bbox[1] + bbox[3]) / 2
|
|
48
|
+
return region[0] <= center_x <= region[2] and region[1] <= center_y <= region[3]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _script_region_memberships(
|
|
52
|
+
chars: list[dict[str, Any]],
|
|
53
|
+
tight_bboxes: dict[int, BBox],
|
|
54
|
+
regions: list[BBox],
|
|
55
|
+
) -> list[int | None]:
|
|
56
|
+
"""按页面 tight 中心把字符分配到公式区域,区域外返回 None。"""
|
|
57
|
+
memberships: list[int | None] = []
|
|
58
|
+
for char in chars:
|
|
59
|
+
char_idx = char.get("char_idx")
|
|
60
|
+
tight_bbox = tight_bboxes.get(char_idx) if isinstance(char_idx, int) else None
|
|
61
|
+
region_index = None
|
|
62
|
+
if tight_bbox is not None:
|
|
63
|
+
region_index = next(
|
|
64
|
+
(index for index, region in enumerate(regions) if _bbox_center_inside_region(tight_bbox, region)),
|
|
65
|
+
None,
|
|
66
|
+
)
|
|
67
|
+
memberships.append(region_index)
|
|
68
|
+
return memberships
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _script_char_text(char: dict[str, Any]) -> str:
|
|
72
|
+
"""返回单字符脚本判定使用的稳定文本。"""
|
|
73
|
+
return str(char.get("char", ""))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _is_cjk_text(text: str) -> bool:
|
|
77
|
+
"""判断单字符是否属于 CJK、日文假名或韩文书写系统。"""
|
|
78
|
+
if len(text) != 1:
|
|
79
|
+
return False
|
|
80
|
+
codepoint = ord(text)
|
|
81
|
+
return (
|
|
82
|
+
0x3400 <= codepoint <= 0x4DBF
|
|
83
|
+
or 0x4E00 <= codepoint <= 0x9FFF
|
|
84
|
+
or 0xF900 <= codepoint <= 0xFAFF
|
|
85
|
+
or 0x3040 <= codepoint <= 0x30FF
|
|
86
|
+
or 0xAC00 <= codepoint <= 0xD7AF
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _is_math_identifier_char(text: str) -> bool:
|
|
91
|
+
"""识别可与拉丁 base/index 共同组成数学 token 的字母数字字符。"""
|
|
92
|
+
if len(text) != 1 or _is_cjk_text(text):
|
|
93
|
+
return False
|
|
94
|
+
if text.isascii():
|
|
95
|
+
return text.isalnum()
|
|
96
|
+
if "0" <= text <= "9":
|
|
97
|
+
return True
|
|
98
|
+
category = unicodedata.category(text)
|
|
99
|
+
unicode_name = unicodedata.name(text, "")
|
|
100
|
+
return (
|
|
101
|
+
text in _PDF_SCRIPT_MATH_BASE_CHARS
|
|
102
|
+
or "GREEK" in unicode_name
|
|
103
|
+
or "MATHEMATICAL" in unicode_name
|
|
104
|
+
or category in {"Lu", "Ll", "Lm"}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _is_math_script_token_char(text: str) -> bool:
|
|
109
|
+
"""判断字符是否属于可按 source order 重新锚定的数学 token。"""
|
|
110
|
+
return _is_math_identifier_char(text) or text in _PDF_SCRIPT_TOKEN_CONNECTORS
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _iter_math_script_tokens(chars: list[dict[str, Any]]) -> list[list[int]]:
|
|
114
|
+
"""按连续数学 identifier 和连接符切分局部 token,并在 CJK 边界断开。"""
|
|
115
|
+
tokens: list[list[int]] = []
|
|
116
|
+
current: list[int] = []
|
|
117
|
+
for index, char in enumerate(chars):
|
|
118
|
+
if _is_math_script_token_char(_script_char_text(char)):
|
|
119
|
+
current.append(index)
|
|
120
|
+
continue
|
|
121
|
+
if current:
|
|
122
|
+
tokens.append(current)
|
|
123
|
+
current = []
|
|
124
|
+
if current:
|
|
125
|
+
tokens.append(current)
|
|
126
|
+
return tokens
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _citation_script_indices(chars: list[dict[str, Any]], roles: list[ScriptRole]) -> set[int]:
|
|
130
|
+
"""识别方括号引用区间,避免保守 token 规则删除数字引用。"""
|
|
131
|
+
protected: set[int] = set()
|
|
132
|
+
for start, char in enumerate(chars):
|
|
133
|
+
closing = _PDF_SCRIPT_CITATION_BRACKETS.get(_script_char_text(char))
|
|
134
|
+
if closing is None:
|
|
135
|
+
continue
|
|
136
|
+
for end in range(start + 1, min(len(chars), start + 16)):
|
|
137
|
+
if _script_char_text(chars[end]) != closing:
|
|
138
|
+
continue
|
|
139
|
+
if any(roles[index] != "body" and _script_char_text(chars[index]).isalnum() for index in range(start + 1, end)):
|
|
140
|
+
protected.update(range(start, end + 1))
|
|
141
|
+
break
|
|
142
|
+
return protected
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _token_origin(
|
|
146
|
+
char: dict[str, Any],
|
|
147
|
+
origins: dict[int, tuple[float, float]],
|
|
148
|
+
) -> float | None:
|
|
149
|
+
"""读取 token 字符的局部正向 origin y。"""
|
|
150
|
+
char_idx = char.get("char_idx")
|
|
151
|
+
origin = origins.get(char_idx) if isinstance(char_idx, int) else None
|
|
152
|
+
return float(origin[1]) if origin is not None else None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _token_tight_height(
|
|
156
|
+
char: dict[str, Any],
|
|
157
|
+
tight_bboxes: dict[int, BBox],
|
|
158
|
+
) -> float:
|
|
159
|
+
"""读取 token 字符的局部正向 tight 高度。"""
|
|
160
|
+
char_idx = char.get("char_idx")
|
|
161
|
+
bbox = tight_bboxes.get(char_idx) if isinstance(char_idx, int) else None
|
|
162
|
+
return max(0.0, bbox[3] - bbox[1]) if bbox is not None else 0.0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _has_adjacent_math_base(
|
|
166
|
+
chars: list[dict[str, Any]],
|
|
167
|
+
index: int,
|
|
168
|
+
roles: list[ScriptRole],
|
|
169
|
+
tight_bboxes: dict[int, BBox],
|
|
170
|
+
origins: dict[int, tuple[float, float]],
|
|
171
|
+
) -> bool:
|
|
172
|
+
"""判断孤立索引左侧是否存在紧邻且位移明确的非 CJK 数学 base。"""
|
|
173
|
+
if index <= 0 or roles[index - 1] != "body":
|
|
174
|
+
return False
|
|
175
|
+
base_text = _script_char_text(chars[index - 1])
|
|
176
|
+
if not _is_math_identifier_char(base_text):
|
|
177
|
+
return False
|
|
178
|
+
base_origin = _token_origin(chars[index - 1], origins)
|
|
179
|
+
script_origin = _token_origin(chars[index], origins)
|
|
180
|
+
base_height = _token_tight_height(chars[index - 1], tight_bboxes)
|
|
181
|
+
base_idx = chars[index - 1].get("char_idx")
|
|
182
|
+
script_idx = chars[index].get("char_idx")
|
|
183
|
+
base_bbox = tight_bboxes.get(base_idx) if isinstance(base_idx, int) else None
|
|
184
|
+
script_bbox = tight_bboxes.get(script_idx) if isinstance(script_idx, int) else None
|
|
185
|
+
if base_origin is None or script_origin is None or base_bbox is None or script_bbox is None:
|
|
186
|
+
return False
|
|
187
|
+
return (
|
|
188
|
+
_bbox_axis_overlap(base_bbox, script_bbox, axis="y") > 0
|
|
189
|
+
or _horizontal_gap_between_bboxes(base_bbox, script_bbox) <= max(2.0, 0.5 * base_height)
|
|
190
|
+
) and abs(script_origin - base_origin) >= max(0.35, 0.08 * base_height)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _horizontal_gap_between_bboxes(first: BBox, second: BBox) -> float:
|
|
194
|
+
"""返回两个 tight bbox 的水平间隙。"""
|
|
195
|
+
return max(0.0, first[0] - second[2], second[0] - first[2])
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _token_split_position(
|
|
199
|
+
chars: list[dict[str, Any]],
|
|
200
|
+
token: list[int],
|
|
201
|
+
roles: list[ScriptRole],
|
|
202
|
+
tight_bboxes: dict[int, BBox],
|
|
203
|
+
origins: dict[int, tuple[float, float]],
|
|
204
|
+
) -> int | None:
|
|
205
|
+
"""用最左 origin 簇和显式连接符确定 base 与索引的分界。"""
|
|
206
|
+
alnum_positions = [index for index in token if _is_math_identifier_char(_script_char_text(chars[index]))]
|
|
207
|
+
if len(alnum_positions) < 2:
|
|
208
|
+
return None
|
|
209
|
+
first = alnum_positions[0]
|
|
210
|
+
first_origin = _token_origin(chars[first], origins)
|
|
211
|
+
first_height = _token_tight_height(chars[first], tight_bboxes)
|
|
212
|
+
origin_tolerance = max(0.35, 0.06 * first_height)
|
|
213
|
+
leading_connectors = [
|
|
214
|
+
index for index in token if index < first and _script_char_text(chars[index]) in _PDF_SCRIPT_TOKEN_CONNECTORS
|
|
215
|
+
]
|
|
216
|
+
if leading_connectors:
|
|
217
|
+
return alnum_positions[1]
|
|
218
|
+
scripted_positions = [position for position in alnum_positions[1:] if roles[position] != "body"]
|
|
219
|
+
if roles[first] == "body" and scripted_positions:
|
|
220
|
+
first_scripted = scripted_positions[0]
|
|
221
|
+
prefix = [index for index in token if index < first_scripted]
|
|
222
|
+
suffix = [index for index in token if index >= first_scripted]
|
|
223
|
+
if (
|
|
224
|
+
len(prefix) >= 2
|
|
225
|
+
and all(roles[index] == "body" and _script_char_text(chars[index]).isalpha() for index in prefix)
|
|
226
|
+
and all(roles[index] == "sup" and _script_char_text(chars[index]).isdigit() for index in suffix)
|
|
227
|
+
):
|
|
228
|
+
# 姓名、词语后的数字上标已具备明确边界,不用正文内部的下伸字形重新切分。
|
|
229
|
+
return first_scripted
|
|
230
|
+
for position in alnum_positions[1:]:
|
|
231
|
+
if any(_script_char_text(chars[index]) in _PDF_SCRIPT_TOKEN_CONNECTORS for index in range(first + 1, position)):
|
|
232
|
+
return position
|
|
233
|
+
origin = _token_origin(chars[position], origins)
|
|
234
|
+
if first_origin is not None and origin is not None and abs(origin - first_origin) > origin_tolerance:
|
|
235
|
+
return position
|
|
236
|
+
if roles[first] == "body" and scripted_positions:
|
|
237
|
+
return scripted_positions[0]
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _script_geometry_is_aligned(
|
|
242
|
+
chars: list[dict[str, Any]],
|
|
243
|
+
first: int,
|
|
244
|
+
second: int,
|
|
245
|
+
tight_bboxes: dict[int, BBox],
|
|
246
|
+
origins: dict[int, tuple[float, float]],
|
|
247
|
+
) -> bool:
|
|
248
|
+
"""判断两个字符是否处在同一 displaced baseline 上。"""
|
|
249
|
+
first_origin = _token_origin(chars[first], origins)
|
|
250
|
+
second_origin = _token_origin(chars[second], origins)
|
|
251
|
+
first_height = _token_tight_height(chars[first], tight_bboxes)
|
|
252
|
+
second_height = _token_tight_height(chars[second], tight_bboxes)
|
|
253
|
+
first_idx = chars[first].get("char_idx")
|
|
254
|
+
second_idx = chars[second].get("char_idx")
|
|
255
|
+
first_bbox = tight_bboxes.get(first_idx) if isinstance(first_idx, int) else None
|
|
256
|
+
second_bbox = tight_bboxes.get(second_idx) if isinstance(second_idx, int) else None
|
|
257
|
+
if first_origin is None or second_origin is None or first_bbox is None or second_bbox is None:
|
|
258
|
+
return False
|
|
259
|
+
scale = max(first_height, second_height, 1.0)
|
|
260
|
+
first_center = (first_bbox[1] + first_bbox[3]) / 2
|
|
261
|
+
second_center = (second_bbox[1] + second_bbox[3]) / 2
|
|
262
|
+
return abs(first_origin - second_origin) <= max(0.35, 0.06 * scale) and abs(first_center - second_center) <= max(
|
|
263
|
+
0.75,
|
|
264
|
+
0.3 * scale,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _nearest_nonspace_index(
|
|
269
|
+
chars: list[dict[str, Any]],
|
|
270
|
+
start: int,
|
|
271
|
+
step: Literal[-1, 1],
|
|
272
|
+
) -> int | None:
|
|
273
|
+
"""从指定位置向前或向后查找最近的非空白字符。"""
|
|
274
|
+
index = start + step
|
|
275
|
+
while 0 <= index < len(chars):
|
|
276
|
+
if not _script_char_text(chars[index]).isspace():
|
|
277
|
+
return index
|
|
278
|
+
index += step
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _close_spaced_script_operators(
|
|
283
|
+
chars: list[dict[str, Any]],
|
|
284
|
+
roles: list[ScriptRole],
|
|
285
|
+
tight_bboxes: dict[int, BBox],
|
|
286
|
+
origins: dict[int, tuple[float, float]],
|
|
287
|
+
) -> None:
|
|
288
|
+
"""跨少量 PDF 空格闭合同基线的 `1 - x` 一类角标 run。"""
|
|
289
|
+
for seed, role in enumerate(list(roles)):
|
|
290
|
+
if role == "body" or not _is_math_identifier_char(_script_char_text(chars[seed])):
|
|
291
|
+
continue
|
|
292
|
+
operator_index = _nearest_nonspace_index(chars, seed, 1)
|
|
293
|
+
if operator_index is None or operator_index - seed > 3:
|
|
294
|
+
continue
|
|
295
|
+
if _script_char_text(chars[operator_index]) not in _PDF_SCRIPT_SPACED_OPERATORS:
|
|
296
|
+
continue
|
|
297
|
+
target = _nearest_nonspace_index(chars, operator_index, 1)
|
|
298
|
+
if target is None or target - operator_index > 3:
|
|
299
|
+
continue
|
|
300
|
+
if not _is_math_identifier_char(_script_char_text(chars[target])):
|
|
301
|
+
continue
|
|
302
|
+
if not _script_geometry_is_aligned(chars, seed, operator_index, tight_bboxes, origins):
|
|
303
|
+
continue
|
|
304
|
+
if not _script_geometry_is_aligned(chars, seed, target, tight_bboxes, origins):
|
|
305
|
+
continue
|
|
306
|
+
roles[operator_index] = role
|
|
307
|
+
roles[target] = role
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _close_compact_aligned_script_suffixes(
|
|
311
|
+
chars: list[dict[str, Any]],
|
|
312
|
+
raw_roles: list[ScriptRole],
|
|
313
|
+
refined_roles: list[ScriptRole],
|
|
314
|
+
tight_bboxes: dict[int, BBox],
|
|
315
|
+
origins: dict[int, tuple[float, float]],
|
|
316
|
+
) -> None:
|
|
317
|
+
"""把已有可信角标 run 后同基线的紧凑连字符后缀整体闭合。"""
|
|
318
|
+
for joiner_index in range(1, len(chars) - 1):
|
|
319
|
+
if _script_char_text(chars[joiner_index]) not in _PDF_SCRIPT_COMPACT_JOINERS:
|
|
320
|
+
continue
|
|
321
|
+
left_seed = joiner_index - 1
|
|
322
|
+
role = refined_roles[left_seed]
|
|
323
|
+
if role == "body" or not _is_math_identifier_char(_script_char_text(chars[left_seed])):
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
left_start = left_seed
|
|
327
|
+
while (
|
|
328
|
+
left_start > 0
|
|
329
|
+
and refined_roles[left_start - 1] == role
|
|
330
|
+
and _is_math_identifier_char(_script_char_text(chars[left_start - 1]))
|
|
331
|
+
):
|
|
332
|
+
left_start -= 1
|
|
333
|
+
if left_seed - left_start + 1 < 2:
|
|
334
|
+
continue
|
|
335
|
+
anchor_index = left_start - 1
|
|
336
|
+
if (
|
|
337
|
+
anchor_index < 0
|
|
338
|
+
or refined_roles[anchor_index] != "body"
|
|
339
|
+
or not _is_math_identifier_char(_script_char_text(chars[anchor_index]))
|
|
340
|
+
):
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
suffix_start = joiner_index + 1
|
|
344
|
+
suffix_end = suffix_start
|
|
345
|
+
while suffix_end < len(chars) and _is_math_identifier_char(_script_char_text(chars[suffix_end])):
|
|
346
|
+
suffix_end += 1
|
|
347
|
+
if suffix_end - suffix_start < 2:
|
|
348
|
+
continue
|
|
349
|
+
restored_indices = range(joiner_index, suffix_end)
|
|
350
|
+
if any(raw_roles[index] != role for index in restored_indices):
|
|
351
|
+
continue
|
|
352
|
+
if not _script_geometry_is_aligned(chars, left_seed, joiner_index, tight_bboxes, origins):
|
|
353
|
+
continue
|
|
354
|
+
if any(
|
|
355
|
+
not _script_geometry_is_aligned(chars, left_seed, index, tight_bboxes, origins)
|
|
356
|
+
for index in range(suffix_start, suffix_end)
|
|
357
|
+
):
|
|
358
|
+
continue
|
|
359
|
+
refined_roles[joiner_index:suffix_end] = [role] * (suffix_end - joiner_index)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _protected_subscript_indices(
|
|
363
|
+
chars: list[dict[str, Any]],
|
|
364
|
+
roles: list[ScriptRole],
|
|
365
|
+
tokens: list[list[int]],
|
|
366
|
+
tight_bboxes: dict[int, BBox],
|
|
367
|
+
origins: dict[int, tuple[float, float]],
|
|
368
|
+
) -> set[int]:
|
|
369
|
+
"""找出拥有内部 base 或与其同基线连通的下标字符。"""
|
|
370
|
+
protected: set[int] = set()
|
|
371
|
+
for token in tokens:
|
|
372
|
+
for position, index in enumerate(token):
|
|
373
|
+
if roles[index] != "sub" or not _is_math_identifier_char(_script_char_text(chars[index])):
|
|
374
|
+
continue
|
|
375
|
+
if any(
|
|
376
|
+
earlier < index and roles[earlier] == "body" and _is_math_identifier_char(_script_char_text(chars[earlier]))
|
|
377
|
+
for earlier in token[:position]
|
|
378
|
+
) or _has_adjacent_math_base(chars, index, roles, tight_bboxes, origins):
|
|
379
|
+
protected.add(index)
|
|
380
|
+
changed = True
|
|
381
|
+
while changed:
|
|
382
|
+
changed = False
|
|
383
|
+
for index, role in enumerate(roles):
|
|
384
|
+
if role != "sub" or index in protected or not _is_math_identifier_char(_script_char_text(chars[index])):
|
|
385
|
+
continue
|
|
386
|
+
for seed in tuple(protected):
|
|
387
|
+
start, end = sorted((seed, index))
|
|
388
|
+
if end - start > 5 or not _script_geometry_is_aligned(chars, seed, index, tight_bboxes, origins):
|
|
389
|
+
continue
|
|
390
|
+
if all(
|
|
391
|
+
_script_char_text(chars[bridge]).isspace()
|
|
392
|
+
or _script_char_text(chars[bridge]) in _PDF_SCRIPT_TOKEN_CONNECTORS
|
|
393
|
+
or _script_char_text(chars[bridge]) in _PDF_SCRIPT_SIGN_CHARS
|
|
394
|
+
or _script_char_text(chars[bridge]) == "."
|
|
395
|
+
for bridge in range(start + 1, end)
|
|
396
|
+
):
|
|
397
|
+
protected.add(index)
|
|
398
|
+
changed = True
|
|
399
|
+
break
|
|
400
|
+
return protected
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _refine_math_script_tokens(
|
|
404
|
+
chars: list[dict[str, Any]],
|
|
405
|
+
roles: list[ScriptRole],
|
|
406
|
+
tight_bboxes: dict[int, BBox],
|
|
407
|
+
origins: dict[int, tuple[float, float]],
|
|
408
|
+
*,
|
|
409
|
+
formula_region: bool,
|
|
410
|
+
) -> list[ScriptRole]:
|
|
411
|
+
"""以最左稳定簇保护 base,并对弱单字符和复杂未分段 token 保守拒识。"""
|
|
412
|
+
refined = list(roles)
|
|
413
|
+
citation_indices = _citation_script_indices(chars, refined)
|
|
414
|
+
complex_unsegmented_token = False
|
|
415
|
+
tokens = _iter_math_script_tokens(chars)
|
|
416
|
+
token_alnum_positions = {
|
|
417
|
+
tuple(token): [index for index in token if _is_math_identifier_char(_script_char_text(chars[index]))]
|
|
418
|
+
for token in tokens
|
|
419
|
+
}
|
|
420
|
+
token_splits = {
|
|
421
|
+
tuple(token): _token_split_position(
|
|
422
|
+
chars,
|
|
423
|
+
token,
|
|
424
|
+
refined,
|
|
425
|
+
tight_bboxes,
|
|
426
|
+
origins,
|
|
427
|
+
)
|
|
428
|
+
for token in tokens
|
|
429
|
+
}
|
|
430
|
+
token_families: dict[str, list[tuple[int, ...]]] = {}
|
|
431
|
+
for token in tokens:
|
|
432
|
+
key = tuple(token)
|
|
433
|
+
alnum_positions = token_alnum_positions[key]
|
|
434
|
+
if len(alnum_positions) >= 2:
|
|
435
|
+
token_families.setdefault(_script_char_text(chars[alnum_positions[0]]), []).append(key)
|
|
436
|
+
trusted_family_bases = {
|
|
437
|
+
base
|
|
438
|
+
for base, members in token_families.items()
|
|
439
|
+
if len(members) >= 3
|
|
440
|
+
or any(token_splits[member] is not None for member in members)
|
|
441
|
+
or any(any(_script_char_text(chars[index]) in _PDF_SCRIPT_TOKEN_CONNECTORS for index in member) for member in members)
|
|
442
|
+
}
|
|
443
|
+
for token in tokens:
|
|
444
|
+
if any(index in citation_indices for index in token):
|
|
445
|
+
continue
|
|
446
|
+
token_key = tuple(token)
|
|
447
|
+
alnum_positions = token_alnum_positions[token_key]
|
|
448
|
+
if not alnum_positions or not any(refined[index] != "body" for index in token):
|
|
449
|
+
continue
|
|
450
|
+
token_roles = {refined[index] for index in token if refined[index] != "body"}
|
|
451
|
+
if token_roles == {"sup", "sub"}:
|
|
452
|
+
complex_unsegmented_token = True
|
|
453
|
+
if len(alnum_positions) == 1:
|
|
454
|
+
continue
|
|
455
|
+
first_position = alnum_positions[0]
|
|
456
|
+
suffix_positions = alnum_positions[1:]
|
|
457
|
+
if (
|
|
458
|
+
refined[first_position] == "sup"
|
|
459
|
+
and all(refined[index] == "body" for index in suffix_positions)
|
|
460
|
+
and len(suffix_positions) >= 2
|
|
461
|
+
and all(_script_char_text(chars[index]).isalpha() for index in suffix_positions)
|
|
462
|
+
):
|
|
463
|
+
continue
|
|
464
|
+
split_position = token_splits[token_key]
|
|
465
|
+
if split_position is None and _script_char_text(chars[alnum_positions[0]]) in trusted_family_bases:
|
|
466
|
+
split_position = alnum_positions[1]
|
|
467
|
+
if split_position is None:
|
|
468
|
+
if all(refined[index] != "body" for index in alnum_positions):
|
|
469
|
+
for index in token:
|
|
470
|
+
refined[index] = "body"
|
|
471
|
+
continue
|
|
472
|
+
base_positions = [index for index in alnum_positions if index < split_position]
|
|
473
|
+
base_origins = [origin for index in base_positions if (origin := _token_origin(chars[index], origins)) is not None]
|
|
474
|
+
base_heights = [_token_tight_height(chars[index], tight_bboxes) for index in base_positions]
|
|
475
|
+
base_origin = statistics.median(base_origins) if base_origins else None
|
|
476
|
+
base_height = statistics.median([height for height in base_heights if height > 0]) if any(base_heights) else 0.0
|
|
477
|
+
for index in token:
|
|
478
|
+
if index < split_position or _script_char_text(chars[index]) in _PDF_SCRIPT_TOKEN_CONNECTORS:
|
|
479
|
+
refined[index] = "body"
|
|
480
|
+
continue
|
|
481
|
+
text = _script_char_text(chars[index])
|
|
482
|
+
if not _is_math_identifier_char(text) or refined[index] != "body":
|
|
483
|
+
continue
|
|
484
|
+
origin = _token_origin(chars[index], origins)
|
|
485
|
+
if base_origin is None or origin is None:
|
|
486
|
+
continue
|
|
487
|
+
shift = origin - base_origin
|
|
488
|
+
if abs(shift) >= max(0.35, 0.08 * base_height):
|
|
489
|
+
refined[index] = "sub" if shift > 0 else "sup"
|
|
490
|
+
_close_spaced_script_operators(
|
|
491
|
+
chars,
|
|
492
|
+
refined,
|
|
493
|
+
tight_bboxes,
|
|
494
|
+
origins,
|
|
495
|
+
)
|
|
496
|
+
if complex_unsegmented_token and not formula_region:
|
|
497
|
+
for index in range(len(refined)):
|
|
498
|
+
if index not in citation_indices:
|
|
499
|
+
refined[index] = "body"
|
|
500
|
+
scripted_alnum = [
|
|
501
|
+
index for index, role in enumerate(refined) if role != "body" and _script_char_text(chars[index]).isalnum()
|
|
502
|
+
]
|
|
503
|
+
if not formula_region and len(scripted_alnum) >= 2 and any(_script_char_text(char) in {"∑", "∫"} for char in chars):
|
|
504
|
+
for index in range(len(refined)):
|
|
505
|
+
if index not in citation_indices:
|
|
506
|
+
refined[index] = "body"
|
|
507
|
+
has_compact_multiply = any(
|
|
508
|
+
_script_char_text(char) == "×"
|
|
509
|
+
and 0 < index < len(chars) - 1
|
|
510
|
+
and not _script_char_text(chars[index - 1]).isspace()
|
|
511
|
+
and not _script_char_text(chars[index + 1]).isspace()
|
|
512
|
+
for index, char in enumerate(chars)
|
|
513
|
+
)
|
|
514
|
+
if not formula_region and len(scripted_alnum) >= 2 and has_compact_multiply:
|
|
515
|
+
for index in range(len(refined)):
|
|
516
|
+
if index not in citation_indices:
|
|
517
|
+
refined[index] = "body"
|
|
518
|
+
if not formula_region:
|
|
519
|
+
for operator_index, char in enumerate(chars):
|
|
520
|
+
operator = _script_char_text(char)
|
|
521
|
+
nearby = [candidate for candidate in scripted_alnum if abs(candidate - operator_index) <= 5]
|
|
522
|
+
if (
|
|
523
|
+
operator in {"/", "⁄"}
|
|
524
|
+
and any(candidate < operator_index for candidate in nearby)
|
|
525
|
+
and any(candidate > operator_index for candidate in nearby)
|
|
526
|
+
):
|
|
527
|
+
for candidate in nearby:
|
|
528
|
+
if candidate not in citation_indices:
|
|
529
|
+
refined[candidate] = "body"
|
|
530
|
+
protected_subscripts = _protected_subscript_indices(
|
|
531
|
+
chars,
|
|
532
|
+
refined,
|
|
533
|
+
tokens,
|
|
534
|
+
tight_bboxes,
|
|
535
|
+
origins,
|
|
536
|
+
)
|
|
537
|
+
for index, role in enumerate(list(refined)):
|
|
538
|
+
if (
|
|
539
|
+
role == "sub"
|
|
540
|
+
and index not in citation_indices
|
|
541
|
+
and index not in protected_subscripts
|
|
542
|
+
and _is_math_identifier_char(_script_char_text(chars[index]))
|
|
543
|
+
):
|
|
544
|
+
refined[index] = "body"
|
|
545
|
+
for index, role in enumerate(list(refined)):
|
|
546
|
+
if role == "body" or index in citation_indices:
|
|
547
|
+
continue
|
|
548
|
+
text = _script_char_text(chars[index])
|
|
549
|
+
if text.isalnum() or text in _PDF_SCRIPT_AUTHOR_MARKS:
|
|
550
|
+
continue
|
|
551
|
+
if text in {",", ","} and all(
|
|
552
|
+
0 <= neighbor < len(refined)
|
|
553
|
+
and refined[neighbor] == role
|
|
554
|
+
and (_script_char_text(chars[neighbor]).isalnum() or _script_char_text(chars[neighbor]) in _PDF_SCRIPT_AUTHOR_MARKS)
|
|
555
|
+
for neighbor in (index - 1, index + 1)
|
|
556
|
+
):
|
|
557
|
+
continue
|
|
558
|
+
if text == "." and all(
|
|
559
|
+
0 <= neighbor < len(refined) and refined[neighbor] == role and _script_char_text(chars[neighbor]).isdigit()
|
|
560
|
+
for neighbor in (index - 1, index + 1)
|
|
561
|
+
):
|
|
562
|
+
continue
|
|
563
|
+
if text in _PDF_SCRIPT_SIGN_CHARS:
|
|
564
|
+
sign_neighbors = [
|
|
565
|
+
neighbor
|
|
566
|
+
for step in (-1, 1)
|
|
567
|
+
if (neighbor := _nearest_nonspace_index(chars, index, step)) is not None
|
|
568
|
+
and abs(neighbor - index) <= 3
|
|
569
|
+
and refined[neighbor] == role
|
|
570
|
+
]
|
|
571
|
+
if sign_neighbors and any(_script_char_text(chars[neighbor]).isdigit() for neighbor in sign_neighbors):
|
|
572
|
+
continue
|
|
573
|
+
if text in _PDF_SCRIPT_TRAILING_MARKS:
|
|
574
|
+
previous = _nearest_nonspace_index(chars, index, -1)
|
|
575
|
+
body_prefix = previous - 1 if previous is not None else -1
|
|
576
|
+
if (
|
|
577
|
+
role == "sup"
|
|
578
|
+
and previous is not None
|
|
579
|
+
and index - previous == 1
|
|
580
|
+
and body_prefix >= 0
|
|
581
|
+
and refined[body_prefix] == "body"
|
|
582
|
+
and _script_char_text(chars[body_prefix]).isalpha()
|
|
583
|
+
and refined[previous] == role
|
|
584
|
+
and roles[index] == role
|
|
585
|
+
and _script_char_text(chars[previous]).isalpha()
|
|
586
|
+
and _script_geometry_is_aligned(chars, previous, index, tight_bboxes, origins)
|
|
587
|
+
):
|
|
588
|
+
continue
|
|
589
|
+
refined[index] = "body"
|
|
590
|
+
if not formula_region:
|
|
591
|
+
_close_compact_aligned_script_suffixes(
|
|
592
|
+
chars,
|
|
593
|
+
roles,
|
|
594
|
+
refined,
|
|
595
|
+
tight_bboxes,
|
|
596
|
+
origins,
|
|
597
|
+
)
|
|
598
|
+
return refined
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _bbox_axis_overlap(first: BBox, second: BBox, *, axis: Literal["x", "y"]) -> float:
|
|
602
|
+
"""返回两个 bbox 在指定轴上的绝对重叠长度。"""
|
|
603
|
+
start, end = (0, 2) if axis == "x" else (1, 3)
|
|
604
|
+
return max(0.0, min(first[end], second[end]) - max(first[start], second[start]))
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _fraction_member_indices(
|
|
608
|
+
page_size: tuple[float, float],
|
|
609
|
+
all_chars: list[dict[str, Any]],
|
|
610
|
+
tight_bboxes: dict[int, BBox],
|
|
611
|
+
drawing_lines: Sequence[Any],
|
|
612
|
+
angle: int,
|
|
613
|
+
) -> set[int]:
|
|
614
|
+
"""按页面方向一次识别分数线两侧的上下叠字,供复杂分式整块拒识。"""
|
|
615
|
+
if not all_chars or not drawing_lines:
|
|
616
|
+
return set()
|
|
617
|
+
local_chars: list[tuple[int, str, BBox]] = []
|
|
618
|
+
local_heights = []
|
|
619
|
+
for char in all_chars:
|
|
620
|
+
char_idx = char.get("char_idx")
|
|
621
|
+
text = _script_char_text(char)
|
|
622
|
+
bbox = tight_bboxes.get(char_idx) if isinstance(char_idx, int) else None
|
|
623
|
+
if not isinstance(char_idx, int) or bbox is None or not text.isprintable() or text.isspace():
|
|
624
|
+
continue
|
|
625
|
+
local_bbox = _rotate_bbox_to_upright(bbox, page_size, angle)
|
|
626
|
+
local_chars.append((char_idx, text, local_bbox))
|
|
627
|
+
local_heights.append(local_bbox[3] - local_bbox[1])
|
|
628
|
+
scale = statistics.median([height for height in local_heights if height > 0]) if local_heights else 8.0
|
|
629
|
+
members: set[int] = set()
|
|
630
|
+
for drawing in drawing_lines:
|
|
631
|
+
raw_bbox = _coerce_bbox(getattr(drawing, "bbox", drawing))
|
|
632
|
+
if raw_bbox is None:
|
|
633
|
+
continue
|
|
634
|
+
local_rule = _rotate_bbox_to_upright(raw_bbox, page_size, angle)
|
|
635
|
+
width = local_rule[2] - local_rule[0]
|
|
636
|
+
height = local_rule[3] - local_rule[1]
|
|
637
|
+
if width < max(2.0, 0.45 * scale) or width > 12.0 * scale or height > max(1.25, 0.25 * scale):
|
|
638
|
+
continue
|
|
639
|
+
rule_y = (local_rule[1] + local_rule[3]) / 2
|
|
640
|
+
aligned = [
|
|
641
|
+
(char_idx, bbox)
|
|
642
|
+
for char_idx, text, bbox in local_chars
|
|
643
|
+
if text.isalnum()
|
|
644
|
+
and abs((bbox[1] + bbox[3]) / 2 - rule_y) <= 2.25 * scale
|
|
645
|
+
and (
|
|
646
|
+
_bbox_axis_overlap(bbox, local_rule, axis="x") > 0
|
|
647
|
+
or local_rule[0] - 0.25 * scale <= (bbox[0] + bbox[2]) / 2 <= local_rule[2] + 0.25 * scale
|
|
648
|
+
)
|
|
649
|
+
]
|
|
650
|
+
above = [
|
|
651
|
+
(char_idx, bbox)
|
|
652
|
+
for char_idx, bbox in aligned
|
|
653
|
+
if bbox[3] <= rule_y + 0.2 * scale and rule_y - bbox[3] <= 1.75 * scale
|
|
654
|
+
]
|
|
655
|
+
below = [
|
|
656
|
+
(char_idx, bbox)
|
|
657
|
+
for char_idx, bbox in aligned
|
|
658
|
+
if bbox[1] >= rule_y - 0.2 * scale and bbox[1] - rule_y <= 1.75 * scale
|
|
659
|
+
]
|
|
660
|
+
# 超过局部公式尺度的长横线更像脚注/段落分隔线,不用于分式成员抑制。
|
|
661
|
+
if width > 8.0 * scale:
|
|
662
|
+
continue
|
|
663
|
+
if above and below:
|
|
664
|
+
members.update(char_idx for char_idx, _bbox in above)
|
|
665
|
+
members.update(char_idx for char_idx, _bbox in below)
|
|
666
|
+
return members
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _strong_structural_script_roles(
|
|
670
|
+
chars: list[dict[str, Any]],
|
|
671
|
+
tight_bboxes: dict[int, BBox],
|
|
672
|
+
origins: dict[int, tuple[float, float]],
|
|
673
|
+
) -> dict[int, ScriptRole]:
|
|
674
|
+
"""提取可在恢复公式区域中保留的引用和邻接 base 强脚本证据。"""
|
|
675
|
+
|
|
676
|
+
roles = classify_char_script_roles(
|
|
677
|
+
chars,
|
|
678
|
+
tight_bboxes=tight_bboxes,
|
|
679
|
+
origins=origins,
|
|
680
|
+
)
|
|
681
|
+
strong_roles: dict[int, ScriptRole] = {}
|
|
682
|
+
for index in _citation_script_indices(chars, roles):
|
|
683
|
+
if roles[index] != "body":
|
|
684
|
+
strong_roles[index] = roles[index]
|
|
685
|
+
for index, role in enumerate(roles):
|
|
686
|
+
if role != "sup" or not _is_math_identifier_char(_script_char_text(chars[index])):
|
|
687
|
+
continue
|
|
688
|
+
base_height = _token_tight_height(chars[index - 1], tight_bboxes) if index > 0 else 0.0
|
|
689
|
+
script_height = _token_tight_height(chars[index], tight_bboxes)
|
|
690
|
+
if base_height <= 0 or script_height > 0.8 * base_height:
|
|
691
|
+
continue
|
|
692
|
+
next_index = _nearest_nonspace_index(chars, index, 1)
|
|
693
|
+
if next_index is not None and _is_math_script_token_char(_script_char_text(chars[next_index])):
|
|
694
|
+
continue
|
|
695
|
+
if _has_adjacent_math_base(
|
|
696
|
+
chars,
|
|
697
|
+
index,
|
|
698
|
+
roles,
|
|
699
|
+
tight_bboxes,
|
|
700
|
+
origins,
|
|
701
|
+
):
|
|
702
|
+
strong_roles[index] = role
|
|
703
|
+
return strong_roles
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _classify_script_runs(
|
|
707
|
+
chars: list[dict[str, Any]],
|
|
708
|
+
local_tight_bboxes: dict[int, BBox],
|
|
709
|
+
local_origins: dict[int, tuple[float, float]],
|
|
710
|
+
memberships: list[int | None],
|
|
711
|
+
) -> tuple[list[str], list[int], list[bool]]:
|
|
712
|
+
"""按公式区域边界分段分类,并要求公式段内部存在稳定 body。"""
|
|
713
|
+
roles: list[ScriptRole] = ["body"] * len(chars)
|
|
714
|
+
body_counts = [0] * len(chars)
|
|
715
|
+
formula_flags = [False] * len(chars)
|
|
716
|
+
start = 0
|
|
717
|
+
while start < len(chars):
|
|
718
|
+
membership = memberships[start]
|
|
719
|
+
end = start + 1
|
|
720
|
+
while end < len(chars) and memberships[end] == membership:
|
|
721
|
+
end += 1
|
|
722
|
+
run_chars = chars[start:end]
|
|
723
|
+
run_indices = {int(char["char_idx"]) for char in run_chars if isinstance(char.get("char_idx"), int)}
|
|
724
|
+
run_roles = classify_char_script_roles(
|
|
725
|
+
run_chars,
|
|
726
|
+
tight_bboxes={index: local_tight_bboxes[index] for index in run_indices if index in local_tight_bboxes},
|
|
727
|
+
origins={index: local_origins[index] for index in run_indices if index in local_origins},
|
|
728
|
+
)
|
|
729
|
+
run_roles = _refine_math_script_tokens(
|
|
730
|
+
run_chars,
|
|
731
|
+
run_roles,
|
|
732
|
+
local_tight_bboxes,
|
|
733
|
+
local_origins,
|
|
734
|
+
formula_region=membership is not None,
|
|
735
|
+
)
|
|
736
|
+
visible = [
|
|
737
|
+
index
|
|
738
|
+
for index, char in enumerate(run_chars)
|
|
739
|
+
if str(char.get("char", "")).isprintable() and not str(char.get("char", "")).isspace()
|
|
740
|
+
]
|
|
741
|
+
body_count = sum(run_roles[index] == "body" and str(run_chars[index].get("char", "")).isalnum() for index in visible)
|
|
742
|
+
marked_count = sum(run_roles[index] != "body" for index in visible)
|
|
743
|
+
body_tight_heights = [
|
|
744
|
+
local_tight_bboxes[int(run_chars[index]["char_idx"])][3] - local_tight_bboxes[int(run_chars[index]["char_idx"])][1]
|
|
745
|
+
for index in visible
|
|
746
|
+
if run_roles[index] == "body"
|
|
747
|
+
and isinstance(run_chars[index].get("char_idx"), int)
|
|
748
|
+
and int(run_chars[index]["char_idx"]) in local_tight_bboxes
|
|
749
|
+
]
|
|
750
|
+
script_tight_heights = [
|
|
751
|
+
local_tight_bboxes[int(run_chars[index]["char_idx"])][3] - local_tight_bboxes[int(run_chars[index]["char_idx"])][1]
|
|
752
|
+
for index in visible
|
|
753
|
+
if run_roles[index] != "body"
|
|
754
|
+
and isinstance(run_chars[index].get("char_idx"), int)
|
|
755
|
+
and int(run_chars[index]["char_idx"]) in local_tight_bboxes
|
|
756
|
+
]
|
|
757
|
+
stable_formula_body = (
|
|
758
|
+
body_count > 0
|
|
759
|
+
and bool(body_tight_heights)
|
|
760
|
+
and (not script_tight_heights or max(body_tight_heights) >= 1.1 * max(script_tight_heights))
|
|
761
|
+
)
|
|
762
|
+
if membership is not None and (not stable_formula_body or marked_count >= len(visible)):
|
|
763
|
+
run_roles = ["body"] * len(run_chars)
|
|
764
|
+
for offset, role in enumerate(run_roles, start=start):
|
|
765
|
+
roles[offset] = role
|
|
766
|
+
body_counts[offset] = body_count
|
|
767
|
+
formula_flags[offset] = membership is not None
|
|
768
|
+
start = end
|
|
769
|
+
return roles, body_counts, formula_flags
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _script_line_char_roles(
|
|
773
|
+
line: Any,
|
|
774
|
+
page_size: tuple[float, float],
|
|
775
|
+
tight_bboxes: dict[int, BBox],
|
|
776
|
+
origins: dict[int, tuple[float, float]],
|
|
777
|
+
fraction_members: set[int],
|
|
778
|
+
) -> tuple[list[dict[str, Any]], list[ScriptRole], list[int], list[bool]]:
|
|
779
|
+
"""按正文同款公式分段返回原字符及其上下标角色。"""
|
|
780
|
+
|
|
781
|
+
chars = _ordered_line_chars(line)
|
|
782
|
+
if not chars:
|
|
783
|
+
return [], [], [], []
|
|
784
|
+
angle = int(getattr(line, "angle", 0) or 0) % 360
|
|
785
|
+
local_chars: list[dict[str, Any]] = []
|
|
786
|
+
local_tight_bboxes: dict[int, BBox] = {}
|
|
787
|
+
local_origins: dict[int, tuple[float, float]] = {}
|
|
788
|
+
for char in chars:
|
|
789
|
+
local_char = dict(char)
|
|
790
|
+
bbox = _coerce_bbox(char.get("bbox"))
|
|
791
|
+
if bbox is not None:
|
|
792
|
+
local_char["bbox"] = _rotate_bbox_to_upright(bbox, page_size, angle)
|
|
793
|
+
local_chars.append(local_char)
|
|
794
|
+
char_idx = char.get("char_idx")
|
|
795
|
+
if not isinstance(char_idx, int):
|
|
796
|
+
continue
|
|
797
|
+
tight_bbox = tight_bboxes.get(char_idx)
|
|
798
|
+
if tight_bbox is not None:
|
|
799
|
+
local_tight_bboxes[char_idx] = _rotate_bbox_to_upright(
|
|
800
|
+
tight_bbox,
|
|
801
|
+
page_size,
|
|
802
|
+
angle,
|
|
803
|
+
)
|
|
804
|
+
origin = origins.get(char_idx)
|
|
805
|
+
if origin is not None:
|
|
806
|
+
local_origins[char_idx] = _rotate_origin_to_upright(
|
|
807
|
+
origin,
|
|
808
|
+
page_size,
|
|
809
|
+
angle,
|
|
810
|
+
)
|
|
811
|
+
regions = [bbox for value in getattr(line, "inline_math_regions", []) if (bbox := _coerce_bbox(value)) is not None]
|
|
812
|
+
memberships = _script_region_memberships(chars, tight_bboxes, regions)
|
|
813
|
+
roles, body_counts, formula_flags = _classify_script_runs(
|
|
814
|
+
local_chars,
|
|
815
|
+
local_tight_bboxes,
|
|
816
|
+
local_origins,
|
|
817
|
+
memberships,
|
|
818
|
+
)
|
|
819
|
+
if bool(getattr(line, "compact_formula_cluster", False)) or (
|
|
820
|
+
bool(getattr(line, "restored_inline_cluster", False)) and bool(regions)
|
|
821
|
+
):
|
|
822
|
+
strong_structural_roles = _strong_structural_script_roles(
|
|
823
|
+
local_chars,
|
|
824
|
+
local_tight_bboxes,
|
|
825
|
+
local_origins,
|
|
826
|
+
)
|
|
827
|
+
roles = [strong_structural_roles.get(index, "body") for index in range(len(roles))]
|
|
828
|
+
for index, char in enumerate(chars):
|
|
829
|
+
char_idx = char.get("char_idx")
|
|
830
|
+
if isinstance(char_idx, int) and char_idx in fraction_members:
|
|
831
|
+
roles[index] = "body"
|
|
832
|
+
return chars, roles, body_counts, formula_flags
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _script_line_payload(
|
|
836
|
+
line: Any,
|
|
837
|
+
page_size: tuple[float, float],
|
|
838
|
+
tight_bboxes: dict[int, BBox],
|
|
839
|
+
origins: dict[int, tuple[float, float]],
|
|
840
|
+
fraction_members: set[int],
|
|
841
|
+
) -> PDFTextScriptLine | None:
|
|
842
|
+
"""把 Flash 行转换为公式分段后的紧凑上下标 sidecar。"""
|
|
843
|
+
|
|
844
|
+
chars, roles, body_counts, formula_flags = _script_line_char_roles(
|
|
845
|
+
line,
|
|
846
|
+
page_size,
|
|
847
|
+
tight_bboxes,
|
|
848
|
+
origins,
|
|
849
|
+
fraction_members,
|
|
850
|
+
)
|
|
851
|
+
if not chars:
|
|
852
|
+
return None
|
|
853
|
+
angle = int(getattr(line, "angle", 0) or 0) % 360
|
|
854
|
+
compact_parts: list[str] = []
|
|
855
|
+
compact_roles: list[str] = []
|
|
856
|
+
compact_bboxes: list[BBox | None] = []
|
|
857
|
+
compact_body_counts: list[int] = []
|
|
858
|
+
compact_formula_flags: list[bool] = []
|
|
859
|
+
for index, char in enumerate(chars):
|
|
860
|
+
fragment = _normalize_match_fragment(char.get("char"))
|
|
861
|
+
if not fragment:
|
|
862
|
+
continue
|
|
863
|
+
compact_parts.append(fragment)
|
|
864
|
+
char_idx = char.get("char_idx")
|
|
865
|
+
page_tight_bbox = tight_bboxes.get(char_idx) if isinstance(char_idx, int) else None
|
|
866
|
+
compact_roles.extend([roles[index]] * len(fragment))
|
|
867
|
+
compact_bboxes.extend([page_tight_bbox] * len(fragment))
|
|
868
|
+
compact_body_counts.extend([body_counts[index]] * len(fragment))
|
|
869
|
+
compact_formula_flags.extend([formula_flags[index]] * len(fragment))
|
|
870
|
+
text = "".join(compact_parts)
|
|
871
|
+
if not text:
|
|
872
|
+
return None
|
|
873
|
+
ranges: list[PDFTextScriptRange] = []
|
|
874
|
+
start = 0
|
|
875
|
+
while start < len(compact_roles):
|
|
876
|
+
role = compact_roles[start]
|
|
877
|
+
end = start + 1
|
|
878
|
+
while end < len(compact_roles) and compact_roles[end] == role:
|
|
879
|
+
end += 1
|
|
880
|
+
if role in {"sup", "sub"}:
|
|
881
|
+
range_bboxes = [bbox for bbox in compact_bboxes[start:end] if bbox is not None]
|
|
882
|
+
if range_bboxes:
|
|
883
|
+
page_bbox = (
|
|
884
|
+
min(bbox[0] for bbox in range_bboxes),
|
|
885
|
+
min(bbox[1] for bbox in range_bboxes),
|
|
886
|
+
max(bbox[2] for bbox in range_bboxes),
|
|
887
|
+
max(bbox[3] for bbox in range_bboxes),
|
|
888
|
+
)
|
|
889
|
+
ranges.append(
|
|
890
|
+
PDFTextScriptRange(
|
|
891
|
+
start=start,
|
|
892
|
+
end=end,
|
|
893
|
+
style="superscript" if role == "sup" else "subscript",
|
|
894
|
+
bbox=page_bbox,
|
|
895
|
+
stable_body_count=max(compact_body_counts[start:end], default=0),
|
|
896
|
+
formula_region=any(compact_formula_flags[start:end]),
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
start = end
|
|
900
|
+
return PDFTextScriptLine(
|
|
901
|
+
bbox=getattr(line, "bbox"),
|
|
902
|
+
text=text,
|
|
903
|
+
script_ranges=tuple(ranges),
|
|
904
|
+
source_index=int(getattr(line, "source_index", 0) or 0),
|
|
905
|
+
angle=angle,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def detect_pdf_text_script_lines(
|
|
910
|
+
lines: list[Any],
|
|
911
|
+
page_size: tuple[float, float],
|
|
912
|
+
tight_bboxes: dict[int, BBox],
|
|
913
|
+
origins: dict[int, tuple[float, float]],
|
|
914
|
+
*,
|
|
915
|
+
all_chars: list[dict[str, Any]] | None = None,
|
|
916
|
+
drawing_lines: Sequence[Any] | None = None,
|
|
917
|
+
) -> list[PDFTextScriptLine]:
|
|
918
|
+
"""检测 Flash 剩余自然文本行中的上下标候选。"""
|
|
919
|
+
resolved_chars = all_chars or []
|
|
920
|
+
resolved_drawings = drawing_lines or []
|
|
921
|
+
fraction_members_by_angle = {
|
|
922
|
+
angle: _fraction_member_indices(
|
|
923
|
+
page_size,
|
|
924
|
+
resolved_chars,
|
|
925
|
+
tight_bboxes,
|
|
926
|
+
resolved_drawings,
|
|
927
|
+
angle,
|
|
928
|
+
)
|
|
929
|
+
for angle in {int(getattr(line, "angle", 0) or 0) % 360 for line in lines}
|
|
930
|
+
}
|
|
931
|
+
return [
|
|
932
|
+
payload
|
|
933
|
+
for line in lines
|
|
934
|
+
if (
|
|
935
|
+
payload := _script_line_payload(
|
|
936
|
+
line,
|
|
937
|
+
page_size,
|
|
938
|
+
tight_bboxes,
|
|
939
|
+
origins,
|
|
940
|
+
fraction_members_by_angle[int(getattr(line, "angle", 0) or 0) % 360],
|
|
941
|
+
)
|
|
942
|
+
)
|
|
943
|
+
is not None
|
|
944
|
+
]
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
__all__ = [
|
|
948
|
+
"_rotate_origin_to_upright",
|
|
949
|
+
"_bbox_center_inside_region",
|
|
950
|
+
"_script_region_memberships",
|
|
951
|
+
"_script_char_text",
|
|
952
|
+
"_is_cjk_text",
|
|
953
|
+
"_is_math_identifier_char",
|
|
954
|
+
"_is_math_script_token_char",
|
|
955
|
+
"_iter_math_script_tokens",
|
|
956
|
+
"_citation_script_indices",
|
|
957
|
+
"_token_origin",
|
|
958
|
+
"_token_tight_height",
|
|
959
|
+
"_has_adjacent_math_base",
|
|
960
|
+
"_horizontal_gap_between_bboxes",
|
|
961
|
+
"_token_split_position",
|
|
962
|
+
"_script_geometry_is_aligned",
|
|
963
|
+
"_nearest_nonspace_index",
|
|
964
|
+
"_close_spaced_script_operators",
|
|
965
|
+
"_close_compact_aligned_script_suffixes",
|
|
966
|
+
"_protected_subscript_indices",
|
|
967
|
+
"_refine_math_script_tokens",
|
|
968
|
+
"_bbox_axis_overlap",
|
|
969
|
+
"_fraction_member_indices",
|
|
970
|
+
"_strong_structural_script_roles",
|
|
971
|
+
"_classify_script_runs",
|
|
972
|
+
"_script_line_char_roles",
|
|
973
|
+
"_script_line_payload",
|
|
974
|
+
"detect_pdf_text_script_lines",
|
|
975
|
+
]
|