@lamnn96/imgo 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 +127 -0
- package/bin/imgo.js +53 -0
- package/lib/processor.js +264 -0
- package/lib/utils.js +47 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Imgo
|
|
2
|
+
|
|
3
|
+
Compress & convert images to web-friendly formats right from your terminal.
|
|
4
|
+
|
|
5
|
+
Supports: **JPG, PNG, WebP, TIFF, AVIF, HEIC, HEIF, GIF, SVG**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g imgo
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
After installation, add the following to your `~/.zshrc` to ensure the command is available globally:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Open .zshrc
|
|
17
|
+
nano ~/.zshrc
|
|
18
|
+
|
|
19
|
+
# Add this line at the end of the file
|
|
20
|
+
export PATH="$(npm prefix -g)/bin:$PATH"
|
|
21
|
+
|
|
22
|
+
# Save and reload
|
|
23
|
+
source ~/.zshrc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Verify installation:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
imgo --version
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Navigate to the folder containing your images, then run:
|
|
35
|
+
|
|
36
|
+
### Compress only (keep original format)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
imgo
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Compresses all images in the current directory while preserving their original format. HEIC/HEIF files are automatically converted to JPEG.
|
|
43
|
+
|
|
44
|
+
### Compress + Convert to WebP
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
imgo -w
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Compress + Convert to PNG
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
imgo -p
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Compress + Convert to JPEG
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
imgo -j
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Options
|
|
63
|
+
|
|
64
|
+
| Flag | Description |
|
|
65
|
+
|------|-------------|
|
|
66
|
+
| `-w` | Convert all images to WebP |
|
|
67
|
+
| `-p` | Convert all images to PNG |
|
|
68
|
+
| `-j` | Convert all images to JPEG |
|
|
69
|
+
| `-q, --quality <number>` | Output quality 1-100 (default: 80) |
|
|
70
|
+
| `-d, --dir <path>` | Source directory (default: current directory) |
|
|
71
|
+
| `-V, --version` | Show version |
|
|
72
|
+
| `-h, --help` | Show help |
|
|
73
|
+
|
|
74
|
+
### Examples
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Compress all images in current folder
|
|
78
|
+
imgo
|
|
79
|
+
|
|
80
|
+
# Convert to WebP with custom quality
|
|
81
|
+
imgo -w -q 90
|
|
82
|
+
|
|
83
|
+
# Process images from a specific folder
|
|
84
|
+
imgo -w -d ~/Pictures/photos
|
|
85
|
+
|
|
86
|
+
# Convert to JPEG with lower quality for smaller size
|
|
87
|
+
imgo -j -q 60
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Output
|
|
91
|
+
|
|
92
|
+
All processed images are saved to a `result/` folder inside the source directory.
|
|
93
|
+
|
|
94
|
+
After processing, you'll see a summary table:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
IMGO
|
|
98
|
+
Compress + Convert → WEBP | Quality: 80
|
|
99
|
+
Source: /Users/you/photos
|
|
100
|
+
Output: /Users/you/photos/result
|
|
101
|
+
|
|
102
|
+
✔ IMG_9998.HEIC → IMG_9998.webp 1.86 MB → 476.0 KB -75.0%
|
|
103
|
+
✔ IMG_9999.HEIC → IMG_9999.webp 2.61 MB → 926.2 KB -65.3%
|
|
104
|
+
|
|
105
|
+
Summary: 2 image(s) processed
|
|
106
|
+
Total: 5.0 MB → 568 KB (-88.9%)
|
|
107
|
+
Saved: 4.5 MB
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Supported Input Formats
|
|
111
|
+
|
|
112
|
+
- JPEG / JPG
|
|
113
|
+
- PNG
|
|
114
|
+
- WebP
|
|
115
|
+
- TIFF
|
|
116
|
+
- AVIF
|
|
117
|
+
- HEIC / HEIF (Apple photos)
|
|
118
|
+
- GIF
|
|
119
|
+
- SVG
|
|
120
|
+
|
|
121
|
+
## Requirements
|
|
122
|
+
|
|
123
|
+
- Node.js >= 18.0.0
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
package/bin/imgo.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { processImages } from '../lib/processor.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = require('../package.json');
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
const banner = chalk.bold.hex('#FF6B35')(`
|
|
14
|
+
██╗███╗ ███╗ ██████╗ ██████╗
|
|
15
|
+
██║████╗ ████║██╔════╝ ██╔═══██╗
|
|
16
|
+
██║██╔████╔██║██║ ███╗██║ ██║
|
|
17
|
+
██║██║╚██╔╝██║██║ ██║██║ ██║
|
|
18
|
+
██║██║ ╚═╝ ██║╚██████╔╝╚██████╔╝
|
|
19
|
+
╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name('imgo')
|
|
24
|
+
.version(pkg.version)
|
|
25
|
+
.description(`${banner}\n ${chalk.dim('Compress & convert images to web-friendly formats')}\n`)
|
|
26
|
+
.option('-w', 'Compress and convert all images to WebP')
|
|
27
|
+
.option('-p', 'Compress and convert all images to PNG')
|
|
28
|
+
.option('-j', 'Compress and convert all images to JPEG')
|
|
29
|
+
.option('-q, --quality <number>', 'Output quality (1-100)', '80')
|
|
30
|
+
.option('-d, --dir <path>', 'Source directory (default: current directory)', '.')
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
let outputFormat = null;
|
|
33
|
+
|
|
34
|
+
if (options.w) outputFormat = 'webp';
|
|
35
|
+
else if (options.p) outputFormat = 'png';
|
|
36
|
+
else if (options.j) outputFormat = 'jpeg';
|
|
37
|
+
|
|
38
|
+
const quality = Math.min(100, Math.max(1, parseInt(options.quality, 10) || 80));
|
|
39
|
+
const sourceDir = options.dir;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await processImages({
|
|
43
|
+
sourceDir,
|
|
44
|
+
outputFormat,
|
|
45
|
+
quality,
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(chalk.red(`\n✖ Error: ${err.message}\n`));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program.parse();
|
package/lib/processor.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { readdir, readFile, stat, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import convert from 'heic-convert';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import {
|
|
8
|
+
isSupportedImage,
|
|
9
|
+
formatBytes,
|
|
10
|
+
formatReduction,
|
|
11
|
+
getOutputExtension,
|
|
12
|
+
stripExtension,
|
|
13
|
+
getExtension,
|
|
14
|
+
} from './utils.js';
|
|
15
|
+
|
|
16
|
+
const QUALITY_PRESETS = {
|
|
17
|
+
webp: { quality: 80, effort: 6, smartSubsample: true },
|
|
18
|
+
png: { compressionLevel: 9, adaptiveFiltering: true, palette: false },
|
|
19
|
+
jpeg: { quality: 80, mozjpeg: true },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function buildSharpPipeline(pipeline, format, quality) {
|
|
23
|
+
switch (format) {
|
|
24
|
+
case 'webp':
|
|
25
|
+
return pipeline.webp({ ...QUALITY_PRESETS.webp, quality });
|
|
26
|
+
case 'png':
|
|
27
|
+
return pipeline.png({ ...QUALITY_PRESETS.png });
|
|
28
|
+
case 'jpeg':
|
|
29
|
+
case 'jpg':
|
|
30
|
+
return pipeline.jpeg({ ...QUALITY_PRESETS.jpeg, quality });
|
|
31
|
+
default:
|
|
32
|
+
return pipeline;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function detectFormatFromExt(ext) {
|
|
37
|
+
const map = {
|
|
38
|
+
'.jpg': 'jpeg', '.jpeg': 'jpeg',
|
|
39
|
+
'.png': 'png',
|
|
40
|
+
'.webp': 'webp',
|
|
41
|
+
'.tiff': 'tiff', '.tif': 'tiff',
|
|
42
|
+
'.avif': 'avif',
|
|
43
|
+
'.gif': 'gif',
|
|
44
|
+
};
|
|
45
|
+
return map[ext] || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isHeic(ext) {
|
|
49
|
+
return ext === '.heic' || ext === '.heif';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function decodeHeic(inputPath) {
|
|
53
|
+
const inputBuffer = await readFile(inputPath);
|
|
54
|
+
const jpegBuffer = await convert({
|
|
55
|
+
buffer: inputBuffer,
|
|
56
|
+
format: 'JPEG',
|
|
57
|
+
quality: 1,
|
|
58
|
+
});
|
|
59
|
+
return sharp(jpegBuffer);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function compressInOriginalFormat(pipeline, ext, quality) {
|
|
63
|
+
const format = detectFormatFromExt(ext);
|
|
64
|
+
if (!format) return pipeline;
|
|
65
|
+
|
|
66
|
+
switch (format) {
|
|
67
|
+
case 'jpeg':
|
|
68
|
+
return pipeline.jpeg({ quality, mozjpeg: true });
|
|
69
|
+
case 'png':
|
|
70
|
+
return pipeline.png({ compressionLevel: 9, adaptiveFiltering: true });
|
|
71
|
+
case 'webp':
|
|
72
|
+
return pipeline.webp({ quality, effort: 6, smartSubsample: true });
|
|
73
|
+
case 'tiff':
|
|
74
|
+
return pipeline.tiff({ quality, compression: 'deflate' });
|
|
75
|
+
case 'avif':
|
|
76
|
+
return pipeline.avif({ quality, effort: 4 });
|
|
77
|
+
case 'gif':
|
|
78
|
+
return pipeline.gif();
|
|
79
|
+
default:
|
|
80
|
+
return pipeline;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function processImages({ sourceDir, outputFormat, quality }) {
|
|
85
|
+
const absDir = resolve(sourceDir);
|
|
86
|
+
const resultDir = join(absDir, 'result');
|
|
87
|
+
|
|
88
|
+
let files;
|
|
89
|
+
try {
|
|
90
|
+
files = await readdir(absDir);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(`Cannot read directory: ${absDir}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const imageFiles = files.filter(
|
|
96
|
+
(f) => isSupportedImage(f) && f !== 'result'
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (imageFiles.length === 0) {
|
|
100
|
+
console.log(chalk.yellow('\n⚠ No supported images found in this directory.\n'));
|
|
101
|
+
console.log(chalk.dim(' Supported formats: JPG, PNG, WebP, TIFF, AVIF, HEIC, HEIF, GIF, SVG\n'));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await mkdir(resultDir, { recursive: true });
|
|
106
|
+
|
|
107
|
+
const modeLabel = outputFormat
|
|
108
|
+
? `Compress + Convert → ${outputFormat.toUpperCase()}`
|
|
109
|
+
: 'Compress (keep original format)';
|
|
110
|
+
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.bold.hex('#FF6B35')(' IMGO'));
|
|
113
|
+
console.log(chalk.dim(` ${modeLabel} | Quality: ${quality}`));
|
|
114
|
+
console.log(chalk.dim(` Source: ${absDir}`));
|
|
115
|
+
console.log(chalk.dim(` Output: ${resultDir}`));
|
|
116
|
+
console.log('');
|
|
117
|
+
|
|
118
|
+
const spinner = ora({
|
|
119
|
+
text: `Processing ${imageFiles.length} image(s)...`,
|
|
120
|
+
color: 'cyan',
|
|
121
|
+
}).start();
|
|
122
|
+
|
|
123
|
+
const results = [];
|
|
124
|
+
let totalOriginal = 0;
|
|
125
|
+
let totalResult = 0;
|
|
126
|
+
let successCount = 0;
|
|
127
|
+
let failCount = 0;
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < imageFiles.length; i++) {
|
|
130
|
+
const file = imageFiles[i];
|
|
131
|
+
const inputPath = join(absDir, file);
|
|
132
|
+
const ext = getExtension(file);
|
|
133
|
+
const baseName = stripExtension(file);
|
|
134
|
+
|
|
135
|
+
let outExt;
|
|
136
|
+
if (outputFormat) {
|
|
137
|
+
outExt = getOutputExtension(outputFormat);
|
|
138
|
+
} else {
|
|
139
|
+
outExt = ext === '.heic' || ext === '.heif' ? '.jpg' : ext;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const outputFileName = `${baseName}${outExt}`;
|
|
143
|
+
const outputPath = join(resultDir, outputFileName);
|
|
144
|
+
|
|
145
|
+
spinner.text = chalk.dim(` [${i + 1}/${imageFiles.length}] ${file}`);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const inputStat = await stat(inputPath);
|
|
149
|
+
const originalSize = inputStat.size;
|
|
150
|
+
|
|
151
|
+
let pipeline;
|
|
152
|
+
if (isHeic(ext)) {
|
|
153
|
+
pipeline = await decodeHeic(inputPath);
|
|
154
|
+
} else {
|
|
155
|
+
pipeline = sharp(inputPath, { failOn: 'none' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (outputFormat) {
|
|
159
|
+
pipeline = buildSharpPipeline(pipeline, outputFormat, quality);
|
|
160
|
+
} else {
|
|
161
|
+
if (isHeic(ext)) {
|
|
162
|
+
pipeline = pipeline.jpeg({ quality, mozjpeg: true });
|
|
163
|
+
} else if (ext === '.svg') {
|
|
164
|
+
pipeline = pipeline.png({ compressionLevel: 9 });
|
|
165
|
+
} else {
|
|
166
|
+
pipeline = compressInOriginalFormat(pipeline, ext, quality);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await pipeline.toFile(outputPath);
|
|
171
|
+
|
|
172
|
+
const outputStat = await stat(outputPath);
|
|
173
|
+
const newSize = outputStat.size;
|
|
174
|
+
|
|
175
|
+
totalOriginal += originalSize;
|
|
176
|
+
totalResult += newSize;
|
|
177
|
+
successCount++;
|
|
178
|
+
|
|
179
|
+
results.push({
|
|
180
|
+
file,
|
|
181
|
+
outputFile: outputFileName,
|
|
182
|
+
originalSize,
|
|
183
|
+
newSize,
|
|
184
|
+
success: true,
|
|
185
|
+
});
|
|
186
|
+
} catch (err) {
|
|
187
|
+
failCount++;
|
|
188
|
+
results.push({
|
|
189
|
+
file,
|
|
190
|
+
outputFile: outputFileName,
|
|
191
|
+
originalSize: 0,
|
|
192
|
+
newSize: 0,
|
|
193
|
+
success: false,
|
|
194
|
+
error: err.message,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
spinner.stop();
|
|
200
|
+
|
|
201
|
+
results.forEach((r) => {
|
|
202
|
+
if (r.success) {
|
|
203
|
+
console.log(
|
|
204
|
+
chalk.green(' ✔ ') +
|
|
205
|
+
chalk.white(r.file) +
|
|
206
|
+
chalk.dim(' → ') +
|
|
207
|
+
chalk.cyan(r.outputFile) +
|
|
208
|
+
chalk.dim(' ') +
|
|
209
|
+
chalk.dim(formatBytes(r.originalSize)) +
|
|
210
|
+
chalk.dim(' → ') +
|
|
211
|
+
chalk.white(formatBytes(r.newSize)) +
|
|
212
|
+
' ' +
|
|
213
|
+
formatReduction(r.originalSize, r.newSize)
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
console.log(
|
|
217
|
+
chalk.red(' ✖ ') +
|
|
218
|
+
chalk.red(r.file) +
|
|
219
|
+
chalk.dim(' — ') +
|
|
220
|
+
chalk.red(r.error)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
console.log('');
|
|
226
|
+
|
|
227
|
+
if (successCount > 0) {
|
|
228
|
+
const totalSavedPercent = ((totalOriginal - totalResult) / totalOriginal) * 100;
|
|
229
|
+
const savedSign = totalSavedPercent >= 0 ? '-' : '+';
|
|
230
|
+
const savedColor = totalSavedPercent >= 0 ? chalk.green : chalk.red;
|
|
231
|
+
|
|
232
|
+
console.log(
|
|
233
|
+
chalk.bold(' Summary: ') +
|
|
234
|
+
chalk.white(`${successCount} image(s) processed`) +
|
|
235
|
+
(failCount > 0 ? chalk.red(` | ${failCount} failed`) : '')
|
|
236
|
+
);
|
|
237
|
+
console.log(
|
|
238
|
+
chalk.bold(' Total: ') +
|
|
239
|
+
chalk.dim(formatBytes(totalOriginal)) +
|
|
240
|
+
chalk.dim(' → ') +
|
|
241
|
+
chalk.white(formatBytes(totalResult)) +
|
|
242
|
+
' ' +
|
|
243
|
+
savedColor(`(${savedSign}${Math.abs(totalSavedPercent).toFixed(1)}%)`)
|
|
244
|
+
);
|
|
245
|
+
console.log(
|
|
246
|
+
chalk.bold(' Saved: ') +
|
|
247
|
+
savedColor(formatBytes(totalOriginal - totalResult))
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (failCount > 0) {
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(chalk.red.bold(' Errors:'));
|
|
254
|
+
results
|
|
255
|
+
.filter((r) => !r.success)
|
|
256
|
+
.forEach((r) => {
|
|
257
|
+
console.log(chalk.red(` ✖ ${r.file}: ${r.error}`));
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log(chalk.dim(` Results saved to: ${resultDir}`));
|
|
263
|
+
console.log('');
|
|
264
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
4
|
+
'.jpg', '.jpeg', '.png', '.webp', '.tiff', '.tif',
|
|
5
|
+
'.avif', '.heic', '.heif', '.gif', '.svg',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export function isSupportedImage(filename) {
|
|
9
|
+
const ext = filename.toLowerCase().slice(filename.lastIndexOf('.'));
|
|
10
|
+
return SUPPORTED_EXTENSIONS.has(ext);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatBytes(bytes) {
|
|
14
|
+
if (bytes === 0) return '0 B';
|
|
15
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
16
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
17
|
+
const value = bytes / Math.pow(1024, i);
|
|
18
|
+
return `${value.toFixed(value < 10 ? 2 : 1)} ${units[i]}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatReduction(originalSize, newSize) {
|
|
22
|
+
if (originalSize === 0) return '0%';
|
|
23
|
+
const reduction = ((originalSize - newSize) / originalSize) * 100;
|
|
24
|
+
const sign = reduction >= 0 ? '-' : '+';
|
|
25
|
+
const color = reduction >= 0 ? chalk.green : chalk.red;
|
|
26
|
+
return color(`${sign}${Math.abs(reduction).toFixed(1)}%`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getOutputExtension(format) {
|
|
30
|
+
const map = {
|
|
31
|
+
webp: '.webp',
|
|
32
|
+
png: '.png',
|
|
33
|
+
jpeg: '.jpg',
|
|
34
|
+
jpg: '.jpg',
|
|
35
|
+
};
|
|
36
|
+
return map[format] || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function stripExtension(filename) {
|
|
40
|
+
const lastDot = filename.lastIndexOf('.');
|
|
41
|
+
return lastDot > 0 ? filename.slice(0, lastDot) : filename;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getExtension(filename) {
|
|
45
|
+
const lastDot = filename.lastIndexOf('.');
|
|
46
|
+
return lastDot > 0 ? filename.slice(lastDot).toLowerCase() : '';
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lamnn96/imgo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Compress and convert images to web-friendly formats (WebP, PNG, JPEG) right from your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/processor.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"imgo": "bin/imgo.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"image",
|
|
12
|
+
"compress",
|
|
13
|
+
"convert",
|
|
14
|
+
"webp",
|
|
15
|
+
"png",
|
|
16
|
+
"jpeg",
|
|
17
|
+
"heic",
|
|
18
|
+
"optimization",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"author": "Lam Nguyen",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"cli-table3": "^0.6.5",
|
|
29
|
+
"commander": "^12.1.0",
|
|
30
|
+
"glob": "^11.0.0",
|
|
31
|
+
"heic-convert": "^2.1.0",
|
|
32
|
+
"ora": "^8.1.0",
|
|
33
|
+
"sharp": "^0.33.5"
|
|
34
|
+
}
|
|
35
|
+
}
|