fix/refac: heic image handling

This commit is contained in:
Timothy Jaeryang Baek 2025-07-03 00:04:13 +04:00
parent bb506edcc3
commit e1efd54a70
5 changed files with 145 additions and 81 deletions

7
package-lock.json generated
View file

@ -43,6 +43,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"focus-trap": "^7.6.4", "focus-trap": "^7.6.4",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"heic2any": "^0.0.4",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"html-entities": "^2.5.3", "html-entities": "^2.5.3",
"html2canvas-pro": "^1.5.11", "html2canvas-pro": "^1.5.11",
@ -7315,6 +7316,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/heic2any": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz",
"integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==",
"license": "MIT"
},
"node_modules/heimdalljs": { "node_modules/heimdalljs": {
"version": "0.2.6", "version": "0.2.6",
"resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz",

View file

@ -87,6 +87,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"focus-trap": "^7.6.4", "focus-trap": "^7.6.4",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"heic2any": "^0.0.4",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"html-entities": "^2.5.3", "html-entities": "^2.5.3",
"html2canvas-pro": "^1.5.11", "html2canvas-pro": "^1.5.11",

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import heic2any from 'heic2any';
import { tick, getContext, onMount, onDestroy } from 'svelte'; import { tick, getContext, onMount, onDestroy } from 'svelte';
@ -78,7 +79,7 @@
}; };
const inputFilesHandler = async (inputFiles) => { const inputFilesHandler = async (inputFiles) => {
inputFiles.forEach((file) => { inputFiles.forEach(async (file) => {
console.info('Processing file:', { console.info('Processing file:', {
name: file.name, name: file.name,
type: file.type, type: file.type,
@ -102,43 +103,50 @@
return; return;
} }
if ( if (file['type'].startsWith('image/')) {
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type']) const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
) { // Quick shortcut so we dont do unnecessary work.
const settingsCompression = settings?.imageCompression ?? false;
const configWidth = config?.file?.image_compression?.width ?? null;
const configHeight = config?.file?.image_compression?.height ?? null;
// If neither settings nor config wants compression, return original URL.
if (!settingsCompression && !configWidth && !configHeight) {
return imageUrl;
}
// Default to null (no compression unless set)
let width = null;
let height = null;
// If user/settings want compression, pick their preferred size.
if (settingsCompression) {
width = settings?.imageCompressionSize?.width ?? null;
height = settings?.imageCompressionSize?.height ?? null;
}
// Apply config limits as an upper bound if any
if (configWidth && (width === null || width > configWidth)) {
width = configWidth;
}
if (configHeight && (height === null || height > configHeight)) {
height = configHeight;
}
// Do the compression if required
if (width || height) {
return await compressImage(imageUrl, width, height);
}
return imageUrl;
};
let reader = new FileReader(); let reader = new FileReader();
reader.onload = async (event) => { reader.onload = async (event) => {
let imageUrl = event.target.result; let imageUrl = event.target.result;
if ( // Compress the image if settings or config require it
($settings?.imageCompression ?? false) || imageUrl = await compressImageHandler(imageUrl, $settings, $config);
($config?.file?.image_compression?.width ?? null) ||
($config?.file?.image_compression?.height ?? null)
) {
let width = null;
let height = null;
if ($settings?.imageCompression ?? false) {
width = $settings?.imageCompressionSize?.width ?? null;
height = $settings?.imageCompressionSize?.height ?? null;
}
if (
($config?.file?.image_compression?.width ?? null) ||
($config?.file?.image_compression?.height ?? null)
) {
if (width > ($config?.file?.image_compression?.width ?? null)) {
width = $config?.file?.image_compression?.width ?? null;
}
if (height > ($config?.file?.image_compression?.height ?? null)) {
height = $config?.file?.image_compression?.height ?? null;
}
}
if (width || height) {
imageUrl = await compressImage(imageUrl, width, height);
}
}
files = [ files = [
...files, ...files,
@ -149,7 +157,11 @@
]; ];
}; };
reader.readAsDataURL(file); reader.readAsDataURL(
file['type'] === 'image/heic'
? await heic2any({ blob: file, toType: 'image/jpeg' })
: file
);
} else { } else {
uploadFileHandler(file); uploadFileHandler(file);
} }

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { marked } from 'marked'; import { marked } from 'marked';
import heic2any from 'heic2any';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@ -320,7 +321,7 @@
return; return;
} }
inputFiles.forEach((file) => { inputFiles.forEach(async (file) => {
console.log('Processing file:', { console.log('Processing file:', {
name: file.name, name: file.name,
type: file.type, type: file.type,
@ -344,46 +345,53 @@
return; return;
} }
if ( if (file['type'].startsWith('image/')) {
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
) {
if (visionCapableModels.length === 0) { if (visionCapableModels.length === 0) {
toast.error($i18n.t('Selected model(s) do not support image inputs')); toast.error($i18n.t('Selected model(s) do not support image inputs'));
return; return;
} }
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
// Quick shortcut so we dont do unnecessary work.
const settingsCompression = settings?.imageCompression ?? false;
const configWidth = config?.file?.image_compression?.width ?? null;
const configHeight = config?.file?.image_compression?.height ?? null;
// If neither settings nor config wants compression, return original URL.
if (!settingsCompression && !configWidth && !configHeight) {
return imageUrl;
}
// Default to null (no compression unless set)
let width = null;
let height = null;
// If user/settings want compression, pick their preferred size.
if (settingsCompression) {
width = settings?.imageCompressionSize?.width ?? null;
height = settings?.imageCompressionSize?.height ?? null;
}
// Apply config limits as an upper bound if any
if (configWidth && (width === null || width > configWidth)) {
width = configWidth;
}
if (configHeight && (height === null || height > configHeight)) {
height = configHeight;
}
// Do the compression if required
if (width || height) {
return await compressImage(imageUrl, width, height);
}
return imageUrl;
};
let reader = new FileReader(); let reader = new FileReader();
reader.onload = async (event) => { reader.onload = async (event) => {
let imageUrl = event.target.result; let imageUrl = event.target.result;
if ( imageUrl = await compressImageHandler(imageUrl, $settings, $config);
($settings?.imageCompression ?? false) ||
($config?.file?.image_compression?.width ?? null) ||
($config?.file?.image_compression?.height ?? null)
) {
let width = null;
let height = null;
if ($settings?.imageCompression ?? false) {
width = $settings?.imageCompressionSize?.width ?? null;
height = $settings?.imageCompressionSize?.height ?? null;
}
if (
($config?.file?.image_compression?.width ?? null) ||
($config?.file?.image_compression?.height ?? null)
) {
if (width > ($config?.file?.image_compression?.width ?? null)) {
width = $config?.file?.image_compression?.width ?? null;
}
if (height > ($config?.file?.image_compression?.height ?? null)) {
height = $config?.file?.image_compression?.height ?? null;
}
}
if (width || height) {
imageUrl = await compressImage(imageUrl, width, height);
}
}
files = [ files = [
...files, ...files,
@ -393,7 +401,11 @@
} }
]; ];
}; };
reader.readAsDataURL(file); reader.readAsDataURL(
file['type'] === 'image/heic'
? await heic2any({ blob: file, toType: 'image/jpeg' })
: file
);
} else { } else {
uploadFileHandler(file); uploadFileHandler(file);
} }

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getContext, onDestroy, onMount, tick } from 'svelte'; import { getContext, onDestroy, onMount, tick } from 'svelte';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import heic2any from 'heic2any';
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
@ -328,7 +329,7 @@
const inputFilesHandler = async (inputFiles) => { const inputFilesHandler = async (inputFiles) => {
console.log('Input files handler called with:', inputFiles); console.log('Input files handler called with:', inputFiles);
inputFiles.forEach((file) => { inputFiles.forEach(async (file) => {
console.log('Processing file:', { console.log('Processing file:', {
name: file.name, name: file.name,
type: file.type, type: file.type,
@ -352,21 +353,48 @@
return; return;
} }
if ( if (file['type'].startsWith('image/')) {
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type']) const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
) { // Quick shortcut so we dont do unnecessary work.
const settingsCompression = settings?.imageCompression ?? false;
const configWidth = config?.file?.image_compression?.width ?? null;
const configHeight = config?.file?.image_compression?.height ?? null;
// If neither settings nor config wants compression, return original URL.
if (!settingsCompression && !configWidth && !configHeight) {
return imageUrl;
}
// Default to null (no compression unless set)
let width = null;
let height = null;
// If user/settings want compression, pick their preferred size.
if (settingsCompression) {
width = settings?.imageCompressionSize?.width ?? null;
height = settings?.imageCompressionSize?.height ?? null;
}
// Apply config limits as an upper bound if any
if (configWidth && (width === null || width > configWidth)) {
width = configWidth;
}
if (configHeight && (height === null || height > configHeight)) {
height = configHeight;
}
// Do the compression if required
if (width || height) {
return await compressImage(imageUrl, width, height);
}
return imageUrl;
};
let reader = new FileReader(); let reader = new FileReader();
reader.onload = async (event) => { reader.onload = async (event) => {
let imageUrl = event.target.result; let imageUrl = event.target.result;
if ($settings?.imageCompression ?? false) { imageUrl = await compressImageHandler(imageUrl, $settings, $config);
const width = $settings?.imageCompressionSize?.width ?? null;
const height = $settings?.imageCompressionSize?.height ?? null;
if (width || height) {
imageUrl = await compressImage(imageUrl, width, height);
}
}
files = [ files = [
...files, ...files,
@ -377,7 +405,11 @@
]; ];
note.data.files = files; note.data.files = files;
}; };
reader.readAsDataURL(file); reader.readAsDataURL(
file['type'] === 'image/heic'
? await heic2any({ blob: file, toType: 'image/jpeg' })
: file
);
} else { } else {
uploadFileHandler(file); uploadFileHandler(file);
} }