mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
co-authored by
stirling-image
parent
c2c104e887
commit
5be8be3dc3
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user