img-reducer-tool 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Image Reducer Tool
2
+
3
+ CLI tool for optimizing image file sizes while maintaining quality.
4
+
5
+ ## Installation
6
+ ```bash
7
+ npm install -g img-reducer-tool
8
+ ```
9
+
10
+ ## Usage
11
+ ```bash
12
+ img-reducer-tool -i <input> -q <quality> -s <size> [options]
13
+ ```
14
+
15
+ ## Parameters
16
+
17
+ ### `-i <file | folder | all>` (Required)
18
+ Input source for image optimization:
19
+ - **Single image file**: Path to a specific image
20
+ - **Folder**: Directory containing images to process
21
+ - **"all"**: Process all images in current directory
22
+
23
+ ### `-q <1-100>` (Required)
24
+ Initial compression quality:
25
+ - Higher values preserve more quality
26
+ - Lower values reduce file size more aggressively
27
+ - Minimum value is 1
28
+
29
+ ### `-s <size>` (Required)
30
+ Maximum target size in KB:
31
+ - Example: `1000` = 1 MB
32
+ - Tool will attempt to reach closest possible size if target cannot be met
33
+
34
+ ### `-format <jpg|jpeg|png|webp|avif>` (Optional)
35
+ Convert image to different format during optimization
36
+
37
+ ### `-overwrite` (Optional)
38
+ Replace original file:
39
+ - **Default behavior**: Creates new file with "-reduced" suffix
40
+ - **With flag**: Removes original and keeps optimized version only
41
+
42
+ ## Supported Formats
43
+
44
+ - JPG / JPEG
45
+ - PNG
46
+ - WebP
47
+ - AVIF
48
+
49
+ ## Examples
50
+
51
+ **Optimize single image:**
52
+ ```bash
53
+ img-reducer-tool -i photo.png -q 80 -s 800
54
+ ```
55
+
56
+ **Convert format:**
57
+ ```bash
58
+ img-reducer-tool -i photo.png -q 80 -s 500 -format webp
59
+ ```
60
+
61
+ **Process folder:**
62
+ ```bash
63
+ img-reducer-tool -i ./images -q 70 -s 600
64
+ ```
65
+
66
+ **Process all images in current directory:**
67
+ ```bash
68
+ img-reducer-tool -i all -q 75 -s 700
69
+ ```
70
+
71
+ **Overwrite original file:**
72
+ ```bash
73
+ img-reducer-tool -i photo.jpg -q 70 -s 500 -overwrite
74
+ ```
75
+
76
+ ## Important Notes
77
+
78
+ - Minimum quality value is 1
79
+ - Closest possible size will be used if target cannot be met
80
+ - Files with "-reduced" suffix are automatically skipped
81
+ - Overwrite mode removes the original file permanently
82
+
83
+ ## License
84
+
85
+ MIT
86
+
87
+ ## Contributing
88
+
89
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import { reduceImageHelper } from "../src/reduce.js";
3
+ import { resolveImages } from "../src/files.js";
4
+ import path from "path";
5
+ import { errorFormatNotSupported, errorUnexpected, banner, line, showConfig, showSuccess, errorMissingParams, errorImageRequired, errorInvalidQuality, errorInvalidSize, errorFileNotFound, showMultipleSuccess, showProcessing, showGeneralInfo } from "../src/console.js";
6
+
7
+ const args = process.argv.slice(2);
8
+ const VALID_FORMATS = ["jpg", "jpeg", "png", "webp", "avif"];
9
+
10
+ if (args[0] == "--info") {
11
+ showGeneralInfo()
12
+ process.exit(1);
13
+ }
14
+
15
+ let values = {
16
+ images: [],
17
+ quality: 0,
18
+ size: 0,
19
+ format: "",
20
+ overwrite: false
21
+ };
22
+
23
+ banner();
24
+
25
+ if (args.length < 6) {
26
+ errorMissingParams();
27
+ process.exit(1);
28
+ }
29
+
30
+ for (let i = 0; i < args.length; i++) {
31
+ const arg = args[i];
32
+
33
+ if (arg === "-i") {
34
+ i++;
35
+ while (args[i] && !args[i].startsWith("-")) {
36
+ values.images.push(args[i]);
37
+ i++;
38
+ }
39
+ i--;
40
+ continue;
41
+ }
42
+
43
+ if (arg === "-q") values.quality = Number(args[i + 1]);
44
+ if (arg === "-s") values.size = Number(args[i + 1]);
45
+ if (arg === "-format") values.format = args[i + 1];
46
+ if (arg === "-overwrite") values.overwrite = true;
47
+ }
48
+
49
+
50
+ if (values.format && !VALID_FORMATS.includes(values.format)) {
51
+ errorFormatNotSupported(values.format)
52
+ process.exit(1);
53
+ }
54
+
55
+ if (!values.images.length) {
56
+ errorImageRequired();
57
+ process.exit(1);
58
+ }
59
+
60
+ if (!values.quality || values.quality < 1 || values.quality > 100) {
61
+ errorInvalidQuality();
62
+ process.exit(1);
63
+ }
64
+
65
+ if (!values.size || values.size <= 0) {
66
+ errorInvalidSize();
67
+ process.exit(1);
68
+ }
69
+
70
+ let images = [];
71
+
72
+ try {
73
+ if (values.images.length === 1 && values.images[0] === "all") {
74
+ images = resolveImages("all");
75
+ } else {
76
+ images = values.images.flatMap(img => resolveImages(img));
77
+ }
78
+ } catch (err) {
79
+ if (err.message === "FILE_NOT_FOUND") {
80
+ errorFileNotFound();
81
+ process.exit(1);
82
+ }
83
+ throw err;
84
+ }
85
+
86
+ showConfig({
87
+ image: images.length === 1
88
+ ? path.basename(images[0])
89
+ : `${images.length} images`,
90
+ quality: values.quality,
91
+ size: values.size,
92
+ format: values.format,
93
+ overwrite: values.overwrite,
94
+ });
95
+
96
+ const isMultiple = images.length > 1;
97
+ let index = 1;
98
+ const results = [];
99
+
100
+ showProcessing(images.length)
101
+
102
+ for (const imagePath of images) {
103
+ try {
104
+ const result = await reduceImageHelper({
105
+ imagePath,
106
+ quality: values.quality,
107
+ size: values.size,
108
+ format: values.format,
109
+ overwrite: values.overwrite,
110
+ });
111
+
112
+ console.log(`[${index}/${images.length}] ${result.input}`);
113
+
114
+ if (!isMultiple) {
115
+ showSuccess(result.output, result.finalSizeKB);
116
+ } else {
117
+ results.push({
118
+ output: result.output,
119
+ finalSizeKB: result.finalSizeKB
120
+ });
121
+ index++;
122
+ }
123
+
124
+ } catch (err) {
125
+ errorUnexpected(err.message)
126
+ line();
127
+ }
128
+ }
129
+
130
+ if (isMultiple) {
131
+ showMultipleSuccess(results);
132
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "img-reducer-tool",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "img-reducer-tool": "bin/imgreducer.js"
7
+ },
8
+ "dependencies": {
9
+ "chalk": "^5.6.2",
10
+ "sharp": "^0.34.5"
11
+ },
12
+ "description": "CLI tool for optimizing image file sizes while maintaining quality.",
13
+ "devDependencies": {},
14
+ "scripts": {
15
+ "test": "echo \"Error: no test specified\" && exit 1"
16
+ },
17
+ "keywords": ["image", "compress", "cli", "sharp"],
18
+ "author": "William Molina",
19
+ "license": "ISC"
20
+ }
package/src/console.js ADDED
@@ -0,0 +1,216 @@
1
+ import chalk from 'chalk';
2
+
3
+ const colors = {
4
+ primary: chalk.cyan,
5
+ success: chalk.green,
6
+ error: chalk.red,
7
+ warning: chalk.yellow,
8
+ muted: chalk.gray,
9
+ bold: chalk.bold,
10
+ dim: chalk.dim
11
+ };
12
+
13
+ const symbols = {
14
+ success: '✓',
15
+ error: '✗',
16
+ info: 'ℹ',
17
+ arrow: '→',
18
+ bullet: '•'
19
+ };
20
+
21
+ export const banner = () => {
22
+ console.log(colors.success(`
23
+ ██████ ▄██▄▄██▄ ▄████▄ ▄██████ ▄█████ ████████ ▄█████▄ ▄█████▄ ██
24
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
25
+ ██ ██ ██ ██ ██ ██ ██ ███ █████ ██ ██ ██ ██ ██ ██
26
+ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██
27
+ ██████ ██ ██ ██ ██ ██ ▀█████▀ ▀█████ ██ ▀█████▀ ▀█████▀ ██████
28
+ `));
29
+ console.log(colors.success('Version 1.0.0'));
30
+ };
31
+
32
+ export const line = () => {
33
+ console.log(colors.muted('─'.repeat(60)));
34
+ };
35
+
36
+ export const section = (title) => {
37
+ console.log('');
38
+ console.log(colors.bold(title));
39
+ line();
40
+ };
41
+
42
+ export const errorMissingParams = () => {
43
+ console.log('');
44
+ console.log(colors.error(`${symbols.error} Missing required parameters`));
45
+ console.log('');
46
+ console.log('Usage:');
47
+ console.log(colors.muted(' img-reducer-tool -i <image> -q <quality> -s <size>'));
48
+ console.log('');
49
+ console.log('Required parameters:');
50
+ console.log(` ${colors.bold('-i')} ${colors.dim('Image file path')}`);
51
+ console.log(` ${colors.bold('-q')} ${colors.dim('Quality (1-100)')}`);
52
+ console.log(` ${colors.bold('-s')} ${colors.dim('Target size in KB')}`);
53
+ console.log('');
54
+ console.log(colors.muted('Run --info for detailed documentation'));
55
+ console.log('');
56
+ };
57
+
58
+ export const errorImageRequired = () => {
59
+ console.log('');
60
+ console.log(colors.error(`${symbols.error} Image path is required`));
61
+ console.log(colors.muted(' Use -i to specify an image file'));
62
+ console.log('');
63
+ };
64
+
65
+ export const errorFormatNotSupported = (format) => {
66
+ console.log('');
67
+ console.log(colors.error(`${symbols.error} This format is not supported: ${format}`));
68
+
69
+ section('Supported Formats');
70
+ console.log('jpg, jpeg, png, webp, avif');
71
+ console.log('');
72
+ console.log(colors.muted('Run --info for detailed documentation'));
73
+ console.log('');
74
+ };
75
+
76
+ export const errorUnexpected = (error) => {
77
+ console.log('');
78
+ console.log(colors.error(`Unexpected error: ${error}`));
79
+ console.log('');
80
+ console.log(colors.muted('Run --info for detailed documentation'));
81
+ console.log('');
82
+ };
83
+
84
+ export const errorInvalidQuality = () => {
85
+ console.log('');
86
+ console.log(colors.error(`${symbols.error} Invalid quality value`));
87
+ console.log(colors.muted(' Quality must be between 1 and 100'));
88
+ console.log('');
89
+ console.log(colors.muted('Run --info for detailed documentation'));
90
+ console.log('');
91
+ };
92
+
93
+ export const errorInvalidSize = () => {
94
+ console.log('');
95
+ console.log(colors.error(`${symbols.error} Invalid size value`));
96
+ console.log(colors.muted(' Size must be greater than 0 (in KB)'));
97
+ console.log('');
98
+ console.log(colors.muted('Run --info for detailed documentation'));
99
+ console.log('');
100
+ };
101
+
102
+ export const errorFileNotFound = () => {
103
+ console.log('');
104
+ console.log(colors.error(`${symbols.error} Image file not found`));
105
+ console.log(colors.muted(' Please verify the file path'));
106
+ console.log('');
107
+ console.log(colors.muted('Run --info for detailed documentation'));
108
+ console.log('');
109
+ };
110
+
111
+ export const showConfig = ({ image, quality, size, format, overwrite }) => {
112
+ section('Configuration');
113
+ console.log(`File: ${colors.primary(image)}`);
114
+ console.log(`Quality: ${colors.primary(quality + '%')}`);
115
+ console.log(`Target size: ${colors.primary(size + ' KB')}`);
116
+ console.log(`Format change: ${format ? colors.success('Yes') : colors.muted('No')}`);
117
+ console.log(`Overwrite: ${overwrite ? colors.warning('Yes') : colors.muted('No')}`);
118
+ line();
119
+ };
120
+
121
+ export const showProcessing = (length) => {
122
+ const imageText = length === 1 ? 'image' : 'images';
123
+ console.log(colors.muted(`Processing ${length} ${imageText}...`));
124
+ console.log('');
125
+ };
126
+
127
+ export const showSuccess = (file, sizeKB) => {
128
+ line();
129
+ console.log(colors.success(`${symbols.success} Completed successfully`));
130
+ console.log('');
131
+ console.log('Output:');
132
+ console.log(`${symbols.bullet} ${colors.bold(file)} ${colors.muted(symbols.arrow)} ${colors.primary(Math.round(sizeKB) + ' KB')}`);
133
+ line();
134
+ };
135
+
136
+ export const showMultipleSuccess = (files) => {
137
+ line();
138
+ console.log(colors.success(`${symbols.success} Completed successfully`));
139
+ console.log('');
140
+ console.log(`Output files (${files.length}):`);
141
+
142
+ for (const file of files) {
143
+ console.log(`${symbols.bullet} ${colors.bold(file.output)} ${colors.muted(symbols.arrow)} ${colors.primary(Math.round(file.finalSizeKB) + ' KB')}`);
144
+ }
145
+
146
+ console.log('');
147
+ line();
148
+ };
149
+
150
+ export const showGeneralInfo = () => {
151
+ banner();
152
+
153
+ section('Overview');
154
+ console.log('CLI tool for optimizing image file sizes while maintaining quality.');
155
+ console.log('');
156
+
157
+ section('Parameters');
158
+
159
+ console.log(colors.bold(' -i') + colors.dim(' <file | folder | all>'));
160
+ console.log(' Single image file');
161
+ console.log(' Folder containing images');
162
+ console.log(' "all" for current directory');
163
+ console.log('');
164
+
165
+ console.log(colors.bold(' -q') + colors.dim(' <1-100>'));
166
+ console.log(' Initial compression quality');
167
+ console.log(' Higher values preserve more quality');
168
+ console.log('');
169
+
170
+ console.log(colors.bold(' -s') + colors.dim(' <size>'));
171
+ console.log(' Maximum target size in KB');
172
+ console.log(' Example: 1000 = 1 MB');
173
+ console.log('');
174
+
175
+ console.log(colors.bold(' -format') + colors.dim(' <jpg|jpeg|png|webp|avif>'));
176
+ console.log(' Convert to different format (optional)');
177
+ console.log('');
178
+
179
+ console.log(colors.bold(' -overwrite'));
180
+ console.log(' Replace original file');
181
+ console.log(' Default: creates "-reduced" suffix');
182
+ console.log('');
183
+
184
+ section('Supported Formats');
185
+ console.log(' jpg, jpeg, png, webp, avif');
186
+ console.log('');
187
+
188
+ section('Examples');
189
+
190
+ console.log(colors.dim(' Optimize single image:'));
191
+ console.log(' img-reducer-tool -i photo.png -q 80 -s 800');
192
+ console.log('');
193
+
194
+ console.log(colors.dim(' Convert format:'));
195
+ console.log(' img-reducer-tool -i photo.png -q 80 -s 500 -format webp');
196
+ console.log('');
197
+
198
+ console.log(colors.dim(' Process folder:'));
199
+ console.log(' img-reducer-tool -i ./images -q 70 -s 600');
200
+ console.log('');
201
+
202
+ console.log(colors.dim(' Process all images:'));
203
+ console.log(' img-reducer-tool -i all -q 75 -s 700');
204
+ console.log('');
205
+
206
+ console.log(colors.dim(' Overwrite original:'));
207
+ console.log(' img-reducer-tool -i photo.jpg -q 70 -s 500 -overwrite');
208
+ console.log('');
209
+
210
+ section('Notes');
211
+ console.log(` ${symbols.bullet} Minimum quality value is 1`);
212
+ console.log(` ${symbols.bullet} Closest possible size will be used if target cannot be met`);
213
+ console.log(` ${symbols.bullet} Files with "-reduced" suffix are skipped`);
214
+ console.log(` ${symbols.bullet} Overwrite mode removes the original file`);
215
+ line();
216
+ };
package/src/files.js ADDED
@@ -0,0 +1,43 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const VALID_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".avif"];
5
+
6
+ export const resolveImages = (input) => {
7
+ const cwd = process.cwd();
8
+ const fullPath = path.resolve(cwd, input);
9
+
10
+ if (input === "all") {
11
+ return fs
12
+ .readdirSync(cwd)
13
+ .filter(file => {
14
+ const ext = path.extname(file).toLowerCase();
15
+ return (
16
+ VALID_EXTENSIONS.includes(ext) &&
17
+ !file.includes("-reduced")
18
+ );
19
+ })
20
+ .map(file => path.join(cwd, file));
21
+ }
22
+
23
+ if (!fs.existsSync(fullPath)) {
24
+ throw new Error("FILE_NOT_FOUND");
25
+ }
26
+
27
+ const stats = fs.statSync(fullPath);
28
+
29
+ if (stats.isDirectory()) {
30
+ return fs
31
+ .readdirSync(fullPath)
32
+ .filter(file => {
33
+ const ext = path.extname(file).toLowerCase();
34
+ return (
35
+ VALID_EXTENSIONS.includes(ext) &&
36
+ !file.includes("-reduced")
37
+ );
38
+ })
39
+ .map(file => path.join(fullPath, file));
40
+ }
41
+
42
+ return [fullPath];
43
+ };
package/src/reduce.js ADDED
@@ -0,0 +1,85 @@
1
+ // src/reduce.js
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import sharp from "sharp";
5
+
6
+ export const reduceImageHelper = async ({ imagePath, quality, size, format, overwrite }) => {
7
+ const imageBuffer = fs.readFileSync(imagePath);
8
+ const originalSizeKB = imageBuffer.length / 1024;
9
+
10
+ const inputExt = path.extname(imagePath);
11
+ const dir = path.dirname(imagePath);
12
+
13
+ const rawName = path.basename(imagePath, inputExt).replace(/-reduced$/, "");
14
+ const targetFormat = (format || inputExt.replace(".", "")).toLowerCase();
15
+ const outputExt = `.${targetFormat}`;
16
+
17
+ const outputName = overwrite
18
+ ? `${rawName}${outputExt}`
19
+ : `${rawName}-reduced${outputExt}`;
20
+
21
+ const outputPath = path.join(dir, outputName);
22
+
23
+ let currentQuality = quality;
24
+ let outputBuffer = imageBuffer;
25
+ let outputSizeKB = originalSizeKB;
26
+
27
+ while (outputSizeKB > size && currentQuality > 1) {
28
+ let pipeline = sharp(imageBuffer);
29
+
30
+ switch (targetFormat) {
31
+ case "jpg":
32
+ case "jpeg":
33
+ pipeline = pipeline.jpeg({
34
+ quality: currentQuality,
35
+ mozjpeg: true
36
+ });
37
+ break;
38
+ case "png":
39
+ pipeline = pipeline.png({
40
+ compressionLevel: 9,
41
+ quality: currentQuality,
42
+ });
43
+ break;
44
+ case "webp":
45
+ pipeline = pipeline.webp({
46
+ quality: currentQuality,
47
+ effort: 6
48
+ });
49
+ break;
50
+ case "avif":
51
+ pipeline = pipeline.avif({
52
+ quality: currentQuality,
53
+ effort: 9
54
+ });
55
+ break;
56
+ default:
57
+ throw new Error("UNSUPPORTED_FORMAT");
58
+ }
59
+
60
+ outputBuffer = await pipeline.toBuffer();
61
+ outputSizeKB = outputBuffer.length / 1024;
62
+
63
+ if (outputSizeKB > size * 2) {
64
+ currentQuality -= 10;
65
+ } else if (outputSizeKB > size * 1.5) {
66
+ currentQuality -= 5;
67
+ } else {
68
+ currentQuality -= 2;
69
+ }
70
+ }
71
+
72
+ fs.writeFileSync(outputPath, outputBuffer);
73
+
74
+ if (overwrite && outputPath !== imagePath) {
75
+ fs.unlinkSync(imagePath);
76
+ }
77
+
78
+ return {
79
+ input: path.basename(imagePath),
80
+ output: outputName,
81
+ originalSizeKB,
82
+ finalSizeKB: outputSizeKB,
83
+ overwritten: overwrite
84
+ };
85
+ };