fabric-vectr 6.7.11 → 6.7.13
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/dist/index.js +34 -9
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.min.mjs.map +1 -1
- package/dist/index.mjs +34 -9
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +34 -9
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.d.ts +6 -6
- package/dist/index.node.mjs +34 -9
- package/dist/index.node.mjs.map +1 -1
- package/dist/package.json.min.mjs +1 -1
- package/dist/package.json.mjs +1 -1
- package/dist/src/Pattern/Pattern.d.ts.map +1 -1
- package/dist/src/Pattern/Pattern.min.mjs +1 -1
- package/dist/src/Pattern/Pattern.min.mjs.map +1 -1
- package/dist/src/Pattern/Pattern.mjs +5 -1
- package/dist/src/Pattern/Pattern.mjs.map +1 -1
- package/dist/src/shapes/Group.d.ts.map +1 -1
- package/dist/src/shapes/Group.min.mjs +1 -1
- package/dist/src/shapes/Group.min.mjs.map +1 -1
- package/dist/src/shapes/Group.mjs +11 -1
- package/dist/src/shapes/Group.mjs.map +1 -1
- package/dist/src/shapes/Image.d.ts +2 -2
- package/dist/src/shapes/Image.d.ts.map +1 -1
- package/dist/src/shapes/Image.min.mjs +1 -1
- package/dist/src/shapes/Image.min.mjs.map +1 -1
- package/dist/src/shapes/Image.mjs +4 -2
- package/dist/src/shapes/Image.mjs.map +1 -1
- package/dist/src/util/misc/objectEnlive.d.ts +6 -1
- package/dist/src/util/misc/objectEnlive.d.ts.map +1 -1
- package/dist/src/util/misc/objectEnlive.min.mjs +1 -1
- package/dist/src/util/misc/objectEnlive.min.mjs.map +1 -1
- package/dist/src/util/misc/objectEnlive.mjs +16 -5
- package/dist/src/util/misc/objectEnlive.mjs.map +1 -1
- package/dist-extensions/src/Pattern/Pattern.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Group.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Image.d.ts +2 -2
- package/dist-extensions/src/shapes/Image.d.ts.map +1 -1
- package/dist-extensions/src/util/misc/objectEnlive.d.ts +6 -1
- package/dist-extensions/src/util/misc/objectEnlive.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Pattern/Pattern.spec.ts +63 -0
- package/src/Pattern/Pattern.ts +3 -1
- package/src/shapes/Group.spec.ts +40 -0
- package/src/shapes/Group.ts +20 -1
- package/src/shapes/Image.spec.ts +21 -2
- package/src/shapes/Image.ts +24 -12
- package/src/util/misc/objectEnlive.spec.ts +49 -2
- package/src/util/misc/objectEnlive.ts +29 -5
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as objectEnlive from '../util/misc/objectEnlive';
|
|
2
|
+
import { Pattern } from './Pattern';
|
|
3
|
+
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('Pattern', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('enlives from serialized object', async () => {
|
|
12
|
+
const pattern = await Pattern.fromObject({
|
|
13
|
+
type: 'pattern',
|
|
14
|
+
source: '',
|
|
15
|
+
repeat: 'repeat-x',
|
|
16
|
+
offsetX: 12,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(pattern).toBeInstanceOf(Pattern);
|
|
20
|
+
expect(pattern.repeat).toBe('repeat-x');
|
|
21
|
+
expect(pattern.offsetX).toBe(12);
|
|
22
|
+
expect(pattern.sourceToString()).toBe('');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('falls back to an empty source when the pattern image fails to load', async () => {
|
|
26
|
+
const loadImageSpy = vi.spyOn(objectEnlive, 'loadImage');
|
|
27
|
+
const emptyImage = new Image();
|
|
28
|
+
loadImageSpy.mockResolvedValueOnce(emptyImage);
|
|
29
|
+
|
|
30
|
+
const pattern = await Pattern.fromObject({
|
|
31
|
+
type: 'pattern',
|
|
32
|
+
source: 'bad-url',
|
|
33
|
+
repeat: 'repeat',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(pattern).toBeInstanceOf(Pattern);
|
|
37
|
+
expect(pattern.source).toBe(emptyImage);
|
|
38
|
+
expect(loadImageSpy).toHaveBeenCalledWith('bad-url', {
|
|
39
|
+
crossOrigin: undefined,
|
|
40
|
+
fallbackToEmptyImage: true,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('preserves abort errors', async () => {
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const loadImageSpy = vi.spyOn(objectEnlive, 'loadImage');
|
|
47
|
+
const abortError = new Error('aborted');
|
|
48
|
+
|
|
49
|
+
controller.abort();
|
|
50
|
+
loadImageSpy.mockRejectedValueOnce(abortError);
|
|
51
|
+
|
|
52
|
+
await expect(
|
|
53
|
+
Pattern.fromObject(
|
|
54
|
+
{
|
|
55
|
+
type: 'pattern',
|
|
56
|
+
source: 'bad-url',
|
|
57
|
+
repeat: 'repeat',
|
|
58
|
+
},
|
|
59
|
+
{ signal: controller.signal },
|
|
60
|
+
),
|
|
61
|
+
).rejects.toBe(abortError);
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/Pattern/Pattern.ts
CHANGED
|
@@ -208,9 +208,11 @@ export class Pattern {
|
|
|
208
208
|
}: SerializedPatternOptions,
|
|
209
209
|
options?: Abortable,
|
|
210
210
|
): Promise<Pattern> {
|
|
211
|
+
const { crossOrigin } = otherOptions;
|
|
211
212
|
const img = await loadImage(source, {
|
|
212
213
|
...options,
|
|
213
|
-
crossOrigin
|
|
214
|
+
crossOrigin,
|
|
215
|
+
fallbackToEmptyImage: true,
|
|
214
216
|
});
|
|
215
217
|
return new this({
|
|
216
218
|
...otherOptions,
|
package/src/shapes/Group.spec.ts
CHANGED
|
@@ -11,6 +11,9 @@ import { Rect } from './Rect';
|
|
|
11
11
|
import { FabricObject } from './Object/FabricObject';
|
|
12
12
|
import { FabricImage } from './Image';
|
|
13
13
|
import { SignalAbortedError } from '../util/internals/console';
|
|
14
|
+
import { calcPlaneChangeMatrix } from '../util/misc/planeChange';
|
|
15
|
+
import { matrixToSVG } from '../util/misc/svgExport';
|
|
16
|
+
import { ActiveSelection } from './ActiveSelection';
|
|
14
17
|
|
|
15
18
|
import { describe, expect, it, test, vi } from 'vitest';
|
|
16
19
|
|
|
@@ -82,6 +85,43 @@ describe('Group', () => {
|
|
|
82
85
|
});
|
|
83
86
|
});
|
|
84
87
|
|
|
88
|
+
it('导出 SVG 时会补上组内多选对象的活动选择平面变换', () => {
|
|
89
|
+
const object = new Rect({
|
|
90
|
+
width: 40,
|
|
91
|
+
height: 20,
|
|
92
|
+
left: 120,
|
|
93
|
+
top: 80,
|
|
94
|
+
angle: 15,
|
|
95
|
+
strokeWidth: 0,
|
|
96
|
+
});
|
|
97
|
+
const sibling = new Rect({
|
|
98
|
+
width: 10,
|
|
99
|
+
height: 10,
|
|
100
|
+
left: 10,
|
|
101
|
+
top: 10,
|
|
102
|
+
strokeWidth: 0,
|
|
103
|
+
});
|
|
104
|
+
const canvas = new Canvas();
|
|
105
|
+
const group = new Group([object, sibling], {
|
|
106
|
+
left: 300,
|
|
107
|
+
top: 200,
|
|
108
|
+
angle: 25,
|
|
109
|
+
strokeWidth: 0,
|
|
110
|
+
});
|
|
111
|
+
canvas.add(group);
|
|
112
|
+
const activeSelection = new ActiveSelection([object], { canvas });
|
|
113
|
+
|
|
114
|
+
const planeChangeMatrix = calcPlaneChangeMatrix(
|
|
115
|
+
activeSelection.calcTransformMatrix(),
|
|
116
|
+
group.calcTransformMatrix(),
|
|
117
|
+
);
|
|
118
|
+
const svg = group.toSVG();
|
|
119
|
+
|
|
120
|
+
expect(object.group).toBe(activeSelection);
|
|
121
|
+
expect(object.parent).toBe(group);
|
|
122
|
+
expect(svg).toContain(`<g transform="${matrixToSVG(planeChangeMatrix)}">`);
|
|
123
|
+
});
|
|
124
|
+
|
|
85
125
|
describe('With fit-content layout manager', () => {
|
|
86
126
|
test('will serialize correctly without default values', async () => {
|
|
87
127
|
const { group } = makeGenericGroup({
|
package/src/shapes/Group.ts
CHANGED
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
enlivenObjectEnlivables,
|
|
15
15
|
enlivenObjects,
|
|
16
16
|
} from '../util/misc/objectEnlive';
|
|
17
|
+
import { calcPlaneChangeMatrix } from '../util/misc/planeChange';
|
|
17
18
|
import { applyTransformToObject } from '../util/misc/objectTransforms';
|
|
19
|
+
import { matrixToSVG } from '../util/misc/svgExport';
|
|
18
20
|
import { FabricObject } from './Object/FabricObject';
|
|
19
21
|
import { Rect } from './Rect';
|
|
20
22
|
import { classRegistry } from '../ClassRegistry';
|
|
@@ -640,9 +642,26 @@ export class Group
|
|
|
640
642
|
_toSVG(reviver?: TSVGReviver) {
|
|
641
643
|
const svgString = ['<g ', 'COMMON_PARTS', ' >\n'];
|
|
642
644
|
const bg = this._createSVGBgRect(reviver);
|
|
645
|
+
const groupTransformMatrix = this.calcTransformMatrix();
|
|
643
646
|
bg && svgString.push('\t\t', bg);
|
|
644
647
|
for (let i = 0; i < this._objects.length; i++) {
|
|
645
|
-
|
|
648
|
+
const object = this._objects[i];
|
|
649
|
+
if (object.group && object.group !== this) {
|
|
650
|
+
// 组内对象被 ActiveSelection 临时接管时,需要补回当前组坐标系的变换。
|
|
651
|
+
const planeChangeMatrix = calcPlaneChangeMatrix(
|
|
652
|
+
object.group.calcTransformMatrix(),
|
|
653
|
+
groupTransformMatrix,
|
|
654
|
+
);
|
|
655
|
+
svgString.push(
|
|
656
|
+
'\t\t<g transform="',
|
|
657
|
+
matrixToSVG(planeChangeMatrix),
|
|
658
|
+
'">\n\t\t',
|
|
659
|
+
object.toSVG(reviver),
|
|
660
|
+
'\t\t</g>\n',
|
|
661
|
+
);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
svgString.push('\t\t', object.toSVG(reviver));
|
|
646
665
|
}
|
|
647
666
|
svgString.push('</g>\n');
|
|
648
667
|
return svgString;
|
package/src/shapes/Image.spec.ts
CHANGED
|
@@ -2,11 +2,14 @@ import { FabricImage } from './Image';
|
|
|
2
2
|
import { Shadow } from '../Shadow';
|
|
3
3
|
import { Brightness } from '../filters/Brightness';
|
|
4
4
|
import { loadSVGFromString } from '../parser/loadSVGFromString';
|
|
5
|
+
import * as objectEnlive from '../util/misc/objectEnlive';
|
|
5
6
|
|
|
6
7
|
const mockImage = new Image(100, 100);
|
|
7
8
|
|
|
8
|
-
vi.mock('../util/misc/objectEnlive', () => {
|
|
9
|
-
const all = vi.importActual('../util/misc/objectEnlive')
|
|
9
|
+
vi.mock('../util/misc/objectEnlive', async () => {
|
|
10
|
+
const all = await vi.importActual<typeof import('../util/misc/objectEnlive')>(
|
|
11
|
+
'../util/misc/objectEnlive',
|
|
12
|
+
);
|
|
10
13
|
return {
|
|
11
14
|
...all,
|
|
12
15
|
loadImage: vi.fn(async (src) => {
|
|
@@ -28,6 +31,22 @@ vi.mock('../filters/FilterBackend', () => ({
|
|
|
28
31
|
}));
|
|
29
32
|
|
|
30
33
|
describe('FabricImage', () => {
|
|
34
|
+
test('fromObject requests empty-image fallback for deserialization', async () => {
|
|
35
|
+
const image = await FabricImage.fromObject({
|
|
36
|
+
type: 'image',
|
|
37
|
+
src: 'broken-image-url',
|
|
38
|
+
crossOrigin: null,
|
|
39
|
+
cropX: 0,
|
|
40
|
+
cropY: 0,
|
|
41
|
+
} as any);
|
|
42
|
+
|
|
43
|
+
expect(image).toBeInstanceOf(FabricImage);
|
|
44
|
+
expect(objectEnlive.loadImage).toHaveBeenCalledWith('broken-image-url', {
|
|
45
|
+
crossOrigin: null,
|
|
46
|
+
fallbackToEmptyImage: true,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
31
50
|
describe('Svg export', () => {
|
|
32
51
|
test('It exports an svg with styles for an image with stroke', () => {
|
|
33
52
|
const imgElement = new Image(200, 200);
|
package/src/shapes/Image.ts
CHANGED
|
@@ -799,21 +799,33 @@ export class FabricImage<
|
|
|
799
799
|
options?: Abortable,
|
|
800
800
|
) {
|
|
801
801
|
return Promise.all([
|
|
802
|
-
loadImage(src!, {
|
|
802
|
+
loadImage(src!, {
|
|
803
|
+
...options,
|
|
804
|
+
crossOrigin,
|
|
805
|
+
fallbackToEmptyImage: true,
|
|
806
|
+
}),
|
|
803
807
|
f && enlivenObjects<BaseFilter<string>>(f, options),
|
|
804
808
|
// TODO: redundant - handled by enlivenObjectEnlivables
|
|
805
|
-
rf && enlivenObjects<
|
|
809
|
+
rf && enlivenObjects<Resize>([rf], options),
|
|
806
810
|
enlivenObjectEnlivables(object, options),
|
|
807
|
-
]).then(
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
811
|
+
]).then(
|
|
812
|
+
([el, filters = [], resizeFilters = [], hydratedProps = {}]: [
|
|
813
|
+
HTMLImageElement,
|
|
814
|
+
BaseFilter<string, Record<string, any>>[]?,
|
|
815
|
+
Resize[]?,
|
|
816
|
+
Record<string, any>?,
|
|
817
|
+
]) => {
|
|
818
|
+
const [resizeFilter] = resizeFilters;
|
|
819
|
+
return new this(el, {
|
|
820
|
+
...object,
|
|
821
|
+
// TODO: this creates a difference between image creation and restoring from JSON
|
|
822
|
+
src,
|
|
823
|
+
filters,
|
|
824
|
+
resizeFilter,
|
|
825
|
+
...hydratedProps,
|
|
826
|
+
});
|
|
827
|
+
},
|
|
828
|
+
);
|
|
817
829
|
}
|
|
818
830
|
|
|
819
831
|
/**
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as dom from './dom';
|
|
2
|
+
import { enlivenObjects, loadImage } from './objectEnlive';
|
|
2
3
|
import { Rect, type RectProps } from '../../shapes/Rect';
|
|
3
4
|
import { Shadow } from '../../Shadow';
|
|
4
5
|
import { classRegistry } from '../../ClassRegistry';
|
|
6
|
+
import { FabricError } from '../internals/console';
|
|
5
7
|
|
|
6
|
-
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
9
|
|
|
8
10
|
const mockedRectWithCustomProperty = {
|
|
9
11
|
type: 'rect',
|
|
@@ -68,3 +70,48 @@ describe('enlivenObjects', () => {
|
|
|
68
70
|
expect(rect.custom3).toBeInstanceOf(Test);
|
|
69
71
|
});
|
|
70
72
|
});
|
|
73
|
+
|
|
74
|
+
const createMockImage = () => {
|
|
75
|
+
let currentSrc = '';
|
|
76
|
+
return {
|
|
77
|
+
onload: null as null | (() => void),
|
|
78
|
+
onerror: null as null | (() => void),
|
|
79
|
+
crossOrigin: null as string | null,
|
|
80
|
+
complete: false,
|
|
81
|
+
naturalWidth: 0,
|
|
82
|
+
naturalHeight: 0,
|
|
83
|
+
get src() {
|
|
84
|
+
return currentSrc;
|
|
85
|
+
},
|
|
86
|
+
set src(value: string) {
|
|
87
|
+
currentSrc = value;
|
|
88
|
+
if (value === 'bad-url') {
|
|
89
|
+
queueMicrotask(() => this.onerror?.());
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
} as unknown as HTMLImageElement;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
describe('loadImage', () => {
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
vi.restoreAllMocks();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rejects loading errors by default', async () => {
|
|
101
|
+
vi.spyOn(dom, 'createImage').mockReturnValue(createMockImage());
|
|
102
|
+
|
|
103
|
+
await expect(loadImage('bad-url')).rejects.toEqual(
|
|
104
|
+
new FabricError('Error loading bad-url'),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('falls back to an empty image when requested', async () => {
|
|
109
|
+
const mockImage = createMockImage();
|
|
110
|
+
vi.spyOn(dom, 'createImage').mockReturnValue(mockImage);
|
|
111
|
+
|
|
112
|
+
const image = await loadImage('bad-url', { fallbackToEmptyImage: true });
|
|
113
|
+
|
|
114
|
+
expect(image).toBe(mockImage);
|
|
115
|
+
expect(image.src).toBe('');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -10,7 +10,7 @@ import { createImage } from './dom';
|
|
|
10
10
|
import { classRegistry } from '../../ClassRegistry';
|
|
11
11
|
import type { BaseFilter } from '../../filters/BaseFilter';
|
|
12
12
|
import type { FabricObject as BaseFabricObject } from '../../shapes/Object/Object';
|
|
13
|
-
import { FabricError, SignalAbortedError } from '../internals/console';
|
|
13
|
+
import { FabricError, SignalAbortedError, log } from '../internals/console';
|
|
14
14
|
import type { Shadow } from '../../Shadow';
|
|
15
15
|
|
|
16
16
|
export type LoadImageOptions = Abortable & {
|
|
@@ -18,6 +18,12 @@ export type LoadImageOptions = Abortable & {
|
|
|
18
18
|
* cors value for the image loading, default to anonymous
|
|
19
19
|
*/
|
|
20
20
|
crossOrigin?: TCrossOrigin;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve with an empty image instead of rejecting when the image fails to load.
|
|
24
|
+
* Useful for deserialization flows where one bad asset should not fail the entire document.
|
|
25
|
+
*/
|
|
26
|
+
fallbackToEmptyImage?: boolean;
|
|
21
27
|
};
|
|
22
28
|
|
|
23
29
|
/**
|
|
@@ -28,7 +34,11 @@ export type LoadImageOptions = Abortable & {
|
|
|
28
34
|
*/
|
|
29
35
|
export const loadImage = (
|
|
30
36
|
url: string,
|
|
31
|
-
{
|
|
37
|
+
{
|
|
38
|
+
signal,
|
|
39
|
+
crossOrigin = null,
|
|
40
|
+
fallbackToEmptyImage = false,
|
|
41
|
+
}: LoadImageOptions = {},
|
|
32
42
|
) =>
|
|
33
43
|
new Promise<HTMLImageElement>(function (resolve, reject) {
|
|
34
44
|
if (signal && signal.aborted) {
|
|
@@ -43,9 +53,12 @@ export const loadImage = (
|
|
|
43
53
|
};
|
|
44
54
|
signal.addEventListener('abort', abort, { once: true });
|
|
45
55
|
}
|
|
46
|
-
const
|
|
56
|
+
const cleanup = function () {
|
|
47
57
|
img.onload = img.onerror = null;
|
|
48
58
|
abort && signal?.removeEventListener('abort', abort);
|
|
59
|
+
};
|
|
60
|
+
const done = function () {
|
|
61
|
+
cleanup();
|
|
49
62
|
resolve(img);
|
|
50
63
|
};
|
|
51
64
|
if (!url) {
|
|
@@ -54,8 +67,19 @@ export const loadImage = (
|
|
|
54
67
|
}
|
|
55
68
|
img.onload = done;
|
|
56
69
|
img.onerror = function () {
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
const failedSrc = img.src;
|
|
71
|
+
cleanup();
|
|
72
|
+
if (fallbackToEmptyImage) {
|
|
73
|
+
log(
|
|
74
|
+
'warn',
|
|
75
|
+
'Image failed to load, continuing with an empty image source',
|
|
76
|
+
failedSrc,
|
|
77
|
+
);
|
|
78
|
+
img.src = '';
|
|
79
|
+
resolve(img);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
reject(new FabricError(`Error loading ${failedSrc}`));
|
|
59
83
|
};
|
|
60
84
|
crossOrigin && (img.crossOrigin = crossOrigin);
|
|
61
85
|
img.src = url;
|