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,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
+ )