kreuzberg 4.0.0.rc1
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 +7 -0
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/.rubocop.yaml +534 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +157 -0
- data/README.md +421 -0
- data/Rakefile +25 -0
- data/Steepfile +47 -0
- data/examples/async_patterns.rb +340 -0
- data/ext/kreuzberg_rb/extconf.rb +35 -0
- data/ext/kreuzberg_rb/native/Cargo.toml +36 -0
- data/ext/kreuzberg_rb/native/README.md +425 -0
- data/ext/kreuzberg_rb/native/build.rs +17 -0
- data/ext/kreuzberg_rb/native/include/ieeefp.h +11 -0
- data/ext/kreuzberg_rb/native/include/msvc_compat/strings.h +14 -0
- data/ext/kreuzberg_rb/native/include/strings.h +20 -0
- data/ext/kreuzberg_rb/native/include/unistd.h +47 -0
- data/ext/kreuzberg_rb/native/src/lib.rs +2939 -0
- data/extconf.rb +28 -0
- data/kreuzberg.gemspec +105 -0
- data/lib/kreuzberg/api_proxy.rb +142 -0
- data/lib/kreuzberg/cache_api.rb +45 -0
- data/lib/kreuzberg/cli.rb +55 -0
- data/lib/kreuzberg/cli_proxy.rb +127 -0
- data/lib/kreuzberg/config.rb +684 -0
- data/lib/kreuzberg/errors.rb +50 -0
- data/lib/kreuzberg/extraction_api.rb +84 -0
- data/lib/kreuzberg/mcp_proxy.rb +186 -0
- data/lib/kreuzberg/ocr_backend_protocol.rb +113 -0
- data/lib/kreuzberg/post_processor_protocol.rb +86 -0
- data/lib/kreuzberg/result.rb +216 -0
- data/lib/kreuzberg/setup_lib_path.rb +79 -0
- data/lib/kreuzberg/validator_protocol.rb +89 -0
- data/lib/kreuzberg/version.rb +5 -0
- data/lib/kreuzberg.rb +82 -0
- data/pkg/kreuzberg-4.0.0.rc1.gem +0 -0
- data/sig/kreuzberg/internal.rbs +184 -0
- data/sig/kreuzberg.rbs +468 -0
- data/spec/binding/cache_spec.rb +227 -0
- data/spec/binding/cli_proxy_spec.rb +87 -0
- data/spec/binding/cli_spec.rb +54 -0
- data/spec/binding/config_spec.rb +345 -0
- data/spec/binding/config_validation_spec.rb +283 -0
- data/spec/binding/error_handling_spec.rb +213 -0
- data/spec/binding/errors_spec.rb +66 -0
- data/spec/binding/plugins/ocr_backend_spec.rb +307 -0
- data/spec/binding/plugins/postprocessor_spec.rb +269 -0
- data/spec/binding/plugins/validator_spec.rb +274 -0
- data/spec/examples.txt +104 -0
- data/spec/fixtures/config.toml +39 -0
- data/spec/fixtures/config.yaml +42 -0
- data/spec/fixtures/invalid_config.toml +4 -0
- data/spec/smoke/package_spec.rb +178 -0
- data/spec/spec_helper.rb +42 -0
- data/vendor/kreuzberg/Cargo.toml +134 -0
- data/vendor/kreuzberg/README.md +175 -0
- data/vendor/kreuzberg/build.rs +460 -0
- data/vendor/kreuzberg/src/api/error.rs +81 -0
- data/vendor/kreuzberg/src/api/handlers.rs +199 -0
- data/vendor/kreuzberg/src/api/mod.rs +79 -0
- data/vendor/kreuzberg/src/api/server.rs +353 -0
- data/vendor/kreuzberg/src/api/types.rs +170 -0
- data/vendor/kreuzberg/src/bin/profile_extract.rs +455 -0
- data/vendor/kreuzberg/src/cache/mod.rs +1143 -0
- data/vendor/kreuzberg/src/chunking/mod.rs +677 -0
- data/vendor/kreuzberg/src/core/batch_mode.rs +35 -0
- data/vendor/kreuzberg/src/core/config.rs +1032 -0
- data/vendor/kreuzberg/src/core/extractor.rs +903 -0
- data/vendor/kreuzberg/src/core/io.rs +327 -0
- data/vendor/kreuzberg/src/core/mime.rs +615 -0
- data/vendor/kreuzberg/src/core/mod.rs +42 -0
- data/vendor/kreuzberg/src/core/pipeline.rs +906 -0
- data/vendor/kreuzberg/src/embeddings.rs +323 -0
- data/vendor/kreuzberg/src/error.rs +431 -0
- data/vendor/kreuzberg/src/extraction/archive.rs +954 -0
- data/vendor/kreuzberg/src/extraction/docx.rs +40 -0
- data/vendor/kreuzberg/src/extraction/email.rs +854 -0
- data/vendor/kreuzberg/src/extraction/excel.rs +688 -0
- data/vendor/kreuzberg/src/extraction/html.rs +553 -0
- data/vendor/kreuzberg/src/extraction/image.rs +368 -0
- data/vendor/kreuzberg/src/extraction/libreoffice.rs +564 -0
- data/vendor/kreuzberg/src/extraction/mod.rs +77 -0
- data/vendor/kreuzberg/src/extraction/office_metadata/app_properties.rs +398 -0
- data/vendor/kreuzberg/src/extraction/office_metadata/core_properties.rs +247 -0
- data/vendor/kreuzberg/src/extraction/office_metadata/custom_properties.rs +240 -0
- data/vendor/kreuzberg/src/extraction/office_metadata/mod.rs +128 -0
- data/vendor/kreuzberg/src/extraction/pandoc/batch.rs +275 -0
- data/vendor/kreuzberg/src/extraction/pandoc/mime_types.rs +178 -0
- data/vendor/kreuzberg/src/extraction/pandoc/mod.rs +491 -0
- data/vendor/kreuzberg/src/extraction/pandoc/server.rs +496 -0
- data/vendor/kreuzberg/src/extraction/pandoc/subprocess.rs +1188 -0
- data/vendor/kreuzberg/src/extraction/pandoc/version.rs +162 -0
- data/vendor/kreuzberg/src/extraction/pptx.rs +3000 -0
- data/vendor/kreuzberg/src/extraction/structured.rs +490 -0
- data/vendor/kreuzberg/src/extraction/table.rs +328 -0
- data/vendor/kreuzberg/src/extraction/text.rs +269 -0
- data/vendor/kreuzberg/src/extraction/xml.rs +333 -0
- data/vendor/kreuzberg/src/extractors/archive.rs +425 -0
- data/vendor/kreuzberg/src/extractors/docx.rs +479 -0
- data/vendor/kreuzberg/src/extractors/email.rs +129 -0
- data/vendor/kreuzberg/src/extractors/excel.rs +344 -0
- data/vendor/kreuzberg/src/extractors/html.rs +410 -0
- data/vendor/kreuzberg/src/extractors/image.rs +195 -0
- data/vendor/kreuzberg/src/extractors/mod.rs +268 -0
- data/vendor/kreuzberg/src/extractors/pandoc.rs +201 -0
- data/vendor/kreuzberg/src/extractors/pdf.rs +496 -0
- data/vendor/kreuzberg/src/extractors/pptx.rs +234 -0
- data/vendor/kreuzberg/src/extractors/structured.rs +126 -0
- data/vendor/kreuzberg/src/extractors/text.rs +242 -0
- data/vendor/kreuzberg/src/extractors/xml.rs +128 -0
- data/vendor/kreuzberg/src/image/dpi.rs +164 -0
- data/vendor/kreuzberg/src/image/mod.rs +6 -0
- data/vendor/kreuzberg/src/image/preprocessing.rs +417 -0
- data/vendor/kreuzberg/src/image/resize.rs +89 -0
- data/vendor/kreuzberg/src/keywords/config.rs +154 -0
- data/vendor/kreuzberg/src/keywords/mod.rs +237 -0
- data/vendor/kreuzberg/src/keywords/processor.rs +267 -0
- data/vendor/kreuzberg/src/keywords/rake.rs +294 -0
- data/vendor/kreuzberg/src/keywords/types.rs +68 -0
- data/vendor/kreuzberg/src/keywords/yake.rs +163 -0
- data/vendor/kreuzberg/src/language_detection/mod.rs +942 -0
- data/vendor/kreuzberg/src/lib.rs +102 -0
- data/vendor/kreuzberg/src/mcp/mod.rs +32 -0
- data/vendor/kreuzberg/src/mcp/server.rs +1966 -0
- data/vendor/kreuzberg/src/ocr/cache.rs +469 -0
- data/vendor/kreuzberg/src/ocr/error.rs +37 -0
- data/vendor/kreuzberg/src/ocr/hocr.rs +216 -0
- data/vendor/kreuzberg/src/ocr/mod.rs +58 -0
- data/vendor/kreuzberg/src/ocr/processor.rs +847 -0
- data/vendor/kreuzberg/src/ocr/table/mod.rs +4 -0
- data/vendor/kreuzberg/src/ocr/table/tsv_parser.rs +144 -0
- data/vendor/kreuzberg/src/ocr/tesseract_backend.rs +450 -0
- data/vendor/kreuzberg/src/ocr/types.rs +393 -0
- data/vendor/kreuzberg/src/ocr/utils.rs +47 -0
- data/vendor/kreuzberg/src/ocr/validation.rs +206 -0
- data/vendor/kreuzberg/src/pdf/error.rs +122 -0
- data/vendor/kreuzberg/src/pdf/images.rs +139 -0
- data/vendor/kreuzberg/src/pdf/metadata.rs +346 -0
- data/vendor/kreuzberg/src/pdf/mod.rs +50 -0
- data/vendor/kreuzberg/src/pdf/rendering.rs +369 -0
- data/vendor/kreuzberg/src/pdf/table.rs +420 -0
- data/vendor/kreuzberg/src/pdf/text.rs +161 -0
- data/vendor/kreuzberg/src/plugins/extractor.rs +1010 -0
- data/vendor/kreuzberg/src/plugins/mod.rs +209 -0
- data/vendor/kreuzberg/src/plugins/ocr.rs +629 -0
- data/vendor/kreuzberg/src/plugins/processor.rs +641 -0
- data/vendor/kreuzberg/src/plugins/registry.rs +1324 -0
- data/vendor/kreuzberg/src/plugins/traits.rs +258 -0
- data/vendor/kreuzberg/src/plugins/validator.rs +955 -0
- data/vendor/kreuzberg/src/stopwords/mod.rs +1470 -0
- data/vendor/kreuzberg/src/text/mod.rs +19 -0
- data/vendor/kreuzberg/src/text/quality.rs +697 -0
- data/vendor/kreuzberg/src/text/string_utils.rs +217 -0
- data/vendor/kreuzberg/src/text/token_reduction/cjk_utils.rs +164 -0
- data/vendor/kreuzberg/src/text/token_reduction/config.rs +100 -0
- data/vendor/kreuzberg/src/text/token_reduction/core.rs +796 -0
- data/vendor/kreuzberg/src/text/token_reduction/filters.rs +902 -0
- data/vendor/kreuzberg/src/text/token_reduction/mod.rs +160 -0
- data/vendor/kreuzberg/src/text/token_reduction/semantic.rs +619 -0
- data/vendor/kreuzberg/src/text/token_reduction/simd_text.rs +147 -0
- data/vendor/kreuzberg/src/types.rs +873 -0
- data/vendor/kreuzberg/src/utils/mod.rs +17 -0
- data/vendor/kreuzberg/src/utils/quality.rs +959 -0
- data/vendor/kreuzberg/src/utils/string_utils.rs +381 -0
- data/vendor/kreuzberg/stopwords/af_stopwords.json +53 -0
- data/vendor/kreuzberg/stopwords/ar_stopwords.json +482 -0
- data/vendor/kreuzberg/stopwords/bg_stopwords.json +261 -0
- data/vendor/kreuzberg/stopwords/bn_stopwords.json +400 -0
- data/vendor/kreuzberg/stopwords/br_stopwords.json +1205 -0
- data/vendor/kreuzberg/stopwords/ca_stopwords.json +280 -0
- data/vendor/kreuzberg/stopwords/cs_stopwords.json +425 -0
- data/vendor/kreuzberg/stopwords/da_stopwords.json +172 -0
- data/vendor/kreuzberg/stopwords/de_stopwords.json +622 -0
- data/vendor/kreuzberg/stopwords/el_stopwords.json +849 -0
- data/vendor/kreuzberg/stopwords/en_stopwords.json +1300 -0
- data/vendor/kreuzberg/stopwords/eo_stopwords.json +175 -0
- data/vendor/kreuzberg/stopwords/es_stopwords.json +734 -0
- data/vendor/kreuzberg/stopwords/et_stopwords.json +37 -0
- data/vendor/kreuzberg/stopwords/eu_stopwords.json +100 -0
- data/vendor/kreuzberg/stopwords/fa_stopwords.json +801 -0
- data/vendor/kreuzberg/stopwords/fi_stopwords.json +849 -0
- data/vendor/kreuzberg/stopwords/fr_stopwords.json +693 -0
- data/vendor/kreuzberg/stopwords/ga_stopwords.json +111 -0
- data/vendor/kreuzberg/stopwords/gl_stopwords.json +162 -0
- data/vendor/kreuzberg/stopwords/gu_stopwords.json +226 -0
- data/vendor/kreuzberg/stopwords/ha_stopwords.json +41 -0
- data/vendor/kreuzberg/stopwords/he_stopwords.json +196 -0
- data/vendor/kreuzberg/stopwords/hi_stopwords.json +227 -0
- data/vendor/kreuzberg/stopwords/hr_stopwords.json +181 -0
- data/vendor/kreuzberg/stopwords/hu_stopwords.json +791 -0
- data/vendor/kreuzberg/stopwords/hy_stopwords.json +47 -0
- data/vendor/kreuzberg/stopwords/id_stopwords.json +760 -0
- data/vendor/kreuzberg/stopwords/it_stopwords.json +634 -0
- data/vendor/kreuzberg/stopwords/ja_stopwords.json +136 -0
- data/vendor/kreuzberg/stopwords/kn_stopwords.json +84 -0
- data/vendor/kreuzberg/stopwords/ko_stopwords.json +681 -0
- data/vendor/kreuzberg/stopwords/ku_stopwords.json +64 -0
- data/vendor/kreuzberg/stopwords/la_stopwords.json +51 -0
- data/vendor/kreuzberg/stopwords/lt_stopwords.json +476 -0
- data/vendor/kreuzberg/stopwords/lv_stopwords.json +163 -0
- data/vendor/kreuzberg/stopwords/ml_stopwords.json +1 -0
- data/vendor/kreuzberg/stopwords/mr_stopwords.json +101 -0
- data/vendor/kreuzberg/stopwords/ms_stopwords.json +477 -0
- data/vendor/kreuzberg/stopwords/ne_stopwords.json +490 -0
- data/vendor/kreuzberg/stopwords/nl_stopwords.json +415 -0
- data/vendor/kreuzberg/stopwords/no_stopwords.json +223 -0
- data/vendor/kreuzberg/stopwords/pl_stopwords.json +331 -0
- data/vendor/kreuzberg/stopwords/pt_stopwords.json +562 -0
- data/vendor/kreuzberg/stopwords/ro_stopwords.json +436 -0
- data/vendor/kreuzberg/stopwords/ru_stopwords.json +561 -0
- data/vendor/kreuzberg/stopwords/si_stopwords.json +193 -0
- data/vendor/kreuzberg/stopwords/sk_stopwords.json +420 -0
- data/vendor/kreuzberg/stopwords/sl_stopwords.json +448 -0
- data/vendor/kreuzberg/stopwords/so_stopwords.json +32 -0
- data/vendor/kreuzberg/stopwords/st_stopwords.json +33 -0
- data/vendor/kreuzberg/stopwords/sv_stopwords.json +420 -0
- data/vendor/kreuzberg/stopwords/sw_stopwords.json +76 -0
- data/vendor/kreuzberg/stopwords/ta_stopwords.json +129 -0
- data/vendor/kreuzberg/stopwords/te_stopwords.json +54 -0
- data/vendor/kreuzberg/stopwords/th_stopwords.json +118 -0
- data/vendor/kreuzberg/stopwords/tl_stopwords.json +149 -0
- data/vendor/kreuzberg/stopwords/tr_stopwords.json +506 -0
- data/vendor/kreuzberg/stopwords/uk_stopwords.json +75 -0
- data/vendor/kreuzberg/stopwords/ur_stopwords.json +519 -0
- data/vendor/kreuzberg/stopwords/vi_stopwords.json +647 -0
- data/vendor/kreuzberg/stopwords/yo_stopwords.json +62 -0
- data/vendor/kreuzberg/stopwords/zh_stopwords.json +796 -0
- data/vendor/kreuzberg/stopwords/zu_stopwords.json +31 -0
- data/vendor/kreuzberg/tests/api_tests.rs +966 -0
- data/vendor/kreuzberg/tests/archive_integration.rs +543 -0
- data/vendor/kreuzberg/tests/batch_orchestration.rs +542 -0
- data/vendor/kreuzberg/tests/batch_processing.rs +304 -0
- data/vendor/kreuzberg/tests/chunking_offset_demo.rs +92 -0
- data/vendor/kreuzberg/tests/concurrency_stress.rs +509 -0
- data/vendor/kreuzberg/tests/config_features.rs +580 -0
- data/vendor/kreuzberg/tests/config_loading_tests.rs +439 -0
- data/vendor/kreuzberg/tests/core_integration.rs +493 -0
- data/vendor/kreuzberg/tests/csv_integration.rs +424 -0
- data/vendor/kreuzberg/tests/docx_metadata_extraction_test.rs +124 -0
- data/vendor/kreuzberg/tests/email_integration.rs +325 -0
- data/vendor/kreuzberg/tests/error_handling.rs +393 -0
- data/vendor/kreuzberg/tests/format_integration.rs +159 -0
- data/vendor/kreuzberg/tests/helpers/mod.rs +142 -0
- data/vendor/kreuzberg/tests/image_integration.rs +253 -0
- data/vendor/kreuzberg/tests/keywords_integration.rs +479 -0
- data/vendor/kreuzberg/tests/keywords_quality.rs +509 -0
- data/vendor/kreuzberg/tests/mime_detection.rs +428 -0
- data/vendor/kreuzberg/tests/ocr_configuration.rs +510 -0
- data/vendor/kreuzberg/tests/ocr_errors.rs +676 -0
- data/vendor/kreuzberg/tests/ocr_quality.rs +627 -0
- data/vendor/kreuzberg/tests/ocr_stress.rs +469 -0
- data/vendor/kreuzberg/tests/pandoc_integration.rs +503 -0
- data/vendor/kreuzberg/tests/pdf_integration.rs +43 -0
- data/vendor/kreuzberg/tests/pipeline_integration.rs +1412 -0
- data/vendor/kreuzberg/tests/plugin_ocr_backend_test.rs +771 -0
- data/vendor/kreuzberg/tests/plugin_postprocessor_test.rs +561 -0
- data/vendor/kreuzberg/tests/plugin_system.rs +921 -0
- data/vendor/kreuzberg/tests/plugin_validator_test.rs +783 -0
- data/vendor/kreuzberg/tests/registry_integration_tests.rs +607 -0
- data/vendor/kreuzberg/tests/security_validation.rs +404 -0
- data/vendor/kreuzberg/tests/stopwords_integration_test.rs +888 -0
- data/vendor/kreuzberg/tests/test_fastembed.rs +609 -0
- data/vendor/kreuzberg/tests/xlsx_metadata_extraction_test.rs +87 -0
- metadata +471 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
use std::fmt;
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone)]
|
|
4
|
+
pub enum PdfError {
|
|
5
|
+
InvalidPdf(String),
|
|
6
|
+
PasswordRequired,
|
|
7
|
+
InvalidPassword,
|
|
8
|
+
EncryptionNotSupported(String),
|
|
9
|
+
PageNotFound(usize),
|
|
10
|
+
TextExtractionFailed(String),
|
|
11
|
+
RenderingFailed(String),
|
|
12
|
+
MetadataExtractionFailed(String),
|
|
13
|
+
IOError(String),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl fmt::Display for PdfError {
|
|
17
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
18
|
+
match self {
|
|
19
|
+
PdfError::InvalidPdf(msg) => write!(f, "Invalid PDF: {}", msg),
|
|
20
|
+
PdfError::PasswordRequired => write!(f, "PDF is password-protected"),
|
|
21
|
+
PdfError::InvalidPassword => write!(f, "Invalid password provided"),
|
|
22
|
+
PdfError::EncryptionNotSupported(msg) => {
|
|
23
|
+
write!(f, "Encryption not supported: {}", msg)
|
|
24
|
+
}
|
|
25
|
+
PdfError::PageNotFound(page) => write!(f, "Page {} not found", page),
|
|
26
|
+
PdfError::TextExtractionFailed(msg) => write!(f, "Text extraction failed: {}", msg),
|
|
27
|
+
PdfError::RenderingFailed(msg) => write!(f, "Page rendering failed: {}", msg),
|
|
28
|
+
PdfError::MetadataExtractionFailed(msg) => {
|
|
29
|
+
write!(f, "Metadata extraction failed: {}", msg)
|
|
30
|
+
}
|
|
31
|
+
PdfError::IOError(msg) => write!(f, "I/O error: {}", msg),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl std::error::Error for PdfError {}
|
|
37
|
+
|
|
38
|
+
// NOTE: No From<std::io::Error> impl - IO errors must bubble up unchanged per error handling policy
|
|
39
|
+
|
|
40
|
+
impl From<lopdf::Error> for PdfError {
|
|
41
|
+
fn from(err: lopdf::Error) -> Self {
|
|
42
|
+
match err {
|
|
43
|
+
lopdf::Error::IO(_) => panic!("lopdf IO errors should not be converted to PdfError - let them bubble up"),
|
|
44
|
+
_ => PdfError::InvalidPdf(err.to_string()),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub type Result<T> = std::result::Result<T, PdfError>;
|
|
50
|
+
|
|
51
|
+
#[cfg(test)]
|
|
52
|
+
mod tests {
|
|
53
|
+
use super::*;
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn test_invalid_pdf_error() {
|
|
57
|
+
let err = PdfError::InvalidPdf("corrupted header".to_string());
|
|
58
|
+
assert_eq!(err.to_string(), "Invalid PDF: corrupted header");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[test]
|
|
62
|
+
fn test_password_required_error() {
|
|
63
|
+
let err = PdfError::PasswordRequired;
|
|
64
|
+
assert_eq!(err.to_string(), "PDF is password-protected");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[test]
|
|
68
|
+
fn test_invalid_password_error() {
|
|
69
|
+
let err = PdfError::InvalidPassword;
|
|
70
|
+
assert_eq!(err.to_string(), "Invalid password provided");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn test_encryption_not_supported_error() {
|
|
75
|
+
let err = PdfError::EncryptionNotSupported("AES-256".to_string());
|
|
76
|
+
assert_eq!(err.to_string(), "Encryption not supported: AES-256");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[test]
|
|
80
|
+
fn test_page_not_found_error() {
|
|
81
|
+
let err = PdfError::PageNotFound(5);
|
|
82
|
+
assert_eq!(err.to_string(), "Page 5 not found");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn test_text_extraction_failed_error() {
|
|
87
|
+
let err = PdfError::TextExtractionFailed("no text layer".to_string());
|
|
88
|
+
assert_eq!(err.to_string(), "Text extraction failed: no text layer");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn test_rendering_failed_error() {
|
|
93
|
+
let err = PdfError::RenderingFailed("out of memory".to_string());
|
|
94
|
+
assert_eq!(err.to_string(), "Page rendering failed: out of memory");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn test_metadata_extraction_failed_error() {
|
|
99
|
+
let err = PdfError::MetadataExtractionFailed("invalid metadata".to_string());
|
|
100
|
+
assert_eq!(err.to_string(), "Metadata extraction failed: invalid metadata");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn test_io_error() {
|
|
105
|
+
let err = PdfError::IOError("read failed".to_string());
|
|
106
|
+
assert_eq!(err.to_string(), "I/O error: read failed");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[test]
|
|
110
|
+
fn test_error_debug() {
|
|
111
|
+
let err = PdfError::InvalidPassword;
|
|
112
|
+
let debug_str = format!("{:?}", err);
|
|
113
|
+
assert!(debug_str.contains("InvalidPassword"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn test_error_clone() {
|
|
118
|
+
let err1 = PdfError::PageNotFound(3);
|
|
119
|
+
let err2 = err1.clone();
|
|
120
|
+
assert_eq!(err1.to_string(), err2.to_string());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
use super::error::{PdfError, Result};
|
|
2
|
+
use lopdf::Document;
|
|
3
|
+
use serde::{Deserialize, Serialize};
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
6
|
+
pub struct PdfImage {
|
|
7
|
+
pub page_number: usize,
|
|
8
|
+
pub image_index: usize,
|
|
9
|
+
pub width: i64,
|
|
10
|
+
pub height: i64,
|
|
11
|
+
pub color_space: Option<String>,
|
|
12
|
+
pub bits_per_component: Option<i64>,
|
|
13
|
+
pub filters: Vec<String>,
|
|
14
|
+
pub data: Vec<u8>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[derive(Debug)]
|
|
18
|
+
pub struct PdfImageExtractor {
|
|
19
|
+
document: Document,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl PdfImageExtractor {
|
|
23
|
+
pub fn new(pdf_bytes: &[u8]) -> Result<Self> {
|
|
24
|
+
Self::new_with_password(pdf_bytes, None)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn new_with_password(pdf_bytes: &[u8], password: Option<&str>) -> Result<Self> {
|
|
28
|
+
let mut doc =
|
|
29
|
+
Document::load_mem(pdf_bytes).map_err(|e| PdfError::InvalidPdf(format!("Failed to load PDF: {}", e)))?;
|
|
30
|
+
|
|
31
|
+
if doc.is_encrypted() {
|
|
32
|
+
if let Some(pwd) = password {
|
|
33
|
+
doc.decrypt(pwd).map_err(|_| PdfError::InvalidPassword)?;
|
|
34
|
+
} else {
|
|
35
|
+
return Err(PdfError::PasswordRequired);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Ok(Self { document: doc })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn extract_images(&self) -> Result<Vec<PdfImage>> {
|
|
43
|
+
let mut all_images = Vec::new();
|
|
44
|
+
let pages = self.document.get_pages();
|
|
45
|
+
|
|
46
|
+
for (page_num, page_id) in pages.iter() {
|
|
47
|
+
let images = self
|
|
48
|
+
.document
|
|
49
|
+
.get_page_images(*page_id)
|
|
50
|
+
.map_err(|e| PdfError::MetadataExtractionFailed(format!("Failed to get page images: {}", e)))?;
|
|
51
|
+
|
|
52
|
+
for (img_index, img) in images.iter().enumerate() {
|
|
53
|
+
let filters = img.filters.clone().unwrap_or_default();
|
|
54
|
+
|
|
55
|
+
all_images.push(PdfImage {
|
|
56
|
+
page_number: *page_num as usize,
|
|
57
|
+
image_index: img_index + 1,
|
|
58
|
+
width: img.width,
|
|
59
|
+
height: img.height,
|
|
60
|
+
color_space: img.color_space.clone(),
|
|
61
|
+
bits_per_component: img.bits_per_component,
|
|
62
|
+
filters,
|
|
63
|
+
data: img.content.to_vec(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Ok(all_images)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn extract_images_from_page(&self, page_number: u32) -> Result<Vec<PdfImage>> {
|
|
72
|
+
let pages = self.document.get_pages();
|
|
73
|
+
let page_id = pages
|
|
74
|
+
.get(&page_number)
|
|
75
|
+
.ok_or(PdfError::PageNotFound(page_number as usize))?;
|
|
76
|
+
|
|
77
|
+
let images = self
|
|
78
|
+
.document
|
|
79
|
+
.get_page_images(*page_id)
|
|
80
|
+
.map_err(|e| PdfError::MetadataExtractionFailed(format!("Failed to get page images: {}", e)))?;
|
|
81
|
+
|
|
82
|
+
let mut page_images = Vec::new();
|
|
83
|
+
for (img_index, img) in images.iter().enumerate() {
|
|
84
|
+
let filters = img.filters.clone().unwrap_or_default();
|
|
85
|
+
|
|
86
|
+
page_images.push(PdfImage {
|
|
87
|
+
page_number: page_number as usize,
|
|
88
|
+
image_index: img_index + 1,
|
|
89
|
+
width: img.width,
|
|
90
|
+
height: img.height,
|
|
91
|
+
color_space: img.color_space.clone(),
|
|
92
|
+
bits_per_component: img.bits_per_component,
|
|
93
|
+
filters,
|
|
94
|
+
data: img.content.to_vec(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Ok(page_images)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pub fn get_image_count(&self) -> Result<usize> {
|
|
102
|
+
let images = self.extract_images()?;
|
|
103
|
+
Ok(images.len())
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pub fn extract_images_from_pdf(pdf_bytes: &[u8]) -> Result<Vec<PdfImage>> {
|
|
108
|
+
let extractor = PdfImageExtractor::new(pdf_bytes)?;
|
|
109
|
+
extractor.extract_images()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub fn extract_images_from_pdf_with_password(pdf_bytes: &[u8], password: &str) -> Result<Vec<PdfImage>> {
|
|
113
|
+
let extractor = PdfImageExtractor::new_with_password(pdf_bytes, Some(password))?;
|
|
114
|
+
extractor.extract_images()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[cfg(test)]
|
|
118
|
+
mod tests {
|
|
119
|
+
use super::*;
|
|
120
|
+
|
|
121
|
+
#[test]
|
|
122
|
+
fn test_extractor_creation() {
|
|
123
|
+
let result = PdfImageExtractor::new(b"not a pdf");
|
|
124
|
+
assert!(result.is_err());
|
|
125
|
+
assert!(matches!(result.unwrap_err(), PdfError::InvalidPdf(_)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[test]
|
|
129
|
+
fn test_extract_images_invalid_pdf() {
|
|
130
|
+
let result = extract_images_from_pdf(b"not a pdf");
|
|
131
|
+
assert!(result.is_err());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn test_extract_images_empty_pdf() {
|
|
136
|
+
let result = extract_images_from_pdf(b"");
|
|
137
|
+
assert!(result.is_err());
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
use super::error::{PdfError, Result};
|
|
2
|
+
use pdfium_render::prelude::*;
|
|
3
|
+
use serde::{Deserialize, Serialize};
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
6
|
+
pub struct PdfMetadata {
|
|
7
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
8
|
+
pub title: Option<String>,
|
|
9
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
10
|
+
pub subject: Option<String>,
|
|
11
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
12
|
+
pub authors: Option<Vec<String>>,
|
|
13
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
14
|
+
pub keywords: Option<Vec<String>>,
|
|
15
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
16
|
+
pub created_at: Option<String>,
|
|
17
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
18
|
+
pub modified_at: Option<String>,
|
|
19
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
20
|
+
pub created_by: Option<String>,
|
|
21
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
22
|
+
pub producer: Option<String>,
|
|
23
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
24
|
+
pub page_count: Option<usize>,
|
|
25
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
26
|
+
pub pdf_version: Option<String>,
|
|
27
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
28
|
+
pub is_encrypted: Option<bool>,
|
|
29
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
30
|
+
pub width: Option<i64>,
|
|
31
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
32
|
+
pub height: Option<i64>,
|
|
33
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
34
|
+
pub summary: Option<String>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn extract_metadata(pdf_bytes: &[u8]) -> Result<PdfMetadata> {
|
|
38
|
+
extract_metadata_with_password(pdf_bytes, None)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub fn extract_metadata_with_password(pdf_bytes: &[u8], password: Option<&str>) -> Result<PdfMetadata> {
|
|
42
|
+
let bindings = Pdfium::bind_to_library(Pdfium::pdfium_platform_library_name_at_path("./"))
|
|
43
|
+
.or_else(|_| Pdfium::bind_to_system_library())
|
|
44
|
+
.map_err(|e| PdfError::MetadataExtractionFailed(format!("Failed to initialize Pdfium: {}", e)))?;
|
|
45
|
+
|
|
46
|
+
let pdfium = Pdfium::new(bindings);
|
|
47
|
+
|
|
48
|
+
let document = pdfium.load_pdf_from_byte_slice(pdf_bytes, password).map_err(|e| {
|
|
49
|
+
let err_msg = e.to_string();
|
|
50
|
+
if (err_msg.contains("password") || err_msg.contains("Password")) && password.is_some() {
|
|
51
|
+
PdfError::InvalidPassword
|
|
52
|
+
} else if err_msg.contains("password") || err_msg.contains("Password") {
|
|
53
|
+
PdfError::PasswordRequired
|
|
54
|
+
} else {
|
|
55
|
+
PdfError::MetadataExtractionFailed(err_msg)
|
|
56
|
+
}
|
|
57
|
+
})?;
|
|
58
|
+
|
|
59
|
+
extract_metadata_from_document(&document)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn extract_metadata_with_passwords(pdf_bytes: &[u8], passwords: &[&str]) -> Result<PdfMetadata> {
|
|
63
|
+
let mut last_error = None;
|
|
64
|
+
|
|
65
|
+
for password in passwords {
|
|
66
|
+
match extract_metadata_with_password(pdf_bytes, Some(password)) {
|
|
67
|
+
Ok(metadata) => return Ok(metadata),
|
|
68
|
+
Err(err) => {
|
|
69
|
+
last_error = Some(err);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if let Some(err) = last_error {
|
|
76
|
+
return Err(err);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
extract_metadata(pdf_bytes)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub(crate) fn extract_metadata_from_document(document: &PdfDocument<'_>) -> Result<PdfMetadata> {
|
|
83
|
+
let pdf_metadata = document.metadata();
|
|
84
|
+
|
|
85
|
+
let mut metadata = PdfMetadata {
|
|
86
|
+
pdf_version: format_pdf_version(document.version()),
|
|
87
|
+
..Default::default()
|
|
88
|
+
};
|
|
89
|
+
metadata.page_count = Some(document.pages().len() as usize);
|
|
90
|
+
metadata.is_encrypted = document
|
|
91
|
+
.permissions()
|
|
92
|
+
.security_handler_revision()
|
|
93
|
+
.ok()
|
|
94
|
+
.map(|revision| revision != PdfSecurityHandlerRevision::Unprotected);
|
|
95
|
+
|
|
96
|
+
metadata.title = pdf_metadata
|
|
97
|
+
.get(PdfDocumentMetadataTagType::Title)
|
|
98
|
+
.map(|tag| tag.value().to_string());
|
|
99
|
+
|
|
100
|
+
metadata.subject = pdf_metadata
|
|
101
|
+
.get(PdfDocumentMetadataTagType::Subject)
|
|
102
|
+
.map(|tag| tag.value().to_string());
|
|
103
|
+
|
|
104
|
+
if let Some(author_tag) = pdf_metadata.get(PdfDocumentMetadataTagType::Author) {
|
|
105
|
+
let authors = parse_authors(author_tag.value());
|
|
106
|
+
if !authors.is_empty() {
|
|
107
|
+
metadata.authors = Some(authors);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if let Some(keywords_tag) = pdf_metadata.get(PdfDocumentMetadataTagType::Keywords) {
|
|
112
|
+
let keywords = parse_keywords(keywords_tag.value());
|
|
113
|
+
if !keywords.is_empty() {
|
|
114
|
+
metadata.keywords = Some(keywords);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if let Some(created_tag) = pdf_metadata.get(PdfDocumentMetadataTagType::CreationDate) {
|
|
119
|
+
metadata.created_at = Some(parse_pdf_date(created_tag.value()));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if let Some(modified_tag) = pdf_metadata.get(PdfDocumentMetadataTagType::ModificationDate) {
|
|
123
|
+
metadata.modified_at = Some(parse_pdf_date(modified_tag.value()));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
metadata.created_by = pdf_metadata
|
|
127
|
+
.get(PdfDocumentMetadataTagType::Creator)
|
|
128
|
+
.map(|tag| tag.value().to_string());
|
|
129
|
+
|
|
130
|
+
metadata.producer = pdf_metadata
|
|
131
|
+
.get(PdfDocumentMetadataTagType::Producer)
|
|
132
|
+
.map(|tag| tag.value().to_string());
|
|
133
|
+
|
|
134
|
+
if !document.pages().is_empty()
|
|
135
|
+
&& let Ok(page_rect) = document.pages().page_size(0)
|
|
136
|
+
{
|
|
137
|
+
metadata.width = Some(page_rect.width().value.round() as i64);
|
|
138
|
+
metadata.height = Some(page_rect.height().value.round() as i64);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if metadata.summary.is_none() {
|
|
142
|
+
metadata.summary = Some(generate_summary(&metadata));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Ok(metadata)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fn parse_authors(author_str: &str) -> Vec<String> {
|
|
149
|
+
let author_str = author_str.replace(" and ", ", ");
|
|
150
|
+
let mut authors = Vec::new();
|
|
151
|
+
|
|
152
|
+
for segment in author_str.split(';') {
|
|
153
|
+
for author in segment.split(',') {
|
|
154
|
+
let trimmed = author.trim();
|
|
155
|
+
if !trimmed.is_empty() {
|
|
156
|
+
authors.push(trimmed.to_string());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
authors
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn parse_keywords(keywords_str: &str) -> Vec<String> {
|
|
165
|
+
keywords_str
|
|
166
|
+
.replace(';', ",")
|
|
167
|
+
.split(',')
|
|
168
|
+
.filter_map(|k| {
|
|
169
|
+
let trimmed = k.trim();
|
|
170
|
+
if trimmed.is_empty() {
|
|
171
|
+
None
|
|
172
|
+
} else {
|
|
173
|
+
Some(trimmed.to_string())
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.collect()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn parse_pdf_date(date_str: &str) -> String {
|
|
180
|
+
let cleaned = date_str.trim();
|
|
181
|
+
|
|
182
|
+
if cleaned.starts_with("D:") && cleaned.len() >= 10 {
|
|
183
|
+
let year = &cleaned[2..6];
|
|
184
|
+
let month = &cleaned[6..8];
|
|
185
|
+
let day = &cleaned[8..10];
|
|
186
|
+
|
|
187
|
+
if cleaned.len() >= 16 {
|
|
188
|
+
let hour = &cleaned[10..12];
|
|
189
|
+
let minute = &cleaned[12..14];
|
|
190
|
+
let second = &cleaned[14..16];
|
|
191
|
+
format!("{}-{}-{}T{}:{}:{}Z", year, month, day, hour, minute, second)
|
|
192
|
+
} else if cleaned.len() >= 14 {
|
|
193
|
+
let hour = &cleaned[10..12];
|
|
194
|
+
let minute = &cleaned[12..14];
|
|
195
|
+
format!("{}-{}-{}T{}:{}:00Z", year, month, day, hour, minute)
|
|
196
|
+
} else {
|
|
197
|
+
format!("{}-{}-{}T00:00:00Z", year, month, day)
|
|
198
|
+
}
|
|
199
|
+
} else if cleaned.len() >= 8 {
|
|
200
|
+
let year = &cleaned[0..4];
|
|
201
|
+
let month = &cleaned[4..6];
|
|
202
|
+
let day = &cleaned[6..8];
|
|
203
|
+
format!("{}-{}-{}T00:00:00Z", year, month, day)
|
|
204
|
+
} else {
|
|
205
|
+
date_str.to_string()
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn generate_summary(metadata: &PdfMetadata) -> String {
|
|
210
|
+
let mut parts = Vec::new();
|
|
211
|
+
|
|
212
|
+
if let Some(page_count) = metadata.page_count {
|
|
213
|
+
let plural = if page_count != 1 { "s" } else { "" };
|
|
214
|
+
parts.push(format!("PDF document with {} page{}.", page_count, plural));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if let Some(ref version) = metadata.pdf_version {
|
|
218
|
+
parts.push(format!("PDF version {}.", version));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if metadata.is_encrypted == Some(true) {
|
|
222
|
+
parts.push("Document is encrypted.".to_string());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
parts.join(" ")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn format_pdf_version(version: PdfDocumentVersion) -> Option<String> {
|
|
229
|
+
match version {
|
|
230
|
+
PdfDocumentVersion::Unset => None,
|
|
231
|
+
PdfDocumentVersion::Pdf1_0 => Some("1.0".to_string()),
|
|
232
|
+
PdfDocumentVersion::Pdf1_1 => Some("1.1".to_string()),
|
|
233
|
+
PdfDocumentVersion::Pdf1_2 => Some("1.2".to_string()),
|
|
234
|
+
PdfDocumentVersion::Pdf1_3 => Some("1.3".to_string()),
|
|
235
|
+
PdfDocumentVersion::Pdf1_4 => Some("1.4".to_string()),
|
|
236
|
+
PdfDocumentVersion::Pdf1_5 => Some("1.5".to_string()),
|
|
237
|
+
PdfDocumentVersion::Pdf1_6 => Some("1.6".to_string()),
|
|
238
|
+
PdfDocumentVersion::Pdf1_7 => Some("1.7".to_string()),
|
|
239
|
+
PdfDocumentVersion::Pdf2_0 => Some("2.0".to_string()),
|
|
240
|
+
PdfDocumentVersion::Other(value) => {
|
|
241
|
+
if value >= 10 {
|
|
242
|
+
Some(format!("{}.{}", value / 10, value % 10))
|
|
243
|
+
} else {
|
|
244
|
+
Some(value.to_string())
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[cfg(test)]
|
|
251
|
+
mod tests {
|
|
252
|
+
use super::*;
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn test_parse_authors_single() {
|
|
256
|
+
let authors = parse_authors("John Doe");
|
|
257
|
+
assert_eq!(authors, vec!["John Doe"]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn test_parse_authors_multiple_comma() {
|
|
262
|
+
let authors = parse_authors("John Doe, Jane Smith");
|
|
263
|
+
assert_eq!(authors, vec!["John Doe", "Jane Smith"]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#[test]
|
|
267
|
+
fn test_parse_authors_multiple_and() {
|
|
268
|
+
let authors = parse_authors("John Doe and Jane Smith");
|
|
269
|
+
assert_eq!(authors, vec!["John Doe", "Jane Smith"]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[test]
|
|
273
|
+
fn test_parse_authors_semicolon() {
|
|
274
|
+
let authors = parse_authors("John Doe;Jane Smith");
|
|
275
|
+
assert_eq!(authors, vec!["John Doe", "Jane Smith"]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[test]
|
|
279
|
+
fn test_parse_keywords() {
|
|
280
|
+
let keywords = parse_keywords("pdf, document, test");
|
|
281
|
+
assert_eq!(keywords, vec!["pdf", "document", "test"]);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[test]
|
|
285
|
+
fn test_parse_keywords_semicolon() {
|
|
286
|
+
let keywords = parse_keywords("pdf;document;test");
|
|
287
|
+
assert_eq!(keywords, vec!["pdf", "document", "test"]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#[test]
|
|
291
|
+
fn test_parse_keywords_empty() {
|
|
292
|
+
let keywords = parse_keywords("");
|
|
293
|
+
assert!(keywords.is_empty());
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn test_parse_pdf_date_full() {
|
|
298
|
+
let date = parse_pdf_date("D:20230115123045");
|
|
299
|
+
assert_eq!(date, "2023-01-15T12:30:45Z");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn test_parse_pdf_date_no_time() {
|
|
304
|
+
let date = parse_pdf_date("D:20230115");
|
|
305
|
+
assert_eq!(date, "2023-01-15T00:00:00Z");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[test]
|
|
309
|
+
fn test_parse_pdf_date_no_prefix() {
|
|
310
|
+
let date = parse_pdf_date("20230115");
|
|
311
|
+
assert_eq!(date, "2023-01-15T00:00:00Z");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[test]
|
|
315
|
+
fn test_generate_summary() {
|
|
316
|
+
let metadata = PdfMetadata {
|
|
317
|
+
page_count: Some(10),
|
|
318
|
+
pdf_version: Some("1.7".to_string()),
|
|
319
|
+
is_encrypted: Some(false),
|
|
320
|
+
..Default::default()
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
let summary = generate_summary(&metadata);
|
|
324
|
+
assert!(summary.contains("10 pages"));
|
|
325
|
+
assert!(summary.contains("1.7"));
|
|
326
|
+
assert!(!summary.contains("encrypted"));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
fn test_generate_summary_single_page() {
|
|
331
|
+
let metadata = PdfMetadata {
|
|
332
|
+
page_count: Some(1),
|
|
333
|
+
..Default::default()
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
let summary = generate_summary(&metadata);
|
|
337
|
+
assert!(summary.contains("1 page."));
|
|
338
|
+
assert!(!summary.contains("pages"));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#[test]
|
|
342
|
+
fn test_extract_metadata_invalid_pdf() {
|
|
343
|
+
let result = extract_metadata(b"not a pdf");
|
|
344
|
+
assert!(result.is_err());
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//! PDF document processing utilities.
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides low-level PDF processing functions for text extraction,
|
|
4
|
+
//! metadata parsing, image extraction, and page rendering. Used internally by
|
|
5
|
+
//! the PDF extractor plugin.
|
|
6
|
+
//!
|
|
7
|
+
//! # Features
|
|
8
|
+
//!
|
|
9
|
+
//! - **Text extraction**: Extract text content from PDFs using `pdfium-render`
|
|
10
|
+
//! - **Metadata extraction**: Parse PDF metadata (title, author, creation date, etc.)
|
|
11
|
+
//! - **Image extraction**: Extract embedded images from PDF pages
|
|
12
|
+
//! - **Page rendering**: Render PDF pages to images for OCR processing
|
|
13
|
+
//! - **Error handling**: Comprehensive PDF-specific error types
|
|
14
|
+
//!
|
|
15
|
+
//! # Example
|
|
16
|
+
//!
|
|
17
|
+
//! ```rust
|
|
18
|
+
//! use kreuzberg::pdf::{extract_text_from_pdf, extract_metadata};
|
|
19
|
+
//!
|
|
20
|
+
//! # fn example() -> kreuzberg::Result<()> {
|
|
21
|
+
//! let pdf_bytes = std::fs::read("document.pdf")?;
|
|
22
|
+
//!
|
|
23
|
+
//! // Extract text
|
|
24
|
+
//! let text = extract_text_from_pdf(&pdf_bytes)?;
|
|
25
|
+
//! println!("Text: {}", text);
|
|
26
|
+
//!
|
|
27
|
+
//! // Extract metadata
|
|
28
|
+
//! let metadata = extract_metadata(&pdf_bytes)?;
|
|
29
|
+
//! println!("Page count: {:?}", metadata.page_count);
|
|
30
|
+
//! # Ok(())
|
|
31
|
+
//! # }
|
|
32
|
+
//! ```
|
|
33
|
+
//!
|
|
34
|
+
//! # Note
|
|
35
|
+
//!
|
|
36
|
+
//! This module is always available. The `ocr` feature enables additional
|
|
37
|
+
//! functionality in the PDF extractor for rendering pages to images.
|
|
38
|
+
pub mod error;
|
|
39
|
+
pub mod images;
|
|
40
|
+
pub mod metadata;
|
|
41
|
+
pub mod rendering;
|
|
42
|
+
pub mod table;
|
|
43
|
+
pub mod text;
|
|
44
|
+
|
|
45
|
+
pub use error::PdfError;
|
|
46
|
+
pub use images::{PdfImage, PdfImageExtractor, extract_images_from_pdf};
|
|
47
|
+
pub use metadata::extract_metadata;
|
|
48
|
+
pub use rendering::{PageRenderOptions, render_page_to_image};
|
|
49
|
+
pub use table::extract_words_from_page;
|
|
50
|
+
pub use text::extract_text_from_pdf;
|