mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
OCR: a scanned euro sign reads as €, not E (#796)
The ocrs crate's default alphabet has an E where its recognition model was trained with € (ocrs-models DEFAULT_ALPHABET), so a scanned "€250" was recognised as "E250". pdfcraft-ocr now gives ocrs the alphabet the model was trained with. the_alphabet_is_the_trained_one checks the alphabet without the models; recognize_text_reads_the_euro_sign reads a scan end to end when they are installed. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f8334d0cd3
commit
c7fcee4ed9
2 files changed
+33
No files matched your search
@@ -1697,6 +1697,24 @@ fn backgrounds_and_watermarks_from_files() {
|
||||
|
||||
/// Scan & OCR ▸ Recognize text on a page that is only a picture of text (needs the models:
|
||||
/// `cargo xtask models`; skipped without them).
|
||||
#[test]
|
||||
fn recognize_text_reads_the_euro_sign() {
|
||||
if !ocr::available() {
|
||||
eprintln!("skipped: OCR models not installed");
|
||||
return;
|
||||
}
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000);
|
||||
let text = s.create_from_text("t", "Total amount due: €250 by Friday").unwrap();
|
||||
let id = s.open("text.pdf", None, text, None).unwrap();
|
||||
let png = export::Exporter::new(s.get(id).unwrap()).png(0, 150.0).unwrap();
|
||||
let scan = s.create_from_images(&[("scan.png".into(), png)]).unwrap();
|
||||
let id = s.open("scan.pdf", None, scan, None).unwrap();
|
||||
s.recognize_text(id, &[], ocr::OcrSettings::default()).unwrap();
|
||||
let text = page_texts(&s, id)[0].clone();
|
||||
// The model was trained with € where the ocrs crate's alphabet has a second E.
|
||||
assert!(text.contains("€250"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_text_makes_a_scanned_page_searchable() {
|
||||
if !ocr::available() {
|
||||
|
||||
@@ -18,6 +18,11 @@ pub use pdfcraft_fonts::helvetica_width;
|
||||
pub const DETECTION_MODEL: &str = "text-detection.rten";
|
||||
pub const RECOGNITION_MODEL: &str = "text-recognition.rten";
|
||||
|
||||
/// The recognition model's alphabet, as it was trained (ocrs-models' `DEFAULT_ALPHABET`): its
|
||||
/// output classes follow this order. The ocrs crate's own copy has an `E` where the training had
|
||||
/// `€`, which made a scanned "€250" read as "E250".
|
||||
pub const ALPHABET: &str = " 0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~€ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
/// The languages the models read (ISO 639-1); all use the Latin alphabet without accents.
|
||||
pub const LANGUAGES: &[(&str, &str)] = &[("en", "English")];
|
||||
|
||||
@@ -109,6 +114,7 @@ impl Ocr {
|
||||
let params = ocrs::OcrEngineParams {
|
||||
detection_model: Some(load(&models.detection)?),
|
||||
recognition_model: Some(load(&models.recognition)?),
|
||||
alphabet: Some(ALPHABET.into()),
|
||||
..Default::default()
|
||||
};
|
||||
let engine = ocrs::OcrEngine::new(params).map_err(|e| OcrError::Load("ocr engine".into(), e.to_string()))?;
|
||||
@@ -278,6 +284,15 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The message tells end users what to do; release builds have no cargo.
|
||||
#[test]
|
||||
fn the_alphabet_is_the_trained_one() {
|
||||
let chars: Vec<char> = ALPHABET.chars().collect();
|
||||
assert_eq!(chars.len(), 96);
|
||||
// The euro sign follows the ASCII punctuation (class 44; 0 is the CTC blank), not a second E.
|
||||
assert_eq!(chars.iter().position(|&c| c == '€'), Some(43));
|
||||
assert_eq!(chars.iter().filter(|&&c| c == 'E').count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_models_message_is_for_end_users() {
|
||||
let message = OcrError::NoModels.to_string();
|
||||
|
||||
Reference in new issue
Block a user