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 adalah service Rust kecil yang mengubah halaman pertama PDF menjadi gambar WebP yang tajam dan berukuran seragam, untuk daftar dan kartu dokumen. Ia tak pernah menerima file dari aplikasi dan tak pernah mengembalikan file: ia hanya diberi dua link berumur pendek — satu untuk membaca PDF dari object storage, satu untuk menulis gambarnya kembali — dan byte-nya mengalir antara storage dan renderer tanpa melewati pihak lain.

Saya membangunnya karena setiap layar yang menampilkan daftar dokumen jauh lebih mudah dipakai bila tiap dokumen memperlihatkan halaman pertamanya, sementara merender PDF justru jenis pekerjaan yang tak sepatutnya dikerjakan server aplikasi sendiri. Pekerjaan itu lambat, rakus memori, dan bergantung pada library render native. Jadi saya memberinya satu service dengan satu tugas, dipanggil di latar belakang setiap kali dokumen baru masuk.

Sekilas

Bahasa & frameworkRust, dengan actix-web, Poppler dan Cairo untuk render, serta reqwest untuk transfer ke storage
ArsitekturFlat Layered
Versi2.11.3
StatusStabil, berjalan di production
BentukService HTTP tanpa state yang dipanggil antar-server, tanpa database dan tanpa kredensial storage
Kode sumberPrivat
Apa module ini dan bagaimana keadaannya.

Yang dikerjakannya

A. Merender halaman pertama sebagai gambar kertasnya

Halaman pertama digambar pada 144 titik per inci di atas kanvas putih. Surat hasil scan, formulir, dan slide semuanya tampak seperti halaman aslinya, tanpa area transparan yang berubah hitam atau abu-abu di latar gelap.

B. Membuat setiap thumbnail berukuran sama, tanpa distorsi

Halaman datang dalam segala ukuran dan orientasi. Service memasukkan tiap halaman ke dalam 1200 kali 1600 piksel dengan proporsi tetap terjaga: halaman A4 menjadi 1131 kali 1600, dan daftar dokumen tersusun rapi tanpa ada yang tertarik melebar.

C. Memindahkan byte langsung antara storage dan renderer

Pemanggil mengirim dua presigned link: satu untuk mengunduh PDF, satu untuk meng-upload gambar WebP. Service mengambil, merender, meng-upload, lalu menjawab dengan konfirmasi singkat. Ia tak memegang kredensial storage, tak menyimpan apa pun di disk, dan tak punya database, jadi berapa pun salinannya bisa berjalan berdampingan.

Kedua link harus menunjuk origin storage yang diharapkan dan masuk ke bucket yang diharapkan, sampai ke satu file, sebelum apa pun diunduh atau di-upload. Link ke tempat lain ditolak dengan error validasi.

E. Meninggalkan jejak setiap render

Tiap request mencatat dokumen dan entri audit mana yang menjadi miliknya, ukuran dan SHA-256 PDF yang diterimanya, serta berapa lama unduhan, render, dan upload masing-masing berlangsung, sebagai bagian dari trace pemanggil.

Antarmuka

KemampuanKegunaan
Rendermerender halaman pertama PDF di balik satu presigned link dan meng-upload-nya sebagai WebP ke link lain
Livenessuntuk platform container
Antarmuka HTTP service ini.

Request hanya membawa dua link itu; jawabannya konfirmasi berisi request id, tak pernah gambar:

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=…"
}

Standar

StandarYang dijaminnya di sini
PDF, dirender Popplerhalaman digambar oleh library yang sama dengan yang dipakai banyak penampil PDF desktop
WebP, losslessteks yang tajam dalam format yang ditampilkan semua browser masa kini
Presigned URL, AWS Signature Version 4akses berbatas waktu ke satu objek, tanpa kredensial storage di dalam service
W3C Trace Context dan OpenTelemetryunduhan, render, dan upload tampil sebagai langkah di dalam trace pemanggil
UUIDv7request id yang tak pernah bertabrakan dan terurut menurut waktu, bila pemanggil tak mengirimnya
Standar yang dipatuhi service, dan yang dijamin masing-masing.

