Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resize filter should include canvas zoom and group zoom #5117

Merged
merged 10 commits into from
Jul 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
});
});
});
})();