refactor: share preview blob loading

This commit is contained in:
Dariusz L
2026-08-13 08:09:04 +02:00
parent 459df09a70
commit d68ee89f35
3 changed files with 225 additions and 90 deletions
+44 -42
View File
@@ -2,6 +2,42 @@ import { createModuleLogger } from "../log_system/log_funcs.js";
import { withErrorHandling, createValidationError } from "../ErrorHandler.js";
import { getFlattenedCanvasBlob, supportsFlattenedCanvasBlob } from './CanvasBlobUtils.js';
const log = createModuleLogger('PreviewUtils');
async function loadPreviewImageFromBlob(blob, options) {
const isCanvasSource = options.source === 'canvas';
const previewImage = new Image();
previewImage.src = URL.createObjectURL(blob);
await new Promise((resolve, reject) => {
previewImage.onload = () => {
log.debug(isCanvasSource ? "Preview image loaded successfully" : "Preview image from blob loaded successfully", isCanvasSource
? {
width: previewImage.width,
height: previewImage.height,
nodeId: options.node?.id
}
: {
width: previewImage.width,
height: previewImage.height
});
resolve();
};
previewImage.onerror = (error) => {
const errorMessage = isCanvasSource
? "Failed to load preview image"
: "Failed to load preview image from blob";
log.error(errorMessage, error);
reject(createValidationError(errorMessage, isCanvasSource
? { error, blob: blob.size }
: { error, blobSize: blob.size }));
};
});
if (options.updateNodeImages && options.node) {
options.node.imgs = [previewImage];
log.debug(isCanvasSource
? "Node images updated with new preview"
: "Node images updated with blob preview");
}
return previewImage;
}
/**
* Creates a preview image from canvas and updates node
* @param canvas - Canvas object with canvasLayers
@@ -44,30 +80,11 @@ export const createPreviewFromCanvas = withErrorHandling(async function (canvas,
if (!blob) {
throw createValidationError("Failed to generate canvas blob for preview", { canvas, options });
}
// Create preview image
const previewImage = new Image();
previewImage.src = URL.createObjectURL(blob);
// Wait for image to load
await new Promise((resolve, reject) => {
previewImage.onload = () => {
log.debug("Preview image loaded successfully", {
width: previewImage.width,
height: previewImage.height,
nodeId: node.id
});
resolve();
};
previewImage.onerror = (error) => {
log.error("Failed to load preview image", error);
reject(createValidationError("Failed to load preview image", { error, blob: blob?.size }));
};
return loadPreviewImageFromBlob(blob, {
source: 'canvas',
node,
updateNodeImages
});
// Update node images if requested
if (updateNodeImages) {
node.imgs = [previewImage];
log.debug("Node images updated with new preview");
}
return previewImage;
}, 'createPreviewFromCanvas');
/**
* Creates a preview image from a blob
@@ -88,26 +105,11 @@ export const createPreviewFromBlob = withErrorHandling(async function (blob, nod
updateNodeImages,
hasNode: !!node
});
const previewImage = new Image();
previewImage.src = URL.createObjectURL(blob);
await new Promise((resolve, reject) => {
previewImage.onload = () => {
log.debug("Preview image from blob loaded successfully", {
width: previewImage.width,
height: previewImage.height
});
resolve();
};
previewImage.onerror = (error) => {
log.error("Failed to load preview image from blob", error);
reject(createValidationError("Failed to load preview image from blob", { error, blobSize: blob.size }));
};
return loadPreviewImageFromBlob(blob, {
source: 'blob',
node,
updateNodeImages
});
if (updateNodeImages && node) {
node.imgs = [previewImage];
log.debug("Node images updated with blob preview");
}
return previewImage;
}, 'createPreviewFromBlob');
/**
* Updates node preview after canvas changes
+67 -48
View File
@@ -18,6 +18,65 @@ export interface PreviewOptions {
customBlob?: Blob;
}
type PreviewBlobSource = 'canvas' | 'blob';
interface PreviewBlobLoadOptions {
source: PreviewBlobSource;
node?: ComfyNode;
updateNodeImages?: boolean;
}
async function loadPreviewImageFromBlob(
blob: Blob,
options: PreviewBlobLoadOptions
): Promise<HTMLImageElement> {
const isCanvasSource = options.source === 'canvas';
const previewImage = new Image();
previewImage.src = URL.createObjectURL(blob);
await new Promise<void>((resolve, reject) => {
previewImage.onload = () => {
log.debug(
isCanvasSource ? "Preview image loaded successfully" : "Preview image from blob loaded successfully",
isCanvasSource
? {
width: previewImage.width,
height: previewImage.height,
nodeId: options.node?.id
}
: {
width: previewImage.width,
height: previewImage.height
}
);
resolve();
};
previewImage.onerror = (error) => {
const errorMessage = isCanvasSource
? "Failed to load preview image"
: "Failed to load preview image from blob";
log.error(errorMessage, error);
reject(createValidationError(
errorMessage,
isCanvasSource
? { error, blob: blob.size }
: { error, blobSize: blob.size }
));
};
});
if (options.updateNodeImages && options.node) {
options.node.imgs = [previewImage];
log.debug(
isCanvasSource
? "Node images updated with new preview"
: "Node images updated with blob preview"
);
}
return previewImage;
}
/**
* Creates a preview image from canvas and updates node
* @param canvas - Canvas object with canvasLayers
@@ -77,33 +136,11 @@ export const createPreviewFromCanvas = withErrorHandling(async function(
throw createValidationError("Failed to generate canvas blob for preview", { canvas, options });
}
// Create preview image
const previewImage = new Image();
previewImage.src = URL.createObjectURL(blob);
// Wait for image to load
await new Promise<void>((resolve, reject) => {
previewImage.onload = () => {
log.debug("Preview image loaded successfully", {
width: previewImage.width,
height: previewImage.height,
nodeId: node.id
});
resolve();
};
previewImage.onerror = (error) => {
log.error("Failed to load preview image", error);
reject(createValidationError("Failed to load preview image", { error, blob: blob?.size }));
};
return loadPreviewImageFromBlob(blob, {
source: 'canvas',
node,
updateNodeImages
});
// Update node images if requested
if (updateNodeImages) {
node.imgs = [previewImage];
log.debug("Node images updated with new preview");
}
return previewImage;
}, 'createPreviewFromCanvas');
/**
@@ -131,29 +168,11 @@ export const createPreviewFromBlob = withErrorHandling(async function(
hasNode: !!node
});
const previewImage = new Image();
previewImage.src = URL.createObjectURL(blob);
await new Promise<void>((resolve, reject) => {
previewImage.onload = () => {
log.debug("Preview image from blob loaded successfully", {
width: previewImage.width,
height: previewImage.height
});
resolve();
};
previewImage.onerror = (error) => {
log.error("Failed to load preview image from blob", error);
reject(createValidationError("Failed to load preview image from blob", { error, blobSize: blob.size }));
};
return loadPreviewImageFromBlob(blob, {
source: 'blob',
node,
updateNodeImages
});
if (updateNodeImages && node) {
node.imgs = [previewImage];
log.debug("Node images updated with blob preview");
}
return previewImage;
}, 'createPreviewFromBlob');
/**
+114
View File
@@ -0,0 +1,114 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createPreviewFromBlob,
createPreviewFromCanvas,
} from '../js/utils/PreviewUtils.js';
function installPreviewStubs({ fail = false } = {}) {
const originalImage = Object.getOwnPropertyDescriptor(globalThis, 'Image');
const originalUrl = Object.getOwnPropertyDescriptor(globalThis, 'URL');
const createdUrls = [];
Object.defineProperty(globalThis, 'Image', {
configurable: true,
value: class TestImage {
width = 64;
height = 32;
onload = null;
onerror = null;
set src(value) {
this._src = value;
setTimeout(() => {
if (fail) {
this.onerror?.(new Error('preview image decode failed'));
} else {
this.onload?.();
}
}, 0);
}
get src() {
return this._src;
}
},
});
Object.defineProperty(globalThis, 'URL', {
configurable: true,
value: {
createObjectURL(blob) {
const url = `blob:preview-${createdUrls.length + 1}`;
createdUrls.push({ blob, url });
return url;
},
},
});
return {
createdUrls,
restore() {
if (originalImage) {
Object.defineProperty(globalThis, 'Image', originalImage);
} else {
delete globalThis.Image;
}
if (originalUrl) {
Object.defineProperty(globalThis, 'URL', originalUrl);
} else {
delete globalThis.URL;
}
},
};
}
test('canvas and Blob preview functions preserve loading and node update behavior', async () => {
const stubs = installPreviewStubs();
const blob = new Blob(['preview'], { type: 'image/png' });
const canvas = {
canvasLayers: {
async getFlattenedCanvasAsBlob() {
return blob;
},
},
};
const canvasNode = { id: 1, imgs: [] };
const blobNode = { id: 2, imgs: [] };
const untouchedNode = { id: 3, imgs: [] };
try {
const canvasImage = await createPreviewFromCanvas(canvas, canvasNode, { includeMask: false });
const blobImage = await createPreviewFromBlob(blob, blobNode, true);
const untouchedImage = await createPreviewFromBlob(blob, untouchedNode);
assert.equal(canvasImage.src, 'blob:preview-1');
assert.equal(blobImage.src, 'blob:preview-2');
assert.equal(untouchedImage.src, 'blob:preview-3');
assert.deepEqual(canvasNode.imgs, [canvasImage]);
assert.deepEqual(blobNode.imgs, [blobImage]);
assert.deepEqual(untouchedNode.imgs, []);
assert.equal(stubs.createdUrls.length, 3);
} 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' });
try {
await assert.rejects(
() => createPreviewFromBlob(new Blob([], { type: 'image/png' })),
error => error.message === 'Blob cannot be empty'
);
await assert.rejects(
() => createPreviewFromBlob(blob),
error => error.message === 'Failed to load preview image from blob'
);
} finally {
stubs.restore();
}
});