Most "free online converters" upload your files to a server. For an iPhone photo of your passport or a scanned ID card, that is not great. So I built Daily Solutions – a set of image, PDF, video and health tools where nothing leaves your browser.
Here is how it works under the hood, and a few things that surprised me along the way.
The stack
- Next.js with every tool page statically generated (80+ pages, each with its own title, FAQs and JSON-LD)
- Canvas + WebAssembly for images
- pdf-lib and PDF.js for PDFs
- ffmpeg.wasm for audio and video
- No backend for file processing at all – the server only serves static files
Images: the browser can do more than you think
canvas.toBlob() already encodes JPG and PNG everywhere, and WebP in most browsers. For the rest I load small codecs only when they are needed:
-
HEIC (iPhone photos) is decoded with libheif via
heic2any - AVIF/WebP fall back to jSquash (Squoosh codecs compiled to WASM) when the browser can't encode them natively
- TIFF via UTIF.js, GIF via gifenc, and BMP is a 30-line hand-written encoder
The feature people use most is "compress image to 50 KB" (exam and job portals love strict limits). That is a binary search over the quality setting:
let lo = 0.05, hi = 0.95, best = null;
for (let i = 0; i < 7; i++) {
const q = (lo + hi) / 2;
const blob = await encode(canvas, format, q);
if (blob.size <= maxBytes) { best = blob; lo = q; } else { hi = q; }
}
Seven encodes get within about 1% of the best possible quality. If even the lowest quality doesn't fit, the tool says so and suggests a smaller width instead of silently returning an ugly image.
PDFs: pdf-lib for structure, PDF.js for pixels
Merging, splitting, rotating, deleting and reordering pages are pure structural operations – pdf-lib copies pages without touching their content, so quality is preserved and it is fast.
Compression is harder. A lossless re-save with object streams helps a little; for scanned documents the real win is re-rendering each page with PDF.js and embedding it as an optimized JPEG. To hit a target like "under 200 KB", I binary-search over a ladder of (scale, quality) steps, so it takes about 3 renders instead of 7.
Gotcha: PDF.js renders through requestAnimationFrame. In a background tab rAF is paused, so compression silently stopped whenever a user switched tabs. Rendering with intent: "print" avoids rAF and fixed it.
Video: ffmpeg.wasm
The FFmpeg core is ~30 MB, so it is loaded only when someone presses "Convert" and is then cached by the browser. Two lessons:
-
Bundlers rewrite the worker's dynamic
import(), which breaks loading the core. Serving ffmpeg's worker files unbundled from/publicand passing an absoluteclassWorkerURLsolved it. -
ultrafastx264 makes files bigger than the input;superfastis a much better speed/size balance for MOV → MP4.
Things I'd tell my past self
- Turbopack hung forever on my Windows machine; webpack builds fine.
-
next/font/googleneeds network access at build time – system fonts are faster anyway. - A static site with
dynamicParams = falsegives real 404s for unknown URLs (important for SEO).
Try it
The code is on GitHub. Feedback welcome – which tool should I add next?
Top comments (0)