refactor: share preview blob loading
This commit is contained in:
+44
-42
@@ -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
@@ -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');
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user