diff --git a/repos/kaleido/cc/kaleido.cc b/repos/kaleido/cc/kaleido.cc index 84e4a3da..3f43909f 100644 --- a/repos/kaleido/cc/kaleido.cc +++ b/repos/kaleido/cc/kaleido.cc @@ -313,6 +313,11 @@ void Kaleido::OnExportComplete( int exitCode = std::system(command.c_str()); if (exitCode != 0) { kaleido::utils::writeJsonMessage(exitCode, "SVG to EMF conversion failed"); + + // cleanup temporary files + std::remove(inFileName.c_str()); + std::remove(outFileName.c_str()); + ExportNext(); return; } diff --git a/repos/kaleido/js/package-lock.json b/repos/kaleido/js/package-lock.json index 4e974fcf..e8b470ef 100644 --- a/repos/kaleido/js/package-lock.json +++ b/repos/kaleido/js/package-lock.json @@ -1,5 +1,5 @@ { - "name": "orca_next", + "name": "kaleido", "version": "1.0.0", "lockfileVersion": 1, "requires": true, @@ -1275,6 +1275,11 @@ "process": "~0.11.0" } }, + "tinycolor2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" + }, "tty-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", diff --git a/repos/kaleido/js/package.json b/repos/kaleido/js/package.json index d05d6544..68f142bd 100644 --- a/repos/kaleido/js/package.json +++ b/repos/kaleido/js/package.json @@ -1,5 +1,5 @@ { - "name": "orca_next", + "name": "kaleido", "version": "1.0.0", "description": "", "main": "index.js", @@ -13,7 +13,8 @@ "dependencies": { "fast-isnumeric": "^1.1.4", "is-plain-obj": "^2.1.0", - "semver": "^7.3.2" + "semver": "^7.3.2", + "tinycolor2": "^1.4.1" }, "devDependencies": { "browserify": "^16.5.1", diff --git a/repos/kaleido/js/src/plotly/render.js b/repos/kaleido/js/src/plotly/render.js index 1da555f3..e2b3d767 100644 --- a/repos/kaleido/js/src/plotly/render.js +++ b/repos/kaleido/js/src/plotly/render.js @@ -3,6 +3,13 @@ const semver = require('semver') const cst = require('./constants') const parse = require('./parse') +const tinycolor = require('tinycolor2') + +const fillUrl = /(^|; )fill: url\('#([^']*)'\);/ +const fillRgbaColor = /(^|; )fill: rgba\(([^)]*)\);/ +const fillOpacityZero = /(^|\s*)fill-opacity: 0;/ +const strokeOpacityZero = /(^|; )stroke-opacity: 0;/ +const opacityZero = /(^|; )opacity: 0;/ /** * @param {object} info : info object @@ -185,6 +192,31 @@ function render (info, mapboxAccessToken, topojsonURL) { }) } + if (PRINT_TO_EMF) { + exportPromise = exportPromise.then((response) => { + + // Get background color from figure's definition + var bgColor; + var bgColorStr; + if ((figure.layout || {}).paper_bgcolor) { + // Get background color from layout, if any + bgColorStr = figure.layout.paper_bgcolor; + } else if (((((figure.layout || {}).template) || {}).layout || {}).paper_bgcolor) { + // Get background color from template, if any + bgColorStr = figure.layout.template.layout.paper_bgcolor; + } else { + // Background color is white + bgColorStr = "white"; + } + + + var color = tinycolor(bgColorStr).toRgb() + bgColor = [color.r, color.g, color.b] + // return response; + return cleanSvg(response, bgColor) + }) + } + return exportPromise .catch((err) => { errorCode = 525 @@ -198,4 +230,128 @@ function decodeSVG (imgData) { return window.decodeURIComponent(imgData.replace(cst.imgPrefix.svg, '')) } +function cleanSvg (response, bgColor) { + + const svg = response.result + // Import svg string into a dom element + const doc = new DOMParser().parseFromString(svg, 'application/xml'); + const fragment = doc.children[0]; + + // Remove path and rectangles that are compleletely transparent + fragment.querySelectorAll('rect, path').forEach(function (node) { + var style = node.getAttribute('style') + if (style && ( + (style.match(fillOpacityZero) && style.match(strokeOpacityZero)) || + style.match(opacityZero) + )) node.remove() + }) + + // Set fill color to background color if its fill-opacity is 0 but stroke-opacity isn't 0 + fragment.querySelectorAll('rect').forEach(function (node) { + var style = node.getAttribute('style') + if (!style) return + var m = style.match(fillOpacityZero) + + if (m) { + var sep = m[1] + var rgbFill = `${sep}fill: rgb(${bgColor[0]},${bgColor[1]},${bgColor[2]});` + style = style.replace(m[0], rgbFill) + node.setAttribute('style', style) + } + }) + + // Fix black legends by removing rect.legendtoggle + // regexp: svg = svg.replace(/]+>/g, '') + fragment.querySelectorAll('rect.legendtoggle').forEach(node => node.remove()) + + // Remove colorbar background if it's transparent + fragment.querySelectorAll('rect.cbbg').forEach(function (node) { + var style = node.getAttribute('style') + if (style && style.match(fillOpacityZero)) node.remove() + }) + + // Fix fill in colorbars + fragment.querySelectorAll('rect.cbfill').forEach(function (node) { + var style = node.getAttribute('style') + if (style && style.match(fillUrl)) { + var gradientId = style.match(fillUrl)[2] + + var el = fragment.getElementById(gradientId) + // Inkscape doesn't deal well with gradientUnits="objectBoundingBox" + el.setAttribute('gradientUnits', 'userSpaceOnUse') + var height = node.getAttribute('height') + el.setAttribute('y1', height) + } + }) + + + + // Fix path with rgba color for fill + fragment.querySelectorAll('path').forEach(function (node) { + var style = node.getAttribute('style') + if (!style) return + var m = style.match(fillRgbaColor) + if (m) { + var sep = m[1] + var rgba = m[2].split(',') + if (rgba[3] === 0) { + node.remove() + } else { + var rgbFill = `${sep}fill: rgb(${rgba.slice(0, 3).join(',')})` + style = style.replace(m[0], rgbFill) + node.setAttribute('style', style) + } + } + }) + + // Fix black background in rasterized images (WebGL) + const canvas = document.createElement("canvas"); + + const promises = []; + fragment.querySelectorAll('image').forEach(function (node) { + var dataType = 'data:image/png;base64' + var href = node.getAttribute('xlink:href') + var parts = href.split(',') + if (parts[0] === dataType) { + const ctx = canvas.getContext("2d"); + + const promise = new Promise((resolve, reject) => { + const img = new Image(); + img.addEventListener("load", () => resolve(img)); + img.addEventListener("error", err => reject(err)); + img.src = href; + }).then((img) => { + ctx.drawImage(img, 0, 0); + const image = ctx.getImageData(0, 0, img.width, img.height) + + for (var y = 0; y < image.height; y++) { + for (var x = 0; x < image.width; x++) { + var idx = (image.width * y + x) << 2 + + var alpha = image.data[idx + 3] + if (alpha < 255) { + // Manually do alpha composition (https://en.wikipedia.org/wiki/Alpha_compositing) + image.data[idx] = image.data[idx] * alpha / 255 + bgColor[0] * (1 - alpha / 255) + image.data[idx + 1] = image.data[idx + 1] * alpha / 255 + bgColor[1] * (1 - alpha / 255) + image.data[idx + 2] = image.data[idx + 2] * alpha / 255 + bgColor[2] * (1 - alpha / 255) + + image.data[idx + 3] = 255 + } + } + } + + ctx.putImageData(image, 0, 0) + node.setAttribute('xlink:href', canvas.toDataURL()) + }); + + promises.push(promise) + } + }) + + return Promise.all(promises).then(() => { + response.result = fragment.outerHTML + return response + }) +} + module.exports = render diff --git a/repos/kaleido/py/tests/baselines/plotly/mapbox.emf b/repos/kaleido/py/tests/baselines/plotly/mapbox.emf index d7398691..a8131b7e 100644 Binary files a/repos/kaleido/py/tests/baselines/plotly/mapbox.emf and b/repos/kaleido/py/tests/baselines/plotly/mapbox.emf differ diff --git a/repos/kaleido/py/tests/baselines/plotly/mathjax.emf b/repos/kaleido/py/tests/baselines/plotly/mathjax.emf index b574ae4a..ed2a756b 100644 Binary files a/repos/kaleido/py/tests/baselines/plotly/mathjax.emf and b/repos/kaleido/py/tests/baselines/plotly/mathjax.emf differ diff --git a/repos/kaleido/py/tests/baselines/plotly/simple.emf b/repos/kaleido/py/tests/baselines/plotly/simple.emf index 5487b51c..8a348223 100644 Binary files a/repos/kaleido/py/tests/baselines/plotly/simple.emf and b/repos/kaleido/py/tests/baselines/plotly/simple.emf differ diff --git a/repos/kaleido/py/tests/baselines/plotly/topojson.emf b/repos/kaleido/py/tests/baselines/plotly/topojson.emf index d92a42bd..7205fe8a 100644 Binary files a/repos/kaleido/py/tests/baselines/plotly/topojson.emf and b/repos/kaleido/py/tests/baselines/plotly/topojson.emf differ diff --git a/repos/kaleido/py/tests/baselines/plotly/webgl.emf b/repos/kaleido/py/tests/baselines/plotly/webgl.emf index 296fedf5..1a31178f 100644 Binary files a/repos/kaleido/py/tests/baselines/plotly/webgl.emf and b/repos/kaleido/py/tests/baselines/plotly/webgl.emf differ diff --git a/repos/kaleido/py/tests/plotly/fixtures.py b/repos/kaleido/py/tests/plotly/fixtures.py index 43adc590..bf243b5d 100644 --- a/repos/kaleido/py/tests/plotly/fixtures.py +++ b/repos/kaleido/py/tests/plotly/fixtures.py @@ -63,4 +63,5 @@ def all_figures(): (mapbox_figure(), 'mapbox') ] -all_formats = ['png', 'jpeg', 'webp', 'svg', 'pdf', 'eps', 'emf'] +# all_formats = ['png', 'jpeg', 'webp', 'svg', 'pdf', 'eps', 'emf'] +all_formats = ['emf']