diff --git a/.travis.yml b/.travis.yml index 326f3a74..86b4e0d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -85,14 +85,14 @@ jobs: script: npm run build:fast && npm run test:visual - stage: Visual Tests env: LAUNCHER=Chrome - install: npm install testem@1.18.4 qunit@2.6.1 + install: npm install testem@1.18.4 qunit@2.6.2 script: npm run build:fast && testem ci --port 8080 -f testem-visual.json -l $LAUNCHER addons: apt: packages: # avoid installing packages - stage: Visual Tests env: LAUNCHER=Firefox - install: npm install testem@1.18.4 qunit@2.6.1 + install: npm install testem@1.18.4 qunit@2.6.2 script: npm run build:fast && testem ci --port 8080 -f testem-visual.json -l $LAUNCHER addons: apt: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7068d957..a14a5d85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.6.0] +- Fix: avoid ie11 to throw on weird draw images [#5428](https://github.com/fabricjs/fabric.js/pull/5428) +- Fix: a rare case of invisible clipPath [#5477](https://github.com/fabricjs/fabric.js/pull/5477) +- Fix: testability of code under node when webgl is involved [#5478](https://github.com/fabricjs/fabric.js/pull/5478) +- Add: Grapeheme text wrapping for Textbox (Textbox.splitByGrapheme) [#5479](https://github.com/fabricjs/fabric.js/pull/5479) +- Add: fabric.Object.toCanvasElement [#5481](https://github.com/fabricjs/fabric.js/pull/5481) + ## [2.5.0] - Fix: textbox transform report newScaleX and newScaleY values [#5464](https://github.com/fabricjs/fabric.js/pull/5464) - Fix: export of svg and gradient with transforms [#5456](https://github.com/fabricjs/fabric.js/pull/5456) diff --git a/HEADER.js b/HEADER.js index 181ae26e..d447dd87 100644 --- a/HEADER.js +++ b/HEADER.js @@ -1,6 +1,6 @@ /*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ -var fabric = fabric || { version: '2.5.0' }; +var fabric = fabric || { version: '2.6.0' }; if (typeof exports !== 'undefined') { exports.fabric = fabric; } diff --git a/package.json b/package.json index 4c7a6713..390f8e5a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fabric", "description": "Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.", "homepage": "http://fabricjs.com/", - "version": "2.5.0", + "version": "2.6.0", "authors": "Juriy Zaytsev ", "contributors": [ { @@ -68,7 +68,7 @@ "eslint": "4.18.x", "istanbul": "0.4.x", "onchange": "^3.x.x", - "qunit": "^2.6.1", + "qunit": "2.6.2", "testem": "^1.18.4", "uglify-js": "3.3.x", "pixelmatch": "^4.0.2", diff --git a/src/filters/webgl_backend.class.js b/src/filters/webgl_backend.class.js index e69484e3..f352b242 100644 --- a/src/filters/webgl_backend.class.js +++ b/src/filters/webgl_backend.class.js @@ -219,51 +219,6 @@ return pipelineState; }, - /** - * The same as the applyFilter method but with additional logging of WebGL - * errors. - */ - applyFiltersDebug: function(filters, source, width, height, targetCanvas, cacheKey) { - // The following code is useful when debugging a specific issue but adds ~10x slowdown. - var gl = this.gl; - var ret = this.applyFilters(filters, source, width, height, targetCanvas, cacheKey); - var glError = gl.getError(); - if (glError !== gl.NO_ERROR) { - var errorString = this.glErrorToString(gl, glError); - var error = new Error('WebGL Error ' + errorString); - error.glErrorCode = glError; - throw error; - } - return ret; - }, - - glErrorToString: function(context, errorCode) { - if (!context) { - return 'Context undefined for error code: ' + errorCode; - } - else if (typeof errorCode !== 'number') { - return 'Error code is not a number'; - } - switch (errorCode) { - case context.NO_ERROR: - return 'NO_ERROR'; - case context.INVALID_ENUM: - return 'INVALID_ENUM'; - case context.INVALID_VALUE: - return 'INVALID_VALUE'; - case context.INVALID_OPERATION: - return 'INVALID_OPERATION'; - case context.INVALID_FRAMEBUFFER_OPERATION: - return 'INVALID_FRAMEBUFFER_OPERATION'; - case context.OUT_OF_MEMORY: - return 'OUT_OF_MEMORY'; - case context.CONTEXT_LOST_WEBGL: - return 'CONTEXT_LOST_WEBGL'; - default: - return 'UNKNOWN_ERROR'; - } - }, - /** * Detach event listeners, remove references, and clean up caches. */ @@ -357,9 +312,11 @@ if (this.gpuInfo) { return this.gpuInfo; } - var gl = this.gl; + var gl = this.gl, gpuInfo = { renderer: '', vendor: '' }; + if (!gl) { + return gpuInfo; + } var ext = gl.getExtension('WEBGL_debug_renderer_info'); - var gpuInfo = { renderer: '', vendor: '' }; if (ext) { var renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); var vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL); diff --git a/src/mixins/canvas_dataurl_exporter.mixin.js b/src/mixins/canvas_dataurl_exporter.mixin.js index e4ad0f70..804357f2 100644 --- a/src/mixins/canvas_dataurl_exporter.mixin.js +++ b/src/mixins/canvas_dataurl_exporter.mixin.js @@ -1,7 +1,4 @@ (function () { - - var supportQuality = fabric.StaticCanvas.supports('toDataURLWithQuality'); - fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { /** @@ -43,7 +40,7 @@ quality = options.quality || 1, multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? this.getRetinaScaling() : 1), canvasEl = this.toCanvasElement(multiplier, options); - return this.__toDataURL(canvasEl, format, quality); + return fabric.util.toDataURL(canvasEl, format, quality); }, /** @@ -93,17 +90,6 @@ this.interactive = originalInteractive; return canvasEl; }, - - /** - * since 2.5.0 does not need to be on canvas instance anymore. - * leave it here for context; - * @private - */ - __toDataURL: function(canvasEl, format, quality) { - return supportQuality - ? canvasEl.toDataURL('image/' + format, quality) - : canvasEl.toDataURL('image/' + format); - } }); })(); diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 6e3af836..5fab5ee1 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -45,6 +45,15 @@ */ 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 @@ -93,7 +102,7 @@ /** * key used to retrieve the texture representing this image - * since 2.0.0 + * @since 2.0.0 * @type String * @default */ @@ -101,7 +110,7 @@ /** * Image crop in pixels from original image size. - * since 2.0.0 + * @since 2.0.0 * @type Number * @default */ @@ -109,7 +118,7 @@ /** * Image crop in pixels from original image size. - * since 2.0.0 + * @since 2.0.0 * @type Number * @default */ @@ -349,7 +358,13 @@ if (element.toDataURL) { return element.toDataURL(); } - return element.src; + + if (this.srcFromAttribute) { + return element.getAttribute('src'); + } + else { + return element.src; + } } else { return this.src || ''; @@ -505,14 +520,15 @@ }, _renderFill: function(ctx) { - var w = this.width, h = this.height, sW = w * this._filterScalingX, sH = h * this._filterScalingY, - x = -w / 2, y = -h / 2, elementToDraw = this._element; - elementToDraw && ctx.drawImage(elementToDraw, - this.cropX * this._filterScalingX, - this.cropY * this._filterScalingY, - sW, - sH, - x, y, w, h); + 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); }, /** diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index da967d3f..fe7c1504 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1187,8 +1187,10 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ drawObject: function(ctx, forClipping) { - + var originalFill = this.fill, originalStroke = this.stroke; if (forClipping) { + this.fill = 'black'; + this.stroke = ''; this._setClippingProperties(ctx); } else { @@ -1198,6 +1200,8 @@ } this._render(ctx); this._drawClipPath(ctx); + this.fill = originalFill; + this.stroke = originalStroke; }, _drawClipPath: function(ctx) { @@ -1558,6 +1562,7 @@ /** * Creates an instance of fabric.Image out of an object + * could make use of both toDataUrl or toCanvasElement. * @param {Function} callback callback, invoked with an instance as a first argument * @param {Object} [options] for clone as image, passed to toDataURL * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" @@ -1573,20 +1578,16 @@ * @return {fabric.Object} thisArg */ cloneAsImage: function(callback, options) { - var dataUrl = this.toDataURL(options); - fabric.util.loadImage(dataUrl, function(img) { - if (callback) { - callback(new fabric.Image(img)); - } - }); + var canvasEl = this.toCanvasElement(options); + if (callback) { + callback(new fabric.Image(canvasEl)); + } return this; }, /** - * Converts an object into a data-url-like string + * Converts an object into a HTMLCanvas element * @param {Object} options Options object - * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" - * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 @@ -1597,11 +1598,12 @@ * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format */ - toDataURL: function(options) { + toCanvasElement: function(options) { options || (options = { }); var utils = fabric.util, origParams = utils.saveObjectTransform(this), - originalShadow = this.shadow, abs = Math.abs; + originalShadow = this.shadow, abs = Math.abs, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? fabric.devicePixelRatio : 1); if (options.withoutTransform) { utils.resetObjectTransform(this); @@ -1627,7 +1629,7 @@ el.width += el.width % 2 ? 2 - el.width % 2 : 0; el.height += el.height % 2 ? 2 - el.height % 2 : 0; var canvas = new fabric.StaticCanvas(el, { - enableRetinaScaling: options.enableRetinaScaling, + enableRetinaScaling: false, renderOnAddRemove: false, skipOffscreen: false, }); @@ -1638,10 +1640,10 @@ var originalCanvas = this.canvas; canvas.add(this); - var data = canvas.toDataURL(options); + var canvasEl = canvas.toCanvasElement(multiplier || 1, options); this.shadow = originalShadow; - this.set(origParams).setCoords(); this.canvas = originalCanvas; + this.set(origParams).setCoords(); // canvas.dispose will call image.dispose that will nullify the elements // since this canvas is a simple element for the process, we remove references // to objects in this way in order to avoid object trashing. @@ -1649,7 +1651,27 @@ canvas.dispose(); canvas = null; - return data; + return canvasEl; + }, + + /** + * Converts an object into a data-url-like string + * @param {Object} options Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + */ + toDataURL: function(options) { + options || (options = { }); + return fabric.util.toDataURL(this.toCanvasElement(options), options.format || 'png', options.quality || 1); }, /** diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js index 72bd787a..34b2a54f 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -64,6 +64,20 @@ */ _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'), + /** + * Use this regular expression to split strings in breakable lines + * @private + */ + _wordJoiners: /[ \t\r\u200B\u200C]/, + + /** + * Use this boolean property in order to split strings that have no white space concept. + * this is a cheap way to help with chinese/japaense + * @type Boolean + * @since 2.6.0 + */ + splitByGrapheme: false, + /** * Unlike superclass's version of this function, Textbox does not update * its width. @@ -300,19 +314,20 @@ * to. */ _wrapLine: function(_line, lineIndex, desiredWidth, reservedSpace) { - var lineWidth = 0, - graphemeLines = [], - line = [], + var lineWidth = 0, + splitByGrapheme = this.splitByGrapheme, + graphemeLines = [], + line = [], // spaces in different languges? - words = _line.split(this._reSpaceAndTab), - word = '', - offset = 0, - infix = ' ', - wordWidth = 0, - infixWidth = 0, + words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners), + word = '', + offset = 0, + infix = splitByGrapheme ? '' : ' ', + wordWidth = 0, + infixWidth = 0, largestWordWidth = 0, lineJustStarted = true, - additionalSpace = this._getWidthOfCharSpacing(), + additionalSpace = splitByGrapheme ? 0 : this._getWidthOfCharSpacing(), reservedSpace = reservedSpace || 0; desiredWidth -= reservedSpace; @@ -406,7 +421,7 @@ * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { - return this.callSuper('toObject', ['minWidth'].concat(propertiesToInclude)); + return this.callSuper('toObject', ['minWidth', 'splitByGrapheme'].concat(propertiesToInclude)); } }); diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 12ba7d83..ca041cd7 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -1795,7 +1795,7 @@ * (either those of HTMLCanvasElement itself, or rendering context) * * @param {String} methodName Method to check support for; - * Could be one of "getImageData", "toDataURL", "toDataURLWithQuality" or "setLineDash" + * Could be one of "setLineDash" * @return {Boolean | null} `true` if method is supported (or at least exists), * `null` if canvas element or context can not be initialized */ @@ -1813,23 +1813,9 @@ switch (methodName) { - case 'getImageData': - return typeof ctx.getImageData !== 'undefined'; - case 'setLineDash': return typeof ctx.setLineDash !== 'undefined'; - case 'toDataURL': - return typeof el.toDataURL !== 'undefined'; - - case 'toDataURLWithQuality': - try { - el.toDataURL('image/jpeg', 0); - return true; - } - catch (e) { } - return false; - default: return null; } diff --git a/src/util/misc.js b/src/util/misc.js index 03902d58..73a08fb1 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -583,6 +583,7 @@ /** * Creates a canvas element that is a copy of another and is also painted + * @param {CanvasElement} canvas to copy size and content of * @static * @memberOf fabric.util * @return {CanvasElement} initialized canvas element @@ -595,6 +596,19 @@ return newCanvas; }, + /** + * since 2.6.0 moved from canvas instance to utility. + * @param {CanvasElement} canvasEl to copy size and content of + * @param {String} format 'jpeg' or 'png', in some browsers 'webp' is ok too + * @param {Number} quality <= 1 and > 0 + * @static + * @memberOf fabric.util + * @return {String} data url + */ + toDataURL: function(canvasEl, format, quality) { + return canvasEl.toDataURL('image/' + format, quality); + }, + /** * Creates image element (works on client and node) * @static diff --git a/test/unit/canvas.js b/test/unit/canvas.js index 613bd92b..3315fa3a 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -1170,16 +1170,11 @@ QUnit.test('toDataURL', function(assert) { assert.ok(typeof canvas.toDataURL === 'function'); - if (!fabric.Canvas.supports('toDataURL')) { - window.alert('toDataURL is not supported by this environment. Some of the tests can not be run.'); - } - else { - var dataURL = canvas.toDataURL(); - // don't compare actual data url, as it is often browser-dependent - // this.assertIdentical(emptyImageCanvasData, canvas.toDataURL('png')); - assert.equal(typeof dataURL, 'string'); - assert.equal(dataURL.substring(0, 21), 'data:image/png;base64'); - } + var dataURL = canvas.toDataURL(); + // don't compare actual data url, as it is often browser-dependent + // this.assertIdentical(emptyImageCanvasData, canvas.toDataURL('png')); + assert.equal(typeof dataURL, 'string'); + assert.equal(dataURL.substring(0, 21), 'data:image/png;base64'); }); // QUnit.test('getPointer', function(assert) { diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index dbc60c66..e88e5c1d 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -482,20 +482,15 @@ QUnit.test('toDataURL', function(assert) { assert.ok(typeof canvas.toDataURL === 'function'); - if (!fabric.Canvas.supports('toDataURL')) { - window.alert('toDataURL is not supported by this environment. Some of the tests can not be run.'); - } - else { - var rect = new fabric.Rect({width: 100, height: 100, fill: 'red', top: 0, left: 0}); - canvas.add(rect); - var dataURL = canvas.toDataURL(); - // don't compare actual data url, as it is often browser-dependent - // this.assertIdentical(emptyImageCanvasData, canvas.toDataURL('png')); - assert.equal(typeof dataURL, 'string'); - assert.equal(dataURL.substring(0, 21), 'data:image/png;base64'); - //we can just compare that the dataUrl generated differs from the dataURl of an empty canvas. - assert.equal(dataURL.substring(200, 210) !== 'AAAAAAAAAA', true); - } + var rect = new fabric.Rect({width: 100, height: 100, fill: 'red', top: 0, left: 0}); + canvas.add(rect); + var dataURL = canvas.toDataURL(); + // don't compare actual data url, as it is often browser-dependent + // this.assertIdentical(emptyImageCanvasData, canvas.toDataURL('png')); + assert.equal(typeof dataURL, 'string'); + assert.equal(dataURL.substring(0, 21), 'data:image/png;base64'); + //we can just compare that the dataUrl generated differs from the dataURl of an empty canvas. + assert.equal(dataURL.substring(200, 210) !== 'AAAAAAAAAA', true); }); QUnit.test('toDataURL with enableRetinaScaling: true and no multiplier', function(assert) { @@ -604,39 +599,28 @@ }); QUnit.test('toDataURL jpeg', function(assert) { - if (!fabric.Canvas.supports('toDataURL')) { - window.alert('toDataURL is not supported by this environment. Some of the tests can not be run.'); + try { + var dataURL = canvas.toDataURL({ format: 'jpeg' }); + assert.equal(dataURL.substring(0, 22), 'data:image/jpeg;base64'); } - else { - try { - var dataURL = canvas.toDataURL({ format: 'jpeg' }); - assert.equal(dataURL.substring(0, 22), 'data:image/jpeg;base64'); - } - // node-canvas does not support jpeg data urls - catch (err) { - assert.ok(true); - } + // node-canvas does not support jpeg data urls + catch (err) { + assert.ok(true); } }); QUnit.test('toDataURL cropping', function(assert) { var done = assert.async(); assert.ok(typeof canvas.toDataURL === 'function'); - if (!fabric.Canvas.supports('toDataURL')) { - window.alert('toDataURL is not supported by this environment. Some of the tests can not be run.'); - done(); - } - else { - var croppingWidth = 75, - croppingHeight = 50, - dataURL = canvas.toDataURL({width: croppingWidth, height: croppingHeight}); + var croppingWidth = 75, + croppingHeight = 50, + dataURL = canvas.toDataURL({width: croppingWidth, height: croppingHeight}); - fabric.Image.fromURL(dataURL, function (img) { - assert.equal(img.width, croppingWidth, 'Width of exported image should correspond to cropping width'); - assert.equal(img.height, croppingHeight, 'Height of exported image should correspond to cropping height'); - done(); - }); - } + fabric.Image.fromURL(dataURL, function (img) { + assert.equal(img.width, croppingWidth, 'Width of exported image should correspond to cropping width'); + assert.equal(img.height, croppingHeight, 'Height of exported image should correspond to cropping height'); + done(); + }); }); QUnit.test('centerObjectH', function(assert) { diff --git a/test/unit/image.js b/test/unit/image.js index ad8248a6..b64b9677 100644 --- a/test/unit/image.js +++ b/test/unit/image.js @@ -25,57 +25,59 @@ } var IMG_SRC = fabric.isLikelyNode ? (__dirname + '/../fixtures/test_image.gif') : getAbsolutePath('../fixtures/test_image.gif'), + IMG_SRC_REL = fabric.isLikelyNode ? (__dirname + '/../fixtures/test_image.gif') : '/fixtures/test_image.gif', IMG_WIDTH = 276, IMG_HEIGHT = 110; var REFERENCE_IMG_OBJECT = { - 'version': fabric.version, - 'type': 'image', - 'originX': 'left', - 'originY': 'top', - 'left': 0, - 'top': 0, - 'width': IMG_WIDTH, // node-canvas doesn't seem to allow setting width/height on image objects - 'height': IMG_HEIGHT, // or does it now? - 'fill': 'rgb(0,0,0)', - 'stroke': null, - 'strokeWidth': 0, - 'strokeDashArray': null, - 'strokeLineCap': 'butt', - 'strokeDashOffset': 0, - 'strokeLineJoin': 'miter', - 'strokeMiterLimit': 4, - 'scaleX': 1, - 'scaleY': 1, - 'angle': 0, - 'flipX': false, - 'flipY': false, - 'opacity': 1, - 'src': IMG_SRC, - 'shadow': null, - 'visible': true, - 'backgroundColor': '', - 'clipTo': null, - 'filters': [], - 'fillRule': 'nonzero', - 'paintFirst': 'fill', - 'globalCompositeOperation': 'source-over', - 'skewX': 0, - 'skewY': 0, - 'transformMatrix': null, - 'crossOrigin': '', - 'cropX': 0, - 'cropY': 0 + version: fabric.version, + type: 'image', + originX: 'left', + originY: 'top', + left: 0, + top: 0, + width: IMG_WIDTH, // node-canvas doesn't seem to allow setting width/height on image objects + height: IMG_HEIGHT, // or does it now? + fill: 'rgb(0,0,0)', + stroke: null, + strokeWidth: 0, + strokeDashArray: null, + strokeLineCap: 'butt', + strokeDashOffset: 0, + strokeLineJoin: 'miter', + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + src: IMG_SRC, + shadow: null, + visible: true, + backgroundColor: '', + clipTo: null, + filters: [], + fillRule: 'nonzero', + paintFirst: 'fill', + globalCompositeOperation: 'source-over', + skewX: 0, + skewY: 0, + transformMatrix: null, + crossOrigin: '', + cropX: 0, + cropY: 0 }; function _createImageElement() { return fabric.document.createElement('img'); } - function _createImageObject(width, height, callback, options) { + function _createImageObject(width, height, callback, options, src) { options = options || {}; + src = src || IMG_SRC; var elImage = _createImageElement(); - setSrc(elImage, IMG_SRC, function() { + setSrc(elImage, src, function() { options.width = width; options.height = height; callback(new fabric.Image(elImage, options)); @@ -90,6 +92,10 @@ return _createImageObject(IMG_WIDTH / 2, IMG_HEIGHT / 2, callback, options); } + function createImageObjectWithSrc(callback, options, src) { + return _createImageObject(IMG_WIDTH, IMG_HEIGHT, callback, options, src); + } + function setSrc(img, src, callback) { img.onload = function() { callback && callback(); @@ -294,6 +300,19 @@ }); }); + QUnit.test('getSrc with srcFromAttribute', function(assert) { + var done = assert.async(); + createImageObjectWithSrc(function(image) { + assert.equal(image.getSrc(), IMG_SRC_REL); + done(); + }, + { + srcFromAttribute: true + }, + IMG_SRC_REL + ); + }); + QUnit.test('getElement', function(assert) { var elImage = _createImageElement(); var image = new fabric.Image(elImage); @@ -780,4 +799,26 @@ done(); }); }); + + QUnit.test('_renderFill respects source boundaries ', function (assert) { + fabric.Image.prototype._renderFill.call({ + cropX: -1, + cropY: -1, + _filterScalingX: 1, + _filterScalingY: 1, + width: 300, + height: 300, + _element: { + naturalWidth: 200, + height: 200, + }, + }, { + drawImage: function(src, sX, sY, sW, sH) { + assert.ok(sX >= 0, 'sX should be positive'); + assert.ok(sY >= 0, 'sY should be positive'); + assert.ok(sW <= 200, 'sW should not be larger than image width'); + assert.ok(sH <= 200, 'sH should not be larger than image height'); + } + }); + }); })(); diff --git a/test/unit/object.js b/test/unit/object.js index ab037dca..8081a7d0 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -410,74 +410,71 @@ QUnit.test('cloneAsImage', function(assert) { var done = assert.async(); var cObj = new fabric.Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0 }); - assert.ok(typeof cObj.cloneAsImage === 'function'); - - if (!fabric.Canvas.supports('toDataURL')) { - fabric.log('`toDataURL` is not supported by this environment; skipping `cloneAsImage` test (as it relies on `toDataURL`)'); + cObj.cloneAsImage(function(image) { + assert.ok(image); + assert.ok(image instanceof fabric.Image); + assert.equal(image.width, 100, 'the image has same dimension of object'); done(); - } - else { - cObj.cloneAsImage(function(image) { - assert.ok(image); - assert.ok(image instanceof fabric.Image); - assert.equal(image.width, 100, 'the image has same dimension of object'); - done(); - }); - } + }); }); QUnit.test('cloneAsImage with retina scaling enabled', function(assert) { var done = assert.async(); var cObj = new fabric.Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0 }); fabric.devicePixelRatio = 2; - if (!fabric.Canvas.supports('toDataURL')) { - fabric.log('`toDataURL` is not supported by this environment; skipping `cloneAsImage` test (as it relies on `toDataURL`)'); + cObj.cloneAsImage(function(image) { + assert.ok(image); + assert.ok(image instanceof fabric.Image); + assert.equal(image.width, 200, 'the image has been scaled by retina'); + fabric.devicePixelRatio = 1; done(); - } - else { - cObj.cloneAsImage(function(image) { - assert.ok(image); - assert.ok(image instanceof fabric.Image); - assert.equal(image.width, 200, 'the image has been scaled by retina'); - fabric.devicePixelRatio = 1; - done(); - }, { enableRetinaScaling: true }); - } + }, { enableRetinaScaling: true }); }); - QUnit.test('toDataURL', function(assert) { - // var data = - // 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQA'+ - // 'AABkCAYAAABw4pVUAAAA+UlEQVR4nO3RoRHAQBDEsOu/6YR+B2s'+ - // 'gIO4Z3919pMwDMCRtHoAhafMADEmbB2BI2jwAQ9LmARiSNg/AkLR5AI'+ - // 'akzQMwJG0egCFp8wAMSZsHYEjaPABD0uYBGJI2D8CQtHkAhqTNAzAkbR'+ - // '6AIWnzAAxJmwdgSNo8AEPS5gEYkjYPwJC0eQCGpM0DMCRtHoAhafMADEm'+ - // 'bB2BI2jwAQ9LmARiSNg/AkLR5AIakzQMwJG0egCFp8wAMSZsHYEjaPABD0'+ - // 'uYBGJI2D8CQtHkAhqTNAzAkbR6AIWnzAAxJmwdgSNo8AEPS5gEYkjYPw'+ - // 'JC0eQCGpM0DMCRtHsDjB5K06yueJFXJAAAAAElFTkSuQmCC'; + QUnit.test('toCanvasElement', function(assert) { + var cObj = new fabric.Rect({ + width: 100, height: 100, fill: 'red', strokeWidth: 0 + }); + assert.ok(typeof cObj.toCanvasElement === 'function'); + + var canvasEl = cObj.toCanvasElement(); + + assert.ok(typeof canvasEl.getContext === 'function', 'the element returned is a canvas'); + }); + + QUnit.test('toCanvasElement does not modify oCoords on zoomed canvas', function(assert) { + var cObj = new fabric.Rect({ + width: 100, height: 100, fill: 'red', strokeWidth: 0 + }); + canvas.setZoom(2); + canvas.add(cObj); + var originaloCoords = cObj.oCoords; + var originalaCoords = cObj.aCoords; + cObj.toCanvasElement(); + assert.deepEqual(cObj.oCoords, originaloCoords, 'cObj did not get object coords changed'); + assert.deepEqual(cObj.aCoords, originalaCoords, 'cObj did not get absolute coords changed'); + }); + + + QUnit.test('toDataURL', function(assert) { var cObj = new fabric.Rect({ width: 100, height: 100, fill: 'red', strokeWidth: 0 }); assert.ok(typeof cObj.toDataURL === 'function'); - if (!fabric.Canvas.supports('toDataURL')) { - window.alert('toDataURL is not supported by this environment. Some of the tests can not be run.'); - } - else { - var dataURL = cObj.toDataURL(); - assert.equal(typeof dataURL, 'string'); - assert.equal(dataURL.substring(0, 21), 'data:image/png;base64'); + var dataURL = cObj.toDataURL(); + assert.equal(typeof dataURL, 'string'); + assert.equal(dataURL.substring(0, 21), 'data:image/png;base64'); - try { - dataURL = cObj.toDataURL({ format: 'jpeg' }); - assert.equal(dataURL.substring(0, 22), 'data:image/jpeg;base64'); - } - catch (err) { - fabric.log('jpeg toDataURL not supported'); - } + try { + dataURL = cObj.toDataURL({ format: 'jpeg' }); + assert.equal(dataURL.substring(0, 22), 'data:image/jpeg;base64'); + } + catch (err) { + fabric.log('jpeg toDataURL not supported'); } }); @@ -496,16 +493,10 @@ width: 100, height: 100, fill: 'red' }); canvas.add(cObj); + var objCanvas = cObj.canvas; + cObj.toDataURL(); - if (!fabric.Canvas.supports('toDataURL')) { - window.alert('toDataURL is not supported by this environment. Some of the tests can not be run.'); - } - else { - var objCanvas = cObj.canvas; - cObj.toDataURL(); - - assert.equal(objCanvas, cObj.canvas); - } + assert.equal(objCanvas, cObj.canvas); }); QUnit.test('isType', function(assert) { diff --git a/test/unit/textbox.js b/test/unit/textbox.js index 96319083..9a60129e 100644 --- a/test/unit/textbox.js +++ b/test/unit/textbox.js @@ -31,17 +31,17 @@ opacity: 1, shadow: null, visible: true, - clipTo: null, - text: 'x', - fontSize: 40, - fontWeight: 'normal', - fontFamily: 'Times New Roman', - fontStyle: 'normal', - lineHeight: 1.16, - underline: false, - overline: false, - linethrough: false, - textAlign: 'left', + clipTo: null, + text: 'x', + fontSize: 40, + fontWeight: 'normal', + fontFamily: 'Times New Roman', + fontStyle: 'normal', + lineHeight: 1.16, + underline: false, + overline: false, + linethrough: false, + textAlign: 'left', backgroundColor: '', textBackgroundColor: '', fillRule: 'nonzero', @@ -52,7 +52,8 @@ transformMatrix: null, charSpacing: 0, styles: { }, - minWidth: 20 + minWidth: 20, + splitByGrapheme: false, }; QUnit.test('constructor', function(assert) { @@ -129,6 +130,30 @@ textbox.initDimensions(); assert.equal(textbox.textLines[0], 'xa', 'first line match expectations spacing 800'); }); + QUnit.test('wrapping with different things', function(assert) { + var textbox = new fabric.Textbox('xa\u200Bxb\u200Bxc\u200Cxd\u200Cxe ya yb id', { + width: 16, + }); + assert.equal(textbox.textLines[0], 'xa', '0 line match expectations'); + assert.equal(textbox.textLines[1], 'xb', '1 line match expectations'); + assert.equal(textbox.textLines[2], 'xc', '2 line match expectations'); + assert.equal(textbox.textLines[3], 'xd', '3 line match expectations'); + assert.equal(textbox.textLines[4], 'xe', '4 line match expectations'); + assert.equal(textbox.textLines[5], 'ya', '5 line match expectations'); + assert.equal(textbox.textLines[6], 'yb', '6 line match expectations'); + }); + QUnit.test('wrapping with splitByGrapheme', function(assert) { + var textbox = new fabric.Textbox('xaxbxcxdxeyaybid', { + width: 1, + splitByGrapheme: true, + }); + assert.equal(textbox.textLines[0], 'x', '0 line match expectations splitByGrapheme'); + assert.equal(textbox.textLines[1], 'a', '1 line match expectations splitByGrapheme'); + assert.equal(textbox.textLines[2], 'x', '2 line match expectations splitByGrapheme'); + assert.equal(textbox.textLines[3], 'b', '3 line match expectations splitByGrapheme'); + assert.equal(textbox.textLines[4], 'x', '4 line match expectations splitByGrapheme'); + assert.equal(textbox.textLines[5], 'c', '5 line match expectations splitByGrapheme'); + }); QUnit.test('wrapping with custom space', function(assert) { var textbox = new fabric.Textbox('xa xb xc xd xe ya yb id', { width: 2000, diff --git a/test/visual/clippath.js b/test/visual/clippath.js index 4945573a..17e7e309 100644 --- a/test/visual/clippath.js +++ b/test/visual/clippath.js @@ -48,6 +48,22 @@ percentage: 0.06, }); + function clipping02(canvas, callback) { + var clipPath = new fabric.Circle({ radius: 50, strokeWidth: 40, top: -50, left: -50, fill: '' }); + var obj = new fabric.Rect({ top: 0, left: 0, strokeWidth: 0, width: 200, height: 200, fill: 'rgba(0,255,0,0.5)'}); + obj.clipPath = clipPath; + canvas.add(obj); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'falsy values for fill are handled', + code: clipping02, + golden: 'clipping01.png', + percentage: 0.06, + }); + function clipping1(canvas, callback) { var zoom = 20; canvas.setZoom(zoom);