Some checks failed
cad / cad-truth-gates (push) Has been cancelled
cad / cad-core-checks (push) Has been cancelled
cad / cad-consumers (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
nigig-map / test (push) Has been cancelled
nigig-site / Owned paths and honest test contracts (push) Has been cancelled
nigig-site / Cargo check-all-targets (push) Has been cancelled
nigig-site / Cargo clippy-site-owned (push) Has been cancelled
nigig-site / Cargo contained-media-export-fixtures (push) Has been cancelled
nigig-site / Cargo containment-storage-crypto (push) Has been cancelled
nigig-site / Cargo core-clippy (push) Has been cancelled
nigig-site / Cargo core-contracts (push) Has been cancelled
nigig-site / Cargo integration-non-live (push) Has been cancelled
nigig-site / Cargo production-dependency-containment (push) Has been cancelled
nigig-site / Cargo site02-crypto (push) Has been cancelled
nigig-site / Cargo site02-repository (push) Has been cancelled
nigig-site / Cargo site02-store (push) Has been cancelled
nigig-site / Cargo unit (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (macos-latest) (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (ubuntu-latest) (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (windows-latest) (push) Has been cancelled
nigig-site / SITE-02 desktop runtime and normal shutdown (push) Has been cancelled
nigig-site / SITE-02 migration, recovery, and fault corpus (push) Has been cancelled
nigig-site / Media limits (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Real server interoperability (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Security and supply-chain baseline (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
nigig-site / Release capability gate (push) Has been cancelled
- providers/tesseract: system tesseract (TSV → text + normalized word boxes), installed-language detection; default_engine() picks Vision/Tesseract/none - image_prep: decode, validate, apply rotation before recognition - jobs: background OcrJob with progress, cancel, per-item results - ui_state: no fake success toasts; clipboard/export/share report real outcomes - ingest: real decode validation, size caps, shared PDF pages (no duplication) - export: atomic writes, paginated multi-page PDF, refuse non-WinAnsi text - capture: fixed drag anchor; settings: atomic save + applied to new files - drop unused deps; tests: 78 pass incl. 5 real-OCR on a fixture - EXECUTION_PLAN: correct overstated status
133 lines
4.3 KiB
Rust
133 lines
4.3 KiB
Rust
//! OCR-T1 end-to-end with a REAL engine: fixture image → system Tesseract →
|
|
//! text + boxes → export. Skips (with a loud message) when `tesseract` is
|
|
//! not installed; set `NIGIG_OCR_REQUIRE_TESSERACT=1` (CI) to make a missing
|
|
//! engine a failure instead of a skip.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use nigig_ocr::providers::TesseractEngine;
|
|
use nigig_ocr::{
|
|
run_item, FileItem, InMemoryClipboard, JobEvent, OcrEngine, OcrError, OcrJob, OcrLanguage,
|
|
Settings, UiAction, UiState,
|
|
};
|
|
|
|
const FIXTURE: &[u8] = include_bytes!("fixtures/hello.png");
|
|
|
|
fn engine() -> Option<TesseractEngine> {
|
|
let e = TesseractEngine::detect();
|
|
if e.is_none() {
|
|
if std::env::var_os("NIGIG_OCR_REQUIRE_TESSERACT").is_some() {
|
|
panic!("tesseract required but not found");
|
|
}
|
|
eprintln!("SKIP: tesseract not installed — real OCR not exercised");
|
|
}
|
|
e
|
|
}
|
|
|
|
fn norm(s: &str) -> String {
|
|
s.split_whitespace().collect::<Vec<_>>().join(" ")
|
|
}
|
|
|
|
#[test]
|
|
fn recognises_fixture_text_and_boxes() {
|
|
let Some(e) = engine() else { return };
|
|
let mut item = FileItem::new(FIXTURE.to_vec());
|
|
let r = run_item(&e, &mut item).expect("ocr");
|
|
assert_eq!(norm(&r.text), "Hello Nairobi 2026 Invoice total 4500");
|
|
assert_eq!(r.boxes.len(), 6);
|
|
for b in &r.boxes {
|
|
assert!(
|
|
b.x >= 0.0 && b.y >= 0.0 && b.x + b.w <= 1.0 && b.y + b.h <= 1.0,
|
|
"{b:?}"
|
|
);
|
|
}
|
|
// Top-left origin: first line's words sit above the second line's.
|
|
assert!(r.boxes[0].y < r.boxes[3].y);
|
|
assert_eq!(item.recognized_text.as_deref(), Some(r.text.as_str()));
|
|
}
|
|
|
|
#[test]
|
|
fn rotation_is_applied_before_recognition() {
|
|
let Some(e) = engine() else { return };
|
|
// Store the fixture rotated 90° clockwise, then ask the app to rotate it back.
|
|
let img = image::load_from_memory(FIXTURE).unwrap().rotate90();
|
|
let mut bytes = Vec::new();
|
|
img.write_to(
|
|
&mut std::io::Cursor::new(&mut bytes),
|
|
image::ImageFormat::Png,
|
|
)
|
|
.unwrap();
|
|
let mut item = FileItem::new(bytes);
|
|
item.rotate_left();
|
|
let r = run_item(&e, &mut item).unwrap();
|
|
assert!(
|
|
norm(&r.text).contains("Hello Nairobi 2026"),
|
|
"got {:?}",
|
|
r.text
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn uninstalled_language_and_barcode_are_refused() {
|
|
let Some(e) = engine() else { return };
|
|
let mut item = FileItem::new(FIXTURE.to_vec());
|
|
if !e
|
|
.supported_languages()
|
|
.contains(&OcrLanguage::SimplifiedChinese)
|
|
{
|
|
item.options.language = OcrLanguage::SimplifiedChinese;
|
|
assert!(matches!(
|
|
run_item(&e, &mut item),
|
|
Err(OcrError::UnsupportedLanguage(_))
|
|
));
|
|
}
|
|
item.options.language = OcrLanguage::English;
|
|
item.options.barcode_mode = true;
|
|
assert_eq!(
|
|
e.recognize(&item).unwrap_err(),
|
|
OcrError::BarcodeUnsupported
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn background_job_with_real_engine() {
|
|
let Some(e) = engine() else { return };
|
|
let items = vec![
|
|
(0, FileItem::new(FIXTURE.to_vec())),
|
|
(1, FileItem::new(b"junk".to_vec())),
|
|
];
|
|
let evs = OcrJob::spawn(Arc::new(e), items).wait();
|
|
let results: Vec<bool> = evs
|
|
.iter()
|
|
.filter_map(|ev| match ev {
|
|
JobEvent::Finished { result, .. } => Some(result.is_ok()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(results, vec![true, false]);
|
|
}
|
|
|
|
#[test]
|
|
fn ui_flow_drop_ocr_copy_export_for_real() {
|
|
let Some(e) = engine() else { return };
|
|
let dir = nigig_ocr::ingest::resolved_dir(
|
|
std::env::temp_dir().join(format!("ocr-real-{}", std::process::id())),
|
|
);
|
|
let mut ui = UiState::with_services(
|
|
Settings::default(),
|
|
Arc::new(e),
|
|
Box::new(InMemoryClipboard::default()),
|
|
Some(dir.clone()),
|
|
);
|
|
assert_eq!(
|
|
ui.apply(UiAction::DropBytes(FIXTURE.to_vec())),
|
|
"Added file 0"
|
|
);
|
|
assert!(ui.apply(UiAction::DoOcr).starts_with("OCR done: 1/1"));
|
|
assert_eq!(ui.apply(UiAction::Copy), "Copied");
|
|
assert!(norm(&ui.clipboard().last_copied().unwrap()).contains("Invoice total 4500"));
|
|
let m = ui.apply(UiAction::SavePdf { all: false });
|
|
let p = m.strip_prefix("PDF File Exported: ").expect(&m);
|
|
assert!(std::fs::read(p).unwrap().starts_with(b"%PDF-1.4"));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|