fabric-vectr 6.7.12 → 6.7.14

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 (59) hide show
  1. package/dist/index.js +45 -21
  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 +45 -21
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/index.node.cjs +45 -21
  10. package/dist/index.node.cjs.map +1 -1
  11. package/dist/index.node.mjs +45 -21
  12. package/dist/index.node.mjs.map +1 -1
  13. package/dist/package.json.min.mjs +1 -1
  14. package/dist/package.json.mjs +1 -1
  15. package/dist/src/Pattern/Pattern.d.ts.map +1 -1
  16. package/dist/src/Pattern/Pattern.min.mjs +1 -1
  17. package/dist/src/Pattern/Pattern.min.mjs.map +1 -1
  18. package/dist/src/Pattern/Pattern.mjs +5 -1
  19. package/dist/src/Pattern/Pattern.mjs.map +1 -1
  20. package/dist/src/canvas/StaticCanvas.d.ts.map +1 -1
  21. package/dist/src/canvas/StaticCanvas.min.mjs +1 -1
  22. package/dist/src/canvas/StaticCanvas.min.mjs.map +1 -1
  23. package/dist/src/canvas/StaticCanvas.mjs +15 -12
  24. package/dist/src/canvas/StaticCanvas.mjs.map +1 -1
  25. package/dist/src/shapes/Image.d.ts +2 -2
  26. package/dist/src/shapes/Image.d.ts.map +1 -1
  27. package/dist/src/shapes/Image.min.mjs +1 -1
  28. package/dist/src/shapes/Image.min.mjs.map +1 -1
  29. package/dist/src/shapes/Image.mjs +4 -2
  30. package/dist/src/shapes/Image.mjs.map +1 -1
  31. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.d.ts.map +1 -1
  32. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.min.mjs +1 -1
  33. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.min.mjs.map +1 -1
  34. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.mjs +5 -1
  35. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.mjs.map +1 -1
  36. package/dist/src/util/misc/objectEnlive.d.ts +6 -1
  37. package/dist/src/util/misc/objectEnlive.d.ts.map +1 -1
  38. package/dist/src/util/misc/objectEnlive.min.mjs +1 -1
  39. package/dist/src/util/misc/objectEnlive.min.mjs.map +1 -1
  40. package/dist/src/util/misc/objectEnlive.mjs +16 -5
  41. package/dist/src/util/misc/objectEnlive.mjs.map +1 -1
  42. package/dist-extensions/src/Pattern/Pattern.d.ts.map +1 -1
  43. package/dist-extensions/src/canvas/StaticCanvas.d.ts.map +1 -1
  44. package/dist-extensions/src/shapes/Image.d.ts +2 -2
  45. package/dist-extensions/src/shapes/Image.d.ts.map +1 -1
  46. package/dist-extensions/src/shapes/Object/FabricObjectSVGExportMixin.d.ts.map +1 -1
  47. package/dist-extensions/src/util/misc/objectEnlive.d.ts +6 -1
  48. package/dist-extensions/src/util/misc/objectEnlive.d.ts.map +1 -1
  49. package/package.json +33 -33
  50. package/src/Pattern/Pattern.spec.ts +63 -0
  51. package/src/Pattern/Pattern.ts +3 -1
  52. package/src/canvas/StaticCanvas.spec.ts +23 -0
  53. package/src/canvas/StaticCanvas.ts +19 -12
  54. package/src/shapes/Image.spec.ts +21 -2
  55. package/src/shapes/Image.ts +24 -12
  56. package/src/shapes/Object/FabricObjectSVGExportMixin.ts +5 -1
  57. package/src/shapes/Text/TextSVGExportMixin.spec.ts +16 -0
  58. package/src/util/misc/objectEnlive.spec.ts +49 -2
  59. 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,
@@ -1,4 +1,7 @@
1
1
  import { StaticCanvas } from './StaticCanvas';
2
+ import { Group } from '../shapes/Group';
3
+ import { Path } from '../shapes/Path';
4
+ import { FabricText } from '../shapes/Text/Text';
2
5
 
3
6
  import { it, expect, describe } from 'vitest';
4
7
 
@@ -24,4 +27,24 @@ describe('StaticCanvas', () => {
24
27
  */
25
28
  expect(dataURL).toMatch(/^data:image\/(webp|png)/);
26
29
  });
