@mcpcn/mcp-image-compressor 1.0.1 → 1.0.3
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 +104 -76
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41,7 +41,16 @@ const sharp = __importStar(require("sharp"));
|
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
// 支持的图片格式
|
|
44
|
-
const SUPPORTED_FORMATS = [
|
|
44
|
+
const SUPPORTED_FORMATS = [
|
|
45
|
+
"jpeg",
|
|
46
|
+
"jpg",
|
|
47
|
+
"png",
|
|
48
|
+
"webp",
|
|
49
|
+
"gif",
|
|
50
|
+
"avif",
|
|
51
|
+
"tiff",
|
|
52
|
+
"bmp",
|
|
53
|
+
];
|
|
45
54
|
// 工具函数:检查文件是否是图片
|
|
46
55
|
function isImage(filePath) {
|
|
47
56
|
const ext = path.extname(filePath).toLowerCase().slice(1);
|
|
@@ -52,8 +61,8 @@ function getImagesFromDirectory(dirPath) {
|
|
|
52
61
|
try {
|
|
53
62
|
const files = fs.readdirSync(dirPath);
|
|
54
63
|
return files
|
|
55
|
-
.map(file => path.join(dirPath, file))
|
|
56
|
-
.filter(filePath => {
|
|
64
|
+
.map((file) => path.join(dirPath, file))
|
|
65
|
+
.filter((filePath) => {
|
|
57
66
|
const stat = fs.statSync(filePath);
|
|
58
67
|
return stat.isFile() && isImage(filePath);
|
|
59
68
|
});
|
|
@@ -88,28 +97,28 @@ function parseSize(sizeStr) {
|
|
|
88
97
|
const s = sizeStr.trim().toLowerCase();
|
|
89
98
|
const match = s.match(/^(\d+(?:\.\d+)?)(b|kb|k|mb|m|gb|g)?$/);
|
|
90
99
|
if (!match) {
|
|
91
|
-
throw new Error(
|
|
100
|
+
throw new Error("Invalid size format. Examples: 200k, 500kb, 1m, 1mb, 1g, 1024");
|
|
92
101
|
}
|
|
93
102
|
const value = parseFloat(match[1]);
|
|
94
|
-
const unit =
|
|
103
|
+
const unit = match[2] || "b";
|
|
95
104
|
if (value <= 0) {
|
|
96
|
-
throw new Error(
|
|
105
|
+
throw new Error("Size must be greater than 0");
|
|
97
106
|
}
|
|
98
107
|
let multiplier = 1;
|
|
99
108
|
switch (unit) {
|
|
100
|
-
case
|
|
109
|
+
case "b":
|
|
101
110
|
multiplier = 1;
|
|
102
111
|
break;
|
|
103
|
-
case
|
|
104
|
-
case
|
|
112
|
+
case "kb":
|
|
113
|
+
case "k":
|
|
105
114
|
multiplier = 1024;
|
|
106
115
|
break;
|
|
107
|
-
case
|
|
108
|
-
case
|
|
116
|
+
case "mb":
|
|
117
|
+
case "m":
|
|
109
118
|
multiplier = 1024 * 1024;
|
|
110
119
|
break;
|
|
111
|
-
case
|
|
112
|
-
case
|
|
120
|
+
case "gb":
|
|
121
|
+
case "g":
|
|
113
122
|
multiplier = 1024 * 1024 * 1024;
|
|
114
123
|
break;
|
|
115
124
|
default:
|
|
@@ -126,7 +135,7 @@ function parsePercent(percentStr) {
|
|
|
126
135
|
}
|
|
127
136
|
const percent = parseFloat(match[1]);
|
|
128
137
|
if (percent <= 0 || percent > 100) {
|
|
129
|
-
throw new Error(
|
|
138
|
+
throw new Error("Percent must be between 0 and 100");
|
|
130
139
|
}
|
|
131
140
|
return percent;
|
|
132
141
|
}
|
|
@@ -149,11 +158,12 @@ async function compressToSize(inputPaths, targetSize, overwrite = false, outputP
|
|
|
149
158
|
const images = processInputPaths(inputPaths);
|
|
150
159
|
const targetBytes = parseSize(targetSize);
|
|
151
160
|
const results = [];
|
|
161
|
+
const errors = [];
|
|
152
162
|
for (const imagePath of images) {
|
|
153
163
|
try {
|
|
154
|
-
const outputFilePath = overwrite
|
|
155
|
-
imagePath
|
|
156
|
-
generateOutputPath(imagePath, outputPath, `_${targetSize.toLowerCase()}`);
|
|
164
|
+
const outputFilePath = overwrite
|
|
165
|
+
? imagePath
|
|
166
|
+
: generateOutputPath(imagePath, outputPath, `_${targetSize.toLowerCase()}`);
|
|
157
167
|
const image = sharp.default(imagePath);
|
|
158
168
|
const metadata = await image.metadata();
|
|
159
169
|
const stats = fs.statSync(imagePath);
|
|
@@ -171,9 +181,7 @@ async function compressToSize(inputPaths, targetSize, overwrite = false, outputP
|
|
|
171
181
|
let bestBuffer = null;
|
|
172
182
|
while (left <= right) {
|
|
173
183
|
const quality = Math.floor((left + right) / 2);
|
|
174
|
-
const buffer = await image
|
|
175
|
-
.jpeg({ quality })
|
|
176
|
-
.toBuffer();
|
|
184
|
+
const buffer = await image.jpeg({ quality }).toBuffer();
|
|
177
185
|
if (buffer.length <= targetBytes) {
|
|
178
186
|
bestQuality = quality;
|
|
179
187
|
bestBuffer = buffer;
|
|
@@ -188,14 +196,16 @@ async function compressToSize(inputPaths, targetSize, overwrite = false, outputP
|
|
|
188
196
|
results.push(outputFilePath);
|
|
189
197
|
}
|
|
190
198
|
else {
|
|
191
|
-
throw new Error(
|
|
199
|
+
throw new Error("Could not achieve target size while maintaining acceptable quality");
|
|
192
200
|
}
|
|
193
201
|
}
|
|
194
202
|
catch (error) {
|
|
195
|
-
|
|
203
|
+
const errorMsg = `Error compressing ${imagePath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
204
|
+
console.error(errorMsg);
|
|
205
|
+
errors.push(errorMsg);
|
|
196
206
|
}
|
|
197
207
|
}
|
|
198
|
-
return results;
|
|
208
|
+
return { results, errors };
|
|
199
209
|
}
|
|
200
210
|
/**
|
|
201
211
|
* 图片压缩到原始大小的百分比
|
|
@@ -204,11 +214,12 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
|
|
|
204
214
|
const images = processInputPaths(inputPaths);
|
|
205
215
|
const targetPercent = parsePercent(percent);
|
|
206
216
|
const results = [];
|
|
217
|
+
const errors = [];
|
|
207
218
|
for (const imagePath of images) {
|
|
208
219
|
try {
|
|
209
|
-
const outputFilePath = overwrite
|
|
210
|
-
imagePath
|
|
211
|
-
generateOutputPath(imagePath, outputPath, `_${targetPercent}%`);
|
|
220
|
+
const outputFilePath = overwrite
|
|
221
|
+
? imagePath
|
|
222
|
+
: generateOutputPath(imagePath, outputPath, `_${targetPercent}%`);
|
|
212
223
|
const stats = fs.statSync(imagePath);
|
|
213
224
|
const targetBytes = Math.floor(stats.size * (targetPercent / 100));
|
|
214
225
|
const image = sharp.default(imagePath);
|
|
@@ -220,9 +231,7 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
|
|
|
220
231
|
let bestBuffer = null;
|
|
221
232
|
while (left <= right) {
|
|
222
233
|
const quality = Math.floor((left + right) / 2);
|
|
223
|
-
const buffer = await image
|
|
224
|
-
.jpeg({ quality })
|
|
225
|
-
.toBuffer();
|
|
234
|
+
const buffer = await image.jpeg({ quality }).toBuffer();
|
|
226
235
|
if (buffer.length <= targetBytes) {
|
|
227
236
|
bestQuality = quality;
|
|
228
237
|
bestBuffer = buffer;
|
|
@@ -237,14 +246,16 @@ async function compressToPercent(inputPaths, percent, overwrite = false, outputP
|
|
|
237
246
|
results.push(outputFilePath);
|
|
238
247
|
}
|
|
239
248
|
else {
|
|
240
|
-
throw new Error(
|
|
249
|
+
throw new Error("Could not achieve target size while maintaining acceptable quality");
|
|
241
250
|
}
|
|
242
251
|
}
|
|
243
252
|
catch (error) {
|
|
244
|
-
|
|
253
|
+
const errorMsg = `Error compressing ${imagePath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
254
|
+
console.error(errorMsg);
|
|
255
|
+
errors.push(errorMsg);
|
|
245
256
|
}
|
|
246
257
|
}
|
|
247
|
-
return results;
|
|
258
|
+
return { results, errors };
|
|
248
259
|
}
|
|
249
260
|
const COMPRESS_TO_SIZE_TOOL = {
|
|
250
261
|
name: "image_compress_to_size",
|
|
@@ -255,23 +266,23 @@ const COMPRESS_TO_SIZE_TOOL = {
|
|
|
255
266
|
inputPaths: {
|
|
256
267
|
type: "array",
|
|
257
268
|
items: { type: "string" },
|
|
258
|
-
description: "原图片路径数组,支持图片文件和目录"
|
|
269
|
+
description: "原图片路径数组,支持图片文件和目录",
|
|
259
270
|
},
|
|
260
271
|
size: {
|
|
261
272
|
type: "string",
|
|
262
|
-
description: "要压缩到的的目标大小(如1mb/500kb)"
|
|
273
|
+
description: "要压缩到的的目标大小(如1mb/500kb)",
|
|
263
274
|
},
|
|
264
275
|
overwrite: {
|
|
265
276
|
type: "boolean",
|
|
266
|
-
description: "是否覆盖原图(可选,默认为false)"
|
|
277
|
+
description: "是否覆盖原图(可选,默认为false)",
|
|
267
278
|
},
|
|
268
279
|
outputPath: {
|
|
269
280
|
type: "string",
|
|
270
|
-
description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)"
|
|
271
|
-
}
|
|
281
|
+
description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)",
|
|
282
|
+
},
|
|
272
283
|
},
|
|
273
|
-
required: ["inputPaths", "size"]
|
|
274
|
-
}
|
|
284
|
+
required: ["inputPaths", "size"],
|
|
285
|
+
},
|
|
275
286
|
};
|
|
276
287
|
const COMPRESS_TO_PERCENT_TOOL = {
|
|
277
288
|
name: "image_compress_to_percent",
|
|
@@ -282,41 +293,45 @@ const COMPRESS_TO_PERCENT_TOOL = {
|
|
|
282
293
|
inputPaths: {
|
|
283
294
|
type: "array",
|
|
284
295
|
items: { type: "string" },
|
|
285
|
-
description: "原图片路径数组,支持图片文件和目录"
|
|
296
|
+
description: "原图片路径数组,支持图片文件和目录",
|
|
286
297
|
},
|
|
287
298
|
percent: {
|
|
288
299
|
type: "string",
|
|
289
|
-
description: "要压缩到原size的百分之多少(如50或50%)"
|
|
300
|
+
description: "要压缩到原size的百分之多少(如50或50%)",
|
|
290
301
|
},
|
|
291
302
|
overwrite: {
|
|
292
303
|
type: "boolean",
|
|
293
|
-
description: "是否覆盖原图(可选,默认为false)"
|
|
304
|
+
description: "是否覆盖原图(可选,默认为false)",
|
|
294
305
|
},
|
|
295
306
|
outputPath: {
|
|
296
307
|
type: "string",
|
|
297
|
-
description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)"
|
|
298
|
-
}
|
|
308
|
+
description: "存储路径(可选,不覆盖原图时如果没传则为原图所在目录)",
|
|
309
|
+
},
|
|
299
310
|
},
|
|
300
|
-
required: ["inputPaths", "percent"]
|
|
301
|
-
}
|
|
311
|
+
required: ["inputPaths", "percent"],
|
|
312
|
+
},
|
|
302
313
|
};
|
|
303
|
-
const IMAGE_TOOLS = [
|
|
304
|
-
COMPRESS_TO_SIZE_TOOL,
|
|
305
|
-
COMPRESS_TO_PERCENT_TOOL,
|
|
306
|
-
];
|
|
314
|
+
const IMAGE_TOOLS = [COMPRESS_TO_SIZE_TOOL, COMPRESS_TO_PERCENT_TOOL];
|
|
307
315
|
async function handleCompressToSize(inputPaths, size, overwrite = false, outputPath) {
|
|
308
316
|
try {
|
|
309
|
-
const results = await compressToSize(inputPaths, size, overwrite, outputPath);
|
|
317
|
+
const { results, errors } = await compressToSize(inputPaths, size, overwrite, outputPath);
|
|
318
|
+
const hasErrors = errors.length > 0;
|
|
319
|
+
const totalProcessed = results.length + errors.length;
|
|
310
320
|
return {
|
|
311
|
-
content: [
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
312
323
|
type: "text",
|
|
313
324
|
text: JSON.stringify({
|
|
314
|
-
success:
|
|
315
|
-
message:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
325
|
+
success: !hasErrors,
|
|
326
|
+
message: hasErrors
|
|
327
|
+
? `处理了 ${totalProcessed} 张图片,成功压缩 ${results.length} 张,失败 ${errors.length} 张到目标大小 ${size}`
|
|
328
|
+
: `成功压缩了 ${results.length} 张图片到目标大小 ${size}`,
|
|
329
|
+
compressedFiles: results,
|
|
330
|
+
errors: hasErrors ? errors : undefined,
|
|
331
|
+
}, null, 2),
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
isError: hasErrors,
|
|
320
335
|
};
|
|
321
336
|
}
|
|
322
337
|
catch (error) {
|
|
@@ -325,17 +340,24 @@ async function handleCompressToSize(inputPaths, size, overwrite = false, outputP
|
|
|
325
340
|
}
|
|
326
341
|
async function handleCompressToPercent(inputPaths, percent, overwrite = false, outputPath) {
|
|
327
342
|
try {
|
|
328
|
-
const results = await compressToPercent(inputPaths, percent, overwrite, outputPath);
|
|
343
|
+
const { results, errors } = await compressToPercent(inputPaths, percent, overwrite, outputPath);
|
|
344
|
+
const hasErrors = errors.length > 0;
|
|
345
|
+
const totalProcessed = results.length + errors.length;
|
|
329
346
|
return {
|
|
330
|
-
content: [
|
|
347
|
+
content: [
|
|
348
|
+
{
|
|
331
349
|
type: "text",
|
|
332
350
|
text: JSON.stringify({
|
|
333
|
-
success:
|
|
334
|
-
message:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
351
|
+
success: !hasErrors,
|
|
352
|
+
message: hasErrors
|
|
353
|
+
? `处理了 ${totalProcessed} 张图片,成功压缩 ${results.length} 张,失败 ${errors.length} 张到原始大小的 ${parsePercent(percent)}%`
|
|
354
|
+
: `成功压缩了 ${results.length} 张图片到原始大小的 ${parsePercent(percent)}%`,
|
|
355
|
+
compressedFiles: results,
|
|
356
|
+
errors: hasErrors ? errors : undefined,
|
|
357
|
+
}, null, 2),
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
isError: hasErrors,
|
|
339
361
|
};
|
|
340
362
|
}
|
|
341
363
|
catch (error) {
|
|
@@ -359,30 +381,36 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
359
381
|
try {
|
|
360
382
|
switch (request.params.name) {
|
|
361
383
|
case "image_compress_to_size": {
|
|
362
|
-
const { inputPaths, size, overwrite, outputPath } = request.params
|
|
384
|
+
const { inputPaths, size, overwrite, outputPath } = request.params
|
|
385
|
+
.arguments;
|
|
363
386
|
return await handleCompressToSize(inputPaths, size, overwrite, outputPath);
|
|
364
387
|
}
|
|
365
388
|
case "image_compress_to_percent": {
|
|
366
|
-
const { inputPaths, percent, overwrite, outputPath } = request.params
|
|
389
|
+
const { inputPaths, percent, overwrite, outputPath } = request.params
|
|
390
|
+
.arguments;
|
|
367
391
|
return await handleCompressToPercent(inputPaths, percent, overwrite, outputPath);
|
|
368
392
|
}
|
|
369
393
|
default:
|
|
370
394
|
return {
|
|
371
|
-
content: [
|
|
395
|
+
content: [
|
|
396
|
+
{
|
|
372
397
|
type: "text",
|
|
373
|
-
text: `未知工具: ${request.params.name}
|
|
374
|
-
}
|
|
375
|
-
|
|
398
|
+
text: `未知工具: ${request.params.name}`,
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
isError: true,
|
|
376
402
|
};
|
|
377
403
|
}
|
|
378
404
|
}
|
|
379
405
|
catch (error) {
|
|
380
406
|
return {
|
|
381
|
-
content: [
|
|
407
|
+
content: [
|
|
408
|
+
{
|
|
382
409
|
type: "text",
|
|
383
|
-
text: `错误: ${error instanceof Error ? error.message : String(error)}
|
|
384
|
-
}
|
|
385
|
-
|
|
410
|
+
text: `错误: ${error instanceof Error ? error.message : String(error)}`,
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
isError: true,
|
|
386
414
|
};
|
|
387
415
|
}
|
|
388
416
|
});
|