]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
ui: Add HEIC/HEIF image support (#24137)
authorNicolas Mowen <redacted>
Sun, 14 Jun 2026 18:42:16 +0000 (12:42 -0600)
committerGitHub <redacted>
Sun, 14 Jun 2026 18:42:16 +0000 (20:42 +0200)
* Add boilerplate for file types

* Add heic-to and implement conversion

* Load heic library from CDN

* Use jpg instead of png for conversion

* Move const to constants file

tools/ui/src/lib/constants/image-size.ts
tools/ui/src/lib/constants/supported-file-types.ts
tools/ui/src/lib/enums/files.enums.ts
tools/ui/src/lib/utils/file-type.ts
tools/ui/src/lib/utils/heic-to-jpeg.ts [new file with mode: 0644]
tools/ui/src/lib/utils/process-uploaded-files.ts

index bcc1da08b1936719c9e3dd34cd495a410d486fcc..8a7f921fa019a0cec1a6b1e28b7fc30a3ecd5ec9 100644 (file)
@@ -1 +1,3 @@
 export const MEGAPIXELS_TO_PIXELS = 1_000_000;
+
+export const HEIC_JPEG_QUALITY = 0.85;
index 4141161548d43acac8691a1be92e71ec92b3d799..cbe780aa57e7e623aff9076cb83c3cf0c849f3a3 100644 (file)
@@ -63,6 +63,10 @@ export const IMAGE_FILE_TYPES = {
        [FileTypeImage.SVG]: {
                extensions: [FileExtensionImage.SVG],
                mimeTypes: [MimeTypeImage.SVG]
+       },
+       [FileTypeImage.HEIC]: {
+               extensions: [FileExtensionImage.HEIC, FileExtensionImage.HEIF],
+               mimeTypes: [MimeTypeImage.HEIC, MimeTypeImage.HEIF]
        }
 } as const;
 
index 8008a1040b2f8eb12778714ef2b609e06ab50256..dbe07ea58677a6d6a77d4a17c36a47314daa8d4d 100644 (file)
@@ -25,7 +25,9 @@ export enum FileTypeImage {
        PNG = 'png',
        GIF = 'gif',
        WEBP = 'webp',
-       SVG = 'svg'
+       SVG = 'svg',
+       HEIC = 'heic',
+       HEIF = 'heif'
 }
 
 export enum FileTypeAudio {
@@ -90,7 +92,9 @@ export enum FileExtensionImage {
        PNG = '.png',
        GIF = '.gif',
        WEBP = '.webp',
-       SVG = '.svg'
+       SVG = '.svg',
+       HEIC = '.heic',
+       HEIF = '.heif'
 }
 
 export enum FileExtensionAudio {
@@ -205,7 +209,9 @@ export enum MimeTypeImage {
        WEBP = 'image/webp',
        SVG = 'image/svg+xml',
        ICO = 'image/x-icon',
-       ICO_MICROSOFT = 'image/vnd.microsoft.icon'
+       ICO_MICROSOFT = 'image/vnd.microsoft.icon',
+       HEIC = 'image/heic',
+       HEIF = 'image/heif'
 }
 
 export enum MimeTypeText {
index d14efbc3505659c12b4e12e3aa09cdabffdf6a6d..e61564174e8d78bec875a6b1f2880268f80ee5e0 100644 (file)
@@ -30,6 +30,8 @@ export function getFileTypeCategory(mimeType: string): FileTypeCategory | null {
                case MimeTypeImage.GIF:
                case MimeTypeImage.WEBP:
                case MimeTypeImage.SVG:
+               case MimeTypeImage.HEIC:
+               case MimeTypeImage.HEIF:
                        return FileTypeCategory.IMAGE;
 
                // Audio
@@ -118,6 +120,8 @@ export function getFileTypeCategoryByExtension(filename: string): FileTypeCatego
                case FileExtensionImage.GIF:
                case FileExtensionImage.WEBP:
                case FileExtensionImage.SVG:
+               case FileExtensionImage.HEIC:
+               case FileExtensionImage.HEIF:
                        return FileTypeCategory.IMAGE;
 
                // Audio
diff --git a/tools/ui/src/lib/utils/heic-to-jpeg.ts b/tools/ui/src/lib/utils/heic-to-jpeg.ts
new file mode 100644 (file)
index 0000000..4ccb992
--- /dev/null
@@ -0,0 +1,56 @@
+import { MimeTypeImage } from '$lib/enums';
+import { HEIC_JPEG_QUALITY } from '$lib/constants/image-size';
+
+// heic requires a relatively large decoder, in order to reduce primary bundle size
+// we lazily load this decoder from a CDN when needed, and cache it for future conversions
+const HEIC_TO_CDN_URL = 'https://cdn.jsdelivr.net/npm/heic-to@1.5.2/dist/heic-to.js';
+
+interface HeicToModule {
+       heicTo(args: { blob: Blob; type: string; quality?: number }): Promise<Blob>;
+}
+
+let modulePromise: Promise<HeicToModule> | null = null;
+
+/**
+ * Lazily load the heic-to decoder from the CDN and cache it
+ * @returns Promise resolving to the heic-to module
+ */
+function getHeicTo(): Promise<HeicToModule> {
+       if (!modulePromise) {
+               modulePromise = import(/* @vite-ignore */ HEIC_TO_CDN_URL) as Promise<HeicToModule>;
+       }
+
+       return modulePromise;
+}
+
+/**
+ * Convert a HEIC/HEIF file to a compressed JPEG data URL
+ * @param file - The HEIC/HEIF file to convert
+ * @returns Promise resolving to JPEG data URL
+ */
+export async function heicFileToJpegDataURL(file: File | Blob): Promise<string> {
+       const { heicTo } = await getHeicTo();
+       const jpegBlob = await heicTo({
+               blob: file,
+               type: MimeTypeImage.JPEG,
+               quality: HEIC_JPEG_QUALITY
+       });
+
+       return new Promise((resolve, reject) => {
+               const reader = new FileReader();
+               reader.onload = () => resolve(reader.result as string);
+               reader.onerror = () => reject(reader.error);
+               reader.readAsDataURL(jpegBlob);
+       });
+}
+
+/**
+ * Check if a MIME type represents a HEIC/HEIF image
+ * @param mimeType - The MIME type to check
+ * @returns True if the MIME type is image/heic or image/heif
+ */
+export function isHeicMimeType(mimeType: string): boolean {
+       const normalized = mimeType.trim().toLowerCase();
+
+       return normalized === MimeTypeImage.HEIC || normalized === MimeTypeImage.HEIF;
+}
index d0110f6cbcf126be4f8556731f8d717871e66e1f..91b619cfb925fe10f19576e2d44bbe98f580177d 100644 (file)
@@ -1,5 +1,6 @@
 import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
 import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
+import { heicFileToJpegDataURL, isHeicMimeType } from './heic-to-jpeg';
 import { FileTypeCategory } from '$lib/enums';
 import { SETTINGS_KEYS } from '$lib/constants';
 import { modelsStore } from '$lib/stores/models.svelte';
@@ -68,7 +69,7 @@ export async function processFilesToChatUploaded(
                        if (getFileTypeCategory(file.type) === FileTypeCategory.IMAGE) {
                                let preview = await readFileAsDataURL(file);
 
-                               // Normalize SVG and WebP to PNG in previews
+                               // Normalize SVG and WebP to PNG, and HEIC to compressed JPEG, in previews
                                if (isSvgMimeType(file.type)) {
                                        try {
                                                preview = await svgBase64UrlToPngDataURL(preview);
@@ -81,6 +82,13 @@ export async function processFilesToChatUploaded(
                                        } catch (err) {
                                                console.error('Failed to convert WebP to PNG:', err);
                                        }
+                               } else if (isHeicMimeType(file.type)) {
+                                       try {
+                                               preview = await heicFileToJpegDataURL(file);
+                                       } catch (err) {
+                                               console.error('Failed to convert HEIC to PNG:', err);
+                                               continue;
+                                       }
                                }
 
                                results.push({ ...base, preview });