bulk image compress - nodejs
const fs = require("fs");
const path = require("path");
const sharp = require("sharp");
// ============================================================
// BULK IMAGE COMPRESSOR
// Node.js + Sharp + libvips
// ============================================================
const SOURCE_DIR = path.join(__dirname, "creek-bay");
const TARGET_DIR = path.join(__dirname, "creek-bay-compress");
// ------------------------------------------------------------
// COMPRESSION QUALITY
//
// 1 = maximum compression / lowest quality
// 100 = highest quality / lowest compression
//
// Recommended starting values:
//
// JPEG : 60 - 80
// WebP : 60 - 80
// AVIF : 40 - 70
//
// ------------------------------------------------------------
const COMPRESSION_QUALITY = 60;
// ------------------------------------------------------------
// OUTPUT FORMAT
//
// null = KEEP ORIGINAL FORMAT
//
// "webp" = Convert all compatible images to WebP
// "avif" = Convert all compatible images to AVIF
// "jpeg" = Convert all compatible images to JPEG
// "png" = Convert all compatible images to PNG
//
// Examples:
//
// const OUTPUT_FORMAT = null;
// const OUTPUT_FORMAT = "webp";
// const OUTPUT_FORMAT = "avif";
//
// ------------------------------------------------------------
const OUTPUT_FORMAT = "webp";
// ------------------------------------------------------------
// MAX WIDTH
//
// null = don't resize
//
// Example:
//
// const MAX_WIDTH = 1920;
//
// ------------------------------------------------------------
const MAX_WIDTH = null;
// ------------------------------------------------------------
// MAX HEIGHT
//
// null = don't resize
//
// Example:
//
// const MAX_HEIGHT = 1080;
//
// ------------------------------------------------------------
const MAX_HEIGHT = null;
// ------------------------------------------------------------
// PRESERVE METADATA
//
// false = remove metadata
// true = preserve metadata
//
// Removing metadata can reduce file size significantly.
//
// ------------------------------------------------------------
const PRESERVE_METADATA = false;
// ------------------------------------------------------------
// SKIP SMALL FILES
//
// null = process everything
//
// Example:
//
// const SKIP_IF_SMALLER_THAN = 100 * 1024;
//
// means skip images smaller than 100 KB.
//
// ------------------------------------------------------------
const SKIP_IF_SMALLER_THAN = null;
// ============================================================
// SUPPORTED INPUT EXTENSIONS
// ============================================================
const SUPPORTED_EXTENSIONS = [
".jpg",
".jpeg",
".png",
".webp",
".avif",
".tif",
".tiff",
".gif",
".svg"
];
// ============================================================
// CREATE TARGET DIRECTORY
// ============================================================
if (!fs.existsSync(TARGET_DIR)) {
fs.mkdirSync(TARGET_DIR, {
recursive: true
});
}
// ============================================================
// FORMAT FILE SIZE
// ============================================================
function formatSize(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(2)} KB`;
}
if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}
return `${(
bytes /
1024 /
1024 /
1024
).toFixed(2)} GB`;
}
// ============================================================
// CALCULATE REDUCTION
// ============================================================
function getReduction(originalSize, compressedSize) {
if (
!originalSize ||
originalSize <= 0
) {
return 0;
}
return (
(originalSize - compressedSize) /
originalSize
) * 100;
}
// ============================================================
// GET OUTPUT EXTENSION
// ============================================================
function getOutputExtension(
inputExtension,
outputFormat
) {
// --------------------------------------------------------
// KEEP ORIGINAL
// --------------------------------------------------------
if (outputFormat === null) {
return inputExtension;
}
// --------------------------------------------------------
// USER SELECTED FORMAT
// --------------------------------------------------------
switch (
outputFormat.toLowerCase()
) {
case "jpg":
case "jpeg":
return ".jpg";
case "webp":
return ".webp";
case "avif":
return ".avif";
case "png":
return ".png";
case "tif":
case "tiff":
return ".tiff";
default:
throw new Error(
`Unsupported output format: ${outputFormat}`
);
}
}
// ============================================================
// COMPRESS ONE IMAGE
// ============================================================
async function compressImage(filename) {
const inputPath =
path.join(
SOURCE_DIR,
filename
);
const inputExtension =
path.extname(filename)
.toLowerCase();
const basename =
path.basename(
filename,
inputExtension
);
try {
// ----------------------------------------------------
// ORIGINAL FILE SIZE
// ----------------------------------------------------
const originalStats =
fs.statSync(inputPath);
const originalSize =
originalStats.size;
// ----------------------------------------------------
// SKIP SMALL FILES
// ----------------------------------------------------
if (
SKIP_IF_SMALLER_THAN !== null &&
originalSize <
SKIP_IF_SMALLER_THAN
) {
console.log(
`SKIPPED: ${filename}`
);
console.log(
`Reason: smaller than configured limit`
);
// Still copy the file to output
const outputPath =
path.join(
TARGET_DIR,
filename
);
fs.copyFileSync(
inputPath,
outputPath
);
return {
filename,
outputFilename: filename,
originalSize,
compressedSize: originalSize,
saved: 0,
reduction: 0,
status: "skipped"
};
}
// ----------------------------------------------------
// READ IMAGE METADATA
// ----------------------------------------------------
let metadata;
try {
metadata =
await sharp(inputPath)
.metadata();
} catch (metadataError) {
console.log(
`Could not read image: ${filename}`
);
console.log(
metadataError.message
);
return {
filename,
originalSize,
compressedSize: 0,
saved: 0,
reduction: 0,
status: "error",
error: metadataError.message
};
}
const inputFormat =
metadata.format;
// ----------------------------------------------------
// DETERMINE OUTPUT FORMAT
// ----------------------------------------------------
let outputFormat;
if (
OUTPUT_FORMAT === null
) {
// Use actual image format
outputFormat =
inputFormat;
} else {
outputFormat =
OUTPUT_FORMAT
.toLowerCase();
}
// ----------------------------------------------------
// SVG SPECIAL HANDLING
// ----------------------------------------------------
if (
inputExtension === ".svg"
) {
// -----------------------------------------------
// SVG + KEEP ORIGINAL
// -----------------------------------------------
if (
OUTPUT_FORMAT === null
) {
const outputPath =
path.join(
TARGET_DIR,
filename
);
fs.copyFileSync(
inputPath,
outputPath
);
console.log("");
console.log(
`SVG: ${filename}`
);
console.log(
"SVG kept in original format."
);
return {
filename,
outputFilename: filename,
originalSize,
compressedSize: originalSize,
saved: 0,
reduction: 0,
status: "copied"
};
}
// -----------------------------------------------
// SVG + USER SELECTED FORMAT
//
// Sharp can rasterize SVG into WebP/AVIF/JPEG/PNG.
// -----------------------------------------------
console.log("");
console.log(
`SVG conversion: ${filename}`
);
}
// ----------------------------------------------------
// OUTPUT EXTENSION
// ----------------------------------------------------
const outputExtension =
getOutputExtension(
inputExtension,
OUTPUT_FORMAT
);
// ----------------------------------------------------
// OUTPUT PATH
// ----------------------------------------------------
const outputFilename =
basename +
outputExtension;
const outputPath =
path.join(
TARGET_DIR,
outputFilename
);
// ----------------------------------------------------
// START SHARP PIPELINE
// ----------------------------------------------------
let image =
sharp(inputPath);
// ----------------------------------------------------
// AUTO ORIENTATION
//
// Fix camera EXIF orientation.
// ----------------------------------------------------
image =
image.rotate();
// ----------------------------------------------------
// RESIZE
//
// If both are null:
//
// NO RESIZE
//
// If width only:
//
// Width limited, height automatic
//
// If height only:
//
// Height limited, width automatic
//
// If both:
//
// Fit inside both dimensions
// Aspect ratio preserved
//
// ----------------------------------------------------
if (
MAX_WIDTH !== null ||
MAX_HEIGHT !== null
) {
image =
image.resize({
width: MAX_WIDTH,
height: MAX_HEIGHT,
fit: "inside",
withoutEnlargement: true
});
}
// ----------------------------------------------------
// METADATA
// ----------------------------------------------------
if (
PRESERVE_METADATA
) {
image =
image.withMetadata();
}
// ====================================================
// OUTPUT FORMAT
// ====================================================
switch (outputFormat) {
// =================================================
// JPEG
// =================================================
case "jpeg":
case "jpg":
image =
image.jpeg({
quality: COMPRESSION_QUALITY,
mozjpeg: true,
progressive: true
});
break;
// =================================================
// PNG
// =================================================
case "png":
image =
image.png({
compressionLevel: 9,
adaptiveFiltering: true
});
break;
// =================================================
// WEBP
// =================================================
case "webp":
image =
image.webp({
quality: COMPRESSION_QUALITY
});
break;
// =================================================
// AVIF
// =================================================
case "avif":
image =
image.avif({
quality: COMPRESSION_QUALITY
});
break;
// =================================================
// TIFF
// =================================================
case "tiff":
case "tif":
image =
image.tiff({
quality: COMPRESSION_QUALITY,
compression: "jpeg"
});
break;
// =================================================
// GIF
// =================================================
case "gif":
// Sharp is not being used here for GIF output.
// Keep the original GIF.
//
// If you want proper GIF optimization later,
// we can add gifsicle separately.
console.log(
`GIF kept unchanged: ${filename}`
);
fs.copyFileSync(
inputPath,
outputPath
);
return {
filename,
outputFilename,
originalSize,
compressedSize:
originalSize,
saved: 0,
reduction: 0,
status: "copied"
};
// =================================================
// SVG
// =================================================
case "svg":
// SVG is vector.
// Don't process it as a raster image.
fs.copyFileSync(
inputPath,
outputPath
);
return {
filename,
outputFilename,
originalSize,
compressedSize:
originalSize,
saved: 0,
reduction: 0,
status: "copied"
};
// =================================================
// UNKNOWN
// =================================================
default:
throw new Error(
`Unsupported output format: ${outputFormat}`
);
}
// ====================================================
// WRITE OUTPUT
// ====================================================
await image.toFile(
outputPath
);
// ====================================================
// GET COMPRESSED FILE SIZE
// ====================================================
const compressedStats =
fs.statSync(
outputPath
);
const compressedSize =
compressedStats.size;
// ====================================================
// SAVINGS
// ====================================================
const saved =
originalSize -
compressedSize;
const reduction =
getReduction(
originalSize,
compressedSize
);
// ====================================================
// DISPLAY RESULT
// ====================================================
console.log("");
console.log(
`✓ ${filename}`
);
console.log(
` Input: ${inputFormat}`
);
console.log(
` Output: ${outputFormat}`
);
console.log(
` Original: ${formatSize(originalSize)}`
);
console.log(
` Compressed: ${formatSize(compressedSize)}`
);
console.log(
` Saved: ${formatSize(saved)}`
);
console.log(
` Reduction: ${reduction.toFixed(2)}%`
);
return {
filename,
outputFilename,
originalSize,
compressedSize,
saved,
reduction,
status: "compressed"
};
} catch (error) {
console.log("");
console.error(
`✗ ERROR: ${filename}`
);
console.error(
error.message
);
return {
filename,
outputFilename: filename,
originalSize: 0,
compressedSize: 0,
saved: 0,
reduction: 0,
status: "error",
error: error.message
};
}
}
// ============================================================
// MAIN FUNCTION
// ============================================================
async function main() {
console.log("");
console.log(
"================================================"
);
console.log(
" BULK IMAGE COMPRESSOR"
);
console.log(
"================================================"
);
console.log("");
// --------------------------------------------------------
// DISPLAY SETTINGS
// --------------------------------------------------------
console.log(
`Source: ${SOURCE_DIR}`
);
console.log(
`Target: ${TARGET_DIR}`
);
console.log(
`Quality: ${COMPRESSION_QUALITY}`
);
console.log(
`Output: ${
OUTPUT_FORMAT === null
? "Original format"
: OUTPUT_FORMAT
}`
);
console.log(
`Max Width: ${
MAX_WIDTH === null
? "No limit"
: MAX_WIDTH + " px"
}`
);
console.log(
`Max Height: ${
MAX_HEIGHT === null
? "No limit"
: MAX_HEIGHT + " px"
}`
);
console.log(
`Metadata: ${
PRESERVE_METADATA
? "Preserved"
: "Removed"
}`
);
console.log("");
// --------------------------------------------------------
// CHECK SOURCE
// --------------------------------------------------------
if (
!fs.existsSync(SOURCE_DIR)
) {
console.error(
"Source directory does not exist:"
);
console.error(
SOURCE_DIR
);
return;
}
// --------------------------------------------------------
// READ SOURCE DIRECTORY
// --------------------------------------------------------
const files =
fs.readdirSync(
SOURCE_DIR, {
withFileTypes: true
}
);
// --------------------------------------------------------
// FILTER IMAGE FILES
// --------------------------------------------------------
const images =
files
.filter(
file =>
file.isFile()
)
.map(
file =>
file.name
)
.filter(
filename => {
const extension =
path.extname(
filename
).toLowerCase();
return SUPPORTED_EXTENSIONS
.includes(
extension
);
}
);
// --------------------------------------------------------
// NO IMAGES
// --------------------------------------------------------
if (
images.length === 0
) {
console.log(
"No supported images found."
);
return;
}
console.log(
`Found ${images.length} image(s).`
);
console.log("");
// --------------------------------------------------------
// PROCESS IMAGES
// --------------------------------------------------------
const results = [];
for (
const filename of images
) {
const result =
await compressImage(
filename
);
results.push(
result
);
}
// ========================================================
// SUMMARY
// ========================================================
let totalOriginal = 0;
let totalCompressed = 0;
let successful = 0;
let skipped = 0;
let copied = 0;
let errors = 0;
results.forEach(
result => {
totalOriginal +=
result.originalSize || 0;
totalCompressed +=
result.compressedSize || 0;
if (
result.status ===
"compressed"
) {
successful++;
}
if (
result.status ===
"skipped"
) {
skipped++;
}
if (
result.status ===
"copied"
) {
copied++;
}
if (
result.status ===
"error"
) {
errors++;
}
}
);
// --------------------------------------------------------
// TOTAL SAVINGS
// --------------------------------------------------------
const totalSaved =
totalOriginal -
totalCompressed;
const totalReduction =
getReduction(
totalOriginal,
totalCompressed
);
// ========================================================
// FINAL REPORT
// ========================================================
console.log("");
console.log(
"================================================"
);
console.log(
" COMPRESSION COMPLETE"
);
console.log(
"================================================"
);
console.log("");
console.log(
`Images found: ${images.length}`
);
console.log(
`Compressed: ${successful}`
);
console.log(
`Copied: ${copied}`
);
console.log(
`Skipped: ${skipped}`
);
console.log(
`Errors: ${errors}`
);
console.log("");
console.log(
`Original size: ${formatSize(totalOriginal)}`
);
console.log(
`Compressed size: ${formatSize(totalCompressed)}`
);
console.log(
`Total saved: ${formatSize(totalSaved)}`
);
console.log(
`Total reduction: ${totalReduction.toFixed(2)}%`
);
console.log("");
console.log(
`Output directory: ${TARGET_DIR}`
);
console.log("");
console.log(
"================================================"
);
console.log("");
}
// ============================================================
// START APPLICATION
// ============================================================
main();
==========================================
{
"name": "node-image-compressor",
"version": "1.0.0",
"description": "node image compressor",
"license": "ISC",
"author": "shimanta das",
"type": "commonjs",
"main": "compress.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"sharp": "^0.35.4"
}
}