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,902 @@
|
|
|
1
|
+
"""PDF Path、Image 与 Form 对象提取,保持原生提取算法与资源语义。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import ctypes
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
import math
|
|
8
|
+
from typing import Any, Iterator
|
|
9
|
+
import pypdfium2 as pdfium
|
|
10
|
+
import pypdfium2.raw as pdfium_c
|
|
11
|
+
from ...schema import BBox
|
|
12
|
+
|
|
13
|
+
from .native_contracts import (
|
|
14
|
+
DRAWING_FORM_MAX_DEPTH,
|
|
15
|
+
DRAWING_LINE_AXIS_ABSOLUTE_TOLERANCE,
|
|
16
|
+
DRAWING_LINE_AXIS_RATIO_TOLERANCE,
|
|
17
|
+
DRAWING_LINE_MERGE_TOLERANCE,
|
|
18
|
+
DRAWING_LINE_MIN_LENGTH,
|
|
19
|
+
DRAWING_THIN_RECT_MAX_THICKNESS,
|
|
20
|
+
DRAWING_THIN_RECT_MIN_ASPECT_RATIO,
|
|
21
|
+
PDFDrawingLine,
|
|
22
|
+
PDFImageInfo,
|
|
23
|
+
PDFPathInfo,
|
|
24
|
+
PDF_IMAGE_FINGERPRINT_MAX_RAW_BYTES,
|
|
25
|
+
_PathSubpath,
|
|
26
|
+
)
|
|
27
|
+
from .native_coordinates import (
|
|
28
|
+
_apply_pdf_matrix,
|
|
29
|
+
_drawing_page_size,
|
|
30
|
+
_get_raw_object_matrix,
|
|
31
|
+
_multiply_pdf_matrices,
|
|
32
|
+
_transform_drawing_point,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("docvortex.document.pdf.document")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _walk_raw_page_objects_with_depth(
|
|
39
|
+
container: Any,
|
|
40
|
+
*,
|
|
41
|
+
is_form: bool,
|
|
42
|
+
parent_matrix: tuple[float, float, float, float, float, float],
|
|
43
|
+
depth: int,
|
|
44
|
+
target_type: int,
|
|
45
|
+
) -> Iterator[tuple[Any, tuple[float, float, float, float, float, float], int]]:
|
|
46
|
+
"""递归遍历目标对象,并携带累积矩阵及所在 Form 深度。"""
|
|
47
|
+
if depth >= DRAWING_FORM_MAX_DEPTH:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
count_objects = pdfium_c.FPDFFormObj_CountObjects if is_form else pdfium_c.FPDFPage_CountObjects
|
|
51
|
+
get_object = pdfium_c.FPDFFormObj_GetObject if is_form else pdfium_c.FPDFPage_GetObject
|
|
52
|
+
try:
|
|
53
|
+
object_count = int(count_objects(container))
|
|
54
|
+
except Exception:
|
|
55
|
+
return
|
|
56
|
+
if object_count < 0:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
for object_index in range(object_count):
|
|
60
|
+
try:
|
|
61
|
+
raw_obj = get_object(container, object_index)
|
|
62
|
+
if not raw_obj:
|
|
63
|
+
continue
|
|
64
|
+
object_type = int(pdfium_c.FPDFPageObj_GetType(raw_obj))
|
|
65
|
+
object_matrix = _get_raw_object_matrix(raw_obj)
|
|
66
|
+
if object_matrix is None:
|
|
67
|
+
continue
|
|
68
|
+
combined_matrix = _multiply_pdf_matrices(object_matrix, parent_matrix)
|
|
69
|
+
except Exception:
|
|
70
|
+
# 单个损坏页面对象不能中断同页其他目标对象遍历。
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if object_type == target_type:
|
|
74
|
+
yield raw_obj, combined_matrix, depth
|
|
75
|
+
if object_type == pdfium_c.FPDF_PAGEOBJ_FORM:
|
|
76
|
+
yield from _walk_raw_page_objects_with_depth(
|
|
77
|
+
raw_obj,
|
|
78
|
+
is_form=True,
|
|
79
|
+
parent_matrix=combined_matrix,
|
|
80
|
+
depth=depth + 1,
|
|
81
|
+
target_type=target_type,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _walk_raw_page_objects(
|
|
86
|
+
container: Any,
|
|
87
|
+
*,
|
|
88
|
+
is_form: bool,
|
|
89
|
+
parent_matrix: tuple[float, float, float, float, float, float],
|
|
90
|
+
depth: int,
|
|
91
|
+
target_type: int,
|
|
92
|
+
) -> Iterator[tuple[Any, tuple[float, float, float, float, float, float]]]:
|
|
93
|
+
"""兼容既有调用,仅返回目标对象及累积到页面坐标的矩阵。"""
|
|
94
|
+
|
|
95
|
+
for raw_obj, matrix, _form_depth in _walk_raw_page_objects_with_depth(
|
|
96
|
+
container,
|
|
97
|
+
is_form=is_form,
|
|
98
|
+
parent_matrix=parent_matrix,
|
|
99
|
+
depth=depth,
|
|
100
|
+
target_type=target_type,
|
|
101
|
+
):
|
|
102
|
+
yield raw_obj, matrix
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _walk_raw_path_objects(
|
|
106
|
+
container: Any,
|
|
107
|
+
*,
|
|
108
|
+
is_form: bool,
|
|
109
|
+
parent_matrix: tuple[float, float, float, float, float, float],
|
|
110
|
+
depth: int,
|
|
111
|
+
) -> Iterator[tuple[Any, tuple[float, float, float, float, float, float]]]:
|
|
112
|
+
"""递归遍历页面或 Form 中的 Path,并携带累积到页面坐标的矩阵。"""
|
|
113
|
+
yield from _walk_raw_page_objects(
|
|
114
|
+
container,
|
|
115
|
+
is_form=is_form,
|
|
116
|
+
parent_matrix=parent_matrix,
|
|
117
|
+
depth=depth,
|
|
118
|
+
target_type=pdfium_c.FPDF_PAGEOBJ_PATH,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _iter_raw_path_objects(
|
|
123
|
+
page: pdfium.PdfPage,
|
|
124
|
+
) -> Iterator[tuple[Any, tuple[float, float, float, float, float, float]]]:
|
|
125
|
+
"""从页面根对象开始遍历全部 Path,包括嵌套 Form 中的 Path。"""
|
|
126
|
+
identity = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
|
127
|
+
yield from _walk_raw_path_objects(
|
|
128
|
+
page,
|
|
129
|
+
is_form=False,
|
|
130
|
+
parent_matrix=identity,
|
|
131
|
+
depth=0,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _iter_raw_path_objects_with_depth(
|
|
136
|
+
page: pdfium.PdfPage,
|
|
137
|
+
) -> Iterator[tuple[Any, tuple[float, float, float, float, float, float], int]]:
|
|
138
|
+
"""遍历全部 Path,并保留其所在 Form 深度供上层过滤图标。"""
|
|
139
|
+
|
|
140
|
+
identity = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
|
141
|
+
yield from _walk_raw_page_objects_with_depth(
|
|
142
|
+
page,
|
|
143
|
+
is_form=False,
|
|
144
|
+
parent_matrix=identity,
|
|
145
|
+
depth=0,
|
|
146
|
+
target_type=pdfium_c.FPDF_PAGEOBJ_PATH,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _iter_raw_image_objects(
|
|
151
|
+
page: pdfium.PdfPage,
|
|
152
|
+
) -> Iterator[tuple[Any, tuple[float, float, float, float, float, float]]]:
|
|
153
|
+
"""从页面根对象开始遍历全部点阵图,包括嵌套 Form 中的图片。"""
|
|
154
|
+
identity = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
|
155
|
+
yield from _walk_raw_page_objects(
|
|
156
|
+
page,
|
|
157
|
+
is_form=False,
|
|
158
|
+
parent_matrix=identity,
|
|
159
|
+
depth=0,
|
|
160
|
+
target_type=pdfium_c.FPDF_PAGEOBJ_IMAGE,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _iter_raw_root_form_objects(page: pdfium.PdfPage) -> Iterator[Any]:
|
|
165
|
+
"""只遍历页面根层的 Form,避免把同一矢量图的嵌套子 Form 重复输出。"""
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
object_count = int(pdfium_c.FPDFPage_CountObjects(page))
|
|
169
|
+
except Exception:
|
|
170
|
+
return
|
|
171
|
+
if object_count < 0:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
for object_index in range(object_count):
|
|
175
|
+
try:
|
|
176
|
+
raw_obj = pdfium_c.FPDFPage_GetObject(page, object_index)
|
|
177
|
+
if raw_obj and int(pdfium_c.FPDFPageObj_GetType(raw_obj)) == pdfium_c.FPDF_PAGEOBJ_FORM:
|
|
178
|
+
yield raw_obj
|
|
179
|
+
except Exception:
|
|
180
|
+
# 单个损坏对象不能阻断同页其他顶层 Form 的提取。
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _get_raw_object_rgba(
|
|
185
|
+
raw_obj: Any,
|
|
186
|
+
color_getter: Any,
|
|
187
|
+
) -> tuple[int, int, int, int] | None:
|
|
188
|
+
"""读取对象 RGBA;旧 PDFium、不支持的对象或读取失败时返回 None。"""
|
|
189
|
+
|
|
190
|
+
red = ctypes.c_uint()
|
|
191
|
+
green = ctypes.c_uint()
|
|
192
|
+
blue = ctypes.c_uint()
|
|
193
|
+
alpha = ctypes.c_uint()
|
|
194
|
+
try:
|
|
195
|
+
ok = color_getter(
|
|
196
|
+
raw_obj,
|
|
197
|
+
ctypes.byref(red),
|
|
198
|
+
ctypes.byref(green),
|
|
199
|
+
ctypes.byref(blue),
|
|
200
|
+
ctypes.byref(alpha),
|
|
201
|
+
)
|
|
202
|
+
except Exception:
|
|
203
|
+
return None
|
|
204
|
+
if not ok:
|
|
205
|
+
return None
|
|
206
|
+
return (
|
|
207
|
+
int(red.value),
|
|
208
|
+
int(green.value),
|
|
209
|
+
int(blue.value),
|
|
210
|
+
int(alpha.value),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _get_raw_object_alpha(raw_obj: Any, color_getter: Any) -> int:
|
|
215
|
+
"""读取对象颜色 alpha;旧 PDFium 或读取失败时按不透明处理。"""
|
|
216
|
+
|
|
217
|
+
rgba = _get_raw_object_rgba(raw_obj, color_getter)
|
|
218
|
+
return rgba[3] if rgba is not None else 255
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _get_path_visibility(raw_obj: Any) -> tuple[bool, bool]:
|
|
222
|
+
"""分别判断 Path 的填充与描边是否实际可见。"""
|
|
223
|
+
fill_mode = ctypes.c_int()
|
|
224
|
+
stroke = ctypes.c_int()
|
|
225
|
+
try:
|
|
226
|
+
ok = pdfium_c.FPDFPath_GetDrawMode(raw_obj, ctypes.byref(fill_mode), ctypes.byref(stroke))
|
|
227
|
+
except Exception:
|
|
228
|
+
return False, False
|
|
229
|
+
if not ok:
|
|
230
|
+
return False, False
|
|
231
|
+
|
|
232
|
+
fill_visible = (
|
|
233
|
+
fill_mode.value != pdfium_c.FPDF_FILLMODE_NONE and _get_raw_object_alpha(raw_obj, pdfium_c.FPDFPageObj_GetFillColor) > 0
|
|
234
|
+
)
|
|
235
|
+
stroke_visible = bool(stroke.value) and _get_raw_object_alpha(raw_obj, pdfium_c.FPDFPageObj_GetStrokeColor) > 0
|
|
236
|
+
return fill_visible, stroke_visible
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _get_raw_stroke_width(raw_obj: Any) -> float:
|
|
240
|
+
"""读取 Path 在对象局部坐标中的描边宽度。"""
|
|
241
|
+
stroke_width = ctypes.c_float()
|
|
242
|
+
try:
|
|
243
|
+
ok = pdfium_c.FPDFPageObj_GetStrokeWidth(raw_obj, ctypes.byref(stroke_width))
|
|
244
|
+
except Exception:
|
|
245
|
+
return 0.0
|
|
246
|
+
if not ok:
|
|
247
|
+
return 0.0
|
|
248
|
+
width = abs(float(stroke_width.value))
|
|
249
|
+
return width if math.isfinite(width) else 0.0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _get_segment_stroke_width(
|
|
253
|
+
raw_width: float,
|
|
254
|
+
start: tuple[float, float],
|
|
255
|
+
end: tuple[float, float],
|
|
256
|
+
matrix: tuple[float, float, float, float, float, float],
|
|
257
|
+
) -> float:
|
|
258
|
+
"""按线段局部法向量换算非等比矩阵下的实际描边宽度。"""
|
|
259
|
+
delta_x = end[0] - start[0]
|
|
260
|
+
delta_y = end[1] - start[1]
|
|
261
|
+
segment_length = math.hypot(delta_x, delta_y)
|
|
262
|
+
a, b, c, d, _, _ = matrix
|
|
263
|
+
if segment_length <= 0:
|
|
264
|
+
scale = math.sqrt(abs(a * d - b * c))
|
|
265
|
+
else:
|
|
266
|
+
normal_x = -delta_y / segment_length
|
|
267
|
+
normal_y = delta_x / segment_length
|
|
268
|
+
transformed_x = a * normal_x + c * normal_y
|
|
269
|
+
transformed_y = b * normal_x + d * normal_y
|
|
270
|
+
scale = math.hypot(transformed_x, transformed_y)
|
|
271
|
+
width = raw_width * scale
|
|
272
|
+
return width if math.isfinite(width) else 0.0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _read_raw_path_subpaths(raw_obj: Any) -> list[_PathSubpath]:
|
|
276
|
+
"""读取 Path 段并拆为子路径,只把 LINETO 与闭合边记录为直线段。"""
|
|
277
|
+
try:
|
|
278
|
+
segment_count = int(pdfium_c.FPDFPath_CountSegments(raw_obj))
|
|
279
|
+
except Exception:
|
|
280
|
+
return []
|
|
281
|
+
if segment_count <= 0:
|
|
282
|
+
return []
|
|
283
|
+
|
|
284
|
+
subpaths: list[_PathSubpath] = []
|
|
285
|
+
current_subpath: _PathSubpath | None = None
|
|
286
|
+
current_point: tuple[float, float] | None = None
|
|
287
|
+
subpath_start: tuple[float, float] | None = None
|
|
288
|
+
|
|
289
|
+
for segment_index in range(segment_count):
|
|
290
|
+
try:
|
|
291
|
+
segment = pdfium_c.FPDFPath_GetPathSegment(raw_obj, segment_index)
|
|
292
|
+
if not segment:
|
|
293
|
+
continue
|
|
294
|
+
x = ctypes.c_float()
|
|
295
|
+
y = ctypes.c_float()
|
|
296
|
+
if not pdfium_c.FPDFPathSegment_GetPoint(segment, ctypes.byref(x), ctypes.byref(y)):
|
|
297
|
+
continue
|
|
298
|
+
point = (float(x.value), float(y.value))
|
|
299
|
+
segment_type = int(pdfium_c.FPDFPathSegment_GetType(segment))
|
|
300
|
+
segment_closes = bool(pdfium_c.FPDFPathSegment_GetClose(segment))
|
|
301
|
+
except Exception:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
if segment_type == pdfium_c.FPDF_SEGMENT_MOVETO:
|
|
305
|
+
if current_subpath is not None and current_subpath.points:
|
|
306
|
+
subpaths.append(current_subpath)
|
|
307
|
+
current_subpath = _PathSubpath(points=[point], straight_segments=[])
|
|
308
|
+
current_point = point
|
|
309
|
+
subpath_start = point
|
|
310
|
+
else:
|
|
311
|
+
if current_subpath is None:
|
|
312
|
+
current_subpath = _PathSubpath(points=[point], straight_segments=[])
|
|
313
|
+
current_point = point
|
|
314
|
+
subpath_start = point
|
|
315
|
+
else:
|
|
316
|
+
current_subpath.points.append(point)
|
|
317
|
+
if segment_type == pdfium_c.FPDF_SEGMENT_LINETO and current_point is not None:
|
|
318
|
+
current_subpath.straight_segments.append((current_point, point))
|
|
319
|
+
current_point = point
|
|
320
|
+
|
|
321
|
+
if segment_closes and current_subpath is not None and current_point is not None and subpath_start is not None:
|
|
322
|
+
if current_point != subpath_start:
|
|
323
|
+
current_subpath.straight_segments.append((current_point, subpath_start))
|
|
324
|
+
current_subpath.closed = True
|
|
325
|
+
current_point = subpath_start
|
|
326
|
+
|
|
327
|
+
if current_subpath is not None and current_subpath.points:
|
|
328
|
+
subpaths.append(current_subpath)
|
|
329
|
+
return subpaths
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _transform_path_subpath(
|
|
333
|
+
subpath: _PathSubpath,
|
|
334
|
+
matrix: tuple[float, float, float, float, float, float],
|
|
335
|
+
page_bbox: BBox,
|
|
336
|
+
page_rotation: int,
|
|
337
|
+
) -> _PathSubpath:
|
|
338
|
+
"""将一个子路径从对象局部坐标转换为页面左上坐标。"""
|
|
339
|
+
|
|
340
|
+
def transform(point: tuple[float, float]) -> tuple[float, float]:
|
|
341
|
+
"""先应用对象/Form 矩阵,再应用页面坐标与旋转转换。"""
|
|
342
|
+
return _transform_drawing_point(_apply_pdf_matrix(point, matrix), page_bbox, page_rotation)
|
|
343
|
+
|
|
344
|
+
return _PathSubpath(
|
|
345
|
+
points=[transform(point) for point in subpath.points],
|
|
346
|
+
straight_segments=[(transform(start), transform(end)) for start, end in subpath.straight_segments],
|
|
347
|
+
closed=subpath.closed,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _path_info_from_object(
|
|
352
|
+
raw_obj: Any,
|
|
353
|
+
matrix: tuple[float, float, float, float, float, float],
|
|
354
|
+
page_bbox: BBox,
|
|
355
|
+
page_rotation: int,
|
|
356
|
+
form_depth: int,
|
|
357
|
+
source_index: int,
|
|
358
|
+
*,
|
|
359
|
+
subpaths: list[_PathSubpath] | None = None,
|
|
360
|
+
) -> PDFPathInfo | None:
|
|
361
|
+
"""将一个原始 Path 转换为页面几何;贝塞尔控制点也纳入保守 bbox。"""
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
segment_count = int(pdfium_c.FPDFPath_CountSegments(raw_obj))
|
|
365
|
+
except Exception:
|
|
366
|
+
return None
|
|
367
|
+
if segment_count <= 0:
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
fill_visible, stroke_visible = _get_path_visibility(raw_obj)
|
|
371
|
+
points = [
|
|
372
|
+
point
|
|
373
|
+
for raw_subpath in (_read_raw_path_subpaths(raw_obj) if subpaths is None else subpaths)
|
|
374
|
+
for point in _transform_path_subpath(
|
|
375
|
+
raw_subpath,
|
|
376
|
+
matrix,
|
|
377
|
+
page_bbox,
|
|
378
|
+
page_rotation,
|
|
379
|
+
).points
|
|
380
|
+
]
|
|
381
|
+
if not points:
|
|
382
|
+
return None
|
|
383
|
+
coordinates = [coordinate for point in points for coordinate in point]
|
|
384
|
+
if not all(math.isfinite(value) for value in coordinates):
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
page_width, page_height = _drawing_page_size(page_bbox, page_rotation)
|
|
388
|
+
stroke_margin = 0.0
|
|
389
|
+
if stroke_visible:
|
|
390
|
+
a, b, c, d, _, _ = matrix
|
|
391
|
+
stroke_scale = max(math.hypot(a, b), math.hypot(c, d))
|
|
392
|
+
stroke_margin = 0.5 * _get_raw_stroke_width(raw_obj) * stroke_scale
|
|
393
|
+
bbox = (
|
|
394
|
+
max(0.0, min(point[0] for point in points) - stroke_margin),
|
|
395
|
+
max(0.0, min(point[1] for point in points) - stroke_margin),
|
|
396
|
+
min(page_width, max(point[0] for point in points) + stroke_margin),
|
|
397
|
+
min(page_height, max(point[1] for point in points) + stroke_margin),
|
|
398
|
+
)
|
|
399
|
+
if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]:
|
|
400
|
+
return None
|
|
401
|
+
return PDFPathInfo(
|
|
402
|
+
bbox=bbox,
|
|
403
|
+
segment_count=segment_count,
|
|
404
|
+
fill_visible=fill_visible,
|
|
405
|
+
stroke_visible=stroke_visible,
|
|
406
|
+
form_depth=form_depth,
|
|
407
|
+
source_index=source_index,
|
|
408
|
+
fill_rgba=(
|
|
409
|
+
_get_raw_object_rgba(
|
|
410
|
+
raw_obj,
|
|
411
|
+
pdfium_c.FPDFPageObj_GetFillColor,
|
|
412
|
+
)
|
|
413
|
+
if fill_visible
|
|
414
|
+
else None
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _get_thin_filled_subpath_line(
|
|
420
|
+
subpath: _PathSubpath,
|
|
421
|
+
page_size: tuple[float, float],
|
|
422
|
+
) -> PDFDrawingLine | None:
|
|
423
|
+
"""把闭合的细长填充子路径折叠为一条中心线,避免把矩形四边重复输出。"""
|
|
424
|
+
if not subpath.closed or len(subpath.points) < 4:
|
|
425
|
+
return None
|
|
426
|
+
x_values = [point[0] for point in subpath.points]
|
|
427
|
+
y_values = [point[1] for point in subpath.points]
|
|
428
|
+
x0, x1 = min(x_values), max(x_values)
|
|
429
|
+
y0, y1 = min(y_values), max(y_values)
|
|
430
|
+
width = x1 - x0
|
|
431
|
+
height = y1 - y0
|
|
432
|
+
long_side = max(width, height)
|
|
433
|
+
short_side = min(width, height)
|
|
434
|
+
if (
|
|
435
|
+
long_side < DRAWING_LINE_MIN_LENGTH
|
|
436
|
+
or short_side > DRAWING_THIN_RECT_MAX_THICKNESS
|
|
437
|
+
or long_side < DRAWING_THIN_RECT_MIN_ASPECT_RATIO * max(short_side, 0.01)
|
|
438
|
+
):
|
|
439
|
+
return None
|
|
440
|
+
if width >= height:
|
|
441
|
+
return _make_axis_drawing_line((x0, (y0 + y1) / 2), (x1, (y0 + y1) / 2), short_side, page_size)
|
|
442
|
+
return _make_axis_drawing_line(((x0 + x1) / 2, y0), ((x0 + x1) / 2, y1), short_side, page_size)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _make_axis_drawing_line(
|
|
446
|
+
start: tuple[float, float],
|
|
447
|
+
end: tuple[float, float],
|
|
448
|
+
width: float,
|
|
449
|
+
page_size: tuple[float, float],
|
|
450
|
+
) -> PDFDrawingLine | None:
|
|
451
|
+
"""将近水平或近竖直线段吸附到坐标轴、裁剪到页面并生成公开结果。"""
|
|
452
|
+
page_width, page_height = page_size
|
|
453
|
+
x0, y0 = start
|
|
454
|
+
x1, y1 = end
|
|
455
|
+
if not all(math.isfinite(value) for value in (x0, y0, x1, y1, width)):
|
|
456
|
+
return None
|
|
457
|
+
delta_x = abs(x1 - x0)
|
|
458
|
+
delta_y = abs(y1 - y0)
|
|
459
|
+
|
|
460
|
+
if delta_x >= delta_y and delta_y <= max(
|
|
461
|
+
DRAWING_LINE_AXIS_ABSOLUTE_TOLERANCE,
|
|
462
|
+
delta_x * DRAWING_LINE_AXIS_RATIO_TOLERANCE,
|
|
463
|
+
):
|
|
464
|
+
coordinate = (y0 + y1) / 2
|
|
465
|
+
if coordinate < 0 or coordinate > page_height:
|
|
466
|
+
return None
|
|
467
|
+
main_start = max(0.0, min(x0, x1))
|
|
468
|
+
main_end = min(page_width, max(x0, x1))
|
|
469
|
+
if main_end - main_start < DRAWING_LINE_MIN_LENGTH:
|
|
470
|
+
return None
|
|
471
|
+
line_width = max(0.0, width, delta_y)
|
|
472
|
+
half_width = line_width / 2
|
|
473
|
+
bbox = (
|
|
474
|
+
main_start,
|
|
475
|
+
max(0.0, coordinate - half_width),
|
|
476
|
+
main_end,
|
|
477
|
+
min(page_height, coordinate + half_width),
|
|
478
|
+
)
|
|
479
|
+
return PDFDrawingLine(
|
|
480
|
+
start=(main_start, coordinate),
|
|
481
|
+
end=(main_end, coordinate),
|
|
482
|
+
bbox=bbox,
|
|
483
|
+
width=line_width,
|
|
484
|
+
orientation="horizontal",
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
if delta_y > delta_x and delta_x <= max(
|
|
488
|
+
DRAWING_LINE_AXIS_ABSOLUTE_TOLERANCE,
|
|
489
|
+
delta_y * DRAWING_LINE_AXIS_RATIO_TOLERANCE,
|
|
490
|
+
):
|
|
491
|
+
coordinate = (x0 + x1) / 2
|
|
492
|
+
if coordinate < 0 or coordinate > page_width:
|
|
493
|
+
return None
|
|
494
|
+
main_start = max(0.0, min(y0, y1))
|
|
495
|
+
main_end = min(page_height, max(y0, y1))
|
|
496
|
+
if main_end - main_start < DRAWING_LINE_MIN_LENGTH:
|
|
497
|
+
return None
|
|
498
|
+
line_width = max(0.0, width, delta_x)
|
|
499
|
+
half_width = line_width / 2
|
|
500
|
+
bbox = (
|
|
501
|
+
max(0.0, coordinate - half_width),
|
|
502
|
+
main_start,
|
|
503
|
+
min(page_width, coordinate + half_width),
|
|
504
|
+
main_end,
|
|
505
|
+
)
|
|
506
|
+
return PDFDrawingLine(
|
|
507
|
+
start=(coordinate, main_start),
|
|
508
|
+
end=(coordinate, main_end),
|
|
509
|
+
bbox=bbox,
|
|
510
|
+
width=line_width,
|
|
511
|
+
orientation="vertical",
|
|
512
|
+
)
|
|
513
|
+
return None
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _extract_path_drawing_lines(
|
|
517
|
+
raw_obj: Any,
|
|
518
|
+
matrix: tuple[float, float, float, float, float, float],
|
|
519
|
+
page_bbox: BBox,
|
|
520
|
+
page_rotation: int,
|
|
521
|
+
*,
|
|
522
|
+
subpaths: list[_PathSubpath] | None = None,
|
|
523
|
+
) -> list[PDFDrawingLine]:
|
|
524
|
+
"""从单个 Path 提取可见直线,坏 Path 返回空结果且不影响同页其他对象。"""
|
|
525
|
+
fill_visible, stroke_visible = _get_path_visibility(raw_obj)
|
|
526
|
+
if not fill_visible and not stroke_visible:
|
|
527
|
+
return []
|
|
528
|
+
|
|
529
|
+
page_size = _drawing_page_size(page_bbox, page_rotation)
|
|
530
|
+
raw_stroke_width = _get_raw_stroke_width(raw_obj) if stroke_visible else 0.0
|
|
531
|
+
drawing_lines: list[PDFDrawingLine] = []
|
|
532
|
+
for raw_subpath in _read_raw_path_subpaths(raw_obj) if subpaths is None else subpaths:
|
|
533
|
+
subpath = _transform_path_subpath(raw_subpath, matrix, page_bbox, page_rotation)
|
|
534
|
+
if fill_visible:
|
|
535
|
+
filled_line = _get_thin_filled_subpath_line(subpath, page_size)
|
|
536
|
+
if filled_line is not None:
|
|
537
|
+
drawing_lines.append(filled_line)
|
|
538
|
+
# 细长填充矩形已经折叠为中心线,不再输出其描边四条边。
|
|
539
|
+
continue
|
|
540
|
+
if not stroke_visible:
|
|
541
|
+
continue
|
|
542
|
+
for (raw_start, raw_end), (segment_start, segment_end) in zip(
|
|
543
|
+
raw_subpath.straight_segments,
|
|
544
|
+
subpath.straight_segments,
|
|
545
|
+
):
|
|
546
|
+
stroke_width = _get_segment_stroke_width(raw_stroke_width, raw_start, raw_end, matrix)
|
|
547
|
+
drawing_line = _make_axis_drawing_line(segment_start, segment_end, stroke_width, page_size)
|
|
548
|
+
if drawing_line is not None:
|
|
549
|
+
drawing_lines.append(drawing_line)
|
|
550
|
+
return drawing_lines
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _line_axis_coordinate(line: PDFDrawingLine) -> float:
|
|
554
|
+
"""返回绘图线在垂直于自身方向的轴坐标。"""
|
|
555
|
+
return line.start[1] if line.orientation == "horizontal" else line.start[0]
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _line_main_interval(line: PDFDrawingLine) -> tuple[float, float]:
|
|
559
|
+
"""返回绘图线沿自身方向的起止区间。"""
|
|
560
|
+
if line.orientation == "horizontal":
|
|
561
|
+
return line.start[0], line.end[0]
|
|
562
|
+
return line.start[1], line.end[1]
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _combine_collinear_line_group(
|
|
566
|
+
lines: list[PDFDrawingLine],
|
|
567
|
+
page_size: tuple[float, float],
|
|
568
|
+
) -> PDFDrawingLine | None:
|
|
569
|
+
"""把坐标接近且区间相连的一组线合并为一条稳定中心线。"""
|
|
570
|
+
orientation = lines[0].orientation
|
|
571
|
+
intervals = [_line_main_interval(line) for line in lines]
|
|
572
|
+
lengths = [max(end - start, 0.01) for start, end in intervals]
|
|
573
|
+
coordinates = [_line_axis_coordinate(line) for line in lines]
|
|
574
|
+
total_length = sum(lengths)
|
|
575
|
+
coordinate = sum(value * length for value, length in zip(coordinates, lengths)) / total_length
|
|
576
|
+
coordinate_span = max(coordinates) - min(coordinates)
|
|
577
|
+
width = max(max(line.width for line in lines), coordinate_span + max(line.width for line in lines))
|
|
578
|
+
main_start = min(start for start, _ in intervals)
|
|
579
|
+
main_end = max(end for _, end in intervals)
|
|
580
|
+
if orientation == "horizontal":
|
|
581
|
+
return _make_axis_drawing_line((main_start, coordinate), (main_end, coordinate), width, page_size)
|
|
582
|
+
return _make_axis_drawing_line((coordinate, main_start), (coordinate, main_end), width, page_size)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _merge_orientation_lines(
|
|
586
|
+
lines: list[PDFDrawingLine],
|
|
587
|
+
page_size: tuple[float, float],
|
|
588
|
+
) -> list[PDFDrawingLine]:
|
|
589
|
+
"""按轴坐标聚类,再合并间距不超过约 2pt 的同方向线段。"""
|
|
590
|
+
if not lines:
|
|
591
|
+
return []
|
|
592
|
+
coordinate_clusters: list[list[PDFDrawingLine]] = []
|
|
593
|
+
for line in sorted(lines, key=lambda item: (_line_axis_coordinate(item), _line_main_interval(item)[0])):
|
|
594
|
+
if (
|
|
595
|
+
not coordinate_clusters
|
|
596
|
+
or _line_axis_coordinate(line) - min(_line_axis_coordinate(item) for item in coordinate_clusters[-1])
|
|
597
|
+
> DRAWING_LINE_MERGE_TOLERANCE
|
|
598
|
+
):
|
|
599
|
+
coordinate_clusters.append([line])
|
|
600
|
+
else:
|
|
601
|
+
coordinate_clusters[-1].append(line)
|
|
602
|
+
|
|
603
|
+
merged: list[PDFDrawingLine] = []
|
|
604
|
+
for cluster in coordinate_clusters:
|
|
605
|
+
interval_group: list[PDFDrawingLine] = []
|
|
606
|
+
interval_end = -math.inf
|
|
607
|
+
for line in sorted(cluster, key=lambda item: _line_main_interval(item)[0]):
|
|
608
|
+
line_start, line_end = _line_main_interval(line)
|
|
609
|
+
if interval_group and line_start > interval_end + DRAWING_LINE_MERGE_TOLERANCE:
|
|
610
|
+
combined = _combine_collinear_line_group(interval_group, page_size)
|
|
611
|
+
if combined is not None:
|
|
612
|
+
merged.append(combined)
|
|
613
|
+
interval_group = []
|
|
614
|
+
interval_end = -math.inf
|
|
615
|
+
interval_group.append(line)
|
|
616
|
+
interval_end = max(interval_end, line_end)
|
|
617
|
+
if interval_group:
|
|
618
|
+
combined = _combine_collinear_line_group(interval_group, page_size)
|
|
619
|
+
if combined is not None:
|
|
620
|
+
merged.append(combined)
|
|
621
|
+
return merged
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _merge_collinear_drawing_lines(
|
|
625
|
+
lines: list[PDFDrawingLine],
|
|
626
|
+
page_size: tuple[float, float],
|
|
627
|
+
) -> list[PDFDrawingLine]:
|
|
628
|
+
"""合并水平和竖直共线段,并按页面视觉位置稳定排序。"""
|
|
629
|
+
horizontal = _merge_orientation_lines(
|
|
630
|
+
[line for line in lines if line.orientation == "horizontal"],
|
|
631
|
+
page_size,
|
|
632
|
+
)
|
|
633
|
+
vertical = _merge_orientation_lines(
|
|
634
|
+
[line for line in lines if line.orientation == "vertical"],
|
|
635
|
+
page_size,
|
|
636
|
+
)
|
|
637
|
+
return sorted(
|
|
638
|
+
[*horizontal, *vertical],
|
|
639
|
+
key=lambda line: (line.bbox[1], line.bbox[0], line.orientation),
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _image_bbox_from_matrix(
|
|
644
|
+
matrix: tuple[float, float, float, float, float, float],
|
|
645
|
+
page_bbox: BBox,
|
|
646
|
+
page_rotation: int,
|
|
647
|
+
) -> BBox | None:
|
|
648
|
+
"""把点阵图单位矩形经对象/Form 矩阵转换并裁剪为页面左上坐标 bbox。"""
|
|
649
|
+
page_points = [
|
|
650
|
+
_transform_drawing_point(
|
|
651
|
+
_apply_pdf_matrix(point, matrix),
|
|
652
|
+
page_bbox,
|
|
653
|
+
page_rotation,
|
|
654
|
+
)
|
|
655
|
+
for point in ((0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (1.0, 1.0))
|
|
656
|
+
]
|
|
657
|
+
coordinates = [coordinate for point in page_points for coordinate in point]
|
|
658
|
+
if not all(math.isfinite(value) for value in coordinates):
|
|
659
|
+
return None
|
|
660
|
+
|
|
661
|
+
page_width, page_height = _drawing_page_size(page_bbox, page_rotation)
|
|
662
|
+
left = max(0.0, min(point[0] for point in page_points))
|
|
663
|
+
top = max(0.0, min(point[1] for point in page_points))
|
|
664
|
+
right = min(page_width, max(point[0] for point in page_points))
|
|
665
|
+
bottom = min(page_height, max(point[1] for point in page_points))
|
|
666
|
+
if right <= left or bottom <= top:
|
|
667
|
+
return None
|
|
668
|
+
return left, top, right, bottom
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _form_bbox_from_object(
|
|
672
|
+
raw_obj: Any,
|
|
673
|
+
page_bbox: BBox,
|
|
674
|
+
page_rotation: int,
|
|
675
|
+
) -> BBox | None:
|
|
676
|
+
"""读取顶层 Form 的页面坐标边界,并转换、裁剪到视觉页面范围。"""
|
|
677
|
+
|
|
678
|
+
left = ctypes.c_float()
|
|
679
|
+
bottom = ctypes.c_float()
|
|
680
|
+
right = ctypes.c_float()
|
|
681
|
+
top = ctypes.c_float()
|
|
682
|
+
try:
|
|
683
|
+
ok = pdfium_c.FPDFPageObj_GetBounds(
|
|
684
|
+
raw_obj,
|
|
685
|
+
ctypes.byref(left),
|
|
686
|
+
ctypes.byref(bottom),
|
|
687
|
+
ctypes.byref(right),
|
|
688
|
+
ctypes.byref(top),
|
|
689
|
+
)
|
|
690
|
+
except Exception:
|
|
691
|
+
return None
|
|
692
|
+
if not ok:
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
raw_bbox = (float(left.value), float(bottom.value), float(right.value), float(top.value))
|
|
696
|
+
if not all(math.isfinite(value) for value in raw_bbox):
|
|
697
|
+
return None
|
|
698
|
+
page_points = [
|
|
699
|
+
_transform_drawing_point(point, page_bbox, page_rotation)
|
|
700
|
+
for point in (
|
|
701
|
+
(raw_bbox[0], raw_bbox[1]),
|
|
702
|
+
(raw_bbox[0], raw_bbox[3]),
|
|
703
|
+
(raw_bbox[2], raw_bbox[1]),
|
|
704
|
+
(raw_bbox[2], raw_bbox[3]),
|
|
705
|
+
)
|
|
706
|
+
]
|
|
707
|
+
page_width, page_height = _drawing_page_size(page_bbox, page_rotation)
|
|
708
|
+
visual_left = max(0.0, min(point[0] for point in page_points))
|
|
709
|
+
visual_top = max(0.0, min(point[1] for point in page_points))
|
|
710
|
+
visual_right = min(page_width, max(point[0] for point in page_points))
|
|
711
|
+
visual_bottom = min(page_height, max(point[1] for point in page_points))
|
|
712
|
+
if visual_right <= visual_left or visual_bottom <= visual_top:
|
|
713
|
+
return None
|
|
714
|
+
return visual_left, visual_top, visual_right, visual_bottom
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _extract_page_image_bboxes(
|
|
718
|
+
page: pdfium.PdfPage,
|
|
719
|
+
page_bbox: BBox,
|
|
720
|
+
page_rotation: int,
|
|
721
|
+
) -> list[BBox]:
|
|
722
|
+
"""在调用方持有 PDFium 锁时提取全部有效点阵图 bbox,并隔离单对象异常。"""
|
|
723
|
+
image_bboxes: list[BBox] = []
|
|
724
|
+
for _raw_obj, matrix in _iter_raw_image_objects(page):
|
|
725
|
+
try:
|
|
726
|
+
image_bbox = _image_bbox_from_matrix(matrix, page_bbox, page_rotation)
|
|
727
|
+
except Exception:
|
|
728
|
+
# 单个损坏 Image 对象不能中断同页其他点阵图提取。
|
|
729
|
+
continue
|
|
730
|
+
if image_bbox is not None:
|
|
731
|
+
image_bboxes.append(image_bbox)
|
|
732
|
+
return sorted(image_bboxes, key=lambda bbox: (bbox[1], bbox[0], bbox[3], bbox[2]))
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _get_raw_image_fingerprint(raw_obj: Any, page: pdfium.PdfPage) -> str | None:
|
|
736
|
+
"""读取受限大小的图片原始流,并结合像素宽高生成 SHA-256 指纹。"""
|
|
737
|
+
|
|
738
|
+
metadata = pdfium_c.FPDF_IMAGEOBJ_METADATA()
|
|
739
|
+
try:
|
|
740
|
+
if not pdfium_c.FPDFImageObj_GetImageMetadata(
|
|
741
|
+
raw_obj,
|
|
742
|
+
page.raw,
|
|
743
|
+
ctypes.byref(metadata),
|
|
744
|
+
):
|
|
745
|
+
return None
|
|
746
|
+
width = int(metadata.width)
|
|
747
|
+
height = int(metadata.height)
|
|
748
|
+
raw_size = int(pdfium_c.FPDFImageObj_GetImageDataRaw(raw_obj, None, 0))
|
|
749
|
+
except Exception:
|
|
750
|
+
return None
|
|
751
|
+
if width <= 0 or height <= 0 or raw_size <= 0 or raw_size > PDF_IMAGE_FINGERPRINT_MAX_RAW_BYTES:
|
|
752
|
+
return None
|
|
753
|
+
|
|
754
|
+
try:
|
|
755
|
+
buffer = ctypes.create_string_buffer(raw_size)
|
|
756
|
+
except (MemoryError, OverflowError):
|
|
757
|
+
return None
|
|
758
|
+
try:
|
|
759
|
+
bytes_read = int(pdfium_c.FPDFImageObj_GetImageDataRaw(raw_obj, buffer, raw_size))
|
|
760
|
+
except Exception:
|
|
761
|
+
return None
|
|
762
|
+
if bytes_read <= 0 or bytes_read > raw_size:
|
|
763
|
+
return None
|
|
764
|
+
|
|
765
|
+
digest = hashlib.sha256()
|
|
766
|
+
digest.update(f"{width}:{height}:".encode("ascii"))
|
|
767
|
+
digest.update(buffer.raw[:bytes_read])
|
|
768
|
+
return digest.hexdigest()
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _extract_page_image_infos(
|
|
772
|
+
page: pdfium.PdfPage,
|
|
773
|
+
page_bbox: BBox,
|
|
774
|
+
page_rotation: int,
|
|
775
|
+
) -> list[PDFImageInfo]:
|
|
776
|
+
"""提取全部有效点阵图几何;单图指纹失败时保留 bbox 并按放行处理。"""
|
|
777
|
+
|
|
778
|
+
image_infos: list[PDFImageInfo] = []
|
|
779
|
+
for raw_obj, matrix in _iter_raw_image_objects(page):
|
|
780
|
+
try:
|
|
781
|
+
image_bbox = _image_bbox_from_matrix(matrix, page_bbox, page_rotation)
|
|
782
|
+
except Exception:
|
|
783
|
+
# 单个损坏 Image 对象不能中断同页其他点阵图提取。
|
|
784
|
+
continue
|
|
785
|
+
if image_bbox is None:
|
|
786
|
+
continue
|
|
787
|
+
image_infos.append(
|
|
788
|
+
PDFImageInfo(
|
|
789
|
+
bbox=image_bbox,
|
|
790
|
+
fingerprint=_get_raw_image_fingerprint(raw_obj, page),
|
|
791
|
+
)
|
|
792
|
+
)
|
|
793
|
+
return sorted(
|
|
794
|
+
image_infos,
|
|
795
|
+
key=lambda info: (info.bbox[1], info.bbox[0], info.bbox[3], info.bbox[2]),
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _extract_page_form_bboxes(
|
|
800
|
+
page: pdfium.PdfPage,
|
|
801
|
+
page_bbox: BBox,
|
|
802
|
+
page_rotation: int,
|
|
803
|
+
) -> list[BBox]:
|
|
804
|
+
"""在调用方持有 PDFium 锁时提取顶层 Form bbox,并隔离单对象异常。"""
|
|
805
|
+
|
|
806
|
+
form_bboxes: list[BBox] = []
|
|
807
|
+
for raw_obj in _iter_raw_root_form_objects(page):
|
|
808
|
+
try:
|
|
809
|
+
form_bbox = _form_bbox_from_object(raw_obj, page_bbox, page_rotation)
|
|
810
|
+
except Exception:
|
|
811
|
+
# PDFium 遇到个别损坏 Form 时,保留同页其他有效结果。
|
|
812
|
+
continue
|
|
813
|
+
if form_bbox is not None:
|
|
814
|
+
form_bboxes.append(form_bbox)
|
|
815
|
+
return sorted(form_bboxes, key=lambda bbox: (bbox[1], bbox[0], bbox[3], bbox[2]))
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _extract_page_path_infos(
|
|
819
|
+
page: pdfium.PdfPage,
|
|
820
|
+
page_bbox: BBox,
|
|
821
|
+
page_rotation: int,
|
|
822
|
+
) -> list[PDFPathInfo]:
|
|
823
|
+
"""在调用方持有 PDFium 锁时提取 Path 信息,并隔离单对象异常。"""
|
|
824
|
+
|
|
825
|
+
path_infos: list[PDFPathInfo] = []
|
|
826
|
+
for source_index, (raw_obj, matrix, form_depth) in enumerate(_iter_raw_path_objects_with_depth(page)):
|
|
827
|
+
try:
|
|
828
|
+
path_info = _path_info_from_object(
|
|
829
|
+
raw_obj,
|
|
830
|
+
matrix,
|
|
831
|
+
page_bbox,
|
|
832
|
+
page_rotation,
|
|
833
|
+
form_depth,
|
|
834
|
+
source_index,
|
|
835
|
+
)
|
|
836
|
+
except Exception:
|
|
837
|
+
# PDFium 遇到个别损坏 Path 时,保留同页其他对象的有效结果。
|
|
838
|
+
continue
|
|
839
|
+
if path_info is not None:
|
|
840
|
+
path_infos.append(path_info)
|
|
841
|
+
return path_infos
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _extract_page_paths_and_lines(
|
|
845
|
+
page: pdfium.PdfPage,
|
|
846
|
+
page_bbox: BBox,
|
|
847
|
+
page_rotation: int,
|
|
848
|
+
) -> tuple[list[PDFDrawingLine], list[PDFPathInfo]]:
|
|
849
|
+
"""一次遍历和解码 Path,分别隔离绘图线与路径信息的派生异常。"""
|
|
850
|
+
|
|
851
|
+
drawing_lines: list[PDFDrawingLine] = []
|
|
852
|
+
path_infos: list[PDFPathInfo] = []
|
|
853
|
+
for source_index, (raw_obj, matrix, form_depth) in enumerate(_iter_raw_path_objects_with_depth(page)):
|
|
854
|
+
try:
|
|
855
|
+
subpaths = _read_raw_path_subpaths(raw_obj)
|
|
856
|
+
except Exception:
|
|
857
|
+
continue
|
|
858
|
+
try:
|
|
859
|
+
drawing_lines.extend(_extract_path_drawing_lines(raw_obj, matrix, page_bbox, page_rotation, subpaths=subpaths))
|
|
860
|
+
except Exception:
|
|
861
|
+
pass
|
|
862
|
+
try:
|
|
863
|
+
info = _path_info_from_object(
|
|
864
|
+
raw_obj,
|
|
865
|
+
matrix,
|
|
866
|
+
page_bbox,
|
|
867
|
+
page_rotation,
|
|
868
|
+
form_depth,
|
|
869
|
+
source_index,
|
|
870
|
+
subpaths=subpaths,
|
|
871
|
+
)
|
|
872
|
+
except Exception:
|
|
873
|
+
continue
|
|
874
|
+
if info is not None:
|
|
875
|
+
path_infos.append(info)
|
|
876
|
+
return _merge_collinear_drawing_lines(drawing_lines, _drawing_page_size(page_bbox, page_rotation)), path_infos
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _extract_page_drawing_lines(
|
|
880
|
+
page: pdfium.PdfPage,
|
|
881
|
+
page_bbox: BBox,
|
|
882
|
+
page_rotation: int,
|
|
883
|
+
) -> list[PDFDrawingLine]:
|
|
884
|
+
"""在调用方持有 PDFium 锁时提取整页绘图线,并隔离单个对象异常。"""
|
|
885
|
+
drawing_lines: list[PDFDrawingLine] = []
|
|
886
|
+
for raw_obj, matrix in _iter_raw_path_objects(page):
|
|
887
|
+
try:
|
|
888
|
+
drawing_lines.extend(
|
|
889
|
+
_extract_path_drawing_lines(
|
|
890
|
+
raw_obj,
|
|
891
|
+
matrix,
|
|
892
|
+
page_bbox,
|
|
893
|
+
page_rotation,
|
|
894
|
+
)
|
|
895
|
+
)
|
|
896
|
+
except Exception:
|
|
897
|
+
# PDFium 遇到个别损坏 Path 时,保留同页其他对象的有效结果。
|
|
898
|
+
continue
|
|
899
|
+
return _merge_collinear_drawing_lines(
|
|
900
|
+
drawing_lines,
|
|
901
|
+
_drawing_page_size(page_bbox, page_rotation),
|
|
902
|
+
)
|