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.
Files changed (81) hide show
  1. package/README.md +31 -0
  2. package/README.zh-CN.md +32 -0
  3. package/dist/commands/text-edit.cjs +223 -0
  4. package/dist/commands/text-edit.d.cts +23 -0
  5. package/dist/commands/text-edit.d.ts +23 -0
  6. package/dist/commands/text-edit.js +218 -0
  7. package/dist/fabric/object-registry.cjs +7 -1
  8. package/dist/fabric/object-registry.js +7 -1
  9. package/dist/filters/filter-worker.cjs +281 -0
  10. package/dist/filters/filter-worker.d.cts +26 -0
  11. package/dist/filters/filter-worker.d.ts +26 -0
  12. package/dist/filters/filter-worker.js +281 -0
  13. package/dist/filters/pipeline.cjs +91 -0
  14. package/dist/filters/pipeline.d.cts +35 -0
  15. package/dist/filters/pipeline.d.ts +35 -0
  16. package/dist/filters/pipeline.js +87 -0
  17. package/dist/filters/worker-entry.cjs +78 -0
  18. package/dist/filters/worker-entry.d.cts +29 -0
  19. package/dist/filters/worker-entry.d.ts +29 -0
  20. package/dist/filters/worker-entry.js +78 -0
  21. package/dist/filters.cjs +9 -0
  22. package/dist/filters.d.cts +3 -0
  23. package/dist/filters.d.ts +3 -0
  24. package/dist/filters.js +3 -0
  25. package/dist/import/svg-import.cjs +102 -0
  26. package/dist/import/svg-import.d.cts +1 -0
  27. package/dist/import/svg-import.d.ts +1 -0
  28. package/dist/import/svg-import.js +103 -1
  29. package/dist/index.cjs +7 -0
  30. package/dist/index.d.cts +2 -1
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.js +2 -1
  33. package/dist/performance/batch.cjs +31 -0
  34. package/dist/performance/batch.d.cts +4 -0
  35. package/dist/performance/batch.d.ts +4 -0
  36. package/dist/performance/batch.js +31 -0
  37. package/dist/performance/dirty-regions.cjs +378 -0
  38. package/dist/performance/dirty-regions.d.cts +23 -0
  39. package/dist/performance/dirty-regions.d.ts +23 -0
  40. package/dist/performance/dirty-regions.js +378 -0
  41. package/dist/performance/monitor.cjs +60 -0
  42. package/dist/performance/monitor.d.cts +20 -0
  43. package/dist/performance/monitor.d.ts +20 -0
  44. package/dist/performance/monitor.js +60 -0
  45. package/dist/performance/spatial-index.cjs +78 -0
  46. package/dist/performance/spatial-index.d.cts +21 -0
  47. package/dist/performance/spatial-index.d.ts +21 -0
  48. package/dist/performance/spatial-index.js +78 -0
  49. package/dist/performance.cjs +9 -0
  50. package/dist/performance.d.cts +5 -0
  51. package/dist/performance.d.ts +5 -0
  52. package/dist/performance.js +5 -0
  53. package/dist/text/bounded-textbox.cjs +306 -0
  54. package/dist/text/bounded-textbox.d.cts +53 -0
  55. package/dist/text/bounded-textbox.d.ts +53 -0
  56. package/dist/text/bounded-textbox.js +306 -0
  57. package/dist/text/mobile-input.cjs +146 -0
  58. package/dist/text/mobile-input.d.cts +19 -0
  59. package/dist/text/mobile-input.d.ts +19 -0
  60. package/dist/text/mobile-input.js +145 -0
  61. package/dist/text/register.cjs +17 -0
  62. package/dist/text/register.d.cts +5 -0
  63. package/dist/text/register.d.ts +5 -0
  64. package/dist/text/register.js +16 -0
  65. package/dist/text/shaping.cjs +125 -0
  66. package/dist/text/shaping.d.cts +28 -0
  67. package/dist/text/shaping.d.ts +28 -0
  68. package/dist/text/shaping.js +121 -0
  69. package/dist/text/vertical-orientation.cjs +192 -0
  70. package/dist/text/vertical-orientation.d.cts +10 -0
  71. package/dist/text/vertical-orientation.d.ts +10 -0
  72. package/dist/text/vertical-orientation.js +190 -0
  73. package/dist/text/vertical-text.cjs +336 -0
  74. package/dist/text/vertical-text.d.cts +54 -0
  75. package/dist/text/vertical-text.d.ts +54 -0
  76. package/dist/text/vertical-text.js +336 -0
  77. package/dist/text.cjs +21 -0
  78. package/dist/text.d.cts +7 -0
  79. package/dist/text.d.ts +7 -0
  80. package/dist/text.js +7 -0
  81. 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;