image-video-optimizer 3.4.0 → 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,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
@@ -51,7 +51,7 @@ Create a `.image-video-optimizer.conf` file in your target directory to customiz
51
51
 
52
52
  ```ini
53
53
  # Image settings
54
- img_max_width=1080 # Maximum width for images (pixels)
54
+ img_max_width=720 # Maximum width for images (pixels)
55
55
  img_format=jpg # Target format for image conversion
56
56
 
57
57
  # Video settings
@@ -68,7 +68,7 @@ pdf_compress=true # Enable/disable PDF compression
68
68
  ### Default Configuration
69
69
 
70
70
  If no configuration file is found, these defaults are used:
71
- - `img_max_width`: 1080
71
+ - `img_max_width`: 720
72
72
  - `img_format`: jpg
73
73
  - `video_max_width`: 720
74
74
  - `video_encode`: h264
@@ -79,7 +79,7 @@ If no configuration file is found, these defaults are used:
79
79
 
80
80
  ### Images
81
81
  - Input: jpg, jpeg, png, gif, bmp, tiff, webp
82
- - Output: jpg, png, webp (configurable)
82
+ - Output: jpg, png, webp (as it is but compressed with better algorithms like avif webp)
83
83
 
84
84
  ### Videos
85
85
  - Input: avi, mov, wmv, flv, webm, mkv, m4v, mp4
@@ -148,10 +148,10 @@ image-video-optimizer ./photos --verbose
148
148
  ### Custom configuration
149
149
  Create `.image-video-optimizer.conf`:
150
150
  ```ini
151
- img_max_width=1920
151
+ img_max_width=720
152
152
  img_format=webp
153
- video_max_width=1080
154
- video_encode=h265
153
+ video_max_width=720
154
+ video_encode=h264
155
155
  audio_ext=mp3
156
156
  pdf_compress=true
157
157
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-video-optimizer",
3
- "version": "3.4.0",
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
@@ -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;