PDF Thumbnailer
On this page
- At a glance
- What it does
- A. Renders the first page as a picture of the paper
- B. Keeps every thumbnail the same size, without distortion
- C. Moves the bytes directly between storage and the renderer
- D. Accepts only links to the storage it knows
- E. Leaves a trace of every render
- Interface
- Standards
- Inside
- Design decisions
- Specifications
- Related
- Terms
PDF Thumbnailer is a small Rust service that turns the first page of a PDF into a sharp, uniformly sized WebP image for document lists and cards. It never receives a file from the application and never returns one: it is handed two short-lived links — one to read the PDF from object storage, one to write the image back — and the bytes travel between storage and the renderer without passing through anyone else.
I built it because every screen that lists documents is easier to use when each one shows its first page, and rendering PDFs is exactly the kind of work an application server should not do itself. It is slow, memory-hungry, and depends on a native rendering library, so I gave it one service with one job, called in the background when a new document arrives.
At a glance
| Language & framework | Rust, with actix-web, Poppler and Cairo for rendering, and reqwest for storage transfers |
|---|---|
| Architecture | Flat Layered |
| Version | 2.11.3 |
| Status | Stable, in production |
| Form | A stateless HTTP service called server to server, with no database and no storage credentials |
| Source code | Private |
What it does
A. Renders the first page as a picture of the paper
The first page is drawn at 144 dots per inch onto a white canvas, so a scanned letter, a form, and a slide all look like the page they are, with no transparent areas that turn black or grey on a dark background.
B. Keeps every thumbnail the same size, without distortion
Pages come in every size and orientation. The service fits each one inside 1200 by 1600 pixels while keeping its proportions, so an A4 page becomes 1131 by 1600 and a list of documents lines up without stretching any of them.
C. Moves the bytes directly between storage and the renderer
The caller sends two presigned links: one to download the PDF, one to upload the WebP image. The service fetches, renders, uploads, and answers with a short confirmation. It holds no storage credentials, keeps nothing on disk, and has no database, so any number of copies can run side by side.
D. Accepts only links to the storage it knows
Both links must point to the expected storage origin and into the expected bucket, down to a file, before anything is downloaded or uploaded. A link anywhere else is refused with a validation error.
E. Leaves a trace of every render
Each request records which document and audit entry it belongs to, the size and SHA-256 of the PDF it received, and how long the download, render, and upload each took, as part of the caller's trace.
Interface
| Capability | Purpose |
|---|---|
| Render | render the first page of the PDF behind one presigned link and upload it as WebP to another |
| Liveness | for the container platform |
The request carries the two links and nothing else; the answer is a confirmation with the request id, never an image:
Standards
| Standard | What it guarantees here |
|---|---|
| PDF, rendered by Poppler | pages drawn by the same library many desktop PDF viewers use |
| WebP, lossless | sharp text in a format every current browser displays |
| Presigned URLs, AWS Signature Version 4 | time-limited access to one object, without storage credentials in the service |
| W3C Trace Context and OpenTelemetry | download, render, and upload appear as steps inside the caller's trace |
| UUIDv7 | request ids that never collide and sort by time, when the caller sends none |
Inside
The service follows the Flat Layered shape I use for Rust services that must work quickly: a route, a controller, a service, a validation, and helpers, side by side and one level deep. There is no repository layer because there is nothing to store; the rendering itself lives in one helper, and the storage transfers in one HTTP client shared by every request.
Design decisions
One job, rewritten in Rust, on the renderer that actually builds. The service began in Python, and I rewrote it in Rust. My first choice of renderer, pdfium, could not be found at build time; switching to its automatic download did not settle it, so the next day I moved to Poppler with Cairo — a library that installs from the operating system's packages and links like any other — and with it came the first working thumbnails.
One scale, applied once. The first Poppler version produced thumbnails whose content sat small in the top-left corner of a mostly white image. The scale was being applied twice: once to size the canvas and again when drawing onto it. I replaced the two calculations with one final scale — the rendering resolution multiplied by whatever ratio fits the page into the size limit — and applied it to the drawing context exactly once, so the page fills the canvas edge to edge. A white background is painted first and the canvas has no alpha channel, because a PDF page is paper, not a transparent layer.
Lossless, and small because of its size limit. Document pages are dominated by small letters and thin lines, and lossless WebP keeps every one of them exactly as rendered. What keeps the file small is the limit on its dimensions, not the compression.
The service never touches a credential or a file it was not given. Taking links rather than files means the application does not stream PDFs through itself, the service does not hold storage keys, and storage decides how long each link lives. The caller requests the upload slot in a temporary area and promotes the image only after the service confirms success, so a failed render leaves nothing behind in the permanent area.
Allowed sources are part of the code, and they are compared, not matched as text. Each link is parsed and its origin — scheme, host, and port — compared exactly with an allowlist that lives in the source, so changing it requires a release, not just a configuration edit. The bucket is compared as a whole path segment, and the path must continue to an actual file.
Rendering never blocks the server, and the evidence is computed once. Poppler is a blocking C library, so each render runs on a blocking worker thread while the server keeps answering other requests. The SHA-256 of the downloaded PDF is computed once, before the bytes move to that thread, and recorded for the audit trail.
Telemetry shows every step, and hides every signature. Telemetry moved from writing straight to Elasticsearch to OpenTelemetry through a collector, continuing the trace of the job that called the service. The download and the upload each appear as a client span, but the query string of a presigned link — which carries its signature — is never recorded as an attribute. The caller's request id is honoured so the audit chain stays intact, and correlation headers for the user, the document, and the audit entry are recorded with each request. Metrics use delta temporality: for a while the request counter kept arriving while the duration histogram was silently dropped by the collector's Elasticsearch exporter, so the service looked monitored but had no latency data at all. Metric labels use the route pattern and never ids or addresses, and the client address comes from forwarded headers only when they arrive from trusted proxies.
Health that the platform can actually check. A liveness endpoint answers without touching anything else, because the service has no dependency of its own to report. When it was added, the container stayed unhealthy even though the service ran — the slim base image had no curl for the health check to call — so the image now includes it, like the other Rust services beside it. The image runs as a non-root user and reads its configuration from a mounted volume.
Specifications
| Inputs | A presigned link to read a PDF and a presigned link to upload the thumbnail |
|---|---|
| Outputs | A lossless WebP image of the first page, uploaded to the given link; a JSON confirmation to the caller |
| Rendering | 144 DPI, fitted inside 1200 × 1600 pixels with proportions kept, on a white background |
| State | None: no database, no files on disk, no storage credentials |
| Observability | OpenTelemetry traces, logs, and metrics over OTLP, or standard output only |
| Runs as | A container image, as a non-root user, with configuration mounted from a volume |
Related
The storage service that issues both links: the one the PDF is read from and the upload slot the thumbnail is written to.
Where the service's traces, logs, and metrics go once they leave it.
Terms
- Thumbnail
- A small image of a document's first page, shown in lists and cards so a reader recognises the document at a glance.
- Presigned URL
- A temporary link, signed with storage credentials, that allows one specific upload or download without giving the client those credentials.
- Poppler
- An open-source library that reads PDF files and draws their pages.
- Lossless compression
- Compression that makes a file smaller without changing a single pixel of the image.
- Origin
- The scheme, host, and port of a web address — the part that identifies which server a link points to.
Technologies covered
- Rust
- Poppler
- OpenTelemetry
- S3