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 +89 -0
- package/bin/imgreducer.js +132 -0
- package/package.json +20 -0
- package/src/console.js +216 -0
- package/src/files.js +43 -0
- package/src/reduce.js +85 -0
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
|
+
};
|