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.
Files changed (364) hide show
  1. docvortex/__init__.py +21 -0
  2. docvortex/analyzers/__init__.py +3 -0
  3. docvortex/analyzers/native/__init__.py +37 -0
  4. docvortex/analyzers/native/_shared/__init__.py +3 -0
  5. docvortex/analyzers/native/_shared/hyperlink.py +16 -0
  6. docvortex/analyzers/native/_shared/image.py +8 -0
  7. docvortex/analyzers/native/_shared/markup/__init__.py +47 -0
  8. docvortex/analyzers/native/_shared/markup/anchors.py +30 -0
  9. docvortex/analyzers/native/_shared/markup/formula.py +37 -0
  10. docvortex/analyzers/native/_shared/markup/projector.py +57 -0
  11. docvortex/analyzers/native/_shared/markup/styles.py +19 -0
  12. docvortex/analyzers/native/_shared/mathml.py +17 -0
  13. docvortex/analyzers/native/_shared/names.py +7 -0
  14. docvortex/analyzers/native/_shared/xycut.py +414 -0
  15. docvortex/analyzers/native/contracts.py +44 -0
  16. docvortex/analyzers/native/csv.py +351 -0
  17. docvortex/analyzers/native/epub/__init__.py +18 -0
  18. docvortex/analyzers/native/epub/constants.py +45 -0
  19. docvortex/analyzers/native/epub/converter.py +80 -0
  20. docvortex/analyzers/native/epub/errors.py +20 -0
  21. docvortex/analyzers/native/epub/metadata.py +31 -0
  22. docvortex/analyzers/native/epub/package.py +649 -0
  23. docvortex/analyzers/native/epub/xhtml.py +306 -0
  24. docvortex/analyzers/native/html/__init__.py +6 -0
  25. docvortex/analyzers/native/html/anchors.py +271 -0
  26. docvortex/analyzers/native/html/constants.py +25 -0
  27. docvortex/analyzers/native/html/contracts.py +7 -0
  28. docvortex/analyzers/native/html/converter.py +117 -0
  29. docvortex/analyzers/native/html/document.py +389 -0
  30. docvortex/analyzers/native/html/errors.py +12 -0
  31. docvortex/analyzers/native/html/resources.py +365 -0
  32. docvortex/analyzers/native/html/selector.py +421 -0
  33. docvortex/analyzers/native/models.py +209 -0
  34. docvortex/analyzers/native/ofd/__init__.py +14 -0
  35. docvortex/analyzers/native/ofd/constants.py +59 -0
  36. docvortex/analyzers/native/ofd/converter.py +31 -0
  37. docvortex/analyzers/native/ofd/errors.py +16 -0
  38. docvortex/analyzers/native/ofd/geometry.py +215 -0
  39. docvortex/analyzers/native/ofd/images.py +124 -0
  40. docvortex/analyzers/native/ofd/metadata.py +53 -0
  41. docvortex/analyzers/native/ofd/models.py +170 -0
  42. docvortex/analyzers/native/ofd/package.py +346 -0
  43. docvortex/analyzers/native/ofd/path.py +201 -0
  44. docvortex/analyzers/native/ofd/reading_order.py +293 -0
  45. docvortex/analyzers/native/ofd/resources.py +122 -0
  46. docvortex/analyzers/native/ofd/scene.py +347 -0
  47. docvortex/analyzers/native/ofd/table.py +240 -0
  48. docvortex/analyzers/native/ofd/text.py +521 -0
  49. docvortex/analyzers/native/office/__init__.py +3 -0
  50. docvortex/analyzers/native/office/doc/__init__.py +3 -0
  51. docvortex/analyzers/native/office/doc/bookmarks.py +81 -0
  52. docvortex/analyzers/native/office/doc/doc_converter.py +573 -0
  53. docvortex/analyzers/native/office/doc/fib.py +261 -0
  54. docvortex/analyzers/native/office/doc/fields.py +101 -0
  55. docvortex/analyzers/native/office/doc/formatting.py +171 -0
  56. docvortex/analyzers/native/office/doc/images.py +162 -0
  57. docvortex/analyzers/native/office/doc/lists.py +356 -0
  58. docvortex/analyzers/native/office/doc/models.py +164 -0
  59. docvortex/analyzers/native/office/doc/parser.py +851 -0
  60. docvortex/analyzers/native/office/doc/pieces.py +256 -0
  61. docvortex/analyzers/native/office/doc/records.py +40 -0
  62. docvortex/analyzers/native/office/doc/sprm.py +269 -0
  63. docvortex/analyzers/native/office/doc/styles.py +215 -0
  64. docvortex/analyzers/native/office/docx/__init__.py +3 -0
  65. docvortex/analyzers/native/office/docx/context.py +71 -0
  66. docvortex/analyzers/native/office/docx/docx_converter.py +614 -0
  67. docvortex/analyzers/native/office/docx/equationxml.py +119 -0
  68. docvortex/analyzers/native/office/docx/fields.py +816 -0
  69. docvortex/analyzers/native/office/docx/formatting_types.py +23 -0
  70. docvortex/analyzers/native/office/docx/main.py +50 -0
  71. docvortex/analyzers/native/office/docx/numbering.py +491 -0
  72. docvortex/analyzers/native/office/docx/office_xml.py +57 -0
  73. docvortex/analyzers/native/office/docx/package_normalizer.py +248 -0
  74. docvortex/analyzers/native/office/docx/resources.py +526 -0
  75. docvortex/analyzers/native/office/docx/styles.py +693 -0
  76. docvortex/analyzers/native/office/docx/tables.py +515 -0
  77. docvortex/analyzers/native/office/equation/__init__.py +3 -0
  78. docvortex/analyzers/native/office/equation/image.py +470 -0
  79. docvortex/analyzers/native/office/equation/latex_dict.py +324 -0
  80. docvortex/analyzers/native/office/equation/mtef.py +885 -0
  81. docvortex/analyzers/native/office/equation/mtef_v5.py +941 -0
  82. docvortex/analyzers/native/office/equation/omml.py +561 -0
  83. docvortex/analyzers/native/office/equation/ooxml.py +62 -0
  84. docvortex/analyzers/native/office/errors.py +33 -0
  85. docvortex/analyzers/native/office/image.py +307 -0
  86. docvortex/analyzers/native/office/legacy/__init__.py +3 -0
  87. docvortex/analyzers/native/office/legacy/binary.py +43 -0
  88. docvortex/analyzers/native/office/legacy/officeart.py +362 -0
  89. docvortex/analyzers/native/office/legacy/ole.py +110 -0
  90. docvortex/analyzers/native/office/limits.py +12 -0
  91. docvortex/analyzers/native/office/odf/__init__.py +3 -0
  92. docvortex/analyzers/native/office/odf/chart.py +76 -0
  93. docvortex/analyzers/native/office/odf/constants.py +70 -0
  94. docvortex/analyzers/native/office/odf/converters.py +403 -0
  95. docvortex/analyzers/native/office/odf/errors.py +18 -0
  96. docvortex/analyzers/native/office/odf/metadata.py +91 -0
  97. docvortex/analyzers/native/office/odf/models.py +176 -0
  98. docvortex/analyzers/native/office/odf/package.py +270 -0
  99. docvortex/analyzers/native/office/odf/styles.py +329 -0
  100. docvortex/analyzers/native/office/odf/table.py +469 -0
  101. docvortex/analyzers/native/office/odf/text.py +1002 -0
  102. docvortex/analyzers/native/office/ooxml_chart.py +1016 -0
  103. docvortex/analyzers/native/office/opc.py +38 -0
  104. docvortex/analyzers/native/office/ppt/__init__.py +3 -0
  105. docvortex/analyzers/native/office/ppt/models.py +118 -0
  106. docvortex/analyzers/native/office/ppt/parser.py +1895 -0
  107. docvortex/analyzers/native/office/ppt/ppt_converter.py +292 -0
  108. docvortex/analyzers/native/office/ppt/records.py +131 -0
  109. docvortex/analyzers/native/office/ppt/style_text.py +247 -0
  110. docvortex/analyzers/native/office/pptx/__init__.py +3 -0
  111. docvortex/analyzers/native/office/pptx/context.py +113 -0
  112. docvortex/analyzers/native/office/pptx/lists.py +558 -0
  113. docvortex/analyzers/native/office/pptx/main.py +20 -0
  114. docvortex/analyzers/native/office/pptx/package_normalizer.py +321 -0
  115. docvortex/analyzers/native/office/pptx/pptx_converter.py +323 -0
  116. docvortex/analyzers/native/office/pptx/resources.py +329 -0
  117. docvortex/analyzers/native/office/pptx/shapes.py +393 -0
  118. docvortex/analyzers/native/office/pptx/text_styles.py +625 -0
  119. docvortex/analyzers/native/office/pptx/titles.py +178 -0
  120. docvortex/analyzers/native/office/rich_text.py +420 -0
  121. docvortex/analyzers/native/office/rtf/__init__.py +3 -0
  122. docvortex/analyzers/native/office/rtf/converter.py +708 -0
  123. docvortex/analyzers/native/office/rtf/lexer.py +213 -0
  124. docvortex/analyzers/native/office/rtf/math.py +339 -0
  125. docvortex/analyzers/native/office/rtf/models.py +185 -0
  126. docvortex/analyzers/native/office/rtf/parser.py +1553 -0
  127. docvortex/analyzers/native/office/spreadsheet/__init__.py +3 -0
  128. docvortex/analyzers/native/office/spreadsheet/html.py +81 -0
  129. docvortex/analyzers/native/office/spreadsheet/models.py +79 -0
  130. docvortex/analyzers/native/office/spreadsheet/projector.py +928 -0
  131. docvortex/analyzers/native/office/streams.py +18 -0
  132. docvortex/analyzers/native/office/xls/__init__.py +3 -0
  133. docvortex/analyzers/native/office/xls/chart.py +132 -0
  134. docvortex/analyzers/native/office/xls/embedded_chart.py +299 -0
  135. docvortex/analyzers/native/office/xls/models.py +109 -0
  136. docvortex/analyzers/native/office/xls/number_format.py +521 -0
  137. docvortex/analyzers/native/office/xls/parser.py +1145 -0
  138. docvortex/analyzers/native/office/xls/records.py +201 -0
  139. docvortex/analyzers/native/office/xls/strings.py +205 -0
  140. docvortex/analyzers/native/office/xls/xls_converter.py +354 -0
  141. docvortex/analyzers/native/office/xlsx/__init__.py +3 -0
  142. docvortex/analyzers/native/office/xlsx/main.py +20 -0
  143. docvortex/analyzers/native/office/xlsx/ooxml_ole.py +522 -0
  144. docvortex/analyzers/native/office/xlsx/package_normalizer.py +310 -0
  145. docvortex/analyzers/native/office/xlsx/xlsx_converter.py +716 -0
  146. docvortex/analyzers/native/pdf/__init__.py +3 -0
  147. docvortex/analyzers/native/pdf/auxiliary_text.py +1671 -0
  148. docvortex/analyzers/native/pdf/char_geometry.py +1662 -0
  149. docvortex/analyzers/native/pdf/code_blocks.py +535 -0
  150. docvortex/analyzers/native/pdf/formulas.py +1985 -0
  151. docvortex/analyzers/native/pdf/geometry.py +281 -0
  152. docvortex/analyzers/native/pdf/graphics.py +1501 -0
  153. docvortex/analyzers/native/pdf/index_blocks.py +268 -0
  154. docvortex/analyzers/native/pdf/inline/__init__.py +3 -0
  155. docvortex/analyzers/native/pdf/inline/common.py +112 -0
  156. docvortex/analyzers/native/pdf/inline/detection.py +590 -0
  157. docvortex/analyzers/native/pdf/inline/matching.py +1185 -0
  158. docvortex/analyzers/native/pdf/inline/materialize.py +457 -0
  159. docvortex/analyzers/native/pdf/inline/scripts.py +975 -0
  160. docvortex/analyzers/native/pdf/inline/types.py +385 -0
  161. docvortex/analyzers/native/pdf/line_layout.py +1106 -0
  162. docvortex/analyzers/native/pdf/line_merging.py +1223 -0
  163. docvortex/analyzers/native/pdf/models.py +246 -0
  164. docvortex/analyzers/native/pdf/native_text.py +1004 -0
  165. docvortex/analyzers/native/pdf/pipeline.py +1390 -0
  166. docvortex/analyzers/native/pdf/script_geometry.py +636 -0
  167. docvortex/analyzers/native/pdf/shared.py +30 -0
  168. docvortex/analyzers/native/pdf/spatial_text.py +383 -0
  169. docvortex/analyzers/native/pdf/table_annotations.py +446 -0
  170. docvortex/analyzers/native/pdf/table_constants.py +48 -0
  171. docvortex/analyzers/native/pdf/table_detection.py +227 -0
  172. docvortex/analyzers/native/pdf/table_filled_grid.py +218 -0
  173. docvortex/analyzers/native/pdf/table_geometry.py +40 -0
  174. docvortex/analyzers/native/pdf/table_materialization.py +444 -0
  175. docvortex/analyzers/native/pdf/table_recovery/__init__.py +15 -0
  176. docvortex/analyzers/native/pdf/table_recovery/candidate.py +356 -0
  177. docvortex/analyzers/native/pdf/table_recovery/contracts.py +154 -0
  178. docvortex/analyzers/native/pdf/table_recovery/engine.py +609 -0
  179. docvortex/analyzers/native/pdf/table_recovery/geometry.py +164 -0
  180. docvortex/analyzers/native/pdf/table_recovery/sparse_common.py +85 -0
  181. docvortex/analyzers/native/pdf/table_recovery/sparse_hybrid.py +804 -0
  182. docvortex/analyzers/native/pdf/table_recovery/sparse_multiline.py +1122 -0
  183. docvortex/analyzers/native/pdf/table_recovery/text.py +414 -0
  184. docvortex/analyzers/native/pdf/table_recovery/text_grid.py +618 -0
  185. docvortex/analyzers/native/pdf/table_recovery/vector.py +1931 -0
  186. docvortex/analyzers/native/pdf/table_rows.py +34 -0
  187. docvortex/analyzers/native/pdf/table_rules.py +1129 -0
  188. docvortex/analyzers/native/pdf/table_text_styles.py +283 -0
  189. docvortex/analyzers/native/pdf/tables.py +147 -0
  190. docvortex/analyzers/native/pdf/text_assembly/__init__.py +3 -0
  191. docvortex/analyzers/native/pdf/text_assembly/annotations.py +581 -0
  192. docvortex/analyzers/native/pdf/text_assembly/assembly.py +292 -0
  193. docvortex/analyzers/native/pdf/text_assembly/common.py +477 -0
  194. docvortex/analyzers/native/pdf/text_assembly/footnotes.py +394 -0
  195. docvortex/analyzers/native/pdf/text_assembly/merging.py +1274 -0
  196. docvortex/analyzers/native/pdf/text_assembly/rows.py +692 -0
  197. docvortex/analyzers/native/pdf/text_blocks.py +82 -0
  198. docvortex/analyzers/native/pdf/text_styles.py +55 -0
  199. docvortex/analyzers/native/pdf/title_analysis/__init__.py +3 -0
  200. docvortex/analyzers/native/pdf/title_analysis/body_profile.py +215 -0
  201. docvortex/analyzers/native/pdf/title_analysis/common.py +117 -0
  202. docvortex/analyzers/native/pdf/title_analysis/document_profile.py +164 -0
  203. docvortex/analyzers/native/pdf/title_analysis/lane_titles.py +758 -0
  204. docvortex/analyzers/native/pdf/title_analysis/page_titles.py +1024 -0
  205. docvortex/analyzers/native/pdf/title_analysis/prototype.py +194 -0
  206. docvortex/analyzers/native/pdf/title_analysis/structural.py +1081 -0
  207. docvortex/analyzers/native/pdf/titles.py +75 -0
  208. docvortex/analyzers/native/pdf/typography.py +19 -0
  209. docvortex/analyzers/native/pdf/visual_annotations.py +1262 -0
  210. docvortex/api.py +180 -0
  211. docvortex/assets/__init__.py +5 -0
  212. docvortex/assets/store.py +51 -0
  213. docvortex/cli.py +54 -0
  214. docvortex/codecs/__init__.py +3 -0
  215. docvortex/codecs/html/__init__.py +22 -0
  216. docvortex/codecs/html/contracts.py +236 -0
  217. docvortex/codecs/html/materializer.py +331 -0
  218. docvortex/codecs/html/parser.py +763 -0
  219. docvortex/codecs/html/resources.py +34 -0
  220. docvortex/codecs/json.py +17 -0
  221. docvortex/content/__init__.py +5 -0
  222. docvortex/content/inline.py +248 -0
  223. docvortex/content/markup/__init__.py +44 -0
  224. docvortex/content/markup/anchors.py +188 -0
  225. docvortex/content/markup/formula.py +280 -0
  226. docvortex/content/markup/projector.py +1237 -0
  227. docvortex/content/markup/styles.py +327 -0
  228. docvortex/content/mathml.py +167 -0
  229. docvortex/content/normalization.py +188 -0
  230. docvortex/content/spans.py +183 -0
  231. docvortex/content/table/__init__.py +18 -0
  232. docvortex/content/table/blocks.py +152 -0
  233. docvortex/content/table/content.py +425 -0
  234. docvortex/content/table/document.py +104 -0
  235. docvortex/content/table/html.py +399 -0
  236. docvortex/content/table/models.py +76 -0
  237. docvortex/content/table/rules.py +42 -0
  238. docvortex/content/table/structure.py +221 -0
  239. docvortex/content/tree.py +5 -0
  240. docvortex/document/__init__.py +3 -0
  241. docvortex/document/contracts.py +22 -0
  242. docvortex/document/detection.py +389 -0
  243. docvortex/document/filetypes.py +175 -0
  244. docvortex/document/page_range.py +167 -0
  245. docvortex/document/pdf/__init__.py +19 -0
  246. docvortex/document/pdf/classify.py +1138 -0
  247. docvortex/document/pdf/constants.py +132 -0
  248. docvortex/document/pdf/diagnostics.py +350 -0
  249. docvortex/document/pdf/document.py +582 -0
  250. docvortex/document/pdf/font_runtime.py +335 -0
  251. docvortex/document/pdf/geometry.py +31 -0
  252. docvortex/document/pdf/images.py +654 -0
  253. docvortex/document/pdf/native_annotations.py +367 -0
  254. docvortex/document/pdf/native_contracts.py +169 -0
  255. docvortex/document/pdf/native_coordinates.py +216 -0
  256. docvortex/document/pdf/native_lifecycle.py +16 -0
  257. docvortex/document/pdf/native_objects.py +902 -0
  258. docvortex/document/pdf/native_text_geometry.py +315 -0
  259. docvortex/document/pdf/pdfium.py +325 -0
  260. docvortex/document/pdf/raster.py +46 -0
  261. docvortex/document/pdf/text/__init__.py +62 -0
  262. docvortex/document/pdf/text/contracts.py +211 -0
  263. docvortex/document/pdf/text/extract.py +165 -0
  264. docvortex/document/pdf/text/geometry.py +16 -0
  265. docvortex/document/pdf/text/groups.py +162 -0
  266. docvortex/document/pdf/visual_geometry.py +201 -0
  267. docvortex/document/pdf/visuals.py +343 -0
  268. docvortex/document/source.py +92 -0
  269. docvortex/errors.py +21 -0
  270. docvortex/export/__init__.py +3 -0
  271. docvortex/export/bundle.py +98 -0
  272. docvortex/export/files.py +66 -0
  273. docvortex/export/middle.py +208 -0
  274. docvortex/foundation/__init__.py +3 -0
  275. docvortex/foundation/geometry.py +125 -0
  276. docvortex/foundation/hyperlink.py +65 -0
  277. docvortex/foundation/image.py +48 -0
  278. docvortex/foundation/image_encoding.py +30 -0
  279. docvortex/foundation/image_payload.py +280 -0
  280. docvortex/foundation/language.py +92 -0
  281. docvortex/foundation/platform.py +38 -0
  282. docvortex/foundation/text.py +153 -0
  283. docvortex/foundation/type_identity.py +20 -0
  284. docvortex/foundation/xml_names.py +20 -0
  285. docvortex/options.py +30 -0
  286. docvortex/postprocess/__init__.py +3 -0
  287. docvortex/postprocess/content.py +53 -0
  288. docvortex/postprocess/document.py +19 -0
  289. docvortex/postprocess/lists.py +236 -0
  290. docvortex/postprocess/page_blocks.py +214 -0
  291. docvortex/postprocess/pages.py +95 -0
  292. docvortex/postprocess/paragraphs.py +580 -0
  293. docvortex/postprocess/visual.py +715 -0
  294. docvortex/render/__init__.py +48 -0
  295. docvortex/render/_internal/__init__.py +3 -0
  296. docvortex/render/_internal/common/__init__.py +3 -0
  297. docvortex/render/_internal/common/context.py +43 -0
  298. docvortex/render/_internal/common/html_table.py +178 -0
  299. docvortex/render/_internal/common/index.py +33 -0
  300. docvortex/render/_internal/common/list_items.py +158 -0
  301. docvortex/render/_internal/common/planner.py +140 -0
  302. docvortex/render/_internal/docx/__init__.py +3 -0
  303. docvortex/render/_internal/docx/assets.py +202 -0
  304. docvortex/render/_internal/docx/inline.py +434 -0
  305. docvortex/render/_internal/docx/math.py +220 -0
  306. docvortex/render/_internal/docx/renderer.py +905 -0
  307. docvortex/render/_internal/docx/styles.py +195 -0
  308. docvortex/render/_internal/docx/table.py +442 -0
  309. docvortex/render/_internal/epub/__init__.py +5 -0
  310. docvortex/render/_internal/epub/assets.py +173 -0
  311. docvortex/render/_internal/epub/package.py +249 -0
  312. docvortex/render/_internal/epub/renderer.py +1156 -0
  313. docvortex/render/_internal/html/__init__.py +3 -0
  314. docvortex/render/_internal/html/inline.py +346 -0
  315. docvortex/render/_internal/html/renderer.py +1041 -0
  316. docvortex/render/_internal/html/sanitizer.py +478 -0
  317. docvortex/render/_internal/html/table.py +121 -0
  318. docvortex/render/_internal/latex/__init__.py +1 -0
  319. docvortex/render/_internal/latex/assets.py +85 -0
  320. docvortex/render/_internal/latex/inline.py +145 -0
  321. docvortex/render/_internal/latex/renderer.py +506 -0
  322. docvortex/render/_internal/latex/table.py +347 -0
  323. docvortex/render/_internal/markdown/__init__.py +3 -0
  324. docvortex/render/_internal/markdown/assets.py +78 -0
  325. docvortex/render/_internal/markdown/blocks.py +635 -0
  326. docvortex/render/_internal/markdown/escaping.py +51 -0
  327. docvortex/render/_internal/markdown/inline.py +260 -0
  328. docvortex/render/_internal/markdown/renderer.py +93 -0
  329. docvortex/render/_internal/markdown/table.py +281 -0
  330. docvortex/render/_internal/pdf/__init__.py +3 -0
  331. docvortex/render/_internal/pdf/assets.py +197 -0
  332. docvortex/render/_internal/pdf/formula.py +417 -0
  333. docvortex/render/_internal/pdf/inline.py +343 -0
  334. docvortex/render/_internal/pdf/renderer.py +734 -0
  335. docvortex/render/_internal/pdf/styles.py +206 -0
  336. docvortex/render/_internal/pdf/table.py +272 -0
  337. docvortex/render/_internal/structured_content/__init__.py +3 -0
  338. docvortex/render/_internal/structured_content/renderer.py +193 -0
  339. docvortex/render/api.py +199 -0
  340. docvortex/render/contracts.py +205 -0
  341. docvortex/render/docx.py +38 -0
  342. docvortex/render/epub.py +35 -0
  343. docvortex/render/fragments.py +70 -0
  344. docvortex/render/html.py +29 -0
  345. docvortex/render/latex.py +24 -0
  346. docvortex/render/markdown.py +50 -0
  347. docvortex/render/pdf.py +25 -0
  348. docvortex/render/structured_content.py +23 -0
  349. docvortex/resources/epub/docvortex.css +91 -0
  350. docvortex/resources/fasttext-langdetect/lid.176.ftz +0 -0
  351. docvortex/resources/fonts/DroidSansFallbackFull.ttf +0 -0
  352. docvortex/resources/fonts/NOTICE +190 -0
  353. docvortex/resources/fonts/manifest.json +9 -0
  354. docvortex/resources/html/docvortex.css +601 -0
  355. docvortex/resources/html/docvortex.min.css +1 -0
  356. docvortex/result.py +91 -0
  357. docvortex/schema.py +1141 -0
  358. docvortex/version.py +3 -0
  359. docvortex-0.2.1.dist-info/METADATA +193 -0
  360. docvortex-0.2.1.dist-info/RECORD +364 -0
  361. docvortex-0.2.1.dist-info/WHEEL +5 -0
  362. docvortex-0.2.1.dist-info/entry_points.txt +2 -0
  363. docvortex-0.2.1.dist-info/licenses/LICENSE.md +21 -0
  364. 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
+ ]