Integrar OCR en el navegador con Tesseract.js
Selecciona una imagen local, haz clic en «Recognize text» y copia su texto sin subir la imagen. Esta guía ofrece a los desarrolladores web un ejemplo completo de Tesseract.js con indicaciones sobre la carga, reintentos con el mismo archivo y una sola tarea de reconocimiento a la vez. Comienza con una captura de pantalla clara de texto impreso en inglés; el resultado del OCR sigue necesitando revisión.
Qué se ejecuta en el navegador
Tesseract.js ejecuta el motor de OCR Tesseract mediante WebAssembly en un worker. El navegador realiza el reconocimiento, por lo que la velocidad y el uso de memoria dependen del dispositivo y de la imagen del lector. No se garantiza un resultado instantáneo. El alcance del proyecto también excluye la entrada directa de PDF: convierte las páginas del PDF en imágenes por separado antes de reconocerlas.
Este ejemplo fija Tesseract.js y su núcleo en la versión 7.0.0, y los datos del idioma inglés en
la versión 1.0.0. Utiliza la salida de texto, que está habilitada de forma predeterminada.
La API del worker
inicializa el idioma en la llamada asíncrona createWorker('eng', 1, options); los antiguos pasos
loadLanguage() y initialize() no son necesarios.
Compatibilidad y requisitos del navegador
Usa un navegador actual con Web Workers, workers anidados y WebAssembly. El ejemplo completo se probó en Chromium 145 y 152 en Linux. También necesitas Python 3 para servir los dos archivos localmente, además de una conexión de red para cargar desde jsDelivr los scripts, WASM y datos del idioma en las versiones fijadas. No necesitas compilar con Node.js ni instalar paquetes.
En este ejemplo, la imagen permanece en el navegador, pero las descargas de recursos siguen conectándose a una CDN. Almacenar en caché solo los datos del idioma no permite que la página funcione sin conexión. Una aplicación sin conexión también debe servir o almacenar en caché su HTML, scripts, worker, WASM y recursos de idioma; esta guía no instala una caché sin conexión. Consulta las opciones de alojamiento de recursos del proyecto.
Primeros pasos con Tesseract.js
Instalación
Crea un directorio nuevo y vacío llamado tesseract-browser. Si ese nombre ya existe,
elige otro directorio en lugar de reemplazar sus archivos. Guarda los siguientes dos bloques como
index.html
y ocr-worker.js dentro de él. Son archivos sencillos para el navegador, sin
framework ni backend.
Ejemplo básico: reconocer texto en una imagen
Guarda esto como index.html. El selector de archivos y el botón permanecen
deshabilitados mientras hay una tarea pendiente. Si después seleccionas otra imagen, se borra el
resultado anterior; al hacer clic de nuevo en el botón, se reintenta con el archivo seleccionado
sin necesidad de un nuevo evento de selección de archivo.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Read text from a local image</title>
</head>
<body>
<h1>Read text from a local image</h1>
<form id="ocrForm">
<fieldset id="controls">
<legend>Recognize English text</legend>
<label for="imageInput">Image (JPEG, PNG, or WebP; up to 5 MiB)</label>
<input id="imageInput" type="file" accept="image/jpeg,image/png,image/webp" />
<button type="submit">Recognize text</button>
</fieldset>
</form>
<p id="status" role="status">Choose an image to begin.</p>
<label for="result">Recognized text</label>
<textarea id="result" rows="12" cols="60" readonly></textarea>
<script>
const form = document.getElementById('ocrForm')
const controls = document.getElementById('controls')
const imageInput = document.getElementById('imageInput')
const status = document.getElementById('status')
const result = document.getElementById('result')
let busy = false
async function recognizeImage(file) {
let task
let timer
let timedOut = false
try {
task = new Worker('./ocr-worker.js')
return await new Promise((resolve, reject) => {
const fail = () => reject(new Error('OCR task failed.'))
timer = setTimeout(() => {
timedOut = true
fail()
}, 90_000)
task.onerror = (event) => {
event.preventDefault()
fail()
}
task.onmessage = ({ data }) => {
if (data.type === 'result') resolve(data.text)
else if (data.type === 'error') fail()
else if (data.type === 'progress') {
status.textContent = data.status === 'recognizing text'
? 'Recognizing text… ' + Math.round(data.progress * 100) + '%'
: 'Loading OCR assets…'
}
}
task.postMessage(file)
})
} catch {
throw new Error(timedOut
? 'OCR timed out after 90 seconds. Try a smaller image or retry.'
: 'OCR failed. Check your connection and image, then try again.')
} finally {
clearTimeout(timer)
task?.terminate()
}
}
async function validateAndPerformOCR(file) {
if (!file || !['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
throw new Error('Choose a JPEG, PNG, or WebP image.')
}
if (file.size === 0 || file.size > 5 * 1024 * 1024) {
throw new Error('Choose a nonempty image of 5 MiB or smaller.')
}
return recognizeImage(file)
}
imageInput.addEventListener('change', () => {
if (busy) return
result.value = ''
status.textContent = 'Selection changed. Click Recognize text.'
})
form.addEventListener('submit', async (event) => {
event.preventDefault()
if (busy || controls.disabled) return
const file = imageInput.files[0]
busy = true
controls.disabled = true
result.value = ''
status.textContent = 'Loading OCR assets…'
try {
const text = await validateAndPerformOCR(file)
result.value = text
status.textContent = text.trim()
? 'Finished: ' + file.name + '. You can copy the text below.'
: 'No text found. Try a clearer image of printed text.'
} catch (error) {
status.textContent = error instanceof Error
? error.message
: 'OCR failed. Try another image.'
} finally {
busy = false
controls.disabled = false
}
})
if (typeof Worker === 'undefined' || typeof WebAssembly === 'undefined') {
controls.disabled = true
status.textContent = 'Use a browser with Web Workers and WebAssembly.'
}
</script>
</body>
</html>
Guarda esto como ocr-worker.js. La página controla este worker externo y puede
terminarlo incluso si Tesseract nunca termina de inicializarse. Esto es importante porque una
descarga fallida de datos de idioma puede dejar
createWorker() pendiente en la versión 7.0.0.
Su errorHandler informa del fallo directamente a la página; el límite de
90 segundos también cubre una descarga bloqueada. Este límite es una política de la demostración,
no un tiempo de reconocimiento esperado.
importScripts('https://cdn.jsdelivr.net/npm/tesseract.js@7.0.0/dist/tesseract.min.js')
async function performOCR(file) {
const worker = await Tesseract.createWorker('eng', 1, {
workerPath: 'https://cdn.jsdelivr.net/npm/tesseract.js@7.0.0/dist/worker.min.js',
corePath: 'https://cdn.jsdelivr.net/npm/tesseract.js-core@7.0.0',
langPath: 'https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng@1.0.0/4.0.0_best_int',
logger: ({ status, progress }) => postMessage({ type: 'progress', status, progress }),
errorHandler: () => postMessage({ type: 'error' }),
})
try {
const { data: { text } } = await worker.recognize(file)
return text
} finally {
await worker.terminate()
}
}
self.onmessage = async ({ data: file }) => {
try {
const text = await performOCR(file)
postMessage({ type: 'result', text })
} catch {
postMessage({ type: 'error' })
}
}
Desde el directorio padre de tesseract-browser, inicia un servidor local en una terminal:
(cd tesseract-browser && python3 -m http.server --bind 127.0.0.1 0)
El puerto 0 solicita al sistema operativo un puerto disponible.
Abre la dirección http://127.0.0.1:PORT/ que muestra el servidor. Usa HTTP en lugar de
hacer doble clic en el archivo HTML, ya que la carga del worker depende del origen de la página.
Detén el servidor con Ctrl+C cuando termines. Al iniciarlo de nuevo, se sirven los mismos archivos
sin sobrescribirlos.
Elige una captura de pantalla pequeña que contenga «BROWSER OCR TEST» y haz clic en «Recognize text». Deberías ver indicaciones sobre la carga, el progreso del reconocimiento y las palabras extraídas en «Recognized text». Una imagen en blanco debería mostrar «No text found». El archivo original nunca se modifica, y la página no guarda las imágenes ni el texto reconocido en el almacenamiento.
Gestión de errores y validación
El límite de cinco MiB es la política de entrada de esta demostración, no el máximo de Tesseract.
El tamaño del archivo comprimido no limita la memoria que ocupan los píxeles decodificados, así
que comienza con imágenes pequeñas en dispositivos móviles. La lista de tipos MIME permitidos
ayuda a detectar una selección incorrecta; un archivo dañado etiquetado como
image/png deberá fallar durante el reconocimiento de todos modos.
Ni un tipo MIME de imagen ni el
atributo accept
del selector demuestran que el contenido sea válido.
Si el OCR falla, revisa la imagen y el panel Red del navegador para detectar solicitudes fallidas de scripts, WASM o datos de idioma; después, haz clic de nuevo en «Recognize text». Puedes reintentar con el mismo archivo. Un resultado vacío no es un error del worker ni demuestra que la imagen de origen no contenga texto. Un contraste bajo, letras diminutas o un idioma de reconocimiento incorrecto también pueden producir resultados vacíos o imprecisos.
Manejo de varios idiomas
Para texto que combine inglés y alemán, añade esta función a ocr-worker.js y
reemplaza la llamada performOCR(file) del manejador por
performMultilingualOCR(file). El array de idiomas selecciona modelos; no traduce sus resultados.
Esta variante usa las URL de idioma predeterminadas de Tesseract para que cada idioma pueda cargar
sus propios datos, en lugar de la ruta con versión fija y exclusiva para inglés del ejemplo principal.
async function performMultilingualOCR(file, languages = ['eng', 'deu']) {
const worker = await Tesseract.createWorker(languages, 1, {
workerPath: 'https://cdn.jsdelivr.net/npm/tesseract.js@7.0.0/dist/worker.min.js',
corePath: 'https://cdn.jsdelivr.net/npm/tesseract.js-core@7.0.0',
logger: ({ status, progress }) => postMessage({ type: 'progress', status, progress }),
errorHandler: () => postMessage({ type: 'error' }),
})
try {
const { data: { text } } = await worker.recognize(file)
return text
} finally {
await worker.terminate()
}
}
Optimización del rendimiento
Preprocesamiento de imágenes
Primero, compara los resultados con tus imágenes reales. El recorte de bordes excesivos o la corrección de la rotación pueden ayudar; reducir las letras o aumentar el contraste puede eliminar detalles útiles. La función auxiliar opcional que aparece a continuación limita el ancho a 1.000 píxeles como medida para reducir el uso de memoria, no como recomendación para mejorar la precisión.
Añádela dentro del script index.html y reemplaza return recognizeImage(file) en
validateAndPerformOCR por return optimizedOCR(file). Mantén la validación antes del
preprocesamiento.
async function preprocessImage(file) {
const url = URL.createObjectURL(file)
try {
const img = new Image()
await new Promise((resolve, reject) => {
img.onload = resolve
img.onerror = () => reject(new Error('Unable to decode image.'))
img.src = url
})
const canvas = document.createElement('canvas')
const maxWidth = 1000
const scale = img.width > maxWidth ? maxWidth / img.width : 1
canvas.width = Math.max(1, Math.round(img.width * scale))
canvas.height = Math.max(1, Math.round(img.height * scale))
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Canvas processing is unavailable.')
ctx.filter = 'grayscale(100%) contrast(150%)'
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
return await new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob)
else reject(new Error('Unable to encode processed image.'))
}, 'image/png')
})
} finally {
URL.revokeObjectURL(url)
}
}
async function optimizedOCR(file) {
const processedImage = await preprocessImage(file)
return recognizeImage(processedImage)
}
Gestión de memoria
La demostración de una sola imagen crea un worker nuevo por intento para simplificar su ciclo de
vida. Para un lote, reutiliza un worker de Tesseract inicializado y reconoce las imágenes de forma
secuencial. Esto conserva el orden de entrada y evita cargar un motor de OCR independiente para
cada imagen. La función rechaza la operación con la primera imagen que falla y termina su worker
inicializado en finally.
Añade esto a ocr-worker.js. Para un experimento mínimo de dos pasadas con la
página actual, reemplaza const text = await performOCR(file) del manejador por
const text = (await batchProcessImages([file, file])).join('\n'). El resultado contiene dos copias en orden; en una aplicación,
pasa en su lugar tu array ordenado de archivos de imagen. El límite de tiempo externo sigue
aplicándose a toda la tarea, así que elige un límite de lote adecuado antes de ampliar la interfaz.
async function batchProcessImages(files) {
const worker = await Tesseract.createWorker('eng', 1, {
workerPath: 'https://cdn.jsdelivr.net/npm/tesseract.js@7.0.0/dist/worker.min.js',
corePath: 'https://cdn.jsdelivr.net/npm/tesseract.js-core@7.0.0',
langPath: 'https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng@1.0.0/4.0.0_best_int',
logger: ({ status, progress }) => postMessage({ type: 'progress', status, progress }),
errorHandler: () => postMessage({ type: 'error' }),
})
const results = []
try {
for (const file of files) {
const { data: { text } } = await worker.recognize(file)
results.push(text)
}
return results
} finally {
await worker.terminate()
}
}
Consideraciones de seguridad y buenas prácticas
Mantén el texto reconocido como texto. Este ejemplo lo asigna a value
de un textarea de solo lectura, por lo que el HTML reconocido no puede ejecutarse. Si llevas el
resultado a otra vista, no lo insertes mediante innerHTML.
El reconocimiento local describe dónde procesa la imagen este código; no es una garantía general
de privacidad para cualquier página que lo incorpore. Los scripts de una página pueden acceder
a los archivos seleccionados. Revisa esas dependencias y los demás scripts de tu sitio antes de
manejar documentos sensibles. Si alojas los recursos de OCR por tu cuenta, sirve WASM con
application/wasm y conserva el paquete completo del núcleo de la versión
correspondiente para que Tesseract pueda seleccionar una compilación compatible con el dispositivo.
