refactor: centralize preview blob loading
This commit is contained in:
+11
-5
@@ -8,12 +8,13 @@ import { addStylesheet, getUrl, loadTemplate } from "./utils/ResourceManager.js"
|
||||
import { Canvas } from "./Canvas.js";
|
||||
import { clearAllCanvasStates, getCanvasState, setCanvasState } from "./db.js";
|
||||
import { createCanvas } from "./utils/CommonUtils.js";
|
||||
import { loadImage, loadImageFromBlob } from "./utils/ImageUtils.js";
|
||||
import { loadImageFromBlob } from "./utils/ImageUtils.js";
|
||||
import { createModuleLogger } from "./log_system/log_funcs.js";
|
||||
import { showErrorNotification, showSuccessNotification, showInfoNotification, showWarningNotification } from "./utils/NotificationUtils.js";
|
||||
import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js";
|
||||
import { exportCanvasImage } from "./utils/CanvasExportUtils.js";
|
||||
import { getFlattenedCanvasBlob } from "./utils/CanvasBlobUtils.js";
|
||||
import { loadPreviewImage } from "./utils/PreviewUtils.js";
|
||||
import { fetchMattingModelStatus } from "./utils/MattingUtils.js";
|
||||
import { setupSAMDetectorHook } from "./SAMDetectorIntegration.js";
|
||||
const log = createModuleLogger('Canvas_view');
|
||||
@@ -1140,10 +1141,12 @@ async function createCanvasWidget(node, widget, app) {
|
||||
if (blob) {
|
||||
// For large images, use blob URL for better performance
|
||||
if (blob.size > 2 * 1024 * 1024) { // 2MB threshold
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
void loadImage(blobUrl).then(img => {
|
||||
void loadPreviewImage(blob, {
|
||||
source: 'canvas',
|
||||
urlMode: 'object-url'
|
||||
}).then(img => {
|
||||
node.imgs = [img];
|
||||
log.debug(`Using blob URL for large image (${(blob.size / 1024 / 1024).toFixed(1)}MB): ${blobUrl.substring(0, 50)}...`);
|
||||
log.debug(`Using blob URL for large image (${(blob.size / 1024 / 1024).toFixed(1)}MB): ${img.src.substring(0, 50)}...`);
|
||||
// Clean up old blob URLs to prevent memory leaks
|
||||
if (node.imgs.length > 1) {
|
||||
const oldImg = node.imgs[0];
|
||||
@@ -1155,7 +1158,10 @@ async function createCanvasWidget(node, widget, app) {
|
||||
}
|
||||
else {
|
||||
// For smaller images, use data URI as before
|
||||
void loadImageFromBlob(blob).then(img => {
|
||||
void loadPreviewImage(blob, {
|
||||
source: 'canvas',
|
||||
urlMode: 'data-url'
|
||||
}).then(img => {
|
||||
node.imgs = [img];
|
||||
log.debug(`Using data URI for small image (${(blob.size / 1024).toFixed(1)}KB): ${img.src.substring(0, 50)}...`);
|
||||
}).catch(() => undefined);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { createModuleLogger } from "../log_system/log_funcs.js";
|
||||
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||
import { resolveCanvasBlob, supportsFlattenedCanvasBlob } from './CanvasBlobUtils.js';
|
||||
import { loadImage } from './ImageUtils.js';
|
||||
import { loadImage, loadImageFromBlob } from './ImageUtils.js';
|
||||
const log = createModuleLogger('PreviewUtils');
|
||||
async function loadPreviewImageFromBlob(blob, options) {
|
||||
export async function loadPreviewImage(blob, options) {
|
||||
const isCanvasSource = options.source === 'canvas';
|
||||
const previewUrl = URL.createObjectURL(blob);
|
||||
let previewImage;
|
||||
try {
|
||||
previewImage = await loadImage(previewUrl);
|
||||
if (options.urlMode === 'data-url') {
|
||||
return await loadImageFromBlob(blob);
|
||||
}
|
||||
const previewUrl = URL.createObjectURL(blob);
|
||||
return await loadImage(previewUrl);
|
||||
}
|
||||
catch (error) {
|
||||
const errorMessage = isCanvasSource
|
||||
@@ -19,6 +21,13 @@ async function loadPreviewImageFromBlob(blob, options) {
|
||||
? { error, blob: blob.size }
|
||||
: { error, blobSize: blob.size });
|
||||
}
|
||||
}
|
||||
async function loadPreviewForNode(blob, options) {
|
||||
const isCanvasSource = options.source === 'canvas';
|
||||
const previewImage = await loadPreviewImage(blob, {
|
||||
source: options.source,
|
||||
urlMode: 'object-url'
|
||||
});
|
||||
log.debug(isCanvasSource ? "Preview image loaded successfully" : "Preview image from blob loaded successfully", isCanvasSource
|
||||
? {
|
||||
width: previewImage.width,
|
||||
@@ -80,7 +89,7 @@ export const createPreviewFromCanvas = withErrorHandling(async function (canvas,
|
||||
if (!blob) {
|
||||
throw createValidationError("Failed to generate canvas blob for preview", { canvas, options });
|
||||
}
|
||||
return loadPreviewImageFromBlob(blob, {
|
||||
return loadPreviewForNode(blob, {
|
||||
source: 'canvas',
|
||||
node,
|
||||
updateNodeImages
|
||||
@@ -105,7 +114,7 @@ export const createPreviewFromBlob = withErrorHandling(async function (blob, nod
|
||||
updateNodeImages,
|
||||
hasNode: !!node
|
||||
});
|
||||
return loadPreviewImageFromBlob(blob, {
|
||||
return loadPreviewForNode(blob, {
|
||||
source: 'blob',
|
||||
node,
|
||||
updateNodeImages
|
||||
|
||||
+11
-5
@@ -14,12 +14,13 @@ import { addStylesheet, getUrl, loadTemplate } from "./utils/ResourceManager.js"
|
||||
import {Canvas} from "./Canvas.js";
|
||||
import {clearAllCanvasStates, getCanvasState, setCanvasState} from "./db.js";
|
||||
import {generateUniqueFileName, createCanvas} from "./utils/CommonUtils.js";
|
||||
import { loadImage, loadImageFromBlob } from "./utils/ImageUtils.js";
|
||||
import { loadImageFromBlob } from "./utils/ImageUtils.js";
|
||||
import {createModuleLogger} from "./log_system/log_funcs.js";
|
||||
import {showErrorNotification, showSuccessNotification, showInfoNotification, showWarningNotification} from "./utils/NotificationUtils.js";
|
||||
import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js";
|
||||
import { exportCanvasImage, type CanvasExportAction } from "./utils/CanvasExportUtils.js";
|
||||
import { getFlattenedCanvasBlob, type CanvasBlobVariant } from "./utils/CanvasBlobUtils.js";
|
||||
import { loadPreviewImage } from "./utils/PreviewUtils.js";
|
||||
import { fetchMattingModelStatus } from "./utils/MattingUtils.js";
|
||||
import { registerImageInClipspace, startSAMDetectorMonitoring, setupSAMDetectorHook } from "./SAMDetectorIntegration.js";
|
||||
import type { ComfyNode, Layer, AddMode } from './types';
|
||||
@@ -1340,10 +1341,12 @@ $el("label.lf-clipboard-switch.lf-mask-switch", {
|
||||
if (blob) {
|
||||
// For large images, use blob URL for better performance
|
||||
if (blob.size > 2 * 1024 * 1024) { // 2MB threshold
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
void loadImage(blobUrl).then(img => {
|
||||
void loadPreviewImage(blob, {
|
||||
source: 'canvas',
|
||||
urlMode: 'object-url'
|
||||
}).then(img => {
|
||||
node.imgs = [img];
|
||||
log.debug(`Using blob URL for large image (${(blob.size / 1024 / 1024).toFixed(1)}MB): ${blobUrl.substring(0, 50)}...`);
|
||||
log.debug(`Using blob URL for large image (${(blob.size / 1024 / 1024).toFixed(1)}MB): ${img.src.substring(0, 50)}...`);
|
||||
// Clean up old blob URLs to prevent memory leaks
|
||||
if (node.imgs.length > 1) {
|
||||
const oldImg = node.imgs[0];
|
||||
@@ -1354,7 +1357,10 @@ $el("label.lf-clipboard-switch.lf-mask-switch", {
|
||||
}).catch(() => undefined);
|
||||
} else {
|
||||
// For smaller images, use data URI as before
|
||||
void loadImageFromBlob(blob).then(img => {
|
||||
void loadPreviewImage(blob, {
|
||||
source: 'canvas',
|
||||
urlMode: 'data-url'
|
||||
}).then(img => {
|
||||
node.imgs = [img];
|
||||
log.debug(`Using data URI for small image (${(blob.size / 1024).toFixed(1)}KB): ${img.src.substring(0, 50)}...`);
|
||||
}).catch(() => undefined);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createModuleLogger } from "../log_system/log_funcs.js";
|
||||
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
|
||||
import { resolveCanvasBlob, supportsFlattenedCanvasBlob, type CanvasBlobVariant } from './CanvasBlobUtils.js';
|
||||
import { loadImage } from './ImageUtils.js';
|
||||
import { loadImage, loadImageFromBlob } from './ImageUtils.js';
|
||||
import type { ComfyNode } from '../types';
|
||||
|
||||
const log = createModuleLogger('PreviewUtils');
|
||||
@@ -19,7 +19,14 @@ export interface PreviewOptions {
|
||||
customBlob?: Blob;
|
||||
}
|
||||
|
||||
type PreviewBlobSource = 'canvas' | 'blob';
|
||||
export type PreviewBlobSource = 'canvas' | 'blob';
|
||||
|
||||
export type PreviewUrlMode = 'data-url' | 'object-url';
|
||||
|
||||
export interface PreviewImageLoadOptions {
|
||||
source: PreviewBlobSource;
|
||||
urlMode?: PreviewUrlMode;
|
||||
}
|
||||
|
||||
interface PreviewBlobLoadOptions {
|
||||
source: PreviewBlobSource;
|
||||
@@ -27,15 +34,19 @@ interface PreviewBlobLoadOptions {
|
||||
updateNodeImages?: boolean;
|
||||
}
|
||||
|
||||
async function loadPreviewImageFromBlob(
|
||||
export async function loadPreviewImage(
|
||||
blob: Blob,
|
||||
options: PreviewBlobLoadOptions
|
||||
options: PreviewImageLoadOptions
|
||||
): Promise<HTMLImageElement> {
|
||||
const isCanvasSource = options.source === 'canvas';
|
||||
const previewUrl = URL.createObjectURL(blob);
|
||||
let previewImage: HTMLImageElement;
|
||||
|
||||
try {
|
||||
previewImage = await loadImage(previewUrl);
|
||||
if (options.urlMode === 'data-url') {
|
||||
return await loadImageFromBlob(blob);
|
||||
}
|
||||
|
||||
const previewUrl = URL.createObjectURL(blob);
|
||||
return await loadImage(previewUrl);
|
||||
} catch (error) {
|
||||
const errorMessage = isCanvasSource
|
||||
? "Failed to load preview image"
|
||||
@@ -48,6 +59,17 @@ async function loadPreviewImageFromBlob(
|
||||
: { error, blobSize: blob.size }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreviewForNode(
|
||||
blob: Blob,
|
||||
options: PreviewBlobLoadOptions
|
||||
): Promise<HTMLImageElement> {
|
||||
const isCanvasSource = options.source === 'canvas';
|
||||
const previewImage = await loadPreviewImage(blob, {
|
||||
source: options.source,
|
||||
urlMode: 'object-url'
|
||||
});
|
||||
|
||||
log.debug(
|
||||
isCanvasSource ? "Preview image loaded successfully" : "Preview image from blob loaded successfully",
|
||||
@@ -135,7 +157,7 @@ export const createPreviewFromCanvas = withErrorHandling(async function(
|
||||
throw createValidationError("Failed to generate canvas blob for preview", { canvas, options });
|
||||
}
|
||||
|
||||
return loadPreviewImageFromBlob(blob, {
|
||||
return loadPreviewForNode(blob, {
|
||||
source: 'canvas',
|
||||
node,
|
||||
updateNodeImages
|
||||
@@ -167,7 +189,7 @@ export const createPreviewFromBlob = withErrorHandling(async function(
|
||||
hasNode: !!node
|
||||
});
|
||||
|
||||
return loadPreviewImageFromBlob(blob, {
|
||||
return loadPreviewForNode(blob, {
|
||||
source: 'blob',
|
||||
node,
|
||||
updateNodeImages
|
||||
|
||||
@@ -24,7 +24,7 @@ test('Blob and File image entry points use the shared loading helpers', () => {
|
||||
assert.match(clipboardSource, /loadImageFromBlob\(file\)/);
|
||||
assert.match(interactionsSource, /loadImageFromBlob\(file\)/);
|
||||
assert.match(viewSource, /loadImageFromBlob\(file\)/);
|
||||
assert.match(viewSource, /loadImageFromBlob\(blob\)/);
|
||||
assert.match(viewSource, /loadPreviewImage\(blob,/);
|
||||
assert.match(layersSource, /blobToDataUrl\(blob\)/);
|
||||
assert.match(ioSource, /blobToDataUrl\(imageBlob\)/);
|
||||
assert.match(ioSource, /blobToDataUrl\(maskBlob\)/);
|
||||
|
||||
@@ -176,3 +176,11 @@ test('canvas blob callers preserve plain and mask method responsibilities', () =
|
||||
assert.match(blobUtilsSource, /getFlattenedCanvasWithMaskAsBlob/);
|
||||
assert.match(blobUtilsSource, /export async function resolveCanvasBlob/);
|
||||
});
|
||||
|
||||
test('CanvasView preserves size-dependent preview loading strategies', () => {
|
||||
const canvasViewSource = sourceFiles[3];
|
||||
|
||||
assert.match(canvasViewSource, /blob\.size > 2 \* 1024 \* 1024/);
|
||||
assert.match(canvasViewSource, /loadPreviewImage\(blob, \{[\s\S]*urlMode: 'object-url'/);
|
||||
assert.match(canvasViewSource, /loadPreviewImage\(blob, \{[\s\S]*urlMode: 'data-url'/);
|
||||
});
|
||||
|
||||
@@ -23,5 +23,5 @@ test('targeted image consumers use the shared Image loading lifecycle', () => {
|
||||
assert.match(previewSource, /loadImage\(previewUrl\)/);
|
||||
|
||||
assert.doesNotMatch(viewSource, /const img = new Image\(\)/);
|
||||
assert.match(viewSource, /loadImage\(blobUrl\)/);
|
||||
assert.match(viewSource, /loadPreviewImage\(blob,/);
|
||||
});
|
||||
|
||||
@@ -4,11 +4,13 @@ import test from 'node:test';
|
||||
import {
|
||||
createPreviewFromBlob,
|
||||
createPreviewFromCanvas,
|
||||
loadPreviewImage,
|
||||
} from '../js/utils/PreviewUtils.js';
|
||||
|
||||
function installPreviewStubs({ fail = false } = {}) {
|
||||
const originalImage = Object.getOwnPropertyDescriptor(globalThis, 'Image');
|
||||
const originalUrl = Object.getOwnPropertyDescriptor(globalThis, 'URL');
|
||||
const originalFileReader = Object.getOwnPropertyDescriptor(globalThis, 'FileReader');
|
||||
const createdUrls = [];
|
||||
|
||||
Object.defineProperty(globalThis, 'Image', {
|
||||
@@ -47,6 +49,20 @@ function installPreviewStubs({ fail = false } = {}) {
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(globalThis, 'FileReader', {
|
||||
configurable: true,
|
||||
value: class TestFileReader {
|
||||
result = null;
|
||||
onload = null;
|
||||
onerror = null;
|
||||
|
||||
readAsDataURL() {
|
||||
this.result = 'data:image/png;base64,encoded';
|
||||
setTimeout(() => this.onload?.(), 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
createdUrls,
|
||||
restore() {
|
||||
@@ -60,6 +76,11 @@ function installPreviewStubs({ fail = false } = {}) {
|
||||
} else {
|
||||
delete globalThis.URL;
|
||||
}
|
||||
if (originalFileReader) {
|
||||
Object.defineProperty(globalThis, 'FileReader', originalFileReader);
|
||||
} else {
|
||||
delete globalThis.FileReader;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -95,6 +116,23 @@ test('canvas and Blob preview functions preserve loading and node update behavio
|
||||
}
|
||||
});
|
||||
|
||||
test('shared preview loader supports data URLs without creating an object URL', async () => {
|
||||
const stubs = installPreviewStubs();
|
||||
const blob = new Blob(['preview'], { type: 'image/png' });
|
||||
|
||||
try {
|
||||
const image = await loadPreviewImage(blob, {
|
||||
source: 'canvas',
|
||||
urlMode: 'data-url',
|
||||
});
|
||||
|
||||
assert.equal(image.src, 'data:image/png;base64,encoded');
|
||||
assert.equal(stubs.createdUrls.length, 0);
|
||||
} finally {
|
||||
stubs.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('Blob preview validation and image load failures preserve current errors', async () => {
|
||||
const stubs = installPreviewStubs({ fail: true });
|
||||
const blob = new Blob(['preview'], { type: 'image/png' });
|
||||
|
||||
Reference in New Issue
Block a user