image-video-optimizer 3.0.3 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/videoProcessor.js +93 -121
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-video-optimizer",
3
- "version": "3.0.3",
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": {
@@ -1,141 +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
- ])
50
- .output(tmpPath);
25
+ // Check if format conversion is needed
26
+ const needsFormatConversion = ext !== outputExt && ext !== (outputFormat === 'jpg' ? '.jpeg' : outputExt);
51
27
 
52
- command
53
- .on('end', async () => {
54
- try {
55
- const tmpStat = await new Promise((res, rej) => {
56
- fs.stat(tmpPath, (err, stat) => {
57
- if (err) rej(err);
58
- else res(stat);
59
- });
60
- });
28
+ // Read and process image
29
+ let image = sharp(imagePath);
61
30
 
62
- const optimizedSize = tmpStat.size;
31
+ // Get image metadata
32
+ const metadata = await image.metadata();
63
33
 
64
- if (optimizedSize < originalSize) {
65
- // Remove original and rename temp to final
66
- await fs.promises.unlink(videoPath);
67
- 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
+ }
68
58
 
69
- resolve({
70
- success: true,
71
- originalSize,
72
- optimizedSize,
73
- message: `✓ ${path.basename(videoPath)} → ${path.basename(finalPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
74
- filePath: finalPath
75
- });
76
- } else {
77
- // Remove temp file if not smaller
78
- await fs.promises.unlink(tmpPath);
59
+ // Write to temporary file
60
+ await image.toFile(tmpPath);
79
61
 
80
- resolve({
81
- success: false,
82
- originalSize,
83
- optimizedSize,
84
- message: `○ ${path.basename(videoPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
85
- filePath: videoPath
86
- });
87
- }
88
- } catch (error) {
89
- resolve({
90
- success: false,
91
- originalSize,
92
- optimizedSize: 0,
93
- message: `✗ ${path.basename(videoPath)} - Post-processing error: ${error.message}`,
94
- error: true
95
- });
96
- }
97
- })
98
- .on('error', (err) => {
99
- // Clean up temp file on error
100
- fs.unlink(tmpPath, () => {
101
- let errorMsg = err.message;
102
-
103
- // Provide helpful suggestions for common errors
104
- if (err.message.includes('Unknown encoder') || err.message.includes('Encoder (')) {
105
- errorMsg = `Codec "${outputCodec}" not supported. Try: video_encode=h264`;
106
- } else if (err.message.includes('No such file') || err.message.includes('does not exist')) {
107
- errorMsg = `Input file not found or cannot be read`;
108
- } else if (err.message.includes('ENOENT') || err.message.includes('ffmpeg')) {
109
- errorMsg = `FFmpeg not found. Install with: brew install ffmpeg`;
110
- } else if (err.message.includes('Conversion failed') || err.message.includes('exited with code')) {
111
- errorMsg = `Video encoding failed. Check file format and try different codec`;
112
- } else {
113
- errorMsg = `Encoding error: ${err.message.substring(0, 80)}`;
114
- }
115
-
116
- resolve({
117
- success: false,
118
- originalSize,
119
- optimizedSize: 0,
120
- message: `✗ ${path.basename(videoPath)} - ${errorMsg}`,
121
- error: true
122
- });
123
- });
124
- })
125
- .run();
126
- });
127
- } catch (error) {
128
- 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 {
129
93
  success: false,
130
- originalSize: 0,
131
- optimizedSize: 0,
132
- message: `✗ ${path.basename(videoPath)} - Error: ${error.message}`,
133
- error: true
134
- });
94
+ originalSize,
95
+ optimizedSize,
96
+ message: `○ ${path.basename(imagePath)} - Already optimized (${(originalSize / 1024).toFixed(1)}KB)`,
97
+ filePath: imagePath
98
+ };
135
99
  }
136
- });
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
+ }
137
109
  }
138
110
 
139
111
  module.exports = {
140
- processVideo
112
+ processImage
141
113
  };