image-video-optimizer 3.3.33 → 3.5.1

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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "processed": [],
3
+ "failed": [],
4
+ "startTime": null,
5
+ "lastRun": "2026-04-14T14:04:49.168Z"
6
+ }
@@ -1,4 +1,9 @@
1
- img_max_width=1080
2
- img_format=jpg
3
- video_max_width=720
4
- video_encode=h264
1
+ # Image Video Optimizer Configuration
2
+ # Generated automatically
3
+
4
+ max_image_width=720 # Compress images wider than this (pixels)
5
+ img_format=null # Target format for image conversion (null=no conversion)
6
+ video_max_width=720 # Maximum width for videos (pixels)
7
+ video_encode=h264 # Video encoding format
8
+ audio_ext=mp3 # Audio extension for audio files
9
+ pdf_compress=true # Enable/disable PDF compression
package/README.md CHANGED
@@ -25,7 +25,12 @@ npm install -g image-video-optimizer
25
25
  ### Basic Usage
26
26
 
27
27
  ```bash
28
- image-video-optimizer /path/to/directory
28
+ image-video-optimizer . # will proceed the current directory
29
+ image-video-optimizer /path/to/directory # will proceed the specified directory
30
+ image-video-optimizer /path/to/directory/file.jpg # will proceed the specified image file
31
+ image-video-optimizer /path/to/directory/file.pdf # will proceed the specified pdf file
32
+ image-video-optimizer /path/to/directory/file.mp4 # will proceed the specified video file
33
+ image-video-optimizer /path/to/directory/file.mp3 # will proceed the specified audio file
29
34
  ```
30
35
 
31
36
  ### Options
@@ -34,7 +39,7 @@ image-video-optimizer /path/to/directory
34
39
  image-video-optimizer /path/to/directory [options]
35
40
  ```
36
41
 
37
- - `<directory>`: Target directory to optimize (required)
42
+ - `<target>`: Target directory or file to optimize (required)
38
43
  - `-r, --reset`: Reset status file and start fresh
39
44
  - `-v, --verbose`: Enable verbose logging
40
45
  - `-V, --version`: Show version number
@@ -46,7 +51,7 @@ Create a `.image-video-optimizer.conf` file in your target directory to customiz
46
51
 
47
52
  ```ini
48
53
  # Image settings
49
- img_max_width=1080 # Maximum width for images (pixels)
54
+ img_max_width=720 # Maximum width for images (pixels)
50
55
  img_format=jpg # Target format for image conversion
51
56
 
52
57
  # Video settings
@@ -63,7 +68,7 @@ pdf_compress=true # Enable/disable PDF compression
63
68
  ### Default Configuration
64
69
 
65
70
  If no configuration file is found, these defaults are used:
66
- - `img_max_width`: 1080
71
+ - `img_max_width`: 720
67
72
  - `img_format`: jpg
68
73
  - `video_max_width`: 720
69
74
  - `video_encode`: h264
@@ -74,7 +79,7 @@ If no configuration file is found, these defaults are used:
74
79
 
75
80
  ### Images
76
81
  - Input: jpg, jpeg, png, gif, bmp, tiff, webp
77
- - Output: jpg, png, webp (configurable)
82
+ - Output: jpg, png, webp (as it is but compressed with better algorithms like avif webp)
78
83
 
79
84
  ### Videos
80
85
  - Input: avi, mov, wmv, flv, webm, mkv, m4v, mp4
@@ -143,10 +148,10 @@ image-video-optimizer ./photos --verbose
143
148
  ### Custom configuration
144
149
  Create `.image-video-optimizer.conf`:
145
150
  ```ini
146
- img_max_width=1920
151
+ img_max_width=720
147
152
  img_format=webp
148
- video_max_width=1080
149
- video_encode=h265
153
+ video_max_width=720
154
+ video_encode=h264
150
155
  audio_ext=mp3
151
156
  pdf_compress=true
152
157
  ```
package/bin/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { Command } = require('commander');
4
4
  const path = require('path');
5
+ const fs = require('fs');
5
6
  const { optimize, resetStatus } = require('../src/index');
6
7
 
7
8
  const program = new Command();
@@ -9,25 +10,29 @@ const program = new Command();
9
10
  program
10
11
  .name('image-video-optimizer')
11
12
  .description('CLI tool to optimize and compress images, videos, audio, and PDFs')
12
- .version('3.3.33');
13
+ .version('3.4.0');
13
14
 
14
15
  program
