const MIN_RECOVERY_RATIO: f64 = 0.98;
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PageRecovery {
pub page: usize,
pub extracted_chars: usize,
pub rendered_chars: usize,
}
impl PageRecovery {
pub fn recovery_ratio(&self) -> f64 {
if self.extracted_chars == 0 {
return 1.0;
}
self.rendered_chars as f64 / self.extracted_chars as f64
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ConversionReport {
pub pages: usize,
pub extracted_words: usize,
pub rendered_words: usize,
pub extracted_chars: usize,
pub rendered_chars: usize,
pub headings_found: usize,
pub lists_found: usize,
pub tables_found: usize,
pub citations_found: usize,
pub per_page: Vec<PageRecovery>,
}
impl ConversionReport {
pub fn recovery_ratio(&self) -> f64 {
if self.extracted_chars == 0 {
return 1.0;
}
self.rendered_chars as f64 / self.extracted_chars as f64
}
pub fn word_recovery_ratio(&self) -> f64 {
if self.extracted_words == 0 {
return 1.0;
}
self.rendered_words as f64 / self.extracted_words as f64
}
pub fn passes(&self) -> bool {
self.recovery_ratio() >= MIN_RECOVERY_RATIO
}
#[cfg(feature = "serde")]
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"pages": self.pages,
"recovery_ratio": self.recovery_ratio(),
"passes": self.passes(),
"extracted_chars": self.extracted_chars,
"rendered_chars": self.rendered_chars,
"word_recovery_ratio": self.word_recovery_ratio(),
"extracted_words": self.extracted_words,
"rendered_words": self.rendered_words,
"headings_found": self.headings_found,
"lists_found": self.lists_found,
"tables_found": self.tables_found,
"citations_found": self.citations_found,
"per_page": self
.per_page
.iter()
.map(|p| {
serde_json::json!({
"page": p.page,
"extracted_chars": p.extracted_chars,
"rendered_chars": p.rendered_chars,
"recovery_ratio": p.recovery_ratio(),
})
})
.collect::<Vec<_>>(),
})
}
}
impl std::fmt::Display for ConversionReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Pages: {}", self.pages)?;
writeln!(f)?;
writeln!(f, "Recovery (characters, authoritative for PASS/FAIL):")?;
writeln!(f, " Extracted: {} chars", self.extracted_chars)?;
writeln!(f, " Rendered: {} chars", self.rendered_chars)?;
writeln!(f, " Ratio: {:.1}%", self.recovery_ratio() * 100.0)?;
if !self.per_page.is_empty() {
writeln!(f)?;
writeln!(f, " Per page:")?;
for page in &self.per_page {
writeln!(
f,
" Page {:>3}: {:>5.1}% ({}/{} chars)",
page.page,
page.recovery_ratio() * 100.0,
page.rendered_chars,
page.extracted_chars,
)?;
}
}
writeln!(f)?;
writeln!(f, "Words (informational only, not authoritative -- see kopitiam-wwr):")?;
writeln!(f, " Extracted: {}", self.extracted_words)?;
writeln!(f, " Rendered: {}", self.rendered_words)?;
writeln!(f)?;
writeln!(f, "Headings: {}", self.headings_found)?;
writeln!(f, "Lists: {}", self.lists_found)?;
writeln!(f, "Tables: {}", self.tables_found)?;
writeln!(f, "Citations: {}", self.citations_found)?;
writeln!(f)?;
write!(f, "Status: {}", if self.passes() { "PASS" } else { "FAIL" })
}
}
#[cfg(all(test, feature = "serde"))]
mod json_tests {
use super::*;
fn sample() -> ConversionReport {
ConversionReport {
pages: 2,
extracted_words: 10,
rendered_words: 10,
extracted_chars: 100,
rendered_chars: 98,
headings_found: 3,
lists_found: 1,
tables_found: 2,
citations_found: 4,
per_page: vec![
PageRecovery { page: 1, extracted_chars: 60, rendered_chars: 60 },
PageRecovery { page: 2, extracted_chars: 40, rendered_chars: 38 },
],
}
}
#[test]
fn to_json_carries_the_computed_signals_a_caller_gates_on() {
let json = sample().to_json();
assert_eq!(json["pages"], 2);
assert!((json["recovery_ratio"].as_f64().unwrap() - 0.98).abs() < 1e-9);
assert_eq!(json["passes"], true);
assert_eq!(json["extracted_chars"], 100);
assert_eq!(json["rendered_chars"], 98);
assert_eq!(json["headings_found"], 3);
assert_eq!(json["tables_found"], 2);
assert_eq!(json["citations_found"], 4);
}
#[test]
fn to_json_includes_per_page_ratios_from_card_i_b() {
let json = sample().to_json();
let per_page = json["per_page"].as_array().unwrap();
assert_eq!(per_page.len(), 2);
assert_eq!(per_page[0]["page"], 1);
assert!((per_page[0]["recovery_ratio"].as_f64().unwrap() - 1.0).abs() < 1e-9);
assert_eq!(per_page[1]["page"], 2);
assert!((per_page[1]["recovery_ratio"].as_f64().unwrap() - 0.95).abs() < 1e-9);
}
}