# 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, and 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 25 September 2026. Every file is parsed in a Firecracker microVM with its own kernel, so a malformed or malicious PDF from a user never touches your application server. ## Two kinds of PDF, two tools A PDF made by a word processor carries its text; extraction reads it back. A scanned PDF is a picture of a page; it has no text until OCR recognises it. pypdf's documentation says as much: a page that holds only an image gives little or no text, and OCR software such as Tesseract is the tool for it (pypdf docs, read 25 September 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` | Versions are Launchpad's for noble, read 25 September 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. When it comes back nearly empty, render the pages to images and recognise 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. `tesseract imagename outputbase` writes `outputbase.txt` in English by default (Tesseract's documentation, read 25 September 2026). Other languages need their data packages in the image. ## Pages for a vision model Some documents are better read by a model that sees the page: forms, tables with merged cells, 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 the fields out. That script is untrusted code. Run it in the same sandbox, with the internet already off, and 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 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 once | 100 sandboxes at once on a paid account to start | ## What it costs Take 2,000 PDFs a month, a mix of digital files and scans. Each gets a 2 vCPU, 4 GiB sandbox for 40 seconds, and extraction plus OCR uses 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 and 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's 50 trial hours cover 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 25 September 2026. - [Tesseract command-line usage](https://tesseract-ocr.github.io/tessdoc/Command-Line-Usage.html): `tesseract imagename outputbase` and searchable PDF output, read 25 September 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 25 September 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 25 September 2026.