Skip to content

Commit

Permalink
Resize filter should include canvas zoom and group zoom (fabricjs#5117)
Browse files Browse the repository at this point in the history
* change in zoom

* added a test

* fixed

* disable gl

* add percentage

* more group things

* more group things

* added unit test

* display percentage

* lint
  • Loading branch information
asturur authored Jul 23, 2018
1 parent 7d96c67 commit 58c2952
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 17 deletions.
4 changes: 3 additions & 1 deletion src/filters/resize_filter.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

/**
* Resize type
* for webgl resizyType is just lanczos, for canvas2d can be:
* bilinear, hermite, sliceHacl, lanczos.
* @param {String} resizeType
* @default
*/
Expand All @@ -50,7 +52,7 @@
scaleY: 0,

/**
* LanczosLobes parameter for lanczos filter
* LanczosLobes parameter for lanczos filter, valid for resizeType lanczos
* @param {Number} lanczosLobes
* @default
*/
Expand Down
19 changes: 10 additions & 9 deletions src/shapes/image.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -397,6 +397,8 @@
this._element = elementToFilter;
this._filterScalingX = 1;
this._filterScalingY = 1;
this._lastScaleX = 1;
this._lastScaleY = 1;
return;
}
if (!fabric.filterBackend) {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
},

/**
Expand Down
23 changes: 18 additions & 5 deletions src/shapes/object.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}
Expand Down
Binary file added test/fixtures/parrot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 22 additions & 2 deletions test/unit/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
Binary file added test/visual/golden/parrot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
173 changes: 173 additions & 0 deletions test/visual/resize_filter.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
})();

0 comments on commit 58c2952

Please sign in to comment.