saveSvgAsPng.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. (function() {
  2. const out$ = typeof exports != 'undefined' && exports || typeof define != 'undefined' && {} || this || window;
  3. if (typeof define !== 'undefined') define('save-svg-as-png', [], () => out$);
  4. const xmlns = 'http://www.w3.org/2000/xmlns/';
  5. const 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;">]>';
  6. const urlRegex = /url\(["']?(.+?)["']?\)/;
  7. const fontFormats = {
  8. woff2: 'font/woff2',
  9. woff: 'font/woff',
  10. otf: 'application/x-font-opentype',
  11. ttf: 'application/x-font-ttf',
  12. eot: 'application/vnd.ms-fontobject',
  13. sfnt: 'application/font-sfnt',
  14. svg: 'image/svg+xml'
  15. };
  16. const isElement = obj => obj instanceof HTMLElement || obj instanceof SVGElement;
  17. const requireDomNode = el => {
  18. if (!isElement(el)) throw new Error(`an HTMLElement or SVGElement is required; got ${el}`);
  19. };
  20. const isExternal = url => url && url.lastIndexOf('http',0) === 0 && url.lastIndexOf(window.location.host) === -1;
  21. const getFontMimeTypeFromUrl = fontUrl => {
  22. const formats = Object.keys(fontFormats)
  23. .filter(extension => fontUrl.indexOf(`.${extension}`) > 0)
  24. .map(extension => fontFormats[extension]);
  25. if (formats) return formats[0];
  26. console.error(`Unknown font format for ${fontUrl}. Fonts may not be working correctly.`);
  27. return 'application/octet-stream';
  28. };
  29. const arrayBufferToBase64 = buffer => {
  30. let binary = '';
  31. const bytes = new Uint8Array(buffer);
  32. for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
  33. return window.btoa(binary);
  34. }
  35. const getDimension = (el, clone, dim) => {
  36. const v =
  37. (el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal[dim]) ||
  38. (clone.getAttribute(dim) !== null && !clone.getAttribute(dim).match(/%$/) && parseInt(clone.getAttribute(dim))) ||
  39. el.getBoundingClientRect()[dim] ||
  40. parseInt(clone.style[dim]) ||
  41. parseInt(window.getComputedStyle(el).getPropertyValue(dim));
  42. return typeof v === 'undefined' || v === null || isNaN(parseFloat(v)) ? 0 : v;
  43. };
  44. const getDimensions = (el, clone, width, height) => {
  45. if (el.tagName === 'svg') return {
  46. width: width || getDimension(el, clone, 'width'),
  47. height: height || getDimension(el, clone, 'height')
  48. };
  49. else if (el.getBBox) {
  50. const {x, y, width, height} = el.getBBox();
  51. return {
  52. width: x + width,
  53. height: y + height
  54. };
  55. }
  56. };
  57. const reEncode = data =>
  58. decodeURIComponent(
  59. encodeURIComponent(data)
  60. .replace(/%([0-9A-F]{2})/g, (match, p1) => {
  61. const c = String.fromCharCode(`0x${p1}`);
  62. return c === '%' ? '%25' : c;
  63. })
  64. );
  65. const uriToBlob = uri => {
  66. const byteString = window.atob(uri.split(',')[1]);
  67. const mimeString = uri.split(',')[0].split(':')[1].split(';')[0]
  68. const buffer = new ArrayBuffer(byteString.length);
  69. const intArray = new Uint8Array(buffer);
  70. for (let i = 0; i < byteString.length; i++) {
  71. intArray[i] = byteString.charCodeAt(i);
  72. }
  73. return new Blob([buffer], {type: mimeString});
  74. };
  75. const query = (el, selector) => {
  76. if (!selector) return;
  77. try {
  78. return el.querySelector(selector) || el.parentNode && el.parentNode.querySelector(selector);
  79. } catch(err) {
  80. console.warn(`Invalid CSS selector "${selector}"`, err);
  81. }
  82. };
  83. const detectCssFont = (rule, href) => {
  84. // Match CSS font-face rules to external links.
  85. // @font-face {
  86. // src: local('Abel'), url(https://fonts.gstatic.com/s/abel/v6/UzN-iejR1VoXU2Oc-7LsbvesZW2xOQ-xsNqO47m55DA.woff2);
  87. // }
  88. const match = rule.cssText.match(urlRegex);
  89. const url = (match && match[1]) || '';
  90. if (!url || url.match(/^data:/) || url === 'about:blank') return;
  91. const fullUrl =
  92. url.startsWith('../') ? `${href}/../${url}`
  93. : url.startsWith('./') ? `${href}/.${url}`
  94. : url;
  95. return {
  96. text: rule.cssText,
  97. format: getFontMimeTypeFromUrl(fullUrl),
  98. url: fullUrl
  99. };
  100. };
  101. const inlineImages = el => Promise.all(
  102. Array.from(el.querySelectorAll('image')).map(image => {
  103. let href = image.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || image.getAttribute('href');
  104. if (!href) return Promise.resolve(null);
  105. if (isExternal(href)) {
  106. href += (href.indexOf('?') === -1 ? '?' : '&') + 't=' + new Date().valueOf();
  107. }
  108. return new Promise((resolve, reject) => {
  109. const canvas = document.createElement('canvas');
  110. const img = new Image();
  111. img.crossOrigin = 'anonymous';
  112. img.src = href;
  113. img.onerror = () => reject(new Error(`Could not load ${href}`));
  114. img.onload = () => {
  115. canvas.width = img.width;
  116. canvas.height = img.height;
  117. canvas.getContext('2d').drawImage(img, 0, 0);
  118. image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', canvas.toDataURL('image/png'));
  119. resolve(true);
  120. };
  121. });
  122. })
  123. );
  124. const cachedFonts = {};
  125. const inlineFonts = fonts => Promise.all(
  126. fonts.map(font =>
  127. new Promise((resolve, reject) => {
  128. if (cachedFonts[font.url]) return resolve(cachedFonts[font.url]);
  129. const req = new XMLHttpRequest();
  130. req.addEventListener('load', () => {
  131. // TODO: it may also be worth it to wait until fonts are fully loaded before
  132. // attempting to rasterize them. (e.g. use https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet)
  133. const fontInBase64 = arrayBufferToBase64(req.response);
  134. const fontUri = font.text.replace(urlRegex, `url("data:${font.format};base64,${fontInBase64}")`)+'\n';
  135. cachedFonts[font.url] = fontUri;
  136. resolve(fontUri);
  137. });
  138. req.addEventListener('error', e => {
  139. console.warn(`Failed to load font from: ${font.url}`, e);
  140. cachedFonts[font.url] = null;
  141. resolve(null);
  142. });
  143. req.addEventListener('abort', e => {
  144. console.warn(`Aborted loading font from: ${font.url}`, e);
  145. resolve(null);
  146. });
  147. req.open('GET', font.url);
  148. req.responseType = 'arraybuffer';
  149. req.send();
  150. })
  151. )
  152. ).then(fontCss => fontCss.filter(x => x).join(''));
  153. let cachedRules = null;
  154. const styleSheetRules = () => {
  155. if (cachedRules) return cachedRules;
  156. return cachedRules = Array.from(document.styleSheets).map(sheet => {
  157. try {
  158. return {rules: sheet.cssRules, href: sheet.href};
  159. } catch (e) {
  160. console.warn(`Stylesheet could not be loaded: ${sheet.href}`, e);
  161. return {};
  162. }
  163. });
  164. };
  165. const inlineCss = (el, options) => {
  166. const {
  167. selectorRemap,
  168. modifyStyle,
  169. modifyCss,
  170. fonts
  171. } = options || {};
  172. const generateCss = modifyCss || ((selector, properties) => {
  173. const sel = selectorRemap ? selectorRemap(selector) : selector;
  174. const props = modifyStyle ? modifyStyle(properties) : properties;
  175. return `${sel}{${props}}\n`;
  176. });
  177. const css = [];
  178. const detectFonts = typeof fonts === 'undefined';
  179. const fontList = fonts || [];
  180. styleSheetRules().forEach(({rules, href}) => {
  181. if (!rules) return;
  182. Array.from(rules).forEach(rule => {
  183. if (typeof rule.style != 'undefined') {
  184. if (query(el, rule.selectorText)) css.push(generateCss(rule.selectorText, rule.style.cssText));
  185. else if (detectFonts && rule.cssText.match(/^@font-face/)) {
  186. const font = detectCssFont(rule, href);
  187. //if (font) fontList.push(font);
  188. } else css.push(rule.cssText);
  189. }
  190. });
  191. });
  192. return inlineFonts(fontList).then(fontCss => css.join('\n') + fontCss);
  193. };
  194. out$.prepareSvg = (el, options, done) => {
  195. requireDomNode(el);
  196. const {
  197. left = 0,
  198. top = 0,
  199. width: w,
  200. height: h,
  201. scale = 1,
  202. responsive = false,
  203. } = options || {};
  204. return inlineImages(el).then(() => {
  205. let clone = el.cloneNode(true);
  206. const {width, height} = getDimensions(el, clone, w, h);
  207. if (el.tagName !== 'svg') {
  208. if (el.getBBox) {
  209. clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, ''));
  210. const svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
  211. svg.appendChild(clone);
  212. clone = svg;
  213. } else {
  214. console.error('Attempted to render non-SVG element', el);
  215. return;
  216. }
  217. }
  218. clone.setAttribute('version', '1.1');
  219. clone.setAttribute('viewBox', [left, top, width, height].join(' '));
  220. if (!clone.getAttribute('xmlns')) clone.setAttributeNS(xmlns, 'xmlns', 'http://www.w3.org/2000/svg');
  221. if (!clone.getAttribute('xmlns:xlink')) clone.setAttributeNS(xmlns, 'xmlns:xlink', 'http://www.w3.org/1999/xlink');
  222. if (responsive) {
  223. clone.removeAttribute('width');
  224. clone.removeAttribute('height');
  225. clone.setAttribute('preserveAspectRatio', 'xMinYMin meet');
  226. } else {
  227. clone.setAttribute('width', width * scale);
  228. clone.setAttribute('height', height * scale);
  229. }
  230. Array.from(clone.querySelectorAll('foreignObject > *')).forEach(foreignObject => {
  231. if (!foreignObject.getAttribute('xmlns'))
  232. foreignObject.setAttributeNS(xmlns, 'xmlns', 'http://www.w3.org/1999/xhtml');
  233. });
  234. return inlineCss(el, options).then(css => {
  235. const style = document.createElement('style');
  236. style.setAttribute('type', 'text/css');
  237. style.innerHTML = `<![CDATA[\n${css}\n]]>`;
  238. const defs = document.createElement('defs');
  239. defs.appendChild(style);
  240. clone.insertBefore(defs, clone.firstChild);
  241. const outer = document.createElement('div');
  242. outer.appendChild(clone);
  243. const src = outer.innerHTML.replace(/NS\d+:href/gi, 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href');
  244. if (typeof done === 'function') done(src, width, height);
  245. else return {src, width, height};
  246. });
  247. });
  248. };
  249. out$.svgAsDataUri = (el, options, done) => {
  250. requireDomNode(el);
  251. const result = out$.prepareSvg(el, options)
  252. .then(({src}) => `data:image/svg+xml;base64,${window.btoa(reEncode(doctype+src))}`);
  253. if (typeof done === 'function') return result.then(done);
  254. return result;
  255. };
  256. out$.svgAsPngUri = (el, options, done) => {
  257. requireDomNode(el);
  258. const {
  259. encoderType = 'image/png',
  260. encoderOptions = 0.8,
  261. backgroundColor,
  262. canvg
  263. } = options || {};
  264. const convertToPng = ({src, width, height}) => {
  265. const canvas = document.createElement('canvas');
  266. const context = canvas.getContext('2d');
  267. const pixelRatio = window.devicePixelRatio || 1;
  268. canvas.width = width * pixelRatio;
  269. canvas.height = height * pixelRatio;
  270. canvas.style.width = `${canvas.width}px`;
  271. canvas.style.height = `${canvas.height}px`;
  272. context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
  273. if (canvg) canvg(canvas, src);
  274. else context.drawImage(src, 0, 0);
  275. if (backgroundColor) {
  276. context.globalCompositeOperation = 'destination-over';
  277. context.fillStyle = backgroundColor;
  278. context.fillRect(0, 0, canvas.width, canvas.height);
  279. }
  280. let png;
  281. try {
  282. png = canvas.toDataURL(encoderType, encoderOptions);
  283. } catch (e) {
  284. if ((typeof SecurityError !== 'undefined' && e instanceof SecurityError) || e.name === 'SecurityError') {
  285. console.error('Rendered SVG images cannot be downloaded in this browser.');
  286. return;
  287. } else throw e;
  288. }
  289. if (typeof done === 'function') done(png);
  290. return Promise.resolve(png);
  291. }
  292. if (canvg) return out$.prepareSvg(el, options).then(convertToPng);
  293. else return out$.svgAsDataUri(el, options).then(uri => {
  294. return new Promise((resolve, reject) => {
  295. const image = new Image();
  296. image.onload = () => resolve(convertToPng({
  297. src: image,
  298. width: image.width,
  299. height: image.height
  300. }));
  301. image.onerror = () => {
  302. reject(`There was an error loading the data URI as an image on the following SVG\n${window.atob(uri.slice(26))}Open the following link to see browser's diagnosis\n${uri}`);
  303. }
  304. image.src = uri;
  305. })
  306. });
  307. };
  308. out$.download = (name, uri) => {
  309. if (navigator.msSaveOrOpenBlob) navigator.msSaveOrOpenBlob(uriToBlob(uri), name);
  310. else {
  311. const saveLink = document.createElement('a');
  312. if ('download' in saveLink) {
  313. saveLink.download = name;
  314. saveLink.style.display = 'none';
  315. document.body.appendChild(saveLink);
  316. try {
  317. const blob = uriToBlob(uri);
  318. const url = URL.createObjectURL(blob);
  319. saveLink.href = url;
  320. saveLink.onclick = () => requestAnimationFrame(() => URL.revokeObjectURL(url));
  321. } catch (e) {
  322. console.warn('This browser does not support object URLs. Falling back to string URL.');
  323. saveLink.href = uri;
  324. }
  325. saveLink.click();
  326. document.body.removeChild(saveLink);
  327. }
  328. else {
  329. window.open(uri, '_temp', 'menubar=no,toolbar=no,status=no');
  330. }
  331. }
  332. };
  333. out$.saveSvg = (el, name, options) => {
  334. requireDomNode(el);
  335. out$.svgAsDataUri(el, options || {}, uri => out$.download(name, uri));
  336. };
  337. out$.saveSvgAsPng = (el, name, options) => {
  338. requireDomNode(el);
  339. out$.svgAsPngUri(el, options || {}, uri => out$.download(name, uri));
  340. };
  341. })();