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.
Files changed (52) hide show
  1. package/dist/index.js +34 -9
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.min.js +1 -1
  4. package/dist/index.min.js.map +1 -1
  5. package/dist/index.min.mjs +1 -1
  6. package/dist/index.min.mjs.map +1 -1
  7. package/dist/index.mjs +34 -9
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/index.node.cjs +34 -9
  10. package/dist/index.node.cjs.map +1 -1
  11. package/dist/index.node.d.ts +6 -6
  12. package/dist/index.node.mjs +34 -9
  13. package/dist/index.node.mjs.map +1 -1
  14. package/dist/package.json.min.mjs +1 -1
  15. package/dist/package.json.mjs +1 -1
  16. package/dist/src/Pattern/Pattern.d.ts.map +1 -1
  17. package/dist/src/Pattern/Pattern.min.mjs +1 -1
  18. package/dist/src/Pattern/Pattern.min.mjs.map +1 -1
  19. package/dist/src/Pattern/Pattern.mjs +5 -1
  20. package/dist/src/Pattern/Pattern.mjs.map +1 -1
  21. package/dist/src/shapes/Group.d.ts.map +1 -1
  22. package/dist/src/shapes/Group.min.mjs +1 -1
  23. package/dist/src/shapes/Group.min.mjs.map +1 -1
  24. package/dist/src/shapes/Group.mjs +11 -1
  25. package/dist/src/shapes/Group.mjs.map +1 -1
  26. package/dist/src/shapes/Image.d.ts +2 -2
  27. package/dist/src/shapes/Image.d.ts.map +1 -1
  28. package/dist/src/shapes/Image.min.mjs +1 -1
  29. package/dist/src/shapes/Image.min.mjs.map +1 -1
  30. package/dist/src/shapes/Image.mjs +4 -2
  31. package/dist/src/shapes/Image.mjs.map +1 -1
  32. package/dist/src/util/misc/objectEnlive.d.ts +6 -1
  33. package/dist/src/util/misc/objectEnlive.d.ts.map +1 -1
  34. package/dist/src/util/misc/objectEnlive.min.mjs +1 -1
  35. package/dist/src/util/misc/objectEnlive.min.mjs.map +1 -1
  36. package/dist/src/util/misc/objectEnlive.mjs +16 -5
  37. package/dist/src/util/misc/objectEnlive.mjs.map +1 -1
  38. package/dist-extensions/src/Pattern/Pattern.d.ts.map +1 -1
  39. package/dist-extensions/src/shapes/Group.d.ts.map +1 -1
  40. package/dist-extensions/src/shapes/Image.d.ts +2 -2
  41. package/dist-extensions/src/shapes/Image.d.ts.map +1 -1
  42. package/dist-extensions/src/util/misc/objectEnlive.d.ts +6 -1
  43. package/dist-extensions/src/util/misc/objectEnlive.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/Pattern/Pattern.spec.ts +63 -0
  46. package/src/Pattern/Pattern.ts +3 -1
  47. package/src/shapes/Group.spec.ts +40 -0
  48. package/src/shapes/Group.ts +20 -1
  49. package/src/shapes/Image.spec.ts +21 -2
  50. package/src/shapes/Image.ts +24 -12
  51. package/src/util/misc/objectEnlive.spec.ts +49 -2
  52. 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
+ });
@@ -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: otherOptions.crossOrigin,
214
+ crossOrigin,
215
+ fallbackToEmptyImage: true,
214
216
  });
215
217
  return new this({
216
218
  ...otherOptions,
@@ -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({
@@ -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
- svgString.push('\t\t', this._objects[i].toSVG(reviver));
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;
@@ -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);
@@ -799,21 +799,33 @@ export class FabricImage<
799
799
  options?: Abortable,
800
800
  ) {
801
801
  return Promise.all([
802
- loadImage(src!, { ...options, crossOrigin }),
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<BaseFilter<'Resize'>>([rf], options),
809
+ rf && enlivenObjects<Resize>([rf], options),
806
810
  enlivenObjectEnlivables(object, options),
807
- ]).then(([el, filters = [], [resizeFilter] = [], hydratedProps = {}]) => {
808
- return new this(el, {
809
- ...object,
810
- // TODO: this creates a difference between image creation and restoring from JSON
811
- src,
812
- filters,
813
- resizeFilter,
814
- ...hydratedProps,
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 { enlivenObjects } from './objectEnlive';
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
- { signal, crossOrigin = null }: LoadImageOptions = {},
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 done = function () {
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
- abort && signal?.removeEventListener('abort', abort);
58
- reject(new FabricError(`Error loading ${img.src}`));
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;