fabricjs-document-engine 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/README.zh-CN.md +32 -0
- package/dist/commands/text-edit.cjs +223 -0
- package/dist/commands/text-edit.d.cts +23 -0
- package/dist/commands/text-edit.d.ts +23 -0
- package/dist/commands/text-edit.js +218 -0
- package/dist/fabric/object-registry.cjs +7 -1
- package/dist/fabric/object-registry.js +7 -1
- package/dist/filters/filter-worker.cjs +281 -0
- package/dist/filters/filter-worker.d.cts +26 -0
- package/dist/filters/filter-worker.d.ts +26 -0
- package/dist/filters/filter-worker.js +281 -0
- package/dist/filters/pipeline.cjs +91 -0
- package/dist/filters/pipeline.d.cts +35 -0
- package/dist/filters/pipeline.d.ts +35 -0
- package/dist/filters/pipeline.js +87 -0
- package/dist/filters/worker-entry.cjs +78 -0
- package/dist/filters/worker-entry.d.cts +29 -0
- package/dist/filters/worker-entry.d.ts +29 -0
- package/dist/filters/worker-entry.js +78 -0
- package/dist/filters.cjs +9 -0
- package/dist/filters.d.cts +3 -0
- package/dist/filters.d.ts +3 -0
- package/dist/filters.js +3 -0
- package/dist/import/svg-import.cjs +102 -0
- package/dist/import/svg-import.d.cts +1 -0
- package/dist/import/svg-import.d.ts +1 -0
- package/dist/import/svg-import.js +103 -1
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/performance/batch.cjs +31 -0
- package/dist/performance/batch.d.cts +4 -0
- package/dist/performance/batch.d.ts +4 -0
- package/dist/performance/batch.js +31 -0
- package/dist/performance/dirty-regions.cjs +378 -0
- package/dist/performance/dirty-regions.d.cts +23 -0
- package/dist/performance/dirty-regions.d.ts +23 -0
- package/dist/performance/dirty-regions.js +378 -0
- package/dist/performance/monitor.cjs +60 -0
- package/dist/performance/monitor.d.cts +20 -0
- package/dist/performance/monitor.d.ts +20 -0
- package/dist/performance/monitor.js +60 -0
- package/dist/performance/spatial-index.cjs +78 -0
- package/dist/performance/spatial-index.d.cts +21 -0
- package/dist/performance/spatial-index.d.ts +21 -0
- package/dist/performance/spatial-index.js +78 -0
- package/dist/performance.cjs +9 -0
- package/dist/performance.d.cts +5 -0
- package/dist/performance.d.ts +5 -0
- package/dist/performance.js +5 -0
- package/dist/text/bounded-textbox.cjs +306 -0
- package/dist/text/bounded-textbox.d.cts +53 -0
- package/dist/text/bounded-textbox.d.ts +53 -0
- package/dist/text/bounded-textbox.js +306 -0
- package/dist/text/mobile-input.cjs +146 -0
- package/dist/text/mobile-input.d.cts +19 -0
- package/dist/text/mobile-input.d.ts +19 -0
- package/dist/text/mobile-input.js +145 -0
- package/dist/text/register.cjs +17 -0
- package/dist/text/register.d.cts +5 -0
- package/dist/text/register.d.ts +5 -0
- package/dist/text/register.js +16 -0
- package/dist/text/shaping.cjs +125 -0
- package/dist/text/shaping.d.cts +28 -0
- package/dist/text/shaping.d.ts +28 -0
- package/dist/text/shaping.js +121 -0
- package/dist/text/vertical-orientation.cjs +192 -0
- package/dist/text/vertical-orientation.d.cts +10 -0
- package/dist/text/vertical-orientation.d.ts +10 -0
- package/dist/text/vertical-orientation.js +190 -0
- package/dist/text/vertical-text.cjs +336 -0
- package/dist/text/vertical-text.d.cts +54 -0
- package/dist/text/vertical-text.d.ts +54 -0
- package/dist/text/vertical-text.js +336 -0
- package/dist/text.cjs +21 -0
- package/dist/text.d.cts +7 -0
- package/dist/text.d.ts +7 -0
- package/dist/text.js +7 -0
- package/package.json +31 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
const require_concurrency = require("../util/concurrency.cjs");
|
|
2
|
+
const require_pipeline = require("./pipeline.cjs");
|
|
3
|
+
let fabric = require("fabric");
|
|
4
|
+
//#region src/filters/filter-worker.ts
|
|
5
|
+
function sizeOf(element) {
|
|
6
|
+
return {
|
|
7
|
+
width: element.naturalWidth || element.width,
|
|
8
|
+
height: element.naturalHeight || element.height
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function abortReason(signal) {
|
|
12
|
+
return signal.reason ?? new DOMException("The filter run was aborted", "AbortError");
|
|
13
|
+
}
|
|
14
|
+
function createFilterWorker(options = {}) {
|
|
15
|
+
const engine = options.engine;
|
|
16
|
+
const latest = /* @__PURE__ */ new WeakMap();
|
|
17
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
18
|
+
const queue = [];
|
|
19
|
+
let slots;
|
|
20
|
+
let workersFailed = options.worker === false;
|
|
21
|
+
let nextId = 0;
|
|
22
|
+
function makeWorker() {
|
|
23
|
+
if (typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") return void 0;
|
|
24
|
+
try {
|
|
25
|
+
return options.createWorker ? options.createWorker() : new Worker(new URL("./worker-entry.js", void 0), { type: "module" });
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function pool() {
|
|
31
|
+
if (workersFailed) return void 0;
|
|
32
|
+
if (slots) return slots;
|
|
33
|
+
const made = [];
|
|
34
|
+
for (let index = 0; index < Math.max(1, options.poolSize ?? 1); index += 1) {
|
|
35
|
+
const worker = makeWorker();
|
|
36
|
+
if (!worker) break;
|
|
37
|
+
const slot = {
|
|
38
|
+
worker,
|
|
39
|
+
job: void 0
|
|
40
|
+
};
|
|
41
|
+
worker.onmessage = (event) => onReply(slot, event.data);
|
|
42
|
+
worker.onerror = (event) => {
|
|
43
|
+
event.preventDefault?.();
|
|
44
|
+
workersFailed = true;
|
|
45
|
+
const job = slot.job;
|
|
46
|
+
slot.job = void 0;
|
|
47
|
+
slots?.forEach((each) => each.worker.terminate());
|
|
48
|
+
slots = void 0;
|
|
49
|
+
if (job && !job.settled) runOnMainThread(job);
|
|
50
|
+
queue.splice(0).forEach((waiting) => void runOnMainThread(waiting));
|
|
51
|
+
};
|
|
52
|
+
made.push(slot);
|
|
53
|
+
}
|
|
54
|
+
if (made.length === 0) {
|
|
55
|
+
workersFailed = true;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
return slots = made;
|
|
59
|
+
}
|
|
60
|
+
function settle(job, outcome) {
|
|
61
|
+
if (job.settled) return;
|
|
62
|
+
job.settled = true;
|
|
63
|
+
jobs.delete(job.id);
|
|
64
|
+
if ("error" in outcome) job.reject(outcome.error);
|
|
65
|
+
else job.resolve(outcome.updated);
|
|
66
|
+
}
|
|
67
|
+
function cancel(job, reason) {
|
|
68
|
+
if (job.settled) return;
|
|
69
|
+
if (job.slot) job.slot.worker.postMessage({
|
|
70
|
+
type: "cancel",
|
|
71
|
+
id: job.id
|
|
72
|
+
});
|
|
73
|
+
settle(job, reason === void 0 ? { updated: false } : { error: reason });
|
|
74
|
+
}
|
|
75
|
+
function commit(job, draw) {
|
|
76
|
+
if (job.settled) return;
|
|
77
|
+
if (latest.get(job.image) !== job.id || engine && engine.getDocumentInfo().session !== job.session) {
|
|
78
|
+
settle(job, { updated: false });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const image = job.image;
|
|
82
|
+
const write = () => {
|
|
83
|
+
image.filters = job.filters;
|
|
84
|
+
image.removeTexture?.(`${image.cacheKey}_filtered`);
|
|
85
|
+
if (!draw) {
|
|
86
|
+
image._element = image._originalElement;
|
|
87
|
+
image._filteredEl = void 0;
|
|
88
|
+
image._filterScalingX = 1;
|
|
89
|
+
image._filterScalingY = 1;
|
|
90
|
+
} else {
|
|
91
|
+
const { width, height } = sizeOf(image._originalElement);
|
|
92
|
+
let target = image._filteredEl;
|
|
93
|
+
if (image._element === image._originalElement || !target) {
|
|
94
|
+
target = fabric.util.createCanvasElement();
|
|
95
|
+
image._filteredEl = target;
|
|
96
|
+
} else {
|
|
97
|
+
image._lastScaleX = 1;
|
|
98
|
+
image._lastScaleY = 1;
|
|
99
|
+
}
|
|
100
|
+
image._element = target;
|
|
101
|
+
target.width = width;
|
|
102
|
+
target.height = height;
|
|
103
|
+
draw(target.getContext("2d"), width, height);
|
|
104
|
+
if (target.width !== image._originalElement.width || target.height !== image._originalElement.height) {
|
|
105
|
+
image._filterScalingX = target.width / image._originalElement.width;
|
|
106
|
+
image._filterScalingY = target.height / image._originalElement.height;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
job.image.set("dirty", true);
|
|
110
|
+
};
|
|
111
|
+
if (engine) engine.transaction("Apply filters", write);
|
|
112
|
+
else write();
|
|
113
|
+
job.image.canvas?.requestRenderAll();
|
|
114
|
+
settle(job, { updated: true });
|
|
115
|
+
}
|
|
116
|
+
async function runOnMainThread(job) {
|
|
117
|
+
if (job.settled) return;
|
|
118
|
+
const image = job.image;
|
|
119
|
+
const active = job.filters.filter((filter) => filter && !filter.isNeutralState());
|
|
120
|
+
if (!require_pipeline.canRunInWorker(active)) {
|
|
121
|
+
await require_concurrency.yieldToEventLoop();
|
|
122
|
+
if (job.settled || latest.get(job.image) !== job.id) return settle(job, { updated: false });
|
|
123
|
+
if (engine) engine.transaction("Apply filters", () => image.applyFilters(job.filters));
|
|
124
|
+
else image.applyFilters(job.filters);
|
|
125
|
+
image.filters = job.filters;
|
|
126
|
+
job.image.canvas?.requestRenderAll();
|
|
127
|
+
job.options.onProgress?.(1);
|
|
128
|
+
return settle(job, { updated: true });
|
|
129
|
+
}
|
|
130
|
+
const { width, height } = sizeOf(image._originalElement);
|
|
131
|
+
const scratch = fabric.util.createCanvasElement();
|
|
132
|
+
scratch.width = width;
|
|
133
|
+
scratch.height = height;
|
|
134
|
+
const ctx = scratch.getContext("2d", { willReadFrequently: true });
|
|
135
|
+
ctx.drawImage(image._originalElement, 0, 0, width, height);
|
|
136
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
137
|
+
try {
|
|
138
|
+
const result = await require_pipeline.runFilterPipeline(active, {
|
|
139
|
+
sourceWidth: width,
|
|
140
|
+
sourceHeight: height,
|
|
141
|
+
imageData,
|
|
142
|
+
originalImageData: imageData,
|
|
143
|
+
originalEl: image._originalElement,
|
|
144
|
+
canvasEl: scratch,
|
|
145
|
+
ctx,
|
|
146
|
+
filterBackend: void 0
|
|
147
|
+
}, {
|
|
148
|
+
bandRows: options.bandRows,
|
|
149
|
+
pause: require_concurrency.yieldToEventLoop,
|
|
150
|
+
isCancelled: () => job.settled,
|
|
151
|
+
onProgress: job.options.onProgress
|
|
152
|
+
});
|
|
153
|
+
if (!result) return;
|
|
154
|
+
commit(job, (target, targetWidth, targetHeight) => {
|
|
155
|
+
if (result.width !== targetWidth || result.height !== targetHeight) {
|
|
156
|
+
target.canvas.width = result.width;
|
|
157
|
+
target.canvas.height = result.height;
|
|
158
|
+
}
|
|
159
|
+
target.putImageData(result, 0, 0);
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
settle(job, { error });
|
|
163
|
+
} finally {
|
|
164
|
+
scratch.width = 0;
|
|
165
|
+
scratch.height = 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function start(slot, job) {
|
|
169
|
+
slot.job = job;
|
|
170
|
+
job.slot = slot;
|
|
171
|
+
const image = job.image;
|
|
172
|
+
let bitmap;
|
|
173
|
+
try {
|
|
174
|
+
bitmap = await createImageBitmap(image._originalElement);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
slot.job = void 0;
|
|
177
|
+
settle(job, { error });
|
|
178
|
+
pump();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (job.settled || slot.job !== job) {
|
|
182
|
+
bitmap.close();
|
|
183
|
+
if (slot.job === job) slot.job = void 0;
|
|
184
|
+
pump();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const active = job.filters.filter((filter) => filter && !filter.isNeutralState());
|
|
188
|
+
const message = {
|
|
189
|
+
type: "apply",
|
|
190
|
+
id: job.id,
|
|
191
|
+
bitmap,
|
|
192
|
+
filters: active.map((filter) => filter.toObject()),
|
|
193
|
+
bandRows: options.bandRows
|
|
194
|
+
};
|
|
195
|
+
slot.worker.postMessage(message, [bitmap]);
|
|
196
|
+
}
|
|
197
|
+
function onReply(slot, reply) {
|
|
198
|
+
const job = slot.job?.id === reply.id ? slot.job : void 0;
|
|
199
|
+
if (reply.type === "progress") {
|
|
200
|
+
if (job && !job.settled) job.options.onProgress?.(reply.done);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (job) slot.job = void 0;
|
|
204
|
+
if (reply.type === "done") {
|
|
205
|
+
if (job) commit(job, (ctx) => {
|
|
206
|
+
if (reply.bitmap.width !== ctx.canvas.width || reply.bitmap.height !== ctx.canvas.height) {
|
|
207
|
+
ctx.canvas.width = reply.bitmap.width;
|
|
208
|
+
ctx.canvas.height = reply.bitmap.height;
|
|
209
|
+
}
|
|
210
|
+
ctx.drawImage(reply.bitmap, 0, 0);
|
|
211
|
+
});
|
|
212
|
+
reply.bitmap.close();
|
|
213
|
+
} else if (reply.type === "error" && job) settle(job, { error: /* @__PURE__ */ new Error(`The filter worker failed: ${reply.message}`) });
|
|
214
|
+
pump();
|
|
215
|
+
}
|
|
216
|
+
function pump() {
|
|
217
|
+
const available = pool();
|
|
218
|
+
if (!available) {
|
|
219
|
+
queue.splice(0).forEach((job) => void runOnMainThread(job));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const slot of available) {
|
|
223
|
+
if (slot.job) continue;
|
|
224
|
+
let job = queue.shift();
|
|
225
|
+
while (job && job.settled) job = queue.shift();
|
|
226
|
+
if (!job) return;
|
|
227
|
+
start(slot, job);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
get mode() {
|
|
232
|
+
return pool() ? "worker" : "main-thread";
|
|
233
|
+
},
|
|
234
|
+
apply(image, filters, applyOptions = {}) {
|
|
235
|
+
const list = [...filters ?? image.filters ?? []];
|
|
236
|
+
const id = nextId += 1;
|
|
237
|
+
const previous = latest.get(image);
|
|
238
|
+
latest.set(image, id);
|
|
239
|
+
if (previous !== void 0) {
|
|
240
|
+
const replaced = jobs.get(previous);
|
|
241
|
+
if (replaced) cancel(replaced);
|
|
242
|
+
}
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const job = {
|
|
245
|
+
id,
|
|
246
|
+
image,
|
|
247
|
+
filters: list,
|
|
248
|
+
options: applyOptions,
|
|
249
|
+
session: engine?.getDocumentInfo().session,
|
|
250
|
+
settled: false,
|
|
251
|
+
resolve,
|
|
252
|
+
reject
|
|
253
|
+
};
|
|
254
|
+
jobs.set(id, job);
|
|
255
|
+
const { signal } = applyOptions;
|
|
256
|
+
if (signal?.aborted) return cancel(job, abortReason(signal));
|
|
257
|
+
signal?.addEventListener("abort", () => cancel(job, abortReason(signal)), { once: true });
|
|
258
|
+
if (list.every((filter) => !filter || filter.isNeutralState())) {
|
|
259
|
+
commit(job, void 0);
|
|
260
|
+
applyOptions.onProgress?.(1);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (!require_pipeline.canRunInWorker(list.filter((filter) => filter && !filter.isNeutralState())) || !pool()) {
|
|
264
|
+
runOnMainThread(job);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
queue.push(job);
|
|
268
|
+
pump();
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
terminate() {
|
|
272
|
+
slots?.forEach((slot) => slot.worker.terminate());
|
|
273
|
+
slots = void 0;
|
|
274
|
+
workersFailed = true;
|
|
275
|
+
[...jobs.values()].forEach((job) => cancel(job));
|
|
276
|
+
queue.length = 0;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
exports.createFilterWorker = createFilterWorker;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DocumentEngine } from "../engine/create-document-engine.cjs";
|
|
2
|
+
import { FabricImage } from "fabric";
|
|
3
|
+
//#region src/filters/filter-worker.d.ts
|
|
4
|
+
export interface ImageFilter {
|
|
5
|
+
type?: string;
|
|
6
|
+
isNeutralState(): boolean;
|
|
7
|
+
toObject(): Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface FilterWorkerOptions {
|
|
10
|
+
createWorker?: () => Worker;
|
|
11
|
+
worker?: boolean;
|
|
12
|
+
poolSize?: number;
|
|
13
|
+
bandRows?: number;
|
|
14
|
+
engine?: DocumentEngine;
|
|
15
|
+
}
|
|
16
|
+
export interface ApplyFilterOptions {
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
onProgress?: (done: number) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface FilterWorker {
|
|
21
|
+
apply(image: FabricImage, filters?: ImageFilter[], options?: ApplyFilterOptions): Promise<boolean>;
|
|
22
|
+
readonly mode: "worker" | "main-thread";
|
|
23
|
+
terminate(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function createFilterWorker(options?: FilterWorkerOptions): FilterWorker;
|
|
26
|
+
//#endregion
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DocumentEngine } from "../engine/create-document-engine.js";
|
|
2
|
+
import { FabricImage } from "fabric";
|
|
3
|
+
//#region src/filters/filter-worker.d.ts
|
|
4
|
+
export interface ImageFilter {
|
|
5
|
+
type?: string;
|
|
6
|
+
isNeutralState(): boolean;
|
|
7
|
+
toObject(): Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface FilterWorkerOptions {
|
|
10
|
+
createWorker?: () => Worker;
|
|
11
|
+
worker?: boolean;
|
|
12
|
+
poolSize?: number;
|
|
13
|
+
bandRows?: number;
|
|
14
|
+
engine?: DocumentEngine;
|
|
15
|
+
}
|
|
16
|
+
export interface ApplyFilterOptions {
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
onProgress?: (done: number) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface FilterWorker {
|
|
21
|
+
apply(image: FabricImage, filters?: ImageFilter[], options?: ApplyFilterOptions): Promise<boolean>;
|
|
22
|
+
readonly mode: "worker" | "main-thread";
|
|
23
|
+
terminate(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function createFilterWorker(options?: FilterWorkerOptions): FilterWorker;
|
|
26
|
+
//#endregion
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { yieldToEventLoop } from "../util/concurrency.js";
|
|
2
|
+
import { canRunInWorker, runFilterPipeline } from "./pipeline.js";
|
|
3
|
+
import { util } from "fabric";
|
|
4
|
+
//#region src/filters/filter-worker.ts
|
|
5
|
+
function sizeOf(element) {
|
|
6
|
+
return {
|
|
7
|
+
width: element.naturalWidth || element.width,
|
|
8
|
+
height: element.naturalHeight || element.height
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function abortReason(signal) {
|
|
12
|
+
return signal.reason ?? new DOMException("The filter run was aborted", "AbortError");
|
|
13
|
+
}
|
|
14
|
+
function createFilterWorker(options = {}) {
|
|
15
|
+
const engine = options.engine;
|
|
16
|
+
const latest = /* @__PURE__ */ new WeakMap();
|
|
17
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
18
|
+
const queue = [];
|
|
19
|
+
let slots;
|
|
20
|
+
let workersFailed = options.worker === false;
|
|
21
|
+
let nextId = 0;
|
|
22
|
+
function makeWorker() {
|
|
23
|
+
if (typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") return void 0;
|
|
24
|
+
try {
|
|
25
|
+
return options.createWorker ? options.createWorker() : new Worker(new URL("./worker-entry.js", import.meta.url), { type: "module" });
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function pool() {
|
|
31
|
+
if (workersFailed) return void 0;
|
|
32
|
+
if (slots) return slots;
|
|
33
|
+
const made = [];
|
|
34
|
+
for (let index = 0; index < Math.max(1, options.poolSize ?? 1); index += 1) {
|
|
35
|
+
const worker = makeWorker();
|
|
36
|
+
if (!worker) break;
|
|
37
|
+
const slot = {
|
|
38
|
+
worker,
|
|
39
|
+
job: void 0
|
|
40
|
+
};
|
|
41
|
+
worker.onmessage = (event) => onReply(slot, event.data);
|
|
42
|
+
worker.onerror = (event) => {
|
|
43
|
+
event.preventDefault?.();
|
|
44
|
+
workersFailed = true;
|
|
45
|
+
const job = slot.job;
|
|
46
|
+
slot.job = void 0;
|
|
47
|
+
slots?.forEach((each) => each.worker.terminate());
|
|
48
|
+
slots = void 0;
|
|
49
|
+
if (job && !job.settled) runOnMainThread(job);
|
|
50
|
+
queue.splice(0).forEach((waiting) => void runOnMainThread(waiting));
|
|
51
|
+
};
|
|
52
|
+
made.push(slot);
|
|
53
|
+
}
|
|
54
|
+
if (made.length === 0) {
|
|
55
|
+
workersFailed = true;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
return slots = made;
|
|
59
|
+
}
|
|
60
|
+
function settle(job, outcome) {
|
|
61
|
+
if (job.settled) return;
|
|
62
|
+
job.settled = true;
|
|
63
|
+
jobs.delete(job.id);
|
|
64
|
+
if ("error" in outcome) job.reject(outcome.error);
|
|
65
|
+
else job.resolve(outcome.updated);
|
|
66
|
+
}
|
|
67
|
+
function cancel(job, reason) {
|
|
68
|
+
if (job.settled) return;
|
|
69
|
+
if (job.slot) job.slot.worker.postMessage({
|
|
70
|
+
type: "cancel",
|
|
71
|
+
id: job.id
|
|
72
|
+
});
|
|
73
|
+
settle(job, reason === void 0 ? { updated: false } : { error: reason });
|
|
74
|
+
}
|
|
75
|
+
function commit(job, draw) {
|
|
76
|
+
if (job.settled) return;
|
|
77
|
+
if (latest.get(job.image) !== job.id || engine && engine.getDocumentInfo().session !== job.session) {
|
|
78
|
+
settle(job, { updated: false });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const image = job.image;
|
|
82
|
+
const write = () => {
|
|
83
|
+
image.filters = job.filters;
|
|
84
|
+
image.removeTexture?.(`${image.cacheKey}_filtered`);
|
|
85
|
+
if (!draw) {
|
|
86
|
+
image._element = image._originalElement;
|
|
87
|
+
image._filteredEl = void 0;
|
|
88
|
+
image._filterScalingX = 1;
|
|
89
|
+
image._filterScalingY = 1;
|
|
90
|
+
} else {
|
|
91
|
+
const { width, height } = sizeOf(image._originalElement);
|
|
92
|
+
let target = image._filteredEl;
|
|
93
|
+
if (image._element === image._originalElement || !target) {
|
|
94
|
+
target = util.createCanvasElement();
|
|
95
|
+
image._filteredEl = target;
|
|
96
|
+
} else {
|
|
97
|
+
image._lastScaleX = 1;
|
|
98
|
+
image._lastScaleY = 1;
|
|
99
|
+
}
|
|
100
|
+
image._element = target;
|
|
101
|
+
target.width = width;
|
|
102
|
+
target.height = height;
|
|
103
|
+
draw(target.getContext("2d"), width, height);
|
|
104
|
+
if (target.width !== image._originalElement.width || target.height !== image._originalElement.height) {
|
|
105
|
+
image._filterScalingX = target.width / image._originalElement.width;
|
|
106
|
+
image._filterScalingY = target.height / image._originalElement.height;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
job.image.set("dirty", true);
|
|
110
|
+
};
|
|
111
|
+
if (engine) engine.transaction("Apply filters", write);
|
|
112
|
+
else write();
|
|
113
|
+
job.image.canvas?.requestRenderAll();
|
|
114
|
+
settle(job, { updated: true });
|
|
115
|
+
}
|
|
116
|
+
async function runOnMainThread(job) {
|
|
117
|
+
if (job.settled) return;
|
|
118
|
+
const image = job.image;
|
|
119
|
+
const active = job.filters.filter((filter) => filter && !filter.isNeutralState());
|
|
120
|
+
if (!canRunInWorker(active)) {
|
|
121
|
+
await yieldToEventLoop();
|
|
122
|
+
if (job.settled || latest.get(job.image) !== job.id) return settle(job, { updated: false });
|
|
123
|
+
if (engine) engine.transaction("Apply filters", () => image.applyFilters(job.filters));
|
|
124
|
+
else image.applyFilters(job.filters);
|
|
125
|
+
image.filters = job.filters;
|
|
126
|
+
job.image.canvas?.requestRenderAll();
|
|
127
|
+
job.options.onProgress?.(1);
|
|
128
|
+
return settle(job, { updated: true });
|
|
129
|
+
}
|
|
130
|
+
const { width, height } = sizeOf(image._originalElement);
|
|
131
|
+
const scratch = util.createCanvasElement();
|
|
132
|
+
scratch.width = width;
|
|
133
|
+
scratch.height = height;
|
|
134
|
+
const ctx = scratch.getContext("2d", { willReadFrequently: true });
|
|
135
|
+
ctx.drawImage(image._originalElement, 0, 0, width, height);
|
|
136
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
137
|
+
try {
|
|
138
|
+
const result = await runFilterPipeline(active, {
|
|
139
|
+
sourceWidth: width,
|
|
140
|
+
sourceHeight: height,
|
|
141
|
+
imageData,
|
|
142
|
+
originalImageData: imageData,
|
|
143
|
+
originalEl: image._originalElement,
|
|
144
|
+
canvasEl: scratch,
|
|
145
|
+
ctx,
|
|
146
|
+
filterBackend: void 0
|
|
147
|
+
}, {
|
|
148
|
+
bandRows: options.bandRows,
|
|
149
|
+
pause: yieldToEventLoop,
|
|
150
|
+
isCancelled: () => job.settled,
|
|
151
|
+
onProgress: job.options.onProgress
|
|
152
|
+
});
|
|
153
|
+
if (!result) return;
|
|
154
|
+
commit(job, (target, targetWidth, targetHeight) => {
|
|
155
|
+
if (result.width !== targetWidth || result.height !== targetHeight) {
|
|
156
|
+
target.canvas.width = result.width;
|
|
157
|
+
target.canvas.height = result.height;
|
|
158
|
+
}
|
|
159
|
+
target.putImageData(result, 0, 0);
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
settle(job, { error });
|
|
163
|
+
} finally {
|
|
164
|
+
scratch.width = 0;
|
|
165
|
+
scratch.height = 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function start(slot, job) {
|
|
169
|
+
slot.job = job;
|
|
170
|
+
job.slot = slot;
|
|
171
|
+
const image = job.image;
|
|
172
|
+
let bitmap;
|
|
173
|
+
try {
|
|
174
|
+
bitmap = await createImageBitmap(image._originalElement);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
slot.job = void 0;
|
|
177
|
+
settle(job, { error });
|
|
178
|
+
pump();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (job.settled || slot.job !== job) {
|
|
182
|
+
bitmap.close();
|
|
183
|
+
if (slot.job === job) slot.job = void 0;
|
|
184
|
+
pump();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const active = job.filters.filter((filter) => filter && !filter.isNeutralState());
|
|
188
|
+
const message = {
|
|
189
|
+
type: "apply",
|
|
190
|
+
id: job.id,
|
|
191
|
+
bitmap,
|
|
192
|
+
filters: active.map((filter) => filter.toObject()),
|
|
193
|
+
bandRows: options.bandRows
|
|
194
|
+
};
|
|
195
|
+
slot.worker.postMessage(message, [bitmap]);
|
|
196
|
+
}
|
|
197
|
+
function onReply(slot, reply) {
|
|
198
|
+
const job = slot.job?.id === reply.id ? slot.job : void 0;
|
|
199
|
+
if (reply.type === "progress") {
|
|
200
|
+
if (job && !job.settled) job.options.onProgress?.(reply.done);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (job) slot.job = void 0;
|
|
204
|
+
if (reply.type === "done") {
|
|
205
|
+
if (job) commit(job, (ctx) => {
|
|
206
|
+
if (reply.bitmap.width !== ctx.canvas.width || reply.bitmap.height !== ctx.canvas.height) {
|
|
207
|
+
ctx.canvas.width = reply.bitmap.width;
|
|
208
|
+
ctx.canvas.height = reply.bitmap.height;
|
|
209
|
+
}
|
|
210
|
+
ctx.drawImage(reply.bitmap, 0, 0);
|
|
211
|
+
});
|
|
212
|
+
reply.bitmap.close();
|
|
213
|
+
} else if (reply.type === "error" && job) settle(job, { error: /* @__PURE__ */ new Error(`The filter worker failed: ${reply.message}`) });
|
|
214
|
+
pump();
|
|
215
|
+
}
|
|
216
|
+
function pump() {
|
|
217
|
+
const available = pool();
|
|
218
|
+
if (!available) {
|
|
219
|
+
queue.splice(0).forEach((job) => void runOnMainThread(job));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const slot of available) {
|
|
223
|
+
if (slot.job) continue;
|
|
224
|
+
let job = queue.shift();
|
|
225
|
+
while (job && job.settled) job = queue.shift();
|
|
226
|
+
if (!job) return;
|
|
227
|
+
start(slot, job);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
get mode() {
|
|
232
|
+
return pool() ? "worker" : "main-thread";
|
|
233
|
+
},
|
|
234
|
+
apply(image, filters, applyOptions = {}) {
|
|
235
|
+
const list = [...filters ?? image.filters ?? []];
|
|
236
|
+
const id = nextId += 1;
|
|
237
|
+
const previous = latest.get(image);
|
|
238
|
+
latest.set(image, id);
|
|
239
|
+
if (previous !== void 0) {
|
|
240
|
+
const replaced = jobs.get(previous);
|
|
241
|
+
if (replaced) cancel(replaced);
|
|
242
|
+
}
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const job = {
|
|
245
|
+
id,
|
|
246
|
+
image,
|
|
247
|
+
filters: list,
|
|
248
|
+
options: applyOptions,
|
|
249
|
+
session: engine?.getDocumentInfo().session,
|
|
250
|
+
settled: false,
|
|
251
|
+
resolve,
|
|
252
|
+
reject
|
|
253
|
+
};
|
|
254
|
+
jobs.set(id, job);
|
|
255
|
+
const { signal } = applyOptions;
|
|
256
|
+
if (signal?.aborted) return cancel(job, abortReason(signal));
|
|
257
|
+
signal?.addEventListener("abort", () => cancel(job, abortReason(signal)), { once: true });
|
|
258
|
+
if (list.every((filter) => !filter || filter.isNeutralState())) {
|
|
259
|
+
commit(job, void 0);
|
|
260
|
+
applyOptions.onProgress?.(1);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (!canRunInWorker(list.filter((filter) => filter && !filter.isNeutralState())) || !pool()) {
|
|
264
|
+
runOnMainThread(job);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
queue.push(job);
|
|
268
|
+
pump();
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
terminate() {
|
|
272
|
+
slots?.forEach((slot) => slot.worker.terminate());
|
|
273
|
+
slots = void 0;
|
|
274
|
+
workersFailed = true;
|
|
275
|
+
[...jobs.values()].forEach((job) => cancel(job));
|
|
276
|
+
queue.length = 0;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
export { createFilterWorker };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//#region src/filters/pipeline.ts
|
|
2
|
+
const PIXEL_FILTERS = /* @__PURE__ */ new Set([
|
|
3
|
+
"Brightness",
|
|
4
|
+
"Contrast",
|
|
5
|
+
"Saturation",
|
|
6
|
+
"Vibrance",
|
|
7
|
+
"Grayscale",
|
|
8
|
+
"Invert",
|
|
9
|
+
"Gamma",
|
|
10
|
+
"Noise",
|
|
11
|
+
"RemoveColor",
|
|
12
|
+
"BlendColor",
|
|
13
|
+
"HueRotation",
|
|
14
|
+
"ColorMatrix",
|
|
15
|
+
"Sepia",
|
|
16
|
+
"Brownie",
|
|
17
|
+
"Vintage",
|
|
18
|
+
"Kodachrome",
|
|
19
|
+
"Technicolor",
|
|
20
|
+
"Polaroid",
|
|
21
|
+
"BlackWhite"
|
|
22
|
+
]);
|
|
23
|
+
const WORKER_FILTERS = /* @__PURE__ */ new Set([
|
|
24
|
+
...PIXEL_FILTERS,
|
|
25
|
+
"Blur",
|
|
26
|
+
"Convolute",
|
|
27
|
+
"Pixelate"
|
|
28
|
+
]);
|
|
29
|
+
function typeOf(filter) {
|
|
30
|
+
const own = filter.type ?? filter.constructor?.type;
|
|
31
|
+
return typeof own === "string" ? own : "";
|
|
32
|
+
}
|
|
33
|
+
function flattenFilters(filters) {
|
|
34
|
+
const flat = [];
|
|
35
|
+
const visit = (filter) => {
|
|
36
|
+
if (typeOf(filter) === "Composed") (filter.subFilters ?? []).forEach(visit);
|
|
37
|
+
else flat.push(filter);
|
|
38
|
+
};
|
|
39
|
+
filters.forEach(visit);
|
|
40
|
+
return flat;
|
|
41
|
+
}
|
|
42
|
+
function canRunInWorker(filters) {
|
|
43
|
+
return flattenFilters(filters).every((filter) => WORKER_FILTERS.has(typeOf(filter)));
|
|
44
|
+
}
|
|
45
|
+
async function runFilterPipeline(filters, state, options = {}) {
|
|
46
|
+
const steps = [];
|
|
47
|
+
for (const filter of flattenFilters(filters)) {
|
|
48
|
+
if (filter.isNeutralState?.()) continue;
|
|
49
|
+
const last = steps[steps.length - 1];
|
|
50
|
+
if (PIXEL_FILTERS.has(typeOf(filter)) && last && PIXEL_FILTERS.has(typeOf(last[0]))) last.push(filter);
|
|
51
|
+
else steps.push([filter]);
|
|
52
|
+
}
|
|
53
|
+
const bandRows = Math.max(1, Math.floor(options.bandRows ?? 64));
|
|
54
|
+
const units = steps.reduce((sum, step) => sum + (PIXEL_FILTERS.has(typeOf(step[0])) ? Math.ceil(state.imageData.height / bandRows) : 1), 0);
|
|
55
|
+
let done = 0;
|
|
56
|
+
const advance = async () => {
|
|
57
|
+
done += 1;
|
|
58
|
+
options.onProgress?.(units === 0 ? 1 : done / units);
|
|
59
|
+
if (options.isCancelled?.()) return false;
|
|
60
|
+
await options.pause?.();
|
|
61
|
+
return !options.isCancelled?.();
|
|
62
|
+
};
|
|
63
|
+
for (const step of steps) {
|
|
64
|
+
if (!PIXEL_FILTERS.has(typeOf(step[0]))) {
|
|
65
|
+
step[0].applyTo2d(state);
|
|
66
|
+
if (!await advance()) return void 0;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const whole = state.imageData;
|
|
70
|
+
const rowBytes = whole.width * 4;
|
|
71
|
+
for (let row = 0; row < whole.height; row += bandRows) {
|
|
72
|
+
const rows = Math.min(bandRows, whole.height - row);
|
|
73
|
+
const band = new ImageData(new Uint8ClampedArray(whole.data.buffer, whole.data.byteOffset + row * rowBytes, rows * rowBytes), whole.width, rows);
|
|
74
|
+
const bandState = {
|
|
75
|
+
...state,
|
|
76
|
+
imageData: band,
|
|
77
|
+
sourceHeight: rows
|
|
78
|
+
};
|
|
79
|
+
for (const filter of step) filter.applyTo2d(bandState);
|
|
80
|
+
if (!await advance()) return void 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (units === 0) options.onProgress?.(1);
|
|
84
|
+
return state.imageData;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
exports.PIXEL_FILTERS = PIXEL_FILTERS;
|
|
88
|
+
exports.WORKER_FILTERS = WORKER_FILTERS;
|
|
89
|
+
exports.canRunInWorker = canRunInWorker;
|
|
90
|
+
exports.flattenFilters = flattenFilters;
|
|
91
|
+
exports.runFilterPipeline = runFilterPipeline;
|