Skip to content

9. Reading a document

A librarian receives a box and a delivery note. Rather than typing the ISBNs back in, we read the document.

The supplier's delivery note, as it arrives

Where the line runs

kewos/ocr turns a document into text, and stops there. It does not know what an ISBN is, and it must not: recognising one is business knowledge, so it lives in the module.

Same line as everywhere else in this product. The kernel supplies a generic capability, the module puts its meaning into it.

Reading

from kewos.ocr import extract_text
result = extract_text(
document,
mime_type="application/pdf",
default=tenant_default, # the tenant's ocr.engine parameter
fallback=tenant_fallback,
)

The module names no engine: it passes through what the customer configured. The choice belongs to the installation, not to the code.

On the delivery note above, with the pdf_text engine:

engine: pdf_text | fell back: False
--- extracted text ---
NORTHWIND BOOK SUPPLY
Delivery note DN-2026-0431 3 September 2026
Deliver to: Kewos Public Library
Qty ISBN Title
2 9780441013593 Dune
1 9780553380163 A Brief History of Time
3 9781408839997 The Silk Roads
1 9780062316097 Sapiens
Total 7 items. Please check on receipt.

Exact, instant, free: the PDF already carried its text, so there was no OCR at all.

Recognising

#: An ISBN-13 as printed: thirteen digits, optionally grouped by spaces or
#: hyphens. Deliberately narrow -- a looser pattern matches phone numbers and
#: order references, and a wrong match is worse than a missed one here.
_ISBN13 = re.compile(r"\b(97[89])[-\s]?(\d[-\s]?){9}\d\b")
def is_valid_isbn13(isbn: str) -> bool:
"""Check the ISBN-13 checksum."""
digits = re.sub(r"[-\s]", "", isbn)
if len(digits) != 13 or not digits.isdigit():
return False
total = sum(int(d) * (1 if i % 2 == 0 else 3) for i, d in enumerate(digits[:12]))
return (10 - total % 10) % 10 == int(digits[12])

The result keeps the line it came from, so whoever checks sees context and not a bare number:

9780441013593 <- 2 9780441013593 Dune
9780553380163 <- 1 9780553380163 A Brief History of Time
9781408839997 <- 3 9781408839997 The Silk Roads
9780062316097 <- 1 9780062316097 Sapiens

And if the document is a photograph

The same code, another engine. Here is what the local engine read on the same page, rendered as an image at 200 dots per inch:

NORTHWIND BOOK SUPPLY
Delivery note DN-2026-0431 3 September 2026
Deliver to: Kewos Public Library
Qty ISBN Title
2 9780441013593 Dune
1 9780553380163 A Brief History of Time
3 9781408839997 The Silk Roads
1 9780062316097 Sapiens
Total 7 items. Please check on receipt.

The column alignment is gone, blank lines have appeared, and the four ISBNs come out identical. That is why the recognition works line by line with a pattern, and never on column positions.

The case that matters: a scan

A scanned PDF carries no text layer. The engine says so, rather than answering “nothing found”:

from kewos.ocr import NoTextLayerError
try:
lines = read_delivery_note(document, mime_type="application/pdf", default="pdf_text")
except NoTextLayerError:
# Not a failure: an answer. Ask for an image engine, or ask the supplier
# for a real PDF.
...

Where to make this call

Not inside an HTTP request. A read is slow, and on the AI side it goes out over the network. Put it in a background job, and let the screen show the wait.

Next

PDF documents: producing a document rather than reading one.