Di dalamnya

Service ini mengikuti bentuk Flat Layered yang saya pakai untuk service Rust yang harus cepat jadi: route, controller, service, validasi, dan helper berjajar satu tingkat. Tak ada lapisan repository karena memang tak ada yang disimpan; render-nya sendiri tinggal di satu helper, dan transfer ke storage di satu HTTP client yang dipakai bersama oleh semua request.

Keputusan rancangan

Satu tugas, ditulis ulang dalam Rust, di atas renderer yang benar-benar bisa di-build. Service ini awalnya berbahasa Python, lalu saya menulisnya ulang dalam Rust. Renderer pilihan pertama saya, pdfium, tak ditemukan saat build; beralih ke unduhan otomatisnya pun tak menyelesaikannya, jadi keesokan harinya saya pindah ke Poppler dengan Cairo — library yang terpasang dari paket sistem operasi dan di-link seperti library lain — dan bersamanya lahir thumbnail pertama yang berfungsi.

Satu skala, diterapkan sekali. Versi Poppler pertama menghasilkan thumbnail yang isinya duduk kecil di pojok kiri atas, dikelilingi gambar yang sebagian besar putih. Skalanya diterapkan dua kali: sekali untuk menentukan ukuran kanvas, dan sekali lagi saat menggambar di atasnya. Saya mengganti dua perhitungan itu dengan satu skala akhir — resolusi render dikalikan rasio yang memasukkan halaman ke dalam batas ukuran — dan menerapkannya ke context gambar tepat satu kali, sehingga halaman memenuhi kanvas dari tepi ke tepi. Latar putih dicat lebih dulu dan kanvas tak punya alpha channel, karena halaman PDF adalah kertas, bukan lapisan transparan.

Lossless, dan kecil karena batas ukurannya. Halaman dokumen didominasi huruf kecil dan garis tipis, dan WebP lossless menjaga setiap huruf dan garis itu persis seperti hasil render-nya. Yang membuat file tetap kecil adalah batas dimensinya, bukan kompresinya.

Service tak pernah menyentuh kredensial maupun file yang tak diberikan kepadanya. Menerima link ketimbang file membuat aplikasi tak perlu mengalirkan PDF melewati dirinya, service tak memegang key storage, dan storage-lah yang menentukan umur tiap link. Pemanggil meminta slot upload di area sementara dan baru mempromosikan gambarnya setelah service mengonfirmasi berhasil, jadi render yang gagal tak meninggalkan apa pun di area permanen.

Sumber yang diizinkan adalah bagian dari kode, dan dibandingkan, bukan dicocokkan sebagai teks. Tiap link di-parse dan origin-nya — skema, host, dan port — dibandingkan persis dengan allowlist yang tinggal di source. Mengubahnya butuh rilis, bukan sekadar mengedit konfigurasi. Bucket dibandingkan sebagai satu segmen path utuh, dan path harus berlanjut sampai ke file sungguhan.

Render tak pernah memblokir server, dan buktinya dihitung sekali. Poppler adalah library C yang memblokir, jadi setiap render berjalan di thread pekerja blocking sementara server tetap menjawab request lain. SHA-256 dari PDF yang diunduh dihitung sekali, sebelum byte-nya pindah ke thread itu, lalu dicatat untuk jejak audit.

Telemetry memperlihatkan setiap langkah, dan menyembunyikan setiap signature. Telemetry pindah dari menulis langsung ke Elasticsearch menjadi OpenTelemetry lewat collector, melanjutkan trace dari job yang memanggil service. Unduhan dan upload masing-masing tampil sebagai client span, tapi query string presigned link — yang membawa signature-nya — tak pernah dicatat sebagai atribut. Request id dari pemanggil dihormati supaya rantai audit tetap utuh, dan header korelasi untuk user, dokumen, dan entri audit dicatat bersama tiap request. Metrics memakai delta temporality: sempat counter request terus masuk sementara histogram durasi dibuang diam-diam oleh exporter Elasticsearch milik collector, sehingga service tampak terpantau padahal tak punya data latensi sama sekali. Label metrics memakai pola route, tak pernah id atau alamat, dan alamat client diambil dari header forwarded hanya bila datang dari trusted proxy.

Kesehatan yang benar-benar bisa diperiksa platform. Endpoint liveness menjawab tanpa menyentuh apa pun lagi, karena service tak punya dependensi sendiri untuk dilaporkan. Saat endpoint itu ditambahkan, container tetap berstatus unhealthy padahal service berjalan — image dasar yang ramping tak punya curl untuk dipanggil health check — jadi kini image menyertakannya, seperti service Rust lain di sampingnya. Image berjalan sebagai user non-root dan membaca konfigurasinya dari volume yang di-mount.

Spesifikasi

MasukanPresigned link untuk membaca PDF dan presigned link untuk meng-upload thumbnail
HasilGambar WebP lossless dari halaman pertama, di-upload ke link yang diberikan; konfirmasi JSON untuk pemanggil
Render144 DPI, dimasukkan ke dalam 1200 × 1600 piksel dengan proporsi terjaga, di atas latar putih
StateTidak ada: tanpa database, tanpa file di disk, tanpa kredensial storage
ObservabilityTrace, log, dan metrics OpenTelemetry lewat OTLP, atau stdout saja
Berjalan sebagaiImage container, sebagai user non-root, dengan konfigurasi di-mount dari volume
Yang diterima, dikembalikan, dan dibutuhkan service.

Terkait

Service storage yang menerbitkan kedua link: tempat PDF dibaca dan slot upload tempat thumbnail ditulis.

Ke mana trace, log, dan metrics service ini pergi setelah meninggalkannya.

Istilah

Thumbnail
Gambar kecil halaman pertama dokumen, ditampilkan di daftar dan kartu supaya pembaca mengenali dokumen dalam sekali lihat.
Presigned URL
Link sementara yang ditandatangani dengan kredensial storage, yang mengizinkan satu upload atau download tertentu tanpa memberikan kredensial itu kepada client.
Poppler
Library open source yang membaca file PDF dan menggambar halaman-halamannya.
Lossless compression
Kompresi yang membuat file lebih kecil tanpa mengubah satu piksel pun dari gambarnya.
Origin
Skema, host, dan port dari alamat web — bagian yang menentukan server mana yang ditunjuk satu link.

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

Sistem informasi kepegawaian Pemerintah Kota Tangerang Selatan: pegawai mengurus record-nya sendiri dan mengikuti layanannya, admin bekerja tepat pada bagian organisasinya, dan setiap aturan berasal dari peraturan yang menetapkannya. Saya pimpin dan bangun sejak 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

Inti Laravel sistem kepegawaian aparatur sipil negara: record pegawai dan riwayat karier, usulan swalayan yang diperiksa, keputusan kenaikan gaji berkala, kenaikan pangkat, dan pensiun, aturan dari peraturan, bagan organisasi berversi, dan akses berlingkup sampai satu 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

Inti Laravel dari platform penandatanganan dokumen: memegang user, identitas, organisasi, sistem client, dokumen beserta versi bertanda tangannya, stempel, dan sertifikat, serta mengorkestrasi service yang menandatangani, menstempel waktu, merender, dan menyimpan — tanpa pernah memegang 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

Platform tanda tangan digital Pemerintah Kota Tangerang Selatan: penanda tangan membuktikan identitasnya, menandatangani PDF hingga PAdES B-LTA dengan sertifikat dari otoritas milik platform sendiri, dan siapa pun bisa memverifikasinya — versi 2, yang saya rancang, bangun, dan operasikan.

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.

Service Rust kecil di antara aplikasi dan storage kompatibel S3: ia membagikan presigned link supaya browser berbicara langsung dengan storage, memberi tiap file siklus hidup, menyalin antar-bucket di sisi server, dan menjauhkan kredensial storage dari setiap aplikasi.

12 min read Read more →