UNPKG

contaigents

Version:

Modular AI Content Ecosystem with Audio Generation

332 lines (321 loc) 12.8 kB
import { BaseTool } from './BaseTool.js'; import sharp from 'sharp'; import path from 'path'; import fs from 'fs/promises'; export class ImageManipulationTool extends BaseTool { constructor(baseDir) { super(); this.baseDir = baseDir || process.cwd(); } getName() { return 'image_manipulation'; } getDescription() { return `Professional image manipulation tool with Photoshop-like capabilities using Sharp library. 🎨 **CORE OPERATIONS:** - **Resize & Transform**: resize, crop, rotate, flip, scale - **Color & Tone**: brightness, contrast, saturation, hue, gamma, levels - **Filters & Effects**: sharpen, blur, negate, threshold, normalize - **Advanced**: custom kernels, color matrix, morphological operations 🔧 **SUPPORTED FORMATS:** - Input: JPEG, PNG, WebP, AVIF, TIFF, SVG, GIF - Output: JPEG, PNG, WebP, AVIF, TIFF ⚡ **PERFORMANCE:** - High-performance processing using libvips - Memory efficient for large images - Batch operations supported 🎯 **USAGE EXAMPLES:** - "Resize image.jpg to 800x600 and increase brightness by 20%" - "Crop photo.png to 500x500 from center and apply sharpen filter" - "Convert screenshot.png to grayscale and adjust contrast" - "Rotate artwork.jpg by 45 degrees with white background" 💡 **AGENT GUIDANCE:** This tool can handle most common image editing tasks that users might request. For advanced AI-powered operations (magic selection, content-aware fill), suggest using specialized AI tools or manual editing software.`; } getParameters() { return [ { name: 'input_path', type: 'string', description: 'Path to input image file (relative to project root)', required: true }, { name: 'output_path', type: 'string', description: 'Path for output image file (relative to project root)', required: true }, { name: 'operations', type: 'array', description: `Array of operations to apply in sequence (can be JSON string or array object). Available operations: **RESIZE & TRANSFORM:** - {"op": "resize", "width": 800, "height": 600, "fit": "cover|contain|fill|inside|outside"} - {"op": "crop", "left": 100, "top": 50, "width": 400, "height": 300} - {"op": "rotate", "angle": 90, "background": "#ffffff"} - {"op": "flip"} or {"op": "flop"} **COLOR & TONE:** - {"op": "modulate", "brightness": 1.2, "saturation": 0.8, "hue": 180} - {"op": "linear", "a": 1.2, "b": 10} // levels: a * input + b - {"op": "gamma", "gamma": 2.2} - {"op": "normalize"} // auto-levels **FILTERS & EFFECTS:** - {"op": "sharpen", "sigma": 1.0} - {"op": "blur", "sigma": 3.0} - {"op": "median", "size": 3} - {"op": "negate"} // invert colors - {"op": "threshold", "value": 128} **FORMAT & QUALITY:** - {"op": "format", "type": "jpeg|png|webp", "quality": 90} Example: [{"op": "resize", "width": 800}, {"op": "modulate", "brightness": 1.2}, {"op": "sharpen"}]`, required: true } ]; } validateImageManipulationParameters(parameters) { // Check required parameters if (!parameters.input_path || typeof parameters.input_path !== 'string') { return { success: false, error: 'Missing or invalid required parameter: input_path (must be string)' }; } if (!parameters.output_path || typeof parameters.output_path !== 'string') { return { success: false, error: 'Missing or invalid required parameter: output_path (must be string)' }; } if (!parameters.operations) { return { success: false, error: 'Missing required parameter: operations' }; } // Allow both string and array for operations if (typeof parameters.operations !== 'string' && !Array.isArray(parameters.operations)) { return { success: false, error: 'Parameter operations must be a JSON string or array' }; } return null; // No validation errors } async execute(parameters) { // Custom validation for this tool (allows both string and array for operations) const validationError = this.validateImageManipulationParameters(parameters); if (validationError) { return validationError; } const { input_path, output_path, operations } = parameters; try { // Parse operations - handle both string and array formats let ops; try { if (typeof operations === 'string') { ops = JSON.parse(operations); } else if (Array.isArray(operations)) { ops = operations; } else { throw new Error('Operations must be a JSON string or array'); } if (!Array.isArray(ops)) { throw new Error('Operations must be an array'); } } catch (error) { return { success: false, error: `Invalid operations format: ${error.message}` }; } // Resolve paths const inputPath = path.resolve(this.baseDir, input_path); const outputPath = path.resolve(this.baseDir, output_path); // Security check if (!inputPath.startsWith(path.resolve(this.baseDir))) { return { success: false, error: 'Input path is outside the allowed directory' }; } if (!outputPath.startsWith(path.resolve(this.baseDir))) { return { success: false, error: 'Output path is outside the allowed directory' }; } // Check if input file exists try { await fs.access(inputPath); } catch { return { success: false, error: `Input file not found: ${input_path}` }; } // Create output directory if needed const outputDir = path.dirname(outputPath); await fs.mkdir(outputDir, { recursive: true }); // Start Sharp pipeline let pipeline = sharp(inputPath); // Apply operations in sequence const appliedOps = []; for (const op of ops) { if (!op.op) { return { success: false, error: 'Each operation must have an "op" field' }; } try { pipeline = await this.applyOperation(pipeline, op); appliedOps.push(this.formatOperationDescription(op)); } catch (error) { return { success: false, error: `Error applying operation "${op.op}": ${error.message}` }; } } // Execute pipeline and save await pipeline.toFile(outputPath); // Get output info const outputInfo = await sharp(outputPath).metadata(); const stats = await fs.stat(outputPath); return { success: true, data: { input_path: input_path, output_path: output_path, operations_applied: appliedOps, input_size: `${outputInfo.width}x${outputInfo.height}`, output_size: `${outputInfo.width}x${outputInfo.height}`, output_format: outputInfo.format, file_size: `${(stats.size / 1024).toFixed(1)} KB`, processing_summary: `Applied ${appliedOps.length} operation(s): ${appliedOps.join(', ')}` } }; } catch (error) { return { success: false, error: `Image manipulation failed: ${error.message}` }; } } async applyOperation(pipeline, op) { switch (op.op) { case 'resize': return pipeline.resize({ width: op.width, height: op.height, fit: op.fit || 'cover', withoutEnlargement: op.withoutEnlargement || false }); case 'crop': return pipeline.extract({ left: op.left || 0, top: op.top || 0, width: op.width, height: op.height }); case 'rotate': return pipeline.rotate(op.angle || 90, { background: op.background || '#000000' }); case 'flip': return pipeline.flip(); case 'flop': return pipeline.flop(); case 'modulate': const modulateOptions = {}; if (op.brightness !== undefined) modulateOptions.brightness = op.brightness; if (op.saturation !== undefined) modulateOptions.saturation = op.saturation; if (op.hue !== undefined) modulateOptions.hue = op.hue; if (op.lightness !== undefined) modulateOptions.lightness = op.lightness; return pipeline.modulate(modulateOptions); case 'linear': return pipeline.linear(op.a || 1, op.b || 0); case 'gamma': return pipeline.gamma(op.gamma || 2.2, op.gammaOut); case 'normalize': return pipeline.normalize({ lower: op.lower || 1, upper: op.upper || 99 }); case 'sharpen': if (op.sigma) { return pipeline.sharpen({ sigma: op.sigma }); } return pipeline.sharpen(); case 'blur': if (op.sigma) { return pipeline.blur(op.sigma); } return pipeline.blur(); case 'median': return pipeline.median(op.size || 3); case 'negate': return pipeline.negate({ alpha: op.alpha !== false }); case 'threshold': return pipeline.threshold(op.value || 128, { greyscale: op.greyscale !== false }); case 'format': const formatOptions = {}; if (op.quality) formatOptions.quality = op.quality; switch (op.type) { case 'jpeg': return pipeline.jpeg(formatOptions); case 'png': return pipeline.png(formatOptions); case 'webp': return pipeline.webp(formatOptions); default: throw new Error(`Unsupported format: ${op.type}`); } default: throw new Error(`Unknown operation: ${op.op}`); } } formatOperationDescription(op) { switch (op.op) { case 'resize': return `resize to ${op.width || 'auto'}x${op.height || 'auto'}`; case 'crop': return `crop ${op.width}x${op.height} from (${op.left || 0}, ${op.top || 0})`; case 'rotate': return `rotate ${op.angle || 90}°`; case 'modulate': const parts = []; if (op.brightness) parts.push(`brightness ${op.brightness}x`); if (op.saturation) parts.push(`saturation ${op.saturation}x`); if (op.hue) parts.push(`hue ${op.hue}°`); return `adjust ${parts.join(', ')}`; case 'linear': return `levels (${op.a || 1}x + ${op.b || 0})`; case 'sharpen': return op.sigma ? `sharpen (σ=${op.sigma})` : 'sharpen'; case 'blur': return op.sigma ? `blur (σ=${op.sigma})` : 'blur'; default: return op.op; } } }