Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 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 | 6x 6x 6x 6x 6x 24x 1x 23x 1x 22x 22x 21x 1x 20x 1x 19x 1x 18x 2x 16x 16x 16x 2x 2x 14x 9x 5x 3x 2x 2x 16x 16x 16x 1x 16x 16x 2x 2x 16x 14x 14x 6x | /**
* Image compression utility using sharp.
* Compresses images before R2 storage. Lossless/near-lossless by format.
* Strips EXIF metadata, auto-rotates, preserves dimensions and transparency.
*/
const sharp = require('sharp');
const log = require('./log');
const MAX_DIMENSION = 4096;
const MIN_DIMENSION = 100;
const COMPRESSION_TIMEOUT_MS = 10000;
async function compressImage(buffer, mimeType) {
if (!buffer || buffer.length === 0) {
throw new Error('Empty image buffer');
}
if (mimeType === 'image/svg+xml') {
throw new Error('SVG format not supported — XSS risk');
}
const originalSize = buffer.length;
const metadata = await sharp(buffer).metadata();
if (metadata.width > MAX_DIMENSION || metadata.height > MAX_DIMENSION) {
throw new Error(
`Image dimensions ${metadata.width}x${metadata.height} exceed maximum ${MAX_DIMENSION}x${MAX_DIMENSION}`,
);
}
if (metadata.width < MIN_DIMENSION || metadata.height < MIN_DIMENSION) {
throw new Error(
`Image dimensions ${metadata.width}x${metadata.height} below minimum ${MIN_DIMENSION}x${MIN_DIMENSION}`,
);
}
// Animated images: pass through (can't optimise without losing frames)
if (metadata.pages && metadata.pages > 1) {
return { buffer, mimeType, originalSize, compressedSize: originalSize };
}
// GIF passthrough (after dimension validation)
if (mimeType === 'image/gif') {
return { buffer, mimeType, originalSize, compressedSize: originalSize };
}
let pipeline = sharp(buffer, { failOn: 'error' }).rotate();
let outputMime = mimeType;
if (mimeType === 'image/heic' || mimeType === 'image/heif') {
pipeline = pipeline.jpeg({ quality: 95, mozjpeg: true });
outputMime = 'image/jpeg';
} else if (mimeType === 'image/jpeg') {
pipeline = pipeline.jpeg({ quality: 95, mozjpeg: true });
} else if (mimeType === 'image/png') {
pipeline = pipeline.png({ effort: 10, compressionLevel: 9, depth: 8 });
} else if (mimeType === 'image/webp') {
pipeline = pipeline.webp({ quality: 95, nearLossless: true });
} else E{
log.warn('imageCompressor', 'Unsupported MIME type, returning original', {
mimeType,
originalSize,
});
return { buffer, mimeType, originalSize, compressedSize: originalSize };
}
// Do NOT call withMetadata() — sharp strips EXIF by default.
pipeline = pipeline.toColorspace('srgb');
let timer;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error('Image compression timed out')),
COMPRESSION_TIMEOUT_MS,
);
});
let compressed;
try {
compressed = await Promise.race([pipeline.toBuffer(), timeoutPromise]);
} catch (err) {
log.warn('imageCompressor', 'Compression failed, returning original', {
error: err.message,
format: outputMime,
originalSize,
});
return { buffer, mimeType, originalSize, compressedSize: originalSize };
} finally {
clearTimeout(timer);
}
log.info('imageCompressor', 'Image compressed', {
originalSize,
compressedSize: compressed.length,
format: outputMime,
ratio: `${((1 - compressed.length / originalSize) * 100).toFixed(1)}%`,
});
return {
buffer: compressed,
mimeType: outputMime,
originalSize,
compressedSize: compressed.length,
};
}
module.exports = { compressImage };
|