Skip to content

plotly-graph PDF with electron's printToPDF #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Sep 1, 2017
Merged
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
33 changes: 33 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
version: 2
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle 2.0 is blazing fast 🐎 - see https://circleci.com/docs/2.0/


jobs:
build:
docker:
# see https://circleci.com/docs/2.0/circleci-images/
# use '-browsers' version to have access to xvfb wrappers
- image: circleci/node:6.10.3-browsers

steps:
- checkout

# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-

- run: npm install

- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}

# install pdftops
- run: sudo apt-get install poppler-utils

- run: npm test

- store_artifacts:
path: build
15 changes: 3 additions & 12 deletions bin/args.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const plotlyGraphCst = require('../src/component/plotly-graph/constants')
const minimist = require('minimist')

const PLOTLYJS_STRING = ['plotly', 'mapbox-access-token', 'topojson', 'mathjax', 'batik']
const PLOTLYJS_STRING = ['plotly', 'mapbox-access-token', 'topojson', 'mathjax']

const PLOTLYJS_ALIAS = {
'plotly': ['plotlyjs', 'plotly-js', 'plotly_js', 'plotlyJS', 'plotlyJs'],
Expand All @@ -13,8 +13,7 @@ const PLOTLYJS_DEFAULT = {
'plotly': '',
'mapbox-access-token': process.env.MAPBOX_ACCESS_TOKEN || '',
'topojson': '',
'mathjax': '',
'batik': process.env.BATIK_RASTERIZER_PATH || ''
'mathjax': ''
}

const DESCRIPTION = {
Expand All @@ -32,9 +31,7 @@ const DESCRIPTION = {

topojson: `Sets path to topojson files. By default topojson files on the plot.ly CDN are used.`,

mathjax: `Sets path to MathJax files. Required to export LaTeX characters.`,

batik: 'Sets path to batik-rasterizer jar file. Required to export PDF and EPS formats.'
mathjax: `Sets path to MathJax files. Required to export LaTeX characters.`
}

const EXPORTER_MINIMIST_CONFIG = {
Expand Down Expand Up @@ -151,9 +148,6 @@ exports.getExporterHelpMsg = function () {
--mathjax ${formatAliases('mathjax')}
${DESCRIPTION.mathjax}

--batik
${DESCRIPTION.batik}

--format ${formatAliases('format')}
Sets the output format (${Object.keys(plotlyGraphCst.contentFormat).join(', ')}). Applies to all output images.

Expand Down Expand Up @@ -219,9 +213,6 @@ exports.getServerHelpMsg = function () {
--mathjax ${formatAliases('mathjax')}
${DESCRIPTION.mathjax}

--batik
${DESCRIPTION.batik}

--debug
${DESCRIPTION.debug}
`
Expand Down
23 changes: 1 addition & 22 deletions bin/plotly-export-server_electron.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const plotlyExporter = require('../')
const Batik = require('../src/util/batik')
const { getServerArgs, getServerHelpMsg } = require('./args')
const pkg = require('../package.json')

Expand All @@ -20,32 +19,12 @@ if (argv.help) {
// - try https://github.com/indexzero/node-portfinder

let app
let batik

if (argv.batik) {
if (!Batik.isJavaInstalled()) {
console.warn('Missing binaries for PDF exports')
process.exit(1)
}
if (!Batik.isPdftopsInstalled()) {
console.warn('Missing binaries for EPS exports')
process.exit(1)
}

batik = new Batik(argv.batik)

if (!batik.doesBatikJarExist()) {
console.warn('Path to batik-rasterizer jar file does not exist')
process.exit(1)
}
}

const plotlyJsOpts = {
plotlyJS: argv.plotlyJS,
mapboxAccessToken: argv['mapbox-access-token'],
mathjax: argv.mathjax,
topojson: argv.topojson,
batik: batik
topojson: argv.topojson
}

const opts = {
Expand Down
1 change: 0 additions & 1 deletion bin/plotly-graph-exporter_electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ getStdin().then((txt) => {
mapboxAccessToken: argv['mapbox-access-token'],
mathjax: argv.mathjax,
topojson: argv.topojson,
batik: argv.batik || path.join(__dirname, '..', 'build', 'batik-1.7', 'batik-rasterizer.jar'),
format: argv.format,
scale: argv.scale,
width: argv.width,
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"plotly-export-server": "./bin/plotly-export-server.js"
},
"scripts": {
"pretest": "node test/pretest.js",
"test:lint": "standard | snazzy",
"test:unit": "tap test/unit/*_test.js",
"test:integration": "tap test/integration/*_test.js",
Expand All @@ -35,6 +36,7 @@
"request": "^2.81.0",
"run-parallel": "^1.1.6",
"run-parallel-limit": "^1.0.3",
"run-series": "^1.1.4",
"semver": "^5.4.1",
"string-to-stream": "^1.1.0",
"uuid": "^3.1.0"
Expand Down
7 changes: 7 additions & 0 deletions src/component/plotly-dashboard/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
iframeLoadDelay: 5000,

statusMsg: {
525: 'print to PDF error'
}
}
37 changes: 15 additions & 22 deletions src/component/plotly-dashboard/render.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const IFRAME_LOAD_TIMEOUT = 5000
const remote = require('../../util/remote')
const cst = require('./constants')

/**
* @param {object} info : info object
Expand All @@ -12,56 +13,48 @@ const IFRAME_LOAD_TIMEOUT = 5000
* - imgData
*/
function render (info, opts, sendToMain) {
// Cannot require 'remote' in the module scope
// as this file gets required in main process first
// during the coerce-component step
//
// TODO
// - maybe require this in <html> from create-index,
// so that we don't have to worry about requiring it
// inside the function body AND to make mockable for testing
const {BrowserWindow} = require('electron').remote

let win = new BrowserWindow({
let win = remote.createBrowserWindow({
width: info.width,
height: info.height
})

const contents = win.webContents
const result = {}
let contents = win.webContents
let errorCode = null

const done = () => {
win.close()

if (errorCode) {
result.msg = cst.statusMsg[errorCode]
}
sendToMain(errorCode, result)
}

win.on('closed', () => {
win = null
})

// ... or plain index.html + `win.executeJavascript`
win.loadURL(info.url)

// TODO
// - find better solution than IFRAME_LOAD_TIMEOUT
// - but really, we shouldn't be using iframes in embed view?
// - use `content.capturePage` to render PNGs and JPEGs
// - or use batik?

contents.once('did-finish-load', () => {
setTimeout(() => {
contents.printToPDF({}, (err, imgData) => {
if (err) {
result.msg = 'print to PDF error'
sendToMain(525, result)
errorCode = 525
return done()
}

result.imgData = imgData
sendToMain(null, result)
done()
return done()
})
}, IFRAME_LOAD_TIMEOUT)
}, cst.iframeLoadDelay)
})

win.loadURL(info.url)
}

module.exports = render
8 changes: 7 additions & 1 deletion src/component/plotly-graph/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,11 @@ module.exports = {
svg: /^data:image\/svg\+xml,/
},

mathJaxConfigQuery: '?config=TeX-AMS-MML_SVG'
mathJaxConfigQuery: '?config=TeX-AMS-MML_SVG',

// config option passed in render step
plotGlPixelRatio: 3,

// time [in ms] after which printToPDF errors when image isn't loaded
pdfPageLoadImgTimeout: 2000
}
68 changes: 36 additions & 32 deletions src/component/plotly-graph/convert.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const Batik = require('../../util/batik')
const Pdftops = require('../../util/pdftops')
const cst = require('./constants')

/** plotly-graph convert
Expand All @@ -7,7 +7,7 @@ const cst = require('./constants')
* - format {string} (from parse)
* - imgData {string} (from render)
* @param {object} opts : component options
* - batik {string or instance of Batik}
* - pdftops {string or instance of Pdftops)
* @param {function} reply
* - errorCode {number or null}
* - result {object}
Expand All @@ -19,59 +19,63 @@ function convert (info, opts, reply) {
const imgData = info.imgData
const format = info.format

const result = {
head: {'Content-Type': cst.contentFormat[format]}
}

const result = {}
let errorCode = null
let body
let bodyLength

const done = () => {
if (errorCode) {
result.msg = cst.statusMsg[errorCode]
} else {
result.body = body
result.bodyLength = bodyLength
result.head = {
'Content-Type': cst.contentFormat[format],
'Content-Length': bodyLength
}
}
reply(errorCode, result)
}

const pdf2eps = (pdf, cb) => {
const pdftops = opts.pdftops instanceof Pdftops
? opts.pdftops
: new Pdftops(opts.pdftops)

pdftops.pdf2eps(pdf, {id: info.id}, (err, eps) => {
if (err) {
errorCode = 530
result.error = err
return done()
}
cb(eps)
})
}

// TODO
// - should pdf and eps format be part of a streambed-only component?
// - should we use batik for that or something?
// - is the 'encoded' option still relevant?

switch (format) {
case 'png':
case 'jpeg':
case 'webp':
const body = result.body = Buffer.from(imgData, 'base64')
result.bodyLength = result.head['Content-Length'] = body.length
case 'pdf':
body = Buffer.from(imgData, 'base64')
bodyLength = body.length
return done()
case 'svg':
// see http://stackoverflow.com/a/12205668/800548
result.body = imgData
result.bodyLength = encodeURI(imgData).split(/%..|./).length - 1
body = imgData
bodyLength = encodeURI(imgData).split(/%..|./).length - 1
return done()
case 'pdf':
case 'eps':
if (!opts.batik) {
errorCode = 530
result.error = new Error('path to batik-rasterizer jar not given')
pdf2eps(imgData, (eps) => {
body = eps
bodyLength = body.length
return done()
}

const batik = opts.batik instanceof Batik
? opts.batik
: new Batik(opts.batik)

batik.convertSVG(info.imgData, {format: format}, (err, buf) => {
if (err) {
errorCode = 530
result.error = err
return done()
}

result.bodyLength = result.head['Content-Length'] = buf.length
result.body = buf
done()
})
break
}
}

Expand Down
Loading