From 833657ad68e924e6448baf853c71af1eddfa1155 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sun, 10 May 2026 21:16:32 +0200 Subject: [PATCH] feat(journey): convert HEIC/HEIF uploads to JPEG for cross-platform compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HEIC is an Apple-only format not recognised as an image by many browsers and platforms. heic-to (lazy-loaded) now converts HEIC/HEIF files to JPEG before upload in both the gallery and entry editor photo pickers. Embedded metadata (EXIF, GPS) may be lost during conversion — documented in the Journey Journal wiki page. --- client/package-lock.json | 7 +++++++ client/package.json | 1 + client/src/pages/JourneyDetailPage.tsx | 7 +++++-- client/src/utils/convertHeic.ts | 11 +++++++++++ wiki/Journey-Journal.md | 1 + 5 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 client/src/utils/convertHeic.ts diff --git a/client/package-lock.json b/client/package-lock.json index f829027e..7ddbaaa2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,6 +11,7 @@ "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", "dexie": "^4.4.2", + "heic-to": "^1.4.2", "leaflet": "^1.9.4", "lucide-react": "^0.344.0", "mapbox-gl": "^3.22.0", @@ -5827,6 +5828,12 @@ "dev": true, "license": "MIT" }, + "node_modules/heic-to": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.4.2.tgz", + "integrity": "sha512-y69thwxfNcEm2Vk8lbOD/cMabnvMJyOREfJYiCHcXCDqlfcPyJoBhyRc8+iDe1B95LRfpbTOpzxzY1xbRkdwBA==", + "license": "LGPL-3.0" + }, "node_modules/hsl-to-hex": { "version": "1.0.0", "license": "MIT", diff --git a/client/package.json b/client/package.json index ab8585b7..80def49d 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", "dexie": "^4.4.2", + "heic-to": "^1.4.2", "leaflet": "^1.9.4", "lucide-react": "^0.344.0", "mapbox-gl": "^3.22.0", diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index e7aa51f9..360247e7 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react' import { formatLocationName } from '../utils/formatters' +import { normalizeImageFiles } from '../utils/convertHeic' import { createPortal } from 'react-dom' import { useParams, useNavigate } from 'react-router-dom' import { useJourneyStore } from '../store/journeyStore' @@ -1027,8 +1028,9 @@ function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick, if (!files?.length) return setGalleryUploading(true) try { + const normalized = await normalizeImageFiles(files) const formData = new FormData() - for (const f of files) formData.append('photos', f) + for (const f of normalized) formData.append('photos', f) await journeyApi.uploadGalleryPhotos(journeyId, formData) toast.success(t('journey.photosUploaded', { count: files.length })) onRefresh() @@ -2265,7 +2267,8 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa if (!files?.length) return // Queue files locally until Save so cancel/close actually discards. This // keeps photo behavior consistent with text fields — no silent persistence. - setPendingFiles(prev => [...prev, ...Array.from(files)]) + const normalized = await normalizeImageFiles(files) + setPendingFiles(prev => [...prev, ...normalized]) } return ( diff --git a/client/src/utils/convertHeic.ts b/client/src/utils/convertHeic.ts new file mode 100644 index 00000000..70172207 --- /dev/null +++ b/client/src/utils/convertHeic.ts @@ -0,0 +1,11 @@ +export async function normalizeImageFile(file: File): Promise { + const { isHeic, heicTo } = await import('heic-to') + if (!(await isHeic(file))) return file + const blob = await heicTo({ blob: file, type: 'image/jpeg', quality: 0.92 }) + const jpegName = file.name.replace(/\.(heic|heif)$/i, '.jpg') + return new File([blob], jpegName, { type: 'image/jpeg' }) +} + +export async function normalizeImageFiles(files: FileList | File[]): Promise { + return Promise.all(Array.from(files).map(normalizeImageFile)) +} diff --git a/wiki/Journey-Journal.md b/wiki/Journey-Journal.md index 5dd39145..ceea0ca7 100644 --- a/wiki/Journey-Journal.md +++ b/wiki/Journey-Journal.md @@ -37,6 +37,7 @@ Each entry corresponds to a day in your journey. The entry editor provides: - **Weather** — choose one of six values: Sunny, Partly cloudy, Cloudy, Rainy, Stormy, Cold. - **Photos** — attach photos to the entry. The first photo becomes the card thumbnail in list views. + > **Note on HEIC files:** HEIC is an Apple-only format that many browsers and platforms do not recognise as an image. To ensure broad compatibility, HEIC/HEIF files are automatically converted to JPEG before upload. This conversion may result in the loss of embedded metadata (EXIF data such as GPS coordinates, camera information, etc.). - **Pros / Cons** — optional verdict cards. Add items to a **Pros** list (thumbs-up) or a **Cons** list (thumbs-down) to summarise what you loved or what could have been better. These are stored in the `pros_cons.pros` and `pros_cons.cons` arrays on the entry. - **Tags** — free-form labels (e.g. "hidden gem", "best meal"). - **Location** — pin the entry to a map location.