PDF Thumbnailer
On this page
- Sekilas
- Yang dikerjakannya
- A. Merender halaman pertama sebagai gambar kertasnya
- B. Membuat setiap thumbnail berukuran sama, tanpa distorsi
- C. Memindahkan byte langsung antara storage dan renderer
- D. Hanya menerima link ke storage yang dikenalnya
- E. Meninggalkan jejak setiap render
- Antarmuka
- Standar
- Di dalamnya
- Keputusan rancangan
- Spesifikasi
- Terkait
- Istilah
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 & framework | Rust, dengan actix-web, Poppler dan Cairo untuk render, serta reqwest untuk transfer ke storage |
|---|---|
| Arsitektur | Flat Layered |
| Versi | 2.11.3 |
| Status | Stabil, berjalan di production |
| Bentuk | Service HTTP tanpa state yang dipanggil antar-server, tanpa database dan tanpa kredensial storage |
| Kode sumber | Privat |
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.
D. Hanya menerima link ke storage yang dikenalnya
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
| Kemampuan | Kegunaan |
|---|---|
| Render | merender halaman pertama PDF di balik satu presigned link dan meng-upload-nya sebagai WebP ke link lain |
| Liveness | untuk platform container |
Request hanya membawa dua link itu; jawabannya konfirmasi berisi request id, tak pernah gambar:
Standar
| Standar | Yang dijaminnya di sini |
|---|---|
| PDF, dirender Poppler | halaman digambar oleh library yang sama dengan yang dipakai banyak penampil PDF desktop |
| WebP, lossless | teks yang tajam dalam format yang ditampilkan semua browser masa kini |
| Presigned URL, AWS Signature Version 4 | akses berbatas waktu ke satu objek, tanpa kredensial storage di dalam service |
| W3C Trace Context dan OpenTelemetry | unduhan, render, dan upload tampil sebagai langkah di dalam trace pemanggil |
| UUIDv7 | request id yang tak pernah bertabrakan dan terurut menurut waktu, bila pemanggil tak mengirimnya |
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
| Masukan | Presigned link untuk membaca PDF dan presigned link untuk meng-upload thumbnail |
|---|---|
| Hasil | Gambar WebP lossless dari halaman pertama, di-upload ke link yang diberikan; konfirmasi JSON untuk pemanggil |
| Render | 144 DPI, dimasukkan ke dalam 1200 × 1600 piksel dengan proporsi terjaga, di atas latar putih |
| State | Tidak ada: tanpa database, tanpa file di disk, tanpa kredensial storage |
| Observability | Trace, log, dan metrics OpenTelemetry lewat OTLP, atau stdout saja |
| Berjalan sebagai | Image container, sebagai user non-root, dengan konfigurasi di-mount dari volume |
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