NewConnect Claude, Gemini, ChatGPT, and other AI agents to API Ninjas via our MCP server

How to OCR a PDF

Image to Text APIUpdated August 28, 2026
Create a free API key and it will be pre-filled into every code sample on this page. No credit card required.
To OCR a PDF, render each page to an image and run optical character recognition on it — scanned PDFs have no text layer, so ordinary extraction returns nothing. The pipeline in this guide renders pages at 300 DPI with pypdfium2, recognizes them with the Image to Text API, and reassembles the words in reading order.

Prerequisites

You need three things:

  • Python 3.8 or newer.
  • pypdfium2 (Chrome's PDF engine, pip-installable with no system dependencies) and requests.
  • A free API Ninjas API key — issued instantly, no credit card; existing accounts find it on the profile page.
shell
pip install pypdfium2 requests

Step 1: Confirm the PDF actually needs OCR

OCR is the slow, lossy path — only take it when there is no text layer to extract. A practical threshold: fewer than ~50 characters per page means scanned:

needs_ocr.py
from pypdf import PdfReader reader = PdfReader('document.pdf') text = ''.join((page.extract_text() or '') for page in reader.pages) needs_ocr = len(text.strip()) < 50 * len(reader.pages) print('Needs OCR' if needs_ocr else 'Has a text layer — extract it instead')

If a text layer exists, follow How to Convert a PDF to Text instead — extraction is instant and exact.

Step 2: Render pages to images

OCR engines read pixels, so the first job is turning each page into a high-resolution image. PDF points are 1/72 of an inch, so scale=300/72 renders at 300 DPI — the accuracy sweet spot:

render.py
import pypdfium2 as pdfium pdf = pdfium.PdfDocument('scanned.pdf') print(f'{len(pdf)} pages') # Render page 1 at 300 DPI (PDF points are 1/72 inch) image = pdf[0].render(scale=300 / 72).to_pil() image.save('page1.png')

Step 3: Recognize the text

POST each rendered page to /v1/imagetotext as multipart form data. The full pipeline — render, recognize, join, with per-page progress:

ocr_pdf.py
import io import os import pypdfium2 as pdfium import requests session = requests.Session() session.headers['X-Api-Key'] = os.environ.get('API_NINJAS_KEY', 'YOUR_API_KEY') OCR_URL = 'https://api.api-ninjas.com/v1/imagetotext' def ocr_page(image) -> list: """OCR one PIL image; returns the API's word list.""" buffer = io.BytesIO() image.save(buffer, format='PNG') buffer.seek(0) response = session.post( OCR_URL, files={'image': ('page.png', buffer, 'image/png')}, timeout=30, ) response.raise_for_status() return response.json() def ocr_pdf(path: str) -> str: pdf = pdfium.PdfDocument(path) page_texts = [] for index in range(len(pdf)): image = pdf[index].render(scale=300 / 72).to_pil() words = ocr_page(image) page_texts.append(' '.join(w['text'] for w in words)) print(f'Page {index + 1}/{len(pdf)}: {len(words)} words') return '\n\n'.join(page_texts) text = ocr_pdf('scanned.pdf') with open('scanned.txt', 'w', encoding='utf-8') as f: f.write(text)

Each request returns the recognized words with pixel bounding boxes:

api.api-ninjas.com
POST/v1/imagetotext
200 OKapplication/json
[ { "text": "INVOICE", "bounding_box": { "x1": 412, "y1": 118, "x2": 604, "y2": 158 } }, { "text": "No.", "bounding_box": { "x1": 412, "y1": 176, "x2": 452, "y2": 200 } }, { "text": "2041", "bounding_box": { "x1": 458, "y1": 176, "x2": 512, "y2": 200 } } ]

Step 4: Rebuild reading order

The bounding boxes are what separate an OCR API from a plain text dump — they let you reconstruct lines, find the value next to a label, or extract tables. Words that share a y position belong to the same line:

lines.py
def words_to_lines(words: list, tolerance: int = 12) -> list: """Group OCR words into reading-order lines by their y position.""" lines = [] for word in sorted(words, key=lambda w: (w['bounding_box']['y1'], w['bounding_box']['x1'])): y = word['bounding_box']['y1'] if lines and abs(lines[-1][0] - y) <= tolerance: lines[-1][1].append(word) else: lines.append((y, [word])) return [ ' '.join(w['text'] for w in sorted(ws, key=lambda w: w['bounding_box']['x1'])) for _, ws in lines ] for line in words_to_lines(ocr_page(image)): print(line)

Step 5: Tune accuracy

FactorRecommendation
Resolution300 DPI (scale=300/72) — the single biggest lever.
FormatPNG — lossless, so no JPEG artifacts around character edges.
OrientationRotate sideways pages upright: image.rotate(90, expand=True).
Source qualityRe-scan faint originals — OCR cannot recover what the scanner lost.
BordersCrop dark scanner edges and hole-punch shadows before sending.

One adjacent goal worth naming: if what you want is the same PDF but searchable — the scan with an invisible text layer on top — that's the open-source ocrmypdf tool in one command. Use the API pipeline when your application needs the text itself:

shell
pip install ocrmypdf ocrmypdf scanned.pdf searchable.pdf

Where to go from here

Since every page is an independent request, the pipeline parallelizes trivially with a thread pool — no GPUs or native OCR dependencies to manage, and premium plans raise the request limits for production volume. If the scan contains tables, the bounding boxes reconstruct rows and columns — that's How to Convert PDF to CSV. The complete script is below.

ocr_pdf.py

The complete pipeline from this guide: render, recognize, rebuild reading order, write a .txt per PDF.

Frequently asked questions

How do I OCR a PDF for free?

Render the pages with the open-source pypdfium2 library and send them to the Image to Text API, which has a free tier with no credit card required. The whole pipeline is about 30 lines of Python.

How do I know if a PDF needs OCR?

Try selecting text in a viewer, or extract with pypdf in code. Little or no text back means the pages are scanned images. PDFs with a text layer should be extracted directly instead — it is faster and exact.

What resolution should I render at?

About 300 DPI — in pypdfium2 that is render(scale=300/72). Below roughly 150 DPI accuracy drops fast; above 300 DPI files get large with little gain.

Can OCR handle rotated or skewed scans?

Slight skew is tolerated, but rotate sideways or upside-down pages upright before sending — image.rotate(90, expand=True) on the rendered PIL image.

How do I make the PDF searchable instead of extracting text?

That output — the original scan with an invisible text layer — is a different goal; the open-source ocrmypdf tool produces it in one command. Use the API pipeline when your application needs the text itself.

Does OCR work on handwriting?

Printed text is the reliable case. Neat handwriting often works; cursive is hit-or-miss with any OCR engine. Test your documents in the browser on the Image to Text API page — the demo is free.