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"

  }

}




Popular posts from this blog

MCSL 216 MCA NEW Practical ~ common questions suggestions

jitsi

Dev Cheatsheet