mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
API: - Replace string concatenation with template literals (batch, pipeline, tool-factory, content-aware-resize, passport-photo, remove-background) - Remove unused imports (teams, color-adjustments, image-enhancement) - Replace non-null assertions with guard clauses in bg-effects and stitch - Use optional chaining in docs route Image-engine: - Remove unused OutputFormat imports (compress, convert) - Use local variables instead of reassigning parameters (optimize-for-web, sharpen) Web: - Fix useExhaustiveDependencies: remove genuinely redundant deps, add biome-ignore comments for intentional patterns (src prop, cleanup fns) - Replace non-null assertions with null-safe alternatives - Add accessible titles to inline SVGs (color-settings, image-enhancement-settings) - Fix noLabelWithoutControl: associate labels via htmlFor/id or use <span> (image-to-base64-settings, edit-metadata-settings, qr-generate-settings) - Replace div[role="button"] with <button> (pdf-to-image-settings) - Use stable keys instead of array indices (collage-preview, collage-settings) - Fix noStaticElementInteractions in collage-preview (role="none") - Remove unused function parameters and imports
109 lines
2.6 KiB
TypeScript
109 lines
2.6 KiB
TypeScript
import type { Sharp, SharpenAdvancedOptions, SharpenOptions } from "../types.js";
|
|
|
|
export async function sharpen(image: Sharp, options: SharpenOptions): Promise<Sharp> {
|
|
const { value } = options;
|
|
|
|
if (value <= 0) return image;
|
|
if (value > 100) {
|
|
throw new Error("Sharpness value must be between 0 and 100");
|
|
}
|
|
|
|
// Map 0-100 to sigma 0.5-10
|
|
const sigma = 0.5 + (value / 100) * 9.5;
|
|
|
|
return image.sharpen({ sigma });
|
|
}
|
|
|
|
const DENOISE_KERNEL: Record<string, number> = {
|
|
light: 3,
|
|
medium: 5,
|
|
strong: 7,
|
|
};
|
|
|
|
export async function sharpenAdvanced(
|
|
image: Sharp,
|
|
options: SharpenAdvancedOptions,
|
|
): Promise<Sharp> {
|
|
const { method, denoise } = options;
|
|
|
|
// Optional noise reduction pre-pass
|
|
let pipeline = image;
|
|
if (denoise && denoise !== "off") {
|
|
const kernelSize = DENOISE_KERNEL[denoise];
|
|
if (kernelSize) {
|
|
pipeline = pipeline.median(kernelSize);
|
|
}
|
|
}
|
|
|
|
switch (method) {
|
|
case "adaptive":
|
|
return sharpenAdaptive(pipeline, options);
|
|
case "unsharp-mask":
|
|
return sharpenUnsharpMask(pipeline, options);
|
|
case "high-pass":
|
|
return sharpenHighPass(pipeline, options);
|
|
default:
|
|
throw new Error(`Unknown sharpening method: ${method}`);
|
|
}
|
|
}
|
|
|
|
function sharpenAdaptive(image: Sharp, options: SharpenAdvancedOptions): Sharp {
|
|
const sigma = options.sigma ?? 1.0;
|
|
const m1 = options.m1 ?? 1.0;
|
|
const m2 = options.m2 ?? 3.0;
|
|
const x1 = options.x1 ?? 2.0;
|
|
const y2 = options.y2 ?? 12;
|
|
const y3 = options.y3 ?? 20;
|
|
return image.sharpen({ sigma, m1, m2, x1, y2, y3 });
|
|
}
|
|
|
|
function sharpenUnsharpMask(image: Sharp, options: SharpenAdvancedOptions): Sharp {
|
|
const amount = options.amount ?? 100;
|
|
const radius = options.radius ?? 1.0;
|
|
const threshold = options.threshold ?? 0;
|
|
const sigma = radius;
|
|
const intensity = amount / 100;
|
|
const x1 = (threshold / 255) * 10;
|
|
return image.sharpen({ sigma, m1: intensity, m2: intensity, x1, y2: 15, y3: 25 });
|
|
}
|
|
|
|
function sharpenHighPass(image: Sharp, options: SharpenAdvancedOptions): Sharp {
|
|
const strength = options.strength ?? 50;
|
|
const kernelSize = options.kernelSize ?? 3;
|
|
const s = strength / 100;
|
|
|
|
if (kernelSize === 5) {
|
|
const k = [
|
|
0,
|
|
-s,
|
|
-s,
|
|
-s,
|
|
0,
|
|
-s,
|
|
s,
|
|
s * 2,
|
|
s,
|
|
-s,
|
|
-s,
|
|
s * 2,
|
|
1 + s * 8,
|
|
s * 2,
|
|
-s,
|
|
-s,
|
|
s,
|
|
s * 2,
|
|
s,
|
|
-s,
|
|
0,
|
|
-s,
|
|
-s,
|
|
-s,
|
|
0,
|
|
];
|
|
return image.convolve({ width: 5, height: 5, kernel: k });
|
|
}
|
|
|
|
const k = [0, -s, 0, -s, 1 + 4 * s, -s, 0, -s, 0];
|
|
return image.convolve({ width: 3, height: 3, kernel: k });
|
|
}
|