From 7abc6bb04fed9d82640716cc410db21fe1e9eb00 Mon Sep 17 00:00:00 2001 From: Hezheng Yin Date: Fri, 14 Jun 2019 02:35:11 -0700 Subject: [PATCH] 1.3.2 --- README.md | 2 +- package.json | 2 +- saveSvgAsPng.js | 396 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 saveSvgAsPng.js diff --git a/README.md b/README.md index bfe3952..29b9f67 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/Persper/js-callgraph.svg?branch=master)](https://travis-ci.org/Persper/js-callgraph) -[![NPM version](https://img.shields.io/badge/npm-v1.3.1-blue.svg)](https://www.npmjs.com/package/@persper/js-callgraph) +[![NPM version](https://img.shields.io/badge/npm-v1.3.2-blue.svg)](https://www.npmjs.com/package/@persper/js-callgraph) [![License](https://img.shields.io/badge/license-EPL--2.0-green.svg)](https://www.eclipse.org/legal/epl-2.0/) This project implements a field-based call graph construction algorithm for JavaScript as described in diff --git a/package.json b/package.json index 9865da8..9fb0d78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@persper/js-callgraph", - "version": "1.3.1", + "version": "1.3.2", "description": "Builds static call graph for Javascript with a field-based algorithm.", "main": "index.js", "dependencies": { diff --git a/saveSvgAsPng.js b/saveSvgAsPng.js new file mode 100644 index 0000000..d0496ba --- /dev/null +++ b/saveSvgAsPng.js @@ -0,0 +1,396 @@ +(function() { + const out$ = typeof exports != 'undefined' && exports || typeof define != 'undefined' && {} || this || window; + if (typeof define !== 'undefined') define('save-svg-as-png', [], () => out$); + out$.default = out$; + + const xmlNs = 'http://www.w3.org/2000/xmlns/'; + const xhtmlNs = 'http://www.w3.org/1999/xhtml'; + const svgNs = 'http://www.w3.org/2000/svg'; + const doctype = ']>'; + const urlRegex = /url\(["']?(.+?)["']?\)/; + const fontFormats = { + woff2: 'font/woff2', + woff: 'font/woff', + otf: 'application/x-font-opentype', + ttf: 'application/x-font-ttf', + eot: 'application/vnd.ms-fontobject', + sfnt: 'application/font-sfnt', + svg: 'image/svg+xml' + }; + + const isElement = obj => obj instanceof HTMLElement || obj instanceof SVGElement; + const requireDomNode = el => { + if (!isElement(el)) throw new Error(`an HTMLElement or SVGElement is required; got ${el}`); + }; + const requireDomNodePromise = el => + new Promise((resolve, reject) => { + if (isElement(el)) resolve(el) + else reject(new Error(`an HTMLElement or SVGElement is required; got ${el}`)); + }) + const isExternal = url => url && url.lastIndexOf('http',0) === 0 && url.lastIndexOf(window.location.host) === -1; + + const getFontMimeTypeFromUrl = fontUrl => { + const formats = Object.keys(fontFormats) + .filter(extension => fontUrl.indexOf(`.${extension}`) > 0) + .map(extension => fontFormats[extension]); + if (formats) return formats[0]; + console.error(`Unknown font format for ${fontUrl}. Fonts may not be working correctly.`); + return 'application/octet-stream'; + }; + + const arrayBufferToBase64 = buffer => { + let binary = ''; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); + return window.btoa(binary); + } + + const getDimension = (el, clone, dim) => { + const v = + (el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal[dim]) || + (clone.getAttribute(dim) !== null && !clone.getAttribute(dim).match(/%$/) && parseInt(clone.getAttribute(dim))) || + el.getBoundingClientRect()[dim] || + parseInt(clone.style[dim]) || + parseInt(window.getComputedStyle(el).getPropertyValue(dim)); + return typeof v === 'undefined' || v === null || isNaN(parseFloat(v)) ? 0 : v; + }; + + const getDimensions = (el, clone, width, height) => { + if (el.tagName === 'svg') return { + width: width || getDimension(el, clone, 'width'), + height: height || getDimension(el, clone, 'height') + }; + else if (el.getBBox) { + const {x, y, width, height} = el.getBBox(); + return { + width: x + width, + height: y + height + }; + } + }; + + const reEncode = data => + decodeURIComponent( + encodeURIComponent(data) + .replace(/%([0-9A-F]{2})/g, (match, p1) => { + const c = String.fromCharCode(`0x${p1}`); + return c === '%' ? '%25' : c; + }) + ); + + const uriToBlob = uri => { + const byteString = window.atob(uri.split(',')[1]); + const mimeString = uri.split(',')[0].split(':')[1].split(';')[0] + const buffer = new ArrayBuffer(byteString.length); + const intArray = new Uint8Array(buffer); + for (let i = 0; i < byteString.length; i++) { + intArray[i] = byteString.charCodeAt(i); + } + return new Blob([buffer], {type: mimeString}); + }; + + const query = (el, selector) => { + if (!selector) return; + try { + return el.querySelector(selector) || el.parentNode && el.parentNode.querySelector(selector); + } catch(err) { + console.warn(`Invalid CSS selector "${selector}"`, err); + } + }; + + const detectCssFont = (rule, href) => { + // Match CSS font-face rules to external links. + // @font-face { + // src: local('Abel'), url(https://fonts.gstatic.com/s/abel/v6/UzN-iejR1VoXU2Oc-7LsbvesZW2xOQ-xsNqO47m55DA.woff2); + // } + const match = rule.cssText.match(urlRegex); + const url = (match && match[1]) || ''; + if (!url || url.match(/^data:/) || url === 'about:blank') return; + const fullUrl = + url.startsWith('../') ? `${href}/../${url}` + : url.startsWith('./') ? `${href}/.${url}` + : url; + return { + text: rule.cssText, + format: getFontMimeTypeFromUrl(fullUrl), + url: fullUrl + }; + }; + + const inlineImages = el => Promise.all( + Array.from(el.querySelectorAll('image')).map(image => { + let href = image.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || image.getAttribute('href'); + if (!href) return Promise.resolve(null); + if (isExternal(href)) { + href += (href.indexOf('?') === -1 ? '?' : '&') + 't=' + new Date().valueOf(); + } + return new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.src = href; + img.onerror = () => reject(new Error(`Could not load ${href}`)); + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext('2d').drawImage(img, 0, 0); + image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', canvas.toDataURL('image/png')); + resolve(true); + }; + }); + }) + ); + + const cachedFonts = {}; + const inlineFonts = fonts => Promise.all( + fonts.map(font => + new Promise((resolve, reject) => { + if (cachedFonts[font.url]) return resolve(cachedFonts[font.url]); + + const req = new XMLHttpRequest(); + req.addEventListener('load', () => { + // TODO: it may also be worth it to wait until fonts are fully loaded before + // attempting to rasterize them. (e.g. use https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet) + const fontInBase64 = arrayBufferToBase64(req.response); + const fontUri = font.text.replace(urlRegex, `url("data:${font.format};base64,${fontInBase64}")`)+'\n'; + cachedFonts[font.url] = fontUri; + resolve(fontUri); + }); + req.addEventListener('error', e => { + console.warn(`Failed to load font from: ${font.url}`, e); + cachedFonts[font.url] = null; + resolve(null); + }); + req.addEventListener('abort', e => { + console.warn(`Aborted loading font from: ${font.url}`, e); + resolve(null); + }); + req.open('GET', font.url); + req.responseType = 'arraybuffer'; + req.send(); + }) + ) + ).then(fontCss => fontCss.filter(x => x).join('')); + + let cachedRules = null; + const styleSheetRules = () => { + if (cachedRules) return cachedRules; + return cachedRules = Array.from(document.styleSheets).map(sheet => { + try { + return {rules: sheet.cssRules, href: sheet.href}; + } catch (e) { + console.warn(`Stylesheet could not be loaded: ${sheet.href}`, e); + return {}; + } + }); + }; + + const inlineCss = (el, options) => { + const { + selectorRemap, + modifyStyle, + modifyCss, + fonts + } = options || {}; + const generateCss = modifyCss || ((selector, properties) => { + const sel = selectorRemap ? selectorRemap(selector) : selector; + const props = modifyStyle ? modifyStyle(properties) : properties; + return `${sel}{${props}}\n`; + }); + const css = []; + const detectFonts = typeof fonts === 'undefined'; + const fontList = fonts || []; + styleSheetRules().forEach(({rules, href}) => { + if (!rules) return; + Array.from(rules).forEach(rule => { + if (typeof rule.style != 'undefined') { + if (query(el, rule.selectorText)) css.push(generateCss(rule.selectorText, rule.style.cssText)); + else if (detectFonts && rule.cssText.match(/^@font-face/)) { + const font = detectCssFont(rule, href); + if (font) fontList.push(font); + } else css.push(rule.cssText); + } + }); + }); + + return inlineFonts(fontList).then(fontCss => css.join('\n') + fontCss); + }; + + const downloadOptions = () => { + if (!navigator.msSaveOrOpenBlob && !('download' in document.createElement('a'))) { + return {popup: window.open()}; + } + }; + + out$.prepareSvg = (el, options, done) => { + requireDomNode(el); + const { + left = 0, + top = 0, + width: w, + height: h, + scale = 1, + responsive = false, + } = options || {}; + + return inlineImages(el).then(() => { + let clone = el.cloneNode(true); + clone.style.backgroundColor = (options || {}).backgroundColor || el.style.backgroundColor; + const {width, height} = getDimensions(el, clone, w, h); + + if (el.tagName !== 'svg') { + if (el.getBBox) { + if (clone.getAttribute('transform') != null) { + clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, '')); + } + const svg = document.createElementNS('http://www.w3.org/2000/svg','svg'); + svg.appendChild(clone); + clone = svg; + } else { + console.error('Attempted to render non-SVG element', el); + return; + } + } + + clone.setAttribute('version', '1.1'); + clone.setAttribute('viewBox', [left, top, width, height].join(' ')); + if (!clone.getAttribute('xmlns')) clone.setAttributeNS(xmlNs, 'xmlns', svgNs); + if (!clone.getAttribute('xmlns:xlink')) clone.setAttributeNS(xmlNs, 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); + + if (responsive) { + clone.removeAttribute('width'); + clone.removeAttribute('height'); + clone.setAttribute('preserveAspectRatio', 'xMinYMin meet'); + } else { + clone.setAttribute('width', width * scale); + clone.setAttribute('height', height * scale); + } + + Array.from(clone.querySelectorAll('foreignObject > *')).forEach(foreignObject => { + foreignObject.setAttributeNS(xmlNs, 'xmlns', foreignObject.tagName === 'svg' ? svgNs : xhtmlNs); + }); + + return inlineCss(el, options).then(css => { + const style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.innerHTML = ``; + + const defs = document.createElement('defs'); + defs.appendChild(style); + clone.insertBefore(defs, clone.firstChild); + + const outer = document.createElement('div'); + outer.appendChild(clone); + const src = outer.innerHTML.replace(/NS\d+:href/gi, 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href'); + + if (typeof done === 'function') done(src, width, height); + else return {src, width, height}; + }); + }); + }; + + out$.svgAsDataUri = (el, options, done) => { + requireDomNode(el); + return out$.prepareSvg(el, options) + .then(({src, width, height}) => { + const svgXml = `data:image/svg+xml;base64,${window.btoa(reEncode(doctype+src))}`; + if (typeof done === 'function') { + done(svgXml, width, height); + } + return svgXml; + }); + }; + + out$.svgAsPngUri = (el, options, done) => { + requireDomNode(el); + const { + encoderType = 'image/png', + encoderOptions = 0.8, + canvg + } = options || {}; + + const convertToPng = ({src, width, height}) => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + const pixelRatio = window.devicePixelRatio || 1; + + canvas.width = width * pixelRatio; + canvas.height = height * pixelRatio; + canvas.style.width = `${canvas.width}px`; + canvas.style.height = `${canvas.height}px`; + context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + + if (canvg) canvg(canvas, src); + else context.drawImage(src, 0, 0); + + let png; + try { + png = canvas.toDataURL(encoderType, encoderOptions); + } catch (e) { + if ((typeof SecurityError !== 'undefined' && e instanceof SecurityError) || e.name === 'SecurityError') { + console.error('Rendered SVG images cannot be downloaded in this browser.'); + return; + } else throw e; + } + if (typeof done === 'function') done(png, canvas.width, canvas.height); + return Promise.resolve(png); + } + + if (canvg) return out$.prepareSvg(el, options).then(convertToPng); + else return out$.svgAsDataUri(el, options).then(uri => { + return new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => resolve(convertToPng({ + src: image, + width: image.width, + height: image.height + })); + image.onerror = () => { + 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}`); + } + image.src = uri; + }) + }); + }; + + out$.download = (name, uri, options) => { + if (navigator.msSaveOrOpenBlob) navigator.msSaveOrOpenBlob(uriToBlob(uri), name); + else { + const saveLink = document.createElement('a'); + if ('download' in saveLink) { + saveLink.download = name; + saveLink.style.display = 'none'; + document.body.appendChild(saveLink); + try { + const blob = uriToBlob(uri); + const url = URL.createObjectURL(blob); + saveLink.href = url; + saveLink.onclick = () => requestAnimationFrame(() => URL.revokeObjectURL(url)); + } catch (e) { + console.error(e); + console.warn('Error while getting object URL. Falling back to string URL.'); + saveLink.href = uri; + } + saveLink.click(); + document.body.removeChild(saveLink); + } else if (options && options.popup) { + options.popup.document.title = name; + options.popup.location.replace(uri); + } + } + }; + + out$.saveSvg = (el, name, options) => { + const downloadOpts = downloadOptions(); // don't inline, can't be async + return requireDomNodePromise(el) + .then(el => out$.svgAsDataUri(el, options || {})) + .then(uri => out$.download(name, uri, downloadOpts)); + }; + + out$.saveSvgAsPng = (el, name, options) => { + const downloadOpts = downloadOptions(); // don't inline, can't be async + return requireDomNodePromise(el) + .then(el => out$.svgAsPngUri(el, options || {})) + .then(uri => out$.download(name, uri, downloadOpts)); + }; +})();