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,347 @@
|
|
|
1
|
+
"""把 OFD 文档、模板、图层与资源组合成逐页场景。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
9
|
+
|
|
10
|
+
from ....schema import BBox
|
|
11
|
+
from .constants import MAX_OBJECT_RECURSION, MAX_PAGE_COUNT, OFD_NAMESPACES
|
|
12
|
+
from .errors import OfdParseError, OfdResourceLimitError
|
|
13
|
+
from .geometry import Affine, bbox_intersection, parse_affine, parse_st_box, transform_bbox
|
|
14
|
+
from .images import build_image_item
|
|
15
|
+
from .models import OfdPageScene, PageBuildContext, PageRef, ResourceRegistry, TemplateRef
|
|
16
|
+
from .package import OfdPackage, element_text, first_child, first_descendant, local_name, namespace_name, parse_int
|
|
17
|
+
from .path import OfdPathBudget, build_axis_lines
|
|
18
|
+
from .resources import merge_registries, parse_resource_part, resolve_draw_param
|
|
19
|
+
from .text import FontMetricResolver, OfdTextBudget, build_text_lines
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OfdSceneBuilder:
|
|
23
|
+
"""按文档页树构造 OFD 页面场景。"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, package: OfdPackage) -> None:
|
|
26
|
+
"""绑定当前包和跨页共享预算。"""
|
|
27
|
+
self.package = package
|
|
28
|
+
self.text_budget = OfdTextBudget()
|
|
29
|
+
self.path_budget = OfdPathBudget()
|
|
30
|
+
self.font_metrics = FontMetricResolver(package)
|
|
31
|
+
self._paint_order = 0
|
|
32
|
+
self._page_count = 0
|
|
33
|
+
|
|
34
|
+
def build(self) -> list[OfdPageScene]:
|
|
35
|
+
"""按 DocBody 和 Pages 声明顺序构造全部页面。"""
|
|
36
|
+
scenes: list[OfdPageScene] = []
|
|
37
|
+
try:
|
|
38
|
+
for document_ref in self.package.document_refs():
|
|
39
|
+
document_root = self._required_ofd_xml(document_ref.document_part)
|
|
40
|
+
scenes.extend(self._build_document(document_ref.document_part, document_root, len(scenes)))
|
|
41
|
+
return scenes
|
|
42
|
+
finally:
|
|
43
|
+
self.font_metrics.close()
|
|
44
|
+
|
|
45
|
+
def _required_ofd_xml(self, part_name: str) -> etree._Element:
|
|
46
|
+
"""读取必需核心 XML 并限制为已知 OFD 命名空间。"""
|
|
47
|
+
root = self.package.xml_part(part_name, required=True)
|
|
48
|
+
assert root is not None
|
|
49
|
+
namespace = namespace_name(root.tag)
|
|
50
|
+
if namespace not in OFD_NAMESPACES:
|
|
51
|
+
raise OfdParseError(f"Malformed OFD package: unsupported namespace {namespace!r} in {part_name!r}")
|
|
52
|
+
return root
|
|
53
|
+
|
|
54
|
+
def _resolve_part(self, base_part: str, element: etree._Element | None, *, required: bool = False) -> str | None:
|
|
55
|
+
"""解析一个 ST_Loc 元素并按需要求结果存在。"""
|
|
56
|
+
part = self.package.resolve_reference(base_part, element_text(element)) if element is not None else None
|
|
57
|
+
if required and (part is None or not self.package.has_part(part)):
|
|
58
|
+
raise OfdParseError(f"Malformed OFD package: invalid required location from {base_part!r}")
|
|
59
|
+
return part
|
|
60
|
+
|
|
61
|
+
def _document_resources(self, document_part: str, document_root: etree._Element) -> ResourceRegistry:
|
|
62
|
+
"""解析文档级 PublicRes 与 DocumentRes。"""
|
|
63
|
+
common_data = first_descendant(document_root, "CommonData")
|
|
64
|
+
public_element = first_child(common_data, "PublicRes")
|
|
65
|
+
document_resource_element = first_child(common_data, "DocumentRes")
|
|
66
|
+
public_part = self._resolve_part(document_part, public_element, required=public_element is not None)
|
|
67
|
+
document_resource_part = self._resolve_part(
|
|
68
|
+
document_part,
|
|
69
|
+
document_resource_element,
|
|
70
|
+
required=document_resource_element is not None,
|
|
71
|
+
)
|
|
72
|
+
return merge_registries(
|
|
73
|
+
parse_resource_part(self.package, public_part),
|
|
74
|
+
parse_resource_part(self.package, document_resource_part),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _page_area(self, root: etree._Element, fallback: BBox | None, name: str) -> BBox | None:
|
|
78
|
+
"""读取页面区域,缺失或非法时返回文档级回退。"""
|
|
79
|
+
area = first_descendant(root, "Area")
|
|
80
|
+
if area is None:
|
|
81
|
+
area = first_descendant(root, "PageArea")
|
|
82
|
+
value = parse_st_box(element_text(first_descendant(area, name))) if area is not None else None
|
|
83
|
+
return value or fallback
|
|
84
|
+
|
|
85
|
+
def _template_refs(self, document_part: str, document_root: etree._Element) -> dict[int, TemplateRef]:
|
|
86
|
+
"""读取 CommonData 中的模板页面映射。"""
|
|
87
|
+
common_data = first_descendant(document_root, "CommonData")
|
|
88
|
+
result: dict[int, TemplateRef] = {}
|
|
89
|
+
if common_data is None:
|
|
90
|
+
return result
|
|
91
|
+
for element in common_data.iter():
|
|
92
|
+
if local_name(element.tag) != "TemplatePage":
|
|
93
|
+
continue
|
|
94
|
+
template_id = parse_int(element.get("ID"))
|
|
95
|
+
page_part = self.package.resolve_reference(document_part, element.get("BaseLoc"))
|
|
96
|
+
if template_id is not None and page_part is not None:
|
|
97
|
+
result[template_id] = TemplateRef(template_id=template_id, page_part=page_part)
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
def _page_refs(self, document_part: str, document_root: etree._Element) -> list[PageRef]:
|
|
101
|
+
"""按 Document.xml 的 Pages 子节点顺序读取页面引用。"""
|
|
102
|
+
pages = first_descendant(document_root, "Pages")
|
|
103
|
+
if pages is None:
|
|
104
|
+
raise OfdParseError(f"Malformed OFD package: {document_part!r} has no Pages")
|
|
105
|
+
result: list[PageRef] = []
|
|
106
|
+
for element in pages:
|
|
107
|
+
if local_name(element.tag) != "Page":
|
|
108
|
+
continue
|
|
109
|
+
self._page_count += 1
|
|
110
|
+
if self._page_count > MAX_PAGE_COUNT:
|
|
111
|
+
raise OfdResourceLimitError(f"OFD resource limit exceeded: max_page_count={MAX_PAGE_COUNT}")
|
|
112
|
+
page_part = self.package.resolve_reference(document_part, element.get("BaseLoc"))
|
|
113
|
+
if page_part is None or not self.package.has_part(page_part):
|
|
114
|
+
raise OfdParseError(f"Malformed OFD package: invalid page BaseLoc in {document_part!r}")
|
|
115
|
+
result.append(PageRef(page_id=parse_int(element.get("ID")), page_part=page_part))
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
def _build_document(
|
|
119
|
+
self,
|
|
120
|
+
document_part: str,
|
|
121
|
+
document_root: etree._Element,
|
|
122
|
+
page_index_offset: int,
|
|
123
|
+
) -> list[OfdPageScene]:
|
|
124
|
+
"""构造单个 Document.xml 声明的全部页面。"""
|
|
125
|
+
common_data = first_descendant(document_root, "CommonData")
|
|
126
|
+
default_physical = self._page_area(common_data, None, "PhysicalBox") if common_data is not None else None
|
|
127
|
+
default_content = self._page_area(common_data, None, "ContentBox") if common_data is not None else None
|
|
128
|
+
document_resources = self._document_resources(document_part, document_root)
|
|
129
|
+
templates = self._template_refs(document_part, document_root)
|
|
130
|
+
scenes: list[OfdPageScene] = []
|
|
131
|
+
for local_page_idx, page_ref in enumerate(self._page_refs(document_part, document_root)):
|
|
132
|
+
page_root = self._required_ofd_xml(page_ref.page_part)
|
|
133
|
+
physical_box = self._page_area(page_root, default_physical, "PhysicalBox")
|
|
134
|
+
if physical_box is None:
|
|
135
|
+
raise OfdParseError(f"Malformed OFD package: page {page_ref.page_part!r} has no PhysicalBox")
|
|
136
|
+
content_box = self._page_area(page_root, default_content, "ContentBox")
|
|
137
|
+
page_resource_element = first_descendant(page_root, "PageRes")
|
|
138
|
+
page_resource_part = self._resolve_part(
|
|
139
|
+
page_ref.page_part,
|
|
140
|
+
page_resource_element,
|
|
141
|
+
required=page_resource_element is not None,
|
|
142
|
+
)
|
|
143
|
+
resources = merge_registries(document_resources, parse_resource_part(self.package, page_resource_part))
|
|
144
|
+
scene = OfdPageScene(
|
|
145
|
+
page_idx=page_index_offset + local_page_idx,
|
|
146
|
+
physical_box=physical_box,
|
|
147
|
+
content_box=content_box,
|
|
148
|
+
)
|
|
149
|
+
base_context = PageBuildContext(
|
|
150
|
+
transform=Affine(),
|
|
151
|
+
clip_bbox=physical_box,
|
|
152
|
+
layer_type="Body",
|
|
153
|
+
template_id=None,
|
|
154
|
+
draw_style={},
|
|
155
|
+
)
|
|
156
|
+
template_uses = [element for element in page_root.iter() if local_name(element.tag) == "Template"]
|
|
157
|
+
for template_use in template_uses:
|
|
158
|
+
if (template_use.get("ZOrder") or "Background").casefold() == "foreground":
|
|
159
|
+
continue
|
|
160
|
+
self._append_template(template_use, templates, resources, scene, base_context)
|
|
161
|
+
content = first_descendant(page_root, "Content")
|
|
162
|
+
if content is not None:
|
|
163
|
+
self._walk(content, base_context, resources, scene, depth=0, composite_stack=frozenset())
|
|
164
|
+
for template_use in template_uses:
|
|
165
|
+
if (template_use.get("ZOrder") or "Background").casefold() != "foreground":
|
|
166
|
+
continue
|
|
167
|
+
self._append_template(template_use, templates, resources, scene, base_context)
|
|
168
|
+
scenes.append(scene)
|
|
169
|
+
return scenes
|
|
170
|
+
|
|
171
|
+
def _append_template(
|
|
172
|
+
self,
|
|
173
|
+
template_use: etree._Element,
|
|
174
|
+
templates: dict[int, TemplateRef],
|
|
175
|
+
resources: ResourceRegistry,
|
|
176
|
+
scene: OfdPageScene,
|
|
177
|
+
base_context: PageBuildContext,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""把一个页面引用的模板内容合并到当前场景。"""
|
|
180
|
+
template_id = parse_int(template_use.get("TemplateID"))
|
|
181
|
+
template_ref = templates.get(template_id) if template_id is not None else None
|
|
182
|
+
if template_ref is None:
|
|
183
|
+
logger.warning(f"OFD_TEMPLATE_MISSING: template_id={template_id}")
|
|
184
|
+
return
|
|
185
|
+
template_root = self._required_ofd_xml(template_ref.page_part)
|
|
186
|
+
template_resource_element = first_descendant(template_root, "PageRes")
|
|
187
|
+
template_resource_part = self._resolve_part(
|
|
188
|
+
template_ref.page_part,
|
|
189
|
+
template_resource_element,
|
|
190
|
+
required=template_resource_element is not None,
|
|
191
|
+
)
|
|
192
|
+
template_resources = merge_registries(resources, parse_resource_part(self.package, template_resource_part))
|
|
193
|
+
content = first_descendant(template_root, "Content")
|
|
194
|
+
if content is None:
|
|
195
|
+
return
|
|
196
|
+
context = replace(base_context, template_id=template_id)
|
|
197
|
+
self._walk(content, context, template_resources, scene, depth=0, composite_stack=frozenset())
|
|
198
|
+
|
|
199
|
+
def _child_context(self, element: etree._Element, context: PageBuildContext) -> PageBuildContext | None:
|
|
200
|
+
"""为 PageBlock 或 CompositeObject 计算子级变换和裁剪。"""
|
|
201
|
+
boundary = parse_st_box(element.get("Boundary"))
|
|
202
|
+
if boundary is None:
|
|
203
|
+
return context
|
|
204
|
+
boundary_page = transform_bbox(boundary, context.transform)
|
|
205
|
+
if boundary_page is None:
|
|
206
|
+
return None
|
|
207
|
+
clip = bbox_intersection(context.clip_bbox, boundary_page)
|
|
208
|
+
if clip is None:
|
|
209
|
+
return None
|
|
210
|
+
transform = context.transform.compose(Affine.translation(boundary[0], boundary[1])).compose(
|
|
211
|
+
parse_affine(element.get("CTM"))
|
|
212
|
+
)
|
|
213
|
+
return replace(context, transform=transform, clip_bbox=clip)
|
|
214
|
+
|
|
215
|
+
def _resolved_style(
|
|
216
|
+
self,
|
|
217
|
+
element: etree._Element,
|
|
218
|
+
context: PageBuildContext,
|
|
219
|
+
resources: ResourceRegistry,
|
|
220
|
+
) -> dict[str, str]:
|
|
221
|
+
"""按父上下文、DrawParam 和对象直接属性解析最终样式。"""
|
|
222
|
+
style = dict(context.draw_style)
|
|
223
|
+
try:
|
|
224
|
+
style.update(resolve_draw_param(resources, parse_int(element.get("DrawParam"))))
|
|
225
|
+
except OfdResourceLimitError:
|
|
226
|
+
raise
|
|
227
|
+
except ValueError as exc:
|
|
228
|
+
raise OfdParseError(str(exc)) from exc
|
|
229
|
+
style.update({str(key): str(value) for key, value in element.attrib.items()})
|
|
230
|
+
return style
|
|
231
|
+
|
|
232
|
+
def _walk(
|
|
233
|
+
self,
|
|
234
|
+
element: etree._Element,
|
|
235
|
+
context: PageBuildContext,
|
|
236
|
+
resources: ResourceRegistry,
|
|
237
|
+
scene: OfdPageScene,
|
|
238
|
+
*,
|
|
239
|
+
depth: int,
|
|
240
|
+
composite_stack: frozenset[int],
|
|
241
|
+
) -> None:
|
|
242
|
+
"""按绘制顺序递归展开页面、模板和复合图元对象。"""
|
|
243
|
+
if depth > MAX_OBJECT_RECURSION:
|
|
244
|
+
raise OfdResourceLimitError(f"OFD resource limit exceeded: max_object_recursion={MAX_OBJECT_RECURSION}")
|
|
245
|
+
for child in element:
|
|
246
|
+
if not isinstance(child.tag, str):
|
|
247
|
+
continue
|
|
248
|
+
name = local_name(child.tag)
|
|
249
|
+
child_context = context
|
|
250
|
+
if name == "Layer":
|
|
251
|
+
child_context = replace(
|
|
252
|
+
context,
|
|
253
|
+
layer_type=(child.get("Type") or "Body"),
|
|
254
|
+
draw_style=self._resolved_style(child, context, resources),
|
|
255
|
+
)
|
|
256
|
+
elif name == "PageBlock":
|
|
257
|
+
resolved_context = self._child_context(child, context)
|
|
258
|
+
if resolved_context is None:
|
|
259
|
+
continue
|
|
260
|
+
child_context = replace(
|
|
261
|
+
resolved_context,
|
|
262
|
+
draw_style=self._resolved_style(child, context, resources),
|
|
263
|
+
)
|
|
264
|
+
if name == "TextObject":
|
|
265
|
+
lines = build_text_lines(
|
|
266
|
+
child,
|
|
267
|
+
parent_transform=context.transform,
|
|
268
|
+
parent_clip=context.clip_bbox,
|
|
269
|
+
resources=resources,
|
|
270
|
+
package=self.package,
|
|
271
|
+
font_metrics=self.font_metrics,
|
|
272
|
+
budget=self.text_budget,
|
|
273
|
+
paint_order=self._paint_order,
|
|
274
|
+
layer_type=context.layer_type,
|
|
275
|
+
template_id=context.template_id,
|
|
276
|
+
resolved_style=self._resolved_style(child, context, resources),
|
|
277
|
+
)
|
|
278
|
+
scene.text_lines.extend(lines)
|
|
279
|
+
self._paint_order += max(1, len(lines))
|
|
280
|
+
continue
|
|
281
|
+
if name == "PathObject":
|
|
282
|
+
scene.axis_lines.extend(
|
|
283
|
+
build_axis_lines(
|
|
284
|
+
child,
|
|
285
|
+
parent_transform=context.transform,
|
|
286
|
+
parent_clip=context.clip_bbox,
|
|
287
|
+
paint_order=self._paint_order,
|
|
288
|
+
template_id=context.template_id,
|
|
289
|
+
budget=self.path_budget,
|
|
290
|
+
resolved_style=self._resolved_style(child, context, resources),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
self._paint_order += 1
|
|
294
|
+
continue
|
|
295
|
+
if name == "ImageObject":
|
|
296
|
+
image = build_image_item(
|
|
297
|
+
child,
|
|
298
|
+
parent_transform=context.transform,
|
|
299
|
+
parent_clip=context.clip_bbox,
|
|
300
|
+
resources=resources,
|
|
301
|
+
package=self.package,
|
|
302
|
+
paint_order=self._paint_order,
|
|
303
|
+
layer_type=context.layer_type,
|
|
304
|
+
template_id=context.template_id,
|
|
305
|
+
resolved_style=self._resolved_style(child, context, resources),
|
|
306
|
+
)
|
|
307
|
+
if image is not None:
|
|
308
|
+
scene.images.append(image)
|
|
309
|
+
self._paint_order += 1
|
|
310
|
+
continue
|
|
311
|
+
if name == "CompositeObject":
|
|
312
|
+
resource_id = parse_int(child.get("ResourceID"))
|
|
313
|
+
composite = resources.composites.get(resource_id) if resource_id is not None else None
|
|
314
|
+
if composite is None:
|
|
315
|
+
logger.warning(f"OFD_COMPOSITE_MISSING: resource_id={resource_id}")
|
|
316
|
+
continue
|
|
317
|
+
if resource_id in composite_stack:
|
|
318
|
+
raise OfdParseError(f"OFD CompositeObject cycle detected at id={resource_id}")
|
|
319
|
+
resolved_context = self._child_context(child, context)
|
|
320
|
+
if resolved_context is None:
|
|
321
|
+
continue
|
|
322
|
+
resolved_context = replace(
|
|
323
|
+
resolved_context,
|
|
324
|
+
draw_style=self._resolved_style(child, context, resources),
|
|
325
|
+
)
|
|
326
|
+
content = first_descendant(composite.element, "Content")
|
|
327
|
+
if content is not None:
|
|
328
|
+
self._walk(
|
|
329
|
+
content,
|
|
330
|
+
resolved_context,
|
|
331
|
+
resources,
|
|
332
|
+
scene,
|
|
333
|
+
depth=depth + 1,
|
|
334
|
+
composite_stack=composite_stack | {resource_id},
|
|
335
|
+
)
|
|
336
|
+
continue
|
|
337
|
+
self._walk(
|
|
338
|
+
child,
|
|
339
|
+
child_context,
|
|
340
|
+
resources,
|
|
341
|
+
scene,
|
|
342
|
+
depth=depth + 1,
|
|
343
|
+
composite_stack=composite_stack,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
__all__ = ["OfdSceneBuilder"]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""基于 OFD 原生横竖线与文字框恢复高置信全线表。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ....schema import BBox
|
|
8
|
+
from .constants import MAX_TABLE_INTERSECTION_CHECKS
|
|
9
|
+
from .errors import OfdResourceLimitError
|
|
10
|
+
from .geometry import bbox_area, bbox_center, bbox_intersection
|
|
11
|
+
from .models import AxisLine, TextLine
|
|
12
|
+
from .text import format_line_html
|
|
13
|
+
|
|
14
|
+
_COORD_TOLERANCE = 0.6
|
|
15
|
+
_INTERSECTION_TOLERANCE = 0.8
|
|
16
|
+
_MAX_GRID_CELLS = 10_000
|
|
17
|
+
_MAX_TABLE_AXIS_LINES = 2_000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class OfdTableRegion:
|
|
22
|
+
"""保存一个已物化 OFD 表格及其消耗的文字行。"""
|
|
23
|
+
|
|
24
|
+
bbox: BBox
|
|
25
|
+
html: str
|
|
26
|
+
paint_order: int
|
|
27
|
+
consumed_line_ids: frozenset[int]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class OfdTableBudget:
|
|
32
|
+
"""累计限制整份 OFD 的表格线段相交比较次数。"""
|
|
33
|
+
|
|
34
|
+
intersection_check_count: int = 0
|
|
35
|
+
|
|
36
|
+
def charge_intersection_check(self) -> None:
|
|
37
|
+
"""累计一次线段相交比较并在超限时失败。"""
|
|
38
|
+
self.intersection_check_count += 1
|
|
39
|
+
if self.intersection_check_count > MAX_TABLE_INTERSECTION_CHECKS:
|
|
40
|
+
raise OfdResourceLimitError(
|
|
41
|
+
f"OFD resource limit exceeded: max_table_intersection_checks={MAX_TABLE_INTERSECTION_CHECKS}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _line_coord(line: AxisLine) -> float:
|
|
46
|
+
"""返回轴向线段在垂直方向上的中心坐标。"""
|
|
47
|
+
if line.orientation == "horizontal":
|
|
48
|
+
return (line.bbox[1] + line.bbox[3]) / 2.0
|
|
49
|
+
return (line.bbox[0] + line.bbox[2]) / 2.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _line_interval(line: AxisLine) -> tuple[float, float]:
|
|
53
|
+
"""返回轴向线段沿自身方向的区间。"""
|
|
54
|
+
return (line.bbox[0], line.bbox[2]) if line.orientation == "horizontal" else (line.bbox[1], line.bbox[3])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _touches(first: AxisLine, second: AxisLine) -> bool:
|
|
58
|
+
"""判断两条轴向线段是否相交或共线连接。"""
|
|
59
|
+
if first.orientation == second.orientation:
|
|
60
|
+
if abs(_line_coord(first) - _line_coord(second)) > _COORD_TOLERANCE:
|
|
61
|
+
return False
|
|
62
|
+
first_interval = _line_interval(first)
|
|
63
|
+
second_interval = _line_interval(second)
|
|
64
|
+
return min(first_interval[1], second_interval[1]) + _INTERSECTION_TOLERANCE >= max(
|
|
65
|
+
first_interval[0], second_interval[0]
|
|
66
|
+
)
|
|
67
|
+
horizontal = first if first.orientation == "horizontal" else second
|
|
68
|
+
vertical = second if first.orientation == "horizontal" else first
|
|
69
|
+
x = _line_coord(vertical)
|
|
70
|
+
y = _line_coord(horizontal)
|
|
71
|
+
return (
|
|
72
|
+
horizontal.bbox[0] - _INTERSECTION_TOLERANCE <= x <= horizontal.bbox[2] + _INTERSECTION_TOLERANCE
|
|
73
|
+
and vertical.bbox[1] - _INTERSECTION_TOLERANCE <= y <= vertical.bbox[3] + _INTERSECTION_TOLERANCE
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _components(lines: list[AxisLine], budget: OfdTableBudget) -> list[list[AxisLine]]:
|
|
78
|
+
"""按线段相交关系构造确定性的连通分量。"""
|
|
79
|
+
parents = list(range(len(lines)))
|
|
80
|
+
|
|
81
|
+
def find(index: int) -> int:
|
|
82
|
+
"""查找并压缩一个并查集根节点。"""
|
|
83
|
+
while parents[index] != index:
|
|
84
|
+
parents[index] = parents[parents[index]]
|
|
85
|
+
index = parents[index]
|
|
86
|
+
return index
|
|
87
|
+
|
|
88
|
+
def union(first: int, second: int) -> None:
|
|
89
|
+
"""合并两个线段分量。"""
|
|
90
|
+
first_root = find(first)
|
|
91
|
+
second_root = find(second)
|
|
92
|
+
if first_root != second_root:
|
|
93
|
+
parents[max(first_root, second_root)] = min(first_root, second_root)
|
|
94
|
+
|
|
95
|
+
for first_index, first in enumerate(lines):
|
|
96
|
+
for second_index in range(first_index + 1, len(lines)):
|
|
97
|
+
budget.charge_intersection_check()
|
|
98
|
+
if _touches(first, lines[second_index]):
|
|
99
|
+
union(first_index, second_index)
|
|
100
|
+
grouped: dict[int, list[AxisLine]] = {}
|
|
101
|
+
for index, line in enumerate(lines):
|
|
102
|
+
grouped.setdefault(find(index), []).append(line)
|
|
103
|
+
return list(grouped.values())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _cluster_coordinates(values: list[float]) -> list[float]:
|
|
107
|
+
"""把近邻线坐标聚合为稳定网格轨道。"""
|
|
108
|
+
clusters: list[list[float]] = []
|
|
109
|
+
for value in sorted(values):
|
|
110
|
+
if not clusters or value - clusters[-1][-1] > _COORD_TOLERANCE:
|
|
111
|
+
clusters.append([value])
|
|
112
|
+
else:
|
|
113
|
+
clusters[-1].append(value)
|
|
114
|
+
return [sum(cluster) / len(cluster) for cluster in clusters]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _covers_interval(lines: list[AxisLine], start: float, end: float) -> bool:
|
|
118
|
+
"""判断共线片段是否基本覆盖给定区间。"""
|
|
119
|
+
intervals = sorted(_line_interval(line) for line in lines)
|
|
120
|
+
cursor = start
|
|
121
|
+
for left, right in intervals:
|
|
122
|
+
if right < cursor - _INTERSECTION_TOLERANCE:
|
|
123
|
+
continue
|
|
124
|
+
if left > cursor + _INTERSECTION_TOLERANCE:
|
|
125
|
+
return False
|
|
126
|
+
cursor = max(cursor, right)
|
|
127
|
+
if cursor >= end - _INTERSECTION_TOLERANCE:
|
|
128
|
+
return True
|
|
129
|
+
return cursor >= end - _INTERSECTION_TOLERANCE
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _component_grid(component: list[AxisLine]) -> tuple[list[float], list[float], BBox] | None:
|
|
133
|
+
"""从线段分量恢复有完整外框的网格轨道。"""
|
|
134
|
+
horizontal = [line for line in component if line.orientation == "horizontal"]
|
|
135
|
+
vertical = [line for line in component if line.orientation == "vertical"]
|
|
136
|
+
if len(horizontal) < 2 or len(vertical) < 2:
|
|
137
|
+
return None
|
|
138
|
+
xs = _cluster_coordinates([_line_coord(line) for line in vertical])
|
|
139
|
+
ys = _cluster_coordinates([_line_coord(line) for line in horizontal])
|
|
140
|
+
if len(xs) < 2 or len(ys) < 2 or (len(xs) - 1) * (len(ys) - 1) > _MAX_GRID_CELLS:
|
|
141
|
+
return None
|
|
142
|
+
left, right = xs[0], xs[-1]
|
|
143
|
+
top, bottom = ys[0], ys[-1]
|
|
144
|
+
outer_horizontal = [line for line in horizontal if abs(_line_coord(line) - top) <= _COORD_TOLERANCE]
|
|
145
|
+
outer_horizontal += [line for line in horizontal if abs(_line_coord(line) - bottom) <= _COORD_TOLERANCE]
|
|
146
|
+
outer_vertical = [line for line in vertical if abs(_line_coord(line) - left) <= _COORD_TOLERANCE]
|
|
147
|
+
outer_vertical += [line for line in vertical if abs(_line_coord(line) - right) <= _COORD_TOLERANCE]
|
|
148
|
+
top_ok = _covers_interval(
|
|
149
|
+
[line for line in outer_horizontal if abs(_line_coord(line) - top) <= _COORD_TOLERANCE],
|
|
150
|
+
left,
|
|
151
|
+
right,
|
|
152
|
+
)
|
|
153
|
+
bottom_ok = _covers_interval(
|
|
154
|
+
[line for line in outer_horizontal if abs(_line_coord(line) - bottom) <= _COORD_TOLERANCE], left, right
|
|
155
|
+
)
|
|
156
|
+
left_ok = _covers_interval(
|
|
157
|
+
[line for line in outer_vertical if abs(_line_coord(line) - left) <= _COORD_TOLERANCE],
|
|
158
|
+
top,
|
|
159
|
+
bottom,
|
|
160
|
+
)
|
|
161
|
+
right_ok = _covers_interval(
|
|
162
|
+
[line for line in outer_vertical if abs(_line_coord(line) - right) <= _COORD_TOLERANCE], top, bottom
|
|
163
|
+
)
|
|
164
|
+
if not (top_ok and bottom_ok and left_ok and right_ok):
|
|
165
|
+
return None
|
|
166
|
+
return xs, ys, (left, top, right, bottom)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _cell_index(values: list[float], coordinate: float) -> int | None:
|
|
170
|
+
"""返回坐标所在的相邻轨道区间索引。"""
|
|
171
|
+
for index, (start, end) in enumerate(zip(values, values[1:], strict=True)):
|
|
172
|
+
if start - _COORD_TOLERANCE <= coordinate <= end + _COORD_TOLERANCE:
|
|
173
|
+
return index
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _serialize_grid(xs: list[float], ys: list[float], lines: list[TextLine]) -> str:
|
|
178
|
+
"""把网格内文字按单元格序列化为安全 HTML。"""
|
|
179
|
+
cells: dict[tuple[int, int], list[TextLine]] = {}
|
|
180
|
+
for line in lines:
|
|
181
|
+
center_x, center_y = bbox_center(line.bbox)
|
|
182
|
+
column = _cell_index(xs, center_x)
|
|
183
|
+
row = _cell_index(ys, center_y)
|
|
184
|
+
if row is None or column is None:
|
|
185
|
+
continue
|
|
186
|
+
cells.setdefault((row, column), []).append(line)
|
|
187
|
+
output = ["<table>"]
|
|
188
|
+
for row in range(len(ys) - 1):
|
|
189
|
+
output.append("<tr>")
|
|
190
|
+
for column in range(len(xs) - 1):
|
|
191
|
+
items = sorted(cells.get((row, column), []), key=lambda item: (item.bbox[1], item.bbox[0], item.paint_order))
|
|
192
|
+
content = "<br>".join(format_line_html(item.text, item.styles) for item in items if item.text.strip())
|
|
193
|
+
output.append(f"<td>{content}</td>")
|
|
194
|
+
output.append("</tr>")
|
|
195
|
+
output.append("</table>")
|
|
196
|
+
return "".join(output)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def recover_tables(
|
|
200
|
+
axis_lines: list[AxisLine],
|
|
201
|
+
text_lines: list[TextLine],
|
|
202
|
+
budget: OfdTableBudget,
|
|
203
|
+
) -> list[OfdTableRegion]:
|
|
204
|
+
"""从页面轴向线段中恢复互不重叠的高置信表格。"""
|
|
205
|
+
if len(axis_lines) < 4 or len(text_lines) < 2 or len(axis_lines) > _MAX_TABLE_AXIS_LINES:
|
|
206
|
+
return []
|
|
207
|
+
candidates: list[OfdTableRegion] = []
|
|
208
|
+
for component in _components(axis_lines, budget):
|
|
209
|
+
grid = _component_grid(component)
|
|
210
|
+
if grid is None:
|
|
211
|
+
continue
|
|
212
|
+
xs, ys, bbox = grid
|
|
213
|
+
contained: list[TextLine] = []
|
|
214
|
+
for line in text_lines:
|
|
215
|
+
center_x, center_y = bbox_center(line.bbox)
|
|
216
|
+
if (
|
|
217
|
+
bbox_intersection(line.bbox, bbox) is not None
|
|
218
|
+
and bbox[0] <= center_x <= bbox[2]
|
|
219
|
+
and bbox[1] <= center_y <= bbox[3]
|
|
220
|
+
):
|
|
221
|
+
contained.append(line)
|
|
222
|
+
if len(contained) < 2:
|
|
223
|
+
continue
|
|
224
|
+
candidates.append(
|
|
225
|
+
OfdTableRegion(
|
|
226
|
+
bbox=bbox,
|
|
227
|
+
html=_serialize_grid(xs, ys, contained),
|
|
228
|
+
paint_order=min((line.paint_order for line in component), default=0),
|
|
229
|
+
consumed_line_ids=frozenset(id(line) for line in contained),
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
selected: list[OfdTableRegion] = []
|
|
233
|
+
for candidate in sorted(candidates, key=lambda item: (-bbox_area(item.bbox), item.bbox[1], item.bbox[0])):
|
|
234
|
+
if any(bbox_intersection(candidate.bbox, existing.bbox) is not None for existing in selected):
|
|
235
|
+
continue
|
|
236
|
+
selected.append(candidate)
|
|
237
|
+
return sorted(selected, key=lambda item: (item.bbox[1], item.bbox[0], item.paint_order))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
__all__ = ["OfdTableBudget", "OfdTableRegion", "recover_tables"]
|