image-video-optimizer 3.0.2 → 3.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-video-optimizer",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "CLI tool to optimize and compress images and videos with configurable settings",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -47,8 +47,13 @@ async function processImage(imagePath, config) {
47
47
  image = image.png({ compressionLevel: 9 });
48
48
  } else if (outputFormat === 'webp') {
49
49
  image = image.webp({ quality: 80 });
50
+ } else if (outputFormat === 'gif') {
51
+ image = image.gif();
52
+ } else if (outputFormat === 'tiff') {
53
+ image = image.tiff();
50
54
  } else {
51
- image = image[outputFormat]({ quality: 80 });
55
+ // Default to jpeg for unknown formats
56
+ image = image.jpeg({ quality: 80, progressive: true });
52
57
  }
53
58
 
54
59
  // Write to temporary file
@@ -1,140 +1,113 @@
1
- const ffmpeg = require('fluent-ffmpeg');
2
1
  const fs = require('fs');
2
+ const sharp = require('sharp');
3
3
  const path = require('path');
4
4
 
5
5
  /**
6
- * Process a single video file
7
- * @param {string} videoPath - Path to the video
6
+ * Process a single image file
7
+ * @param {string} imagePath - Path to the image
8
8
  * @param {object} config - Configuration object
9
9
  * @returns {Promise<{success: boolean, originalSize: number, optimizedSize: number, message: string}>}
10
10
  */
