DEV Community

Cover image for I built an image compressor that never uploads your files
Lytepic
Lytepic

Posted on

I built an image compressor that never uploads your files

Most online image compressors send your photo to a server, compress it there, and send it back. For a job that a modern browser can easily handle natively, this traditional approach is slower than it needs to be. More importantly, it poses a privacy risk for sensitive items like ID photos or personal pictures.

To solve this, I built Lytepic — a free, lightweight image compressor that runs entirely client-side in your browser. No servers, no uploads, 100% private.


🛠 The Basics

The core logic relies on two simple steps: drawing the image onto an HTML5 canvas at the desired dimensions, and then exporting it at a specific quality setting.

const canvas = document.createElement('canvas');
canvas.width = w; 
canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);

canvas.toBlob(blob => { 
  /* Handle download or preview here */ 
}, 'image/jpeg', 0.8);
Enter fullscreen mode Exit fullscreen mode

🎯 Hitting an Exact Target Size (e.g., "Under 100 KB")

Government portals and online forms often require files to be strictly under a specific limit, like 50 KB or 100 KB. A manual quality slider isn't helpful here.

To automate this, Lytepic features a Target Size input box. The system tests a baseline quality of 50% first. If that fits within the limit, it runs a quick binary search between 50% and 92% to find the absolute highest quality that still respects the file boundary. If even 50% quality exceeds the target, it dynamically shrinks the image dimensions using the size ratio and retries the loop.

Here is a simplified version of the logic:

let lo = await toBlob(canvas, 'image/jpeg', 0.5);

if (lo.size <= target) {
  let a = 0.5, b = 0.92, best = lo;
  for (let i = 0; i < 6; i++) {
    const m = (a + b) / 2;
    const blob = await toBlob(canvas, 'image/jpeg', m);
    if (blob.size <= target) { 
      best = blob; 
      a = m; 
    } else { 
      b = m; 
    }
  }
  return best;
}

// If it doesn't fit at 50% quality, scale down dimensions and retry
const ratio = Math.sqrt(target / lo.size) * 0.95; 
Enter fullscreen mode Exit fullscreen mode

Note: The algorithm targets roughly 97% of the requested size to safely account for systems calculating kilobytes differently (1,000 bytes vs 1,024 bytes).


⚠️ Gotchas I Ran Into

Building a client-side tool comes with unique browser quirks:

  • Transparent PNG to JPG Turns Black: Since JPEG lacks an alpha channel, transparent areas render pitch black. The fix is to programmatically fill the canvas with a solid white background before drawing the source image.
  • Canvas Size Limits: Mobile browsers enforce strict canvas area restrictions. Extremely high-resolution photos will crash the canvas if not scaled down appropriately beforehand.
  • WebP Export Instability: Not every browser version encodes WebP flawlessly. If unsupported, .toBlob() quietly falls back to standard types. You must explicitly inspect blob.type to notify the user.
  • PNG Ignores Quality Arguments: PNG compression is entirely lossless, meaning the quality parameter does nothing. To drastically reduce a PNG's size, you must convert it to WebP/JPG or lower its dimensions.
  • EXIF Metadata Drop: Passing an image through a canvas automatically strips out camera profiles and location coordinates. While a technical limitation, it doubles as an excellent built-in privacy feature.

🚀 What's Next?

Adding batch processing (compressing multiple images at once) is the next logical step on the roadmap.

If you have any architecture ideas or feature requests, let me know in the comments below!

🔗 Try it out here: Lytepic Image Compressor (Previously known as Squeeze)

Top comments (0)