From 1970c0684bd3b6ceca798b9dcb96e259b8a3b111 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 4 Jun 2026 12:06:26 -0600 Subject: [PATCH 1/3] Add boilerplate for file types --- tools/ui/src/lib/constants/supported-file-types.ts | 4 ++++ tools/ui/src/lib/enums/files.enums.ts | 12 +++++++++--- tools/ui/src/lib/utils/file-type.ts | 4 ++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tools/ui/src/lib/constants/supported-file-types.ts b/tools/ui/src/lib/constants/supported-file-types.ts index 4141161548d..cbe780aa57e 100644 --- a/tools/ui/src/lib/constants/supported-file-types.ts +++ b/tools/ui/src/lib/constants/supported-file-types.ts @@ -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; diff --git a/tools/ui/src/lib/enums/files.enums.ts b/tools/ui/src/lib/enums/files.enums.ts index 8008a1040b2..dbe07ea5867 100644 --- a/tools/ui/src/lib/enums/files.enums.ts +++ b/tools/ui/src/lib/enums/files.enums.ts @@ -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 { diff --git a/tools/ui/src/lib/utils/file-type.ts b/tools/ui/src/lib/utils/file-type.ts index d14efbc3505..e61564174e8 100644 --- a/tools/ui/src/lib/utils/file-type.ts +++ b/tools/ui/src/lib/utils/file-type.ts @@ -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 From 04b7e03ecf301cea8a2b2d7d755cc8828c289337 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 4 Jun 2026 12:19:04 -0600 Subject: [PATCH 2/3] Add heic-to and implement conversion --- tools/ui/package-lock.json | 8 +++++ tools/ui/package.json | 1 + tools/ui/src/lib/utils/heic-to-png.ts | 32 +++++++++++++++++++ .../src/lib/utils/process-uploaded-files.ts | 9 +++++- 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tools/ui/src/lib/utils/heic-to-png.ts diff --git a/tools/ui/package-lock.json b/tools/ui/package-lock.json index ffd4f6ca029..3038863314a 100644 --- a/tools/ui/package-lock.json +++ b/tools/ui/package-lock.json @@ -38,6 +38,7 @@ "eslint-plugin-storybook": "10.2.4", "eslint-plugin-svelte": "3.15.0", "globals": "16.3.0", + "heic-to": "1.5.2", "highlight.js": "11.11.1", "http-server": "14.1.1", "mdast": "3.0.0", @@ -6658,6 +6659,13 @@ "he": "bin/he" } }, + "node_modules/heic-to": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.5.2.tgz", + "integrity": "sha512-8Fns+lZHAWmz5U5IUxDeXKwIf3foBoKNPLxxFY4B0MkLjNuomEIHCoDbDE+x/llFK3NCEO1cu4+n3iUKY+Svmw==", + "dev": true, + "license": "LGPL-3.0" + }, "node_modules/highlight.js": { "version": "11.11.1", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", diff --git a/tools/ui/package.json b/tools/ui/package.json index 4f5ef4d64fa..b44eea52804 100644 --- a/tools/ui/package.json +++ b/tools/ui/package.json @@ -53,6 +53,7 @@ "eslint-plugin-storybook": "10.2.4", "eslint-plugin-svelte": "3.15.0", "globals": "16.3.0", + "heic-to": "1.5.2", "highlight.js": "11.11.1", "http-server": "14.1.1", "mdast": "3.0.0", diff --git a/tools/ui/src/lib/utils/heic-to-png.ts b/tools/ui/src/lib/utils/heic-to-png.ts new file mode 100644 index 00000000000..abb966b0181 --- /dev/null +++ b/tools/ui/src/lib/utils/heic-to-png.ts @@ -0,0 +1,32 @@ +import { heicTo } from 'heic-to'; +import { MimeTypeImage } from '$lib/enums'; + +/** + * Convert a HEIC/HEIF file to a PNG data URL + * @param file - The HEIC/HEIF file to convert + * @returns Promise resolving to PNG data URL + */ +export async function heicFileToPngDataURL(file: File | Blob): Promise { + const pngBlob = await heicTo({ + blob: file, + type: MimeTypeImage.PNG + }); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(pngBlob); + }); +} + +/** + * 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; +} diff --git a/tools/ui/src/lib/utils/process-uploaded-files.ts b/tools/ui/src/lib/utils/process-uploaded-files.ts index d0110f6cbcf..0f782bcd8f4 100644 --- a/tools/ui/src/lib/utils/process-uploaded-files.ts +++ b/tools/ui/src/lib/utils/process-uploaded-files.ts @@ -1,5 +1,6 @@ import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png'; import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png'; +import { heicFileToPngDataURL, isHeicMimeType } from './heic-to-png'; 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, WebP and HEIC to PNG in previews if (isSvgMimeType(file.type)) { try { preview = await svgBase64UrlToPngDataURL(preview); @@ -81,6 +82,12 @@ export async function processFilesToChatUploaded( } catch (err) { console.error('Failed to convert WebP to PNG:', err); } + } else if (isHeicMimeType(file.type)) { + try { + preview = await heicFileToPngDataURL(file); + } catch (err) { + console.error('Failed to convert HEIC to PNG:', err); + } } results.push({ ...base, preview }); From 3d21058cfcf6262a4f3f2ebb282bdd8da94636a9 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 5 Jun 2026 06:41:50 -0600 Subject: [PATCH 3/3] Load heic library from CDN --- tools/ui/package-lock.json | 8 ----- tools/ui/package.json | 1 - tools/ui/src/lib/utils/heic-to-png.ts | 29 +++++++++++++++---- .../src/lib/utils/process-uploaded-files.ts | 1 + 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/tools/ui/package-lock.json b/tools/ui/package-lock.json index 3038863314a..ffd4f6ca029 100644 --- a/tools/ui/package-lock.json +++ b/tools/ui/package-lock.json @@ -38,7 +38,6 @@ "eslint-plugin-storybook": "10.2.4", "eslint-plugin-svelte": "3.15.0", "globals": "16.3.0", - "heic-to": "1.5.2", "highlight.js": "11.11.1", "http-server": "14.1.1", "mdast": "3.0.0", @@ -6659,13 +6658,6 @@ "he": "bin/he" } }, - "node_modules/heic-to": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.5.2.tgz", - "integrity": "sha512-8Fns+lZHAWmz5U5IUxDeXKwIf3foBoKNPLxxFY4B0MkLjNuomEIHCoDbDE+x/llFK3NCEO1cu4+n3iUKY+Svmw==", - "dev": true, - "license": "LGPL-3.0" - }, "node_modules/highlight.js": { "version": "11.11.1", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", diff --git a/tools/ui/package.json b/tools/ui/package.json index b44eea52804..4f5ef4d64fa 100644 --- a/tools/ui/package.json +++ b/tools/ui/package.json @@ -53,7 +53,6 @@ "eslint-plugin-storybook": "10.2.4", "eslint-plugin-svelte": "3.15.0", "globals": "16.3.0", - "heic-to": "1.5.2", "highlight.js": "11.11.1", "http-server": "14.1.1", "mdast": "3.0.0", diff --git a/tools/ui/src/lib/utils/heic-to-png.ts b/tools/ui/src/lib/utils/heic-to-png.ts index abb966b0181..6cc4b111876 100644 --- a/tools/ui/src/lib/utils/heic-to-png.ts +++ b/tools/ui/src/lib/utils/heic-to-png.ts @@ -1,16 +1,35 @@ -import { heicTo } from 'heic-to'; import { MimeTypeImage } from '$lib/enums'; +// 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 }): Promise; +} + +let modulePromise: Promise | 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 { + if (!modulePromise) { + modulePromise = import(/* @vite-ignore */ HEIC_TO_CDN_URL) as Promise; + } + + return modulePromise; +} + /** * Convert a HEIC/HEIF file to a PNG data URL * @param file - The HEIC/HEIF file to convert * @returns Promise resolving to PNG data URL */ export async function heicFileToPngDataURL(file: File | Blob): Promise { - const pngBlob = await heicTo({ - blob: file, - type: MimeTypeImage.PNG - }); + const { heicTo } = await getHeicTo(); + const pngBlob = await heicTo({ blob: file, type: MimeTypeImage.PNG }); return new Promise((resolve, reject) => { const reader = new FileReader(); diff --git a/tools/ui/src/lib/utils/process-uploaded-files.ts b/tools/ui/src/lib/utils/process-uploaded-files.ts index 0f782bcd8f4..913bf7aa4ea 100644 --- a/tools/ui/src/lib/utils/process-uploaded-files.ts +++ b/tools/ui/src/lib/utils/process-uploaded-files.ts @@ -87,6 +87,7 @@ export async function processFilesToChatUploaded( preview = await heicFileToPngDataURL(file); } catch (err) { console.error('Failed to convert HEIC to PNG:', err); + continue; } }