Skip to content

Commit

Permalink
Clip path parsing (#4786)
Browse files Browse the repository at this point in the history
* first pass done

* restarted-clippaths

* some changes to element parser

* shared attribute

* done one piece

* cleaned

* mmm going there

* so far so good

* a very first draft

* removed dist

* sovled conflict

* now solved

* now solved

* some improvements

* toObject and fromObject added

* toObject and fromObject added

* more small changes

* added simple tests

* bumpedup qunit

* a test for svg export

* no ist

* more svg exporpt

* fix lint

* make possible to clip canvas

* improved JSOCS

* no builds

* invalidate cache anyway

* changes

* changes

* changes

* mmm working
  • Loading branch information
asturur authored Aug 22, 2018
1 parent e7eca14 commit 2a476e4
Show file tree
Hide file tree
Showing 23 changed files with 408 additions and 140 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ jobs:
packages: # avoid installing packages
- stage: Unit Tests
env: LAUNCHER=Chrome
install: npm install [email protected] qunit@2.4.1
install: npm install [email protected] qunit@2.6.1
addons:
apt:
packages: # avoid installing packages
- stage: Unit Tests
env: LAUNCHER=Firefox
install: npm install [email protected] qunit@2.4.1
install: npm install [email protected] qunit@2.6.1
addons:
apt:
packages: # avoid installing packages
Expand Down
18 changes: 9 additions & 9 deletions HEADER.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ fabric.isLikelyNode = typeof Buffer !== 'undefined' &&
* @type array
*/
fabric.SHARED_ATTRIBUTES = [
"display",
"transform",
"fill", "fill-opacity", "fill-rule",
"opacity",
"stroke", "stroke-dasharray", "stroke-linecap",
"stroke-linejoin", "stroke-miterlimit",
"stroke-opacity", "stroke-width",
"id", "paint-order",
"instantiated_by_use"
'display',
'transform',
'fill', 'fill-opacity', 'fill-rule',
'opacity',
'stroke', 'stroke-dasharray', 'stroke-linecap',
'stroke-linejoin', 'stroke-miterlimit',
'stroke-opacity', 'stroke-width',
'id', 'paint-order',
'instantiated_by_use', 'clip-path'
];
/* _FROM_SVG_END_ */

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"eslint": "4.18.x",
"istanbul": "0.4.x",
"onchange": "^3.x.x",
"qunit": "^2.4.1",
"qunit": "^2.6.1",
"testem": "^1.18.4",
"uglify-js": "3.3.x",
"pixelmatch": "^4.0.2"
Expand Down
171 changes: 107 additions & 64 deletions src/elements_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,79 +8,122 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp
this.regexUrl = /^url\(['"]?#([^'"]+)['"]?\)/g;
};

fabric.ElementsParser.prototype.parse = function() {
this.instances = new Array(this.elements.length);
this.numElements = this.elements.length;
(function(proto) {
proto.parse = function() {
this.instances = new Array(this.elements.length);
this.numElements = this.elements.length;
this.createObjects();
};

this.createObjects();
};
proto.createObjects = function() {
var _this = this;
this.elements.forEach(function(element, i) {
element.setAttribute('svgUid', _this.svgUid);
_this.createObject(element, i);
});
};

fabric.ElementsParser.prototype.createObjects = function() {
for (var i = 0, len = this.elements.length; i < len; i++) {
this.elements[i].setAttribute('svgUid', this.svgUid);
(function(_obj, i) {
setTimeout(function() {
_obj.createObject(_obj.elements[i], i);
}, 0);
})(this, i);
}
};
proto.findTag = function(el) {
return fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))];
};

