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

fix(canvas): respect imageSmoothingEnabled for canvas export #6280

Merged
merged 10 commits into from
Apr 20, 2020
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
2 changes: 1 addition & 1 deletion HEADER.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ fabric.SHARED_ATTRIBUTES = [
'stroke-linejoin', 'stroke-miterlimit',
'stroke-opacity', 'stroke-width',
'id', 'paint-order', 'vector-effect',
'instantiated_by_use', 'clip-path'
'instantiated_by_use', 'clip-path',
];
/* _FROM_SVG_END_ */

Expand Down
10 changes: 7 additions & 3 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
opacity: 'opacity',
'clip-path': 'clipPath',
'clip-rule': 'clipRule',
'vector-effect': 'strokeUniform'
'vector-effect': 'strokeUniform',
'image-rendering': 'imageSmoothing',
},

colorAttributes = {
Expand Down Expand Up @@ -83,8 +84,8 @@
if ((attr === 'fill' || attr === 'stroke') && value === 'none') {
value = '';
}
else if (attr === 'vector-effect') {
value = value === 'non-scaling-stroke';
else if (attr === 'strokeUniform') {
return (value === 'non-scaling-stroke');
}
else if (attr === 'strokeDashArray') {
if (value === 'none') {
Expand Down Expand Up @@ -137,6 +138,9 @@
else if (attr === 'href' || attr === 'xlink:href' || attr === 'font') {
return value;
}
else if (attr === 'imageSmoothing') {
return (value === 'optimizeQuality' ? true : false);
}
else {
parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize);
}
Expand Down
39 changes: 34 additions & 5 deletions src/shapes/image.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@
*/
cropY: 0,

/**
* Indicates whether this canvas will use image smoothing when painting this image.
* Also influence if the cacheCanvas for this image uses imageSmoothing
* @since 4.0.0-beta.11
* @type Boolean
* @default
*/
imageSmoothing: true,

/**
* Constructor
* @param {HTMLImageElement | String} element Image element
Expand Down Expand Up @@ -304,8 +313,11 @@
* of the instance
*/
_toSVG: function() {
var svgString = [], imageMarkup = [], strokeSvg,
x = -this.width / 2, y = -this.height / 2, clipPath = '';
var svgString = [], imageMarkup = [], strokeSvg, element = this._element,
x = -this.width / 2, y = -this.height / 2, clipPath = '', imageRendering = '';
if (!element) {
return [];
}
if (this.hasCrop()) {
var clipPathId = fabric.Object.__uid++;
svgString.push(
Expand All @@ -315,13 +327,17 @@
);
clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" ';
}
if (!this.imageSmoothing) {
imageRendering = '" image-rendering="optimizeSpeed';
}
imageMarkup.push('\t<image ', 'COMMON_PARTS', 'xlink:href="', this.getSvgSrc(true),
'" x="', x - this.cropX, '" y="', y - this.cropY,
// we're essentially moving origin of transformation from top/left corner to the center of the shape
// by wrapping it in container <g> element with actual transformation, then offsetting object to the top/left
// so that object's center aligns with container's left/top
'" width="', this._element.width || this._element.naturalWidth,
'" height="', this._element.height || this._element.height,
'" width="', element.width || element.naturalWidth,
'" height="', element.height || element.height,
imageRendering,
'"', clipPath,
'></image>\n');

Expand Down Expand Up @@ -495,13 +511,24 @@
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
fabric.util.setImageSmoothing(ctx, this.imageSmoothing);
if (this.isMoving !== true && this.resizeFilter && this._needsResize()) {
this.applyResizeFilters();
}
this._stroke(ctx);
this._renderPaintInOrder(ctx);
},

/**
* Paint the cached copy of the object on the target context.
* it will set the imageSmoothing for the draw operation
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
drawCacheOnCanvas: function(ctx) {
fabric.util.setImageSmoothing(ctx, this.imageSmoothing);
fabric.Object.prototype.drawCacheOnCanvas.call(this, ctx);
},

/**
* Decide if the object should cache or not. Create its own cache level
* needsItsOwnCache should be used when the object drawing method requires
Expand Down Expand Up @@ -726,7 +753,9 @@
* @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement}
*/
fabric.Image.ATTRIBUTE_NAMES =
fabric.SHARED_ATTRIBUTES.concat('x y width height preserveAspectRatio xlink:href crossOrigin'.split(' '));
fabric.SHARED_ATTRIBUTES.concat(
'x y width height preserveAspectRatio xlink:href crossOrigin image-rendering'.split(' ')
);

/**
* Returns {@link fabric.Image} instance from an SVG element
Expand Down
1 change: 0 additions & 1 deletion src/shapes/object.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,6 @@
strokeLineCap: this.strokeLineCap,
strokeDashOffset: this.strokeDashOffset,
strokeLineJoin: this.strokeLineJoin,
// TODO: add this before release
// strokeUniform: this.strokeUniform,
strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS),
scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS),
Expand Down
1 change: 0 additions & 1 deletion src/shapes/rect.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@
options = options || { };

var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES);

parsedAttributes.left = parsedAttributes.left || 0;
parsedAttributes.top = parsedAttributes.top || 0;
parsedAttributes.height = parsedAttributes.height || 0;
Expand Down
15 changes: 1 addition & 14 deletions src/static_canvas.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,6 @@
this._objects = [];
this._createLowerCanvas(el);
this._initOptions(options);
this._setImageSmoothing();
// only initialize retina scaling once
if (!this.interactive) {
this._initRetinaScaling();
Expand Down Expand Up @@ -425,18 +424,6 @@
return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback);
},

/**
* @private
* @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-imagesmoothingenabled|WhatWG Canvas Standard}
*/
_setImageSmoothing: function() {
var ctx = this.getContext();

ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled
|| ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled;
ctx.imageSmoothingEnabled = this.imageSmoothingEnabled;
},

/**
* @private
* @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage}
Expand Down Expand Up @@ -619,7 +606,6 @@
this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles();
}
this._initRetinaScaling();
this._setImageSmoothing();
this.calcOffset();

if (!options.cssOnly) {
Expand Down Expand Up @@ -908,6 +894,7 @@
this.cancelRequestedRender();
this.calcViewportBoundaries();
this.clearContext(ctx);
fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled);
this.fire('before:render', { ctx: ctx, });
this._renderBackground(ctx);

Expand Down
53 changes: 16 additions & 37 deletions src/util/dom_misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,41 +252,6 @@
fabric.util.makeElementSelectable = makeElementSelectable;
})();

(function() {

/**
* Inserts a script element with a given url into a document; invokes callback, when that script is finished loading
* @memberOf fabric.util
* @param {String} url URL of a script to load
* @param {Function} callback Callback to execute when script is finished loading
*/
function getScript(url, callback) {
var headEl = fabric.document.getElementsByTagName('head')[0],
scriptEl = fabric.document.createElement('script'),
loading = true;

/** @ignore */
scriptEl.onload = /** @ignore */ scriptEl.onreadystatechange = function(e) {
if (loading) {
if (typeof this.readyState === 'string' &&
this.readyState !== 'loaded' &&
this.readyState !== 'complete') {
return;
}
loading = false;
callback(e || fabric.window.event);
scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null;
}
};
scriptEl.src = url;
headEl.appendChild(scriptEl);
// causes issue in Opera
// headEl.removeChild(scriptEl);
}

fabric.util.getScript = getScript;
})();

function getNodeCanvas(element) {
var impl = fabric.jsdomImplForWrapper(element);
return impl._canvas || impl._image;
Expand All @@ -307,14 +272,28 @@
}
}

