tidyscan
Scanned pages turned upright and straightened into a PDF, losslessly, and optionally made searchable.
- Upright: Tesseract's orientation detection decides the quarter turn, only when it is sure and the script is one you allow (Latin by default); an unsure upside-down page is settled by reading it both ways round.
- Straight: a page laid crooked on the glass (up to 45 degrees) is measured on its text lines and checked by measuring again.
- Lossless: JPEG pages go into the PDF byte for byte, 1-bit pages
Flate-compressed; the turn is the page's
/Rotate, the skew a drawing matrix. Nothing is resampled. Standard library only. - Searchable (optional): a text layer by OCRmyPDF, read by Tesseract, or by PaddleOCR (PP-OCRv5 on OpenVINO, CPU) for receipts.
When anything is unsure, the page is kept as scanned. Written for the scans of scankeyd (the Scan key of Brother network printers to a folder), and usable for any scanned pages.
Install
pip install tidyscan # pages to PDF only
pip install 'tidyscan[straighten]' # plus Pillow: turning upright and straightening
pip install 'tidyscan[ocr]' # plus OCRmyPDF: the text layer
pip install 'tidyscan[paddle]' # plus RapidOCR and OpenVINO: PaddleOCR
Python 3.11 or newer, on Linux. Turning and straightening also need the
tesseract program with its osd data, OCR its language data (Debian:
tesseract-ocr tesseract-ocr-osd tesseract-ocr-nld ...). PaddleOCR needs its
models once (checked by sha256, never downloaded by themselves):
tidyscan models fetch # into ~/.local/share/tidyscan/models
tidyscan models path # where they are looked up, and whether they are there
Use
tidyscan --ocr eng+nld -o out.pdf page1.jpg page2.pbm
tidyscan --ocr nld --engine paddle -o receipt.pdf receipt.jpg
JPEG and PBM pages go in as they are. Other formats (PNG, TIFF, every page of
a multi-page TIFF) are read with Pillow and converted first, to PBM when
black-and-white, else to JPEG (quality 95): for those the PDF holds the
conversion. Options: --dpi (300), --no-rotate, --no-deskew,
--min-confidence (3), --engine, -q. Exit code 0, or 1 for a page that is
not a JPEG or PBM; an OCR failure writes the PDF without text and says so.
import tidyscan
from tidyscan import ocr, pdf
pages = [open("page1.jpg", "rb").read()]
turns, skews = zip(*(tidyscan.straighten(p, 300) for p in pages))
data = pdf.build(pages, 300, list(turns), list(skews))
data = ocr.add_text_layer(data, "eng+nld") # raises ocr.OcrError; engine="tesseract" (default), "paddle", "auto"
A page is JPEG bytes, binary PBM (P4) bytes, or an object with to_pbm().
straighten() never raises and logs to the tidyscan logger; turn= takes a
turn already known (from the scanner), budget= the seconds of Tesseract it
may spend on a page (300; what is not measured by then is kept as scanned).
Which OCR engine
| engine | words found | words right | 1 page | each further page | memory |
|---|---|---|---|---|---|
Tesseract 5.5 (--engine tesseract, the default) |
90% | 87% | 3.6 s | 2.7 s | 0.15 GB |
PaddleOCR PP-OCRv5 (--engine paddle) |
83% | 92% | 6.1 s | 9.9 s | 0.9 GB |
Tesseract reads letters, documents, forms and book pages better, faster and
in less memory. PaddleOCR reads receipts, photo captions and tables better
(it adds far fewer wrong words on tables), and phone photos of receipts that
Tesseract cannot read at all. --engine auto takes PaddleOCR when it is
installed and the languages are Latin-script ones, else Tesseract; Tesseract
also takes over a document PaddleOCR fails on. PaddleOCR's Latin recogniser
reads Dutch, English and some 30 other languages at once. Measured on 45 pages
in 188 scan-like variants, through the whole pipeline, on 2 cores of an Intel
N150: docs/benchmarks.md.
As a service: tidyscan watch
tidyscan watch IN OUT turns document folders into PDFs, oldest first: a
folder IN/<function>/<name>/ with the pages (page-0001.jpg,
page-0002.pbm, ...) and a document.json (optional "resolution", default
300; "pages", default the page files in order; "ocr": true for a text
layer), as scankeyd writes them.
Each becomes OUT/<name>.pdf, written as a hidden .part file and renamed;
then the folder goes. A document that cannot be read moves to IN/.failed/;
when OCR fails, the PDF is written without text. IN and its function folders
must be writable for this user, as each folder goes once its PDF is there.
| Option | Environment | Default | |
|---|---|---|---|
--ocr LANGS |
OCR_LANGUAGES |
eng |
text layer languages, e.g. eng+nld |
--engine |
OCR_ENGINE |
tesseract |
tesseract, paddle or auto |
--ocr-folders |
OCR_FOLDERS |
ocr |
for documents without an "ocr" field: the function folders that get a text layer (comma list, all, or empty for none) |
--no-rotate, --no-deskew |
ROTATE, DESKEW |
auto |
on, off or auto (on when Tesseract is there) |
--pdfa |
PDFA |
off |
PDF/A-2b for the documents with a text layer (still lossless: 1-bit pages become CCITT G4) |
--status FILE |
STATUS_FILE |
none | a JSON status file for a monitor: when, last_ok, last_file, last_error, failed, waiting, and the counts pdfs, unreadable, no_text |
--keep-folders |
off | OUT/<function>/<name>.pdf |
|
--interval |
5 | seconds between looks at IN | |
--once |
convert what is there, then stop |
On/off settings take on/off, true/false, yes/no or 1/0; any
other value stops the start with a message.
Docker: three images, built from this repository (docker build ...):
--target base (pages to PDF only, ~77 MB, no copyleft software), the default
full (with Tesseract and OCRmyPDF, ~480 MB) and --target paddle (with
PaddleOCR, ~1 GB, amd64 only). They run tidyscan watch /in /out as uid 1000:
docker build -t tidyscan .
docker run -d --network none --read-only --tmpfs /tmp --env-file scan.env \
-v ./scans/new:/in -v ./scans/pdf:/out tidyscan
OCR languages for the image: --build-arg OCR_LANGS="eng nld deu fra".
Limits
- Turning upright only for scripts you allow (Latin by default); skew up to 45 degrees. Photos and nearly blank pages are kept as scanned, and so are phone photos of receipts (Tesseract finds no text lines on them).
- At most 5 minutes of Tesseract per page; a page above 200 million pixels is not read at all.
- OCRmyPDF gets 600 s per document: a long document read by PaddleOCR (about 10 s a page) may run out and is then read by Tesseract.
- PaddleOCR: Latin script only; OCRmyPDF must run in the same Python as
tidyscan and RapidOCR (the engine is the OCRmyPDF plugin
tidyscan.ocrmypdf_paddle, also usable on its own:ocrmypdf --plugin tidyscan.ocrmypdf_paddle).TIDYSCAN_PADDLE_THREADSsets its threads.
How well it works
Measured with tests/eval_deskew.py on 1946 cases from 69 pages, each turned,
tilted or mirrored by a known amount (06-11-2026):
- Upright: of 381 text pages that needed a turn, 96% upright and none
turned the wrong way (OCRmyPDF's
--rotate-pages: 29%; Tesseract OSD alone: 77%, with 53 wrong turns). No upright page was turned away. - Straight: within 0.5 degrees on 100%, 100% and 96% of the pages tilted
slightly, medium and steeply. jdeskew with
angle_max=45measures as well but turns no page upright; it, OCRmyPDF, ImageMagick, unpaper and Leptonica straighten by rotating the image, which resamples it.
Details, the other tools and the pages: docs/benchmarks.md.
Development
With uv:
uv sync
uv run ruff check && uv run ruff format --check && uv run ty check
uv run python -m unittest tests.test_tidyscan
Tests that need Tesseract, OCRmyPDF or the PaddleOCR models are skipped when
those are missing. The benchmarks (tests/eval_*.py) download their pages and
are not part of the unit tests.
Licence
BSD-3-Clause, see LICENSE; the repository follows REUSE. Written by Luuk Visser with Claude Code (Claude Opus 5.5).
What the optional parts use is not included in this package: OCRmyPDF
(MPL-2.0), Tesseract, RapidOCR, OpenVINO, OpenCV and PaddleOCR's PP-OCRv5
models (Apache-2.0; the models come with their own notice). The full and
paddle images add Debian packages under their own licences, among them
Ghostscript (AGPL-3.0, used by OCRmyPDF); the base image has no copyleft
software.
Metadata
Release files for tidyscan 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| tidyscan-0.3.0.tar.gz | 124.0 kB | Without provenance | Details for tidyscan-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tidyscan-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for tidyscan-0.3.0-py3-none-any.whl |
Total release size: 167.4 kB
Release files / tidyscan-0.3.0.tar.gz
| Download URL | tidyscan-0.3.0.tar.gz |
|---|---|
| Size | 124.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
27e3561b97df4b905f8b521a82362d2639bbecd6371ef2d5bf3d545c97fb76f9
|
|
BLAKE2b-256 checksum How to use checksums |
0244fb94aaa39731a28ff3f174fa16d81aa898db94443eb1f164bebda43019b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / tidyscan-0.3.0-py3-none-any.whl
| Download URL | tidyscan-0.3.0-py3-none-any.whl |
|---|---|
| Size | 43.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d1e8f23b3a4654fe61ef0da9dab5d0d1109e35a784046d95aca4d0ca1ad4e2c4
|
|
BLAKE2b-256 checksum How to use checksums |
754eb623756d2a6046f2959dc527b54b5fce037fe76c1c4e056394eb155fdc2b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for tidyscan 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.