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,403 @@
|
|
|
1
|
+
"""ODT、ODS、ODP 到 DocVortex raw model-list 的原生 converter。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, BinaryIO, Iterator
|
|
8
|
+
|
|
9
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
10
|
+
|
|
11
|
+
from .....schema import BlockType
|
|
12
|
+
from .....content.spans import inline_span_plain_text, text_spans
|
|
13
|
+
from .constants import OdfSuffix, qname
|
|
14
|
+
from .models import InlineNote
|
|
15
|
+
from .package import OdfPackage
|
|
16
|
+
from .styles import OdfStyles
|
|
17
|
+
from .table import OdfTableExpansionBudget, parse_table_grid, split_table_regions, table_grid_to_html
|
|
18
|
+
from .text import (
|
|
19
|
+
OdfBlockParser,
|
|
20
|
+
OdfMasterPageChange,
|
|
21
|
+
OdfTextExpansionBudget,
|
|
22
|
+
collect_emittable_anchor_targets,
|
|
23
|
+
flatten_block_text,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_LENGTH_RE = re.compile(r"^\s*(?P<value>[+-]?(?:\d+(?:\.\d*)?|\.\d+))(?P<unit>cm|mm|in|pt|pc|px)?\s*$")
|
|
28
|
+
_LENGTH_TO_PT = {"": 1.0, "pt": 1.0, "pc": 12.0, "in": 72.0, "cm": 72.0 / 2.54, "mm": 72.0 / 25.4, "px": 0.75}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class _OdfContext:
|
|
33
|
+
"""保存 converter 一次调用内共享的包、内容树、样式和正文。"""
|
|
34
|
+
|
|
35
|
+
package: OdfPackage
|
|
36
|
+
content_root: etree._Element
|
|
37
|
+
styles: OdfStyles
|
|
38
|
+
body: etree._Element
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class _PositionedBlocks:
|
|
43
|
+
"""保存幻灯片对象的阅读顺序坐标、XML 序号和 raw blocks。"""
|
|
44
|
+
|
|
45
|
+
y: float
|
|
46
|
+
x: float
|
|
47
|
+
order: int
|
|
48
|
+
title: bool
|
|
49
|
+
blocks: list[dict[str, Any]]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _open_context(file_binary: BinaryIO, suffix: OdfSuffix) -> _OdfContext:
|
|
53
|
+
"""读取调用方流并建立已验证 ODF 包、样式和正文上下文。"""
|
|
54
|
+
package = OdfPackage(file_binary.read())
|
|
55
|
+
try:
|
|
56
|
+
content_root = package.validate_document(suffix)
|
|
57
|
+
styles_root = package.xml_part("styles.xml")
|
|
58
|
+
styles = OdfStyles(styles_root, content_root)
|
|
59
|
+
return _OdfContext(
|
|
60
|
+
package=package,
|
|
61
|
+
content_root=content_root,
|
|
62
|
+
styles=styles,
|
|
63
|
+
body=package.body_element(content_root, suffix),
|
|
64
|
+
)
|
|
65
|
+
except Exception:
|
|
66
|
+
package.close()
|
|
67
|
+
raise
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _new_page(
|
|
71
|
+
pages: list[list[dict[str, Any]]],
|
|
72
|
+
page_masters: list[str | None],
|
|
73
|
+
master_name: str | None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""追加一个新逻辑页及其 master-page 归属。"""
|
|
76
|
+
pages.append([])
|
|
77
|
+
page_masters.append(master_name)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _flush_notes(parser: OdfBlockParser, page: list[dict[str, Any]]) -> None:
|
|
81
|
+
"""把解析器累计脚注追加为当前页面的 PAGE_FOOTNOTE blocks。"""
|
|
82
|
+
for note in parser.drain_notes():
|
|
83
|
+
page.append({"type": BlockType.PAGE_FOOTNOTE, "content": text_spans(note)})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _append_flow_items(
|
|
87
|
+
items: list[dict[str, Any] | InlineNote],
|
|
88
|
+
*,
|
|
89
|
+
parser: OdfBlockParser,
|
|
90
|
+
page: list[dict[str, Any]],
|
|
91
|
+
) -> None:
|
|
92
|
+
"""把段落结果和脚注按统一流语义追加到当前章节页。"""
|
|
93
|
+
for item in items:
|
|
94
|
+
if isinstance(item, InlineNote):
|
|
95
|
+
parser.notes.append(item.content)
|
|
96
|
+
else:
|
|
97
|
+
page.append(item)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _master_auxiliary_blocks(
|
|
101
|
+
master_page: etree._Element | None,
|
|
102
|
+
*,
|
|
103
|
+
package: OdfPackage,
|
|
104
|
+
styles: OdfStyles,
|
|
105
|
+
anchor_targets: frozenset[str],
|
|
106
|
+
text_expansion_budget: OdfTextExpansionBudget,
|
|
107
|
+
table_expansion_budget: OdfTableExpansionBudget,
|
|
108
|
+
) -> list[dict[str, Any]]:
|
|
109
|
+
"""从 master-page 的 header/footer 中提取页面辅助文本。"""
|
|
110
|
+
if master_page is None:
|
|
111
|
+
return []
|
|
112
|
+
parser = OdfBlockParser(
|
|
113
|
+
package,
|
|
114
|
+
styles,
|
|
115
|
+
anchor_targets=anchor_targets,
|
|
116
|
+
text_expansion_budget=text_expansion_budget,
|
|
117
|
+
table_expansion_budget=table_expansion_budget,
|
|
118
|
+
)
|
|
119
|
+
result: list[dict[str, Any]] = []
|
|
120
|
+
for tag_name, block_type in (("header", BlockType.HEADER), ("footer", BlockType.FOOTER)):
|
|
121
|
+
element = master_page.find(qname("style", tag_name))
|
|
122
|
+
if element is None:
|
|
123
|
+
element = master_page.find(qname("style", f"{tag_name}-left"))
|
|
124
|
+
if element is None:
|
|
125
|
+
continue
|
|
126
|
+
for block in parser.parse_container(element):
|
|
127
|
+
content = block.get("content")
|
|
128
|
+
if isinstance(content, list) and inline_span_plain_text(span for span in content if isinstance(span, dict)).strip():
|
|
129
|
+
result.append({"type": block_type, "content": content})
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_odt_pages(context: _OdfContext) -> list[list[dict[str, Any]]]:
|
|
134
|
+
"""仅按 master-page 章节变化递归构造 ODT 逻辑页。"""
|
|
135
|
+
anchor_targets = collect_emittable_anchor_targets(context.content_root, context.styles)
|
|
136
|
+
text_expansion_budget = OdfTextExpansionBudget()
|
|
137
|
+
parser = OdfBlockParser(
|
|
138
|
+
context.package,
|
|
139
|
+
context.styles,
|
|
140
|
+
anchor_targets=anchor_targets,
|
|
141
|
+
text_expansion_budget=text_expansion_budget,
|
|
142
|
+
)
|
|
143
|
+
pages: list[list[dict[str, Any]]] = [[]]
|
|
144
|
+
page_masters: list[str | None] = [None]
|
|
145
|
+
current_master: str | None = None
|
|
146
|
+
|
|
147
|
+
def apply_master_page(requested_master: str | None) -> None:
|
|
148
|
+
"""按段落或列表事件切换 ODT 虚拟页及其 master-page。"""
|
|
149
|
+
nonlocal current_master
|
|
150
|
+
master_changed = requested_master is not None and current_master is not None and requested_master != current_master
|
|
151
|
+
if master_changed and pages[-1]:
|
|
152
|
+
_flush_notes(parser, pages[-1])
|
|
153
|
+
_new_page(pages, page_masters, requested_master)
|
|
154
|
+
if requested_master is not None:
|
|
155
|
+
current_master = requested_master
|
|
156
|
+
page_masters[-1] = current_master
|
|
157
|
+
|
|
158
|
+
def walk(parent: etree._Element) -> None:
|
|
159
|
+
"""递归遍历 ODT block 容器并维护当前页与 master-page。"""
|
|
160
|
+
for child in parent:
|
|
161
|
+
if not isinstance(child.tag, str):
|
|
162
|
+
continue
|
|
163
|
+
if child.tag in {qname("text", "p"), qname("text", "h")}:
|
|
164
|
+
requested_master = context.styles.paragraph_master_page_name(child.get(qname("text", "style-name")))
|
|
165
|
+
apply_master_page(requested_master)
|
|
166
|
+
_append_flow_items(
|
|
167
|
+
parser.parse_paragraph(child),
|
|
168
|
+
parser=parser,
|
|
169
|
+
page=pages[-1],
|
|
170
|
+
)
|
|
171
|
+
elif child.tag in {
|
|
172
|
+
qname("text", "section"),
|
|
173
|
+
qname("text", "index-body"),
|
|
174
|
+
qname("text", "index-title"),
|
|
175
|
+
}:
|
|
176
|
+
walk(child)
|
|
177
|
+
elif child.tag == qname("text", "list"):
|
|
178
|
+
for item in parser.parse_list_blocks(child, emit_master_page_changes=True):
|
|
179
|
+
if isinstance(item, OdfMasterPageChange):
|
|
180
|
+
apply_master_page(item.master_page_name)
|
|
181
|
+
elif isinstance(item, InlineNote):
|
|
182
|
+
parser.notes.append(item.content)
|
|
183
|
+
else:
|
|
184
|
+
pages[-1].append(item)
|
|
185
|
+
else:
|
|
186
|
+
pages[-1].extend(parser.parse_element(child))
|
|
187
|
+
|
|
188
|
+
walk(context.body)
|
|
189
|
+
_flush_notes(parser, pages[-1])
|
|
190
|
+
while len(pages) > 1 and not pages[-1]:
|
|
191
|
+
pages.pop()
|
|
192
|
+
page_masters.pop()
|
|
193
|
+
for page, master_name in zip(pages, page_masters, strict=True):
|
|
194
|
+
page.extend(
|
|
195
|
+
_master_auxiliary_blocks(
|
|
196
|
+
context.styles.master_page(master_name),
|
|
197
|
+
package=context.package,
|
|
198
|
+
styles=context.styles,
|
|
199
|
+
anchor_targets=anchor_targets,
|
|
200
|
+
text_expansion_budget=text_expansion_budget,
|
|
201
|
+
table_expansion_budget=parser.table_expansion_budget,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
return pages or [[]]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _length_to_points(value: str | None) -> float:
|
|
208
|
+
"""把 ODF SVG 长度转换为用于阅读顺序比较的 point。"""
|
|
209
|
+
match = _LENGTH_RE.match(value or "")
|
|
210
|
+
if match is None:
|
|
211
|
+
return 0.0
|
|
212
|
+
return float(match.group("value")) * _LENGTH_TO_PT.get(match.group("unit") or "", 1.0)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _iter_slide_shapes(
|
|
216
|
+
parent: etree._Element,
|
|
217
|
+
*,
|
|
218
|
+
x_offset: float = 0.0,
|
|
219
|
+
y_offset: float = 0.0,
|
|
220
|
+
) -> Iterator[tuple[etree._Element, float, float]]:
|
|
221
|
+
"""递归展开幻灯片 group,并产出可见 shape 及近似绝对坐标。"""
|
|
222
|
+
for child in parent:
|
|
223
|
+
if not isinstance(child.tag, str) or child.tag == qname("presentation", "notes"):
|
|
224
|
+
continue
|
|
225
|
+
if child.tag == qname("draw", "g"):
|
|
226
|
+
group_x = x_offset + _length_to_points(child.get(qname("svg", "x")))
|
|
227
|
+
group_y = y_offset + _length_to_points(child.get(qname("svg", "y")))
|
|
228
|
+
yield from _iter_slide_shapes(child, x_offset=group_x, y_offset=group_y)
|
|
229
|
+
continue
|
|
230
|
+
if child.tag in {
|
|
231
|
+
qname("draw", "frame"),
|
|
232
|
+
qname("draw", "custom-shape"),
|
|
233
|
+
qname("draw", "rect"),
|
|
234
|
+
qname("draw", "ellipse"),
|
|
235
|
+
qname("draw", "caption"),
|
|
236
|
+
}:
|
|
237
|
+
yield (
|
|
238
|
+
child,
|
|
239
|
+
x_offset + _length_to_points(child.get(qname("svg", "x"))),
|
|
240
|
+
y_offset + _length_to_points(child.get(qname("svg", "y"))),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _shape_blocks(shape: etree._Element, parser: OdfBlockParser) -> list[dict[str, Any]]:
|
|
245
|
+
"""把 frame 或带文本 custom-shape 转为页面 raw blocks。"""
|
|
246
|
+
if shape.tag == qname("draw", "frame"):
|
|
247
|
+
return parser.parse_frame_blocks(shape)
|
|
248
|
+
return parser.parse_container(shape)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _notes_blocks(page: etree._Element, parser: OdfBlockParser) -> list[dict[str, Any]]:
|
|
252
|
+
"""提取 ODP speaker notes,并聚合为页面脚注。"""
|
|
253
|
+
notes = page.find(qname("presentation", "notes"))
|
|
254
|
+
if notes is None:
|
|
255
|
+
return []
|
|
256
|
+
blocks: list[dict[str, Any]] = []
|
|
257
|
+
for frame in notes.iter(qname("draw", "frame")):
|
|
258
|
+
blocks.extend(parser.parse_frame_blocks(frame))
|
|
259
|
+
visible = flatten_block_text(blocks)
|
|
260
|
+
return [{"type": BlockType.PAGE_FOOTNOTE, "content": text_spans(visible)}] if visible else []
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _parse_odp_pages(context: _OdfContext) -> list[list[dict[str, Any]]]:
|
|
264
|
+
"""保持一页一 slide,并按坐标和 XML 顺序构造 ODP model-list。"""
|
|
265
|
+
parser = OdfBlockParser(context.package, context.styles)
|
|
266
|
+
pages: list[list[dict[str, Any]]] = []
|
|
267
|
+
document_title_emitted = False
|
|
268
|
+
for page in context.body:
|
|
269
|
+
if page.tag != qname("draw", "page"):
|
|
270
|
+
continue
|
|
271
|
+
if not context.styles.drawing_page_is_visible(page):
|
|
272
|
+
continue
|
|
273
|
+
positioned: list[_PositionedBlocks] = []
|
|
274
|
+
for order, (shape, x, y) in enumerate(_iter_slide_shapes(page)):
|
|
275
|
+
presentation_class = shape.get(qname("presentation", "class"), "")
|
|
276
|
+
if presentation_class in {"page-number", "date-time", "footer", "header"}:
|
|
277
|
+
continue
|
|
278
|
+
blocks = _shape_blocks(shape, parser)
|
|
279
|
+
if not blocks:
|
|
280
|
+
continue
|
|
281
|
+
positioned.append(
|
|
282
|
+
_PositionedBlocks(
|
|
283
|
+
y=y,
|
|
284
|
+
x=x,
|
|
285
|
+
order=order,
|
|
286
|
+
title=presentation_class in {"title", "subtitle"},
|
|
287
|
+
blocks=blocks,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
title_entries = sorted((item for item in positioned if item.title), key=lambda item: (item.y, item.x, item.order))
|
|
291
|
+
body_entries = sorted((item for item in positioned if not item.title), key=lambda item: (item.y, item.x, item.order))
|
|
292
|
+
output: list[dict[str, Any]] = []
|
|
293
|
+
for entry in title_entries:
|
|
294
|
+
visible = flatten_block_text(entry.blocks)
|
|
295
|
+
if visible:
|
|
296
|
+
title_type = BlockType.DOC_TITLE if not document_title_emitted else BlockType.PARAGRAPH_TITLE
|
|
297
|
+
output.append(
|
|
298
|
+
{
|
|
299
|
+
"type": title_type,
|
|
300
|
+
"level": 1 if title_type == BlockType.DOC_TITLE else 2,
|
|
301
|
+
"content": text_spans(visible.replace("\n", " ")),
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
document_title_emitted = True
|
|
305
|
+
output.extend(
|
|
306
|
+
block for block in entry.blocks if block.get("type") in {BlockType.IMAGE, BlockType.TABLE, BlockType.CHART}
|
|
307
|
+
)
|
|
308
|
+
for entry in body_entries:
|
|
309
|
+
output.extend(entry.blocks)
|
|
310
|
+
_flush_notes(parser, output)
|
|
311
|
+
output.extend(_notes_blocks(page, parser))
|
|
312
|
+
_flush_notes(parser, output)
|
|
313
|
+
pages.append(output)
|
|
314
|
+
return pages or [[]]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _sheet_blocks(sheet: etree._Element, parser: OdfBlockParser) -> list[dict[str, Any]]:
|
|
318
|
+
"""把一个可见 ODS sheet 拆为数据区域和锚定视觉对象。"""
|
|
319
|
+
grid = parse_table_grid(sheet, parser.render_cell_html, expansion_budget=parser.table_expansion_budget)
|
|
320
|
+
blocks: list[dict[str, Any]] = []
|
|
321
|
+
for region in split_table_regions(grid):
|
|
322
|
+
content = table_grid_to_html(region)
|
|
323
|
+
if content:
|
|
324
|
+
blocks.append({"type": BlockType.TABLE, "content": content})
|
|
325
|
+
blocks.extend(parser.drain_cell_visuals())
|
|
326
|
+
for shapes in sheet.iter(qname("table", "shapes")):
|
|
327
|
+
for frame in shapes.iter(qname("draw", "frame")):
|
|
328
|
+
for block in parser.parse_frame_blocks(frame):
|
|
329
|
+
if block.get("type") in {BlockType.IMAGE, BlockType.CHART, BlockType.EQUATION}:
|
|
330
|
+
blocks.append(block)
|
|
331
|
+
_flush_notes(parser, blocks)
|
|
332
|
+
return blocks
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _parse_ods_pages(context: _OdfContext) -> list[list[dict[str, Any]]]:
|
|
336
|
+
"""保持一页一可见 sheet,并在多表时添加工作表标题。"""
|
|
337
|
+
parser = OdfBlockParser(context.package, context.styles, collect_cell_visuals=True)
|
|
338
|
+
sheet_pages: list[tuple[str, list[dict[str, Any]]]] = []
|
|
339
|
+
for sheet in context.body:
|
|
340
|
+
if sheet.tag != qname("table", "table"):
|
|
341
|
+
continue
|
|
342
|
+
if sheet.get(qname("table", "display"), "true").casefold() == "false":
|
|
343
|
+
continue
|
|
344
|
+
if not context.styles.table_is_visible(sheet.get(qname("table", "style-name"))):
|
|
345
|
+
continue
|
|
346
|
+
name = sheet.get(qname("table", "name"), "Sheet")
|
|
347
|
+
sheet_pages.append((name, _sheet_blocks(sheet, parser)))
|
|
348
|
+
if sum(bool(blocks) for _, blocks in sheet_pages) > 1:
|
|
349
|
+
for name, blocks in sheet_pages:
|
|
350
|
+
if blocks:
|
|
351
|
+
blocks.insert(0, {"type": BlockType.PARAGRAPH_TITLE, "level": 2, "content": text_spans(name)})
|
|
352
|
+
return [blocks for _, blocks in sheet_pages] or [[]]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class OdtConverter:
|
|
356
|
+
"""把 OpenDocument Text 转换为 DocVortex 分页 raw blocks。"""
|
|
357
|
+
|
|
358
|
+
def __init__(self) -> None:
|
|
359
|
+
"""初始化空分页结果,等待 convert 填充。"""
|
|
360
|
+
self.pages: list[list[dict[str, Any]]] = []
|
|
361
|
+
|
|
362
|
+
def convert(self, file_binary: BinaryIO) -> None:
|
|
363
|
+
"""解析调用方持有的 ODT 流,并保持调用方流打开。"""
|
|
364
|
+
context = _open_context(file_binary, "odt")
|
|
365
|
+
try:
|
|
366
|
+
self.pages = _parse_odt_pages(context)
|
|
367
|
+
finally:
|
|
368
|
+
context.package.close()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class OdpConverter:
|
|
372
|
+
"""把 OpenDocument Presentation 转换为逐幻灯片 raw blocks。"""
|
|
373
|
+
|
|
374
|
+
def __init__(self) -> None:
|
|
375
|
+
"""初始化空幻灯片结果,等待 convert 填充。"""
|
|
376
|
+
self.pages: list[list[dict[str, Any]]] = []
|
|
377
|
+
|
|
378
|
+
def convert(self, file_binary: BinaryIO) -> None:
|
|
379
|
+
"""解析调用方持有的 ODP 流,并保持调用方流打开。"""
|
|
380
|
+
context = _open_context(file_binary, "odp")
|
|
381
|
+
try:
|
|
382
|
+
self.pages = _parse_odp_pages(context)
|
|
383
|
+
finally:
|
|
384
|
+
context.package.close()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class OdsConverter:
|
|
388
|
+
"""把 OpenDocument Spreadsheet 转换为逐可见工作表 raw blocks。"""
|
|
389
|
+
|
|
390
|
+
def __init__(self) -> None:
|
|
391
|
+
"""初始化空工作表结果,等待 convert 填充。"""
|
|
392
|
+
self.pages: list[list[dict[str, Any]]] = []
|
|
393
|
+
|
|
394
|
+
def convert(self, file_binary: BinaryIO) -> None:
|
|
395
|
+
"""解析调用方持有的 ODS 流,并保持调用方流打开。"""
|
|
396
|
+
context = _open_context(file_binary, "ods")
|
|
397
|
+
try:
|
|
398
|
+
self.pages = _parse_ods_pages(context)
|
|
399
|
+
finally:
|
|
400
|
+
context.package.close()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
__all__ = ["OdpConverter", "OdsConverter", "OdtConverter"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""OpenDocument 内部稳定错误类型。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OdfParseError(ValueError):
|
|
7
|
+
"""表示 OpenDocument 包或语义结构不可解析。"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OdfResourceLimitError(OdfParseError):
|
|
11
|
+
"""表示 OpenDocument 输入超过固定安全边界。"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OdfEncryptedError(OdfParseError):
|
|
15
|
+
"""表示 OpenDocument 包包含不支持的加密成员。"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["OdfEncryptedError", "OdfParseError", "OdfResourceLimitError"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""读取 OpenDocument meta.xml 与结构页数。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import BinaryIO, Final
|
|
6
|
+
|
|
7
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
8
|
+
|
|
9
|
+
from .constants import OdfSuffix, qname
|
|
10
|
+
from .package import OdfPackage
|
|
11
|
+
from .styles import OdfStyles
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MAX_ODT_METADATA_PAGE_COUNT: Final = 10_000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _first_text(root: etree._Element | None, *tags: str) -> str | None:
|
|
18
|
+
"""返回多个候选标签中首个非空文本。"""
|
|
19
|
+
if root is None:
|
|
20
|
+
return None
|
|
21
|
+
for tag in tags:
|
|
22
|
+
element = root.find(f".//{tag}")
|
|
23
|
+
if element is not None:
|
|
24
|
+
value = "".join(element.itertext()).strip()
|
|
25
|
+
if value:
|
|
26
|
+
return value
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _odt_page_count(meta_root: etree._Element | None) -> int | None:
|
|
31
|
+
"""读取 ODT 生产者记录的布局页数,缺失或非法时返回空。"""
|
|
32
|
+
if meta_root is None:
|
|
33
|
+
return None
|
|
34
|
+
statistic = meta_root.find(f".//{qname('meta', 'document-statistic')}")
|
|
35
|
+
if statistic is None:
|
|
36
|
+
return None
|
|
37
|
+
try:
|
|
38
|
+
value = int(statistic.get(qname("meta", "page-count"), ""))
|
|
39
|
+
except ValueError:
|
|
40
|
+
return None
|
|
41
|
+
return min(value, MAX_ODT_METADATA_PAGE_COUNT) if value >= 1 else None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _visible_sheet_count(body: etree._Element, styles: OdfStyles) -> int:
|
|
45
|
+
"""统计未被 table:display 或表格样式隐藏的 ODS 工作表。"""
|
|
46
|
+
count = 0
|
|
47
|
+
for sheet in body:
|
|
48
|
+
if sheet.tag != qname("table", "table"):
|
|
49
|
+
continue
|
|
50
|
+
if sheet.get(qname("table", "display"), "true").casefold() == "false":
|
|
51
|
+
continue
|
|
52
|
+
if styles.table_is_visible(sheet.get(qname("table", "style-name"))):
|
|
53
|
+
count += 1
|
|
54
|
+
return count
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def extract_odf_metadata(file_binary: BinaryIO, suffix: OdfSuffix) -> dict[str, object | None]:
|
|
58
|
+
"""提取 ODF 标题作者等元数据及稳定文档页数。"""
|
|
59
|
+
package = OdfPackage(file_binary.read())
|
|
60
|
+
try:
|
|
61
|
+
content_root = package.validate_document(suffix)
|
|
62
|
+
styles_root = package.xml_part("styles.xml")
|
|
63
|
+
styles = OdfStyles(styles_root, content_root)
|
|
64
|
+
body = package.body_element(content_root, suffix)
|
|
65
|
+
meta_root = package.xml_part("meta.xml")
|
|
66
|
+
keywords = []
|
|
67
|
+
if meta_root is not None:
|
|
68
|
+
for keyword in meta_root.iter(qname("meta", "keyword")):
|
|
69
|
+
value = "".join(keyword.itertext()).strip()
|
|
70
|
+
if value:
|
|
71
|
+
keywords.append(value)
|
|
72
|
+
if suffix == "odt":
|
|
73
|
+
page_count = _odt_page_count(meta_root)
|
|
74
|
+
elif suffix == "odp":
|
|
75
|
+
page_count = sum(
|
|
76
|
+
1 for child in body if child.tag == qname("draw", "page") and styles.drawing_page_is_visible(child)
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
page_count = _visible_sheet_count(body, styles)
|
|
80
|
+
return {
|
|
81
|
+
"page_count": page_count or 1,
|
|
82
|
+
"title": _first_text(meta_root, qname("dc", "title")),
|
|
83
|
+
"author": _first_text(meta_root, qname("dc", "creator"), qname("meta", "initial-creator")),
|
|
84
|
+
"subject": _first_text(meta_root, qname("dc", "subject")),
|
|
85
|
+
"keywords": ", ".join(keywords) or None,
|
|
86
|
+
}
|
|
87
|
+
finally:
|
|
88
|
+
package.close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = ["extract_odf_metadata"]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""OpenDocument 内部行内、样式与表格模型。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, TypeAlias, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class TextStyle:
|
|
11
|
+
"""保存可继承的 ODF 行内样式最终值。"""
|
|
12
|
+
|
|
13
|
+
bold: bool = False
|
|
14
|
+
italic: bool = False
|
|
15
|
+
underline: bool = False
|
|
16
|
+
strikethrough: bool = False
|
|
17
|
+
superscript: bool = False
|
|
18
|
+
subscript: bool = False
|
|
19
|
+
|
|
20
|
+
def names(self) -> tuple[str, ...]:
|
|
21
|
+
"""按 DocVortex 内联协议的稳定顺序返回已启用样式名。"""
|
|
22
|
+
result: list[str] = []
|
|
23
|
+
if self.bold:
|
|
24
|
+
result.append("bold")
|
|
25
|
+
if self.italic:
|
|
26
|
+
result.append("italic")
|
|
27
|
+
if self.underline:
|
|
28
|
+
result.append("underline")
|
|
29
|
+
if self.strikethrough:
|
|
30
|
+
result.append("strikethrough")
|
|
31
|
+
if self.superscript:
|
|
32
|
+
result.append("superscript")
|
|
33
|
+
if self.subscript:
|
|
34
|
+
result.append("subscript")
|
|
35
|
+
return tuple(result)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class TextStyleDelta:
|
|
40
|
+
"""保存 ODF 样式层级中可显式覆盖的三态字段。"""
|
|
41
|
+
|
|
42
|
+
bold: bool | None = None
|
|
43
|
+
italic: bool | None = None
|
|
44
|
+
underline: bool | None = None
|
|
45
|
+
strikethrough: bool | None = None
|
|
46
|
+
superscript: bool | None = None
|
|
47
|
+
subscript: bool | None = None
|
|
48
|
+
|
|
49
|
+
def merge(self, child: TextStyleDelta) -> TextStyleDelta:
|
|
50
|
+
"""用子样式的非空字段覆盖当前样式。"""
|
|
51
|
+
return TextStyleDelta(
|
|
52
|
+
bold=self.bold if child.bold is None else child.bold,
|
|
53
|
+
italic=self.italic if child.italic is None else child.italic,
|
|
54
|
+
underline=self.underline if child.underline is None else child.underline,
|
|
55
|
+
strikethrough=self.strikethrough if child.strikethrough is None else child.strikethrough,
|
|
56
|
+
superscript=self.superscript if child.superscript is None else child.superscript,
|
|
57
|
+
subscript=self.subscript if child.subscript is None else child.subscript,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def resolve(self) -> TextStyle:
|
|
61
|
+
"""把未声明字段按关闭处理并返回最终样式。"""
|
|
62
|
+
return TextStyle(
|
|
63
|
+
bold=bool(self.bold),
|
|
64
|
+
italic=bool(self.italic),
|
|
65
|
+
underline=bool(self.underline),
|
|
66
|
+
strikethrough=bool(self.strikethrough),
|
|
67
|
+
superscript=bool(self.superscript),
|
|
68
|
+
subscript=bool(self.subscript),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class ListLevel:
|
|
74
|
+
"""保存一个 ODF 列表层级的通用编号语义。"""
|
|
75
|
+
|
|
76
|
+
ordered: bool = False
|
|
77
|
+
start: int = 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class InlineText:
|
|
82
|
+
"""保存带样式和可选超链接的行内文本。"""
|
|
83
|
+
|
|
84
|
+
text: str
|
|
85
|
+
style: TextStyle = TextStyle()
|
|
86
|
+
hyperlink: str | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class InlineMath:
|
|
91
|
+
"""保存不含外围标记的行内 LaTeX。"""
|
|
92
|
+
|
|
93
|
+
latex: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True, slots=True)
|
|
97
|
+
class InlineBreak:
|
|
98
|
+
"""表示段内显式换行。"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True, slots=True)
|
|
102
|
+
class InlineNote:
|
|
103
|
+
"""保存应随当前行内内容归属的 ODF note body。"""
|
|
104
|
+
|
|
105
|
+
content: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class InlineImage:
|
|
110
|
+
"""保存表格单元格中允许内联呈现的图片 data URI。"""
|
|
111
|
+
|
|
112
|
+
data_uri: str
|
|
113
|
+
alt: str = ""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class InlineBlockGroup:
|
|
118
|
+
"""在行内流中保存段外 block 及其与内联图片的配对关系。"""
|
|
119
|
+
|
|
120
|
+
blocks: tuple[dict[str, Any], ...]
|
|
121
|
+
inline_image_rendered: bool = False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
InlineAtom: TypeAlias = Union[
|
|
125
|
+
InlineText,
|
|
126
|
+
InlineMath,
|
|
127
|
+
InlineBreak,
|
|
128
|
+
InlineNote,
|
|
129
|
+
InlineImage,
|
|
130
|
+
InlineBlockGroup,
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(slots=True)
|
|
135
|
+
class GridCell:
|
|
136
|
+
"""保存 ODF 表格原点单元格的 HTML 与跨度。"""
|
|
137
|
+
|
|
138
|
+
html: str = ""
|
|
139
|
+
row_span: int = 1
|
|
140
|
+
col_span: int = 1
|
|
141
|
+
header: bool = False
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def has_content(self) -> bool:
|
|
145
|
+
"""返回单元格是否包含可见或结构化 HTML。"""
|
|
146
|
+
return bool(self.html.strip())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(slots=True)
|
|
150
|
+
class TableGrid:
|
|
151
|
+
"""保存带合并占位的 ODF 二维表格。"""
|
|
152
|
+
|
|
153
|
+
rows: list[list[GridCell | None]] = field(default_factory=list)
|
|
154
|
+
header_rows: int = 0
|
|
155
|
+
covered: set[tuple[int, int]] = field(default_factory=set)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def width(self) -> int:
|
|
159
|
+
"""返回网格最大视觉列数。"""
|
|
160
|
+
return max((len(row) for row in self.rows), default=0)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
__all__ = [
|
|
164
|
+
"GridCell",
|
|
165
|
+
"InlineAtom",
|
|
166
|
+
"InlineBlockGroup",
|
|
167
|
+
"InlineBreak",
|
|
168
|
+
"InlineImage",
|
|
169
|
+
"InlineMath",
|
|
170
|
+
"InlineNote",
|
|
171
|
+
"InlineText",
|
|
172
|
+
"ListLevel",
|
|
173
|
+
"TableGrid",
|
|
174
|
+
"TextStyle",
|
|
175
|
+
"TextStyleDelta",
|
|
176
|
+
]
|