kreuzberg 4.10.3 → 4.10.4
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.
- checksums.yaml +4 -4
- data/ext/kreuzberg_rb/native/Cargo.toml +2 -2
- data/lib/kreuzberg/version.rb +1 -1
- data/vendor/Cargo.toml +11 -11
- data/vendor/kreuzberg/Cargo.toml +13 -13
- data/vendor/kreuzberg/README.md +1 -1
- data/vendor/kreuzberg/src/extractors/csv.rs +61 -9
- data/vendor/kreuzberg/src/extractors/epub/mod.rs +55 -91
- data/vendor/kreuzberg/src/mcp/server.rs +1 -1
- data/vendor/kreuzberg/tests/docx_formatting_test.rs +53 -33
- data/vendor/kreuzberg/tests/epub_markdown_headings_tests.rs +14 -12
- data/vendor/kreuzberg-ffi/Cargo.toml +3 -3
- data/vendor/kreuzberg-ffi/kreuzberg.h +2 -2
- data/vendor/kreuzberg-paddle-ocr/Cargo.toml +1 -1
- data/vendor/kreuzberg-pdfium-render/Cargo.toml +2 -2
- data/vendor/kreuzberg-tesseract/Cargo.toml +4 -4
- data/vendor/kreuzberg-tesseract/src/api.rs +11 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dfacdc270872e87de7d3be21475808d50ec8f5ca34655eef45c740007de1b222
|
|
4
|
+
data.tar.gz: bb632e33fc5dc77dbe3740eac10075541974c95999bd6af65dff138c0acb5dbf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 34206c5ddf11c69313124109d5cd81ae67f6ed8b3250636359f556a94fd79cc3fd37def7754e4dd46656aa7e286c0cb1bc88c0c2f2a36d841eb941da23cfb8b4
|
|
7
|
+
data.tar.gz: 2a1ff1c6d8a20045c329feae6d64e0a98f23dd11e1bf6887f1ef788031a2b191c08533c21e4ff149a7474b2257d6325dad28fe04e2fef594ee69dd9bd2e078d8
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "kreuzberg-rb"
|
|
3
|
-
version = "4.10.
|
|
3
|
+
version = "4.10.4"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.91"
|
|
6
6
|
authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
|
|
@@ -65,7 +65,7 @@ tokio = { version = "1.53.1", features = [
|
|
|
65
65
|
"time",
|
|
66
66
|
"io-util",
|
|
67
67
|
] }
|
|
68
|
-
html-to-markdown-rs = { version = "3.
|
|
68
|
+
html-to-markdown-rs = { version = "3.14.3", default-features = false }
|
|
69
69
|
|
|
70
70
|
[dev-dependencies]
|
|
71
71
|
pretty_assertions = "1.4"
|
data/lib/kreuzberg/version.rb
CHANGED
data/vendor/Cargo.toml
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
members = ["kreuzberg", "kreuzberg-ffi", "kreuzberg-tesseract", "kreuzberg-paddle-ocr", "kreuzberg-pdfium-render"]
|
|
3
3
|
|
|
4
4
|
[workspace.package]
|
|
5
|
-
version = "4.10.
|
|
5
|
+
version = "4.10.4"
|
|
6
6
|
edition = "2024"
|
|
7
7
|
rust-version = "1.91"
|
|
8
8
|
authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
|
|
@@ -17,10 +17,10 @@ async-trait = "0.1.92"
|
|
|
17
17
|
base64 = "0.23.1"
|
|
18
18
|
blake3 = "1"
|
|
19
19
|
bytes = { version = "1", features = ["serde"] }
|
|
20
|
-
cfb = "0.
|
|
20
|
+
cfb = "0.15"
|
|
21
21
|
chrono = "0.4"
|
|
22
22
|
clap = { version = "4.6", features = ["derive", "color", "suggestions"] }
|
|
23
|
-
comrak = { version = "0.
|
|
23
|
+
comrak = { version = "0.55", default-features = false }
|
|
24
24
|
console_error_panic_hook = "0.1"
|
|
25
25
|
criterion = { version = "0.8", features = ["html_reports"] }
|
|
26
26
|
ctor = "1.0"
|
|
@@ -28,17 +28,17 @@ dbase = "0.8"
|
|
|
28
28
|
futures = "0.3"
|
|
29
29
|
getrandom = { version = "0.4.3", features = ["wasm_js"] }
|
|
30
30
|
hex = "0.4.3"
|
|
31
|
-
html-to-markdown-rs = { version = "3.
|
|
31
|
+
html-to-markdown-rs = { version = "3.14.3", default-features = false }
|
|
32
32
|
image = { version = "0.25.10", default-features = false }
|
|
33
33
|
itertools = "0.15"
|
|
34
34
|
js-sys = "0.3"
|
|
35
|
-
kreuzberg = { path = "./crates/kreuzberg", version = "4.10.
|
|
36
|
-
kreuzberg-ffi = { path = "./crates/kreuzberg-ffi", version = "4.10.
|
|
35
|
+
kreuzberg = { path = "./crates/kreuzberg", version = "4.10.4", default-features = false }
|
|
36
|
+
kreuzberg-ffi = { path = "./crates/kreuzberg-ffi", version = "4.10.4" }
|
|
37
37
|
lazy_static = "1.5.0"
|
|
38
38
|
libc = "0.2.189"
|
|
39
|
-
liter-llm = { version = "
|
|
39
|
+
liter-llm = { version = "2.0.3", features = ["native-http"], default-features = false }
|
|
40
40
|
log = "0.4"
|
|
41
|
-
lzma-rust2 = { version = "0.
|
|
41
|
+
lzma-rust2 = { version = "0.21" }
|
|
42
42
|
memmap2 = "0.9.11"
|
|
43
43
|
minijinja = "2"
|
|
44
44
|
num_cpus = "1.17.0"
|
|
@@ -47,16 +47,16 @@ ort = { version = "=2.0.0-rc.12", features = ["std", "api-18"], default-features
|
|
|
47
47
|
parking_lot = "0.12.5"
|
|
48
48
|
pdfium-render = { package = "kreuzberg-pdfium-render", path = "crates/kreuzberg-pdfium-render", version = "4.3" }
|
|
49
49
|
rayon = "1.12.0"
|
|
50
|
-
reqwest = { version = "0.13.
|
|
50
|
+
reqwest = { version = "0.13.5", default-features = false }
|
|
51
51
|
serde = { version = "1.0.229", features = ["derive"] }
|
|
52
52
|
serde_json = { version = "1.0.151" }
|
|
53
53
|
serde_toon_format = "0.1"
|
|
54
54
|
tempfile = "3.27.0"
|
|
55
55
|
thiserror = "2.0.20"
|
|
56
56
|
tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros", "sync", "process", "fs", "time", "io-util"] }
|
|
57
|
-
toml = "1.1.
|
|
57
|
+
toml = "1.1.6"
|
|
58
58
|
tracing = "0.1"
|
|
59
|
-
tree-sitter-language-pack = { version = "1.
|
|
59
|
+
tree-sitter-language-pack = { version = "1.20.0", features = ["serde"], default-features = false }
|
|
60
60
|
wasm-bindgen = { version = "0.2", features = ["enable-interning"] }
|
|
61
61
|
wasm-bindgen-futures = "0.4"
|
|
62
62
|
web-sys = { version = "0.3", features = ["Blob", "File", "FileReader", "console", "TextDecoder", "ImageData", "Window", "Response"] }
|
data/vendor/kreuzberg/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "kreuzberg"
|
|
3
|
-
version = "4.10.
|
|
3
|
+
version = "4.10.4"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.91"
|
|
6
6
|
authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
|
|
@@ -200,20 +200,20 @@ bitvec = "1.1"
|
|
|
200
200
|
blake3 = "1"
|
|
201
201
|
bytes = { version = "1", features = ["serde"] }
|
|
202
202
|
calamine = { version = "0.36.1", features = ["dates"], optional = true }
|
|
203
|
-
cfb = { version = "0.
|
|
203
|
+
cfb = { version = "0.15", optional = true }
|
|
204
204
|
chardetng = { version = "1.0.0", optional = true }
|
|
205
205
|
chrono = { version = "0.4", optional = true }
|
|
206
|
-
comrak = { version = "0.
|
|
206
|
+
comrak = { version = "0.55", default-features = false }
|
|
207
207
|
dashmap = "6.2"
|
|
208
208
|
dbase = { version = "0.8", optional = true }
|
|
209
|
-
dirs = "
|
|
210
|
-
encoding_rs = { version = "0.8.
|
|
209
|
+
dirs = "7"
|
|
210
|
+
encoding_rs = { version = "0.8.41" }
|
|
211
211
|
fast_image_resize = { version = "6.1.0", optional = true }
|
|
212
212
|
flate2 = { version = "1.1", optional = true }
|
|
213
213
|
hayro-jbig2 = { version = "0.3", default-features = false, features = ["std"], optional = true }
|
|
214
214
|
hayro-jpeg2000 = { version = "0.3", default-features = false, features = ["std", "simd"], optional = true }
|
|
215
215
|
hex = "0.4.3"
|
|
216
|
-
html-to-markdown-rs = { version = "3.
|
|
216
|
+
html-to-markdown-rs = { version = "3.14.3", default-features = false, features = ["inline-images", "metadata"], optional = true }
|
|
217
217
|
image = { version = "0.25.10", default-features = false, features = [
|
|
218
218
|
"png",
|
|
219
219
|
"jpeg",
|
|
@@ -231,10 +231,10 @@ kamadak-exif = { version = "0.6.1", optional = true }
|
|
|
231
231
|
|
|
232
232
|
kreuzberg-tesseract = { path = "../kreuzberg-tesseract", optional = true }
|
|
233
233
|
libc = "0.2.189"
|
|
234
|
-
liter-llm = { version = "
|
|
234
|
+
liter-llm = { version = "2.0.3", features = ["native-http"], default-features = false, optional = true }
|
|
235
235
|
log = "0.4"
|
|
236
|
-
lopdf = { version = "0.
|
|
237
|
-
mail-parser = { version = "0.11.
|
|
236
|
+
lopdf = { version = "0.45.0", default-features = false, features = ["chrono", "rayon"], optional = true }
|
|
237
|
+
mail-parser = { version = "0.11.9", optional = true }
|
|
238
238
|
memchr = "2.8.3"
|
|
239
239
|
memmap2 = "0.9.11"
|
|
240
240
|
mime_guess = "2.0"
|
|
@@ -255,7 +255,7 @@ quick-xml = { version = "=0.41.0", features = ["serialize"], optional = true }
|
|
|
255
255
|
rake = { version = "0.3.6", optional = true }
|
|
256
256
|
rayon = "1.12.0"
|
|
257
257
|
regex = "1.13.1"
|
|
258
|
-
rmcp = { version = "3.
|
|
258
|
+
rmcp = { version = "3.4.0", features = [
|
|
259
259
|
"server",
|
|
260
260
|
"macros",
|
|
261
261
|
"base64",
|
|
@@ -283,7 +283,7 @@ tiff = { version = "0.11", optional = true }
|
|
|
283
283
|
# is implemented for the same Tokenizer type used by Kreuzberg.
|
|
284
284
|
tokenizers = { version = "0.23.2", optional = true, default-features = false, features = ["http", "fancy-regex"] }
|
|
285
285
|
tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros", "sync", "process", "fs", "time", "io-util"], optional = true }
|
|
286
|
-
toml = "1.1.
|
|
286
|
+
toml = "1.1.6"
|
|
287
287
|
tower = { version = "0.5", features = ["timeout", "limit", "util"], optional = true }
|
|
288
288
|
tower-http = { version = "0.7", features = [
|
|
289
289
|
"cors",
|
|
@@ -327,7 +327,7 @@ optional = true
|
|
|
327
327
|
# Override getrandom to enable js feature for WASM targets
|
|
328
328
|
# This is needed because ring/rustls (via ureq) depend on getrandom without js feature
|
|
329
329
|
getrandom = { version = "0.4.3", features = ["wasm_js"] }
|
|
330
|
-
tree-sitter-language-pack = { version = "1.
|
|
330
|
+
tree-sitter-language-pack = { version = "1.20.0", features = ["serde"], default-features = false, optional = true }
|
|
331
331
|
wasm-bindgen-rayon = { version = "1.3", optional = true }
|
|
332
332
|
|
|
333
333
|
[build-dependencies]
|
|
@@ -340,7 +340,7 @@ criterion = { version = "0.8", features = ["html_reports"] }
|
|
|
340
340
|
dotenvy = "0.15"
|
|
341
341
|
filetime = "0.2"
|
|
342
342
|
image = { version = "0.25.10", default-features = false, features = ["png"] }
|
|
343
|
-
jsonschema = "0.
|
|
343
|
+
jsonschema = "0.56"
|
|
344
344
|
serial_test = "3.5.0"
|
|
345
345
|
tar = "0.4.46"
|
|
346
346
|
tempfile = "3.27.0"
|
data/vendor/kreuzberg/README.md
CHANGED
|
@@ -60,7 +60,7 @@ High-performance document intelligence library for Rust. Extract text, metadata,
|
|
|
60
60
|
|
|
61
61
|
This is the core Rust library that powers the Python, TypeScript, and Ruby bindings.
|
|
62
62
|
|
|
63
|
-
> **🚀 Version 4.10.
|
|
63
|
+
> **🚀 Version 4.10.4 Release**
|
|
64
64
|
> This is a pre-release version. We invite you to test the library and [report any issues](https://github.com/kreuzberg-dev/kreuzberg-lts/issues) you encounter.
|
|
65
65
|
>
|
|
66
66
|
> **Note**: The Rust crate is not currently published to crates.io for this RC. Use git dependencies or language bindings (Python, TypeScript, Ruby) instead.
|
|
@@ -7,7 +7,7 @@ use std::borrow::Cow;
|
|
|
7
7
|
use std::sync::LazyLock;
|
|
8
8
|
|
|
9
9
|
use crate::Result;
|
|
10
|
-
use crate::core::config::ExtractionConfig;
|
|
10
|
+
use crate::core::config::{ExtractionConfig, OutputFormat};
|
|
11
11
|
use crate::plugins::{DocumentExtractor, Plugin};
|
|
12
12
|
use crate::text::utf8_validation;
|
|
13
13
|
use crate::types::Table;
|
|
@@ -71,7 +71,7 @@ impl DocumentExtractor for CsvExtractor {
|
|
|
71
71
|
&self,
|
|
72
72
|
content: &[u8],
|
|
73
73
|
mime_type: &str,
|
|
74
|
-
|
|
74
|
+
config: &ExtractionConfig,
|
|
75
75
|
) -> Result<InternalDocument> {
|
|
76
76
|
tracing::debug!(format = "csv", size_bytes = content.len(), "extraction starting");
|
|
77
77
|
let text = decode_csv_bytes(content);
|
|
@@ -114,14 +114,21 @@ impl DocumentExtractor for CsvExtractor {
|
|
|
114
114
|
};
|
|
115
115
|
|
|
116
116
|
let mut builder = InternalDocumentBuilder::new("csv");
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
if matches!(config.output_format, OutputFormat::Plain) {
|
|
118
|
+
let content_text = build_content_text(&table.cells, has_header);
|
|
119
|
+
for section in content_text.split("\n\n") {
|
|
120
|
+
let section = section.trim();
|
|
121
|
+
if !section.is_empty() {
|
|
122
|
+
builder.push_paragraph(section, vec![], None, None);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
builder.push_table(table.clone(), None, None);
|
|
127
|
+
}
|
|
124
128
|
let mut doc = builder.build();
|
|
129
|
+
if matches!(config.output_format, OutputFormat::Plain) {
|
|
130
|
+
doc.tables.push(table);
|
|
131
|
+
}
|
|
125
132
|
doc.mime_type = Cow::Owned(mime_type.to_string());
|
|
126
133
|
|
|
127
134
|
doc.metadata = Metadata {
|
|
@@ -392,6 +399,51 @@ fn infer_column_types(rows: &[Vec<String>], has_header: bool) -> Vec<String> {
|
|
|
392
399
|
.collect()
|
|
393
400
|
}
|
|
394
401
|
|
|
402
|
+
/// Build embedding-friendly labeled text for a header/data-row CSV.
|
|
403
|
+
///
|
|
404
|
+
/// Each data row becomes a `Row N:` block with `Header: value` pairs for its
|
|
405
|
+
/// non-empty cells, separated by blank lines. Rows without a header fall back
|
|
406
|
+
/// to space-joined cells so plain output stays readable.
|
|
407
|
+
fn build_content_text(rows: &[Vec<String>], has_header: bool) -> String {
|
|
408
|
+
if rows.is_empty() {
|
|
409
|
+
return String::new();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if !has_header || rows.len() < 2 {
|
|
413
|
+
return rows
|
|
414
|
+
.iter()
|
|
415
|
+
.map(|row| {
|
|
416
|
+
row.iter()
|
|
417
|
+
.map(|cell| cell.trim())
|
|
418
|
+
.filter(|cell| !cell.is_empty())
|
|
419
|
+
.collect::<Vec<_>>()
|
|
420
|
+
.join(" ")
|
|
421
|
+
})
|
|
422
|
+
.filter(|line| !line.is_empty())
|
|
423
|
+
.collect::<Vec<_>>()
|
|
424
|
+
.join("\n");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let headers = &rows[0];
|
|
428
|
+
let mut sections = Vec::with_capacity(rows.len() - 1);
|
|
429
|
+
|
|
430
|
+
for (i, row) in rows[1..].iter().enumerate() {
|
|
431
|
+
let mut lines = vec![format!("Row {}:", i + 1)];
|
|
432
|
+
for (header, value) in headers.iter().zip(row.iter()) {
|
|
433
|
+
let h = header.trim();
|
|
434
|
+
let v = value.trim();
|
|
435
|
+
if !h.is_empty() && !v.is_empty() {
|
|
436
|
+
lines.push(format!(" {}: {}", h, v));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if lines.len() > 1 {
|
|
440
|
+
sections.push(lines.join("\n"));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
sections.join("\n\n")
|
|
445
|
+
}
|
|
446
|
+
|
|
395
447
|
/// Build a Markdown table from parsed rows.
|
|
396
448
|
fn build_markdown_table(rows: &[Vec<String>]) -> String {
|
|
397
449
|
if rows.is_empty() {
|
|
@@ -19,7 +19,6 @@ use crate::Result;
|
|
|
19
19
|
use crate::core::config::{ExtractionConfig, OutputFormat};
|
|
20
20
|
use crate::plugins::{DocumentExtractor, Plugin};
|
|
21
21
|
use crate::types::Metadata;
|
|
22
|
-
use crate::types::ProcessingWarning;
|
|
23
22
|
use crate::types::internal::InternalDocument;
|
|
24
23
|
use crate::types::internal_builder::InternalDocumentBuilder;
|
|
25
24
|
use crate::types::metadata::{EpubMetadata, FormatMetadata};
|
|
@@ -56,53 +55,24 @@ impl Default for EpubExtractor {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
#[cfg(feature = "office")]
|
|
59
|
-
#[allow(dead_code)]
|
|
60
58
|
struct RenderedSpineDocument {
|
|
61
59
|
content_fragment: String,
|
|
62
60
|
content_fully_converted: bool,
|
|
63
|
-
document: Option<crate::types::document_structure::DocumentStructure>,
|
|
64
|
-
warnings: Vec<ProcessingWarning>,
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
#[cfg(feature = "office")]
|
|
68
|
-
#[allow(dead_code)]
|
|
69
64
|
fn trim_trailing_newlines(s: &str) -> &str {
|
|
70
65
|
s.trim_end_matches(['\n', '\r'])
|
|
71
66
|
}
|
|
72
67
|
|
|
73
68
|
#[cfg(feature = "office")]
|
|
74
|
-
#[allow(dead_code)]
|
|
75
69
|
impl EpubExtractor {
|
|
76
|
-
fn build_fallback_document_structure(
|
|
77
|
-
document: &content::EpubSpineDocument,
|
|
78
|
-
index: usize,
|
|
79
|
-
) -> crate::types::document_structure::DocumentStructure {
|
|
80
|
-
use crate::types::builder::DocumentStructureBuilder;
|
|
81
|
-
|
|
82
|
-
let mut builder = DocumentStructureBuilder::new().source_format("epub");
|
|
83
|
-
let chapter_title =
|
|
84
|
-
extract_title_from_xhtml(&document.xhtml).unwrap_or_else(|| format!("Chapter {}", index + 1));
|
|
85
|
-
builder.push_heading(1, &chapter_title, None, None);
|
|
86
|
-
|
|
87
|
-
let text = extract_text_from_xhtml(&document.xhtml);
|
|
88
|
-
for paragraph in text.split("\n\n") {
|
|
89
|
-
let trimmed = paragraph.trim();
|
|
90
|
-
if !trimmed.is_empty() {
|
|
91
|
-
builder.push_paragraph(trimmed, vec![], None, None);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
builder.build()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
70
|
/// Render a spine document once.
|
|
99
71
|
fn render_spine_document(
|
|
100
72
|
document: &content::EpubSpineDocument,
|
|
101
|
-
index: usize,
|
|
102
73
|
config: &ExtractionConfig,
|
|
103
74
|
) -> RenderedSpineDocument {
|
|
104
75
|
let wants_markup = matches!(config.output_format, OutputFormat::Markdown | OutputFormat::Djot);
|
|
105
|
-
let mut warnings = Vec::new();
|
|
106
76
|
|
|
107
77
|
let (content_fragment, content_fully_converted) = if wants_markup {
|
|
108
78
|
let html_options = super::html::apply_content_filter_to_html_options(
|
|
@@ -116,13 +86,12 @@ impl EpubExtractor {
|
|
|
116
86
|
) {
|
|
117
87
|
Ok((converted, _)) => (trim_trailing_newlines(&converted).to_string(), true),
|
|
118
88
|
Err(err) => {
|
|
119
|
-
|
|
120
|
-
source
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
89
|
+
tracing::warn!(
|
|
90
|
+
source = "epub",
|
|
91
|
+
file_path = %document.file_path,
|
|
92
|
+
"XHTML conversion failed for spine item; falling back to plain text"
|
|
93
|
+
);
|
|
94
|
+
tracing::debug!(error = %err, "epub xhtml conversion error detail");
|
|
126
95
|
(extract_text_from_xhtml(&document.xhtml).trim_end().to_string(), false)
|
|
127
96
|
}
|
|
128
97
|
}
|
|
@@ -130,61 +99,12 @@ impl EpubExtractor {
|
|
|
130
99
|
(extract_text_from_xhtml(&document.xhtml).trim_end().to_string(), true)
|
|
131
100
|
};
|
|
132
101
|
|
|
133
|
-
let document = if config.include_document_structure {
|
|
134
|
-
let chapter_structure = crate::extraction::html::structure::build_document_structure(&document.xhtml);
|
|
135
|
-
|
|
136
|
-
if chapter_structure.nodes.is_empty() {
|
|
137
|
-
warnings.push(ProcessingWarning {
|
|
138
|
-
source: std::borrow::Cow::Borrowed("epub"),
|
|
139
|
-
message: std::borrow::Cow::Owned(format!(
|
|
140
|
-
"Document structure extraction produced no nodes for spine item '{}'; falling back to plain-text structure",
|
|
141
|
-
document.file_path
|
|
142
|
-
)),
|
|
143
|
-
});
|
|
144
|
-
Some(Self::build_fallback_document_structure(document, index))
|
|
145
|
-
} else {
|
|
146
|
-
Some(chapter_structure)
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
None
|
|
150
|
-
};
|
|
151
|
-
|
|
152
102
|
RenderedSpineDocument {
|
|
153
103
|
content_fragment,
|
|
154
104
|
content_fully_converted,
|
|
155
|
-
document,
|
|
156
|
-
warnings,
|
|
157
105
|
}
|
|
158
106
|
}
|
|
159
107
|
|
|
160
|
-
fn build_document_structure(
|
|
161
|
-
rendered_documents: &[RenderedSpineDocument],
|
|
162
|
-
) -> Option<crate::types::document_structure::DocumentStructure> {
|
|
163
|
-
use crate::types::builder::DocumentStructureBuilder;
|
|
164
|
-
|
|
165
|
-
let mut builder = DocumentStructureBuilder::new().source_format("epub");
|
|
166
|
-
let mut has_nodes = false;
|
|
167
|
-
|
|
168
|
-
for rendered in rendered_documents {
|
|
169
|
-
let Some(chapter_structure) = &rendered.document else {
|
|
170
|
-
continue;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
for node in &chapter_structure.nodes {
|
|
174
|
-
has_nodes = true;
|
|
175
|
-
builder.push_raw(
|
|
176
|
-
node.content.clone(),
|
|
177
|
-
None,
|
|
178
|
-
None,
|
|
179
|
-
node.content_layer,
|
|
180
|
-
node.annotations.clone(),
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if has_nodes { Some(builder.build()) } else { None }
|
|
186
|
-
}
|
|
187
|
-
|
|
188
108
|
/// Build an `InternalDocument` from the EPUB spine.
|
|
189
109
|
///
|
|
190
110
|
/// Walks each chapter's XHTML content using the HTML structure walker,
|
|
@@ -199,11 +119,17 @@ impl EpubExtractor {
|
|
|
199
119
|
manifest_dir: &str,
|
|
200
120
|
nav_hrefs: &AHashSet<String>,
|
|
201
121
|
cover_image_path: Option<&str>,
|
|
122
|
+
config: &ExtractionConfig,
|
|
202
123
|
) -> Option<InternalDocument> {
|
|
203
124
|
use crate::types::internal::{ElementKind, InternalElement};
|
|
204
125
|
|
|
205
126
|
let mut builder = InternalDocumentBuilder::new("epub");
|
|
206
127
|
|
|
128
|
+
// Accumulate pre-rendered markdown/djot when all chapters convert successfully
|
|
129
|
+
let wants_markup = matches!(config.output_format, OutputFormat::Markdown | OutputFormat::Djot);
|
|
130
|
+
let mut pre_rendered_fragments = Vec::new();
|
|
131
|
+
let mut all_converted_successfully = wants_markup;
|
|
132
|
+
|
|
207
133
|
if let Some(cover_path) = cover_image_path {
|
|
208
134
|
let mut buf = Vec::new();
|
|
209
135
|
if let Ok(mut entry) = archive.by_name(cover_path) {
|
|
@@ -260,6 +186,20 @@ impl EpubExtractor {
|
|
|
260
186
|
let normalized = content::normalize_xhtml(&xhtml_content);
|
|
261
187
|
let sanitized = strip_specialized_navigation_sections(&strip_document_head(&normalized));
|
|
262
188
|
|
|
189
|
+
// If markdown/djot output requested, try to pre-render this chapter
|
|
190
|
+
if wants_markup {
|
|
191
|
+
let spine_doc = content::EpubSpineDocument {
|
|
192
|
+
file_path: file_path.clone(),
|
|
193
|
+
xhtml: sanitized.clone(),
|
|
194
|
+
};
|
|
195
|
+
let rendered = Self::render_spine_document(&spine_doc, config);
|
|
196
|
+
if rendered.content_fully_converted {
|
|
197
|
+
pre_rendered_fragments.push(rendered.content_fragment);
|
|
198
|
+
} else {
|
|
199
|
+
all_converted_successfully = false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
263
203
|
if looks_like_navigation_document(&sanitized) {
|
|
264
204
|
continue;
|
|
265
205
|
}
|
|
@@ -404,7 +344,23 @@ impl EpubExtractor {
|
|
|
404
344
|
}
|
|
405
345
|
}
|
|
406
346
|
|
|
407
|
-
|
|
347
|
+
let mut doc = builder.build();
|
|
348
|
+
|
|
349
|
+
// If markdown/djot was requested and all chapters converted successfully,
|
|
350
|
+
// store the pre-rendered content and mark the output format so
|
|
351
|
+
// derive_extraction_result uses it directly instead of re-rendering.
|
|
352
|
+
if all_converted_successfully && !pre_rendered_fragments.is_empty() {
|
|
353
|
+
let combined = pre_rendered_fragments.join("\n\n");
|
|
354
|
+
doc.pre_rendered_content = Some(combined);
|
|
355
|
+
let format_name = match config.output_format {
|
|
356
|
+
OutputFormat::Markdown => "markdown",
|
|
357
|
+
OutputFormat::Djot => "djot",
|
|
358
|
+
_ => "plain",
|
|
359
|
+
};
|
|
360
|
+
doc.metadata.output_format = Some(format_name.to_string());
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
Some(doc)
|
|
408
364
|
}
|
|
409
365
|
}
|
|
410
366
|
|
|
@@ -514,7 +470,6 @@ impl DocumentExtractor for EpubExtractor {
|
|
|
514
470
|
config: &ExtractionConfig,
|
|
515
471
|
) -> Result<InternalDocument> {
|
|
516
472
|
tracing::debug!(format = "epub", size_bytes = content.len(), "extraction starting");
|
|
517
|
-
let _ = config;
|
|
518
473
|
let cursor = Cursor::new(content.to_vec());
|
|
519
474
|
|
|
520
475
|
let mut archive = ZipArchive::new(cursor).map_err(|e| crate::KreuzbergError::Parsing {
|
|
@@ -567,11 +522,19 @@ impl DocumentExtractor for EpubExtractor {
|
|
|
567
522
|
.collect();
|
|
568
523
|
|
|
569
524
|
let cover_image_path = package.metadata.cover_image_href.as_deref();
|
|
570
|
-
let mut doc =
|
|
571
|
-
|
|
572
|
-
|
|
525
|
+
let mut doc = Self::build_internal_document(
|
|
526
|
+
&mut archive,
|
|
527
|
+
&spine_hrefs,
|
|
528
|
+
&manifest_dir,
|
|
529
|
+
&nav_hrefs,
|
|
530
|
+
cover_image_path,
|
|
531
|
+
config,
|
|
532
|
+
)
|
|
533
|
+
.unwrap_or_else(|| InternalDocumentBuilder::new("epub").build());
|
|
573
534
|
doc.mime_type = Cow::Owned(mime_type.to_string());
|
|
574
535
|
|
|
536
|
+
// Preserve output_format if it was set by build_internal_document (markdown/djot pre-rendering)
|
|
537
|
+
let output_format = doc.metadata.output_format.take();
|
|
575
538
|
doc.metadata = Metadata {
|
|
576
539
|
title: package.metadata.title,
|
|
577
540
|
authors: package.metadata.creator.map(|c| vec![c]),
|
|
@@ -579,6 +542,7 @@ impl DocumentExtractor for EpubExtractor {
|
|
|
579
542
|
created_at: package.metadata.date,
|
|
580
543
|
format: Some(epub_format_metadata),
|
|
581
544
|
additional: metadata_map,
|
|
545
|
+
output_format,
|
|
582
546
|
..Default::default()
|
|
583
547
|
};
|
|
584
548
|
|
|
@@ -841,7 +841,7 @@ fn chunk_text_impl(_params: super::params::ChunkTextParams) -> Result<CallToolRe
|
|
|
841
841
|
|
|
842
842
|
#[tool_handler]
|
|
843
843
|
impl ServerHandler for KreuzbergMcp {
|
|
844
|
-
fn get_info(&self) ->
|
|
844
|
+
fn get_info(&self) -> ServerConfig {
|
|
845
845
|
let mut capabilities = ServerCapabilities::default();
|
|
846
846
|
capabilities.tools = Some(ToolsCapability::default());
|
|
847
847
|
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
mod helpers;
|
|
10
10
|
|
|
11
11
|
use helpers::{assert_non_empty_content, get_test_file_path};
|
|
12
|
-
use kreuzberg::ExtractionConfig;
|
|
13
12
|
use kreuzberg::extract_file;
|
|
13
|
+
use kreuzberg::{ExtractionConfig, OutputFormat};
|
|
14
14
|
|
|
15
15
|
#[tokio::test]
|
|
16
16
|
async fn test_docx_bold_rendered_as_markdown() {
|
|
@@ -19,9 +19,11 @@ async fn test_docx_bold_rendered_as_markdown() {
|
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
let config = ExtractionConfig {
|
|
23
|
+
output_format: OutputFormat::Markdown,
|
|
24
|
+
..Default::default()
|
|
25
|
+
};
|
|
26
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
25
27
|
|
|
26
28
|
assert_non_empty_content(&result);
|
|
27
29
|
assert!(
|
|
@@ -38,9 +40,11 @@ async fn test_docx_italic_rendered_as_markdown() {
|
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
let config = ExtractionConfig {
|
|
44
|
+
output_format: OutputFormat::Markdown,
|
|
45
|
+
..Default::default()
|
|
46
|
+
};
|
|
47
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
44
48
|
|
|
45
49
|
assert_non_empty_content(&result);
|
|
46
50
|
assert!(
|
|
@@ -57,9 +61,11 @@ async fn test_docx_hyperlink_rendered_as_markdown() {
|
|
|
57
61
|
return;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
let config = ExtractionConfig {
|
|
65
|
+
output_format: OutputFormat::Markdown,
|
|
66
|
+
..Default::default()
|
|
67
|
+
};
|
|
68
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
63
69
|
|
|
64
70
|
assert_non_empty_content(&result);
|
|
65
71
|
assert!(
|
|
@@ -81,9 +87,11 @@ async fn test_docx_mixed_formatting_on_same_line() {
|
|
|
81
87
|
return;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
let
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
let config = ExtractionConfig {
|
|
91
|
+
output_format: OutputFormat::Markdown,
|
|
92
|
+
..Default::default()
|
|
93
|
+
};
|
|
94
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
87
95
|
|
|
88
96
|
assert_non_empty_content(&result);
|
|
89
97
|
let content = &result.content;
|
|
@@ -101,9 +109,11 @@ async fn test_docx_title_rendered_as_h1() {
|
|
|
101
109
|
return;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
let config = ExtractionConfig {
|
|
113
|
+
output_format: OutputFormat::Markdown,
|
|
114
|
+
..Default::default()
|
|
115
|
+
};
|
|
116
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
107
117
|
|
|
108
118
|
assert_non_empty_content(&result);
|
|
109
119
|
assert!(
|
|
@@ -120,9 +130,11 @@ async fn test_docx_heading_hierarchy() {
|
|
|
120
130
|
return;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
let
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
let config = ExtractionConfig {
|
|
134
|
+
output_format: OutputFormat::Markdown,
|
|
135
|
+
..Default::default()
|
|
136
|
+
};
|
|
137
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
126
138
|
|
|
127
139
|
assert_non_empty_content(&result);
|
|
128
140
|
let content = &result.content;
|
|
@@ -172,9 +184,11 @@ async fn test_docx_bullet_list_rendered() {
|
|
|
172
184
|
return;
|
|
173
185
|
}
|
|
174
186
|
|
|
175
|
-
let
|
|
176
|
-
|
|
177
|
-
|
|
187
|
+
let config = ExtractionConfig {
|
|
188
|
+
output_format: OutputFormat::Markdown,
|
|
189
|
+
..Default::default()
|
|
190
|
+
};
|
|
191
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
178
192
|
|
|
179
193
|
assert_non_empty_content(&result);
|
|
180
194
|
assert!(
|
|
@@ -191,9 +205,11 @@ async fn test_docx_numbered_list_rendered() {
|
|
|
191
205
|
return;
|
|
192
206
|
}
|
|
193
207
|
|
|
194
|
-
let
|
|
195
|
-
|
|
196
|
-
|
|
208
|
+
let config = ExtractionConfig {
|
|
209
|
+
output_format: OutputFormat::Markdown,
|
|
210
|
+
..Default::default()
|
|
211
|
+
};
|
|
212
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
197
213
|
|
|
198
214
|
assert_non_empty_content(&result);
|
|
199
215
|
assert!(
|
|
@@ -210,14 +226,16 @@ async fn test_docx_nested_list_indentation() {
|
|
|
210
226
|
return;
|
|
211
227
|
}
|
|
212
228
|
|
|
213
|
-
let
|
|
214
|
-
|
|
215
|
-
|
|
229
|
+
let config = ExtractionConfig {
|
|
230
|
+
output_format: OutputFormat::Markdown,
|
|
231
|
+
..Default::default()
|
|
232
|
+
};
|
|
233
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
216
234
|
|
|
217
235
|
assert_non_empty_content(&result);
|
|
218
236
|
assert!(
|
|
219
|
-
result.content.contains("
|
|
220
|
-
"Nested lists should be
|
|
237
|
+
result.content.contains("- List item 1.1"),
|
|
238
|
+
"Nested lists should be present. Got:\n{}",
|
|
221
239
|
result.content
|
|
222
240
|
);
|
|
223
241
|
}
|
|
@@ -252,9 +270,11 @@ async fn test_docx_tables_in_markdown_output() {
|
|
|
252
270
|
return;
|
|
253
271
|
}
|
|
254
272
|
|
|
255
|
-
let
|
|
256
|
-
|
|
257
|
-
|
|
273
|
+
let config = ExtractionConfig {
|
|
274
|
+
output_format: OutputFormat::Markdown,
|
|
275
|
+
..Default::default()
|
|
276
|
+
};
|
|
277
|
+
let result = extract_file(&path, None, &config).await.expect("Should extract DOCX");
|
|
258
278
|
|
|
259
279
|
assert_non_empty_content(&result);
|
|
260
280
|
assert!(
|
|
@@ -100,24 +100,25 @@ async fn test_epub_markdown_output_keeps_headings() {
|
|
|
100
100
|
.extract_bytes(&bytes, "application/epub+zip", &config)
|
|
101
101
|
.await
|
|
102
102
|
.expect("EPUB extraction should succeed");
|
|
103
|
-
let result = derive_extraction_result(doc, false, kreuzberg::OutputFormat::
|
|
103
|
+
let result = derive_extraction_result(doc, false, kreuzberg::OutputFormat::Markdown);
|
|
104
104
|
|
|
105
105
|
assert!(
|
|
106
106
|
result.processing_warnings.is_empty(),
|
|
107
107
|
"Expected no warnings, got: {:?}",
|
|
108
108
|
result.processing_warnings
|
|
109
109
|
);
|
|
110
|
+
let content_to_check = result.formatted_content.as_deref().unwrap_or(&result.content);
|
|
110
111
|
assert!(
|
|
111
|
-
|
|
112
|
+
content_to_check.contains("# Chapter One"),
|
|
112
113
|
"Expected Markdown heading, got:\n{}",
|
|
113
|
-
|
|
114
|
+
content_to_check
|
|
114
115
|
);
|
|
115
116
|
assert!(
|
|
116
|
-
!
|
|
117
|
+
!content_to_check.starts_with("---"),
|
|
117
118
|
"Expected no YAML frontmatter injection, got:\n{}",
|
|
118
|
-
|
|
119
|
+
content_to_check
|
|
119
120
|
);
|
|
120
|
-
assert!(
|
|
121
|
+
assert!(content_to_check.contains("Some text."), "Expected body text");
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
#[tokio::test]
|
|
@@ -134,24 +135,25 @@ async fn test_epub_djot_output_keeps_headings() {
|
|
|
134
135
|
.extract_bytes(&bytes, "application/epub+zip", &config)
|
|
135
136
|
.await
|
|
136
137
|
.expect("EPUB extraction should succeed");
|
|
137
|
-
let result = derive_extraction_result(doc, false, kreuzberg::OutputFormat::
|
|
138
|
+
let result = derive_extraction_result(doc, false, kreuzberg::OutputFormat::Djot);
|
|
138
139
|
|
|
139
140
|
assert!(
|
|
140
141
|
result.processing_warnings.is_empty(),
|
|
141
142
|
"Expected no warnings, got: {:?}",
|
|
142
143
|
result.processing_warnings
|
|
143
144
|
);
|
|
145
|
+
let content_to_check = result.formatted_content.as_deref().unwrap_or(&result.content);
|
|
144
146
|
assert!(
|
|
145
|
-
|
|
147
|
+
content_to_check.contains("# Chapter One"),
|
|
146
148
|
"Expected Djot heading, got:\n{}",
|
|
147
|
-
|
|
149
|
+
content_to_check
|
|
148
150
|
);
|
|
149
151
|
assert!(
|
|
150
|
-
!
|
|
152
|
+
!content_to_check.starts_with("---"),
|
|
151
153
|
"Expected no YAML frontmatter injection, got:\n{}",
|
|
152
|
-
|
|
154
|
+
content_to_check
|
|
153
155
|
);
|
|
154
|
-
assert!(
|
|
156
|
+
assert!(content_to_check.contains("Some text."), "Expected body text");
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
#[tokio::test]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "kreuzberg-ffi"
|
|
3
|
-
version = "4.10.
|
|
3
|
+
version = "4.10.4"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.91"
|
|
6
6
|
authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
|
|
@@ -29,8 +29,8 @@ tree-sitter = ["kreuzberg/tree-sitter"]
|
|
|
29
29
|
ahash = { version = "0.8.12", features = ["serde"] }
|
|
30
30
|
async-trait = "0.1.92"
|
|
31
31
|
ctor = "1.0"
|
|
32
|
-
html-to-markdown-rs = { version = "3.
|
|
33
|
-
kreuzberg = { path = "../kreuzberg", version = "4.10.
|
|
32
|
+
html-to-markdown-rs = { version = "3.14.3", default-features = false }
|
|
33
|
+
kreuzberg = { path = "../kreuzberg", version = "4.10.4", default-features = false, features = ["bundled-pdfium", "full"] }
|
|
34
34
|
log = "0.4"
|
|
35
35
|
rayon = { version = "1.12.0", optional = true }
|
|
36
36
|
serde = { version = "1.0.229", features = ["derive"] }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "kreuzberg-pdfium-render"
|
|
3
|
-
version = "4.10.
|
|
3
|
+
version = "4.10.4"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.91"
|
|
6
6
|
authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
|
|
@@ -58,7 +58,7 @@ wasm-bindgen-futures = "0.4"
|
|
|
58
58
|
web-sys = { version = "0.3", features = ["Blob", "File", "FileReader", "console", "TextDecoder", "ImageData", "Window", "Response"] }
|
|
59
59
|
|
|
60
60
|
[build-dependencies]
|
|
61
|
-
bindgen = { version = "0.
|
|
61
|
+
bindgen = { version = "0.73.2", optional = true }
|
|
62
62
|
|
|
63
63
|
[lints.rust.unexpected_cfgs]
|
|
64
64
|
level = "allow"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "kreuzberg-tesseract"
|
|
3
|
-
version = "4.10.
|
|
3
|
+
version = "4.10.4"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.91"
|
|
6
6
|
authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
|
|
@@ -38,21 +38,21 @@ dynamic-linking = []
|
|
|
38
38
|
thiserror = "2.0.20"
|
|
39
39
|
|
|
40
40
|
[build-dependencies]
|
|
41
|
-
cc = { version = "
|
|
41
|
+
cc = { version = "1.4.7", optional = true }
|
|
42
42
|
cmake = { version = "0.1.58", optional = true }
|
|
43
43
|
zip = { version = ">=7.0.0", optional = true, default-features = false, features = [
|
|
44
44
|
"deflate-flate2-zlib-rs",
|
|
45
45
|
] }
|
|
46
46
|
|
|
47
47
|
[target.'cfg(not(target_os = "windows"))'.build-dependencies]
|
|
48
|
-
reqwest = { version = "0.13.
|
|
48
|
+
reqwest = { version = "0.13.5", default-features = false, features = [
|
|
49
49
|
"blocking",
|
|
50
50
|
"rustls",
|
|
51
51
|
], optional = true }
|
|
52
52
|
|
|
53
53
|
# Use native-tls on Windows to avoid aws-lc-sys CMake build issues with MinGW
|
|
54
54
|
[target.'cfg(target_os = "windows")'.build-dependencies]
|
|
55
|
-
reqwest = { version = "0.13.
|
|
55
|
+
reqwest = { version = "0.13.5", default-features = false, features = [
|
|
56
56
|
"blocking",
|
|
57
57
|
"native-tls",
|
|
58
58
|
], optional = true }
|
|
@@ -934,6 +934,17 @@ impl TesseractAPI {
|
|
|
934
934
|
if vec_ptr.is_null() {
|
|
935
935
|
return Err(TesseractError::NullPointerError);
|
|
936
936
|
}
|
|
937
|
+
|
|
938
|
+
struct TextArrayGuard(*mut *mut c_char);
|
|
939
|
+
impl Drop for TextArrayGuard {
|
|
940
|
+
fn drop(&mut self) {
|
|
941
|
+
// SAFETY: `TessDeleteTextArray()` deallocates both the array
|
|
942
|
+
// and all contained strings.
|
|
943
|
+
unsafe { TessDeleteTextArray(self.0) };
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
let _text_array = TextArrayGuard(vec_ptr);
|
|
947
|
+
|
|
937
948
|
let mut result = Vec::new();
|
|
938
949
|
let mut i = 0;
|
|
939
950
|
loop {
|
|
@@ -947,8 +958,6 @@ impl TesseractAPI {
|
|
|
947
958
|
result.push(c_str.to_str()?.to_owned());
|
|
948
959
|
i += 1;
|
|
949
960
|
}
|
|
950
|
-
// SAFETY: TessDeleteTextArray() deallocates both the array and all contained strings:
|
|
951
|
-
unsafe { TessDeleteTextArray(vec_ptr) };
|
|
952
961
|
Ok(result)
|
|
953
962
|
}
|
|
954
963
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kreuzberg
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.10.
|
|
4
|
+
version: 4.10.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Na'aman Hirschfeld
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|