fabricjs-document-engine 1.1.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 +70 -1
- package/README.zh-CN.md +71 -1
- package/dist/assets/asset-pipeline.cjs +10 -8
- package/dist/assets/asset-pipeline.d.cts +1 -1
- package/dist/assets/asset-pipeline.d.ts +1 -1
- package/dist/assets/asset-pipeline.js +10 -8
- package/dist/assets/image-check.cjs +4 -3
- package/dist/assets/image-check.d.cts +1 -1
- package/dist/assets/image-check.d.ts +1 -1
- package/dist/assets/image-check.js +4 -3
- package/dist/commands/clipboard.cjs +5 -0
- package/dist/commands/clipboard.js +5 -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/document/document-format.d.cts +15 -0
- package/dist/document/document-format.d.ts +15 -0
- package/dist/document/validate-document.cjs +8 -0
- package/dist/document/validate-document.js +8 -0
- package/dist/engine/create-document-engine.cjs +115 -21
- package/dist/engine/create-document-engine.d.cts +24 -6
- package/dist/engine/create-document-engine.d.ts +24 -6
- package/dist/engine/create-document-engine.js +116 -22
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/export-options.cjs +3 -0
- package/dist/export/export-options.d.cts +6 -0
- package/dist/export/export-options.d.ts +6 -0
- package/dist/export/export-options.js +3 -0
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/render-export.cjs +42 -4
- package/dist/export/render-export.js +43 -5
- package/dist/export/svg/clip-masks.cjs +39 -0
- package/dist/export/svg/clip-masks.js +37 -0
- package/dist/export/svg/embed-assets.cjs +1 -0
- package/dist/export/svg/embed-assets.js +1 -0
- package/dist/export/svg/raster-object.cjs +82 -0
- package/dist/export/svg/raster-object.js +81 -0
- package/dist/{pdf → export/svg}/text-decorations.cjs +3 -3
- package/dist/{pdf → export/svg}/text-decorations.js +3 -3
- package/dist/export/svg/text-on-path.cjs +0 -10
- package/dist/export/svg/text-on-path.js +1 -10
- package/dist/fabric/fabric-adapter.cjs +5 -3
- package/dist/fabric/fabric-adapter.js +5 -3
- package/dist/fabric/object-registry.cjs +7 -1
- package/dist/fabric/object-registry.js +7 -1
- package/dist/fabric/page-state.cjs +69 -0
- package/dist/fabric/page-state.js +64 -0
- 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/history/apply-state.cjs +19 -2
- package/dist/history/apply-state.js +19 -2
- package/dist/history/create-history.cjs +53 -13
- package/dist/history/create-history.d.cts +8 -0
- package/dist/history/create-history.d.ts +8 -0
- package/dist/history/create-history.js +53 -13
- package/dist/history/snapshot.cjs +10 -1
- package/dist/history/snapshot.js +10 -2
- 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 +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/migrations/migrate-document.cjs +2 -1
- package/dist/migrations/migrate-document.js +2 -1
- package/dist/pdf/export-pdf.cjs +8 -67
- package/dist/pdf/export-pdf.js +8 -67
- 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/recovery/recovery-controller.cjs +148 -29
- package/dist/recovery/recovery-controller.d.cts +8 -0
- package/dist/recovery/recovery-controller.d.ts +8 -0
- package/dist/recovery/recovery-controller.js +148 -29
- package/dist/security/content-limits.cjs +43 -3
- package/dist/security/content-limits.d.cts +8 -0
- package/dist/security/content-limits.d.ts +8 -0
- package/dist/security/content-limits.js +39 -4
- 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/dist/util/concurrency.cjs +16 -3
- package/dist/util/concurrency.js +16 -3
- package/package.json +31 -1
- package/schema/document-v1.schema.json +5 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/filters/pipeline.d.ts
|
|
2
|
+
export declare const PIXEL_FILTERS: ReadonlySet<string>;
|
|
3
|
+
export declare const WORKER_FILTERS: ReadonlySet<string>;
|
|
4
|
+
export interface PipelineFilter {
|
|
5
|
+
type: string;
|
|
6
|
+
applyTo2d(state: PipelineState): void;
|
|
7
|
+
isNeutralState?(): boolean;
|
|
8
|
+
subFilters?: PipelineFilter[];
|
|
9
|
+
}
|
|
10
|
+
export interface PipelineState {
|
|
11
|
+
sourceWidth: number;
|
|
12
|
+
sourceHeight: number;
|
|
13
|
+
imageData: ImageData;
|
|
14
|
+
originalImageData: ImageData;
|
|
15
|
+
originalEl: unknown;
|
|
16
|
+
canvasEl: unknown;
|
|
17
|
+
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
|
18
|
+
filterBackend: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface PipelineOptions {
|
|
21
|
+
bandRows?: number;
|
|
22
|
+
onProgress?: (done: number) => void;
|
|
23
|
+
isCancelled?: () => boolean;
|
|
24
|
+
pause?: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare function flattenFilters<Filter extends {
|
|
27
|
+
type?: unknown;
|
|
28
|
+
subFilters?: Filter[];
|
|
29
|
+
}>(filters: readonly Filter[]): Filter[];
|
|
30
|
+
export declare function canRunInWorker(filters: ReadonlyArray<{
|
|
31
|
+
type?: unknown;
|
|
32
|
+
subFilters?: unknown[];
|
|
33
|
+
}>): boolean;
|
|
34
|
+
export declare function runFilterPipeline(filters: readonly PipelineFilter[], state: PipelineState, options?: PipelineOptions): Promise<ImageData | undefined>;
|
|
35
|
+
//#endregion
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/filters/pipeline.d.ts
|
|
2
|
+
export declare const PIXEL_FILTERS: ReadonlySet<string>;
|
|
3
|
+
export declare const WORKER_FILTERS: ReadonlySet<string>;
|
|
4
|
+
export interface PipelineFilter {
|
|
5
|
+
type: string;
|
|
6
|
+
applyTo2d(state: PipelineState): void;
|
|
7
|
+
isNeutralState?(): boolean;
|
|
8
|
+
subFilters?: PipelineFilter[];
|
|
9
|
+
}
|
|
10
|
+
export interface PipelineState {
|
|
11
|
+
sourceWidth: number;
|
|
12
|
+
sourceHeight: number;
|
|
13
|
+
imageData: ImageData;
|
|
14
|
+
originalImageData: ImageData;
|
|
15
|
+
originalEl: unknown;
|
|
16
|
+
canvasEl: unknown;
|
|
17
|
+
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
|
18
|
+
filterBackend: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface PipelineOptions {
|
|
21
|
+
bandRows?: number;
|
|
22
|
+
onProgress?: (done: number) => void;
|
|
23
|
+
isCancelled?: () => boolean;
|
|
24
|
+
pause?: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare function flattenFilters<Filter extends {
|
|
27
|
+
type?: unknown;
|
|
28
|
+
subFilters?: Filter[];
|
|
29
|
+
}>(filters: readonly Filter[]): Filter[];
|
|
30
|
+
export declare function canRunInWorker(filters: ReadonlyArray<{
|
|
31
|
+
type?: unknown;
|
|
32
|
+
subFilters?: unknown[];
|
|
33
|
+
}>): boolean;
|
|
34
|
+
export declare function runFilterPipeline(filters: readonly PipelineFilter[], state: PipelineState, options?: PipelineOptions): Promise<ImageData | undefined>;
|
|
35
|
+
//#endregion
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
export { PIXEL_FILTERS, WORKER_FILTERS, canRunInWorker, flattenFilters, runFilterPipeline };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const require_pipeline = require("./pipeline.cjs");
|
|
2
|
+
let fabric = require("fabric");
|
|
3
|
+
//#region src/filters/worker-entry.ts
|
|
4
|
+
const scope = self;
|
|
5
|
+
const running = /* @__PURE__ */ new Set();
|
|
6
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
7
|
+
const channel = new MessageChannel();
|
|
8
|
+
const waiting = [];
|
|
9
|
+
channel.port1.onmessage = () => waiting.shift()?.();
|
|
10
|
+
const pause = () => new Promise((resolve) => {
|
|
11
|
+
waiting.push(resolve);
|
|
12
|
+
channel.port2.postMessage(null);
|
|
13
|
+
});
|
|
14
|
+
async function apply(message) {
|
|
15
|
+
const { id, bitmap } = message;
|
|
16
|
+
running.add(id);
|
|
17
|
+
try {
|
|
18
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
19
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
20
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
21
|
+
bitmap.close();
|
|
22
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
23
|
+
const filters = await Promise.all(message.filters.map((json) => fabric.classRegistry.getClass(String(json.type)).fromObject(json)));
|
|
24
|
+
const result = await require_pipeline.runFilterPipeline(filters, {
|
|
25
|
+
sourceWidth: canvas.width,
|
|
26
|
+
sourceHeight: canvas.height,
|
|
27
|
+
imageData,
|
|
28
|
+
originalImageData: imageData,
|
|
29
|
+
originalEl: canvas,
|
|
30
|
+
canvasEl: canvas,
|
|
31
|
+
ctx,
|
|
32
|
+
filterBackend: void 0
|
|
33
|
+
}, {
|
|
34
|
+
bandRows: message.bandRows,
|
|
35
|
+
pause,
|
|
36
|
+
isCancelled: () => cancelled.has(id),
|
|
37
|
+
onProgress: (done) => scope.postMessage({
|
|
38
|
+
type: "progress",
|
|
39
|
+
id,
|
|
40
|
+
done
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
if (!result) {
|
|
44
|
+
scope.postMessage({
|
|
45
|
+
type: "cancelled",
|
|
46
|
+
id
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (result.width !== canvas.width || result.height !== canvas.height) {
|
|
51
|
+
canvas.width = result.width;
|
|
52
|
+
canvas.height = result.height;
|
|
53
|
+
}
|
|
54
|
+
ctx.putImageData(result, 0, 0);
|
|
55
|
+
const output = canvas.transferToImageBitmap();
|
|
56
|
+
scope.postMessage({
|
|
57
|
+
type: "done",
|
|
58
|
+
id,
|
|
59
|
+
bitmap: output
|
|
60
|
+
}, [output]);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
scope.postMessage({
|
|
63
|
+
type: "error",
|
|
64
|
+
id,
|
|
65
|
+
message: error instanceof Error ? error.message : String(error)
|
|
66
|
+
});
|
|
67
|
+
} finally {
|
|
68
|
+
running.delete(id);
|
|
69
|
+
cancelled.delete(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
scope.onmessage = (event) => {
|
|
73
|
+
const message = event.data;
|
|
74
|
+
if (message.type === "cancel") {
|
|
75
|
+
if (running.has(message.id)) cancelled.add(message.id);
|
|
76
|
+
} else apply(message);
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/filters/worker-entry.d.ts
|
|
2
|
+
export interface ApplyMessage {
|
|
3
|
+
type: "apply";
|
|
4
|
+
id: number;
|
|
5
|
+
bitmap: ImageBitmap;
|
|
6
|
+
filters: Array<Record<string, unknown>>;
|
|
7
|
+
bandRows?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CancelMessage {
|
|
10
|
+
type: "cancel";
|
|
11
|
+
id: number;
|
|
12
|
+
}
|
|
13
|
+
export type WorkerReply = {
|
|
14
|
+
type: "progress";
|
|
15
|
+
id: number;
|
|
16
|
+
done: number;
|
|
17
|
+
} | {
|
|
18
|
+
type: "done";
|
|
19
|
+
id: number;
|
|
20
|
+
bitmap: ImageBitmap;
|
|
21
|
+
} | {
|
|
22
|
+
type: "cancelled";
|
|
23
|
+
id: number;
|
|
24
|
+
} | {
|
|
25
|
+
type: "error";
|
|
26
|
+
id: number;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/filters/worker-entry.d.ts
|
|
2
|
+
export interface ApplyMessage {
|
|
3
|
+
type: "apply";
|
|
4
|
+
id: number;
|
|
5
|
+
bitmap: ImageBitmap;
|
|
6
|
+
filters: Array<Record<string, unknown>>;
|
|
7
|
+
bandRows?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CancelMessage {
|
|
10
|
+
type: "cancel";
|
|
11
|
+
id: number;
|
|
12
|
+
}
|
|
13
|
+
export type WorkerReply = {
|
|
14
|
+
type: "progress";
|
|
15
|
+
id: number;
|
|
16
|
+
done: number;
|
|
17
|
+
} | {
|
|
18
|
+
type: "done";
|
|
19
|
+
id: number;
|
|
20
|
+
bitmap: ImageBitmap;
|
|
21
|
+
} | {
|
|
22
|
+
type: "cancelled";
|
|
23
|
+
id: number;
|
|
24
|
+
} | {
|
|
25
|
+
type: "error";
|
|
26
|
+
id: number;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { runFilterPipeline } from "./pipeline.js";
|
|
2
|
+
import { classRegistry } from "fabric";
|
|
3
|
+
//#region src/filters/worker-entry.ts
|
|
4
|
+
const scope = self;
|
|
5
|
+
const running = /* @__PURE__ */ new Set();
|
|
6
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
7
|
+
const channel = new MessageChannel();
|
|
8
|
+
const waiting = [];
|
|
9
|
+
channel.port1.onmessage = () => waiting.shift()?.();
|
|
10
|
+
const pause = () => new Promise((resolve) => {
|
|
11
|
+
waiting.push(resolve);
|
|
12
|
+
channel.port2.postMessage(null);
|
|
13
|
+
});
|
|
14
|
+
async function apply(message) {
|
|
15
|
+
const { id, bitmap } = message;
|
|
16
|
+
running.add(id);
|
|
17
|
+
try {
|
|
18
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
19
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
20
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
21
|
+
bitmap.close();
|
|
22
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
23
|
+
const filters = await Promise.all(message.filters.map((json) => classRegistry.getClass(String(json.type)).fromObject(json)));
|
|
24
|
+
const result = await runFilterPipeline(filters, {
|
|
25
|
+
sourceWidth: canvas.width,
|
|
26
|
+
sourceHeight: canvas.height,
|
|
27
|
+
imageData,
|
|
28
|
+
originalImageData: imageData,
|
|
29
|
+
originalEl: canvas,
|
|
30
|
+
canvasEl: canvas,
|
|
31
|
+
ctx,
|
|
32
|
+
filterBackend: void 0
|
|
33
|
+
}, {
|
|
34
|
+
bandRows: message.bandRows,
|
|
35
|
+
pause,
|
|
36
|
+
isCancelled: () => cancelled.has(id),
|
|
37
|
+
onProgress: (done) => scope.postMessage({
|
|
38
|
+
type: "progress",
|
|
39
|
+
id,
|
|
40
|
+
done
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
if (!result) {
|
|
44
|
+
scope.postMessage({
|
|
45
|
+
type: "cancelled",
|
|
46
|
+
id
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (result.width !== canvas.width || result.height !== canvas.height) {
|
|
51
|
+
canvas.width = result.width;
|
|
52
|
+
canvas.height = result.height;
|
|
53
|
+
}
|
|
54
|
+
ctx.putImageData(result, 0, 0);
|
|
55
|
+
const output = canvas.transferToImageBitmap();
|
|
56
|
+
scope.postMessage({
|
|
57
|
+
type: "done",
|
|
58
|
+
id,
|
|
59
|
+
bitmap: output
|
|
60
|
+
}, [output]);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
scope.postMessage({
|
|
63
|
+
type: "error",
|
|
64
|
+
id,
|
|
65
|
+
message: error instanceof Error ? error.message : String(error)
|
|
66
|
+
});
|
|
67
|
+
} finally {
|
|
68
|
+
running.delete(id);
|
|
69
|
+
cancelled.delete(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
scope.onmessage = (event) => {
|
|
73
|
+
const message = event.data;
|
|
74
|
+
if (message.type === "cancel") {
|
|
75
|
+
if (running.has(message.id)) cancelled.add(message.id);
|
|
76
|
+
} else apply(message);
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
package/dist/filters.cjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_pipeline = require("./filters/pipeline.cjs");
|
|
3
|
+
const require_filter_worker = require("./filters/filter-worker.cjs");
|
|
4
|
+
exports.PIXEL_FILTERS = require_pipeline.PIXEL_FILTERS;
|
|
5
|
+
exports.WORKER_FILTERS = require_pipeline.WORKER_FILTERS;
|
|
6
|
+
exports.canRunInWorker = require_pipeline.canRunInWorker;
|
|
7
|
+
exports.createFilterWorker = require_filter_worker.createFilterWorker;
|
|
8
|
+
exports.flattenFilters = require_pipeline.flattenFilters;
|
|
9
|
+
exports.runFilterPipeline = require_pipeline.runFilterPipeline;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ApplyFilterOptions, FilterWorker, FilterWorkerOptions, ImageFilter, createFilterWorker } from "./filters/filter-worker.cjs";
|
|
2
|
+
import { PIXEL_FILTERS, PipelineFilter, PipelineOptions, PipelineState, WORKER_FILTERS, canRunInWorker, flattenFilters, runFilterPipeline } from "./filters/pipeline.cjs";
|
|
3
|
+
export { type ApplyFilterOptions, type FilterWorker, type FilterWorkerOptions, type ImageFilter, PIXEL_FILTERS, type PipelineFilter, type PipelineOptions, type PipelineState, WORKER_FILTERS, canRunInWorker, createFilterWorker, flattenFilters, runFilterPipeline };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ApplyFilterOptions, FilterWorker, FilterWorkerOptions, ImageFilter, createFilterWorker } from "./filters/filter-worker.js";
|
|
2
|
+
import { PIXEL_FILTERS, PipelineFilter, PipelineOptions, PipelineState, WORKER_FILTERS, canRunInWorker, flattenFilters, runFilterPipeline } from "./filters/pipeline.js";
|
|
3
|
+
export { type ApplyFilterOptions, type FilterWorker, type FilterWorkerOptions, type ImageFilter, PIXEL_FILTERS, type PipelineFilter, type PipelineOptions, type PipelineState, WORKER_FILTERS, canRunInWorker, createFilterWorker, flattenFilters, runFilterPipeline };
|
package/dist/filters.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { PIXEL_FILTERS, WORKER_FILTERS, canRunInWorker, flattenFilters, runFilterPipeline } from "./filters/pipeline.js";
|
|
2
|
+
import { createFilterWorker } from "./filters/filter-worker.js";
|
|
3
|
+
export { PIXEL_FILTERS, WORKER_FILTERS, canRunInWorker, createFilterWorker, flattenFilters, runFilterPipeline };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const require_page_state = require("../fabric/page-state.cjs");
|
|
1
2
|
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
2
3
|
const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
|
|
4
|
+
const require_snapshot = require("./snapshot.cjs");
|
|
3
5
|
//#region src/history/apply-state.ts
|
|
4
6
|
function arrangeObjects(current, change, created) {
|
|
5
7
|
const arranged = [];
|
|
@@ -20,10 +22,24 @@ function arrangeObjects(current, change, created) {
|
|
|
20
22
|
const position = new Map(change.order.map((id, index) => [id, index]));
|
|
21
23
|
return arranged.sort((first, second) => (position.get(require_object_ids.readObjectId(first) ?? "") ?? Infinity) - (position.get(require_object_ids.readObjectId(second) ?? "") ?? Infinity));
|
|
22
24
|
}
|
|
23
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Applies a change. `isStale` is checked after every wait: when it returns
|
|
27
|
+
* true the canvas now shows another document, so nothing is applied and the
|
|
28
|
+
* call returns false.
|
|
29
|
+
*/
|
|
30
|
+
async function applyStateChange(canvas, change, isStale = () => false) {
|
|
31
|
+
const page = change.objects.get(require_snapshot.PAGE_KEY);
|
|
32
|
+
if (page) {
|
|
33
|
+
if (isStale()) return false;
|
|
34
|
+
await require_page_state.applyPage(canvas, JSON.parse(page));
|
|
35
|
+
}
|
|
24
36
|
const serializedObjects = [];
|
|
25
|
-
for (const json of change.objects
|
|
37
|
+
for (const [id, json] of change.objects) if (id !== "__page__" && json !== null) serializedObjects.push(JSON.parse(json));
|
|
26
38
|
const createdObjects = await require_fabric_adapter.createObjects(serializedObjects);
|
|
39
|
+
if (isStale()) {
|
|
40
|
+
createdObjects.forEach((object) => object.dispose?.());
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
27
43
|
const createdById = new Map(createdObjects.map((object) => [require_object_ids.readObjectId(object) ?? "", object]));
|
|
28
44
|
canvas.discardActiveObject?.();
|
|
29
45
|
const current = canvas.getObjects();
|
|
@@ -45,6 +61,7 @@ async function applyStateChange(canvas, change) {
|
|
|
45
61
|
canvas.renderOnAddRemove = renderOnAddRemove;
|
|
46
62
|
}
|
|
47
63
|
canvas.requestRenderAll();
|
|
64
|
+
return true;
|
|
48
65
|
}
|
|
49
66
|
//#endregion
|
|
50
67
|
exports.applyStateChange = applyStateChange;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { applyPage } from "../fabric/page-state.js";
|
|
1
2
|
import { readObjectId } from "../fabric/object-ids.js";
|
|
2
3
|
import { createObjects } from "../fabric/fabric-adapter.js";
|
|
4
|
+
import { PAGE_KEY } from "./snapshot.js";
|
|
3
5
|
//#region src/history/apply-state.ts
|
|
4
6
|
function arrangeObjects(current, change, created) {
|
|
5
7
|
const arranged = [];
|
|
@@ -20,10 +22,24 @@ function arrangeObjects(current, change, created) {
|
|
|
20
22
|
const position = new Map(change.order.map((id, index) => [id, index]));
|
|
21
23
|
return arranged.sort((first, second) => (position.get(readObjectId(first) ?? "") ?? Infinity) - (position.get(readObjectId(second) ?? "") ?? Infinity));
|
|
22
24
|
}
|
|
23
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Applies a change. `isStale` is checked after every wait: when it returns
|
|
27
|
+
* true the canvas now shows another document, so nothing is applied and the
|
|
28
|
+
* call returns false.
|
|
29
|
+
*/
|
|
30
|
+
async function applyStateChange(canvas, change, isStale = () => false) {
|
|
31
|
+
const page = change.objects.get(PAGE_KEY);
|
|
32
|
+
if (page) {
|
|
33
|
+
if (isStale()) return false;
|
|
34
|
+
await applyPage(canvas, JSON.parse(page));
|
|
35
|
+
}
|
|
24
36
|
const serializedObjects = [];
|
|
25
|
-
for (const json of change.objects
|
|
37
|
+
for (const [id, json] of change.objects) if (id !== "__page__" && json !== null) serializedObjects.push(JSON.parse(json));
|
|
26
38
|
const createdObjects = await createObjects(serializedObjects);
|
|
39
|
+
if (isStale()) {
|
|
40
|
+
createdObjects.forEach((object) => object.dispose?.());
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
27
43
|
const createdById = new Map(createdObjects.map((object) => [readObjectId(object) ?? "", object]));
|
|
28
44
|
canvas.discardActiveObject?.();
|
|
29
45
|
const current = canvas.getObjects();
|
|
@@ -45,6 +61,7 @@ async function applyStateChange(canvas, change) {
|
|
|
45
61
|
canvas.renderOnAddRemove = renderOnAddRemove;
|
|
46
62
|
}
|
|
47
63
|
canvas.requestRenderAll();
|
|
64
|
+
return true;
|
|
48
65
|
}
|
|
49
66
|
//#endregion
|
|
50
67
|
export { applyStateChange };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
const require_snapshot = require("./snapshot.cjs");
|
|
1
2
|
const require_apply_state = require("./apply-state.cjs");
|
|
2
3
|
const require_history_stack = require("./history-stack.cjs");
|
|
3
|
-
const require_snapshot = require("./snapshot.cjs");
|
|
4
4
|
//#region src/history/create-history.ts
|
|
5
5
|
const verbsByAction = {
|
|
6
6
|
drag: "Move",
|
|
@@ -24,18 +24,23 @@ function describePendingChanges(changes) {
|
|
|
24
24
|
return changes.length === 1 ? `${verb} ${changes[0].noun}` : `${verb} ${changes.length} objects`;
|
|
25
25
|
}
|
|
26
26
|
function createHistory(options) {
|
|
27
|
-
const { canvas,
|
|
27
|
+
const { canvas, serializeState, onChange, onContentChange, onLiveChange } = options;
|
|
28
|
+
const takeSnapshot = () => {
|
|
29
|
+
const { objects, page } = serializeState();
|
|
30
|
+
return require_snapshot.createSnapshot(objects, page);
|
|
31
|
+
};
|
|
28
32
|
const stack = require_history_stack.createHistoryStack({
|
|
29
33
|
steps: Math.max(1, options.limit ?? 100),
|
|
30
34
|
bytes: Math.max(0, options.maxBytes ?? 67108864)
|
|
31
35
|
});
|
|
32
|
-
let snapshot =
|
|
36
|
+
let snapshot = takeSnapshot();
|
|
33
37
|
let pendingChanges = [];
|
|
34
38
|
let flushScheduled = false;
|
|
35
39
|
let transactionDepth = 0;
|
|
36
40
|
let pausedDepth = 0;
|
|
37
41
|
let applying = false;
|
|
38
42
|
let queue = Promise.resolve();
|
|
43
|
+
let epoch = 0;
|
|
39
44
|
function state() {
|
|
40
45
|
const undoLabels = stack.undoLabels();
|
|
41
46
|
const redoLabels = stack.redoLabels();
|
|
@@ -48,7 +53,7 @@ function createHistory(options) {
|
|
|
48
53
|
}
|
|
49
54
|
function commit(label) {
|
|
50
55
|
pendingChanges = [];
|
|
51
|
-
const nextSnapshot =
|
|
56
|
+
const nextSnapshot = takeSnapshot();
|
|
52
57
|
const difference = require_snapshot.diffSnapshots(snapshot, nextSnapshot);
|
|
53
58
|
snapshot = nextSnapshot;
|
|
54
59
|
if (difference === null) return false;
|
|
@@ -86,26 +91,56 @@ function createHistory(options) {
|
|
|
86
91
|
notice(verbsByAction[action] ?? "Transform", event.target);
|
|
87
92
|
};
|
|
88
93
|
const handleTextEdited = ({ target }) => notice("Edit", target);
|
|
94
|
+
const handleTextChanged = () => {
|
|
95
|
+
if (!applying && pausedDepth === 0) onLiveChange?.();
|
|
96
|
+
};
|
|
89
97
|
const listeners = [
|
|
90
98
|
["object:added", handleAdded],
|
|
91
99
|
["object:removed", handleRemoved],
|
|
92
100
|
["object:modified", handleModified],
|
|
93
|
-
["text:editing:exited", handleTextEdited]
|
|
101
|
+
["text:editing:exited", handleTextEdited],
|
|
102
|
+
["text:changed", handleTextChanged]
|
|
94
103
|
];
|
|
95
104
|
const eventTarget = canvas;
|
|
96
105
|
listeners.forEach(([name, handler]) => eventTarget.on(name, handler));
|
|
97
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Puts the canvas back to the last recorded snapshot. The snapshot is the
|
|
108
|
+
* state before the transaction, so the difference to undo is exactly what
|
|
109
|
+
* the transaction changed: one serialization, then only the changed
|
|
110
|
+
* objects are rebuilt.
|
|
111
|
+
*/
|
|
112
|
+
function rollBack() {
|
|
113
|
+
const difference = require_snapshot.diffSnapshots(snapshot, takeSnapshot());
|
|
114
|
+
pendingChanges = [];
|
|
115
|
+
if (difference === null) return Promise.resolve();
|
|
116
|
+
applying = true;
|
|
117
|
+
return runInOrder(async () => {
|
|
118
|
+
try {
|
|
119
|
+
await require_apply_state.applyStateChange(canvas, difference.before);
|
|
120
|
+
} finally {
|
|
121
|
+
applying = false;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function transaction(label, work, transactionOptions = {}) {
|
|
98
126
|
if (transactionDepth === 0) commitPendingChanges();
|
|
127
|
+
const outermost = transactionDepth === 0;
|
|
99
128
|
transactionDepth += 1;
|
|
100
129
|
const finish = () => {
|
|
101
130
|
transactionDepth -= 1;
|
|
102
131
|
if (transactionDepth === 0) commit(label);
|
|
103
132
|
};
|
|
133
|
+
const fail = () => {
|
|
134
|
+
transactionDepth -= 1;
|
|
135
|
+
if (transactionDepth > 0) return void 0;
|
|
136
|
+
if (outermost && transactionOptions.rollback) return rollBack();
|
|
137
|
+
commit(label);
|
|
138
|
+
};
|
|
104
139
|
let result;
|
|
105
140
|
try {
|
|
106
141
|
result = work();
|
|
107
142
|
} catch (error) {
|
|
108
|
-
|
|
143
|
+
fail()?.catch(() => void 0);
|
|
109
144
|
throw error;
|
|
110
145
|
}
|
|
111
146
|
if (!isPromiseLike(result)) {
|
|
@@ -115,8 +150,8 @@ function createHistory(options) {
|
|
|
115
150
|
return Promise.resolve(result).then((value) => {
|
|
116
151
|
finish();
|
|
117
152
|
return value;
|
|
118
|
-
}, (error) => {
|
|
119
|
-
|
|
153
|
+
}, async (error) => {
|
|
154
|
+
await fail();
|
|
120
155
|
throw error;
|
|
121
156
|
});
|
|
122
157
|
}
|
|
@@ -148,16 +183,19 @@ function createHistory(options) {
|
|
|
148
183
|
commitPendingChanges();
|
|
149
184
|
const step = takeStep();
|
|
150
185
|
if (step === void 0) return false;
|
|
186
|
+
const startedIn = epoch;
|
|
151
187
|
applying = true;
|
|
188
|
+
let applied;
|
|
152
189
|
try {
|
|
153
|
-
await require_apply_state.applyStateChange(canvas, pickChange(step));
|
|
190
|
+
applied = await require_apply_state.applyStateChange(canvas, pickChange(step), () => epoch !== startedIn);
|
|
154
191
|
} catch (error) {
|
|
155
|
-
putBack(step);
|
|
192
|
+
if (epoch === startedIn) putBack(step);
|
|
156
193
|
throw error;
|
|
157
194
|
} finally {
|
|
158
195
|
applying = false;
|
|
159
196
|
}
|
|
160
|
-
|
|
197
|
+
if (!applied) return false;
|
|
198
|
+
snapshot = takeSnapshot();
|
|
161
199
|
moveTo(step);
|
|
162
200
|
onContentChange();
|
|
163
201
|
onChange(state());
|
|
@@ -170,6 +208,7 @@ function createHistory(options) {
|
|
|
170
208
|
return commit(label);
|
|
171
209
|
},
|
|
172
210
|
transaction,
|
|
211
|
+
settled: () => queue.then(() => void 0),
|
|
173
212
|
undo: () => travel(() => stack.takeUndo(), (step) => step.before, (step) => stack.returnUndo(step), (step) => stack.returnRedo(step)),
|
|
174
213
|
redo: () => travel(() => stack.takeRedo(), (step) => step.after, (step) => stack.returnRedo(step), (step) => stack.returnUndo(step)),
|
|
175
214
|
state,
|
|
@@ -182,7 +221,8 @@ function createHistory(options) {
|
|
|
182
221
|
reset() {
|
|
183
222
|
pendingChanges = [];
|
|
184
223
|
stack.clear();
|
|
185
|
-
|
|
224
|
+
epoch += 1;
|
|
225
|
+
snapshot = takeSnapshot();
|
|
186
226
|
onChange(state());
|
|
187
227
|
},
|
|
188
228
|
destroy() {
|
|
@@ -10,4 +10,12 @@ export interface HistoryState {
|
|
|
10
10
|
undoLabel: string | undefined;
|
|
11
11
|
redoLabel: string | undefined;
|
|
12
12
|
}
|
|
13
|
+
export interface TransactionOptions {
|
|
14
|
+
/**
|
|
15
|
+
* When `work` throws or rejects, put the canvas back as it was before the
|
|
16
|
+
* transaction, record no undo step and leave the document's unsaved state
|
|
17
|
+
* alone. Applies to the outermost transaction.
|
|
18
|
+
*/
|
|
19
|
+
rollback?: boolean;
|
|
20
|
+
}
|
|
13
21
|
//#endregion
|
|
@@ -10,4 +10,12 @@ export interface HistoryState {
|
|
|
10
10
|
undoLabel: string | undefined;
|
|
11
11
|
redoLabel: string | undefined;
|
|
12
12
|
}
|
|
13
|
+
export interface TransactionOptions {
|
|
14
|
+
/**
|
|
15
|
+
* When `work` throws or rejects, put the canvas back as it was before the
|
|
16
|
+
* transaction, record no undo step and leave the document's unsaved state
|
|
17
|
+
* alone. Applies to the outermost transaction.
|
|
18
|
+
*/
|
|
19
|
+
rollback?: boolean;
|
|
20
|
+
}
|
|
13
21
|
//#endregion
|