π§ Project Overview
Have you ever uploaded a photo of your ID or a bank statement to an online βImage to PDFβ converter? Most of us have, but the reality is uncomfortable: we donβt know which servers the files end up on, whether temporary copies get stored, or how anonymous those services really are.
With that concern in mind, I built my own solution and used it to put a key principle into practice: system design strictly driven by use cases and simplicity (YAGNI).
Current Features:
- 100% client-side and private: images never leave your device. Works completely offline.
- Native drag & drop: upload and reorder thumbnail cards using the HTML5 Drag & Drop API, no libraries.
- Client-side editor and redaction: blackout boxes over sensitive information (IDs, addresses, faces) and adjustable filters (brightness, contrast, B&W, sepia).
- Tiled watermarks: repeating diagonal stamping across all pages.
- Page options: fit to image, A4, or US Letter in Portrait, Landscape, or Auto orientation with custom margins.
- Zero build step: plain HTML5, CSS3, and JavaScript with pdf-lib.
π‘ The Initial Temptation vs. The Real Decision
At first, the typical development inertia invites you to set up:
- β FastAPI backend + Docker
- β Daemons and cron jobs to clean temporary files from disk
- β React/Preact frontend + heavy drag & drop libraries + Tailwind
- β Complex build pipelines (Vite/Webpack)
But does the use case actually need a server? The answer was NO. Before writing any code, I audited the proposed architecture against the use case and eliminated or replaced every layer that added nothing:
| Proposed component | Decision | Rationale |
|---|---|---|
| FastAPI + Uvicorn | Eliminated | Processing files in-memory in the browser eliminates server costs, latency, network transfer, and server-side data leaks. |
| Temp storage & cleanup daemons | Eliminated | Browser RAM handles byte streams natively without disk I/O or race conditions. |
| Docker & cloud hosting | Eliminated | The static app runs on GitHub Pages or via file:/// for $0/mo. |
| Preact + react-dropzone + dnd-kit | Replaced with native HTML5 | <input type="file" multiple>, <label for="...">, and the native Drag & Drop API replaced 200MB+ of node_modules. |
| Tailwind + bundlers | Replaced with vanilla CSS | Modern CSS with variables, flexbox, and grid eliminates the entire build pipeline. |
| Node/Jest test runner | Replaced with in-browser runner | test.html validates PDF generation, ordering, watermarks, and canvas filters directly in any browser. |
ποΈ Architecture: two static files
The result is an application consisting of two static files
(index.html + pdf-lib.min.js), deployable to GitHub Pages with
infinite scalability, maximum privacy, and zero maintenance.
graph LR
U[Drop / File picker<br/>native HTML5] --> O[URL.createObjectURL<br/>thumbnails in memory]
O --> E[Edit & redact modal<br/>Canvas 2D: blackout boxes<br/>brightness, contrast, B&W, sepia]
O --> R[Reorder and rotate<br/>HTML5 Drag & Drop<br/>array splice + 90 degrees]
E --> P[PDF compilation<br/>pdf-lib]
R --> P
P --> D[Instant download<br/>Blob URL + revokeObjectURL]
The pipeline in detail:
- Ingestion: native file input and drag-and-drop events read
the selected
Fileobjects and assign random IDs and lightweight object URLs. Nothing touches a disk or a server. - Client-side redaction and editing: when editing, the image is rendered onto an offscreen canvas. The user draws blackout rectangles to mask sensitive text; redactions and color matrix adjustments are rendered to a JPEG canvas stream.
- Reordering and state management: cards use native HTML5
drag events (
dragstart,dragover,drop) to reorder the item array in memory. - PDF generation (
pdf-lib): direct lossless embedding for unedited JPEGs and PNGs (preserves original fidelity and bypasses re-encoding); rotated, filtered, or redacted images go through the Canvas 2D API. Scaling calculations adjust the image aspect ratio to the chosen page size (fit to image, A4, US Letter) and margins. If a watermark is requested,StandardFonts.HelveticaBoldis stamped in a repeating diagonal grid across the entire page. - Zero-server export: the compiled byte array is wrapped in a
Blob, attached to an ephemeral anchor tag, triggered for download, and cleaned up from memory viaURL.revokeObjectURL.
π Key Technical Decisions
β 1. Zero bytes transferred
Images are processed in the browserβs RAM. Total privacy, zero latency, and full offline support. The use case β converting sensitive documents β is precisely the worst possible scenario for uploading bytes to a third party.
β 2. Native HTML5 instead of node_modules
The Canvas 2D API, native Drag & Drop, and <input type="file">
replaced hundreds of megabytes of dependencies. The only external
artifact is pdf-lib.min.js, served locally next to the HTML.
β 3. Redaction is a privacy feature, not a design one
Being able to cover sensitive data with blackout boxes before generating the PDF is part of the same privacy contract: the edit never touches the original image and never leaves the browser.
β 4. $0/mo infrastructure
No backend, no containers, no cron jobs, no pipelines. The entire
operation is a GitHub Pages deployment (or opening index.html
directly in the browser) plus an in-browser test suite (test.html)
that validates PDFs, ordering, and watermarks with zero host
dependencies.
π User Surface
graph LR
ADD["Add images<br/>drag & drop / file input"] --> GRID["Thumbnail grid<br/>reorder, rotate, sort"]
GRID --> EDIT["Edit<br/>redaction + filters"]
GRID --> CFG["Page options<br/>Fit / A4 / Letter<br/>orientation and margins"]
EDIT --> GEN["Generate PDF<br/>+ optional watermark"]
CFG --> GEN
GEN --> DL["Instant download<br/>straight from memory"]
π Current Outcome
βοΈ Full app in production on GitHub Pages β two static files, zero pipeline.
βοΈ Privacy contract verified by design: there is no server infrastructure that could store anything.
βοΈ Automated test suite running in-browser (test.html) with no
Node runner or host dependencies.
βοΈ Offline-capable: once loaded, the page needs no connection at all.
π Conclusion
Sometimes the best architecture is not the one that adds more layers or microservices, but the one brave enough to remove them when the use case allows it. imageToPdf proves that a converter with editing, redaction, watermarks, and page control needs nothing more than native HTML5 and a client-side PDF library β zero bytes transferred, zero infrastructure, zero maintenance.
Want to try it or read the source code?
- π Repository
- π Demo
π§ Interested in a similar approach?
If you are evaluating whether your use case really needs a backend (or want to talk about YAGNI and aggressive stack simplification), feel free to reach out π