Skip to content

PDF Thumbnailer

By Romi Fadli, S.Kom. 8 min read Storage & Data
On this page
PDF pages of different sizes passing through a renderer that reads from and writes back to storage, producing thumbnails of one uniform size

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 & frameworkRust, with actix-web, Poppler and Cairo for rendering, and reqwest for storage transfers
ArchitectureFlat Layered
Version2.11.3
StatusStable, in production
FormA stateless HTTP service called server to server, with no database and no storage credentials
Source codePrivate
What the module is and where it stands.

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.

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

CapabilityPurpose
Renderrender the first page of the PDF behind one presigned link and upload it as WebP to another
Livenessfor the container platform
The service's HTTP interface.

The request carries the two links and nothing else; the answer is a confirmation with the request id, never an image:

json
{
  "document_url": "https://storage.example.org/documents/contract.pdf?X-Amz-Signature=…",
  "upload_thumbnail_to_url": "https://storage.example.org/thumbnails/0199a3c2.webp?X-Amz-Signature=…"
}

Standards

StandardWhat it guarantees here
PDF, rendered by Popplerpages drawn by the same library many desktop PDF viewers use
WebP, losslesssharp text in a format every current browser displays
Presigned URLs, AWS Signature Version 4time-limited access to one object, without storage credentials in the service
W3C Trace Context and OpenTelemetrydownload, render, and upload appear as steps inside the caller's trace
UUIDv7request ids that never collide and sort by time, when the caller sends none
The standards the service follows, and what each one guarantees.

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

InputsA presigned link to read a PDF and a presigned link to upload the thumbnail
OutputsA lossless WebP image of the first page, uploaded to the given link; a JSON confirmation to the caller
Rendering144 DPI, fitted inside 1200 × 1600 pixels with proportions kept, on a white background
StateNone: no database, no files on disk, no storage credentials
ObservabilityOpenTelemetry traces, logs, and metrics over OTLP, or standard output only
Runs asA container image, as a non-root user, with configuration mounted from a volume
What the service takes, returns, and needs.

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

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

Employees and administrators use the Web App, which talks to the Core API; the Core API uses storage, regions, face and preview services, sends documents to the Aurograf signing platform, sends logs and traces to the log stack, and imports from the old system

SIMPEG

Programming

The personnel information system of the City Government of South Tangerang: employees keep their own records and follow their services, administrators work on exactly their part of the organisation, and every rule comes from the regulation that sets it. Led and built by me since 2023.

10 min read Read more →
The core API at the centre, fed by storage, address, face and preview services, with documents sent across a boundary to an external signing platform

The Laravel core of a civil-service personnel system: employee records and career histories, self-service proposals with review, salary-step, promotion and retirement decisions, rules taken from regulations, a versioned organisation chart, and access scoped down to a single unit.

11 min read Read more →
A central core holding people, organisations, documents, and certificates, connected to eight surrounding services for storage, addresses, certificates, timestamps, the root, signing, thumbnails, and face challenges

The Laravel core of a document-signing platform: it owns users, identities, organisations, client systems, documents and their signed versions, stamps, and certificates, and orchestrates the services that sign, timestamp, render, and store — without ever holding a file.

9 min read Read more →
A purple root circle linked to a blue and an orange intermediate circle, which lead to a signed document with a green seal and to a clock.

Aurograf

DevOps

The digital signature platform of the City Government of South Tangerang: signers prove who they are, sign PDFs to PAdES B-LTA with certificates from the platform's own authorities, and anyone can verify the result — version 2, which I designed, built and operate.

10 min read Read more →

Related Articles

A browser window connected directly to a storage bucket by a thick green band, while an orange box with a key above them hands the browser a dashed link.

A small Rust service between applications and S3-compatible storage: it hands out presigned links so browsers talk to storage directly, gives each file a lifecycle, copies between buckets on the server, and keeps storage credentials out of every application.

12 min read Read more →