# How to extract text from PDFs, scanned ones included, in a sandbox Open each PDF in a throwaway sandbox. Pull the embedded text with Poppler or pypdf. Run Tesseract OCR on pages that are only images. **On Runtime, OCR pays for the CPU it uses and nothing while it waits.** 2,000 PDFs a month, with 30 CPU-seconds of extraction and OCR each, cost $1.08 on a 2 vCPU, 4 GiB sandbox, at the rates Runtime publishes, read 26 October 2026. Every file is parsed in a Firecracker microVM with its own kernel. A malformed or malicious PDF from a user therefore never touches your application server. ## Two kinds of PDF, two tools A PDF made by a word processor carries its text. Extraction reads that text back. A scanned PDF is a picture of a page. It has no text until OCR recognizes it. pypdf's documentation says as much: a page that holds only an image gives little or no text. OCR software such as Tesseract is the tool for that case (pypdf docs, read 26 October 2026). | Tool | What it does | Ubuntu 24.04 package | | ---------------------- | ---------------------------------------------------- | ----------------------- | | `pdftotext` | Embedded text to a plain-text file | `poppler-utils` 24.02.0 | | `pdftoppm` | Each page to a PNG image, for OCR or a vision model | `poppler-utils` 24.02.0 | | `pdfinfo`, `pdfimages` | Metadata and page count; the images inside the file | `poppler-utils` 24.02.0 | | `tesseract` | Text from an image; with `pdf`, a searchable PDF | `tesseract-ocr` 5.3.4 | | pypdf | Page-by-page text from Python, `page.extract_text()` | `pip install pypdf` | The versions are Launchpad's package versions, read 26 October 2026. ## Build the image once ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build({ name: "pdf", recipe: { apt: ["poppler-utils", "tesseract-ocr"], pip: ["pypdf"] }, }); ``` ```python check from withruntime import Runtime Runtime().images.build(name="pdf", recipe={"apt": ["poppler-utils", "tesseract-ocr"], "pip": ["pypdf"]}) ``` ## Extract, and fall back to OCR Try the embedded text first. If it comes back nearly empty, render the pages to images and recognize them. ```ts check import { readFile } from "node:fs/promises"; import { Runtime } from "withruntime"; const runtime = new Runtime(); await using sbx = await runtime.sandboxes.create({ image: "pdf", network: { internet: false }, timeoutSeconds: 900, onLeaseEnd: "stop", }); await sbx.files.write("/workspace/doc.pdf", await readFile("invoice.pdf")); await sbx.exec(["pdftotext", "doc.pdf", "doc.txt"], { check: true, timeoutMs: 120_000 }); let text = await sbx.files.readText("/workspace/doc.txt"); if (text.trim().length < 50) { // A scan: pages to PNG at 300 dpi, then OCR each one. await sbx.exec("pdftoppm -r 300 -png doc.pdf page", { check: true, timeoutMs: 300_000 }); await sbx.exec('for f in page-*.png; do tesseract "$f" "${f%.png}"; done', { timeoutMs: 900_000, }); const parts = (await sbx.files.glob("page-*.txt")).sort(); const pages = await Promise.all(parts.map((p) => sbx.files.readText(`/workspace/${p}`))); text = pages.join("\n\f"); } console.log(text.slice(0, 500)); ``` ```python check from withruntime import Runtime runtime = Runtime() with runtime.sandboxes.create( image="pdf", network={"internet": False}, timeout_seconds=900, on_lease_end="stop", ) as sbx: with open("invoice.pdf", "rb") as source: sbx.files.write("/workspace/doc.pdf", source.read()) sbx.exec(["pdftotext", "doc.pdf", "doc.txt"], check=True, timeout_ms=120_000) text = sbx.files.read_text("/workspace/doc.txt") if len(text.strip()) < 50: # A scan: pages to PNG at 300 dpi, then OCR each one. sbx.exec("pdftoppm -r 300 -png doc.pdf page", check=True, timeout_ms=300_000) sbx.exec('for f in page-*.png; do tesseract "$f" "${f%.png}"; done', timeout_ms=900_000) parts = sorted(sbx.files.glob("page-*.txt")) text = "\n\f".join(sbx.files.read_text(f"/workspace/{p}") for p in parts) print(text[:500]) ``` The 50-character threshold is a starting point. Tune it on your own documents. By default, `tesseract imagename outputbase` writes `outputbase.txt` in English (Tesseract's documentation, read 26 October 2026). Other languages need their data packages installed in the image. ## Pages for a vision model Some documents are better read by a model that sees the page, such as forms, tables with merged cells and handwriting. The same `pdftoppm` step produces one PNG per page. Read them back with `files.read` and send them to your model. Only the images leave the sandbox, and only because your code chose to fetch them. ## Let an agent write the parser For a stack of similar documents, such as a supplier's invoices, a model can write a small pypdf or regex script that pulls out the fields. That script is untrusted code. Run it in the same sandbox, with the internet already off. Check its JSON against your schema outside the sandbox before you store anything. The loop is the one in [AI data pipelines](/use-cases/ai-data-pipelines): generate, run with a time limit, validate, retry with the error. ## What PDF work needs from its sandbox | Need | How Runtime covers it | | ------------------------------- | ------------------------------------------------------------------------- | | Parsing files from strangers | A microVM with its own kernel; the host enforces network and resources | | No exfiltration of the contents | Internet off at create; root in the guest cannot turn it back on | | OCR's CPU bursts | Shared CPU bursts up to the sandbox's vCPUs; you pay for what is measured | | Big scans | Uploads in 1 MiB pieces, each checked by SHA-256; set `diskMiB` for room | | A page that makes OCR hang | `timeoutMs` per command, 60 seconds by default and up to 24 hours | | Documents already in a bucket | Mount S3, R2 or Google Cloud Storage read-only as a directory | | Many files at the same time | 100 sandboxes at the same time on a paid account | ## What it costs Take 2,000 PDFs a month, a mix of digital files and scans. Each one gets a 2 vCPU, 4 GiB sandbox for 40 seconds. Extraction and OCR use 30 CPU-seconds: ``` CPU: 2,000 × 30 s / 3,600 × $0.025 = $0.42 Memory: 2,000 × 40 s / 3,600 × 4 GiB × $0.0075 = $0.67 Total: $1.08 ``` The CPU line is measured use, at $0.025 per vCPU-hour. The memory line is $0.0075 per GiB-hour while the sandbox runs ([pricing](/docs/pricing)). Digital PDFs that need no OCR finish in a few seconds and cost less. Tesseract is free software, so there is no per-page OCR fee on top. A new account gets 100 free hours of a 1 GB machine in its first 30 days, which covers the first experiments, with no card. ## Sources - [pypdf: extract text](https://pypdf.readthedocs.io/en/stable/user/extract-text.html): `PdfReader`, `extract_text()` and the note on scanned pages, read 26 October 2026. - [Tesseract command-line usage](https://tesseract-ocr.github.io/tessdoc/Command-Line-Usage.html): `tesseract imagename outputbase` and searchable PDF output, read 26 October 2026. - [Poppler](https://poppler.freedesktop.org/) and Launchpad's [poppler-utils](https://launchpad.net/ubuntu/noble/+package/poppler-utils) and [tesseract-ocr](https://launchpad.net/ubuntu/noble/+package/tesseract-ocr) pages for Ubuntu 24.04, read 26 October 2026. Related: [document conversion](/use-cases/document-conversion), [data analysis agent](/use-cases/data-analysis-agent), [run untrusted LLM code safely](/use-cases/run-untrusted-llm-code), [Python in a sandbox](/languages/python). Facts on this page were checked on 26 October 2026.