diff --git a/frontend/package.json b/frontend/package.json index c5dc38f54..f3d703ea3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,8 +14,6 @@ "dependencies": { "axios": "^0.16.1", "csshint": "^0.3.3", - "cytoscape": "^3.2.11", - "cytoscape-dagre": "^2.2.0", "d3": "^4.7.4", "d3-format": "^1.2.1", "dagre": "^0.8.2", diff --git a/frontend/src/graph/ui/Chart.vue b/frontend/src/graph/ui/Chart.vue index 11e22e1e6..3c4beb9a0 100644 --- a/frontend/src/graph/ui/Chart.vue +++ b/frontend/src/graph/ui/Chart.vue @@ -11,6 +11,10 @@ // service import {getPluginGraphsGraph} from '../../service'; +// The name 'svgToPngDownloadHelper' is just a placeholder. +// Loading the JS lib file will bind saveSvgAsPng to window. +import * as svgToPngDownloadHelper from './svgToPngDownloadHelper.js'; + // for d3 drawing import * as d3 from 'd3'; @@ -31,32 +35,8 @@ export default { watch: { doDownload: function(val) { if (this.doDownload) { - // TODO(daming-lu): .svg is ugly and colorless. let svg = this.$refs.graphSvg; - - // get svg source. - let serializer = new XMLSerializer(); - let source = serializer.serializeToString(svg); - - // add name spaces. - if (!source.match(/^]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) { - source = source.replace(/^]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)) { - source = source.replace(/^ 0) { + return supportedFormats[extension]; + } + } + + // If you see this error message, you probably need to update code above. + console.error('Unknown font format for ' + fontUrl+ '; Fonts may not be working correctly'); + return 'application/octet-stream'; + } + + function processFontQueue(queue) { + if (queue.length > 0) { + // load fonts one by one until we have anything in the queue: + var font = queue.pop(); + processNext(font); + } else { + // no more fonts to load. + cssLoadedCallback(css); + } + + function processNext(font) { + // TODO: This could benefit from caching. + var oReq = new XMLHttpRequest(); + oReq.addEventListener('load', fontLoaded); + oReq.addEventListener('error', transferFailed); + oReq.addEventListener('abort', transferFailed); + oReq.open('GET', font.url); + oReq.responseType = 'arraybuffer'; + oReq.send(); + + function fontLoaded() { + // TODO: it may be also worth 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 ) + var fontBits = oReq.response; + var fontInBase64 = arrayBufferToBase64(fontBits); + updateFontStyle(font, fontInBase64); + } + + function transferFailed(e) { + console.warn('Failed to load font from: ' + font.url); + console.warn(e) + css += font.text + '\n'; + processFontQueue(queue); + } + + function updateFontStyle(font, fontInBase64) { + var dataUrl = 'url("data:' + font.format + ';base64,' + fontInBase64 + '")'; + css += font.text.replace(font.fontUrlRegexp, dataUrl) + '\n'; + + // schedule next font download on next tick. + setTimeout(function() { + processFontQueue(queue) + }, 0); + } + + } + } + + function arrayBufferToBase64(buffer) { + var binary = ''; + var bytes = new Uint8Array(buffer); + var len = bytes.byteLength; + + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + + return window.btoa(binary); + } + } + + function getDimension(el, clone, dim) { + var 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; + } + + function reEncode(data) { + data = encodeURIComponent(data); + data = data.replace(/%([0-9A-F]{2})/g, function(match, p1) { + var c = String.fromCharCode('0x'+p1); + return c === '%' ? '%25' : c; + }); + return decodeURIComponent(data); + } + + out$.prepareSvg = function(el, options, cb) { + requireDomNode(el); + + options = options || {}; + options.scale = options.scale || 1; + options.responsive = options.responsive || false; + var xmlns = "http://www.w3.org/2000/xmlns/"; + + inlineImages(el, function() { + var outer = document.createElement("div"); + var clone = el.cloneNode(true); + var width, height; + if(el.tagName == 'svg') { + width = options.width || getDimension(el, clone, 'width'); + height = options.height || getDimension(el, clone, 'height'); + } else if(el.getBBox) { + var box = el.getBBox(); + width = box.x + box.width; + height = box.y + box.height; + clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, '')); + + var 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"); + if (!clone.getAttribute('xmlns')) { + clone.setAttributeNS(xmlns, "xmlns", "http://www.w3.org/2000/svg"); + } + if (!clone.getAttribute('xmlns:xlink')) { + clone.setAttributeNS(xmlns, "xmlns:xlink", "http://www.w3.org/1999/xlink"); + } + + if (options.responsive) { + clone.removeAttribute('width'); + clone.removeAttribute('height'); + clone.setAttribute('preserveAspectRatio', 'xMinYMin meet'); + } else { + clone.setAttribute("width", width * options.scale); + clone.setAttribute("height", height * options.scale); + } + + clone.setAttribute("viewBox", [ + options.left || 0, + options.top || 0, + width, + height + ].join(" ")); + + var fos = clone.querySelectorAll('foreignObject > *'); + for (var i = 0; i < fos.length; i++) { + if (!fos[i].getAttribute('xmlns')) { + fos[i].setAttributeNS(xmlns, "xmlns", "http://www.w3.org/1999/xhtml"); + } + } + + outer.appendChild(clone); + + // In case of custom fonts we need to fetch font first, and then inline + // its url into data-uri format (encode as base64). That's why style + // processing is done asynchonously. Once all inlining is finshed + // cssLoadedCallback() is called. + styles(el, options, cssLoadedCallback); + + function cssLoadedCallback(css) { + // here all fonts are inlined, so that we can render them properly. + var s = document.createElement('style'); + s.setAttribute('type', 'text/css'); + s.innerHTML = "\n" + css + "\n"; + var defs = document.createElement('defs'); + defs.appendChild(s); + clone.insertBefore(defs, clone.firstChild); + + if (cb) { + var outHtml = outer.innerHTML; + outHtml = outHtml.replace(/NS\d+:href/gi, 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href'); + cb(outHtml, width, height); + } + } + }); + } + + out$.svgAsDataUri = function(el, options, cb) { + out$.prepareSvg(el, options, function(svg) { + var uri = 'data:image/svg+xml;base64,' + window.btoa(reEncode(doctype + svg)); + if (cb) { + cb(uri); + } + }); + } + + out$.svgAsPngUri = function(el, options, cb) { + requireDomNode(el); + + options = options || {}; + options.encoderType = options.encoderType || 'image/png'; + options.encoderOptions = options.encoderOptions || 0.8; + + var convertToPng = function(src, w, h) { + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + canvas.width = w; + canvas.height = h; + + var pixelRatio = window.devicePixelRatio || 1; + + canvas.style.width = canvas.width+'px'; + canvas.style.height = canvas.height+'px'; + canvas.width *= pixelRatio; + canvas.height *= pixelRatio; + + context.setTransform(pixelRatio,0,0,pixelRatio,0,0); + + if(options.canvg) { + options.canvg(canvas, src); + } else { + context.drawImage(src, 0, 0); + } + + if(options.backgroundColor){ + context.globalCompositeOperation = 'destination-over'; + context.fillStyle = options.backgroundColor; + context.fillRect(0, 0, canvas.width, canvas.height); + } + + var png; + try { + png = canvas.toDataURL(options.encoderType, options.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; + } + } + cb(png); + } + + if(options.canvg) { + out$.prepareSvg(el, options, convertToPng); + } else { + out$.svgAsDataUri(el, options, function(uri) { + var image = new Image(); + + image.onload = function() { + convertToPng(image, image.width, image.height); + } + + image.onerror = function() { + console.error( + 'There was an error loading the data URI as an image on the following SVG\n', + window.atob(uri.slice(26)), '\n', + "Open the following link to see browser's diagnosis\n", + uri); + } + + image.src = uri; + }); + } + } + + out$.download = function(name, uri) { + if (navigator.msSaveOrOpenBlob) { + navigator.msSaveOrOpenBlob(uriToBlob(uri), name); + } else { + var saveLink = document.createElement('a'); + var downloadSupported = 'download' in saveLink; + if (downloadSupported) { + saveLink.download = name; + saveLink.style.display = 'none'; + document.body.appendChild(saveLink); + try { + var blob = uriToBlob(uri); + var url = URL.createObjectURL(blob); + saveLink.href = url; + saveLink.onclick = function() { + requestAnimationFrame(function() { + URL.revokeObjectURL(url); + }) + }; + } catch (e) { + console.warn('This browser does not support object URLs. Falling back to string URL.'); + saveLink.href = uri; + } + saveLink.click(); + document.body.removeChild(saveLink); + } + else { + window.open(uri, '_temp', 'menubar=no,toolbar=no,status=no'); + } + } + } + + function uriToBlob(uri) { + var byteString = window.atob(uri.split(',')[1]); + var mimeString = uri.split(',')[0].split(':')[1].split(';')[0] + var buffer = new ArrayBuffer(byteString.length); + var intArray = new Uint8Array(buffer); + for (var i = 0; i < byteString.length; i++) { + intArray[i] = byteString.charCodeAt(i); + } + return new Blob([buffer], {type: mimeString}); + } + + out$.saveSvg = function(el, name, options) { + requireDomNode(el); + + options = options || {}; + out$.svgAsDataUri(el, options, function(uri) { + out$.download(name, uri); + }); + } + + out$.saveSvgAsPng = function(el, name, options) { + requireDomNode(el); + + options = options || {}; + out$.svgAsPngUri(el, options, function(uri) { + out$.download(name, uri); + }); + } + + // if define is defined create as an AMD module + if (typeof define !== 'undefined') { + define(function() { + return out$; + }); + } + +})();