Sign inSign up

edgaras0x4e/paddleocr-pdf-api

By edgaras0x4e

•Updated 3 months ago

GPU-accelerated PDF OCR API that extracts text from scanned documents and returns markdown in JSON.

Image
Languages & frameworks
Machine learning & AI
Data science
0

5.4K

edgaras0x4e/paddleocr-pdf-api repository overview

⁠Self-Hosted PDF OCR API for Large Documents

A self-hosted PDF OCR API powered by PaddleOCR⁠ models (PaddleOCR-VL by default). Runs via Docker, processes PDFs page-by-page, and returns markdown content in JSON responses. Good support (not perfect) for Latvian and Lithuanian languages.

⁠Contents

⁠OCR engines

The API runs one of three OCR engines, selected with the OCR_ENGINE variable. All engines share the same API and job flow:

OCR_ENGINEModelsOutputHardware
vl (default)PaddleOCR-VL-1.6 (0.9B) + PP-DocLayoutV3Markdown, full document understandingGPU, ~8.5GB VRAM
textPP-OCRv5 server detection + recognitionPlain text lines, no layout markupGPU or CPU
structurePP-StructureV3 layout parsing (tables, formulas, reading order), no VLMMarkdownGPU, ~10.5GB VRAM

All engines accept the same input formats: PDF, PNG, JPG, JPEG, BMP, TIFF, WEBP.

Each engine has its own baked image (models included, no first-run download):

Image tagEngineRuns on
latest-vl-bakedvlGPU
latest-structure-bakedstructureGPU
latest-text-bakedtextCPU

Baked images preselect their engine, so OCR_ENGINE does not need to be set. The non-baked latest image is GPU-based and works with any engine; it downloads the selected engine's models at runtime.

⁠Requirements

Every image needs Docker. The GPU images also need the NVIDIA Container Toolkit⁠ and an NVIDIA GPU:

ImageNeeds
latestDocker + NVIDIA GPU (for vl engine)
latest-vl-bakedDocker + NVIDIA GPU (~8.5GB VRAM)
latest-structure-bakedDocker + NVIDIA GPU (~10.5GB VRAM)
latest-text-bakedDocker

⁠Quick start

Using the Docker Hub image⁠:

services:
  paddleocr:
    image: edgaras0x4e/paddleocr-pdf-api:latest
    ports:
      - "8099:8000"
    environment:
      - OCR_ENGINE=vl              # or text, or structure
    volumes:
      - ocr-data:/data
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    restart: unless-stopped

volumes:
  ocr-data:
docker compose up -d

Or build from source:

git clone https://github.com/Edgaras0x4E/paddleocr-pdf-api.git && cd paddleocr-pdf-api
docker compose up --build -d

The API will be available at http://localhost:8099. On first startup the model (~2GB) is downloaded and loaded into GPU memory. The API accepts requests immediately, but jobs will start processing once the model is ready.

Baked images (no first-run download): for scale-to-zero or cold-start-sensitive deployments, use your engine's baked tag instead (see OCR engines⁠). The models are pre-baked into the image, so the container starts without downloading anything; cold-start warmup is only the model load into memory.

⁠Usage

⁠Submit a PDF

Also accepts single image files as a bonus: .png, .jpg, .jpeg, .bmp, .tif, .tiff, .webp (processed as a 1-page job).

curl -X POST http://localhost:8099/ocr -F "[email protected]"
{
  "job_id": "994e7b398bb44d8ab5eade4d2ef57a15",
  "filename": "document.pdf",
  "status": "queued"
}
⁠Check progress
curl http://localhost:8099/ocr/{job_id}
{
  "job_id": "994e7b398bb44d8ab5eade4d2ef57a15",
  "filename": "document.pdf",
  "status": "processing",
  "total_pages": 185,
  "processed_pages": 42,
  "error": null
}
⁠Get a single page
curl http://localhost:8099/ocr/{job_id}/pages/1
{
  "job_id": "994e7b398bb44d8ab5eade4d2ef57a15",
  "page_num": 1,
  "markdown": "## Chapter 1\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit..."
}
⁠Get all pages
curl http://localhost:8099/ocr/{job_id}/result
{
  "job_id": "994e7b398bb44d8ab5eade4d2ef57a15",
  "filename": "document.pdf",
  "status": "completed",
  "total_pages": 185,
  "processed_pages": 185,
  "pages": [
    {"page_num": 1, "markdown": "## Chapter 1\n\nLorem ipsum dolor sit amet..."},
    {"page_num": 2, "markdown": "..."}
  ]
}
⁠List all jobs
curl http://localhost:8099/jobs
{
  "jobs": [
    {
      "job_id": "994e7b398bb44d8ab5eade4d2ef57a15",
      "filename": "document.pdf",
      "status": "completed",
      "total_pages": 185,
      "processed_pages": 185
    }
  ]
}
⁠Cancel a job
curl -X POST http://localhost:8099/ocr/{job_id}/cancel
{
  "job_id": "994e7b398bb44d8ab5eade4d2ef57a15",
  "status": "cancelling"
}
⁠Delete a job
curl -X DELETE http://localhost:8099/ocr/{job_id}
{
  "status": "deleted"
}

