From e620959e43a31fd1f966978e5e78a6642b3ed6b7 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Fri, 15 Oct 2021 15:10:48 -0400 Subject: [PATCH 1/7] Use libraries for more robust support of exporting images from charts --- client/package.json | 2 + .../chart/components/screenshot.js | 124 ++---------------- client/yarn.lock | 10 ++ 3 files changed, 21 insertions(+), 115 deletions(-) diff --git a/client/package.json b/client/package.json index 053fc60a063..647f989d319 100644 --- a/client/package.json +++ b/client/package.json @@ -36,7 +36,9 @@ "decode-uri-component": "^0.2.0", "deep-diff": "^1.0.2", "deep-equal": "^2.0.5", + "dom-to-image": "^2.6.0", "elkjs": "^0.6.2", + "file-saver": "^2.0.5", "flush-promises": "^1.0.2", "glob": "^7.1.6", "handsontable": "^2.0.0", diff --git a/client/src/mvc/visualization/chart/components/screenshot.js b/client/src/mvc/visualization/chart/components/screenshot.js index 506982d1f0b..00e9e760fce 100644 --- a/client/src/mvc/visualization/chart/components/screenshot.js +++ b/client/src/mvc/visualization/chart/components/screenshot.js @@ -1,107 +1,21 @@ /** This class enables users to export/download a chart as PNG, SVG or PDF. */ -/** PNG export */ import $ from "jquery"; +import domtoimage from "dom-to-image"; +import { saveAs } from "file-saver"; + function createPNG(options) { - if (options.$el.find("svg").length > 0) { - _svg2png(options); - } else { - _canvas2png(options); - } -} - -function _downloadData(imageData, filename) { - const link = document.createElement("a"); - link.download = filename; - link.href = imageData; - link.click(); -} -function _downloadPNGData(imageData, filename) { - _downloadData(imageData.replace("image/png", "image/octet-stream"), `${filename || "GalaxyImage"}.png`); -} - -function _toImage($el, x_offset, y_offset, newContext) { - var tagname = $el.prop("tagName").toLowerCase(); - var p = $el.position(); - var left = - x_offset + - p.left + - parseInt($el.css("marginLeft"), 10) + - parseInt($el.css("borderLeftWidth"), 10) + - parseInt($el.css("paddingLeft"), 10); - var top = - y_offset + - p.top + - parseInt($el.css("marginTop"), 10) + - parseInt($el.css("borderTopWidth"), 10) + - parseInt($el.css("paddingTop"), 10); - if (tagname == "div" || tagname == "span") { - $el.children().each(function () { - _toImage($(this), left, top); - }); - } else if (tagname == "canvas") { - newContext.drawImage($el[0], left, top); - } -} - -/** Convert canvas to png */ -function _canvas2png(options) { - const $canvas = options.$el.find(".charts-viewer-canvas"); - try { - if ($canvas.width() !== 0 && $canvas.height() !== 0) { - var newCanvas = document.createElement("canvas"); - newCanvas.width = $canvas.outerWidth(true); - newCanvas.height = $canvas.outerHeight(true); - var newContext = newCanvas.getContext("2d"); - newContext.save(); - newContext.fillStyle = "rgb(255,255,255)"; - newContext.fillRect(0, 0, newCanvas.width, newCanvas.height); - newContext.restore(); - newContext.translate(0, 0); - newContext.textAlign = "left"; - newContext.textBaseline = "top"; - $canvas.children().each(function () { - _toImage($(this), 0, 0, newContext); - }); - const imgData = newCanvas.toDataURL("image/png"); - if (imgData) { - _downloadPNGData(imgData, options.title); - } - } - } catch (err) { - console.debug("FAILED - screenshot::_canvas2png() - " + err); - if (options.error) { - options.error("Please reduce your visualization to a single panel and try again."); - } - } -} - -/** Convert svg to png */ -function _svg2png(options) { - var scale = 5; - var xml = toXML(options); - var canvas = document.createElement("canvas"); - var context = canvas.getContext("2d"); - var source = new Image(); - var $container = $('
').append($(canvas)); - $("body").append($container); - canvas.width = xml.width * scale; - canvas.height = xml.height * scale; - source.src = "data:image/svg+xml;base64," + window.btoa(xml.string); - source.onload = function () { - context.drawImage(source, 0, 0, canvas.width, canvas.height); - const imageData = canvas.toDataURL("image/png"); - _downloadPNGData(imageData, options.title); - $container.remove(); - }; + domtoimage.toBlob(options.$el[0]).then(function (blob) { + saveAs(blob, `${options.title || "my-chart"}.png`); + }); } /** SVG export */ function createSVG(options) { - const imageData = `data:none/none;base64,${window.btoa(toXML(options).string)}`; - const filename = `${options.title || "GalaxyImage"}.svg`; - _downloadData(imageData, filename); + domtoimage.toSvg(options.$el[0]).then(function (dataUrl) { + saveAs(dataUrl, `${options.title || "my-chart"}.svg`); + }); } /** PDF export */ @@ -164,7 +78,6 @@ function toXML(options) { var offsetX = 0; $svg.each(function () { var $svg = $(this).clone(); - _inline($svg); var $g = $('').attr("xmlns", "http://www.w3.org/2000/svg"); $g.append($svg.find("g").first()); $composite.append($g); @@ -177,25 +90,6 @@ function toXML(options) { }; } -/** inlines CSS code */ -function _inline($target) { - for (var sheet_id in document.styleSheets) { - var sheet = document.styleSheets[sheet_id]; - var rules = sheet.cssRules; - if (rules) { - for (var idx = 0, len = rules.length; idx < len; idx++) { - try { - $target.find(rules[idx].selectorText).each(function (i, elem) { - elem.style.cssText += rules[idx].style.cssText; - }); - } catch (err) { - console.error(err); - } - } - } - } -} - export default { createPNG: createPNG, createSVG: createSVG, diff --git a/client/yarn.lock b/client/yarn.lock index a42d657c21f..3c6d9ed9d3d 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -5284,6 +5284,11 @@ dom-serializer@0: domelementtype "~1.1.1" entities "~1.1.1" +dom-to-image@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867" + integrity sha1-ilA2CAiMh7HCL5A0rgMuGJiVWGc= + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -6280,6 +6285,11 @@ file-loader@^6.0.0: loader-utils "^2.0.0" schema-utils "^2.6.5" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" From e77a99203db012017d57f21de21ba6e5479d2b17 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Fri, 15 Oct 2021 15:43:09 -0400 Subject: [PATCH 2/7] Use jspdf to generate pdfs on the fly for downloads --- client/package.json | 1 + .../chart/components/screenshot.js | 73 +-------- client/yarn.lock | 150 +++++++++++++++--- 3 files changed, 136 insertions(+), 88 deletions(-) diff --git a/client/package.json b/client/package.json index 647f989d319..6abebc8f2eb 100644 --- a/client/package.json +++ b/client/package.json @@ -53,6 +53,7 @@ "jquery-ui": "^1.12.1", "jquery.complexify": "^0.5.2", "jquery.cookie": "^1.4.1", + "jspdf": "^2.4.0", "linkifyjs": "^2.1.9", "markdown-it": "^11.0.0", "markdown-it-regexp": "^0.4.0", diff --git a/client/src/mvc/visualization/chart/components/screenshot.js b/client/src/mvc/visualization/chart/components/screenshot.js index 00e9e760fce..c2b8da137dc 100644 --- a/client/src/mvc/visualization/chart/components/screenshot.js +++ b/client/src/mvc/visualization/chart/components/screenshot.js @@ -4,6 +4,7 @@ import $ from "jquery"; import domtoimage from "dom-to-image"; import { saveAs } from "file-saver"; +import { jsPDF } from "jspdf"; function createPNG(options) { domtoimage.toBlob(options.$el[0]).then(function (blob) { @@ -20,74 +21,12 @@ function createSVG(options) { /** PDF export */ function createPDF(options) { - var xml = toXML(options); - var data = { - filename: "visualization", - type: "application/pdf", - height: xml.height, - width: xml.width, - scale: 2, - svg: xml.string, - }; - var $el = $("body"); - var form = $el.find("#viewer-form"); - if (form.length === 0) { - form = $("
", { - id: "viewer-form", - method: "post", - action: "http://export.highcharts.com/", - display: "none", - }); - $el.append(form); - } - form.empty(); - for (var name in data) { - var input = $("", { - type: "hidden", - name: name, - value: data[name], - }); - form.append(input); - } - try { - form.submit(); - } catch (err) { - console.log(err); - } -} + domtoimage.toPng(options.$el[0]).then(function (dataUrl) { + const doc = new jsPDF(); + doc.addImage(dataUrl, "png", 0, 0); + doc.save(`${options.title || "my-chart"}.pdf`); + }); -/** XML export */ -function toXML(options) { - var $svg = options.$el.find("svg"); - if ($svg.length == 0) { - if (options.error) { - options.error("No SVG found. This visualization type does not support SVG/PDF export."); - return; - } - } - var nsvgs = $svg.length; - var height = parseInt($svg.first().css("height")); - var width = parseInt($svg.first().css("width")); - var serializer = new XMLSerializer(); - var $composite = $("").attr({ - version: "1.1", - xmlns: "http://www.w3.org/2000/svg", - width: width * nsvgs, - height: height, - }); - var offsetX = 0; - $svg.each(function () { - var $svg = $(this).clone(); - var $g = $('').attr("xmlns", "http://www.w3.org/2000/svg"); - $g.append($svg.find("g").first()); - $composite.append($g); - offsetX += width; - }); - return { - string: serializer.serializeToString($composite[0]), - height: height, - width: width, - }; } export default { diff --git a/client/yarn.lock b/client/yarn.lock index 3c6d9ed9d3d..8fa999d1d1d 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1115,10 +1115,10 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.2": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" - integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== +"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" + integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== dependencies: regenerator-runtime "^0.13.4" @@ -1988,6 +1988,11 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.0.tgz#dc85454b953178cc6043df5208b9e949b54a3bc4" integrity sha512-/rM+sWiuOZ5dvuVzV37sUuklsbg+JPOP8d+nNFlo2ZtfpzPiPvh1/gc8liWOLBqe+sR+ZM7guPaIcTt6UZTo7Q== +"@types/raf@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2" + integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw== + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -3192,6 +3197,16 @@ base64-arraybuffer@0.1.5: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= +base64-arraybuffer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45" + integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ== + +base64-arraybuffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c" + integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA== + base64-js@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" @@ -3502,6 +3517,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +btoa@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + buble@0.19.8: version "0.19.8" resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.8.tgz#d642f0081afab66dccd897d7b6360d94030b9d3d" @@ -3749,6 +3769,20 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000890, caniuse-lite@^1.0.30000929, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001168.tgz" integrity sha512-P2zmX7swIXKu+GMMR01TWa4csIKELTNnZKc+f1CjebmZJQtTAEXmpQSoKVJVVcvPGAA0TEYTOUp3VehavZSFPQ== +canvg@^3.0.6: + version "3.0.9" + resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.9.tgz#9ba095f158b94b97ca2c9c1c40785b11dc08df6d" + integrity sha512-rDXcnRPuz4QHoCilMeoTxql+fvGqNAxp+qV/KHD8rOiJSAfVjFclbdUNHD2Uqfthr+VMg17bD2bVuk6F07oLGw== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/raf" "^3.4.0" + core-js "^3.8.3" + raf "^3.4.1" + regenerator-runtime "^0.13.7" + rgbcolor "^1.0.1" + stackblur-canvas "^2.0.0" + svg-pathdata "^6.0.3" + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -4544,10 +4578,15 @@ core-js@^2.6.5: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== -core-js@^3.6.0, core-js@^3.6.4: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" - integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== +core-js@^3.6.0, core-js@^3.8.3: + version "3.18.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.3.tgz#86a0bba2d8ec3df860fefcc07a8d119779f01509" + integrity sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw== + +core-js@^3.6.4: + version "3.18.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.2.tgz#63a551e8a29f305cd4123754846e65896619ba5b" + integrity sha512-zNhPOUoSgoizoSQFdX1MeZO16ORRb9FFQLts8gSYbZU5FcgXhp24iMWMxnOQo5uIaIG7/6FA/IqJPwev1o9ZXQ== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -4649,6 +4688,13 @@ css-initials@^0.3.1: resolved "https://registry.yarnpkg.com/css-initials/-/css-initials-0.3.1.tgz#0406d78e586fd12b9984a3f7d8a87fcbb2073208" integrity sha512-fkshKv9vV8AmcxkAWVQ9DmEAKiqe09GHdnFaXecp0NIfsGnXIHVJAHfsxdRy9KXV0/KiWdjBqrCYto2fYIO4xQ== +css-line-break@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.0.1.tgz#3dc74c2ed5eb64211480281932475790243e7338" + integrity sha512-gwKYIMUn7xodIcb346wgUhE2Dt5O1Kmrc16PWi8sL4FTfyDj8P5095rzH7+O8CTZudJr+uw2GCI/hwEkDJFI2w== + dependencies: + base64-arraybuffer "^0.2.0" + css-loader@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" @@ -5311,6 +5357,11 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +dompurify@^2.2.0: + version "2.3.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c" + integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== + domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -6258,6 +6309,11 @@ fetch-cookie@0.10.1: dependencies: tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0" +fflate@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" + integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== + figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -6759,7 +6815,7 @@ glob-watcher@^5.0.3: just-debounce "^1.0.0" object.defaults "^1.1.0" -glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: +glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -7199,6 +7255,14 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html2canvas@^1.0.0-rc.5: + version "1.3.2" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.3.2.tgz#951cc8388a3ce939fdac02131007ee28124afc27" + integrity sha512-4+zqv87/a1LsaCrINV69wVLGG8GBZcYBboz1JPWEgiXcWoD9kroLzccsBRU/L9UlfV2MAZ+3J92U9IQPVMDeSQ== + dependencies: + css-line-break "2.0.1" + text-segmentation "^1.0.2" + http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -8902,6 +8966,21 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jspdf@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-2.4.0.tgz#53d6d2acc63203b0b3688949597dd11a633b1db5" + integrity sha512-nsZ92YfbNG/EimR1yqmOkxf2D4iJRypBsw7pvP1aPiIEnoGITaLl6XDR/GYA36/R29vMZSBedpEhBCzutSGytA== + dependencies: + "@babel/runtime" "^7.14.0" + atob "^2.1.2" + btoa "^1.2.1" + fflate "^0.4.8" + optionalDependencies: + canvg "^3.0.6" + core-js "^3.6.0" + dompurify "^2.2.0" + html2canvas "^1.0.0-rc.5" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -12097,6 +12176,13 @@ qunit@^2.10.0: node-watch "0.6.1" resolve "1.9.0" +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" @@ -12536,10 +12622,10 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.4: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== regenerator-transform@^0.14.2: version "0.14.4" @@ -12946,6 +13032,11 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= +rgbcolor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" + integrity sha1-1lBezbMEplldom+ktDMHMGd1lF0= + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -12953,14 +13044,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@^2.5.4: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== - dependencies: - glob "^7.0.5" - -rimraf@^2.6.3, rimraf@^2.7.1: +rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -13761,6 +13845,11 @@ stack-utils@^2.0.2: dependencies: escape-string-regexp "^2.0.0" +stackblur-canvas@^2.0.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.5.0.tgz#aa87bbed1560fdcd3138fff344fc6a1c413ebac4" + integrity sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ== + stackframe@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303" @@ -14128,6 +14217,11 @@ sver-compat@^1.5.0: es6-iterator "^2.0.1" es6-symbol "^3.1.1" +svg-pathdata@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" + integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== + svgo@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985" @@ -14262,6 +14356,13 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-segmentation@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.2.tgz#1f828fa14aa101c114ded1bda35ba7dcc17c9858" + integrity sha512-uTqvLxdBrVnx/CFQOtnf8tfzSXFm+1Qxau7Xi54j4OPTZokuDOX8qncQzrg2G8ZicAMOM8TgzFAYTb+AqNO4Cw== + dependencies: + utrie "^1.0.1" + text-table@0.2.0, text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -14938,6 +15039,13 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +utrie@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.1.tgz#e155235ebcbddc89ae09261ab6e773ce61401b2f" + integrity sha512-JPaDXF3vzgZxfeEwutdGzlrNoVFL5UvZcbO6Qo9D4GoahrieUPoMU8GCpVpR7MQqcKhmShIh8VlbEN3PLM3EBg== + dependencies: + base64-arraybuffer "^1.0.1" + uuid@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" From 309499e4e10cbc15b7d71c3a8bef04c0971525d1 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Fri, 15 Oct 2021 17:26:16 -0400 Subject: [PATCH 3/7] PDF no longer redirects to a third party; we generate it on the fly now. --- .../src/mvc/visualization/chart/views/menu.js | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/client/src/mvc/visualization/chart/views/menu.js b/client/src/mvc/visualization/chart/views/menu.js index 5978d2d1214..a1bb798213f 100644 --- a/client/src/mvc/visualization/chart/views/menu.js +++ b/client/src/mvc/visualization/chart/views/menu.js @@ -55,26 +55,14 @@ export default Backbone.View.extend({ title: "Save as PDF", icon: "fa-file-o", onclick: () => { - app.modal.show({ - title: "Send visualization data for PDF creation", - body: "Galaxy does not provide integrated PDF export scripts. You may click 'Continue' to create the PDF by using a 3rd party service (https://export.highcharts.com).", - buttons: { - Cancel: () => { - app.modal.hide(); + this._wait(app.chart, () => { + Screenshot.createPDF({ + $el: app.viewer.$el, + title: app.chart.get("title"), + error: (err) => { + app.message.update({ message: err, status: "danger" }); }, - Continue: () => { - app.modal.hide(); - this._wait(app.chart, () => { - Screenshot.createPDF({ - $el: app.viewer.$el, - title: app.chart.get("title"), - error: (err) => { - app.message.update({ message: err, status: "danger" }); - }, - }); - }); - }, - }, + }); }); }, }); From 563b165683e142142b4db68215b713448f90aa97 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Fri, 15 Oct 2021 19:55:09 -0400 Subject: [PATCH 4/7] Use dynamic import for screenshot due to large libs; remove jquery, refactor a bit. --- .../chart/components/screenshot.js | 19 ++---- .../src/mvc/visualization/chart/views/menu.js | 59 ++++++++++++------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/client/src/mvc/visualization/chart/components/screenshot.js b/client/src/mvc/visualization/chart/components/screenshot.js index c2b8da137dc..82b65674f4d 100644 --- a/client/src/mvc/visualization/chart/components/screenshot.js +++ b/client/src/mvc/visualization/chart/components/screenshot.js @@ -1,36 +1,29 @@ /** This class enables users to export/download a chart as PNG, SVG or PDF. */ -import $ from "jquery"; - import domtoimage from "dom-to-image"; import { saveAs } from "file-saver"; import { jsPDF } from "jspdf"; -function createPNG(options) { +export function createPNG(options) { domtoimage.toBlob(options.$el[0]).then(function (blob) { saveAs(blob, `${options.title || "my-chart"}.png`); }); } /** SVG export */ -function createSVG(options) { +export function createSVG(options) { domtoimage.toSvg(options.$el[0]).then(function (dataUrl) { saveAs(dataUrl, `${options.title || "my-chart"}.svg`); }); } /** PDF export */ -function createPDF(options) { +export function createPDF(options) { domtoimage.toPng(options.$el[0]).then(function (dataUrl) { const doc = new jsPDF(); - doc.addImage(dataUrl, "png", 0, 0); + const w = doc.internal.pageSize.getWidth(); + const h = doc.internal.pageSize.getHeight(); + doc.addImage(dataUrl, "png", w * 0.1, h * 0.1, w * 0.8, h * 0.8); doc.save(`${options.title || "my-chart"}.pdf`); }); - } - -export default { - createPNG: createPNG, - createSVG: createSVG, - createPDF: createPDF, -}; diff --git a/client/src/mvc/visualization/chart/views/menu.js b/client/src/mvc/visualization/chart/views/menu.js index a1bb798213f..5671b0843de 100644 --- a/client/src/mvc/visualization/chart/views/menu.js +++ b/client/src/mvc/visualization/chart/views/menu.js @@ -1,7 +1,6 @@ /** This class renders the chart menu options. */ import Backbone from "backbone"; import Ui from "mvc/ui/ui-misc"; -import Screenshot from "mvc/visualization/chart/components/screenshot"; export default Backbone.View.extend({ initialize: function (app) { @@ -24,13 +23,7 @@ export default Backbone.View.extend({ icon: "fa-file", onclick: () => { this._wait(app.chart, () => { - Screenshot.createPNG({ - $el: app.viewer.$el, - title: app.chart.get("title"), - error: (err) => { - app.message.update({ message: err, status: "danger" }); - }, - }); + this.downloadAs("png"); }); }, }); @@ -40,13 +33,7 @@ export default Backbone.View.extend({ icon: "fa-file-text-o", onclick: () => { this._wait(app.chart, () => { - Screenshot.createSVG({ - $el: app.viewer.$el, - title: app.chart.get("title"), - error: (err) => { - app.message.update({ message: err, status: "danger" }); - }, - }); + this.downloadAs("svg"); }); }, }); @@ -56,13 +43,7 @@ export default Backbone.View.extend({ icon: "fa-file-o", onclick: () => { this._wait(app.chart, () => { - Screenshot.createPDF({ - $el: app.viewer.$el, - title: app.chart.get("title"), - error: (err) => { - app.message.update({ message: err, status: "danger" }); - }, - }); + this.downloadAs("pdf"); }); }, }); @@ -118,6 +99,40 @@ export default Backbone.View.extend({ this.render(); }, + downloadAs: function (filetype) { + import(/* webpackChunkName: "Screenshot" */ "mvc/visualization/chart/components/screenshot").then( + (Screenshot) => { + if (filetype === "png") { + Screenshot.createPNG({ + $el: this.app.viewer.$el, + title: this.app.chart.get("title"), + error: (err) => { + this.app.message.update({ message: err, status: "danger" }); + }, + }); + } else if (filetype === "svg") { + Screenshot.createSVG({ + $el: this.app.viewer.$el, + title: this.app.chart.get("title"), + error: (err) => { + this.app.message.update({ message: err, status: "danger" }); + }, + }); + } else if (filetype === "pdf") { + Screenshot.createPDF({ + $el: this.app.viewer.$el, + title: this.app.chart.get("title"), + error: (err) => { + this.app.message.update({ message: err, status: "danger" }); + }, + }); + } else { + this.app.message.update({ message: "Unknown artifact type.", status: "danger" }); + } + } + ); + }, + render: function () { var visible = this.model.get("visible"); this.app.$el[visible ? "removeClass" : "addClass"]("charts-fullscreen"); From 0f3e776dc6ae62f9c660f9399eba97de740f573e Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Fri, 15 Oct 2021 20:21:50 -0400 Subject: [PATCH 5/7] Fix webpack config to shift chunks out of libs for screenshot deps --- client/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/webpack.config.js b/client/webpack.config.js index 4eb2694f59c..29e6ec9a38a 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -53,7 +53,7 @@ module.exports = (env = {}, argv = {}) => { }, libs: { name: "libs", - test: /node_modules[\\/](?!(handsontable|pikaday|moment|elkjs)[\\/])|galaxy\/scripts\/libs/, + test: /node_modules[\\/](?!(jspdf|canvg|html2canvas|handsontable|pikaday|moment|elkjs)[\\/])|galaxy\/scripts\/libs/, chunks: "all", priority: -10, }, From 3f85fa8cac8104de49ddce92f7c1c209017540f7 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Fri, 15 Oct 2021 20:23:49 -0400 Subject: [PATCH 6/7] Force a white background (which charts can overwrite, but otherwise these generally don't look right) --- client/src/mvc/visualization/chart/components/screenshot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/mvc/visualization/chart/components/screenshot.js b/client/src/mvc/visualization/chart/components/screenshot.js index 82b65674f4d..a449c281948 100644 --- a/client/src/mvc/visualization/chart/components/screenshot.js +++ b/client/src/mvc/visualization/chart/components/screenshot.js @@ -5,7 +5,7 @@ import { saveAs } from "file-saver"; import { jsPDF } from "jspdf"; export function createPNG(options) { - domtoimage.toBlob(options.$el[0]).then(function (blob) { + domtoimage.toBlob(options.$el[0], { bgcolor: "#fff" }).then(function (blob) { saveAs(blob, `${options.title || "my-chart"}.png`); }); } From 4170ec35714a9a4fa8cc7143e38da9dfbc861102 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Fri, 15 Oct 2021 21:27:49 -0400 Subject: [PATCH 7/7] Fix wonky scaling math for image embed into pdf, drop big header comments since it's just three methods now. --- .../chart/components/screenshot.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/client/src/mvc/visualization/chart/components/screenshot.js b/client/src/mvc/visualization/chart/components/screenshot.js index a449c281948..c352d24580c 100644 --- a/client/src/mvc/visualization/chart/components/screenshot.js +++ b/client/src/mvc/visualization/chart/components/screenshot.js @@ -10,20 +10,32 @@ export function createPNG(options) { }); } -/** SVG export */ export function createSVG(options) { domtoimage.toSvg(options.$el[0]).then(function (dataUrl) { saveAs(dataUrl, `${options.title || "my-chart"}.svg`); }); } -/** PDF export */ export function createPDF(options) { domtoimage.toPng(options.$el[0]).then(function (dataUrl) { const doc = new jsPDF(); - const w = doc.internal.pageSize.getWidth(); - const h = doc.internal.pageSize.getHeight(); - doc.addImage(dataUrl, "png", w * 0.1, h * 0.1, w * 0.8, h * 0.8); + // Calculate scaled image size. We want to leave a small margin, but + // fill the page width otherwise. + const pageWidth = doc.internal.pageSize.getWidth(); + const pageHeight = doc.internal.pageSize.getHeight(); + const elementWidth = options.$el.width(); + const elementHeight = options.$el.height(); + const scale = Math.min(pageWidth / elementWidth, pageHeight / elementHeight) * 0.95; + + doc.addImage( + dataUrl, + "png", + pageWidth * 0.025, + pageHeight * 0.025, + scale * elementWidth, + scale * elementHeight + ); + doc.save(`${options.title || "my-chart"}.pdf`); }); }