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,98 @@
1
+ """保存及读取不依赖源文件、网络或宿主缓存的结果包。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict
6
+ from hashlib import sha256
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ..assets import AssetStore
12
+ from ..codecs.json import load_middle, load_model
13
+ from ..foundation.image_payload import INLINE_IMAGE_DATA_URI_RE, parse_image_data_uri_strict
14
+ from ..result import Diagnostic, DocumentResult, ExportResult
15
+ from .files import materialize_middle, validate_materialized_assets
16
+ from .middle import _commit_export_files, _resolve_export_target, _validate_export_path_relationships
17
+
18
+
19
+ def _externalize_model(value: Any, assets: AssetStore) -> Any:
20
+ """在原始分析副本中外置图片及 HTML 内嵌素材,保持可独立重放。"""
21
+ if isinstance(value, list):
22
+ return [_externalize_model(item, assets) for item in value]
23
+ if isinstance(value, dict):
24
+ result = {key: _externalize_model(item, assets) for key, item in value.items()}
25
+ image = value.get("image_base64")
26
+ if isinstance(image, str):
27
+ result["image_path"] = _add_image(image, assets)
28
+ result.pop("image_base64", None)
29
+ return result
30
+ if isinstance(value, str) and "data:image/" in value and not value.startswith("data:image/"):
31
+ return INLINE_IMAGE_DATA_URI_RE.sub(lambda match: _add_image(match.group(0), assets), value)
32
+ return value
33
+
34
+
35
+ def _add_image(data_uri: str, assets: AssetStore) -> str:
36
+ """按内容摘要登记分析阶段素材,避免产生不稳定文件名。"""
37
+ payload, extension = parse_image_data_uri_strict(data_uri)
38
+ path = f"images/{sha256(payload).hexdigest()}.{extension}"
39
+ assets.add(path, payload)
40
+ return path
41
+
42
+
43
+ def save_bundle(result: DocumentResult, path: Path, *, overwrite: bool = False) -> ExportResult:
44
+ """将中间协议与素材清单放入同一文件事务。"""
45
+ middle, assets = materialize_middle(result.middle_json, result.assets)
46
+ validate_materialized_assets(middle, assets)
47
+ files: dict[str, bytes] = {"middle.json": middle.to_json(skip_defaults=False).encode("utf-8")}
48
+ manifest: dict[str, Any] = {
49
+ "schema": "docvortex.bundle",
50
+ "schema_version": "2.0",
51
+ "middle": "middle.json",
52
+ "diagnostics": [asdict(item) for item in result.diagnostics],
53
+ }
54
+ if result.model_json is not None:
55
+ model = _externalize_model(result.model_json.to_dict(skip_defaults=False), assets)
56
+ files["model.json"] = json.dumps(model, ensure_ascii=False, indent=2).encode("utf-8")
57
+ manifest["model"] = "model.json"
58
+ manifest["assets"] = [
59
+ {"path": name, "size": len(payload), "sha256": sha256(payload).hexdigest()} for name, payload in sorted(assets.items())
60
+ ]
61
+ if {"manifest.json", "middle.json", "model.json"}.intersection(assets):
62
+ raise ValueError("Bundle assets conflict with reserved document files")
63
+ files.update(assets)
64
+ files["manifest.json"] = json.dumps(manifest, ensure_ascii=False, indent=2).encode("utf-8")
65
+ _validate_export_path_relationships(list(files))
66
+ path = path.absolute()
67
+ _commit_export_files({_resolve_export_target(path, name): value for name, value in files.items()}, overwrite=overwrite)
68
+ return ExportResult(path / "manifest.json", tuple(path / name for name in assets))
69
+
70
+
71
+ def load_bundle(path: str | Path) -> DocumentResult:
72
+ """校验结果包版本、路径及素材摘要后恢复可渲染结果。"""
73
+ root = Path(path).absolute()
74
+ manifest = json.loads(_resolve_export_target(root, "manifest.json").read_text(encoding="utf-8"))
75
+ if manifest.get("schema") != "docvortex.bundle" or manifest.get("schema_version") != "2.0":
76
+ raise ValueError("Unsupported DocVortex bundle schema")
77
+ if manifest.get("middle") != "middle.json" or manifest.get("model") not in {None, "model.json"}:
78
+ raise ValueError("Invalid bundle document paths")
79
+ assets = AssetStore()
80
+ for item in manifest["assets"]:
81
+ if item["path"] in {"manifest.json", "middle.json", "model.json"}:
82
+ raise ValueError("Bundle asset uses a reserved document path")
83
+ payload = _resolve_export_target(root, item["path"]).read_bytes()
84
+ if len(payload) != item["size"] or sha256(payload).hexdigest() != item["sha256"]:
85
+ raise ValueError(f"Bundle asset integrity mismatch: {item['path']}")
86
+ assets.add(item["path"], payload)
87
+ middle = load_middle(json.loads(_resolve_export_target(root, "middle.json").read_text(encoding="utf-8")))
88
+ validate_materialized_assets(middle, assets)
89
+ model = (
90
+ load_model(json.loads(_resolve_export_target(root, "model.json").read_text(encoding="utf-8")))
91
+ if manifest.get("model")
92
+ else None
93
+ )
94
+ diagnostics = tuple(Diagnostic(**item) for item in manifest.get("diagnostics", []))
95
+ return DocumentResult(middle_json=middle, assets=assets, model_json=model, diagnostics=diagnostics)
96
+
97
+
98
+ __all__ = ["save_bundle", "load_bundle"]
@@ -0,0 +1,66 @@
1
+ """独立渲染文件和文档素材的统一写出。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from ..assets import AssetStore
8
+ from ..content.tree import iter_child_blocks
9
+ from ..schema import MiddleJson
10
+ from ..result import ExportResult, RenderArtifact
11
+ from .middle import _commit_export_files, _prepare_export_copy, _resolve_export_target, _validate_export_path_relationships
12
+
13
+
14
+ def materialize_middle(middle_json: MiddleJson, assets: AssetStore | None = None) -> tuple[MiddleJson, AssetStore]:
15
+ """在文档副本上外置所有内嵌图片,保留调用者提供的外部素材。"""
16
+ document, embedded = _prepare_export_copy(middle_json)
17
+ result = assets.copy() if assets is not None else AssetStore()
18
+ for path, payload in embedded.items():
19
+ result.add(path, payload)
20
+ pending = [block for page in document.pages for block in page.blocks]
21
+ while pending:
22
+ block = pending.pop()
23
+ pending.extend(iter_child_blocks(block))
24
+ image_path = getattr(block, "image_path", None)
25
+ if image_path and image_path in result and getattr(block, "image_url", None):
26
+ block.image_url = None
27
+ return document, result
28
+
29
+
30
+ def validate_materialized_assets(middle_json: MiddleJson, assets: AssetStore) -> None:
31
+ """拒绝依赖外部文件或网络的未物化图片,保证结果包能离线重渲染。"""
32
+ from bs4 import BeautifulSoup
33
+
34
+ pending = [block for page in middle_json.pages for block in page.blocks]
35
+ while pending:
36
+ block = pending.pop()
37
+ pending.extend(iter_child_blocks(block))
38
+ image_path = getattr(block, "image_path", None)
39
+ if image_path and image_path not in assets:
40
+ raise ValueError(f"Missing materialized asset: {image_path}")
41
+ if getattr(block, "image_url", None) and not image_path:
42
+ raise ValueError("An external image must be materialized before saving a bundle")
43
+ content = getattr(block, "content", None)
44
+ if str(block.type) not in {"table_body", "chart_body", "image_body"} or not isinstance(content, str):
45
+ continue
46
+ if "<img" not in content.lower():
47
+ continue
48
+ for image in BeautifulSoup(content, "html.parser").find_all("img"):
49
+ reference = image.get("src")
50
+ if reference and reference not in assets:
51
+ raise ValueError(f"Missing materialized HTML image: {reference}")
52
+
53
+
54
+ def write_artifact(artifact: RenderArtifact, path: Path, *, overwrite: bool) -> ExportResult:
55
+ """检查主文件与素材的路径关系后,以同一事务写出。"""
56
+ path = path.absolute()
57
+ relative_files = {path.name: artifact.content, **dict(artifact.assets)}
58
+ if path.name in artifact.assets:
59
+ raise ValueError("Main file conflicts with an asset")
60
+ _validate_export_path_relationships(list(relative_files))
61
+ targets = {_resolve_export_target(path.parent, name): payload for name, payload in relative_files.items()}
62
+ _commit_export_files(targets, overwrite=overwrite)
63
+ return ExportResult(path=path, asset_paths=tuple(path.parent / name for name in artifact.assets))
64
+
65
+
66
+ __all__ = ["materialize_middle", "validate_materialized_assets", "write_artifact"]
@@ -0,0 +1,208 @@
1
+ """语义文档与图片旁文件的原子导出。"""
2
+
3
+ from __future__ import annotations
4
+ import os
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from tempfile import NamedTemporaryFile
8
+ from typing import Any, Sequence
9
+ from ..schema import MiddleJson, BlockBase, _iter_child_blocks
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class MiddleJsonExportResult:
14
+ """Middle JSON 图片外置后的对象副本与实际文件路径。"""
15
+
16
+ middle_json: MiddleJson
17
+ json_path: Path
18
+ image_paths: tuple[Path, ...]
19
+
20
+
21
+ def _iter_page_blocks(blocks: Sequence[BlockBase]) -> list[BlockBase]:
22
+ """按深度优先顺序展开页面 block 树,供图片外置统一遍历。"""
23
+ result: list[BlockBase] = []
24
+ pending: list[BlockBase] = list(reversed(blocks))
25
+ while pending:
26
+ block = pending.pop()
27
+ result.append(block)
28
+ pending.extend(reversed(_iter_child_blocks(block)))
29
+ return result
30
+
31
+
32
+ def _register_export_file(files: dict[str, bytes], relative_path: str, payload: bytes) -> None:
33
+ """登记待写文件,并在同名内容冲突时立即终止导出。"""
34
+ existing = files.get(relative_path)
35
+ if existing is not None and existing != payload:
36
+ raise ValueError(f"Conflicting image payload for path: {relative_path}")
37
+ files[relative_path] = payload
38
+
39
+
40
+ def _prepare_export_copy(middle_json: MiddleJson) -> tuple[MiddleJson, dict[str, bytes]]:
41
+ """复制对象、解析直接及 HTML 图片,并回填副本中的相对路径。"""
42
+ from ..foundation.image_payload import (
43
+ INLINE_IMAGE_DATA_URI_RE,
44
+ parse_image_data_uri_strict,
45
+ )
46
+
47
+ exported = middle_json.model_copy(deep=True)
48
+ image_files: dict[str, bytes] = {}
49
+ for page in exported.pages:
50
+ for block in _iter_page_blocks(page.blocks):
51
+ data_uri = getattr(block, "image_base64", None)
52
+ if data_uri is not None:
53
+ if block.index is None:
54
+ raise ValueError(f"Image carrier requires index: page_idx={page.page_idx}, type={block.type}")
55
+ image_bytes, extension = parse_image_data_uri_strict(data_uri)
56
+ relative_path = f"images/page_{page.page_idx}_{block.type}_{block.index}.{extension}"
57
+ _register_export_file(image_files, relative_path, image_bytes)
58
+ block.image_path = relative_path # type: ignore[attr-defined]
59
+ block.image_base64 = None # type: ignore[attr-defined]
60
+
61
+ content = getattr(block, "content", None)
62
+ if not isinstance(content, str) or "data:image/" not in content:
63
+ continue
64
+ if block.index is None:
65
+ raise ValueError(f"HTML image carrier requires index: page_idx={page.page_idx}, type={block.type}")
66
+ ordinal = 0
67
+
68
+ def _replace_data_uri(match: Any) -> str:
69
+ """将当前 HTML data URI 登记为 sidecar,并返回确定性相对路径。"""
70
+ nonlocal ordinal
71
+ ordinal += 1
72
+ image_bytes, extension = parse_image_data_uri_strict(match.group(0))
73
+ relative_path = f"images/page_{page.page_idx}_{block.type}_{block.index}_{ordinal}.{extension}"
74
+ _register_export_file(image_files, relative_path, image_bytes)
75
+ return relative_path
76
+
77
+ block.content = INLINE_IMAGE_DATA_URI_RE.sub(_replace_data_uri, content) # type: ignore[assignment]
78
+ return exported, image_files
79
+
80
+
81
+ def _resolve_export_target(output_root: Path, relative_path: str) -> Path:
82
+ """校验导出相对路径并确保解析后的目标仍位于文档输出目录内。"""
83
+ from ..foundation.image_payload import validate_image_sidecar_path
84
+
85
+ safe_path = validate_image_sidecar_path(relative_path)
86
+ if output_root.is_symlink():
87
+ raise ValueError(f"Export directory must not be a symlink: {output_root}")
88
+ if output_root.exists() and not output_root.is_dir():
89
+ raise ValueError(f"Export directory must be a directory: {output_root}")
90
+ root = output_root.resolve()
91
+ target = root / safe_path
92
+ current_parent = root
93
+ for path_part in Path(safe_path).parts[:-1]:
94
+ current_parent /= path_part
95
+ if current_parent.is_symlink():
96
+ raise ValueError(f"Export path contains a symlink: {relative_path}")
97
+ if current_parent.exists() and not current_parent.is_dir():
98
+ raise ValueError(f"Export path parent is not a directory: {relative_path}")
99
+ try:
100
+ target.parent.resolve(strict=False).relative_to(root)
101
+ except ValueError as exc:
102
+ raise ValueError(f"Export path escapes output directory: {relative_path}") from exc
103
+ return target
104
+
105
+
106
+ def _commit_export_files(files: dict[Path, bytes], *, overwrite: bool) -> None:
107
+ """预检冲突后以临时文件提交,并在提交失败时恢复已有文件。"""
108
+ pending: dict[Path, bytes] = {}
109
+ originals: dict[Path, bytes | None] = {}
110
+ for target, payload in files.items():
111
+ if target.is_symlink():
112
+ raise ValueError(f"Export target must not be a symlink: {target}")
113
+ if target.exists():
114
+ if not target.is_file():
115
+ raise ValueError(f"Export target is not a regular file: {target}")
116
+ current = target.read_bytes()
117
+ if current == payload:
118
+ continue
119
+ if not overwrite:
120
+ raise FileExistsError(f"Export target already exists with different content: {target}")
121
+ originals[target] = current
122
+ else:
123
+ originals[target] = None
124
+ pending[target] = payload
125
+
126
+ temp_paths: dict[Path, Path] = {}
127
+ committed: list[Path] = []
128
+ try:
129
+ for target, payload in pending.items():
130
+ target.parent.mkdir(parents=True, exist_ok=True)
131
+ with NamedTemporaryFile(
132
+ mode="wb",
133
+ prefix=".docvortex-export-",
134
+ dir=target.parent,
135
+ delete=False,
136
+ ) as temp_file:
137
+ temp_file.write(payload)
138
+ temp_paths[target] = Path(temp_file.name)
139
+ for target, temp_path in temp_paths.items():
140
+ os.replace(temp_path, target)
141
+ committed.append(target)
142
+ except Exception:
143
+ for temp_path in temp_paths.values():
144
+ temp_path.unlink(missing_ok=True)
145
+ for target in reversed(committed):
146
+ original = originals[target]
147
+ if original is None:
148
+ target.unlink(missing_ok=True)
149
+ else:
150
+ target.write_bytes(original)
151
+ raise
152
+
153
+
154
+ def _validate_export_path_relationships(relative_paths: list[str]) -> None:
155
+ """拒绝任一导出文件占用另一文件的父目录,避免提交阶段才产生冲突。"""
156
+ path_parts = {relative_path: Path(relative_path).parts for relative_path in relative_paths}
157
+ for relative_path, parts in path_parts.items():
158
+ for other_path, other_parts in path_parts.items():
159
+ if relative_path == other_path or len(parts) >= len(other_parts):
160
+ continue
161
+ if other_parts[: len(parts)] == parts:
162
+ raise ValueError(f"Export file path conflicts with a required directory: {relative_path} -> {other_path}")
163
+
164
+
165
+ def _export_middle_json(
166
+ middle_json: MiddleJson,
167
+ output_dir: Path,
168
+ *,
169
+ json_name: str,
170
+ overwrite: bool,
171
+ ) -> MiddleJsonExportResult:
172
+ """构造完整导出事务,保证 JSON 与图片使用同一份规范化对象副本。"""
173
+ from ..foundation.image_payload import validate_image_sidecar_path
174
+
175
+ exported, image_files = _prepare_export_copy(middle_json)
176
+ json_text = exported.to_json(
177
+ exclude_block_fields={"image_base64"},
178
+ )
179
+ if "data:image/" in json_text.lower():
180
+ raise ValueError("Exported Middle JSON still contains an inline image data URI")
181
+ json_bytes = json_text.encode("utf-8")
182
+ safe_json_name = validate_image_sidecar_path(json_name)
183
+ relative_files = dict(image_files)
184
+ if safe_json_name in relative_files:
185
+ raise ValueError(f"JSON path conflicts with an exported image: {safe_json_name}")
186
+ relative_files[safe_json_name] = json_bytes
187
+ _validate_export_path_relationships(list(relative_files))
188
+ absolute_files = {
189
+ _resolve_export_target(output_dir, relative_path): payload for relative_path, payload in relative_files.items()
190
+ }
191
+ _commit_export_files(absolute_files, overwrite=overwrite)
192
+ json_path = _resolve_export_target(output_dir, safe_json_name)
193
+ image_paths = tuple(_resolve_export_target(output_dir, relative_path) for relative_path in sorted(image_files))
194
+ return MiddleJsonExportResult(
195
+ middle_json=exported,
196
+ json_path=json_path,
197
+ image_paths=image_paths,
198
+ )
199
+
200
+
201
+ def export_middle_json(
202
+ middle_json: MiddleJson, output_dir: str | Path, *, json_name: str = "middle_json.json", overwrite: bool = False
203
+ ) -> MiddleJsonExportResult:
204
+ """显式导出语义协议及图片,保持基础 schema 无文件系统依赖。"""
205
+ return _export_middle_json(middle_json, Path(output_dir), json_name=json_name, overwrite=overwrite)
206
+
207
+
208
+ __all__ = ["MiddleJsonExportResult", "export_middle_json"]
@@ -0,0 +1,3 @@
1
+ """DocVortex 模块边界。"""
2
+
3
+ __all__ = []
@@ -0,0 +1,125 @@
1
+ import math
2
+
3
+ import numpy as np
4
+
5
+ from ..schema import BBox, IntBBox
6
+
7
+
8
+ def normalize_to_int_bbox(
9
+ box: list[list[float]] | list[float] | BBox | None,
10
+ image_size: tuple[int, int] | None = None,
11
+ ) -> IntBBox | None:
12
+ if box is None:
13
+ return None
14
+
15
+ arr = np.asarray(box, dtype=np.float64)
16
+ if arr.size == 0:
17
+ return None
18
+
19
+ if arr.ndim == 2 and arr.shape[-1] == 2:
20
+ xs = arr[:, 0]
21
+ ys = arr[:, 1]
22
+ xmin = float(np.min(xs))
23
+ ymin = float(np.min(ys))
24
+ xmax = float(np.max(xs))
25
+ ymax = float(np.max(ys))
26
+ else:
27
+ flat = arr.reshape(-1)
28
+ if flat.size == 4:
29
+ xmin, ymin, xmax, ymax = [float(v) for v in flat]
30
+ elif flat.size >= 8:
31
+ xs = flat[0::2]
32
+ ys = flat[1::2]
33
+ xmin = float(np.min(xs))
34
+ ymin = float(np.min(ys))
35
+ xmax = float(np.max(xs))
36
+ ymax = float(np.max(ys))
37
+ else:
38
+ return None
39
+
40
+ xmin = math.floor(xmin)
41
+ ymin = math.floor(ymin)
42
+ xmax = math.ceil(xmax)
43
+ ymax = math.ceil(ymax)
44
+
45
+ if image_size is not None:
46
+ height, width = image_size
47
+ xmin = max(0, min(int(width), xmin))
48
+ ymin = max(0, min(int(height), ymin))
49
+ xmax = max(0, min(int(width), xmax))
50
+ ymax = max(0, min(int(height), ymax))
51
+
52
+ if xmax <= xmin or ymax <= ymin:
53
+ return None
54
+
55
+ return (int(xmin), int(ymin), int(xmax), int(ymax))
56
+
57
+
58
+ def bbox_relative_pos(bbox1: BBox, bbox2: BBox) -> tuple[bool, bool, bool, bool]:
59
+ """返回 bbox1 相对 bbox2 的左、右、下、上位置关系。"""
60
+ x1, y1, x1b, y1b = bbox1
61
+ x2, y2, x2b, y2b = bbox2
62
+ left = x2b < x1
63
+ right = x1b < x2
64
+ bottom = y2b < y1
65
+ top = y1b < y2
66
+ return left, right, bottom, top
67
+
68
+
69
+ def bbox_distance(bbox1: BBox, bbox2: BBox) -> float:
70
+ """计算两个不相交矩形之间的最短欧氏距离。"""
71
+ x1, y1, x1b, y1b = bbox1
72
+ x2, y2, x2b, y2b = bbox2
73
+ left, right, bottom, top = bbox_relative_pos(bbox1, bbox2)
74
+ if top and left:
75
+ return math.hypot(x1 - x2b, y1b - y2)
76
+ if left and bottom:
77
+ return math.hypot(x1 - x2b, y1 - y2b)
78
+ if bottom and right:
79
+ return math.hypot(x1b - x2, y1 - y2b)
80
+ if right and top:
81
+ return math.hypot(x1b - x2, y1b - y2)
82
+ if left:
83
+ return x1 - x2b
84
+ if right:
85
+ return x2 - x1b
86
+ if bottom:
87
+ return y1 - y2b
88
+ if top:
89
+ return y2 - y1b
90
+ return 0.0
91
+
92
+
93
+ def bbox_center_distance(bbox1: BBox, bbox2: BBox) -> float:
94
+ """计算两个矩形中心点之间的欧氏距离。"""
95
+ center1 = ((bbox1[0] + bbox1[2]) / 2, (bbox1[1] + bbox1[3]) / 2)
96
+ center2 = ((bbox2[0] + bbox2[2]) / 2, (bbox2[1] + bbox2[3]) / 2)
97
+ return math.hypot(center1[0] - center2[0], center1[1] - center2[1])
98
+
99
+
100
+ def calculate_overlap_area_2_minbox_area_ratio(bbox1: BBox, bbox2: BBox) -> float:
101
+ """计算交集面积占两个矩形较小面积的比例。"""
102
+ x_left = max(bbox1[0], bbox2[0])
103
+ y_top = max(bbox1[1], bbox2[1])
104
+ x_right = min(bbox1[2], bbox2[2])
105
+ y_bottom = min(bbox1[3], bbox2[3])
106
+ if x_right <= x_left or y_bottom <= y_top:
107
+ return 0.0
108
+ overlap = (x_right - x_left) * (y_bottom - y_top)
109
+ area1 = max(0.0, (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]))
110
+ area2 = max(0.0, (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]))
111
+ min_area = min(area1, area2)
112
+ return overlap / min_area if min_area > 0 else 0.0
113
+
114
+
115
+ def calculate_overlap_area_in_bbox1_area_ratio(bbox1: BBox, bbox2: BBox) -> float:
116
+ """计算两个矩形交集面积占 bbox1 面积的比例。"""
117
+ x_left = max(bbox1[0], bbox2[0])
118
+ y_top = max(bbox1[1], bbox2[1])
119
+ x_right = min(bbox1[2], bbox2[2])
120
+ y_bottom = min(bbox1[3], bbox2[3])
121
+ if x_right <= x_left or y_bottom <= y_top:
122
+ return 0.0
123
+ overlap = (x_right - x_left) * (y_bottom - y_top)
124
+ area1 = max(0.0, (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]))
125
+ return overlap / area1 if area1 > 0 else 0.0
@@ -0,0 +1,65 @@
1
+ """不依赖解析器层的安全超链接目标校验。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Collection
6
+ from typing import Any, Final
7
+ from urllib.parse import urlsplit
8
+
9
+
10
+ DEFAULT_EXTERNAL_HYPERLINK_SCHEMES: Final = frozenset({"http", "https", "mailto", "tel"})
11
+ OFFICE_EXTERNAL_HYPERLINK_SCHEMES: Final = DEFAULT_EXTERNAL_HYPERLINK_SCHEMES | {"ftp"}
12
+
13
+
14
+ def sanitize_hyperlink_target(
15
+ value: Any,
16
+ *,
17
+ allowed_schemes: Collection[str] = DEFAULT_EXTERNAL_HYPERLINK_SCHEMES,
18
+ allow_relative: bool = False,
19
+ allow_fragment: bool = False,
20
+ allow_root_relative: bool = False,
21
+ ) -> str | None:
22
+ """按调用方策略保留安全外链、相对链接或文档内 fragment。"""
23
+ if value is None:
24
+ return None
25
+ normalized = str(value).strip()
26
+ if (
27
+ not normalized
28
+ or normalized == "."
29
+ or any(ord(char) < 0x20 or ord(char) == 0x7F or 0xD800 <= ord(char) <= 0xDFFF for char in normalized)
30
+ or any(char in normalized for char in ("<", ">", "\\"))
31
+ or normalized.startswith("//")
32
+ ):
33
+ return None
34
+ try:
35
+ parsed = urlsplit(normalized)
36
+ except ValueError:
37
+ return None
38
+ scheme = parsed.scheme.casefold()
39
+ allowed = {item.casefold() for item in allowed_schemes}
40
+ if scheme:
41
+ if scheme not in allowed:
42
+ return None
43
+ if scheme in {"http", "https", "ftp"}:
44
+ try:
45
+ if not parsed.netloc or parsed.hostname is None:
46
+ return None
47
+ except ValueError:
48
+ return None
49
+ elif not parsed.path:
50
+ return None
51
+ return normalized
52
+ if parsed.netloc:
53
+ return None
54
+ if normalized.startswith("#"):
55
+ return normalized if allow_fragment and normalized[1:].strip() else None
56
+ if normalized.startswith("/") and not allow_root_relative:
57
+ return None
58
+ return normalized if allow_relative else None
59
+
60
+
61
+ __all__ = [
62
+ "DEFAULT_EXTERNAL_HYPERLINK_SCHEMES",
63
+ "OFFICE_EXTERNAL_HYPERLINK_SCHEMES",
64
+ "sanitize_hyperlink_target",
65
+ ]
@@ -0,0 +1,48 @@
1
+ """跨模型共享的轻量图像统计与裁剪原语。"""
2
+
3
+ import cv2
4
+ import numpy as np
5
+ from PIL import Image
6
+
7
+ from ..schema import BBox
8
+ from .geometry import normalize_to_int_bbox
9
+
10
+
11
+ def calculate_contrast(img: np.ndarray, img_mode: str) -> float:
12
+ """
13
+ 计算给定图像的对比度。
14
+ :param img: 图像,类型为numpy.ndarray
15
+ :Param img_mode = 图像的色彩通道,'rgb' 或 'bgr'
16
+ :return: 图像的对比度值
17
+ """
18
+ if img_mode == "rgb":
19
+ # 将RGB图像转换为灰度图
20
+ gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
21
+ elif img_mode == "bgr":
22
+ # 将BGR图像转换为灰度图
23
+ gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
24
+ else:
25
+ raise ValueError("Invalid image mode. Please provide 'rgb' or 'bgr'.")
26
+
27
+ # 计算均值和标准差
28
+ mean_value = np.mean(gray_img)
29
+ std_dev = np.std(gray_img)
30
+ # 对比度定义为标准差除以平均值(加上小常数避免除零错误)
31
+ contrast = std_dev / (mean_value + 1e-6)
32
+ # logger.debug(f"contrast: {contrast}")
33
+ return round(float(contrast), 2)
34
+
35
+
36
+ def crop_pil_image(bbox: BBox, image: Image.Image) -> Image.Image:
37
+ """按 0-1 归一化 bbox 裁剪 Pillow 图像。"""
38
+ width, height = image.size
39
+ scaled_bbox = normalize_to_int_bbox(
40
+ [bbox[0] * width, bbox[1] * height, bbox[2] * width, bbox[3] * height],
41
+ image_size=(height, width),
42
+ )
43
+ if scaled_bbox is None:
44
+ return image.crop((0, 0, 0, 0))
45
+ return image.crop(scaled_bbox)
46
+
47
+
48
+ __all__ = ["calculate_contrast", "crop_pil_image"]
@@ -0,0 +1,30 @@
1
+ """Flash 各格式复用的轻量图片编码能力。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from io import BytesIO
7
+
8
+ from PIL import Image
9
+
10
+
11
+ def image_to_bytes(
12
+ image: Image.Image,
13
+ image_format: str = "JPEG",
14
+ ) -> bytes:
15
+ """按指定格式把 Pillow 图片编码为字节。"""
16
+ with BytesIO() as image_buffer:
17
+ image.save(image_buffer, format=image_format)
18
+ return image_buffer.getvalue()
19
+
20
+
21
+ def image_to_b64str(
22
+ image: Image.Image,
23
+ image_format: str = "JPEG",
24
+ ) -> str:
25
+ """按指定格式把 Pillow 图片编码为 data URI。"""
26
+ image_bytes = image_to_bytes(image, image_format)
27
+ return f"data:image/{image_format.lower()};base64,{base64.b64encode(image_bytes).decode('utf-8')}"
28
+
29
+
30
+ __all__ = ["image_to_b64str", "image_to_bytes"]