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,885 @@
|
|
|
1
|
+
"""安全解析 Equation Native 中的 MTEF v3/v5 公式。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import re
|
|
7
|
+
import struct
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from ..errors import LegacyOfficeResourceLimitError
|
|
12
|
+
from ..limits import MAX_RECORD_DEPTH, MAX_RECORDS
|
|
13
|
+
from ..legacy.ole import BoundedOleReader
|
|
14
|
+
|
|
15
|
+
_OBJECT_POOL_EQUATION_RE = re.compile(
|
|
16
|
+
r"^ObjectPool/_([0-9]+)/Equation Native$",
|
|
17
|
+
re.IGNORECASE,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_XF_NULL = 0x10
|
|
21
|
+
_XF_RULER = 0x20
|
|
22
|
+
_XF_EMBELL = 0x20
|
|
23
|
+
_XF_LSPACE = 0x40
|
|
24
|
+
_XF_LMOVE = 0x80
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _MtefError(ValueError):
|
|
28
|
+
"""MTEF 输入不完整、不受支持或超过安全限制。"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class _Node:
|
|
33
|
+
"""MTEF 解析后的最小公式语法节点。"""
|
|
34
|
+
|
|
35
|
+
kind: str
|
|
36
|
+
value: object | None = None
|
|
37
|
+
children: tuple[_Node, ...] = ()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _MtefReader:
|
|
41
|
+
"""有界读取 MTEF v3 record tree。"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, data: bytes) -> None:
|
|
44
|
+
"""校验五字节 MTEF v3 头并初始化游标。"""
|
|
45
|
+
|
|
46
|
+
if len(data) < 5 or data[0] != 3:
|
|
47
|
+
raise _MtefError("Equation Native does not contain MTEF v3")
|
|
48
|
+
self.data = data
|
|
49
|
+
self.pos = 5
|
|
50
|
+
self.depth = 0
|
|
51
|
+
self.records = 0
|
|
52
|
+
|
|
53
|
+
def _charge(self) -> None:
|
|
54
|
+
"""计入一条 record,超过共享上限时拒绝对象。"""
|
|
55
|
+
|
|
56
|
+
self.records += 1
|
|
57
|
+
if self.records > MAX_RECORDS:
|
|
58
|
+
raise LegacyOfficeResourceLimitError(f"MTEF record count exceeds max_records={MAX_RECORDS}")
|
|
59
|
+
|
|
60
|
+
def _u8(self) -> int:
|
|
61
|
+
"""有界读取一个无符号字节。"""
|
|
62
|
+
|
|
63
|
+
if self.pos >= len(self.data):
|
|
64
|
+
raise _MtefError("MTEF record is truncated")
|
|
65
|
+
value = self.data[self.pos]
|
|
66
|
+
self.pos += 1
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
def _u16(self) -> int:
|
|
70
|
+
"""有界读取小端 u16。"""
|
|
71
|
+
|
|
72
|
+
if self.pos + 2 > len(self.data):
|
|
73
|
+
raise _MtefError("MTEF u16 is truncated")
|
|
74
|
+
value = int(struct.unpack_from("<H", self.data, self.pos)[0])
|
|
75
|
+
self.pos += 2
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
def _skip_nudge(self) -> None:
|
|
79
|
+
"""跳过短或长格式的 nudge 偏移。"""
|
|
80
|
+
|
|
81
|
+
dx = self._u8()
|
|
82
|
+
dy = self._u8()
|
|
83
|
+
if dx == 128 and dy == 128:
|
|
84
|
+
self._u16()
|
|
85
|
+
self._u16()
|
|
86
|
+
|
|
87
|
+
def _enter(self) -> None:
|
|
88
|
+
"""进入一个嵌套 object list 并限制深度。"""
|
|
89
|
+
|
|
90
|
+
self.depth += 1
|
|
91
|
+
if self.depth > MAX_RECORD_DEPTH:
|
|
92
|
+
raise LegacyOfficeResourceLimitError(f"MTEF nesting exceeds max_record_depth={MAX_RECORD_DEPTH}")
|
|
93
|
+
|
|
94
|
+
def _leave(self) -> None:
|
|
95
|
+
"""离开一个嵌套 object list。"""
|
|
96
|
+
|
|
97
|
+
self.depth -= 1
|
|
98
|
+
|
|
99
|
+
def _skip_ruler(self) -> None:
|
|
100
|
+
"""跳过不影响公式语义的 RULER record。"""
|
|
101
|
+
|
|
102
|
+
count = self._u8()
|
|
103
|
+
for _ in range(count):
|
|
104
|
+
self._u8()
|
|
105
|
+
self._u16()
|
|
106
|
+
|
|
107
|
+
def _skip_font(self) -> None:
|
|
108
|
+
"""跳过 FONT 的 typeface、style 和零结尾名称。"""
|
|
109
|
+
|
|
110
|
+
self._u8()
|
|
111
|
+
self._u8()
|
|
112
|
+
while self._u8() != 0:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def _skip_size(self) -> None:
|
|
116
|
+
"""跳过三种长度形式的 SIZE record。"""
|
|
117
|
+
|
|
118
|
+
first = self._u8()
|
|
119
|
+
if first == 100:
|
|
120
|
+
self._u8()
|
|
121
|
+
self._u16()
|
|
122
|
+
elif first == 101:
|
|
123
|
+
self._u16()
|
|
124
|
+
else:
|
|
125
|
+
self._u8()
|
|
126
|
+
|
|
127
|
+
def _parse_embellishments(self) -> tuple[int, ...]:
|
|
128
|
+
"""读取 CHAR 后以 END 终止的 embellishment 列表。"""
|
|
129
|
+
|
|
130
|
+
values: list[int] = []
|
|
131
|
+
while True:
|
|
132
|
+
tag = self._u8()
|
|
133
|
+
self._charge()
|
|
134
|
+
record_type = tag & 0x0F
|
|
135
|
+
flags = tag & 0xF0
|
|
136
|
+
if record_type == 0:
|
|
137
|
+
return tuple(values)
|
|
138
|
+
if record_type != 6:
|
|
139
|
+
raise _MtefError("MTEF embellishment list contains a non-EMBELL record")
|
|
140
|
+
if flags & _XF_LMOVE:
|
|
141
|
+
self._skip_nudge()
|
|
142
|
+
values.append(self._u8())
|
|
143
|
+
|
|
144
|
+
def _parse_line(self, flags: int) -> _Node:
|
|
145
|
+
"""解析一个 LINE slot。"""
|
|
146
|
+
|
|
147
|
+
if flags & _XF_NULL:
|
|
148
|
+
return _Node("sequence")
|
|
149
|
+
if flags & _XF_LMOVE:
|
|
150
|
+
self._skip_nudge()
|
|
151
|
+
if flags & _XF_LSPACE:
|
|
152
|
+
self._u8()
|
|
153
|
+
if flags & _XF_RULER:
|
|
154
|
+
ruler_tag = self._u8()
|
|
155
|
+
if ruler_tag & 0x0F != 7:
|
|
156
|
+
raise _MtefError("MTEF LINE ruler tag is invalid")
|
|
157
|
+
self._skip_ruler()
|
|
158
|
+
self._enter()
|
|
159
|
+
children = self._parse_list()
|
|
160
|
+
self._leave()
|
|
161
|
+
return _Node("sequence", children=children)
|
|
162
|
+
|
|
163
|
+
def _parse_template(self, flags: int) -> _Node:
|
|
164
|
+
"""解析 TMPL selector、variation、options 和 slots。"""
|
|
165
|
+
|
|
166
|
+
if flags & _XF_LMOVE:
|
|
167
|
+
self._skip_nudge()
|
|
168
|
+
selector = self._u8()
|
|
169
|
+
variation = self._u8()
|
|
170
|
+
options = self._u8()
|
|
171
|
+
self._enter()
|
|
172
|
+
slots = self._parse_slots()
|
|
173
|
+
self._leave()
|
|
174
|
+
return _Node("template", (selector, variation, options), slots)
|
|
175
|
+
|
|
176
|
+
def _parse_pile(self, flags: int) -> _Node:
|
|
177
|
+
"""解析纵向 PILE 中的 LINE slots。"""
|
|
178
|
+
|
|
179
|
+
if flags & _XF_LMOVE:
|
|
180
|
+
self._skip_nudge()
|
|
181
|
+
self._u8()
|
|
182
|
+
self._u8()
|
|
183
|
+
if flags & _XF_RULER:
|
|
184
|
+
ruler_tag = self._u8()
|
|
185
|
+
if ruler_tag & 0x0F != 7:
|
|
186
|
+
raise _MtefError("MTEF PILE ruler tag is invalid")
|
|
187
|
+
self._skip_ruler()
|
|
188
|
+
self._enter()
|
|
189
|
+
slots = self._parse_slots()
|
|
190
|
+
self._leave()
|
|
191
|
+
return _Node("pile", children=slots)
|
|
192
|
+
|
|
193
|
+
def _parse_matrix(self, flags: int) -> _Node:
|
|
194
|
+
"""解析 MATRIX 维度、分隔线位图和逐格 LINE。"""
|
|
195
|
+
|
|
196
|
+
if flags & _XF_LMOVE:
|
|
197
|
+
self._skip_nudge()
|
|
198
|
+
self._u8()
|
|
199
|
+
self._u8()
|
|
200
|
+
self._u8()
|
|
201
|
+
rows = self._u8()
|
|
202
|
+
cols = self._u8()
|
|
203
|
+
if rows == 0 or cols == 0 or rows * cols > 4096:
|
|
204
|
+
raise _MtefError("MTEF matrix dimensions are invalid")
|
|
205
|
+
for entry_count in (rows + 1, cols + 1):
|
|
206
|
+
for _ in range((2 * entry_count + 7) // 8):
|
|
207
|
+
self._u8()
|
|
208
|
+
cells: list[_Node] = []
|
|
209
|
+
self._enter()
|
|
210
|
+
while len(cells) < rows * cols:
|
|
211
|
+
tag = self._u8()
|
|
212
|
+
self._charge()
|
|
213
|
+
record_type = tag & 0x0F
|
|
214
|
+
flags = tag & 0xF0
|
|
215
|
+
if record_type == 0:
|
|
216
|
+
continue
|
|
217
|
+
if record_type == 1:
|
|
218
|
+
cells.append(self._parse_line(flags))
|
|
219
|
+
elif record_type == 7:
|
|
220
|
+
self._skip_ruler()
|
|
221
|
+
elif record_type == 8:
|
|
222
|
+
self._skip_font()
|
|
223
|
+
elif record_type == 9:
|
|
224
|
+
self._skip_size()
|
|
225
|
+
elif 10 <= record_type <= 14:
|
|
226
|
+
continue
|
|
227
|
+
else:
|
|
228
|
+
cells.append(self._parse_record(record_type, flags))
|
|
229
|
+
self._leave()
|
|
230
|
+
if self.pos < len(self.data) and self.data[self.pos] & 0x0F == 0:
|
|
231
|
+
self.pos += 1
|
|
232
|
+
self._charge()
|
|
233
|
+
return _Node("matrix", (rows, cols), tuple(cells))
|
|
234
|
+
|
|
235
|
+
def _parse_record(self, record_type: int, flags: int) -> _Node:
|
|
236
|
+
"""解析一条已读取 tag 的语义 record。"""
|
|
237
|
+
|
|
238
|
+
if record_type == 1:
|
|
239
|
+
return self._parse_line(flags)
|
|
240
|
+
if record_type == 2:
|
|
241
|
+
if flags & _XF_LMOVE:
|
|
242
|
+
self._skip_nudge()
|
|
243
|
+
typeface = self._u8()
|
|
244
|
+
character = self._u16()
|
|
245
|
+
embellishments = self._parse_embellishments() if flags & _XF_EMBELL else ()
|
|
246
|
+
return _Node("character", (typeface, character, embellishments))
|
|
247
|
+
if record_type == 3:
|
|
248
|
+
return self._parse_template(flags)
|
|
249
|
+
if record_type == 4:
|
|
250
|
+
return self._parse_pile(flags)
|
|
251
|
+
if record_type == 5:
|
|
252
|
+
return self._parse_matrix(flags)
|
|
253
|
+
if record_type == 6:
|
|
254
|
+
if flags & _XF_LMOVE:
|
|
255
|
+
self._skip_nudge()
|
|
256
|
+
return _Node("embellishment", self._u8())
|
|
257
|
+
if record_type == 7:
|
|
258
|
+
self._skip_ruler()
|
|
259
|
+
return _Node("metadata")
|
|
260
|
+
if record_type == 8:
|
|
261
|
+
self._skip_font()
|
|
262
|
+
return _Node("metadata")
|
|
263
|
+
if record_type == 9:
|
|
264
|
+
self._skip_size()
|
|
265
|
+
return _Node("metadata")
|
|
266
|
+
if 10 <= record_type <= 14:
|
|
267
|
+
return _Node("metadata")
|
|
268
|
+
raise _MtefError(f"unsupported MTEF v3 record type: {record_type}")
|
|
269
|
+
|
|
270
|
+
def _parse_list(self) -> tuple[_Node, ...]:
|
|
271
|
+
"""解析以 END 终止的普通 object list。"""
|
|
272
|
+
|
|
273
|
+
nodes: list[_Node] = []
|
|
274
|
+
while True:
|
|
275
|
+
tag = self._u8()
|
|
276
|
+
self._charge()
|
|
277
|
+
record_type = tag & 0x0F
|
|
278
|
+
if record_type == 0:
|
|
279
|
+
return tuple(nodes)
|
|
280
|
+
node = self._parse_record(record_type, tag & 0xF0)
|
|
281
|
+
if node.kind != "metadata":
|
|
282
|
+
nodes.append(node)
|
|
283
|
+
|
|
284
|
+
def _parse_slots(self) -> tuple[_Node, ...]:
|
|
285
|
+
"""解析 template/pile 中每个 LINE 对应的 slot。"""
|
|
286
|
+
|
|
287
|
+
slots: list[_Node] = []
|
|
288
|
+
while True:
|
|
289
|
+
tag = self._u8()
|
|
290
|
+
self._charge()
|
|
291
|
+
record_type = tag & 0x0F
|
|
292
|
+
flags = tag & 0xF0
|
|
293
|
+
if record_type == 0:
|
|
294
|
+
return tuple(slots)
|
|
295
|
+
if record_type == 1:
|
|
296
|
+
slots.append(self._parse_line(flags))
|
|
297
|
+
elif record_type == 7:
|
|
298
|
+
self._skip_ruler()
|
|
299
|
+
elif record_type == 8:
|
|
300
|
+
self._skip_font()
|
|
301
|
+
elif record_type == 9:
|
|
302
|
+
self._skip_size()
|
|
303
|
+
elif 10 <= record_type <= 14:
|
|
304
|
+
continue
|
|
305
|
+
else:
|
|
306
|
+
slots.append(self._parse_record(record_type, flags))
|
|
307
|
+
|
|
308
|
+
def parse(self) -> _Node:
|
|
309
|
+
"""解析根 object list 并拒绝没有可见内容的对象。"""
|
|
310
|
+
|
|
311
|
+
children = self._parse_list()
|
|
312
|
+
root = _Node("sequence", children=children)
|
|
313
|
+
if not _render_node(root).strip():
|
|
314
|
+
raise _MtefError("MTEF formula contains no visible content")
|
|
315
|
+
return root
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
_CHAR_LATEX = {
|
|
319
|
+
"α": r"\alpha ",
|
|
320
|
+
"β": r"\beta ",
|
|
321
|
+
"γ": r"\gamma ",
|
|
322
|
+
"δ": r"\delta ",
|
|
323
|
+
"ε": r"\epsilon ",
|
|
324
|
+
"θ": r"\theta ",
|
|
325
|
+
"λ": r"\lambda ",
|
|
326
|
+
"μ": r"\mu ",
|
|
327
|
+
"π": r"\pi ",
|
|
328
|
+
"ρ": r"\rho ",
|
|
329
|
+
"σ": r"\sigma ",
|
|
330
|
+
"φ": r"\phi ",
|
|
331
|
+
"ω": r"\omega ",
|
|
332
|
+
"Γ": r"\Gamma ",
|
|
333
|
+
"Δ": r"\Delta ",
|
|
334
|
+
"Θ": r"\Theta ",
|
|
335
|
+
"Λ": r"\Lambda ",
|
|
336
|
+
"Ξ": r"\Xi ",
|
|
337
|
+
"Π": r"\Pi ",
|
|
338
|
+
"Σ": r"\Sigma ",
|
|
339
|
+
"Φ": r"\Phi ",
|
|
340
|
+
"Ψ": r"\Psi ",
|
|
341
|
+
"Ω": r"\Omega ",
|
|
342
|
+
"≤": r"\leq ",
|
|
343
|
+
"≥": r"\geq ",
|
|
344
|
+
"≠": r"\ne ",
|
|
345
|
+
"±": r"\pm ",
|
|
346
|
+
"∓": r"\mp ",
|
|
347
|
+
"−": "-",
|
|
348
|
+
"×": r"\times ",
|
|
349
|
+
"÷": r"\div ",
|
|
350
|
+
"∞": r"\infty ",
|
|
351
|
+
"∂": r"\partial ",
|
|
352
|
+
"∈": r"\in ",
|
|
353
|
+
"∉": r"\notin ",
|
|
354
|
+
"∪": r"\cup ",
|
|
355
|
+
"∩": r"\cap ",
|
|
356
|
+
"→": r"\rightarrow ",
|
|
357
|
+
"←": r"\leftarrow ",
|
|
358
|
+
"↔": r"\leftrightarrow ",
|
|
359
|
+
"·": r"\cdot ",
|
|
360
|
+
"∑": r"\sum ",
|
|
361
|
+
"∏": r"\prod ",
|
|
362
|
+
"∐": r"\coprod ",
|
|
363
|
+
"∫": r"\int ",
|
|
364
|
+
"∮": r"\oint ",
|
|
365
|
+
"ℏ": r"\hbar ",
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _render_character(character: int) -> str:
|
|
370
|
+
"""把 MTEF 字符转为安全 LaTeX。"""
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
value = chr(character)
|
|
374
|
+
except ValueError as exc:
|
|
375
|
+
raise _MtefError("MTEF character code is invalid") from exc
|
|
376
|
+
mapped = _CHAR_LATEX.get(value)
|
|
377
|
+
if mapped is not None:
|
|
378
|
+
return mapped
|
|
379
|
+
if value == "\\":
|
|
380
|
+
return r"\backslash "
|
|
381
|
+
if value in "#$%&_{}":
|
|
382
|
+
return f"\\{value}"
|
|
383
|
+
if value == "~":
|
|
384
|
+
return r"\sim "
|
|
385
|
+
if value == "^":
|
|
386
|
+
return r"\hat{}"
|
|
387
|
+
if ord(value) < 0x20:
|
|
388
|
+
raise _MtefError("MTEF formula contains a control character")
|
|
389
|
+
return value
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _escape_latex_text(value: str) -> str:
|
|
393
|
+
"""转义 ``\text{}`` 中会改变 LaTeX 结构的字符。"""
|
|
394
|
+
|
|
395
|
+
replacements = {
|
|
396
|
+
"\\": r"\textbackslash{}",
|
|
397
|
+
"{": r"\{",
|
|
398
|
+
"}": r"\}",
|
|
399
|
+
"#": r"\#",
|
|
400
|
+
"$": r"\$",
|
|
401
|
+
"%": r"\%",
|
|
402
|
+
"&": r"\&",
|
|
403
|
+
"_": r"\_",
|
|
404
|
+
"^": r"\textasciicircum{}",
|
|
405
|
+
"~": r"\textasciitilde{}",
|
|
406
|
+
}
|
|
407
|
+
return "".join(replacements.get(character, character) for character in value)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _apply_embellishments(value: str, embellishments: tuple[int, ...]) -> str:
|
|
411
|
+
"""按记录顺序把常见 embellishment 转为 LaTeX。"""
|
|
412
|
+
|
|
413
|
+
for embellishment in embellishments:
|
|
414
|
+
if embellishment == 2:
|
|
415
|
+
value = rf"\dot{{{value}}}"
|
|
416
|
+
elif embellishment == 3:
|
|
417
|
+
value = rf"\ddot{{{value}}}"
|
|
418
|
+
elif embellishment == 4:
|
|
419
|
+
value = rf"\overset{{\ldots}}{{{value}}}"
|
|
420
|
+
elif embellishment == 5:
|
|
421
|
+
value = f"{value}'"
|
|
422
|
+
elif embellishment == 6:
|
|
423
|
+
value = f"{value}''"
|
|
424
|
+
elif embellishment == 7:
|
|
425
|
+
value = f"{{}}'{value}"
|
|
426
|
+
elif embellishment == 8:
|
|
427
|
+
value = rf"\widetilde{{{value}}}"
|
|
428
|
+
elif embellishment == 9:
|
|
429
|
+
value = rf"\widehat{{{value}}}"
|
|
430
|
+
elif embellishment == 10:
|
|
431
|
+
value = rf"\not{{{value}}}"
|
|
432
|
+
elif embellishment == 11:
|
|
433
|
+
value = rf"\overrightarrow{{{value}}}"
|
|
434
|
+
elif embellishment == 12:
|
|
435
|
+
value = rf"\overleftarrow{{{value}}}"
|
|
436
|
+
elif embellishment == 13:
|
|
437
|
+
value = rf"\overleftrightarrow{{{value}}}"
|
|
438
|
+
elif embellishment == 14:
|
|
439
|
+
value = rf"\overrightharpoon{{{value}}}"
|
|
440
|
+
elif embellishment == 15:
|
|
441
|
+
value = rf"\overleftharpoon{{{value}}}"
|
|
442
|
+
elif embellishment == 16:
|
|
443
|
+
value = rf"\bar{{{value}}}"
|
|
444
|
+
elif embellishment == 17:
|
|
445
|
+
value = rf"\overline{{{value}}}"
|
|
446
|
+
elif embellishment == 18:
|
|
447
|
+
value = f"{value}'''"
|
|
448
|
+
elif embellishment == 19:
|
|
449
|
+
value = rf"\overparen{{{value}}}"
|
|
450
|
+
elif embellishment == 20:
|
|
451
|
+
value = rf"\underparen{{{value}}}"
|
|
452
|
+
elif embellishment == 21:
|
|
453
|
+
value = rf"\xcancel{{{value}}}"
|
|
454
|
+
elif embellishment == 22:
|
|
455
|
+
value = rf"\cancel{{{value}}}"
|
|
456
|
+
elif embellishment == 23:
|
|
457
|
+
value = rf"\bcancel{{{value}}}"
|
|
458
|
+
elif embellishment == 24:
|
|
459
|
+
value = rf"\overset{{\cdots}}{{{value}}}"
|
|
460
|
+
elif embellishment == 25:
|
|
461
|
+
value = rf"\underset{{\cdot}}{{{value}}}"
|
|
462
|
+
elif embellishment == 26:
|
|
463
|
+
value = rf"\underset{{\cdot\cdot}}{{{value}}}"
|
|
464
|
+
elif embellishment == 27:
|
|
465
|
+
value = rf"\underset{{\cdots}}{{{value}}}"
|
|
466
|
+
elif embellishment == 28:
|
|
467
|
+
value = rf"\underset{{\cdots\cdot}}{{{value}}}"
|
|
468
|
+
elif embellishment == 29:
|
|
469
|
+
value = rf"\underline{{{value}}}"
|
|
470
|
+
elif embellishment == 30:
|
|
471
|
+
value = rf"\underset{{\sim}}{{{value}}}"
|
|
472
|
+
elif embellishment == 31:
|
|
473
|
+
value = rf"\underparen{{{value}}}"
|
|
474
|
+
elif embellishment == 32:
|
|
475
|
+
value = rf"\underset{{\frown}}{{{value}}}"
|
|
476
|
+
elif embellishment == 33:
|
|
477
|
+
value = rf"\underrightarrow{{{value}}}"
|
|
478
|
+
elif embellishment == 34:
|
|
479
|
+
value = rf"\underleftarrow{{{value}}}"
|
|
480
|
+
elif embellishment == 35:
|
|
481
|
+
value = rf"\underleftrightarrow{{{value}}}"
|
|
482
|
+
elif embellishment == 36:
|
|
483
|
+
value = rf"\underrightharpoon{{{value}}}"
|
|
484
|
+
elif embellishment == 37:
|
|
485
|
+
value = rf"\underleftharpoon{{{value}}}"
|
|
486
|
+
else:
|
|
487
|
+
raise _MtefError(f"unsupported MTEF embellishment: {embellishment}")
|
|
488
|
+
return value
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _slot(slots: tuple[_Node, ...], index: int) -> str:
|
|
492
|
+
"""渲染一个可选 template slot。"""
|
|
493
|
+
|
|
494
|
+
return _render_node(slots[index]).strip() if index < len(slots) else ""
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _fence(selector: int, variation: int, body: str) -> str:
|
|
498
|
+
"""渲染左右 fence 及单边 variation。"""
|
|
499
|
+
|
|
500
|
+
pairs = {
|
|
501
|
+
0: (r"\langle", r"\rangle"),
|
|
502
|
+
1: ("(", ")"),
|
|
503
|
+
2: (r"\{", r"\}"),
|
|
504
|
+
3: ("[", "]"),
|
|
505
|
+
4: ("|", "|"),
|
|
506
|
+
5: (r"\|", r"\|"),
|
|
507
|
+
6: (r"\lfloor", r"\rfloor"),
|
|
508
|
+
7: (r"\lceil", r"\rceil"),
|
|
509
|
+
}
|
|
510
|
+
left, right = pairs[selector]
|
|
511
|
+
if variation == 1:
|
|
512
|
+
right = "."
|
|
513
|
+
elif variation == 2:
|
|
514
|
+
left = "."
|
|
515
|
+
elif variation != 0:
|
|
516
|
+
raise _MtefError("unsupported MTEF fence variation")
|
|
517
|
+
return rf"\left{left}{body}\right{right}"
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _big_operator(selector: int, slots: tuple[_Node, ...]) -> str:
|
|
521
|
+
"""渲染积分、求和、乘积及集合大运算。"""
|
|
522
|
+
|
|
523
|
+
operators = {
|
|
524
|
+
21: r"\int",
|
|
525
|
+
22: r"\iint",
|
|
526
|
+
23: r"\iiint",
|
|
527
|
+
24: r"\int",
|
|
528
|
+
25: r"\iint",
|
|
529
|
+
26: r"\iiint",
|
|
530
|
+
29: r"\sum",
|
|
531
|
+
30: r"\sum",
|
|
532
|
+
31: r"\prod",
|
|
533
|
+
32: r"\prod",
|
|
534
|
+
33: r"\coprod",
|
|
535
|
+
34: r"\coprod",
|
|
536
|
+
35: r"\bigcup",
|
|
537
|
+
36: r"\bigcup",
|
|
538
|
+
37: r"\bigcap",
|
|
539
|
+
38: r"\bigcap",
|
|
540
|
+
}
|
|
541
|
+
operator = operators[selector]
|
|
542
|
+
main = _slot(slots, 0)
|
|
543
|
+
upper = _slot(slots, 1)
|
|
544
|
+
lower = _slot(slots, 2)
|
|
545
|
+
limits = (rf"_{{{lower}}}" if lower else "") + (rf"^{{{upper}}}" if upper else "")
|
|
546
|
+
return rf"{operator}{limits}{{{main}}}"
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _render_template(selector: int, variation: int, options: int, slots: tuple[_Node, ...]) -> str:
|
|
550
|
+
"""把一个受支持的 MTEF v3 template 转为 LaTeX。"""
|
|
551
|
+
|
|
552
|
+
del options
|
|
553
|
+
if 0 <= selector <= 7:
|
|
554
|
+
return _fence(selector, variation, _slot(slots, 0))
|
|
555
|
+
if selector == 8:
|
|
556
|
+
return rf"\left\{{\left\{{{_slot(slots, 0)}"
|
|
557
|
+
if selector == 9:
|
|
558
|
+
return rf"{_slot(slots, 0)}\right\}}\right\}}"
|
|
559
|
+
if selector == 10:
|
|
560
|
+
return rf"{_slot(slots, 0)}\right\}}\left\{{"
|
|
561
|
+
if selector == 11:
|
|
562
|
+
return rf"\left\{{{_slot(slots, 0)}\right)"
|
|
563
|
+
if selector == 12:
|
|
564
|
+
return rf"\left({_slot(slots, 0)}\right\}}"
|
|
565
|
+
if selector == 13:
|
|
566
|
+
radicand = _slot(slots, 0)
|
|
567
|
+
index = _slot(slots, 1)
|
|
568
|
+
return rf"\sqrt[{index}]{{{radicand}}}" if variation == 1 and index else rf"\sqrt{{{radicand}}}"
|
|
569
|
+
if selector == 14:
|
|
570
|
+
return rf"\frac{{{_slot(slots, 0)}}}{{{_slot(slots, 1)}}}"
|
|
571
|
+
if selector in {15, 44}:
|
|
572
|
+
subscript = _slot(slots, 0)
|
|
573
|
+
superscript = _slot(slots, 1)
|
|
574
|
+
return (rf"_{{{subscript}}}" if subscript else "") + (rf"^{{{superscript}}}" if superscript else "")
|
|
575
|
+
if selector == 16:
|
|
576
|
+
return rf"\underline{{{_slot(slots, 0)}}}"
|
|
577
|
+
if selector == 17:
|
|
578
|
+
return rf"\overline{{{_slot(slots, 0)}}}"
|
|
579
|
+
if selector in {18, 19, 20}:
|
|
580
|
+
arrow = {18: r"\leftarrow", 19: r"\rightarrow", 20: r"\leftrightarrow"}[selector]
|
|
581
|
+
return rf"\overset{{{_slot(slots, 0)}}}{{{arrow}}}" if variation == 0 else rf"\underset{{{_slot(slots, 0)}}}{{{arrow}}}"
|
|
582
|
+
if selector == 27:
|
|
583
|
+
return rf"\overbrace{{{_slot(slots, 0)}}}^{{{_slot(slots, 1)}}}"
|
|
584
|
+
if selector == 28:
|
|
585
|
+
return rf"\underbrace{{{_slot(slots, 0)}}}_{{{_slot(slots, 1)}}}"
|
|
586
|
+
if 21 <= selector <= 38:
|
|
587
|
+
return _big_operator(selector, slots)
|
|
588
|
+
if selector == 39:
|
|
589
|
+
main = _slot(slots, 0)
|
|
590
|
+
lower = _slot(slots, 1)
|
|
591
|
+
upper = _slot(slots, 2)
|
|
592
|
+
return rf"\lim_{{{lower}}}^{{{upper}}}{{{main}}}"
|
|
593
|
+
if selector == 40:
|
|
594
|
+
return rf"\overline{{{_slot(slots, 1)}}}\smash{{\big) {_slot(slots, 0)}}}"
|
|
595
|
+
if selector == 41:
|
|
596
|
+
return rf"{{{_slot(slots, 0)}}}/{{{_slot(slots, 1)}}}"
|
|
597
|
+
if selector in {42, 43}:
|
|
598
|
+
operator = _slot(slots, 3) or (r"\int" if selector == 42 else r"\sum")
|
|
599
|
+
main = _slot(slots, 0)
|
|
600
|
+
upper = _slot(slots, 1)
|
|
601
|
+
lower = _slot(slots, 2)
|
|
602
|
+
return operator + (rf"_{{{lower}}}" if lower else "") + (rf"^{{{upper}}}" if upper else "") + rf"{{{main}}}"
|
|
603
|
+
if selector == 45:
|
|
604
|
+
return rf"\left\langle {_slot(slots, 0)}\middle|{_slot(slots, 1)}\right\rangle"
|
|
605
|
+
if selector in {46, 47}:
|
|
606
|
+
command = "under" if selector == 46 else "over"
|
|
607
|
+
direction = {0: "leftarrow", 1: "rightarrow", 2: "leftrightarrow"}.get(variation)
|
|
608
|
+
if direction is None:
|
|
609
|
+
raise _MtefError("unsupported MTEF vector variation")
|
|
610
|
+
return rf"\{command}{direction}{{{_slot(slots, 0)}}}"
|
|
611
|
+
if selector == 48:
|
|
612
|
+
return rf"\overparen{{{_slot(slots, 0)}}}"
|
|
613
|
+
raise _MtefError(f"unsupported MTEF v3 template selector: {selector}")
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _render_node(node: _Node) -> str:
|
|
617
|
+
"""递归序列化 MTEF AST。"""
|
|
618
|
+
|
|
619
|
+
if node.kind == "sequence":
|
|
620
|
+
return "".join(_render_node(child) for child in node.children)
|
|
621
|
+
if node.kind == "character":
|
|
622
|
+
_typeface, character, embellishments = node.value # type: ignore[misc]
|
|
623
|
+
return _apply_embellishments(_render_character(int(character)), tuple(embellishments))
|
|
624
|
+
if node.kind == "character_v5":
|
|
625
|
+
character, embellishments, style_bits, typeface, _function_start = node.value # type: ignore[misc]
|
|
626
|
+
value = _apply_embellishments(
|
|
627
|
+
_render_character(character) if isinstance(character, int) else str(character),
|
|
628
|
+
tuple(embellishments),
|
|
629
|
+
)
|
|
630
|
+
if int(typeface) in {1, 12}:
|
|
631
|
+
value = rf"\mathrm{{{value}}}"
|
|
632
|
+
if int(style_bits) == 1:
|
|
633
|
+
return rf"\mathbf{{{value}}}"
|
|
634
|
+
if int(style_bits) == 2:
|
|
635
|
+
return rf"\mathit{{{value}}}"
|
|
636
|
+
if int(style_bits) == 3:
|
|
637
|
+
return rf"\boldsymbol{{\mathit{{{value}}}}}"
|
|
638
|
+
return value
|
|
639
|
+
if node.kind == "text":
|
|
640
|
+
return rf"\text{{{_escape_latex_text(str(node.value))}}}"
|
|
641
|
+
if node.kind == "function":
|
|
642
|
+
function_name = str(node.value)
|
|
643
|
+
known = {
|
|
644
|
+
"arccos",
|
|
645
|
+
"arcsin",
|
|
646
|
+
"arctan",
|
|
647
|
+
"cos",
|
|
648
|
+
"cosh",
|
|
649
|
+
"cot",
|
|
650
|
+
"coth",
|
|
651
|
+
"csc",
|
|
652
|
+
"det",
|
|
653
|
+
"exp",
|
|
654
|
+
"gcd",
|
|
655
|
+
"hom",
|
|
656
|
+
"ker",
|
|
657
|
+
"lg",
|
|
658
|
+
"lim",
|
|
659
|
+
"ln",
|
|
660
|
+
"log",
|
|
661
|
+
"max",
|
|
662
|
+
"min",
|
|
663
|
+
"sec",
|
|
664
|
+
"sin",
|
|
665
|
+
"sinh",
|
|
666
|
+
"sup",
|
|
667
|
+
"tan",
|
|
668
|
+
"tanh",
|
|
669
|
+
}
|
|
670
|
+
return rf"\{function_name} " if function_name in known else rf"\operatorname{{{function_name}}}"
|
|
671
|
+
if node.kind == "template":
|
|
672
|
+
selector, variation, options = node.value # type: ignore[misc]
|
|
673
|
+
return _render_template(int(selector), int(variation), int(options), node.children)
|
|
674
|
+
if node.kind == "fraction_semantic":
|
|
675
|
+
numerator = _slot(node.children, 0)
|
|
676
|
+
denominator = _slot(node.children, 1)
|
|
677
|
+
if bool(node.value):
|
|
678
|
+
return rf"{{{numerator}}}/{{{denominator}}}"
|
|
679
|
+
return rf"\frac{{{numerator}}}{{{denominator}}}"
|
|
680
|
+
if node.kind == "root_semantic":
|
|
681
|
+
radicand = _slot(node.children, 0)
|
|
682
|
+
index = _slot(node.children, 1)
|
|
683
|
+
return rf"\sqrt[{index}]{{{radicand}}}" if bool(node.value) and index else rf"\sqrt{{{radicand}}}"
|
|
684
|
+
if node.kind == "scripts_semantic":
|
|
685
|
+
subscript = _slot(node.children, 0)
|
|
686
|
+
superscript = _slot(node.children, 1)
|
|
687
|
+
scripts = (rf"_{{{subscript}}}" if subscript else "") + (rf"^{{{superscript}}}" if superscript else "")
|
|
688
|
+
return ("{}" if bool(node.value) else "") + scripts
|
|
689
|
+
if node.kind == "fence_semantic":
|
|
690
|
+
default_left, default_right = node.value # type: ignore[misc]
|
|
691
|
+
body = _slot(node.children, 0)
|
|
692
|
+
left = _slot(node.children, 1) or str(default_left)
|
|
693
|
+
right = _slot(node.children, 2) or str(default_right)
|
|
694
|
+
if re.fullmatch(r"\\[A-Za-z]+", left):
|
|
695
|
+
left += " "
|
|
696
|
+
if re.fullmatch(r"\\[A-Za-z]+", right):
|
|
697
|
+
right += " "
|
|
698
|
+
return rf"\left{left}{body}\right{right}"
|
|
699
|
+
if node.kind == "bar_semantic":
|
|
700
|
+
under, doubled = node.value # type: ignore[misc]
|
|
701
|
+
command = "underline" if bool(under) else "overline"
|
|
702
|
+
value = rf"\{command}{{{_slot(node.children, 0)}}}"
|
|
703
|
+
return rf"\{command}{{{value}}}" if bool(doubled) else value
|
|
704
|
+
if node.kind == "arrow_semantic":
|
|
705
|
+
if not isinstance(node.value, int):
|
|
706
|
+
raise _MtefError("MTEF arrow semantic value is invalid")
|
|
707
|
+
variation = node.value
|
|
708
|
+
body = _slot(node.children, 0)
|
|
709
|
+
arrow = _slot(node.children, 1)
|
|
710
|
+
if not arrow:
|
|
711
|
+
if variation & 0x02:
|
|
712
|
+
arrow = r"\leftrightharpoons"
|
|
713
|
+
elif variation & 0x01:
|
|
714
|
+
arrow = r"\Leftrightarrow"
|
|
715
|
+
elif variation & 0x10 and not variation & 0x20:
|
|
716
|
+
arrow = r"\leftarrow"
|
|
717
|
+
elif variation & 0x20 and not variation & 0x10:
|
|
718
|
+
arrow = r"\rightarrow"
|
|
719
|
+
else:
|
|
720
|
+
arrow = r"\leftrightarrow"
|
|
721
|
+
if variation & 0x08 and not variation & 0x04:
|
|
722
|
+
return rf"\underset{{{body}}}{{{arrow}}}"
|
|
723
|
+
return rf"\overset{{{body}}}{{{arrow}}}" if body else arrow
|
|
724
|
+
if node.kind == "big_operator_semantic":
|
|
725
|
+
main = _slot(node.children, 0)
|
|
726
|
+
upper = _slot(node.children, 1)
|
|
727
|
+
lower = _slot(node.children, 2)
|
|
728
|
+
default_operator = str(node.value)
|
|
729
|
+
if default_operator in {
|
|
730
|
+
r"\sum",
|
|
731
|
+
r"\prod",
|
|
732
|
+
r"\coprod",
|
|
733
|
+
r"\bigcup",
|
|
734
|
+
r"\bigcap",
|
|
735
|
+
}:
|
|
736
|
+
operator = default_operator
|
|
737
|
+
else:
|
|
738
|
+
operator = _slot(node.children, 3) or default_operator
|
|
739
|
+
if not operator:
|
|
740
|
+
raise _MtefError("MTEF big operator has no visible operator")
|
|
741
|
+
limits = (rf"_{{{lower}}}" if lower else "") + (rf"^{{{upper}}}" if upper else "")
|
|
742
|
+
return operator + limits + rf"{{{main}}}"
|
|
743
|
+
if node.kind == "limit_semantic":
|
|
744
|
+
main = _slot(node.children, 0)
|
|
745
|
+
lower = _slot(node.children, 1)
|
|
746
|
+
upper = _slot(node.children, 2)
|
|
747
|
+
return r"\lim" + (rf"_{{{lower}}}" if lower else "") + (rf"^{{{upper}}}" if upper else "") + rf"{{{main}}}"
|
|
748
|
+
if node.kind == "horizontal_fence_semantic":
|
|
749
|
+
brace, top = node.value # type: ignore[misc]
|
|
750
|
+
main = _slot(node.children, 0)
|
|
751
|
+
small = _slot(node.children, 1)
|
|
752
|
+
if bool(brace):
|
|
753
|
+
command = "overbrace" if bool(top) else "underbrace"
|
|
754
|
+
else:
|
|
755
|
+
command = "overbracket" if bool(top) else "underbracket"
|
|
756
|
+
suffix = rf"^{{{small}}}" if bool(top) and small else rf"_{{{small}}}" if small else ""
|
|
757
|
+
return rf"\{command}{{{main}}}{suffix}"
|
|
758
|
+
if node.kind == "long_division_semantic":
|
|
759
|
+
dividend = _slot(node.children, 0)
|
|
760
|
+
quotient = _slot(node.children, 1)
|
|
761
|
+
return rf"\overline{{{quotient}}}\smash{{\big) {dividend}}}" if bool(node.value) else rf"\smash{{\big) {dividend}}}"
|
|
762
|
+
if node.kind == "dirac_semantic":
|
|
763
|
+
if not isinstance(node.value, int):
|
|
764
|
+
raise _MtefError("MTEF Dirac semantic value is invalid")
|
|
765
|
+
variation = node.value
|
|
766
|
+
left = _slot(node.children, 0)
|
|
767
|
+
right = _slot(node.children, 1)
|
|
768
|
+
if variation == 0x01:
|
|
769
|
+
return rf"\left\langle {left}\right|"
|
|
770
|
+
if variation == 0x02:
|
|
771
|
+
return rf"\left|{right}\right\rangle"
|
|
772
|
+
return rf"\left\langle {left}\middle|{right}\right\rangle"
|
|
773
|
+
if node.kind == "vector_semantic":
|
|
774
|
+
if not isinstance(node.value, int):
|
|
775
|
+
raise _MtefError("MTEF vector semantic value is invalid")
|
|
776
|
+
variation = node.value
|
|
777
|
+
left = bool(variation & 0x01)
|
|
778
|
+
right = bool(variation & 0x02)
|
|
779
|
+
under = bool(variation & 0x04)
|
|
780
|
+
harpoon = bool(variation & 0x08)
|
|
781
|
+
if harpoon:
|
|
782
|
+
direction = "leftharpoon" if left else "rightharpoon"
|
|
783
|
+
elif left and right:
|
|
784
|
+
direction = "leftrightarrow"
|
|
785
|
+
elif left:
|
|
786
|
+
direction = "leftarrow"
|
|
787
|
+
else:
|
|
788
|
+
direction = "rightarrow"
|
|
789
|
+
return rf"\{'under' if under else 'over'}{direction}{{{_slot(node.children, 0)}}}"
|
|
790
|
+
if node.kind == "accent_semantic":
|
|
791
|
+
return rf"\{str(node.value)}{{{_slot(node.children, 0)}}}"
|
|
792
|
+
if node.kind == "strike_semantic":
|
|
793
|
+
return rf"\{str(node.value)}{{{_slot(node.children, 0)}}}"
|
|
794
|
+
if node.kind == "box_semantic":
|
|
795
|
+
return rf"\boxed{{{_slot(node.children, 0)}}}"
|
|
796
|
+
if node.kind == "pile":
|
|
797
|
+
rows = [_render_node(child).strip() for child in node.children]
|
|
798
|
+
return r"\begin{gathered}" + r"\\".join(rows) + r"\end{gathered}"
|
|
799
|
+
if node.kind == "matrix":
|
|
800
|
+
rows, cols = node.value # type: ignore[misc]
|
|
801
|
+
rendered = [_render_node(child).strip() for child in node.children]
|
|
802
|
+
matrix_rows = ["&".join(rendered[row * int(cols) : (row + 1) * int(cols)]) for row in range(int(rows))]
|
|
803
|
+
return r"\begin{matrix}" + r"\\".join(matrix_rows) + r"\end{matrix}"
|
|
804
|
+
if node.kind in {"metadata", "embellishment"}:
|
|
805
|
+
return ""
|
|
806
|
+
raise _MtefError(f"unknown MTEF AST node: {node.kind}")
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def decode_mtef_v3(data: bytes) -> str | None:
|
|
810
|
+
"""把 MTEF v3 字节流转换为 LaTeX;不可信对象失败时返回 None。"""
|
|
811
|
+
|
|
812
|
+
try:
|
|
813
|
+
latex = _render_node(_MtefReader(data).parse()).strip()
|
|
814
|
+
except LegacyOfficeResourceLimitError:
|
|
815
|
+
raise
|
|
816
|
+
except (ArithmeticError, IndexError, struct.error, UnicodeError, _MtefError, ValueError):
|
|
817
|
+
return None
|
|
818
|
+
return latex or None
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def decode_mtef_v5(data: bytes) -> str | None:
|
|
822
|
+
"""延迟加载独立 v5 reader,并返回其完整 LaTeX 结果。"""
|
|
823
|
+
|
|
824
|
+
from .mtef_v5 import decode_mtef_v5 as _decode_mtef_v5
|
|
825
|
+
|
|
826
|
+
return _decode_mtef_v5(data)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def decode_mtef(data: bytes) -> str | None:
|
|
830
|
+
"""仅按 MTEF header 首字节分派 v3/v5,其他版本整体拒绝。"""
|
|
831
|
+
|
|
832
|
+
if not data:
|
|
833
|
+
return None
|
|
834
|
+
if data[0] == 3:
|
|
835
|
+
return decode_mtef_v3(data)
|
|
836
|
+
if data[0] == 5:
|
|
837
|
+
return decode_mtef_v5(data)
|
|
838
|
+
return None
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def decode_equation_native(data: bytes) -> str | None:
|
|
842
|
+
"""校验 28 字节 EQNOLEFILEHDR 并按 header 解码 MTEF v3/v5。"""
|
|
843
|
+
|
|
844
|
+
if len(data) < 28:
|
|
845
|
+
return None
|
|
846
|
+
header_size, version, _clipboard_format, object_size = struct.unpack_from("<HIHI", data, 0)
|
|
847
|
+
if header_size < 28 or header_size > len(data) or version != 0x0002_0000:
|
|
848
|
+
return None
|
|
849
|
+
object_end = header_size + object_size
|
|
850
|
+
if object_end < header_size or object_end > len(data):
|
|
851
|
+
return None
|
|
852
|
+
return decode_mtef(data[header_size:object_end])
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def decode_equation_object(data: bytes) -> str | None:
|
|
856
|
+
"""从独立公式 OLE 对象读取 Equation Native 并转为 LaTeX。"""
|
|
857
|
+
|
|
858
|
+
try:
|
|
859
|
+
with BoundedOleReader(data) as ole:
|
|
860
|
+
native = ole.read_stream("Equation Native")
|
|
861
|
+
except LegacyOfficeResourceLimitError:
|
|
862
|
+
raise
|
|
863
|
+
except ValueError:
|
|
864
|
+
return None
|
|
865
|
+
return decode_equation_native(native)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def read_object_pool_equations(ole: BoundedOleReader) -> dict[int, str]:
|
|
869
|
+
"""只读提取 DOC ObjectPool 中可安全解码的 Equation Native streams。"""
|
|
870
|
+
|
|
871
|
+
equations: dict[int, str] = {}
|
|
872
|
+
for stream_name in ole.stream_names(prefix="ObjectPool/"):
|
|
873
|
+
match = _OBJECT_POOL_EQUATION_RE.match(stream_name)
|
|
874
|
+
if match is None:
|
|
875
|
+
continue
|
|
876
|
+
storage_id = int(match.group(1))
|
|
877
|
+
latex = decode_equation_native(ole.read_stream(stream_name))
|
|
878
|
+
if latex is None:
|
|
879
|
+
logger.warning(
|
|
880
|
+
"DOC_MTEF_FALLBACK: storage_id={} has an invalid or unsupported Equation Native stream",
|
|
881
|
+
storage_id,
|
|
882
|
+
)
|
|
883
|
+
continue
|
|
884
|
+
equations.setdefault(storage_id, latex)
|
|
885
|
+
return equations
|