import { StaticCanvas } from './StaticCanvas';
import { Canvas } from './Canvas';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { TMat2D } from '../typedefs';
import { FabricText, Gradient, Pattern, version } from '../../fabric';
import { config } from '../config';
import { Rect } from '../shapes/Rect';
import { Circle } from '../shapes/Circle';
import type { FabricObject } from '../shapes/Object/Object';
import { getFabricDocument } from '../env';
import { FabricImage } from '../shapes/Image';
import { Point } from '../Point';
import { Group } from '../shapes/Group';
import { Path } from '../shapes/Path';
import { Ellipse } from '../shapes/Ellipse';
import { Line } from '../shapes/Line';
import { Polyline } from '../shapes/Polyline';
import { Triangle } from '../shapes/Triangle';
import { Polygon } from '../shapes/Polygon';
import TEST_IMAGE_GIF from '../../test/fixtures/test_image.gif';
import { isJSDOM } from '../../vitest.extend';
const CANVAS_SVG =
'\n\n' +
'';
const CANVAS_SVG_VIEWBOX =
'\n\n' +
'';
const RECT_JSON =
'{"version":"' +
version +
'","objects":[{"type":"Rect","version":"' +
version +
'","originX":"left","originY":"top","left":0,"top":0,"width":10,"height":10,"fill":"rgb(0,0,0)",' +
'"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,' +
'"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,' +
'"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"rx":0,"ry":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}';
const PATH_DATALESS_JSON =
'{"version":"' +
version +
'","objects":[{"type":"Path","version":"' +
version +
'","originX":"left","originY":"top","left":99.5,"top":99.5,"width":200,"height":200,"fill":"rgb(0,0,0)",' +
'"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,' +
'"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,' +
'"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"sourcePath":"http://example.com/"}]}';
const PATH_JSON =
'{"version":"' +
version +
'","objects": [{"type": "path", "version":"' +
version +
'", "originX": "left", "originY": "top", "left": 268, "top": 266, "width": 51, "height": 49,' +
' "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 1, "scaleX": 1, "scaleY": 1, ' +
'"angle": 0, "flipX": false, "flipY": false, "opacity": 1, "path": [["M", 18.511, 13.99],' +
' ["c", 0, 0, -2.269, -4.487, -12.643, 4.411], ["c", 0, 0, 4.824, -14.161, 19.222, -9.059],' +
' ["l", 0.379, -2.1], ["c", -0.759, -0.405, -1.375, -1.139, -1.645, -2.117], ["c", -0.531, ' +
'-1.864, 0.371, -3.854, 1.999, -4.453], ["c", 0.312, -0.118, 0.633, -0.169, 0.953, -0.169], ' +
'["c", 1.299, 0, 2.514, 0.953, 2.936, 2.455], ["c", 0.522, 1.864, -0.372, 3.854, -1.999, ' +
'4.453], ["c", -0.229, 0.084, -0.464, 0.127, -0.692, 0.152], ["l", -0.379, 2.37], ["c", ' +
'1.146, 0.625, 2.024, 1.569, 2.674, 2.758], ["c", 3.213, 2.514, 8.561, 4.184, 11.774, -8.232],' +
' ["c", 0, 0, 0.86, 16.059, -12.424, 14.533], ["c", 0.008, 2.859, 0.615, 5.364, -0.076, 8.224],' +
' ["c", 8.679, 3.146, 15.376, 14.389, 17.897, 18.168], ["l", 2.497, -2.151], ["l", 1.206, 1.839],' +
' ["l", -3.889, 3.458], ["C", 46.286, 48.503, 31.036, 32.225, 22.72, 35.81], ["c", -1.307, 2.851,' +
' -3.56, 6.891, -7.481, 8.848], ["c", -4.689, 2.336, -9.084, -0.802, -11.277, -2.868], ["l",' +
' -1.948, 3.104], ["l", -1.628, -1.333], ["l", 3.138, -4.689], ["c", 0.025, 0, 9, 1.932, 9, 1.932], ' +
'["c", 0.877, -9.979, 2.893, -12.905, 4.942, -15.621], ["C", 17.878, 21.775, 18.713, 17.397, 18.511, ' +
'13.99], ["z"]]}], "background": "#ff5555", "overlay":"rgba(0,0,0,0.2)"}';
const PATH_WITHOUT_DEFAULTS_JSON =
'{"version":"' +
version +
'","objects": [{"type": "path", "version":"' +
version +
'", "left": 268, "top": 266, "width": 51, "height": 49, "path": [["M", 18.511, 13.99],' +
' ["c", 0, 0, -2.269, -4.487, -12.643, 4.411], ["c", 0, 0, 4.824, -14.161, 19.222, -9.059],' +
' ["l", 0.379, -2.1], ["c", -0.759, -0.405, -1.375, -1.139, -1.645, -2.117], ["c", -0.531, ' +
'-1.864, 0.371, -3.854, 1.999, -4.453], ["c", 0.312, -0.118, 0.633, -0.169, 0.953, -0.169], ' +
'["c", 1.299, 0, 2.514, 0.953, 2.936, 2.455], ["c", 0.522, 1.864, -0.372, 3.854, -1.999, ' +
'4.453], ["c", -0.229, 0.084, -0.464, 0.127, -0.692, 0.152], ["l", -0.379, 2.37], ["c", ' +
'1.146, 0.625, 2.024, 1.569, 2.674, 2.758], ["c", 3.213, 2.514, 8.561, 4.184, 11.774, -8.232],' +
' ["c", 0, 0, 0.86, 16.059, -12.424, 14.533], ["c", 0.008, 2.859, 0.615, 5.364, -0.076, 8.224],' +
' ["c", 8.679, 3.146, 15.376, 14.389, 17.897, 18.168], ["l", 2.497, -2.151], ["l", 1.206, 1.839],' +
' ["l", -3.889, 3.458], ["C", 46.286, 48.503, 31.036, 32.225, 22.72, 35.81], ["c", -1.307, 2.851,' +
' -3.56, 6.891, -7.481, 8.848], ["c", -4.689, 2.336, -9.084, -0.802, -11.277, -2.868], ["l",' +
' -1.948, 3.104], ["l", -1.628, -1.333], ["l", 3.138, -4.689], ["c", 0.025, 0, 9, 1.932, 9, 1.932], ' +
'["c", 0.877, -9.979, 2.893, -12.905, 4.942, -15.621], ["C", 17.878, 21.775, 18.713, 17.397, 18.511, ' +
'13.99], ["z"]]}], "background": "#ff5555","overlay": "rgba(0,0,0,0.2)"}';
const RECT_JSON_WITH_PADDING =
'{"version":"' +
version +
'","objects":[{"type":"Rect","version":"' +
version +
'","originX":"left","originY":"top","left":0,"top":0,"width":10,"height":20,"fill":"rgb(0,0,0)",' +
'"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,' +
'"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,' +
'"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"rx":0,"ry":0,"padding":123,"foo":"bar"}]}';
const IMG_SRC = isJSDOM() ? 'test_image.gif' : TEST_IMAGE_GIF;
const IMG_WIDTH = 276;
const IMG_HEIGHT = 110;
const REFERENCE_IMG_OBJECT: Partial<
FabricImage & { version: string; src: string; crossOrigin: null }
> = {
version: version,
type: 'Image',
originX: 'left',
originY: 'top',
left: 0,
top: 0,
width: IMG_WIDTH, // node-canvas doesn't seem to allow setting width/height on image objects
height: IMG_HEIGHT, // or does it now?
fill: 'rgb(0,0,0)',
stroke: null,
strokeWidth: 0,
strokeDashArray: null,
strokeDashOffset: 0,
strokeLineCap: 'butt',
strokeLineJoin: 'miter',
strokeMiterLimit: 4,
scaleX: 1,
scaleY: 1,
angle: 0,
flipX: false,
flipY: false,
opacity: 1,
src: IMG_SRC,
shadow: null,
visible: true,
backgroundColor: '',
filters: [],
fillRule: 'nonzero',
paintFirst: 'fill',
globalCompositeOperation: 'source-over',
crossOrigin: null,
skewX: 0,
skewY: 0,
cropX: 0,
cropY: 0,
strokeUniform: false,
};
describe('StaticCanvas', () => {
const canvas = new StaticCanvas(undefined, {
renderOnAddRemove: false,
enableRetinaScaling: false,
width: 200,
height: 200,
});
const canvas2 = new StaticCanvas(undefined, {
renderOnAddRemove: false,
enableRetinaScaling: false,
width: 200,
height: 200,
});
const lowerCanvasEl = canvas.lowerCanvasEl;
beforeEach(() => {
canvas.clear();
canvas.setDimensions({ width: 200, height: 200 });
canvas2.setDimensions({ width: 200, height: 200 });
canvas.backgroundColor = StaticCanvas.getDefaults().backgroundColor;
canvas.backgroundImage = StaticCanvas.getDefaults().backgroundImage;
canvas.overlayColor = StaticCanvas.getDefaults().overlayColor;
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
canvas.calcOffset();
canvas.requestRenderAll = StaticCanvas.prototype.requestRenderAll;
canvas.cancelRequestedRender();
canvas2.cancelRequestedRender();
canvas.renderOnAddRemove = false;
canvas2.renderOnAddRemove = false;
});
afterEach(() => {
canvas.cancelRequestedRender();
canvas2.cancelRequestedRender();
config.configure({ devicePixelRatio: 1 });
});
it('toBlob', async () => {
const canvas = new StaticCanvas(undefined, { width: 300, height: 300 });
const blob = await canvas.toBlob({
multiplier: 3,
});
expect(blob).toBeInstanceOf(Blob);
expect(blob?.type).toBe('image/png');
});
it('attempts webp format but may fallback to png in node environment', () => {
const canvas = new StaticCanvas(undefined, { width: 300, height: 300 });
const dataURL = canvas.toDataURL({
format: 'webp',
multiplier: 1,
});
/**
* In browser environments this would be 'data:image/webp'
* In Node.js environment (node-canvas) it falls back to PNG.
* @see https://github.com/Automattic/node-canvas/issues/1258 for possible workaround
*/
expect(dataURL).toMatch(/^data:image\/(webp|png)/);
});
it('prevents multiple canvas initialization', () => {
const canvas = new StaticCanvas();
expect(canvas.lowerCanvasEl).toBeTruthy();
expect(() => new StaticCanvas(canvas.lowerCanvasEl)).toThrow();
});
it('has correct initial properties', () => {
const canvas = new StaticCanvas();
expect('backgroundColor' in canvas).toBeTruthy();
expect('overlayColor' in canvas).toBeTruthy();
expect('includeDefaultValues' in canvas).toBeTruthy();
expect('renderOnAddRemove' in canvas).toBeTruthy();
expect('controlsAboveOverlay' in canvas).toBeTruthy();
expect('allowTouchScrolling' in canvas).toBeTruthy();
expect('imageSmoothingEnabled' in canvas).toBeTruthy();
expect('backgroundVpt' in canvas).toBeTruthy();
expect('overlayVpt' in canvas).toBeTruthy();
expect(Array.isArray(canvas._objects)).toBeTruthy();
expect(canvas._objects.length).toBe(0);
expect(canvas.includeDefaultValues).toBe(true);
expect(canvas.renderOnAddRemove).toBe(true);
expect(canvas.controlsAboveOverlay).toBe(false);
expect(canvas.allowTouchScrolling).toBe(false);
expect(canvas.imageSmoothingEnabled).toBe(true);
expect(canvas.backgroundVpt).toBe(true);
expect(canvas.overlayVpt).toBe(true);
expect(canvas.viewportTransform).not.toBe(canvas2.viewportTransform);
});
it('provides getObjects method', () => {
expect(canvas.getObjects).toBeTypeOf('function');
expect(canvas.getObjects()).toEqual([]);
expect(canvas.getObjects().length).toBe(0);
});
it('provides getElement method', () => {
expect(canvas.getElement).toBeTypeOf('function');
expect(canvas.getElement()).toBe(lowerCanvasEl);
});
it('provides item method to access objects by index', () => {
const rect = makeRect();
expect(canvas.item).toBeTypeOf('function');
canvas.add(rect);
expect(canvas.item(0)).toBe(rect);
});
it('calculates offset correctly', () => {
expect(canvas.calcOffset).toBeTypeOf('function');
expect(canvas.calcOffset()).toEqual({ left: 0, top: 0 });
});
it('adds objects to the canvas', () => {
const rect1 = makeRect();
const rect2 = makeRect();
const rect3 = makeRect();
const rect4 = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.renderOnAddRemove = true;
canvas.requestRenderAll = countRenderAll;
expect(canvas.add).toBeTypeOf('function');
expect(canvas.add(rect1)).toBe(1);
expect(canvas.item(0)).toBe(rect1);
expect(renderAllCount).toBe(1);
expect(canvas.add(rect2, rect3, rect4)).toBe(4);
expect(canvas.getObjects().length).toBe(4);
expect(renderAllCount).toBe(2);
canvas.add();
expect(renderAllCount).toBe(2);
expect(canvas.item(1)).toBe(rect2);
expect(canvas.item(2)).toBe(rect3);
expect(canvas.item(3)).toBe(rect4);
});
it('handles objects that belong to a different canvas', () => {
const rect1 = makeRect();
const control: {
action: string;
canvas: StaticCanvas;
target: FabricObject;
}[] = [];
canvas.on('object:added', (opt) => {
control.push({
action: 'added',
canvas: canvas,
target: opt.target,
});
});
canvas.on('object:removed', (opt) => {
control.push({
action: 'removed',
canvas: canvas,
target: opt.target,
});
});
canvas2.on('object:added', (opt) => {
control.push({
action: 'added',
canvas: canvas2,
target: opt.target,
});
});
canvas.add(rect1);
expect(canvas.item(0)).toBe(rect1);
canvas2.add(rect1);
expect(canvas.item(0)).toBeUndefined();
expect(canvas.size()).toBe(0);
expect(canvas2.item(0)).toBe(rect1);
const expected = [
{ action: 'added', target: rect1, canvas: canvas },
{ action: 'removed', target: rect1, canvas: canvas },
{ action: 'added', target: rect1, canvas: canvas2 },
];
expect(control).toEqual(expected);
});
it('respects renderOnAddRemove setting', () => {
const rect = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.renderOnAddRemove = false;
canvas.requestRenderAll = countRenderAll;
canvas.add(rect);
expect(renderAllCount).toBe(0);
expect(canvas.item(0)).toBe(rect);
canvas.add(makeRect(), makeRect(), makeRect());
expect(canvas.getObjects().length).toBe(4);
expect(renderAllCount).toBe(0);
});
it('fires object:added events', () => {
const objectsAdded: FabricObject[] = [];
canvas.on('object:added', function (e) {
objectsAdded.push(e.target);
});
const rect = new Rect({ width: 10, height: 20 });
canvas.add(rect);
expect(objectsAdded[0]).toBe(rect);
const circle1 = new Circle();
const circle2 = new Circle();
canvas.add(circle1, circle2);
expect(objectsAdded[1]).toBe(circle1);
expect(objectsAdded[2]).toBe(circle2);
const circle3 = new Circle();
canvas.insertAt(2, circle3);
expect(objectsAdded[3]).toBe(circle3);
});
it('inserts objects at specified positions', () => {
const rect1 = makeRect();
const rect2 = makeRect();
let renderAllCount = 0;
canvas.add(rect1, rect2);
expect(canvas.insertAt).toBeTypeOf('function');
function countRenderAll() {
renderAllCount++;
}
canvas.requestRenderAll = countRenderAll;
canvas.renderOnAddRemove = true;
expect(renderAllCount).toBe(0);
const rect = makeRect();
canvas.insertAt(1, rect);
expect(renderAllCount).toBe(1);
expect(canvas.item(1)).toBe(rect);
canvas.insertAt(2, rect);
expect(renderAllCount).toBe(2);
expect(canvas.item(2)).toBe(rect);
canvas.insertAt(2, rect);
expect(renderAllCount).toBe(3);
});
it('respects renderOnAddRemove when inserting objects', () => {
const rect1 = makeRect();
const rect2 = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.renderOnAddRemove = false;
canvas.requestRenderAll = countRenderAll;
canvas.add(rect1, rect2);
expect(renderAllCount).toBe(0);
const rect = makeRect();
canvas.insertAt(1, rect);
expect(renderAllCount).toBe(0);
expect(canvas.item(1)).toBe(rect);
canvas.insertAt(2, rect);
expect(renderAllCount).toBe(0);
});
it('removes objects correctly', () => {
const rect1 = makeRect();
const rect2 = makeRect();
const rect3 = makeRect();
const rect4 = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.add(rect1, rect2, rect3, rect4);
canvas.requestRenderAll = countRenderAll;
canvas.renderOnAddRemove = true;
expect(canvas.remove).toBeTypeOf('function');
expect(renderAllCount).toBe(0);
expect(canvas.remove(rect1)[0]).toBe(rect1);
expect(canvas.item(0)).toBe(rect2);
canvas.remove(rect2, rect3);
expect(renderAllCount).toBe(2);
expect(canvas.item(0)).toBe(rect4);
canvas.remove(rect4);
expect(renderAllCount).toBe(3);
expect(canvas.isEmpty()).toBe(true);
});
it('respects renderOnAddRemove when removing objects', () => {
const rect1 = makeRect();
const rect2 = makeRect();
let renderAllCount = 0;
function countRenderAll() {
renderAllCount++;
}
canvas.requestRenderAll = countRenderAll;
canvas.renderOnAddRemove = false;
canvas.add(rect1, rect2);
expect(renderAllCount).toBe(0);
expect(canvas.remove(rect1)[0]).toBe(rect1);
expect(renderAllCount).toBe(0);
expect(canvas.item(0)).toBe(rect2);
});
it('fires object:removed events', () => {
const objectsRemoved: FabricObject[] = [];
canvas.on('object:removed', function (e) {
objectsRemoved.push(e.target);
});
const rect = new Rect({ width: 10, height: 20 });
const circle1 = new Circle();
const circle2 = new Circle();
canvas.add(rect, circle1, circle2);
expect(canvas.item(0)).toBe(rect);
expect(canvas.item(1)).toBe(circle1);
expect(canvas.item(2)).toBe(circle2);
canvas.remove(rect);
expect(objectsRemoved[0]).toBe(rect);
expect(rect.canvas).toBeUndefined();
canvas.remove(circle1, circle2);
expect(objectsRemoved[1]).toBe(circle1);
expect(circle1.canvas).toBeUndefined();
expect(objectsRemoved[2]).toBe(circle2);
expect(circle2.canvas).toBeUndefined();
expect(canvas.isEmpty()).toBe(true);
});
it('provides clearContext method', () => {
expect(canvas.clearContext).toBeTypeOf('function');
canvas.clearContext(canvas.contextContainer);
});
it('clears the canvas completely', () => {
expect(canvas.clear).toBeTypeOf('function');
const bg = new Rect({ width: 10, height: 20 });
canvas.backgroundColor = '#FF0000';
canvas.overlayColor = '#FF0000';
canvas.backgroundImage = bg;
canvas.overlayImage = bg;
const objectsRemoved: FabricObject[] = [];
canvas.on('object:removed', function (e) {
objectsRemoved.push(e.target);
});
const rect1 = makeRect();
const rect2 = makeRect();
const rect3 = makeRect();
canvas.add(rect1, rect2, rect3);
canvas.clear();
expect(canvas.getObjects().length).toBe(0);
expect(objectsRemoved[0]).toBe(rect1);
expect(objectsRemoved[1]).toBe(rect2);
expect(objectsRemoved[2]).toBe(rect3);
expect(canvas.backgroundColor).toBe('');
expect(canvas.overlayColor).toBe('');
expect(canvas.backgroundImage).toBeUndefined();
expect(canvas.overlayImage).toBeUndefined();
});
it('provides renderAll method', () => {
expect(canvas.renderAll).toBeTypeOf('function');
canvas.renderAll();
});
// TODO: was also commented out prior to vitest migration
// it('sets canvas dimensions correctly', () => {
// expect(canvas.setDimensions).toBeTypeOf('function');
// canvas.setDimensions({ width: 4, height: 5 });
// expect(canvas.getWidth()).toBe(4);
// expect(canvas.getHeight()).toBe(5);
// expect(canvas.lowerCanvasEl.style.width).toBe('5px');
// expect(canvas.lowerCanvasEl.style.height).toBe('4px');
// });
it('exports to canvas element of correct size', () => {
expect(canvas.toCanvasElement).toBeTypeOf('function');
const canvasEl = canvas.toCanvasElement();
expect(canvasEl.width).toBe(canvas.getWidth());
expect(canvasEl.height).toBe(canvas.getHeight());
});
it('exports to canvas element with multiplier', () => {
expect(canvas.toCanvasElement).toBeTypeOf('function');
const multiplier = 2;
const canvasEl = canvas.toCanvasElement(multiplier);
expect(canvasEl.width).toBe(canvas.getWidth() * multiplier);
expect(canvasEl.height).toBe(canvas.getHeight() * multiplier);
});
it('generates data URL correctly', () => {
expect(canvas.toDataURL).toBeTypeOf('function');
const rect = new Rect({
width: 100,
height: 100,
fill: 'red',
top: 0,
left: 0,
});
canvas.add(rect);
const dataURL = canvas.toDataURL();
// don't compare actual data url, as it is often browser-dependent
expect(typeof dataURL).toBe('string');
expect(dataURL.substring(0, 21)).toBe('data:image/png;base64');
//we can just compare that the dataUrl generated differs from the dataURl of an empty canvas
expect(dataURL.substring(200, 210) !== 'AAAAAAAAAA').toBe(true);
});
it('supports retina scaling in data URL generation', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
// @ts-expect-error -- multiplier is missing in options and it is mandatory per typescript
const dataUrl = c.toDataURL({ enableRetinaScaling: true });
c.cancelRequestedRender();
return new Promise((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width * config.devicePixelRatio);
expect(img.height).toBe(c.height * config.devicePixelRatio);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: true with multiplier = 1', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
const dataUrl = c.toDataURL({ enableRetinaScaling: true, multiplier: 1 });
c.cancelRequestedRender();
return new Promise((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width * config.devicePixelRatio);
expect(img.height).toBe(c.height * config.devicePixelRatio);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: true with multiplier = 3', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
const dataUrl = c.toDataURL({ enableRetinaScaling: true, multiplier: 3 });
c.cancelRequestedRender();
return new Promise((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width * config.devicePixelRatio * 3);
expect(img.height).toBe(c.height * config.devicePixelRatio * 3);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: false with no multiplier', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
// @ts-expect-error -- multiplier is missing in options and it is mandatory per typescript
const dataUrl = c.toDataURL({ enableRetinaScaling: false });
c.cancelRequestedRender();
return new Promise((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width);
expect(img.height).toBe(c.height);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: false with multiplier = 1', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
const dataUrl = c.toDataURL({
enableRetinaScaling: false,
multiplier: 1,
});
c.cancelRequestedRender();
return new Promise((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width);
expect(img.height).toBe(c.height);
resolve();
};
img.src = dataUrl;
});
});
it('handles enableRetinaScaling: false with multiplier = 3', async () => {
config.configure({ devicePixelRatio: 2 });
const c = new StaticCanvas(undefined, {
enableRetinaScaling: true,
width: 10,
height: 10,
});
const dataUrl = c.toDataURL({
enableRetinaScaling: false,
multiplier: 3,
});
c.cancelRequestedRender();
return new Promise((resolve) => {
const img = getFabricDocument().createElement('img');
img.onload = () => {
expect(img.width).toBe(c.width * 3);
expect(img.height).toBe(c.height * 3);
resolve();
};
img.src = dataUrl;
});
});
it('generates JPEG data URL correctly', () => {
try {
// @ts-expect-error -- multiplier is mandatory option per typescript types
const dataURL = canvas.toDataURL({ format: 'jpeg' });
expect(dataURL.substring(0, 22)).toBe('data:image/jpeg;base64');
} catch {
// node-canvas does not support jpeg data urls
expect(true).toBeTruthy();
}
});
it('supports cropping in data URL generation', async () => {
expect(canvas.toDataURL).toBeTypeOf('function');
const croppingWidth = 75;
const croppingHeight = 50;
// @ts-expect-error -- multiplier is mandatory option per typescript types
const dataURL = canvas.toDataURL({
width: croppingWidth,
height: croppingHeight,
});
const img = await FabricImage.fromURL(dataURL);
expect(img.width).toBe(croppingWidth);
expect(img.height).toBe(croppingHeight);
});
it('centers objects horizontally', () => {
expect(canvas.centerObjectH).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
canvas.add(rect);
canvas.centerObjectH(rect);
expect(rect.getCenterPoint().x).toBe(canvas.width / 2);
canvas.setZoom(4);
expect(rect.getCenterPoint().x).toBe(canvas.height / 2);
canvas.setZoom(1);
});
it('centers objects vertically', () => {
expect(canvas.centerObjectV).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
canvas.add(rect);
canvas.centerObjectV(rect);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
canvas.setZoom(2);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
});
it('centers objects both horizontally and vertically', () => {
expect(canvas.centerObject).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
canvas.add(rect);
canvas.centerObject(rect);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
expect(rect.getCenterPoint().x).toBe(canvas.height / 2);
canvas.setZoom(4);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
expect(rect.getCenterPoint().x).toBe(canvas.height / 2);
canvas.setZoom(1);
});
it('centers objects horizontally in viewport', () => {
expect(canvas.viewportCenterObjectH).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
const pan = 10;
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
canvas.add(rect);
const oldY = rect.top;
canvas.viewportCenterObjectH(rect);
expect(rect.getCenterPoint().x).toBe(canvas.width / 2);
expect(rect.top).toBe(oldY);
canvas.setZoom(2);
canvas.viewportCenterObjectH(rect);
expect(rect.getCenterPoint().x).toBe(canvas.width / (2 * canvas.getZoom()));
expect(rect.top).toBe(oldY);
canvas.absolutePan(new Point(pan, pan));
canvas.viewportCenterObjectH(rect);
expect(rect.getCenterPoint().x).toBe(
(canvas.width / 2 + pan) / canvas.getZoom(),
);
expect(rect.top).toBe(oldY);
});
it('centers objects vertically in viewport', () => {
expect(canvas.viewportCenterObjectV).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
const pan = 10;
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
canvas.add(rect);
const oldX = rect.left;
canvas.viewportCenterObjectV(rect);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
expect(rect.left).toBe(oldX);
canvas.setZoom(2);
canvas.viewportCenterObjectV(rect);
expect(rect.getCenterPoint().y).toBe(
canvas.height / (2 * canvas.getZoom()),
);
expect(rect.left).toBe(oldX);
canvas.absolutePan(new Point(pan, pan));
canvas.viewportCenterObjectV(rect);
expect(rect.getCenterPoint().y).toBe(
(canvas.height / 2 + pan) / canvas.getZoom(),
);
expect(rect.left).toBe(oldX);
});
it('centers objects in viewport both horizontally and vertically', () => {
expect(canvas.viewportCenterObject).toBeTypeOf('function');
const rect = makeRect({ left: 102, top: 202 });
const pan = 10;
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
canvas.add(rect);
canvas.viewportCenterObject(rect);
expect(rect.getCenterPoint().y).toBe(canvas.height / 2);
expect(rect.getCenterPoint().x).toBe(canvas.width / 2);
canvas.setZoom(2);
canvas.viewportCenterObject(rect);
expect(rect.getCenterPoint().y).toBe(
canvas.height / (2 * canvas.getZoom()),
);
expect(rect.getCenterPoint().x).toBe(canvas.width / (2 * canvas.getZoom()));
canvas.absolutePan(new Point(pan, pan));
canvas.viewportCenterObject(rect);
expect(rect.getCenterPoint().y).toBe(
(canvas.height / 2 + pan) / canvas.getZoom(),
);
expect(rect.getCenterPoint().x).toBe(
(canvas.width / 2 + pan) / canvas.getZoom(),
);
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
});
it('generates SVG correctly', () => {
expect(canvas.toSVG).toBeTypeOf('function');
canvas.clear();
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
const svg = canvas.toSVG();
expect(svg).toEqualSVG(CANVAS_SVG);
});
it('supports different encodings in SVG (ISO-8859-1)', () => {
expect(canvas.toSVG).toBeTypeOf('function');
canvas.clear();
canvas.viewportTransform = [1, 0, 0, 1, 0, 0];
// @ts-expect-error -- TS2322: Type 'ISO-8859-1' is not assignable to type 'UTF-8, seems like types don't allow this encoding
const svg = canvas.toSVG({ encoding: 'ISO-8859-1' });
const svgDefaultEncoding = canvas.toSVG();
expect(svg).not.toBe(svgDefaultEncoding);
expect(svg).toEqualSVG(
CANVAS_SVG.replace('encoding="UTF-8"', 'encoding="ISO-8859-1"'),
);
});
it('can generate SVG without preamble', () => {
expect(canvas.toSVG).toBeTypeOf('function');
const withPreamble = canvas.toSVG();
const withoutPreamble = canvas.toSVG({ suppressPreamble: true });
expect(withPreamble).not.toBe(withoutPreamble);
expect(withoutPreamble.slice(0, 4)).toBe('