From 9ed41b8fe51f959ceab1181fd0c9bf30f8e245a2 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 13 Jan 2019 16:04:07 +0100 Subject: [PATCH] Add toCanvasElement for object, fixes a bug, speedUp cloneAsImage (#5481) * a couple of changes * removed unnecessary branching --- src/mixins/canvas_dataurl_exporter.mixin.js | 16 +-- src/shapes/object.class.js | 48 ++++++--- src/static_canvas.class.js | 16 +-- src/util/misc.js | 14 +++ test/unit/canvas.js | 15 +-- test/unit/canvas_static.js | 62 +++++------- test/unit/object.js | 107 +++++++++----------- 7 files changed, 126 insertions(+), 152 deletions(-) diff --git a/src/mixins/canvas_dataurl_exporter.mixin.js b/src/mixins/canvas_dataurl_exporter.mixin.js index e4ad0f706b3..804357f2608 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/object.class.js b/src/shapes/object.class.js index 16774452d5f..190245c06e0 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1544,6 +1544,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" @@ -1559,20 +1560,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 @@ -1583,11 +1580,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); @@ -1613,7 +1611,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, }); @@ -1624,10 +1622,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. @@ -1635,7 +1633,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/static_canvas.class.js b/src/static_canvas.class.js index 12ba7d83cdd..ca041cd766d 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 03902d586d5..73a08fb1e46 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 613bd92b1b1..3315fa3a8b3 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 dbc60c66194..e88e5c1db2d 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/object.js b/test/unit/object.js index ab037dca452..8081a7d0412 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.'); + 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'); } - else { - 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'); - } + 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) {