/**
* Image Tools Page - Upload, filter, and download images
* Supports chaining multiple filters with dynamic parameter schemas
*/
const ImageToolsPage = () => {
const {
Box, Typography, Card, CardContent, Button,
Select, MenuItem, FormControl, InputLabel, Slider,
CircularProgress, Alert, Chip, Paper, Checkbox, FormControlLabel
} = MaterialUI;
// State
const [availableFilters, setAvailableFilters] = React.useState([]);
const [filtersLoading, setFiltersLoading] = React.useState(true);
const [selectedFile, setSelectedFile] = React.useState(null);
const [previewUrl, setPreviewUrl] = React.useState(null);
const [resultUrl, setResultUrl] = React.useState(null);
const [selectedFilter, setSelectedFilter] = React.useState('');
const [pendingParams, setPendingParams] = React.useState({});
const [filterChain, setFilterChain] = React.useState([]);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
// Fetch filter definitions from API on mount
React.useEffect(() => {
const fetchFilters = async () => {
try {
const response = await fetch('/image/filters');
if (!response.ok) throw new Error('Failed to fetch filters');
const filters = await response.json();
setAvailableFilters(filters);
if (filters.length > 0) {
setSelectedFilter(filters[0].id);
setPendingParams(getDefaultParams(filters[0]));
}
} catch (err) {
setError('Failed to load filters: ' + err.message);
} finally {
setFiltersLoading(false);
}
};
fetchFilters();
}, []);
// Get current filter config from dropdown
const currentFilterConfig = availableFilters.find(f => f.id === selectedFilter);
// Get default params for a filter
const getDefaultParams = (filterDef) => {
const defaults = {};
(filterDef?.parameters || []).forEach(p => {
defaults[p.id] = p.default;
});
return defaults;
};
// Check if a parameter should be visible based on showWhen condition
const isParamVisible = (param, currentParams) => {
if (!param.showWhen) return true;
const { param: dependsOn, values } = param.showWhen;
return values.includes(currentParams[dependsOn]);
};
// Handle filter selection change
const handleFilterChange = (newFilterId) => {
setSelectedFilter(newFilterId);
const filterDef = availableFilters.find(f => f.id === newFilterId);
setPendingParams(getDefaultParams(filterDef));
};
// Handle parameter value change
const handleParamChange = (paramId, value) => {
setPendingParams(prev => ({ ...prev, [paramId]: value }));
};
// Generate unique ID for filter chain items
const generateUid = () => Date.now().toString(36) + Math.random().toString(36).substr(2);
// Add filter to chain
const handleAddFilter = () => {
if (!currentFilterConfig) return;
setFilterChain(prev => [...prev, {
id: currentFilterConfig.id,
name: currentFilterConfig.name,
params: { ...pendingParams },
uid: generateUid()
}]);
};
// Remove filter from chain by uid
const handleRemoveFilter = (uid) => {
setFilterChain(prev => prev.filter(f => f.uid !== uid));
};
// Format filter label for display in chain
const formatFilterLabel = (filter) => {
const filterDef = availableFilters.find(f => f.id === filter.id);
if (!filterDef || !filterDef.parameters || filterDef.parameters.length === 0) {
return filter.name;
}
// Show key param values
const paramValues = filterDef.parameters
.filter(p => isParamVisible(p, filter.params))
.map(p => {
const val = filter.params[p.id];
if (p.type === 'checkbox') return val ? p.label : null;
if (p.type === 'slider') return `${p.label}: ${Number(val).toFixed(1)}`;
return `${val}`;
})
.filter(Boolean)
.slice(0, 2); // Show max 2 params
return paramValues.length > 0
? `${filter.name} (${paramValues.join(', ')})`
: filter.name;
};
// Render parameter input based on schema
const renderParamInput = (param) => {
if (!isParamVisible(param, pendingParams)) return null;
const value = pendingParams[param.id];
switch (param.type) {
case 'slider':
return (
{param.label}: {Number(value).toFixed(param.step < 1 ? 1 : 0)}
handleParamChange(param.id, v)}
min={param.min}
max={param.max}
step={param.step}
size="small"
/>
);
case 'dropdown':
return (
{param.label}
);
case 'checkbox':
return (
handleParamChange(param.id, e.target.checked)}
size="small"
/>
}
label={param.label}
/>
);
default:
return null;
}
};
// Handle file selection
const handleFileSelect = (event) => {
const file = event.target.files[0];
if (!file) return;
// Validate file type
const allowedTypes = ['image/png', 'image/jpeg', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
setError('Please select a PNG, JPEG, or WebP image');
return;
}
setSelectedFile(file);
setError(null);
setResultUrl(null);
// Revoke old preview URL to free memory
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
// Create new preview URL
const url = URL.createObjectURL(file);
setPreviewUrl(url);
};
// Handle drag and drop
const handleDrop = (event) => {
event.preventDefault();
const file = event.dataTransfer.files[0];
if (file) {
handleFileSelect({ target: { files: [file] } });
}
};
const handleDragOver = (event) => {
event.preventDefault();
};
// Apply all filters in chain
const handleApplyFilters = async () => {
if (!selectedFile) {
setError('Please select an image first');
return;
}
if (filterChain.length === 0) {
setError('Please add at least one filter to the chain');
return;
}
setLoading(true);
setError(null);
try {
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('filters', JSON.stringify(filterChain.map(f => ({
filter: f.id,
params: f.params
}))));
const response = await fetch('/image/apply-filters', {
method: 'POST',
body: formData
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || 'Failed to process image');
}
// Get the processed image as a blob
const blob = await response.blob();
// Revoke old result URL
if (resultUrl) {
URL.revokeObjectURL(resultUrl);
}
// Create URL for the result
const url = URL.createObjectURL(blob);
setResultUrl(url);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// Download result
const handleDownload = () => {
if (!resultUrl) return;
const filterNames = filterChain.map(f => f.id).join('-');
const link = document.createElement('a');
link.href = resultUrl;
link.download = `filtered-${filterNames}-${selectedFile?.name || 'image.png'}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Cleanup URLs on unmount
React.useEffect(() => {
return () => {
if (previewUrl) URL.revokeObjectURL(previewUrl);
if (resultUrl) URL.revokeObjectURL(resultUrl);
};
}, []);
return (
Image Tools
Upload an image, build a filter chain, and download the result.
{error && (
setError(null)}>
{error}
)}
{/* Upload Area */}
1. Select Image
document.getElementById('file-input').click()}
>
{selectedFile ? selectedFile.name : 'Click or drag an image here'}
Supports PNG, JPEG, WebP
{/* Filter Selection */}
2. Build Filter Chain
{filtersLoading ? (
Loading filters...
) : (
<>
{/* Add filter controls */}
Filter
{/* Dynamic parameter inputs */}
{currentFilterConfig?.parameters?.map(param => renderParamInput(param))}
>
)}
{/* Filter chain display */}
{filterChain.length > 0 && (
Filter Chain ({filterChain.length} filter{filterChain.length !== 1 ? 's' : ''}):
{filterChain.map((filter, index) => (
{index > 0 && (
→
)}
handleRemoveFilter(filter.uid)}
color="primary"
variant="outlined"
/>
))}
)}
{/* Apply button */}
{/* Preview and Result */}
{/* Original Preview */}
{previewUrl && (
Original
)}
{/* Processed Result */}
{resultUrl && (
Result
)}
);
};
// Expose globally for app.jsx
window.ImageToolsPage = ImageToolsPage;