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