99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import { processImageOnCanvas, type ProcessingStage } from "./canvas-pipeline";
|
|
import { ImageToolError, type ImageErrorCode } from "./errors";
|
|
import { IMAGE_LIMITS } from "./limits";
|
|
import type { ProcessImageRequest, ProcessImageResult } from "./types";
|
|
|
|
let requestSequence = 0;
|
|
|
|
export async function processImage(
|
|
request: ProcessImageRequest,
|
|
options: {
|
|
readonly signal?: AbortSignal;
|
|
readonly onStage?: (stage: ProcessingStage) => void;
|
|
} = {},
|
|
): Promise<ProcessImageResult> {
|
|
if (supportsWorkerPipeline()) return processInWorker(request, options);
|
|
return processImageOnCanvas(request, options);
|
|
}
|
|
|
|
function processInWorker(
|
|
request: ProcessImageRequest,
|
|
options: {
|
|
readonly signal?: AbortSignal;
|
|
readonly onStage?: (stage: ProcessingStage) => void;
|
|
},
|
|
): Promise<ProcessImageResult> {
|
|
const id = ++requestSequence;
|
|
return new Promise((resolve, reject) => {
|
|
const worker = new Worker(
|
|
new URL("../workers/image.worker.ts", import.meta.url),
|
|
{
|
|
type: "module",
|
|
name: `image-tools-${id}`,
|
|
},
|
|
);
|
|
let settled = false;
|
|
const finish = (callback: () => void) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
window.clearTimeout(timeout);
|
|
options.signal?.removeEventListener("abort", abort);
|
|
worker.terminate();
|
|
callback();
|
|
};
|
|
const abort = () => {
|
|
finish(() =>
|
|
reject(
|
|
new ImageToolError("ABORTED", "The image operation was cancelled."),
|
|
),
|
|
);
|
|
};
|
|
const timeout = window.setTimeout(() => {
|
|
finish(() =>
|
|
reject(
|
|
new ImageToolError(
|
|
"PROCESSING_TIMEOUT",
|
|
`Image processing exceeded the ${IMAGE_LIMITS.processingTimeoutMs / 1000}-second safety limit.`,
|
|
),
|
|
),
|
|
);
|
|
}, IMAGE_LIMITS.processingTimeoutMs);
|
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
worker.onerror = () => {
|
|
finish(() =>
|
|
reject(
|
|
new ImageToolError(
|
|
"DECODE_FAILED",
|
|
"The image worker stopped unexpectedly.",
|
|
),
|
|
),
|
|
);
|
|
};
|
|
worker.onmessage = (
|
|
event: MessageEvent<
|
|
| { id: number; type: "progress"; stage: ProcessingStage }
|
|
| { id: number; type: "result"; result: ProcessImageResult }
|
|
| { id: number; type: "error"; code: ImageErrorCode; message: string }
|
|
>,
|
|
) => {
|
|
const data = event.data;
|
|
if (data.id !== id || settled) return;
|
|
if (data.type === "progress") {
|
|
options.onStage?.(data.stage);
|
|
} else if (data.type === "result") {
|
|
finish(() => resolve(data.result));
|
|
} else {
|
|
finish(() => reject(new ImageToolError(data.code, data.message)));
|
|
}
|
|
};
|
|
if (options.signal?.aborted) abort();
|
|
else worker.postMessage({ id, request });
|
|
});
|
|
}
|
|
|
|
function supportsWorkerPipeline(): boolean {
|
|
return (
|
|
typeof Worker !== "undefined" && typeof OffscreenCanvas !== "undefined"
|
|
);
|
|
}
|