kreuzberg 4.0.8 → 4.1.1

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 (312) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +1 -1
  4. data/ext/kreuzberg_rb/native/Cargo.lock +94 -98
  5. data/ext/kreuzberg_rb/native/Cargo.toml +4 -2
  6. data/ext/kreuzberg_rb/native/src/batch.rs +139 -0
  7. data/ext/kreuzberg_rb/native/src/config/mod.rs +10 -0
  8. data/ext/kreuzberg_rb/native/src/config/types.rs +1058 -0
  9. data/ext/kreuzberg_rb/native/src/error_handling.rs +125 -0
  10. data/ext/kreuzberg_rb/native/src/extraction.rs +79 -0
  11. data/ext/kreuzberg_rb/native/src/gc_guarded_value.rs +35 -0
  12. data/ext/kreuzberg_rb/native/src/helpers.rs +176 -0
  13. data/ext/kreuzberg_rb/native/src/lib.rs +342 -3622
  14. data/ext/kreuzberg_rb/native/src/metadata.rs +34 -0
  15. data/ext/kreuzberg_rb/native/src/plugins/mod.rs +92 -0
  16. data/ext/kreuzberg_rb/native/src/plugins/ocr_backend.rs +159 -0
  17. data/ext/kreuzberg_rb/native/src/plugins/post_processor.rs +126 -0
  18. data/ext/kreuzberg_rb/native/src/plugins/validator.rs +99 -0
  19. data/ext/kreuzberg_rb/native/src/result.rs +326 -0
  20. data/ext/kreuzberg_rb/native/src/validation.rs +4 -0
  21. data/lib/kreuzberg/config.rb +99 -2
  22. data/lib/kreuzberg/result.rb +107 -2
  23. data/lib/kreuzberg/types.rb +104 -0
  24. data/lib/kreuzberg/version.rb +1 -1
  25. data/lib/kreuzberg.rb +0 -4
  26. data/sig/kreuzberg.rbs +105 -1
  27. data/spec/fixtures/config.toml +1 -1
  28. data/spec/fixtures/config.yaml +1 -1
  29. data/vendor/Cargo.toml +3 -3
  30. data/vendor/kreuzberg/Cargo.toml +5 -4
  31. data/vendor/kreuzberg/README.md +1 -1
  32. data/vendor/kreuzberg/src/api/config.rs +69 -0
  33. data/vendor/kreuzberg/src/api/handlers.rs +99 -2
  34. data/vendor/kreuzberg/src/api/mod.rs +14 -7
  35. data/vendor/kreuzberg/src/api/router.rs +214 -0
  36. data/vendor/kreuzberg/src/api/startup.rs +243 -0
  37. data/vendor/kreuzberg/src/api/types.rs +78 -0
  38. data/vendor/kreuzberg/src/cache/cleanup.rs +277 -0
  39. data/vendor/kreuzberg/src/cache/core.rs +428 -0
  40. data/vendor/kreuzberg/src/cache/mod.rs +21 -843
  41. data/vendor/kreuzberg/src/cache/utilities.rs +156 -0
  42. data/vendor/kreuzberg/src/chunking/boundaries.rs +301 -0
  43. data/vendor/kreuzberg/src/chunking/builder.rs +294 -0
  44. data/vendor/kreuzberg/src/chunking/config.rs +52 -0
  45. data/vendor/kreuzberg/src/chunking/core.rs +1017 -0
  46. data/vendor/kreuzberg/src/chunking/mod.rs +14 -2211
  47. data/vendor/kreuzberg/src/chunking/processor.rs +10 -0
  48. data/vendor/kreuzberg/src/chunking/validation.rs +686 -0
  49. data/vendor/kreuzberg/src/core/config/extraction/core.rs +169 -0
  50. data/vendor/kreuzberg/src/core/config/extraction/env.rs +179 -0
  51. data/vendor/kreuzberg/src/core/config/extraction/loaders.rs +204 -0
  52. data/vendor/kreuzberg/src/core/config/extraction/mod.rs +42 -0
  53. data/vendor/kreuzberg/src/core/config/extraction/types.rs +93 -0
  54. data/vendor/kreuzberg/src/core/config/formats.rs +135 -0
  55. data/vendor/kreuzberg/src/core/config/mod.rs +20 -0
  56. data/vendor/kreuzberg/src/core/config/ocr.rs +73 -0
  57. data/vendor/kreuzberg/src/core/config/page.rs +57 -0
  58. data/vendor/kreuzberg/src/core/config/pdf.rs +111 -0
  59. data/vendor/kreuzberg/src/core/config/processing.rs +312 -0
  60. data/vendor/kreuzberg/src/core/config_validation/dependencies.rs +187 -0
  61. data/vendor/kreuzberg/src/core/config_validation/mod.rs +386 -0
  62. data/vendor/kreuzberg/src/core/config_validation/sections.rs +401 -0
  63. data/vendor/kreuzberg/src/core/extractor/batch.rs +246 -0
  64. data/vendor/kreuzberg/src/core/extractor/bytes.rs +116 -0
  65. data/vendor/kreuzberg/src/core/extractor/file.rs +240 -0
  66. data/vendor/kreuzberg/src/core/extractor/helpers.rs +71 -0
  67. data/vendor/kreuzberg/src/core/extractor/legacy.rs +62 -0
  68. data/vendor/kreuzberg/src/core/extractor/mod.rs +490 -0
  69. data/vendor/kreuzberg/src/core/extractor/sync.rs +208 -0
  70. data/vendor/kreuzberg/src/core/mime.rs +15 -0
  71. data/vendor/kreuzberg/src/core/mod.rs +4 -1
  72. data/vendor/kreuzberg/src/core/pipeline/cache.rs +60 -0
  73. data/vendor/kreuzberg/src/core/pipeline/execution.rs +89 -0
  74. data/vendor/kreuzberg/src/core/pipeline/features.rs +108 -0
  75. data/vendor/kreuzberg/src/core/pipeline/format.rs +392 -0
  76. data/vendor/kreuzberg/src/core/pipeline/initialization.rs +67 -0
  77. data/vendor/kreuzberg/src/core/pipeline/mod.rs +135 -0
  78. data/vendor/kreuzberg/src/core/pipeline/tests.rs +975 -0
  79. data/vendor/kreuzberg/src/core/server_config/env.rs +90 -0
  80. data/vendor/kreuzberg/src/core/server_config/loader.rs +202 -0
  81. data/vendor/kreuzberg/src/core/server_config/mod.rs +380 -0
  82. data/vendor/kreuzberg/src/core/server_config/tests/basic_tests.rs +124 -0
  83. data/vendor/kreuzberg/src/core/server_config/tests/env_tests.rs +216 -0
  84. data/vendor/kreuzberg/src/core/server_config/tests/file_loading_tests.rs +341 -0
  85. data/vendor/kreuzberg/src/core/server_config/tests/mod.rs +5 -0
  86. data/vendor/kreuzberg/src/core/server_config/validation.rs +17 -0
  87. data/vendor/kreuzberg/src/embeddings.rs +136 -13
  88. data/vendor/kreuzberg/src/extraction/{archive.rs → archive/mod.rs} +45 -239
  89. data/vendor/kreuzberg/src/extraction/archive/sevenz.rs +98 -0
  90. data/vendor/kreuzberg/src/extraction/archive/tar.rs +118 -0
  91. data/vendor/kreuzberg/src/extraction/archive/zip.rs +101 -0
  92. data/vendor/kreuzberg/src/extraction/html/converter.rs +592 -0
  93. data/vendor/kreuzberg/src/extraction/html/image_handling.rs +95 -0
  94. data/vendor/kreuzberg/src/extraction/html/mod.rs +53 -0
  95. data/vendor/kreuzberg/src/extraction/html/processor.rs +659 -0
  96. data/vendor/kreuzberg/src/extraction/html/stack_management.rs +103 -0
  97. data/vendor/kreuzberg/src/extraction/html/types.rs +28 -0
  98. data/vendor/kreuzberg/src/extraction/mod.rs +6 -2
  99. data/vendor/kreuzberg/src/extraction/pptx/container.rs +159 -0
  100. data/vendor/kreuzberg/src/extraction/pptx/content_builder.rs +168 -0
  101. data/vendor/kreuzberg/src/extraction/pptx/elements.rs +132 -0
  102. data/vendor/kreuzberg/src/extraction/pptx/image_handling.rs +57 -0
  103. data/vendor/kreuzberg/src/extraction/pptx/metadata.rs +160 -0
  104. data/vendor/kreuzberg/src/extraction/pptx/mod.rs +558 -0
  105. data/vendor/kreuzberg/src/extraction/pptx/parser.rs +388 -0
  106. data/vendor/kreuzberg/src/extraction/transform/content.rs +205 -0
  107. data/vendor/kreuzberg/src/extraction/transform/elements.rs +211 -0
  108. data/vendor/kreuzberg/src/extraction/transform/mod.rs +480 -0
  109. data/vendor/kreuzberg/src/extraction/transform/types.rs +27 -0
  110. data/vendor/kreuzberg/src/extractors/archive.rs +2 -0
  111. data/vendor/kreuzberg/src/extractors/bibtex.rs +2 -0
  112. data/vendor/kreuzberg/src/extractors/djot_format/attributes.rs +134 -0
  113. data/vendor/kreuzberg/src/extractors/djot_format/conversion.rs +223 -0
  114. data/vendor/kreuzberg/src/extractors/djot_format/extractor.rs +172 -0
  115. data/vendor/kreuzberg/src/extractors/djot_format/mod.rs +24 -0
  116. data/vendor/kreuzberg/src/extractors/djot_format/parsing/block_handlers.rs +271 -0
  117. data/vendor/kreuzberg/src/extractors/djot_format/parsing/content_extraction.rs +257 -0
  118. data/vendor/kreuzberg/src/extractors/djot_format/parsing/event_handlers.rs +101 -0
  119. data/vendor/kreuzberg/src/extractors/djot_format/parsing/inline_handlers.rs +201 -0
  120. data/vendor/kreuzberg/src/extractors/djot_format/parsing/mod.rs +16 -0
  121. data/vendor/kreuzberg/src/extractors/djot_format/parsing/state.rs +78 -0
  122. data/vendor/kreuzberg/src/extractors/djot_format/parsing/table_extraction.rs +68 -0
  123. data/vendor/kreuzberg/src/extractors/djot_format/parsing/text_extraction.rs +61 -0
  124. data/vendor/kreuzberg/src/extractors/djot_format/rendering.rs +452 -0
  125. data/vendor/kreuzberg/src/extractors/docbook.rs +2 -0
  126. data/vendor/kreuzberg/src/extractors/docx.rs +12 -1
  127. data/vendor/kreuzberg/src/extractors/email.rs +2 -0
  128. data/vendor/kreuzberg/src/extractors/epub/content.rs +333 -0
  129. data/vendor/kreuzberg/src/extractors/epub/metadata.rs +137 -0
  130. data/vendor/kreuzberg/src/extractors/epub/mod.rs +186 -0
  131. data/vendor/kreuzberg/src/extractors/epub/parsing.rs +86 -0
  132. data/vendor/kreuzberg/src/extractors/excel.rs +4 -0
  133. data/vendor/kreuzberg/src/extractors/fictionbook.rs +2 -0
  134. data/vendor/kreuzberg/src/extractors/frontmatter_utils.rs +466 -0
  135. data/vendor/kreuzberg/src/extractors/html.rs +80 -8
  136. data/vendor/kreuzberg/src/extractors/image.rs +8 -1
  137. data/vendor/kreuzberg/src/extractors/jats/elements.rs +350 -0
  138. data/vendor/kreuzberg/src/extractors/jats/metadata.rs +21 -0
  139. data/vendor/kreuzberg/src/extractors/{jats.rs → jats/mod.rs} +10 -412
  140. data/vendor/kreuzberg/src/extractors/jats/parser.rs +52 -0
  141. data/vendor/kreuzberg/src/extractors/jupyter.rs +2 -0
  142. data/vendor/kreuzberg/src/extractors/latex/commands.rs +93 -0
  143. data/vendor/kreuzberg/src/extractors/latex/environments.rs +157 -0
  144. data/vendor/kreuzberg/src/extractors/latex/metadata.rs +27 -0
  145. data/vendor/kreuzberg/src/extractors/latex/mod.rs +146 -0
  146. data/vendor/kreuzberg/src/extractors/latex/parser.rs +231 -0
  147. data/vendor/kreuzberg/src/extractors/latex/utilities.rs +126 -0
  148. data/vendor/kreuzberg/src/extractors/markdown.rs +39 -162
  149. data/vendor/kreuzberg/src/extractors/mod.rs +9 -1
  150. data/vendor/kreuzberg/src/extractors/odt.rs +2 -0
  151. data/vendor/kreuzberg/src/extractors/opml/core.rs +165 -0
  152. data/vendor/kreuzberg/src/extractors/opml/mod.rs +31 -0
  153. data/vendor/kreuzberg/src/extractors/opml/parser.rs +479 -0
  154. data/vendor/kreuzberg/src/extractors/orgmode.rs +2 -0
  155. data/vendor/kreuzberg/src/extractors/pdf/extraction.rs +106 -0
  156. data/vendor/kreuzberg/src/extractors/{pdf.rs → pdf/mod.rs} +25 -324
  157. data/vendor/kreuzberg/src/extractors/pdf/ocr.rs +214 -0
  158. data/vendor/kreuzberg/src/extractors/pdf/pages.rs +51 -0
  159. data/vendor/kreuzberg/src/extractors/pptx.rs +9 -2
  160. data/vendor/kreuzberg/src/extractors/rst.rs +2 -0
  161. data/vendor/kreuzberg/src/extractors/rtf/encoding.rs +116 -0
  162. data/vendor/kreuzberg/src/extractors/rtf/formatting.rs +24 -0
  163. data/vendor/kreuzberg/src/extractors/rtf/images.rs +72 -0
  164. data/vendor/kreuzberg/src/extractors/rtf/metadata.rs +216 -0
  165. data/vendor/kreuzberg/src/extractors/rtf/mod.rs +142 -0
  166. data/vendor/kreuzberg/src/extractors/rtf/parser.rs +259 -0
  167. data/vendor/kreuzberg/src/extractors/rtf/tables.rs +83 -0
  168. data/vendor/kreuzberg/src/extractors/structured.rs +2 -0
  169. data/vendor/kreuzberg/src/extractors/text.rs +4 -0
  170. data/vendor/kreuzberg/src/extractors/typst.rs +2 -0
  171. data/vendor/kreuzberg/src/extractors/xml.rs +2 -0
  172. data/vendor/kreuzberg/src/keywords/processor.rs +14 -0
  173. data/vendor/kreuzberg/src/language_detection/processor.rs +10 -0
  174. data/vendor/kreuzberg/src/lib.rs +2 -2
  175. data/vendor/kreuzberg/src/mcp/errors.rs +312 -0
  176. data/vendor/kreuzberg/src/mcp/format.rs +211 -0
  177. data/vendor/kreuzberg/src/mcp/mod.rs +9 -3
  178. data/vendor/kreuzberg/src/mcp/params.rs +196 -0
  179. data/vendor/kreuzberg/src/mcp/server.rs +39 -1438
  180. data/vendor/kreuzberg/src/mcp/tools/cache.rs +179 -0
  181. data/vendor/kreuzberg/src/mcp/tools/extraction.rs +403 -0
  182. data/vendor/kreuzberg/src/mcp/tools/mime.rs +150 -0
  183. data/vendor/kreuzberg/src/mcp/tools/mod.rs +11 -0
  184. data/vendor/kreuzberg/src/ocr/backends/easyocr.rs +96 -0
  185. data/vendor/kreuzberg/src/ocr/backends/mod.rs +7 -0
  186. data/vendor/kreuzberg/src/ocr/backends/paddleocr.rs +27 -0
  187. data/vendor/kreuzberg/src/ocr/backends/tesseract.rs +134 -0
  188. data/vendor/kreuzberg/src/ocr/hocr.rs +60 -16
  189. data/vendor/kreuzberg/src/ocr/language_registry.rs +11 -235
  190. data/vendor/kreuzberg/src/ocr/mod.rs +1 -0
  191. data/vendor/kreuzberg/src/ocr/processor/config.rs +203 -0
  192. data/vendor/kreuzberg/src/ocr/processor/execution.rs +494 -0
  193. data/vendor/kreuzberg/src/ocr/processor/mod.rs +265 -0
  194. data/vendor/kreuzberg/src/ocr/processor/validation.rs +145 -0
  195. data/vendor/kreuzberg/src/ocr/tesseract_backend.rs +41 -24
  196. data/vendor/kreuzberg/src/pdf/bindings.rs +21 -8
  197. data/vendor/kreuzberg/src/pdf/hierarchy/bounding_box.rs +289 -0
  198. data/vendor/kreuzberg/src/pdf/hierarchy/clustering.rs +199 -0
  199. data/vendor/kreuzberg/src/pdf/{hierarchy.rs → hierarchy/extraction.rs} +6 -346
  200. data/vendor/kreuzberg/src/pdf/hierarchy/mod.rs +18 -0
  201. data/vendor/kreuzberg/src/plugins/extractor/mod.rs +319 -0
  202. data/vendor/kreuzberg/src/plugins/extractor/registry.rs +434 -0
  203. data/vendor/kreuzberg/src/plugins/extractor/trait.rs +391 -0
  204. data/vendor/kreuzberg/src/plugins/mod.rs +13 -0
  205. data/vendor/kreuzberg/src/plugins/ocr.rs +11 -0
  206. data/vendor/kreuzberg/src/plugins/processor/mod.rs +365 -0
  207. data/vendor/kreuzberg/src/plugins/processor/registry.rs +37 -0
  208. data/vendor/kreuzberg/src/plugins/processor/trait.rs +284 -0
  209. data/vendor/kreuzberg/src/plugins/registry/extractor.rs +416 -0
  210. data/vendor/kreuzberg/src/plugins/registry/mod.rs +116 -0
  211. data/vendor/kreuzberg/src/plugins/registry/ocr.rs +293 -0
  212. data/vendor/kreuzberg/src/plugins/registry/processor.rs +304 -0
  213. data/vendor/kreuzberg/src/plugins/registry/validator.rs +238 -0
  214. data/vendor/kreuzberg/src/plugins/validator/mod.rs +424 -0
  215. data/vendor/kreuzberg/src/plugins/validator/registry.rs +355 -0
  216. data/vendor/kreuzberg/src/plugins/validator/trait.rs +276 -0
  217. data/vendor/kreuzberg/src/stopwords/languages/asian.rs +40 -0
  218. data/vendor/kreuzberg/src/stopwords/languages/germanic.rs +36 -0
  219. data/vendor/kreuzberg/src/stopwords/languages/mod.rs +10 -0
  220. data/vendor/kreuzberg/src/stopwords/languages/other.rs +44 -0
  221. data/vendor/kreuzberg/src/stopwords/languages/romance.rs +36 -0
  222. data/vendor/kreuzberg/src/stopwords/languages/slavic.rs +36 -0
  223. data/vendor/kreuzberg/src/stopwords/mod.rs +7 -33
  224. data/vendor/kreuzberg/src/text/quality.rs +1 -1
  225. data/vendor/kreuzberg/src/text/quality_processor.rs +10 -0
  226. data/vendor/kreuzberg/src/text/token_reduction/core/analysis.rs +238 -0
  227. data/vendor/kreuzberg/src/text/token_reduction/core/mod.rs +8 -0
  228. data/vendor/kreuzberg/src/text/token_reduction/core/punctuation.rs +54 -0
  229. data/vendor/kreuzberg/src/text/token_reduction/core/reducer.rs +384 -0
  230. data/vendor/kreuzberg/src/text/token_reduction/core/sentence_selection.rs +68 -0
  231. data/vendor/kreuzberg/src/text/token_reduction/core/word_filtering.rs +156 -0
  232. data/vendor/kreuzberg/src/text/token_reduction/filters/general.rs +377 -0
  233. data/vendor/kreuzberg/src/text/token_reduction/filters/html.rs +51 -0
  234. data/vendor/kreuzberg/src/text/token_reduction/filters/markdown.rs +285 -0
  235. data/vendor/kreuzberg/src/text/token_reduction/filters.rs +131 -246
  236. data/vendor/kreuzberg/src/types/djot.rs +209 -0
  237. data/vendor/kreuzberg/src/types/extraction.rs +301 -0
  238. data/vendor/kreuzberg/src/types/formats.rs +443 -0
  239. data/vendor/kreuzberg/src/types/metadata.rs +560 -0
  240. data/vendor/kreuzberg/src/types/mod.rs +281 -0
  241. data/vendor/kreuzberg/src/types/page.rs +182 -0
  242. data/vendor/kreuzberg/src/types/serde_helpers.rs +132 -0
  243. data/vendor/kreuzberg/src/types/tables.rs +39 -0
  244. data/vendor/kreuzberg/src/utils/quality/heuristics.rs +58 -0
  245. data/vendor/kreuzberg/src/utils/{quality.rs → quality/mod.rs} +168 -489
  246. data/vendor/kreuzberg/src/utils/quality/patterns.rs +117 -0
  247. data/vendor/kreuzberg/src/utils/quality/scoring.rs +178 -0
  248. data/vendor/kreuzberg/src/utils/string_pool/buffer_pool.rs +325 -0
  249. data/vendor/kreuzberg/src/utils/string_pool/interned.rs +102 -0
  250. data/vendor/kreuzberg/src/utils/string_pool/language_pool.rs +119 -0
  251. data/vendor/kreuzberg/src/utils/string_pool/mime_pool.rs +235 -0
  252. data/vendor/kreuzberg/src/utils/string_pool/mod.rs +41 -0
  253. data/vendor/kreuzberg/tests/api_chunk.rs +313 -0
  254. data/vendor/kreuzberg/tests/api_embed.rs +6 -9
  255. data/vendor/kreuzberg/tests/batch_orchestration.rs +1 -0
  256. data/vendor/kreuzberg/tests/concurrency_stress.rs +7 -0
  257. data/vendor/kreuzberg/tests/core_integration.rs +1 -0
  258. data/vendor/kreuzberg/tests/docx_metadata_extraction_test.rs +130 -0
  259. data/vendor/kreuzberg/tests/epub_native_extractor_tests.rs +5 -14
  260. data/vendor/kreuzberg/tests/format_integration.rs +2 -0
  261. data/vendor/kreuzberg/tests/helpers/mod.rs +1 -0
  262. data/vendor/kreuzberg/tests/html_table_test.rs +11 -11
  263. data/vendor/kreuzberg/tests/ocr_configuration.rs +16 -0
  264. data/vendor/kreuzberg/tests/ocr_errors.rs +18 -0
  265. data/vendor/kreuzberg/tests/ocr_quality.rs +9 -0
  266. data/vendor/kreuzberg/tests/ocr_stress.rs +1 -0
  267. data/vendor/kreuzberg/tests/pipeline_integration.rs +50 -0
  268. data/vendor/kreuzberg/tests/plugin_ocr_backend_test.rs +13 -0
  269. data/vendor/kreuzberg/tests/plugin_system.rs +12 -0
  270. data/vendor/kreuzberg/tests/pptx_regression_tests.rs +504 -0
  271. data/vendor/kreuzberg/tests/registry_integration_tests.rs +2 -0
  272. data/vendor/kreuzberg-ffi/Cargo.toml +2 -1
  273. data/vendor/kreuzberg-ffi/benches/result_view_benchmark.rs +2 -0
  274. data/vendor/kreuzberg-ffi/kreuzberg.h +347 -178
  275. data/vendor/kreuzberg-ffi/src/config/html.rs +318 -0
  276. data/vendor/kreuzberg-ffi/src/config/loader.rs +154 -0
  277. data/vendor/kreuzberg-ffi/src/config/merge.rs +104 -0
  278. data/vendor/kreuzberg-ffi/src/config/mod.rs +385 -0
  279. data/vendor/kreuzberg-ffi/src/config/parse.rs +91 -0
  280. data/vendor/kreuzberg-ffi/src/config/serialize.rs +118 -0
  281. data/vendor/kreuzberg-ffi/src/config_builder.rs +598 -0
  282. data/vendor/kreuzberg-ffi/src/error.rs +46 -14
  283. data/vendor/kreuzberg-ffi/src/helpers.rs +10 -0
  284. data/vendor/kreuzberg-ffi/src/html_options.rs +421 -0
  285. data/vendor/kreuzberg-ffi/src/lib.rs +16 -0
  286. data/vendor/kreuzberg-ffi/src/panic_shield.rs +11 -0
  287. data/vendor/kreuzberg-ffi/src/plugins/ocr_backend.rs +2 -0
  288. data/vendor/kreuzberg-ffi/src/result.rs +148 -122
  289. data/vendor/kreuzberg-ffi/src/result_view.rs +4 -0
  290. data/vendor/kreuzberg-tesseract/Cargo.toml +2 -2
  291. metadata +201 -28
  292. data/vendor/kreuzberg/src/api/server.rs +0 -518
  293. data/vendor/kreuzberg/src/core/config.rs +0 -1914
  294. data/vendor/kreuzberg/src/core/config_validation.rs +0 -949
  295. data/vendor/kreuzberg/src/core/extractor.rs +0 -1200
  296. data/vendor/kreuzberg/src/core/pipeline.rs +0 -1223
  297. data/vendor/kreuzberg/src/core/server_config.rs +0 -1220
  298. data/vendor/kreuzberg/src/extraction/html.rs +0 -1830
  299. data/vendor/kreuzberg/src/extraction/pptx.rs +0 -3102
  300. data/vendor/kreuzberg/src/extractors/epub.rs +0 -696
  301. data/vendor/kreuzberg/src/extractors/latex.rs +0 -653
  302. data/vendor/kreuzberg/src/extractors/opml.rs +0 -635
  303. data/vendor/kreuzberg/src/extractors/rtf.rs +0 -809
  304. data/vendor/kreuzberg/src/ocr/processor.rs +0 -858
  305. data/vendor/kreuzberg/src/plugins/extractor.rs +0 -1042
  306. data/vendor/kreuzberg/src/plugins/processor.rs +0 -650
  307. data/vendor/kreuzberg/src/plugins/registry.rs +0 -1339
  308. data/vendor/kreuzberg/src/plugins/validator.rs +0 -967
  309. data/vendor/kreuzberg/src/text/token_reduction/core.rs +0 -832
  310. data/vendor/kreuzberg/src/types.rs +0 -1713
  311. data/vendor/kreuzberg/src/utils/string_pool.rs +0 -762
  312. data/vendor/kreuzberg-ffi/src/config.rs +0 -1341
