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,59 @@
|
|
|
1
|
+
"""OFD 命名空间、版本与安全资源上限。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Final
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
OFD_NAMESPACE: Final = "http://www.ofdspec.org/2016"
|
|
9
|
+
OFD_LEGACY_NAMESPACE: Final = "http://www.ofdspec.org"
|
|
10
|
+
OFD_NAMESPACES: Final = frozenset({OFD_NAMESPACE, OFD_LEGACY_NAMESPACE})
|
|
11
|
+
OFD_KNOWN_VERSIONS: Final = frozenset({"1.0", "1.1", "1.2"})
|
|
12
|
+
|
|
13
|
+
MAX_ENTRY_BYTES: Final = 128 * 1024 * 1024
|
|
14
|
+
MAX_TOTAL_BYTES: Final = 512 * 1024 * 1024
|
|
15
|
+
MAX_ENTRY_COUNT: Final = 100_000
|
|
16
|
+
MAX_XML_DEPTH: Final = 256
|
|
17
|
+
MAX_XML_NODES: Final = 2_000_000
|
|
18
|
+
MAX_DOCUMENT_COUNT: Final = 10_000
|
|
19
|
+
MAX_PAGE_COUNT: Final = 10_000
|
|
20
|
+
MAX_ASSET_TOTAL_BYTES: Final = 128 * 1024 * 1024
|
|
21
|
+
MAX_EXPANDED_TEXT_BYTES: Final = 64 * 1024 * 1024
|
|
22
|
+
MAX_EXPANDED_GLYPHS: Final = 2_000_000
|
|
23
|
+
MAX_GLYPH_TOKENS: Final = 2_000_000
|
|
24
|
+
MAX_DELTA_TOKENS: Final = MAX_EXPANDED_GLYPHS
|
|
25
|
+
MAX_PATH_COMMANDS: Final = 2_000_000
|
|
26
|
+
MAX_PATH_TOKENS: Final = MAX_PATH_COMMANDS * 8
|
|
27
|
+
MAX_TABLE_INTERSECTION_CHECKS: Final = MAX_PATH_COMMANDS
|
|
28
|
+
MAX_OBJECT_RECURSION: Final = 64
|
|
29
|
+
MAX_DRAW_PARAM_INHERITANCE: Final = 64
|
|
30
|
+
MAX_FONT_BYTES: Final = 32 * 1024 * 1024
|
|
31
|
+
|
|
32
|
+
MM_TO_POINTS: Final = 72.0 / 25.4
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"MAX_ASSET_TOTAL_BYTES",
|
|
37
|
+
"MAX_DOCUMENT_COUNT",
|
|
38
|
+
"MAX_DELTA_TOKENS",
|
|
39
|
+
"MAX_DRAW_PARAM_INHERITANCE",
|
|
40
|
+
"MAX_ENTRY_BYTES",
|
|
41
|
+
"MAX_ENTRY_COUNT",
|
|
42
|
+
"MAX_EXPANDED_GLYPHS",
|
|
43
|
+
"MAX_EXPANDED_TEXT_BYTES",
|
|
44
|
+
"MAX_FONT_BYTES",
|
|
45
|
+
"MAX_GLYPH_TOKENS",
|
|
46
|
+
"MAX_OBJECT_RECURSION",
|
|
47
|
+
"MAX_PAGE_COUNT",
|
|
48
|
+
"MAX_PATH_COMMANDS",
|
|
49
|
+
"MAX_PATH_TOKENS",
|
|
50
|
+
"MAX_TABLE_INTERSECTION_CHECKS",
|
|
51
|
+
"MAX_TOTAL_BYTES",
|
|
52
|
+
"MAX_XML_DEPTH",
|
|
53
|
+
"MAX_XML_NODES",
|
|
54
|
+
"MM_TO_POINTS",
|
|
55
|
+
"OFD_KNOWN_VERSIONS",
|
|
56
|
+
"OFD_LEGACY_NAMESPACE",
|
|
57
|
+
"OFD_NAMESPACE",
|
|
58
|
+
"OFD_NAMESPACES",
|
|
59
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""OFD 二进制流到分页 raw model-list 的转换入口。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import BinaryIO
|
|
6
|
+
|
|
7
|
+
from .constants import MAX_TOTAL_BYTES
|
|
8
|
+
from .errors import OfdResourceLimitError
|
|
9
|
+
from .package import OfdPackage
|
|
10
|
+
from .reading_order import OfdReadingOrderProjector
|
|
11
|
+
from .scene import OfdSceneBuilder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OfdConverter:
|
|
15
|
+
"""编排 OFD 包读取、场景构建和阅读顺序投影。"""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
"""初始化空的分页输出。"""
|
|
19
|
+
self.pages: list[list[dict[str, object]]] = []
|
|
20
|
+
|
|
21
|
+
def convert(self, file_binary: BinaryIO) -> None:
|
|
22
|
+
"""读取整份 OFD 并更新分页 model-list。"""
|
|
23
|
+
file_bytes = file_binary.read(MAX_TOTAL_BYTES + 1)
|
|
24
|
+
if len(file_bytes) > MAX_TOTAL_BYTES:
|
|
25
|
+
raise OfdResourceLimitError(f"OFD resource limit exceeded: max_total_bytes={MAX_TOTAL_BYTES}")
|
|
26
|
+
with OfdPackage(file_bytes) as package:
|
|
27
|
+
scenes = OfdSceneBuilder(package).build()
|
|
28
|
+
self.pages = OfdReadingOrderProjector(scenes).project()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ["OfdConverter"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""OFD 原生解析错误类型。"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OfdParseError(ValueError):
|
|
5
|
+
"""表示 OFD 包结构或必需内容不合法。"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OfdEncryptedError(OfdParseError):
|
|
9
|
+
"""表示 OFD 包或成员使用了不支持的加密。"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OfdResourceLimitError(OfdParseError):
|
|
13
|
+
"""表示 OFD 输入超过固定资源预算。"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["OfdEncryptedError", "OfdParseError", "OfdResourceLimitError"]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""OFD 毫米坐标、仿射矩阵与 bbox 工具。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from ....schema import BBox
|
|
10
|
+
from .constants import MM_TO_POINTS
|
|
11
|
+
|
|
12
|
+
Point = tuple[float, float]
|
|
13
|
+
Quad = tuple[Point, Point, Point, Point]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class Affine:
|
|
18
|
+
"""保存 OFD 六参数仿射矩阵。"""
|
|
19
|
+
|
|
20
|
+
a: float = 1.0
|
|
21
|
+
b: float = 0.0
|
|
22
|
+
c: float = 0.0
|
|
23
|
+
d: float = 1.0
|
|
24
|
+
e: float = 0.0
|
|
25
|
+
f: float = 0.0
|
|
26
|
+
|
|
27
|
+
def apply(self, point: Point) -> Point:
|
|
28
|
+
"""把一个局部点变换到目标坐标空间。"""
|
|
29
|
+
x, y = point
|
|
30
|
+
return (self.a * x + self.c * y + self.e, self.b * x + self.d * y + self.f)
|
|
31
|
+
|
|
32
|
+
def compose(self, inner: Affine) -> Affine:
|
|
33
|
+
"""返回先执行 inner、再执行当前矩阵的组合结果。"""
|
|
34
|
+
return Affine(
|
|
35
|
+
a=self.a * inner.a + self.c * inner.b,
|
|
36
|
+
b=self.b * inner.a + self.d * inner.b,
|
|
37
|
+
c=self.a * inner.c + self.c * inner.d,
|
|
38
|
+
d=self.b * inner.c + self.d * inner.d,
|
|
39
|
+
e=self.a * inner.e + self.c * inner.f + self.e,
|
|
40
|
+
f=self.b * inner.e + self.d * inner.f + self.f,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def translation(cls, x: float, y: float) -> Affine:
|
|
45
|
+
"""构造只包含平移的矩阵。"""
|
|
46
|
+
return cls(e=x, f=y)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def rotation(cls, angle: float) -> Affine:
|
|
50
|
+
"""构造绕局部原点旋转指定角度的矩阵。"""
|
|
51
|
+
radians = math.radians(angle)
|
|
52
|
+
cosine = math.cos(radians)
|
|
53
|
+
sine = math.sin(radians)
|
|
54
|
+
return cls(a=cosine, b=sine, c=-sine, d=cosine)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_numbers(value: str | None, *, expected: int | None = None) -> tuple[float, ...] | None:
|
|
58
|
+
"""把空白分隔的有限数值解析为元组。"""
|
|
59
|
+
if not value:
|
|
60
|
+
return None
|
|
61
|
+
try:
|
|
62
|
+
numbers = tuple(float(item) for item in value.replace(",", " ").split())
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
return None
|
|
65
|
+
if expected is not None and len(numbers) != expected:
|
|
66
|
+
return None
|
|
67
|
+
if not all(math.isfinite(item) for item in numbers):
|
|
68
|
+
return None
|
|
69
|
+
return numbers
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_st_box(value: str | None) -> BBox | None:
|
|
73
|
+
"""把 OFD 的 x/y/width/height 转换为 x0/y0/x1/y1。"""
|
|
74
|
+
numbers = parse_numbers(value, expected=4)
|
|
75
|
+
if numbers is None:
|
|
76
|
+
return None
|
|
77
|
+
x, y, width, height = numbers
|
|
78
|
+
if width <= 0 or height <= 0:
|
|
79
|
+
return None
|
|
80
|
+
return (x, y, x + width, y + height)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_affine(value: str | None) -> Affine:
|
|
84
|
+
"""解析可选 CTM,缺失或非法时返回单位矩阵。"""
|
|
85
|
+
numbers = parse_numbers(value, expected=6)
|
|
86
|
+
return Affine(*numbers) if numbers is not None else Affine()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def rect_quad(bbox: BBox) -> Quad:
|
|
90
|
+
"""把轴对齐矩形转换为顺时针四点。"""
|
|
91
|
+
x0, y0, x1, y1 = bbox
|
|
92
|
+
return ((x0, y0), (x1, y0), (x1, y1), (x0, y1))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def quad_bbox(points: Iterable[Point]) -> BBox | None:
|
|
96
|
+
"""计算一组有限点的轴对齐外接框。"""
|
|
97
|
+
materialized = list(points)
|
|
98
|
+
if not materialized or not all(math.isfinite(value) for point in materialized for value in point):
|
|
99
|
+
return None
|
|
100
|
+
xs = [point[0] for point in materialized]
|
|
101
|
+
ys = [point[1] for point in materialized]
|
|
102
|
+
bbox = (min(xs), min(ys), max(xs), max(ys))
|
|
103
|
+
return bbox if bbox[2] > bbox[0] and bbox[3] > bbox[1] else None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def transform_quad(quad: Quad, transform: Affine) -> Quad:
|
|
107
|
+
"""把四点按给定仿射矩阵变换。"""
|
|
108
|
+
return tuple(transform.apply(point) for point in quad) # type: ignore[return-value]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def transform_bbox(bbox: BBox, transform: Affine) -> BBox | None:
|
|
112
|
+
"""变换矩形四角并返回目标空间 AABB。"""
|
|
113
|
+
return quad_bbox(transform_quad(rect_quad(bbox), transform))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def bbox_union(bboxes: Iterable[BBox]) -> BBox | None:
|
|
117
|
+
"""合并全部有效 bbox。"""
|
|
118
|
+
materialized = list(bboxes)
|
|
119
|
+
if not materialized:
|
|
120
|
+
return None
|
|
121
|
+
return (
|
|
122
|
+
min(item[0] for item in materialized),
|
|
123
|
+
min(item[1] for item in materialized),
|
|
124
|
+
max(item[2] for item in materialized),
|
|
125
|
+
max(item[3] for item in materialized),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def bbox_intersection(first: BBox, second: BBox) -> BBox | None:
|
|
130
|
+
"""返回两个 bbox 的非退化交集。"""
|
|
131
|
+
bbox = (
|
|
132
|
+
max(first[0], second[0]),
|
|
133
|
+
max(first[1], second[1]),
|
|
134
|
+
min(first[2], second[2]),
|
|
135
|
+
min(first[3], second[3]),
|
|
136
|
+
)
|
|
137
|
+
return bbox if bbox[2] > bbox[0] and bbox[3] > bbox[1] else None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def bbox_center(bbox: BBox) -> Point:
|
|
141
|
+
"""返回 bbox 中心点。"""
|
|
142
|
+
return ((bbox[0] + bbox[2]) / 2.0, (bbox[1] + bbox[3]) / 2.0)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def bbox_area(bbox: BBox) -> float:
|
|
146
|
+
"""返回非负 bbox 面积。"""
|
|
147
|
+
return max(0.0, bbox[2] - bbox[0]) * max(0.0, bbox[3] - bbox[1])
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def bbox_overlap_ratio(first: BBox, second: BBox) -> float:
|
|
151
|
+
"""返回交集相对较小 bbox 的面积比例。"""
|
|
152
|
+
intersection = bbox_intersection(first, second)
|
|
153
|
+
smaller = min(bbox_area(first), bbox_area(second))
|
|
154
|
+
return bbox_area(intersection) / smaller if intersection is not None and smaller > 0 else 0.0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def transform_angle(transform: Affine) -> float:
|
|
158
|
+
"""返回局部 X 轴经过矩阵后的页面角度。"""
|
|
159
|
+
return math.degrees(math.atan2(transform.b, transform.a)) % 360.0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def canonical_angle(angle: float, *, tolerance: float = 5.0) -> int:
|
|
163
|
+
"""把接近直角的角度收敛为 0/90/180/270。"""
|
|
164
|
+
normalized = angle % 360.0
|
|
165
|
+
nearest = min((0, 90, 180, 270, 360), key=lambda item: abs(normalized - item))
|
|
166
|
+
if abs(normalized - nearest) <= tolerance:
|
|
167
|
+
return nearest % 360
|
|
168
|
+
return int(round(normalized)) % 360
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def bbox_to_points(bbox: BBox) -> list[float]:
|
|
172
|
+
"""把毫米 bbox 转换为供共享 XYCut 使用的 point 坐标。"""
|
|
173
|
+
return [value * MM_TO_POINTS for value in bbox]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def normalize_bbox(bbox: BBox, physical_box: BBox) -> list[float] | None:
|
|
177
|
+
"""把页面 bbox 裁剪并归一化到 PhysicalBox。"""
|
|
178
|
+
clipped = bbox_intersection(bbox, physical_box)
|
|
179
|
+
if clipped is None:
|
|
180
|
+
return None
|
|
181
|
+
width = physical_box[2] - physical_box[0]
|
|
182
|
+
height = physical_box[3] - physical_box[1]
|
|
183
|
+
if width <= 0 or height <= 0:
|
|
184
|
+
return None
|
|
185
|
+
values = [
|
|
186
|
+
round((clipped[0] - physical_box[0]) / width, 3),
|
|
187
|
+
round((clipped[1] - physical_box[1]) / height, 3),
|
|
188
|
+
round((clipped[2] - physical_box[0]) / width, 3),
|
|
189
|
+
round((clipped[3] - physical_box[1]) / height, 3),
|
|
190
|
+
]
|
|
191
|
+
values = [max(0.0, min(1.0, value)) for value in values]
|
|
192
|
+
return values if values[2] > values[0] and values[3] > values[1] else None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = [
|
|
196
|
+
"Affine",
|
|
197
|
+
"Point",
|
|
198
|
+
"Quad",
|
|
199
|
+
"bbox_area",
|
|
200
|
+
"bbox_center",
|
|
201
|
+
"bbox_intersection",
|
|
202
|
+
"bbox_overlap_ratio",
|
|
203
|
+
"bbox_to_points",
|
|
204
|
+
"bbox_union",
|
|
205
|
+
"canonical_angle",
|
|
206
|
+
"normalize_bbox",
|
|
207
|
+
"parse_affine",
|
|
208
|
+
"parse_numbers",
|
|
209
|
+
"parse_st_box",
|
|
210
|
+
"quad_bbox",
|
|
211
|
+
"rect_quad",
|
|
212
|
+
"transform_angle",
|
|
213
|
+
"transform_bbox",
|
|
214
|
+
"transform_quad",
|
|
215
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""OFD ImageObject 资源解码与安全图片投影。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from PIL import Image, UnidentifiedImageError
|
|
10
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
11
|
+
|
|
12
|
+
from docvortex.foundation.image_encoding import image_to_b64str
|
|
13
|
+
from ....schema import BBox
|
|
14
|
+
from ....foundation.image_payload import validate_decoded_raster_size
|
|
15
|
+
from .errors import OfdParseError
|
|
16
|
+
from .geometry import Affine, bbox_intersection, canonical_angle, parse_affine, parse_st_box, transform_angle, transform_bbox
|
|
17
|
+
from .models import ImageItem, ResourceRegistry
|
|
18
|
+
from .package import OfdPackage, parse_int
|
|
19
|
+
|
|
20
|
+
_JBIG2_SIGNATURE = b"\x97JB2\r\n\x1a\n"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _serialize_raster(data: bytes, *, alpha: int, angle: int) -> str | None:
|
|
24
|
+
"""使用 Pillow 校验、旋转并编码常见位图。"""
|
|
25
|
+
try:
|
|
26
|
+
with Image.open(BytesIO(data)) as source:
|
|
27
|
+
validate_decoded_raster_size(*source.size)
|
|
28
|
+
source.load()
|
|
29
|
+
image = source.copy()
|
|
30
|
+
except (Image.DecompressionBombError, UnidentifiedImageError, OSError, SyntaxError, ValueError):
|
|
31
|
+
return None
|
|
32
|
+
if angle == 90:
|
|
33
|
+
image = image.transpose(Image.Transpose.ROTATE_270)
|
|
34
|
+
elif angle == 180:
|
|
35
|
+
image = image.transpose(Image.Transpose.ROTATE_180)
|
|
36
|
+
elif angle == 270:
|
|
37
|
+
image = image.transpose(Image.Transpose.ROTATE_90)
|
|
38
|
+
if alpha < 255:
|
|
39
|
+
rgba = image.convert("RGBA")
|
|
40
|
+
current_alpha = rgba.getchannel("A")
|
|
41
|
+
current_alpha = current_alpha.point(lambda value: round(value * alpha / 255))
|
|
42
|
+
rgba.putalpha(current_alpha)
|
|
43
|
+
image = rgba
|
|
44
|
+
if image.mode in {"RGBA", "LA"} or (image.mode == "P" and "transparency" in image.info):
|
|
45
|
+
return image_to_b64str(image.convert("RGBA"), image_format="PNG")
|
|
46
|
+
return image_to_b64str(image.convert("RGB"), image_format="JPEG")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_image_item(
|
|
50
|
+
image_object: etree._Element,
|
|
51
|
+
*,
|
|
52
|
+
parent_transform: Affine,
|
|
53
|
+
parent_clip: BBox,
|
|
54
|
+
resources: ResourceRegistry,
|
|
55
|
+
package: OfdPackage,
|
|
56
|
+
paint_order: int,
|
|
57
|
+
layer_type: str,
|
|
58
|
+
template_id: int | None,
|
|
59
|
+
resolved_style: dict[str, str] | None = None,
|
|
60
|
+
) -> ImageItem | None:
|
|
61
|
+
"""解析一个 ImageObject,并保留不支持图片的占位几何。"""
|
|
62
|
+
style = resolved_style or {}
|
|
63
|
+
if (style.get("Visible") or image_object.get("Visible") or "true").casefold() in {"false", "0"}:
|
|
64
|
+
return None
|
|
65
|
+
boundary = parse_st_box(image_object.get("Boundary"))
|
|
66
|
+
if boundary is None:
|
|
67
|
+
return None
|
|
68
|
+
page_bbox = transform_bbox(boundary, parent_transform)
|
|
69
|
+
if page_bbox is None:
|
|
70
|
+
return None
|
|
71
|
+
page_bbox = bbox_intersection(page_bbox, parent_clip)
|
|
72
|
+
if page_bbox is None:
|
|
73
|
+
return None
|
|
74
|
+
raw_resource_id = image_object.get("ResourceID") or image_object.get("ResouceID")
|
|
75
|
+
resource_id = parse_int(raw_resource_id)
|
|
76
|
+
media = resources.media.get(resource_id) if resource_id is not None else None
|
|
77
|
+
object_id = parse_int(image_object.get("ID"))
|
|
78
|
+
if raw_resource_id is not None and media is None:
|
|
79
|
+
raise OfdParseError(
|
|
80
|
+
f"Malformed OFD package: ImageObject id={object_id} references missing media resource {raw_resource_id!r}"
|
|
81
|
+
)
|
|
82
|
+
if media is None:
|
|
83
|
+
return ImageItem(
|
|
84
|
+
bbox=page_bbox,
|
|
85
|
+
image_base64=None,
|
|
86
|
+
paint_order=paint_order,
|
|
87
|
+
object_id=object_id,
|
|
88
|
+
layer_type=layer_type,
|
|
89
|
+
template_id=template_id,
|
|
90
|
+
diagnostic="missing_media_resource",
|
|
91
|
+
)
|
|
92
|
+
data = package.read_part(media.media_part, required=True, asset=True)
|
|
93
|
+
assert data is not None
|
|
94
|
+
if data.startswith(_JBIG2_SIGNATURE):
|
|
95
|
+
diagnostic = "unsupported_jbig2"
|
|
96
|
+
payload = None
|
|
97
|
+
else:
|
|
98
|
+
try:
|
|
99
|
+
alpha = min(255, max(0, int(float(style.get("Alpha") or image_object.get("Alpha") or 255))))
|
|
100
|
+
except ValueError:
|
|
101
|
+
alpha = 255
|
|
102
|
+
ctm = parse_affine(image_object.get("CTM"))
|
|
103
|
+
raw_angle = transform_angle(parent_transform.compose(ctm))
|
|
104
|
+
angle = canonical_angle(raw_angle)
|
|
105
|
+
if angle not in {0, 90, 180, 270} or not math.isclose(math.remainder(raw_angle, 90.0), 0.0, abs_tol=5.0):
|
|
106
|
+
diagnostic = "unsupported_image_affine"
|
|
107
|
+
payload = None
|
|
108
|
+
else:
|
|
109
|
+
payload = _serialize_raster(data, alpha=alpha, angle=angle)
|
|
110
|
+
diagnostic = None if payload is not None else "unsupported_image_payload"
|
|
111
|
+
if diagnostic:
|
|
112
|
+
logger.warning(f"OFD_IMAGE_SKIPPED: code={diagnostic}, object_id={object_id}, part={media.media_part!r}")
|
|
113
|
+
return ImageItem(
|
|
114
|
+
bbox=page_bbox,
|
|
115
|
+
image_base64=payload,
|
|
116
|
+
paint_order=paint_order,
|
|
117
|
+
object_id=object_id,
|
|
118
|
+
layer_type=layer_type,
|
|
119
|
+
template_id=template_id,
|
|
120
|
+
diagnostic=diagnostic,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = ["build_image_item"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""从 OFD.xml 与 Document.xml 提取 Doclib 基础元数据。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import BinaryIO
|
|
6
|
+
|
|
7
|
+
from .constants import MAX_PAGE_COUNT, MAX_TOTAL_BYTES
|
|
8
|
+
from .errors import OfdResourceLimitError
|
|
9
|
+
from .package import OfdPackage, first_descendant, local_name
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_ofd_metadata(file_binary: BinaryIO) -> dict[str, object | None]:
|
|
13
|
+
"""返回全部 DocBody 的总页数和首个非空文档元数据。"""
|
|
14
|
+
file_bytes = file_binary.read(MAX_TOTAL_BYTES + 1)
|
|
15
|
+
if len(file_bytes) > MAX_TOTAL_BYTES:
|
|
16
|
+
raise OfdResourceLimitError(f"OFD resource limit exceeded: max_total_bytes={MAX_TOTAL_BYTES}")
|
|
17
|
+
with OfdPackage(file_bytes) as package:
|
|
18
|
+
refs = package.document_refs()
|
|
19
|
+
page_count = 0
|
|
20
|
+
metadata: dict[str, str | None] = {
|
|
21
|
+
"title": None,
|
|
22
|
+
"author": None,
|
|
23
|
+
"subject": None,
|
|
24
|
+
"keywords": None,
|
|
25
|
+
}
|
|
26
|
+
key_map = {
|
|
27
|
+
"Title": "title",
|
|
28
|
+
"Author": "author",
|
|
29
|
+
"Subject": "subject",
|
|
30
|
+
"Keywords": "keywords",
|
|
31
|
+
}
|
|
32
|
+
for ref in refs:
|
|
33
|
+
document_root = package.xml_part(ref.document_part, required=True)
|
|
34
|
+
assert document_root is not None
|
|
35
|
+
pages = first_descendant(document_root, "Pages")
|
|
36
|
+
if pages is not None:
|
|
37
|
+
for child in pages:
|
|
38
|
+
if local_name(child.tag) != "Page":
|
|
39
|
+
continue
|
|
40
|
+
page_count += 1
|
|
41
|
+
if page_count > MAX_PAGE_COUNT:
|
|
42
|
+
raise OfdResourceLimitError(f"OFD resource limit exceeded: max_page_count={MAX_PAGE_COUNT}")
|
|
43
|
+
for source_key, target_key in key_map.items():
|
|
44
|
+
if metadata[target_key] is None and ref.metadata.get(source_key):
|
|
45
|
+
metadata[target_key] = ref.metadata[source_key]
|
|
46
|
+
return {
|
|
47
|
+
"page_count": page_count,
|
|
48
|
+
**metadata,
|
|
49
|
+
"is_image_based": 0,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = ["extract_ofd_metadata"]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""OFD 解析器内部使用的确定性数据模型。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
9
|
+
|
|
10
|
+
from ....schema import BBox
|
|
11
|
+
from .geometry import Affine, Point, Quad
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class FontResource:
|
|
16
|
+
"""保存字体资源及可选内嵌字体成员路径。"""
|
|
17
|
+
|
|
18
|
+
resource_id: int
|
|
19
|
+
font_name: str
|
|
20
|
+
family_name: str
|
|
21
|
+
font_part: str | None
|
|
22
|
+
bold: bool = False
|
|
23
|
+
italic: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class MediaResource:
|
|
28
|
+
"""保存多媒体资源的成员路径与声明格式。"""
|
|
29
|
+
|
|
30
|
+
resource_id: int
|
|
31
|
+
media_type: str
|
|
32
|
+
media_format: str
|
|
33
|
+
media_part: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class CompositeResource:
|
|
38
|
+
"""保存可递归展开的复合图元资源。"""
|
|
39
|
+
|
|
40
|
+
resource_id: int
|
|
41
|
+
width: float
|
|
42
|
+
height: float
|
|
43
|
+
element: etree._Element
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class ResourceRegistry:
|
|
48
|
+
"""保存当前文档或页面作用域内的资源索引。"""
|
|
49
|
+
|
|
50
|
+
fonts: dict[int, FontResource] = field(default_factory=dict)
|
|
51
|
+
media: dict[int, MediaResource] = field(default_factory=dict)
|
|
52
|
+
composites: dict[int, CompositeResource] = field(default_factory=dict)
|
|
53
|
+
draw_params: dict[int, dict[str, str]] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class GlyphItem:
|
|
58
|
+
"""保存一个语义字符在页面空间中的几何。"""
|
|
59
|
+
|
|
60
|
+
text: str
|
|
61
|
+
bbox: BBox
|
|
62
|
+
quad: Quad
|
|
63
|
+
origin: Point
|
|
64
|
+
glyph_id: int | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(slots=True)
|
|
68
|
+
class TextLine:
|
|
69
|
+
"""保存一个 TextCode 恢复出的可排序文字行。"""
|
|
70
|
+
|
|
71
|
+
text: str
|
|
72
|
+
bbox: BBox
|
|
73
|
+
glyphs: list[GlyphItem]
|
|
74
|
+
angle: int
|
|
75
|
+
font_size: float
|
|
76
|
+
paint_order: int
|
|
77
|
+
object_id: int | None
|
|
78
|
+
layer_type: str
|
|
79
|
+
template_id: int | None
|
|
80
|
+
styles: tuple[str, ...] = ()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class AxisLine:
|
|
85
|
+
"""保存页面空间中的一条可见水平或垂直线。"""
|
|
86
|
+
|
|
87
|
+
bbox: BBox
|
|
88
|
+
orientation: str
|
|
89
|
+
width: float
|
|
90
|
+
paint_order: int
|
|
91
|
+
template_id: int | None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True, slots=True)
|
|
95
|
+
class ImageItem:
|
|
96
|
+
"""保存页面图片载荷、几何和绘制来源。"""
|
|
97
|
+
|
|
98
|
+
bbox: BBox
|
|
99
|
+
image_base64: str | None
|
|
100
|
+
paint_order: int
|
|
101
|
+
object_id: int | None
|
|
102
|
+
layer_type: str
|
|
103
|
+
template_id: int | None
|
|
104
|
+
diagnostic: str | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(slots=True)
|
|
108
|
+
class OfdPageScene:
|
|
109
|
+
"""保存一页 OFD 的原生场景和可投影对象。"""
|
|
110
|
+
|
|
111
|
+
page_idx: int
|
|
112
|
+
physical_box: BBox
|
|
113
|
+
content_box: BBox | None
|
|
114
|
+
text_lines: list[TextLine] = field(default_factory=list)
|
|
115
|
+
axis_lines: list[AxisLine] = field(default_factory=list)
|
|
116
|
+
images: list[ImageItem] = field(default_factory=list)
|
|
117
|
+
diagnostics: list[dict[str, Any]] = field(default_factory=list)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True, slots=True)
|
|
121
|
+
class OfdDocumentRef:
|
|
122
|
+
"""保存 OFD.xml 中一个 DocBody 的入口和元数据。"""
|
|
123
|
+
|
|
124
|
+
document_part: str
|
|
125
|
+
signatures_part: str | None
|
|
126
|
+
metadata: dict[str, str]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True, slots=True)
|
|
130
|
+
class PageRef:
|
|
131
|
+
"""保存 Document.xml 页树中的一个页面引用。"""
|
|
132
|
+
|
|
133
|
+
page_id: int | None
|
|
134
|
+
page_part: str
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True, slots=True)
|
|
138
|
+
class TemplateRef:
|
|
139
|
+
"""保存模板 ID 到模板页面成员的映射。"""
|
|
140
|
+
|
|
141
|
+
template_id: int
|
|
142
|
+
page_part: str
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True, slots=True)
|
|
146
|
+
class PageBuildContext:
|
|
147
|
+
"""保存递归图元构建所需的父级状态。"""
|
|
148
|
+
|
|
149
|
+
transform: Affine
|
|
150
|
+
clip_bbox: BBox
|
|
151
|
+
layer_type: str
|
|
152
|
+
template_id: int | None
|
|
153
|
+
draw_style: dict[str, str] = field(default_factory=dict)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"AxisLine",
|
|
158
|
+
"CompositeResource",
|
|
159
|
+
"FontResource",
|
|
160
|
+
"GlyphItem",
|
|
161
|
+
"ImageItem",
|
|
162
|
+
"MediaResource",
|
|
163
|
+
"OfdDocumentRef",
|
|
164
|
+
"OfdPageScene",
|
|
165
|
+
"PageBuildContext",
|
|
166
|
+
"PageRef",
|
|
167
|
+
"ResourceRegistry",
|
|
168
|
+
"TemplateRef",
|
|
169
|
+
"TextLine",
|
|
170
|
+
]
|