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,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 };
|
|
@@ -127,12 +127,110 @@ function contentBounds(objects) {
|
|
|
127
127
|
height: 0
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
function dropBrokenUses(document) {
|
|
131
|
+
for (const use of Array.from(document.getElementsByTagName("use"))) {
|
|
132
|
+
const link = (use.getAttribute("href") ?? use.getAttributeNS(XLINK, "href") ?? "").trim();
|
|
133
|
+
if (!link.startsWith("#") || link.length === 1 || document.getElementById(link.slice(1)) === null) use.remove();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const GROUP_MARK = "data-fde-svg-group";
|
|
137
|
+
function markGroups(document) {
|
|
138
|
+
for (const name of ["g", "a"]) for (const element of Array.from(document.documentElement.getElementsByTagName(name))) element.setAttribute(GROUP_MARK, "");
|
|
139
|
+
}
|
|
140
|
+
function svgMetadata(element) {
|
|
141
|
+
const metadata = {};
|
|
142
|
+
const id = element.getAttribute("id");
|
|
143
|
+
if (id) metadata.svgId = id;
|
|
144
|
+
const className = element.getAttribute("class");
|
|
145
|
+
if (className) metadata.svgClass = className;
|
|
146
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
147
|
+
if (!attribute.name.startsWith("data-") || attribute.name === GROUP_MARK) continue;
|
|
148
|
+
(metadata.svgData ?? (metadata.svgData = {}))[attribute.name.slice(5)] = attribute.value;
|
|
149
|
+
}
|
|
150
|
+
return metadata;
|
|
151
|
+
}
|
|
152
|
+
function ownOpacity(element) {
|
|
153
|
+
const fromStyle = /(?:^|;)\s*opacity\s*:\s*([0-9.]+)/.exec(element.getAttribute("style") ?? "")?.[1];
|
|
154
|
+
const value = Number.parseFloat(fromStyle ?? element.getAttribute("opacity") ?? "1");
|
|
155
|
+
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 1;
|
|
156
|
+
}
|
|
157
|
+
function rebuildGroups(objects, elements, root) {
|
|
158
|
+
const top = [];
|
|
159
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
160
|
+
const groupNodes = /* @__PURE__ */ new Set();
|
|
161
|
+
const chains = /* @__PURE__ */ new Map();
|
|
162
|
+
const elementOf = /* @__PURE__ */ new Map();
|
|
163
|
+
objects.forEach((object, index) => {
|
|
164
|
+
const element = elements[index];
|
|
165
|
+
elementOf.set(object, element);
|
|
166
|
+
const chain = [];
|
|
167
|
+
for (let parent = element.parentElement; parent && parent !== root; parent = parent.parentElement) if (parent.hasAttribute(GROUP_MARK)) chain.push(parent);
|
|
168
|
+
chains.set(object, chain);
|
|
169
|
+
let children = top;
|
|
170
|
+
for (let level = chain.length - 1; level >= 0; level -= 1) {
|
|
171
|
+
const groupElement = chain[level];
|
|
172
|
+
let node = nodes.get(groupElement);
|
|
173
|
+
if (!node) {
|
|
174
|
+
node = {
|
|
175
|
+
element: groupElement,
|
|
176
|
+
children: []
|
|
177
|
+
};
|
|
178
|
+
nodes.set(groupElement, node);
|
|
179
|
+
groupNodes.add(node);
|
|
180
|
+
children.push(node);
|
|
181
|
+
}
|
|
182
|
+
children = node.children;
|
|
183
|
+
}
|
|
184
|
+
children.push(object);
|
|
185
|
+
const own = element.getAttribute("id");
|
|
186
|
+
const leaf = object;
|
|
187
|
+
if (!own && chain.length > 0 && leaf.id !== void 0) delete leaf.id;
|
|
188
|
+
const inherited = chain.reduce((product, groupElement) => product * ownOpacity(groupElement), 1);
|
|
189
|
+
if (inherited > 0) object.opacity = Math.min(1, object.opacity / inherited);
|
|
190
|
+
object.set(svgMetadata(element));
|
|
191
|
+
});
|
|
192
|
+
const hoistClip = (node, group) => {
|
|
193
|
+
if (!node.element.hasAttribute("clip-path")) return;
|
|
194
|
+
let clip;
|
|
195
|
+
for (const object of objects) {
|
|
196
|
+
const chain = chains.get(object);
|
|
197
|
+
const at = chain.indexOf(node.element);
|
|
198
|
+
if (at === -1) continue;
|
|
199
|
+
if (elementOf.get(object).hasAttribute("clip-path") || chain.slice(0, at).some((between) => between.hasAttribute("clip-path")) || !object.clipPath) continue;
|
|
200
|
+
if (!clip) {
|
|
201
|
+
const absolute = fabric.util.multiplyTransformMatrices(object.calcTransformMatrix(), object.clipPath.calcTransformMatrix());
|
|
202
|
+
clip = object.clipPath;
|
|
203
|
+
fabric.util.applyTransformToObject(clip, fabric.util.multiplyTransformMatrices(fabric.util.invertTransform(group.calcTransformMatrix()), absolute));
|
|
204
|
+
}
|
|
205
|
+
object.clipPath = void 0;
|
|
206
|
+
object.set("dirty", true);
|
|
207
|
+
}
|
|
208
|
+
if (clip) group.clipPath = clip;
|
|
209
|
+
};
|
|
210
|
+
const build = (item) => {
|
|
211
|
+
if (!groupNodes.has(item)) return item;
|
|
212
|
+
const node = item;
|
|
213
|
+
const group = new fabric.Group(node.children.map(build));
|
|
214
|
+
group.set({
|
|
215
|
+
opacity: ownOpacity(node.element),
|
|
216
|
+
...svgMetadata(node.element)
|
|
217
|
+
});
|
|
218
|
+
const id = node.element.getAttribute("id");
|
|
219
|
+
if (id) group.id = id;
|
|
220
|
+
hoistClip(node, group);
|
|
221
|
+
group.setCoords();
|
|
222
|
+
return group;
|
|
223
|
+
};
|
|
224
|
+
return top.map(build);
|
|
225
|
+
}
|
|
130
226
|
/** Parses and places an SVG. Adding the result to a canvas is up to the caller. */
|
|
131
227
|
async function readSvg(svg, options = {}, limits = {}) {
|
|
132
228
|
if (typeof svg !== "string" || svg.trim().length === 0) refuse("The SVG is empty");
|
|
133
229
|
const warnings = [];
|
|
134
230
|
const document = parse(svg);
|
|
135
231
|
sanitize(document, limits, warnings);
|
|
232
|
+
dropBrokenUses(document);
|
|
233
|
+
if (options.preserveGroups) markGroups(document);
|
|
136
234
|
let parsed;
|
|
137
235
|
try {
|
|
138
236
|
parsed = await (0, fabric.parseSVGDocument)(document, void 0, {
|
|
@@ -145,6 +243,10 @@ async function readSvg(svg, options = {}, limits = {}) {
|
|
|
145
243
|
}
|
|
146
244
|
if (options.signal?.aborted) throw new require_errors.DocumentEngineError("LOAD_ABORTED", "The SVG import was cancelled");
|
|
147
245
|
let objects = parsed.objects.filter((object) => object !== null && object !== void 0);
|
|
246
|
+
if (options.preserveGroups) {
|
|
247
|
+
const elements = parsed.elements.filter((_, index) => parsed.objects[index] !== null && parsed.objects[index] !== void 0);
|
|
248
|
+
objects = rebuildGroups(objects, elements, document.documentElement);
|
|
249
|
+
}
|
|
148
250
|
const declared = {
|
|
149
251
|
width: parsed.options.width ?? 0,
|
|
150
252
|
height: parsed.options.height ?? 0
|
|
@@ -19,6 +19,7 @@ export interface SvgImportOptions {
|
|
|
19
19
|
offscreen?: "keep" | "drop" | "clip";
|
|
20
20
|
/** One group (the default), or separate objects. */
|
|
21
21
|
as?: "group" | "objects";
|
|
22
|
+
preserveGroups?: boolean;
|
|
22
23
|
/** Where the viewport's top-left corner lands on the canvas. Default 0, 0. */
|
|
23
24
|
left?: number;
|
|
24
25
|
top?: number;
|
|
@@ -19,6 +19,7 @@ export interface SvgImportOptions {
|
|
|
19
19
|
offscreen?: "keep" | "drop" | "clip";
|
|
20
20
|
/** One group (the default), or separate objects. */
|
|
21
21
|
as?: "group" | "objects";
|
|
22
|
+
preserveGroups?: boolean;
|
|
22
23
|
/** Where the viewport's top-left corner lands on the canvas. Default 0, 0. */
|
|
23
24
|
left?: number;
|
|
24
25
|
top?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DocumentEngineError, isDocumentEngineError } from "../engine/errors.js";
|
|
2
2
|
import { isSafeImageUrl } from "../security/content-limits.js";
|
|
3
|
-
import { FixedLayout, Group, LayoutManager, Rect, parseSVGDocument } from "fabric";
|
|
3
|
+
import { FixedLayout, Group, LayoutManager, Rect, parseSVGDocument, util } from "fabric";
|
|
4
4
|
//#region src/import/svg-import.ts
|
|
5
5
|
const XLINK = "http://www.w3.org/1999/xlink";
|
|
6
6
|
const REMOVED_ELEMENTS = [
|
|
@@ -127,12 +127,110 @@ function contentBounds(objects) {
|
|
|
127
127
|
height: 0
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
function dropBrokenUses(document) {
|
|
131
|
+
for (const use of Array.from(document.getElementsByTagName("use"))) {
|
|
132
|
+
const link = (use.getAttribute("href") ?? use.getAttributeNS(XLINK, "href") ?? "").trim();
|
|
133
|
+
if (!link.startsWith("#") || link.length === 1 || document.getElementById(link.slice(1)) === null) use.remove();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const GROUP_MARK = "data-fde-svg-group";
|
|
137
|
+
function markGroups(document) {
|
|
138
|
+
for (const name of ["g", "a"]) for (const element of Array.from(document.documentElement.getElementsByTagName(name))) element.setAttribute(GROUP_MARK, "");
|
|
139
|
+
}
|
|
140
|
+
function svgMetadata(element) {
|
|
141
|
+
const metadata = {};
|
|
142
|
+
const id = element.getAttribute("id");
|
|
143
|
+
if (id) metadata.svgId = id;
|
|
144
|
+
const className = element.getAttribute("class");
|
|
145
|
+
if (className) metadata.svgClass = className;
|
|
146
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
147
|
+
if (!attribute.name.startsWith("data-") || attribute.name === GROUP_MARK) continue;
|
|
148
|
+
(metadata.svgData ?? (metadata.svgData = {}))[attribute.name.slice(5)] = attribute.value;
|
|
149
|
+
}
|
|
150
|
+
return metadata;
|
|
151
|
+
}
|
|
152
|
+
function ownOpacity(element) {
|
|
153
|
+
const fromStyle = /(?:^|;)\s*opacity\s*:\s*([0-9.]+)/.exec(element.getAttribute("style") ?? "")?.[1];
|
|
154
|
+
const value = Number.parseFloat(fromStyle ?? element.getAttribute("opacity") ?? "1");
|
|
155
|
+
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 1;
|
|
156
|
+
}
|
|
157
|
+
function rebuildGroups(objects, elements, root) {
|
|
158
|
+
const top = [];
|
|
159
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
160
|
+
const groupNodes = /* @__PURE__ */ new Set();
|
|
161
|
+
const chains = /* @__PURE__ */ new Map();
|
|
162
|
+
const elementOf = /* @__PURE__ */ new Map();
|
|
163
|
+
objects.forEach((object, index) => {
|
|
164
|
+
const element = elements[index];
|
|
165
|
+
elementOf.set(object, element);
|
|
166
|
+
const chain = [];
|
|
167
|
+
for (let parent = element.parentElement; parent && parent !== root; parent = parent.parentElement) if (parent.hasAttribute(GROUP_MARK)) chain.push(parent);
|
|
168
|
+
chains.set(object, chain);
|
|
169
|
+
let children = top;
|
|
170
|
+
for (let level = chain.length - 1; level >= 0; level -= 1) {
|
|
171
|
+
const groupElement = chain[level];
|
|
172
|
+
let node = nodes.get(groupElement);
|
|
173
|
+
if (!node) {
|
|
174
|
+
node = {
|
|
175
|
+
element: groupElement,
|
|
176
|
+
children: []
|
|
177
|
+
};
|
|
178
|
+
nodes.set(groupElement, node);
|
|
179
|
+
groupNodes.add(node);
|
|
180
|
+
children.push(node);
|
|
181
|
+
}
|
|
182
|
+
children = node.children;
|
|
183
|
+
}
|
|
184
|
+
children.push(object);
|
|
185
|
+
const own = element.getAttribute("id");
|
|
186
|
+
const leaf = object;
|
|
187
|
+
if (!own && chain.length > 0 && leaf.id !== void 0) delete leaf.id;
|
|
188
|
+
const inherited = chain.reduce((product, groupElement) => product * ownOpacity(groupElement), 1);
|
|
189
|
+
if (inherited > 0) object.opacity = Math.min(1, object.opacity / inherited);
|
|
190
|
+
object.set(svgMetadata(element));
|
|
191
|
+
});
|
|
192
|
+
const hoistClip = (node, group) => {
|
|
193
|
+
if (!node.element.hasAttribute("clip-path")) return;
|
|
194
|
+
let clip;
|
|
195
|
+
for (const object of objects) {
|
|
196
|
+
const chain = chains.get(object);
|
|
197
|
+
const at = chain.indexOf(node.element);
|
|
198
|
+
if (at === -1) continue;
|
|
199
|
+
if (elementOf.get(object).hasAttribute("clip-path") || chain.slice(0, at).some((between) => between.hasAttribute("clip-path")) || !object.clipPath) continue;
|
|
200
|
+
if (!clip) {
|
|
201
|
+
const absolute = util.multiplyTransformMatrices(object.calcTransformMatrix(), object.clipPath.calcTransformMatrix());
|
|
202
|
+
clip = object.clipPath;
|
|
203
|
+
util.applyTransformToObject(clip, util.multiplyTransformMatrices(util.invertTransform(group.calcTransformMatrix()), absolute));
|
|
204
|
+
}
|
|
205
|
+
object.clipPath = void 0;
|
|
206
|
+
object.set("dirty", true);
|
|
207
|
+
}
|
|
208
|
+
if (clip) group.clipPath = clip;
|
|
209
|
+
};
|
|
210
|
+
const build = (item) => {
|
|
211
|
+
if (!groupNodes.has(item)) return item;
|
|
212
|
+
const node = item;
|
|
213
|
+
const group = new Group(node.children.map(build));
|
|
214
|
+
group.set({
|
|
215
|
+
opacity: ownOpacity(node.element),
|
|
216
|
+
...svgMetadata(node.element)
|
|
217
|
+
});
|
|
218
|
+
const id = node.element.getAttribute("id");
|
|
219
|
+
if (id) group.id = id;
|
|
220
|
+
hoistClip(node, group);
|
|
221
|
+
group.setCoords();
|
|
222
|
+
return group;
|
|
223
|
+
};
|
|
224
|
+
return top.map(build);
|
|
225
|
+
}
|
|
130
226
|
/** Parses and places an SVG. Adding the result to a canvas is up to the caller. */
|
|
131
227
|
async function readSvg(svg, options = {}, limits = {}) {
|
|
132
228
|
if (typeof svg !== "string" || svg.trim().length === 0) refuse("The SVG is empty");
|
|
133
229
|
const warnings = [];
|
|
134
230
|
const document = parse(svg);
|
|
135
231
|
sanitize(document, limits, warnings);
|
|
232
|
+
dropBrokenUses(document);
|
|
233
|
+
if (options.preserveGroups) markGroups(document);
|
|
136
234
|
let parsed;
|
|
137
235
|
try {
|
|
138
236
|
parsed = await parseSVGDocument(document, void 0, {
|
|
@@ -145,6 +243,10 @@ async function readSvg(svg, options = {}, limits = {}) {
|
|
|
145
243
|
}
|
|
146
244
|
if (options.signal?.aborted) throw new DocumentEngineError("LOAD_ABORTED", "The SVG import was cancelled");
|
|
147
245
|
let objects = parsed.objects.filter((object) => object !== null && object !== void 0);
|
|
246
|
+
if (options.preserveGroups) {
|
|
247
|
+
const elements = parsed.elements.filter((_, index) => parsed.objects[index] !== null && parsed.objects[index] !== void 0);
|
|
248
|
+
objects = rebuildGroups(objects, elements, document.documentElement);
|
|
249
|
+
}
|
|
148
250
|
const declared = {
|
|
149
251
|
width: parsed.options.width ?? 0,
|
|
150
252
|
height: parsed.options.height ?? 0
|