perf(ocr): downscale photos to 1600px long edge before recognize

Phone cameras emit 12+ MP images (~4000 px long edge). Tesseract
on a full-resolution shot was painfully slow — minutes, not
seconds. Downscaling to 1600 px on the long edge before
worker.recognize() makes OCR 4-8× faster with no real accuracy
loss for printed text.

Implementation: createImageBitmap -> draw to a sized HTMLCanvas
-> canvas.toBlob() at JPEG 0.85. Smaller-than-1600 px inputs are
passed through unchanged. Status line briefly shows the
downscale ratio so the timing is interpretable.
This commit is contained in:
João Pedro Battistella Nadas
2026-06-09 18:48:40 +02:00
parent 6d6b52e10e
commit dcac01e51b
2 changed files with 34 additions and 2 deletions

View File

@@ -329,15 +329,46 @@
scanStatus.classList.toggle("error", error);
}
// Downscale to at most OCR_MAX_DIM on the long edge. A typical phone
// photo is 4000+ px on the long edge — Tesseract is 4-8× faster at
// 1600 px with no real accuracy loss for printed text.
const OCR_MAX_DIM = 1600;
async function downscaleForOCR(file) {
const bitmap = await createImageBitmap(file);
const long = Math.max(bitmap.width, bitmap.height);
if (long <= OCR_MAX_DIM) {
bitmap.close?.();
return { blob: file, w: bitmap.width, h: bitmap.height, scaled: false };
}
const scale = OCR_MAX_DIM / long;
const w = Math.round(bitmap.width * scale);
const h = Math.round(bitmap.height * scale);
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0, w, h);
bitmap.close?.();
const blob = await new Promise(resolve =>
canvas.toBlob(resolve, "image/jpeg", 0.85)
);
return { blob, w, h, scaled: true, origLong: long };
}
async function runOCR(file) {
const src = sourceSel.value;
const lang = src === "auto" ? TESS_AUTO : (TESS_LANGS[src] || TESS_AUTO);
scanBtn.disabled = true;
setScanStatus("Loading OCR engine…");
setScanStatus("Preparing image…");
const t0 = performance.now();
try {
const { blob, w, h, scaled, origLong } = await downscaleForOCR(file);
if (scaled) {
setScanStatus(`Downscaled ${origLong}px → ${Math.max(w, h)}px`);
}
// Lazy import — first scan triggers the ~5 MB JS + language pack
// download; cached by the browser after that.
const { createWorker } = await import("https://esm.sh/tesseract.js@5");
@@ -350,7 +381,7 @@
}
},
});
const { data: { text } } = await worker.recognize(file);
const { data: { text } } = await worker.recognize(blob);
await worker.terminate();
editor.dispatch({