Examples
Every example below runs the exact same conversion twice: once as a
CLI invocation, once as a Library call. Pick the tab
that matches how you're using scanlayer. Flags and keyword arguments are named
identically wherever possible (--lang / lang=,
--dpi / dpi=, and so on).
Single file → searchable PDF
The default case: one scanned image or photo in, one searchable PDF out.
scanlayer invoice.jpg -o invoice.pdf --lang fra+eng --dpi 300
import scanlayer
result = scanlayer.convert(
"invoice.jpg", "invoice.pdf",
lang="fra+eng", dpi=300,
)
print(f"{result.words_count} words, {result.mean_confidence:.1f}% confidence")
print(f"PSM {result.best_psm} won in {result.elapsed_ms:.0f} ms")
--dpi/dpi= and logs the fact. The flag only matters for images with no embedded resolution (most phone photos).
Batch conversion
Multiple inputs, one output per input, glob patterns and mixed extensions welcome. A bad file in the batch never aborts the rest.
scanlayer *.jpg -o ./converted/
import scanlayer
result = scanlayer.convert_batch(["*.jpg"], "./converted/")
print(f"{result.succeeded} ok, {result.failed} failed")
for path, err in result.failures:
print(f" {path}: {err}")
The CLI signals a partial failure with exit code 5; the library never raises
for a per-file problem, check BatchResult.failures instead. See
exit codes and
convert_batch().
Merge several pages into one PDF
Combine multiple images, in argument order, into a single multi-page searchable PDF. PDF-only: there's no sensible multi-page schema for txt/json/tsv/hocr.
scanlayer page1.jpg page2.jpg page3.jpg -o report.pdf --merge
import scanlayer
result = scanlayer.convert_merge(
["page1.jpg", "page2.jpg", "page3.jpg"], "report.pdf",
)
print(result.words_count, "words across", result.column_count, "max columns")
Full detail, including feeding it a native multi-page PDF instead of separate images, is in Multi-Page and Merge.
Exporting txt / json / tsv / hocr instead of a PDF
Same OCR pipeline, no PDF built at all, useful when you only want the recognized text and its coordinates/confidence.
scanlayer invoice.jpg -o invoice.json --format json
import scanlayer
result = scanlayer.convert(
"invoice.jpg", "invoice.json", output_format="json",
)
import json
with open("invoice.json") as f:
payload = json.load(f)
print(payload["text"][:120])
Schema for each of the four formats is in Output Formats.
Controlling orientation correction
Three modes: automatic (default), fully disabled, or an exact manual angle.
# Disable EXIF + OSD + deskew entirely, use the page exactly as loaded
scanlayer scan.jpg -o scan.pdf --orientation none
# Skip auto-detection, force a known angle (clockwise degrees)
scanlayer scan.jpg -o scan.pdf --orientation 10
import scanlayer
# Automatic (default): EXIF tag, then OSD, then a fine deskew
scanlayer.convert("scan.jpg", "scan.pdf")
# Disabled: use the pixels exactly as loaded
scanlayer.convert("scan.jpg", "scan.pdf", orientation="none")
# Manual: skip detection, rotate exactly 10° clockwise
scanlayer.convert("scan.jpg", "scan.pdf", orientation=10)
Constrained fields: whitelist / blacklist / PSM
Narrowing what Tesseract is allowed to recognize helps a lot on structured fields like invoice numbers, reference codes, or amounts.
scanlayer reference-code.jpg -o code.txt --format txt --whitelist 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ- --psm 7
import scanlayer
result = scanlayer.convert(
"reference-code.jpg", "code.txt", output_format="txt",
char_whitelist="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-",
)
# For a single, known text layout, pin the PSM globally instead of
# letting all 4 candidates race:
scanlayer.configure(psm_candidates=[7]) # 7 = single text line
Debug overlay for a run that looks wrong
scanlayer invoice.jpg -o invoice.pdf --debug-image
# also writes invoice_debug.png next to invoice.pdf
import scanlayer
result = scanlayer.convert(
"invoice.jpg", "invoice.pdf", debug_image=True,
)
print("overlay saved to:", result.debug_image_path)
What the colors and header band mean is in Debug Visualization.
Applying a saved configuration profile
scanlayer invoice.jpg -o invoice.pdf --config settings.json
import scanlayer
scanlayer.configure_from_file("settings.json")
# per-call overrides still win over the profile:
scanlayer.configure(lang="eng")
scanlayer.convert("invoice.jpg", "invoice.pdf")
{
"tesseract_cmd": "/usr/bin/tesseract",
"lang": "fra+eng",
"default_dpi": 300,
"min_word_confidence": 40,
"psm_candidates": [3, 4, 6, 11],
"multi_column_detection": true,
"jpeg_quality": 82,
"log_level": "INFO"
}
Full precedence rules (call args > configure() > env vars > defaults)
are in Configuration.
Handling errors the way the library expects
There is no library-only equivalent for this one: exit codes are the CLI's version of the same exception hierarchy. See the mapping in CLI Reference.
import scanlayer
from scanlayer.utils.validators import ValidationError
from scanlayer.utils.errors import PipelineError, BlankPageDetectedError
try:
result = scanlayer.convert("maybe_blank.jpg", "out.pdf")
except BlankPageDetectedError:
# page looked blank; force it through as a background-only PDF
result = scanlayer.convert("maybe_blank.jpg", "out.pdf", force=True)
except ValidationError as exc:
print(f"bad input or environment, nothing ran: {exc}")
except PipelineError as exc:
print(f"started processing and failed partway through: {exc}")
Native PDF as input
Feed scanlayer an existing (non-searchable) PDF directly; every page is rasterized with poppler first, then processed like any image.
# one output file per page
scanlayer scanned-report.pdf -o ./pages/
# OR re-run OCR over it and get a single PDF back
scanlayer scanned-report.pdf -o ocred-report.pdf --merge
# convert_batch() rasterizes .pdf inputs automatically
import scanlayer
result = scanlayer.convert_batch(["scanned-report.pdf"], "./pages/")
# convert()/convert_merge() take already-rasterized image paths;
# use convert_batch(merge=True) to go straight from a PDF to one
# merged, re-OCR'd PDF:
scanlayer.convert_batch(
["scanned-report.pdf"], "ocred-report.pdf", merge=True,
)
DependencyError rather than a raw subprocess failure.
Advanced: running one pipeline stage at a time
For programs that want to swap in their own logic for one stage, a custom layout
analyzer instead of reorder_reading_order(), for example, or writing OCR
words somewhere other than disk before deciding whether to build a PDF at all.
from PIL import Image
from scanlayer.utils.validators import validate_all
from scanlayer.preprocessing.enhance import preprocess
from scanlayer.ocr.engine import extract_words
from scanlayer.layout.columns import reorder_reading_order
from scanlayer.pdf.builder import build_searchable_pdf
input_abs, output_abs = validate_all("invoice.jpg", "invoice.pdf", "pdf")
with Image.open(input_abs) as img:
image = img.copy()
result = preprocess(image, dpi=300, orientation=None)
ocr_result = extract_words(
result.ocr_image, result.ocr_scale,
effective_dpi=result.effective_dpi, lang="fra+eng",
)
words, column_count = reorder_reading_order(
ocr_result.words, page_width=result.background.width,
)
build_searchable_pdf(result.background, words, output_abs, dpi=300)
Every function used above is documented, with its dataclass fields, in Library API → Low-level API.