Skip to content

Commit 54b4173

Browse files
committed
add printToPDF renderer logic for plotly-graph
- when *batik* option isn't defined, use win.webContents.printToPDF - step-by-step: + use svg image string to render image in DOM + call window.getSelection.selectAllChidren to grab only img div + call printToPDF on img div
1 parent 090b200 commit 54b4173

File tree

4 files changed

+289
-57
lines changed

4 files changed

+289
-57
lines changed

bin/plotly-graph-exporter_electron.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ getStdin().then((txt) => {
4545
mapboxAccessToken: argv['mapbox-access-token'],
4646
mathjax: argv.mathjax,
4747
topojson: argv.topojson,
48-
batik: argv.batik || path.join(__dirname, '..', 'build', 'batik-1.7', 'batik-rasterizer.jar'),
48+
batik: argv.batik,
4949
format: argv.format,
5050
scale: argv.scale,
5151
width: argv.width,

src/component/plotly-graph/convert.js

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,46 +32,58 @@ function convert (info, opts, reply) {
3232
reply(errorCode, result)
3333
}
3434

35+
const toBuffer = () => {
36+
const body = result.body = Buffer.from(imgData, 'base64')
37+
result.bodyLength = result.head['Content-Length'] = body.length
38+
return done()
39+
}
40+
41+
const convertSVG = () => {
42+
const batik = opts.batik instanceof Batik
43+
? opts.batik
44+
: new Batik(opts.batik)
45+
46+
batik.convertSVG(imgData, {format: format}, (err, buf) => {
47+
if (err) {
48+
errorCode = 530
49+
result.error = err
50+
return done()
51+
}
52+
53+
result.bodyLength = result.head['Content-Length'] = buf.length
54+
result.body = buf
55+
return done()
56+
})
57+
}
58+
59+
// TODO
60+
const pdf2eps = () => {}
61+
3562
// TODO
36-
// - should pdf and eps format be part of a streambed-only component?
37-
// - should we use batik for that or something?
3863
// - is the 'encoded' option still relevant?
3964

4065
switch (format) {
4166
case 'png':
4267
case 'jpeg':
4368
case 'webp':
44-
const body = result.body = Buffer.from(imgData, 'base64')
45-
result.bodyLength = result.head['Content-Length'] = body.length
46-
return done()
69+
return toBuffer()
4770
case 'svg':
4871
// see http://stackoverflow.com/a/12205668/800548
4972
result.body = imgData
5073
result.bodyLength = encodeURI(imgData).split(/%..|./).length - 1
5174
return done()
5275
case 'pdf':
76+
if (opts.batik) {
77+
return convertSVG()
78+
} else {
79+
return toBuffer()
80+
}
5381
case 'eps':
54-
if (!opts.batik) {
55-
errorCode = 530
56-
result.error = new Error('path to batik-rasterizer jar not given')
57-
return done()
82+
if (opts.batik) {
83+
return convertSVG()
84+
} else {
85+
return pdf2eps()
5886
}
59-
60-
const batik = opts.batik instanceof Batik
61-
? opts.batik
62-
: new Batik(opts.batik)
63-
64-
batik.convertSVG(info.imgData, {format: format}, (err, buf) => {
65-
if (err) {
66-
errorCode = 530
67-
result.error = err
68-
return done()
69-
}
70-
71-
result.bodyLength = result.head['Content-Length'] = buf.length
72-
result.body = buf
73-
done()
74-
})
7587
}
7688
}
7789

src/component/plotly-graph/render.js

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/* global Plotly:false */
22

3-
const cst = require('./constants')
43
const semver = require('semver')
4+
const remote = require('../../util/remote')
5+
const cst = require('./constants')
56

67
/**
78
* @param {object} info : info object
@@ -12,6 +13,7 @@ const semver = require('semver')
1213
* - scale
1314
* @param {object} opts : component options
1415
* - mapboxAccessToken
16+
* - batik
1517
* @param {function} sendToMain
1618
* - errorCode
1719
* - result
@@ -40,26 +42,34 @@ function render (info, opts, sendToMain) {
4042
// - figure out if we still need this:
4143
// https://github.com/plotly/streambed/blob/7311d4386d80d45999797e87992f43fb6ecf48a1/image_server/server_app/main.js#L224-L229
4244
// - increase pixel ratio images (scale up here, scale down in convert) ??
45+
// + scale down using https://github.com/oliver-moran/jimp ??
4346
// - does webp (via batik) support transparency now?
4447

48+
const PDF_OR_EPS = (format === 'pdf' || format === 'eps')
49+
const PRINT_TO_PDF = PDF_OR_EPS && !opts.batik
50+
4551
const imgOpts = {
46-
format: (format === 'pdf' || format === 'eps') ? 'svg' : format,
52+
format: PDF_OR_EPS ? 'svg' : format,
4753
width: info.scale * info.width,
4854
height: info.scale * info.height,
4955
// return image data w/o the leading 'data:image' spec
50-
imageDataOnly: true,
56+
imageDataOnly: !PRINT_TO_PDF,
5157
// blend jpeg background color as jpeg does not support transparency
5258
setBackground: format === 'jpeg' ? 'opaque' : ''
5359
}
5460

5561
let promise
5662

5763
if (semver.gte(Plotly.version, '1.30.0')) {
58-
promise = Plotly.toImage({
59-
data: figure.data,
60-
layout: figure.layout,
61-
config: config
62-
}, imgOpts)
64+
promise = Plotly
65+
.toImage({data: figure.data, layout: figure.layout, config: config}, imgOpts)
66+
.then((imgData) => {
67+
if (PRINT_TO_PDF) {
68+
return toPDF(imgData, imgOpts)
69+
} else {
70+
return imgData
71+
}
72+
})
6373
} else if (semver.gte(Plotly.version, '1.11.0')) {
6474
const gd = document.createElement('div')
6575

@@ -69,13 +79,20 @@ function render (info, opts, sendToMain) {
6979
.then((imgData) => {
7080
Plotly.purge(gd)
7181

72-
switch (imgOpts.format) {
82+
switch (format) {
7383
case 'png':
7484
case 'jpeg':
7585
case 'webp':
7686
return imgData.replace(cst.imgPrefix.base64, '')
7787
case 'svg':
78-
return decodeURIComponent(imgData.replace(cst.imgPrefix.svg, ''))
88+
return decodeSVG(imgData)
89+
case 'pdf':
90+
case 'eps':
91+
if (PRINT_TO_PDF) {
92+
return toPDF(imgData, imgOpts, info)
93+
} else {
94+
return decodeSVG(imgData)
95+
}
7996
}
8097
})
8198
} else {
@@ -95,4 +112,59 @@ function render (info, opts, sendToMain) {
95112
})
96113
}
97114

115+
function decodeSVG (imgData) {
116+
return window.decodeURIComponent(imgData.replace(cst.imgPrefix.svg, ''))
117+
}
118+
119+
/**
120+
* See https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentsprinttopdfoptions-callback
121+
* for other available options
122+
*/
123+
function toPDF (imgData, imgOpts, info) {
124+
const win = remote.getCurrentWindow()
125+
126+
// TODO
127+
// - how to (robustly) get pixel to microns (for pageSize) conversion factor
128+
// - this work great, except runner app can't get all pdf to generate
129+
// when parallelLimit > 1 ???
130+
// + figure out why???
131+
// + maybe restrict that in coerce-opts?
132+
const printOpts = {
133+
marginsType: 1,
134+
printSelectionOnly: true,
135+
pageSize: {
136+
width: (imgOpts.width) / 0.0035,
137+
height: (imgOpts.height) / 0.0035
138+
}
139+
}
140+
141+
return new Promise((resolve, reject) => {
142+
const div = document.createElement('div')
143+
const img = document.createElement('img')
144+
145+
document.body.appendChild(div)
146+
div.appendChild(img)
147+
148+
img.addEventListener('load', () => {
149+
window.getSelection().selectAllChildren(div)
150+
151+
win.webContents.printToPDF(printOpts, (err, pdfData) => {
152+
document.body.removeChild(div)
153+
154+
if (err) {
155+
return reject(new Error('electron print to PDF error'))
156+
}
157+
return resolve(pdfData)
158+
})
159+
})
160+
161+
img.addEventListener('error', () => {
162+
document.body.removeChild(div)
163+
return reject(new Error('image failed to load'))
164+
})
165+
166+
img.src = imgData
167+
})
168+
}
169+
98170
module.exports = render

0 commit comments

Comments
 (0)