11
- async function processVideo(videoPath, config) {
12
- return new Promise((resolve) => {
13
- try {
14
- fs.stat(videoPath, async (err, stat) => {
15
- if (err) {
16
- return resolve({
17
- success: false,
18
- originalSize: 0,
19
- optimizedSize: 0,
20
- message: `✗ ${path.basename(videoPath)} - Cannot access file`,
21
- error: true
22
- });
23
- }
11
+ async function processImage(imagePath, config) {
12
+ try {
13
+ const stat = await fs.promises.stat(imagePath);
14
+ const originalSize = stat.size;
15
+ const tmpPath = imagePath + '.tmp';
16
+ const ext = path.extname(imagePath).toLowerCase();
17
+ const directory = path.dirname(imagePath);
18
+ const filename = path.basename(imagePath, ext);
24
19
 
25
- const originalSize = stat.size;
26
- const tmpPath = videoPath + '.tmp.mp4';
27
- const ext = path.extname(videoPath).toLowerCase();
28
- const directory = path.dirname(videoPath);
29
- const filename = path.basename(videoPath, ext);
30
- const finalPath = path.join(directory, filename + '.mp4');
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);
31
24
 
32
- // Create ffmpeg command
33
- // Map codec names to ffmpeg codec names
34
- const codecMap = {
35
- 'h264': 'libx264',
36
- 'h265': 'libx265',
37
- 'vp8': 'libvpx',
38
- 'vp9': 'libvpx-vp9'
39
- };
40
-
41
- const outputCodec = codecMap[config.video_encode.toLowerCase()] || config.video_encode || 'libx264';
42
-
43
- const command = ffmpeg(videoPath)
44
- .videoCodec(outputCodec)
45
- .size(`${config.video_max_width}x?`)
46
- .outputOptions([
47
- '-preset fast',
48
- '-crf 28',
49
- '-c:a aac' // Use AAC for audio (widely compatible)
50
- ])
51
- .output(tmpPath);
25
+ // Check if format conversion is needed
26
+ const needsFormatConversion = ext !== outputExt && ext !== (outputFormat === 'jpg' ? '.jpeg' : outputExt);
52
27
 
53
- command
54
- .on('end', async () => {
55
- try {
56
- const tmpStat = await new Promise((res, rej) => {
57
- fs.stat(tmpPath, (err, stat) => {
58
- if (err) rej(err);
59
- else res(stat);
60
- });
61
- });
28
+ // Read and process image
29
+ let image = sharp(imagePath);
62
30
 
63
- const optimizedSize = tmpStat.size;
31
+ // Get image metadata
32
+ const metadata = await image.metadata();
64
33
 
65
- if (optimizedSize < originalSize) {
66
- // Remove original and rename temp to final
67
- await fs.promises.unlink(videoPath);
68
- await fs.promises.rename(tmpPath, finalPath);
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
39
+ });
40
+ wasResized = true;
41
+ }
42
+
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
+ }
69
58
 
70
- resolve({
71
- success: true,
72
- originalSize,
73
- optimizedSize,
74
- message: `✓ ${path.basename(videoPath)} → ${path.basename(finalPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
75
- filePath: finalPath
76
- });
77
- } else {
78
- // Remove temp file if not smaller
79
- await fs.promises.unlink(tmpPath);
59
+ // Write to temporary file
60
+ await image.toFile(tmpPath);
80
61
 
81
- resolve({
82
- success: false,
83
- originalSize,
84
- optimizedSize,
85
- message: `○ ${path.basename(videoPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
86
- filePath: videoPath
87
- });
88
- }
89
- } catch (error) {
90
- resolve({
91
- success: false,
92
- originalSize,
93
- optimizedSize: 0,
94
- message: `✗ ${path.basename(videoPath)} - Post-processing error: ${error.message}`,
95
- error: true
96
- });
97
- }
98
- })
99
- .on('error', (err) => {
100
- // Clean up temp file on error
101
- fs.unlink(tmpPath, () => {
102
- let errorMsg = err.message;
103
-
104
- // Provide helpful suggestions for common errors
105
- if (err.message.includes('Unknown encoder') || err.message.includes('Encoder')) {
106
- errorMsg = `Video codec error. Check available codecs with: ffmpeg -encoders | grep lib`;
107
- } else if (err.message.includes('is not available')) {
108
- errorMsg = `Codec not available. Verify FFmpeg installation: ffmpeg -version`;
109
- } else if (err.message.includes('ENOENT') || err.message.includes('ffmpeg')) {
110
- errorMsg = `FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt-get install ffmpeg (Linux)`;
111
- } else if (err.message.includes('Conversion failed') || err.message.includes('exited with code')) {
112
- errorMsg = `Video encoding failed. Try a different codec in config: video_encode=h265 or video_encode=vp8`;
113
- }
114
-
115
- resolve({
116
- success: false,
117
- originalSize,
118
- optimizedSize: 0,
119
- message: `✗ ${path.basename(videoPath)} - ${errorMsg}`,
120
- error: true
121
- });
122
- });
123
- })
124
- .run();
125
- });
126
- } catch (error) {
127
- resolve({
62
+ // Check if optimized file is smaller
63
+ const tmpStat = await fs.promises.stat(tmpPath);
64
+ const optimizedSize = tmpStat.size;
65
+
66
+ // Decide whether to replace: if smaller OR if format conversion needed OR if resized
67
+ const shouldReplace = optimizedSize < originalSize || needsFormatConversion || wasResized;
68
+
69
+ 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);
75
+
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)`;
80
+
81
+ return {
82
+ success: true,
83
+ originalSize,
84
+ optimizedSize,
85
+ message: `✓ ${action} ${sizeInfo}`,
86
+ filePath: finalPath
87
+ };
88
+ } else {
89
+ // Remove temp file if not replacing
90
+ await fs.promises.unlink(tmpPath);
91
+
92
+ return {
128
93
  success: false,
129
- originalSize: 0,
130
- optimizedSize: 0,
131
- message: `✗ ${path.basename(videoPath)} - Error: ${error.message}`,
132
- error: true
133
- });
94
+ originalSize,
95
+ optimizedSize,
96
+ message: `○ ${path.basename(imagePath)} - Already optimized (${(originalSize / 1024).toFixed(1)}KB)`,
97
+ filePath: imagePath
98
+ };
134
99
  }
135
- });
100
+ } catch (error) {
101
+ return {
102
+ success: false,
103
+ originalSize: 0,
104
+ optimizedSize: 0,
105
+ message: `✗ ${path.basename(imagePath)} - Error: ${error.message}`,
106
+ error: true
107
+ };
108
+ }
136
109
  }
137
110
 
138
111
  module.exports = {
139
- processVideo
112
+ processImage
140
113
  };