image-video-optimizer 3.4.0 → 4.0.2

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,8 +1,8 @@
1
1
  # Image Video Optimizer Configuration
2
2
  # Generated automatically
3
3
 
4
- img_max_width=1080 # Maximum width for images (pixels)
5
- img_format=jpg # Target format for image conversion
4
+ max_image_width=720 # Compress images wider than this (pixels)
5
+ img_format=null # Target format for image conversion (null=no conversion)
6
6
  video_max_width=720 # Maximum width for videos (pixels)
7
7
  video_encode=h264 # Video encoding format
8
8
  audio_ext=mp3 # Audio extension for audio files
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  A powerful CLI tool to optimize images, videos, audio, and PDFs with configurable resize and compression settings.
4
4
 
5
+ ## now Smart Image Compression: Compares original, WebP, AVIF formats in v4
5
6
  ## Features
6
7
 
7
8
  - **Image Optimization**: Resize and convert images to specified formats
@@ -51,7 +52,7 @@ Create a `.image-video-optimizer.conf` file in your target directory to customiz
51
52
 
52
53
  ```ini
53
54
  # Image settings
54
- img_max_width=1080 # Maximum width for images (pixels)
55
+ img_max_width=720 # Maximum width for images (pixels)
55
56
  img_format=jpg # Target format for image conversion
56
57
 
57
58
  # Video settings
@@ -68,7 +69,7 @@ pdf_compress=true # Enable/disable PDF compression
68
69
  ### Default Configuration
69
70
 
70
71
  If no configuration file is found, these defaults are used:
71
- - `img_max_width`: 1080
72
+ - `img_max_width`: 720
72
73
  - `img_format`: jpg
73
74
  - `video_max_width`: 720
74
75
  - `video_encode`: h264
@@ -79,7 +80,7 @@ If no configuration file is found, these defaults are used:
79
80
 
80
81
  ### Images
81
82
  - Input: jpg, jpeg, png, gif, bmp, tiff, webp
82
- - Output: jpg, png, webp (configurable)
83
+ - Output: jpg, png, webp (as it is but compressed with better algorithms like avif webp)
83
84
 
84
85
  ### Videos
85
86
  - Input: avi, mov, wmv, flv, webm, mkv, m4v, mp4
@@ -148,10 +149,10 @@ image-video-optimizer ./photos --verbose
148
149
  ### Custom configuration
149
150
  Create `.image-video-optimizer.conf`:
150
151
  ```ini
151
- img_max_width=1920
152
+ img_max_width=720
152
153
  img_format=webp
153
- video_max_width=1080
154
- video_encode=h265
154
+ video_max_width=720
155
+ video_encode=h264
155
156
  audio_ext=mp3
156
157
  pdf_compress=true
157
158
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "image-video-optimizer",
3
- "version": "3.4.0",
4
- "description": "CLI tool to optimize and compress images and videos with configurable settings",
3
+ "version": "4.0.2",
4
+ "description": "CLI tool to optimize and compress images, videos and pdfs with configurable settings",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "image-video-optimizer": "./bin/cli.js"
@@ -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
@@ -200,8 +198,7 @@ async function optimize(targetPath, verbose = false) {
200
198
  const status = loadStatus(targetDir);
201
199
 
202
200
  console.log('Configuration:');
203
- console.log(` Image Max Width: ${config.img_max_width}px`);
204
- console.log(` Image Format: ${config.img_format}`);
201
+ console.log(` Image Max Width: ${config.max_image_width}px`);
205
202
  console.log(` Video Max Width: ${config.video_max_width}px`);
206
203
  console.log(` Video Encode: ${config.video_encode}`);
207
204
  console.log(` Audio Extension: ${config.audio_ext}`);
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;