npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gunny/compress-image

v0.1.0

Published

Compress images in the browser using OffscreenCanvas and Web Workers

Readme

@gunny/compress-image

Compress images in the browser before upload. Uses OffscreenCanvas and can offload the work to a Web Worker so the main thread stays responsive.

  • 🖼️ Decodes, resizes and re-encodes JPEG / WebP / PNG (any browser-decodable input)
  • 🎨 Actually shrinks PNGs via a built-in palette quantization + DEFLATE encoder
  • ⚙️ Configurable quality, format, maxWidth / maxHeight
  • 🧵 Worker-safe: call compress() from your own Web Worker
  • 🪟 Built on OffscreenCanvas + createImageBitmap
  • 🧩 TypeScript-first, ESM only, zero runtime dependencies

Install

npm install @gunny/compress-image

Or with your package manager of choice:

pnpm add @gunny/compress-image
yarn add @gunny/compress-image

Prefer no build step? Use it straight from a CDN — see CDN.

Quick start

import { compress } from '@gunny/compress-image';

const input = document.querySelector<HTMLInputElement>('#file')!;
const file = input.files![0];

const result = await compress(file, {
  quality: 0.8,
  format: 'image/webp',
  maxWidth: 1920,
  maxHeight: 1080,
});

// result.blob is ready to upload
const formData = new FormData();
formData.append('file', result.blob, 'photo.webp');

CDN

Load the library straight from a CDN with an ESM import — no bundler required. Because the package ships ESM only, use a <script type="module"> tag and import the built entry:

<input type="file" id="file" accept="image/*" />

<script type="module">
  import { compress } from 'https://unpkg.com/@gunny/compress-image/dist/index.js';

  const input = document.querySelector('#file');

  input.addEventListener('change', async () => {
    const file = input.files?.[0];
    if (!file) return;

    const result = await compress(file, { quality: 0.8, format: 'image/webp' });
    console.log(result.blob);
  });
</script>

The bare package URL resolves to the same entry through the package's main field, so this also works:

<script type="module">
  import { compress } from 'https://unpkg.com/@gunny/compress-image';
</script>

API

compress(source, options?)

function compress(
  source: Blob | File | ImageBitmap,
  options?: CompressOptions,
): Promise<CompressResult>

The core compression routine. Accepts a Blob/File or ImageBitmap, runs on the current thread (main thread or inside your own worker), and returns a Promise<CompressResult>.

Options

| Option | Type | Default | Description | | -------------- | --------- | -------------- | ------------------------------------------------------------------------ | | quality | number | 0.8 | Output quality 0–1. For PNG it maps to the palette size (lossy quantization). | | format | string | 'image/jpeg' | Output MIME type: image/jpeg, image/webp, image/png, … | | maxWidth | number | 4096 | Maximum output width in px. | | maxHeight | number | 4096 | Maximum output height in px. | | background | string | '#ffffff' | Fill color for formats without alpha (e.g. JPEG). | | allowUpscale | boolean | false | Allow small images to be enlarged to the max dimensions. | | allowLarger | boolean | false | Return the re-encoded blob even when it is not smaller than the original. |

Note about PNG: browsers only re-encode PNG losslessly through a canvas, so convertToBlob cannot shrink a PNG and often makes it larger. For image/png this library therefore reads the raw pixels and re-encodes them with a built-in, dependency-free encoder (UPNG-style): opaque images (and images whose alpha is only fully opaque/transparent) are quantized to an indexed-color palette, images with partially transparent pixels are quantized in RGBA space and written as true-color RGBA, and the result is DEFLATE-compressed. quality maps to the number of colors, except quality: 1 writes the image losslessly (true-color, no quantization). As with every format, the original image is returned when the result is not smaller than the input — this is what result.unchanged reports. Set allowLarger: true to force the re-encoded output.

CompressResult