⁠API reference

MethodEndpointDescription
POST/ocrUpload a PDF for processing
GET/ocr/{job_id}Get job status and progress
GET/ocr/{job_id}/pages/{page_num}Get markdown for a specific page
GET/ocr/{job_id}/resultGet all completed pages
POST/ocr/{job_id}/cancelCancel a queued or running job
DELETE/ocr/{job_id}Delete a job and its data
GET/jobsList all jobs

⁠Configuration

Environment variables set in docker-compose.yml:

VariableDefaultDescription
API_KEY(empty)Optional API key. When set, all requests must include an X-API-Key header
OCR_ENGINEvlOCR engine: vl, text, or structure (see OCR engines⁠)
OCR_DPI200DPI for PDF page rendering
OCR_MARKDOWN_IGNORE_LABELSnumber, footnote, header, header_image, footer, footer_image, aside_textComma-separated layout labels dropped from the markdown (vl and structure engines).
DATABASE_URL(empty)Empty uses SQLite at DB_PATH. A postgresql://... URL uses PostgreSQL instead.
RUN_MODEserverserver is the always-on API. job reads one file from stdin, processes it, and exits.
DB_PATH/data/ocr.dbSQLite database path (used when DATABASE_URL is empty)
UPLOAD_DIR/data/uploadsUpload storage path
⁠Database backend

By default the API stores jobs and results in SQLite at /data/ocr.db, persisted on the /data volume. No configuration is needed.

To use PostgreSQL instead, set DATABASE_URL:

environment:
  - DATABASE_URL=postgresql://user:password@host:5432/ocr

The schema is created automatically on startup. With PostgreSQL the database is external, so the API is stateless apart from the uploaded files held on /data during processing. Multiple server containers can share one PostgreSQL database.

⁠Job mode

By default the container runs as an always-on server (RUN_MODE=server). Set RUN_MODE=job to instead process a single file and exit. The file is piped in on stdin; the result is written to the same database as server mode and read back through the API:

docker run --rm -i --gpus all \
  -e RUN_MODE=job \
  edgaras0x4e/paddleocr-pdf-api:latest < document.pdf

The container reads the file from stdin, runs the same OCR pipeline, saves the result to the database, and exits (0 on success, non-zero on failure). It does not start the HTTP server. The baked image variant avoids the model download on each run, which suits repeated one-shot jobs.

To keep the result, point the run at a persistent database:

# PostgreSQL (external DB, nothing mounted)
docker run --rm -i --gpus all \
  -e RUN_MODE=job -e DATABASE_URL=postgresql://user:password@host:5432/ocr \
  edgaras0x4e/paddleocr-pdf-api:latest-vl-baked < document.pdf

# SQLite (mount the /data volume so the database persists)
docker run --rm -i --gpus all \
  -e RUN_MODE=job -v ocr-data:/data \
  edgaras0x4e/paddleocr-pdf-api:latest-vl-baked < document.pdf
⁠Image descriptions

When enabled, cropped image regions (photos, charts, seals, logos) detected by the layout model are sent to an OpenAI-compatible vision model, and the returned description is inlined in the page markdown as a > **[Label]** ... blockquote. Disabled by default - the original behavior (stripping image tags) is preserved.