15
- .argument('[directory]', 'Target directory to optimize', process.cwd())
16
+ .argument('[target]', 'Target directory or file to optimize', process.cwd())
16
17
  .option('-r, --reset', 'Reset status file and start fresh')
17
18
  .option('-v, --verbose', 'Show detailed processing information')
18
- .action(async (directory, options) => {
19
+ .action(async (target, options) => {
19
20
  try {
20
- const targetDir = path.resolve(directory);
21
+ const targetPath = path.resolve(target);
21
22
 
22
23
  if (options.reset) {
24
+ // For reset, we need to determine if it's a file or directory
25
+ const targetDir = fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()
26
+ ? targetPath
27
+ : path.dirname(targetPath);
23
28
  resetStatus(targetDir);
24
29
  return;
25
30
  }
26
31
 
27
32
  console.log('\nšŸš€ Image Video Optimizer\n');
28
- console.log('Target directory: ' + targetDir);
33
+ console.log('Target: ' + targetPath);
29
34
 
30
- await optimize(targetDir, options.verbose);
35
+ await optimize(targetPath, options.verbose);
31
36
 
32
37
  console.log('\nāœ… Optimization complete!\n');
33
38
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-video-optimizer",
3
- "version": "3.3.33",
3
+ "version": "3.5.1",
4
4
  "description": "CLI tool to optimize and compress images and videos with configurable settings",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -3,98 +3,165 @@ const sharp = require('sharp');
3
3
  const path = require('path');
4
4
 
5
5
  /**
6
- * Process a single image file
6
+ * Get compression options for a specific format
7
+ * @param {string} format - Output format (jpg, png, webp, avif)
8
+ * @returns {object} Sharp compression options
9
+ */
10
+ function getCompressionOptions(format) {
11
+ const options = {
12
+ jpg: { quality: 80, progressive: true },
13
+ jpeg: { quality: 80, progressive: true },
14
+ png: { compressionLevel: 9 },
15
+ webp: { quality: 80 },
16
+ avif: { quality: 75 }
17
+ };
18
+ return options[format.toLowerCase()] || options.jpg;
19
+ }
20
+
21
+ /**
22
+ * Compress image to a specific format
23
+ * @param {object} image - Sharp image object
24
+ * @param {string} format - Target format
25
+ * @returns {object} Sharp pipeline with format applied
26
+ */
27
+ function applyFormat(image, format) {
28
+ const formatLower = format.toLowerCase();
29
+ const options = getCompressionOptions(formatLower);
30
+
31
+ switch (formatLower) {
32
+ case 'jpg':
33
+ case 'jpeg':
34
+ return image.jpeg(options);
35
+ case 'png':
36
+ return image.png(options);
37
+ case 'webp':
38
+ return image.webp(options);
39
+ case 'avif':
40
+ return image.avif(options);
41
+ case 'gif':
42
+ return image.gif();
43
+ case 'tiff':
44
+ return image.tiff();
45
+ default:
46
+ return image.jpeg(options);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Process image with multiple formats and keep the smallest
7
52
  * @param {string} imagePath - Path to the image
8
53
  * @param {object} config - Configuration object
9
- * @returns {Promise<{success: boolean, originalSize: number, optimizedSize: number, message: string}>}
54
+ * @returns {Promise<{success: boolean, originalSize: number, optimizedSize: number, message: string, filePath: string}>}
10
55
  */
11
56
  async function processImage(imagePath, config) {
12
57
  try {
13
58
  const stat = await fs.promises.stat(imagePath);
14
59
  const originalSize = stat.size;
15
- const tmpPath = imagePath + '.tmp';
16
60
  const ext = path.extname(imagePath).toLowerCase();
17
61
  const directory = path.dirname(imagePath);
18
62
  const filename = path.basename(imagePath, ext);
19
63
 
20
- // Determine output format
21
- const outputFormat = config.img_format.toLowerCase();
22
- const outputExt = outputFormat === 'jpg' ? '.jpg' : `.${outputFormat}`;
23
- const finalPath = path.join(directory, filename + outputExt);
24
-
25
- // Check if format conversion is needed
26
- const needsFormatConversion = ext !== outputExt && ext !== (outputFormat === 'jpg' ? '.jpeg' : outputExt);
64
+ // Get max_image_width from config (default to 720)
65
+ const maxImageWidth = config.max_image_width || 720;
27
66
 
28
- // Read and process image
67
+ // Read and get metadata
29
68
  let image = sharp(imagePath);
30
-
31
- // Get image metadata
32
69
  const metadata = await image.metadata();
33
70
 
34
- // Resize if width exceeds max_width
35
- let wasResized = false;
36
- if (metadata.width > config.img_max_width) {
37
- image = image.resize(config.img_max_width, null, {
38
- withoutEnlargement: true
71
+ // Step 1: Apply conditional crop/resize based on max_image_width
72
+ let wasCropped = false;
73
+ if (metadata.width > maxImageWidth) {
74
+ image = image.resize(maxImageWidth, null, {
75
+ withoutEnlargement: true,
76
+ fit: 'inside'
39
77
  });
40
- wasResized = true;
78
+ wasCropped = true;
41
79
  }
42
80
 
43
- // Convert to target format with optimization
44
- if (outputFormat === 'jpg' || outputFormat === 'jpeg') {
45
- image = image.jpeg({ quality: 80, progressive: true });
46
- } else if (outputFormat === 'png') {
47
- image = image.png({ compressionLevel: 9 });
48
- } else if (outputFormat === 'webp') {
49
- image = image.webp({ quality: 80 });
50
- } else if (outputFormat === 'gif') {
51
- image = image.gif();
52
- } else if (outputFormat === 'tiff') {
53
- image = image.tiff();
54
- } else {
55
- // Default to jpeg for unknown formats
56
- image = image.jpeg({ quality: 80, progressive: true });
57
- }
81
+ // Step 2: Compress original image format to temp file
82
+ const originalTmpPath = path.join(directory, `${filename}.original.tmp`);
83
+ const originalFormatImage = applyFormat(image.clone(), ext.replace('.', ''));
84
+ await originalFormatImage.toFile(originalTmpPath);
85
+ const originalTmpStat = await fs.promises.stat(originalTmpPath);
86
+ const originalCompressedSize = originalTmpStat.size;
87
+
88
+ // Step 3: Compress to AVIF and WebP formats to temp files
89
+ const avifTmpPath = path.join(directory, `${filename}.avif.tmp`);
90
+ const webpTmpPath = path.join(directory, `${filename}.webp.tmp`);
91
+
92
+ const avifImage = applyFormat(image.clone(), 'avif');
93
+ await avifImage.toFile(avifTmpPath);
94
+ const avifTmpStat = await fs.promises.stat(avifTmpPath);
95
+ const avifCompressedSize = avifTmpStat.size;
58
96
 
59
- // Write to temporary file
60
- await image.toFile(tmpPath);
97
+ const webpImage = applyFormat(image.clone(), 'webp');
98
+ await webpImage.toFile(webpTmpPath);
99
+ const webpTmpStat = await fs.promises.stat(webpTmpPath);
100
+ const webpCompressedSize = webpTmpStat.size;
61
101
 
62
- // Check if optimized file is smaller
63
- const tmpStat = await fs.promises.stat(tmpPath);
64
- const optimizedSize = tmpStat.size;
102
+ // Step 4: Compare sizes and determine best format
103
+ const formatComparison = [
104
+ { format: 'original', size: originalCompressedSize, tmpPath: originalTmpPath },
105
+ { format: 'avif', size: avifCompressedSize, tmpPath: avifTmpPath },
106
+ { format: 'webp', size: webpCompressedSize, tmpPath: webpTmpPath }
107
+ ];
65
108
 
66
- // Decide whether to replace: if smaller OR if format conversion needed OR if resized
67
- const shouldReplace = optimizedSize < originalSize || needsFormatConversion || wasResized;
109
+ // Sort by size to find the smallest
110
+ formatComparison.sort((a, b) => a.size - b.size);
111
+ const bestFormat = formatComparison[0];
112
+
113
+ // Step 5: Calculate compression savings
114
+ const optimizedSize = bestFormat.size;
115
+ const savedSize = originalSize - optimizedSize;
116
+ const savedPercent = ((savedSize / originalSize) * 100).toFixed(1);
117
+
118
+ // Step 6: Decide whether to replace
119
+ const shouldReplace = optimizedSize < originalSize || wasCropped;
68
120
 
69
121
  if (shouldReplace) {
70
- // Remove original and rename temp to final path
71
- if (finalPath !== imagePath) {
72
- await fs.promises.unlink(imagePath);
73
- }
74
- await fs.promises.rename(tmpPath, finalPath);
122
+ // Read the best file and save it with original filename + extension
123
+ const bestFileContent = await fs.promises.readFile(bestFormat.tmpPath);
124
+ const finalPath = imagePath; // Keep original filename and extension
125
+
126
+ await fs.promises.writeFile(finalPath, bestFileContent);
75
127
 
76
- const action = needsFormatConversion ? `${path.basename(imagePath)} → ${path.basename(finalPath)}` : path.basename(finalPath);
77
- const sizeInfo = optimizedSize < originalSize
78
- ? `(${(originalSize / 1024).toFixed(1)}KB → ${(optimizedSize / 1024).toFixed(1)}KB)`
79
- : `(${(originalSize / 1024).toFixed(1)}KB)`;
128
+ // Clean up temp files
129
+ await Promise.all([
130
+ fs.promises.unlink(originalTmpPath).catch(() => {}),
131
+ fs.promises.unlink(avifTmpPath).catch(() => {}),
132
+ fs.promises.unlink(webpTmpPath).catch(() => {})
133
+ ]);
134
+
135
+ const compressionInfo = wasCropped ? `[CROPPED to ${maxImageWidth}px] ` : '';
136
+ const sizeInfo = `${(originalSize / 1024).toFixed(1)}KB → ${(optimizedSize / 1024).toFixed(1)}KB (${savedPercent}% saved)`;
137
+ const formatInfo = bestFormat.format !== 'original' ? ` [${bestFormat.format.toUpperCase()} codec]` : '';
80
138
 
81
139
  return {
82
140
  success: true,
83
141
  originalSize,
84
142
  optimizedSize,
85
- message: `āœ“ ${action} ${sizeInfo}`,
86
- filePath: finalPath
143
+ savedSize,
144
+ savedPercent: parseFloat(savedPercent),
145
+ message: `āœ“ ${path.basename(imagePath)} ${compressionInfo}${sizeInfo}${formatInfo}`,
146
+ filePath: finalPath,
147
+ usedFormat: bestFormat.format,
148
+ wasCropped
87
149
  };
88
150
  } else {
89
- // Remove temp file if not replacing
90
- await fs.promises.unlink(tmpPath);
151
+ // Clean up temp files
152
+ await Promise.all([
153
+ fs.promises.unlink(originalTmpPath).catch(() => {}),
154
+ fs.promises.unlink(avifTmpPath).catch(() => {}),
155
+ fs.promises.unlink(webpTmpPath).catch(() => {})
156
+ ]);
91
157
 
92
158
  return {
93
159
  success: false,
94
160
  originalSize,
95
- optimizedSize,
161
+ optimizedSize: originalSize,
96
162
  message: `ā—‹ ${path.basename(imagePath)} - Already optimized (${(originalSize / 1024).toFixed(1)}KB)`,
97
- filePath: imagePath
163
+ filePath: imagePath,
164
+ usedFormat: 'original'
98
165
  };
99
166
  }
100
167
  } catch (error) {
package/src/index.js CHANGED
@@ -7,8 +7,7 @@ const audioProcessor = require('./audioProcessor');
7
7
  const pdfProcessor = require('./pdfProcessor');
8
8
 
9
9
  const DEFAULT_CONFIG = {
10
- img_max_width: 1080,
11
- img_format: 'jpg',
10
+ max_image_width: 720,
12
11
  video_max_width: 720,
13
12
  video_encode: 'h264',
14
13
  audio_ext: 'mp3',
@@ -70,8 +69,7 @@ function ensureConfigFile(targetDir) {
70
69
  const configContent = `# Image Video Optimizer Configuration
71
70
  # Generated automatically
72
71
 
73
- img_max_width=${DEFAULT_CONFIG.img_max_width} # Maximum width for images (pixels)
74
- img_format=${DEFAULT_CONFIG.img_format} # Target format for image conversion
72
+ max_image_width=${DEFAULT_CONFIG.max_image_width} # Compress images wider than this (pixels)
75
73
  video_max_width=${DEFAULT_CONFIG.video_max_width} # Maximum width for videos (pixels)
76
74
  video_encode=${DEFAULT_CONFIG.video_encode} # Video encoding format
77
75
  audio_ext=${DEFAULT_CONFIG.audio_ext} # Audio extension for audio files
@@ -178,15 +176,19 @@ function markFailed(filePath, error, status, targetDir) {
178
176
 
179
177
  /**
180
178
  * Main optimization function
181
- * @param {string} targetDir - Target directory to optimize
179
+ * @param {string} targetPath - Target directory or file to optimize
182
180
  * @param {boolean} verbose - Show detailed processing information
183
181
  */
184
- async function optimize(targetDir, verbose = false) {
185
- // Validate directory exists
186
- if (!fs.existsSync(targetDir)) {
187
- throw new Error(`Directory does not exist: ${targetDir}`);
182
+ async function optimize(targetPath, verbose = false) {
183
+ // Validate target exists
184
+ if (!fs.existsSync(targetPath)) {
185
+ throw new Error(`Target does not exist: ${targetPath}`);
188
186
  }
189
187
 
188
+ const stats = fs.statSync(targetPath);
189
+ const isFile = stats.isFile();
190
+ const targetDir = isFile ? path.dirname(targetPath) : targetPath;
191
+
190
192
  // Ensure config file exists
191
193
  ensureConfigFile(targetDir);
192
194
 
@@ -196,8 +198,7 @@ async function optimize(targetDir, verbose = false) {
196
198
  const status = loadStatus(targetDir);
197
199
 
198
200
  console.log('Configuration:');
199
- console.log(` Image Max Width: ${config.img_max_width}px`);
200
- console.log(` Image Format: ${config.img_format}`);
201
+ console.log(` Image Max Width: ${config.max_image_width}px`);
201
202
  console.log(` Video Max Width: ${config.video_max_width}px`);
202
203
  console.log(` Video Encode: ${config.video_encode}`);
203
204
  console.log(` Audio Extension: ${config.audio_ext}`);
@@ -208,9 +209,33 @@ async function optimize(targetDir, verbose = false) {
208
209
  }
209
210
  console.log('');
210
211
 
211
- // Search for media files
212
+ // Search for media files or process single file
212
213
  console.log('šŸ” Searching for media files...');
213
- const { images, videos, audio, documents } = await fileSearcher.searchMediaFiles(targetDir, { verbose });
214
+
215
+ let images = [], videos = [], audio = [], documents = [];
216
+
217
+ if (isFile) {
218
+ // Handle single file
219
+ const ext = path.extname(targetPath).toLowerCase().slice(1);
220
+ if (fileSearcher.SUPPORTED_IMAGES.includes(ext)) {
221
+ images = [targetPath];
222
+ } else if (fileSearcher.SUPPORTED_VIDEOS.includes(ext)) {
223
+ videos = [targetPath];
224
+ } else if (fileSearcher.SUPPORTED_AUDIO.includes(ext)) {
225
+ audio = [targetPath];
226
+ } else if (fileSearcher.SUPPORTED_DOCUMENTS.includes(ext)) {
227
+ documents = [targetPath];
228
+ } else {
229
+ throw new Error(`Unsupported file type: ${ext}`);
230
+ }
231
+ } else {
232
+ // Handle directory
233
+ const result = await fileSearcher.searchMediaFiles(targetDir, { verbose });
234
+ images = result.images;
235
+ videos = result.videos;
236
+ audio = result.audio;
237
+ documents = result.documents;
238
+ }
214
239
 
215
240
  // Filter out already processed files
216
241
  const pendingImages = images.filter(img => !isProcessed(img, status));
package/src/config.js DELETED
@@ -1,57 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- const DEFAULT_CONFIG = {
5
- img_max_width: 1080,
6
- img_format: 'jpg',
7
- video_max_width: 720,
8
- video_encode: 'h264'
9
- };
10
-
11
- class Config {
12
- constructor(targetDir) {
13
- this.targetDir = targetDir;
14
- this.configPath = path.join(targetDir, '.image-video-optimizer.conf');
15
- this.config = { ...DEFAULT_CONFIG };
16
- this.loadConfig();
17
- }
18
-
19
- loadConfig() {
20
- try {
21
- if (fs.existsSync(this.configPath)) {
22
- const configContent = fs.readFileSync(this.configPath, 'utf8');
23
- const lines = configContent.split('\n');
24
-
25
- lines.forEach(line => {
26
- line = line.trim();
27
- if (line && !line.startsWith('#')) {
28
- const [key, value] = line.split('=').map(s => s.trim());
29
- if (key && value) {
30
- if (key.includes('width') || key.includes('max_width')) {
31
- this.config[key] = parseInt(value, 10);
32
- } else {
33
- this.config[key] = value;
34
- }
35
- }
36
- }
37
- });
38
-
39
- console.log(`Loaded configuration from ${this.configPath}`);
40
- } else {
41
- console.log('Using default configuration');
42
- }
43
- } catch (error) {
44
- console.warn('Error loading configuration file, using defaults:', error.message);
45
- }
46
- }
47
-
48
- get(key) {
49
- return this.config[key];
50
- }
51
-
52
- getAll() {
53
- return { ...this.config };
54
- }
55
- }
56
-
57
- module.exports = Config;