feat: add Optimize for Web tool (#68)

* feat(image-engine): add OptimizeForWebOptions type

* feat(image-engine): add optimizeForWeb operation

* feat(shared): add optimize-for-web tool definition and i18n

* feat(api): add optimize-for-web route with preview endpoint

* feat(web): add optimize-for-web settings component with live preview

* feat(web): register optimize-for-web in tool registry

* fix(web): align toggle switch translate with codebase pattern

---------

Co-authored-by: stirling-image <stirling-image@users.noreply.github.com>
This commit is contained in:
stirling-image
2026-04-14 16:16:04 +08:00
committed by GitHub
co-authored by stirling-image
parent c2c104e887
commit 5be8be3dc3
9 changed files with 590 additions and 0 deletions
+1
View File
@@ -11,6 +11,7 @@ export { editMetadata } from "./operations/edit-metadata.js";
export { flip } from "./operations/flip.js";
export { grayscale } from "./operations/grayscale.js";
export { invert } from "./operations/invert.js";
export { optimizeForWeb } from "./operations/optimize-for-web.js";
export { resize } from "./operations/resize.js";
export { rotate } from "./operations/rotate.js";
export { saturation } from "./operations/saturation.js";
@@ -0,0 +1,43 @@
import type { OptimizeForWebOptions, Sharp } from "../types.js";
export async function optimizeForWeb(image: Sharp, options: OptimizeForWebOptions): Promise<Sharp> {
const {
format,
quality,
maxWidth,
maxHeight,
progressive = true,
stripMetadata = true,
} = options;
// Step 1: Resize if max dimensions are set
if (maxWidth || maxHeight) {
image = image.resize({
width: maxWidth,
height: maxHeight,
fit: "inside",
withoutEnlargement: true,
});
}
// Step 2: Preserve metadata only if requested
// Sharp strips metadata by default on output, so we only need to act
// when the user wants to KEEP metadata.
if (!stripMetadata) {
image = image.withMetadata();
}
// Step 3: Convert to target format with optimized settings
switch (format) {
case "webp":
return image.webp({ quality, effort: 4 });
case "jpeg":
return image.jpeg({ quality, progressive, mozjpeg: true });
case "avif":
return image.avif({ quality, effort: 4 });
case "png":
return image.png({ compressionLevel: 9, palette: true });
default:
throw new Error(`Unsupported format: ${format}`);
}
}
+9
View File
@@ -172,3 +172,12 @@ export interface CorrectionParams {
/** Denoise strength. 0 = off, 1-5 = median kernel size. */
denoise: number;
}
export interface OptimizeForWebOptions {
format: "webp" | "jpeg" | "avif" | "png";
quality: number;
maxWidth?: number;
maxHeight?: number;
progressive?: boolean;
stripMetadata?: boolean;
}
+9
View File
@@ -55,6 +55,15 @@ export const TOOLS: Tool[] = [
route: "/compress",
},
// Optimization
{
id: "optimize-for-web",
name: "Optimize for Web",
description:
"Optimize images for web with format conversion, quality control, and live preview",
category: "optimization",
icon: "Globe",
route: "/optimize-for-web",
},
{
id: "strip-metadata",
name: "Remove Metadata",
+5
View File
@@ -35,6 +35,11 @@ export const en = {
rotate: { name: "Rotate & Flip", description: "Rotate, flip, and straighten images" },
convert: { name: "Convert", description: "Convert between image formats" },
compress: { name: "Compress", description: "Reduce file size by quality or target size" },
"optimize-for-web": {
name: "Optimize for Web",
description:
"Optimize images for web with format conversion, quality control, and live preview",
},
"strip-metadata": { name: "Remove Metadata", description: "Remove EXIF, GPS, and camera info" },
"edit-metadata": {
name: "Edit Metadata",