use serde::{Deserialize, Serialize};
use crate::{
error::{RapidOcrError, Result},
ocr::pipeline::config::EngineConfig,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct TableOcrResolution {
pub limit_side_len: usize,
pub limit_type: String,
pub max_side_len: usize,
}
impl Default for TableOcrResolution {
fn default() -> Self {
Self {
limit_side_len: 736,
limit_type: "max".to_string(),
max_side_len: 736,
}
}
}
impl TableOcrResolution {
pub fn apply(&self, config: &mut EngineConfig) -> Result<()> {
if self.limit_side_len == 0 {
return Err(RapidOcrError::Config(
"table OCR resolution: limit_side_len must be greater than zero; a zero here \
would make every det input collapse to the 32-pixel floor"
.to_string(),
));
}
if self.max_side_len == 0 {
return Err(RapidOcrError::Config(
"table OCR resolution: max_side_len must be greater than zero".to_string(),
));
}
match self.limit_type.as_str() {
"min" | "max" => {}
other => {
return Err(RapidOcrError::Config(format!(
"table OCR resolution: limit_type must be \"min\" or \"max\", got {other:?}; \
src/ocr/det/preprocess.rs treats anything other than \"max\" as the min \
branch, so an unknown value would silently behave like \"min\""
)));
}
}
config.global.max_side_len = self.max_side_len;
config.det.limit_side_len = self.limit_side_len;
config.det.limit_type = self.limit_type.clone();
Ok(())
}
pub fn describe(&self) -> String {
match self.limit_type.as_str() {
"min" => format!(
"table OCR resolution: limit_type=min limit_side_len={} max_side_len={} \
(enlarges until the short side reaches limit_side_len; never shrinks)",
self.limit_side_len, self.max_side_len
),
_ => format!(
"table OCR resolution: limit_type=max max_side_len={} (never enlarges; the live \
long-side cap is max_side_len; limit_side_len={} is ignored under limit_type=max)",
self.max_side_len, self.limit_side_len
),
}
}
}
#[cfg(test)]
mod tests {
use super::TableOcrResolution;
use crate::ocr::pipeline::config::EngineConfig;
#[test]
fn the_default_keeps_the_official_number_but_never_upscales() {
let resolution = TableOcrResolution::default();
assert_eq!(resolution.limit_side_len, 736);
assert_eq!(resolution.limit_type, "max");
assert_eq!(resolution.max_side_len, 736);
}
#[test]
fn applying_overwrites_instead_of_filling_in_gaps() {
let mut config = EngineConfig::default();
config.global.max_side_len = 2000;
config.det.limit_side_len = 960;
config.det.limit_type = "max".to_string();
TableOcrResolution {
limit_side_len: 736,
limit_type: "min".to_string(),
max_side_len: 1280,
}
.apply(&mut config)
.expect("the default resolution is valid");
assert_eq!(config.global.max_side_len, 1280);
assert_eq!(config.det.limit_side_len, 736);
assert_eq!(config.det.limit_type, "min");
}
#[test]
fn an_unknown_limit_type_is_refused_rather_than_treated_as_min() {
let error = TableOcrResolution {
limit_type: "smallest".to_string(),
..TableOcrResolution::default()
}
.apply(&mut EngineConfig::default())
.expect_err("an unknown limit_type must not be accepted");
let text = error.to_string();
assert!(text.contains("smallest"), "{text}");
assert!(text.contains("\"min\" or \"max\""), "{text}");
}
#[test]
fn a_zero_side_len_is_refused() {
let error = TableOcrResolution {
limit_side_len: 0,
..TableOcrResolution::default()
}
.apply(&mut EngineConfig::default())
.expect_err("limit_side_len = 0 must not be accepted");
assert!(error.to_string().contains("limit_side_len"), "{error}");
}
#[test]
fn the_description_names_all_three_knobs() {
let described = TableOcrResolution::default().describe();
assert!(described.contains("limit_type=max"), "{described}");
assert!(described.contains("max_side_len=736"), "{described}");
assert!(described.contains("limit_side_len=736"), "{described}");
assert!(
described.contains("ignored under limit_type=max"),
"{described}"
);
}
#[test]
fn the_min_description_says_it_enlarges_rather_than_ignoring() {
let described = TableOcrResolution {
limit_side_len: 320,
limit_type: "min".to_string(),
max_side_len: 2000,
}
.describe();
assert!(described.contains("limit_type=min"), "{described}");
assert!(described.contains("limit_side_len=320"), "{described}");
assert!(described.contains("max_side_len=2000"), "{described}");
assert!(described.contains("never shrinks"), "{described}");
assert!(!described.contains("ignored"), "{described}");
}
#[test]
fn the_limit_side_len_is_ignored_under_the_max_policy() {
use crate::ocr::det::detector::resolve_limit_side_len_like_python;
for configured in [320usize, 736, 960, 1280, 2000] {
assert_eq!(
resolve_limit_side_len_like_python("max", configured, 800),
960,
"limit_type=max must ignore the configured {configured}"
);
assert_eq!(
resolve_limit_side_len_like_python("max", configured, 1200),
1500
);
assert_eq!(
resolve_limit_side_len_like_python("max", configured, 1800),
2000
);
}
assert_eq!(resolve_limit_side_len_like_python("min", 320, 1800), 320);
assert_eq!(resolve_limit_side_len_like_python("min", 2000, 800), 2000);
}
#[test]
fn the_default_makes_the_detector_side_resize_a_no_op() {
use crate::ocr::det::detector::resolve_limit_side_len_like_python;
let default = TableOcrResolution::default();
let resolved = resolve_limit_side_len_like_python(
&default.limit_type,
default.limit_side_len,
default.max_side_len,
);
assert_eq!(resolved, 960);
assert!(
resolved >= default.max_side_len,
"the bucket {resolved} must not shrink below the pre-resize cap {}",
default.max_side_len
);
}
#[test]
fn the_default_puts_a_hard_bound_on_the_detector_input_long_side() {
use crate::config::RecImage;
use crate::ocr::pipeline::image_ops::resize_image_within_bounds;
let default = TableOcrResolution::default();
let mut config = EngineConfig::default();
default.apply(&mut config).expect("apply must succeed");
let width = 4_129;
let height = 700;
let image =
RecImage::from_bgr_u8(width, height, vec![0_u8; width * height * 3]).expect("image");
let (bounded, _, _) = resize_image_within_bounds(
image,
config.global.min_side_len,
config.global.max_side_len,
)
.expect("resize must succeed");
let long_side = bounded.width().max(bounded.height());
assert!(
long_side <= default.max_side_len,
"the applied max_side_len ({}) must bound the long side, got {long_side}",
config.global.max_side_len
);
assert!(
long_side < EngineConfig::default().global.max_side_len,
"the test image must be larger than the engine default, otherwise it proves nothing"
);
}
#[test]
fn the_default_uses_one_number_for_both_side_limits() {
let default = TableOcrResolution::default();
assert_eq!(
default.max_side_len, default.limit_side_len,
"the default deliberately states the table-path scale once, as the same number in \
both fields; if they diverge, the module docs' claim that the number is still the \
official 736 becomes ambiguous"
);
}
}