Skip to content
SH-02CDetail C

PDF API

Solo system

A public FastAPI service that compresses, converts, OCRs, redacts, protects and repairs PDFs. Heavy tools run as subprocesses with timeouts, and repair runs in a separate process under a memory limit, so a hostile file cannot take the worker down. It ships to Cloud Run through a keyless pipeline whose smoke test demands a 401.

Type
API / Documents
Status
Deployed

One upload, contained. In order: POST /v2/<tool>, request id · one error envelope; then API key, fail-closed in production; then Chunked read, stops at the size cap; then Thread pool, blocking work off the event loop; then Subprocess tools, each with a timeout, each in a temporary directory; then Repair worker, a separate process under a memory limit and a wall clock.

Side lane — the pipeline: Lint; Tests, 213 of them; Keyless deploy, OIDC, no stored credential; Smoke test, demands a 401.

Fig. 01 — One upload, contained
  1. 1Cost tracks vector paths, not pages: a 0.03 s count refuses first.
  2. 2Redaction also clears the bookmarks, where titles leak.
  3. 3The timeout budget is itself a test: every tool must finish under the proxy’s deadline.
Problem
Four production timeouts from one 1.4 MB file: every retry died near the 55-second mark.
Constraint
One container, one request at a time, a 60-second ceiling — and uploads from strangers.
Decision
Measure what the cost tracks: vector paths, not pages or bytes. Count them first, in 0.03 s, and refuse with an answer the user can act on.
Outcome
The same file now gets a clear 'split it' at once. 213 tests guard the service, among them a purpose-built corpus of corrupt files.

General notes

  1. 01The Ghostscript version is gated at build against the distro's security floor.
  2. 02Concurrency 1 and 2 GiB are a documented floor, not a default.
  3. 03Redaction also clears the bookmarks, where titles leak.
  4. 04The timeout budget is itself a test.
  5. 05Spreadsheet cells are neutralised against formula injection.

Bill of quantities

Item, quantity, and the command that measured it.
ItemQuantity
Endpointsgrep -c '@router.post' app/router_v2.py15
Application lineswc -l over git ls-files app/*.py3,035
Test filesgit ls-files tests | grep -c '.py$'33
Test functionsgrep -rhoE 'def test_[a-zA-Z0-9_]+' tests | wc -l213
Binary fixturesls tests/fixtures | wc -l11
OCR languagesgrep -ohE 'tesseract-ocr-[a-z]{3}' Dockerfile | sort -u | wc -l8
CI jobsawk over the jobs: block of .github/workflows/deploy.yml2
Commits, default branch, non-mergegit rev-list --count --no-merges origin/main, 2026-09-1859

Detail 1:1

            # pdf2docx inspects every vector path hunting for table borders, so            # its cost tracks path count — not pages, not bytes. Measured on            # 10-page files against the 45s subprocess budget:            #            #   vector items |  2 010 | 15 010 | 30 010 | 40 010 | 50 010            #   convert time |  0.6 s |  2.5 s | 10.0 s | 19.4 s | 30.7 s            #            # Superlinear, and on hardware faster than the 2-vCPU container.            # A larger file of 180 plain-text pages carries zero paths and            # converts in 7.8 s, which is why this counts paths and not size.            #            # This is the shape behind four 504s on 2026-08-13: one 1.4 MB PDF,            # four retries in five minutes, every one dying at ~55 s. Counting            # costs 0.03 s, so saying no quickly is nearly free.            vector_items = sum(                len(drawing.get("items", ()))                for page in doc                for drawing in page.get_cdrawings()            )            if vector_items > get_settings().max_vector_items:                raise ApiError(                    status_code=422,                    code="too_complex_pdf",                    message=(                        "Este PDF tem demasiados elementos gráficos para converter "                        "para Word dentro do tempo limite. Divida-o primeiro com a "                        "ferramenta Dividir PDF e converta cada parte."                    ),                )
Detail 1:1 — app/services/pdf_tools.py, lines 11201148, as of 2026-09-09 (7bf5770). The measured table behind the refusal, the sentence that makes it cheap, and the guard that spends the 0.03 s and says no.

Stack used

PythonFastAPIPyMuPDFpikepdfOCRmyPDFGhostscriptTesseractLibreOfficeDockerCloud RunGitHub Actionspytest
Detail C

Legend — the marks on these sheets

SH-xx
Sheet number. Every drawing in a set carries one, and the set is read in that order.
ESC 1:20
Scale. One unit on the drawing stands for twenty on the thing drawn.
DETAIL A/B/C
Detail callout. A lettered part of a drawing, enlarged on a sheet of its own.
SECTION C-C
Section mark. The line a cut is taken along, lettered at both ends.
BH-01
Borehole log. What a trial hole found, recorded downward by depth.
EOB
End of borehole. The depth at which the log stops.
REV. 2026
Revision. The issue a sheet belongs to; a later revision supersedes it.
As-built register
A table of what was actually built, set against what was drawn.

Press ? to open or close. Escape closes.