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 +1 -1
- package/src/imageProcessor.js +6 -1
- package/src/videoProcessor.js +93 -120
package/package.json
CHANGED
package/src/imageProcessor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/videoProcessor.js
CHANGED
|
@@ -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
|
|
7
|
-
* @param {string}
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
31
|
+
// Get image metadata
|
|
32
|
+
const metadata = await image.metadata();
|
|
64
33
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
130
|
-
optimizedSize
|
|
131
|
-
message:
|
|
132
|
-
|
|
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
|
-
|
|
112
|
+
processImage
|
|
140
113
|
};
|