svgutils.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. /**
  2. * Package: svgedit.utilities
  3. *
  4. * Licensed under the Apache License, Version 2
  5. *
  6. * Copyright(c) 2010 Alexis Deveria
  7. * Copyright(c) 2010 Jeff Schiller
  8. */
  9. // Dependencies:
  10. // 1) jQuery
  11. // 2) browser.js
  12. // 3) svgtransformlist.js
  13. var svgedit = svgedit || {};
  14. (function() {
  15. if (!svgedit.utilities) {
  16. svgedit.utilities = {};
  17. }
  18. // Constants
  19. // String used to encode base64.
  20. var KEYSTR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
  21. var SVGNS = 'http://www.w3.org/2000/svg';
  22. var XLINKNS = 'http://www.w3.org/1999/xlink';
  23. var XMLNS = "http://www.w3.org/XML/1998/namespace";
  24. // Much faster than running getBBox() every time
  25. var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use';
  26. var visElems_arr = visElems.split(',');
  27. //var hidElems = 'clipPath,defs,desc,feGaussianBlur,filter,linearGradient,marker,mask,metadata,pattern,radialGradient,stop,switch,symbol,title,textPath';
  28. var editorContext_ = null;
  29. var domdoc_ = null;
  30. var domcontainer_ = null;
  31. var svgroot_ = null;
  32. svgedit.utilities.init = function(editorContext) {
  33. editorContext_ = editorContext;
  34. domdoc_ = editorContext.getDOMDocument();
  35. domcontainer_ = editorContext.getDOMContainer();
  36. svgroot_ = editorContext.getSVGRoot();
  37. };
  38. // Function: svgedit.utilities.toXml
  39. // Converts characters in a string to XML-friendly entities.
  40. //
  41. // Example: "&" becomes "&"
  42. //
  43. // Parameters:
  44. // str - The string to be converted
  45. //
  46. // Returns:
  47. // The converted string
  48. svgedit.utilities.toXml = function(str) {
  49. return $('<p/>').text(str).html();
  50. };
  51. // Function: svgedit.utilities.fromXml
  52. // Converts XML entities in a string to single characters.
  53. // Example: "&amp;" becomes "&"
  54. //
  55. // Parameters:
  56. // str - The string to be converted
  57. //
  58. // Returns:
  59. // The converted string
  60. svgedit.utilities.fromXml = function(str) {
  61. return $('<p/>').html(str).text();
  62. };
  63. // This code was written by Tyler Akins and has been placed in the
  64. // public domain. It would be nice if you left this header intact.
  65. // Base64 code from Tyler Akins -- http://rumkin.com
  66. // schiller: Removed string concatenation in favour of Array.join() optimization,
  67. // also precalculate the size of the array needed.
  68. // Function: svgedit.utilities.encode64
  69. // Converts a string to base64
  70. svgedit.utilities.encode64 = function(input) {
  71. // base64 strings are 4/3 larger than the original string
  72. // input = svgedit.utilities.encodeUTF8(input); // convert non-ASCII characters
  73. input = svgedit.utilities.convertToXMLReferences(input);
  74. if(window.btoa) return window.btoa(input); // Use native if available
  75. var output = new Array( Math.floor( (input.length + 2) / 3 ) * 4 );
  76. var chr1, chr2, chr3;
  77. var enc1, enc2, enc3, enc4;
  78. var i = 0, p = 0;
  79. do {
  80. chr1 = input.charCodeAt(i++);
  81. chr2 = input.charCodeAt(i++);
  82. chr3 = input.charCodeAt(i++);
  83. enc1 = chr1 >> 2;
  84. enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
  85. enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
  86. enc4 = chr3 & 63;
  87. if (isNaN(chr2)) {
  88. enc3 = enc4 = 64;
  89. } else if (isNaN(chr3)) {
  90. enc4 = 64;
  91. }
  92. output[p++] = KEYSTR.charAt(enc1);
  93. output[p++] = KEYSTR.charAt(enc2);
  94. output[p++] = KEYSTR.charAt(enc3);
  95. output[p++] = KEYSTR.charAt(enc4);
  96. } while (i < input.length);
  97. return output.join('');
  98. };
  99. // Function: svgedit.utilities.decode64
  100. // Converts a string from base64
  101. svgedit.utilities.decode64 = function(input) {
  102. if(window.atob) return window.atob(input);
  103. var output = "";
  104. var chr1, chr2, chr3 = "";
  105. var enc1, enc2, enc3, enc4 = "";
  106. var i = 0;
  107. // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
  108. input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
  109. do {
  110. enc1 = KEYSTR.indexOf(input.charAt(i++));
  111. enc2 = KEYSTR.indexOf(input.charAt(i++));
  112. enc3 = KEYSTR.indexOf(input.charAt(i++));
  113. enc4 = KEYSTR.indexOf(input.charAt(i++));
  114. chr1 = (enc1 << 2) | (enc2 >> 4);
  115. chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
  116. chr3 = ((enc3 & 3) << 6) | enc4;
  117. output = output + String.fromCharCode(chr1);
  118. if (enc3 != 64) {
  119. output = output + String.fromCharCode(chr2);
  120. }
  121. if (enc4 != 64) {
  122. output = output + String.fromCharCode(chr3);
  123. }
  124. chr1 = chr2 = chr3 = "";
  125. enc1 = enc2 = enc3 = enc4 = "";
  126. } while (i < input.length);
  127. return unescape(output);
  128. };
  129. // Currently not being used, so commented out for now
  130. // based on http://phpjs.org/functions/utf8_encode:577
  131. // codedread:does not seem to work with webkit-based browsers on OSX
  132. // "encodeUTF8": function(input) {
  133. // //return unescape(encodeURIComponent(input)); //may or may not work
  134. // var output = '';
  135. // for (var n = 0; n < input.length; n++){
  136. // var c = input.charCodeAt(n);
  137. // if (c < 128) {
  138. // output += input[n];
  139. // }
  140. // else if (c > 127) {
  141. // if (c < 2048){
  142. // output += String.fromCharCode((c >> 6) | 192);
  143. // }
  144. // else {
  145. // output += String.fromCharCode((c >> 12) | 224) + String.fromCharCode((c >> 6) & 63 | 128);
  146. // }
  147. // output += String.fromCharCode((c & 63) | 128);
  148. // }
  149. // }
  150. // return output;
  151. // },
  152. // Function: svgedit.utilities.convertToXMLReferences
  153. // Converts a string to use XML references
  154. svgedit.utilities.convertToXMLReferences = function(input) {
  155. var output = '';
  156. for (var n = 0; n < input.length; n++){
  157. var c = input.charCodeAt(n);
  158. if (c < 128) {
  159. output += input[n];
  160. } else if(c > 127) {
  161. output += ("&#" + c + ";");
  162. }
  163. }
  164. return output;
  165. };
  166. // Function: svgedit.utilities.text2xml
  167. // Cross-browser compatible method of converting a string to an XML tree
  168. // found this function here: http://groups.google.com/group/jquery-dev/browse_thread/thread/c6d11387c580a77f
  169. svgedit.utilities.text2xml = function(sXML) {
  170. if(sXML.indexOf('<svg:svg') >= 0) {
  171. sXML = sXML.replace(/<(\/?)svg:/g, '<$1').replace('xmlns:svg', 'xmlns');
  172. }
  173. var out;
  174. try{
  175. var dXML = (window.DOMParser)?new DOMParser():new ActiveXObject("Microsoft.XMLDOM");
  176. dXML.async = false;
  177. } catch(e){
  178. throw new Error("XML Parser could not be instantiated");
  179. };
  180. try{
  181. if(dXML.loadXML) out = (dXML.loadXML(sXML))?dXML:false;
  182. else out = dXML.parseFromString(sXML, "text/xml");
  183. }
  184. catch(e){ throw new Error("Error parsing XML string"); };
  185. return out;
  186. };
  187. // Function: svgedit.utilities.bboxToObj
  188. // Converts a SVGRect into an object.
  189. //
  190. // Parameters:
  191. // bbox - a SVGRect
  192. //
  193. // Returns:
  194. // An object with properties names x, y, width, height.
  195. svgedit.utilities.bboxToObj = function(bbox) {
  196. return {
  197. x: bbox.x,
  198. y: bbox.y,
  199. width: bbox.width,
  200. height: bbox.height
  201. }
  202. };
  203. // Function: svgedit.utilities.walkTree
  204. // Walks the tree and executes the callback on each element in a top-down fashion
  205. //
  206. // Parameters:
  207. // elem - DOM element to traverse
  208. // cbFn - Callback function to run on each element
  209. svgedit.utilities.walkTree = function(elem, cbFn){
  210. if (elem && elem.nodeType == 1) {
  211. cbFn(elem);
  212. var i = elem.childNodes.length;
  213. while (i--) {
  214. svgedit.utilities.walkTree(elem.childNodes.item(i), cbFn);
  215. }
  216. }
  217. };
  218. // Function: svgedit.utilities.walkTreePost
  219. // Walks the tree and executes the callback on each element in a depth-first fashion
  220. // TODO: FIXME: Shouldn't this be calling walkTreePost?
  221. //
  222. // Parameters:
  223. // elem - DOM element to traverse
  224. // cbFn - Callback function to run on each element
  225. svgedit.utilities.walkTreePost = function(elem, cbFn) {
  226. if (elem && elem.nodeType == 1) {
  227. var i = elem.childNodes.length;
  228. while (i--) {
  229. svgedit.utilities.walkTree(elem.childNodes.item(i), cbFn);
  230. }
  231. cbFn(elem);
  232. }
  233. };
  234. // Function: svgedit.utilities.getUrlFromAttr
  235. // Extracts the URL from the url(...) syntax of some attributes.
  236. // Three variants:
  237. // * <circle fill="url(someFile.svg#foo)" />
  238. // * <circle fill="url('someFile.svg#foo')" />
  239. // * <circle fill='url("someFile.svg#foo")' />
  240. //
  241. // Parameters:
  242. // attrVal - The attribute value as a string
  243. //
  244. // Returns:
  245. // String with just the URL, like someFile.svg#foo
  246. svgedit.utilities.getUrlFromAttr = function(attrVal) {
  247. if (attrVal) {
  248. // url("#somegrad")
  249. if (attrVal.indexOf('url("') === 0) {
  250. return attrVal.substring(5,attrVal.indexOf('"',6));
  251. }
  252. // url('#somegrad')
  253. else if (attrVal.indexOf("url('") === 0) {
  254. return attrVal.substring(5,attrVal.indexOf("'",6));
  255. }
  256. else if (attrVal.indexOf("url(") === 0) {
  257. return attrVal.substring(4,attrVal.indexOf(')'));
  258. }
  259. }
  260. return null;
  261. };
  262. // Function: svgedit.utilities.getHref
  263. // Returns the given element's xlink:href value
  264. svgedit.utilities.getHref = function(elem) {
  265. return elem.getAttributeNS(XLINKNS, "href");
  266. }
  267. // Function: svgedit.utilities.setHref
  268. // Sets the given element's xlink:href value
  269. svgedit.utilities.setHref = function(elem, val) {
  270. elem.setAttributeNS(XLINKNS, "xlink:href", val);
  271. }
  272. // Function: findDefs
  273. // Parameters:
  274. // svgElement - The <svg> element.
  275. //
  276. // Returns:
  277. // The document's <defs> element, create it first if necessary
  278. svgedit.utilities.findDefs = function(svgElement) {
  279. var svgElement = editorContext_.getSVGContent().documentElement;
  280. var defs = svgElement.getElementsByTagNameNS(SVGNS, "defs");
  281. if (defs.length > 0) {
  282. defs = defs[0];
  283. }
  284. else {
  285. // first child is a comment, so call nextSibling
  286. defs = svgElement.insertBefore( svgElement.ownerDocument.createElementNS(SVGNS, "defs" ), svgElement.firstChild.nextSibling);
  287. }
  288. return defs;
  289. };
  290. // TODO(codedread): Consider moving the next to functions to bbox.js
  291. // Function: svgedit.utilities.getPathBBox
  292. // Get correct BBox for a path in Webkit
  293. // Converted from code found here:
  294. // http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
  295. //
  296. // Parameters:
  297. // path - The path DOM element to get the BBox for
  298. //
  299. // Returns:
  300. // A BBox-like object
  301. svgedit.utilities.getPathBBox = function(path) {
  302. var seglist = path.pathSegList;
  303. var tot = seglist.numberOfItems;
  304. var bounds = [[], []];
  305. var start = seglist.getItem(0);
  306. var P0 = [start.x, start.y];
  307. for(var i=0; i < tot; i++) {
  308. var seg = seglist.getItem(i);
  309. if(!seg.x) continue;
  310. // Add actual points to limits
  311. bounds[0].push(P0[0]);
  312. bounds[1].push(P0[1]);
  313. if(seg.x1) {
  314. var P1 = [seg.x1, seg.y1],
  315. P2 = [seg.x2, seg.y2],
  316. P3 = [seg.x, seg.y];
  317. for(var j=0; j < 2; j++) {
  318. var calc = function(t) {
  319. return Math.pow(1-t,3) * P0[j]
  320. + 3 * Math.pow(1-t,2) * t * P1[j]
  321. + 3 * (1-t) * Math.pow(t,2) * P2[j]
  322. + Math.pow(t,3) * P3[j];
  323. };
  324. var b = 6 * P0[j] - 12 * P1[j] + 6 * P2[j];
  325. var a = -3 * P0[j] + 9 * P1[j] - 9 * P2[j] + 3 * P3[j];
  326. var c = 3 * P1[j] - 3 * P0[j];
  327. if(a == 0) {
  328. if(b == 0) {
  329. continue;
  330. }
  331. var t = -c / b;
  332. if(0 < t && t < 1) {
  333. bounds[j].push(calc(t));
  334. }
  335. continue;
  336. }
  337. var b2ac = Math.pow(b,2) - 4 * c * a;
  338. if(b2ac < 0) continue;
  339. var t1 = (-b + Math.sqrt(b2ac))/(2 * a);
  340. if(0 < t1 && t1 < 1) bounds[j].push(calc(t1));
  341. var t2 = (-b - Math.sqrt(b2ac))/(2 * a);
  342. if(0 < t2 && t2 < 1) bounds[j].push(calc(t2));
  343. }
  344. P0 = P3;
  345. } else {
  346. bounds[0].push(seg.x);
  347. bounds[1].push(seg.y);
  348. }
  349. }
  350. var x = Math.min.apply(null, bounds[0]);
  351. var w = Math.max.apply(null, bounds[0]) - x;
  352. var y = Math.min.apply(null, bounds[1]);
  353. var h = Math.max.apply(null, bounds[1]) - y;
  354. return {
  355. 'x': x,
  356. 'y': y,
  357. 'width': w,
  358. 'height': h
  359. };
  360. };
  361. // Function: groupBBFix
  362. // Get the given/selected element's bounding box object, checking for
  363. // horizontal/vertical lines (see issue 717)
  364. // Note that performance is currently terrible, so some way to improve would
  365. // be great.
  366. //
  367. // Parameters:
  368. // selected - Container or <use> DOM element
  369. function groupBBFix(selected) {
  370. if(svgedit.browser.supportsHVLineContainerBBox()) {
  371. try { return selected.getBBox();} catch(e){}
  372. }
  373. var ref = $.data(selected, 'ref');
  374. var matched = null;
  375. if(ref) {
  376. var copy = $(ref).children().clone().attr('visibility', 'hidden');
  377. $(svgroot_).append(copy);
  378. matched = copy.filter('line, path');
  379. } else {
  380. matched = $(selected).find('line, path');
  381. }
  382. var issue = false;
  383. if(matched.length) {
  384. matched.each(function() {
  385. var bb = this.getBBox();
  386. if(!bb.width || !bb.height) {
  387. issue = true;
  388. }
  389. });
  390. if(issue) {
  391. var elems = ref ? copy : $(selected).children();
  392. ret = getStrokedBBox(elems);
  393. } else {
  394. ret = selected.getBBox();
  395. }
  396. } else {
  397. ret = selected.getBBox();
  398. }
  399. if(ref) {
  400. copy.remove();
  401. }
  402. return ret;
  403. }
  404. // Function: svgedit.utilities.getBBox
  405. // Get the given/selected element's bounding box object, convert it to be more
  406. // usable when necessary
  407. //
  408. // Parameters:
  409. // elem - Optional DOM element to get the BBox for
  410. svgedit.utilities.getBBox = function(elem) {
  411. var selected = elem || editorContext_.geSelectedElements()[0];
  412. if (elem.nodeType != 1) return null;
  413. var ret = null;
  414. var elname = selected.nodeName;
  415. switch ( elname ) {
  416. case 'text':
  417. if(selected.textContent === '') {
  418. selected.textContent = 'a'; // Some character needed for the selector to use.
  419. ret = selected.getBBox();
  420. selected.textContent = '';
  421. } else {
  422. try { ret = selected.getBBox();} catch(e){}
  423. }
  424. break;
  425. case 'path':
  426. if(!svgedit.browser.supportsPathBBox()) {
  427. ret = svgedit.utilities.getPathBBox(selected);
  428. } else {
  429. try { ret = selected.getBBox();} catch(e){}
  430. }
  431. break;
  432. case 'g':
  433. case 'a':
  434. ret = groupBBFix(selected);
  435. break;
  436. default:
  437. if(elname === 'use') {
  438. ret = groupBBFix(selected, true);
  439. }
  440. if(elname === 'use' || elname === 'foreignObject') {
  441. if(!ret) ret = selected.getBBox();
  442. if(!svgedit.browser.isWebkit()) {
  443. var bb = {};
  444. bb.width = ret.width;
  445. bb.height = ret.height;
  446. bb.x = ret.x + parseFloat(selected.getAttribute('x')||0);
  447. bb.y = ret.y + parseFloat(selected.getAttribute('y')||0);
  448. ret = bb;
  449. }
  450. } else if(~visElems_arr.indexOf(elname)) {
  451. try { ret = selected.getBBox();}
  452. catch(e) {
  453. // Check if element is child of a foreignObject
  454. var fo = $(selected).closest("foreignObject");
  455. if(fo.length) {
  456. try {
  457. ret = fo[0].getBBox();
  458. } catch(e) {
  459. ret = null;
  460. }
  461. } else {
  462. ret = null;
  463. }
  464. }
  465. }
  466. }
  467. if(ret) {
  468. ret = svgedit.utilities.bboxToObj(ret);
  469. }
  470. // get the bounding box from the DOM (which is in that element's coordinate system)
  471. return ret;
  472. };
  473. // Function: svgedit.utilities.getRotationAngle
  474. // Get the rotation angle of the given/selected DOM element
  475. //
  476. // Parameters:
  477. // elem - Optional DOM element to get the angle for
  478. // to_rad - Boolean that when true returns the value in radians rather than degrees
  479. //
  480. // Returns:
  481. // Float with the angle in degrees or radians
  482. svgedit.utilities.getRotationAngle = function(elem, to_rad) {
  483. var selected = elem || editorContext_.getSelectedElements()[0];
  484. // find the rotation transform (if any) and set it
  485. var tlist = svgedit.transformlist.getTransformList(selected);
  486. if(!tlist) return 0; // <svg> elements have no tlist
  487. var N = tlist.numberOfItems;
  488. for (var i = 0; i < N; ++i) {
  489. var xform = tlist.getItem(i);
  490. if (xform.type == 4) {
  491. return to_rad ? xform.angle * Math.PI / 180.0 : xform.angle;
  492. }
  493. }
  494. return 0.0;
  495. };
  496. // Function: getElem
  497. // Get a DOM element by ID within the SVG root element.
  498. //
  499. // Parameters:
  500. // id - String with the element's new ID
  501. if (svgedit.browser.supportsSelectors()) {
  502. svgedit.utilities.getElem = function(id) {
  503. // querySelector lookup
  504. return svgroot_.querySelector('#'+id);
  505. };
  506. } else if (svgedit.browser.supportsXpath()) {
  507. svgedit.utilities.getElem = function(id) {
  508. // xpath lookup
  509. return domdoc_.evaluate(
  510. 'svg:svg[@id="svgroot"]//svg:*[@id="'+id+'"]',
  511. domcontainer_,
  512. function() { return "http://www.w3.org/2000/svg"; },
  513. 9,
  514. null).singleNodeValue;
  515. };
  516. } else {
  517. svgedit.utilities.getElem = function(id) {
  518. // jQuery lookup: twice as slow as xpath in FF
  519. return $(svgroot_).find('[id=' + id + ']')[0];
  520. };
  521. }
  522. // Function: assignAttributes
  523. // Assigns multiple attributes to an element.
  524. //
  525. // Parameters:
  526. // node - DOM element to apply new attribute values to
  527. // attrs - Object with attribute keys/values
  528. // suspendLength - Optional integer of milliseconds to suspend redraw
  529. // unitCheck - Boolean to indicate the need to use svgedit.units.setUnitAttr
  530. svgedit.utilities.assignAttributes = function(node, attrs, suspendLength, unitCheck) {
  531. if(!suspendLength) suspendLength = 0;
  532. // Opera has a problem with suspendRedraw() apparently
  533. var handle = null;
  534. if (!svgedit.browser.isOpera()) svgroot_.suspendRedraw(suspendLength);
  535. for (var i in attrs) {
  536. var ns = (i.substr(0,4) === "xml:" ? XMLNS :
  537. i.substr(0,6) === "xlink:" ? XLINKNS : null);
  538. if(ns) {
  539. node.setAttributeNS(ns, i, attrs[i]);
  540. } else if(!unitCheck) {
  541. node.setAttribute(i, attrs[i]);
  542. } else {
  543. svgedit.units.setUnitAttr(node, i, attrs[i]);
  544. }
  545. }
  546. if (!svgedit.browser.isOpera()) svgroot_.unsuspendRedraw(handle);
  547. };
  548. // Function: cleanupElement
  549. // Remove unneeded (default) attributes, makes resulting SVG smaller
  550. //
  551. // Parameters:
  552. // element - DOM element to clean up
  553. svgedit.utilities.cleanupElement = function(element) {
  554. var handle = svgroot_.suspendRedraw(60);
  555. var defaults = {
  556. 'fill-opacity':1,
  557. 'stop-opacity':1,
  558. 'opacity':1,
  559. 'stroke':'none',
  560. 'stroke-dasharray':'none',
  561. 'stroke-linejoin':'miter',
  562. 'stroke-linecap':'butt',
  563. 'stroke-opacity':1,
  564. 'stroke-width':1,
  565. 'rx':0,
  566. 'ry':0
  567. }
  568. for(var attr in defaults) {
  569. var val = defaults[attr];
  570. if(element.getAttribute(attr) == val) {
  571. element.removeAttribute(attr);
  572. }
  573. }
  574. svgroot_.unsuspendRedraw(handle);
  575. };
  576. })();