@ideal-photography/shared
Version:
Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.
490 lines (425 loc) • 17 kB
JavaScript
/**
* Image validation utilities for product images
* Provides comprehensive validation beyond basic file checks
*/
/**
* Validate image dimensions for specific use cases
* @param {Object} dimensions - Image dimensions {width, height}
* @param {string} useCase - Use case ('product', 'thumbnail', 'banner', 'avatar')
* @returns {Object} Validation result
*/
export const validateImageDimensions = (dimensions, useCase = 'product') => {
const { width, height } = dimensions;
const requirements = {
product: {
minWidth: 300,
minHeight: 300,
maxWidth: 4000,
maxHeight: 4000,
preferredAspectRatio: 1, // 1:1
aspectRatioTolerance: 0.3
},
thumbnail: {
minWidth: 150,
minHeight: 150,
maxWidth: 500,
maxHeight: 500,
preferredAspectRatio: 1,
aspectRatioTolerance: 0.1
},
banner: {
minWidth: 800,
minHeight: 300,
maxWidth: 2000,
maxHeight: 800,
preferredAspectRatio: 2.5, // 5:2
aspectRatioTolerance: 0.5
},
avatar: {
minWidth: 100,
minHeight: 100,
maxWidth: 1000,
maxHeight: 1000,
preferredAspectRatio: 1,
aspectRatioTolerance: 0.05
}
};
const req = requirements[useCase] || requirements.product;
const errors = [];
const warnings = [];
// Check minimum dimensions
if (width < req.minWidth || height < req.minHeight) {
errors.push(`Image too small: ${width}x${height}. Minimum required: ${req.minWidth}x${req.minHeight}`);
}
// Check maximum dimensions
if (width > req.maxWidth || height > req.maxHeight) {
warnings.push(`Image very large: ${width}x${height}. Consider resizing to improve performance. Maximum recommended: ${req.maxWidth}x${req.maxHeight}`);
}
// Check aspect ratio
const aspectRatio = width / height;
const aspectRatioDiff = Math.abs(aspectRatio - req.preferredAspectRatio);
if (aspectRatioDiff > req.aspectRatioTolerance) {
const preferredRatio = req.preferredAspectRatio === 1 ? '1:1 (square)' : `${req.preferredAspectRatio}:1`;
warnings.push(`Aspect ratio ${aspectRatio.toFixed(2)}:1 differs from preferred ${preferredRatio} for ${useCase} images`);
}
// Check for unusual dimensions
if (width % 2 !== 0 || height % 2 !== 0) {
warnings.push('Odd pixel dimensions may cause issues with some image processing. Consider even dimensions.');
}
// Check for extremely wide or tall images
if (aspectRatio > 5 || aspectRatio < 0.2) {
warnings.push(`Extreme aspect ratio (${aspectRatio.toFixed(2)}:1) may not display well in all contexts`);
}
return {
isValid: errors.length === 0,
errors,
warnings,
aspectRatio,
useCase,
recommendations: generateDimensionRecommendations(dimensions, req)
};
};
/**
* Generate dimension recommendations
* @param {Object} dimensions - Current dimensions
* @param {Object} requirements - Dimension requirements
* @returns {Array} Array of recommendations
*/
const generateDimensionRecommendations = (dimensions, requirements) => {
const { width, height } = dimensions;
const { preferredAspectRatio, minWidth, minHeight } = requirements;
const recommendations = [];
// Recommend optimal dimensions
if (width < minWidth * 2 || height < minHeight * 2) {
const optimalWidth = Math.max(minWidth * 2, 600);
const optimalHeight = Math.max(minHeight * 2, 600);
recommendations.push(`For best quality, use images at least ${optimalWidth}x${optimalHeight}`);
}
// Recommend aspect ratio adjustment
const currentRatio = width / height;
if (Math.abs(currentRatio - preferredAspectRatio) > 0.1) {
if (preferredAspectRatio === 1) {
const newDimension = Math.min(width, height);
recommendations.push(`For square format, crop to ${newDimension}x${newDimension}`);
} else {
const newHeight = Math.round(width / preferredAspectRatio);
recommendations.push(`For optimal aspect ratio, resize to ${width}x${newHeight}`);
}
}
return recommendations;
};
/**
* Validate image file format and quality
* @param {Object} file - File object with metadata
* @param {Object} options - Validation options
* @returns {Object} Validation result
*/
export const validateImageFormat = (file, options = {}) => {
const {
allowedFormats = ['image/jpeg', 'image/png', 'image/webp'],
preferredFormat = 'image/jpeg',
maxQuality = 95,
minQuality = 60
} = options;
const errors = [];
const warnings = [];
const recommendations = [];
// Check if format is allowed
if (!allowedFormats.includes(file.mimetype)) {
errors.push(`Format ${file.mimetype} not allowed. Allowed formats: ${allowedFormats.join(', ')}`);
}
// Format-specific recommendations
if (file.mimetype === 'image/png' && file.size > 2 * 1024 * 1024) {
recommendations.push('PNG files are large. Consider JPEG for photographs or WebP for better compression');
}
if (file.mimetype === 'image/jpeg') {
// Try to estimate JPEG quality (this is approximate)
const estimatedQuality = estimateJpegQuality(file);
if (estimatedQuality && estimatedQuality < minQuality) {
warnings.push(`JPEG quality appears low (estimated ${estimatedQuality}%). Consider using higher quality images.`);
}
if (estimatedQuality && estimatedQuality > maxQuality) {
recommendations.push(`JPEG quality is very high (estimated ${estimatedQuality}%). You can reduce file size by using 85-90% quality.`);
}
}
// Format recommendations based on content
if (file.mimetype === 'image/png' && !hasTransparency(file)) {
recommendations.push('PNG without transparency detected. JPEG would be more efficient for this image.');
}
return {
isValid: errors.length === 0,
errors,
warnings,
recommendations,
detectedFormat: file.mimetype,
preferredFormat
};
};
/**
* Estimate JPEG quality (approximate)
* @param {Object} file - File object
* @returns {number|null} Estimated quality percentage
*/
const estimateJpegQuality = (file) => {
// This is a rough estimation based on file size vs dimensions
// More accurate methods would require reading JPEG headers
if (!file.width || !file.height || !file.size) {
return null;
}
const pixels = file.width * file.height;
const bytesPerPixel = file.size / pixels;
// Very rough estimation
if (bytesPerPixel < 0.5) return 60;
if (bytesPerPixel < 1) return 75;
if (bytesPerPixel < 2) return 85;
if (bytesPerPixel < 3) return 95;
return 100;
};
/**
* Check if PNG has transparency (placeholder - would need actual image analysis)
* @param {Object} file - File object
* @returns {boolean} Whether image has transparency
*/
const hasTransparency = (file) => {
// Placeholder - in real implementation, you'd analyze the PNG file
// For now, assume all PNGs might have transparency
return file.mimetype === 'image/png';
};
/**
* Validate image content for appropriateness
* @param {Object} file - File object
* @param {Object} options - Validation options
* @returns {Promise<Object>} Validation result
*/
export const validateImageContent = async (file, options = {}) => {
const {
checkForFaces = false,
checkForText = false,
checkBrightness = true,
checkContrast = true,
checkBlur = true
} = options;
// This is a placeholder for content validation
// In a real implementation, you might use:
// - Computer vision APIs (Google Vision, AWS Rekognition, Azure Computer Vision)
// - Machine learning models for content analysis
// - Image analysis libraries
const warnings = [];
const recommendations = [];
try {
// Placeholder validations
if (checkBrightness) {
// Simulate brightness check
const brightness = Math.random() * 100;
if (brightness < 20) {
warnings.push('Image appears very dark. Consider brightening for better visibility.');
} else if (brightness > 90) {
warnings.push('Image appears very bright. Consider reducing brightness to avoid overexposure.');
}
}
if (checkContrast) {
// Simulate contrast check
const contrast = Math.random() * 100;
if (contrast < 30) {
recommendations.push('Low contrast detected. Increasing contrast may improve image clarity.');
}
}
if (checkBlur) {
// Simulate blur detection
const sharpness = Math.random() * 100;
if (sharpness < 40) {
warnings.push('Image appears blurry. Sharp, clear images work best for product display.');
}
}
return {
isValid: true, // Content issues are typically warnings, not errors
errors: [],
warnings,
recommendations,
analysis: {
brightness: checkBrightness ? Math.random() * 100 : null,
contrast: checkContrast ? Math.random() * 100 : null,
sharpness: checkBlur ? Math.random() * 100 : null,
hasFaces: checkForFaces ? Math.random() > 0.7 : null,
hasText: checkForText ? Math.random() > 0.5 : null
}
};
} catch (error) {
return {
isValid: true, // Don't fail upload for content analysis errors
errors: [],
warnings: [`Content analysis failed: ${error.message}`],
recommendations: [],
analysis: null
};
}
};
/**
* Comprehensive image validation combining all checks
* @param {Object} file - File object with metadata
* @param {Object} options - Validation options
* @returns {Promise<Object>} Complete validation result
*/
export const validateImageComprehensive = async (file, options = {}) => {
const {
useCase = 'product',
checkContent = false,
maxFileSize = 5 * 1024 * 1024, // 5MB
...otherOptions
} = options;
const results = {
isValid: true,
errors: [],
warnings: [],
recommendations: [],
checks: {}
};
try {
// Basic file validation
if (!file) {
results.errors.push('No file provided');
results.isValid = false;
return results;
}
// File size check
if (file.size > maxFileSize) {
results.errors.push(`File size (${(file.size / 1024 / 1024).toFixed(2)}MB) exceeds maximum (${maxFileSize / 1024 / 1024}MB)`);
results.isValid = false;
}
// Dimension validation
if (file.width && file.height) {
const dimensionCheck = validateImageDimensions(
{ width: file.width, height: file.height },
useCase
);
results.checks.dimensions = dimensionCheck;
if (!dimensionCheck.isValid) {
results.isValid = false;
}
results.errors.push(...dimensionCheck.errors);
results.warnings.push(...dimensionCheck.warnings);
results.recommendations.push(...dimensionCheck.recommendations);
}
// Format validation
const formatCheck = validateImageFormat(file, otherOptions);
results.checks.format = formatCheck;
if (!formatCheck.isValid) {
results.isValid = false;
}
results.errors.push(...formatCheck.errors);
results.warnings.push(...formatCheck.warnings);
results.recommendations.push(...formatCheck.recommendations);
// Content validation (optional)
if (checkContent) {
const contentCheck = await validateImageContent(file, otherOptions);
results.checks.content = contentCheck;
// Content issues are typically warnings, not blocking errors
results.warnings.push(...contentCheck.warnings);
results.recommendations.push(...contentCheck.recommendations);
}
// Additional business logic validations
if (file.originalname) {
const filename = file.originalname.toLowerCase();
// Check for inappropriate filenames
const inappropriateKeywords = ['temp', 'tmp', 'test', 'untitled', 'screenshot'];
if (inappropriateKeywords.some(keyword => filename.includes(keyword))) {
results.warnings.push('Filename suggests this might be a temporary or test image. Consider using a descriptive filename.');
}
// Check filename length
if (filename.length > 100) {
results.warnings.push('Very long filename detected. Consider shortening for better compatibility.');
}
}
// Performance recommendations
if (file.size > 1024 * 1024) { // 1MB
results.recommendations.push('Large file size detected. Consider optimizing the image to improve loading times.');
}
return results;
} catch (error) {
console.error('Error in comprehensive image validation:', error);
return {
isValid: false,
errors: [`Validation failed: ${error.message}`],
warnings: [],
recommendations: [],
checks: {}
};
}
};
/**
* Batch validate multiple images
* @param {Array} files - Array of file objects
* @param {Object} options - Validation options
* @returns {Promise<Object>} Batch validation results
*/
export const validateImagesBatch = async (files, options = {}) => {
if (!Array.isArray(files) || files.length === 0) {
return {
isValid: false,
errors: ['No files provided for validation'],
results: [],
summary: { total: 0, valid: 0, invalid: 0, warnings: 0 }
};
}
const results = [];
const batchErrors = [];
let validCount = 0;
let warningCount = 0;
// Validate each file
for (let i = 0; i < files.length; i++) {
try {
const file = files[i];
const validation = await validateImageComprehensive(file, {
...options,
fileIndex: i
});
validation.fileIndex = i;
validation.fileName = file.originalname || file.name || `file_${i}`;
results.push(validation);
if (validation.isValid) {
validCount++;
}
if (validation.warnings.length > 0) {
warningCount++;
}
} catch (error) {
console.error(`Error validating file ${i}:`, error);
results.push({
fileIndex: i,
fileName: files[i]?.originalname || files[i]?.name || `file_${i}`,
isValid: false,
errors: [`Validation error: ${error.message}`],
warnings: [],
recommendations: [],
checks: {}
});
}
}
// Check batch-level constraints
const totalSize = files.reduce((sum, file) => sum + (file.size || 0), 0);
const maxBatchSize = options.maxBatchSize || 15 * 1024 * 1024; // 15MB total
if (totalSize > maxBatchSize) {
batchErrors.push(`Total batch size (${(totalSize / 1024 / 1024).toFixed(2)}MB) exceeds maximum (${maxBatchSize / 1024 / 1024}MB)`);
}
const isValid = validCount === files.length && batchErrors.length === 0;
return {
isValid,
errors: batchErrors,
results,
summary: {
total: files.length,
valid: validCount,
invalid: files.length - validCount,
warnings: warningCount,
totalSize,
averageSize: Math.round(totalSize / files.length)
}
};
};
export default {
validateImageDimensions,
validateImageFormat,
validateImageContent,
validateImageComprehensive,
validateImagesBatch
};