fabric.ElementsParser.prototype.createObject = function(el, index) {
var klass = fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))];
if (klass && klass.fromElement) {
try {
this._createObject(klass, el, index);
proto.createObject = function(el, index) {
var klass = this.findTag(el);
if (klass && klass.fromElement) {
try {
klass.fromElement(el, this.createCallback(index, el), this.options);
}
catch (err) {
fabric.log(err);
}
}
catch (err) {
fabric.log(err);
else {
this.checkIfDone();
}
}
else {
this.checkIfDone();
}
};
};

fabric.ElementsParser.prototype._createObject = function(klass, el, index) {
klass.fromElement(el, this.createCallback(index, el), this.options);
};
proto.createCallback = function(index, el) {
var _this = this;
return function(obj) {
var _options;
_this.resolveGradient(obj, 'fill');
_this.resolveGradient(obj, 'stroke');
if (obj instanceof fabric.Image) {
_options = obj.parsePreserveAspectRatioAttribute(el);
}
obj._removeTransformMatrix(_options);
_this.resolveClipPath(obj);
_this.reviver && _this.reviver(el, obj);
_this.instances[index] = obj;
_this.checkIfDone();
};
};

fabric.ElementsParser.prototype.createCallback = function(index, el) {
var _this = this;
return function(obj) {
var _options;
_this.resolveGradient(obj, 'fill');
_this.resolveGradient(obj, 'stroke');
if (obj instanceof fabric.Image) {
_options = obj.parsePreserveAspectRatioAttribute(el);
proto.extractPropertyDefinition = function(obj, property, storage) {
var value = obj[property];
if (!(/^url\(/).test(value)) {
return;
}
obj._removeTransformMatrix(_options);
_this.reviver && _this.reviver(el, obj);
_this.instances[index] = obj;
_this.checkIfDone();
var id = this.regexUrl.exec(value)[1];
this.regexUrl.lastIndex = 0;
return fabric[storage][this.svgUid][id];
};
};

fabric.ElementsParser.prototype.resolveGradient = function(obj, property) {
proto.resolveGradient = function(obj, property) {
var gradientDef = this.extractPropertyDefinition(obj, property, 'gradientDefs');
if (gradientDef) {
obj.set(property, fabric.Gradient.fromElement(gradientDef, obj));
}
};

var instanceFillValue = obj[property];
if (!(/^url\(/).test(instanceFillValue)) {
return;
}
var gradientId = this.regexUrl.exec(instanceFillValue)[1];
this.regexUrl.lastIndex = 0;
if (fabric.gradientDefs[this.svgUid][gradientId]) {
obj.set(property,
fabric.Gradient.fromElement(fabric.gradientDefs[this.svgUid][gradientId], obj));
}
};
proto.createClipPathCallback = function(obj, container) {
return function(_newObj) {
_newObj._removeTransformMatrix();
_newObj.fillRule = _newObj.clipRule;
container.push(_newObj);
};
};

fabric.ElementsParser.prototype.checkIfDone = function() {
if (--this.numElements === 0) {
this.instances = this.instances.filter(function(el) {
// eslint-disable-next-line no-eq-null, eqeqeq
return el != null;
});
this.callback(this.instances, this.elements);
}
};
proto.resolveClipPath = function(obj) {
var clipPath = this.extractPropertyDefinition(obj, 'clipPath', 'clipPaths'),
element, klass, objTransformInv, container, gTransform, options;
if (clipPath) {
container = [];
objTransformInv = fabric.util.invertTransform(obj.calcTransformMatrix());
for (var i = 0; i < clipPath.length; i++) {
element = clipPath[i];
klass = this.findTag(element);
klass.fromElement(
element,
this.createClipPathCallback(obj, container),
this.options
);
}
clipPath = new fabric.Group(container);
gTransform = fabric.util.multiplyTransformMatrices(
objTransformInv,
clipPath.calcTransformMatrix()
);
var options = fabric.util.qrDecompose(gTransform);
clipPath.flipX = false;
clipPath.flipY = false;
clipPath.set('scaleX', options.scaleX);
clipPath.set('scaleY', options.scaleY);
clipPath.angle = options.angle;
clipPath.skewX = options.skewX;
clipPath.skewY = 0;
clipPath.setPositionByOrigin({ x: options.translateX, y: options.translateY }, 'center', 'center');
obj.clipPath = clipPath;
}
};

proto.checkIfDone = function() {
if (--this.numElements === 0) {
this.instances = this.instances.filter(function(el) {
// eslint-disable-next-line no-eq-null, eqeqeq
return el != null;
});
this.callback(this.instances, this.elements);
}
};
})(fabric.ElementsParser.prototype);
2 changes: 1 addition & 1 deletion src/mixins/itext.svg_export.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
style = filter === '' ? '' : ' style="' + filter + '"',
textDecoration = this.getSvgTextDecoration(this);
markup.push(
'\t<g ', this.getSvgId(), 'transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '"',
'\t<g ', this.getSvgCommons(), 'transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '"',
style, '>\n',
textAndBg.textBgRects.join(''),
'\t\t<text xml:space="preserve" ',
Expand Down
21 changes: 18 additions & 3 deletions src/mixins/object.svg_export.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,11 @@
* Returns id attribute for svg output
* @return {String}
*/
getSvgId: function() {
return this.id ? 'id="' + this.id + '" ' : '';
getSvgCommons: function() {
return [
this.id ? 'id="' + this.id + '" ' : '',
this.clipPath ? 'clip-path="url(#' + this.clipPath.clipPathId + ')" ' : '',
].join('');
},

/**
Expand Down Expand Up @@ -196,7 +199,7 @@
* @private
*/
_createBaseSVGMarkup: function() {
var markup = [];
var markup = [], clipPath = this.clipPath;

if (this.fill && this.fill.toLive) {
markup.push(this.fill.toSVG(this, false));
Expand All @@ -207,6 +210,18 @@
if (this.shadow) {
markup.push(this.shadow.toSVG(this));
}
if (clipPath) {
if (clipPath.clipPathId === undefined) {
clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++;
}
markup.push(
'<clipPath id="' + clipPath.clipPathId + '" ',
'clipPathUnits="objectBoundingBox" ',
'transform="translate(' + (this.width / 2) + ' , ' + (this.height / 2) + ')" >\n\t',
this.clipPath.toSVG(),
'</clipPath>\n'
);
}
return markup;
},

Expand Down
25 changes: 19 additions & 6 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
multiplyTransformMatrices = fabric.util.multiplyTransformMatrices,

svgValidTagNames = ['path', 'circle', 'polygon', 'polyline', 'ellipse', 'rect', 'line',
'image', 'text', 'linearGradient', 'radialGradient', 'stop'],
'image', 'text'],
svgViewBoxElements = ['symbol', 'image', 'marker', 'pattern', 'view', 'svg'],
svgInvalidAncestors = ['pattern', 'defs', 'symbol', 'metadata', 'clipPath', 'mask', 'desc'],
svgValidParents = ['symbol', 'g', 'a', 'svg'],
svgValidParents = ['symbol', 'g', 'a', 'svg', 'clipPath', 'defs'],

attributesMap = {
cx: 'left',
Expand All @@ -45,7 +45,9 @@
'stroke-width': 'strokeWidth',
'text-decoration': 'textDecoration',
'text-anchor': 'textAnchor',
opacity: 'opacity'
opacity: 'opacity',
'clip-path': 'clipPath',
'clip-rule': 'clipRule',
},

colorAttributes = {
Expand All @@ -60,6 +62,7 @@

fabric.cssRules = { };
fabric.gradientDefs = { };
fabric.clipPaths = { };

function normalizeAttr(attr) {
// transform attribute names
Expand Down Expand Up @@ -617,7 +620,7 @@
scaleY + ' ' +
(minX * scaleX + widthDiff) + ' ' +
(minY * scaleY + heightDiff) + ') ';

parsedDim.viewboxTransform = fabric.parseTransformAttribute(matrix);
if (element.nodeName === 'svg') {
el = element.ownerDocument.createElement('g');
// element.firstChild != null
Expand All @@ -630,7 +633,6 @@
el = element;
matrix = el.getAttribute('transform') + matrix;
}

el.setAttribute('transform', matrix);
return parsedDim;
}
Expand Down Expand Up @@ -691,13 +693,24 @@
callback && callback([], {});
return;
}

var clipPaths = { };
descendants.filter(function(el) {
return el.nodeName.replace('svg:', '') === 'clipPath';
}).forEach(function(el) {
clipPaths[el.id] = fabric.util.toArray(el.getElementsByTagName('*')).filter(function(el) {
return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', ''));
});
});
fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc);
fabric.cssRules[svgUid] = fabric.getCSSRules(doc);
fabric.clipPaths[svgUid] = clipPaths;
// Precedence of rules: style > class > attribute
fabric.parseElements(elements, function(instances, elements) {
if (callback) {
callback(instances, options, elements, descendants);
delete fabric.gradientDefs[svgUid];
delete fabric.cssRules[svgUid];
delete fabric.clipPaths[svgUid];
}
}, clone(options), reviver, parsingOptions);
};
Expand Down
2 changes: 1 addition & 1 deletion src/shapes/circle.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@

if (angle === 0) {
markup.push(
'<circle ', this.getSvgId(),
'<circle ', this.getSvgCommons(),
'cx="' + x + '" cy="' + y + '" ',
'r="', this.radius,
'" style="', this.getSvgStyles(),
Expand Down
2 changes: 1 addition & 1 deletion src/shapes/ellipse.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
toSVG: function(reviver) {
var markup = this._createBaseSVGMarkup();
markup.push(
'<ellipse ', this.getSvgId(),
'<ellipse ', this.getSvgCommons(),
'cx="0" cy="0" ',
'rx="', this.rx,
'" ry="', this.ry,
Expand Down
Loading

0 comments on commit 2a476e4

Please sign in to comment.