@simon_he/browser-compress-image
Version:
🚀 A powerful, lightweight browser image compression library with TypeScript support. Compress JPEG, PNG, GIF images with multiple output formats (Blob, File, Base64, ArrayBuffer) and zero dependencies.
517 lines (510 loc) • 18.9 kB
JavaScript
import imageCompression from "browser-image-compression";
import Compressor from "compressorjs";
import gifsicle from "gifsicle-wasm-browser";
//#region src/compressWithBrowserImageCompression.ts
async function compressWithBrowserImageCompression(file, options) {
const { quality, mode, targetWidth, targetHeight, maxWidth, maxHeight, preserveExif = false } = options;
const compressionOptions = {
useWebWorker: true,
initialQuality: quality,
alwaysKeepResolution: mode === "keepSize",
exifOrientation: 1,
fileType: file.type,
preserveExif,
maxSizeMB: file.size * .8 / (1024 * 1024),
maxWidthOrHeight: Math.min(maxWidth || targetWidth, maxHeight || targetHeight) || void 0
};
return await imageCompression(file, compressionOptions);
}
//#endregion
//#region src/compressWithCompressorJS.ts
async function compressWithCompressorJS(file, options) {
const { quality, mode, targetWidth, targetHeight, maxWidth, maxHeight, preserveExif = false } = options;
if (!file.type.includes("jpeg") && !file.type.includes("jpg")) throw new Error("CompressorJS is optimized for JPEG files");
return new Promise((resolve, reject) => {
const compressorOptions = {
quality,
retainExif: preserveExif,
mimeType: file.type,
success: (compressedBlob) => resolve(compressedBlob),
error: reject
};
if (mode === "keepQuality") {
if (targetWidth) compressorOptions.width = targetWidth;
if (targetHeight) compressorOptions.height = targetHeight;
if (maxWidth) compressorOptions.maxWidth = maxWidth;
if (maxHeight) compressorOptions.maxHeight = maxHeight;
}
new Compressor(file, compressorOptions);
});
}
//#endregion
//#region src/compressWithCanvas.ts
async function compressWithCanvas(file, options) {
const { quality, targetWidth, targetHeight, maxWidth, maxHeight } = options;
let finalWidth = targetWidth || maxWidth, finalHeight = targetHeight || maxHeight;
if (!finalWidth && !finalHeight) {
const { width, height } = await getImageDimensions(file);
finalWidth = width;
finalHeight = height;
}
return await smartCanvasCompress(file, finalWidth, finalHeight, quality);
}
async function smartCanvasCompress(file, targetWidth, targetHeight, quality) {
const originalSize = file.size;
const { width: originalWidth, height: originalHeight } = await getImageDimensions(file);
if (targetWidth === originalWidth && targetHeight === originalHeight && originalSize < 100 * 1024) return file;
if (file.type.includes("png")) {
const pngResult = await canvasCompressWithFormat(file, targetWidth, targetHeight, "image/png", void 0);
if (pngResult.size < originalSize * .8) return pngResult;
if (quality < .8) {
const jpegResult = await canvasCompressWithFormat(file, targetWidth, targetHeight, "image/jpeg", Math.max(.7, quality));
if (jpegResult.size < Math.min(pngResult.size, originalSize * .9)) return jpegResult;
}
if (pngResult.size >= originalSize * .95) return file;
return pngResult;
}
if (file.type.includes("jpeg") || file.type.includes("jpg")) {
const qualities = [
quality,
Math.max(.5, quality - .2),
Math.max(.3, quality - .4)
];
for (const q of qualities) {
const result$1 = await canvasCompressWithFormat(file, targetWidth, targetHeight, file.type, q);
if (result$1.size < originalSize * .8) return result$1;
}
return file;
}
const result = await canvasCompressWithFormat(file, targetWidth, targetHeight, file.type, quality);
if (result.size >= originalSize * .95) return file;
return result;
}
async function canvasCompressWithFormat(file, targetWidth, targetHeight, outputType, quality) {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d", {
alpha: outputType.includes("png"),
willReadFrequently: false
});
if (!ctx) {
reject(/* @__PURE__ */ new Error("Failed to get canvas context"));
return;
}
canvas.width = targetWidth;
canvas.height = targetHeight;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
if (outputType.includes("jpeg") && !file.type.includes("jpeg")) {
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, targetWidth, targetHeight);
}
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(/* @__PURE__ */ new Error("Failed to create blob"));
}, outputType, quality);
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(/* @__PURE__ */ new Error("Failed to load image"));
};
img.crossOrigin = "anonymous";
img.src = url;
});
}
function getImageDimensions(file) {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
resolve({
width: img.width,
height: img.height
});
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(/* @__PURE__ */ new Error("Failed to load image"));
};
img.src = url;
});
}
//#endregion
//#region src/compressWithGifsicle.ts
async function compressWithGifsicle(file, options) {
const { quality, mode, targetWidth, targetHeight, maxWidth, maxHeight, preserveExif = false } = options;
if (!file.type.includes("gif")) throw new Error("Gifsicle is only for GIF files");
let command;
if (mode === "keepSize") command = `
-O1
--lossy=${Math.round((1 - quality) * 100)}
${file.name}
-o /out/${file.name}
`;
else {
let resizeOption = "";
if (targetWidth && targetHeight) resizeOption = `--resize ${targetWidth}x${targetHeight}`;
else if (maxWidth || maxHeight) {
const maxSize = Math.min(maxWidth || 9999, maxHeight || 9999);
resizeOption = `--resize-fit ${maxSize}x${maxSize}`;
}
command = `
-O1
${resizeOption}
${file.name}
-o /out/${file.name}
`;
}
const result = await gifsicle.run({
input: [{
file,
name: file.name
}],
command: [command]
});
return result[0];
}
//#endregion
//#region src/convertBlobToType.ts
async function convertBlobToType(blob, type, originalFileName) {
switch (type) {
case "blob": return blob;
case "file": return new File([blob], originalFileName || "compressed", { type: blob.type });
case "base64": return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
case "arrayBuffer": return blob.arrayBuffer();
default: throw new Error(`Unsupported type: ${type}`);
}
}
//#endregion
//#region src/compress.ts
const devLog = {
log: (...args) => {
if (process.env.NODE_ENV === "development") console.log(...args);
},
warn: (...args) => {
if (process.env.NODE_ENV === "development") console.warn(...args);
},
table: (data) => {
if (process.env.NODE_ENV === "development") console.table(data);
}
};
const EXIF_SUPPORTED_TOOLS = ["browser-image-compression", "compressorjs"];
const toolsCollections = {
png: ["browser-image-compression", "canvas"],
gif: ["gifsicle"],
webp: ["canvas", "browser-image-compression"],
others: [
"browser-image-compression",
"compressorjs",
"canvas"
]
};
async function compress(file, qualityOrOptions, type) {
let options;
if (typeof qualityOrOptions === "object") options = qualityOrOptions;
else options = {
quality: qualityOrOptions || .6,
mode: "keepSize",
type: type || "blob"
};
const { quality = .6, mode = "keepSize", targetWidth, targetHeight, maxWidth, maxHeight, preserveExif = false, returnAllResults = false, type: resultType = "blob" } = options;
const compressionOptions = {
quality,
mode,
targetWidth,
targetHeight,
maxWidth,
maxHeight,
preserveExif
};
const tools = file.type.includes("png") ? toolsCollections["png"] : file.type.includes("gif") ? toolsCollections["gif"] : file.type.includes("webp") ? toolsCollections["webp"] : toolsCollections["others"];
if (returnAllResults) return await compressWithMultipleToolsAndReturnAll(file, compressionOptions, tools, resultType);
const bestResult = await compressWithMultipleTools(file, compressionOptions, tools);
return convertBlobToType(bestResult, resultType, file.name);
}
async function compressWithMultipleTools(file, options, tools) {
const totalStartTime = performance.now();
if (options.preserveExif) {
tools = tools.filter((tool) => EXIF_SUPPORTED_TOOLS.includes(tool));
if (tools.length === 0) throw new Error("No EXIF-supporting tools available for this file type. Please disable preserveExif or use a different file format.");
devLog.log("preserveExif=true, filtered tools:", tools);
}
const attempts = [];
const promises = tools.map(async (tool) => {
const startTime = performance.now();
try {
let compressedBlob;
switch (tool) {
case "browser-image-compression":
compressedBlob = await compressWithBrowserImageCompression(file, options);
break;
case "compressorjs":
compressedBlob = await compressWithCompressorJS(file, options);
break;
case "gifsicle":
compressedBlob = await compressWithGifsicle(file, options);
break;
case "canvas":
compressedBlob = await compressWithCanvas(file, options);
break;
default: throw new Error(`Unknown compression tool: ${tool}`);
}
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
return {
tool,
blob: compressedBlob,
size: compressedBlob.size,
success: true,
duration
};
} catch (error) {
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
return {
tool,
blob: file,
size: file.size,
success: false,
error: error instanceof Error ? error.message : String(error),
duration
};
}
});
const results = await Promise.allSettled(promises);
results.forEach((result) => {
if (result.status === "fulfilled") attempts.push(result.value);
else devLog.warn("Compression tool failed:", result.reason);
});
const successfulAttempts = attempts.filter((attempt) => attempt.success);
if (successfulAttempts.length === 0) {
devLog.warn("All compression attempts failed, returning original file");
return file;
}
const bestAttempt = successfulAttempts.reduce((best, current) => current.size < best.size ? current : best);
if (bestAttempt.size >= file.size * .98 && options.quality > .85) {
const totalEndTime$1 = performance.now();
const totalDuration$1 = Math.round(totalEndTime$1 - totalStartTime);
devLog.log(`Best compression (${bestAttempt.tool}) size: ${bestAttempt.size}, original: ${file.size}, using original (total: ${totalDuration$1}ms)`);
return file;
}
const totalEndTime = performance.now();
const totalDuration = Math.round(totalEndTime - totalStartTime);
devLog.log(`Best compression result: ${bestAttempt.tool} (${bestAttempt.size} bytes, ${((file.size - bestAttempt.size) / file.size * 100).toFixed(1)}% reduction, ${bestAttempt.duration}ms) - Total time: ${totalDuration}ms`);
if (successfulAttempts.length > 1) devLog.table(successfulAttempts.map((attempt) => ({
Tool: attempt.tool,
"Size (bytes)": attempt.size,
"Reduction (%)": `${((file.size - attempt.size) / file.size * 100).toFixed(1)}%`,
"Duration (ms)": attempt.duration,
"Speed (MB/s)": `${(file.size / 1024 / 1024 / (attempt.duration / 1e3)).toFixed(2)}`
})));
return bestAttempt.blob;
}
async function compressWithMultipleToolsAndReturnAll(file, options, tools, resultType) {
const totalStartTime = performance.now();
if (options.preserveExif) {
tools = tools.filter((tool) => EXIF_SUPPORTED_TOOLS.includes(tool));
if (tools.length === 0) throw new Error("No EXIF-supporting tools available for this file type. Please disable preserveExif or use a different file format.");
devLog.log("preserveExif=true, filtered tools:", tools);
}
const attempts = [];
const promises = tools.map(async (tool) => {
const startTime = performance.now();
try {
let compressedBlob;
switch (tool) {
case "browser-image-compression":
compressedBlob = await compressWithBrowserImageCompression(file, options);
break;
case "compressorjs":
compressedBlob = await compressWithCompressorJS(file, options);
break;
case "gifsicle":
compressedBlob = await compressWithGifsicle(file, options);
break;
case "canvas":
compressedBlob = await compressWithCanvas(file, options);
break;
default: throw new Error(`Unknown compression tool: ${tool}`);
}
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
return {
tool,
blob: compressedBlob,
size: compressedBlob.size,
success: true,
duration
};
} catch (error) {
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
return {
tool,
blob: file,
size: file.size,
success: false,
error: error instanceof Error ? error.message : String(error),
duration
};
}
});
const results = await Promise.allSettled(promises);
results.forEach((result) => {
if (result.status === "fulfilled") attempts.push(result.value);
else devLog.warn("Compression tool failed:", result.reason);
});
if (attempts.length === 0) throw new Error("All compression attempts failed");
const totalEndTime = performance.now();
const totalDuration = Math.round(totalEndTime - totalStartTime);
const allResults = await Promise.all(attempts.map(async (attempt) => {
const convertedResult = await convertBlobToType(attempt.blob, resultType, file.name);
return {
tool: attempt.tool,
result: convertedResult,
originalSize: file.size,
compressedSize: attempt.size,
compressionRatio: (file.size - attempt.size) / file.size * 100,
duration: attempt.duration,
success: attempt.success,
error: attempt.error
};
}));
const successfulAttempts = attempts.filter((attempt) => attempt.success);
let bestAttempt;
if (successfulAttempts.length > 0) {
bestAttempt = successfulAttempts.reduce((best, current) => current.size < best.size ? current : best);
if (bestAttempt.size >= file.size * .98 && options.quality > .85) bestAttempt = {
tool: "original",
blob: file,
size: file.size,
success: true,
duration: 0
};
} else bestAttempt = {
tool: "original",
blob: file,
size: file.size,
success: true,
duration: 0
};
const bestResult = await convertBlobToType(bestAttempt.blob, resultType, file.name);
devLog.log(`Best compression result: ${bestAttempt.tool} (${bestAttempt.size} bytes, ${((file.size - bestAttempt.size) / file.size * 100).toFixed(1)}% reduction) - Total time: ${totalDuration}ms`);
if (successfulAttempts.length > 1) devLog.table(successfulAttempts.map((attempt) => ({
Tool: attempt.tool,
"Size (bytes)": attempt.size,
"Reduction (%)": `${((file.size - attempt.size) / file.size * 100).toFixed(1)}%`,
"Duration (ms)": attempt.duration,
"Speed (MB/s)": `${(file.size / 1024 / 1024 / (attempt.duration / 1e3)).toFixed(2)}`
})));
return {
bestResult,
bestTool: bestAttempt.tool,
allResults,
totalDuration
};
}
async function compressWithStats(file, qualityOrOptions) {
return await compressWithMultipleToolsWithStats(file, {
quality: typeof qualityOrOptions === "object" ? qualityOrOptions.quality || .6 : qualityOrOptions || .6,
mode: typeof qualityOrOptions === "object" ? qualityOrOptions.mode || "keepSize" : "keepSize",
targetWidth: typeof qualityOrOptions === "object" ? qualityOrOptions.targetWidth : void 0,
targetHeight: typeof qualityOrOptions === "object" ? qualityOrOptions.targetHeight : void 0,
maxWidth: typeof qualityOrOptions === "object" ? qualityOrOptions.maxWidth : void 0,
maxHeight: typeof qualityOrOptions === "object" ? qualityOrOptions.maxHeight : void 0,
preserveExif: typeof qualityOrOptions === "object" ? qualityOrOptions.preserveExif || false : false
});
}
async function compressWithMultipleToolsWithStats(file, options) {
const totalStartTime = performance.now();
let tools = file.type.includes("png") ? toolsCollections["png"] : file.type.includes("gif") ? toolsCollections["gif"] : file.type.includes("webp") ? toolsCollections["webp"] : toolsCollections["others"];
if (options.preserveExif) {
tools = tools.filter((tool) => EXIF_SUPPORTED_TOOLS.includes(tool));
if (tools.length === 0) throw new Error("No EXIF-supporting tools available for this file type. Please disable preserveExif or use a different file format.");
devLog.log("preserveExif=true, filtered tools:", tools);
}
const attempts = [];
const promises = tools.map(async (tool) => {
const startTime = performance.now();
try {
let compressedBlob;
switch (tool) {
case "browser-image-compression":
compressedBlob = await compressWithBrowserImageCompression(file, options);
break;
case "compressorjs":
compressedBlob = await compressWithCompressorJS(file, options);
break;
case "gifsicle":
compressedBlob = await compressWithGifsicle(file, options);
break;
case "canvas":
compressedBlob = await compressWithCanvas(file, options);
break;
default: throw new Error(`Unknown compression tool: ${tool}`);
}
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
return {
tool,
blob: compressedBlob,
size: compressedBlob.size,
success: true,
duration
};
} catch (error) {
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
return {
tool,
blob: file,
size: file.size,
success: false,
error: error instanceof Error ? error.message : String(error),
duration
};
}
});
const results = await Promise.allSettled(promises);
results.forEach((result) => {
if (result.status === "fulfilled") attempts.push(result.value);
});
const successfulAttempts = attempts.filter((attempt) => attempt.success);
const bestAttempt = successfulAttempts.length > 0 ? successfulAttempts.reduce((best, current) => current.size < best.size ? current : best) : {
tool: "none",
blob: file,
size: file.size,
success: false,
duration: 0
};
const totalEndTime = performance.now();
const totalDuration = Math.round(totalEndTime - totalStartTime);
return {
bestTool: bestAttempt.tool,
compressedFile: bestAttempt.blob,
originalSize: file.size,
compressedSize: bestAttempt.size,
compressionRatio: (file.size - bestAttempt.size) / file.size * 100,
totalDuration,
toolsUsed: attempts.map((attempt) => ({
tool: attempt.tool,
size: attempt.size,
duration: attempt.duration,
compressionRatio: (file.size - attempt.size) / file.size * 100,
success: attempt.success,
error: attempt.error
}))
};
}
//#endregion
export { compress, compressWithStats };