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,654 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import multiprocessing
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from concurrent.futures import ALL_COMPLETED, Future, ProcessPoolExecutor, wait
|
|
9
|
+
from concurrent.futures.process import BrokenProcessPool
|
|
10
|
+
import math
|
|
11
|
+
from typing import Any, Callable, Literal
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pypdfium2 as pdfium
|
|
15
|
+
from loguru import logger
|
|
16
|
+
from PIL import Image
|
|
17
|
+
|
|
18
|
+
from .pdfium import close_pdfium_child, close_pdfium_document, initialize_pdfium_runtime, pdfium_guard
|
|
19
|
+
from docvortex.foundation.image_encoding import image_to_b64str
|
|
20
|
+
from .raster import page_to_image
|
|
21
|
+
from ...schema import BBox, IntBBox
|
|
22
|
+
from ...foundation.geometry import normalize_to_int_bbox
|
|
23
|
+
from ...foundation.platform import is_windows_environment
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ImageType:
|
|
27
|
+
"""限定 PDF 页面渲染支持的两种图像返回形态。"""
|
|
28
|
+
|
|
29
|
+
PIL = "pil_img"
|
|
30
|
+
BASE64 = "base64_img"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _positive_int_env(name: str, default: int) -> int:
|
|
34
|
+
"""读取正整数环境变量,缺失或非法时返回默认值。"""
|
|
35
|
+
raw_value = os.getenv(name)
|
|
36
|
+
if raw_value is None:
|
|
37
|
+
return default
|
|
38
|
+
try:
|
|
39
|
+
value = int(raw_value)
|
|
40
|
+
except ValueError:
|
|
41
|
+
return default
|
|
42
|
+
return value if value > 0 else default
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_load_images_timeout() -> int:
|
|
46
|
+
"""返回 PDF 批量渲染的超时秒数。"""
|
|
47
|
+
return _positive_int_env("DOCVORTEX_PDF_RENDER_TIMEOUT", 300)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_load_images_threads() -> int:
|
|
51
|
+
"""返回 PDF 批量渲染允许使用的进程数。"""
|
|
52
|
+
return _positive_int_env("DOCVORTEX_PDF_RENDER_THREADS", 3)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
DEFAULT_PDF_IMAGE_DPI = 200
|
|
56
|
+
# DEFAULT_PDF_IMAGE_DPI = 144
|
|
57
|
+
MAX_PDF_RENDER_PROCESSES = 3
|
|
58
|
+
MIN_PAGES_PER_RENDER_PROCESS = 30
|
|
59
|
+
PDF_RENDER_PROCESS_SPAWN_DELAY_SECONDS = 0.1
|
|
60
|
+
PDF_RENDER_TERMINATE_GRACE_PERIOD_SECONDS = 0.1
|
|
61
|
+
PDF_RENDER_KILL_JOIN_TIMEOUT_SECONDS = 0.1
|
|
62
|
+
|
|
63
|
+
_pdf_render_executor: ProcessPoolExecutor | None = None
|
|
64
|
+
_pdf_render_executor_lock = threading.Lock()
|
|
65
|
+
_pdf_render_atexit_registered = False
|
|
66
|
+
_pdf_render_spawn_submit_lock = threading.Lock()
|
|
67
|
+
_pdf_render_spawn_submit_executor_id: int | None = None
|
|
68
|
+
_pdf_render_spawn_submit_count = 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def pdf_page_to_image(
|
|
72
|
+
page: pdfium.PdfPage,
|
|
73
|
+
dpi: int = DEFAULT_PDF_IMAGE_DPI,
|
|
74
|
+
image_type: Literal["pil_img", "base64_img"] = ImageType.PIL,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
"""将单个 PDFium 页面渲染为 Pillow 图像或 base64 载荷。"""
|
|
77
|
+
pil_img, scale = page_to_image(page, dpi=dpi)
|
|
78
|
+
image_dict: dict[str, Any] = {"scale": scale}
|
|
79
|
+
if image_type == ImageType.BASE64:
|
|
80
|
+
try:
|
|
81
|
+
image_dict["img_base64"] = image_to_b64str(pil_img)
|
|
82
|
+
finally:
|
|
83
|
+
pil_img.close()
|
|
84
|
+
else:
|
|
85
|
+
image_dict["img_pil"] = pil_img
|
|
86
|
+
|
|
87
|
+
return image_dict
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _load_images_from_pdf_worker(
|
|
91
|
+
pdf_bytes: bytes,
|
|
92
|
+
dpi: int,
|
|
93
|
+
start_page_id: int,
|
|
94
|
+
end_page_id: int,
|
|
95
|
+
image_type: Literal["pil_img", "base64_img"],
|
|
96
|
+
) -> list[dict[str, Any]]:
|
|
97
|
+
"""用于进程池的包装函数"""
|
|
98
|
+
started_at = time.monotonic()
|
|
99
|
+
worker_pid = os.getpid()
|
|
100
|
+
page_range = f"{start_page_id + 1}-{end_page_id + 1}"
|
|
101
|
+
logger.debug(
|
|
102
|
+
f"PDF render worker started: pid={worker_pid}, pages={page_range}, dpi={dpi}, "
|
|
103
|
+
f"image_type={image_type}, pdf_bytes={len(pdf_bytes)}"
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
images = load_images_from_pdf_core(pdf_bytes, dpi, start_page_id, end_page_id, image_type)
|
|
107
|
+
except Exception:
|
|
108
|
+
elapsed = time.monotonic() - started_at
|
|
109
|
+
logger.exception(f"PDF render worker failed: pid={worker_pid}, pages={page_range}, elapsed={elapsed:.3f}s")
|
|
110
|
+
raise
|
|
111
|
+
|
|
112
|
+
elapsed = time.monotonic() - started_at
|
|
113
|
+
logger.debug(
|
|
114
|
+
f"PDF render worker completed: pid={worker_pid}, pages={page_range}, elapsed={elapsed:.3f}s, images={len(images)}"
|
|
115
|
+
)
|
|
116
|
+
return images
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _close_image_dicts(images_list: list[dict[str, Any]] | None) -> None:
|
|
120
|
+
"""关闭 image dict 中的 PIL 图片,供异常清理路径释放已生成的图像资源。"""
|
|
121
|
+
for image_dict in images_list or []:
|
|
122
|
+
pil_img = image_dict.get("img_pil")
|
|
123
|
+
if pil_img is None:
|
|
124
|
+
continue
|
|
125
|
+
try:
|
|
126
|
+
pil_img.close()
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _calculate_render_process_count(total_pages: int, threads: int, cpu_count: int | None = None) -> int:
|
|
132
|
+
"""按页数、配置和 CPU 数量计算实际渲染进程数。"""
|
|
133
|
+
requested_threads = max(1, threads)
|
|
134
|
+
available_cpus = max(1, cpu_count if cpu_count is not None else (os.cpu_count() or 1))
|
|
135
|
+
page_limited_threads = max(1, total_pages // MIN_PAGES_PER_RENDER_PROCESS)
|
|
136
|
+
return min(
|
|
137
|
+
available_cpus,
|
|
138
|
+
requested_threads,
|
|
139
|
+
MAX_PDF_RENDER_PROCESSES,
|
|
140
|
+
page_limited_threads,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _build_render_page_ranges(
|
|
145
|
+
start_page_id: int,
|
|
146
|
+
end_page_id: int,
|
|
147
|
+
process_count: int,
|
|
148
|
+
) -> list[tuple[int, int]]:
|
|
149
|
+
"""把闭区间页范围均匀拆分为指定数量的子范围。"""
|
|
150
|
+
total_pages = end_page_id - start_page_id + 1
|
|
151
|
+
base_pages, remainder = divmod(total_pages, process_count)
|
|
152
|
+
page_ranges = []
|
|
153
|
+
current_page = start_page_id
|
|
154
|
+
|
|
155
|
+
for process_idx in range(process_count):
|
|
156
|
+
pages_in_range = base_pages + (1 if process_idx < remainder else 0)
|
|
157
|
+
range_end = current_page + pages_in_range - 1
|
|
158
|
+
page_ranges.append((current_page, range_end))
|
|
159
|
+
current_page = range_end + 1
|
|
160
|
+
|
|
161
|
+
return page_ranges
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _get_render_process_plan(
|
|
165
|
+
start_page_id: int,
|
|
166
|
+
end_page_id: int,
|
|
167
|
+
threads: int,
|
|
168
|
+
cpu_count: int | None = None,
|
|
169
|
+
) -> tuple[int, list[tuple[int, int]]]:
|
|
170
|
+
"""同时返回实际进程数及其页面分片计划。"""
|
|
171
|
+
total_pages = end_page_id - start_page_id + 1
|
|
172
|
+
actual_threads = _calculate_render_process_count(total_pages, threads, cpu_count)
|
|
173
|
+
return actual_threads, _build_render_page_ranges(start_page_id, end_page_id, actual_threads)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _get_pdf_render_pool_capacity(cpu_count: int | None = None) -> int:
|
|
177
|
+
"""返回持久 PDF 渲染进程池的最大容量。"""
|
|
178
|
+
available_cpus = max(1, cpu_count if cpu_count is not None else (os.cpu_count() or 1))
|
|
179
|
+
configured_threads = max(1, get_load_images_threads())
|
|
180
|
+
return min(
|
|
181
|
+
available_cpus,
|
|
182
|
+
configured_threads,
|
|
183
|
+
MAX_PDF_RENDER_PROCESSES,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _exit_pdf_render_worker_when_parent_exits() -> None:
|
|
188
|
+
"""等待父进程退出后立即终止孤立渲染 worker。"""
|
|
189
|
+
parent = multiprocessing.parent_process()
|
|
190
|
+
if parent is None:
|
|
191
|
+
return
|
|
192
|
+
parent.join()
|
|
193
|
+
os._exit(1)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _install_pdf_render_parent_exit_watcher() -> None:
|
|
197
|
+
"""在渲染 worker 中安装父进程退出监听线程。"""
|
|
198
|
+
watcher = threading.Thread(
|
|
199
|
+
target=_exit_pdf_render_worker_when_parent_exits,
|
|
200
|
+
name="docvortex-pdf-render-parent-exit-watcher",
|
|
201
|
+
daemon=True,
|
|
202
|
+
)
|
|
203
|
+
watcher.start()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _initialize_pdf_render_worker() -> None:
|
|
207
|
+
"""保留父进程退出监控,并在本 worker 首次处理文档前安装固定字体。"""
|
|
208
|
+
_install_pdf_render_parent_exit_watcher()
|
|
209
|
+
initialize_pdfium_runtime()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _create_pdf_render_executor(max_workers: int) -> ProcessPoolExecutor:
|
|
213
|
+
"""使用安全 multiprocessing 上下文创建 PDF 渲染进程池。"""
|
|
214
|
+
if is_windows_environment():
|
|
215
|
+
return ProcessPoolExecutor(max_workers=max_workers, initializer=_initialize_pdf_render_worker)
|
|
216
|
+
|
|
217
|
+
start_method = multiprocessing.get_start_method()
|
|
218
|
+
if start_method != "spawn":
|
|
219
|
+
logger.debug(f"PDF image rendering switches multiprocessing start method from {start_method} to spawn")
|
|
220
|
+
return ProcessPoolExecutor(
|
|
221
|
+
max_workers=max_workers,
|
|
222
|
+
mp_context=multiprocessing.get_context("spawn"),
|
|
223
|
+
initializer=_initialize_pdf_render_worker,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return ProcessPoolExecutor(max_workers=max_workers, initializer=_initialize_pdf_render_worker)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _is_pdf_render_pool_still_spawning_workers(executor: ProcessPoolExecutor) -> bool:
|
|
230
|
+
"""判断渲染进程池是否还可能因为 submit 而继续创建新的 worker。"""
|
|
231
|
+
max_workers = getattr(executor, "_max_workers", None)
|
|
232
|
+
if max_workers is None or max_workers <= 1:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
# ProcessPoolExecutor 优先复用空闲 worker;未达到容量不等于本次会创建进程。
|
|
236
|
+
# 只探测并立即归还信号量,避免小批量任务永久重复支付冷启动的 100ms 等待。
|
|
237
|
+
idle_workers = getattr(executor, "_idle_worker_semaphore", None)
|
|
238
|
+
if idle_workers is not None and idle_workers.acquire(blocking=False):
|
|
239
|
+
idle_workers.release()
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
processes = getattr(executor, "_processes", None)
|
|
243
|
+
process_count = 0 if processes is None else len(processes)
|
|
244
|
+
return process_count < max_workers
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _submit_pdf_render_task(
|
|
248
|
+
executor: ProcessPoolExecutor,
|
|
249
|
+
fn: Callable[..., Any],
|
|
250
|
+
*args: Any, # noqa: ANN401
|
|
251
|
+
**kwargs: Any, # noqa: ANN401
|
|
252
|
+
) -> Any: # noqa: ANN401
|
|
253
|
+
"""提交 PDF 渲染任务;冷启动补 worker 时串行 submit 并错开 100ms。"""
|
|
254
|
+
global _pdf_render_spawn_submit_executor_id, _pdf_render_spawn_submit_count
|
|
255
|
+
|
|
256
|
+
with _pdf_render_spawn_submit_lock:
|
|
257
|
+
should_throttle = _is_pdf_render_pool_still_spawning_workers(executor)
|
|
258
|
+
if should_throttle:
|
|
259
|
+
executor_id = id(executor)
|
|
260
|
+
if _pdf_render_spawn_submit_executor_id != executor_id:
|
|
261
|
+
_pdf_render_spawn_submit_executor_id = executor_id
|
|
262
|
+
_pdf_render_spawn_submit_count = 0
|
|
263
|
+
elif _pdf_render_spawn_submit_count > 0:
|
|
264
|
+
time.sleep(PDF_RENDER_PROCESS_SPAWN_DELAY_SECONDS)
|
|
265
|
+
|
|
266
|
+
future = executor.submit(fn, *args, **kwargs)
|
|
267
|
+
|
|
268
|
+
if should_throttle:
|
|
269
|
+
_pdf_render_spawn_submit_count += 1
|
|
270
|
+
|
|
271
|
+
return future
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _get_pdf_render_future_states(
|
|
275
|
+
future_to_range: dict[Future[Any], tuple[int, int]],
|
|
276
|
+
) -> list[dict[str, Any]]:
|
|
277
|
+
"""汇总每个渲染 Future 的页面范围和运行状态。"""
|
|
278
|
+
states = []
|
|
279
|
+
for future, (range_start, range_end) in future_to_range.items():
|
|
280
|
+
try:
|
|
281
|
+
if future.cancelled():
|
|
282
|
+
state = "cancelled"
|
|
283
|
+
elif future.done():
|
|
284
|
+
state = "done"
|
|
285
|
+
elif future.running():
|
|
286
|
+
state = "running"
|
|
287
|
+
else:
|
|
288
|
+
state = "pending"
|
|
289
|
+
except Exception:
|
|
290
|
+
state = "unknown"
|
|
291
|
+
states.append(
|
|
292
|
+
{
|
|
293
|
+
"pages": f"{range_start + 1}-{range_end + 1}",
|
|
294
|
+
"state": state,
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
return states
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _get_pdf_render_worker_states(executor: ProcessPoolExecutor) -> list[dict[str, Any]]:
|
|
301
|
+
"""读取进程池 worker 的 pid、存活状态和退出码。"""
|
|
302
|
+
try:
|
|
303
|
+
process_map = getattr(executor, "_processes", None) or {}
|
|
304
|
+
processes = list(process_map.values())
|
|
305
|
+
except Exception:
|
|
306
|
+
return []
|
|
307
|
+
|
|
308
|
+
states = []
|
|
309
|
+
for process in processes:
|
|
310
|
+
try:
|
|
311
|
+
pid = getattr(process, "pid", None)
|
|
312
|
+
except Exception:
|
|
313
|
+
pid = None
|
|
314
|
+
try:
|
|
315
|
+
exit_code = getattr(process, "exitcode", None)
|
|
316
|
+
except Exception:
|
|
317
|
+
exit_code = None
|
|
318
|
+
try:
|
|
319
|
+
alive = process.is_alive()
|
|
320
|
+
except Exception:
|
|
321
|
+
alive = None
|
|
322
|
+
states.append(
|
|
323
|
+
{
|
|
324
|
+
"pid": pid,
|
|
325
|
+
"alive": alive,
|
|
326
|
+
"exit_code": exit_code,
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
return sorted(states, key=lambda state: (state["pid"] is None, state["pid"] or 0))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _get_pdf_render_executor() -> ProcessPoolExecutor:
|
|
333
|
+
"""惰性创建并复用持久 PDF 渲染进程池。"""
|
|
334
|
+
global _pdf_render_atexit_registered, _pdf_render_executor
|
|
335
|
+
|
|
336
|
+
with _pdf_render_executor_lock:
|
|
337
|
+
if _pdf_render_executor is None:
|
|
338
|
+
if not _pdf_render_atexit_registered:
|
|
339
|
+
atexit.register(shutdown_pdf_render_executor)
|
|
340
|
+
_pdf_render_atexit_registered = True
|
|
341
|
+
max_workers = _get_pdf_render_pool_capacity()
|
|
342
|
+
_pdf_render_executor = _create_pdf_render_executor(max_workers=max_workers)
|
|
343
|
+
logger.debug(f"Created persistent PDF render executor with max_workers={max_workers}")
|
|
344
|
+
return _pdf_render_executor
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _recycle_pdf_render_executor(
|
|
348
|
+
executor: ProcessPoolExecutor | None,
|
|
349
|
+
*,
|
|
350
|
+
terminate_processes: bool,
|
|
351
|
+
) -> None:
|
|
352
|
+
"""从全局缓存移除指定进程池并按需终止 worker。"""
|
|
353
|
+
global _pdf_render_executor
|
|
354
|
+
|
|
355
|
+
if executor is None:
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
with _pdf_render_executor_lock:
|
|
359
|
+
if _pdf_render_executor is executor:
|
|
360
|
+
_pdf_render_executor = None
|
|
361
|
+
|
|
362
|
+
if terminate_processes:
|
|
363
|
+
try:
|
|
364
|
+
_terminate_executor_processes(executor)
|
|
365
|
+
except Exception as exc:
|
|
366
|
+
logger.warning(f"Failed to terminate PDF render executor processes: {exc}")
|
|
367
|
+
try:
|
|
368
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
369
|
+
except Exception as exc:
|
|
370
|
+
logger.warning(f"Failed to shutdown PDF render executor: {exc}")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def shutdown_pdf_render_executor() -> None:
|
|
374
|
+
"""关闭当前持久 PDF 渲染进程池。"""
|
|
375
|
+
global _pdf_render_executor
|
|
376
|
+
|
|
377
|
+
with _pdf_render_executor_lock:
|
|
378
|
+
executor = _pdf_render_executor
|
|
379
|
+
_pdf_render_executor = None
|
|
380
|
+
|
|
381
|
+
if executor is not None:
|
|
382
|
+
_recycle_pdf_render_executor(
|
|
383
|
+
executor,
|
|
384
|
+
terminate_processes=True,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def load_images_from_pdf_bytes_range(
|
|
389
|
+
pdf_bytes: bytes,
|
|
390
|
+
dpi: int = DEFAULT_PDF_IMAGE_DPI,
|
|
391
|
+
start_page_id: int = 0,
|
|
392
|
+
end_page_id: int = 0,
|
|
393
|
+
image_type: Literal["pil_img", "base64_img"] = ImageType.PIL,
|
|
394
|
+
timeout: int | None = None,
|
|
395
|
+
threads: int | None = None,
|
|
396
|
+
) -> list[dict[str, Any]]:
|
|
397
|
+
"""在持久进程池中批量渲染指定闭区间页面。"""
|
|
398
|
+
if end_page_id < start_page_id:
|
|
399
|
+
return []
|
|
400
|
+
|
|
401
|
+
if timeout is None:
|
|
402
|
+
timeout = get_load_images_timeout()
|
|
403
|
+
if threads is None:
|
|
404
|
+
threads = get_load_images_threads()
|
|
405
|
+
|
|
406
|
+
actual_threads, page_ranges = _get_render_process_plan(
|
|
407
|
+
start_page_id,
|
|
408
|
+
end_page_id,
|
|
409
|
+
threads,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
logger.debug(
|
|
413
|
+
f"PDF image rendering uses {actual_threads} processes for pages {start_page_id + 1}-{end_page_id + 1}: {page_ranges}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
render_started_at = time.monotonic()
|
|
417
|
+
executor = _get_pdf_render_executor()
|
|
418
|
+
executor_id = hex(id(executor))
|
|
419
|
+
parent_pid = os.getpid()
|
|
420
|
+
parent_thread = threading.current_thread().name
|
|
421
|
+
recycle_executor = False
|
|
422
|
+
collected_image_lists = []
|
|
423
|
+
try:
|
|
424
|
+
futures: list[Future[Any]] = []
|
|
425
|
+
future_to_range: dict[Future[Any], tuple[int, int]] = {}
|
|
426
|
+
for range_start, range_end in page_ranges:
|
|
427
|
+
page_range = f"{range_start + 1}-{range_end + 1}"
|
|
428
|
+
logger.debug(
|
|
429
|
+
f"Submitting PDF render task: parent_pid={parent_pid}, thread={parent_thread}, "
|
|
430
|
+
f"executor={executor_id}, pages={page_range}"
|
|
431
|
+
)
|
|
432
|
+
future = _submit_pdf_render_task(
|
|
433
|
+
executor,
|
|
434
|
+
_load_images_from_pdf_worker,
|
|
435
|
+
pdf_bytes,
|
|
436
|
+
dpi,
|
|
437
|
+
range_start,
|
|
438
|
+
range_end,
|
|
439
|
+
image_type,
|
|
440
|
+
)
|
|
441
|
+
futures.append(future)
|
|
442
|
+
future_to_range[future] = (range_start, range_end)
|
|
443
|
+
logger.debug(
|
|
444
|
+
f"Submitted PDF render task: parent_pid={parent_pid}, thread={parent_thread}, executor={executor_id}, "
|
|
445
|
+
f"pages={page_range}, workers={_get_pdf_render_worker_states(executor)}"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
_, not_done = wait(futures, timeout=timeout, return_when=ALL_COMPLETED)
|
|
449
|
+
if not_done:
|
|
450
|
+
recycle_executor = True
|
|
451
|
+
elapsed = time.monotonic() - render_started_at
|
|
452
|
+
logger.warning(
|
|
453
|
+
f"PDF image rendering timed out: timeout={timeout}s, elapsed={elapsed:.3f}s, parent_pid={parent_pid}, "
|
|
454
|
+
f"thread={parent_thread}, executor={executor_id}, pages={start_page_id + 1}-{end_page_id + 1}, "
|
|
455
|
+
f"futures={_get_pdf_render_future_states(future_to_range)}, "
|
|
456
|
+
f"workers={_get_pdf_render_worker_states(executor)}"
|
|
457
|
+
)
|
|
458
|
+
raise TimeoutError(f"PDF image rendering timeout after {timeout}s for pages {start_page_id + 1}-{end_page_id + 1}")
|
|
459
|
+
|
|
460
|
+
all_results = []
|
|
461
|
+
for future in futures:
|
|
462
|
+
range_start, _ = future_to_range[future]
|
|
463
|
+
images_list = future.result()
|
|
464
|
+
collected_image_lists.append(images_list)
|
|
465
|
+
all_results.append((range_start, images_list))
|
|
466
|
+
|
|
467
|
+
all_results.sort(key=lambda x: x[0])
|
|
468
|
+
images_list = []
|
|
469
|
+
for _, imgs in all_results:
|
|
470
|
+
images_list.extend(imgs)
|
|
471
|
+
|
|
472
|
+
collected_image_lists.clear()
|
|
473
|
+
elapsed = time.monotonic() - render_started_at
|
|
474
|
+
logger.debug(
|
|
475
|
+
f"PDF image rendering completed: elapsed={elapsed:.3f}s, parent_pid={parent_pid}, thread={parent_thread}, "
|
|
476
|
+
f"executor={executor_id}, pages={start_page_id + 1}-{end_page_id + 1}, images={len(images_list)}"
|
|
477
|
+
)
|
|
478
|
+
return images_list
|
|
479
|
+
except BrokenProcessPool:
|
|
480
|
+
recycle_executor = True
|
|
481
|
+
raise
|
|
482
|
+
except Exception:
|
|
483
|
+
for images_list in collected_image_lists:
|
|
484
|
+
_close_image_dicts(images_list)
|
|
485
|
+
raise
|
|
486
|
+
finally:
|
|
487
|
+
if recycle_executor:
|
|
488
|
+
logger.warning("Recycling persistent PDF render executor after render failure")
|
|
489
|
+
_recycle_pdf_render_executor(
|
|
490
|
+
executor,
|
|
491
|
+
terminate_processes=True,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _terminate_executor_processes(executor: ProcessPoolExecutor) -> None:
|
|
496
|
+
"""强制终止 ProcessPoolExecutor 中的所有子进程"""
|
|
497
|
+
# executor.shutdown() 后 _processes 会被置空,重复回收时直接视为无进程。
|
|
498
|
+
process_map = getattr(executor, "_processes", None) or {}
|
|
499
|
+
processes = list(process_map.values())
|
|
500
|
+
if not processes:
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
alive_processes = []
|
|
504
|
+
for process in processes:
|
|
505
|
+
if not process.is_alive():
|
|
506
|
+
continue
|
|
507
|
+
try:
|
|
508
|
+
process.terminate()
|
|
509
|
+
except Exception:
|
|
510
|
+
pass
|
|
511
|
+
alive_processes.append(process)
|
|
512
|
+
|
|
513
|
+
deadline = time.monotonic() + PDF_RENDER_TERMINATE_GRACE_PERIOD_SECONDS
|
|
514
|
+
for process in alive_processes:
|
|
515
|
+
remaining = max(0.0, deadline - time.monotonic())
|
|
516
|
+
try:
|
|
517
|
+
process.join(timeout=remaining)
|
|
518
|
+
except Exception:
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
for process in alive_processes:
|
|
522
|
+
if not process.is_alive():
|
|
523
|
+
continue
|
|
524
|
+
try:
|
|
525
|
+
kill_process = getattr(process, "kill", None)
|
|
526
|
+
if callable(kill_process):
|
|
527
|
+
kill_process()
|
|
528
|
+
else:
|
|
529
|
+
process.terminate()
|
|
530
|
+
except Exception:
|
|
531
|
+
pass
|
|
532
|
+
|
|
533
|
+
for process in alive_processes:
|
|
534
|
+
if not process.is_alive():
|
|
535
|
+
continue
|
|
536
|
+
try:
|
|
537
|
+
process.join(timeout=PDF_RENDER_KILL_JOIN_TIMEOUT_SECONDS)
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def load_images_from_pdf_core(
|
|
543
|
+
pdf_bytes: bytes,
|
|
544
|
+
dpi: int = DEFAULT_PDF_IMAGE_DPI,
|
|
545
|
+
start_page_id: int = 0,
|
|
546
|
+
end_page_id: int | None = None,
|
|
547
|
+
image_type: Literal["pil_img", "base64_img"] = ImageType.PIL,
|
|
548
|
+
) -> list[dict[str, Any]]:
|
|
549
|
+
"""在当前进程中依次渲染 PDF 页面范围。"""
|
|
550
|
+
images_list = []
|
|
551
|
+
pdf_doc = None
|
|
552
|
+
try:
|
|
553
|
+
with pdfium_guard():
|
|
554
|
+
pdf_doc = pdfium.PdfDocument(pdf_bytes)
|
|
555
|
+
pdf_page_num = len(pdf_doc)
|
|
556
|
+
end_page_id = end_page_id if end_page_id is not None and end_page_id >= 0 else pdf_page_num - 1
|
|
557
|
+
if end_page_id > pdf_page_num - 1:
|
|
558
|
+
end_page_id = pdf_page_num - 1
|
|
559
|
+
|
|
560
|
+
for index in range(start_page_id, end_page_id + 1):
|
|
561
|
+
# logger.debug(f"Converting page {index}/{pdf_page_num} to image")
|
|
562
|
+
page = None
|
|
563
|
+
try:
|
|
564
|
+
page = pdf_doc[index]
|
|
565
|
+
image_dict = pdf_page_to_image(page, dpi=dpi, image_type=image_type)
|
|
566
|
+
images_list.append(image_dict)
|
|
567
|
+
finally:
|
|
568
|
+
close_pdfium_child(page)
|
|
569
|
+
finally:
|
|
570
|
+
close_pdfium_document(pdf_doc)
|
|
571
|
+
|
|
572
|
+
return images_list
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _model_input_bbox(item: dict[str, Any]) -> IntBBox:
|
|
576
|
+
"""把模型输入项的 bbox 向外取整为裁图整数坐标。"""
|
|
577
|
+
bbox = item["bbox"]
|
|
578
|
+
assert bbox is not None
|
|
579
|
+
xmin, ymin, xmax, ymax = [float(value) for value in bbox]
|
|
580
|
+
return math.floor(xmin), math.floor(ymin), math.ceil(xmax), math.ceil(ymax)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def crop_img(
|
|
584
|
+
input_res: dict[str, Any],
|
|
585
|
+
input_img: Image.Image | np.ndarray,
|
|
586
|
+
crop_paste_x: int = 0,
|
|
587
|
+
crop_paste_y: int = 0,
|
|
588
|
+
) -> tuple[Image.Image | np.ndarray, list[int]]:
|
|
589
|
+
"""按模型 bbox 裁图并在四周补白,返回坐标回投所需参数。"""
|
|
590
|
+
crop_xmin, crop_ymin, crop_xmax, crop_ymax = _model_input_bbox(input_res)
|
|
591
|
+
crop_new_width = crop_xmax - crop_xmin + crop_paste_x * 2
|
|
592
|
+
crop_new_height = crop_ymax - crop_ymin + crop_paste_y * 2
|
|
593
|
+
if isinstance(input_img, np.ndarray):
|
|
594
|
+
return_image: Image.Image | np.ndarray = np.ones((crop_new_height, crop_new_width, 3), dtype=np.uint8) * 255
|
|
595
|
+
cropped_img = input_img[crop_ymin:crop_ymax, crop_xmin:crop_xmax]
|
|
596
|
+
return_image[
|
|
597
|
+
crop_paste_y : crop_paste_y + (crop_ymax - crop_ymin),
|
|
598
|
+
crop_paste_x : crop_paste_x + (crop_xmax - crop_xmin),
|
|
599
|
+
] = cropped_img
|
|
600
|
+
else:
|
|
601
|
+
return_image = Image.new("RGB", (crop_new_width, crop_new_height), "white")
|
|
602
|
+
cropped_img = input_img.crop((crop_xmin, crop_ymin, crop_xmax, crop_ymax))
|
|
603
|
+
return_image.paste(cropped_img, (crop_paste_x, crop_paste_y))
|
|
604
|
+
useful_list = [
|
|
605
|
+
crop_paste_x,
|
|
606
|
+
crop_paste_y,
|
|
607
|
+
crop_xmin,
|
|
608
|
+
crop_ymin,
|
|
609
|
+
crop_xmax,
|
|
610
|
+
crop_ymax,
|
|
611
|
+
crop_new_width,
|
|
612
|
+
crop_new_height,
|
|
613
|
+
]
|
|
614
|
+
return return_image, useful_list
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def get_crop_img(bbox: BBox, pil_img: Image.Image, scale: float = 2.0) -> Image.Image:
|
|
618
|
+
"""按缩放 bbox 裁剪 Pillow 图像。"""
|
|
619
|
+
scale_bbox = normalize_to_int_bbox([float(v) * scale for v in bbox])
|
|
620
|
+
if scale_bbox is None:
|
|
621
|
+
return pil_img.crop((0, 0, 0, 0))
|
|
622
|
+
return pil_img.crop(tuple(scale_bbox))
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def get_crop_np_img(bbox: BBox, input_img: Image.Image | np.ndarray, scale: float = 2.0) -> np.ndarray:
|
|
626
|
+
"""按缩放 bbox 裁剪 Pillow 或 NumPy 图像并返回数组。"""
|
|
627
|
+
if isinstance(input_img, Image.Image):
|
|
628
|
+
np_img = np.asarray(input_img)
|
|
629
|
+
elif isinstance(input_img, np.ndarray):
|
|
630
|
+
np_img = input_img
|
|
631
|
+
else:
|
|
632
|
+
raise ValueError("Input must be a pillow object or a numpy array.")
|
|
633
|
+
|
|
634
|
+
height, width = np_img.shape[:2]
|
|
635
|
+
scale_bbox = normalize_to_int_bbox(
|
|
636
|
+
[float(v) * scale for v in bbox],
|
|
637
|
+
image_size=(height, width),
|
|
638
|
+
)
|
|
639
|
+
if scale_bbox is None:
|
|
640
|
+
return np_img[0:0, 0:0]
|
|
641
|
+
|
|
642
|
+
return np_img[scale_bbox[1] : scale_bbox[3], scale_bbox[0] : scale_bbox[2]]
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
__all__ = [
|
|
646
|
+
"ImageType",
|
|
647
|
+
"crop_img",
|
|
648
|
+
"get_crop_img",
|
|
649
|
+
"get_crop_np_img",
|
|
650
|
+
"load_images_from_pdf_bytes_range",
|
|
651
|
+
"load_images_from_pdf_core",
|
|
652
|
+
"pdf_page_to_image",
|
|
653
|
+
"shutdown_pdf_render_executor",
|
|
654
|
+
]
|