mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add support for JXL, Camera RAW, ICO, TGA, PSD, EXR, HDR image formats
Extends the platform to handle 7 new image format families alongside the existing AVIF support gap-fill. Uses the established HEIC decoder pattern (CLI decode → PNG → Sharp) for formats Sharp can't handle natively: Camera RAW via dcraw_emu/LibRaw, PSD/TGA/EXR/HDR via ImageMagick. JXL and ICO are Sharp-native. Adds server-side preview for non-browser-displayable formats and JXL as a new convert output target. All 27 validateImageBuffer callers updated with filename for extension-based format detection.
This commit is contained in:
@@ -10,11 +10,11 @@ interface DropzoneProps {
|
||||
currentFiles?: File[];
|
||||
}
|
||||
|
||||
// Browsers may not map .heic/.heif to image/* in file pickers.
|
||||
// Browsers may not map certain formats (HEIC, JXL, RAW, etc.) to image/* in file pickers.
|
||||
// Append explicit extensions so they are selectable.
|
||||
function expandAccept(accept?: string): string | undefined {
|
||||
if (!accept?.includes("image/*")) return accept;
|
||||
return `${accept},.heic,.heif,.hif`;
|
||||
return `${accept},.heic,.heif,.hif,.jxl,.ico,.dng,.cr2,.nef,.arw,.orf,.rw2,.tga,.psd,.exr,.hdr`;
|
||||
}
|
||||
|
||||
export function Dropzone({ onFiles, accept, multiple = true, currentFiles = [] }: DropzoneProps) {
|
||||
|
||||
@@ -57,7 +57,7 @@ export function FileUploadArea() {
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,.heic,.heif,.hif"
|
||||
accept="image/*,.heic,.heif,.hif,.jxl,.dng,.cr2,.nef,.arw,.orf,.rw2,.tga,.psd,.exr,.hdr"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleInputChange}
|
||||
|
||||
@@ -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", "heif"] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif"];
|
||||
const OUTPUT_FORMATS = ["jpg", "png", "webp", "avif", "tiff", "gif", "heic", "heif", "jxl"] as const;
|
||||
const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif", "jxl"];
|
||||
|
||||
export interface ConvertControlsProps {
|
||||
settings?: Record<string, unknown>;
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { formatHeaders } from "@/lib/api";
|
||||
|
||||
const HEIF_EXTENSIONS = new Set(["heic", "heif", "hif"]);
|
||||
const SERVER_PREVIEW_EXTENSIONS = new Set([
|
||||
"heic", "heif", "hif", // HEIF
|
||||
"jxl", // JPEG XL (Chrome dropped support)
|
||||
"dng", "cr2", "nef", "arw", "orf", "rw2", // Camera RAW
|
||||
"tga", // Targa
|
||||
"psd", // Photoshop
|
||||
"exr", // OpenEXR
|
||||
"hdr", // Radiance HDR
|
||||
]);
|
||||
|
||||
export function needsServerPreview(file: File): boolean {
|
||||
const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
|
||||
return HEIF_EXTENSIONS.has(ext);
|
||||
return SERVER_PREVIEW_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
export async function fetchDecodedPreview(file: File): Promise<string | null> {
|
||||
|
||||
@@ -251,7 +251,7 @@ export function ToolPage() {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.accept = "image/*,.heic,.heif,.hif";
|
||||
input.accept = "image/*,.heic,.heif,.hif,.jxl,.dng,.cr2,.nef,.arw,.orf,.rw2,.tga,.psd,.exr,.hdr";
|
||||
input.onchange = (e) => {
|
||||
const newFiles = Array.from((e.target as HTMLInputElement).files || []);
|
||||
if (newFiles.length > 0) addFiles(newFiles);
|
||||
|
||||
Reference in New Issue
Block a user