30
+
31
+ it('exports text on path inside group to svg defs', () => {
32
+ const canvas = new StaticCanvas(undefined, { width: 300, height: 300 });
33
+ const textPath = new FabricText('Text On Path', {
34
+ id: 'group-text-path',
35
+ path: new Path('M 0 0 L 120 0'),
36
+ fontSize: 20,
37
+ });
38
+ const group = new Group([textPath], {
39
+ left: 40,
40
+ top: 60,
41
+ });
42
+
43
+ canvas.add(group);
44
+
45
+ const svg = canvas.toSVG();
46
+
47
+ expect(svg).toContain('<textPath href="#TEXTPATH_group-text-path" ');
48
+ expect(svg).toContain('id="TEXTPATH_group-text-path"');
49
+ });
27
50
  });
@@ -1083,22 +1083,28 @@ export class StaticCanvas<
1083
1083
  */
1084
1084
  _createPathForText() {
1085
1085
  const pathMarkups: string[] = [];
1086
- let instance,
1087
- i,
1088
- len,
1089
- objects = this._objects;
1090
- for (i = 0, len = objects.length; i < len; i++) {
1091
- instance = objects[i];
1086
+ const objects: FabricObject[] = [];
1087
+
1088
+ this._objects.forEach(function add(object) {
1089
+ objects.push(object);
1090
+ if (isCollection(object)) {
1091
+ object._objects.forEach(add);
1092
+ }
1093
+ });
1094
+
1095
+ objects.forEach((instance) => {
1092
1096
  if (instance.excludeFromExport) {
1093
- continue;
1097
+ return;
1094
1098
  }
1095
1099
 
1096
1100
  if (!isTextObject(instance) || !instance.path) {
1097
- continue;
1101
+ return;
1098
1102
  }
1099
- let pathId = `TEXTPATH_${(instance as any).id}`;
1100
- let pathMarkup = instance.path._toSVG();
1101
- let index = pathMarkup.indexOf('COMMON_PARTS');
1103
+
1104
+ const pathId = `TEXTPATH_${(instance as any).id}`;
1105
+ const pathMarkup = instance.path._toSVG();
1106
+ const index = pathMarkup.indexOf('COMMON_PARTS');
1107
+
1102
1108
  // 加上id, 和路径偏移
1103
1109
  pathMarkup[index] = [
1104
1110
  'id="' + pathId + '" ',
@@ -1109,7 +1115,8 @@ export class StaticCanvas<
1109
1115
  ')" ',
1110
1116
  ].join('');
1111
1117
  pathMarkups.push(pathMarkup.join(''));
1112
- }
1118
+ });
1119
+
1113
1120
  return pathMarkups.join('\n');
1114
1121
  }
1115
1122
 
@@ -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
  /**
@@ -304,7 +304,12 @@ export class FabricObjectSVGExportMixin {
304
304
  ].join('');
305
305
 
306
306
  // James added shadow 放在上面
307
+ // 文本等调用方已经通过 withShadow 挂载 filter 时,避免再额外复制一份可见对象,
308
+ // 但仍然需要输出 filter 定义,否则普通 SVG 会丢失阴影。
307
309
  if (shadow) {
310
+ markup.push(shadow.toSVG(this));
311
+ }
312
+ if (shadow && !withShadow) {
308
313
  const styleInfoWithShadow = 'style="' + this.getSvgStyles(false) + '" ';
309
314
  const commonPiecesWithShadow = [
310
315
  styleInfoWithShadow,
@@ -317,7 +322,6 @@ export class FabricObjectSVGExportMixin {
317
322
  objectMarkupCopy[index] = commonPiecesWithShadow;
318
323
 
319
324
  markup.push(objectMarkupCopy.join(''));
320
- markup.push(shadow.toSVG(this));
321
325
  }
322
326
  // objectMarkup中是导出主对象(如path)的svg,index下标是style,放在commonPieces
323
327
  objectMarkup[index] = commonPieces;
@@ -1,4 +1,5 @@
1
1
  import { FabricText } from './Text';
2
+ import { Shadow } from '../../Shadow';
2
3
 
3
4
  import { describe, expect, it } from 'vitest';
4
5
 
@@ -28,4 +29,19 @@ describe('TextSvgExport', () => {
28
29
  );
29
30
  expect(svgStyles.includes('stroke="none"')).toBe(false);
30
31
  });
32
+
33
+ it('exports shadowed text without duplicating text markup', () => {
34
+ const myText = new FabricText('text', {
35
+ shadow: new Shadow({
36
+ color: 'rgba(0, 0, 0, 0.4)',
37
+ blur: 6,
38
+ offsetX: 4,
39
+ offsetY: 4,
40
+ }),
41
+ });
42
+ const svgString = myText.toSVG();
43
+ expect((svgString.match(/<text /g) || []).length).toBe(1);
44
+ expect(svgString.includes('filter: url(#SVGID_')).toBe(true);
45
+ expect(svgString.includes('<filter id="SVGID_')).toBe(true);
46
+ });
31
47
  });
@@ -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;