Spaces:
Running
Running
File size: 5,123 Bytes
bc5c9ee 970f7c4 bc5c9ee 970f7c4 bc5c9ee 970f7c4 bc5c9ee 970f7c4 bc5c9ee 970f7c4 bc5c9ee 970f7c4 bc5c9ee | 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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | const sharp = require('sharp');
const stream = require('stream');
/**
* PRODUCTION OPTIMIZATION:
* Force sharp to use 1 thread per image operation.
* This is the recommended setting for web servers handling many concurrent requests.
* It prevents thread starvation and memory fragmentation by letting Node's libuv
* thread pool (UV_THREADPOOL_SIZE) handle concurrency at the request level instead.
*/
sharp.concurrency(1);
class ImageProcessor {
/**
* Processes an image to CMYK TIFF format from a readable stream.
* Accepts PNG, JPEG, WebP, AVIF, and TIFF inputs.
* Outputs the highest-quality CMYK TIFF suitable for professional printing.
*
* @param {stream.Readable} input - The incoming stream of image data.
* @param {Object} options - Processing options including quality.
* @returns {Promise<stream.Readable>} - A readable stream of the processed CMYK TIFF image.
*/
processImage(input, options = {}) {
return new Promise((resolve, reject) => {
try {
const { quality = 95 } = options;
// Validate input stream
if (!input || typeof input.pipe !== 'function') {
throw new Error('Invalid input stream provided');
}
let sharpInstance = sharp();
// Handle Sharp initialization errors
sharpInstance.on('error', (error) => {
reject(new Error(`Image processing failed: ${error.message}`));
});
const compressionLevel = Math.max(1, Math.min(9, Math.round(parseInt(quality) / 100 * 9)));
/**
* CMYK conversion pipeline:
* - toColorspace('cmyk'): Converts from any input colorspace to CMYK.
* - compression: 'deflate' provides lossless PNG-style compression.
* - quality: 100 ensures no lossy degradation.
* - predictor: 'horizontal' improves deflate compression ratios on photographic data.
*
* NOTE: The input can be WebP (lossy or lossless). Sharp's libvips will decode
* WebP to full uncompressed pixel data before conversion, so there is ZERO quality
* loss from the transport format — the TIFF output quality depends only on the
* original render resolution (600 DPI), not the transport encoding.
*/
sharpInstance = sharpInstance
.toColorspace('cmyk')
.tiff({
compression: 'deflate',
quality: 100,
level: compressionLevel,
predictor: 'horizontal'
});
// Create transform stream for error handling
const transformStream = new stream.PassThrough();
// Handle stream errors
input.on('error', (error) => {
reject(new Error(`Input stream error: ${error.message}`));
});
transformStream.on('error', (error) => {
reject(new Error(`Processing stream error: ${error.message}`));
});
// Pipe through Sharp with error handling
const processingStream = input.pipe(sharpInstance).pipe(transformStream);
processingStream.on('error', (error) => {
reject(new Error(`Pipeline error: ${error.message}`));
});
resolve(processingStream);
} catch (error) {
reject(new Error(`Setup error: ${error.message}`));
}
});
}
/**
* Validates processing options
* @param {Object} options - Processing options
* @returns {Object} - Validated options
*/
validateOptions(options) {
const { quality } = options;
if (quality && isNaN(quality)) {
throw new Error('Quality must be a number');
}
const qualityValue = quality ? parseInt(quality) : 95;
if (qualityValue < 1 || qualityValue > 100) {
throw new Error('Quality must be between 1 and 100');
}
return { quality: qualityValue };
}
/**
* Validates the input content type.
* Now accepts image/webp for optimized frontend transfers.
* @param {string} contentType - Request content type
*/
validateContentType(contentType) {
if (!contentType) {
throw new Error('Content-Type header is required');
}
const supportedTypes = [
'image/jpeg',
'image/jpg',
'image/png',
'image/tiff',
'image/webp',
'image/avif'
];
if (!supportedTypes.some(type => contentType.startsWith(type))) {
throw new Error(`Unsupported image format. Supported: ${supportedTypes.join(', ')}`);
}
}
}
module.exports = new ImageProcessor(); |