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

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 SUPPLYDelivery note DN-2026-0431 3 September 2026Deliver to: Kewos Public LibraryQty ISBN Title2 9780441013593 Dune1 9780553380163 A Brief History of Time3 9781408839997 The Silk Roads1 9780062316097 SapiensTotal 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 SapiensAnd 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 SUPPLYDelivery note DN-2026-0431 3 September 2026
Deliver to: Kewos Public Library
Qty ISBN Title
2 9780441013593 Dune
1 9780553380163 A Brief History of Time3 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.