function setImageSmoothing(ctx, value) {
ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled
|| ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled;
ctx.imageSmoothingEnabled = value;
}

/**
* setImageSmoothing sets the context imageSmoothingEnabled property.
* Used by canvas and by ImageObject.
* @memberOf fabric.util
* @since 4.0.0
* @param {HTMLRenderingContext2D} ctx to set on
* @param {Boolean} value true or false
*/
fabric.util.setImageSmoothing = setImageSmoothing;
fabric.util.getById = getById;
fabric.util.toArray = toArray;
fabric.util.makeElement = makeElement;
fabric.util.addClass = addClass;
fabric.util.makeElement = makeElement;
fabric.util.wrapElement = wrapElement;
fabric.util.getScrollLeftTop = getScrollLeftTop;
fabric.util.getElementOffset = getElementOffset;
fabric.util.getElementStyle = getElementStyle;
fabric.util.getNodeCanvas = getNodeCanvas;
fabric.util.cleanUpJsdomNode = cleanUpJsdomNode;

Expand Down
43 changes: 43 additions & 0 deletions test/unit/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,28 @@
});
});

QUnit.test('toSVG with imageSmoothing false', function(assert) {
var done = assert.async();
createImageObject(function(image) {
image.imageSmoothing = false;
assert.ok(typeof image.toSVG === 'function');
var expectedSVG = '<g transform="matrix(1 0 0 1 138 55)" >\n\t<image style=\"stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;\" xlink:href=\"' + IMG_SRC + '\" x=\"-138\" y=\"-55\" width=\"276\" height=\"110\" image-rendering=\"optimizeSpeed\"></image>\n</g>\n';
assert.equal(image.toSVG(), expectedSVG);
done();
});
});

QUnit.test('toSVG with missing element', function(assert) {
var done = assert.async();
createImageObject(function(image) {
delete image._element;
assert.ok(typeof image.toSVG === 'function');
var expectedSVG = '<g transform="matrix(1 0 0 1 138 55)" >\n</g>\n';
assert.equal(image.toSVG(), expectedSVG);
done();
});
});

