DEV Community

Cover image for Four different things called "rotate PDF", and only one of them is lossless
Enlh NG
Enlh NG

Posted on Edited on

Four different things called "rotate PDF", and only one of them is lossless

A scanner hands you a PDF with page 3 upside-down. Every "rotate PDF" tool fixes that, but they don't all do the same thing. Depending on the tool, "rotate" can mean one of four operations, and they differ in quality, file size, and whether the fix survives when you send the file on.

This is a field guide to the four, plus a way to detect which pages need fixing automatically. Everything here was measured on small test files I generated.

The four things

# What changes Permanent? Touches the pixels?
1 The viewer's display (the rotate button in your PDF viewer) No, it lasts for that session No
2 The page's /Rotate value (one number in the page dictionary) Yes, saved in the file No
3 The image itself, decoded, rotated and re-encoded (what you get when a tool re-renders pages) Yes Yes
4 The page content stream, wrapped in a transform matrix Yes No, but I didn't test this one

Number 1 is why "I rotated it and it's sideways again for the next person". Numbers 2 and 3 are what most tools do, and the difference between them is the interesting part.

2 vs 3: what rotation costs

My test file has 3 pages, each a single full-page JPEG, like a scanner's output (1.29 MB).

Setting /Rotate to 90 on every page:

  • the file size went from 1,356,310 to 1,356,023 bytes (the few bytes of difference come from the library rewriting the file);
  • the embedded image bytes were byte-for-byte identical (checked by MD5);
  • after four 90° turns the page looked identical to the original (SSIM 1.0 on a render).

Rotating the pixels instead (decode, rotate, save as JPEG q85):

  • the image data is different, because it was compressed again;
  • after one turn, PSNR against a lossless rotation was 39.2 dB; after four turns 36.3 dB; after 24 turns 32.3 dB. At q60 it went from 33.1 dB down to 25.2 dB.

PSNR versus number of pixel rotations

One extra rotation at q85 isn't visible to the eye. But a tool that rotates pixels, run again and again on the same file (rotate, fix another page, rotate back), is quietly re-compressing it each time, and the tool's quality setting becomes a setting you didn't mean to touch. If a tool says it rotates without re-encoding, it's doing number 2.

Does /Rotate behave the same everywhere?

Mostly. I checked three things where I expected trouble.

Odd values. The spec wants a multiple of 90. I wrote six values into a page and read them back with three tools:

Value written pypdf PyMuPDF pdfinfo (poppler)
90 90 90 90
360 360 0 0
450 450 90 90
-90 -90 270 270
45 45 0 45
100 100 0 100

The first four rows describe the same page orientation in every tool (pypdf reports the number as written, the others normalise it). The last two are invalid, and the tools disagree on what to do with them. If you ever write /Rotate yourself, normalise it to 0, 90, 180 or 270.

Inherited /Rotate. The value can live on the parent /Pages node, and pages inherit it. I put /Rotate 90 there, removed it from the page, and extracted page 1 with pypdf, pikepdf, PyMuPDF, qpdf and pdfseparate. All five produced a page that was still rotated by 90. I expected at least one to lose it, and none did.

Text extraction on rotated pages. With /Rotate at 0, 90, 180 and 270, both pdftotext and PyMuPDF returned all 50 lines of my test page intact each time. Rotating by flag doesn't hurt copy and search.

Finding which pages need rotating

"Scanners never get orientation right" is the part worth automating. Tesseract can report the orientation of a page (tesseract page.png - --psm 0, using the osd data file). I ran it on 16 test pages: four kinds of page, each turned by 0, 90, 180 and 270 degrees.

Page kind Orientation detected correctly
Full page of text (34 lines) 4 of 4 (confidence 25 to 34)
3 lines of text 4 of 4 (confidence about 16)
One large word ("INVOICE") 0 of 4, no answer
A photo only 0 of 4, no answer

Detection is solid when the page has real text, and silent when it doesn't. A pipeline has to treat "no answer" as "needs a human", not as "fine".

To apply the fix, add Tesseract's answer to each page's /Rotate:

import pymupdf, subprocess, re

doc = pymupdf.open("scan.pdf")
for page in doc:
    page.get_pixmap(dpi=150).save("p.png")
    out = subprocess.run(["tesseract", "p.png", "-", "--psm", "0"],
                         capture_output=True, text=True)
    m = re.search(r"Rotate:\s+(\d+)", out.stdout + out.stderr)
    if m:                                    # no match = no answer: flag for review
        page.set_rotation((page.rotation + int(m.group(1))) % 360)
doc.save("scan_fixed.pdf")
Enter fullscreen mode Exit fullscreen mode

On a 4-page file turned 0, 90, 180 and 270, the pass applied 0, 270, 180 and 90 (the page turned 90 clockwise needs 270 more), and a second pass over the result reported 0 for all four pages.

What online tools say about rotating (read Nov 2026)

I tested my own script, not these sites. This is what each page states; "not stated" means I didn't find it on the page I read.

Tool Rotation options Limits and sign-in Retention and location Lossless?
Adobe Acrobat online Specific pages or all, left or right Up to 1,500 pages across 100 files, 500 pages per file, 100 MB total; sign-in needed to save Deleted unless you sign in to save Not stated
iLovePDF Select all, portrait or landscape files; right or left; per-file arrows Not stated Not stated Not stated
PDF24 Clockwise or counter-clockwise; per page "No limits and no watermarks"; no registration Deleted after one hour; "in the cloud on our servers" (Germany) Not stated
Smallpdf One page or all; 90, 180 or 270 degrees No size limit stated; no sign-up Files deleted after one hour; location not stated Not stated ("keeps text, images and layout together")
ToolTiny 90° right, 90° left, 180°; all pages only Max size not stated; no account Deleted within 10 minutes (the page says both "from cache" and "from our servers"); processed "in a secure sandbox" after an HTTPS upload The page says it doesn't re-encode, "zero quality loss"

I tried to read Sejda's page and my fetch failed, so I'm making no claims about it.

The "Lossless?" column is the one that matters after this post, and it's mostly empty. Only one page here makes a claim about re-encoding, and I can't verify it from outside. You can check any tool yourself in a minute: rotate a copy, extract the image from the output with PyMuPDF, and compare its hash with the original's. If the image bytes differ, the tool re-encoded it. Also note the options column: "all pages only" and "per page" suit different scans. One sideways page in a 50-page file needs per-page rotation.

Which one should I use?

  1. Mixed orientations in a scan: per-page rotation, then check each result.
  2. Whole file turned the same way: any tool works; prefer one that doesn't re-encode.
  3. Many files: detect with OSD, apply /Rotate, and send anything with no answer to a person.
  4. Sending the file on: a viewer's rotate button doesn't travel with the file. Save the rotation.
  5. Pages with no text (photos, drawings, a single big word): don't trust automatic detection.

Limits of this post

  • Synthetic files: a three-page JPEG scan, a ten-page text file, and four kinds of generated page. Real scans have skew and noise that make orientation detection harder.
  • OSD results are from one Tesseract version with the default osd data and English text.
  • PSNR figures use one image and one JPEG encoder (Pillow); other encoders differ.
  • I did not test the fourth method (content-stream transforms), or annotations and bookmarks on rotated pages.
  • Vendor statements are as they appeared on the day I read them.

If you have a scan where orientation detection got it wrong, tell me the page type (no text, handwriting, table-only) in the comments.

Top comments (0)