const sharp = require('sharp'); const stream = require('stream'); class ImageProcessor { /** * Processes an image to CMYK TIFF format from a readable stream. * @param {stream.Readable} input - The incoming stream of image data. * @param {Object} options - Processing options including quality. * @returns {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))); // Apply CMYK conversion with TIFF compression sharpInstance = sharpInstance .toColorspace('cmyk') .tiff({ compression: 'deflate', // CHANGED: Use PNG-style lossless compression quality: 100, // This is now ignored, but we set to 100 level: compressionLevel, // NEW: Use our mapped level 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; // Validate quality exists and is valid 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 * @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();