refactor: share image upload request construction

This commit is contained in:
Dariusz L
2026-08-13 08:12:08 +02:00
parent d68ee89f35
commit 38886f346d
6 changed files with 93 additions and 60 deletions
+4 -21
View File
@@ -3,6 +3,7 @@ import { createModuleLogger } from "./log_system/log_funcs.js";
import { showErrorNotification } from "./utils/NotificationUtils.js";
import { webSocketManager } from "./utils/WebSocketManager.js";
import { scaleImageToFit, loadImage, blobToDataUrl, tensorToImageData, createImageFromImageData } from "./utils/ImageUtils.js";
import { postImageBlob } from "./utils/ImageUploadUtils.js";
const log = createModuleLogger('CanvasIO');
export class CanvasIO {
constructor(canvas) {
@@ -112,14 +113,8 @@ export class CanvasIO {
if (!blobWithoutMask)
return;
log.debug(`Created blob for image without mask, size: ${blobWithoutMask.size} bytes`);
const formDataWithoutMask = new FormData();
formDataWithoutMask.append("image", blobWithoutMask, fileNameWithoutMask);
formDataWithoutMask.append("overwrite", "true");
try {
const response = await fetch("/upload/image", {
method: "POST",
body: formDataWithoutMask,
});
const response = await postImageBlob({ blob: blobWithoutMask, filename: fileNameWithoutMask }, fetch);
log.debug(`Image without mask upload response: ${response.status}`);
}
catch (error) {
@@ -131,14 +126,8 @@ export class CanvasIO {
if (!blob)
return;
log.debug(`Created blob for main image, size: ${blob.size} bytes`);
const formData = new FormData();
formData.append("image", blob, fileName);
formData.append("overwrite", "true");
try {
const resp = await fetch("/upload/image", {
method: "POST",
body: formData,
});
const resp = await postImageBlob({ blob, filename: fileName }, fetch);
log.debug(`Main image upload response: ${resp.status}`);
if (resp.status === 200) {
const maskFileName = fileName.replace('.png', '_mask.png');
@@ -147,14 +136,8 @@ export class CanvasIO {
if (!maskBlob)
return;
log.debug(`Created blob for mask, size: ${maskBlob.size} bytes`);
const maskFormData = new FormData();
maskFormData.append("image", maskBlob, maskFileName);
maskFormData.append("overwrite", "true");
try {
const maskResp = await fetch("/upload/image", {
method: "POST",
body: maskFormData,
});
const maskResp = await postImageBlob({ blob: maskBlob, filename: maskFileName }, fetch);
log.debug(`Mask upload response: ${maskResp.status}`);
if (maskResp.status === 200) {
const data = await resp.json();
+17 -8
View File
@@ -4,6 +4,18 @@ import { createModuleLogger } from "../log_system/log_funcs.js";
import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js";
import { getFlattenedCanvasBlob, supportsFlattenedCanvasBlob } from './CanvasBlobUtils.js';
const log = createModuleLogger('ImageUploadUtils');
export async function postImageBlob(request, transport = (input, init) => api.fetchApi(input, init)) {
const formData = new FormData();
formData.append("image", request.blob, request.filename);
formData.append("overwrite", (request.overwrite ?? true).toString());
if (request.type !== undefined) {
formData.append("type", request.type);
}
return transport("/upload/image", {
method: "POST",
body: formData,
});
}
async function getCanvasBlobForUpload(canvas, uploadOptions, config) {
if (!canvas) {
throw createValidationError("Canvas is required", { canvas });
@@ -53,15 +65,12 @@ export const uploadImageBlob = withErrorHandling(async function (blob, options =
type,
overwrite
});
// Create FormData
const formData = new FormData();
formData.append("image", blob, filename);
formData.append("overwrite", overwrite.toString());
formData.append("type", type);
// Upload to server
const response = await api.fetchApi("/upload/image", {
method: "POST",
body: formData,
const response = await postImageBlob({
blob,
filename,
overwrite,
type
});
if (!response.ok) {
throw createNetworkError(`Failed to upload image: ${response.statusText}`, {
+13 -21
View File
@@ -3,6 +3,7 @@ import { createModuleLogger } from "./log_system/log_funcs.js";
import { showErrorNotification } from "./utils/NotificationUtils.js";
import { webSocketManager } from "./utils/WebSocketManager.js";
import { scaleImageToFit, loadImage, blobToDataUrl, tensorToImageData, createImageFromImageData } from "./utils/ImageUtils.js";
import { postImageBlob } from "./utils/ImageUploadUtils.js";
import type { Canvas } from './Canvas';
import type { Layer, Shape, AddMode } from './types';
@@ -153,15 +154,12 @@ export class CanvasIO {
tempCanvas.toBlob(async (blobWithoutMask) => {
if (!blobWithoutMask) return;
log.debug(`Created blob for image without mask, size: ${blobWithoutMask.size} bytes`);
const formDataWithoutMask = new FormData();
formDataWithoutMask.append("image", blobWithoutMask, fileNameWithoutMask);
formDataWithoutMask.append("overwrite", "true");
try {
const response = await fetch("/upload/image", {
method: "POST",
body: formDataWithoutMask,
});
const response = await postImageBlob(
{ blob: blobWithoutMask, filename: fileNameWithoutMask },
fetch
);
log.debug(`Image without mask upload response: ${response.status}`);
} catch (error) {
log.error(`Error uploading image without mask:`, error);
@@ -171,15 +169,12 @@ export class CanvasIO {
tempCanvas.toBlob(async (blob) => {
if (!blob) return;
log.debug(`Created blob for main image, size: ${blob.size} bytes`);
const formData = new FormData();
formData.append("image", blob, fileName);
formData.append("overwrite", "true");
try {
const resp = await fetch("/upload/image", {
method: "POST",
body: formData,
});
const resp = await postImageBlob(
{ blob, filename: fileName },
fetch
);
log.debug(`Main image upload response: ${resp.status}`);
if (resp.status === 200) {
@@ -189,15 +184,12 @@ export class CanvasIO {
maskCanvas.toBlob(async (maskBlob) => {
if (!maskBlob) return;
log.debug(`Created blob for mask, size: ${maskBlob.size} bytes`);
const maskFormData = new FormData();
maskFormData.append("image", maskBlob, maskFileName);
maskFormData.append("overwrite", "true");
try {
const maskResp = await fetch("/upload/image", {
method: "POST",
body: maskFormData,
});
const maskResp = await postImageBlob(
{ blob: maskBlob, filename: maskFileName },
fetch
);
log.debug(`Mask upload response: ${maskResp.status}`);
if (maskResp.status === 200) {
+31 -9
View File
@@ -39,6 +39,32 @@ interface CanvasBlobUploadConfig {
emptyBlobMessage: string;
}
interface ImageBlobUploadRequest {
blob: Blob;
filename: string;
overwrite?: boolean;
type?: string;
}
type ImageUploadTransport = (input: string, init: RequestInit) => Promise<Response>;
export async function postImageBlob(
request: ImageBlobUploadRequest,
transport: ImageUploadTransport = (input, init) => api.fetchApi(input, init)
): Promise<Response> {
const formData = new FormData();
formData.append("image", request.blob, request.filename);
formData.append("overwrite", (request.overwrite ?? true).toString());
if (request.type !== undefined) {
formData.append("type", request.type);
}
return transport("/upload/image", {
method: "POST",
body: formData,
});
}
async function getCanvasBlobForUpload(
canvas: any,
uploadOptions: UploadImageOptions,
@@ -104,16 +130,12 @@ export const uploadImageBlob = withErrorHandling(async function(blob: Blob, opti
overwrite
});
// Create FormData
const formData = new FormData();
formData.append("image", blob, filename);
formData.append("overwrite", overwrite.toString());
formData.append("type", type);
// Upload to server
const response = await api.fetchApi("/upload/image", {
method: "POST",
body: formData,
const response = await postImageBlob({
blob,
filename,
overwrite,
type
});
if (!response.ok) {
@@ -24,7 +24,7 @@ test('output paths preserve their current render bounds and mask responsibilitie
test('output rendering keeps persistence and transport in CanvasIO', () => {
assert.match(canvasIOSource, /async _performSave\(fileName: string, outputMode: string\)/);
assert.match(canvasIOSource, /tempCanvas\.toDataURL\('image\/png'\)/);
assert.match(canvasIOSource, /fetch\("\/upload\/image"/);
assert.equal((canvasIOSource.match(/postImageBlob\(/g) ?? []).length, 3);
assert.match(canvasLayersSource, /async getFlattenedCanvasAsBlob\(\)/);
assert.match(canvasLayersSource, /async getFlattenedMaskAsBlob\(\)/);
});
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const [canvasIOSource, imageUploadSource] = await Promise.all([
readFile(new URL('../src/CanvasIO.ts', import.meta.url), 'utf8'),
readFile(new URL('../src/utils/ImageUploadUtils.ts', import.meta.url), 'utf8'),
]);
test('current upload callers preserve their endpoint and form-data contracts', () => {
assert.equal((canvasIOSource.match(/new FormData\(\)/g) ?? []).length, 0);
assert.equal((canvasIOSource.match(/fetch\("\/upload\/image"/g) ?? []).length, 0);
assert.equal((canvasIOSource.match(/postImageBlob\(/g) ?? []).length, 3);
assert.equal((imageUploadSource.match(/new FormData\(\)/g) ?? []).length, 1);
assert.equal((imageUploadSource.match(/api\.fetchApi\(/g) ?? []).length, 1);
assert.match(canvasIOSource, /postImageBlob\(\s*\{ blob: blobWithoutMask, filename: fileNameWithoutMask \},\s*fetch\s*\)/s);
assert.match(canvasIOSource, /postImageBlob\(\s*\{ blob, filename: fileName \},\s*fetch\s*\)/s);
assert.match(canvasIOSource, /postImageBlob\(\s*\{ blob: maskBlob, filename: maskFileName \},\s*fetch\s*\)/s);
assert.match(imageUploadSource, /export async function postImageBlob\(/);
assert.match(imageUploadSource, /formData\.append\("image", request\.blob, request\.filename\)/);
assert.match(imageUploadSource, /formData\.append\("overwrite", \(request\.overwrite \?\? true\)\.toString\(\)\)/);
assert.match(imageUploadSource, /formData\.append\("type", request\.type\)/);
assert.match(imageUploadSource, /return transport\("\/upload\/image"/);
assert.match(imageUploadSource, /postImageBlob\(\{[\s\S]*?overwrite,[\s\S]*?type/s);
});