Refactor image conversion logic in convert-image-to-webp.ts to handle HEIC files and improve compatibility with Safari. Update ReceiptForm and +page components to streamline image upload process, removing camera functionality and enhancing user feedback during image processing. Adjust styles for better accessibility and clarity in image handling.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m50s

This commit is contained in:
eewing
2026-02-19 15:07:32 -06:00
parent 132e376b9f
commit 8b977b5e86
3 changed files with 96 additions and 237 deletions
+82 -30
View File
@@ -1,42 +1,94 @@
import { convertHeicBlobToJpeg } from "./convert-heic";
function isHeicFile(file: File): boolean {
const name = file.name.toLowerCase();
const type = (file.type ?? "").toLowerCase();
return (
name.endsWith(".heic") ||
name.endsWith(".heif") ||
type === "image/heic" ||
type === "image/heif"
);
}
function makeOutputFile(blob: Blob, baseName: string, ext: string): File {
return new File([blob], `${baseName}.${ext}`, {
type: blob.type,
lastModified: Date.now(),
});
}
/**
* Converts an image File to WebP and returns a new File.
* Used so receipt uploads are stored only as WebP.
* Converts an image File to WebP (or JPEG on Safari where WebP isn't supported).
* HEIC from iPhone is handled: if the image fails to load, we decode with heic2any first.
*/
export function convertImageToWebP(file: File, quality = 0.85): Promise<File> {
return new Promise((resolve, reject) => {
const img = new Image();
const baseName = file.name.replace(/\.[^.]+$/i, "") || "image";
const tryCanvas = (sourceBlob: Blob) => {
const url = URL.createObjectURL(sourceBlob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Could not get canvas context"));
return;
}
ctx.drawImage(img, 0, 0);
// Safari doesn't support WebP in toBlob; try WebP first, fall back to JPEG
canvas.toBlob(
(blob) => {
if (blob && blob.type === "image/webp") {
resolve(makeOutputFile(blob, baseName, "webp"));
return;
}
canvas.toBlob(
(jpegBlob) => {
if (!jpegBlob) {
reject(new Error("Failed to encode image"));
return;
}
resolve(makeOutputFile(jpegBlob, baseName, "jpg"));
},
"image/jpeg",
quality
);
},
"image/webp",
quality
);
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error("Failed to load image"));
};
img.src = url;
};
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Could not get canvas context"));
return;
}
ctx.drawImage(img, 0, 0);
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error("Failed to encode WebP"));
return;
}
const baseName = file.name.replace(/\.[^.]+$/i, "") || "image";
const webpFile = new File([blob], `${baseName}.webp`, {
type: "image/webp",
lastModified: Date.now(),
});
resolve(webpFile);
},
"image/webp",
quality
);
tryCanvas(file);
};
img.onerror = () => {
img.onerror = async () => {
URL.revokeObjectURL(url);
reject(new Error("Failed to load image"));
// HEIC often fails to load in Image() on iOS; decode with heic2any then run canvas
if (isHeicFile(file)) {
try {
const jpegBlob = await convertHeicBlobToJpeg(file);
tryCanvas(jpegBlob);
} catch (e) {
reject(e instanceof Error ? e : new Error("Failed to convert HEIC"));
}
} else {
reject(new Error("Failed to load image"));
}
};
img.src = url;
});