fix: convert iPhone HEIC photos to JPEG before upload
compressPhoto skipped HEIC on purpose (Chrome can't decode it), so an iPhone driver's photos went up as HEIC. WFDASH's AI check can't read HEIC and most desktop browsers can't show it — Thabo's first two invoice photos on 2026-10-20 went unread. Now HEIC/HEIF is always re-encoded to JPEG (downscaled to 2000px) when the browser can decode it (Safari on iOS/macOS can). Where it can't, the original is uploaded as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ebe88d40e7
commit
48e081a080
1 changed files with 21 additions and 13 deletions
34
lib/image.ts
34
lib/image.ts
|
|
@ -34,13 +34,17 @@ export const PHOTO_QUALITY = 0.8;
|
|||
export const SKIP_BELOW_BYTES = 150 * 1024;
|
||||
|
||||
/**
|
||||
* Formats a canvas can decode and re-encode for us. HEIC/HEIF is
|
||||
* excluded deliberately: iPhone-origin files land here via `accept`
|
||||
* when a driver picks from the library rather than the camera, and
|
||||
* Chrome cannot decode them into a canvas. They fall through to
|
||||
* `compressPhoto` returning the original untouched.
|
||||
* Formats we try to re-encode. HEIC/HEIF is included (since 2026-10-20):
|
||||
* iPhones send HEIC, Safari on iOS/macOS can decode it into a canvas, and
|
||||
* WFDASH's AI and most desktop browsers can't read HEIC at all — the first
|
||||
* HEIC invoice photos went unread. Where the browser can't decode HEIC
|
||||
* (Chrome), loadImage fails and the original is uploaded as before.
|
||||
*/
|
||||
const REENCODABLE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||
const REENCODABLE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif']);
|
||||
|
||||
function isHeic(file: File): boolean {
|
||||
return /^image\/hei[cf]/i.test(file.type) || /\.(heic|heif)$/i.test(file.name);
|
||||
}
|
||||
|
||||
function loadImage(url: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -62,8 +66,10 @@ function loadImage(url: string): Promise<HTMLImageElement> {
|
|||
* "the best file we can produce", so the happy path stays simple.
|
||||
*/
|
||||
export async function compressPhoto(file: File): Promise<File> {
|
||||
if (file.size <= SKIP_BELOW_BYTES) return file;
|
||||
if (!REENCODABLE_TYPES.has(file.type)) return file;
|
||||
const heic = isHeic(file);
|
||||
// A HEIC is always converted, however small: the point is the format.
|
||||
if (!heic && file.size <= SKIP_BELOW_BYTES) return file;
|
||||
if (!heic && !REENCODABLE_TYPES.has(file.type)) return file;
|
||||
|
||||
// Not available during SSR. Only ever called from a click handler in
|
||||
// the browser, but a guard is cheaper than a confusing crash.
|
||||
|
|
@ -81,10 +87,11 @@ export async function compressPhoto(file: File): Promise<File> {
|
|||
if (img.naturalWidth > 20000 || img.naturalHeight > 20000) return file;
|
||||
|
||||
const longest = Math.max(img.naturalWidth, img.naturalHeight);
|
||||
// Only shrink. Upscaling a small photo wastes bytes and detail.
|
||||
if (longest <= MAX_PHOTO_EDGE) return file;
|
||||
// Only shrink. Upscaling a small photo wastes bytes and detail. A
|
||||
// HEIC is still re-encoded (at its own size) to get a JPEG.
|
||||
if (longest <= MAX_PHOTO_EDGE && !heic) return file;
|
||||
|
||||
const scale = MAX_PHOTO_EDGE / longest;
|
||||
const scale = Math.min(1, MAX_PHOTO_EDGE / longest);
|
||||
const width = Math.max(1, Math.round(img.naturalWidth * scale));
|
||||
const height = Math.max(1, Math.round(img.naturalHeight * scale));
|
||||
|
||||
|
|
@ -106,8 +113,9 @@ export async function compressPhoto(file: File): Promise<File> {
|
|||
if (!blob) return file;
|
||||
|
||||
// If re-encoding somehow produced something bigger, keep the
|
||||
// original rather than making the upload worse.
|
||||
if (blob.size >= file.size) return file;
|
||||
// original rather than making the upload worse — except for HEIC,
|
||||
// where a readable JPEG beats a smaller unreadable file.
|
||||
if (!heic && blob.size >= file.size) return file;
|
||||
|
||||
const stem = file.name.replace(/\.[^.]+$/, '') || 'pickup';
|
||||
return new File([blob], `${stem}.jpg`, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue