const sharp = require('sharp'); const fs = require('fs').promises; const path = require('path'); class ImageProcessor { constructor() { this.supportedFormats = ['png', 'jpg', 'jpeg']; this.maxImageSize = 10 * 1024 * 1024; // 10MB } // ================================================================ // MÉTODOS EXISTENTES (se já existirem no servidor) // ================================================================ async process(imageBuffer, options = {}) { let pipeline = sharp(imageBuffer); // Ajustar brilho if (options.brightness !== undefined) { pipeline = pipeline.modulate({ brightness: options.brightness }); } // Ajustar contraste if (options.contrast !== undefined) { pipeline = pipeline.modulate({ brightness: options.contrast }); } // Ajustar saturação if (options.saturation !== undefined) { pipeline = pipeline.modulate({ saturation: options.saturation }); } return await pipeline.png().toBuffer(); } // ================================================================ // NOVOS MÉTODOS - ROTAÇÃO E FLIP // ================================================================ async rotate(imageBuffer, degrees) { try { if (degrees === 0) return imageBuffer; const sharp = require('sharp'); return await sharp(imageBuffer) .rotate(degrees) .png() .toBuffer(); } catch (error) { console.error('Erro na rotação:', error); throw error; } } async flip(imageBuffer, horizontal, vertical) { try { const sharp = require('sharp'); let pipeline = sharp(imageBuffer); if (horizontal) { pipeline = pipeline.flop(); } if (vertical) { pipeline = pipeline.flip(); } return await pipeline.png().toBuffer(); } catch (error) { console.error('Erro no flip:', error); throw error; } } async rotateAndFlip(imageBuffer, options = {}) { try { let pipeline = sharp(imageBuffer); if (options.rotation && options.rotation !== 0) { pipeline = pipeline.rotate(options.rotation); } if (options.flipH) { pipeline = pipeline.flop(); } if (options.flipV) { pipeline = pipeline.flip(); } return await pipeline.png().toBuffer(); } catch (error) { console.error('Erro ao rotacionar e flipar:', error); throw error; } } // ================================================================ // MÉTODOS DE UTILIDADE // ================================================================ async getThumbnail(imageBuffer, width = 500, height = 500) { try { const sharp = require('sharp'); return await sharp(imageBuffer) .resize(width, height, { fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: 80, force: true }) .toBuffer(); } catch (error) { console.error('Erro ao gerar thumbnail:', error); throw error; } } async getBase64Preview(imageBuffer, maxSize = 300) { try { const thumbnail = await this.getThumbnail(imageBuffer, maxSize, maxSize); return `data:image/png;base64,${thumbnail.toString('base64')}`; } catch (error) { console.error('Erro ao gerar preview base64:', error); throw error; } } // Verifica magic bytes do buffer antes de invocar sharp — rejeita executáveis e arquivos arbitrários static checkMagicBytes(buffer) { if (!buffer || buffer.length < 4) return false; // JPEG: FF D8 FF if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF) return true; // PNG: 89 50 4E 47 if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47) return true; // GIF: GIF8 if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) return true; // WebP: RIFF....WEBP if (buffer.length >= 12 && buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46 && buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50) return true; // BMP: BM if (buffer[0] === 0x42 && buffer[1] === 0x4D) return true; return false; } async validateImage(imageBuffer) { try { // Verifica magic bytes antes de acionar o sharp — bloqueia não-imagens na entrada if (!ImageProcessor.checkMagicBytes(imageBuffer)) { throw new Error('Arquivo rejeitado: assinatura de bytes inválida (não é uma imagem)'); } const metadata = await sharp(imageBuffer).metadata(); if (!metadata.format || !this.supportedFormats.includes(metadata.format.toLowerCase())) { throw new Error(`Formato não suportado: ${metadata.format}`); } if (imageBuffer.length > this.maxImageSize) { throw new Error(`Imagem muito grande: ${(imageBuffer.length / 1024 / 1024).toFixed(2)}MB`); } return true; } catch (error) { console.error('Erro na validação:', error); throw error; } } async resize(imageBuffer, width, height, options = {}) { try { const sharp = require('sharp'); return await sharp(imageBuffer) .resize(width, height, { fit: options.fit || 'inside', withoutEnlargement: true }) .png() .toBuffer(); } catch (error) { console.error('Erro no resize:', error); throw error; } } async getImageMetadata(imageBuffer) { try { const sharp = require('sharp'); const metadata = await sharp(imageBuffer).metadata(); return { format: metadata.format, width: metadata.width, height: metadata.height, size: imageBuffer.length, channels: metadata.channels, hasAlpha: metadata.hasAlpha }; } catch (error) { console.error('Erro ao ler metadata:', error); throw error; } } } // Exportar instância singleton module.exports = new ImageProcessor();