scanlayer
Docs / Overview

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.

bash
scanlayer invoice.jpg -o invoice.pdf --lang fra+eng --dpi 300
pythonconvert_one.py
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 is usually detected for you If the source image carries DPI metadata, scanlayer uses that value instead of --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.

bash
scanlayer *.jpg -o ./converted/
pythonconvert_batch_example.py
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.

bash
scanlayer page1.jpg page2.jpg page3.jpg -o report.pdf --merge
pythonmerge_pages.py
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.

bash
scanlayer invoice.jpg -o invoice.json --format json
pythonexport_json.py
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.

bash
# 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
pythonorientation.py
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.

bash
scanlayer reference-code.jpg -o code.txt --format txt --whitelist 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ- --psm 7
pythonconstrained_field.py
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

bash
scanlayer invoice.jpg -o invoice.pdf --debug-image
# also writes invoice_debug.png next to invoice.pdf
pythondebug_overlay.py
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

bash
scanlayer invoice.jpg -o invoice.pdf --config settings.json
pythonapply_profile.py
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")
jsonsettings.json
{
  "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.

pythonhandle_errors.py
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.

bash
# 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
pythonpdf_input.py
# 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,
)
Requires poppler Native PDF input needs the poppler binaries on PATH (see Installation). Without them scanlayer raises 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.

pythoncustom_pipeline.py
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.