From fee4405d362d4563325b78c92c1221510e8d2dac Mon Sep 17 00:00:00 2001 From: Alexis Deveria Date: Wed, 14 Apr 2010 17:30:25 +0000 Subject: [PATCH] Mostly fixed issue 70 (export as PNG option) by using Gabe Lerner's canvg library git-svn-id: http://svg-edit.googlecode.com/svn/trunk@1521 eee81c28-f429-11dd-99c0-75d572ba1ddd --- editor/canvg/canvg.js | 1451 ++++++++++++++++++++++++++++++ editor/canvg/rgbcolor.js | 288 ++++++ editor/images/svg_edit_icons.svg | 19 + editor/svg-editor.html | 5 + editor/svg-editor.js | 62 +- editor/svgcanvas.js | 41 +- 6 files changed, 1861 insertions(+), 5 deletions(-) create mode 100644 editor/canvg/canvg.js create mode 100644 editor/canvg/rgbcolor.js diff --git a/editor/canvg/canvg.js b/editor/canvg/canvg.js new file mode 100644 index 00000000..d3aa73e7 --- /dev/null +++ b/editor/canvg/canvg.js @@ -0,0 +1,1451 @@ +/* + * canvg.js - Javascript SVG parser and renderer on Canvas + * MIT Licensed + * Gabe Lerner (gabelerner@gmail.com) + * http://code.google.com/p/canvg/ + * + * Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/ + */ +if(!window.console) { + window.console = {}; + window.console.log = function(str) {}; + window.console.dir = function(str) {}; +} +(function(){ + // canvg(target, s) + // target: canvas element or the id of a canvas element + // s: svg string or url to svg file + this.canvg = function (target, s) { + if (typeof target == 'string') { + target = document.getElementById(target); + } + + // reuse class per canvas + var svg; + if (target.svg == null) { + svg = build(); + target.svg = svg; + } + else { + svg = target.svg; + svg.stop(); + } + + var ctx = target.getContext('2d'); + if (s.substr(0,1) == '<') { + // load from xml string + svg.loadXml(ctx, s); + } + else { + // load from url + svg.load(ctx, s); + } + } + + function build() { + var svg = {}; + + svg.FRAMERATE = 30; + + // globals + svg.init = function(ctx) { + svg.Definitions = {}; + svg.Styles = {}; + svg.Animations = []; + svg.ctx = ctx; + svg.ViewPort = new (function () { + this.viewPorts = []; + this.SetCurrent = function(width, height) { this.viewPorts.push({ width: width, height: height }); } + this.RemoveCurrent = function() { this.viewPorts.pop(); } + this.Current = function() { return this.viewPorts[this.viewPorts.length - 1]; } + this.width = function() { return this.Current().width; } + this.height = function() { return this.Current().height; } + this.ComputeSize = function(d) { + if (d != null && typeof(d) == 'number') return d; + if (d == 'x') return this.width(); + if (d == 'y') return this.height(); + return Math.sqrt(Math.pow(this.width(), 2) + Math.pow(this.height(), 2)) / Math.sqrt(2); + } + }); + } + svg.init(); + + // trim + svg.trim = function(s) { return s.replace(/^\s+|\s+$/g, ''); } + + // compress spaces + svg.compressSpaces = function(s) { return s.replace(/[\s\r\t\n]+/gm,' '); } + + // ajax + svg.ajax = function(url) { + var AJAX; + if(window.XMLHttpRequest){AJAX=new XMLHttpRequest();} + else{AJAX=new ActiveXObject('Microsoft.XMLHTTP');} + if(AJAX){ + AJAX.open('GET',url,false); + AJAX.send(null); + return AJAX.responseText; + } + return null; + } + + // parse xml + svg.parseXml = function(xml) { + if (window.DOMParser) + { + var parser = new DOMParser(); + return parser.parseFromString(xml, 'text/xml'); + } + else + { + xml = xml.replace(/]*>/, ''); + var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + } + } + + svg.Property = function(name, value) { + this.name = name; + this.value = value; + + this.hasValue = function() { + return (this.value != null && this.value != ''); + } + + // return the numerical value of the property + this.numValue = function() { + if (!this.hasValue()) return 0; + + var n = parseFloat(this.value); + if ((this.value + '').match(/%$/)) { + n = n / 100.0; + } + return n; + } + + this.valueOrDefault = function(def) { + if (this.hasValue()) return this.value; + return def; + } + + this.numValueOrDefault = function(def) { + if (this.hasValue()) return this.numValue(); + return def; + } + + /* EXTENSIONS */ + var that = this; + + // color extensions + this.Color = { + // augment the current color value with the opacity + addOpacity: function(opacity) { + var newValue = that.value; + if (opacity != null && opacity != '') { + var color = new RGBColor(that.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacity + ')'; + } + } + return new svg.Property(that.name, newValue); + } + } + + // definition extensions + this.Definition = { + // get the definition from the definitions table + getDefinition: function() { + var name = that.value.replace(/^(url\()?#([^\)]+)\)?$/, '$2'); + return svg.Definitions[name]; + } + } + + // length extensions + this.Length = { + DPI: function(viewPort) { + return 96.0; // TODO: compute? + }, + + EM: function(viewPort) { + var em = 12; + + var fontSize = new svg.Property('fontSize', svg.ctx.font.match(/[0-9][^\s\t\n\r\/]*/g)[0]); + if (fontSize.hasValue()) em = fontSize.Length.toPixels(viewPort); + + return em; + }, + + // get the length as pixels + toPixels: function(viewPort) { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/em$/)) return that.numValue() * this.EM(viewPort); + if (s.match(/ex$/)) return that.numValue() * this.EM(viewPort) / 2.0; + if (s.match(/px$/)) return that.numValue(); + if (s.match(/pt$/)) return that.numValue() * 1.25; + if (s.match(/pc$/)) return that.numValue() * 15; + if (s.match(/cm$/)) return that.numValue() * this.DPI(viewPort) / 2.54; + if (s.match(/mm$/)) return that.numValue() * this.DPI(viewPort) / 25.4; + if (s.match(/in$/)) return that.numValue() * this.DPI(viewPort); + if (s.match(/%$/)) return that.numValue() * svg.ViewPort.ComputeSize(viewPort); + return that.numValue(); + } + } + + // time extensions + this.Time = { + // get the time as milliseconds + toMilliseconds: function() { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/s$/)) return that.numValue() * 1000; + if (s.match(/ms$/)) return that.numValue(); + return that.numValue(); + } + } + + // angle extensions + this.Angle = { + // get the angle as radians + toRadians: function() { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/deg$/)) return that.numValue() * (Math.PI / 180.0); + if (s.match(/grad$/)) return that.numValue() * (Math.PI / 200.0); + if (s.match(/rad$/)) return that.numValue(); + return that.numValue() * (Math.PI / 180.0); + } + } + } + + // points and paths + svg.ToNumberArray = function(s) { + var a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' '); + for (var i=0; i this.x2) this.x2 = x; + } + + if (y != null) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + } + this.addX = function(x) { this.addPoint(x, null); } + this.addY = function(y) { this.addPoint(null, y); } + + this.addQuadraticCurve = function(p0x, p0y, p1x, p1y, p2x, p2y) { + var cp1x = p0x + 2/3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp1y = p0y + 2/3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp2x = cp1x + 1/3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + var cp2y = cp1y + 1/3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + + this.addBezierCurve = function(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + var p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + for (i=0; i<=1; i++) { + var f = function(t) { + return Math.pow(1-t, 3) * p0[i] + + 3 * Math.pow(1-t, 2) * t * p1[i] + + 3 * (1-t) * Math.pow(t, 2) * p2[i] + + Math.pow(t, 3) * p3[i]; + } + + var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + var c = 3 * p1[i] - 3 * p0[i]; + + if (a == 0) { + if (b == 0) continue; + var t = -c / b; + if (0 < t && t < 1) { + if (i == 0) this.addX(f(t)); + if (i == 1) this.addY(f(t)); + } + continue; + } + + var b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) continue; + var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (0 < t1 && t1 < 1) { + if (i == 0) this.addX(f(t1)); + if (i == 1) this.addY(f(t1)); + } + var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (0 < t2 && t2 < 1) { + if (i == 0) this.addX(f(t2)); + if (i == 1) this.addY(f(t2)); + } + } + } + + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + + // transforms + svg.Transform = function(v) { + var that = this; + this.Type = {} + + // translate + this.Type.translate = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + } + } + + // rotate + this.Type.rotate = function(s) { + var a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + this.apply = function(ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.Angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + } + } + + this.Type.scale = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + } + } + + this.Type.matrix = function(s) { + this.m = svg.ToNumberArray(s); + this.apply = function(ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + } + } + + this.Type.SkewBase = function(s) { + this.base = that.Type.matrix; + this.base(s); + this.angle = new svg.Property('angle', s); + } + this.Type.SkewBase.prototype = new this.Type.matrix; + + this.Type.skewX = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, 0, Math.tan(this.angle.Angle.toRadians()), 1, 0, 0]; + } + this.Type.skewX.prototype = new this.Type.SkewBase; + + this.Type.skewY = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, Math.tan(this.angle.Angle.toRadians()), 0, 1, 0, 0]; + } + this.Type.skewY.prototype = new this.Type.SkewBase; + + this.transforms = []; + this.apply = function(ctx) { + for (var i=0; i 0) ad = ad - 2 * Math.PI; + if (sweepFlag == 1 && ad < 0) ad = ad + 2 * Math.PI; + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (ctx != null) { + var r = rx > ry ? rx : ry; + var sx = rx > ry ? 1 : rx / ry; + var sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1/sx, 1/sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + } + else if (pp.command.toUpperCase() == 'Z') { + if (ctx != null) ctx.closePath(); + } + } + + return bb; + } + } + svg.Element.path.prototype = new svg.Element.PathElementBase; + + // definitions element + svg.Element.defs = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + } + svg.Element.defs.prototype = new svg.Element.ElementBase; + + // base for gradients + svg.Element.GradientBase = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.gradientUnits = this.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); + + this.stops = []; + for (var i=0; i this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value == 'indefinite') { + this.duration = 0.0 + } + else { + return false; // no updates made + } + } + this.duration = this.duration + delta; + + // if we're past the begin time + var updated = false; + if (this.begin < this.duration) { + var newValue = this.calcValue(); // tween + var attributeType = this.attribute('attributeType').value; + var attributeName = this.attribute('attributeName').value; + + if (this.parent != null) { + if (attributeType == 'CSS') { + this.parent.style(attributeName, true).value = newValue; + } + else { // default or XML + if (this.attribute('type').hasValue()) { + // for transform, etc. + var type = this.attribute('type').value; + this.parent.attribute(attributeName, true).value = type + '(' + newValue + ')'; + } + else { + this.parent.attribute(attributeName, true).value = newValue; + } + } + updated = true; + } + } + + return updated; + } + + // fraction of duration we've covered + this.progress = function() { + return ((this.duration - this.begin) / (this.maxDuration - this.begin)); + } + } + svg.Element.AnimateBase.prototype = new svg.Element.ElementBase; + + // animate element + svg.Element.animate = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var from = this.attribute('from').numValue(); + var to = this.attribute('to').numValue(); + + // tween value linearly + return from + (to - from) * this.progress(); + }; + } + svg.Element.animate.prototype = new svg.Element.AnimateBase; + + // animate color element + svg.Element.animateColor = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var from = new RGBColor(this.attribute('from').value); + var to = new RGBColor(this.attribute('to').value); + + if (from.ok && to.ok) { + // tween color linearly + var r = from.r + (to.r - from.r) * this.progress(); + var g = from.g + (to.g - from.g) * this.progress(); + var b = from.b + (to.b - from.b) * this.progress(); + return 'rgb('+parseInt(r,10)+','+parseInt(g,10)+','+parseInt(b,10)+')'; + } + return this.attribute('from').value; + }; + } + svg.Element.animateColor.prototype = new svg.Element.AnimateBase; + + // animate transform element + svg.Element.animateTransform = function(node) { + this.base = svg.Element.animate; + this.base(node); + } + svg.Element.animateTransform.prototype = new svg.Element.animate; + + // text element + svg.Element.text = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + // accumulate all the child text nodes, then trim them + this.text = ''; + for (var i=0; i + * @link http://www.phpied.com/rgb-color-parser-in-javascript/ + * @license Use it if you like it + */ +function RGBColor(color_string) +{ + this.ok = false; + + // strip any leading # + if (color_string.charAt(0) == '#') { // remove # if any + color_string = color_string.substr(1,6); + } + + color_string = color_string.replace(/ /g,''); + color_string = color_string.toLowerCase(); + + // before getting into regexps, try simple matches + // and overwrite the input + var simple_colors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred : 'cd5c5c', + indigo : '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' + }; + for (var key in simple_colors) { + if (color_string == key) { + color_string = simple_colors[key]; + } + } + // emd of simple type-in colors + + // array of color definition objects + var color_defs = [ + { + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], + process: function (bits){ + return [ + parseInt(bits[1]), + parseInt(bits[2]), + parseInt(bits[3]) + ]; + } + }, + { + re: /^(\w{2})(\w{2})(\w{2})$/, + example: ['#00ff00', '336699'], + process: function (bits){ + return [ + parseInt(bits[1], 16), + parseInt(bits[2], 16), + parseInt(bits[3], 16) + ]; + } + }, + { + re: /^(\w{1})(\w{1})(\w{1})$/, + example: ['#fb0', 'f0f'], + process: function (bits){ + return [ + parseInt(bits[1] + bits[1], 16), + parseInt(bits[2] + bits[2], 16), + parseInt(bits[3] + bits[3], 16) + ]; + } + } + ]; + + // search through the definitions to find a match + for (var i = 0; i < color_defs.length; i++) { + var re = color_defs[i].re; + var processor = color_defs[i].process; + var bits = re.exec(color_string); + if (bits) { + channels = processor(bits); + this.r = channels[0]; + this.g = channels[1]; + this.b = channels[2]; + this.ok = true; + } + + } + + // validate/cleanup values + this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); + this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); + this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); + + // some getters + this.toRGB = function () { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + } + this.toHex = function () { + var r = this.r.toString(16); + var g = this.g.toString(16); + var b = this.b.toString(16); + if (r.length == 1) r = '0' + r; + if (g.length == 1) g = '0' + g; + if (b.length == 1) b = '0' + b; + return '#' + r + g + b; + } + + // help + this.getHelpXML = function () { + + var examples = new Array(); + // add regexps + for (var i = 0; i < color_defs.length; i++) { + var example = color_defs[i].example; + for (var j = 0; j < example.length; j++) { + examples[examples.length] = example[j]; + } + } + // add type-in colors + for (var sc in simple_colors) { + examples[examples.length] = sc; + } + + var xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + for (var i = 0; i < examples.length; i++) { + try { + var list_item = document.createElement('li'); + var list_color = new RGBColor(examples[i]); + var example_div = document.createElement('div'); + example_div.style.cssText = + 'margin: 3px; ' + + 'border: 1px solid black; ' + + 'background:' + list_color.toHex() + '; ' + + 'color:' + list_color.toHex() + ; + example_div.appendChild(document.createTextNode('test')); + var list_item_value = document.createTextNode( + ' ' + examples[i] + ' -> ' + list_color.toRGB() + ' -> ' + list_color.toHex() + ); + list_item.appendChild(example_div); + list_item.appendChild(list_item_value); + xml.appendChild(list_item); + + } catch(e){} + } + return xml; + + } + +} + diff --git a/editor/images/svg_edit_icons.svg b/editor/images/svg_edit_icons.svg index 5f37fefe..d200c3d4 100644 --- a/editor/images/svg_edit_icons.svg +++ b/editor/images/svg_edit_icons.svg @@ -345,6 +345,25 @@ + + + + + + + + + + + + + + + Layer 1 + + + + diff --git a/editor/svg-editor.html b/editor/svg-editor.html index 1592a788..c3f22c2b 100644 --- a/editor/svg-editor.html +++ b/editor/svg-editor.html @@ -112,6 +112,11 @@ script type="text/javascript" src="locale/locale.min.js"> Save Image [S] +
  • +
    + Export as PNG +
  • +
  • Document Properties [I] diff --git a/editor/svg-editor.js b/editor/svg-editor.js index e1a838f0..ff08898e 100644 --- a/editor/svg-editor.js +++ b/editor/svg-editor.js @@ -268,6 +268,7 @@ '#tool_clear div,#layer_new':'new_image', '#tool_save div':'save', + '#tool_export div':'export', '#tool_open div div':'open', '#tool_import div div':'import', '#tool_source':'source', @@ -472,8 +473,7 @@ // can just provide their own custom save handler and might not want the XML prolog svg = "\n" + svg; - // Creates and opens an HTML page that provides a link to the SVG, a preview, and the markup. - // Also includes warning about Mozilla bug #308590 when applicable + // Opens the SVG in new window, with warning about Mozilla bug #308590 when applicable var win = window.open("data:image/svg+xml;base64," + Utils.encode64(svg)); @@ -502,6 +502,32 @@ } }; + var exportHandler = function(window, data) { + + var issues = data.issues; + + if(!$('#export_canvas').length) { + $('', {id: 'export_canvas'}).hide().appendTo('body'); + } + var c = $('#export_canvas')[0]; + + c.width = svgCanvas.contentW; + c.height = svgCanvas.contentH; + canvg(c, data.svg); + var datauri = c.toDataURL('image/png'); + var win = window.open(datauri); + + var note = 'Select "Save As..." in your browser to save this image as a PNG file.'; + + // Check if there's issues + + if(issues.length) { + var pre = "\n \u2022 "; + note += ("\n\nAlso note these issues:" + pre + issues.join(pre)); + } + win.alert(note); + }; + // called when we've selected a different element var selectedChanged = function(window,elems) { var mode = svgCanvas.getMode(); @@ -1219,6 +1245,7 @@ svgCanvas.bind("selected", selectedChanged); svgCanvas.bind("changed", elementChanged); svgCanvas.bind("saved", saveHandler); + svgCanvas.bind("exported", exportHandler); svgCanvas.bind("zoomed", zoomChanged); svgCanvas.bind("extension_added", extAdded); svgCanvas.textActions.setInputElem($("#text")[0]); @@ -1959,6 +1986,36 @@ svgCanvas.save(saveOpts); }; + var clickExport = function() { + if(window.canvg) { + console.log(1); + svgCanvas.rasterExport(); + return; + } else { + $.getScript('canvg/rgbcolor.js', function() { + $.getScript('canvg/canvg.js'); + // Would normally run svgCanvas.rasterExport() here, + // but that causes popup dialog box + }); + } + var count = 0; + + // Run export when window.canvg is created + var timer = setInterval(function() { + count++; + if(window.canvg) { + clearInterval(timer); + svgCanvas.rasterExport(); + return; + } + + if(count > 100) { // 5 seconds + clearInterval(timer); + $.alert("Error: Failed to load CanVG script"); + } + }, 50); + } + // by default, svgCanvas.open() is a no-op. // it is up to an extension mechanism (opera widget, etc) // to call setCustomHandlers() which will make it do something @@ -2995,6 +3052,7 @@ {sel:'#tool_zoom', fn: clickZoom, evt: 'mouseup', key: 9}, {sel:'#tool_clear', fn: clickClear, evt: 'mouseup', key: [modKey+'N', true]}, {sel:'#tool_save', fn: function() { editingsource?saveSourceEditor():clickSave()}, evt: 'mouseup', key: [modKey+'S', true]}, + {sel:'#tool_export', fn: clickExport, evt: 'mouseup'}, {sel:'#tool_open', fn: clickOpen, evt: 'mouseup', key: [modKey+'O', true]}, {sel:'#tool_import', fn: clickImport, evt: 'mouseup'}, {sel:'#tool_source', fn: showSourceEditor, evt: 'click', key: ['U', true]}, diff --git a/editor/svgcanvas.js b/editor/svgcanvas.js index 197a3c24..f0ef0a88 100644 --- a/editor/svgcanvas.js +++ b/editor/svgcanvas.js @@ -3874,6 +3874,8 @@ function BatchCommand(text) { var chardata = []; var textbb, transbb; var xform, imatrix; + var last_x, last_y; + var allow_dbl; function setCursor(index) { var empty = (textinput.value === ""); @@ -4032,6 +4034,8 @@ function BatchCommand(text) { } function selectWord(evt) { + if(!allow_dbl) return; + var ept = transformPoint( evt.pageX, evt.pageY, root_sctm ), mouse_x = ept.x * current_zoom, mouse_y = ept.y * current_zoom; @@ -4052,8 +4056,6 @@ function BatchCommand(text) { } - var last_x, last_y; - return { select: function(target, x, y) { if (curtext == target) { @@ -4097,7 +4099,7 @@ function BatchCommand(text) { }, setCursor: setCursor, toEditMode: function(x, y) { - + allow_dbl = false; current_mode = "textedit"; selectorManager.requestSelector(curtext).showGrips(false); @@ -4110,6 +4112,10 @@ function BatchCommand(text) { var pt = screenToPt(x, y); setCursorFromPoint(pt.x, pt.y); } + + setTimeout(function() { + allow_dbl = true; + }, 300); }, toSelectMode: function(selectElem) { current_mode = "select"; @@ -6187,6 +6193,35 @@ function BatchCommand(text) { call("saved", str); }; + this.rasterExport = function() { + // remove the selected outline before serializing + this.clearSelection(); + + // Check for known CanVG issues + var issues = []; + + // Selector and notice + var issue_list = { + 'feGaussianBlur': 'Blurred elements will appear as un-blurred', + 'text': 'Text may not appear as expected', + 'image': 'Image elements will not appear', + 'foreignObject': 'foreignObject elements will not appear', + 'marker': 'Marker elements (arrows, etc) will not appear', + '[stroke-dasharray]': 'Strokes will appear filled', + '[stroke^=url]': 'Strokes with gradients will not appear' + }; + var content = $(svgcontent); + + $.each(issue_list, function(sel, descr) { + if(content.find(sel).length) { + issues.push(descr); + } + }); + + var str = svgCanvasToString(); + call("exported", {svg: str, issues: issues}); + }; + // Walks the tree and executes the callback on each element in a top-down fashion var walkTree = function(elem, cbFn){ if (elem && elem.nodeType == 1) {