Skip to content

Commit 5f04dc7

Browse files
authored
ui: Add HEIC/HEIF image support (#24137)
* 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
1 parent aedb2a5 commit 5f04dc7

6 files changed

Lines changed: 84 additions & 4 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export const MEGAPIXELS_TO_PIXELS = 1_000_000;
2+
3+
export const HEIC_JPEG_QUALITY = 0.85;

tools/ui/src/lib/constants/supported-file-types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export const IMAGE_FILE_TYPES = {
6363
[FileTypeImage.SVG]: {
6464
extensions: [FileExtensionImage.SVG],
6565
mimeTypes: [MimeTypeImage.SVG]
66+
},
67+
[FileTypeImage.HEIC]: {
68+
extensions: [FileExtensionImage.HEIC, FileExtensionImage.HEIF],
69+
mimeTypes: [MimeTypeImage.HEIC, MimeTypeImage.HEIF]
6670
}
6771
} as const;
6872

tools/ui/src/lib/enums/files.enums.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ export enum FileTypeImage {
2525
PNG = 'png',
2626
GIF = 'gif',
2727
WEBP = 'webp',
28-
SVG = 'svg'
28+
SVG = 'svg',
29+
HEIC = 'heic',
30+
HEIF = 'heif'
2931
}
3032

3133
export enum FileTypeAudio {
@@ -90,7 +92,9 @@ export enum FileExtensionImage {
9092
PNG = '.png',
9193
GIF = '.gif',
9294
WEBP = '.webp',
93-
SVG = '.svg'
95+
SVG = '.svg',
96+
HEIC = '.heic',
97+
HEIF = '.heif'
9498
}
9599

96100
export enum FileExtensionAudio {
@@ -205,7 +209,9 @@ export enum MimeTypeImage {
205209
WEBP = 'image/webp',
206210
SVG = 'image/svg+xml',
207211
ICO = 'image/x-icon',
208-
ICO_MICROSOFT = 'image/vnd.microsoft.icon'
212+
ICO_MICROSOFT = 'image/vnd.microsoft.icon',
213+
HEIC = 'image/heic',
214+
HEIF = 'image/heif'
209215
}
210216

211217
export enum MimeTypeText {

tools/ui/src/lib/utils/file-type.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export function getFileTypeCategory(mimeType: string): FileTypeCategory | null {
3030
case MimeTypeImage.GIF:
3131
case MimeTypeImage.WEBP:
3232
case MimeTypeImage.SVG:
33+
case MimeTypeImage.HEIC:
34+
case MimeTypeImage.HEIF:
3335
return FileTypeCategory.IMAGE;
3436

3537
// Audio
@@ -118,6 +120,8 @@ export function getFileTypeCategoryByExtension(filename: string): FileTypeCatego
118120
case FileExtensionImage.GIF:
119121
case FileExtensionImage.WEBP:
120122
case FileExtensionImage.SVG:
123+
case FileExtensionImage.HEIC:
124+
case FileExtensionImage.HEIF:
121125
return FileTypeCategory.IMAGE;
122126

123127
// Audio
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { MimeTypeImage } from '$lib/enums';
2+
import { HEIC_JPEG_QUALITY } from '$lib/constants/image-size';
3+
4+
// heic requires a relatively large decoder, in order to reduce primary bundle size
5+
// we lazily load this decoder from a CDN when needed, and cache it for future conversions
6+
const HEIC_TO_CDN_URL = 'https://cdn.jsdelivr.net/npm/heic-to@1.5.2/dist/heic-to.js';
7+
8+
interface HeicToModule {
9+
heicTo(args: { blob: Blob; type: string; quality?: number }): Promise<Blob>;
10+
}
11+
12+
let modulePromise: Promise<HeicToModule> | null = null;
13+
14+
/**
15+
* Lazily load the heic-to decoder from the CDN and cache it
16+
* @returns Promise resolving to the heic-to module
17+
*/
18+
function getHeicTo(): Promise<HeicToModule> {
19+
if (!modulePromise) {
20+
modulePromise = import(/* @vite-ignore */ HEIC_TO_CDN_URL) as Promise<HeicToModule>;
21+
}
22+
23+
return modulePromise;
24+
}
25+
26+
/**
27+
* Convert a HEIC/HEIF file to a compressed JPEG data URL
28+
* @param file - The HEIC/HEIF file to convert
29+
* @returns Promise resolving to JPEG data URL
30+
*/
31+
export async function heicFileToJpegDataURL(file: File | Blob): Promise<string> {
32+
const { heicTo } = await getHeicTo();
33+
const jpegBlob = await heicTo({
34+
blob: file,
35+
type: MimeTypeImage.JPEG,
36+
quality: HEIC_JPEG_QUALITY
37+
});
38+
39+
return new Promise((resolve, reject) => {
40+
const reader = new FileReader();
41+
reader.onload = () => resolve(reader.result as string);
42+
reader.onerror = () => reject(reader.error);
43+
reader.readAsDataURL(jpegBlob);
44+
});
45+
}
46+
47+
/**
48+
* Check if a MIME type represents a HEIC/HEIF image
49+
* @param mimeType - The MIME type to check
50+
* @returns True if the MIME type is image/heic or image/heif
51+
*/
52+
export function isHeicMimeType(mimeType: string): boolean {
53+
const normalized = mimeType.trim().toLowerCase();
54+
55+
return normalized === MimeTypeImage.HEIC || normalized === MimeTypeImage.HEIF;
56+
}

tools/ui/src/lib/utils/process-uploaded-files.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
22
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
3+
import { heicFileToJpegDataURL, isHeicMimeType } from './heic-to-jpeg';
34
import { FileTypeCategory } from '$lib/enums';
45
import { SETTINGS_KEYS } from '$lib/constants';
56
import { modelsStore } from '$lib/stores/models.svelte';
@@ -68,7 +69,7 @@ export async function processFilesToChatUploaded(
6869
if (getFileTypeCategory(file.type) === FileTypeCategory.IMAGE) {
6970
let preview = await readFileAsDataURL(file);
7071

71-
// Normalize SVG and WebP to PNG in previews
72+
// Normalize SVG and WebP to PNG, and HEIC to compressed JPEG, in previews
7273
if (isSvgMimeType(file.type)) {
7374
try {
7475
preview = await svgBase64UrlToPngDataURL(preview);
@@ -81,6 +82,13 @@ export async function processFilesToChatUploaded(
8182
} catch (err) {
8283
console.error('Failed to convert WebP to PNG:', err);
8384
}
85+
} else if (isHeicMimeType(file.type)) {
86+
try {
87+
preview = await heicFileToJpegDataURL(file);
88+
} catch (err) {
89+
console.error('Failed to convert HEIC to PNG:', err);
90+
continue;
91+
}
8492
}
8593

8694
results.push({ ...base, preview });

0 commit comments

Comments
 (0)