Fix Sideways PDF Scans: Auto-Rotate Pages with Python
Ever had users upload documents from their phones, only to find pages 2 and 4 rotated sideways or upside down? It's frustrating when you're trying to review contracts, invoices, or forms—constantly tilting your head or manually rotating pages.
This happens all the time with mobile uploads. A user scans a multi-page document on their phone, but some pages end up in landscape orientation while others are portrait, or worse—completely upside down.
In this tutorial, we'll build a Python script that fixes these orientation issues by rotating them to the correct position using the free Apdf API.
The Quick Solution
import requests
API_TOKEN = "YOUR_API_TOKEN_HERE"
API_URL = "https://apdf.io/api/pdf/page/rotate"
# The problematic PDF (uploaded by user)
pdf_url = "https://example.com/uploaded_scan.pdf"
response = requests.post(
API_URL,
headers={
'Authorization': f'Bearer {API_TOKEN}',
'Accept': 'application/json'
},
data={
'file': pdf_url,
'rotations[0][angle]': '+90', # Rotate pages 2,4 clockwise
'rotations[0][pages]': '2,4',
'rotations[1][angle]': '+180', # Flip page 5 upside down
'rotations[1][pages]': '5'
}
)
result = response.json()
print(f"Fixed PDF: {result['file']}")
That's it. You now have a corrected PDF where all pages face the right direction. Let's break down how this works.
Real-World Scenario
- Page 1: Portrait, correct orientation ✓
- Page 2: Landscape, needs 90° clockwise rotation
- Page 3: Portrait, correct orientation ✓
- Page 4: Landscape, needs 90° clockwise rotation
- Page 5: Upside down, needs 180° rotation
Your reviewers waste time manually rotating each page in their PDF viewer. Instead, we'll fix these issues before the document even hits the review queue with one simple API call.
Step 1: Get Your API Token
- Go to Apdf.
- Sign up (it's completely free).
- Copy your API Token from the dashboard.
Next, make sure you have the requests library installed:
pip install requests
Step 2: Understanding Rotation Angles
- +90 (or -270): Rotate 90° clockwise
- +180 (or -180): Flip upside down
- +270 (or -90): Rotate 90° counter-clockwise
- 0: No rotation (useful for normalization)
The + or - prefix is required. Most mobile scan issues need +90 (portrait → landscape) or +180 (upside down pages).
Step 3: Build the Rotation Script
import requests
# Your API credentials
API_TOKEN = "YOUR_API_TOKEN_HERE"
API_URL = "https://apdf.io/api/pdf/page/rotate"
def fix_pdf_rotations(pdf_url, rotation_rules):
\"\"\"
Fix PDF page orientations by applying rotation rules.
Args:
pdf_url: URL of the PDF to fix
rotation_rules: List of dicts with 'angle' and 'pages' keys
Returns:
URL of the corrected PDF
\"\"\"
# Build the request payload
payload = {'file': pdf_url}
for idx, rule in enumerate(rotation_rules):
payload[f'rotations[{idx}][angle]'] = rule['angle']
payload[f'rotations[{idx}][pages]'] = rule['pages']
# Make the API request
response = requests.post(
API_URL,
headers={
'Authorization': f'Bearer {API_TOKEN}',
'Accept': 'application/json'
},
data=payload
)
if response.status_code == 200:
result = response.json()
return result['file']
else:
raise Exception(f"API Error {response.status_code}: {response.text}")
# Example: Fix a document with multiple rotation issues
problematic_pdf = "https://ontheline.trincoll.edu/images/bookdown/sample-local-pdf.pdf"
# Define which pages need rotation and by how much
rotation_rules = [
{'angle': '+90', 'pages': '2'}, # Page 2 is sideways (landscape)
{'angle': '+180', 'pages': 'z'}, # Last page is upside down
]
print("Fixing PDF rotations...")
try:
corrected_url = fix_pdf_rotations(problematic_pdf, rotation_rules)
print(f"\\nSuccess! Corrected PDF available at:")
print(corrected_url)
print("\\nNote: This URL is valid for 1 hour.")
except Exception as e:
print(f"Error: {e}")
Run the script
python fix_rotations.py
Expected Output
Fixing PDF rotations...
Success! Corrected PDF available at:
https://apdf-files.s3.eu-central-1.amazonaws.com/a3f8d92e1b4c5671.pdf
Note: This URL is valid for 1 hour.
You can now email this URL to users, save it to your database, or download it for further processing. Remember that the URL expires after 1 hour, so download and store the file if you need long-term access.
Advanced: Page Selection Syntax
# Single pages
'pages': '2' # Just page 2
# Multiple specific pages
'pages': '2,4,7' # Pages 2, 4, and 7
# Page ranges
'pages': '2-5' # Pages 2 through 5
# Last page shorthand
'pages': 'z' # The last page
# Reverse counting
'pages': 'r1-r3' # Last 3 pages (counting backwards)
# Combined syntax
'pages': '1,3-5,z' # Page 1, pages 3-5, and the last page
Production Use Case: Batch Processing
import requests
from concurrent.futures import ThreadPoolExecutor
API_TOKEN = "YOUR_API_TOKEN_HERE"
API_URL = "https://apdf.io/api/pdf/page/rotate"
def process_upload(upload):
\"\"\"Process a single user upload with known rotation issues.\"\"\"
try:
response = requests.post(
API_URL,
headers={
'Authorization': f'Bearer {API_TOKEN}',
'Accept': 'application/json'
},
data={
'file': upload['url'],
'rotations[0][angle]': '+90',
'rotations[0][pages]': upload['landscape_pages'],
}
)
if response.status_code == 200:
result = response.json()
print(f"✓ Fixed: {upload['filename']}")
return {'filename': upload['filename'], 'fixed_url': result['file']}
else:
print(f"✗ Failed: {upload['filename']} - {response.status_code}")
return None
except Exception as e:
print(f"✗ Error: {upload['filename']} - {str(e)}")
return None
# Simulate a batch of user uploads with rotation issues
user_uploads = [
{
'filename': 'contract_001.pdf',
'url': 'https://example.com/uploads/contract_001.pdf',
'landscape_pages': '2,4'
},
{
'filename': 'invoice_452.pdf',
'url': 'https://example.com/uploads/invoice_452.pdf',
'landscape_pages': '1,z'
},
{
'filename': 'form_w9.pdf',
'url': 'https://example.com/uploads/form_w9.pdf',
'landscape_pages': '3-5'
}
]
print(f"Processing {len(user_uploads)} documents...\\n")
# Process uploads in parallel (up to 5 at a time)
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(process_upload, user_uploads))
# Filter out failed uploads
successful = [r for r in results if r is not None]
print(f"\\nCompleted: {len(successful)}/{len(user_uploads)} documents fixed")
This script processes multiple PDFs concurrently, making it perfect for background job queues in production systems.
What's Next?
- Extract specific pages: After rotation, pull out individual pages using the Extract Pages endpoint.
- Add watermarks: Stamp "REVIEWED" or "APPROVED" on corrected documents with the Overlay endpoint.
Related tutorials
Add Page Numbers and Headers to PDFs with Python
Add consistent page numbers, headers, or footers to existing PDF documents using Python and the Apdf Overlay API. Perfect for preparing reports for distribution without regenerating the original documents.
Convert Scanned PDFs to Searchable Documents with Python
Transform image-based scanned PDFs into fully searchable documents using Python and the Apdf OCR API. Perfect for digitizing paper archives, enabling text selection, and making legacy documents ready for modern search systems.
Building a "Search Inside PDF" Feature for Your Web App
This tutorial shows how to build a PDF search feature using Python and the Apdf Search API. Find specific terms, get page numbers and context lines, and optionally use regex patterns for advanced matching. Includes a Flask web interface example.
Your code made the PDF.
Then it went dark.
Opened, read, re-read, dropped on page 4 — you never see any of it. Share the PDFs you generate through Apdf recipient links, and every signal becomes something you can act on: ping Slack, update the CRM, let an agent follow up. Same account, same API token, one more call.
API · Webhook · MCP