From fa6b8312c6ef770b5d48a66eccc183e34340a7ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Roman=20Kr=C4=8Dek?=
Date: Fri, 18 Jul 2025 11:11:20 +0200
Subject: [PATCH] Perf improvements for HEIC photos
---
package-lock.json | 86 +++++++++++++++-----
package.json | 2 +-
src/lib/components/wizard/StepGallery.svelte | 67 ++++++++++-----
3 files changed, 114 insertions(+), 41 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 789df5e..2bd2fe4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"@types/google.accounts": "^0.0.17",
"@types/uuid": "^10.0.0",
"fontkit": "^2.0.4",
- "heic2any": "^0.0.4",
+ "heic-convert": "^2.1.0",
"idb": "^8.0.3",
"pdf-lib": "^1.17.1",
"uuid": "^11.1.0"
@@ -1084,12 +1084,6 @@
"tslib": "^2.8.0"
}
},
- "node_modules/@swc/helpers/node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
"node_modules/@tailwindcss/node": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
@@ -2256,11 +2250,31 @@
"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/heic-convert": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/heic-convert/-/heic-convert-2.1.0.tgz",
+ "integrity": "sha512-1qDuRvEHifTVAj3pFIgkqGgJIr0M3X7cxEPjEp0oG4mo8GFjq99DpCo8Eg3kg17Cy0MTjxpFdoBHOatj7ZVKtg==",
+ "license": "ISC",
+ "dependencies": {
+ "heic-decode": "^2.0.0",
+ "jpeg-js": "^0.4.4",
+ "pngjs": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/heic-decode": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/heic-decode/-/heic-decode-2.1.0.tgz",
+ "integrity": "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A==",
+ "license": "ISC",
+ "dependencies": {
+ "libheif-js": "^1.19.8"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
},
"node_modules/idb": {
"version": "8.0.3",
@@ -2320,6 +2334,12 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jpeg-js": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
+ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -2330,6 +2350,15 @@
"node": ">=6"
}
},
+ "node_modules/libheif-js": {
+ "version": "1.19.8",
+ "resolved": "https://registry.npmjs.org/libheif-js/-/libheif-js-1.19.8.tgz",
+ "integrity": "sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ==",
+ "license": "LGPL-3.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/lightningcss": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
@@ -2752,6 +2781,12 @@
"tslib": "^1.11.1"
}
},
+ "node_modules/pdf-lib/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -2772,6 +2807,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pngjs": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
+ "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -3149,9 +3193,9 @@
}
},
"node_modules/svelte": {
- "version": "5.36.6",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.36.6.tgz",
- "integrity": "sha512-THFCC4RzI45IHXVruiG/9RpyhapGdsMTm0lpd/1NEBkNUszGsuTET0ifiaYLfP2P6Y6Y4LRdrzloRhZGIIzphA==",
+ "version": "5.36.7",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.36.7.tgz",
+ "integrity": "sha512-QsaFAxL1PZvo9hwaN+x7Sq2U8oJARmsEuM8TEZVy98nx5D5IKzRi8FKkPvmOx9NXScSYnItDGLErBBn/ieIn2A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3175,9 +3219,9 @@
}
},
"node_modules/svelte-check": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.2.tgz",
- "integrity": "sha512-1+31EOYZ7NKN0YDMKusav2hhEoA51GD9Ws6o//0SphMT0ve9mBTsTUEX7OmDMadUP3KjNHsSKtJrqdSaD8CrGQ==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.0.tgz",
+ "integrity": "sha512-Iz8dFXzBNAM7XlEIsUjUGQhbEE+Pvv9odb9+0+ITTgFWZBGeJRRYqHUUglwe2EkLD5LIsQaAc4IUJyvtKuOO5w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3283,9 +3327,9 @@
"license": "MIT"
},
"node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
diff --git a/package.json b/package.json
index 016b904..94441e3 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
"@types/google.accounts": "^0.0.17",
"@types/uuid": "^10.0.0",
"fontkit": "^2.0.4",
- "heic2any": "^0.0.4",
+ "heic-convert": "^2.1.0",
"idb": "^8.0.3",
"pdf-lib": "^1.17.1",
"uuid": "^11.1.0"
diff --git a/src/lib/components/wizard/StepGallery.svelte b/src/lib/components/wizard/StepGallery.svelte
index 8973807..cd03d31 100644
--- a/src/lib/components/wizard/StepGallery.svelte
+++ b/src/lib/components/wizard/StepGallery.svelte
@@ -129,28 +129,59 @@
blob = await response.blob();
}
- // Check for HEIC/HEIF format and convert if necessary
+ // Check for HEIC/HEIF format. If so, start conversion but don't block.
if (
blob.type === 'image/heic' ||
blob.type === 'image/heif' ||
photo.url.toLowerCase().endsWith('.heic')
) {
- console.log(`Converting HEIC image for ${photo.name}...`);
- try {
- const { default: heic2any } = await import('heic2any');
- const conversionResult = await heic2any({
- blob,
- toType: 'image/jpeg',
- quality: 0.9
- });
- blob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult;
- console.log(`Successfully converted HEIC for ${photo.name}`);
- } catch (e) {
- console.error(`Failed to convert HEIC image for ${photo.name}:`, e);
- throw new Error('HEIC conversion failed');
- }
+ console.log(`HEIC detected for ${photo.name}. Starting conversion in background.`);
+ photo.status = 'loading'; // Visually indicate something is happening
+ // Don't await this, let it run in the background
+ convertHeicPhoto(index, blob);
+ return; // End loadPhoto here for HEIC, conversion will handle the rest
}
+ // For non-HEIC images, proceed as normal
+ await processLoadedBlob(index, blob);
+
+ } catch (error) {
+ console.error(`Failed to load photo for ${photo.name}:`, error);
+ photo.status = 'error';
+ }
+ }
+
+ async function convertHeicPhoto(index: number, blob: Blob) {
+ const photo = photos[index];
+ try {
+ console.log(`Converting HEIC with heic-convert for ${photo.name}...`);
+
+ // Dynamically import the browser-specific version of the library
+ const { default: convert } = await import('heic-convert/browser');
+
+ const inputBuffer = await blob.arrayBuffer();
+ const outputBuffer = await convert({
+ buffer: new Uint8Array(inputBuffer), // heic-convert expects a Uint8Array
+ format: 'JPEG',
+ quality: 0.9
+ });
+
+ const convertedBlob = new Blob([outputBuffer], { type: 'image/jpeg' });
+
+ console.log(`Successfully converted HEIC for ${photo.name}`);
+
+ // Now that it's converted, process it like any other image
+ await processLoadedBlob(index, convertedBlob);
+
+ } catch (e) {
+ console.error(`Failed to convert HEIC image for ${photo.name}:`, e);
+ photo.status = 'error';
+ }
+ }
+
+ async function processLoadedBlob(index: number, blob: Blob) {
+ const photo = photos[index];
+ try {
const objectUrl = createImageObjectUrl(blob);
// Test if image loads properly
@@ -184,11 +215,9 @@
// Automatically run face detection to generate crop
await detectFaceForPhoto(index);
} catch (error) {
- console.error(`Failed to load photo for ${photo.name}:`, error);
+ console.error(`Failed to process blob for ${photo.name}:`, error);
photo.status = 'error';
}
-
- // No need to reassign photos array with $state reactivity
}
async function detectFaceForPhoto(index: number) {
@@ -441,7 +470,7 @@
{:else}
-
+
{#each photos as photo, index}