Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions repos/kaleido/cc/kaleido.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
7 changes: 6 additions & 1 deletion repos/kaleido/js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions repos/kaleido/js/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "orca_next",
"name": "kaleido",
"version": "1.0.0",
"description": "",
"main": "index.js",
Expand All @@ -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",
Expand Down
156 changes: 156 additions & 0 deletions repos/kaleido/js/src/plotly/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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(/<rect class="legendtoggle"[^>]+>/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
Binary file modified repos/kaleido/py/tests/baselines/plotly/mapbox.emf
Binary file not shown.
Binary file modified repos/kaleido/py/tests/baselines/plotly/mathjax.emf
Binary file not shown.
Binary file modified repos/kaleido/py/tests/baselines/plotly/simple.emf
Binary file not shown.
Binary file modified repos/kaleido/py/tests/baselines/plotly/topojson.emf
Binary file not shown.
Binary file modified repos/kaleido/py/tests/baselines/plotly/webgl.emf
Binary file not shown.
3 changes: 2 additions & 1 deletion repos/kaleido/py/tests/plotly/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']