saveSvgAsPng.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. (function() {
  2. var out$ = typeof exports != 'undefined' && exports || typeof define != 'undefined' && {} || this;
  3. var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [<!ENTITY nbsp "&#160;">]>';
  4. function isElement(obj) {
  5. return obj instanceof HTMLElement || obj instanceof SVGElement;
  6. }
  7. function requireDomNode(el) {
  8. if (!isElement(el)) {
  9. throw new Error('an HTMLElement or SVGElement is required; got ' + el);
  10. }
  11. }
  12. function isExternal(url) {
  13. return url && url.lastIndexOf('http',0) == 0 && url.lastIndexOf(window.location.host) == -1;
  14. }
  15. function inlineImages(el, callback) {
  16. requireDomNode(el);
  17. var images = el.querySelectorAll('image'),
  18. left = images.length,
  19. checkDone = function() {
  20. if (left === 0) {
  21. callback();
  22. }
  23. };
  24. checkDone();
  25. for (var i = 0; i < images.length; i++) {
  26. (function(image) {
  27. var canvas = document.createElement('canvas');
  28. var ctx = canvas.getContext('2d');
  29. var img = new Image();
  30. img.crossOrigin="anonymous";
  31. var href = image.getAttributeNS("http://www.w3.org/1999/xlink", "href");
  32. href = href || image.getAttribute('href');
  33. if (isExternal(href)) {
  34. href += (href.indexOf('?') === -1 ? '?' : '&') + 't=' + (new Date().valueOf());
  35. }
  36. if (href) {
  37. img.src = href;
  38. img.onload = function() {
  39. canvas.width = img.width;
  40. canvas.height = img.height;
  41. ctx.drawImage(img, 0, 0);
  42. image.setAttributeNS("http://www.w3.org/1999/xlink", "href", canvas.toDataURL('image/png'));
  43. left--;
  44. checkDone();
  45. }
  46. img.onerror = function() {
  47. console.log("Could not load "+href);
  48. left--;
  49. checkDone();
  50. }
  51. } else {
  52. left--;
  53. checkDone();
  54. }
  55. })(images[i]);
  56. }
  57. }
  58. function styles(el, options, cssLoadedCallback) {
  59. var selectorRemap = options.selectorRemap;
  60. var modifyStyle = options.modifyStyle;
  61. var modifyCss = options.modifyCss || function(selector, properties) {
  62. var selector = selectorRemap ? selectorRemap(selector) : selector;
  63. var cssText = modifyStyle ? modifyStyle(properties) : properties;
  64. return selector + " { " + cssText + " }\n";
  65. };
  66. var css = "";
  67. // Each font that has an external link is saved into queue, and processed asynchronously.
  68. var fontsQueue = [];
  69. var sheets = document.styleSheets;
  70. for (var i = 0; i < sheets.length; i++) {
  71. try {
  72. var rules = sheets[i].cssRules;
  73. } catch (e) {
  74. console.warn("Stylesheet could not be loaded: "+sheets[i].href);
  75. continue;
  76. }
  77. if (rules != null) {
  78. for (var j = 0, match; j < rules.length; j++, match = null) {
  79. var rule = rules[j];
  80. if (typeof(rule.style) != "undefined") {
  81. var selectorText;
  82. try {
  83. selectorText = rule.selectorText;
  84. } catch(err) {
  85. console.warn('The following CSS rule has an invalid selector: "' + rule + '"', err);
  86. }
  87. try {
  88. if (selectorText) {
  89. match = el.querySelector(selectorText) || (el.parentNode && el.parentNode.querySelector(selectorText));
  90. }
  91. } catch(err) {
  92. console.warn('Invalid CSS selector "' + selectorText + '"', err);
  93. }
  94. if (match) {
  95. css += modifyCss(rule.selectorText, rule.style.cssText);
  96. } else if(rule.cssText.match(/^@font-face/)) {
  97. // below we are trying to find matches to external link. E.g.
  98. // @font-face {
  99. // // ...
  100. // src: local('Abel'), url(https://fonts.gstatic.com/s/abel/v6/UzN-iejR1VoXU2Oc-7LsbvesZW2xOQ-xsNqO47m55DA.woff2);
  101. // }
  102. //
  103. // This regex will save extrnal link into first capture group
  104. var fontUrlRegexp = /url\(["']?(.+?)["']?\)/;
  105. // TODO: This needs to be changed to support multiple url declarations per font.
  106. var fontUrlMatch = rule.cssText.match(fontUrlRegexp);
  107. var externalFontUrl = (fontUrlMatch && fontUrlMatch[1]) || '';
  108. var fontUrlIsDataURI = externalFontUrl.match(/^data:/);
  109. if (fontUrlIsDataURI) {
  110. // We should ignore data uri - they are already embedded
  111. externalFontUrl = '';
  112. }
  113. if (externalFontUrl === 'about:blank') {
  114. // no point trying to load this
  115. externalFontUrl = '';
  116. }
  117. if (externalFontUrl) {
  118. // okay, we are lucky. We can fetch this font later
  119. //handle url if relative
  120. if (externalFontUrl.startsWith('../')) {
  121. externalFontUrl = sheets[i].href + '/../' + externalFontUrl
  122. } else if (externalFontUrl.startsWith('./')) {
  123. externalFontUrl = sheets[i].href + '/.' + externalFontUrl
  124. }
  125. fontsQueue.push({
  126. text: rule.cssText,
  127. // Pass url regex, so that once font is downladed, we can run `replace()` on it
  128. fontUrlRegexp: fontUrlRegexp,
  129. format: getFontMimeTypeFromUrl(externalFontUrl),
  130. url: externalFontUrl
  131. });
  132. } else {
  133. // otherwise, use previous logic
  134. css += rule.cssText + '\n';
  135. }
  136. }
  137. }
  138. }
  139. }
  140. }
  141. // Now all css is processed, it's time to handle scheduled fonts
  142. processFontQueue(fontsQueue);
  143. function getFontMimeTypeFromUrl(fontUrl) {
  144. var supportedFormats = {
  145. 'woff2': 'font/woff2',
  146. 'woff': 'font/woff',
  147. 'otf': 'application/x-font-opentype',
  148. 'ttf': 'application/x-font-ttf',
  149. 'eot': 'application/vnd.ms-fontobject',
  150. 'sfnt': 'application/font-sfnt',
  151. 'svg': 'image/svg+xml'
  152. };
  153. var extensions = Object.keys(supportedFormats);
  154. for (var i = 0; i < extensions.length; ++i) {
  155. var extension = extensions[i];
  156. // TODO: This is not bullet proof, it needs to handle edge cases...
  157. if (fontUrl.indexOf('.' + extension) > 0) {
  158. return supportedFormats[extension];
  159. }
  160. }
  161. // If you see this error message, you probably need to update code above.
  162. console.error('Unknown font format for ' + fontUrl+ '; Fonts may not be working correctly');
  163. return 'application/octet-stream';
  164. }
  165. function processFontQueue(queue) {
  166. if (queue.length > 0) {
  167. // load fonts one by one until we have anything in the queue:
  168. var font = queue.pop();
  169. processNext(font);
  170. } else {
  171. // no more fonts to load.
  172. cssLoadedCallback(css);
  173. }
  174. function processNext(font) {
  175. // TODO: This could benefit from caching.
  176. var oReq = new XMLHttpRequest();
  177. oReq.addEventListener('load', fontLoaded);
  178. oReq.addEventListener('error', transferFailed);
  179. oReq.addEventListener('abort', transferFailed);
  180. oReq.open('GET', font.url);
  181. oReq.responseType = 'arraybuffer';
  182. oReq.send();
  183. function fontLoaded() {
  184. // TODO: it may be also worth to wait until fonts are fully loaded before
  185. // attempting to rasterize them. (e.g. use https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet )
  186. var fontBits = oReq.response;
  187. var fontInBase64 = arrayBufferToBase64(fontBits);
  188. updateFontStyle(font, fontInBase64);
  189. }
  190. function transferFailed(e) {
  191. console.warn('Failed to load font from: ' + font.url);
  192. console.warn(e)
  193. css += font.text + '\n';
  194. processFontQueue(queue);
  195. }
  196. function updateFontStyle(font, fontInBase64) {
  197. var dataUrl = 'url("data:' + font.format + ';base64,' + fontInBase64 + '")';
  198. css += font.text.replace(font.fontUrlRegexp, dataUrl) + '\n';
  199. // schedule next font download on next tick.
  200. setTimeout(function() {
  201. processFontQueue(queue)
  202. }, 0);
  203. }
  204. }
  205. }
  206. function arrayBufferToBase64(buffer) {
  207. var binary = '';
  208. var bytes = new Uint8Array(buffer);
  209. var len = bytes.byteLength;
  210. for (var i = 0; i < len; i++) {
  211. binary += String.fromCharCode(bytes[i]);
  212. }
  213. return window.btoa(binary);
  214. }
  215. }
  216. function getDimension(el, clone, dim) {
  217. var v = (el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal[dim]) ||
  218. (clone.getAttribute(dim) !== null && !clone.getAttribute(dim).match(/%$/) && parseInt(clone.getAttribute(dim))) ||
  219. el.getBoundingClientRect()[dim] ||
  220. parseInt(clone.style[dim]) ||
  221. parseInt(window.getComputedStyle(el).getPropertyValue(dim));
  222. return (typeof v === 'undefined' || v === null || isNaN(parseFloat(v))) ? 0 : v;
  223. }
  224. function reEncode(data) {
  225. data = encodeURIComponent(data);
  226. data = data.replace(/%([0-9A-F]{2})/g, function(match, p1) {
  227. var c = String.fromCharCode('0x'+p1);
  228. return c === '%' ? '%25' : c;
  229. });
  230. return decodeURIComponent(data);
  231. }
  232. out$.prepareSvg = function(el, options, cb) {
  233. requireDomNode(el);
  234. options = options || {};
  235. options.scale = options.scale || 1;
  236. options.responsive = options.responsive || false;
  237. var xmlns = "http://www.w3.org/2000/xmlns/";
  238. inlineImages(el, function() {
  239. var outer = document.createElement("div");
  240. var clone = el.cloneNode(true);
  241. var width, height;
  242. if(el.tagName == 'svg') {
  243. width = options.width || getDimension(el, clone, 'width');
  244. height = options.height || getDimension(el, clone, 'height');
  245. } else if(el.getBBox) {
  246. var box = el.getBBox();
  247. width = box.x + box.width;
  248. height = box.y + box.height;
  249. clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, ''));
  250. var svg = document.createElementNS('http://www.w3.org/2000/svg','svg')
  251. svg.appendChild(clone)
  252. clone = svg;
  253. } else {
  254. console.error('Attempted to render non-SVG element', el);
  255. return;
  256. }
  257. clone.setAttribute("version", "1.1");
  258. if (!clone.getAttribute('xmlns')) {
  259. clone.setAttributeNS(xmlns, "xmlns", "http://www.w3.org/2000/svg");
  260. }
  261. if (!clone.getAttribute('xmlns:xlink')) {
  262. clone.setAttributeNS(xmlns, "xmlns:xlink", "http://www.w3.org/1999/xlink");
  263. }
  264. if (options.responsive) {
  265. clone.removeAttribute('width');
  266. clone.removeAttribute('height');
  267. clone.setAttribute('preserveAspectRatio', 'xMinYMin meet');
  268. } else {
  269. clone.setAttribute("width", width * options.scale);
  270. clone.setAttribute("height", height * options.scale);
  271. }
  272. clone.setAttribute("viewBox", [
  273. options.left || 0,
  274. options.top || 0,
  275. width,
  276. height
  277. ].join(" "));
  278. var fos = clone.querySelectorAll('foreignObject > *');
  279. for (var i = 0; i < fos.length; i++) {
  280. if (!fos[i].getAttribute('xmlns')) {
  281. fos[i].setAttributeNS(xmlns, "xmlns", "http://www.w3.org/1999/xhtml");
  282. }
  283. }
  284. outer.appendChild(clone);
  285. // In case of custom fonts we need to fetch font first, and then inline
  286. // its url into data-uri format (encode as base64). That's why style
  287. // processing is done asynchonously. Once all inlining is finshed
  288. // cssLoadedCallback() is called.
  289. styles(el, options, cssLoadedCallback);
  290. function cssLoadedCallback(css) {
  291. // here all fonts are inlined, so that we can render them properly.
  292. var s = document.createElement('style');
  293. s.setAttribute('type', 'text/css');
  294. s.innerHTML = "<![CDATA[\n" + css + "\n]]>";
  295. var defs = document.createElement('defs');
  296. defs.appendChild(s);
  297. clone.insertBefore(defs, clone.firstChild);
  298. if (cb) {
  299. var outHtml = outer.innerHTML;
  300. outHtml = outHtml.replace(/NS\d+:href/gi, 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href');
  301. cb(outHtml, width, height);
  302. }
  303. }
  304. });
  305. }
  306. out$.svgAsDataUri = function(el, options, cb) {
  307. out$.prepareSvg(el, options, function(svg) {
  308. var uri = 'data:image/svg+xml;base64,' + window.btoa(reEncode(doctype + svg));
  309. if (cb) {
  310. cb(uri);
  311. }
  312. });
  313. }
  314. out$.svgAsPngUri = function(el, options, cb) {
  315. requireDomNode(el);
  316. options = options || {};
  317. options.encoderType = options.encoderType || 'image/png';
  318. options.encoderOptions = options.encoderOptions || 0.8;
  319. var convertToPng = function(src, w, h) {
  320. var canvas = document.createElement('canvas');
  321. var context = canvas.getContext('2d');
  322. canvas.width = w;
  323. canvas.height = h;
  324. var pixelRatio = window.devicePixelRatio || 1;
  325. canvas.style.width = canvas.width+'px';
  326. canvas.style.height = canvas.height+'px';
  327. canvas.width *= pixelRatio;
  328. canvas.height *= pixelRatio;
  329. context.setTransform(pixelRatio,0,0,pixelRatio,0,0);
  330. if(options.canvg) {
  331. options.canvg(canvas, src);
  332. } else {
  333. context.drawImage(src, 0, 0);
  334. }
  335. if(options.backgroundColor){
  336. context.globalCompositeOperation = 'destination-over';
  337. context.fillStyle = options.backgroundColor;
  338. context.fillRect(0, 0, canvas.width, canvas.height);
  339. }
  340. var png;
  341. try {
  342. png = canvas.toDataURL(options.encoderType, options.encoderOptions);
  343. } catch (e) {
  344. if ((typeof SecurityError !== 'undefined' && e instanceof SecurityError) || e.name == "SecurityError") {
  345. console.error("Rendered SVG images cannot be downloaded in this browser.");
  346. return;
  347. } else {
  348. throw e;
  349. }
  350. }
  351. cb(png);
  352. }
  353. if(options.canvg) {
  354. out$.prepareSvg(el, options, convertToPng);
  355. } else {
  356. out$.svgAsDataUri(el, options, function(uri) {
  357. var image = new Image();
  358. image.onload = function() {
  359. convertToPng(image, image.width, image.height);
  360. }
  361. image.onerror = function() {
  362. console.error(
  363. 'There was an error loading the data URI as an image on the following SVG\n',
  364. window.atob(uri.slice(26)), '\n',
  365. "Open the following link to see browser's diagnosis\n",
  366. uri);
  367. }
  368. image.src = uri;
  369. });
  370. }
  371. }
  372. out$.download = function(name, uri) {
  373. if (navigator.msSaveOrOpenBlob) {
  374. navigator.msSaveOrOpenBlob(uriToBlob(uri), name);
  375. } else {
  376. var saveLink = document.createElement('a');
  377. if ('download' in saveLink) {
  378. saveLink.download = name;
  379. saveLink.style.display = 'none';
  380. document.body.appendChild(saveLink);
  381. try {
  382. var blob = uriToBlob(uri);
  383. var url = URL.createObjectURL(blob);
  384. saveLink.href = url;
  385. saveLink.onclick = function() {
  386. requestAnimationFrame(function() {
  387. URL.revokeObjectURL(url);
  388. })
  389. };
  390. } catch (e) {
  391. console.warn('This browser does not support object URLs. Falling back to string URL.');
  392. saveLink.href = uri;
  393. }
  394. saveLink.click();
  395. document.body.removeChild(saveLink);
  396. }
  397. else {
  398. window.open(uri, '_temp', 'menubar=no,toolbar=no,status=no');
  399. }
  400. }
  401. }
  402. function uriToBlob(uri) {
  403. var byteString = window.atob(uri.split(',')[1]);
  404. var mimeString = uri.split(',')[0].split(':')[1].split(';')[0]
  405. var buffer = new ArrayBuffer(byteString.length);
  406. var intArray = new Uint8Array(buffer);
  407. for (var i = 0; i < byteString.length; i++) {
  408. intArray[i] = byteString.charCodeAt(i);
  409. }
  410. return new Blob([buffer], {type: mimeString});
  411. }
  412. out$.saveSvg = function(el, name, options) {
  413. requireDomNode(el);
  414. options = options || {};
  415. out$.svgAsDataUri(el, options, function(uri) {
  416. out$.download(name, uri);
  417. });
  418. }
  419. out$.saveSvgAsPng = function(el, name, options) {
  420. requireDomNode(el);
  421. options = options || {};
  422. out$.svgAsPngUri(el, options, function(uri) {
  423. out$.download(name, uri);
  424. });
  425. }
  426. // if define is defined create as an AMD module
  427. if (typeof define !== 'undefined') {
  428. define(function() {
  429. return out$;
  430. });
  431. }
  432. })();