use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoutingMode {
Never,
Auto,
Surgical,
Always,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FormulaBackend {
TexTeller,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ModelPrecision {
Fp32,
Fp16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ModelQuantization {
Fp32,
Int8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingOpts {
pub use_onnx: bool,
pub routing_mode: RoutingMode,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderOpts {
pub ort_providers: Vec<String>,
pub encoder_ort_providers: Option<Vec<String>>,
#[serde(default)]
pub decoder_ort_providers: Option<Vec<String>>,
#[serde(default)]
pub layout_ort_providers: Option<Vec<String>>,
#[serde(default)]
pub ocr_ort_providers: Option<Vec<String>>,
#[serde(default)]
pub table_ort_providers: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenderOpts {
pub extract_images: bool,
pub append_unreferenced_images: bool,
pub render_dpi: u32,
pub formula_dpi: u32,
pub min_figure_area_pts: f64,
pub image_output_dir: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelOpts {
pub formula_backend: FormulaBackend,
pub model_precision: ModelPrecision,
pub model_quantization: ModelQuantization,
pub ocr_lang: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextOpts {
pub detect_headings: bool,
pub convert_html_tables: bool,
pub structured_tables: bool,
pub detect_code_blocks: bool,
pub promote_headings: bool,
pub promote_title: bool,
pub min_formula_math_chars: usize,
pub formula_inline_max_width_pts: f64,
pub formula_pad_pts: f64,
pub formula_layout_fallback: bool,
pub math_char_threshold: usize,
pub scanned_text_threshold: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConverterConfig {
#[serde(flatten)]
pub routing: RoutingOpts,
#[serde(flatten)]
pub providers: ProviderOpts,
#[serde(flatten)]
pub render: RenderOpts,
#[serde(flatten)]
pub models: ModelOpts,
#[serde(flatten)]
pub text: TextOpts,
}
impl Default for RoutingOpts {
fn default() -> Self {
Self {
use_onnx: true,
routing_mode: RoutingMode::Auto,
}
}
}
impl Default for ProviderOpts {
fn default() -> Self {
Self {
ort_providers: vec!["CPUExecutionProvider".into()],
encoder_ort_providers: None,
decoder_ort_providers: None,
layout_ort_providers: None,
ocr_ort_providers: None,
table_ort_providers: None,
}
}
}
impl Default for RenderOpts {
fn default() -> Self {
Self {
extract_images: true,
append_unreferenced_images: true,
render_dpi: 300,
formula_dpi: 200,
min_figure_area_pts: 100.0,
image_output_dir: "assets".to_string(),
}
}
}
impl Default for ModelOpts {
fn default() -> Self {
Self {
formula_backend: FormulaBackend::TexTeller,
model_precision: ModelPrecision::Fp32,
model_quantization: ModelQuantization::Int8,
ocr_lang: "en".to_string(),
}
}
}
impl Default for TextOpts {
fn default() -> Self {
Self {
detect_headings: true,
convert_html_tables: true,
structured_tables: true,
detect_code_blocks: true,
promote_headings: true,
promote_title: true,
min_formula_math_chars: 5,
formula_inline_max_width_pts: 220.0,
formula_pad_pts: 4.0,
formula_layout_fallback: false,
math_char_threshold: 30,
scanned_text_threshold: 50,
}
}
}
impl Default for ConverterConfig {
fn default() -> Self {
Self {
routing: RoutingOpts::default(),
providers: ProviderOpts::default(),
render: RenderOpts::default(),
models: ModelOpts::default(),
text: TextOpts::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_routing() {
let c = ConverterConfig::default();
assert_eq!(c.routing.routing_mode, RoutingMode::Auto);
assert_eq!(c.models.formula_backend, FormulaBackend::TexTeller);
assert_eq!(c.models.model_precision, ModelPrecision::Fp32);
}
#[test]
fn config_fields_match_python() {
let c = ConverterConfig::default();
assert_eq!(c.render.render_dpi, 300);
assert_eq!(c.render.formula_dpi, 200);
assert_eq!(c.text.formula_inline_max_width_pts, 220.0);
assert_eq!(c.text.formula_pad_pts, 4.0);
assert_eq!(c.text.min_formula_math_chars, 5);
assert_eq!(c.text.math_char_threshold, 30);
assert_eq!(c.text.scanned_text_threshold, 50);
}
#[test]
fn grouped_config_serde_stays_flat() {
let c = ConverterConfig::default();
let v = serde_json::to_value(&c).unwrap();
for key in [
"routing_mode",
"use_onnx",
"ort_providers",
"encoder_ort_providers",
"render_dpi",
"formula_dpi",
"extract_images",
"model_precision",
"ocr_lang",
"detect_headings",
"scanned_text_threshold",
] {
assert!(v.get(key).is_some(), "missing flat key {key}");
}
for group in ["routing", "providers", "render", "models", "text"] {
assert!(v.get(group).is_none(), "nested group {group} leaked");
}
let back: ConverterConfig = serde_json::from_value(v).unwrap();
assert_eq!(format!("{:?}", back), format!("{:?}", c));
}
#[test]
fn routing_mode_serialization() {
let json = serde_json::to_string(&RoutingMode::Surgical).unwrap();
assert_eq!(json, "\"surgical\"");
let back: RoutingMode = serde_json::from_str(&json).unwrap();
assert_eq!(back, RoutingMode::Surgical);
}
}