From 88b9f7ef21efb0d822993b204f00702afde8345b Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 1 Jan 2017 16:11:54 +0100 Subject: [PATCH] Changes to canvas svg export (#3599) * change to parser * fixed broken function; * some changes to code reuse --- src/gradient.class.js | 6 +- src/parser.js | 281 +++++++++---------------------------- src/shapes/object.class.js | 12 +- src/static_canvas.class.js | 82 ++++++++++- test/unit/canvas_static.js | 4 +- 5 files changed, 150 insertions(+), 235 deletions(-) diff --git a/src/gradient.class.js b/src/gradient.class.js index f4eace81..0488d748 100644 --- a/src/gradient.class.js +++ b/src/gradient.class.js @@ -131,9 +131,9 @@ * @param {Object} colorStop Object with offset and color * @return {fabric.Gradient} thisArg */ - addColorStop: function(colorStop) { - for (var position in colorStop) { - var color = new fabric.Color(colorStop[position]); + addColorStop: function(colorStops) { + for (var position in colorStops) { + var color = new fabric.Color(colorStops[position]); this.colorStops.push({ offset: position, color: color.toRgb(), diff --git a/src/parser.js b/src/parser.js index baec5f83..23e41489 100644 --- a/src/parser.js +++ b/src/parser.js @@ -9,7 +9,6 @@ var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend, - capitalize = fabric.util.string.capitalize, clone = fabric.util.object.clone, toFixed = fabric.util.toFixed, parseUnit = fabric.util.parseUnit, @@ -17,7 +16,7 @@ reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/i, reViewBoxTagNames = /^(symbol|image|marker|pattern|view|svg)$/i, - reNotAllowedAncestors = /^(?:pattern|defs|symbol|metadata)$/i, + reNotAllowedAncestors = /^(?:pattern|defs|symbol|metadata|clipPath|mask)$/i, reAllowedParents = /^(symbol|g|a|svg)$/i, attributesMap = { @@ -155,16 +154,19 @@ */ fabric.parseTransformAttribute = (function() { function rotateMatrix(matrix, args) { - var angle = args[0], - x = (args.length === 3) ? args[1] : 0, - y = (args.length === 3) ? args[2] : 0; + var cos = Math.cos(args[0]), sin = Math.sin(args[0]), + x = 0, y = 0; + if (args.length === 3) { + x = args[1]; + y = args[2]; + } - matrix[0] = Math.cos(angle); - matrix[1] = Math.sin(angle); - matrix[2] = -Math.sin(angle); - matrix[3] = Math.cos(angle); - matrix[4] = x - (matrix[0] * x + matrix[2] * y); - matrix[5] = y - (matrix[1] * x + matrix[3] * y); + matrix[0] = cos; + matrix[1] = sin; + matrix[2] = -sin; + matrix[3] = cos; + matrix[4] = x - (cos * x - sin * y); + matrix[5] = y - (sin * x + cos * y); } function scaleMatrix(matrix, args) { @@ -175,12 +177,8 @@ matrix[3] = multiplierY; } - function skewXMatrix(matrix, args) { - matrix[2] = Math.tan(fabric.util.degreesToRadians(args[0])); - } - - function skewYMatrix(matrix, args) { - matrix[1] = Math.tan(fabric.util.degreesToRadians(args[0])); + function skewMatrix(matrix, args, pos) { + matrix[pos] = Math.tan(fabric.util.degreesToRadians(args[0])); } function translateMatrix(matrix, args) { @@ -280,10 +278,10 @@ scaleMatrix(matrix, args); break; case 'skewX': - skewXMatrix(matrix, args); + skewMatrix(matrix, args, 2); break; case 'skewY': - skewYMatrix(matrix, args); + skewMatrix(matrix, args, 1); break; case 'matrix': matrix = args; @@ -578,6 +576,16 @@ return parsedDim; } + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (element.nodeName && nodeName.test(element.nodeName.replace('svg:', '')) + && !element.getAttribute('instantiated_by_use')) { + return true; + } + } + return false; + } + /** * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback * @static @@ -588,122 +596,50 @@ * It's being passed an array of elements (parsed from a document). * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. */ - fabric.parseSVGDocument = (function() { - - function hasAncestorWithNodeName(element, nodeName) { - while (element && (element = element.parentNode)) { - if (element.nodeName && nodeName.test(element.nodeName.replace('svg:', '')) - && !element.getAttribute('instantiated_by_use')) { - return true; - } - } - return false; + fabric.parseSVGDocument = function(doc, callback, reviver) { + if (!doc) { + return; } - return function(doc, callback, reviver) { - if (!doc) { - return; + parseUseDirectives(doc); + + var svgUid = fabric.Object.__uid++, + options = applyViewboxTransform(doc), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + + options.svgUid = svgUid; + + if (descendants.length === 0 && fabric.isLikelyNode) { + // we're likely in node, where "o3-xml" library fails to gEBTN("*") + // https://github.com/ajaxorg/node-o3-xml/issues/21 + descendants = doc.selectNodes('//*[name(.)!="svg"]'); + var arr = []; + for (var i = 0, len = descendants.length; i < len; i++) { + arr[i] = descendants[i]; } - - parseUseDirectives(doc); - - var startTime = new Date(), - svgUid = fabric.Object.__uid++, - options = applyViewboxTransform(doc), - descendants = fabric.util.toArray(doc.getElementsByTagName('*')); - - options.svgUid = svgUid; - - if (descendants.length === 0 && fabric.isLikelyNode) { - // we're likely in node, where "o3-xml" library fails to gEBTN("*") - // https://github.com/ajaxorg/node-o3-xml/issues/21 - descendants = doc.selectNodes('//*[name(.)!="svg"]'); - var arr = []; - for (var i = 0, len = descendants.length; i < len; i++) { - arr[i] = descendants[i]; - } - descendants = arr; - } - - var elements = descendants.filter(function(el) { - applyViewboxTransform(el); - return reAllowedSVGTagNames.test(el.nodeName.replace('svg:', '')) && - !hasAncestorWithNodeName(el, reNotAllowedAncestors); // http://www.w3.org/TR/SVG/struct.html#DefsElement - }); - - if (!elements || (elements && !elements.length)) { - callback && callback([], {}); - return; - } - - fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); - fabric.cssRules[svgUid] = fabric.getCSSRules(doc); - // Precedence of rules: style > class > attribute - fabric.parseElements(elements, function(instances) { - fabric.documentParsingTime = new Date() - startTime; - if (callback) { - callback(instances, options); - } - }, clone(options), reviver); - }; - })(); - - /** - * Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`) - * @namespace - */ - var svgCache = { - - /** - * @param {String} name - * @param {Function} callback - */ - has: function (name, callback) { - callback(false); - }, - - get: function () { - /* NOOP */ - }, - - set: function () { - /* NOOP */ + descendants = arr; } - }; - /** - * @private - */ - function _enlivenCachedObject(cachedObject) { - - var objects = cachedObject.objects, - options = cachedObject.options; - - objects = objects.map(function (o) { - return fabric[capitalize(o.type)].fromObject(o); + var elements = descendants.filter(function(el) { + applyViewboxTransform(el); + return reAllowedSVGTagNames.test(el.nodeName.replace('svg:', '')) && + !hasAncestorWithNodeName(el, reNotAllowedAncestors); // http://www.w3.org/TR/SVG/struct.html#DefsElement }); - return ({ objects: objects, options: options }); - } - - /** - * @private - */ - function _createSVGPattern(markup, canvas, property) { - if (canvas[property] && canvas[property].toSVG) { - markup.push( - '\t\n', - '\t\t\n\t\n' - ); + if (!elements || (elements && !elements.length)) { + callback && callback([], {}); + return; } - } + + fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); + fabric.cssRules[svgUid] = fabric.getCSSRules(doc); + // Precedence of rules: style > class > attribute + fabric.parseElements(elements, function(instances) { + if (callback) { + callback(instances, options); + } + }, clone(options), reviver); + }; var reFontDeclaration = new RegExp( '(normal|italic)?\\s*(normal|small-caps)?\\s*' + @@ -981,19 +917,9 @@ loadSVGFromURL: function(url, callback, reviver) { url = url.replace(/^\n\s*/, '').trim(); - svgCache.has(url, function (hasUrl) { - if (hasUrl) { - svgCache.get(url, function (value) { - var enlivedRecord = _enlivenCachedObject(value); - callback(enlivedRecord.objects, enlivedRecord.options); - }); - } - else { - new fabric.util.request(url, { - method: 'get', - onComplete: onComplete - }); - } + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete }); function onComplete(r) { @@ -1010,10 +936,6 @@ } fabric.parseSVGDocument(xml.documentElement, function (results, options) { - svgCache.set(url, { - objects: fabric.util.array.invoke(results, 'toObject'), - options: options - }); callback && callback(results, options); }, reviver); } @@ -1045,77 +967,6 @@ fabric.parseSVGDocument(doc.documentElement, function (results, options) { callback(results, options); }, reviver); - }, - - /** - * Creates markup containing SVG font faces, - * font URLs for font faces must be collected by developers - * and are not extracted from the DOM by fabricjs - * @param {Array} objects Array of fabric objects - * @return {String} - */ - createSVGFontFacesMarkup: function(objects) { - var markup = '', fontList = { }, obj, fontFamily, - style, row, rowIndex, _char, charIndex, - fontPaths = fabric.fontPaths; - - for (var i = 0, len = objects.length; i < len; i++) { - obj = objects[i]; - fontFamily = obj.fontFamily; - if (obj.type.indexOf('text') === -1 || fontList[fontFamily] || !fontPaths[fontFamily]) { - continue; - } - fontList[fontFamily] = true; - if (!obj.styles) { - continue; - } - style = obj.styles; - for (rowIndex in style) { - row = style[rowIndex]; - for (charIndex in row) { - _char = row[charIndex]; - fontFamily = _char.fontFamily; - if (!fontList[fontFamily] && fontPaths[fontFamily]) { - fontList[fontFamily] = true; - } - } - } - } - - for (var j in fontList) { - markup += [ - '\t\t@font-face {\n', - '\t\t\tfont-family: \'', j, '\';\n', - '\t\t\tsrc: url(\'', fontPaths[j], '\');\n', - '\t\t}\n' - ].join(''); - } - - if (markup) { - markup = [ - '\t\n' - ].join(''); - } - - return markup; - }, - - /** - * Creates markup containing SVG referenced elements like patterns, gradients etc. - * @param {fabric.Canvas} canvas instance of fabric.Canvas - * @return {String} - */ - createSVGRefElementsMarkup: function(canvas) { - var markup = []; - - _createSVGPattern(markup, canvas, 'backgroundColor'); - _createSVGPattern(markup, canvas, 'overlayColor'); - - return markup.join(''); } }); diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 6b88b65c..e1b646e7 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1672,16 +1672,8 @@ gradient.coords.r2 = options.r2; } - options.gradientTransform && (gradient.gradientTransform = options.gradientTransform); - - for (var position in options.colorStops) { - var color = new fabric.Color(options.colorStops[position]); - gradient.colorStops.push({ - offset: position, - color: color.toRgb(), - opacity: color.getAlpha() - }); - } + gradient.gradientTransform = options.gradientTransform; + fabric.Gradient.prototype.addColorStop.call(gradient, options.colorStops); return this.set(property, fabric.Gradient.forObject(this, gradient)); }, diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 5a833169..cf31a491 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -861,7 +861,7 @@ var object = this[property + 'Color']; if (object) { ctx.fillStyle = object.toLive - ? object.toLive(ctx) + ? object.toLive(ctx, this) : object; ctx.fillRect( @@ -1226,13 +1226,85 @@ viewBox, 'xml:space="preserve">\n', 'Created with Fabric.js ', fabric.version, '\n', - '', - fabric.createSVGFontFacesMarkup(this.getObjects()), - fabric.createSVGRefElementsMarkup(this), + '\n', + this.createSVGFontFacesMarkup(), + this.createSVGRefElementsMarkup(), '\n' ); }, + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @return {String} + */ + createSVGRefElementsMarkup: function() { + var _this = this, + markup = ['backgroundColor', 'overlayColor'].map(function(prop) { + var fill = _this[prop]; + if (fill && fill.toLive) { + return fill.toSVG(_this, false); + } + }); + return markup.join(''); + }, + + /** + * Creates markup containing SVG font faces, + * font URLs for font faces must be collected by developers + * and are not extracted from the DOM by fabricjs + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function() { + var markup = '', fontList = { }, obj, fontFamily, + style, row, rowIndex, _char, charIndex, + fontPaths = fabric.fontPaths, objects = this.getObjects(); + + for (var i = 0, len = objects.length; i < len; i++) { + obj = objects[i]; + fontFamily = obj.fontFamily; + if (obj.type.indexOf('text') === -1 || fontList[fontFamily] || !fontPaths[fontFamily]) { + continue; + } + fontList[fontFamily] = true; + if (!obj.styles) { + continue; + } + style = obj.styles; + for (rowIndex in style) { + row = style[rowIndex]; + for (charIndex in row) { + _char = row[charIndex]; + fontFamily = _char.fontFamily; + if (!fontList[fontFamily] && fontPaths[fontFamily]) { + fontList[fontFamily] = true; + } + } + } + } + + for (var j in fontList) { + markup += [ + '\t\t@font-face {\n', + '\t\t\tfont-family: \'', j, '\';\n', + '\t\t\tsrc: url(\'', fontPaths[j], '\');\n', + '\t\t}\n' + ].join(''); + } + + if (markup) { + markup = [ + '\t\n' + ].join(''); + } + + return markup; + }, + /** * @private */ @@ -1279,7 +1351,7 @@ (this[property].repeat === 'repeat-x' || this[property].repeat === 'no-repeat' ? this[property].source.height : this.height), - '" fill="url(#' + property + 'Pattern)"', + '" fill="url(#' + property.id + ')"', '>\n' ); } diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index ea2af754..2ff555d2 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -3,10 +3,10 @@ // var emptyImageCanvasData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfQAAAH0CAYAAADL1t+KAAAH7ElEQVR4nO3VMQ0AMAzAsPInvYHoMS2yEeTLHADge/M6AADYM3QACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIMHQACDB0AAgwdAAIuMjH4b7osLFBAAAAAElFTkSuQmCC"; var CANVAS_SVG = '\n\n' + - '\nCreated with Fabric.js ' + fabric.version + '\n\n'; + '\nCreated with Fabric.js ' + fabric.version + '\n\n\n'; var CANVAS_SVG_VIEWBOX = '\n\n' + - '\nCreated with Fabric.js ' + fabric.version + '\n\n'; + '\nCreated with Fabric.js ' + fabric.version + '\n\n\n'; var PATH_JSON = '{"objects": [{"type": "path", "originX": "left", "originY": "top", "left": 268, "top": 266, "width": 51, "height": 49,' + ' "fill": "rgb(0,0,0)", "stroke": null, "strokeWidth": 1, "scaleX": 1, "scaleY": 1, ' +