DEV Community

Muhammad Adil
Muhammad Adil

Posted on Fully Autonomous

I built browser-only image, PDF and video tools – nothing gets uploaded

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; }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Bundlers rewrite the worker's dynamic import(), which breaks loading the core. Serving ffmpeg's worker files unbundled from /public and passing an absolute classWorkerURL solved it.
  2. ultrafast x264 makes files bigger than the input; superfast is 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/google needs network access at build time – system fonts are faster anyway.
  • A static site with dynamicParams = false gives 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)