/*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(/'/, '''); }; // 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 $('

').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('= 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: // * // * // * // // 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 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 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;} // 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'), '\\$&'); }; }());