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.
- package/package.json +1 -1
- package/src/videoProcessor.js +93 -121
package/package.json
CHANGED
package/src/videoProcessor.js
CHANGED
|
@@ -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
|
|
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
|
-
])
|
|
50
|
-
.output(tmpPath);
|
|
25
|
+
// Check if format conversion is needed
|
|
26
|
+
const needsFormatConversion = ext !== outputExt && ext !== (outputFormat === 'jpg' ? '.jpeg' : outputExt);
|
|
51
27
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
31
|
+
// Get image metadata
|
|
32
|
+
const metadata = await image.metadata();
|
|
63
33
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
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
|
|
131
|
-
optimizedSize
|
|
132
|
-
message:
|
|
133
|
-
|
|
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
|
-
|
|
112
|
+
processImage
|
|
141
113
|
};
|