From d68ee89f350a871ab53cbc6db1b33e231ee3c092 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 13 Aug 2026 08:09:04 +0200 Subject: [PATCH] refactor: share preview blob loading --- js/utils/PreviewUtils.js | 86 +++++++++--------- src/utils/PreviewUtils.ts | 115 ++++++++++++++---------- tests/test_preview_characterization.mjs | 114 +++++++++++++++++++++++ 3 files changed, 225 insertions(+), 90 deletions(-) create mode 100644 tests/test_preview_characterization.mjs diff --git a/js/utils/PreviewUtils.js b/js/utils/PreviewUtils.js index ff205dd..19aeb99 100644 --- a/js/utils/PreviewUtils.js +++ b/js/utils/PreviewUtils.js @@ -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 diff --git a/src/utils/PreviewUtils.ts b/src/utils/PreviewUtils.ts index d16d6ed..a867535 100644 --- a/src/utils/PreviewUtils.ts +++ b/src/utils/PreviewUtils.ts @@ -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 { + 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 @@ -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((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((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'); /** diff --git a/tests/test_preview_characterization.mjs b/tests/test_preview_characterization.mjs new file mode 100644 index 0000000..05bf428 --- /dev/null +++ b/tests/test_preview_characterization.mjs @@ -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(); + } +});