Spaces:
Running
Running
| 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(); |