diff --git a/CHANGELOG.md b/CHANGELOG.md index fd60d56c929..38e0a1eb3c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(TS): migrate Image [#8443](https://github.com/fabricjs/fabric.js/pull/8443) - chore(TS): migrate Shadow [#8462](https://github.com/fabricjs/fabric.js/pull/8462) - fix(Itext): show incorrect pointer position after scale changed - chore(TS): migrate text classes/mixins [#8408](https://github.com/fabricjs/fabric.js/pull/8408) diff --git a/src/filters/2d_backend.class.ts b/src/filters/2d_backend.class.ts index ecba6db6f14..2ce5f00a129 100644 --- a/src/filters/2d_backend.class.ts +++ b/src/filters/2d_backend.class.ts @@ -1,6 +1,7 @@ /** * Canvas 2D filter backend. */ +import type { BaseFilter } from './base_filter.class'; import { T2DPipelineState } from './typedefs'; export class Canvas2dFilterBackend { @@ -24,8 +25,8 @@ export class Canvas2dFilterBackend { * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. */ applyFilters( - filters: any[], - sourceElement: HTMLImageElement | HTMLCanvasElement, + filters: BaseFilter[], + sourceElement: CanvasImageSource, sourceWidth: number, sourceHeight: number, targetCanvas: HTMLCanvasElement @@ -47,7 +48,7 @@ export class Canvas2dFilterBackend { ctx, filterBackend: this, }; - filters.forEach(function (filter) { + filters.forEach((filter) => { filter.applyTo(pipelineState); }); const { imageData: imageDataPostFilter } = pipelineState; diff --git a/src/filters/FilterBackend.ts b/src/filters/FilterBackend.ts new file mode 100644 index 00000000000..318ca8b8719 --- /dev/null +++ b/src/filters/FilterBackend.ts @@ -0,0 +1,30 @@ +import { fabric } from '../../HEADER'; +import { config } from '../config'; +import { Canvas2dFilterBackend } from './2d_backend.class'; +import { WebGLFilterBackend } from './webgl_backend.class'; +import { webGLProbe } from './WebGLProbe'; + +export type FilterBackend = WebGLFilterBackend | Canvas2dFilterBackend; + +export function initFilterBackend(): FilterBackend { + webGLProbe.queryWebGL(); + if (config.enableGLFiltering && webGLProbe.isSupported(config.textureSize)) { + return new WebGLFilterBackend({ tileSize: config.textureSize }); + } else { + return new Canvas2dFilterBackend(); + } +} + +/** + * @todo refactor to a module w/o assigning to fabric + */ +export function getFilterBackend(): FilterBackend { + if (!fabric.filterBackend) { + fabric.filterBackend = initFilterBackend(); + } + return fabric.filterBackend; +} + +fabric.Canvas2dFilterBackend = Canvas2dFilterBackend; +fabric.WebglFilterBackend = WebGLFilterBackend; +fabric.initFilterBackend = initFilterBackend; diff --git a/src/filters/WebGLProbe.ts b/src/filters/WebGLProbe.ts index f2167ac6505..d07ba8a6a57 100644 --- a/src/filters/WebGLProbe.ts +++ b/src/filters/WebGLProbe.ts @@ -1,8 +1,5 @@ import { fabric } from '../../HEADER'; -import { config } from '../config'; import { createCanvasElement } from '../util/misc/dom'; -import { Canvas2dFilterBackend } from './2d_backend.class'; -import { WebGLFilterBackend } from './webgl_backend.class'; export enum WebGLPrecision { low = 'lowp', @@ -11,7 +8,7 @@ export enum WebGLPrecision { } /** - * Lazy initialize WebGL contants + * Lazy initialize WebGL constants */ class WebGLProbe { maxTextureSize?: number; @@ -63,18 +60,3 @@ class WebGLProbe { } export const webGLProbe = new WebGLProbe(); - -export function initFilterBackend(): - | WebGLFilterBackend - | Canvas2dFilterBackend { - webGLProbe.queryWebGL(); - if (config.enableGLFiltering && webGLProbe.isSupported(config.textureSize)) { - return new WebGLFilterBackend({ tileSize: config.textureSize }); - } else { - return new Canvas2dFilterBackend(); - } -} - -fabric.Canvas2dFilterBackend = Canvas2dFilterBackend; -fabric.WebglFilterBackend = WebGLFilterBackend; -fabric.initFilterBackend = initFilterBackend; diff --git a/src/filters/base_filter.class.ts b/src/filters/base_filter.class.ts index 0bac427f713..3280daa065f 100644 --- a/src/filters/base_filter.class.ts +++ b/src/filters/base_filter.class.ts @@ -230,7 +230,8 @@ export abstract class BaseFilter { * Other filters may need their own version ( ColorMatrix, HueRotation, gamma, ComposedFilter ) * @param {Object} options **/ - isNeutralState(/* options */): boolean { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isNeutralState(options?: any): boolean { const main = this.mainParameter, // @ts-ignore ts you are lying proto = this.__proto__; diff --git a/src/filters/typedefs.ts b/src/filters/typedefs.ts index dfa763f6778..9540ec9e41c 100644 --- a/src/filters/typedefs.ts +++ b/src/filters/typedefs.ts @@ -31,7 +31,7 @@ export type T2DPipelineState = { filterBackend: Canvas2dFilterBackend; canvasEl: HTMLCanvasElement; imageData: ImageData; - originalEl: HTMLCanvasElement | HTMLImageElement; + originalEl: CanvasImageSource; originalImageData?: ImageData; ctx: CanvasRenderingContext2D; helpLayer?: HTMLCanvasElement; diff --git a/src/mixins/object_interactivity.mixin.ts b/src/mixins/object_interactivity.mixin.ts index 64cdc32b747..fad7486b3b4 100644 --- a/src/mixins/object_interactivity.mixin.ts +++ b/src/mixins/object_interactivity.mixin.ts @@ -79,7 +79,7 @@ export class InteractiveFabricObject extends FabricObject { * Constructor * @param {Object} [options] Options object */ - constructor(options: Record) { + constructor(options?: Record) { super(options); } diff --git a/src/parser/parseAttributes.ts b/src/parser/parseAttributes.ts index d17d4b60650..da84938bea7 100644 --- a/src/parser/parseAttributes.ts +++ b/src/parser/parseAttributes.ts @@ -16,9 +16,13 @@ import { setStrokeFillOpacity } from './setStrokeFillOpacity'; * @param {Array} attributes Array of attributes to parse * @return {Object} object containing parsed attributes' names/values */ -export function parseAttributes(element, attributes, svgUid?: string) { +export function parseAttributes( + element, + attributes, + svgUid?: string +): Record { if (!element) { - return; + return {}; } let value, @@ -65,12 +69,10 @@ export function parseAttributes(element, attributes, svgUid?: string) { ); } - let normalizedAttr, - normalizedValue, - normalizedStyle = {}; + const normalizedStyle = {}; for (const attr in ownAttributes) { - normalizedAttr = normalizeAttr(attr); - normalizedValue = normalizeValue( + const normalizedAttr = normalizeAttr(attr); + const normalizedValue = normalizeValue( normalizedAttr, ownAttributes[attr], parentAttributes, @@ -81,7 +83,7 @@ export function parseAttributes(element, attributes, svgUid?: string) { if (normalizedStyle && normalizedStyle.font) { parseFontDeclaration(normalizedStyle.font, normalizedStyle); } - const mergedAttrs = Object.assign(parentAttributes, normalizedStyle); + const mergedAttrs = { ...parentAttributes, ...normalizedStyle }; return svgValidParentsRegEx.test(element.nodeName) ? mergedAttrs : setStrokeFillOpacity(mergedAttrs); diff --git a/src/shapes/image.class.ts b/src/shapes/image.class.ts index 457e9a63740..0d3e06d91aa 100644 --- a/src/shapes/image.class.ts +++ b/src/shapes/image.class.ts @@ -1,771 +1,690 @@ //@ts-nocheck -import { FabricObject } from './fabricObject.class'; -import { initFilterBackend } from '../filters/WebGLProbe'; - -(function (global) { - var fabric = global.fabric, - extend = fabric.util.object.extend; - /** - * Image class - * @class fabric.Image - * @extends fabric.Object - * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} - * @see {@link fabric.Image#initialize} for constructor definition - */ - fabric.Image = fabric.util.createClass( - FabricObject, - /** @lends fabric.Image.prototype */ { - /** - * Type of an object - * @type String - * @default - */ - type: 'image', - - /** - * Width of a stroke. - * For image quality a stroke multiple of 2 gives better results. - * @type Number - * @default - */ - strokeWidth: 0, - - /** - * When calling {@link fabric.Image.getSrc}, return value from element src with `element.getAttribute('src')`. - * This allows for relative urls as image src. - * @since 2.7.0 - * @type Boolean - * @default - */ - srcFromAttribute: false, - - /** - * private - * contains last value of scaleX to detect - * if the Image got resized after the last Render - * @type Number - */ - _lastScaleX: 1, - - /** - * private - * contains last value of scaleY to detect - * if the Image got resized after the last Render - * @type Number - */ - _lastScaleY: 1, - - /** - * private - * contains last value of scaling applied by the apply filter chain - * @type Number - */ - _filterScalingX: 1, - - /** - * private - * contains last value of scaling applied by the apply filter chain - * @type Number - */ - _filterScalingY: 1, - - /** - * minimum scale factor under which any resizeFilter is triggered to resize the image - * 0 will disable the automatic resize. 1 will trigger automatically always. - * number bigger than 1 are not implemented yet. - * @type Number - */ - minimumScaleTrigger: 0.5, - - /** - * List of properties to consider when checking if - * state of an object is changed ({@link fabric.Object#hasStateChanged}) - * as well as for history (undo/redo) purposes - * @type Array - */ - stateProperties: FabricObject.prototype.stateProperties.concat( - 'cropX', - 'cropY' - ), +import { fabric } from '../../HEADER'; +import type { BaseFilter } from '../filters/base_filter.class'; +import { getFilterBackend } from '../filters/FilterBackend'; +import { SHARED_ATTRIBUTES } from '../parser/attributes'; +import { parseAttributes } from '../parser/parseAttributes'; +import { TClassProperties, TSize } from '../typedefs'; +import { cleanUpJsdomNode } from '../util/dom_misc'; +import { createCanvasElement } from '../util/misc/dom'; +import { findScaleToCover, findScaleToFit } from '../util/misc/findScaleTo'; +import { + enlivenObjectEnlivables, + enlivenObjects, + loadImage, + LoadImageOptions, +} from '../util/misc/objectEnlive'; +import { parsePreserveAspectRatioAttribute } from '../util/misc/svgParsing'; +import { FabricObject, fabricObjectDefaultValues } from './fabricObject.class'; + +export type ImageSource = + | HTMLImageElement + | HTMLVideoElement + | HTMLCanvasElement; + +/** + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} + */ +export class Image extends FabricObject { + /** + * When calling {@link Image.getSrc}, return value from element src with `element.getAttribute('src')`. + * This allows for relative urls as image src. + * @since 2.7.0 + * @type Boolean + * @default + */ + srcFromAttribute: boolean; - /** - * List of properties to consider when checking if cache needs refresh - * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single - * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty - * and refreshed at the next render - * @type Array - */ - cacheProperties: FabricObject.prototype.cacheProperties.concat( - 'cropX', - 'cropY' - ), + /** + * private + * contains last value of scaleX to detect + * if the Image got resized after the last Render + * @type Number + */ + protected _lastScaleX = 1; - /** - * key used to retrieve the texture representing this image - * @since 2.0.0 - * @type String - * @default - */ - cacheKey: '', - - /** - * Image crop in pixels from original image size. - * @since 2.0.0 - * @type Number - * @default - */ - cropX: 0, - - /** - * Image crop in pixels from original image size. - * @since 2.0.0 - * @type Number - * @default - */ - cropY: 0, - - /** - * Indicates whether this canvas will use image smoothing when painting this image. - * Also influence if the cacheCanvas for this image uses imageSmoothing - * @since 4.0.0-beta.11 - * @type Boolean - * @default - */ - imageSmoothing: true, - - /** - * Constructor - * Image can be initialized with any canvas drawable or a string. - * The string should be a url and will be loaded as an image. - * Canvas and Image element work out of the box, while videos require extra code to work. - * Please check video element events for seeking. - * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element - * @param {Object} [options] Options object - * @return {fabric.Image} thisArg - */ - initialize: function (element, options) { - options || (options = {}); - this.filters = []; - this.cacheKey = 'texture' + FabricObject.__uid++; - this.callSuper('initialize', options); - this._initElement(element, options); - }, - - /** - * Returns image element which this instance if based on - * @return {HTMLImageElement} Image element - */ - getElement: function () { - return this._element || {}; - }, - - /** - * Sets image element for this instance to a specified one. - * If filters defined they are applied to new image. - * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. - * @param {HTMLImageElement} element - * @param {Object} [options] Options object - * @return {fabric.Image} thisArg - * @chainable - */ - setElement: function (element, options) { - this.removeTexture(this.cacheKey); - this.removeTexture(this.cacheKey + '_filtered'); - this._element = element; - this._originalElement = element; - this._initConfig(options); - element.classList.add(fabric.Image.CSS_CANVAS); - if (this.filters.length !== 0) { - this.applyFilters(); - } - // resizeFilters work on the already filtered copy. - // we need to apply resizeFilters AFTER normal filters. - // applyResizeFilters is run more often than normal filters - // and is triggered by user interactions rather than dev code - if (this.resizeFilter) { - this.applyResizeFilters(); - } - return this; - }, - - /** - * Delete a single texture if in webgl mode - */ - removeTexture: function (key) { - var backend = fabric.filterBackend; - if (backend && backend.evictCachesForKey) { - backend.evictCachesForKey(key); - } - }, - - /** - * Delete textures, reference to elements and eventually JSDOM cleanup - */ - dispose: function () { - this.callSuper('dispose'); - this.removeTexture(this.cacheKey); - this.removeTexture(this.cacheKey + '_filtered'); - this._cacheContext = undefined; - ['_originalElement', '_element', '_filteredEl', '_cacheCanvas'].forEach( - function (element) { - fabric.util.cleanUpJsdomNode(this[element]); - this[element] = undefined; - }.bind(this) - ); - }, - - /** - * Get the crossOrigin value (of the corresponding image element) - */ - getCrossOrigin: function () { - return ( - this._originalElement && (this._originalElement.crossOrigin || null) - ); - }, - - /** - * Returns original size of an image - * @return {Object} Object with "width" and "height" properties - */ - getOriginalSize: function () { - var element = this.getElement(); - return { - width: element.naturalWidth || element.width, - height: element.naturalHeight || element.height, - }; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _stroke: function (ctx) { - if (!this.stroke || this.strokeWidth === 0) { - return; - } - var w = this.width / 2, - h = this.height / 2; - ctx.beginPath(); - ctx.moveTo(-w, -h); - ctx.lineTo(w, -h); - ctx.lineTo(w, h); - ctx.lineTo(-w, h); - ctx.lineTo(-w, -h); - ctx.closePath(); - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of an instance - */ - toObject: function (propertiesToInclude) { - var filters = []; - - this.filters.forEach(function (filterObj) { - if (filterObj) { - filters.push(filterObj.toObject()); - } - }); - var object = extend( - this.callSuper( - 'toObject', - ['cropX', 'cropY'].concat(propertiesToInclude) - ), - { - src: this.getSrc(), - crossOrigin: this.getCrossOrigin(), - filters: filters, - } - ); - if (this.resizeFilter) { - object.resizeFilter = this.resizeFilter.toObject(); - } - return object; - }, - - /** - * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,height. - * @return {Boolean} - */ - hasCrop: function () { - return ( - this.cropX || - this.cropY || - this.width < this._element.width || - this.height < this._element.height - ); - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @return {Array} an array of strings with the specific svg representation - * of the instance - */ - _toSVG: function () { - var svgString = [], - imageMarkup = [], - strokeSvg, - element = this._element, - x = -this.width / 2, - y = -this.height / 2, - clipPath = '', - imageRendering = ''; - if (!element) { - return []; - } - if (this.hasCrop()) { - var clipPathId = FabricObject.__uid++; - svgString.push( - '\n', - '\t\n', - '\n' - ); - clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" '; - } - if (!this.imageSmoothing) { - imageRendering = '" image-rendering="optimizeSpeed'; - } - imageMarkup.push( - '\t\n' - ); - - if (this.stroke || this.strokeDashArray) { - var origFill = this.fill; - this.fill = null; - strokeSvg = [ - '\t\n', - ]; - this.fill = origFill; - } - if (this.paintFirst !== 'fill') { - svgString = svgString.concat(strokeSvg, imageMarkup); - } else { - svgString = svgString.concat(imageMarkup, strokeSvg); - } - return svgString; - }, - /* _TO_SVG_END_ */ - - /** - * Returns source of an image - * @param {Boolean} filtered indicates if the src is needed for svg - * @return {String} Source of an image - */ - getSrc: function (filtered) { - var element = filtered ? this._element : this._originalElement; - if (element) { - if (element.toDataURL) { - return element.toDataURL(); - } - - if (this.srcFromAttribute) { - return element.getAttribute('src'); - } else { - return element.src; - } - } else { - return this.src || ''; - } - }, - - /** - * Loads and sets source of an image\ - * **IMPORTANT**: It is recommended to abort loading tasks before calling this method to prevent race conditions and unnecessary networking - * @param {String} src Source string (URL) - * @param {Object} [options] Options object - * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") - * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes - * @return {Promise} thisArg - */ - setSrc: function (src, options) { - var _this = this; - return fabric.util.loadImage(src, options).then(function (img) { - _this.setElement(img, options); - _this._setWidthHeight(); - return _this; - }); - }, - - /** - * Returns string representation of an instance - * @return {String} String representation of an instance - */ - toString: function () { - return '#'; - }, - - applyResizeFilters: function () { - var filter = this.resizeFilter, - minimumScale = this.minimumScaleTrigger, - objectScale = this.getTotalObjectScaling(), - scaleX = objectScale.x, - scaleY = objectScale.y, - elementToFilter = this._filteredEl || this._originalElement; - if (this.group) { - this.set('dirty', true); - } - if (!filter || (scaleX > minimumScale && scaleY > minimumScale)) { - this._element = elementToFilter; - this._filterScalingX = 1; - this._filterScalingY = 1; - this._lastScaleX = scaleX; - this._lastScaleY = scaleY; - return; - } - if (!fabric.filterBackend) { - fabric.filterBackend = initFilterBackend(); + /** + * private + * contains last value of scaleY to detect + * if the Image got resized after the last Render + * @type Number + */ + protected _lastScaleY = 1; + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + protected _filterScalingX = 1; + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + protected _filterScalingY = 1; + + /** + * minimum scale factor under which any resizeFilter is triggered to resize the image + * 0 will disable the automatic resize. 1 will trigger automatically always. + * number bigger than 1 are not implemented yet. + * @type Number + */ + minimumScaleTrigger: number; + + /** + * key used to retrieve the texture representing this image + * @since 2.0.0 + * @type String + * @default + */ + cacheKey: string; + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropX: number; + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropY: number; + + /** + * Indicates whether this canvas will use image smoothing when painting this image. + * Also influence if the cacheCanvas for this image uses imageSmoothing + * @since 4.0.0-beta.11 + * @type Boolean + * @default + */ + imageSmoothing: boolean; + + preserveAspectRatio: string; + + protected src: string; + + static filters: Record; + + filters: BaseFilter[]; + resizeFilter: BaseFilter; + + protected _element: ImageSource; + protected _originalElement: ImageSource; + protected _filteredEl: ImageSource; + + /** + * Constructor + * Image can be initialized with any canvas drawable or a string. + * The string should be a url and will be loaded as an image. + * Canvas and Image element work out of the box, while videos require extra code to work. + * Please check video element events for seeking. + * @param {ImageSource | string} element Image element + * @param {Object} [options] Options object + */ + constructor(elementId: string, options: any = {}); + constructor(element: ImageSource, options: any = {}); + constructor(arg0: ImageSource | string, options: any = {}) { + super(); + this.filters = []; + this.cacheKey = `texture${FabricObject.__uid++}`; + this.set(options); + this.setElement( + (typeof arg0 === 'string' && fabric.document.getElementById(arg0)) || + arg0, + options + ); + } + + /** + * Returns image element which this instance if based on + */ + getElement() { + return this._element; + } + + /** + * Sets image element for this instance to a specified one. + * If filters defined they are applied to new image. + * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. + * @param {HTMLImageElement} element + * @param {Partial} [size] Options object + */ + setElement(element: ImageSource, size: Partial = {}) { + this.removeTexture(this.cacheKey); + this.removeTexture(`${this.cacheKey}_filtered`); + this._element = element; + this._originalElement = element; + this._setWidthHeight(size); + element.classList.add(Image.CSS_CANVAS); + if (this.filters.length !== 0) { + this.applyFilters(); + } + // resizeFilters work on the already filtered copy. + // we need to apply resizeFilters AFTER normal filters. + // applyResizeFilters is run more often than normal filters + // and is triggered by user interactions rather than dev code + if (this.resizeFilter) { + this.applyResizeFilters(); + } + } + + /** + * Delete a single texture if in webgl mode + */ + removeTexture(key: string) { + const backend = fabric.filterBackend; + if (backend && backend.evictCachesForKey) { + backend.evictCachesForKey(key); + } + } + + /** + * Delete textures, reference to elements and eventually JSDOM cleanup + */ + dispose() { + super.dispose(); + this.removeTexture(this.cacheKey); + this.removeTexture(`${this.cacheKey}_filtered`); + this._cacheContext = null; + ['_originalElement', '_element', '_filteredEl', '_cacheCanvas'].forEach( + (element) => { + cleanUpJsdomNode(this[element as keyof this]); + // @ts-expect-error disposing + this[element] = undefined; + } + ); + } + + /** + * Get the crossOrigin value (of the corresponding image element) + */ + getCrossOrigin() { + return ( + this._originalElement && + ((this._originalElement as any).crossOrigin || null) + ); + } + + /** + * Returns original size of an image + */ + getOriginalSize() { + const element = this.getElement() as any; + if (!element) { + return { + width: 0, + height: 0, + }; + } + return { + width: element.naturalWidth || element.width, + height: element.naturalHeight || element.height, + }; + } + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _stroke(ctx: CanvasRenderingContext2D) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + const w = this.width / 2, + h = this.height / 2; + ctx.beginPath(); + ctx.moveTo(-w, -h); + ctx.lineTo(w, -h); + ctx.lineTo(w, h); + ctx.lineTo(-w, h); + ctx.lineTo(-w, -h); + ctx.closePath(); + } + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject(propertiesToInclude: (keyof this)[] = []): Record { + const filters: Record[] = []; + this.filters.forEach((filterObj) => { + filterObj && filters.push(filterObj.toObject()); + }); + return { + ...super.toObject(['cropX', 'cropY', ...propertiesToInclude]), + src: this.getSrc(), + crossOrigin: this.getCrossOrigin(), + filters, + ...(this.resizeFilter + ? { resizeFilter: this.resizeFilter.toObject() } + : {}), + }; + } + + /** + * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,height. + * @return {Boolean} + */ + hasCrop() { + return ( + !!this.cropX || + !!this.cropY || + this.width < this._element.width || + this.height < this._element.height + ); + } + + /** + * Returns svg representation of an instance + * @return {string[]} an array of strings with the specific svg representation + * of the instance + */ + _toSVG() { + const imageMarkup = [], + element = this._element, + x = -this.width / 2, + y = -this.height / 2; + let svgString = [], + strokeSvg, + clipPath = '', + imageRendering = ''; + if (!element) { + return []; + } + if (this.hasCrop()) { + const clipPathId = FabricObject.__uid++; + svgString.push( + '\n', + '\t\n', + '\n' + ); + clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" '; + } + if (!this.imageSmoothing) { + imageRendering = '" image-rendering="optimizeSpeed'; + } + imageMarkup.push( + '\t\n' + ); + + if (this.stroke || this.strokeDashArray) { + const origFill = this.fill; + this.fill = null; + strokeSvg = [ + '\t\n', + ]; + this.fill = origFill; + } + if (this.paintFirst !== 'fill') { + svgString = svgString.concat(strokeSvg, imageMarkup); + } else { + svgString = svgString.concat(imageMarkup, strokeSvg); + } + return svgString; + } + + /** + * Returns source of an image + * @param {Boolean} filtered indicates if the src is needed for svg + * @return {String} Source of an image + */ + getSrc(filtered?: boolean) { + const element = filtered ? this._element : this._originalElement; + if (element) { + if (element.toDataURL) { + return element.toDataURL(); + } + + if (this.srcFromAttribute) { + return element.getAttribute('src'); + } else { + return element.src; + } + } else { + return this.src || ''; + } + } + + /** + * Alias for getSrc + * @param filtered + * @deprecated + */ + getSvgSrc(filtered?: boolean) { + return this.getSrc(filtered); + } + + /** + * Loads and sets source of an image\ + * **IMPORTANT**: It is recommended to abort loading tasks before calling this method to prevent race conditions and unnecessary networking + * @param {String} src Source string (URL) + * @param {LoadImageOptions} [options] Options object + */ + setSrc(src: string, { crossOrigin, signal }: LoadImageOptions = {}) { + return loadImage(src, { crossOrigin, signal }).then((img) => { + typeof crossOrigin !== 'undefined' && this.set({ crossOrigin }); + this.setElement(img); + }); + } + + /** + * Returns string representation of an instance + * @return {String} String representation of an instance + */ + toString() { + return `#`; + } + + applyResizeFilters() { + const filter = this.resizeFilter, + minimumScale = this.minimumScaleTrigger, + objectScale = this.getTotalObjectScaling(), + scaleX = objectScale.x, + scaleY = objectScale.y, + elementToFilter = this._filteredEl || this._originalElement; + if (this.group) { + this.set('dirty', true); + } + if (!filter || (scaleX > minimumScale && scaleY > minimumScale)) { + this._element = elementToFilter; + this._filterScalingX = 1; + this._filterScalingY = 1; + this._lastScaleX = scaleX; + this._lastScaleY = scaleY; + return; + } + const canvasEl = createCanvasElement(), + sourceWidth = elementToFilter.width, + sourceHeight = elementToFilter.height; + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._lastScaleX = filter.scaleX = scaleX; + this._lastScaleY = filter.scaleY = scaleY; + getFilterBackend().applyFilters( + [filter], + elementToFilter, + sourceWidth, + sourceHeight, + this._element + ); + this._filterScalingX = canvasEl.width / this._originalElement.width; + this._filterScalingY = canvasEl.height / this._originalElement.height; + } + + /** + * Applies filters assigned to this image (from "filters" array) or from filter param + * @method applyFilters + * @param {Array} filters to be applied + * @param {Boolean} forResizing specify if the filter operation is a resize operation + */ + applyFilters(filters: BaseFilter[] = this.filters || []) { + filters = filters.filter((filter) => filter && !filter.isNeutralState()); + this.set('dirty', true); + + // needs to clear out or WEBGL will not resize correctly + this.removeTexture(`${this.cacheKey}_filtered`); + + if (filters.length === 0) { + this._element = this._originalElement; + this._filteredEl = null; + this._filterScalingX = 1; + this._filterScalingY = 1; + return; + } + + const imgElement = this._originalElement, + sourceWidth = imgElement.naturalWidth || imgElement.width, + sourceHeight = imgElement.naturalHeight || imgElement.height; + + if (this._element === this._originalElement) { + // if the element is the same we need to create a new element + const canvasEl = createCanvasElement(); + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._filteredEl = canvasEl; + } else { + // clear the existing element to get new filter data + // also dereference the eventual resized _element + this._element = this._filteredEl; + this._filteredEl + .getContext('2d') + .clearRect(0, 0, sourceWidth, sourceHeight); + // we also need to resize again at next renderAll, so remove saved _lastScaleX/Y + this._lastScaleX = 1; + this._lastScaleY = 1; + } + getFilterBackend().applyFilters( + filters, + this._originalElement, + sourceWidth, + sourceHeight, + this._element + ); + if ( + this._originalElement.width !== this._element.width || + this._originalElement.height !== this._element.height + ) { + this._filterScalingX = this._element.width / this._originalElement.width; + this._filterScalingY = + this._element.height / this._originalElement.height; + } + } + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render(ctx: CanvasRenderingContext2D) { + ctx.imageSmoothingEnabled = this.imageSmoothing; + if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { + this.applyResizeFilters(); + } + this._stroke(ctx); + this._renderPaintInOrder(ctx); + } + + /** + * Paint the cached copy of the object on the target context. + * it will set the imageSmoothing for the draw operation + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas(ctx: CanvasRenderingContext2D) { + ctx.imageSmoothingEnabled = this.imageSmoothing; + super.drawCacheOnCanvas(ctx); + } + + /** + * Decide if the object should cache or not. Create its own cache level + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * This is the special image version where we would like to avoid caching where possible. + * Essentially images do not benefit from caching. They may require caching, and in that + * case we do it. Also caching an image usually ends in a loss of details. + * A full performance audit should be done. + * @return {Boolean} + */ + shouldCache() { + return this.needsItsOwnCache(); + } + + _renderFill(ctx: CanvasRenderingContext2D) { + const elementToDraw = this._element; + if (!elementToDraw) { + return; + } + const scaleX = this._filterScalingX, + scaleY = this._filterScalingY, + w = this.width, + h = this.height, + // crop values cannot be lesser than 0. + cropX = Math.max(this.cropX, 0), + cropY = Math.max(this.cropY, 0), + elWidth = elementToDraw.naturalWidth || elementToDraw.width, + elHeight = elementToDraw.naturalHeight || elementToDraw.height, + sX = cropX * scaleX, + sY = cropY * scaleY, + // the width height cannot exceed element width/height, starting from the crop offset. + sW = Math.min(w * scaleX, elWidth - sX), + sH = Math.min(h * scaleY, elHeight - sY), + x = -w / 2, + y = -h / 2, + maxDestW = Math.min(w, elWidth / scaleX - cropX), + maxDestH = Math.min(h, elHeight / scaleY - cropY); + + elementToDraw && + ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH); + } + + /** + * needed to check if image needs resize + * @private + */ + _needsResize() { + const scale = this.getTotalObjectScaling(); + return scale.x !== this._lastScaleX || scale.y !== this._lastScaleY; + } + + /** + * @private + * @deprecated unused + */ + _resetWidthHeight() { + this.set(this.getOriginalSize()); + } + + /** + * @private + * Set the width and the height of the image object, using the element or the + * options. + */ + _setWidthHeight({ width, height }: Partial = {}) { + const size = this.getOriginalSize(); + this.width = width || size.width; + this.height = height || size.height; + } + + /** + * Calculate offset for center and scale factor for the image in order to respect + * the preserveAspectRatio attribute + * @private + */ + parsePreserveAspectRatioAttribute() { + const pAR = parsePreserveAspectRatioAttribute( + this.preserveAspectRatio || '' + ), + pWidth = this.width, + pHeight = this.height, + parsedAttributes = { width: pWidth, height: pHeight }; + let rWidth = this._element.width, + rHeight = this._element.height, + scaleX = 1, + scaleY = 1, + offsetLeft = 0, + offsetTop = 0, + cropX = 0, + cropY = 0, + offset; + + if (pAR && (pAR.alignX !== 'none' || pAR.alignY !== 'none')) { + if (pAR.meetOrSlice === 'meet') { + scaleX = scaleY = findScaleToFit(this._element, parsedAttributes); + offset = (pWidth - rWidth * scaleX) / 2; + if (pAR.alignX === 'Min') { + offsetLeft = -offset; } - var canvasEl = fabric.util.createCanvasElement(), - cacheKey = this._filteredEl - ? this.cacheKey + '_filtered' - : this.cacheKey, - sourceWidth = elementToFilter.width, - sourceHeight = elementToFilter.height; - canvasEl.width = sourceWidth; - canvasEl.height = sourceHeight; - this._element = canvasEl; - this._lastScaleX = filter.scaleX = scaleX; - this._lastScaleY = filter.scaleY = scaleY; - fabric.filterBackend.applyFilters( - [filter], - elementToFilter, - sourceWidth, - sourceHeight, - this._element, - cacheKey - ); - this._filterScalingX = canvasEl.width / this._originalElement.width; - this._filterScalingY = canvasEl.height / this._originalElement.height; - }, - - /** - * Applies filters assigned to this image (from "filters" array) or from filter param - * @method applyFilters - * @param {Array} filters to be applied - * @param {Boolean} forResizing specify if the filter operation is a resize operation - * @return {thisArg} return the fabric.Image object - * @chainable - */ - applyFilters: function (filters) { - filters = filters || this.filters || []; - filters = filters.filter(function (filter) { - return filter && !filter.isNeutralState(); - }); - this.set('dirty', true); - - // needs to clear out or WEBGL will not resize correctly - this.removeTexture(this.cacheKey + '_filtered'); - - if (filters.length === 0) { - this._element = this._originalElement; - this._filteredEl = null; - this._filterScalingX = 1; - this._filterScalingY = 1; - return this; + if (pAR.alignX === 'Max') { + offsetLeft = offset; } - - var imgElement = this._originalElement, - sourceWidth = imgElement.naturalWidth || imgElement.width, - sourceHeight = imgElement.naturalHeight || imgElement.height; - - if (this._element === this._originalElement) { - // if the element is the same we need to create a new element - var canvasEl = fabric.util.createCanvasElement(); - canvasEl.width = sourceWidth; - canvasEl.height = sourceHeight; - this._element = canvasEl; - this._filteredEl = canvasEl; - } else { - // clear the existing element to get new filter data - // also dereference the eventual resized _element - this._element = this._filteredEl; - this._filteredEl - .getContext('2d') - .clearRect(0, 0, sourceWidth, sourceHeight); - // we also need to resize again at next renderAll, so remove saved _lastScaleX/Y - this._lastScaleX = 1; - this._lastScaleY = 1; + offset = (pHeight - rHeight * scaleY) / 2; + if (pAR.alignY === 'Min') { + offsetTop = -offset; } - if (!fabric.filterBackend) { - fabric.filterBackend = initFilterBackend(); + if (pAR.alignY === 'Max') { + offsetTop = offset; } - fabric.filterBackend.applyFilters( - filters, - this._originalElement, - sourceWidth, - sourceHeight, - this._element, - this.cacheKey - ); - if ( - this._originalElement.width !== this._element.width || - this._originalElement.height !== this._element.height - ) { - this._filterScalingX = - this._element.width / this._originalElement.width; - this._filterScalingY = - this._element.height / this._originalElement.height; + } + if (pAR.meetOrSlice === 'slice') { + scaleX = scaleY = findScaleToCover(this._element, parsedAttributes); + offset = rWidth - pWidth / scaleX; + if (pAR.alignX === 'Mid') { + cropX = offset / 2; } - return this; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function (ctx) { - ctx.imageSmoothingEnabled = this.imageSmoothing; - if ( - this.isMoving !== true && - this.resizeFilter && - this._needsResize() - ) { - this.applyResizeFilters(); + if (pAR.alignX === 'Max') { + cropX = offset; } - this._stroke(ctx); - this._renderPaintInOrder(ctx); - }, - - /** - * Paint the cached copy of the object on the target context. - * it will set the imageSmoothing for the draw operation - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - drawCacheOnCanvas: function (ctx) { - ctx.imageSmoothingEnabled = this.imageSmoothing; - FabricObject.prototype.drawCacheOnCanvas.call(this, ctx); - }, - - /** - * Decide if the object should cache or not. Create its own cache level - * needsItsOwnCache should be used when the object drawing method requires - * a cache step. None of the fabric classes requires it. - * Generally you do not cache objects in groups because the group outside is cached. - * This is the special image version where we would like to avoid caching where possible. - * Essentially images do not benefit from caching. They may require caching, and in that - * case we do it. Also caching an image usually ends in a loss of details. - * A full performance audit should be done. - * @return {Boolean} - */ - shouldCache: function () { - return this.needsItsOwnCache(); - }, - - _renderFill: function (ctx) { - var elementToDraw = this._element; - if (!elementToDraw) { - return; + offset = rHeight - pHeight / scaleY; + if (pAR.alignY === 'Mid') { + cropY = offset / 2; } - var scaleX = this._filterScalingX, - scaleY = this._filterScalingY, - w = this.width, - h = this.height, - min = Math.min, - max = Math.max, - // crop values cannot be lesser than 0. - cropX = max(this.cropX, 0), - cropY = max(this.cropY, 0), - elWidth = elementToDraw.naturalWidth || elementToDraw.width, - elHeight = elementToDraw.naturalHeight || elementToDraw.height, - sX = cropX * scaleX, - sY = cropY * scaleY, - // the width height cannot exceed element width/height, starting from the crop offset. - sW = min(w * scaleX, elWidth - sX), - sH = min(h * scaleY, elHeight - sY), - x = -w / 2, - y = -h / 2, - maxDestW = min(w, elWidth / scaleX - cropX), - maxDestH = min(h, elHeight / scaleY - cropY); - - elementToDraw && - ctx.drawImage( - elementToDraw, - sX, - sY, - sW, - sH, - x, - y, - maxDestW, - maxDestH - ); - }, - - /** - * needed to check if image needs resize - * @private - */ - _needsResize: function () { - var scale = this.getTotalObjectScaling(); - return scale.x !== this._lastScaleX || scale.y !== this._lastScaleY; - }, - - /** - * @private - */ - _resetWidthHeight: function () { - this.set(this.getOriginalSize()); - }, - - /** - * The Image class's initialization method. This method is automatically - * called by the constructor. - * @private - * @param {HTMLImageElement|String} element The element representing the image - * @param {Object} [options] Options object - */ - _initElement: function (element, options) { - this.setElement( - fabric.document.getElementById(element) || element, - options - ); - }, - - /** - * @private - * @param {Object} [options] Options object - */ - _initConfig: function (options) { - options || (options = {}); - this.setOptions(options); - this._setWidthHeight(options); - }, - - /** - * @private - * Set the width and the height of the image object, using the element or the - * options. - * @param {Object} [options] Object with width/height properties - */ - _setWidthHeight: function (options) { - options || (options = {}); - var el = this.getElement(); - this.width = options.width || el.naturalWidth || el.width || 0; - this.height = options.height || el.naturalHeight || el.height || 0; - }, - - /** - * Calculate offset for center and scale factor for the image in order to respect - * the preserveAspectRatio attribute - * @private - * @return {Object} - */ - parsePreserveAspectRatioAttribute: function () { - var pAR = fabric.util.parsePreserveAspectRatioAttribute( - this.preserveAspectRatio || '' - ), - rWidth = this._element.width, - rHeight = this._element.height, - scaleX = 1, - scaleY = 1, - offsetLeft = 0, - offsetTop = 0, - cropX = 0, - cropY = 0, - offset, - pWidth = this.width, - pHeight = this.height, - parsedAttributes = { width: pWidth, height: pHeight }; - if (pAR && (pAR.alignX !== 'none' || pAR.alignY !== 'none')) { - if (pAR.meetOrSlice === 'meet') { - scaleX = scaleY = fabric.util.findScaleToFit( - this._element, - parsedAttributes - ); - offset = (pWidth - rWidth * scaleX) / 2; - if (pAR.alignX === 'Min') { - offsetLeft = -offset; - } - if (pAR.alignX === 'Max') { - offsetLeft = offset; - } - offset = (pHeight - rHeight * scaleY) / 2; - if (pAR.alignY === 'Min') { - offsetTop = -offset; - } - if (pAR.alignY === 'Max') { - offsetTop = offset; - } - } - if (pAR.meetOrSlice === 'slice') { - scaleX = scaleY = fabric.util.findScaleToCover( - this._element, - parsedAttributes - ); - offset = rWidth - pWidth / scaleX; - if (pAR.alignX === 'Mid') { - cropX = offset / 2; - } - if (pAR.alignX === 'Max') { - cropX = offset; - } - offset = rHeight - pHeight / scaleY; - if (pAR.alignY === 'Mid') { - cropY = offset / 2; - } - if (pAR.alignY === 'Max') { - cropY = offset; - } - rWidth = pWidth / scaleX; - rHeight = pHeight / scaleY; - } - } else { - scaleX = pWidth / rWidth; - scaleY = pHeight / rHeight; + if (pAR.alignY === 'Max') { + cropY = offset; } - return { - width: rWidth, - height: rHeight, - scaleX: scaleX, - scaleY: scaleY, - offsetLeft: offsetLeft, - offsetTop: offsetTop, - cropX: cropX, - cropY: cropY, - }; - }, + rWidth = pWidth / scaleX; + rHeight = pHeight / scaleY; + } + } else { + scaleX = pWidth / rWidth; + scaleY = pHeight / rHeight; } - ); + return { + width: rWidth, + height: rHeight, + scaleX, + scaleY, + offsetLeft, + offsetTop, + cropX, + cropY, + }; + } /** * Default CSS class name for canvas @@ -773,97 +692,106 @@ import { initFilterBackend } from '../filters/WebGLProbe'; * @type String * @default */ - fabric.Image.CSS_CANVAS = 'canvas-img'; + static CSS_CANVAS = 'canvas-img'; /** - * Alias for getSrc + * List of attribute names to account for when parsing SVG element (used by {@link Image.fromElement}) * @static + * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement} */ - fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; + static ATTRIBUTE_NAMES = [ + ...SHARED_ATTRIBUTES, + 'x', + 'y', + 'width', + 'height', + 'preserveAspectRatio', + 'xlink:href', + 'crossOrigin', + 'image-rendering', + ]; /** - * Creates an instance of fabric.Image from its object representation + * Creates an instance of Image from its object representation * @static * @param {Object} object Object to create an instance from * @param {object} [options] Options object * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - * @returns {Promise} - */ - fabric.Image.fromObject = function (object, options) { - var _object = Object.assign({}, object), - filters = _object.filters, - resizeFilter = _object.resizeFilter; - // the generic enliving will fail on filters for now - delete _object.resizeFilter; - delete _object.filters; - var imageOptions = Object.assign({}, options, { - crossOrigin: _object.crossOrigin, - }), - filterOptions = Object.assign({}, options, { - namespace: fabric.Image.filters, - }); + * @returns {Promise} + */ + static fromObject( + { filters, resizeFilter, src, crossOrigin, ...object }: any, + options: { signal: AbortSignal } + ): Promise { + const imageOptions = { ...options, crossOrigin }, + filterOptions = { ...options, namespace: Image.filters }; return Promise.all([ - fabric.util.loadImage(_object.src, imageOptions), - filters && fabric.util.enlivenObjects(filters, filterOptions), - resizeFilter && fabric.util.enlivenObjects([resizeFilter], filterOptions), - fabric.util.enlivenObjectEnlivables(_object, options), - ]).then(function (imgAndFilters) { - _object.filters = imgAndFilters[1] || []; - _object.resizeFilter = imgAndFilters[2] && imgAndFilters[2][0]; - return new fabric.Image( - imgAndFilters[0], - Object.assign(_object, imgAndFilters[3]) - ); + loadImage(src, imageOptions), + filters && enlivenObjects(filters, filterOptions), + resizeFilter && enlivenObjects([resizeFilter], filterOptions), + enlivenObjectEnlivables(object, options), + ]).then(([el, filters = [], [resizeFilter] = [], hydratedProps = {}]) => { + return new Image(el, { + ...object, + src, + crossOrigin, + filters, + resizeFilter, + ...hydratedProps, + }); }); - }; + } /** - * Creates an instance of fabric.Image from an URL string + * Creates an instance of Image from an URL string * @static * @param {String} url URL to create an image from - * @param {object} [options] Options object - * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous - * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - * @returns {Promise} - */ - fabric.Image.fromURL = function (url, options) { - return fabric.util.loadImage(url, options || {}).then(function (img) { - return new fabric.Image(img, options); - }); - }; - - /* _FROM_SVG_START_ */ - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) - * @static - * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement} + * @param {LoadImageOptions} [options] Options object + * @returns {Promise} */ - fabric.Image.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( - 'x y width height preserveAspectRatio xlink:href crossOrigin image-rendering'.split( - ' ' - ) - ); + static fromURL(url: string, options: LoadImageOptions = {}): Promise { + return loadImage(url, options).then((img) => new Image(img, options)); + } /** - * Returns {@link fabric.Image} instance from an SVG element + * Returns {@link Image} instance from an SVG element * @static * @param {SVGElement} element Element to parse * @param {Object} [options] Options object * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - * @param {Function} callback Callback to execute when fabric.Image object is created - * @return {fabric.Image} Instance of fabric.Image + * @param {Function} callback Callback to execute when Image object is created */ - fabric.Image.fromElement = function (element, callback, options) { - var parsedAttributes = fabric.parseAttributes( - element, - fabric.Image.ATTRIBUTE_NAMES - ); - fabric.Image.fromURL( - parsedAttributes['xlink:href'], - Object.assign({}, options || {}, parsedAttributes) - ).then(function (fabricImage) { - callback(fabricImage); - }); - }; - /* _FROM_SVG_END_ */ -})(typeof exports !== 'undefined' ? exports : window); + static fromElement( + element: SVGElement, + callback: (image: Image) => any, + options: { signal?: AbortSignal } = {} + ) { + const parsedAttributes = parseAttributes(element, Image.ATTRIBUTE_NAMES); + Image.fromURL(parsedAttributes['xlink:href'], { + ...options, + ...parsedAttributes, + }).then(callback); + } +} + +export const imageDefaultValues: Partial> = { + type: 'image', + strokeWidth: 0, + srcFromAttribute: false, + minimumScaleTrigger: 0.5, + stateProperties: fabricObjectDefaultValues.stateProperties.concat( + 'cropX', + 'cropY' + ), + cacheProperties: fabricObjectDefaultValues.cacheProperties.concat( + 'cropX', + 'cropY' + ), + cropX: 0, + cropY: 0, + imageSmoothing: true, +}; + +Object.assign(Image.prototype, imageDefaultValues); + +fabric.Image = Image; diff --git a/src/util/misc/objectEnlive.ts b/src/util/misc/objectEnlive.ts index cc056d0f575..a67e1ee70db 100644 --- a/src/util/misc/objectEnlive.ts +++ b/src/util/misc/objectEnlive.ts @@ -15,8 +15,14 @@ import { createImage } from './dom'; export const getKlass = (type: string, namespace = fabric): any => namespace[capitalize(camelize(type), true)]; -type LoadImageOptions = { +export type LoadImageOptions = { + /** + * see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + */ signal?: AbortSignal; + /** + * cors value for the image loading, default to anonymous + */ crossOrigin?: TCrossOrigin; }; @@ -24,10 +30,8 @@ type LoadImageOptions = { * Loads image element from given url and resolve it, or catch. * @memberOf fabric.util * @param {String} url URL representing an image - * @param {Object} [options] image loading options - * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous - * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - * @param {Promise} img the loaded image. + * @param {LoadImageOptions} [options] image loading options + * @returns {Promise} the loaded image. */ export const loadImage = ( url: string, @@ -130,7 +134,7 @@ export const enlivenObjectEnlivables = ( serializedObject: any, { signal }: { signal?: AbortSignal } = {} ) => - new Promise((resolve, reject) => { + new Promise>((resolve, reject) => { const instances: any[] = []; signal && signal.addEventListener('abort', reject, { once: true }); // enlive every possible property diff --git a/test/unit/image.js b/test/unit/image.js index 7a1d0234210..ac182b2462b 100644 --- a/test/unit/image.js +++ b/test/unit/image.js @@ -296,7 +296,7 @@ var done = assert.async(); createImageObject(function(image) { assert.ok(typeof image.toString === 'function'); - assert.equal(image.toString(), '#'); + assert.equal(image.toString(), '#'); done(); }); }); @@ -402,7 +402,7 @@ var elImage = _createImageElement(); assert.notEqual(image.getElement(), elImage); - assert.equal(image.setElement(elImage), image, 'chainable'); + image.setElement(elImage); assert.equal(image.getElement(), elImage); assert.equal(image._originalElement, elImage); done(); @@ -421,7 +421,7 @@ }; var elImage = _createImageElement(); fabric.filterBackend.textureCache[image.cacheKey] = 'something'; - assert.equal(image.setElement(elImage), image, 'chainable'); + image.setElement(elImage); assert.equal(fabric.filterBackend.textureCache[image.cacheKey], undefined); fabric.filterBackend = fabricBackend; done();