Tools,

OCRmyPDF --deskew not working? Check the tilt

2.2%

of words found by --deskew at 10 degrees

Try it on your PDF, $0.006 a page

--deskew fixed 3 degrees, not 7

Words found with --deskew, 12 tilted pages
3 degrees
94.3%
4 degrees
87.9%
5 degrees
84.7%
6 degrees
42.7%
7 degrees
5.5%
10 degrees
2.2%

Every one of these runs exited 0. At 10 degrees, 11 of 12 pages came back with no text.

Check the angle it used:

ocrmypdf -v 1 --deskew in.pdf out.pdf 2>&1 | grep "Deskew angle"
    9 Deskew angle: 0.000
   10 Deskew angle: 0.000
   11 Deskew angle: 0.000
   12 Deskew angle: 0.000

Pages 1 to 8 printed no angle line, only [tesseract] Empty page!!. Both mean the page was not turned.

Why: Tesseract sees no text lines

OCRmyPDF asks Tesseract for the angle with --psm 2. On a page tilted 10 degrees, Tesseract found nothing:

tesseract page.png stdout --psm 2
Empty page!!

OCRmyPDF then uses an angle of 0. At 3 degrees, Tesseract printed Deskew angle: 0.0442, in radians: about 2.5 degrees.

Fix: straighten the page first

My script tries every angle from -45 to 45 degrees, turns each page, and saves a new PDF. It needs PyMuPDF, Pillow and NumPy.

python straighten.py in.pdf straight.pdf
ocrmypdf --rotate-pages --deskew straight.pdf out.pdf
straighten.py, 33 lines
import io
import sys

import numpy as np
import pymupdf
from PIL import Image


def sharpness(ink, angle):
    rows = np.asarray(ink.rotate(float(angle))).sum(axis=1, dtype=float)
    return ((rows[1:] - rows[:-1]) ** 2).sum()


def find_angle(gray):
    small = gray.resize((gray.width // 4, gray.height // 4))
    ink = small.point(lambda v: 255 if v < 128 else 0)
    rough = max(range(-45, 46), key=lambda a: sharpness(ink, a))
    fine = np.arange(rough - 1, rough + 1.25, 0.25)
    return float(max(fine, key=lambda a: sharpness(ink, a)))


src, dst = pymupdf.open(sys.argv[1]), pymupdf.open()
for number, page in enumerate(src, 1):
    pix = page.get_pixmap(dpi=300, colorspace=pymupdf.csGRAY)
    gray = Image.frombytes("L", (pix.width, pix.height), pix.samples)
    angle = find_angle(gray)
    print(f"page {number}: turned {angle:+.2f} degrees")
    gray = gray.rotate(angle, Image.BICUBIC, expand=True, fillcolor=255)
    jpeg = io.BytesIO()
    gray.save(jpeg, "JPEG", quality=90, dpi=(300, 300))
    new = dst.new_page(width=gray.width * 0.24, height=gray.height * 0.24)
    new.insert_image(new.rect, stream=jpeg.getvalue())
dst.save(sys.argv[2])
Words found, 12 pages, straightened first
Tilt Straighten first
5 degrees 94.7%
7 degrees 94.7%
10 degrees 92.4%
15 degrees 91.1%
30 degrees 81.6%
45 degrees 80.4%

--deskew found 0.1% or less from 15 degrees up. Tilted the other way, straightening stayed within 1 point.

5.4 to 13.0 s the whole fix, 12 pages

3.0 to 5.8 s --deskew alone

Sideways pages need --rotate-pages

Page --deskew --rotate-pages
Turned 90 degrees 88.9% 94.3%
Upside down 0.3% 94.4%
Turned 270 degrees 0.5% 94.2%

Sideways and tilted too? Use both flags. On pages turned 93 degrees, --rotate-pages alone found 61.0%, and with --deskew 93.6%.

My script first gave 91.5% on those pages.

Want text, not a PDF? Try our Actor

PDF OCR to Markdown, our Apify Actor, turns sideways pages upright and straightens tilts up to 5 degrees by itself.

Actor build 0.1.35, words found
Pages Actor
0 to 5 degrees 94.0% to 94.4%
7 degrees 83.7%
10 degrees 10.9%
10 degrees, straightened first 92.9%
Sideways or upside down 94.4%

Questions people ask

My scans have a typed stamp. Which flags?

Use --force-ocr --deskew. On my stamped scans it found 94.3% of the words, and the typed line on 10 of 12 pages. --skip-text --deskew skipped every page, and --redo-ocr refuses --deskew.

Does the script keep color?

No. It saves gray JPEG pages at 300 dpi, quality 90. Keep your original file.

What does --rotate-pages-threshold do?

OCRmyPDF turns a page only if Tesseract is this sure. The default is 14. My pages turned 90 degrees scored 18.31 to 33.37, so all 12 turned.

Method, limits and sources

How I tested it

I took 12 printed book pages, 4 from each of 3 Wikisource scans, and turned each by a known angle: 0 to 45 degrees both ways, then 90, 180 and 270.

Each PDF ran through OCRmyPDF at its default settings, one run at a time. Words found counts the true words in any order, read with pdftotext, against the proofread Wikisource text.

The Actor is build 0.1.35's own page code, run on this Mac. Scripts and results are in research/bench/ocrmypdf-deskew-2026-11-02/.

What I did not test
  • Real scanner tilt. Mine is a clean turn of a flat page.
  • --clean: it needs unpaper, which I did not install.
  • Color pages, photos and forms.
  • Windows, Linux and Docker.
Versions and sources