@mcpcn/mcp-image-compressor 1.0.3 → 1.0.5
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/dist/index.js +127 -31
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -146,7 +146,9 @@ function generateOutputPath(inputPath, outputPath, suffix, format) {
|
|
|
146
146
|
const newName = `${parsedPath.name}${suffix}${newExt}`;
|
|
147
147
|
if (outputPath) {
|
|
148
148
|
// 确保输出目录存在
|
|
149
|
-
fs.
|
|
149
|
+
if (!fs.existsSync(outputPath)) {
|
|
150
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
151
|
+
}
|
|
150
152
|
return path.join(outputPath, newName);
|
|
151
153
|
}
|
|
152
154
|
return path.join(parsedPath.dir, newName);
|
|
@@ -161,29 +163,45 @@ async function compressToSize(inputPaths, targetSize, overwrite = false, outputP
|
|
|
161
163
|
const errors = [];
|
|
162
164
|
for (const imagePath of images) {
|
|
163
165
|
try {
|
|
164
|
-
|
|
166
|
+
let outputFilePath = overwrite
|
|
165
167
|
? imagePath
|
|
166
168
|
: generateOutputPath(imagePath, outputPath, `_${targetSize.toLowerCase()}`);
|
|
167
|
-
const image = sharp.default(imagePath);
|
|
168
|
-
const metadata = await image.metadata();
|
|
169
169
|
const stats = fs.statSync(imagePath);
|
|
170
|
-
|
|
170
|
+
const originalSize = stats.size;
|
|
171
|
+
if (originalSize <= targetBytes) {
|
|
171
172
|
if (!overwrite) {
|
|
172
173
|
await fs.promises.copyFile(imagePath, outputFilePath);
|
|
173
174
|
}
|
|
174
175
|
results.push(outputFilePath);
|
|
175
176
|
continue;
|
|
176
177
|
}
|
|
178
|
+
const ext = path.extname(imagePath).toLowerCase().slice(1);
|
|
179
|
+
const encodeWithQuality = async (q) => {
|
|
180
|
+
const img = sharp.default(imagePath);
|
|
181
|
+
if (ext === "jpg" || ext === "jpeg") {
|
|
182
|
+
return img.jpeg({ quality: q, mozjpeg: true }).toBuffer();
|
|
183
|
+
}
|
|
184
|
+
if (ext === "png") {
|
|
185
|
+
const clamped = Math.max(1, Math.min(q, 100));
|
|
186
|
+
return img.png({ quality: clamped, compressionLevel: 9, palette: true }).toBuffer();
|
|
187
|
+
}
|
|
188
|
+
if (ext === "webp") {
|
|
189
|
+
return img.webp({ quality: q }).toBuffer();
|
|
190
|
+
}
|
|
191
|
+
if (ext === "avif") {
|
|
192
|
+
return img.avif({ quality: q }).toBuffer();
|
|
193
|
+
}
|
|
194
|
+
// 其他统一转jpeg
|
|
195
|
+
return img.jpeg({ quality: q, mozjpeg: true }).toBuffer();
|
|
196
|
+
};
|
|
177
197
|
// 使用二分查找找到合适的质量值
|
|
178
|
-
let left =
|
|
179
|
-
let right =
|
|
180
|
-
let bestQuality = 100;
|
|
198
|
+
let left = 5;
|
|
199
|
+
let right = 95;
|
|
181
200
|
let bestBuffer = null;
|
|
182
201
|
while (left <= right) {
|
|
183
202
|
const quality = Math.floor((left + right) / 2);
|
|
184
|
-
const buffer = await
|
|
203
|
+
const buffer = await encodeWithQuality(quality);
|
|
185
204
|
if (buffer.length <= targetBytes) {
|
|
186
|
-
bestQuality = quality;
|
|
187
205
|
bestBuffer = buffer;
|
|
188
206
|
left = quality + 1;
|
|
189
207
|
}
|
|
@@ -191,13 +209,61 @@ async function compressToSize(inputPaths, targetSize, overwrite = false, outputP
|
|
|
191
209
|
right = quality - 1;
|
|
192
210
|
}
|
|
193
211
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
let outputBuffer = bestBuffer;
|
|
213
|
+
let outputExt = path.extname(outputFilePath).toLowerCase();
|
|
214
|
+
if (!outputBuffer) {
|
|
215
|
+
const fallback = await encodeWithQuality(Math.max(10, right));
|
|
216
|
+
if (fallback.length < originalSize) {
|
|
217
|
+
outputBuffer = fallback;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// PNG等难以降体积时,尝试有损WebP再二分
|
|
221
|
+
const tryWebp = async () => {
|
|
222
|
+
let l = 5, r = 95;
|
|
223
|
+
let buf = null;
|
|
224
|
+
while (l <= r) {
|
|
225
|
+
const q = Math.floor((l + r) / 2);
|
|
226
|
+
const b = await sharp.default(imagePath).webp({ quality: q }).toBuffer();
|
|
227
|
+
if (b.length <= targetBytes) {
|
|
228
|
+
buf = b;
|
|
229
|
+
l = q + 1;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
r = q - 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// 兜底:若仍未达到,但比原图小,也接受
|
|
236
|
+
if (!buf) {
|
|
237
|
+
const b = await sharp.default(imagePath).webp({ quality: Math.max(10, r) }).toBuffer();
|
|
238
|
+
if (b.length < originalSize)
|
|
239
|
+
return b;
|
|
240
|
+
}
|
|
241
|
+
return buf;
|
|
242
|
+
};
|
|
243
|
+
const webpBuf = await tryWebp();
|
|
244
|
+
if (webpBuf) {
|
|
245
|
+
outputBuffer = webpBuf;
|
|
246
|
+
// 将输出后缀改为 .webp
|
|
247
|
+
if (!overwrite) {
|
|
248
|
+
const parsed = path.parse(outputFilePath);
|
|
249
|
+
outputFilePath = path.join(parsed.dir, `${parsed.name}.webp`);
|
|
250
|
+
outputExt = '.webp';
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
outputExt = '.webp';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
if (!overwrite) {
|
|
258
|
+
await fs.promises.copyFile(imagePath, outputFilePath);
|
|
259
|
+
}
|
|
260
|
+
results.push(outputFilePath);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
200
264
|
}
|
|
265
|
+
await fs.promises.writeFile(outputFilePath, outputBuffer);
|
|
266
|
+
results.push(outputFilePath);
|
|
201
267
|
}
|
|
202
268
|
catch (error) {
|
|
203
269
|
const errorMsg = `Error compressing ${imagePath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -221,19 +287,40 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
|
|
|
221
287
|
? imagePath
|
|
222
288
|
: generateOutputPath(imagePath, outputPath, `_${targetPercent}%`);
|
|
223
289
|
const stats = fs.statSync(imagePath);
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
290
|
+
const originalSize = stats.size;
|
|
291
|
+
if (targetPercent >= 100) {
|
|
292
|
+
if (!overwrite) {
|
|
293
|
+
await fs.promises.copyFile(imagePath, outputFilePath);
|
|
294
|
+
}
|
|
295
|
+
results.push(outputFilePath);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const targetBytes = Math.floor(originalSize * (targetPercent / 100));
|
|
299
|
+
const ext = path.extname(imagePath).toLowerCase().slice(1);
|
|
300
|
+
const encodeWithQuality = async (q) => {
|
|
301
|
+
const img = sharp.default(imagePath);
|
|
302
|
+
if (ext === "jpg" || ext === "jpeg") {
|
|
303
|
+
return img.jpeg({ quality: q, mozjpeg: true }).toBuffer();
|
|
304
|
+
}
|
|
305
|
+
if (ext === "png") {
|
|
306
|
+
const clamped = Math.max(1, Math.min(q, 100));
|
|
307
|
+
return img.png({ quality: clamped, compressionLevel: 9, palette: true }).toBuffer();
|
|
308
|
+
}
|
|
309
|
+
if (ext === "webp") {
|
|
310
|
+
return img.webp({ quality: q }).toBuffer();
|
|
311
|
+
}
|
|
312
|
+
if (ext === "avif") {
|
|
313
|
+
return img.avif({ quality: q }).toBuffer();
|
|
314
|
+
}
|
|
315
|
+
return img.jpeg({ quality: q, mozjpeg: true }).toBuffer();
|
|
316
|
+
};
|
|
317
|
+
let left = 5;
|
|
318
|
+
let right = 95;
|
|
231
319
|
let bestBuffer = null;
|
|
232
320
|
while (left <= right) {
|
|
233
321
|
const quality = Math.floor((left + right) / 2);
|
|
234
|
-
const buffer = await
|
|
322
|
+
const buffer = await encodeWithQuality(quality);
|
|
235
323
|
if (buffer.length <= targetBytes) {
|
|
236
|
-
bestQuality = quality;
|
|
237
324
|
bestBuffer = buffer;
|
|
238
325
|
left = quality + 1;
|
|
239
326
|
}
|
|
@@ -241,13 +328,22 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
|
|
|
241
328
|
right = quality - 1;
|
|
242
329
|
}
|
|
243
330
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
331
|
+
let outputBuffer = bestBuffer;
|
|
332
|
+
if (!outputBuffer) {
|
|
333
|
+
const fallback = await encodeWithQuality(Math.max(10, right));
|
|
334
|
+
if (fallback.length < originalSize) {
|
|
335
|
+
outputBuffer = fallback;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
if (!overwrite) {
|
|
339
|
+
await fs.promises.copyFile(imagePath, outputFilePath);
|
|
340
|
+
}
|
|
341
|
+
results.push(outputFilePath);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
250
344
|
}
|
|
345
|
+
await fs.promises.writeFile(outputFilePath, outputBuffer);
|
|
346
|
+
results.push(outputFilePath);
|
|
251
347
|
}
|
|
252
348
|
catch (error) {
|
|
253
349
|
const errorMsg = `Error compressing ${imagePath}: ${error instanceof Error ? error.message : String(error)}`;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcpcn/mcp-image-compressor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "MCP server for image compression",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"build": "tsc
|
|
7
|
+
"build": "tsc",
|
|
8
8
|
"start": "node dist/index.js",
|
|
9
9
|
"dev": "ts-node index.ts",
|
|
10
10
|
"serve": "node dist/index.js"
|