@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 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();
@@ -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
+ }