| Field | Type | Description | | ---------------------------------- | --------- | ---------------------------------------------------------------------- | | blob | Blob | The compressed image. | | format | string | Output MIME type. | | quality | number | Quality that was applied. | | width / height | number | Output dimensions in px. | | originalWidth / originalHeight | number | Original dimensions in px. | | originalSize | number | Original size in bytes. | | size | number | Compressed size in bytes. | | ratio | number | size / originalSize (lower is better). | | unchanged | boolean | true when the original was returned because compression didn't shrink it. |

Running in a Worker

compress() only uses APIs that are available inside Workers (createImageBitmap, OffscreenCanvas, Blob), so you can call it from your own worker file. Write a small worker that imports the library and wires up a message handler:

// worker.ts
import { compress } from '@gunny/compress-image';
import type { CompressOptions } from '@gunny/compress-image';

interface WorkerRequest {
  source: Blob | ImageBitmap;
  options?: CompressOptions;
}

self.addEventListener('message', async (event: MessageEvent<WorkerRequest>) => {
  try {
    const result = await compress(event.data.source, event.data.options);
    self.postMessage(result);
  } catch (error) {
    self.postMessage({ error: error instanceof Error ? error.message : String(error) });
  }
});

Then load that worker from the main thread. The syntax depends on the bundler:

Vite

// vite-env.d.ts — make sure Vite's client types are loaded
/// <reference types="vite/client" />

// main.ts
import CompressWorker from './worker?worker';

const worker = new CompressWorker();
worker.onmessage = (event) => {
  const result = event.data; // CompressResult | { error: string }
  if ('error' in result) throw new Error(result.error);
  console.log(result.blob);
};
worker.postMessage({ source: file, options: { quality: 0.8, format: 'image/webp' } });

For strict CSP environments, inline the worker instead:

import CompressWorker from './worker?worker&inline';
const worker = new CompressWorker();

Webpack 5

const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.onmessage = (event) => {
  const result = event.data; // CompressResult | { error: string }
  if ('error' in result) throw new Error(result.error);
  console.log(result.blob);
};
worker.postMessage({ source: file, options: { quality: 0.8, format: 'image/webp' } });

CDN (no bundler)

No bundler? Use a module worker that imports compress straight from the CDN. Put the worker logic in its own worker.js file — no template-literal strings needed:

// worker.js
import { compress } from 'https://unpkg.com/@gunny/compress-image';

self.addEventListener('message', async (event) => {
  try {
    const result = await compress(event.data.source, event.data.options);
    self.postMessage(result);
  } catch (error) {
    self.postMessage({ error: error instanceof Error ? error.message : String(error) });
  }
});

Then load it from the page:

<input type="file" id="file" accept="image/*" />

<script type="module">
  const worker = new Worker('./worker.js', { type: 'module' });

  worker.onmessage = (event) => {
    const result = event.data;
    if ('error' in result) throw new Error(result.error);
    console.log(result.blob);
  };

  const input = document.querySelector('#file');
  input.addEventListener('change', () => {
    const file = input.files?.[0];
    if (file) worker.postMessage({ source: file, options: { quality: 0.8, format: 'image/webp' } });
  });
</script>

Module workers must be served over HTTP(S) — file:// is not supported.

Browser support

| Feature | Chrome/Edge | Firefox | Safari | | -------------------------- | ----------- | ------- | ------ | | OffscreenCanvas | 69+ | 105+ | 16.4+ | | createImageBitmap | 50+ | 42+ | 15+ | | Worker + OffscreenCanvas | 69+ | 105+ | 16.4+ |

The library encodes with OffscreenCanvas, so it requires a browser that supports OffscreenCanvas and createImageBitmap (see the table above).

Demo

A small browser demo is included in demo/. Build the library first, then serve:

npm install
npm run build
npm run demo          # http://localhost:8080/

Development

npm install           # install dev dependencies
npm run build         # bundle to dist/ (esm + d.ts)
npm test              # run the vitest suite
npm run typecheck     # type-check the project

License

MIT