@r01al/use-image-resize
v0.1.0
Published
Resize images in a reusable Web Worker pool with a type-safe React hook.
Maintainers
Readme
useImageResize
useImageResize resizes JPEG, PNG, and WebP files in a reusable Web Worker
pool. Decoding, canvas drawing, and encoding stay outside React and off the
main thread.
Installation
npm install @r01al/use-image-resizeReact 18 or newer is required as a peer dependency.
Quick start
import { useImageResize } from '@r01al/use-image-resize';
function ImageUploader() {
const { process, results, isProcessing, error, reset } = useImageResize({
maxWidth: 1600,
maxHeight: 1600,
quality: 0.8,
format: 'image/webp',
concurrency: 'auto',
});
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files ?? []);
try {
await process(files);
} catch (error) {
console.error('At least one image failed', error);
}
};
const upload = async () => {
const formData = new FormData();
for (const result of results) {
formData.append('images', result.file);
}
await fetch('/api/upload', {
method: 'POST',
body: formData,
});
};
return (
<>
<input
type="file"
accept="image/jpeg,image/png,image/webp"
multiple
onChange={(event) => void handleChange(event)}
/>
{results.map((image) => (
<img key={image.id} src={image.previewUrl} width={200} alt="" />
))}
<button disabled={isProcessing || results.length === 0} onClick={upload}>
Upload
</button>
<button onClick={reset}>Reset</button>
{error && <div>{error.message}</div>}
</>
);
}In a Next.js application, call the hook from a Client Component.
API
const {
process,
results,
isProcessing,
error,
reset,
} = useImageResize(options);Options
interface UseImageResizeOptions {
maxWidth?: number;
maxHeight?: number;
quality?: number;
format?: 'image/jpeg' | 'image/png' | 'image/webp';
concurrency?: 'auto' | number;
allowUpscale?: boolean;
}Defaults:
{
maxWidth: 1920,
maxHeight: 1920,
quality: 0.8,
format: 'image/webp',
concurrency: 'auto',
allowUpscale: false,
}Automatic concurrency uses half of the available logical cores, with a fallback of four logical cores, and clamps the pool size between one and four workers. Explicit positive integers are used as provided.
Images keep their aspect ratio. An image is never enlarged unless
allowUpscale is true.
Processing files
The overloads preserve the input shape:
const result = await process(file); // ImageResizeResult
const results = await process([one, two]); // ImageResizeResult[]An optional AbortSignal cancels the current request:
const controller = new AbortController();
const pending = process(files, { signal: controller.signal });
controller.abort();
await pending;Starting a new batch replaces the hook's previous results and cancels an older batch that is still running. All files enter the pool queue; creating many promises does not create one worker per image.
If one file in a batch fails, the returned promise rejects with an
ImageResizeError, while successfully processed files remain available in
results.
Results
interface ImageResizeResult {
id: string;
originalFile: File;
file: File;
blob: Blob;
previewUrl: string;
originalWidth: number;
originalHeight: number;
width: number;
height: number;
originalSize: number;
outputSize: number;
sizeRatio: number;
status: 'done';
}sizeRatio is outputSize / originalSize. A value below 1 means the output
is smaller; a value above 1 accurately represents an output that grew. This
is less ambiguous than calling the value a compression ratio.
The output File keeps the original base name and uses an extension matching
the selected output format. For example, holiday.jpg becomes holiday.webp.
Preview lifetime and reset
Preview URLs use URL.createObjectURL, not Base64. The hook owns those URLs and
revokes them when a new batch replaces the results, reset() is called, or the
component unmounts.
reset() also cancels running and queued work, clears results, and resets the
public error and processing state.
Errors
Errors are instances of ImageResizeError and include one of these codes:
type ImageResizeErrorCode =
| 'UNSUPPORTED_FILE'
| 'DECODE_FAILED'
| 'CANVAS_UNAVAILABLE'
| 'ENCODE_FAILED'
| 'WORKER_FAILED'
| 'INVALID_OPTIONS'
| 'BROWSER_UNSUPPORTED'
| 'CANCELLED';Architecture
useImageResize
-> processImageBatch
-> ImageProcessor
-> ProcessingBackend
-> WorkerBackend
-> WorkerPool
-> Blob worker (vanilla JavaScript string)
-> createImageBitmap
-> calculateResizeDimensions
-> OffscreenCanvas
-> BlobThe worker is self-contained vanilla JavaScript stored in a Blob and started
through an object URL. There is no separate worker script asset to copy or
configure in the consuming bundler. Its object URL is revoked when the backend
is destroyed.
The worker pool keeps a fixed fleet alive, assigns queued jobs to idle workers,
matches typed responses by job ID, replaces crashed workers, and rejects all
unresolved jobs during destruction. ImageProcessor owns file validation,
output File creation, and preview creation. React only owns state and resource
lifetime.
The ProcessingBackend interface leaves a clean extension point for a future
main-thread fallback without coupling it to React or the worker queue.
Browser compatibility
Version 1 intentionally has no main-thread fallback. Processing requires:
- Web Workers created from Blob object URLs
createImageBitmapinside workersOffscreenCanvaswith a 2D context andconvertToBlobFile,Blob, and object URL APIs
The package is safe to import during SSR because browser APIs are not accessed
at module initialization. Actual processing must run in a supported browser.
Applications with a strict Content Security Policy must allow Blob workers,
typically through an appropriate worker-src blob: directive.
Development
npm install
npm run typecheck
npm test
npm run buildnpm pack --dry-run runs all checks and shows the files that would be
published.
License
MIT
