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.io API.

The Quick Solution

Here's what we're building—a script that takes a user-uploaded PDF and rotates specific pages:
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

Imagine you're building a document review system. Users upload scanned contracts via their mobile phones:
  • 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

You'll need an API token to authenticate your requests.
  1. Go to aPDF.io.
  2. Sign up (it's completely free).
  3. Copy your API Token from the dashboard.

Next, make sure you have the requests library installed:

pip install requests

Step 2: Understanding Rotation Angles

The API supports four 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

Create a file named fix_rotations.py. This script takes a PDF URL, applies multiple rotation operations, and returns the corrected file.
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

The API supports flexible page selection beyond simple numbers:
# 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

In a real application, you might process multiple user uploads in a queue. Here's a practical example:
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?

Now that you can fix PDF orientations with simple API calls, here are some related workflows:
  • 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.
Ready to build?
Get your free API token here
Most APIs charge you per document. aPDF.io is built to be a developer-friendly, free alternative that handles the heavy lifting without the monthly subscription.