@@ -0,0 +1,388 @@
1
+ //! XML parsing for PPTX slide content.
2
+ //!
3
+ //! This module handles parsing slide XML, extracting text, tables, lists, images,
4
+ //! and relationships from PowerPoint presentations.
5
+
6
+ use roxmltree::{Document, Node};
7
+
8
+ use crate::error::{KreuzbergError, Result};
9
+ use crate::text::utf8_validation;
10
+
11
+ use super::elements::{
12
+ ElementPosition, Formatting, ImageReference, ListElement, ListItem, ParsedContent, Run, SlideElement, TableCell,
13
+ TableElement, TableRow, TextElement,
14
+ };
15
+
16
+ const P_NAMESPACE: &str = "http://schemas.openxmlformats.org/presentationml/2006/main";
17
+ const A_NAMESPACE: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
18
+ const RELS_NAMESPACE: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
19
+
20
+ pub(super) fn parse_slide_xml(xml_data: &[u8]) -> Result<Vec<SlideElement>> {
21
+ let xml_str = utf8_validation::from_utf8(xml_data)
22
+ .map_err(|_| KreuzbergError::parsing("Invalid UTF-8 in slide XML".to_string()))?;
23
+
24
+ let doc =
25
+ Document::parse(xml_str).map_err(|e| KreuzbergError::parsing(format!("Failed to parse slide XML: {}", e)))?;
26
+
27
+ let root = doc.root_element();
28
+ let ns = root.tag_name().namespace();
29
+
30
+ let c_sld = root
31
+ .descendants()
32
+ .find(|n| n.tag_name().name() == "cSld" && n.tag_name().namespace() == ns)
33
+ .ok_or_else(|| KreuzbergError::parsing("No <p:cSld> tag found".to_string()))?;
34
+
35
+ let sp_tree = c_sld
36
+ .children()
37
+ .find(|n| n.tag_name().name() == "spTree" && n.tag_name().namespace() == ns)
38
+ .ok_or_else(|| KreuzbergError::parsing("No <p:spTree> tag found".to_string()))?;
39
+
40
+ let mut elements = Vec::new();
41
+ for child_node in sp_tree.children().filter(|n| n.is_element()) {
42
+ elements.extend(parse_group(&child_node)?);
43
+ }
44
+
45
+ Ok(elements)
46
+ }
47
+
48
+ fn parse_group(node: &Node) -> Result<Vec<SlideElement>> {
49
+ let mut elements = Vec::new();
50
+
51
+ let tag_name = node.tag_name().name();
52
+ let namespace = node.tag_name().namespace().unwrap_or("");
53
+
54
+ if namespace != P_NAMESPACE {
55
+ return Ok(elements);
56
+ }
57
+
58
+ let position = extract_position(node);
59
+
60
+ match tag_name {
61
+ "sp" => {
62
+ let position = extract_position(node);
63
+ // parse_sp returns None for shapes without txBody (e.g., image placeholders)
64
+ if let Some(content) = parse_sp(node)? {
65
+ match content {
66
+ ParsedContent::Text(text) => elements.push(SlideElement::Text(text, position)),
67
+ ParsedContent::List(list) => elements.push(SlideElement::List(list, position)),
68
+ }
69
+ }
70
+ }
71
+ "graphicFrame" => {
72
+ if let Some(graphic_element) = parse_graphic_frame(node)? {
73
+ elements.push(SlideElement::Table(graphic_element, position));
74
+ }
75
+ }
76
+ "pic" => {
77
+ let image_reference = parse_pic(node)?;
78
+ elements.push(SlideElement::Image(image_reference, position));
79
+ }
80
+ "grpSp" => {
81
+ for child in node.children().filter(|n| n.is_element()) {
82
+ elements.extend(parse_group(&child)?);
83
+ }
84
+ }
85
+ _ => elements.push(SlideElement::Unknown),
86
+ }
87
+
88
+ Ok(elements)
89
+ }
90
+
91
+ fn parse_sp(sp_node: &Node) -> Result<Option<ParsedContent>> {
92
+ // Some shapes like image placeholders (<p:ph type="pic"/>) don't have txBody.
93
+ // These should be skipped gracefully - they contain no text to extract.
94
+ // GitHub Issue #321 Bug 1
95
+ let tx_body_node = match sp_node
96
+ .children()
97
+ .find(|n| n.tag_name().name() == "txBody" && n.tag_name().namespace() == Some(P_NAMESPACE))
98
+ {
99
+ Some(node) => node,
100
+ None => return Ok(None), // Skip shapes without txBody
101
+ };
102
+
103
+ let is_list = tx_body_node.descendants().any(|n| {
104
+ n.is_element()
105
+ && n.tag_name().name() == "pPr"
106
+ && n.tag_name().namespace() == Some(A_NAMESPACE)
107
+ && (n.attribute("lvl").is_some()
108
+ || n.children().any(|child| {
109
+ child.is_element()
110
+ && (child.tag_name().name() == "buAutoNum" || child.tag_name().name() == "buChar")
111
+ }))
112
+ });
113
+
114
+ if is_list {
115
+ Ok(Some(ParsedContent::List(parse_list(&tx_body_node)?)))
116
+ } else {
117
+ Ok(Some(ParsedContent::Text(parse_text(&tx_body_node)?)))
118
+ }
119
+ }
120
+
121
+ pub(super) fn parse_text(tx_body_node: &Node) -> Result<TextElement> {
122
+ let mut runs = Vec::new();
123
+
124
+ for p_node in tx_body_node
125
+ .children()
126
+ .filter(|n| n.is_element() && n.tag_name().name() == "p" && n.tag_name().namespace() == Some(A_NAMESPACE))
127
+ {
128
+ let mut paragraph_runs = parse_paragraph(&p_node, true)?;
129
+ runs.append(&mut paragraph_runs);
130
+ }
131
+
132
+ Ok(TextElement { runs })
133
+ }
134
+
135
+ fn parse_graphic_frame(node: &Node) -> Result<Option<TableElement>> {
136
+ let graphic_data_node = node.descendants().find(|n| {
137
+ n.is_element()
138
+ && n.tag_name().name() == "graphicData"
139
+ && n.tag_name().namespace() == Some(A_NAMESPACE)
140
+ && n.attribute("uri") == Some("http://schemas.openxmlformats.org/drawingml/2006/table")
141
+ });
142
+
143
+ if let Some(graphic_data) = graphic_data_node
144
+ && let Some(tbl_node) = graphic_data
145
+ .children()
146
+ .find(|n| n.is_element() && n.tag_name().name() == "tbl" && n.tag_name().namespace() == Some(A_NAMESPACE))
147
+ {
148
+ let table = parse_table(&tbl_node)?;
149
+ return Ok(Some(table));
150
+ }
151
+
152
+ Ok(None)
153
+ }
154
+
155
+ fn parse_table(tbl_node: &Node) -> Result<TableElement> {
156
+ let mut rows = Vec::new();
157
+
158
+ for tr_node in tbl_node
159
+ .children()
160
+ .filter(|n| n.is_element() && n.tag_name().name() == "tr" && n.tag_name().namespace() == Some(A_NAMESPACE))
161
+ {
162
+ let row = parse_table_row(&tr_node)?;
163
+ rows.push(row);
164
+ }
165
+
166
+ Ok(TableElement { rows })
167
+ }
168
+
169
+ fn parse_table_row(tr_node: &Node) -> Result<TableRow> {
170
+ let mut cells = Vec::new();
171
+
172
+ for tc_node in tr_node
173
+ .children()
174
+ .filter(|n| n.is_element() && n.tag_name().name() == "tc" && n.tag_name().namespace() == Some(A_NAMESPACE))
175
+ {
176
+ let cell = parse_table_cell(&tc_node)?;
177
+ cells.push(cell);
178
+ }
179
+
180
+ Ok(TableRow { cells })
181
+ }
182
+
183
+ fn parse_table_cell(tc_node: &Node) -> Result<TableCell> {
184
+ let mut runs = Vec::new();
185
+
186
+ if let Some(tx_body_node) = tc_node
187
+ .children()
188
+ .find(|n| n.is_element() && n.tag_name().name() == "txBody" && n.tag_name().namespace() == Some(A_NAMESPACE))
189
+ {
190
+ for p_node in tx_body_node
191
+ .children()
192
+ .filter(|n| n.is_element() && n.tag_name().name() == "p" && n.tag_name().namespace() == Some(A_NAMESPACE))
193
+ {
194
+ let mut paragraph_runs = parse_paragraph(&p_node, false)?;
195
+ runs.append(&mut paragraph_runs);
196
+ }
197
+ }
198
+
199
+ Ok(TableCell { runs })
200
+ }
201
+
202
+ fn parse_pic(pic_node: &Node) -> Result<ImageReference> {
203
+ let blip_node = pic_node
204
+ .descendants()
205
+ .find(|n| n.is_element() && n.tag_name().name() == "blip" && n.tag_name().namespace() == Some(A_NAMESPACE))
206
+ .ok_or_else(|| KreuzbergError::parsing("Image blip not found".to_string()))?;
207
+
208
+ let embed_attr = blip_node
209
+ .attribute((RELS_NAMESPACE, "embed"))
210
+ .or_else(|| blip_node.attribute("r:embed"))
211
+ .ok_or_else(|| KreuzbergError::parsing("Image embed attribute not found".to_string()))?;
212
+
213
+ let image_ref = ImageReference {
214
+ id: embed_attr.to_string(),
215
+ target: String::new(),
216
+ };
217
+
218
+ Ok(image_ref)
219
+ }
220
+
221
+ fn parse_list(tx_body_node: &Node) -> Result<ListElement> {
222
+ let mut items = Vec::new();
223
+
224
+ for p_node in tx_body_node
225
+ .children()
226
+ .filter(|n| n.is_element() && n.tag_name().name() == "p" && n.tag_name().namespace() == Some(A_NAMESPACE))
227
+ {
228
+ let (level, is_ordered) = parse_list_properties(&p_node)?;
229
+
230
+ let runs = parse_paragraph(&p_node, true)?;
231
+
232
+ items.push(ListItem {
233
+ level,
234
+ is_ordered,
235
+ runs,
236
+ });
237
+ }
238
+
239
+ Ok(ListElement { items })
240
+ }
241
+
242
+ fn parse_list_properties(p_node: &Node) -> Result<(u32, bool)> {
243
+ let mut level = 1;
244
+ let mut is_ordered = false;
245
+
246
+ if let Some(p_pr_node) = p_node
247
+ .children()
248
+ .find(|n| n.is_element() && n.tag_name().name() == "pPr" && n.tag_name().namespace() == Some(A_NAMESPACE))
249
+ {
250
+ if let Some(lvl_attr) = p_pr_node.attribute("lvl") {
251
+ level = lvl_attr.parse::<u32>().unwrap_or(0) + 1;
252
+ }
253
+
254
+ is_ordered = p_pr_node.children().any(|n| {
255
+ n.is_element() && n.tag_name().namespace() == Some(A_NAMESPACE) && n.tag_name().name() == "buAutoNum"
256
+ });
257
+ }
258
+
259
+ Ok((level, is_ordered))
260
+ }
261
+
262
+ fn parse_paragraph(p_node: &Node, add_new_line: bool) -> Result<Vec<Run>> {
263
+ let run_nodes: Vec<_> = p_node
264
+ .children()
265
+ .filter(|n| n.is_element() && n.tag_name().name() == "r" && n.tag_name().namespace() == Some(A_NAMESPACE))
266
+ .collect();
267
+
268
+ let count = run_nodes.len();
269
+ let mut runs: Vec<Run> = Vec::new();
270
+
271
+ for (idx, r_node) in run_nodes.iter().enumerate() {
272
+ let mut run = parse_run(r_node)?;
273
+
274
+ if add_new_line && idx == count - 1 {
275
+ run.text.push('\n');
276
+ }
277
+
278
+ runs.push(run);
279
+ }
280
+ Ok(runs)
281
+ }
282
+
283
+ fn parse_run(r_node: &Node) -> Result<Run> {
284
+ let mut text = String::new();
285
+ let mut formatting = Formatting::default();
286
+
287
+ if let Some(r_pr_node) = r_node
288
+ .children()
289
+ .find(|n| n.is_element() && n.tag_name().name() == "rPr" && n.tag_name().namespace() == Some(A_NAMESPACE))
290
+ {
291
+ if let Some(b_attr) = r_pr_node.attribute("b") {
292
+ formatting.bold = b_attr == "1" || b_attr.eq_ignore_ascii_case("true");
293
+ }
294
+ if let Some(i_attr) = r_pr_node.attribute("i") {
295
+ formatting.italic = i_attr == "1" || i_attr.eq_ignore_ascii_case("true");
296
+ }
297
+ if let Some(u_attr) = r_pr_node.attribute("u") {
298
+ formatting.underlined = u_attr != "none";
299
+ }
300
+ if let Some(lang_attr) = r_pr_node.attribute("lang") {
301
+ formatting.lang = lang_attr.to_string();
302
+ }
303
+ }
304
+
305
+ if let Some(t_node) = r_node
306
+ .children()
307
+ .find(|n| n.is_element() && n.tag_name().name() == "t" && n.tag_name().namespace() == Some(A_NAMESPACE))
308
+ && let Some(t) = t_node.text()
309
+ {
310
+ text.push_str(t);
311
+ }
312
+ Ok(Run { text, formatting })
313
+ }
314
+
315
+ pub(super) fn extract_position(node: &Node) -> ElementPosition {
316
+ let default = ElementPosition::default();
317
+
318
+ node.descendants()
319
+ .find(|n| n.tag_name().namespace() == Some(A_NAMESPACE) && n.tag_name().name() == "xfrm")
320
+ .and_then(|xfrm| {
321
+ let x = xfrm
322
+ .children()
323
+ .find(|n| n.tag_name().name() == "off" && n.tag_name().namespace() == Some(A_NAMESPACE))
324
+ .and_then(|off| off.attribute("x")?.parse::<i64>().ok())?;
325
+
326
+ let y = xfrm
327
+ .children()
328
+ .find(|n| n.tag_name().name() == "off" && n.tag_name().namespace() == Some(A_NAMESPACE))
329
+ .and_then(|off| off.attribute("y")?.parse::<i64>().ok())?;
330
+
331
+ Some(ElementPosition { x, y })
332
+ })
333
+ .unwrap_or(default)
334
+ }
335
+
336
+ pub(super) fn parse_slide_rels(rels_data: &[u8]) -> Result<Vec<ImageReference>> {
337
+ let xml_str = utf8_validation::from_utf8(rels_data)
338
+ .map_err(|e| KreuzbergError::parsing(format!("Invalid UTF-8 in rels XML: {}", e)))?;
339
+
340
+ let doc =
341
+ Document::parse(xml_str).map_err(|e| KreuzbergError::parsing(format!("Failed to parse rels XML: {}", e)))?;
342
+
343
+ let mut images = Vec::new();
344
+
345
+ for node in doc.descendants() {
346
+ if node.has_tag_name("Relationship")
347
+ && let Some(rel_type) = node.attribute("Type")
348
+ && rel_type.contains("image")
349
+ && let (Some(id), Some(target)) = (node.attribute("Id"), node.attribute("Target"))
350
+ {
351
+ images.push(ImageReference {
352
+ id: id.to_string(),
353
+ target: target.to_string(),
354
+ });
355
+ }
356
+ }
357
+
358
+ Ok(images)
359
+ }
360
+
361
+ pub(super) fn parse_presentation_rels(rels_data: &[u8]) -> Result<Vec<String>> {
362
+ let xml_str = utf8_validation::from_utf8(rels_data)
363
+ .map_err(|e| KreuzbergError::parsing(format!("Invalid UTF-8 in presentation rels: {}", e)))?;
364
+
365
+ let doc = Document::parse(xml_str)
366
+ .map_err(|e| KreuzbergError::parsing(format!("Failed to parse presentation rels: {}", e)))?;
367
+
368
+ let mut slide_paths = Vec::new();
369
+
370
+ for node in doc.descendants() {
371
+ if node.has_tag_name("Relationship")
372
+ && let Some(rel_type) = node.attribute("Type")
373
+ && rel_type.contains("slide")
374
+ && !rel_type.contains("slideMaster")
375
+ && let Some(target) = node.attribute("Target")
376
+ {
377
+ let normalized_target = target.strip_prefix('/').unwrap_or(target);
378
+ let final_path = if normalized_target.starts_with("ppt/") {
379
+ normalized_target.to_string()
380
+ } else {
381
+ format!("ppt/{}", normalized_target)
382
+ };
383
+ slide_paths.push(final_path);
384
+ }
385
+ }
386
+
387
+ Ok(slide_paths)
388
+ }
@@ -0,0 +1,205 @@
1
+ //! Content processing utilities for transformation.
2
+ //!
3
+ //! This module handles processing of page content, tables, and images
4
+ //! during the transformation to semantic elements.
5
+
6
+ use crate::types::{BoundingBox, Element, ElementMetadata, ElementType};
7
+ use std::collections::HashMap;
8
+
9
+ use super::elements::{add_paragraphs, detect_list_items, generate_element_id};
10
+
11
+ /// Process page content to extract paragraphs and list items.
12
+ pub(super) fn process_content(elements: &mut Vec<Element>, content: &str, page_number: usize, title: &Option<String>) {
13
+ let list_items = detect_list_items(content);
14
+ let mut current_byte_offset = 0;
15
+
16
+ for list_item in list_items {
17
+ // Add narrative text/paragraphs before this list item
18
+ if current_byte_offset < list_item.byte_start {
19
+ let text_slice = content[current_byte_offset..list_item.byte_start].trim();
20
+ add_paragraphs(elements, text_slice, page_number, title);
21
+ }
22
+
23
+ // Add the list item itself
24
+ let item_text = content[list_item.byte_start..list_item.byte_end].trim();
25
+ if !item_text.is_empty() {
26
+ let element_id = generate_element_id(item_text, ElementType::ListItem, Some(page_number));
27
+ elements.push(Element {
28
+ element_id,
29
+ element_type: ElementType::ListItem,
30
+ text: item_text.to_string(),
31
+ metadata: ElementMetadata {
32
+ page_number: Some(page_number),
33
+ filename: title.clone(),
34
+ coordinates: None,
35
+ element_index: Some(elements.len()),
36
+ additional: {
37
+ let mut m = HashMap::new();
38
+ m.insert("indent_level".to_string(), list_item.indent_level.to_string());
39
+ m.insert("list_type".to_string(), format!("{:?}", list_item.list_type));
40
+ m
41
+ },
42
+ },
43
+ });
44
+ }
45
+
46
+ current_byte_offset = list_item.byte_end;
47
+ }
48
+
49
+ // Add any remaining narrative text/paragraphs
50
+ if current_byte_offset < content.len() {
51
+ let text_slice = content[current_byte_offset..].trim();
52
+ add_paragraphs(elements, text_slice, page_number, title);
53
+ }
54
+ }
55
+
56
+ /// Format a table as plain text for element representation.
57
+ pub(super) fn format_table_as_text(table: &crate::types::Table) -> String {
58
+ let mut output = String::new();
59
+
60
+ // Simple text representation: rows separated by newlines, cells by tabs
61
+ for row in &table.cells {
62
+ for (i, cell) in row.iter().enumerate() {
63
+ if i > 0 {
64
+ output.push('\t');
65
+ }
66
+ output.push_str(cell);
67
+ }
68
+ output.push('\n');
69
+ }
70
+
71
+ output.trim().to_string()
72
+ }
73
+
74
+ /// Process hierarchy blocks (PDF headings) into Title elements.
75
+ pub(super) fn process_hierarchy(
76
+ elements: &mut Vec<Element>,
77
+ hierarchy: &crate::types::PageHierarchy,
78
+ page_number: usize,
79
+ title: &Option<String>,
80
+ ) {
81
+ for block in &hierarchy.blocks {
82
+ let element_type = match block.level.as_str() {
83
+ "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => ElementType::Title,
84
+ _ => continue, // Body text will be processed separately
85
+ };
86
+
87
+ let coords = block.bbox.as_ref().map(|(left, top, right, bottom)| BoundingBox {
88
+ x0: *left as f64,
89
+ y0: *top as f64,
90
+ x1: *right as f64,
91
+ y1: *bottom as f64,
92
+ });
93
+
94
+ let element_id = generate_element_id(&block.text, element_type, Some(page_number));
95
+ elements.push(Element {
96
+ element_id,
97
+ element_type,
98
+ text: block.text.clone(),
99
+ metadata: ElementMetadata {
100
+ page_number: Some(page_number),
101
+ filename: title.clone(),
102
+ coordinates: coords,
103
+ element_index: Some(elements.len()),
104
+ additional: {
105
+ let mut m = HashMap::new();
106
+ m.insert("level".to_string(), block.level.clone());
107
+ m.insert("font_size".to_string(), block.font_size.to_string());
108
+ m
109
+ },
110
+ },
111
+ });
112
+ }
113
+ }
114
+
115
+ /// Process tables on a page into Table elements.
116
+ pub(super) fn process_tables(
117
+ elements: &mut Vec<Element>,
118
+ tables: &[std::sync::Arc<crate::types::Table>],
119
+ page_number: usize,
120
+ title: &Option<String>,
121
+ ) {
122
+ for table_arc in tables {
123
+ let table = table_arc.as_ref();
124
+ let table_text = format_table_as_text(table);
125
+
126
+ let element_id = generate_element_id(&table_text, ElementType::Table, Some(page_number));
127
+ elements.push(Element {
128
+ element_id,
129
+ element_type: ElementType::Table,
130
+ text: table_text,
131
+ metadata: ElementMetadata {
132
+ page_number: Some(page_number),
133
+ filename: title.clone(),
134
+ coordinates: None, // Tables don't have bbox in current structure
135
+ element_index: Some(elements.len()),
136
+ additional: HashMap::new(),
137
+ },
138
+ });
139
+ }
140
+ }
141
+
142
+ /// Process images on a page into Image elements.
143
+ pub(super) fn process_images(
144
+ elements: &mut Vec<Element>,
145
+ images: &[std::sync::Arc<crate::types::ExtractedImage>],
146
+ page_number: usize,
147
+ title: &Option<String>,
148
+ ) {
149
+ for image_arc in images {
150
+ let image = image_arc.as_ref();
151
+ let image_text = format!(
152
+ "Image: {} ({}x{})",
153
+ image.format,
154
+ image.width.unwrap_or(0),
155
+ image.height.unwrap_or(0)
156
+ );
157
+
158
+ let element_id = generate_element_id(&image_text, ElementType::Image, Some(page_number));
159
+ elements.push(Element {
160
+ element_id,
161
+ element_type: ElementType::Image,
162
+ text: image_text,
163
+ metadata: ElementMetadata {
164
+ page_number: Some(page_number),
165
+ filename: title.clone(),
166
+ coordinates: None, // Images don't have bbox in current structure
167
+ element_index: Some(elements.len()),
168
+ additional: {
169
+ let mut m = HashMap::new();
170
+ m.insert("format".to_string(), image.format.clone());
171
+ if let Some(width) = image.width {
172
+ m.insert("width".to_string(), width.to_string());
173
+ }
174
+ if let Some(height) = image.height {
175
+ m.insert("height".to_string(), height.to_string());
176
+ }
177
+ m
178
+ },
179
+ },
180
+ });
181
+ }
182
+ }
183
+
184
+ /// Add a PageBreak element between pages.
185
+ pub(super) fn add_page_break(
186
+ elements: &mut Vec<Element>,
187
+ current_page: usize,
188
+ next_page: usize,
189
+ title: &Option<String>,
190
+ ) {
191
+ let page_break_text = format!("--- PAGE BREAK (page {} → {}) ---", current_page, next_page);
192
+ let element_id = generate_element_id(&page_break_text, ElementType::PageBreak, Some(current_page));
193
+ elements.push(Element {
194
+ element_id,
195
+ element_type: ElementType::PageBreak,
196
+ text: page_break_text,
197
+ metadata: ElementMetadata {
198
+ page_number: Some(current_page),
199
+ filename: title.clone(),
200
+ coordinates: None,
201
+ element_index: Some(elements.len()),
202
+ additional: HashMap::new(),
203
+ },
204
+ });
205
+ }