Vitron-Front/app/utils/convert-heic.ts
Arda Samadi bac93568f5 feat(upload): convert HEIC/HEIF to JPEG on the client before preview/upload
Most browsers (everything but Safari/WebKit) can't decode HEIC in <img> or
canvas, so picking an iPhone HEIC photo broke the preview and the crop editor,
and could reach the backend in a format it couldn't process. Convert HEIC/HEIF
to JPEG right after selection via heic2any (dynamically imported, browser-only).

- new util app/utils/convert-heic.ts: isHeic() detection (MIME or extension
  when iOS reports no type) + convertHeicToJpeg()/convertHeicFiles(); never
  throws — falls back to the original file on failure.
- wired into the product image picker (store.add.manual) and the store
  logo picker (StoreForm).
- adds heic2any dependency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:43:30 +03:30

67 lines
2.2 KiB
TypeScript

// Client-side HEIC/HEIF -> JPEG conversion.
//
// iPhones store photos as HEIC. Most browsers (everything except Safari/WebKit)
// can't decode HEIC in <img>/canvas, so previewing and cropping a freshly
// picked HEIC file fails. We convert to JPEG right after selection so the rest
// of the upload flow (preview, ImageEditor crop, upload) works everywhere.
//
// heic2any is browser-only (uses a wasm libheif build), so it must be imported
// dynamically inside the handler — never at module top level (SSR would break).
const HEIC_EXT_RE = /\.(heic|heif)$/i;
/** True if the file looks like HEIC/HEIF by MIME type or, when the browser
* reports no/!generic type (common on iOS), by file extension. */
export function isHeic(file: File): boolean {
const type = (file.type || "").toLowerCase();
if (
type === "image/heic" ||
type === "image/heif" ||
type === "image/heic-sequence" ||
type === "image/heif-sequence"
) {
return true;
}
if (type === "" || type === "application/octet-stream") {
return HEIC_EXT_RE.test(file.name);
}
return false;
}
/**
* Returns a JPEG File when given a HEIC/HEIF file; otherwise returns the
* original file unchanged. Never throws — on conversion failure it falls back
* to the original file (the backend's pillow-heif may still handle it).
*/
export async function convertHeicToJpeg(
file: File,
quality = 0.92
): Promise<File> {
if (typeof window === "undefined" || !isHeic(file)) return file;
try {
const heic2any = (await import("heic2any")).default;
const result = (await heic2any({
blob: file,
toType: "image/jpeg",
quality,
})) as Blob | Blob[];
const blob = Array.isArray(result) ? result[0] : result;
const baseName = file.name.replace(HEIC_EXT_RE, "") || "image";
return new File([blob], `${baseName}.jpg`, {
type: "image/jpeg",
lastModified: Date.now(),
});
} catch (e) {
console.error("HEIC->JPEG conversion failed; using original file", e);
return file;
}
}
/** Convert any HEIC/HEIF files in a list, leaving others untouched. */
export async function convertHeicFiles(files: File[]): Promise<File[]> {
return Promise.all(files.map((f) => convertHeicToJpeg(f)));
}