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,293 @@
|
|
|
1
|
+
"""把 OFD 页面场景投影为带 bbox 的有序 raw model-list。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import statistics
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .._shared.xycut import sort_entries
|
|
11
|
+
from ....schema import BlockType
|
|
12
|
+
from .geometry import bbox_center, bbox_union, normalize_bbox
|
|
13
|
+
from .models import OfdPageScene, TextLine
|
|
14
|
+
from .table import OfdTableBudget, OfdTableRegion, recover_tables
|
|
15
|
+
from .text import format_line_spans
|
|
16
|
+
|
|
17
|
+
_PAGE_NUMBER_RE = re.compile(r"^(?:\d+|[IVXLCDM]+)$", re.IGNORECASE)
|
|
18
|
+
_ASCII_WORD_GAP_RATIO = 0.2
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _same_baseline(first: TextLine, second: TextLine) -> bool:
|
|
22
|
+
"""判断两个文字片段是否可在同一视觉基线上拼接。"""
|
|
23
|
+
if first.angle != second.angle or first.styles != second.styles:
|
|
24
|
+
return False
|
|
25
|
+
if (
|
|
26
|
+
min(first.font_size, second.font_size) <= 0
|
|
27
|
+
or max(first.font_size, second.font_size) / min(first.font_size, second.font_size) > 1.35
|
|
28
|
+
):
|
|
29
|
+
return False
|
|
30
|
+
first_height = first.bbox[3] - first.bbox[1]
|
|
31
|
+
second_height = second.bbox[3] - second.bbox[1]
|
|
32
|
+
tolerance = 0.4 * max(first_height, second_height, 0.5)
|
|
33
|
+
if second.bbox[0] + tolerance < first.bbox[0]:
|
|
34
|
+
return False
|
|
35
|
+
center_delta = abs(bbox_center(first.bbox)[1] - bbox_center(second.bbox)[1])
|
|
36
|
+
gap = max(0.0, second.bbox[0] - first.bbox[2])
|
|
37
|
+
return center_delta <= tolerance and gap <= 2.0 * max(first_height, second_height, 0.5)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _join_text(first: TextLine, second: TextLine) -> str:
|
|
41
|
+
"""按语言字符边界和实际字形间距决定同行片段间是否补空格。"""
|
|
42
|
+
if not first.text or not second.text:
|
|
43
|
+
return first.text + second.text
|
|
44
|
+
ascii_word_boundary = (
|
|
45
|
+
first.text[-1].isascii() and second.text[0].isascii() and first.text[-1].isalnum() and second.text[0].isalnum()
|
|
46
|
+
)
|
|
47
|
+
if not ascii_word_boundary:
|
|
48
|
+
return first.text + second.text
|
|
49
|
+
first_glyph = first.glyphs[-1] if first.glyphs else None
|
|
50
|
+
second_glyph = second.glyphs[0] if second.glyphs else None
|
|
51
|
+
first_right = first_glyph.bbox[2] if first_glyph is not None else first.bbox[2]
|
|
52
|
+
second_left = second_glyph.bbox[0] if second_glyph is not None else second.bbox[0]
|
|
53
|
+
gap = second_left - first_right
|
|
54
|
+
glyph_widths = [
|
|
55
|
+
glyph.bbox[2] - glyph.bbox[0]
|
|
56
|
+
for glyph in (first_glyph, second_glyph)
|
|
57
|
+
if glyph is not None and glyph.bbox[2] > glyph.bbox[0]
|
|
58
|
+
]
|
|
59
|
+
reference_width = statistics.median(glyph_widths) if glyph_widths else 0.0
|
|
60
|
+
spacing_threshold = _ASCII_WORD_GAP_RATIO * max(min(first.font_size, second.font_size), reference_width, 0.5)
|
|
61
|
+
needs_space = gap >= spacing_threshold
|
|
62
|
+
return f"{first.text}{' ' if needs_space else ''}{second.text}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def merge_same_baseline_lines(lines: list[TextLine]) -> list[TextLine]:
|
|
66
|
+
"""保守合并相邻 TextObject 形成的同基线文字片段。"""
|
|
67
|
+
|
|
68
|
+
def sort_key(item: TextLine) -> tuple[int, int, float, float, int]:
|
|
69
|
+
"""按自适应基线带和横向位置排列候选片段。"""
|
|
70
|
+
height = max(item.bbox[3] - item.bbox[1], 0.5)
|
|
71
|
+
baseline_bucket = round(bbox_center(item.bbox)[1] / max(0.4 * height, 0.5))
|
|
72
|
+
return item.angle, baseline_bucket, item.bbox[0], item.bbox[1], item.paint_order
|
|
73
|
+
|
|
74
|
+
ordered = sorted(lines, key=sort_key)
|
|
75
|
+
output: list[TextLine] = []
|
|
76
|
+
for line in ordered:
|
|
77
|
+
if output and _same_baseline(output[-1], line):
|
|
78
|
+
previous = output[-1]
|
|
79
|
+
merged_bbox = bbox_union((previous.bbox, line.bbox))
|
|
80
|
+
if merged_bbox is not None:
|
|
81
|
+
output[-1] = replace(
|
|
82
|
+
previous,
|
|
83
|
+
text=_join_text(previous, line),
|
|
84
|
+
bbox=merged_bbox,
|
|
85
|
+
glyphs=previous.glyphs + line.glyphs,
|
|
86
|
+
paint_order=min(previous.paint_order, line.paint_order),
|
|
87
|
+
)
|
|
88
|
+
continue
|
|
89
|
+
output.append(line)
|
|
90
|
+
return output
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _normalized_signature(line: TextLine, scene: OfdPageScene) -> tuple[str, int, int]:
|
|
94
|
+
"""构造跨页重复边缘文字的稳定签名。"""
|
|
95
|
+
width = scene.physical_box[2] - scene.physical_box[0]
|
|
96
|
+
height = scene.physical_box[3] - scene.physical_box[1]
|
|
97
|
+
center_x, center_y = bbox_center(line.bbox)
|
|
98
|
+
text = re.sub(r"\d+", "#", re.sub(r"\s+", "", line.text)).casefold()
|
|
99
|
+
return (
|
|
100
|
+
text,
|
|
101
|
+
round((center_x - scene.physical_box[0]) / max(width, 1.0) * 20),
|
|
102
|
+
round((center_y - scene.physical_box[1]) / max(height, 1.0) * 20),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def repeated_edge_signatures(scenes: list[OfdPageScene]) -> frozenset[tuple[str, int, int]]:
|
|
107
|
+
"""识别至少跨两页重复出现的页边缘文字。"""
|
|
108
|
+
pages_by_signature: dict[tuple[str, int, int], set[int]] = {}
|
|
109
|
+
for scene in scenes:
|
|
110
|
+
height = scene.physical_box[3] - scene.physical_box[1]
|
|
111
|
+
for line in scene.text_lines:
|
|
112
|
+
center_y = bbox_center(line.bbox)[1]
|
|
113
|
+
relative_y = (center_y - scene.physical_box[1]) / max(height, 1.0)
|
|
114
|
+
if 0.18 < relative_y < 0.82:
|
|
115
|
+
continue
|
|
116
|
+
signature = _normalized_signature(line, scene)
|
|
117
|
+
if signature[0]:
|
|
118
|
+
pages_by_signature.setdefault(signature, set()).add(scene.page_idx)
|
|
119
|
+
return frozenset(signature for signature, pages in pages_by_signature.items() if len(pages) >= 2)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _auxiliary_type(line: TextLine, scene: OfdPageScene, repeated: frozenset[tuple[str, int, int]]) -> BlockType | None:
|
|
123
|
+
"""利用 ContentBox、页边缘和跨页重复性识别辅助文字。"""
|
|
124
|
+
physical = scene.physical_box
|
|
125
|
+
width = physical[2] - physical[0]
|
|
126
|
+
height = physical[3] - physical[1]
|
|
127
|
+
center_x, center_y = bbox_center(line.bbox)
|
|
128
|
+
relative_x = (center_x - physical[0]) / max(width, 1.0)
|
|
129
|
+
relative_y = (center_y - physical[1]) / max(height, 1.0)
|
|
130
|
+
normalized_text = line.text.strip()
|
|
131
|
+
if len(normalized_text) <= 6 and _PAGE_NUMBER_RE.fullmatch(normalized_text) and (relative_y <= 0.08 or relative_y >= 0.92):
|
|
132
|
+
return BlockType.PAGE_NUMBER
|
|
133
|
+
outside_content = False
|
|
134
|
+
if scene.content_box is not None:
|
|
135
|
+
content = scene.content_box
|
|
136
|
+
outside_content = not (content[0] <= center_x <= content[2] and content[1] <= center_y <= content[3])
|
|
137
|
+
is_repeated = _normalized_signature(line, scene) in repeated
|
|
138
|
+
if (outside_content and relative_y <= 0.2) or (is_repeated and relative_y <= 0.08):
|
|
139
|
+
return BlockType.HEADER
|
|
140
|
+
if (outside_content and relative_y >= 0.8) or (is_repeated and relative_y >= 0.92):
|
|
141
|
+
return BlockType.FOOTER
|
|
142
|
+
if outside_content and (relative_x <= 0.12 or relative_x >= 0.88):
|
|
143
|
+
return BlockType.ASIDE_TEXT
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _upright_bbox(bbox: list[float], scene: OfdPageScene, angle: int) -> list[float]:
|
|
148
|
+
"""把直角旋转对象的 bbox 转到共享 XYCut 使用的正向坐标。"""
|
|
149
|
+
physical = scene.physical_box
|
|
150
|
+
width = physical[2] - physical[0]
|
|
151
|
+
height = physical[3] - physical[1]
|
|
152
|
+
x0, y0, x1, y1 = (
|
|
153
|
+
bbox[0] - physical[0],
|
|
154
|
+
bbox[1] - physical[1],
|
|
155
|
+
bbox[2] - physical[0],
|
|
156
|
+
bbox[3] - physical[1],
|
|
157
|
+
)
|
|
158
|
+
if angle == 90:
|
|
159
|
+
return [y0, width - x1, y1, width - x0]
|
|
160
|
+
if angle == 180:
|
|
161
|
+
return [width - x1, height - y1, width - x0, height - y0]
|
|
162
|
+
if angle == 270:
|
|
163
|
+
return [height - y1, x0, height - y0, x1]
|
|
164
|
+
return [x0, y0, x1, y1]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class OfdReadingOrderProjector:
|
|
168
|
+
"""执行表格认领、辅助类型判定和 OFD-aware XYCut++。"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, scenes: list[OfdPageScene]) -> None:
|
|
171
|
+
"""缓存跨页重复签名和正文字号基线。"""
|
|
172
|
+
self.scenes = scenes
|
|
173
|
+
self.repeated = repeated_edge_signatures(scenes)
|
|
174
|
+
body_sizes = [line.font_size for scene in scenes for line in scene.text_lines if line.font_size > 0]
|
|
175
|
+
self.body_size = statistics.median(body_sizes) if body_sizes else 1.0
|
|
176
|
+
self.document_title_emitted = False
|
|
177
|
+
self.table_budget = OfdTableBudget()
|
|
178
|
+
|
|
179
|
+
def _text_block(self, line: TextLine, scene: OfdPageScene) -> dict[str, Any]:
|
|
180
|
+
"""把 TextLine 转换为尚未归一化的 raw block。"""
|
|
181
|
+
auxiliary = _auxiliary_type(line, scene, self.repeated)
|
|
182
|
+
block_type: BlockType = auxiliary or BlockType.TEXT
|
|
183
|
+
level: int | None = None
|
|
184
|
+
relative_top = (line.bbox[1] - scene.physical_box[1]) / max(scene.physical_box[3] - scene.physical_box[1], 1.0)
|
|
185
|
+
if auxiliary is None and line.font_size >= 1.8 * self.body_size and relative_top <= 0.35:
|
|
186
|
+
if not self.document_title_emitted:
|
|
187
|
+
block_type = BlockType.DOC_TITLE
|
|
188
|
+
level = 1
|
|
189
|
+
self.document_title_emitted = True
|
|
190
|
+
else:
|
|
191
|
+
block_type = BlockType.PARAGRAPH_TITLE
|
|
192
|
+
level = 2
|
|
193
|
+
elif auxiliary is None and line.font_size >= 1.45 * self.body_size:
|
|
194
|
+
block_type = BlockType.PARAGRAPH_TITLE
|
|
195
|
+
level = 2
|
|
196
|
+
block: dict[str, Any] = {
|
|
197
|
+
"type": block_type,
|
|
198
|
+
"content": format_line_spans(line.text, line.styles),
|
|
199
|
+
"bbox_mm": line.bbox,
|
|
200
|
+
"angle": line.angle,
|
|
201
|
+
"paint_order": line.paint_order,
|
|
202
|
+
"line_bboxes_mm": [line.bbox],
|
|
203
|
+
}
|
|
204
|
+
if level is not None:
|
|
205
|
+
block["level"] = level
|
|
206
|
+
return block
|
|
207
|
+
|
|
208
|
+
def _table_block(self, table: OfdTableRegion) -> dict[str, Any]:
|
|
209
|
+
"""把表格区域转换为尚未归一化的 raw block。"""
|
|
210
|
+
return {
|
|
211
|
+
"type": BlockType.TABLE,
|
|
212
|
+
"content": table.html,
|
|
213
|
+
"bbox_mm": table.bbox,
|
|
214
|
+
"angle": 0,
|
|
215
|
+
"paint_order": table.paint_order,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
def _image_block(self, image: object) -> dict[str, Any]:
|
|
219
|
+
"""把 ImageItem 转换为 raw block或内部占位原子。"""
|
|
220
|
+
payload = getattr(image, "image_base64", None)
|
|
221
|
+
block: dict[str, Any] = {
|
|
222
|
+
"type": BlockType.IMAGE,
|
|
223
|
+
"content": "",
|
|
224
|
+
"bbox_mm": getattr(image, "bbox"),
|
|
225
|
+
"angle": 0,
|
|
226
|
+
"paint_order": getattr(image, "paint_order"),
|
|
227
|
+
"drop_after_sort": payload is None,
|
|
228
|
+
}
|
|
229
|
+
if payload is not None:
|
|
230
|
+
block["image_base64"] = payload
|
|
231
|
+
return block
|
|
232
|
+
|
|
233
|
+
def project_page(self, scene: OfdPageScene) -> list[dict[str, Any]]:
|
|
234
|
+
"""把一页场景投影为最终阅读顺序 raw model-list。"""
|
|
235
|
+
merged_lines = merge_same_baseline_lines(scene.text_lines)
|
|
236
|
+
tables = recover_tables(scene.axis_lines, merged_lines, self.table_budget)
|
|
237
|
+
consumed = {line_id for table in tables for line_id in table.consumed_line_ids}
|
|
238
|
+
blocks = [self._text_block(line, scene) for line in merged_lines if id(line) not in consumed]
|
|
239
|
+
blocks.extend(self._table_block(table) for table in tables)
|
|
240
|
+
blocks.extend(self._image_block(image) for image in scene.images)
|
|
241
|
+
page_center_y = bbox_center(scene.physical_box)[1]
|
|
242
|
+
top = [
|
|
243
|
+
block
|
|
244
|
+
for block in blocks
|
|
245
|
+
if block["type"] == BlockType.HEADER
|
|
246
|
+
or (block["type"] == BlockType.PAGE_NUMBER and bbox_center(block["bbox_mm"])[1] < page_center_y)
|
|
247
|
+
]
|
|
248
|
+
top_ids = {id(block) for block in top}
|
|
249
|
+
bottom = [
|
|
250
|
+
block
|
|
251
|
+
for block in blocks
|
|
252
|
+
if block["type"] == BlockType.FOOTER or (block["type"] == BlockType.PAGE_NUMBER and id(block) not in top_ids)
|
|
253
|
+
]
|
|
254
|
+
bottom_ids = {id(block) for block in bottom}
|
|
255
|
+
body = [block for block in blocks if id(block) not in top_ids and id(block) not in bottom_ids]
|
|
256
|
+
sortable: list[dict[str, Any]] = []
|
|
257
|
+
for block in body:
|
|
258
|
+
upright = _upright_bbox(list(block["bbox_mm"]), scene, int(block.get("angle", 0) or 0) % 360)
|
|
259
|
+
sortable.append({"bbox": [value * 72.0 / 25.4 for value in upright], "payload": block})
|
|
260
|
+
ordered_body = [entry["payload"] for entry in sort_entries(sortable)]
|
|
261
|
+
ordered = sorted(top, key=lambda block: (block["bbox_mm"][1], block["bbox_mm"][0], block["paint_order"]))
|
|
262
|
+
ordered.extend(ordered_body)
|
|
263
|
+
ordered.extend(sorted(bottom, key=lambda block: (block["bbox_mm"][1], block["bbox_mm"][0], block["paint_order"])))
|
|
264
|
+
output: list[dict[str, Any]] = []
|
|
265
|
+
for block in ordered:
|
|
266
|
+
if block.get("drop_after_sort"):
|
|
267
|
+
continue
|
|
268
|
+
normalized = normalize_bbox(block["bbox_mm"], scene.physical_box)
|
|
269
|
+
if normalized is None:
|
|
270
|
+
continue
|
|
271
|
+
result: dict[str, Any] = {
|
|
272
|
+
"type": block["type"],
|
|
273
|
+
"content": block.get("content", ""),
|
|
274
|
+
"bbox": normalized,
|
|
275
|
+
"angle": block.get("angle", 0),
|
|
276
|
+
}
|
|
277
|
+
if "level" in block:
|
|
278
|
+
result["level"] = block["level"]
|
|
279
|
+
if "image_base64" in block:
|
|
280
|
+
result["image_base64"] = block["image_base64"]
|
|
281
|
+
line_bboxes = [normalize_bbox(item, scene.physical_box) for item in block.get("line_bboxes_mm", [])]
|
|
282
|
+
valid_line_bboxes = [item for item in line_bboxes if item is not None]
|
|
283
|
+
if valid_line_bboxes:
|
|
284
|
+
result["lines"] = [{"bbox": item} for item in valid_line_bboxes]
|
|
285
|
+
output.append(result)
|
|
286
|
+
return output
|
|
287
|
+
|
|
288
|
+
def project(self) -> list[list[dict[str, Any]]]:
|
|
289
|
+
"""投影全部页面并保持原始页树顺序。"""
|
|
290
|
+
return [self.project_page(scene) for scene in self.scenes]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
__all__ = ["OfdReadingOrderProjector", "merge_same_baseline_lines", "repeated_edge_signatures"]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""OFD 资源文件索引与作用域合并。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from .constants import MAX_DRAW_PARAM_INHERITANCE, OFD_NAMESPACES
|
|
8
|
+
from .errors import OfdParseError, OfdResourceLimitError
|
|
9
|
+
from .geometry import parse_numbers
|
|
10
|
+
from .models import CompositeResource, FontResource, MediaResource, ResourceRegistry
|
|
11
|
+
from .package import OfdPackage, element_text, first_child, local_name, namespace_name, parse_int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _bool_attr(value: str | None) -> bool:
|
|
15
|
+
"""把 OFD 布尔属性解析为确定值。"""
|
|
16
|
+
return (value or "").strip().casefold() in {"true", "1"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resource_asset_part(package: OfdPackage, resource_part: str, base_loc: str, location: str) -> str | None:
|
|
20
|
+
"""把资源 BaseLoc 与资源内相对路径组合成包成员。"""
|
|
21
|
+
base_part = package.resolve_reference(resource_part, base_loc) if base_loc else posix_parent(resource_part)
|
|
22
|
+
if base_part is None:
|
|
23
|
+
return None
|
|
24
|
+
synthetic_base = f"{base_part.rstrip('/')}/_resource.xml" if base_loc else resource_part
|
|
25
|
+
return package.resolve_reference(synthetic_base, location)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def posix_parent(part_name: str) -> str:
|
|
29
|
+
"""返回包成员的 POSIX 父目录。"""
|
|
30
|
+
return part_name.rsplit("/", 1)[0] if "/" in part_name else ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_resource_part(package: OfdPackage, resource_part: str | None) -> ResourceRegistry:
|
|
34
|
+
"""解析单个 PublicRes、DocumentRes 或 PageRes。"""
|
|
35
|
+
registry = ResourceRegistry()
|
|
36
|
+
if resource_part is None:
|
|
37
|
+
return registry
|
|
38
|
+
root = package.xml_part(resource_part, required=True)
|
|
39
|
+
assert root is not None
|
|
40
|
+
namespace = namespace_name(root.tag)
|
|
41
|
+
if namespace not in OFD_NAMESPACES:
|
|
42
|
+
raise OfdParseError(f"Malformed OFD package: unsupported namespace {namespace!r} in {resource_part!r}")
|
|
43
|
+
base_loc = (root.get("BaseLoc") or "").strip()
|
|
44
|
+
for element in root.iter():
|
|
45
|
+
name = local_name(element.tag)
|
|
46
|
+
resource_id = parse_int(element.get("ID"))
|
|
47
|
+
if resource_id is None:
|
|
48
|
+
continue
|
|
49
|
+
if name == "Font":
|
|
50
|
+
font_file = element_text(first_child(element, "FontFile"))
|
|
51
|
+
font_part = _resource_asset_part(package, resource_part, base_loc, font_file) if font_file else None
|
|
52
|
+
registry.fonts[resource_id] = FontResource(
|
|
53
|
+
resource_id=resource_id,
|
|
54
|
+
font_name=(element.get("FontName") or "").strip(),
|
|
55
|
+
family_name=(element.get("FamilyName") or "").strip(),
|
|
56
|
+
font_part=font_part,
|
|
57
|
+
bold=_bool_attr(element.get("Bold")),
|
|
58
|
+
italic=_bool_attr(element.get("Italic")),
|
|
59
|
+
)
|
|
60
|
+
elif name == "MultiMedia":
|
|
61
|
+
media_file = element_text(first_child(element, "MediaFile"))
|
|
62
|
+
media_part = _resource_asset_part(package, resource_part, base_loc, media_file) if media_file else None
|
|
63
|
+
if media_part:
|
|
64
|
+
registry.media[resource_id] = MediaResource(
|
|
65
|
+
resource_id=resource_id,
|
|
66
|
+
media_type=(element.get("Type") or "").strip(),
|
|
67
|
+
media_format=(element.get("Format") or "").strip(),
|
|
68
|
+
media_part=media_part,
|
|
69
|
+
)
|
|
70
|
+
elif name == "CompositeGraphicUnit":
|
|
71
|
+
size = parse_numbers(f"{element.get('Width') or ''} {element.get('Height') or ''}", expected=2)
|
|
72
|
+
registry.composites[resource_id] = CompositeResource(
|
|
73
|
+
resource_id=resource_id,
|
|
74
|
+
width=size[0] if size else 0.0,
|
|
75
|
+
height=size[1] if size else 0.0,
|
|
76
|
+
element=element,
|
|
77
|
+
)
|
|
78
|
+
elif name == "DrawParam":
|
|
79
|
+
registry.draw_params[resource_id] = {str(key): str(value) for key, value in element.attrib.items()}
|
|
80
|
+
return registry
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def merge_registries(*registries: ResourceRegistry) -> ResourceRegistry:
|
|
84
|
+
"""按 Public→Document→Page 顺序合并资源并记录重复 ID。"""
|
|
85
|
+
merged = ResourceRegistry()
|
|
86
|
+
for registry in registries:
|
|
87
|
+
for field_name in ("fonts", "media", "composites", "draw_params"):
|
|
88
|
+
target = getattr(merged, field_name)
|
|
89
|
+
incoming = getattr(registry, field_name)
|
|
90
|
+
for resource_id, value in incoming.items():
|
|
91
|
+
if resource_id in target:
|
|
92
|
+
logger.warning(f"OFD_DUPLICATE_RESOURCE_ID: kind={field_name}, id={resource_id}; later scope wins")
|
|
93
|
+
target[resource_id] = value
|
|
94
|
+
return merged
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resolve_draw_param(registry: ResourceRegistry, resource_id: int | None) -> dict[str, str]:
|
|
98
|
+
"""迭代解析 DrawParam Relative 继承,并限制深度、检测循环。"""
|
|
99
|
+
if resource_id is None:
|
|
100
|
+
return {}
|
|
101
|
+
inheritance_chain: list[dict[str, str]] = []
|
|
102
|
+
visited: set[int] = set()
|
|
103
|
+
current_id: int | None = resource_id
|
|
104
|
+
while current_id is not None:
|
|
105
|
+
if current_id in visited:
|
|
106
|
+
raise ValueError(f"OFD DrawParam cycle detected at id={current_id}")
|
|
107
|
+
current = registry.draw_params.get(current_id)
|
|
108
|
+
if current is None:
|
|
109
|
+
break
|
|
110
|
+
if len(inheritance_chain) >= MAX_DRAW_PARAM_INHERITANCE:
|
|
111
|
+
raise OfdResourceLimitError(f"OFD resource limit exceeded: max_draw_param_inheritance={MAX_DRAW_PARAM_INHERITANCE}")
|
|
112
|
+
visited.add(current_id)
|
|
113
|
+
inheritance_chain.append(current)
|
|
114
|
+
current_id = parse_int(current.get("Relative"))
|
|
115
|
+
|
|
116
|
+
result: dict[str, str] = {}
|
|
117
|
+
for current in reversed(inheritance_chain):
|
|
118
|
+
result.update({key: value for key, value in current.items() if key not in {"ID", "Relative"}})
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
__all__ = ["merge_registries", "parse_resource_part", "resolve_draw_param"]
|