Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions tools/ui/src/lib/constants/supported-file-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
12 changes: 9 additions & 3 deletions tools/ui/src/lib/enums/files.enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export enum FileTypeImage {
PNG = 'png',
GIF = 'gif',
WEBP = 'webp',
SVG = 'svg'
SVG = 'svg',
HEIC = 'heic',
HEIF = 'heif'
}

export enum FileTypeAudio {
Expand Down Expand Up @@ -90,7 +92,9 @@ export enum FileExtensionImage {
PNG = '.png',
GIF = '.gif',
WEBP = '.webp',
SVG = '.svg'
SVG = '.svg',
HEIC = '.heic',
HEIF = '.heif'
}

export enum FileExtensionAudio {
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions tools/ui/src/lib/utils/file-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions tools/ui/src/lib/utils/heic-to-png.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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<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 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<string> {
const { heicTo } = await getHeicTo();
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;
}
10 changes: 9 additions & 1 deletion tools/ui/src/lib/utils/process-uploaded-files.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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 heicFileToPngDataURL(file);
} catch (err) {
console.error('Failed to convert HEIC to PNG:', err);
continue;
}
}

results.push({ ...base, preview });
Expand Down