@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.
Files changed (2) hide show
  1. package/dist/index.js +127 -31
  2. 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.mkdirSync(outputPath, { recursive: true });
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
- const outputFilePath = overwrite
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
- if (stats.size <= targetBytes) {
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 = 1;
179
- let right = 100;
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 image.jpeg({ quality }).toBuffer();
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
- if (bestBuffer) {
195
- await fs.promises.writeFile(outputFilePath, bestBuffer);
196
- results.push(outputFilePath);
197
- }
198
- else {
199
- throw new Error("Could not achieve target size while maintaining acceptable quality");
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 targetBytes = Math.floor(stats.size * (targetPercent / 100));
225
- const image = sharp.default(imagePath);
226
- const metadata = await image.metadata();
227
- // 使用二分查找找到合适的质量值
228
- let left = 1;
229
- let right = 100;
230
- let bestQuality = 100;
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 image.jpeg({ quality }).toBuffer();
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
- if (bestBuffer) {
245
- await fs.promises.writeFile(outputFilePath, bestBuffer);
246
- results.push(outputFilePath);
247
- }
248
- else {
249
- throw new Error("Could not achieve target size while maintaining acceptable quality");
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",
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 && chmod +x dist/index.js",
7
+ "build": "tsc",
8
8
  "start": "node dist/index.js",
9
9
  "dev": "ts-node index.ts",
10
10
  "serve": "node dist/index.js"