Invoices to data: local OCR
OCR gives you text with typos. Pull out number, date, CIF, NIF and amounts; check the NIF letter, the CIF digit and that base plus VAT is the total.
OCR_SAMPLE is what tesseract.js read on a synthetic invoice of ours, typos included. OCR is that text, or your own image's if you switch on "A model in your browser". Write five functions:
validateNIF(s): a DNI is 8 digits and a letter: the letter is NIF_LETTERS[n % 23]. A NIE starts with X, Y or Z, worth 0, 1 and 2 in front of 7 digits, then the same rule. Accept lower case and surrounding spaces.
validateCIF(s): a letter, 7 digits and a control. Add the digits in even positions (2nd, 4th, 6th) and, for the odd ones, their double reduced to one digit (by adding its digits); c = (10 - sum % 10) % 10. When the first letter is K, P, Q, S, N, W or R the control must be the letter "JABCDEFGHI"[c]; when it is A, B, E or H, the digit c; with any other letter, either is fine.
parseAmount(s): "1.250,00 €" is 1250; with no digits, null.
extractFields(text): { numero, fecha, cif, nif, base, iva, total } with null for whatever is missing. numero looks like 2026-0147, fecha like dd/mm/yyyy; base, iva and total are the first amount with decimals on the "Base imponible", "IVA" and "TOTAL" lines.
checkTotals(f): { ok, diff } with diff = base + iva - total rounded to cents, and ok only when all three exist and diff is 0.
Challenges 0/6
- The NIF letter, for DNI and NIE, lower case and spaces
- The CIF control: a digit for an S.L., a letter for a public body
- Amounts with a thousands dot, a decimal comma and a symbol
- The seven fields of the sample invoice, typos included
- Base plus VAT is the total; a difference is measured in cents
- With the model's text (your image) or the sample: fields and verdict
function validateNIF(s) {
// trim + upper case; DNI: 8 digits + NIF_LETTERS[n % 23]; NIE: X/Y/Z = 0/1/2 in front of 7 digits, same rule
return false;
}
function validateCIF(s) {
// letter + 7 digits + control; even positions as they are, odd ones doubled and reduced to one digit
// c = (10 - sum % 10) % 10; K P Q S N W R want the letter "JABCDEFGHI"[c]; A B E H the digit; the rest either
return false;
}
function parseAmount(s) {
// drop thousands dots, comma to point, Number; null when there are no digits
return null;
}
function extractFields(text) {
// numero 9999-9999, fecha dd/mm/yyyy, cif letter+7 digits+control, nif 8 digits+letter or NIE
// base / iva / total: first amount with two decimals on the line of "Base imponible" / "IVA" / "TOTAL"
return { numero: null, fecha: null, cif: null, nif: null, base: null, iva: null, total: null };
}
function checkTotals(f) {
// diff = base + iva - total rounded to cents; ok only when all three exist and diff is 0
return { ok: false, diff: null };
}
console.log(extractFields(OCR), checkTotals(extractFields(OCR)));The idea
An OCR does not return an invoice: it returns text, and text with mistakes. On our sample it read "FACTU RA", split the number over two lines and wrote "ESOO" in the IBAN. Turning that into data is your job, and it is done with rules you can defend in front of your accountant: the number has this shape, the date that one, and the amounts sit on the line that starts with "Base imponible".
The checks are what separates an extractor from a toy. The NIF letter and the CIF digit are checksums: if the OCR read a 3 for an 8, they do not match and you know before anything is booked. And if base plus VAT is not the total, either the OCR failed or the invoice is wrong; in both cases a person reviews it.
What we measured: tesseract.js 6 with the fast Spanish model (1.1 MB compressed) read the 1240 × 1000 px invoice in 290–475 ms on the DGX Spark on 2026-10-18, with a mean confidence of 92 %, in Node. In the browser it takes a bit longer and runs in a worker. The "best" model (8.4 MB) did not improve this sample.
Everything happens in your tab. Pressing "Load the model" downloads 5.3 MB from this very site; an image you upload never leaves your browser, which is exactly what you need to process invoices holding third-party data without one more data processor.
Run it on your machine
With Python, on the text the OCR produced (CC BY 4.0). With Tesseract installed, tesseract factura-muestra.png out -l spa reproduces it from the image:
curl -O https://vorluxai.com/data/labs/factura-ocr.txt
python3 -c "
import re
t = open('factura-ocr.txt').read()
amount = lambda label: float(re.search(label + r'[^\n]*?(\d[\d.]*,\d{2})', t, re.I).group(1).replace('.', '').replace(',', '.'))
nif = re.search(r'\b(\d{8})([A-Z])\b', t)
print('NIF ok:', 'TRWAGMYFPDXBNJZSQVHLCKE'[int(nif.group(1)) % 23] == nif.group(2))
base, iva, total = amount('Base imponible'), amount('IVA'), amount('TOTAL')
print('base + IVA == total:', round(base + iva - total, 2) == 0, base, iva, total)
" Go deeper: the Tesseract reference →
This in production, with your data? Let's talk for 15 minutes →