QUnit.test('getSrc', function(assert) {
var done = assert.async();
createImageObject(function(image) {
Expand Down Expand Up @@ -512,6 +534,27 @@
});
});

QUnit.test('fromElement imageSmoothing', function(assert) {
var done = assert.async();

var IMAGE_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAARCAYAAADtyJ2fAAAACXBIWXMAAAsSAAALEgHS3X78AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAVBJREFUeNqMU7tOBDEMtENuy614/QE/gZBOuvJK+Et6CiQ6JP6ExxWI7bhL1vgVExYKLPmsTTIzjieHd+MZZSBIAJwEyJU0EWaum+lNljRux3O6nl70Gx/GUwUeyYcDJWZNhMK1aEXYe95Mz4iP44kDTRUZSWSq1YEHri0/HZxXfGSFBN+qDEJTrNI+QXRBviZ7eWCQgjsg+IHiHYB30MhqUxwcmH1Arc2kFDwkBldeFGJLPqs/AbbF2dWgUym6Z2Tb6RVzYxG1wUnmaNcOonZiU0++l6C7FzoQY42g3+8jz+GZ+dWMr1rRH0OjAFhPO+VJFx/vWDqPmk8H97CGBUYUiqAGW0PVe1+aX8j2Ll0tgHtvLx6AK9Tu1ZTFTQ0ojChqGD4qkOzeAuzVfgzsaTym1ClS+IdwtQCFooQMBTumNun1H6Bfcc9/MUn4R3wJMAAZH6MmA4ht4gAAAABJRU5ErkJggg==';

assert.ok(typeof fabric.Image.fromElement === 'function', 'fromElement should exist');

var imageEl = makeImageElement({
width: '14',
height: '17',
'image-rendering': 'optimizeSpeed',
'xlink:href': IMAGE_DATA_URL
});

fabric.Image.fromElement(imageEl, function(imgObject) {
assert.ok(imgObject instanceof fabric.Image);
assert.deepEqual(imgObject.get('imageSmoothing'), false, 'imageSmoothing set to false');
done();
});
});

QUnit.test('fromElement with preserveAspectRatio', function(assert) {
var done = assert.async();

Expand Down
6 changes: 4 additions & 2 deletions test/unit/rect.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,11 @@
elRectWithAttrs.setAttributeNS(namespace, 'stroke-linecap', 'round');
elRectWithAttrs.setAttributeNS(namespace, 'stroke-linejoin', 'bevil');
elRectWithAttrs.setAttributeNS(namespace, 'stroke-miterlimit', 5);
elRectWithAttrs.setAttributeNS(namespace, 'vector-effect', 'non-scaling-stroke');
//elRectWithAttrs.setAttributeNS(namespace, 'transform', 'translate(-10,-20) scale(2) rotate(45) translate(5,10)');

fabric.Rect.fromElement(elRectWithAttrs, function(rectWithAttrs) {
assert.ok(rectWithAttrs instanceof fabric.Rect);
assert.equal(rectWithAttrs.strokeUniform, true, 'strokeUniform is parsed');
var expectedObject = fabric.util.object.extend(REFERENCE_RECT, {
left: 10,
top: 20,
Expand All @@ -148,7 +149,8 @@
strokeLineJoin: 'bevil',
strokeMiterLimit: 5,
rx: 11,
ry: 12
ry: 12,
// strokeUniform: true
});
assert.deepEqual(rectWithAttrs.toObject(), expectedObject);
});
Expand Down
5 changes: 5 additions & 0 deletions test/visual/assets/image-rendering-attr.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions test/visual/generic_rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,5 +311,26 @@
height: 500,
});

function imageSmoothing(fabricCanvas, callback) {
getFixture('greyfloral.png', false, function(img2) {
var fImg = new fabric.Image(img2, { imageSmoothing: false, scaleX: 10, scaleY: 10 });
var fImg2 = new fabric.Image(img2, { left: 400, scaleX: 10, scaleY: 10 });
fabricCanvas.add(fImg);
fabricCanvas.add(fImg2);
fabricCanvas.renderAll();
callback(fabricCanvas.lowerCanvasEl);
});
}

tests.push({
test: 'fabric.Image with imageSmoothing false',
code: imageSmoothing,
// use the same golden on purpose
golden: 'imageSoothingOnObject.png',
percentage: 0.09,
width: 800,
height: 400,
});

tests.forEach(visualTestLoop(QUnit));
})();
Binary file added test/visual/golden/image-rendering-attr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/visual/golden/imageSoothingOnObject.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion test/visual/svg_import.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
// 'clippath-8',
'emoji-b',
'gold-logo',
'svg_missing_clippath'
'svg_missing_clippath',
'image-rendering-attr',
].map(createTestFromSVG);

tests.forEach(visualTestLoop(QUnit));
Expand Down