Perf improvements for HEIC photos
All checks were successful
Build Docker image / build (push) Successful in 1m57s
Build Docker image / verify (push) Successful in 27s
Build Docker image / deploy (push) Successful in 3s

This commit is contained in:
Roman Krček
2025-07-18 11:11:20 +02:00
parent 94e34fbc75
commit fa6b8312c6
3 changed files with 114 additions and 41 deletions

86
package-lock.json generated
View File

@@ -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": {

View File

@@ -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"

View File

@@ -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 @@
</p>
</div>
{:else}
<div class="p-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div class="p-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{#each photos as photo, index}
<PhotoCard
{photo}