/editor/svgutils.js
JavaScript | 726 lines | 686 code | 9 blank | 31 comment | 1 complexity | 73e0ffdfe3b6e77249f62556649166a2 MD5 | raw file
Possible License(s): Apache-2.0
- /*globals $, svgedit, unescape, DOMParser, ActiveXObject, getStrokedBBox*/
- /*jslint vars: true, eqeq: true, bitwise: true, continue: true, forin: true*/
- /**
- * Package: svgedit.utilities
- *
- * Licensed under the MIT License
- *
- * Copyright(c) 2010 Alexis Deveria
- * Copyright(c) 2010 Jeff Schiller
- */
- // Dependencies:
- // 1) jQuery
- // 2) browser.js
- // 3) svgtransformlist.js
- // 4) units.js
- (function() {'use strict';
- if (!svgedit.utilities) {
- svgedit.utilities = {};
- }
- // Constants
- // String used to encode base64.
- var KEYSTR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
- var NS = svgedit.NS;
- // Much faster than running getBBox() every time
- var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use';
- var visElems_arr = visElems.split(',');
- //var hidElems = 'clipPath,defs,desc,feGaussianBlur,filter,linearGradient,marker,mask,metadata,pattern,radialGradient,stop,switch,symbol,title,textPath';
- var editorContext_ = null;
- var domdoc_ = null;
- var domcontainer_ = null;
- var svgroot_ = null;
- svgedit.utilities.init = function(editorContext) {
- editorContext_ = editorContext;
- domdoc_ = editorContext.getDOMDocument();
- domcontainer_ = editorContext.getDOMContainer();
- svgroot_ = editorContext.getSVGRoot();
- };
- // Function: svgedit.utilities.toXml
- // Converts characters in a string to XML-friendly entities.
- //
- // Example: '&' becomes '&'
- //
- // Parameters:
- // str - The string to be converted
- //
- // Returns:
- // The converted string
- svgedit.utilities.toXml = function(str) {
- // ' is ok in XML, but not HTML
- // > does not normally need escaping, though it can if within a CDATA expression (and preceded by "]]")
- return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/, ''');
- };
- // Function: svgedit.utilities.fromXml
- // Converts XML entities in a string to single characters.
- // Example: '&' becomes '&'
- //
- // Parameters:
- // str - The string to be converted
- //
- // Returns:
- // The converted string
- svgedit.utilities.fromXml = function(str) {
- return $('<p/>').html(str).text();
- };
- // This code was written by Tyler Akins and has been placed in the
- // public domain. It would be nice if you left this header intact.
- // Base64 code from Tyler Akins -- http://rumkin.com
- // schiller: Removed string concatenation in favour of Array.join() optimization,
- // also precalculate the size of the array needed.
- // Function: svgedit.utilities.encode64
- // Converts a string to base64
- svgedit.utilities.encode64 = function(input) {
- // base64 strings are 4/3 larger than the original string
- input = svgedit.utilities.encodeUTF8(input); // convert non-ASCII characters
- // input = svgedit.utilities.convertToXMLReferences(input);
- if (window.btoa) {
- return window.btoa(input); // Use native if available
- }
- var output = new Array( Math.floor( (input.length + 2) / 3 ) * 4 );
- var chr1, chr2, chr3;
- var enc1, enc2, enc3, enc4;
- var i = 0, p = 0;
- do {
- chr1 = input.charCodeAt(i++);
- chr2 = input.charCodeAt(i++);
- chr3 = input.charCodeAt(i++);
- enc1 = chr1 >> 2;
- enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
- enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
- enc4 = chr3 & 63;
- if (isNaN(chr2)) {
- enc3 = enc4 = 64;
- } else if (isNaN(chr3)) {
- enc4 = 64;
- }
- output[p++] = KEYSTR.charAt(enc1);
- output[p++] = KEYSTR.charAt(enc2);
- output[p++] = KEYSTR.charAt(enc3);
- output[p++] = KEYSTR.charAt(enc4);
- } while (i < input.length);
- return output.join('');
- };
- // Function: svgedit.utilities.decode64
- // Converts a string from base64
- svgedit.utilities.decode64 = function(input) {
- if(window.atob) {
- return window.atob(input);
- }
- var output = '';
- var chr1, chr2, chr3 = '';
- var enc1, enc2, enc3, enc4 = '';
- var i = 0;
- // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
- input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
- do {
- enc1 = KEYSTR.indexOf(input.charAt(i++));
- enc2 = KEYSTR.indexOf(input.charAt(i++));
- enc3 = KEYSTR.indexOf(input.charAt(i++));
- enc4 = KEYSTR.indexOf(input.charAt(i++));
- chr1 = (enc1 << 2) | (enc2 >> 4);
- chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
- chr3 = ((enc3 & 3) << 6) | enc4;
- output = output + String.fromCharCode(chr1);
- if (enc3 != 64) {
- output = output + String.fromCharCode(chr2);
- }
- if (enc4 != 64) {
- output = output + String.fromCharCode(chr3);
- }
- chr1 = chr2 = chr3 = '';
- enc1 = enc2 = enc3 = enc4 = '';
- } while (i < input.length);
- return unescape(output);
- };
- // based on http://phpjs.org/functions/utf8_encode
- // codedread:does not seem to work with webkit-based browsers on OSX // Brettz9: please test again as function upgraded
- svgedit.utilities.encodeUTF8 = function (argString) {
- //return unescape(encodeURIComponent(input)); //may or may not work
- if (argString === null || typeof argString === 'undefined') {
- return '';
- }
- // .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
- var string = String(argString);
- var utftext = '',
- start, end, stringl = 0;
- start = end = 0;
- stringl = string.length;
- var n;
- for (n = 0; n < stringl; n++) {
- var c1 = string.charCodeAt(n);
- var enc = null;
- if (c1 < 128) {
- end++;
- } else if (c1 > 127 && c1 < 2048) {
- enc = String.fromCharCode(
- (c1 >> 6) | 192, (c1 & 63) | 128
- );
- } else if ((c1 & 0xF800) != 0xD800) {
- enc = String.fromCharCode(
- (c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
- );
- } else {
- // surrogate pairs
- if ((c1 & 0xFC00) != 0xD800) {
- throw new RangeError('Unmatched trail surrogate at ' + n);
- }
- var c2 = string.charCodeAt(++n);
- if ((c2 & 0xFC00) != 0xDC00) {
- throw new RangeError('Unmatched lead surrogate at ' + (n - 1));
- }
- c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000;
- enc = String.fromCharCode(
- (c1 >> 18) | 240, ((c1 >> 12) & 63) | 128, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
- );
- }
- if (enc !== null) {
- if (end > start) {
- utftext += string.slice(start, end);
- }
- utftext += enc;
- start = end = n + 1;
- }
- }
- if (end > start) {
- utftext += string.slice(start, stringl);
- }
- return utftext;
- };
- // Function: svgedit.utilities.convertToXMLReferences
- // Converts a string to use XML references
- svgedit.utilities.convertToXMLReferences = function(input) {
- var n,
- output = '';
- for (n = 0; n < input.length; n++){
- var c = input.charCodeAt(n);
- if (c < 128) {
- output += input[n];
- } else if(c > 127) {
- output += ('&#' + c + ';');
- }
- }
- return output;
- };
- // Function: svgedit.utilities.text2xml
- // Cross-browser compatible method of converting a string to an XML tree
- // found this function here: http://groups.google.com/group/jquery-dev/browse_thread/thread/c6d11387c580a77f
- svgedit.utilities.text2xml = function(sXML) {
- if(sXML.indexOf('<svg:svg') >= 0) {
- sXML = sXML.replace(/<(\/?)svg:/g, '<$1').replace('xmlns:svg', 'xmlns');
- }
- var out, dXML;
- try{
- dXML = (window.DOMParser)?new DOMParser():new ActiveXObject('Microsoft.XMLDOM');
- dXML.async = false;
- } catch(e){
- throw new Error('XML Parser could not be instantiated');
- }
- try{
- if (dXML.loadXML) {
- out = (dXML.loadXML(sXML)) ? dXML : false;
- }
- else {
- out = dXML.parseFromString(sXML, 'text/xml');
- }
- }
- catch(e2){ throw new Error('Error parsing XML string'); }
- return out;
- };
- // Function: svgedit.utilities.bboxToObj
- // Converts a SVGRect into an object.
- //
- // Parameters:
- // bbox - a SVGRect
- //
- // Returns:
- // An object with properties names x, y, width, height.
- svgedit.utilities.bboxToObj = function(bbox) {
- return {
- x: bbox.x,
- y: bbox.y,
- width: bbox.width,
- height: bbox.height
- };
- };
- // Function: svgedit.utilities.walkTree
- // Walks the tree and executes the callback on each element in a top-down fashion
- //
- // Parameters:
- // elem - DOM element to traverse
- // cbFn - Callback function to run on each element
- svgedit.utilities.walkTree = function(elem, cbFn){
- if (elem && elem.nodeType == 1) {
- cbFn(elem);
- var i = elem.childNodes.length;
- while (i--) {
- svgedit.utilities.walkTree(elem.childNodes.item(i), cbFn);
- }
- }
- };
- // Function: svgedit.utilities.walkTreePost
- // Walks the tree and executes the callback on each element in a depth-first fashion
- // TODO: FIXME: Shouldn't this be calling walkTreePost?
- //
- // Parameters:
- // elem - DOM element to traverse
- // cbFn - Callback function to run on each element
- svgedit.utilities.walkTreePost = function(elem, cbFn) {
- if (elem && elem.nodeType == 1) {
- var i = elem.childNodes.length;
- while (i--) {
- svgedit.utilities.walkTree(elem.childNodes.item(i), cbFn);
- }
- cbFn(elem);
- }
- };
- // Function: svgedit.utilities.getUrlFromAttr
- // Extracts the URL from the url(...) syntax of some attributes.
- // Three variants:
- // * <circle fill="url(someFile.svg#foo)" />
- // * <circle fill="url('someFile.svg#foo')" />
- // * <circle fill='url("someFile.svg#foo")' />
- //
- // Parameters:
- // attrVal - The attribute value as a string
- //
- // Returns:
- // String with just the URL, like someFile.svg#foo
- svgedit.utilities.getUrlFromAttr = function(attrVal) {
- if (attrVal) {
- // url("#somegrad")
- if (attrVal.indexOf('url("') === 0) {
- return attrVal.substring(5, attrVal.indexOf('"',6));
- }
- // url('#somegrad')
- if (attrVal.indexOf("url('") === 0) {
- return attrVal.substring(5, attrVal.indexOf("'",6));
- }
- if (attrVal.indexOf("url(") === 0) {
- return attrVal.substring(4, attrVal.indexOf(')'));
- }
- }
- return null;
- };
- // Function: svgedit.utilities.getHref
- // Returns the given element's xlink:href value
- svgedit.utilities.getHref = function(elem) {
- return elem.getAttributeNS(NS.XLINK, 'href');
- };
- // Function: svgedit.utilities.setHref
- // Sets the given element's xlink:href value
- svgedit.utilities.setHref = function(elem, val) {
- elem.setAttributeNS(NS.XLINK, 'xlink:href', val);
- };
- // Function: findDefs
- //
- // Returns:
- // The document's <defs> element, create it first if necessary
- svgedit.utilities.findDefs = function() {
- var svgElement = editorContext_.getSVGContent();
- var defs = svgElement.getElementsByTagNameNS(NS.SVG, 'defs');
- if (defs.length > 0) {
- defs = defs[0];
- } else {
- defs = svgElement.ownerDocument.createElementNS(NS.SVG, 'defs');
- if (svgElement.firstChild) {
- // first child is a comment, so call nextSibling
- svgElement.insertBefore(defs, svgElement.firstChild.nextSibling);
- } else {
- svgElement.appendChild(defs);
- }
- }
- return defs;
- };
- // TODO(codedread): Consider moving the next to functions to bbox.js
- // Function: svgedit.utilities.getPathBBox
- // Get correct BBox for a path in Webkit
- // Converted from code found here:
- // http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
- //
- // Parameters:
- // path - The path DOM element to get the BBox for
- //
- // Returns:
- // A BBox-like object
- svgedit.utilities.getPathBBox = function(path) {
- var seglist = path.pathSegList;
- var tot = seglist.numberOfItems;
- var bounds = [[], []];
- var start = seglist.getItem(0);
- var P0 = [start.x, start.y];
- var i;
- for (i = 0; i < tot; i++) {
- var seg = seglist.getItem(i);
- if(typeof seg.x === 'undefined') {continue;}
- // Add actual points to limits
- bounds[0].push(P0[0]);
- bounds[1].push(P0[1]);
- if(seg.x1) {
- var P1 = [seg.x1, seg.y1],
- P2 = [seg.x2, seg.y2],
- P3 = [seg.x, seg.y];
- var j;
- for (j = 0; j < 2; j++) {
- var calc = function(t) {
- return Math.pow(1-t,3) * P0[j]
- + 3 * Math.pow(1-t,2) * t * P1[j]
- + 3 * (1-t) * Math.pow(t, 2) * P2[j]
- + Math.pow(t,3) * P3[j];
- };
- var b = 6 * P0[j] - 12 * P1[j] + 6 * P2[j];
- var a = -3 * P0[j] + 9 * P1[j] - 9 * P2[j] + 3 * P3[j];
- var c = 3 * P1[j] - 3 * P0[j];
- if (a == 0) {
- if (b == 0) {
- continue;
- }
- var t = -c / b;
- if (0 < t && t < 1) {
- bounds[j].push(calc(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) {bounds[j].push(calc(t1));}
- var t2 = (-b - Math.sqrt(b2ac))/(2 * a);
- if (0 < t2 && t2 < 1) {bounds[j].push(calc(t2));}
- }
- P0 = P3;
- } else {
- bounds[0].push(seg.x);
- bounds[1].push(seg.y);
- }
- }
- var x = Math.min.apply(null, bounds[0]);
- var w = Math.max.apply(null, bounds[0]) - x;
- var y = Math.min.apply(null, bounds[1]);
- var h = Math.max.apply(null, bounds[1]) - y;
- return {
- 'x': x,
- 'y': y,
- 'width': w,
- 'height': h
- };
- };
- // Function: groupBBFix
- // Get the given/selected element's bounding box object, checking for
- // horizontal/vertical lines (see issue 717)
- // Note that performance is currently terrible, so some way to improve would
- // be great.
- //
- // Parameters:
- // selected - Container or <use> DOM element
- function groupBBFix(selected) {
- if(svgedit.browser.supportsHVLineContainerBBox()) {
- try { return selected.getBBox();} catch(e){}
- }
- var ref = $.data(selected, 'ref');
- var matched = null;
- var ret, copy;
- if(ref) {
- copy = $(ref).children().clone().attr('visibility', 'hidden');
- $(svgroot_).append(copy);
- matched = copy.filter('line, path');
- } else {
- matched = $(selected).find('line, path');
- }
- var issue = false;
- if(matched.length) {
- matched.each(function() {
- var bb = this.getBBox();
- if(!bb.width || !bb.height) {
- issue = true;
- }
- });
- if(issue) {
- var elems = ref ? copy : $(selected).children();
- ret = getStrokedBBox(elems); // getStrokedBBox defined in svgcanvas
- } else {
- ret = selected.getBBox();
- }
- } else {
- ret = selected.getBBox();
- }
- if(ref) {
- copy.remove();
- }
- return ret;
- }
- // Function: svgedit.utilities.getBBox
- // Get the given/selected element's bounding box object, convert it to be more
- // usable when necessary
- //
- // Parameters:
- // elem - Optional DOM element to get the BBox for
- svgedit.utilities.getBBox = function(elem) {
- var selected = elem || editorContext_.geSelectedElements()[0];
- if (elem.nodeType != 1) {return null;}
- var ret = null;
- var elname = selected.nodeName;
- switch ( elname ) {
- case 'text':
- if(selected.textContent === '') {
- selected.textContent = 'a'; // Some character needed for the selector to use.
- ret = selected.getBBox();
- selected.textContent = '';
- } else {
- try { ret = selected.getBBox();} catch(e){}
- }
- break;
- case 'path':
- if(!svgedit.browser.supportsPathBBox()) {
- ret = svgedit.utilities.getPathBBox(selected);
- } else {
- try { ret = selected.getBBox();} catch(e2){}
- }
- break;
- case 'g':
- case 'a':
- ret = groupBBFix(selected);
- break;
- default:
- if(elname === 'use') {
- ret = groupBBFix(selected, true);
- }
- if(elname === 'use' || ( elname === 'foreignObject' && svgedit.browser.isWebkit() ) ) {
- if(!ret) {ret = selected.getBBox();}
- // This is resolved in later versions of webkit, perhaps we should
- // have a featured detection for correct 'use' behavior?
- //
- //if(!svgedit.browser.isWebkit()) {
- var bb = {};
- bb.width = ret.width;
- bb.height = ret.height;
- bb.x = ret.x + parseFloat(selected.getAttribute('x')||0);
- bb.y = ret.y + parseFloat(selected.getAttribute('y')||0);
- ret = bb;
- //}
- } else if(~visElems_arr.indexOf(elname)) {
- try { ret = selected.getBBox();}
- catch(e3) {
- // Check if element is child of a foreignObject
- var fo = $(selected).closest('foreignObject');
- if(fo.length) {
- try {
- ret = fo[0].getBBox();
- } catch(e4) {
- ret = null;
- }
- } else {
- ret = null;
- }
- }
- }
- }
- if(ret) {
- ret = svgedit.utilities.bboxToObj(ret);
- }
- // get the bounding box from the DOM (which is in that element's coordinate system)
- return ret;
- };
- // Function: svgedit.utilities.getRotationAngle
- // Get the rotation angle of the given/selected DOM element
- //
- // Parameters:
- // elem - Optional DOM element to get the angle for
- // to_rad - Boolean that when true returns the value in radians rather than degrees
- //
- // Returns:
- // Float with the angle in degrees or radians
- svgedit.utilities.getRotationAngle = function(elem, to_rad) {
- var selected = elem || editorContext_.getSelectedElements()[0];
- // find the rotation transform (if any) and set it
- var tlist = svgedit.transformlist.getTransformList(selected);
- if(!tlist) {return 0;} // <svg> elements have no tlist
- var N = tlist.numberOfItems;
- var i;
- for (i = 0; i < N; ++i) {
- var xform = tlist.getItem(i);
- if (xform.type == 4) {
- return to_rad ? xform.angle * Math.PI / 180.0 : xform.angle;
- }
- }
- return 0.0;
- };
- // Function getRefElem
- // Get the reference element associated with the given attribute value
- //
- // Parameters:
- // attrVal - The attribute value as a string
- svgedit.utilities.getRefElem = function(attrVal) {
- return svgedit.utilities.getElem(svgedit.utilities.getUrlFromAttr(attrVal).substr(1));
- };
- // Function: getElem
- // Get a DOM element by ID within the SVG root element.
- //
- // Parameters:
- // id - String with the element's new ID
- if (svgedit.browser.supportsSelectors()) {
- svgedit.utilities.getElem = function(id) {
- // querySelector lookup
- return svgroot_.querySelector('#'+id);
- };
- } else if (svgedit.browser.supportsXpath()) {
- svgedit.utilities.getElem = function(id) {
- // xpath lookup
- return domdoc_.evaluate(
- 'svg:svg[@id="svgroot"]//svg:*[@id="'+id+'"]',
- domcontainer_,
- function() { return svgedit.NS.SVG; },
- 9,
- null).singleNodeValue;
- };
- } else {
- svgedit.utilities.getElem = function(id) {
- // jQuery lookup: twice as slow as xpath in FF
- return $(svgroot_).find('[id=' + id + ']')[0];
- };
- }
- // Function: assignAttributes
- // Assigns multiple attributes to an element.
- //
- // Parameters:
- // node - DOM element to apply new attribute values to
- // attrs - Object with attribute keys/values
- // suspendLength - Optional integer of milliseconds to suspend redraw
- // unitCheck - Boolean to indicate the need to use svgedit.units.setUnitAttr
- svgedit.utilities.assignAttributes = function(node, attrs, suspendLength, unitCheck) {
- if(!suspendLength) {suspendLength = 0;}
- // Opera has a problem with suspendRedraw() apparently
- var handle = null;
- if (!svgedit.browser.isOpera()) {svgroot_.suspendRedraw(suspendLength);}
- var i;
- for (i in attrs) {
- var ns = (i.substr(0,4) === 'xml:' ? NS.XML :
- i.substr(0,6) === 'xlink:' ? NS.XLINK : null);
- if(ns) {
- node.setAttributeNS(ns, i, attrs[i]);
- } else if(!unitCheck) {
- node.setAttribute(i, attrs[i]);
- } else {
- svgedit.units.setUnitAttr(node, i, attrs[i]);
- }
- }
- if (!svgedit.browser.isOpera()) {svgroot_.unsuspendRedraw(handle);}
- };
- // Function: cleanupElement
- // Remove unneeded (default) attributes, makes resulting SVG smaller
- //
- // Parameters:
- // element - DOM element to clean up
- svgedit.utilities.cleanupElement = function(element) {
- var handle = svgroot_.suspendRedraw(60);
- var defaults = {
- 'fill-opacity':1,
- 'stop-opacity':1,
- 'opacity':1,
- 'stroke':'none',
- 'stroke-dasharray':'none',
- 'stroke-linejoin':'miter',
- 'stroke-linecap':'butt',
- 'stroke-opacity':1,
- 'stroke-width':1,
- 'rx':0,
- 'ry':0
- };
- var attr;
- for (attr in defaults) {
- var val = defaults[attr];
- if(element.getAttribute(attr) == val) {
- element.removeAttribute(attr);
- }
- }
- svgroot_.unsuspendRedraw(handle);
- };
- // Function: snapToGrid
- // round value to for snapping
- // NOTE: This function did not move to svgutils.js since it depends on curConfig.
- svgedit.utilities.snapToGrid = function(value) {
- var stepSize = editorContext_.getSnappingStep();
- var unit = editorContext_.getBaseUnit();
- if (unit !== "px") {
- stepSize *= svgedit.units.getTypeMap()[unit];
- }
- value = Math.round(value/stepSize)*stepSize;
- return value;
- };
- svgedit.utilities.preg_quote = function (str, delimiter) {
- // From: http://phpjs.org/functions
- return String(str).replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&');
- };
- }());