contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
332 lines (321 loc) • 12.8 kB
JavaScript
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;
}
}
}