fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
467 lines (466 loc) • 17.3 kB
JavaScript
import { _defineProperty } from "../../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs";
import { log } from "../util/internals/console.mjs";
import { getEnv, getFabricDocument } from "../env/index.mjs";
import "../constants.mjs";
import { classRegistry } from "../ClassRegistry.mjs";
import { uid } from "../util/internals/uid.mjs";
import { createCanvasElementFor } from "../util/misc/dom.mjs";
import { enlivenObjectEnlivables, enlivenObjects, loadImage } from "../util/misc/objectEnlive.mjs";
import { getDocumentFromElement } from "../util/dom_misc.mjs";
import { escapeXml } from "../util/lang_string.mjs";
import { parsePreserveAspectRatioAttribute } from "../util/misc/svgParsing.mjs";
import { cacheProperties } from "./Object/defaultValues.mjs";
import { FabricObject } from "./Object/FabricObject.mjs";
import { SHARED_ATTRIBUTES } from "../parser/attributes.mjs";
import { parseAttributes } from "../parser/parseAttributes.mjs";
import { findScaleToCover, findScaleToFit } from "../util/misc/findScaleTo.mjs";
import { WebGLFilterBackend } from "../filters/WebGLFilterBackend.mjs";
import { getFilterBackend } from "../filters/FilterBackend.mjs";
//#region src/shapes/Image.ts
const imageDefaultValues = {
strokeWidth: 0,
srcFromAttribute: false,
minimumScaleTrigger: .5,
cropX: 0,
cropY: 0,
imageSmoothing: true
};
const IMAGE_PROPS = ["cropX", "cropY"];
/**
* @see {@link http://fabric5.fabricjs.com/fabric-intro-part-1#images}
*/
var FabricImage = class FabricImage extends FabricObject {
static getDefaults() {
return {
...super.getDefaults(),
...FabricImage.ownDefaults
};
}
constructor(arg0, options) {
super();
_defineProperty(this, "_lastScaleX", 1);
_defineProperty(this, "_lastScaleY", 1);
_defineProperty(this, "_filterScalingX", 1);
_defineProperty(this, "_filterScalingY", 1);
this.filters = [];
Object.assign(this, FabricImage.ownDefaults);
this.setOptions(options);
this.cacheKey = `texture${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, size = {}) {
this.removeTexture(this.cacheKey);
this.removeTexture(`${this.cacheKey}_filtered`);
this._element = element;
this._originalElement = element;
this._setWidthHeight(size);
if (this.filters.length !== 0) this.applyFilters();
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(`${this.cacheKey}_filtered`);
this._cacheContext = null;
[
"_originalElement",
"_element",
"_filteredEl",
"_cacheCanvas"
].forEach((elementKey) => {
const el = this[elementKey];
el && getEnv().dispose(el);
this[elementKey] = void 0;
});
}
/**
* 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(propertiesToInclude = []) {
const filters = [];
this.filters.forEach((filterObj) => {
filterObj && filters.push(filterObj.toObject());
});
return {
...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", " <rect x=\"" + x + "\" y=\"" + y + "\" width=\"" + escapeXml(this.width) + "\" height=\"" + escapeXml(this.height) + "\" />\n", "</clipPath>\n");
clipPath = " clip-path=\"url(#imageCrop_" + clipPathId + ")\" ";
}
if (!this.imageSmoothing) imageRendering = " image-rendering=\"optimizeSpeed\"";
imageMarkup.push(" <image ", "COMMON_PARTS", `xlink:href="${escapeXml(this.getSrc(true))}" x="${x - this.cropX}" y="${y - this.cropY}" width="${element.width || element.naturalWidth}" height="${element.height || element.naturalHeight}"${imageRendering}${clipPath}></image>\n`);
if (this.stroke || this.strokeDashArray) {
const origFill = this.fill;
this.fill = null;
strokeSvg = [`\t<rect x="${x}" y="${y}" width="${escapeXml(this.width)}" height="${escapeXml(this.height)}" style="${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, { crossOrigin, signal } = {}) {
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: "${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
* @param {Array} filters to be applied
* @param {Boolean} forResizing specify if the filter operation is a resize operation
*/
applyFilters(filters = this.filters || []) {
filters = filters.filter((filter) => filter && !filter.isNeutralState());
this.set("dirty", true);
this.removeTexture(`${this.cacheKey}_filtered`);
if (filters.length === 0) {
this._element = this._originalElement;
this._filteredEl = void 0;
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) {
const canvasEl = createCanvasElementFor({
width: sourceWidth,
height: sourceHeight
});
this._element = canvasEl;
this._filteredEl = canvasEl;
} else if (this._filteredEl) {
this._element = this._filteredEl;
this._filteredEl.getContext("2d").clearRect(0, 0, sourceWidth, sourceHeight);
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, 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, 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 } = {}) {
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
};
}
/**
* Creates an instance of FabricImage from its object representation
* @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({ filters: f, resizeFilter: rf, src, crossOrigin, type, ...object }, options) {
return Promise.all([
loadImage(src, {
...options,
crossOrigin
}),
f && enlivenObjects(f, options),
rf ? enlivenObjects([rf], options) : [],
enlivenObjectEnlivables(object, options)
]).then(([el, filters = [], [resizeFilter], hydratedProps = {}]) => {
return new this(el, {
...object,
src,
filters,
resizeFilter,
...hydratedProps
});
});
}
/**
* Creates an instance of Image from an URL string
* @param {String} url URL to create an image from
* @param {LoadImageOptions} [options] Options object
* @returns {Promise<FabricImage>}
*/
static fromURL(url, { crossOrigin = null, signal } = {}, imageOptions) {
return loadImage(url, {
crossOrigin,
signal
}).then((img) => new this(img, imageOptions));
}
/**
* Returns {@link FabricImage} instance from an SVG element
* @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, options = {}, cssRules) {
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, "ATTRIBUTE_NAMES", [
...SHARED_ATTRIBUTES,
"x",
"y",
"width",
"height",
"preserveAspectRatio",
"xlink:href",
"href",
"crossOrigin",
"image-rendering"
]);
classRegistry.setClass(FabricImage);
classRegistry.setSVGClass(FabricImage);
//#endregion
export { FabricImage };
//# sourceMappingURL=Image.mjs.map