mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-15 05:56:36 +00:00
feat: double executor to use with preview
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import { ToolError } from "$lib/core/errors";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
|
||||
type MaybeRunnable = {
|
||||
id: string;
|
||||
domOnly?: boolean;
|
||||
run?: (
|
||||
img: PixelImage,
|
||||
params: Record<string, unknown>,
|
||||
) => Promise<PixelImage> | PixelImage;
|
||||
};
|
||||
|
||||
export async function executeStep(
|
||||
tool: MaybeRunnable,
|
||||
img: PixelImage,
|
||||
params: Record<string, unknown>,
|
||||
): Promise<PixelImage> {
|
||||
if (!tool.run) {
|
||||
throw new Error("errors.noImageRun");
|
||||
}
|
||||
if (tool.domOnly) {
|
||||
return await runDirect(tool, img, params);
|
||||
}
|
||||
if (typeof Worker === "undefined") {
|
||||
return await runDirect(tool, img, params);
|
||||
}
|
||||
const worker = ensureWorker();
|
||||
if (worker === null) {
|
||||
return await runDirect(tool, img, params);
|
||||
}
|
||||
try {
|
||||
return await runInWorker(worker, tool.id, img, params);
|
||||
} catch (workerError) {
|
||||
disableWorker();
|
||||
void workerError;
|
||||
return await runDirect(tool, img, params);
|
||||
}
|
||||
}
|
||||
|
||||
async function runDirect(
|
||||
tool: MaybeRunnable,
|
||||
img: PixelImage,
|
||||
params: Record<string, unknown>,
|
||||
): Promise<PixelImage> {
|
||||
if (!tool.run) {
|
||||
throw new Error("errors.noImageRun");
|
||||
}
|
||||
return await tool.run(img, params);
|
||||
}
|
||||
|
||||
let worker: Worker | null = null;
|
||||
let workerTried = false;
|
||||
let nextRequestId = 1;
|
||||
const pending = new Map<
|
||||
number,
|
||||
{ resolve: (img: PixelImage) => void; reject: (e: Error) => void }
|
||||
>();
|
||||
|
||||
function ensureWorker(): Worker | null {
|
||||
if (workerTried) return worker;
|
||||
workerTried = true;
|
||||
try {
|
||||
const candidate = new Worker(
|
||||
new URL("./executor.worker.ts", import.meta.url),
|
||||
{
|
||||
type: "module",
|
||||
},
|
||||
);
|
||||
candidate.onmessage = (event: MessageEvent) => {
|
||||
const payload = event.data as {
|
||||
id: number;
|
||||
ok: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
data?: Uint8ClampedArray;
|
||||
error?: string;
|
||||
errorKey?: string;
|
||||
errorVars?: Record<string, string | number>;
|
||||
};
|
||||
const entry = pending.get(payload.id);
|
||||
if (!entry) return;
|
||||
pending.delete(payload.id);
|
||||
if (payload.ok && payload.data && payload.width && payload.height) {
|
||||
entry.resolve({
|
||||
width: payload.width,
|
||||
height: payload.height,
|
||||
data: new Uint8ClampedArray(payload.data),
|
||||
});
|
||||
} else if (payload.errorKey) {
|
||||
entry.reject(new ToolError(payload.errorKey, payload.errorVars));
|
||||
} else {
|
||||
entry.reject(new Error(payload.error ?? "errors.workerFailed"));
|
||||
}
|
||||
};
|
||||
candidate.onerror = () => {
|
||||
worker = null;
|
||||
for (const entry of pending.values()) {
|
||||
entry.reject(new Error("errors.workerUnavailable"));
|
||||
}
|
||||
pending.clear();
|
||||
};
|
||||
worker = candidate;
|
||||
} catch {
|
||||
worker = null;
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
|
||||
function disableWorker(): void {
|
||||
if (worker) {
|
||||
worker.terminate();
|
||||
}
|
||||
worker = null;
|
||||
}
|
||||
|
||||
function runInWorker(
|
||||
workerInstance: Worker,
|
||||
toolId: string,
|
||||
img: PixelImage,
|
||||
params: Record<string, unknown>,
|
||||
): Promise<PixelImage> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = nextRequestId++;
|
||||
pending.set(id, { resolve, reject });
|
||||
workerInstance.postMessage({
|
||||
id,
|
||||
toolId,
|
||||
image: {
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
data: new Uint8ClampedArray(img.data),
|
||||
},
|
||||
params,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/// <reference lib="webworker" />
|
||||
import { ToolError } from "$lib/core/errors";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import { getTool } from "$lib/registry-new";
|
||||
import { sanitizeSchemaParams } from "$lib/registry-schema";
|
||||
|
||||
type WorkerRequest = {
|
||||
id: number;
|
||||
toolId: string;
|
||||
image: { width: number; height: number; data: Uint8ClampedArray };
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
|
||||
self.onmessage = (event: MessageEvent<WorkerRequest>) => {
|
||||
void handle(event.data);
|
||||
};
|
||||
|
||||
async function handle(request: WorkerRequest): Promise<void> {
|
||||
try {
|
||||
const tool = getTool(request.toolId);
|
||||
if (!tool?.run) {
|
||||
throw new Error("errors.noImageRun");
|
||||
}
|
||||
const image: PixelImage = {
|
||||
width: request.image.width,
|
||||
height: request.image.height,
|
||||
data: new Uint8ClampedArray(request.image.data),
|
||||
};
|
||||
const output = await tool.run(
|
||||
image,
|
||||
sanitizeSchemaParams(tool.schema, request.params),
|
||||
);
|
||||
const payload = {
|
||||
id: request.id,
|
||||
ok: true,
|
||||
width: output.width,
|
||||
height: output.height,
|
||||
data: output.data,
|
||||
};
|
||||
(self as unknown as Worker).postMessage(payload, [output.data.buffer]);
|
||||
} catch (e) {
|
||||
const toolError = e instanceof ToolError ? e : undefined;
|
||||
(self as unknown as Worker).postMessage({
|
||||
id: request.id,
|
||||
ok: false,
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
errorKey: toolError?.key,
|
||||
errorVars: toolError?.vars,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { executeStep } from "./executor";
|
||||
@@ -1,193 +0,0 @@
|
||||
import type { ToolEntry } from "$lib/registry";
|
||||
|
||||
export type SegmentedField = {
|
||||
id: string;
|
||||
kind: "segmented";
|
||||
label: string;
|
||||
options: string[];
|
||||
selected: number;
|
||||
};
|
||||
export type ColorPairField = {
|
||||
id: string;
|
||||
kind: "color-pair";
|
||||
label: string;
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
export type SliderField = {
|
||||
id: string;
|
||||
kind: "slider" | "direction";
|
||||
label: string;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
suffix: string;
|
||||
space?: boolean;
|
||||
hints?: [string, string];
|
||||
};
|
||||
export type ColorField = {
|
||||
id: string;
|
||||
kind: "color";
|
||||
label: string;
|
||||
value: string;
|
||||
native?: boolean;
|
||||
reference?: boolean;
|
||||
};
|
||||
export type ToggleField = {
|
||||
id: string;
|
||||
kind: "toggle";
|
||||
label: string;
|
||||
checked: boolean;
|
||||
note: string;
|
||||
};
|
||||
|
||||
export type FieldDef =
|
||||
SegmentedField | ColorPairField | SliderField | ColorField | ToggleField;
|
||||
|
||||
export interface PreviewToolView {
|
||||
title: string;
|
||||
lede: string;
|
||||
panelLabel: string;
|
||||
panelStrong: string;
|
||||
stepType: string;
|
||||
toolbarLabel: string;
|
||||
fileName: string;
|
||||
downloadLabel: string;
|
||||
layoutClass: string;
|
||||
previewClass: string;
|
||||
preview: "gradient" | "comparison";
|
||||
fields: FieldDef[];
|
||||
}
|
||||
|
||||
export type FieldValue =
|
||||
number | string | boolean | { from: string; to: string };
|
||||
|
||||
function initValue(f: FieldDef): FieldValue {
|
||||
switch (f.kind) {
|
||||
case "segmented":
|
||||
return f.selected;
|
||||
case "color-pair":
|
||||
return { from: f.from, to: f.to };
|
||||
case "color":
|
||||
return f.value;
|
||||
case "slider":
|
||||
case "direction":
|
||||
return f.value;
|
||||
case "toggle":
|
||||
return f.checked;
|
||||
}
|
||||
}
|
||||
|
||||
export function initForm(view: PreviewToolView): Record<string, FieldValue> {
|
||||
const form: Record<string, FieldValue> = {};
|
||||
for (const f of view.fields) form[f.id] = initValue(f);
|
||||
return form;
|
||||
}
|
||||
|
||||
const gradient: PreviewToolView = {
|
||||
title: "Gradient background.",
|
||||
lede: "Create a clean, export-ready gradient with precise control over color, direction and transparency.",
|
||||
panelLabel: "GRADIENT SETTINGS",
|
||||
panelStrong: "Configure output",
|
||||
stepType: "TOOL 01",
|
||||
toolbarLabel: "OUTPUT PREVIEW",
|
||||
fileName: "gradient.png",
|
||||
downloadLabel: "Download PNG",
|
||||
layoutClass: "gradient-layout",
|
||||
previewClass: "gradient-preview",
|
||||
preview: "gradient",
|
||||
fields: [
|
||||
{
|
||||
id: "type",
|
||||
kind: "segmented",
|
||||
label: "GRADIENT TYPE",
|
||||
options: ["Linear", "Radial"],
|
||||
selected: 0,
|
||||
},
|
||||
{
|
||||
id: "stops",
|
||||
kind: "color-pair",
|
||||
label: "COLOR STOPS",
|
||||
from: "#1769D2",
|
||||
to: "#00A8C7",
|
||||
},
|
||||
{
|
||||
id: "dir",
|
||||
kind: "direction",
|
||||
label: "DIRECTION",
|
||||
value: 135,
|
||||
min: 0,
|
||||
max: 360,
|
||||
suffix: "°",
|
||||
space: true,
|
||||
},
|
||||
{
|
||||
id: "op",
|
||||
kind: "slider",
|
||||
label: "OPACITY",
|
||||
value: 100,
|
||||
min: 0,
|
||||
max: 100,
|
||||
suffix: "%",
|
||||
space: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const background: PreviewToolView = {
|
||||
title: "Remove background.",
|
||||
lede: "Select a background color and tune the edge detection. Changes are processed automatically in your browser.",
|
||||
panelLabel: "REMOVER SETTINGS",
|
||||
panelStrong: "Configure detection",
|
||||
stepType: "TOOL 02",
|
||||
toolbarLabel: "SOURCE / RESULT",
|
||||
fileName: "comparison.png",
|
||||
downloadLabel: "Download result",
|
||||
layoutClass: "remover-layout",
|
||||
previewClass: "remover-preview",
|
||||
preview: "comparison",
|
||||
fields: [
|
||||
{
|
||||
id: "color",
|
||||
kind: "color",
|
||||
label: "BACKGROUND COLOR",
|
||||
value: "#E8EEF2",
|
||||
native: true,
|
||||
reference: true,
|
||||
},
|
||||
{
|
||||
id: "sim",
|
||||
kind: "slider",
|
||||
label: "COLOR SIMILARITY",
|
||||
value: 72,
|
||||
min: 0,
|
||||
max: 100,
|
||||
suffix: "%",
|
||||
space: true,
|
||||
hints: ["strict edges", "more removal"],
|
||||
},
|
||||
{
|
||||
id: "outer",
|
||||
kind: "toggle",
|
||||
label: "OUTER COLOR ONLY",
|
||||
checked: true,
|
||||
note: "Only remove connected background pixels from the edges.",
|
||||
},
|
||||
{
|
||||
id: "mask",
|
||||
kind: "toggle",
|
||||
label: "SHOW MASK",
|
||||
checked: false,
|
||||
note: "Preview the detected transparency mask.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const PREVIEW_TOOL_VIEWS: Record<string, PreviewToolView> = {
|
||||
"linear-gradient-png": gradient,
|
||||
"remove-background-png": background,
|
||||
};
|
||||
|
||||
export function getPreviewView(tool: ToolEntry): PreviewToolView | undefined {
|
||||
return PREVIEW_TOOL_VIEWS[tool.id];
|
||||
}
|
||||
Reference in New Issue
Block a user