ocr 0.1.0

A minimalist OCR library for Rust — from scratch, no external engine
Documentation
use ocr::OcrEngine;

fn ocr(text: &str, scale: u32) -> String {
    let img = ocr::render::render_text(text, scale);
    let engine = OcrEngine::new();
    let result = engine.recognize_image(&img).unwrap();
    result.text
}

#[test]
fn hello_world() {
    assert_eq!(ocr("HELLO WORLD", 8), "HELLO WORLD");
}

#[test]
fn alphabet_uppercase() {
    assert_eq!(ocr("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 8), "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}

#[test]
fn digits() {
    assert_eq!(ocr("0123456789", 8), "0123456789");
}

#[test]
fn lowercase() {
    assert_eq!(ocr("abcdefghijklmnopqrstuvwxyz", 8), "abcdefghijklmnopqrstuvwxyz");
}

#[test]
fn multiline() {
    assert_eq!(ocr("LINE ONE\nLINE TWO\nLINE THREE", 8), "LINE ONE\nLINE TWO\nLINE THREE");
}

#[test]
fn sentence_with_spaces() {
    assert_eq!(ocr("THE QUICK BROWN FOX", 8), "THE QUICK BROWN FOX");
}

#[test]
fn mixed_alnum_with_spaces() {
    assert_eq!(ocr("ABC 123 XYZ", 8), "ABC 123 XYZ");
}

#[test]
fn punctuation() {
    assert_eq!(ocr("HI! OK? YES.", 8), "HI! OK? YES.");
}

#[test]
fn small_scale() {
    assert_eq!(ocr("AB", 4), "AB");
}

#[test]
fn large_scale() {
    assert_eq!(ocr("AB", 12), "AB");
}

#[test]
fn single_char() {
    assert_eq!(ocr("A", 8), "A");
}

#[test]
fn single_digit() {
    assert_eq!(ocr("7", 8), "7");
}

#[test]
fn all_digits_recognized() {
    for d in '0'..='9' {
        let s = d.to_string();
        assert_eq!(ocr(&s, 8), s, "failed for digit '{}'", d);
    }
}

#[test]
fn all_uppercase_recognized() {
    for c in 'A'..='Z' {
        let s = c.to_string();
        assert_eq!(ocr(&s, 8), s, "failed for '{}'", c);
    }
}

#[test]
fn all_lowercase_recognized() {
    for c in 'a'..='z' {
        let s = c.to_string();
        assert_eq!(ocr(&s, 8), s, "failed for '{}'", c);
    }
}

#[test]
fn recognize_bytes() {
    let img = ocr::render::render_text("TEST", 8);
    let mut buf = std::io::Cursor::new(Vec::new());
    img.write_to(&mut buf, image::ImageFormat::Png).unwrap();
    let buf = buf.into_inner();
    let engine = OcrEngine::new();
    let result = engine.recognize_bytes(&buf).unwrap();
    assert_eq!(result.text, "TEST");
}

#[test]
fn empty_image_returns_empty() {
    let img = image::DynamicImage::ImageLuma8(
        image::GrayImage::from_pixel(100, 100, image::Luma([255u8])),
    );
    let engine = OcrEngine::new();
    let result = engine.recognize_image(&img).unwrap();
    assert!(result.text.is_empty());
}

#[test]
fn confidence_above_threshold() {
    let img = ocr::render::render_text("HELLO", 8);
    let engine = OcrEngine::new();
    let result = engine.recognize_image(&img).unwrap();
    assert!(result.confidence > 50.0, "confidence too low: {}", result.confidence);
}

#[test]
fn words_populated() {
    let img = ocr::render::render_text("HI OK", 8);
    let engine = OcrEngine::new();
    let result = engine.recognize_image(&img).unwrap();
    assert_eq!(result.words.len(), 2);
    assert_eq!(result.words[0].text, "HI");
    assert_eq!(result.words[1].text, "OK");
}

#[test]
fn generate_and_recognize_example_images() {
    let cases = [
        ("hello_world.png", "HELLO WORLD"),
        ("alphabet.png", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
        ("digits.png", "0123456789"),
        ("lowercase.png", "abcdefghijklmnopqrstuvwxyz"),
        ("multiline.png", "LINE ONE\nLINE TWO\nLINE THREE"),
        ("sentence.png", "THE QUICK BROWN FOX"),
        ("mixed.png", "ABC 123 XYZ"),
        ("punctuation.png", "HI! OK? YES."),
    ];

    let engine = OcrEngine::new();
    for (filename, expected) in &cases {
        let img = ocr::render::render_text(expected, 8);
        let result = engine.recognize_image(&img).unwrap();
        assert_eq!(
            result.text, *expected,
            "mismatch for {}: expected '{}', got '{}'",
            filename, expected, result.text,
        );
    }
}