VariableDefaultDescription
IMAGE_DESCRIPTION_ENABLEDfalseMaster switch. When false, images are stripped as before.
IMAGE_DESCRIPTION_PROVIDERopenaiopenai (any OpenAI(base_url=…)-compatible endpoint) or azure (uses AzureOpenAI).
IMAGE_DESCRIPTION_API_URLhttps://api.openai.com/v1Base URL. For azure, the resource endpoint, e.g. https://<name>.cognitiveservices.azure.com.
IMAGE_DESCRIPTION_API_KEY(empty)Bearer / API key. Local backends accept any placeholder.
IMAGE_DESCRIPTION_API_VERSION(empty)Azure-only, e.g. 2025-01-01-preview (chat) or 2025-04-01-preview (responses).
IMAGE_DESCRIPTION_API_MODEchat_completionschat_completions (universal) or responses (OpenAI-native / Azure).
IMAGE_DESCRIPTION_MODELgpt-5.5Model name (or Azure deployment name).
IMAGE_DESCRIPTION_PROMPTbuilt-in neutral promptDefault prompt used when no per-label override is set.
IMAGE_DESCRIPTION_PROMPT_<LABEL>(empty)Per-label override, e.g. IMAGE_DESCRIPTION_PROMPT_CHART="Extract numeric data as a markdown table.". Label is the uppercase block_label (IMAGE, CHART, SEAL, HEADER_IMAGE, FOOTER_IMAGE).
IMAGE_DESCRIPTION_LABELSimage,chart,seal,header_image,footer_imageComma-separated labels to describe. table and formula are always skipped (PaddleOCR renders them natively).
IMAGE_DESCRIPTION_MIN_PIXELS10000Skip crops smaller than this area (w × h). Filters out bullet icons.
IMAGE_DESCRIPTION_MAX_EDGE_PX1568Downscale longest edge before sending. 0 disables.
IMAGE_DESCRIPTION_MAX_PER_PAGE10Cap of described images per page.
IMAGE_DESCRIPTION_TIMEOUT60Seconds per request.
IMAGE_DESCRIPTION_MAX_RETRIES2Retries on transient errors.
IMAGE_DESCRIPTION_ON_ERRORskipskip, placeholder (inserts [image description unavailable]), or fail.

Example docker-compose.yml override:

environment:
  - IMAGE_DESCRIPTION_ENABLED=true
  - IMAGE_DESCRIPTION_PROVIDER=azure
  - IMAGE_DESCRIPTION_API_URL="https://<your-resource>.cognitiveservices.azure.com"
  - IMAGE_DESCRIPTION_API_VERSION="2025-04-01-preview"
  - IMAGE_DESCRIPTION_API_MODE=responses
  - IMAGE_DESCRIPTION_MODEL="gpt-5.5"
  - IMAGE_DESCRIPTION_API_KEY="<your-key>"
  - IMAGE_DESCRIPTION_PROMPT_CHART="Extract all data points from this chart as a markdown table."
⁠API key authentication

Uncomment the environment section in docker-compose.yml:

environment:
  - API_KEY=your-secret-key

Then restart:

docker compose down && docker compose up -d

All requests must then include the header:

curl -H "X-API-Key: your-secret-key" http://localhost:8099/jobs

⁠Data persistence

The /data volume stores the SQLite database and uploaded PDFs. This is a named Docker volume (ocr-data) that persists across container restarts and rebuilds. When DATABASE_URL points at PostgreSQL, the database lives in PostgreSQL and /data only holds uploaded files during processing.

⁠Changelog

⁠v0.4.1
  • Added OCR_MARKDOWN_IGNORE_LABELS to control which layout labels are dropped from the markdown output. (#1⁠)
  • Added OCR_ENGINE to select the OCR engine: vl (default), text, or structure, each with its own baked image. (#1⁠)
  • Renamed the baked image to :latest-vl-baked / :v0.4.1-vl-baked (was :latest-baked / :v0.4.0-baked); latest-baked remains an alias.
⁠v0.4.0
  • Added an optional PostgreSQL backend, selected with DATABASE_URL. SQLite remains default.
  • Added an optional job run mode (RUN_MODE=job): the container reads a file, processes it, saves the result to the database, and exits.
⁠v0.3.0
  • Updated the OCR model to PaddleOCR-VL-1.6 (requires paddleocr 3.6.0).
  • Added a baked image variant (:latest-baked / :v0.3.0-baked) with the model pre-baked into the image - no first-run download, for fast cold starts and scale-to-zero deployments.
⁠v0.2.0
  • Accept single image uploads (.png, .jpg, .jpeg, .bmp, .tif, .tiff, .webp) as 1-page jobs.
  • Optional image descriptions via OpenAI / Azure OpenAI models.
  • Fixed tables to markdown tables.

⁠License

MIT

⁠Source code

https://github.com/Edgaras0x4E/paddleocr-pdf-api⁠

Tag summary

Content type

Image

Digest

sha256:b69699dcc…

Size

5.9 GB

Last updated

3 months ago

docker pull edgaras0x4e/paddleocr-pdf-api