mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add AVIF output format support across 6 remaining tools (#85)
Closes #73 AVIF was already supported in the core engine, convert, compress, optimize-for-web, upscale, erase-object, svg-to-raster, and pdf-to-image tools. This adds AVIF as an output format option to the 6 tools that were missing it: split, collage, stitch, image-to-base64, noise-removal, and red-eye-removal. For each tool, both the frontend format selector (with quality slider for AVIF's lossy encoding) and the backend Zod schema + Sharp .avif() encoding were updated. AVIF defaults: quality from the user slider, effort 4 (balanced encode speed). Also fixes pre-existing Biome formatting violations in 5 files that were blocking a clean lint pass.
This commit is contained in:
@@ -25,6 +25,7 @@ const OUTPUT_FORMATS: { value: OutputFormat; label: string }[] = [
|
||||
{ value: "png", label: "PNG" },
|
||||
{ value: "jpeg", label: "JPEG" },
|
||||
{ value: "webp", label: "WebP" },
|
||||
{ value: "avif", label: "AVIF" },
|
||||
];
|
||||
|
||||
const BG_PRESETS = [
|
||||
|
||||
@@ -9,6 +9,7 @@ const OUTPUT_FORMATS = [
|
||||
{ value: "jpeg", label: "JPEG" },
|
||||
{ value: "png", label: "PNG" },
|
||||
{ value: "webp", label: "WebP" },
|
||||
{ value: "avif", label: "AVIF" },
|
||||
] as const;
|
||||
|
||||
export function ImageToBase64Settings() {
|
||||
@@ -68,7 +69,7 @@ export function ImageToBase64Settings() {
|
||||
};
|
||||
|
||||
const hasFiles = files.length > 0;
|
||||
const showQuality = outputFormat === "jpeg" || outputFormat === "webp";
|
||||
const showQuality = outputFormat === "jpeg" || outputFormat === "webp" || outputFormat === "avif";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -13,7 +13,7 @@ const TIERS: { id: Tier; label: string; desc: string }[] = [
|
||||
{ id: "maximum", label: "Maximum", desc: "Best AI model, slowest" },
|
||||
];
|
||||
|
||||
const LOSSY_FORMATS = new Set(["jpeg", "webp"]);
|
||||
const LOSSY_FORMATS = new Set(["jpeg", "webp", "avif"]);
|
||||
|
||||
export interface NoiseRemovalControlsProps {
|
||||
settings?: Record<string, unknown>;
|
||||
@@ -28,7 +28,7 @@ export function NoiseRemovalControls({
|
||||
const [strength, setStrength] = useState(50);
|
||||
const [detailPreservation, setDetailPreservation] = useState(50);
|
||||
const [colorNoise, setColorNoise] = useState(30);
|
||||
const [outputFormat, setOutputFormat] = useState<"original" | "png" | "jpeg" | "webp">(
|
||||
const [outputFormat, setOutputFormat] = useState<"original" | "png" | "jpeg" | "webp" | "avif">(
|
||||
"original",
|
||||
);
|
||||
const [quality, setQuality] = useState(90);
|
||||
@@ -44,7 +44,7 @@ export function NoiseRemovalControls({
|
||||
setDetailPreservation(Number(initialSettings.detailPreservation));
|
||||
if (initialSettings.colorNoise != null) setColorNoise(Number(initialSettings.colorNoise));
|
||||
if (initialSettings.format != null)
|
||||
setOutputFormat(initialSettings.format as "original" | "png" | "jpeg" | "webp");
|
||||
setOutputFormat(initialSettings.format as "original" | "png" | "jpeg" | "webp" | "avif");
|
||||
if (initialSettings.quality != null) setQuality(Number(initialSettings.quality));
|
||||
}, [initialSettings]);
|
||||
|
||||
@@ -164,8 +164,8 @@ export function NoiseRemovalControls({
|
||||
{/* Output format */}
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1">Output Format</p>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{(["original", "png", "jpeg", "webp"] as const).map((f) => (
|
||||
<div className="grid grid-cols-5 gap-1">
|
||||
{(["original", "png", "jpeg", "webp", "avif"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
type="button"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ProgressCard } from "@/components/common/progress-card";
|
||||
import { useToolProcessor } from "@/hooks/use-tool-processor";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
|
||||
const LOSSY_FORMATS = new Set(["jpeg", "webp"]);
|
||||
const LOSSY_FORMATS = new Set(["jpeg", "webp", "avif"]);
|
||||
|
||||
export interface RedEyeRemovalControlsProps {
|
||||
settings?: Record<string, unknown>;
|
||||
@@ -17,7 +17,7 @@ export function RedEyeRemovalControls({
|
||||
}: RedEyeRemovalControlsProps) {
|
||||
const [sensitivity, setSensitivity] = useState(50);
|
||||
const [strength, setStrength] = useState(70);
|
||||
const [outputFormat, setOutputFormat] = useState<"original" | "png" | "jpeg" | "webp">(
|
||||
const [outputFormat, setOutputFormat] = useState<"original" | "png" | "jpeg" | "webp" | "avif">(
|
||||
"original",
|
||||
);
|
||||
const [quality, setQuality] = useState(90);
|
||||
@@ -30,7 +30,7 @@ export function RedEyeRemovalControls({
|
||||
if (initialSettings.sensitivity != null) setSensitivity(Number(initialSettings.sensitivity));
|
||||
if (initialSettings.strength != null) setStrength(Number(initialSettings.strength));
|
||||
if (initialSettings.format != null)
|
||||
setOutputFormat(initialSettings.format as "original" | "png" | "jpeg" | "webp");
|
||||
setOutputFormat(initialSettings.format as "original" | "png" | "jpeg" | "webp" | "avif");
|
||||
if (initialSettings.quality != null) setQuality(Number(initialSettings.quality));
|
||||
}, [initialSettings]);
|
||||
|
||||
@@ -101,8 +101,8 @@ export function RedEyeRemovalControls({
|
||||
{/* Output format */}
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1">Output Format</p>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{(["original", "png", "jpeg", "webp"] as const).map((f) => (
|
||||
<div className="grid grid-cols-5 gap-1">
|
||||
{(["original", "png", "jpeg", "webp", "avif"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
type="button"
|
||||
|
||||
@@ -28,9 +28,10 @@ const OUTPUT_FORMATS = [
|
||||
{ value: "png", label: "PNG" },
|
||||
{ value: "jpg", label: "JPG" },
|
||||
{ value: "webp", label: "WebP" },
|
||||
{ value: "avif", label: "AVIF" },
|
||||
] as const;
|
||||
|
||||
const LOSSY_FORMATS = new Set(["jpg", "webp"]);
|
||||
const LOSSY_FORMATS = new Set(["jpg", "webp", "avif"]);
|
||||
|
||||
export function SplitSettings() {
|
||||
const { files, processing: fileStoreProcessing } = useFileStore();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useFileStore } from "@/stores/file-store";
|
||||
type Direction = "horizontal" | "vertical" | "grid";
|
||||
type ResizeMode = "fit" | "original" | "stretch" | "crop";
|
||||
type Alignment = "start" | "center" | "end";
|
||||
type OutputFormat = "png" | "jpeg" | "webp";
|
||||
type OutputFormat = "png" | "jpeg" | "webp" | "avif";
|
||||
|
||||
export function StitchSettings() {
|
||||
const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes, setJobId } =
|
||||
@@ -227,7 +227,7 @@ export function StitchSettings() {
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Format</p>
|
||||
<div className="grid grid-cols-3 gap-1 mt-1">
|
||||
{(["png", "jpeg", "webp"] as const).map((f) => (
|
||||
{(["png", "jpeg", "webp", "avif"] as const).map((f) => (
|
||||
<button
|
||||
type="button"
|
||||
key={f}
|
||||
@@ -240,7 +240,7 @@ export function StitchSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(format === "jpeg" || format === "webp") && (
|
||||
{(format === "jpeg" || format === "webp" || format === "avif") && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<label htmlFor="stitch-quality" className="text-xs text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user