Files
2025-12-01 05:26:54 +00:00

317 lines
8.9 KiB
JavaScript

/**
* Image Cache Module
* Handles image loading, caching, base64 conversion, and session storage
*/
/**
* Load image from URL as base64 data URL
* Handles both data URLs and regular URLs with cache-busting
*/
export async function loadImageAsBase64(url) {
try {
// Check if it's a data URL (starts with 'data:')
if (url.startsWith('data:')) {
// For data URLs, no cache-busting is needed, just process directly
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
return blobToDataUrl(blob);
} else {
// For regular URLs, add cache-busting parameter
const urlObj = new URL(url, window.location.href);
urlObj.searchParams.set('t', Date.now());
const cacheBustedUrl = urlObj.toString();
const response = await fetch(cacheBustedUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
return blobToDataUrl(blob);
}
} catch (e) {
console.error(`Failed to load image from ${url}:`, e);
return null;
}
}
/**
* Convert blob to data URL
*/
function blobToDataUrl(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
/**
* Convert blob to base64 string (without data URL prefix)
*/
export function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
/**
* Simple hash function to compare images
* Used to detect if an image has changed
*/
export async function simpleHash(str) {
// Convert input to string if it's not already
if (typeof str !== 'string') {
str = String(str);
}
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash.toString();
}
/**
* Save ref_image to cache with fallback to sessionStorage
* @param {string} base64Data - Base64 encoded image data
* @param {string} name - Filename for the image (default: 'ref_image.png')
* @param {Object} options - Configuration options
* @param {boolean} options.skipSessionCache - Skip writing to shared session key
* @returns {Promise<boolean>} - Success status
*/
export async function saveRefImageToCache(
base64Data,
name = 'ref_image.png',
options = {}
) {
try {
// Create a cache key to track the current ref_image
const currentHash = await simpleHash(base64Data);
if (!options.skipSessionCache) {
safeSetSessionItem('spline-editor-ref-image-hash', currentHash);
}
// Try to save the file to the bg folder via the backend API
try {
const formData = new FormData();
formData.append('image', `data:image/png;base64,${base64Data}`);
formData.append('name', name);
const response = await fetch('/wanvideowrapper_qq/save_ref_image', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
if (result.success) {
// Save to sessionStorage as backup (unless skipped)
if (!options.skipSessionCache) {
safeSetSessionItem(
'spline-editor-cached-ref-image',
JSON.stringify({
base64: base64Data,
type: 'image/png',
name,
hash: currentHash,
timestamp: Date.now()
})
);
}
return true;
} else {
console.error('Backend error saving ref image:', result.error);
// Fallback to sessionStorage only
if (!options.skipSessionCache) {
safeSetSessionItem(
'spline-editor-cached-ref-image',
JSON.stringify({
base64: base64Data,
type: 'image/png',
name,
hash: currentHash,
timestamp: Date.now()
})
);
}
return false;
}
} else {
console.error('Failed to save ref image via API:', response.status);
// Fallback to sessionStorage only
if (!options.skipSessionCache) {
safeSetSessionItem(
'spline-editor-cached-ref-image',
JSON.stringify({
base64: base64Data,
type: 'image/png',
name,
hash: currentHash,
timestamp: Date.now()
})
);
}
return false;
}
} catch (error) {
console.warn('API save failed, using sessionStorage fallback:', error);
// Fallback to sessionStorage if API fails
if (!options.skipSessionCache) {
safeSetSessionItem(
'spline-editor-cached-ref-image',
JSON.stringify({
base64: base64Data,
type: 'image/png',
name,
hash: currentHash,
timestamp: Date.now()
})
);
}
return true;
}
} catch (e) {
console.error('Failed to cache ref image:', e);
return false;
}
}
/**
* Check if cached ref_image exists and is different from current
* @param {string|null} currentBase64 - Current base64 image data
* @returns {Promise<Object|null>} - Cached image object or null
*/
export async function getCachedRefImage(currentBase64 = null) {
try {
const cachedData = sessionStorage.getItem('spline-editor-cached-ref-image');
if (!cachedData) {
return null;
}
const parsed = JSON.parse(cachedData);
// Guard: only honor caches explicitly saved for bg image overlays
if (parsed?.name && parsed.name !== 'bg_image.png') {
return null;
}
// If current base64 is provided, check if it's different from cached
if (currentBase64) {
const currentHash = await simpleHash(currentBase64);
if (currentHash === parsed.hash) {
// Same image, no need to update
return parsed;
} else {
// Different image, update cache
await saveRefImageToCache(currentBase64, 'bg_image.png');
return JSON.parse(
sessionStorage.getItem('spline-editor-cached-ref-image')
);
}
}
return parsed;
} catch (e) {
console.error('Failed to get cached ref image:', e);
return null;
}
}
/**
* Load cached ref_image as base64 data URL
* Tries file system first, then falls back to sessionStorage
* @returns {Promise<string|null>} - Data URL or null if not found
*/
export async function loadCachedRefImageAsBase64() {
try {
// First try to load from actual file in bg folder
const timestamp = Date.now();
const refImageUrl = new URL(
`../bg/bg_image.png?t=${timestamp}`,
import.meta.url
).href;
const response = await fetch(refImageUrl);
if (response.ok) {
const blob = await response.blob();
return blobToDataUrl(blob);
}
// If file doesn't exist, try to load from sessionStorage cache
const cachedData = await getCachedRefImage();
if (cachedData) {
return `data:${cachedData.type};base64,${cachedData.base64}`;
}
return null;
} catch (e) {
console.error('Failed to load cached ref image as base64:', e);
// Fallback to sessionStorage cache
try {
const cachedData = await getCachedRefImage();
if (cachedData) {
return `data:${cachedData.type};base64,${cachedData.base64}`;
}
} catch (fallbackError) {
console.error('Fallback cache also failed:', fallbackError);
}
return null;
}
}
/**
* Safe sessionStorage setter to avoid quota errors
* Silently fails if quota exceeded or not available
*/
export function safeSetSessionItem(key, value) {
try {
sessionStorage.setItem(key, value);
} catch (e) {
// Silently ignore quota exceeded or unsupported errors
}
}
/**
* Load and save image data from a source
* Returns both data URL and base64 separately
*/
export async function loadImageAndExtractBase64(imageUrl) {
try {
const dataUrl = await loadImageAsBase64(imageUrl);
if (!dataUrl) {
return { dataUrl: null, base64: null };
}
const base64 = dataUrl.startsWith('data:')
? dataUrl.split(',')[1]
: dataUrl;
return { dataUrl, base64 };
} catch (e) {
console.error('Error loading and extracting image:', e);
return { dataUrl: null, base64: null };
}
}
/**
* Retrieve image dimensions from data URL
*/
export async function getImageDimensions(dataUrl) {
return new Promise(resolve => {
const img = new Image();
img.onload = () => resolve({ width: img.width, height: img.height });
img.onerror = () => resolve({ width: 1, height: 1 });
img.src = dataUrl;
});
}