Skip to content

Commit 440b87d

Browse files
committed
chore: cleanup
1 parent 63221c5 commit 440b87d

3 files changed

Lines changed: 136 additions & 22 deletions

File tree

crates/kreuzberg-py/src/config/types.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,67 @@ impl ExtractionConfig {
472472
self.inner.max_archive_depth = value;
473473
}
474474

475+
/// Get the security limits for archive extraction.
476+
///
477+
/// Returns:
478+
/// Optional dict with keys: max_archive_size, max_compression_ratio,
479+
/// max_files_in_archive, max_nesting_depth, max_entity_length,
480+
/// max_content_size, max_iterations
481+
#[getter]
482+
fn security_limits(&self, py: Python<'_>) -> PyResult<Option<Py<PyDict>>> {
483+
match &self.inner.security_limits {
484+
Some(limits) => {
485+
let dict = PyDict::new(py);
486+
dict.set_item("max_archive_size", limits.max_archive_size)?;
487+
dict.set_item("max_compression_ratio", limits.max_compression_ratio)?;
488+
dict.set_item("max_files_in_archive", limits.max_files_in_archive)?;
489+
dict.set_item("max_nesting_depth", limits.max_nesting_depth)?;
490+
dict.set_item("max_entity_length", limits.max_entity_length)?;
491+
dict.set_item("max_content_size", limits.max_content_size)?;
492+
dict.set_item("max_iterations", limits.max_iterations)?;
493+
Ok(Some(dict.unbind()))
494+
}
495+
None => Ok(None),
496+
}
497+
}
498+
499+
/// Set the security limits for archive extraction.
500+
///
501+
/// Args:
502+
/// value: Optional dict with security limit keys, or None to use defaults.
503+
#[setter]
504+
fn set_security_limits(&mut self, value: Option<Bound<'_, PyDict>>) -> PyResult<()> {
505+
self.inner.security_limits = match value {
506+
Some(dict) => {
507+
let mut limits = kreuzberg::extractors::security::SecurityLimits::default();
508+
if let Some(v) = dict.get_item("max_archive_size")? {
509+
limits.max_archive_size = v.extract()?;
510+
}
511+
if let Some(v) = dict.get_item("max_compression_ratio")? {
512+
limits.max_compression_ratio = v.extract()?;
513+
}
514+
if let Some(v) = dict.get_item("max_files_in_archive")? {
515+
limits.max_files_in_archive = v.extract()?;
516+
}
517+
if let Some(v) = dict.get_item("max_nesting_depth")? {
518+
limits.max_nesting_depth = v.extract()?;
519+
}
520+
if let Some(v) = dict.get_item("max_entity_length")? {
521+
limits.max_entity_length = v.extract()?;
522+
}
523+
if let Some(v) = dict.get_item("max_content_size")? {
524+
limits.max_content_size = v.extract()?;
525+
}
526+
if let Some(v) = dict.get_item("max_iterations")? {
527+
limits.max_iterations = v.extract()?;
528+
}
529+
Some(limits)
530+
}
531+
None => None,
532+
};
533+
Ok(())
534+
}
535+
475536
fn __repr__(&self) -> String {
476537
format!(
477538
"ExtractionConfig(use_cache={}, enable_quality_processing={}, ocr={}, force_ocr={}, extraction_timeout_secs={:?}, force_ocr_pages={:?})",

crates/kreuzberg/src/extractors/image.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,28 +55,37 @@ impl ImageExtractor {
5555

5656
let ocr_result = backend.process_image(content, &ocr_config_with_format).await?;
5757

58+
// Destructure to avoid partial-move issues when propagating OCR elements.
59+
let ocr_content = ocr_result.content;
60+
let ocr_metadata = ocr_result.metadata;
61+
let ocr_elements = ocr_result.ocr_elements;
62+
5863
// Full OCR with TIFF multi-frame support (requires tiff crate)
5964
#[cfg(feature = "ocr")]
6065
{
6166
let ocr_extraction_result = crate::extraction::image::extract_text_from_image_with_ocr(
6267
content,
6368
mime_type,
64-
ocr_result.content,
69+
ocr_content,
6570
config.pages.as_ref(),
6671
)?;
6772

6873
// Build InternalDocument from OCR text
6974
let mut doc = build_image_internal_document(Some(&ocr_extraction_result.content), None);
70-
doc.metadata = ocr_result.metadata;
75+
doc.metadata = ocr_metadata;
76+
// Propagate OCR elements from the backend result into the InternalDocument
77+
// so that derive_extraction_result can populate ExtractionResult::ocr_elements.
78+
inject_ocr_elements_from_vec(&mut doc, ocr_elements);
7179
Ok(doc)
7280
}
7381

7482
// Simplified OCR path for WASM (no TIFF multi-frame support)
7583
#[cfg(not(feature = "ocr"))]
7684
{
7785
let _ = mime_type;
78-
let mut doc = build_image_internal_document(Some(&ocr_result.content), None);
79-
doc.metadata = ocr_result.metadata;
86+
let mut doc = build_image_internal_document(Some(&ocr_content), None);
87+
doc.metadata = ocr_metadata;
88+
inject_ocr_elements_from_vec(&mut doc, ocr_elements);
8089
Ok(doc)
8190
}
8291
}
@@ -253,6 +262,37 @@ impl ImageExtractor {
253262
}
254263
}
255264

265+
/// Inject OCR elements into an `InternalDocument`.
266+
///
267+
/// Converts each `OcrElement` into an `InternalElement` with `ElementKind::OcrText`
268+
/// so that the derive pipeline can reconstruct them into `ExtractionResult::ocr_elements`.
269+
fn inject_ocr_elements_from_vec(doc: &mut InternalDocument, ocr_elements: Option<Vec<crate::types::OcrElement>>) {
270+
use crate::types::document_structure::ContentLayer;
271+
use crate::types::internal::{ElementKind, InternalElement, InternalElementId};
272+
273+
if let Some(ocr_elements) = ocr_elements {
274+
for (i, elem) in ocr_elements.iter().enumerate() {
275+
let kind = ElementKind::OcrText { level: elem.level };
276+
let id = InternalElementId::generate("ocr_text", &elem.text, Some(elem.page_number as u32), i as u32);
277+
doc.elements.push(InternalElement {
278+
id,
279+
kind,
280+
text: elem.text.clone(),
281+
depth: 0,
282+
page: Some(elem.page_number as u32),
283+
bbox: None,
284+
layer: ContentLayer::Body,
285+
annotations: Vec::new(),
286+
attributes: None,
287+
anchor: None,
288+
ocr_geometry: Some(elem.geometry.clone()),
289+
ocr_confidence: Some(elem.confidence.clone()),
290+
ocr_rotation: elem.rotation.clone(),
291+
});
292+
}
293+
}
294+
}
295+
256296
/// Build a simple `InternalDocument` for an image extraction result.
257297
///
258298
/// If OCR text is available, pushes it as a paragraph. Always pushes

tools/e2e-generator/src/java.rs

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2548,33 +2548,31 @@ fn render_render_assertions_java(
25482548
/// Java types implemented as records (use field-name accessors, not getters).
25492549
///
25502550
/// Records use `fieldName()` style accessors instead of `getFieldName()`.
2551-
const JAVA_RECORD_TYPES: &[&str] = &[
2552-
"PdfAnnotation",
2553-
"Table",
2554-
"BoundingBox",
2555-
"Keyword",
2556-
"Uri",
2557-
"ArchiveEntry",
2558-
"ProcessingWarning",
2559-
];
2551+
/// NOTE: Only include types that are actual Java records. Types implemented as
2552+
/// regular classes with getX()/isX() methods must NOT be listed here.
2553+
const JAVA_RECORD_TYPES: &[&str] = &[];
25602554

25612555
/// Special field name mappings for Java (manifest field name -> Java accessor name).
25622556
fn java_field_override(type_name: &str, field_name: &str) -> Option<String> {
25632557
match (type_name, field_name) {
25642558
("ExtractionResult", "document") => Some("getDocumentStructure".to_string()),
2565-
("ExtractionConfig", "enable_quality_processing") => Some("isEnableQualityProcessing".to_string()),
25662559
_ => None,
25672560
}
25682561
}
25692562

25702563
/// Return the correct Java accessor for a field, respecting records vs classes
25712564
/// and any per-field overrides.
2572-
fn to_java_accessor(type_name: &str, field_name: &str) -> String {
2565+
///
2566+
/// When `json_type` is `"boolean"`, non-record types use the `isFieldName()`
2567+
/// convention instead of `getFieldName()`.
2568+
fn to_java_accessor_with_type(type_name: &str, field_name: &str, json_type: Option<&str>) -> String {
25732569
if let Some(override_name) = java_field_override(type_name, field_name) {
25742570
return override_name;
25752571
}
25762572
if JAVA_RECORD_TYPES.contains(&type_name) {
25772573
parity::to_camel_case(field_name)
2574+
} else if json_type == Some("boolean") {
2575+
format!("is{}", parity::to_pascal_case(field_name))
25782576
} else {
25792577
format!("get{}", parity::to_pascal_case(field_name))
25802578
}
@@ -2636,11 +2634,21 @@ pub fn generate_parity(manifest: &ParityManifest, output_root: &Utf8Path) -> Res
26362634
let required_fields: Vec<(&String, String)> = fields
26372635
.iter()
26382636
.filter(|(_, f)| f.required)
2639-
.map(|(name, _)| (name, to_java_accessor("ExtractionResult", name)))
2637+
.map(|(name, f)| {
2638+
(
2639+
name,
2640+
to_java_accessor_with_type("ExtractionResult", name, Some(&f.json_type)),
2641+
)
2642+
})
26402643
.collect();
26412644
let all_fields: Vec<(&String, String)> = fields
2642-
.keys()
2643-
.map(|name| (name, to_java_accessor("ExtractionResult", name)))
2645+
.iter()
2646+
.map(|(name, f)| {
2647+
(
2648+
name,
2649+
to_java_accessor_with_type("ExtractionResult", name, Some(&f.json_type)),
2650+
)
2651+
})
26442652
.collect();
26452653

26462654
writeln!(buffer)?;
@@ -2694,8 +2702,13 @@ pub fn generate_parity(manifest: &ParityManifest, output_root: &Utf8Path) -> Res
26942702
// ExtractionConfig parity
26952703
if let Some(fields) = parity::fields_for_type_and_lang(manifest, "ExtractionConfig", lang) {
26962704
let all_fields: Vec<(&String, String)> = fields
2697-
.keys()
2698-
.map(|name| (name, to_java_accessor("ExtractionConfig", name)))
2705+
.iter()
2706+
.map(|(name, f)| {
2707+
(
2708+
name,
2709+
to_java_accessor_with_type("ExtractionConfig", name, Some(&f.json_type)),
2710+
)
2711+
})
26992712
.collect();
27002713

27012714
writeln!(buffer)?;
@@ -2732,8 +2745,8 @@ pub fn generate_parity(manifest: &ParityManifest, output_root: &Utf8Path) -> Res
27322745
}
27332746

27342747
let all_fields: Vec<(&String, String)> = fields
2735-
.keys()
2736-
.map(|name| (name, to_java_accessor(type_name, name)))
2748+
.iter()
2749+
.map(|(name, f)| (name, to_java_accessor_with_type(type_name, name, Some(&f.json_type))))
27372750
.collect();
27382751

27392752
let method_name = format!("test{}AllGetters", type_name);

0 commit comments

Comments
 (0)