Category: browser Since: 1.0.0 Tags: image, compression, canvas, blob, resize, optimization
shrinkImage
Shrinks an image by limiting its maximum dimensions and optionally reducing output quality.
Usage
import { shrinkImage } from "@petr-ptacek/js-core";
const img = document.querySelector("img")!;
const blob = await shrinkImage(img, {
maxWidth: 1600,
maxHeight: 1600,
});Why This Utility Exists
Image upload forms often require client-side optimization to reduce server load and improve user experience. shrinkImage provides a front-end pre-upload optimization utility that reduces image dimensions while preserving aspect ratio. As a side effect, this also reduces file size through dimension limiting and optional quality reduction. The utility handles canvas rendering, blob creation, and error handling in a single promise-based API.
Signature
function shrinkImage(image: HTMLImageElement, options?: ShrinkImageOptions): Promise<Blob>;Parameters
image(HTMLImageElement): Source image element. Must have validnaturalWidthandnaturalHeightproperties.options(ShrinkImageOptions, optional): Shrinking and compression options.maxWidth(number, optional): Maximum allowed image width. Image is scaled down proportionally if wider.maxHeight(number, optional): Maximum allowed image height. Image is scaled down proportionally if taller.quality(number, optional, default0.8): Output image quality (0–1). Applies only to lossy formats like JPEG and WebP.mimeType(string, optional, default"image/jpeg"): Output MIME type (e.g.,"image/jpeg","image/webp","image/png").
Return Type
Returns a promise that resolves with a Blob containing the resized and optionally compressed image, or rejects if the operation fails.
Type Declarations
type ShrinkImageOptions = {
maxWidth?: number;
maxHeight?: number;
quality?: number;
mimeType?: string;
};Throws / Errors
- Throws
Errorwhenimage.naturalWidthorimage.naturalHeightis not a positive finite number - Throws
ErrorwhenmaxWidthormaxHeightis provided but not a positive finite number - Throws
Errorif the canvas 2D context is unavailable - Rejects if the image blob cannot be created
Design Notes
The implementation:
- Dimension limiting is primary - File size reduction is achieved mainly through dimension constraints
- Quality is secondary - Used to further optimize for lossy formats
- Preserves aspect ratio - The image is never distorted
- Never upscales - If the image is already smaller than constraints, dimensions remain unchanged
- Uses canvas rendering - Delegates to
HTMLCanvasElement.toBlobfor encoding - Non-mutating - The original image element is never modified
- Fit-inside behavior - When both
maxWidthandmaxHeightare provided, the image is scaled so neither dimension exceeds its limit
The utility uses scaleByAspectRatio to calculate final dimensions and delegates encoding to canvas toBlob for maximum compatibility.
When To Use
Use shrinkImage when you need to:
- optimize images before upload
- reduce image dimensions client-side
- compress images with quality control
- fit images inside bounding boxes
- generate optimized thumbnails
- reduce bandwidth for mobile users
When Not To Use
Avoid when:
- you need to store images (use a backend service or cloud storage)
- you need advanced compression features (use specialized image libraries)
- you need to process multiple images concurrently at scale (consider workers)
- you require exact file size guarantees
- the canvas API is not available (check browser support)
Summary
shrinkImage provides a simple, promise-based API for front-end image optimization. It reduces dimensions while preserving aspect ratio and allows optional quality control. The result is a Blob ready for upload or further processing.
Snippets
basic.ts
import { shrinkImage } from "@petr-ptacek/js-core";
// Basic usage: Shrink image before upload
const imageInput = document.querySelector<HTMLInputElement>("input[type='file']")!;
imageInput.addEventListener("change", async (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
// Create image element
const img = new Image();
img.onload = async () => {
try {
// Shrink image to max 1600x1600
const blob = await shrinkImage(img, {
maxWidth: 1600,
maxHeight: 1600,
});
console.log(`Original: ${file.size} bytes`);
console.log(`Optimized: ${blob.size} bytes`);
console.log(`Saved: ${((1 - blob.size / file.size) * 100).toFixed(1)}%`);
} catch (error) {
console.error("Failed to shrink image:", error);
}
};
// Load image from file
img.src = URL.createObjectURL(file);
});form-upload.ts
import { shrinkImage } from "@petr-ptacek/js-core";
// Helper function for form submission with image optimization
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
async function submitFormWithOptimizedImage(
formElement: HTMLFormElement,
imageElement: HTMLImageElement,
maxWidth: number,
maxHeight: number
): Promise<void> {
try {
// Optimize the image
const optimizedBlob = await shrinkImage(imageElement, {
maxWidth,
maxHeight,
quality: 0.8,
});
// Create FormData
const formData = new FormData(formElement);
// Replace or add the image file
formData.set("image", optimizedBlob, "optimized-image.jpg");
// Submit the form
const response = await fetch(formElement.action, {
method: formElement.method || "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Server error: ${response.statusText}`);
}
console.log("Image uploaded successfully");
} catch (error) {
console.error("Failed to upload image:", error);
}
}
// Example usage:
// const form = document.querySelector("form")!;
// const img = document.querySelector("img")!;
// await submitFormWithOptimizedImage(form, img, 1600, 1600);multiple-formats.ts
import { shrinkImage } from "@petr-ptacek/js-core";
// Fit image inside a bounding box (e.g., 1600x1200)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function optimizeImageForGallery(imageElement: HTMLImageElement) {
try {
const blob = await shrinkImage(imageElement, {
maxWidth: 1600,
maxHeight: 1200,
});
console.log("Image optimized for gallery display");
console.log(`Size: ${blob.size} bytes`);
return blob;
} catch (error) {
console.error("Failed to optimize image:", error);
}
}
// Different quality levels for different formats
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function optimizeWithFormats(imageElement: HTMLImageElement) {
// High quality JPEG for thumbnails
const thumbnail = await shrinkImage(imageElement, {
maxWidth: 200,
maxHeight: 200,
quality: 0.85,
mimeType: "image/jpeg",
});
// Medium quality WebP for display
const display = await shrinkImage(imageElement, {
maxWidth: 800,
maxHeight: 800,
quality: 0.75,
mimeType: "image/webp",
});
// High quality PNG for archive (lossless)
const archive = await shrinkImage(imageElement, {
maxWidth: 2000,
maxHeight: 2000,
mimeType: "image/png",
});
return { thumbnail, display, archive };
}