UNPKG

fabric-pure-browser

Version:

Fabric.js package with no node-specific dependencies (node-canvas, jsdom). The project is published once a day (in case if a new version appears) from 'master' branch of https://github.com/fabricjs/fabric.js repository. You can keep original imports in

747 lines (682 loc) 24.6 kB
(function(global) { 'use strict'; var extend = fabric.util.object.extend; if (!global.fabric) { global.fabric = { }; } if (global.fabric.Image) { fabric.warn('fabric.Image is already defined.'); return; } /** * 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(fabric.Object, /** @lends fabric.Image.prototype */ { /** * Type of an object * @type String * @default */ type: 'image', /** * crossOrigin value (one of "", "anonymous", "use-credentials") * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes * @type String * @default */ crossOrigin: '', /** * 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: fabric.Object.prototype.stateProperties.concat('cropX', 'cropY'), /** * 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, /** * Constructor * @param {HTMLImageElement | String} element Image element * @param {Object} [options] Options object * @param {function} [callback] callback function to call after eventual filters applied. * @return {fabric.Image} thisArg */ initialize: function(element, options) { options || (options = { }); this.filters = []; this.cacheKey = 'texture' + fabric.Object.__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); 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 fiters // 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.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)); }, /** * Sets crossOrigin value (on an instance and corresponding image element) * @return {fabric.Image} thisArg * @chainable */ setCrossOrigin: function(value) { this.crossOrigin = value; this._element.crossOrigin = value; return this; }, /** * 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(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderDashedStroke: function(ctx) { var x = -this.width / 2, y = -this.height / 2, w = this.width, h = this.height; ctx.save(); this._setStrokeStyles(ctx, this); ctx.beginPath(); fabric.util.drawDashedLine(ctx, x, y, x + w, y, this.strokeDashArray); fabric.util.drawDashedLine(ctx, x + w, y, x + w, y + h, this.strokeDashArray); fabric.util.drawDashedLine(ctx, x + w, y + h, x, y + h, this.strokeDashArray); fabric.util.drawDashedLine(ctx, x, y + h, x, y, this.strokeDashArray); ctx.closePath(); ctx.restore(); }, /** * 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', ['crossOrigin', 'cropX', 'cropY'].concat(propertiesToInclude) ), { src: this.getSrc(), 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,hight. * @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, x = -this.width / 2, y = -this.height / 2, clipPath = ''; if (this.hasCrop()) { var clipPathId = fabric.Object.__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 + ')" '; } imageMarkup.push('\t<image ', 'COMMON_PARTS', 'xlink:href="', this.getSvgSrc(true), '" x="', x - this.cropX, '" y="', 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="', this._element.width || this._element.naturalWidth, '" height="', this._element.height || this._element.height, '"', clipPath, '></image>\n'); if (this.stroke || this.strokeDashArray) { var origFill = this.fill; this.fill = null; strokeSvg = [ '\t<rect ', 'x="', x, '" y="', y, '" width="', this.width, '" height="', 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; }, /* _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 || ''; } }, /** * Sets source of an image * @param {String} src Source string (URL) * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) * @param {Object} [options] Options object * @return {fabric.Image} thisArg * @chainable */ setSrc: function(src, callback, options) { fabric.util.loadImage(src, function(img) { this.setElement(img, options); this._setWidthHeight(); callback && callback(this); }, this, options && options.crossOrigin); return this; }, /** * Returns string representation of an instance * @return {String} String representation of an instance */ toString: function() { return '#<fabric.Image: { src: "' + this.getSrc() + '" }>'; }, applyResizeFilters: function() { var filter = this.resizeFilter, minimumScale = this.minimumScaleTrigger, objectScale = this.getTotalObjectScaling(), scaleX = objectScale.scaleX, scaleY = objectScale.scaleY, 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 = fabric.initFilterBackend(); } 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; } 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; } if (!fabric.filterBackend) { fabric.filterBackend = fabric.initFilterBackend(); } 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; } return this; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { this.applyResizeFilters(); } this._stroke(ctx); this._renderPaintInOrder(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, w = this.width, h = this.height, sW = Math.min(elementToDraw.naturalWidth || elementToDraw.width, w * this._filterScalingX), sH = Math.min(elementToDraw.naturalHeight || elementToDraw.height, h * this._filterScalingY), x = -w / 2, y = -h / 2, sX = Math.max(0, this.cropX * this._filterScalingX), sY = Math.max(0, this.cropY * this._filterScalingY); elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, w, h); }, /** * needed to check if image needs resize * @private */ _needsResize: function() { var scale = this.getTotalObjectScaling(); return (scale.scaleX !== this._lastScaleX || scale.scaleY !== 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.util.getById(element), options); fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); }, /** * @private * @param {Object} [options] Options object */ _initConfig: function(options) { options || (options = { }); this.setOptions(options); this._setWidthHeight(options); if (this._element && this.crossOrigin) { this._element.crossOrigin = this.crossOrigin; } }, /** * @private * @param {Array} filters to be initialized * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created */ _initFilters: function(filters, callback) { if (filters && filters.length) { fabric.util.enlivenObjects(filters, function(enlivenedObjects) { callback && callback(enlivenedObjects); }, 'fabric.Image.filters'); } else { callback && callback(); } }, /** * @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; } return { width: rWidth, height: rHeight, scaleX: scaleX, scaleY: scaleY, offsetLeft: offsetLeft, offsetTop: offsetTop, cropX: cropX, cropY: cropY }; } }); /** * Default CSS class name for canvas * @static * @type String * @default */ fabric.Image.CSS_CANVAS = 'canvas-img'; /** * Alias for getSrc * @static */ fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; /** * Creates an instance of fabric.Image from its object representation * @static * @param {Object} object Object to create an instance from * @param {Function} callback Callback to invoke when an image instance is created */ fabric.Image.fromObject = function(_object, callback) { var object = fabric.util.object.clone(_object); fabric.util.loadImage(object.src, function(img, error) { if (error) { callback && callback(null, error); return; } fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { object.filters = filters || []; fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { object.resizeFilter = resizeFilters[0]; fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) { object.clipPath = enlivedProps[0]; var image = new fabric.Image(img, object); callback(image); }); }); }); }, null, object.crossOrigin); }; /** * Creates an instance of fabric.Image from an URL string * @static * @param {String} url URL to create an image from * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument) * @param {Object} [imgOptions] Options object */ fabric.Image.fromURL = function(url, callback, imgOptions) { fabric.util.loadImage(url, function(img) { callback && callback(new fabric.Image(img, imgOptions)); }, null, imgOptions && imgOptions.crossOrigin); }; /* _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} */ fabric.Image.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y width height preserveAspectRatio xlink:href crossOrigin'.split(' ')); /** * Returns {@link fabric.Image} instance from an SVG element * @static * @param {SVGElement} element Element to parse * @param {Object} [options] Options object * @param {Function} callback Callback to execute when fabric.Image object is created * @return {fabric.Image} Instance of fabric.Image */ fabric.Image.fromElement = function(element, callback, options) { var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); }; /* _FROM_SVG_END_ */ })(typeof exports !== 'undefined' ? exports : this);