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,307 @@
|
|
|
1
|
+
"""Flash Office 文档的图片识别、转码与占位图生成。"""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import struct
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from pathlib import PurePosixPath
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from metafile_render import MetafileError, MetafileResourceLimitError, render_metafile
|
|
12
|
+
from PIL import Image, ImageDraw, ImageFont, UnidentifiedImageError
|
|
13
|
+
|
|
14
|
+
from docvortex.foundation.image_encoding import image_to_b64str
|
|
15
|
+
|
|
16
|
+
VECTOR_IMAGE_FORMATS = frozenset({"WMF", "EMF"})
|
|
17
|
+
VECTOR_IMAGE_EXTENSIONS = frozenset({".wmf", ".emf"})
|
|
18
|
+
VECTOR_IMAGE_CONTENT_TYPES = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"image/x-wmf",
|
|
21
|
+
"image/wmf",
|
|
22
|
+
"image/x-emf",
|
|
23
|
+
"image/emf",
|
|
24
|
+
"application/x-msmetafile",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
PIL_IMAGE_LOAD_ERRORS = (UnidentifiedImageError, OSError, SyntaxError)
|
|
28
|
+
VECTOR_IMAGE_RENDER_DPI: Final = 200
|
|
29
|
+
STANDARD_VECTOR_PLACEHOLDER_SIZE: Final = (320, 180)
|
|
30
|
+
STANDARD_VECTOR_PLACEHOLDER_LINES: Final = (
|
|
31
|
+
"WMF/EMF placeholder",
|
|
32
|
+
"Metafile preview unavailable",
|
|
33
|
+
"See the original document",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_wmf_payload(image_data: bytes) -> bool:
|
|
38
|
+
"""根据 placeable 或标准 METAHEADER magic 判断原始载荷是否为 WMF。"""
|
|
39
|
+
|
|
40
|
+
return image_data.startswith(b"\xd7\xcd\xc6\x9a") or (
|
|
41
|
+
len(image_data) >= 4 and image_data[:2] in {b"\x01\x00", b"\x02\x00"} and image_data[2:4] == b"\x09\x00"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_emf_payload(image_data: bytes) -> bool:
|
|
46
|
+
"""根据 EMR_HEADER type 与 ENHMETA_SIGNATURE 判断载荷是否为 EMF。"""
|
|
47
|
+
return len(image_data) >= 44 and struct.unpack_from("<I", image_data, 0)[0] == 1 and image_data[40:44] == b" EMF"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_vector_image(pil_image: Image.Image) -> bool:
|
|
51
|
+
"""判断已由 Pillow 打开的图片是否属于 WMF/EMF 矢量格式。"""
|
|
52
|
+
return (getattr(pil_image, "format", None) or "").upper() in VECTOR_IMAGE_FORMATS
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_vector_image_part(part_name: object | None = None, content_type: str | None = None) -> bool:
|
|
56
|
+
"""根据 OOXML 部件扩展名和内容类型判断是否为矢量图片。"""
|
|
57
|
+
suffix = PurePosixPath(str(part_name or "")).suffix.lower()
|
|
58
|
+
if suffix in VECTOR_IMAGE_EXTENSIONS:
|
|
59
|
+
return True
|
|
60
|
+
normalized_content_type = (content_type or "").split(";", 1)[0].strip().lower()
|
|
61
|
+
return normalized_content_type in VECTOR_IMAGE_CONTENT_TYPES
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_valid_vector_image_payload(
|
|
65
|
+
image_data: bytes,
|
|
66
|
+
*,
|
|
67
|
+
part_name: object | None = None,
|
|
68
|
+
content_type: str | None = None,
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""校验 WMF/EMF 最小文件签名,避免为任意伪装字节生成矢量占位图。"""
|
|
71
|
+
label = _vector_image_format_label(part_name, content_type)
|
|
72
|
+
if label == "WMF":
|
|
73
|
+
return _is_wmf_payload(image_data)
|
|
74
|
+
if label == "EMF":
|
|
75
|
+
return _is_emf_payload(image_data)
|
|
76
|
+
return _is_wmf_payload(image_data) or _is_emf_payload(image_data)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _vector_image_format_label(part_name: object | None = None, content_type: str | None = None) -> str:
|
|
80
|
+
"""从 OOXML 部件信息推断用于日志和占位图的矢量格式名称。"""
|
|
81
|
+
suffix = PurePosixPath(str(part_name or "")).suffix.lower()
|
|
82
|
+
normalized_content_type = (content_type or "").lower()
|
|
83
|
+
if suffix == ".wmf" or "wmf" in normalized_content_type:
|
|
84
|
+
return "WMF"
|
|
85
|
+
if suffix == ".emf" or "emf" in normalized_content_type:
|
|
86
|
+
return "EMF"
|
|
87
|
+
return "WMF/EMF"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _load_placeholder_font(font_size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
|
91
|
+
"""按优先级加载占位图字体,均不可用时退回 Pillow 默认字体。"""
|
|
92
|
+
for font_name in ("DejaVuSans.ttf", "Arial.ttf", "LiberationSans-Regular.ttf"):
|
|
93
|
+
try:
|
|
94
|
+
return ImageFont.truetype(font_name, font_size)
|
|
95
|
+
except OSError:
|
|
96
|
+
continue
|
|
97
|
+
return ImageFont.load_default()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def create_text_placeholder(size: tuple[int, int], lines: list[str]) -> Image.Image:
|
|
101
|
+
"""按目标尺寸绘制带边框和居中文案的 RGB 占位图。"""
|
|
102
|
+
width = max(int(size[0]), 1)
|
|
103
|
+
height = max(int(size[1]), 1)
|
|
104
|
+
placeholder = Image.new("RGB", (width, height), (240, 240, 240))
|
|
105
|
+
draw = ImageDraw.Draw(placeholder)
|
|
106
|
+
|
|
107
|
+
border_width = max(1, min(width, height) // 80)
|
|
108
|
+
draw.rectangle(
|
|
109
|
+
(0, 0, width - 1, height - 1),
|
|
110
|
+
outline=(190, 190, 190),
|
|
111
|
+
width=border_width,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
max_text_width = max(width - 16, 1)
|
|
115
|
+
max_text_height = max(height - 16, 1)
|
|
116
|
+
fallback_text = "WMF/EMF"
|
|
117
|
+
text = "\n".join(line for line in lines if line)
|
|
118
|
+
if not text:
|
|
119
|
+
text = fallback_text
|
|
120
|
+
|
|
121
|
+
font = None
|
|
122
|
+
spacing = 4
|
|
123
|
+
bbox = None
|
|
124
|
+
for font_size in range(max(min(width, height) // 7, 10), 7, -1):
|
|
125
|
+
font = _load_placeholder_font(font_size)
|
|
126
|
+
spacing = max(2, font_size // 4)
|
|
127
|
+
bbox = draw.multiline_textbbox((0, 0), text, font=font, spacing=spacing, align="center")
|
|
128
|
+
text_width = bbox[2] - bbox[0]
|
|
129
|
+
text_height = bbox[3] - bbox[1]
|
|
130
|
+
if text_width <= max_text_width and text_height <= max_text_height:
|
|
131
|
+
break
|
|
132
|
+
else:
|
|
133
|
+
text = fallback_text
|
|
134
|
+
font = _load_placeholder_font(max(min(width, height) // 5, 10))
|
|
135
|
+
spacing = 2
|
|
136
|
+
bbox = draw.multiline_textbbox((0, 0), text, font=font, spacing=spacing, align="center")
|
|
137
|
+
|
|
138
|
+
text_width = bbox[2] - bbox[0]
|
|
139
|
+
text_height = bbox[3] - bbox[1]
|
|
140
|
+
origin = ((width - text_width) / 2, (height - text_height) / 2)
|
|
141
|
+
draw.multiline_text(
|
|
142
|
+
origin,
|
|
143
|
+
text,
|
|
144
|
+
fill=(90, 90, 90),
|
|
145
|
+
font=font,
|
|
146
|
+
spacing=spacing,
|
|
147
|
+
align="center",
|
|
148
|
+
)
|
|
149
|
+
return placeholder
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@lru_cache(maxsize=1)
|
|
153
|
+
def _standard_vector_placeholder_data_uri() -> str:
|
|
154
|
+
"""生成并缓存标准 WMF/EMF 占位图,避免每张矢量图重复绘制。"""
|
|
155
|
+
placeholder = create_text_placeholder(
|
|
156
|
+
STANDARD_VECTOR_PLACEHOLDER_SIZE,
|
|
157
|
+
list(STANDARD_VECTOR_PLACEHOLDER_LINES),
|
|
158
|
+
)
|
|
159
|
+
return image_to_b64str(placeholder, image_format="JPEG")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_standard_vector_placeholder_data_uri() -> str:
|
|
163
|
+
"""返回标准 WMF/EMF 占位图 data URI,供 Office 各格式复用。"""
|
|
164
|
+
return _standard_vector_placeholder_data_uri()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def serialize_vector_part_with_placeholder(
|
|
168
|
+
part_name: object | None = None,
|
|
169
|
+
content_type: str | None = None,
|
|
170
|
+
size: tuple[int, int] = (320, 180),
|
|
171
|
+
) -> str:
|
|
172
|
+
"""跳过未加载的矢量部件并返回可嵌入文档的标准占位图。"""
|
|
173
|
+
image_format = _vector_image_format_label(part_name, content_type)
|
|
174
|
+
logger.debug(
|
|
175
|
+
f"Skipping {image_format} image part before Pillow load, "
|
|
176
|
+
f"part_name={part_name}, content_type={content_type}, requested_size={size}"
|
|
177
|
+
)
|
|
178
|
+
return get_standard_vector_placeholder_data_uri()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _render_size_from_emu(render_size_emu: tuple[int, int] | None, *, dpi: int) -> tuple[int, int] | None:
|
|
182
|
+
"""把 OfficeArt ptSize 的 EMU 尺寸按指定 DPI 转换为像素提示。"""
|
|
183
|
+
if render_size_emu is None or render_size_emu[0] <= 0 or render_size_emu[1] <= 0:
|
|
184
|
+
return None
|
|
185
|
+
emu_per_inch = 914_400
|
|
186
|
+
return (
|
|
187
|
+
max(1, round(render_size_emu[0] * dpi / emu_per_inch)),
|
|
188
|
+
max(1, round(render_size_emu[1] * dpi / emu_per_inch)),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _serialize_metafile(
|
|
193
|
+
image_data: bytes,
|
|
194
|
+
image_format: str,
|
|
195
|
+
*,
|
|
196
|
+
size_hint: tuple[int, int] | None = None,
|
|
197
|
+
) -> str | None:
|
|
198
|
+
"""调用 metafile-render WMF/EMF 引擎并生成带 PNG fallback 的 SVG。"""
|
|
199
|
+
try:
|
|
200
|
+
rendered = render_metafile(
|
|
201
|
+
image_data,
|
|
202
|
+
output_format="svg",
|
|
203
|
+
dpi=VECTOR_IMAGE_RENDER_DPI,
|
|
204
|
+
size_hint=size_hint,
|
|
205
|
+
backend="auto",
|
|
206
|
+
)
|
|
207
|
+
except MetafileResourceLimitError as exc:
|
|
208
|
+
logger.warning(f"Generated {image_format} SVG cannot be consumed safely: {exc}. Falling back to PNG.")
|
|
209
|
+
try:
|
|
210
|
+
rendered = render_metafile(
|
|
211
|
+
image_data,
|
|
212
|
+
output_format="png",
|
|
213
|
+
dpi=VECTOR_IMAGE_RENDER_DPI,
|
|
214
|
+
size_hint=size_hint,
|
|
215
|
+
backend="auto",
|
|
216
|
+
)
|
|
217
|
+
except MetafileError as png_exc:
|
|
218
|
+
logger.warning(
|
|
219
|
+
f"Failed to render {image_format} PNG fallback with metafile-render: {png_exc}. Using placeholder instead."
|
|
220
|
+
)
|
|
221
|
+
return None
|
|
222
|
+
except MetafileError as exc:
|
|
223
|
+
logger.warning(f"Failed to render {image_format} image with metafile-render: {exc}. Using placeholder instead.")
|
|
224
|
+
return None
|
|
225
|
+
if rendered.partial:
|
|
226
|
+
diagnostic_codes = sorted({diagnostic.code for diagnostic in rendered.diagnostics})
|
|
227
|
+
logger.warning(
|
|
228
|
+
f"Rendered partial {image_format} image with metafile-render, diagnostics={diagnostic_codes[:16]}, "
|
|
229
|
+
f"size={rendered.width}x{rendered.height}"
|
|
230
|
+
)
|
|
231
|
+
encoded = base64.b64encode(rendered.data).decode("ascii")
|
|
232
|
+
return f"data:{rendered.media_type};base64,{encoded}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def serialize_office_image(
|
|
236
|
+
image_data: bytes,
|
|
237
|
+
*,
|
|
238
|
+
part_name: object | None = None,
|
|
239
|
+
content_type: str | None = None,
|
|
240
|
+
render_size_emu: tuple[int, int] | None = None,
|
|
241
|
+
) -> str | None:
|
|
242
|
+
"""识别并序列化 Office 图片,透明图保留 PNG,普通位图优先使用 JPEG。"""
|
|
243
|
+
is_wmf_payload = _is_wmf_payload(image_data)
|
|
244
|
+
is_emf_payload = _is_emf_payload(image_data)
|
|
245
|
+
if is_wmf_payload:
|
|
246
|
+
part_name = "image.wmf"
|
|
247
|
+
content_type = "image/wmf"
|
|
248
|
+
elif is_emf_payload:
|
|
249
|
+
part_name = "image.emf"
|
|
250
|
+
content_type = "image/emf"
|
|
251
|
+
if is_wmf_payload or is_emf_payload or is_vector_image_part(part_name, content_type):
|
|
252
|
+
image_format = (
|
|
253
|
+
"WMF" if is_wmf_payload else "EMF" if is_emf_payload else _vector_image_format_label(part_name, content_type)
|
|
254
|
+
)
|
|
255
|
+
if not (is_wmf_payload or is_emf_payload):
|
|
256
|
+
logger.warning(
|
|
257
|
+
f"Vector image payload does not match WMF/EMF signature, part_name={part_name}, content_type={content_type}. "
|
|
258
|
+
"Using placeholder instead."
|
|
259
|
+
)
|
|
260
|
+
return serialize_vector_part_with_placeholder(part_name, content_type)
|
|
261
|
+
rendered = _serialize_metafile(
|
|
262
|
+
image_data,
|
|
263
|
+
image_format,
|
|
264
|
+
size_hint=_render_size_from_emu(render_size_emu, dpi=VECTOR_IMAGE_RENDER_DPI),
|
|
265
|
+
)
|
|
266
|
+
if rendered is not None:
|
|
267
|
+
return rendered
|
|
268
|
+
return serialize_vector_part_with_placeholder(part_name, content_type)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
pil_image = Image.open(BytesIO(image_data))
|
|
272
|
+
if is_vector_image(pil_image):
|
|
273
|
+
rendered = _serialize_metafile(
|
|
274
|
+
image_data,
|
|
275
|
+
str(pil_image.format),
|
|
276
|
+
size_hint=_render_size_from_emu(render_size_emu, dpi=VECTOR_IMAGE_RENDER_DPI),
|
|
277
|
+
)
|
|
278
|
+
return rendered if rendered is not None else get_standard_vector_placeholder_data_uri()
|
|
279
|
+
pil_image.load()
|
|
280
|
+
except PIL_IMAGE_LOAD_ERRORS as e:
|
|
281
|
+
logger.warning(f"Warning: image cannot be loaded by Pillow: {e}, part_name={part_name}, content_type={content_type}")
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
if pil_image.mode == "RGB":
|
|
285
|
+
return image_to_b64str(pil_image, image_format="JPEG")
|
|
286
|
+
|
|
287
|
+
if pil_image.mode in {"RGBA", "LA"} or (pil_image.mode == "P" and "transparency" in pil_image.info):
|
|
288
|
+
return image_to_b64str(pil_image.convert("RGBA"), image_format="PNG")
|
|
289
|
+
|
|
290
|
+
return image_to_b64str(pil_image.convert("RGB"), image_format="JPEG")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def ensure_bmp_header(image_data: bytes) -> bytes:
|
|
294
|
+
"""为裸 DIB 补齐 BMP 文件头,无法可靠推断时保留原始载荷。"""
|
|
295
|
+
if image_data.startswith(b"BM") or len(image_data) < 4:
|
|
296
|
+
return image_data
|
|
297
|
+
header_size = int(struct.unpack_from("<I", image_data, 0)[0])
|
|
298
|
+
if header_size < 12 or header_size > len(image_data):
|
|
299
|
+
return image_data
|
|
300
|
+
palette_bytes = 0
|
|
301
|
+
if header_size >= 40 and len(image_data) >= 36:
|
|
302
|
+
bit_count = int(struct.unpack_from("<H", image_data, 14)[0])
|
|
303
|
+
colors_used = int(struct.unpack_from("<I", image_data, 32)[0])
|
|
304
|
+
colors = colors_used or ((1 << bit_count) if bit_count <= 8 else 0)
|
|
305
|
+
palette_bytes = colors * 4
|
|
306
|
+
pixel_offset = min(14 + header_size + palette_bytes, 14 + len(image_data))
|
|
307
|
+
return b"BM" + struct.pack("<IHHI", 14 + len(image_data), 0, 0, pixel_offset) + image_data
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""旧版 Office 二进制格式复用的有界小端读取能力。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_u16(data: bytes, offset: int) -> int | None:
|
|
9
|
+
"""有界读取小端无符号 16 位整数。"""
|
|
10
|
+
if offset < 0 or offset + 2 > len(data):
|
|
11
|
+
return None
|
|
12
|
+
return int(struct.unpack_from("<H", data, offset)[0])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_i16(data: bytes, offset: int) -> int | None:
|
|
16
|
+
"""有界读取小端有符号 16 位整数。"""
|
|
17
|
+
if offset < 0 or offset + 2 > len(data):
|
|
18
|
+
return None
|
|
19
|
+
return int(struct.unpack_from("<h", data, offset)[0])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_u32(data: bytes, offset: int) -> int | None:
|
|
23
|
+
"""有界读取小端无符号 32 位整数。"""
|
|
24
|
+
if offset < 0 or offset + 4 > len(data):
|
|
25
|
+
return None
|
|
26
|
+
return int(struct.unpack_from("<I", data, offset)[0])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_f64(data: bytes, offset: int) -> float | None:
|
|
30
|
+
"""有界读取小端 IEEE-754 双精度浮点数。"""
|
|
31
|
+
if offset < 0 or offset + 8 > len(data):
|
|
32
|
+
return None
|
|
33
|
+
return float(struct.unpack_from("<d", data, offset)[0])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def bounded_slice(data: bytes, offset: int, size: int) -> bytes | None:
|
|
37
|
+
"""返回不越界且不允许负偏移或负长度的字节范围。"""
|
|
38
|
+
if offset < 0 or size < 0 or offset > len(data) - size:
|
|
39
|
+
return None
|
|
40
|
+
return data[offset : offset + size]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["bounded_slice", "get_f64", "get_i16", "get_u16", "get_u32"]
|