<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ImgToolKit</title>
    <description>The latest articles on DEV Community by ImgToolKit (@imgtoolkit).</description>
    <link>https://dev.to/imgtoolkit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3951418%2F374bb6dc-4acf-4a66-b752-a47daad35911.png</url>
      <title>DEV Community: ImgToolKit</title>
      <link>https://dev.to/imgtoolkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imgtoolkit"/>
    <language>en</language>
    <item>
      <title>How I Built 100 Browser-Based Image Tools With No Server (FFmpeg WASM, PDF-lib, AI Background Removal)</title>
      <dc:creator>ImgToolKit</dc:creator>
      <pubDate>Mon, 25 May 2026 21:36:57 +0000</pubDate>
      <link>https://dev.to/imgtoolkit/how-i-built-100-browser-based-image-tools-with-no-server-ffmpeg-wasm-pdf-lib-ai-background-5838</link>
      <guid>https://dev.to/imgtoolkit/how-i-built-100-browser-based-image-tools-with-no-server-ffmpeg-wasm-pdf-lib-ai-background-5838</guid>
      <description>&lt;p&gt;When I started building ImgToolkit, the goal was simple: every image tool site I used either uploaded my files to some server I didn't trust, watermarked the output, or locked the useful features behind a $12/month plan.&lt;/p&gt;

&lt;p&gt;I wanted to build something where everything runs in the browser. Your files never leave your device. No server, no account, no paywall.&lt;/p&gt;

&lt;p&gt;This is the technical breakdown of how I got 100 tools working entirely client-side.&lt;/p&gt;

&lt;p&gt;The core idea: the browser is powerful enough&lt;br&gt;
Modern browsers have the Canvas API, WebAssembly, Web Workers, and file system access. With the right libraries, you can do things that felt server-only three years ago.&lt;/p&gt;

&lt;p&gt;Here's the stack I settled on:&lt;/p&gt;

&lt;p&gt;React + Vite — fast builds, lazy-loaded routes so each tool only loads what it needs&lt;br&gt;
Canvas API — handles 80% of image operations (resize, crop, rotate, watermark, convert formats)&lt;br&gt;
pdf-lib — pure JS PDF manipulation (merge, split, compress, add pages)&lt;br&gt;
pdfjs-dist — PDF rendering to canvas (for PDF to JPG conversion)&lt;br&gt;
FFmpeg WASM — video processing in the browser&lt;br&gt;
&lt;a class="mentioned-user" href="https://dev.to/imgly"&gt;@imgly&lt;/a&gt;/background-removal — AI background removal using ONNX models&lt;br&gt;
Tesseract.js — OCR, runs a full Tesseract engine via WASM&lt;br&gt;
browser-image-compression — handles the heavy lifting for image compression&lt;br&gt;
The interesting challenges&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;FFmpeg in the browser
FFmpeg WASM was the one I was most skeptical about. A 30MB WASM binary that runs a full media processing pipeline in a browser tab?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works. But there are gotchas:&lt;/p&gt;

&lt;p&gt;import { FFmpeg } from "@ffmpeg/ffmpeg";&lt;br&gt;
import { fetchFile, toBlobURL } from "@ffmpeg/util";&lt;br&gt;
const ffmpeg = new FFmpeg();&lt;br&gt;
await ffmpeg.load({&lt;br&gt;
  coreURL: await toBlobURL(&lt;code&gt;/ffmpeg-core.js&lt;/code&gt;, "text/javascript"),&lt;br&gt;
  wasmURL: await toBlobURL(&lt;code&gt;/ffmpeg-core.wasm&lt;/code&gt;, "application/wasm"),&lt;br&gt;
});&lt;br&gt;
await ffmpeg.writeFile("input.mp4", await fetchFile(file));&lt;br&gt;
await ffmpeg.exec(["-i", "input.mp4", "-q:a", "0", "-map", "a", "output.mp3"]);&lt;br&gt;
const data = await ffmpeg.readFile("output.mp3");&lt;/p&gt;

&lt;p&gt;The WASM binary needs SharedArrayBuffer, which requires cross-origin isolation headers (COOP + COEP). Getting those headers right in production took longer than writing the actual tool.&lt;/p&gt;

