gallery optimizations

This commit is contained in:
Vladimir Mandic
2024-03-23 14:46:55 -04:00
parent a8677f5d85
commit dc9c60e174
10 changed files with 221 additions and 35 deletions
+82 -9
View File
@@ -3,6 +3,8 @@
let ws;
let url;
let currentImage;
let pruneImagesTimer;
let outstanding = 0;
const el = {
folders: undefined,
files: undefined,
@@ -49,6 +51,32 @@ class GalleryFolder extends HTMLElement {
}
}
async function createThumb(img) {
const height = opts.extra_networks_card_size;
const width = opts.browser_fixed_width ? opts.extra_networks_card_size : 0;
const canvas = document.createElement('canvas');
const scaleY = height / img.height;
const scaleX = width > 0 ? width / img.width : scaleY;
const scale = Math.min(scaleX, scaleY);
const scaledWidth = img.width * scale;
const scaledHeight = img.height * scale;
canvas.width = scaledWidth;
canvas.height = scaledHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, scaledWidth, scaledHeight);
const dataURL = canvas.toDataURL('image/jpeg', 0.5);
return dataURL;
}
async function delayFetchThumb(fn) {
while (outstanding > 10) await new Promise((resolve) => setTimeout(resolve, 50)); // eslint-disable-line no-promise-executor-return
outstanding++;
const res = await fetch(`/sdapi/v1/browser/thumb?file=${fn}`, { priority: 'low' });
const json = await res.json();
outstanding--;
return json;
}
class GalleryFile extends HTMLElement {
constructor({ folder, file, size, mtime }) {
super();
@@ -60,6 +88,7 @@ class GalleryFile extends HTMLElement {
this.exif = '';
this.width = 0;
this.height = 0;
this.src = `${this.folder}/${this.name}`;
this.shadow = this.attachShadow({ mode: 'open' });
}
@@ -81,22 +110,54 @@ class GalleryFile extends HTMLElement {
}
`;
const cache = opts.browser_cache ? await idbGet(this.hash) : undefined;
this.shadow.appendChild(style);
const img = document.createElement('img');
img.className = 'gallery-file';
img.loading = 'lazy';
img.title = `Folder: ${this.folder}\nFile: ${this.name}\nSize: ${this.size.toLocaleString()} bytes\nModified: ${this.mtime.toLocaleString()}`;
img.onload = async () => {
this.width = img.naturalWidth;
this.height = img.naturalHeight;
img.title += `\nResolution: ${this.width} x ${this.height}`;
this.title = img.title;
// let exif = await getExif(img);
// if (exif) this.exif = exif.replaceAll('<br>', '\n').replace(/<\/?[^>]+(>|$)/g, "");
if (!cache && opts.browser_cache) {
if ((this.width === 0) || (this.height === 0)) { // fetch thumb failed so we use actual image
this.width = img.naturalWidth;
this.height = img.naturalHeight;
}
}
};
img.src = `file=${this.folder}/${this.name}`;
if (cache) {
img.src = cache.img;
this.exif = cache.exif;
this.width = cache.width;
this.height = cache.height;
} else {
try {
const json = await delayFetchThumb(this.src);
img.src = json.data;
this.exif = json.exif;
this.width = json.width;
this.height = json.height;
await idbAdd({
hash: this.hash,
folder: this.folder,
file: this.name,
size: this.size,
mtime: this.mtime,
width: this.width,
height: this.height,
src: this.src,
exif: this.exif,
img: img.src,
// exif: await getExif(img), // alternative client-side exif
// img: await createThumb(img), // alternative client-side thumb
});
} catch (err) { // thumb fetch failed so assign actual image
img.src = `file=${this.src}`;
}
}
img.onclick = () => {
currentImage = `${this.folder}/${this.name}`;
currentImage = this.src;
el.btnSend.click();
};
this.title = img.title;
@@ -147,7 +208,6 @@ async function gallerySearch(evt) {
const op = match[2].trim();
let val = match[3].trim();
if (key === 'mtime') val = new Date(val);
console.log('HERE', key, op, val, f[key]);
if (((op === '=') && (f[key] === val)) || ((op === '>') && (f[key] > val)) || ((op === '<') && (f[key] < val))) {
f.style.display = 'unset';
numFound++;
@@ -229,6 +289,7 @@ async function fetchFiles(evt) { // fetch file-by-file list over websockets
const t0 = performance.now();
let t1 = performance.now();
let lastDir;
let fragment = document.createDocumentFragment();
ws.onmessage = (event) => { // time is 20% list 80% create item
numFiles++;
t1 = performance.now();
@@ -246,11 +307,16 @@ async function fetchFiles(evt) { // fetch file-by-file list over websockets
el.files.appendChild(sep);
}
const file = new GalleryFile(json);
el.files.appendChild(file);
fragment.appendChild(file);
if (numFiles % 100 === 0) {
el.files.appendChild(fragment);
fragment = document.createDocumentFragment();
}
el.status.innerText = `Folder | ${evt.target.name} | ${numFiles.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`;
}
};
ws.onclose = (event) => {
el.files.appendChild(fragment);
// log('gallery ws file enum', event);
};
ws.onerror = (event) => {
@@ -259,6 +325,10 @@ async function fetchFiles(evt) { // fetch file-by-file list over websockets
ws.send(evt.target.name);
}
async function pruneImages() {
// TODO replace img.src with placeholder for images that are not visible
}
async function galleryVisible() {
// if (el.folders.children.length > 0) return;
const res = await fetch('/sdapi/v1/browser/folders');
@@ -270,9 +340,12 @@ async function galleryVisible() {
const f = new GalleryFolder(folder);
el.folders.appendChild(f);
}
pruneImagesTimer = setInterval(pruneImages, 1000);
}
async function galleryHidden() { /**/ }
async function galleryHidden() {
if (pruneImagesTimer) clearInterval(pruneImagesTimer);
}
async function galleryObserve() { // triggered on gradio change to monitor when ui gets sufficiently constructed
log('initBrowser');
+7 -1
View File
@@ -55,7 +55,13 @@ function modalKeyHandler(event) {
}
async function getExif(el) {
const exif = await window.exifr.parse(el, { userComment: true });
let exif = '';
try {
exif = await window.exifr.parse(el, { userComment: true });
} catch (e) {
log('getExif', el, e);
return exif;
}
// let html = `<b>Image</b> <a href="${el.src}" target="_blank">${el.src}</a> <b>Size</b> ${el.naturalWidth}x${el.naturalHeight}<br>`;
let html = '';
let params;
+83
View File
@@ -0,0 +1,83 @@
let db;
async function initIndexDB() {
async function createDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('SDNext');
request.onerror = (evt) => reject(evt);
request.onsuccess = (evt) => {
db = evt.target.result;
const countAll = db
.transaction(['thumbs'], 'readwrite')
.objectStore('thumbs')
.count();
countAll.onsuccess = () => log('initIndexDB', countAll.result);
resolve();
};
request.onupgradeneeded = (evt) => {
db = evt.target.result;
const store = db.createObjectStore('thumbs', { keyPath: 'hash' });
store.createIndex('hash', 'hash', { unique: true });
const index = store.index('hash');
resolve();
};
});
}
if (!db) await createDB();
}
async function add(record) {
if (!db) return null;
return new Promise((resolve, reject) => {
const request = db
.transaction(['thumbs'], 'readwrite')
.objectStore('thumbs')
.add(record);
request.onsuccess = (evt) => resolve(evt);
request.onerror = (evt) => reject(evt);
});
}
async function del(hash) {
if (!db) return null;
return new Promise((resolve, reject) => {
const request = db
.transaction(['thumbs'], 'readwrite')
.objectStore('thumbs')
.delete(hash);
request.onsuccess = (evt) => resolve(evt);
request.onerror = (evt) => reject(evt);
});
}
async function get(hash) {
if (!db) return null;
return new Promise((resolve, reject) => {
const request = db
.transaction(['thumbs'], 'readwrite')
.objectStore('thumbs')
.get(hash);
request.onsuccess = () => resolve(request.result);
request.onerror = (evt) => reject(evt);
});
}
async function put(record) {
if (!db) return null;
return new Promise((resolve, reject) => {
const request = db
.transaction(['thumbs'], 'readwrite')
.objectStore('thumbs')
.put(record);
request.onsuccess = (evt) => resolve(evt);
request.onerror = (evt) => reject(evt);
});
}
window.idbAdd = add;
window.idbDel = del;
window.idbGet = get;
window.idbPut = put;
onUiLoaded(initIndexDB);