fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
672 lines (638 loc) • 23.2 kB
JavaScript
import { defineProperty as _defineProperty, objectSpread2 as _objectSpread2, objectWithoutProperties as _objectWithoutProperties } from '../../_virtual/_rollupPluginBabelHelpers.mjs';
import { getFabricDocument, getEnv } from '../env/index.mjs';
import { getFilterBackend } from '../filters/FilterBackend.mjs';
import { SHARED_ATTRIBUTES } from '../parser/attributes.mjs';
import { parseAttributes } from '../parser/parseAttributes.mjs';
import { uid } from '../util/internals/uid.mjs';
import { createCanvasElementFor } from '../util/misc/dom.mjs';
import { findScaleToFit, findScaleToCover } from '../util/misc/findScaleTo.mjs';
import { loadImage, enlivenObjects, enlivenObjectEnlivables } from '../util/misc/objectEnlive.mjs';
import { parsePreserveAspectRatioAttribute } from '../util/misc/svgParsing.mjs';
import { classRegistry } from '../ClassRegistry.mjs';
import { FabricObject } from './Object/FabricObject.mjs';
import { WebGLFilterBackend } from '../filters/WebGLFilterBackend.mjs';
import { FILL, NONE } from '../constants.mjs';
import { getDocumentFromElement } from '../util/dom_misc.mjs';
import { log } from '../util/internals/console.mjs';
import { cacheProperties } from './Object/defaultValues.mjs';
const _excluded = ["filters", "resizeFilter", "src", "crossOrigin", "type"];
// @todo Would be nice to have filtering code not imported directly.
const imageDefaultValues = {
strokeWidth: 0,
srcFromAttribute: false,
minimumScaleTrigger: 0.5,
cropX: 0,
cropY: 0,
imageSmoothing: true
};
const IMAGE_PROPS = ['cropX', 'cropY'];
/**
* @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images}
*/
class FabricImage extends FabricObject {
static getDefaults() {
return _objectSpread2(_objectSpread2({}, super.getDefaults()), FabricImage.ownDefaults);
}
/**
* 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(arg0, options) {
super();
/**
* When calling {@link FabricImage.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 false
*/
/**
* private
* contains last value of scaleX to detect
* if the Image got resized after the last Render
* @type Number
*/
_defineProperty(this, "_lastScaleX", 1);
/**
* private
* contains last value of scaleY to detect
* if the Image got resized after the last Render
* @type Number
*/
_defineProperty(this, "_lastScaleY", 1);
/**
* private
* contains last value of scaling applied by the apply filter chain
* @type Number
*/
_defineProperty(this, "_filterScalingX", 1);
/**
* private
* contains last value of scaling applied by the apply filter chain
* @type Number
*/
_defineProperty(this, "_filterScalingY", 1);
this.filters = [];
Object.assign(this, FabricImage.ownDefaults);
this.setOptions(options);
this.cacheKey = "texture".concat(uid());
this.setElement(typeof arg0 === 'string' ? (this.canvas && getDocumentFromElement(this.canvas.getElement()) || getFabricDocument()).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<TSize>} [size] Options object
*/
setElement(element) {
var _element$classList;
let size = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.removeTexture(this.cacheKey);
this.removeTexture("".concat(this.cacheKey, "_filtered"));
this._element = element;
this._originalElement = element;
this._setWidthHeight(size);
(_element$classList = element.classList) === null || _element$classList === void 0 || _element$classList.add(FabricImage.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) {
const backend = getFilterBackend(false);
if (backend instanceof WebGLFilterBackend) {
backend.evictCachesForKey(key);
}
}
/**
* Delete textures, reference to elements and eventually JSDOM cleanup
*/
dispose() {
super.dispose();
this.removeTexture(this.cacheKey);
this.removeTexture("".concat(this.cacheKey, "_filtered"));
this._cacheContext = null;
['_originalElement', '_element', '_filteredEl', '_cacheCanvas'].forEach(elementKey => {
const el = this[elementKey];
el && getEnv().dispose(el);
// @ts-expect-error disposing
this[elementKey] = undefined;
});
}
/**
* Get the crossOrigin value (of the corresponding image element)
*/
getCrossOrigin() {
return this._originalElement && (this._originalElement.crossOrigin || null);
}
/**
* Returns original size of an image
*/
getOriginalSize() {
const element = this.getElement();
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) {
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() {
let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
const filters = [];
this.filters.forEach(filterObj => {
filterObj && filters.push(filterObj.toObject());
});
return _objectSpread2(_objectSpread2({}, super.toObject([...IMAGE_PROPS, ...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 = uid();
svgString.push('<clipPath id="imageCrop_' + clipPathId + '">\n', '\t<rect x="' + x + '" y="' + y + '" width="' + this.width + '" height="' + this.height + '" />\n', '</clipPath>\n');
clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" ';
}
if (!this.imageSmoothing) {
imageRendering = ' image-rendering="optimizeSpeed"';
}
imageMarkup.push('\t<image ', 'COMMON_PARTS', "xlink:href=\"".concat(this.getSvgSrc(true), "\" x=\"").concat(x - this.cropX, "\" y=\"").concat(y - this.cropY
// we're essentially moving origin of transformation from top/left corner to the center of the shape
// by wrapping it in container <g> element with actual transformation, then offsetting object to the top/left
// so that object's center aligns with container's left/top
, "\" width=\"").concat(element.width || element.naturalWidth, "\" height=\"").concat(element.height || element.naturalHeight, "\"").concat(imageRendering).concat(clipPath, "></image>\n"));
if (this.stroke || this.strokeDashArray) {
const origFill = this.fill;
this.fill = null;
strokeSvg = ["\t<rect x=\"".concat(x, "\" y=\"").concat(y, "\" width=\"").concat(this.width, "\" height=\"").concat(this.height, "\" style=\"").concat(this.getSvgStyles(), "\" />\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) {
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) {
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) {
let {
crossOrigin,
signal
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
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 "#<Image: { src: \"".concat(this.getSrc(), "\" }>");
}
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 = createCanvasElementFor(elementToFilter),
{
width,
height
} = elementToFilter;
this._element = canvasEl;
this._lastScaleX = filter.scaleX = scaleX;
this._lastScaleY = filter.scaleY = scaleY;
getFilterBackend().applyFilters([filter], elementToFilter, width, height, 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() {
let filters = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 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("".concat(this.cacheKey, "_filtered"));
if (filters.length === 0) {
this._element = this._originalElement;
// this is unsafe and needs to be rethinkend
this._filteredEl = undefined;
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 a reference to _originalElement
// we need to create a new element to host the filtered pixels
const canvasEl = createCanvasElementFor({
width: sourceWidth,
height: sourceHeight
});
this._element = canvasEl;
this._filteredEl = canvasEl;
} else if (this._filteredEl) {
// if the _element is it own element,
// and we also have a _filteredEl, then we clean up _filteredEl
// and we assign it to _element.
// in this way we invalidate the eventual old resize filtered 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, 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;
}
}
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render(ctx) {
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) {
ctx.imageSmoothingEnabled = this.imageSmoothing;
super.drawCacheOnCanvas(ctx);
}
/**
* Decide if the FabricImage should cache or not. Create its own cache level
* needsItsOwnCache should be used when the object drawing method requires
* a cache step.
* 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) {
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() {
let {
width,
height
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
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;
}
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 = 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;
}
return {
width: rWidth,
height: rHeight,
scaleX,
scaleY,
offsetLeft,
offsetTop,
cropX,
cropY
};
}
/**
* Default CSS class name for canvas
* Will be removed from fabric 7
* @static
* @deprecated
* @type String
* @default
*/
/**
* Creates an instance of FabricImage 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<FabricImage>}
*/
static fromObject(_ref, options) {
let {
filters: f,
resizeFilter: rf,
src,
crossOrigin,
type
} = _ref,
object = _objectWithoutProperties(_ref, _excluded);
return Promise.all([loadImage(src, _objectSpread2(_objectSpread2({}, options), {}, {
crossOrigin
})), f && enlivenObjects(f, options),
// TODO: redundant - handled by enlivenObjectEnlivables
rf && enlivenObjects([rf], options), enlivenObjectEnlivables(object, options)]).then(_ref2 => {
let [el, filters = [], [resizeFilter] = [], hydratedProps = {}] = _ref2;
return new this(el, _objectSpread2(_objectSpread2({}, object), {}, {
// TODO: this creates a difference between image creation and restoring from JSON
src,
filters,
resizeFilter
}, hydratedProps));
});
}
/**
* Creates an instance of Image from an URL string
* @static
* @param {String} url URL to create an image from
* @param {LoadImageOptions} [options] Options object
* @returns {Promise<FabricImage>}
*/
static fromURL(url) {
let {
crossOrigin = null,
signal
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
let imageOptions = arguments.length > 2 ? arguments[2] : undefined;
return loadImage(url, {
crossOrigin,
signal
}).then(img => new this(img, imageOptions));
}
/**
* Returns {@link FabricImage} instance from an SVG element
* @static
* @param {HTMLElement} 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 Image object is created
*/
static async fromElement(element) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
let cssRules = arguments.length > 2 ? arguments[2] : undefined;
const parsedAttributes = parseAttributes(element, this.ATTRIBUTE_NAMES, cssRules);
return this.fromURL(parsedAttributes['xlink:href'] || parsedAttributes['href'], options, parsedAttributes).catch(err => {
log('log', 'Unable to parse Image', err);
return null;
});
}
}
_defineProperty(FabricImage, "type", 'Image');
_defineProperty(FabricImage, "cacheProperties", [...cacheProperties, ...IMAGE_PROPS]);
_defineProperty(FabricImage, "ownDefaults", imageDefaultValues);
_defineProperty(FabricImage, "CSS_CANVAS", 'canvas-img');
/**
* List of attribute names to account for when parsing SVG element (used by {@link FabricImage.fromElement})
* @static
* @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement}
*/
_defineProperty(FabricImage, "ATTRIBUTE_NAMES", [...SHARED_ATTRIBUTES, 'x', 'y', 'width', 'height', 'preserveAspectRatio', 'xlink:href', 'href', 'crossOrigin', 'image-rendering']);
classRegistry.setClass(FabricImage);
classRegistry.setSVGClass(FabricImage);
export { FabricImage, imageDefaultValues };
//# sourceMappingURL=Image.mjs.map