&lt;p&gt;Dynamic import on the FFmpeg tools was essential — you don't want 30MB loading on the homepage.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AI background removal with zero server calls
&lt;a class="mentioned-user" href="https://dev.to/imgly"&gt;@imgly&lt;/a&gt;/background-removal uses an ONNX model served from a CDN. First load downloads ~30MB of model weights. After that, it's cached by the browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;import { removeBackground } from "&lt;a class="mentioned-user" href="https://dev.to/imgly"&gt;@imgly&lt;/a&gt;/background-removal";&lt;br&gt;
const blob = await removeBackground(imageFile, {&lt;br&gt;
  publicPath: "&lt;a href="https://cdn.imgly.com/background-removal/.." rel="noopener noreferrer"&gt;https://cdn.imgly.com/background-removal/..&lt;/a&gt;.",&lt;br&gt;
  model: "medium",&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;The result is a PNG with a transparent background, generated entirely in the user's browser using WebGL/WASM inference. No API key, no per-request cost, no server. The quality is genuinely good — comparable to early Remove.bg results.&lt;/p&gt;

&lt;p&gt;The tricky part: onnxruntime-web must be installed as a direct dependency alongside the library, not just a peer dependency. Took me an embarrassingly long time to debug that.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client-side PDF manipulation
pdf-lib is underrated. You can merge, split, compress, add signatures, rotate pages, and add form fields — all in the browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;import { PDFDocument } from "pdf-lib";&lt;br&gt;
const mergedPdf = await PDFDocument.create();&lt;br&gt;
for (const file of files) {&lt;br&gt;
  const bytes = await file.arrayBuffer();&lt;br&gt;
  const pdf = await PDFDocument.load(bytes);&lt;br&gt;
  const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());&lt;br&gt;
  pages.forEach(p =&amp;gt; mergedPdf.addPage(p));&lt;br&gt;
}&lt;br&gt;
const merged = await mergedPdf.save();&lt;/p&gt;

&lt;p&gt;For "compress PDF", I re-encode all images inside the PDF at lower quality. Not perfect, but gets 30–60% size reduction on scanned documents without any server.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lazy loading everything
With 100 tools, the initial bundle would be enormous if not handled carefully. Every tool page is a lazy-loaded route:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;const RemoveBackground = lazy(() =&amp;gt; import("@/pages/remove-background"));&lt;br&gt;
const FfmpegVideoToMp3 = lazy(() =&amp;gt; import("@/pages/video-to-mp3"));&lt;/p&gt;

&lt;p&gt;Heavy libraries (FFmpeg, face-api, background-removal) are dynamically imported inside the page component, not at route level — so they only load when the user actually uses that tool.&lt;/p&gt;

&lt;p&gt;Initial page load is under 100KB of JS. A user who only compresses images never downloads any FFmpeg or ONNX code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Face blurring without a server
@vladmandic/face-api loads face detection models from jsDelivr CDN on first use. The models (~6MB) detect face bounding boxes in the browser. I then apply a CSS blur filter to the canvas region:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ctx.filter = &lt;code&gt;blur(${blurStrength}px)&lt;/code&gt;;&lt;br&gt;
ctx.drawImage(canvas, x, y, w, h, x, y, w, h);&lt;br&gt;
ctx.filter = "none";&lt;/p&gt;

&lt;p&gt;Works surprisingly well on photos with 1–4 faces. Degrades on crowds — but so does every commercial API at that task.&lt;/p&gt;

&lt;p&gt;What I learned&lt;br&gt;
The browser is ready. WebAssembly, ONNX inference, full PDF manipulation, video processing — it all works. The main limits are file size (very large files hit memory limits) and first-load time for WASM binaries.&lt;/p&gt;

&lt;p&gt;Lazy loading is non-negotiable. Without it, you're shipping 50MB of JS to every visitor regardless of which tool they use.&lt;/p&gt;

&lt;p&gt;Headers matter for WASM. SharedArrayBuffer requires COOP: same-origin and COEP: require-corp. Get these wrong and FFmpeg silently fails.&lt;/p&gt;

&lt;p&gt;Client-side means private by default. Users immediately trust a tool more when you can prove their files never leave their device. It's a genuine differentiator, not just a marketing claim.&lt;/p&gt;

&lt;p&gt;The site is at &lt;a href="https://imgtoolkit.com/" rel="noopener noreferrer"&gt;imgtoolkit.com&lt;/a&gt; — 100 tools, all free, all client-side. Happy to answer questions about any part of the implementation.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
