@mvp-factory/holy-upload
Version:
File upload processing system extracted from Holy Habit project with security validation and image optimization
411 lines • 15.5 kB
JavaScript
;
/**
* Image Optimizer
*
* Optimizes images using Sharp library
* Extracted from Holy Habit image processing logic
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImageOptimizer = void 0;
const sharp_1 = __importDefault(require("sharp"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class ImageOptimizer {
/**
* Optimize image file
*
* @param inputPath - Input file path
* @param outputPath - Output file path (optional, defaults to input path)
* @param options - Optimization options
* @returns Optimization result
*/
static async optimize(inputPath, outputPath, options = {}) {
const opts = { ...this.DEFAULT_OPTIONS, ...options };
const output = outputPath || inputPath;
try {
const originalStats = await fs.promises.stat(inputPath);
const originalSize = originalStats.size;
const image = (0, sharp_1.default)(inputPath);
const metadata = await image.metadata();
// Check if resizing is needed
const needsResize = this.needsResize(metadata, opts);
let pipeline = image;
if (needsResize) {
const { width, height } = this.calculateDimensions(metadata, opts);
pipeline = pipeline.resize(width, height, {
fit: 'inside',
withoutEnlargement: true
});
}
// Apply format-specific optimizations
const format = metadata.format;
switch (format) {
case 'jpeg':
pipeline = pipeline.jpeg({
quality: opts.jpegQuality,
progressive: true,
mozjpeg: true
});
break;
case 'png':
pipeline = pipeline.png({
compressionLevel: opts.pngCompression,
progressive: false,
palette: true
});
break;
case 'webp':
pipeline = pipeline.webp({
quality: opts.webpQuality,
effort: 6
});
break;
case 'gif':
// Keep GIF as-is, just resize if needed
break;
default:
// Convert unknown formats to JPEG
pipeline = pipeline.jpeg({
quality: opts.jpegQuality,
progressive: true
});
}
// Save optimized image
await pipeline.toFile(output);
const optimizedStats = await fs.promises.stat(output);
const optimizedSize = optimizedStats.size;
const savings = ((originalSize - optimizedSize) / originalSize) * 100;
return {
success: true,
originalSize,
optimizedSize,
savings: Math.max(0, savings)
};
}
catch (error) {
throw new Error(`Image optimization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Optimize image buffer
*
* @param buffer - Input image buffer
* @param options - Optimization options
* @returns Optimized buffer and metadata
*/
static async optimizeBuffer(buffer, options = {}) {
const opts = { ...this.DEFAULT_OPTIONS, ...options };
const originalSize = buffer.length;
try {
const image = (0, sharp_1.default)(buffer);
const metadata = await image.metadata();
// Check if resizing is needed
const needsResize = this.needsResize(metadata, opts);
let pipeline = image;
if (needsResize) {
const { width, height } = this.calculateDimensions(metadata, opts);
pipeline = pipeline.resize(width, height, {
fit: 'inside',
withoutEnlargement: true
});
}
// Apply format-specific optimizations
const format = metadata.format;
switch (format) {
case 'jpeg':
pipeline = pipeline.jpeg({
quality: opts.jpegQuality,
progressive: true,
mozjpeg: true
});
break;
case 'png':
pipeline = pipeline.png({
compressionLevel: opts.pngCompression,
progressive: false,
palette: true
});
break;
case 'webp':
pipeline = pipeline.webp({
quality: opts.webpQuality,
effort: 6
});
break;
case 'gif':
// Keep GIF as-is, just resize if needed
break;
default:
// Convert unknown formats to JPEG
pipeline = pipeline.jpeg({
quality: opts.jpegQuality,
progressive: true
});
}
const { data, info } = await pipeline.toBuffer({ resolveWithObject: true });
const optimizedSize = data.length;
const savings = ((originalSize - optimizedSize) / originalSize) * 100;
return {
buffer: data,
metadata: info,
savings: Math.max(0, savings)
};
}
catch (error) {
throw new Error(`Image optimization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Convert image to WebP format
*
* @param inputPath - Input file path
* @param outputPath - Output file path
* @param quality - WebP quality (1-100)
* @returns Conversion result
*/
static async convertToWebP(inputPath, outputPath, quality = 85) {
try {
const originalStats = await fs.promises.stat(inputPath);
const originalSize = originalStats.size;
await (0, sharp_1.default)(inputPath)
.webp({ quality, effort: 6 })
.toFile(outputPath);
const webpStats = await fs.promises.stat(outputPath);
const webpSize = webpStats.size;
const savings = ((originalSize - webpSize) / originalSize) * 100;
return {
success: true,
originalSize,
webpSize,
savings: Math.max(0, savings)
};
}
catch (error) {
throw new Error(`WebP conversion failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Generate image thumbnails
*
* @param inputPath - Input file path
* @param sizes - Array of thumbnail sizes
* @param outputDir - Output directory
* @returns Generated thumbnails
*/
static async generateThumbnails(inputPath, sizes, outputDir) {
const baseName = path.basename(inputPath, path.extname(inputPath));
const extension = path.extname(inputPath);
const thumbnails = [];
try {
// Ensure output directory exists
await fs.promises.mkdir(outputDir, { recursive: true });
const image = (0, sharp_1.default)(inputPath);
for (const size of sizes) {
const outputPath = path.join(outputDir, `${baseName}_${size.suffix}${extension}`);
await image
.clone()
.resize(size.width, size.height, {
fit: 'cover',
position: 'center'
})
.toFile(outputPath);
const stats = await fs.promises.stat(outputPath);
thumbnails.push({
path: outputPath,
width: size.width,
height: size.height,
size: stats.size
});
}
return thumbnails;
}
catch (error) {
throw new Error(`Thumbnail generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Get image metadata
*
* @param inputPath - Input file path
* @returns Image metadata
*/
static async getMetadata(inputPath) {
try {
return await (0, sharp_1.default)(inputPath).metadata();
}
catch (error) {
throw new Error(`Failed to read image metadata: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Check if image needs resizing
*
* @param metadata - Image metadata
* @param options - Optimization options
* @returns True if resizing is needed
*/
static needsResize(metadata, options) {
if (!metadata.width || !metadata.height) {
return false;
}
const maxWidth = options.maxWidth || this.DEFAULT_OPTIONS.maxWidth;
const maxHeight = options.maxHeight || this.DEFAULT_OPTIONS.maxHeight;
return metadata.width > maxWidth || metadata.height > maxHeight;
}
/**
* Calculate optimal dimensions
*
* @param metadata - Image metadata
* @param options - Optimization options
* @returns Calculated dimensions
*/
static calculateDimensions(metadata, options) {
if (!metadata.width || !metadata.height) {
throw new Error('Cannot calculate dimensions: image width or height is unknown');
}
const maxWidth = options.maxWidth || this.DEFAULT_OPTIONS.maxWidth;
const maxHeight = options.maxHeight || this.DEFAULT_OPTIONS.maxHeight;
if (!options.maintainAspectRatio) {
return { width: maxWidth, height: maxHeight };
}
const aspectRatio = metadata.width / metadata.height;
let width = maxWidth;
let height = Math.round(width / aspectRatio);
if (height > maxHeight) {
height = maxHeight;
width = Math.round(height * aspectRatio);
}
return { width, height };
}
/**
* Create progressive JPEG
*
* @param inputPath - Input file path
* @param outputPath - Output file path
* @param quality - JPEG quality
* @returns Processing result
*/
static async createProgressiveJPEG(inputPath, outputPath, quality = 85) {
try {
await (0, sharp_1.default)(inputPath)
.jpeg({
quality,
progressive: true,
mozjpeg: true
})
.toFile(outputPath);
const stats = await fs.promises.stat(outputPath);
return {
success: true,
size: stats.size
};
}
catch (error) {
throw new Error(`Progressive JPEG creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Remove EXIF data from image
*
* @param inputPath - Input file path
* @param outputPath - Output file path
* @returns Processing result
*/
static async removeExifData(inputPath, outputPath) {
try {
await (0, sharp_1.default)(inputPath)
.rotate() // Auto-rotate based on EXIF orientation
.withMetadata({}) // Remove all metadata
.toFile(outputPath);
return { success: true };
}
catch (error) {
throw new Error(`EXIF removal failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Batch optimize images in directory
*
* @param inputDir - Input directory
* @param outputDir - Output directory
* @param options - Optimization options
* @returns Batch optimization results
*/
static async batchOptimize(inputDir, outputDir, options = {}) {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
let processed = 0;
let failed = 0;
let totalSavings = 0;
try {
const files = await fs.promises.readdir(inputDir);
// Ensure output directory exists
await fs.promises.mkdir(outputDir, { recursive: true });
for (const file of files) {
const ext = path.extname(file).toLowerCase();
if (imageExtensions.includes(ext)) {
const inputPath = path.join(inputDir, file);
const outputPath = path.join(outputDir, file);
try {
const result = await this.optimize(inputPath, outputPath, options);
totalSavings += result.savings;
processed++;
}
catch (error) {
console.error(`Failed to optimize ${file}:`, error);
failed++;
}
}
}
return { processed, failed, totalSavings: totalSavings / processed || 0 };
}
catch (error) {
throw new Error(`Batch optimization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
exports.ImageOptimizer = ImageOptimizer;
ImageOptimizer.DEFAULT_OPTIONS = {
maxWidth: 1920,
maxHeight: 1080,
jpegQuality: 85,
pngCompression: 8,
webpQuality: 85,
maintainAspectRatio: true
};
//# sourceMappingURL=ImageOptimizer.js.map