- Java 25
- Maven 3.9+
- Node.js 22+
- Docker with Compose
- qpdf 11+ for local Repair PDF development
The default stack uses PostgreSQL and local streaming storage:
docker compose up --buildThe backend's diagnostic port binds to 127.0.0.1:8080; external traffic should use
the Nginx frontend proxy.
On first startup after upgrading, the backend entrypoint moves artifacts from the old
volume root into legacy/, creates the jobs layout, repairs ownership, and then drops
to the unprivileged application user.
For a clean milestone build:
docker compose build --no-cacheThe optional SeaweedFS integration stack is available with:
docker compose --profile s3 up seaweed-master seaweed-volume seaweed-filer seaweed-s3That profile is for local compatibility testing. Production must configure SeaweedFS
IAM separately and inject PDF_STORAGE_S3_ACCESS_KEY and
PDF_STORAGE_S3_SECRET_KEY through a secret store.
Start PostgreSQL first, then:
cd backend
DATABASE_URL=jdbc:postgresql://localhost:5432/pdftools \
DATABASE_USERNAME=pdftools \
DATABASE_PASSWORD=pdftools \
mvn spring-boot:runImportant settings:
| Variable | Default | Purpose |
|---|---|---|
PDF_STORAGE_TYPE |
local |
local or s3 |
PDF_STORAGE_LOCAL_ROOT |
/tmp/pdf-storage/jobs |
Local object root |
PDF_JOB_WORK_ROOT |
/tmp/pdf-work |
Ephemeral worker directories |
PDF_ENABLED_OPERATIONS |
stable operations | Comma-separated submission feature flags |
PDF_MULTIPART_TEMP_DIR |
/tmp/pdf-multipart |
Private multipart spill directory; must be dedicated to one backend instance |
PDF_SECURITY_MAX_OUTPUT_BYTES |
134217728 |
Protect/Unlock output byte limit |
PDF_TO_JPG_MAX_SELECTED_PAGES |
500 |
Maximum pages rendered per conversion |
PDF_TO_JPG_MAX_DPI |
300 |
Maximum render resolution |
PDF_TO_JPG_MAX_PIXELS_PER_PAGE |
20000000 |
Per-page render pixel limit |
PDF_TO_JPG_MAX_WORKER_HEAP_BYTES |
268435456 |
Isolated renderer heap cap |
PDF_TO_JPG_WORKER_TIMEOUT |
5m |
Isolated renderer wall-time cap |
JPG_TO_PDF_MAX_IMAGES |
100 |
Maximum JPEG inputs per conversion |
JPG_TO_PDF_MAX_PIXELS_PER_IMAGE |
50000000 |
Per-image pixel limit |
JPG_TO_PDF_MAX_OUTPUT_BYTES |
134217728 |
Generated PDF byte limit |
JPG_TO_PDF_VALIDATION_WORKER_HEAP_BYTES |
134217728 |
JPEG validator heap cap |
JPG_TO_PDF_VALIDATION_WORKER_TIMEOUT |
2m |
JPEG validator wall-time cap |
WATERMARK_MAX_IMAGE_PIXELS |
4000000 |
Watermark image pixel limit |
WATERMARK_MAX_IMAGE_BYTES |
10485760 |
Watermark image byte limit |
WATERMARK_MAX_IMAGE_DIMENSION |
4096 |
Watermark image side limit |
EDIT_MAX_ELEMENTS |
500 |
Maximum elements in one edit plan |
EDIT_MAX_IMAGES |
10 |
Maximum uploaded edit images |
EDIT_MAX_IMAGE_BYTES |
10485760 |
Per-edit-image byte limit |
EDIT_MAX_TOTAL_DECODED_IMAGE_BYTES |
67108864 |
Aggregate decoded edit-image budget |
REDACT_MAX_AREAS |
500 |
Maximum redaction areas |
REDACT_MAX_AREAS_PER_PAGE |
100 |
Maximum areas on one page |
REDACT_MAX_DOCUMENT_PAGES |
1000 |
Maximum input pages |
REDACT_RENDER_DPI |
200 |
Sanitized raster resolution |
REDACT_JPEG_QUALITY |
95 |
Sanitized raster JPEG quality |
REDACT_MAX_PIXELS_PER_PAGE |
20000000 |
Pre-allocation page pixel limit |
REDACT_MAX_OUTPUT_BYTES |
536870912 |
Sanitized PDF byte limit |
REDACT_WORKER_HEAP_BYTES |
268435456 |
Isolated worker heap cap |
REDACT_WORKER_TIMEOUT |
5m |
Isolated worker wall-time cap |
OFFICE_LIBREOFFICE_BINARY |
soffice |
LibreOffice executable |
OFFICE_CONVERSION_MODE |
queue |
Networkless sidecar or explicit direct mode |
OFFICE_QUEUE_REQUEST_ROOT |
/var/lib/pdf-tools-office/requests |
Backend-write/sidecar-read requests |
OFFICE_QUEUE_RESPONSE_ROOT |
/var/lib/pdf-tools-office/responses |
Sidecar-write/backend-read responses |
OFFICE_QUEUE_SIGNAL_ROOT |
/var/lib/pdf-tools-office/signals |
Backend-write/sidecar-read signals |
OFFICE_QUEUE_WAIT_TIMEOUT |
5m |
Queue wait before conversion starts |
OFFICE_QUEUE_RETENTION |
1h |
Stale queue retention |
OFFICE_QUEUE_CLEANUP_INTERVAL |
1m |
Queue cleanup cadence |
OFFICE_SIDECAR_WORK_ROOT |
/tmp/office-work |
Size-limited sidecar scratch |
OFFICE_WORKER_USER |
officeworker |
Native converter user |
OFFICE_MAX_INPUT_BYTES |
52428800 |
Office input byte limit |
OFFICE_MAX_EXPANDED_INPUT_BYTES |
268435456 |
Expanded OOXML limit |
OFFICE_MAX_ARCHIVE_ENTRIES |
10000 |
OOXML ZIP entry limit |
OFFICE_MAX_OUTPUT_BYTES |
134217728 |
Converted PDF byte limit |
OFFICE_MAX_LOG_BYTES |
1048576 |
Per-conversion log byte limit |
OFFICE_MAX_ADDRESS_SPACE_BYTES |
1073741824 |
Linux address-space cap |
OFFICE_CPU_TIME_SECONDS |
120 |
Native-process CPU limit |
OFFICE_MAX_OPEN_FILES |
256 |
Native-process file-descriptor limit |
OFFICE_MAX_WORKER_PROCESSES |
96 |
Worker-UID process limit |
OFFICE_WALL_TIMEOUT |
2m |
Conversion wall-time cap |
EXCEL_MAX_SHEETS |
100 |
Workbook sheet limit |
EXCEL_MAX_USED_CELLS |
1000000 |
Used-range scan cell limit |
EXCEL_MAX_PREPARED_BYTES |
104857600 |
Prepared workbook byte limit |
HTML_QUEUE_REQUEST_ROOT |
/var/lib/pdf-tools-html/requests |
Backend-write/browser-read requests |
HTML_QUEUE_RESPONSE_ROOT |
/var/lib/pdf-tools-html/responses |
Browser-write/backend-read responses |
HTML_QUEUE_SIGNAL_ROOT |
/var/lib/pdf-tools-html/signals |
Backend-write/browser-read signals |
HTML_QUEUE_WAIT_TIMEOUT |
5m |
HTML queue wait before rendering starts |
HTML_QUEUE_RETENTION |
1h |
Stale HTML queue retention |
HTML_WALL_TIMEOUT |
1m |
Chromium render wall-time limit |
HTML_MAX_INPUT_BYTES |
10485760 |
UTF-8 HTML input byte limit |
HTML_MAX_OUTPUT_BYTES |
67108864 |
Rendered PDF byte limit |
HTML_MAX_PAGES |
200 |
Rendered PDF page limit |
PDF_WORD_MAX_INPUT_BYTES |
52428800 |
PDF-to-Word input byte limit |
PDF_WORD_MAX_PAGES |
200 |
PDF-to-Word page limit |
PDF_WORD_MAX_TEXT_CHARACTERS |
2000000 |
Extracted text character limit |
PDF_WORD_MAX_IMAGES |
200 |
Embedded or rendered image limit |
PDF_WORD_MAX_TOTAL_IMAGE_PIXELS |
200000000 |
Aggregate image pixel budget |
PDF_WORD_MAX_TOTAL_IMAGE_BYTES |
268435456 |
Aggregate encoded image budget |
PDF_WORD_MAX_OUTPUT_BYTES |
134217728 |
DOCX output byte limit |
PDF_WORD_RENDER_DPI |
144 |
Visual-mode page resolution |
PDF_WORD_WORKER_HEAP_BYTES |
536870912 |
Isolated conversion heap cap |
PDF_WORD_WORKER_TIMEOUT |
5m |
Isolated conversion wall-time cap |
PDF_POWERPOINT_MAX_INPUT_BYTES |
52428800 |
PDF-to-PowerPoint input byte limit |
PDF_POWERPOINT_MAX_PAGES |
200 |
Slide/page limit |
PDF_POWERPOINT_MAX_TEXT_CHARACTERS |
2000000 |
Extracted text character limit |
PDF_POWERPOINT_MAX_TEXT_BOXES |
5000 |
Editable text box limit |
PDF_POWERPOINT_MAX_IMAGES |
200 |
Editable or visual image limit |
PDF_POWERPOINT_MAX_TOTAL_IMAGE_BYTES |
268435456 |
Aggregate encoded image budget |
PDF_POWERPOINT_MAX_OUTPUT_BYTES |
134217728 |
PPTX output byte limit |
PDF_POWERPOINT_RENDER_DPI |
144 |
Visual-mode page resolution |
PDF_POWERPOINT_WORKER_HEAP_BYTES |
536870912 |
Isolated conversion heap cap |
PDF_POWERPOINT_WORKER_TIMEOUT |
5m |
Isolated conversion wall-time cap |
PDF_EXCEL_MAX_INPUT_BYTES |
52428800 |
PDF-to-Excel input byte limit |
PDF_EXCEL_MAX_PAGES |
200 |
PDF page limit |
PDF_EXCEL_MAX_TEXT_CHARACTERS |
2000000 |
Extracted text character limit |
PDF_EXCEL_MAX_TABLES |
200 |
Detected table limit |
PDF_EXCEL_MAX_SHEETS |
200 |
Workbook worksheet limit |
PDF_EXCEL_MAX_ROWS_PER_SHEET |
100000 |
Per-worksheet row limit |
PDF_EXCEL_MAX_COLUMNS |
100 |
Detected/generated column limit |
PDF_EXCEL_MAX_CELLS |
1000000 |
Aggregate generated cell limit |
PDF_EXCEL_MAX_OUTPUT_BYTES |
104857600 |
XLSX output byte limit |
PDF_EXCEL_WORKER_HEAP_BYTES |
536870912 |
Isolated worker heap cap |
PDF_EXCEL_WORKER_TIMEOUT |
5m |
Isolated worker wall-time cap |
PDF_MARKDOWN_MAX_INPUT_BYTES |
52428800 |
PDF-to-Markdown input byte limit |
PDF_MARKDOWN_MAX_PAGES |
200 |
PDF page limit |
PDF_MARKDOWN_MAX_TEXT_CHARACTERS |
2000000 |
Extracted text character limit |
PDF_MARKDOWN_MAX_MARKDOWN_CHARACTERS |
4000000 |
Generated Markdown character limit |
PDF_MARKDOWN_MAX_IMAGES |
200 |
Extracted image limit |
PDF_MARKDOWN_MAX_TOTAL_IMAGE_PIXELS |
200000000 |
Aggregate image pixel budget |
PDF_MARKDOWN_MAX_OUTPUT_BYTES |
314572800 |
ZIP output byte limit |
PDF_MARKDOWN_WORKER_HEAP_BYTES |
536870912 |
Isolated worker heap cap |
PDF_MARKDOWN_WORKER_TIMEOUT |
5m |
Isolated worker wall-time cap |
COMPRESS_MAX_INPUT_BYTES |
104857600 |
Compress PDF input byte limit |
COMPRESS_MAX_PAGES |
500 |
PDF page limit |
COMPRESS_MAX_IMAGES |
500 |
Unique image limit |
COMPRESS_MAX_TOTAL_IMAGE_PIXELS |
200000000 |
Aggregate decoded image pixel budget |
COMPRESS_RECOMMENDED_JPEG_QUALITY |
82 |
Balanced JPEG quality |
COMPRESS_EXTREME_JPEG_QUALITY |
60 |
Extreme JPEG quality |
COMPRESS_MAX_OUTPUT_BYTES |
134217728 |
Compressed PDF byte limit |
COMPRESS_WORKER_HEAP_BYTES |
536870912 |
Isolated worker heap cap |
COMPRESS_WORKER_TIMEOUT |
5m |
Isolated worker wall-time cap |
QPDF_BINARY |
qpdf |
qpdf executable used for structural repair |
REPAIR_MAX_INPUT_BYTES |
104857600 |
Repair input byte limit |
REPAIR_MAX_OUTPUT_BYTES |
134217728 |
Repaired PDF byte limit |
REPAIR_MAX_LOG_BYTES |
1048576 |
Aggregate qpdf diagnostic limit |
REPAIR_MAX_ADDRESS_SPACE_BYTES |
1073741824 |
qpdf virtual-address cap |
REPAIR_CPU_TIME_SECONDS |
120 |
qpdf CPU-time cap per phase |
REPAIR_REWRITE_TIMEOUT |
2m |
Structural rewrite wall-time cap |
REPAIR_CHECK_TIMEOUT |
1m |
Post-repair check wall-time cap |
REPAIR_ALLOW_UNSANDBOXED_LINUX |
false |
Trusted-test-only escape hatch when host setpriv lacks seccomp |
PDFA_MAX_INPUT_BYTES |
52428800 |
PDF/A source byte limit |
PDFA_MAX_OUTPUT_BYTES |
134217728 |
Converted PDF/A byte limit |
PDFA_MAX_PAGES |
200 |
Source and output page limit |
PDFA_MAX_RULE_FAILURES |
100 |
Maximum reported veraPDF rule failures |
PDFA_MAX_REPORT_BYTES |
262144 |
Validation report byte limit |
PDFA_VALIDATOR_HEAP_BYTES |
536870912 |
Isolated veraPDF heap cap |
PDFA_VALIDATOR_TIMEOUT |
5m |
Isolated veraPDF wall-time cap |
COMPARE_MAX_INPUT_BYTES |
52428800 |
Per-PDF comparison input limit |
COMPARE_MAX_TOTAL_INPUT_BYTES |
104857600 |
Aggregate comparison input limit |
COMPARE_MAX_PAGES |
200 |
Page limit per document |
COMPARE_MAX_DIFF_MATRIX_CELLS |
1000000 |
Per-page text diff complexity cap |
COMPARE_MAX_PIXELS_PER_PAGE |
10000000 |
Per-page rendered pixel cap |
COMPARE_MAX_TOTAL_RENDER_PIXELS |
200000000 |
Aggregate render/canvas pixel budget |
COMPARE_MAX_REPORT_BYTES |
4194304 |
JSON report byte limit |
COMPARE_MAX_ARCHIVE_BYTES |
314572800 |
ZIP artifact byte limit |
COMPARE_WORKER_HEAP_BYTES |
536870912 |
Isolated comparison heap cap |
COMPARE_WORKER_TIMEOUT |
5m |
Isolated comparison wall-time cap |
PDF_STORAGE_S3_ENDPOINT |
none | SeaweedFS/S3 endpoint |
PDF_STORAGE_S3_BUCKET |
pdf-tools |
Artifact bucket |
Roll out a new operation in two phases across multiple replicas: deploy the binary
everywhere with its key disabled, then add the key to PDF_ENABLED_OPERATIONS.
Workers only dispatch operation keys registered in their local binary.
Docker Compose starts office-converter with network_mode: none, a private PID
namespace, memory/CPU/PID limits, and three one-way queue volumes. The backend can
write requests/signals but only read responses; the sidecar has the inverse mounts.
For local Maven development on macOS, set OFFICE_CONVERSION_MODE=direct;
Seatbelt still denies IP networking and native limits remain active, but filesystem
reads are not isolated and only trusted local fixtures should be used. Linux direct
mode fails closed; use Docker Compose so conversion stays in the sidecar.
HTML rendering uses a separate Playwright sidecar with no network, a read-only root filesystem, non-root Chromium sandboxing, the version-matched Playwright seccomp profile, private tmpfs, and one-way queue volumes. HTML inputs must contain required CSS, scripts, fonts, SVG, and images inline. Local paths and external requests are blocked.
cd frontend
npm ci
npm run devVITE_JOB_API_URL defaults to /api/v1. VITE_API_URL remains available while
legacy operation pages migrate.
cd backend && mvn test
cd frontend && npm run lint && npm test && npm run build
cd frontend && npx playwright install chromium && npm run test:e2e
cd html-converter && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci --ignore-scripts && npm test
docker compose build --no-cacheTests use generated or synthetic fixtures only. Do not add customer or personal data.