feat: full HEIF/HEIC support, content-aware resize performance fix, UI improvements

- Add bidirectional HEIF support: decode (input) and encode (output) via system heif-convert/heif-enc
- Add server-side WebP preview generation for non-browser-previewable formats (HEIC, TIFF)
- Fix content-aware resize failing on HEIF input (decode before passing to caire)
- Fix content-aware resize timeout on large images by downscaling to max 1200px and using JPEG intermediate
- Add HEIF as target format in convert tool
- Add loading spinner for HEIF preview decode in file store
- Fix file picker not accepting HEIF files (explicit .heic,.heif,.hif extensions)
- Extend frontend timeout for medium tools to 180s with 45s progress animation
- Redesign rotate controls with preset buttons and compact flip section
- Remove misleading savings percentage from convert tool
This commit is contained in:
Siddharth Kumar Sah
2026-04-11 23:27:44 +08:00
parent e0869477d4
commit 6f5283019b
18 changed files with 425 additions and 86 deletions
@@ -4,8 +4,8 @@ import { ProgressCard } from "@/components/common/progress-card";
import { useToolProcessor } from "@/hooks/use-tool-processor";
import { useFileStore } from "@/stores/file-store";
const OUTPUT_FORMATS = ["jpg", "png", "webp", "avif", "tiff", "gif", "heic"] as const;
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic"];
const OUTPUT_FORMATS = ["jpg", "png", "webp", "avif", "tiff", "gif", "heic", "heif"] as const;
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif"];
export interface ConvertControlsProps {
onChange?: (settings: Record<string, unknown>) => void;
@@ -132,10 +132,6 @@ export function ConvertSettings() {
<div className="text-xs text-muted-foreground space-y-0.5">
<p>Original: {(originalSize / 1024).toFixed(1)} KB</p>
<p>Processed: {(processedSize / 1024).toFixed(1)} KB</p>
<p>
Savings:{" "}
{originalSize > 0 ? ((1 - processedSize / originalSize) * 100).toFixed(1) : "0"}%
</p>
</div>
)}
@@ -146,7 +146,7 @@ export function PipelineBuilder({
const handleFileSelect = useCallback(() => {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.accept = "image/*,.heic,.heif,.hif";
input.onchange = (e) => {
const f = (e.target as HTMLInputElement).files?.[0];
if (f) setFile(f);
@@ -87,20 +87,45 @@ export function RotateControls({ onChange, onPreviewTransform, resetSignal }: Ro
return (
<div className="space-y-4">
{/* Quick rotate */}
{/* Quick rotate presets */}
<div>
<p className="text-xs text-muted-foreground">Rotate</p>
<div className="flex items-center gap-2 mt-1">
<div className="flex gap-1.5 mt-1">
<button
type="button"
data-testid="rotate-left"
onClick={rotateLeft}
className="flex-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-sm font-medium"
className="flex-1 flex items-center justify-center gap-1 py-2 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-xs font-medium"
title="Rotate 90° counter-clockwise"
>
<RotateCcw className="h-4 w-4" />
Left
<RotateCcw className="h-3.5 w-3.5" />
-90°
</button>
<button
type="button"
onClick={() => setRotation((r) => r + 180)}
className="flex-1 flex items-center justify-center py-2 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-xs font-medium"
title="Rotate 180°"
>
180°
</button>
<button
type="button"
data-testid="rotate-right"
onClick={rotateRight}
className="flex-1 flex items-center justify-center gap-1 py-2 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-xs font-medium"
title="Rotate 90° clockwise"
>
+90°
<RotateCw className="h-3.5 w-3.5" />
</button>
</div>
</div>
{/* Custom angle */}
<div>
<p className="text-xs text-muted-foreground">Angle</p>
<div className="flex items-center justify-center gap-1.5 mt-1">
<button
type="button"
onClick={() => setRotation((r) => r - 1)}
@@ -122,7 +147,7 @@ export function RotateControls({ onChange, onPreviewTransform, resetSignal }: Ro
commitAngleInput();
}
}}
className="w-14 text-center text-sm font-mono font-medium tabular-nums py-1.5 rounded-md bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 pr-4"
className="w-16 text-center text-sm font-mono font-medium tabular-nums py-1.5 rounded-md bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/50 pr-4"
/>
<span className="absolute right-2 text-sm font-mono text-muted-foreground pointer-events-none">
°
@@ -136,16 +161,6 @@ export function RotateControls({ onChange, onPreviewTransform, resetSignal }: Ro
>
<Plus className="h-3.5 w-3.5" />
</button>
<button
type="button"
data-testid="rotate-right"
onClick={rotateRight}
className="flex-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg bg-muted text-muted-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-sm font-medium"
title="Rotate 90° clockwise"
>
Right
<RotateCw className="h-4 w-4" />
</button>
</div>
</div>
@@ -185,26 +200,26 @@ export function RotateControls({ onChange, onPreviewTransform, resetSignal }: Ro
type="button"
data-testid="rotate-flip-h"
onClick={() => setFlipH(!flipH)}
className={`flex-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg text-sm font-medium transition-colors ${
className={`flex-1 flex items-center justify-center gap-1.5 py-2 rounded-lg text-xs font-medium transition-colors ${
flipH
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-primary/10"
}`}
>
<FlipHorizontal className="h-4 w-4" />
<FlipHorizontal className="h-3.5 w-3.5" />
Horizontal
</button>
<button
type="button"
data-testid="rotate-flip-v"
onClick={() => setFlipV(!flipV)}
className={`flex-1 flex items-center justify-center gap-1.5 py-2.5 rounded-lg text-sm font-medium transition-colors ${
className={`flex-1 flex items-center justify-center gap-1.5 py-2 rounded-lg text-xs font-medium transition-colors ${
flipV
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-primary/10"
}`}
>
<FlipVertical className="h-4 w-4" />
<FlipVertical className="h-3.5 w-3.5" />
Vertical
</button>
</div>