Refactor canvas image storage to use IndexedDB

Images used in canvas layers are now stored in a dedicated 'CanvasImages' object store in IndexedDB, referenced by unique imageId. The Canvas class and db.js were updated to support saving, loading, and removing images by imageId, improving performance and scalability. Legacy imageSrc handling is preserved for backward compatibility, and the database schema version was incremented to 2 to support the new store.
This commit is contained in:
Dariusz L
2025-06-25 09:02:28 +02:00
parent c3cc33c711
commit 0fc64df279
2 changed files with 262 additions and 94 deletions

View File

@@ -1,6 +1,14 @@
import {getCanvasState, setCanvasState, removeCanvasState} from "./db.js"; import {getCanvasState, setCanvasState, removeCanvasState, saveImage, getImage, removeImage} from "./db.js";
import {MaskTool} from "./Mask_tool.js"; import {MaskTool} from "./Mask_tool.js";
// Prosta funkcja generująca UUID
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
export class Canvas { export class Canvas {
constructor(node, widget) { constructor(node, widget) {
this.node = node; this.node = node;
@@ -83,6 +91,10 @@ export class Canvas {
this.redoStack = []; this.redoStack = [];
this.historyLimit = 100; this.historyLimit = 100;
this.saveTimeout = null; // Timer dla debouncingu zapisu do IndexedDB
this.lastSavedStateSignature = null; // Sygnatura ostatniego zapisanego stanu
this.imageCache = new Map(); // Pamięć podręczna dla obrazów (imageId -> imageSrc)
// this.saveState(); // Wywołanie przeniesione do loadInitialState // this.saveState(); // Wywołanie przeniesione do loadInitialState
} }
@@ -110,23 +122,77 @@ export class Canvas {
const imagePromises = savedState.layers.map((layerData, index) => { const imagePromises = savedState.layers.map((layerData, index) => {
return new Promise((resolve) => { return new Promise((resolve) => {
if (layerData.imageSrc) { if (layerData.imageId) {
console.log(`Layer ${index}: Loading image from data:URL...`); console.log(`Layer ${index}: Loading image with id: ${layerData.imageId}`);
const img = new Image(); // Sprawdź, czy obraz jest już w pamięci podręcznej
img.onload = () => { if (this.imageCache.has(layerData.imageId)) {
console.log(`Layer ${index}: Image loaded successfully.`); console.log(`Layer ${index}: Image found in cache.`);
const newLayer = {...layerData, image: img}; const imageSrc = this.imageCache.get(layerData.imageId);
delete newLayer.imageSrc; const img = new Image();
resolve(newLayer); img.onload = () => {
}; console.log(`Layer ${index}: Image loaded successfully.`);
img.onerror = () => { const newLayer = {...layerData, image: img};
console.error(`Layer ${index}: Failed to load image from src.`); delete newLayer.imageId;
resolve(newLayer);
};
img.onerror = () => {
console.error(`Layer ${index}: Failed to load image from src.`);
resolve(null);
};
img.src = imageSrc;
} else {
// Wczytaj obraz z IndexedDB
getImage(layerData.imageId).then(imageSrc => {
if (imageSrc) {
console.log(`Layer ${index}: Loading image from data:URL...`);
const img = new Image();
img.onload = () => {
console.log(`Layer ${index}: Image loaded successfully.`);
this.imageCache.set(layerData.imageId, imageSrc); // Zapisz w pamięci podręcznej jako imageSrc
const newLayer = {...layerData, image: img};
delete newLayer.imageId;
resolve(newLayer);
};
img.onerror = () => {
console.error(`Layer ${index}: Failed to load image from src.`);
resolve(null);
};
img.src = imageSrc;
} else {
console.error(`Layer ${index}: Image not found in IndexedDB.`);
resolve(null);
}
}).catch(err => {
console.error(`Layer ${index}: Error loading image from IndexedDB:`, err);
resolve(null);
});
}
} else if (layerData.imageSrc) {
// Obsługa starego formatu z imageSrc
console.log(`Layer ${index}: Found imageSrc, converting to new format with imageId.`);
const imageId = generateUUID();
saveImage(imageId, layerData.imageSrc).then(() => {
console.log(`Layer ${index}: Image saved to IndexedDB with id: ${imageId}`);
this.imageCache.set(imageId, layerData.imageSrc); // Zapisz w pamięci podręcznej jako imageSrc
const img = new Image();
img.onload = () => {
console.log(`Layer ${index}: Image loaded successfully from imageSrc.`);
const newLayer = {...layerData, image: img, imageId};
delete newLayer.imageSrc;
resolve(newLayer);
};
img.onerror = () => {
console.error(`Layer ${index}: Failed to load image from imageSrc.`);
resolve(null);
};
img.src = layerData.imageSrc;
}).catch(err => {
console.error(`Layer ${index}: Error saving image to IndexedDB:`, err);
resolve(null); resolve(null);
}; });
img.src = layerData.imageSrc;
} else { } else {
console.log(`Layer ${index}: No imageSrc found, resolving layer data.`); console.error(`Layer ${index}: No imageId or imageSrc found, skipping layer.`);
resolve({...layerData}); resolve(null); // Pomiń warstwy bez obrazu
} }
}); });
}); });
@@ -135,9 +201,14 @@ export class Canvas {
this.layers = loadedLayers.filter(l => l !== null); this.layers = loadedLayers.filter(l => l !== null);
console.log(`Loaded ${this.layers.length} layers.`); console.log(`Loaded ${this.layers.length} layers.`);
if (this.layers.length === 0) {
console.warn("No valid layers loaded, state may be corrupted.");
return false;
}
this.updateSelectionAfterHistory(); this.updateSelectionAfterHistory();
this.render(); this.render();
console.log("Canvas state loaded successfully from localStorage for node", this.node.id); console.log("Canvas state loaded successfully from IndexedDB for node", this.node.id);
return true; return true;
} catch (e) { } catch (e) {
console.error("Error loading canvas state from IndexedDB:", e); console.error("Error loading canvas state from IndexedDB:", e);
@@ -146,34 +217,70 @@ export class Canvas {
} }
} }
async saveStateToDB() { async saveStateToDB(immediate = false) {
console.log("Attempting to save state to IndexedDB for node:", this.node.id); console.log("Preparing to save state to IndexedDB for node:", this.node.id);
if (!this.node.id) { if (!this.node.id) {
console.error("Node ID is not available for saving state to DB."); console.error("Node ID is not available for saving state to DB.");
return; return;
} }
try { // Oblicz sygnaturę obecnego stanu
const state = { const currentStateSignature = this.getStateSignature(this.layers);
layers: this.layers.map((layer, index) => { if (this.lastSavedStateSignature === currentStateSignature) {
const newLayer = {...layer}; console.log("State unchanged, skipping save to IndexedDB.");
if (layer.image instanceof HTMLImageElement) { return;
console.log(`Layer ${index}: Serializing image to data:URL.`); }
newLayer.imageSrc = layer.image.src;
} else { // Anuluj poprzedni timer, jeśli istnieje
console.log(`Layer ${index}: No HTMLImageElement found.`); if (this.saveTimeout) {
} clearTimeout(this.saveTimeout);
delete newLayer.image; }
return newLayer;
}), const saveFunction = async () => {
viewport: this.viewport, try {
width: this.width, const state = {
height: this.height, layers: await Promise.all(this.layers.map(async (layer, index) => {
}; const newLayer = {...layer};
await setCanvasState(this.node.id, state); if (layer.image instanceof HTMLImageElement) {
console.log("Canvas state saved to IndexedDB."); console.log(`Layer ${index}: Using imageId instead of serializing image.`);
} catch (e) { if (!layer.imageId) {
console.error("Error saving canvas state to IndexedDB:", e); // Jeśli obraz nie ma jeszcze imageId, zapisz go do IndexedDB
layer.imageId = generateUUID();
await saveImage(layer.imageId, layer.image.src);
this.imageCache.set(layer.imageId, layer.image.src); // Zapisz w pamięci podręcznej jako imageSrc
}
newLayer.imageId = layer.imageId;
} else if (!layer.imageId) {
console.error(`Layer ${index}: No image or imageId found, skipping layer.`);
return null; // Pomiń warstwy bez obrazu
}
delete newLayer.image;
return newLayer;
})),
viewport: this.viewport,
width: this.width,
height: this.height,
};
// Filtruj warstwy, które nie mają obrazu
state.layers = state.layers.filter(layer => layer !== null);
if (state.layers.length === 0) {
console.warn("No valid layers to save, skipping save to IndexedDB.");
return;
}
await setCanvasState(this.node.id, state);
console.log("Canvas state saved to IndexedDB.");
this.lastSavedStateSignature = currentStateSignature; // Zaktualizuj sygnaturę zapisanego stanu
} catch (e) {
console.error("Error saving canvas state to IndexedDB:", e);
}
};
if (immediate) {
// Wykonaj zapis natychmiast
await saveFunction();
} else {
// Zaplanuj zapis z opóźnieniem (debouncing)
this.saveTimeout = setTimeout(saveFunction, 1000); // Opóźnienie 1000 ms
} }
} }
@@ -199,8 +306,8 @@ export class Canvas {
getStateSignature(layers) { getStateSignature(layers) {
return JSON.stringify(layers.map(layer => { return JSON.stringify(layers.map(layer => {
const sig = {...layer}; const sig = {...layer};
if (sig.image instanceof HTMLImageElement) { if (sig.imageId) {
sig.imageSrc = sig.image.src; sig.imageId = sig.imageId; // Zachowaj imageId w sygnaturze
} }
delete sig.image; delete sig.image;
return sig; return sig;
@@ -440,22 +547,11 @@ export class Canvas {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = async () => {
const newLayer = { await this.addLayerWithImage(img, {
image: img,
x: this.lastMousePosition.x - img.width / 2, x: this.lastMousePosition.x - img.width / 2,
y: this.lastMousePosition.y - img.height / 2, y: this.lastMousePosition.y - img.height / 2,
width: img.width, });
height: img.height,
rotation: 0,
zIndex: this.layers.length,
blendMode: 'normal',
opacity: 1
};
this.layers.push(newLayer);
this.updateSelection([newLayer]);
this.render();
this.saveState();
}; };
img.src = event.target.result; img.src = event.target.result;
}; };
@@ -540,6 +636,7 @@ export class Canvas {
if (interactionEnded) { if (interactionEnded) {
this.saveState(); this.saveState();
this.saveStateToDB(true); // Zapisz stan natychmiast po zakończeniu interakcji
} }
} }
@@ -1086,12 +1183,18 @@ export class Canvas {
return Math.sqrt(Math.pow(x - handleX, 2) + Math.pow(y - handleY, 2)) <= handleRadius; return Math.sqrt(Math.pow(x - handleX, 2) + Math.pow(y - handleY, 2)) <= handleRadius;
} }
addLayer(image) { async addLayerWithImage(image, layerProps = {}) {
try { try {
console.log("Adding layer with image:", image); console.log("Adding layer with image:", image);
// Wygeneruj unikalny identyfikator dla obrazu i zapisz go do IndexedDB
const imageId = generateUUID();
await saveImage(imageId, image.src);
this.imageCache.set(imageId, image.src); // Zapisz w pamięci podręcznej jako imageSrc
const layer = { const layer = {
image: image, image: image,
imageId: imageId, // Dodaj imageId do warstwy
x: (this.width - image.width) / 2, x: (this.width - image.width) / 2,
y: (this.height - image.height) / 2, y: (this.height - image.height) / 2,
width: image.width, width: image.width,
@@ -1099,7 +1202,8 @@ export class Canvas {
rotation: 0, rotation: 0,
zIndex: this.layers.length, zIndex: this.layers.length,
blendMode: 'normal', blendMode: 'normal',
opacity: 1 opacity: 1,
...layerProps // Nadpisz domyślne właściwości, jeśli podano
}; };
this.layers.push(layer); this.layers.push(layer);
@@ -1108,14 +1212,28 @@ export class Canvas {
this.saveState(); this.saveState();
console.log("Layer added successfully"); console.log("Layer added successfully");
return layer;
} catch (error) { } catch (error) {
console.error("Error adding layer:", error); console.error("Error adding layer:", error);
throw error; throw error;
} }
} }
removeLayer(index) { async addLayer(image) {
return this.addLayerWithImage(image);
}
async removeLayer(index) {
if (index >= 0 && index < this.layers.length) { if (index >= 0 && index < this.layers.length) {
const layer = this.layers[index];
if (layer.imageId) {
// Usuń obraz z IndexedDB, jeśli nie jest używany przez inne warstwy
const isImageUsedElsewhere = this.layers.some((l, i) => i !== index && l.imageId === layer.imageId);
if (!isImageUsedElsewhere) {
await removeImage(layer.imageId);
this.imageCache.delete(layer.imageId); // Usuń z pamięci podręcznej
}
}
this.layers.splice(index, 1); this.layers.splice(index, 1);
this.selectedLayer = this.layers[this.layers.length - 1] || null; this.selectedLayer = this.layers[this.layers.length - 1] || null;
this.render(); this.render();
@@ -1595,6 +1713,9 @@ export class Canvas {
async saveToServer(fileName) { async saveToServer(fileName) {
// Zapisz stan do IndexedDB przed zapisem na serwer
await this.saveStateToDB(true);
return new Promise((resolve) => { return new Promise((resolve) => {
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
const maskCanvas = document.createElement('canvas'); const maskCanvas = document.createElement('canvas');
@@ -2133,26 +2254,18 @@ export class Canvas {
this.height / inputImage.height * 0.8 this.height / inputImage.height * 0.8
); );
const layer = { const layer = await this.addLayerWithImage(image, {
image: image,
x: (this.width - inputImage.width * scale) / 2, x: (this.width - inputImage.width * scale) / 2,
y: (this.height - inputImage.height * scale) / 2, y: (this.height - inputImage.height * scale) / 2,
width: inputImage.width * scale, width: inputImage.width * scale,
height: inputImage.height * scale, height: inputImage.height * scale,
rotation: 0, });
zIndex: this.layers.length
};
if (inputMask) { if (inputMask) {
layer.mask = inputMask.data; layer.mask = inputMask.data;
} }
this.layers.push(layer);
this.selectedLayer = layer;
this.render();
console.log("Layer added successfully"); console.log("Layer added successfully");
return true; return true;
} catch (error) { } catch (error) {
@@ -2512,22 +2625,12 @@ export class Canvas {
img.src = result.image_data; img.src = result.image_data;
}); });
const layer = { await this.addLayerWithImage(img, {
image: img,
x: 0, x: 0,
y: 0, y: 0,
width: this.width, width: this.width,
height: this.height, height: this.height,
rotation: 0, });
zIndex: this.layers.length,
blendMode: 'normal',
opacity: 1
};
this.layers.push(layer);
this.selectedLayers = [layer];
this.selectedLayer = layer;
this.render();
console.log("Latest image imported and placed on canvas successfully."); console.log("Latest image imported and placed on canvas successfully.");
return true; return true;
} else { } else {

View File

@@ -1,6 +1,7 @@
const DB_NAME = 'CanvasNodeDB'; const DB_NAME = 'CanvasNodeDB';
const STORE_NAME = 'CanvasState'; const STATE_STORE_NAME = 'CanvasState';
const DB_VERSION = 1; const IMAGE_STORE_NAME = 'CanvasImages';
const DB_VERSION = 2; // Zwiększono wersję, aby wymusić aktualizację schematu
let db; let db;
@@ -28,9 +29,13 @@ function openDB() {
request.onupgradeneeded = (event) => { request.onupgradeneeded = (event) => {
console.log("Upgrading IndexedDB..."); console.log("Upgrading IndexedDB...");
const db = event.target.result; const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) { if (!db.objectStoreNames.contains(STATE_STORE_NAME)) {
db.createObjectStore(STORE_NAME, {keyPath: 'id'}); db.createObjectStore(STATE_STORE_NAME, {keyPath: 'id'});
console.log("Object store created:", STORE_NAME); console.log("Object store created:", STATE_STORE_NAME);
}
if (!db.objectStoreNames.contains(IMAGE_STORE_NAME)) {
db.createObjectStore(IMAGE_STORE_NAME, {keyPath: 'imageId'});
console.log("Object store created:", IMAGE_STORE_NAME);
} }
}; };
}); });
@@ -40,8 +45,8 @@ export async function getCanvasState(id) {
console.log(`DB: Getting state for id: ${id}`); console.log(`DB: Getting state for id: ${id}`);
const db = await openDB(); const db = await openDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly'); const transaction = db.transaction([STATE_STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME); const store = transaction.objectStore(STATE_STORE_NAME);
const request = store.get(id); const request = store.get(id);
request.onerror = (event) => { request.onerror = (event) => {
@@ -60,8 +65,8 @@ export async function setCanvasState(id, state) {
console.log(`DB: Setting state for id: ${id}`); console.log(`DB: Setting state for id: ${id}`);
const db = await openDB(); const db = await openDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite'); const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME); const store = transaction.objectStore(STATE_STORE_NAME);
const request = store.put({id, state}); const request = store.put({id, state});
request.onerror = (event) => { request.onerror = (event) => {
@@ -80,8 +85,8 @@ export async function removeCanvasState(id) {
console.log(`DB: Removing state for id: ${id}`); console.log(`DB: Removing state for id: ${id}`);
const db = await openDB(); const db = await openDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite'); const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME); const store = transaction.objectStore(STATE_STORE_NAME);
const request = store.delete(id); const request = store.delete(id);
request.onerror = (event) => { request.onerror = (event) => {
@@ -96,12 +101,72 @@ export async function removeCanvasState(id) {
}); });
} }
export async function saveImage(imageId, imageSrc) {
console.log(`DB: Saving image with id: ${imageId}`);
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(IMAGE_STORE_NAME);
const request = store.put({imageId, imageSrc});
request.onerror = (event) => {
console.error("DB: Error saving image:", event.target.error);
reject("Error saving image.");
};
request.onsuccess = () => {
console.log(`DB: Image saved successfully for id: ${imageId}`);
resolve();
};
});
}
export async function getImage(imageId) {
console.log(`DB: Getting image with id: ${imageId}`);
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
const store = transaction.objectStore(IMAGE_STORE_NAME);
const request = store.get(imageId);
request.onerror = (event) => {
console.error("DB: Error getting image:", event.target.error);
reject("Error getting image.");
};
request.onsuccess = (event) => {
console.log(`DB: Get image success for id: ${imageId}`, event.target.result ? 'found' : 'not found');
resolve(event.target.result ? event.target.result.imageSrc : null);
};
});
}
export async function removeImage(imageId) {
console.log(`DB: Removing image with id: ${imageId}`);
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(IMAGE_STORE_NAME);
const request = store.delete(imageId);
request.onerror = (event) => {
console.error("DB: Error removing image:", event.target.error);
reject("Error removing image.");
};
request.onsuccess = () => {
console.log(`DB: Remove image success for id: ${imageId}`);
resolve();
};
});
}
export async function clearAllCanvasStates() { export async function clearAllCanvasStates() {
console.log("DB: Clearing all canvas states..."); console.log("DB: Clearing all canvas states...");
const db = await openDB(); const db = await openDB();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite'); const transaction = db.transaction([STATE_STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME); const store = transaction.objectStore(STATE_STORE_NAME);
const request = store.clear(); const request = store.clear();
request.onerror = (event) => { request.onerror = (event) => {