diff --git a/src/filters/resize_filter.class.js b/src/filters/resize_filter.class.js index 67f53555..f494c0e3 100644 --- a/src/filters/resize_filter.class.js +++ b/src/filters/resize_filter.class.js @@ -30,6 +30,8 @@ /** * Resize type + * for webgl resizyType is just lanczos, for canvas2d can be: + * bilinear, hermite, sliceHacl, lanczos. * @param {String} resizeType * @default */ @@ -50,7 +52,7 @@ scaleY: 0, /** - * LanczosLobes parameter for lanczos filter + * LanczosLobes parameter for lanczos filter, valid for resizeType lanczos * @param {Number} lanczosLobes * @default */ diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 7d39e63d..c725bdcd 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -385,10 +385,10 @@ applyResizeFilters: function() { var filter = this.resizeFilter, - retinaScaling = this.canvas ? this.canvas.getRetinaScaling() : 1, minimumScale = this.minimumScaleTrigger, - scaleX = this.scaleX * retinaScaling, - scaleY = this.scaleY * retinaScaling, + objectScale = this.getTotalObjectScaling(), + scaleX = objectScale.scaleX, + scaleY = objectScale.scaleY, elementToFilter = this._filteredEl || this._originalElement; if (this.group) { this.set('dirty', true); @@ -397,6 +397,8 @@ this._element = elementToFilter; this._filterScalingX = 1; this._filterScalingY = 1; + this._lastScaleX = 1; + this._lastScaleY = 1; return; } if (!fabric.filterBackend) { @@ -408,8 +410,8 @@ canvasEl.width = sourceWidth; canvasEl.height = sourceHeight; this._element = canvasEl; - filter.scaleX = scaleX; - filter.scaleY = scaleY; + 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; @@ -473,9 +475,7 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { - if (this.isMoving === false && this.resizeFilter && this._needsResize()) { - this._lastScaleX = this.scaleX; - this._lastScaleY = this.scaleY; + if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { this.applyResizeFilters(); } this._stroke(ctx); @@ -497,7 +497,8 @@ * @private, needed to check if image needs resize */ _needsResize: function() { - return (this.scaleX !== this._lastScaleX || this.scaleY !== this._lastScaleY); + var scale = this.getTotalObjectScaling(); + return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); }, /** diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 761032df..8550b09b 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -678,12 +678,10 @@ * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache */ _getCacheCanvasDimensions: function() { - var zoom = this.canvas && this.canvas.getZoom() || 1, - objectScale = this.getObjectScaling(), - retina = this.canvas && this.canvas._isRetinaScaling() ? fabric.devicePixelRatio : 1, + var objectScale = this.getTotalObjectScaling(), dim = this._getNonTransformedDimensions(), - zoomX = objectScale.scaleX * zoom * retina, - zoomY = objectScale.scaleY * zoom * retina, + zoomX = objectScale.scaleX, + zoomY = objectScale.scaleY, width = dim.x * zoomX, height = dim.y * zoomY; return { @@ -890,6 +888,21 @@ return { scaleX: scaleX, scaleY: scaleY }; }, + /** + * Return the object scale factor counting also the group scaling, zoom and retina + * @return {Object} object with scaleX and scaleY properties + */ + getTotalObjectScaling: function() { + var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; + if (this.canvas) { + var zoom = this.canvas.getZoom(); + var retina = this.canvas.getRetinaScaling(); + scaleX *= zoom * retina; + scaleY *= zoom * retina; + } + return { scaleX: scaleX, scaleY: scaleY }; + }, + /** * Return the object opacity counting also the group property * @return {Number} diff --git a/test/fixtures/parrot.png b/test/fixtures/parrot.png new file mode 100644 index 00000000..eb9ecac7 Binary files /dev/null and b/test/fixtures/parrot.png differ diff --git a/test/unit/object.js b/test/unit/object.js index 9df3028f..cd3ddb10 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -52,6 +52,9 @@ fabric.perfLimitSizeTotal = 2097152; fabric.maxCacheSideLimit = 4096; fabric.minCacheSideLimit = 256; + fabric.devicePixelRatio = 1; + canvas.enableRetinaScaling = false; + canvas.setZoom(1); canvas.clear(); canvas.backgroundColor = fabric.Canvas.prototype.backgroundColor; canvas.calcOffset(); @@ -989,13 +992,30 @@ assert.equal(typeof deserializedObject.clipTo, 'function'); }); - QUnit.test('getObjectScale', function(assert) { + QUnit.test('getTotalObjectScaling with zoom', function(assert) { + var object = new fabric.Object({ scaleX: 3, scaleY: 2}); + canvas.setZoom(3); + canvas.add(object); + var objectScale = object.getTotalObjectScaling(); + assert.deepEqual(objectScale, { scaleX: object.scaleX * 3, scaleY: object.scaleY * 3 }); + }); + + QUnit.test('getTotalObjectScaling with retina', function(assert) { + var object = new fabric.Object({ scaleX: 3, scaleY: 2}); + canvas.enableRetinaScaling = true; + fabric.devicePixelRatio = 4; + canvas.add(object); + var objectScale = object.getTotalObjectScaling(); + assert.deepEqual(objectScale, { scaleX: object.scaleX * 4, scaleY: object.scaleY * 4 }); + }); + + QUnit.test('getObjectScaling', function(assert) { var object = new fabric.Object({ scaleX: 3, scaleY: 2}); var objectScale = object.getObjectScaling(); assert.deepEqual(objectScale, {scaleX: object.scaleX, scaleY: object.scaleY}); }); - QUnit.test('getObjectScale in group', function(assert) { + QUnit.test('getObjectScaling in group', function(assert) { var object = new fabric.Object({ scaleX: 3, scaleY: 2}); var group = new fabric.Group(); group.scaleX = 2; diff --git a/test/visual/golden/parrot.png b/test/visual/golden/parrot.png new file mode 100644 index 00000000..709753c2 Binary files /dev/null and b/test/visual/golden/parrot.png differ diff --git a/test/visual/resize_filter.js b/test/visual/resize_filter.js new file mode 100644 index 00000000..eb1e3840 --- /dev/null +++ b/test/visual/resize_filter.js @@ -0,0 +1,173 @@ +(function() { + fabric.enableGLFiltering = false; + var _pixelMatch = pixelmatch; + if (fabric.isLikelyNode) { + var fs = global.fs; + _pixelMatch = global.pixelmatch; + } + var fabricCanvas = this.canvas = new fabric.Canvas(null, {enableRetinaScaling: false, renderOnAddRemove: false}); + var pixelmatchOptions = { + includeAA: false, + threshold: 0.095 + }; + fabric.Object.prototype.objectCaching = false; + + function getAbsolutePath(path) { + var isAbsolute = /^https?:/.test(path); + if (isAbsolute) { return path; }; + var imgEl = fabric.document.createElement('img'); + imgEl.src = path; + var src = imgEl.src; + imgEl = null; + return src; + } + + function getFixtureName(filename) { + var finalName = '/../fixtures/' + filename; + return fabric.isLikelyNode ? (__dirname + finalName) : getAbsolutePath('test/fixtures/' + filename); + } + + function getGoldeName(filename) { + var finalName = '/golden/' + filename; + return fabric.isLikelyNode ? (__dirname + finalName) : getAbsolutePath('test/visual/golden/' + filename); + } + + function getImage(filename, original, callback) { + if (fabric.isLikelyNode && original) { + try { + fs.statSync(filename); + } + catch (err) { + var dataUrl = original.toDataURL().split(',')[1]; + console.log('creating original for ', filename); + fs.writeFileSync(filename, dataUrl, { encoding: 'base64' }); + } + } + var img = fabric.document.createElement('img'); + img.onload = function() { + callback(img); + }; + img.onerror = function(err) { + console.log('Image loading errored', err); + }; + img.src = filename; + } + + QUnit.module('Image resize filter test', { + afterEach: function() { + fabricCanvas.setZoom(1); + fabricCanvas.setDimensions({ + width: 300, + height: 150, + }); + fabricCanvas.clear(); + fabricCanvas.renderAll(); + } + }); + + var tests = []; + + function imageResizeTest(canvas, callback) { + getImage(getFixtureName('parrot.png'), false, function(img) { + canvas.setDimensions({ + width: 200, + height: 200, + }); + var zoom = 8; + var image = new fabric.Image(img); + image.resizeFilter = new fabric.Image.filters.Resize({ resizeType: 'lanczos' }); + canvas.setZoom(zoom); + image.scaleToWidth(canvas.width / zoom); + canvas.add(image); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + }); + } + + tests.push({ + test: 'Image resize with canvas zoom', + code: imageResizeTest, + golden: 'parrot.png', + percentage: 0.06, + }); + + function imageResizeTestNoZoom(canvas, callback) { + getImage(getFixtureName('parrot.png'), false, function(img) { + canvas.setDimensions({ + width: 200, + height: 200, + }); + var image = new fabric.Image(img); + image.resizeFilter = new fabric.Image.filters.Resize({ resizeType: 'lanczos' }); + image.scaleToWidth(canvas.width); + canvas.add(image); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + }); + } + + tests.push({ + test: 'Image resize without zoom', + code: imageResizeTestNoZoom, + golden: 'parrot.png', + percentage: 0.06, + }); + + function imageResizeTestGroup(canvas, callback) { + getImage(getFixtureName('parrot.png'), false, function(img) { + canvas.setDimensions({ + width: 200, + height: 200, + }); + var image = new fabric.Image(img, { strokeWidth: 0 }); + image.resizeFilter = new fabric.Image.filters.Resize({ resizeType: 'lanczos' }); + var group = new fabric.Group([image]); + group.strokeWidth = 0; + group.scaleToWidth(canvas.width); + canvas.add(group); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + }); + } + + tests.push({ + test: 'Image resize with scaled group', + code: imageResizeTestGroup, + golden: 'parrot.png', + percentage: 0.06, + }); + + tests.forEach(function(testObj) { + var testName = testObj.test; + var code = testObj.code; + var percentage = testObj.percentage; + var golden = testObj.golden; + QUnit.test(testName, function(assert) { + var done = assert.async(); + code(fabricCanvas, function(renderedCanvas) { + var width = renderedCanvas.width; + var height = renderedCanvas.height; + var totalPixels = width * height; + var imageDataCanvas = renderedCanvas.getContext('2d').getImageData(0, 0, width, height).data; + var canvas = fabric.document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext('2d'); + var output = ctx.getImageData(0, 0, width, height).data; + getImage(getGoldeName(golden), renderedCanvas, function(golden) { + ctx.drawImage(golden, 0, 0); + var imageDataGolden = ctx.getImageData(0, 0, width, height).data; + var differentPixels = _pixelMatch(imageDataCanvas, imageDataGolden, output, width, height, pixelmatchOptions); + var percDiff = differentPixels / totalPixels * 100; + var okDiff = totalPixels * percentage; + assert.ok( + differentPixels < okDiff, + testName + ' has too many different pixels ' + differentPixels + '(' + okDiff + ') representing ' + percDiff + '%' + ); + console.log('Different pixels:', differentPixels, '/', totalPixels, ' diff:', percDiff.toFixed(3), '%'); + done(); + }); + }); + }); + }); +})();