image-resize-compress
v4.1.0
Published
Resize, compress, and convert images in the browser from a File, Blob, or URL - ~3 kB, zero runtime dependencies, TypeScript-first
Maintainers
Readme
image-resize-compress
Resize, compress, and convert images in the browser - from a File, Blob, or
URL - in ~3 kB with zero runtime dependencies.
✨ Live demo - drag in an image and try resizing, compression, format conversion,
targetSize, and the worker option. Everything runs locally; nothing is uploaded. Source indemo/.
Why image-resize-compress?
| Feature | What you get |
| -------------------- | ----------------------------------------------------------- |
| Size | ~3 kB min+gzip |
| Runtime dependencies | Zero |
| Target file size | targetSize via binary search |
| Flexible input | File, Blob, or URL (fromURL) |
| Web worker | Opt-in, zero-config, with silent fallback |
| Cancellation | Native AbortSignal support |
| Progress | onProgress callback (0–100), works on the worker path too |
| EXIF orientation | Handled natively via createImageBitmap |
| Trustworthy releases | Provenance-signed, published from CI |
| Tested for real | Full real-browser test suite (Playwright/Chromium) |
Small, dependency-free, and browser-native - it does resizing, compression, and format conversion, and nothing you don't need.
Installation
npm install image-resize-compress
# or: pnpm add image-resize-compress / yarn add image-resize-compressimport { fromBlob, fromURL, blobToURL, urlToBlob } from 'image-resize-compress';Quickstart
All processing functions take an options object. Every field is optional.
import { fromBlob } from 'image-resize-compress';
// Convert to WebP at 80% quality, cap the longest edge at 1920px:
const out = await fromBlob(file, {
quality: 80,
maxWidthOrHeight: 1920,
format: 'webp',
});Headline feature: hit a target file size
Pass targetSize (in bytes) and the library binary-searches quality
(jpeg/webp only, ≤ 8 encodes) to produce a blob at or under that size:
// Aim for ≤ 200 kB, keep dimensions:
const compressed = await fromBlob(file, {
format: 'jpeg',
targetSize: 200 * 1024,
});It is best-effort: if the target is unreachable it resolves with the smallest blob it managed to produce. It never loops forever.
Off the main thread
Set worker: true to run decode→resize→encode in a Web Worker (via
OffscreenCanvas), keeping the UI responsive:
const out = await fromBlob(bigFile, {
format: 'webp',
worker: true,
quality: 75,
});- Opt-in and silent-fallback. If the environment lacks
OffscreenCanvas, or a strict CSP blocks blob workers, it transparently runs on the main thread - same result, same errors.worker: truenever rejects whereworker: falsewould succeed. - CSP: a strict Content-Security-Policy needs
worker-src blob:(orchild-src blob:). Without it the library silently uses the main thread. - Worth it for images larger than ~5 MB and multi-file batches; pointless for thumbnails.
- Abort behavior: aborting rejects the caller immediately with
AbortError. An already in-flight worker job (e.g. a longtargetSizesearch) still finishes internally before the next queued worker call starts - its late result is simply discarded.
API
fromBlob(blob, options?) → Promise<Blob>
Resize, compress, and/or convert a Blob or File.
| Option | Type | Default | Notes |
| ------------------ | ---------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| quality | number (0–100] | encoder default | jpeg/webp only. Omit to avoid recompressing harder than needed. |
| width | number \| 'auto' | 'auto' | Derived from height/original when 'auto'. |
| height | number \| 'auto' | 'auto' | Derived from width/original when 'auto'. |
| maxWidthOrHeight | number | - | Caps the longest edge, preserves aspect. Excludes width/height. |
| fit | 'stretch' \| 'cover' | 'stretch' | 'cover' scales to fill then center-crops. Needs explicit width+height. |
| format | 'png' \| 'jpeg' \| 'webp' | input format → png | Output format. |
| backgroundColor | string (CSS color) | transparent | Flattens transparency onto this color. |
| targetSize | number (bytes) | - | jpeg/webp only; binary-searches quality. |
| signal | AbortSignal | - | Rejects with AbortError. |
| onProgress | (progress: number) => void | - | Called with 0–100. One terminal 100 for a plain resize; one call per targetSize step. Works on the worker path (relayed). |
| worker | boolean | false | Off-main-thread, silent fallback (see above). |
Throws: TypeError (not a Blob), InvalidImageError (empty/undecodable),
RangeError (bad quality/dimensions/targetSize, or targetSize with png),
UnsupportedFormatError (bad format), ImageTooLargeError (pixel-count guard),
EnvironmentError (not a browser), or AbortError (aborted).
fromURL(url, options?) → Promise<Blob>
Fetch an image from a URL, then process it. Accepts every ResizeOptions field
plus fetchOptions?: RequestInit (headers, credentials, etc.). The server must
allow CORS.
const blob = await fromURL('https://example.com/photo.jpg', {
format: 'webp',
maxWidthOrHeight: 1024,
fetchOptions: { headers: { Authorization: 'Bearer …' } },
});Throws: everything fromBlob throws, plus FetchError (network/CORS or a
non-2xx response - carries .status for HTTP errors) and InvalidImageError
when the URL returns a non-image response.
blobToURL(blob) → Promise<string>
Read a Blob/File into a data-URL string (handy for <img src> previews).
No size cap.
const dataUrl = await blobToURL(resizedBlob);urlToBlob(url, fetchOptions?) → Promise<Blob>
Fetch a URL and return the raw Blob (no processing).
const blob = await urlToBlob('https://example.com/photo.jpg');Error classes
Typed errors are exported so you can branch with instanceof. All extend
ImageProcessError. Abort rejects with a standard DOMException named
AbortError; argument mistakes throw built-in TypeError/RangeError.
import {
fromURL,
ImageProcessError,
InvalidImageError,
UnsupportedFormatError,
ImageTooLargeError,
FetchError,
EnvironmentError,
} from 'image-resize-compress';
try {
await fromURL(url, { format: 'webp' });
} catch (err) {
if (err instanceof FetchError) {
console.error('fetch failed', err.status); // status set for HTTP errors
} else if (err instanceof InvalidImageError) {
console.error('not a usable image');
} else if (err instanceof ImageProcessError) {
console.error('processing failed', err.name);
} else if (err.name === 'AbortError') {
// cancelled - ignore
}
}Recipes
File input → preview
async function onChange(e) {
const file = e.target.files[0];
const resized = await fromBlob(file, {
maxWidthOrHeight: 512,
format: 'webp',
});
img.src = await blobToURL(resized);
}Enforce a max upload size
const under1MB = await fromBlob(file, {
format: 'jpeg',
targetSize: 1024 * 1024,
});Drive a progress bar
// Most granular during a targetSize search (one tick per binary-search step);
// a plain resize reports a single terminal 100.
const out = await fromBlob(file, {
format: 'jpeg',
targetSize: 200 * 1024,
onProgress: (p) => {
bar.value = p; // 0–100
},
});Square avatar (crop, not stretch)
// Scale to fill a 256x256 square, then center-crop the overflow -
// no distortion, unlike the default 'stretch'.
const avatar = await fromBlob(file, {
width: 256,
height: 256,
fit: 'cover',
format: 'webp',
});Abort on component unmount (React)
useEffect(() => {
const controller = new AbortController();
fromBlob(file, { format: 'webp', signal: controller.signal })
.then(setResult)
.catch((err) => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, [file]);HEIC input?
Not supported. Browsers cannot decode HEIC/HEIF via createImageBitmap or
<img>, so there is nothing to resize. Detect it and tell the user to convert
first (most phones can export JPEG):
const isHeic = /\.(heic|heif)$/i.test(file.name) || /hei[cf]/.test(file.type);
if (isHeic) {
// Ask the user for a JPEG/PNG, or run a dedicated HEIC decoder before this.
}SSR (Next.js, etc.)
This library runs only in the browser. Called during server rendering it
throws EnvironmentError (instead of a cryptic document is not defined). Call
it client-side - inside an effect, an event handler, or a 'use client'
component.
Migrating to v4
Breaking: the deprecated positional signature is removed. fromBlob and
fromURL now accept only (input, options?). Callers still on the v2/v3
positional form must switch to the options object — the mapping is identical to
the v3 table below. Extra positional arguments are now silently ignored rather
than mapped, so migrate any remaining positional calls.
Migrating to v3
v3 replaced the positional arguments with an options object. (In v3 the old positional signature still worked with a deprecation warning; it is gone as of v4 — see above.)
| v2 (positional) | v3+ (options object) |
| --------------------------------------------------- | ------------------------------------------------------------------------- |
| fromBlob(file, 80, 'auto', 'auto', 'webp') | fromBlob(file, { quality: 80, format: 'webp' }) |
| fromBlob(file, 80, 200, 'auto', 'jpeg') | fromBlob(file, { quality: 80, width: 200, format: 'jpeg' }) |
| fromURL(url, 75, 200, 'auto', 'webp') | fromURL(url, { quality: 75, width: 200, format: 'webp' }) |
| fromBlob(file, 90, 'auto', 'auto', 'png', '#fff') | fromBlob(file, { quality: 90, format: 'png', backgroundColor: '#fff' }) |
Breaking: quality is now always 0–100. In v2, values below 1 were
treated as a 0–1 fraction (0.8 meant 80%). In v3's options API there is no
dual scale - quality is passed straight through as quality / 100, so 0.8
now means 0.8%, not 80%. A v2 caller who wrote 0.8 for 80% must write 80.
(Values ≥ 1 are unchanged: 50 = 50%, 1 = 1%.)
Breaking: bmp and gif output removed. No major browser can encode
these via canvas.toBlob; v2 silently produced PNG bytes mislabeled with the
requested mime type. v3 throws UnsupportedFormatError instead of lying. Use
png, jpeg, or webp.
Other changes: blobToURL no longer has a 10 MB cap and always resolves to
a string; decode/encode errors are now typed classes; EXIF orientation is
applied automatically; canvas re-encoding strips EXIF/GPS metadata (a privacy
feature).
CDN (VanillaJS)
The IIFE build exposes a global imageResizeCompress:
<script src="https://cdn.jsdelivr.net/npm/image-resize-compress/dist/index.global.js"></script>
<script>
async function resize() {
const file = document.querySelector('#fileInput').files[0];
const blob = await imageResizeCompress.fromBlob(file, {
quality: 75,
format: 'webp',
});
console.log(blob);
}
</script>
<input type="file" id="fileInput" onchange="resize()" />Works on unpkg too (https://unpkg.com/image-resize-compress/dist/index.global.js).
Compatibility
Browser-only; no IE. Works on all evergreen browsers.
| Capability | Requirement |
| ------------------ | ------------------------------------------------------------------------------------- |
| Core decode/encode | createImageBitmap - or falls back to HTMLImageElement decode |
| worker: true | OffscreenCanvas (Chrome, Firefox, Safari ≥ 16.4) - else silent main-thread fallback |
| Everything | Must run in a browser; server-side use throws EnvironmentError |
License
MIT © Alef Duarte
Contributions welcome - see CONTRIBUTING.md and the Code of Conduct.
