The starting line
I was at the office when I noticed a workmate editing photos for Facebook posts. The workflow was simple and painfully manual: open a photo, paste an overlay, resize it, position the frame, add a watermark, save, close, open the next one. Repeat for every image in the batch.
The work was repetitive and time-consuming, especially when handling large batches of images. The kind of task that makes you think there has to be a better way, and then you realize the better way doesn't exist yet. So I built it.
What it needed to do
The core idea was straightforward: upload multiple photos, apply the same treatment to all of them, and export everything at once. But the specifics mattered.
- PNG overlays. Branded frames, logos, decorative borders, any PNG with transparency, dropped on top of every photo in the batch.
- Watermarks. Text or image watermarks positioned once, applied everywhere. Content creators and government offices both need this.
- Social media frames. Pre-sized templates for Facebook, Instagram, and other platforms, photos come out ready to post.
- Resizing and adjustments. Exposure, contrast, highlights, shadows, clarity, Lightroom-style sliders that apply to the whole batch.
- Batch export. Download as individual PNGs or a single ZIP. No click-per-image downloading.
Architecture in one picture
flowchart TB
subgraph browser["Browser (Client)"]
direction LR
upload["Upload (TUS)"]
canvas["Canvas Rendering"]
zip["ZIP Export"]
end
upload --> canvas --> zip
style browser fill:#ece9e2,stroke:#bdb5a6,color:#2b2b25
style upload fill:#ece9e2,stroke:#bdb5a6,color:#2b2b25
style canvas fill:#ece9e2,stroke:#bdb5a6,color:#2b2b25
style zip fill:#ece9e2,stroke:#bdb5a6,color:#2b2b25
The key architectural decision: everything runs in the browser. No uploaded image ever touches a server for processing. The Canvas API handles compositing, the browser handles memory, and the ZIP is assembled client-side using a streaming library. The server only stores the files, it never sees pixels.
Resumable uploads with TUS
The first version used standard multipart uploads. It worked for small photos. It failed for large batches on slow connections, a single dropped connection meant re-uploading everything from the start.
The fix was the TUS protocol, the open standard for resumable file uploads. When a user selects 50 photos, each one uploads in chunks. If the connection drops at chunk 7 of 12, the next request resumes at chunk 8 instead of starting over. On slow or unstable connections, this is the difference between a tool that works and a tool that frustrates.
// Upload flow
1. User selects photos
2. Each photo gets a TUS upload session
3. Chunks resume on connection drop
4. Server stores originals in object storage
5. Client-side Canvas renders the final output
Canvas-based image processing
The rendering pipeline runs entirely in the browser. When the user clicks export, for each photo the client:
- Loads the original image into an offscreen Canvas element.
- Applies the editing adjustments, exposure, contrast, highlights, shadows, clarity, dehaze, vignette, using pixel-level Canvas operations.
- Composites the overlay template on top, respecting portrait and landscape orientation.
- Resizes to the target dimensions if specified.
- Exports the Canvas content as a PNG blob.
Running this on the client means zero server costs for processing, instant preview of every change, and no waiting in a queue. The tradeoff is that the browser's memory is the ceiling, but for the batch sizes this tool targets (dozens, not thousands), the browser handles it comfortably.
Auto orientation detection
Photos come in all orientations. A portrait photo needs a different template placement than a landscape one. The tool detects each image's orientation from its EXIF data and dimensions, then applies the matching template variant automatically. The user doesn't have to think about it, they upload a mixed batch and get consistent results.
Lightroom-style editing
The editing panel gives users fine-grained control over the final output. Exposure, contrast, highlights, shadows, clarity, dehaze, vignette, and split toning for highlights and shadows independently. There are also 17 built-in presets across 5 categories for users who want quick results without manual tweaking.
The editing state is reactive, every slider change re-renders the preview canvas in real time. Users see exactly what they'll get before they export.
ZIP batch export with progress
Exporting 50 photos as individual PNGs means 50 separate downloads. That's not a workflow, that's a chore. The ZIP export bundles everything into a single download with a progress indicator showing which photo is being processed and how many are left.
The ZIP is assembled streaming, as each photo finishes rendering on the Canvas, it gets appended to the ZIP immediately. The user sees progress incrementing in real time instead of waiting for all 50 to render before anything downloads.
What broke, or surprised me
Memory limits on large batches
The first version tried to render all photos simultaneously. The browser crashed on batches larger than about 20 images. The fix was sequential rendering, process one photo at a time, flush the Canvas, move to the next. Slower in theory, but it actually completes.
Template alignment across orientations
Portrait and landscape photos need different template placements, but users upload mixed batches. I initially tried a single template with auto-scaling, which produced misaligned frames on edge cases. The solution was separate template variants per orientation, uploaded once and applied automatically based on each image's dimensions.
Large file uploads on mobile
Mobile users uploading high-resolution photos from their camera roll hit upload timeouts more often than desktop users. TUS solved the resumability problem, but I also added client-side compression as an option, downscale to a configurable maximum dimension before upload. Most users don't need 24-megapixel originals for a Facebook post.
What it does today
- Upload multiple photos and apply branded overlays, watermarks, and frames to every image in the batch.
- Auto portrait/landscape orientation detection with separate template variants.
- Lightroom-style editing: exposure, contrast, highlights, shadows, clarity, dehaze, vignette, split toning.
- 17 built-in presets across 5 categories.
- Resumable chunked uploads via TUS, no lost progress on slow connections.
- Apply edits or presets to all selected photos at once.
- ZIP batch export with real-time progress tracking.
- Live preview of template placement and edits before export.
What I'd do differently next time
- Web Workers for rendering. The Canvas operations currently run on the main thread. Moving them to a Web Worker would keep the UI responsive during large batch exports.
- Server-side processing option. For users with thousands of images or very large files, a server-side pipeline with queue-based processing would be more reliable than browser memory limits.
- Template marketplace. Users keep asking for pre-made templates. A curated marketplace or community gallery would add value without me designing every template.
What I take away
The most valuable thing about this project is not the image processing or the upload pipeline. It's that it started from watching someone do boring, repetitive work and thinking I can fix that. The technical decisions, client-side rendering, TUS uploads, streaming ZIP export, all served one goal: make a tedious task disappear.
