From 090b200c47c6bfc690f626bf5153ca0ef7233cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Tue, 22 Aug 2017 11:34:08 -0400 Subject: [PATCH 01/18] add util/remote + mucho plotly-dashboard tests - adding util/remote makes the renderer's remote module easily mockable using sinon. - add stubProp common test util, to e.g. reduce iframeLoadDelay in tests --- src/component/plotly-dashboard/constants.js | 7 ++ src/component/plotly-dashboard/render.js | 36 +++++------ src/util/remote.js | 18 ++++++ test/common.js | 27 +++++++- test/unit/plotly-dashboard_test.js | 72 +++++++++++++++++++++ 5 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 src/component/plotly-dashboard/constants.js create mode 100644 src/util/remote.js diff --git a/src/component/plotly-dashboard/constants.js b/src/component/plotly-dashboard/constants.js new file mode 100644 index 00000000..7b6ebf55 --- /dev/null +++ b/src/component/plotly-dashboard/constants.js @@ -0,0 +1,7 @@ +module.exports = { + iframeLoadDelay: 5000, + + statusMsg: { + 525: 'print to PDF error' + } +} diff --git a/src/component/plotly-dashboard/render.js b/src/component/plotly-dashboard/render.js index a5fb5a17..5913c924 100644 --- a/src/component/plotly-dashboard/render.js +++ b/src/component/plotly-dashboard/render.js @@ -1,4 +1,5 @@ -const IFRAME_LOAD_TIMEOUT = 5000 +const remote = require('../../util/remote') +const cst = require('./constants') /** * @param {object} info : info object @@ -12,35 +13,28 @@ 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 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? @@ -51,17 +45,17 @@ function render (info, opts, sendToMain) { 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 diff --git a/src/util/remote.js b/src/util/remote.js new file mode 100644 index 00000000..20fbb15e --- /dev/null +++ b/src/util/remote.js @@ -0,0 +1,18 @@ +/* Small wrapper around the renderer process 'remote' module, to + * easily mock it using sinon in test/common.js + * + * More info on the remote module: + * - https://electron.atom.io/docs/api/remote/ + */ + +function load () { + return require('electron').remote +} + +module.exports = { + createBrowserWindow: (opts) => { + const _module = load() + return new _module.BrowserWindow(opts) + }, + getCurrentWindow: () => load().getCurrentWindow() +} diff --git a/test/common.js b/test/common.js index 8557b59c..64eea0c0 100644 --- a/test/common.js +++ b/test/common.js @@ -1,4 +1,6 @@ const path = require('path') +const EventEmitter = require('events') +const sinon = require('sinon') const paths = {} const urls = {} @@ -13,7 +15,30 @@ paths.glob = path.join(paths.root, 'src', 'util', '*') urls.dummy = 'http://dummy.url' urls.plotlyGraphMock = 'https://raw.githubusercontent.com/plotly/plotly.js/master/test/image/mocks/20.json' +function createMockWindow (opts = {}) { + const win = new EventEmitter() + const webContents = new EventEmitter() + + webContents.printToPDF = sinon.stub() + + Object.assign(win, opts, { + webContents: webContents, + loadURL: () => { webContents.emit('did-finish-load') }, + close: sinon.stub() + }) + + return win +} + +function stubProp (obj, key, newVal) { + const oldVal = obj[key] + obj[key] = newVal + return () => { obj[key] = oldVal } +} + module.exports = { paths: paths, - urls: urls + urls: urls, + createMockWindow: createMockWindow, + stubProp: stubProp } diff --git a/test/unit/plotly-dashboard_test.js b/test/unit/plotly-dashboard_test.js index 4868e8b1..41cfdab5 100644 --- a/test/unit/plotly-dashboard_test.js +++ b/test/unit/plotly-dashboard_test.js @@ -1,5 +1,10 @@ const tap = require('tap') +const sinon = require('sinon') + const _module = require('../../src/component/plotly-dashboard') +const cst = require('../../src/component/plotly-dashboard/constants') +const remote = require('../../src/util/remote') +const { createMockWindow, stubProp } = require('../common') tap.test('parse:', t => { const fn = _module.parse @@ -21,3 +26,70 @@ tap.test('parse:', t => { t.end() }) + +tap.test('render:', t => { + const fn = _module.render + const restoreIframeLoadDelay = stubProp(cst, 'iframeLoadDelay', 0) + + t.afterEach((done) => { + remote.createBrowserWindow.restore() + done() + }) + + t.tearDown(() => { + restoreIframeLoadDelay() + }) + + t.test('should call printToPDF', t => { + const win = createMockWindow() + sinon.stub(remote, 'createBrowserWindow').returns(win) + win.webContents.printToPDF.yields(null, '-> image data <-') + + fn({ + width: 500, + height: 500, + url: 'dummy' + }, {}, (errorCode, result) => { + t.ok(win.webContents.printToPDF.calledOnce) + t.ok(win.close.calledOnce) + t.equal(errorCode, null, 'code') + t.equal(result.imgData, '-> image data <-', 'result') + t.end() + }) + }) + + t.test('should handle printToPDF errors', t => { + const win = createMockWindow() + sinon.stub(remote, 'createBrowserWindow').returns(win) + win.webContents.printToPDF.yields(new Error('printToPDF error')) + + fn({ + width: 500, + height: 500, + url: 'dummy' + }, {}, (errorCode, result) => { + t.ok(win.webContents.printToPDF.calledOnce) + t.ok(win.close.calledOnce) + t.equal(errorCode, 525, 'code') + t.equal(result.msg, 'print to PDF error', 'error msg') + t.end() + }) + }) + + t.test('should cleanup window ref if window is manually closed', t => { + const win = createMockWindow() + sinon.stub(remote, 'createBrowserWindow').returns(win) + + fn({ + width: 500, + height: 500, + url: 'dummy' + }) + + t.equal(win.listenerCount('closed'), 1) + win.on('closed', t.end) + win.emit('closed') + }) + + t.end() +}) From 54b417331d938dacd03695edde3c92d299a724fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Tue, 22 Aug 2017 11:37:38 -0400 Subject: [PATCH 02/18] 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 --- bin/plotly-graph-exporter_electron.js | 2 +- src/component/plotly-graph/convert.js | 62 +++++---- src/component/plotly-graph/render.js | 92 +++++++++++-- test/unit/plotly-graph_test.js | 190 +++++++++++++++++++++++--- 4 files changed, 289 insertions(+), 57 deletions(-) diff --git a/bin/plotly-graph-exporter_electron.js b/bin/plotly-graph-exporter_electron.js index 0534efbd..f9c22c38 100644 --- a/bin/plotly-graph-exporter_electron.js +++ b/bin/plotly-graph-exporter_electron.js @@ -45,7 +45,7 @@ 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'), + batik: argv.batik, format: argv.format, scale: argv.scale, width: argv.width, diff --git a/src/component/plotly-graph/convert.js b/src/component/plotly-graph/convert.js index c2fb98ba..f3ba0b85 100644 --- a/src/component/plotly-graph/convert.js +++ b/src/component/plotly-graph/convert.js @@ -32,46 +32,58 @@ function convert (info, opts, reply) { reply(errorCode, result) } + const toBuffer = () => { + const body = result.body = Buffer.from(imgData, 'base64') + result.bodyLength = result.head['Content-Length'] = body.length + return done() + } + + const convertSVG = () => { + const batik = opts.batik instanceof Batik + ? opts.batik + : new Batik(opts.batik) + + batik.convertSVG(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 + return done() + }) + } + + // TODO + const pdf2eps = () => {} + // 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 - return done() + return toBuffer() case 'svg': // see http://stackoverflow.com/a/12205668/800548 result.body = imgData result.bodyLength = encodeURI(imgData).split(/%..|./).length - 1 return done() case 'pdf': + if (opts.batik) { + return convertSVG() + } else { + return toBuffer() + } case 'eps': - if (!opts.batik) { - errorCode = 530 - result.error = new Error('path to batik-rasterizer jar not given') - return done() + if (opts.batik) { + return convertSVG() + } else { + return pdf2eps() } - - 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() - }) } } diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index 08d84995..2e45548f 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -1,7 +1,8 @@ /* global Plotly:false */ -const cst = require('./constants') const semver = require('semver') +const remote = require('../../util/remote') +const cst = require('./constants') /** * @param {object} info : info object @@ -12,6 +13,7 @@ const semver = require('semver') * - scale * @param {object} opts : component options * - mapboxAccessToken + * - batik * @param {function} sendToMain * - errorCode * - result @@ -40,14 +42,18 @@ function render (info, opts, sendToMain) { // - figure out if we still need this: // https://github.com/plotly/streambed/blob/7311d4386d80d45999797e87992f43fb6ecf48a1/image_server/server_app/main.js#L224-L229 // - increase pixel ratio images (scale up here, scale down in convert) ?? + // + scale down using https://github.com/oliver-moran/jimp ?? // - does webp (via batik) support transparency now? + const PDF_OR_EPS = (format === 'pdf' || format === 'eps') + const PRINT_TO_PDF = PDF_OR_EPS && !opts.batik + const imgOpts = { - format: (format === 'pdf' || format === 'eps') ? 'svg' : format, + format: PDF_OR_EPS ? 'svg' : format, width: info.scale * info.width, height: info.scale * info.height, // return image data w/o the leading 'data:image' spec - imageDataOnly: true, + imageDataOnly: !PRINT_TO_PDF, // blend jpeg background color as jpeg does not support transparency setBackground: format === 'jpeg' ? 'opaque' : '' } @@ -55,11 +61,15 @@ function render (info, opts, sendToMain) { let promise if (semver.gte(Plotly.version, '1.30.0')) { - promise = Plotly.toImage({ - data: figure.data, - layout: figure.layout, - config: config - }, imgOpts) + promise = Plotly + .toImage({data: figure.data, layout: figure.layout, config: config}, imgOpts) + .then((imgData) => { + if (PRINT_TO_PDF) { + return toPDF(imgData, imgOpts) + } else { + return imgData + } + }) } else if (semver.gte(Plotly.version, '1.11.0')) { const gd = document.createElement('div') @@ -69,13 +79,20 @@ function render (info, opts, sendToMain) { .then((imgData) => { Plotly.purge(gd) - switch (imgOpts.format) { + switch (format) { case 'png': case 'jpeg': case 'webp': return imgData.replace(cst.imgPrefix.base64, '') case 'svg': - return decodeURIComponent(imgData.replace(cst.imgPrefix.svg, '')) + return decodeSVG(imgData) + case 'pdf': + case 'eps': + if (PRINT_TO_PDF) { + return toPDF(imgData, imgOpts, info) + } else { + return decodeSVG(imgData) + } } }) } else { @@ -95,4 +112,59 @@ function render (info, opts, sendToMain) { }) } +function decodeSVG (imgData) { + return window.decodeURIComponent(imgData.replace(cst.imgPrefix.svg, '')) +} + +/** + * See https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentsprinttopdfoptions-callback + * for other available options + */ +function toPDF (imgData, imgOpts, info) { + const win = remote.getCurrentWindow() + + // TODO + // - how to (robustly) get pixel to microns (for pageSize) conversion factor + // - this work great, except runner app can't get all pdf to generate + // when parallelLimit > 1 ??? + // + figure out why??? + // + maybe restrict that in coerce-opts? + const printOpts = { + marginsType: 1, + printSelectionOnly: true, + pageSize: { + width: (imgOpts.width) / 0.0035, + height: (imgOpts.height) / 0.0035 + } + } + + return new Promise((resolve, reject) => { + const div = document.createElement('div') + const img = document.createElement('img') + + document.body.appendChild(div) + div.appendChild(img) + + img.addEventListener('load', () => { + window.getSelection().selectAllChildren(div) + + win.webContents.printToPDF(printOpts, (err, pdfData) => { + document.body.removeChild(div) + + if (err) { + return reject(new Error('electron print to PDF error')) + } + return resolve(pdfData) + }) + }) + + img.addEventListener('error', () => { + document.body.removeChild(div) + return reject(new Error('image failed to load')) + }) + + img.src = imgData + }) +} + module.exports = render diff --git a/test/unit/plotly-graph_test.js b/test/unit/plotly-graph_test.js index 3212f291..200e66a9 100644 --- a/test/unit/plotly-graph_test.js +++ b/test/unit/plotly-graph_test.js @@ -1,8 +1,10 @@ const tap = require('tap') const sinon = require('sinon') +const EventEmitter = require('events') const _module = require('../../src/component/plotly-graph') -const { paths } = require('../common') +const remote = require('../../src/util/remote') +const { paths, createMockWindow } = require('../common') tap.test('inject:', t => { const fn = _module.inject @@ -318,13 +320,13 @@ tap.test('convert:', t => { const fn = _module.convert t.test('should convert image data to buffer', t => { - const formats = ['png', 'webp', 'jpeg'] + const formats = ['png', 'webp', 'jpeg', 'pdf'] formats.forEach(f => { t.test(`for format ${f}`, t => { fn({imgData: 'asdfDFDASFsafadsf', format: f}, {}, (errorCode, result) => { t.equal(errorCode, null) - t.equal(result.head['Content-Type'], `image/${f}`) + t.match(result.head['Content-Type'], f) t.type(result.body, Buffer) t.type(result.bodyLength, 'number') t.end() @@ -345,7 +347,7 @@ tap.test('convert:', t => { }) }) - t.test('should convert image data to pdf', t => { + t.test('should convert image data to pdf when *batik* option is set', t => { const info = {imgData: '', format: 'pdf'} const opts = {batik: paths.batik} @@ -357,6 +359,22 @@ tap.test('convert:', t => { }) }) + t.test('should pass batik errors', t => { + const info = {imgData: '', format: 'pdf'} + const opts = {batik: 'not-gonna-work'} + + fn(info, opts, (errorCode, result) => { + t.equal(errorCode, 530) + t.match(result.msg, 'image conversion error') + t.end() + }) + }) + + t.test('should image data to eps', t => { + // TODO + t.end() + }) + t.end() }) @@ -371,51 +389,112 @@ tap.test('render:', t => { let Plotly let document + let window t.beforeEach((done) => { global.Plotly = Plotly = {} global.document = document = {} + global.window = window = {} done() }) t.afterEach((done) => { delete global.Plotly delete global.document + delete global.window done() }) const mock130 = () => { Plotly.version = '1.30.0' - Plotly.toImage = sinon.stub().returns(new Promise(resolve => { - resolve('image data') - })) + Plotly.toImage = sinon.stub().returns( + new Promise(resolve => resolve('image data')) + ) } const mock110 = () => { Plotly.version = '1.11.0' - Plotly.toImage = sinon.stub().returns(new Promise(resolve => { - resolve('image data') - })) - Plotly.newPlot = sinon.stub().returns(new Promise(resolve => { - resolve({}) - })) + Plotly.toImage = sinon.stub().resolves( + new Promise(resolve => resolve('image data')) + ) + Plotly.newPlot = sinon.stub().resolves( + new Promise(resolve => resolve({})) + ) Plotly.purge = sinon.stub() + } + + const mockBrowser = () => { + document.body = { + appendChild: sinon.stub(), + removeChild: sinon.stub() + } document.createElement = sinon.stub() + window.getSelection = sinon.stub().returns({selectAllChildren: sinon.stub()}) + window.decodeURIComponent = sinon.stub().returns('decoded image data') + } + + const mockElements = () => { + const div = {appendChild: sinon.stub()} + document.createElement.withArgs('div').returns(div) + + const img = new EventEmitter() + document.createElement.withArgs('img').returns(img) + img.addEventListener = img.addListener + + return { + div: div, + img: img + } + } + + const mockWindow = () => { + const win = createMockWindow() + sinon.stub(remote, 'getCurrentWindow').returns(win) + + return { + win: win, + restore: () => remote.getCurrentWindow.restore() + } } t.test('v1.30.0 and up', t => { - mock130() + t.test('(format png)', t => { + mock130() - fn({}, {}, (errorCode, result) => { - t.equal(result.imgData, 'image data') - t.ok(Plotly.toImage.calledOnce) - t.end() + fn({}, {}, (errorCode, result) => { + t.equal(result.imgData, 'image data') + t.ok(Plotly.toImage.calledOnce) + t.end() + }) }) + + t.test('(format pdf)', t => { + mock130() + mockBrowser() + const {img} = mockElements() + const {win, restore} = mockWindow() + win.webContents.printToPDF.yields(null, 'pdf data') + + fn({format: 'pdf'}, {}, (errorCode, result) => { + t.equal(errorCode, null) + t.equal(result.imgData, 'pdf data') + t.equal(document.createElement.callCount, 2, 'createElement calls') + t.equal(document.body.appendChild.callCount, 1, 'body.appendChild calls') + t.equal(document.body.removeChild.callCount, 1, 'body.removeChild calls') + + restore() + t.end() + }) + setTimeout(() => img.emit('load')) + }) + + t.end() }) t.test('v1.11.0 <= versions < v1.30.0', t => { t.test('(format png)', t => { mock110() + mockBrowser() fn({}, {}, (errorCode, result) => { t.equal(result.imgData, 'image data') @@ -429,7 +508,7 @@ tap.test('render:', t => { t.test('(format svg)', t => { mock110() - global.decodeURIComponent = sinon.stub().returns('decoded image data') + mockBrowser() fn({format: 'svg'}, {}, (errorCode, result) => { t.equal(result.imgData, 'decoded image data') @@ -437,11 +516,45 @@ tap.test('render:', t => { t.ok(Plotly.newPlot.calledOnce) t.ok(Plotly.toImage.calledOnce) t.ok(Plotly.purge.calledOnce) - t.ok(global.decodeURIComponent) + t.end() + }) + }) + + t.test('(format pdf)', t => { + mock110() + mockBrowser() + const {img} = mockElements() + const {win, restore} = mockWindow() + win.webContents.printToPDF.yields(null, 'pdf data') + + fn({format: 'pdf'}, {}, (errorCode, result) => { + t.equal(errorCode, null) + t.equal(result.imgData, 'pdf data') + t.equal(document.createElement.callCount, 3, 'createElement calls') + t.equal(document.body.appendChild.callCount, 1, 'body.appendChild calls') + t.equal(document.body.removeChild.callCount, 1, 'body.removeChild calls') + + restore() + t.end() + }) + setTimeout(() => img.emit('load')) + }) + + t.test('(format pdf with batik support)', t => { + mock110() + mockBrowser() + const {img} = mockElements() + const {win, restore} = mockWindow() + win.webContents.printToPDF.yields(null, 'pdf data') + + fn({format: 'pdf'}, {batik: 'path-to-batik'}, (errorCode, result) => { + t.equal(errorCode, null) + t.equal(result.imgData, 'decoded image data') - delete global.decodeURIComponent + restore() t.end() }) + setTimeout(() => img.emit('load')) }) t.end() @@ -468,6 +581,41 @@ tap.test('render:', t => { }) }) + t.test('should return error code on printToPDF errors', t => { + mock130() + mockBrowser() + const {img} = mockElements() + const {win, restore} = mockWindow() + win.webContents.printToPDF.yields(new Error('oops')) + + fn({format: 'pdf'}, {}, (errorCode, result) => { + t.equal(errorCode, 525) + t.match(result.error, /electron print to PDF error/, 'error') + t.ok(document.body.removeChild.calledOnce) + + restore() + t.end() + }) + setTimeout(() => img.emit('load')) + }) + + t.test('should return error code on createElement(\'img\') errors', t => { + mock130() + mockBrowser() + const {img} = mockElements() + const {restore} = mockWindow() + + fn({format: 'pdf'}, {}, (errorCode, result) => { + t.equal(errorCode, 525) + t.match(result.error, /image failed to load/, 'error') + t.ok(document.body.removeChild.calledOnce) + + restore() + t.end() + }) + setTimeout(() => img.emit('error')) + }) + t.test('should generate svg for format pdf and eps', t => { const formats = ['pdf', 'eps'] From 406b9202cf00f648a1413289fd3e88f74b95eeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Wed, 23 Aug 2017 16:19:55 -0400 Subject: [PATCH 03/18] pass 'scale' to Plotly.toImage - which is functional on branch https://github.com/plotly/plotly.js/compare/to-image-scale --- src/component/plotly-graph/render.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index 2e45548f..9f33686d 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -41,17 +41,16 @@ function render (info, opts, sendToMain) { // TODO // - figure out if we still need this: // https://github.com/plotly/streambed/blob/7311d4386d80d45999797e87992f43fb6ecf48a1/image_server/server_app/main.js#L224-L229 - // - increase pixel ratio images (scale up here, scale down in convert) ?? - // + scale down using https://github.com/oliver-moran/jimp ?? - // - does webp (via batik) support transparency now? const PDF_OR_EPS = (format === 'pdf' || format === 'eps') const PRINT_TO_PDF = PDF_OR_EPS && !opts.batik const imgOpts = { format: PDF_OR_EPS ? 'svg' : format, - width: info.scale * info.width, - height: info.scale * info.height, + width: info.width, + height: info.height, + // works as of https://github.com/plotly/plotly.js/compare/to-image-scale + scale: info.scale, // return image data w/o the leading 'data:image' spec imageDataOnly: !PRINT_TO_PDF, // blend jpeg background color as jpeg does not support transparency From f69172b621b351f5677a65ee6da6896e1ba930df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 24 Aug 2017 12:39:18 -0400 Subject: [PATCH 04/18] rewrite batik util - only expose svg2pdf routine! - i.e. :hocho: eps logic (will be in own pdftops util) and scale logic (to be done in renderer hopefully) --- package.json | 1 + src/util/batik.js | 132 +++++++---------------------------- test/unit/batik_test.js | 148 +++++----------------------------------- 3 files changed, 42 insertions(+), 239 deletions(-) diff --git a/package.json b/package.json index 99634dbf..d96b0b94 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,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" diff --git a/src/util/batik.js b/src/util/batik.js index 94b06466..820ce3bb 100644 --- a/src/util/batik.js +++ b/src/util/batik.js @@ -2,6 +2,7 @@ const fs = require('fs') const path = require('path') const childProcess = require('child_process') const parallel = require('run-parallel') +const series = require('run-series') const uuid = require('uuid/v4') const PATH_TO_BUILD = path.join(__dirname, '..', '..', 'build') @@ -19,32 +20,24 @@ class Batik { */ constructor (batikJar) { this.batikJar = path.resolve(batikJar) - - this.bg = '255.255.255.255' - this.dpi = '300' this.javaBase = 'java -jar -XX:+UseParallelGC -server' this.batikBase = `${this.javaBase} ${this.batikJar}` - this.pdftopsBase = 'pdftops' } - /** Convert svg input + /** Convert svg to pdf * * @param {string} svg : svg string to convert * @param {object} opts : * - id {string} - * - format {string} - * - width {numeric} - * - height {numeric} * @param {function} cb * - err {null || error} * - result {buffer} */ - convertSVG (svg, opts, cb) { + svg2pdf (svg, opts, cb) { const id = opts.id || uuid() - const format = opts.format || 'pdf' - const width = opts.width - const height = opts.height - const isEPS = format === 'eps' + const inPath = path.join(PATH_TO_BUILD, id + '-svg') + const outPath = path.join(PATH_TO_BUILD, id + '-out') + const cmd = `${this.batikBase} -m application/pdf -d ${outPath} ${inPath}` // TODO old batik wrapper had: // @@ -56,78 +49,25 @@ class Batik { // // find out why, and check if it's still needed. - const lookup = { - out: tmpFile(id + '-out', ''), - // TODO do we still need this `\ufeff` addition? - svg: tmpFile(id + '-svg', '\ufeff' + svg) - } - - if (isEPS) { - lookup.eps = tmpFile(id + '-eps', '') - } - - const fileNames = Object.keys(lookup) - const createTasks = fileNames.map(k => lookup[k].create) - const destroyTasks = fileNames.map(k => lookup[k].destroy) - - let args - - switch (format) { - case 'pdf': - case 'eps': - args = `-m application/pdf` - break - default: - args = `-bg ${this.bg} -dpi ${this.dpi} ` - if (width) args += `-w ${width} ` - if (height) args += `-h ${height} ` - break - } - - const done = (err, out) => parallel(destroyTasks, (err2) => { - if (err) { - return cb(err) - } - if (err2) { - return cb(new Error('problem while removing temporary files')) - } - cb(null, out) - }) - - const toBuffer = (outPath) => { - fs.readFile(outPath, (err, buf) => { - if (err) { - return done(new Error('problem while reading output file')) - } - done(null, buf) - }) - } - - // go! - parallel(createTasks, (err) => { - if (err) { - return done(new Error('problem while initializing temporary files')) - } - - const batikCmd = `${this.batikBase} ${args} -d ${lookup.out.path} ${lookup.svg.path}` - - childProcess.exec(batikCmd, (err) => { - if (err) { - return done(new Error('problem while executing batik command')) - } - - if (isEPS) { - const pdftopsCmd = `${this.pdftopsBase} -eps ${lookup.out.path} ${lookup.eps.path}` - - childProcess.exec(pdftopsCmd, (err) => { - if (err) { - return done(new Error('problem while executing pdftops command')) - } - toBuffer(lookup.eps.path) - }) - } else { - toBuffer(lookup.out.path) - } + // TODO do we still need this `\ufeff` addition? + + const createTmpFiles = (cb) => parallel([ + (cb) => fs.writeFile(inPath, '\ufeff' + svg, cb), + (cb) => fs.writeFile(outPath, '', cb) + ], cb) + + const destroyTmpFiles = (cb) => parallel([ + (cb) => fs.unlink(inPath, cb), + (cb) => fs.unlink(outPath, cb) + ], cb) + + series([ + createTmpFiles, + (cb) => childProcess.exec(cmd, cb), + (cb) => fs.readFile(outPath, cb) + ], (err, result) => { + destroyTmpFiles(() => { + cb(err, result[2]) }) }) } @@ -150,28 +90,6 @@ class Batik { } return true } - - /** Is pdftops installed? - * @return {boolean} - */ - static isPdftopsInstalled () { - try { - childProcess.execSync('pdftops -v', {stdio: 'ignore'}) - } catch (e) { - return false - } - return true - } -} - -function tmpFile (fileName, content) { - var pathToFile = path.join(PATH_TO_BUILD, fileName) - - return { - path: pathToFile, - create: (cb) => fs.writeFile(pathToFile, content, 'utf8', cb), - destroy: (cb) => fs.unlink(pathToFile, cb) - } } module.exports = Batik diff --git a/test/unit/batik_test.js b/test/unit/batik_test.js index 65284c89..b5aecd63 100644 --- a/test/unit/batik_test.js +++ b/test/unit/batik_test.js @@ -3,28 +3,17 @@ const sinon = require('sinon') const path = require('path') const fs = require('fs') const childProcess = require('child_process') -const imageSize = require('image-size') const Batik = require('../../src/util/batik') const { paths } = require('../common') const SVG_MOCK = fs.readFileSync(path.join(paths.build, '20.svg')) -tap.test('Batik constructor:', t => { - t.test('', t => { - const batik = new Batik(paths.batik) - t.type(batik.convertSVG, 'function') - t.end() - }) - - t.end() -}) - -tap.test('batik.convertSVG', t => { +tap.test('batik.svg2pdf', t => { t.test('should convert svg to pdf', t => { const batik = new Batik(paths.batik) const outPath = path.join(paths.build, 'batik-test.pdf') - batik.convertSVG(SVG_MOCK, {format: 'pdf'}, (err, result) => { + batik.svg2pdf(SVG_MOCK, {}, (err, result) => { if (err) t.fail(err) t.type(result, Buffer) @@ -39,60 +28,18 @@ tap.test('batik.convertSVG', t => { }) }) - t.test('should convert svg to eps', t => { + t.test('should remove tmp files after conversion', t => { const batik = new Batik(paths.batik) - const outPath = path.join(paths.build, 'batik-test.eps') + const tmpOutPath = path.join(paths.build, 'tmp-out') + const tmpSvgPath = path.join(paths.build, 'tmp-svg') - batik.convertSVG(SVG_MOCK, {format: 'eps'}, (err, result) => { + batik.svg2pdf(SVG_MOCK, {id: 'tmp'}, (err, result) => { if (err) t.fail(err) - t.type(result, Buffer) - - fs.writeFile(outPath, result, (err) => { - if (err) t.fail(err) - const size = fs.statSync(outPath).size - t.ok(size > 1e6, 'min eps file size') - t.ok(size < 2e6, 'max eps file size') - t.end() - }) - }) - }) - - t.test('should convert svg to png', t => { - const batik = new Batik(paths.batik) - const outPath = path.join(paths.build, 'batik-test.png') - - batik.convertSVG(SVG_MOCK, {format: 'png'}, (err, result) => { - if (err) t.fail(err) t.type(result, Buffer) - - fs.writeFile(outPath, result, (err) => { - if (err) t.fail(err) - - const {width, height} = imageSize(outPath) - t.equal(width, 750, 'png width') - t.equal(height, 600, 'png height') - t.end() - }) - }) - }) - - t.test('should rescale svg into png', t => { - const batik = new Batik(paths.batik) - const outPath = path.join(paths.build, 'batik-test.png') - - batik.convertSVG(SVG_MOCK, {format: 'png', width: 200, height: 200}, (err, result) => { - if (err) t.fail(err) - t.type(result, Buffer) - - fs.writeFile(outPath, result, (err) => { - if (err) t.fail(err) - - const {width, height} = imageSize(outPath) - t.equal(width, 200, 'png width') - t.equal(height, 200, 'png height') - t.end() - }) + t.notOk(fs.existsSync(tmpOutPath), 'clears tmp out file') + t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp svg file') + t.end() }) }) @@ -100,74 +47,13 @@ tap.test('batik.convertSVG', t => { const batik = new Batik(paths.batik) batik.batikBase = 'not gonna work' - batik.convertSVG(SVG_MOCK, {}, (err) => { - t.throws(() => { throw err }, /problem while executing batik command/) - t.end() - }) - }) - - t.test('should error out when pdftops command fails', t => { - const batik = new Batik(paths.batik) - batik.pdftopsBase = 'not gonna work' - - batik.convertSVG(SVG_MOCK, {format: 'eps'}, (err) => { - t.throws(() => { throw err }, /problem while executing pdftops command/) - t.end() - }) - }) - - t.test('should error out when something goes wrong when initializing files', t => { - sinon.stub(fs, 'writeFile').yields(true) - - const batik = new Batik(paths.batik) - const outPath = path.join(paths.build, 'tmp-out') - const svgPath = path.join(paths.build, 'tmp-svg') - - batik.convertSVG(SVG_MOCK, {id: 'tmp'}, (err) => { - t.throws(() => { throw err }, /problem while initializing temporary files/) - t.notOk(fs.existsSync(outPath), 'clears tmp out file') - t.notOk(fs.existsSync(svgPath), 'clears tmp svg file') - - fs.writeFile.restore() - t.end() - }) - }) - - t.test('should error out when something goes wrong when reading output files', t => { - sinon.stub(fs, 'readFile').yields(true) - - const batik = new Batik(paths.batik) - const outPath = path.join(paths.build, 'tmp-out') - const svgPath = path.join(paths.build, 'tmp-svg') - - batik.convertSVG(SVG_MOCK, {id: 'tmp'}, (err) => { - t.throws(() => { throw err }, /problem while reading output file/) - t.notOk(fs.existsSync(outPath), 'clears tmp out file') - t.notOk(fs.existsSync(svgPath), 'clears tmp svg file') - - fs.readFile.restore() - t.end() - }) - }) - - t.test('should error out when something goes wrong when removing temporary files', t => { - sinon.stub(fs, 'unlink').yields(true) - - const batik = new Batik(paths.batik) - const outPath = path.join(paths.build, 'tmp-out') - const svgPath = path.join(paths.build, 'tmp-svg') - - batik.convertSVG(SVG_MOCK, {id: 'tmp'}, (err) => { - t.throws(() => { throw err }, /problem while removing temporary files/) - t.ok(fs.existsSync(outPath), 'cannot clear tmp out file') - t.ok(fs.existsSync(svgPath), 'cannot clears tmp svg file') - - fs.unlink.restore() - fs.unlinkSync(outPath) - fs.unlinkSync(svgPath) - t.notOk(fs.existsSync(outPath), 'clears tmp out file') - t.notOk(fs.existsSync(svgPath), 'clears tmp svg file') + const tmpOutPath = path.join(paths.build, 'tmp-out') + const tmpSvgPath = path.join(paths.build, 'tmp-svg') + batik.svg2pdf(SVG_MOCK, {id: 'tmp'}, (err) => { + t.throws(() => { throw err }, /Command failed/) + t.notOk(fs.existsSync(tmpOutPath), 'clears tmp out file') + t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp svg file') t.end() }) }) @@ -190,7 +76,7 @@ tap.test('doesBatikJarExist:', t => { t.end() }) -tap.test('isJavaInstalled / isPdftopsInstalled:', t => { +tap.test('isJavaInstalled', t => { t.afterEach((done) => { childProcess.execSync.restore() done() @@ -199,14 +85,12 @@ tap.test('isJavaInstalled / isPdftopsInstalled:', t => { t.test('should return true when binary execute correctly', t => { sinon.stub(childProcess, 'execSync').returns(true) t.ok(Batik.isJavaInstalled()) - t.ok(Batik.isPdftopsInstalled()) t.end() }) t.test('should return false when binary does not execute correctly', t => { sinon.stub(childProcess, 'execSync').throws() t.notOk(Batik.isJavaInstalled()) - t.notOk(Batik.isPdftopsInstalled()) t.end() }) From b2b3ea38a2dacbf071451ad33e19e61539fbd102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 24 Aug 2017 12:39:40 -0400 Subject: [PATCH 05/18] add pdftops util (to convert pdf2eps) --- src/util/pdftops.js | 64 ++++++++++++++++++++++++++++++ test/unit/pdftops_test.js | 82 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 src/util/pdftops.js create mode 100644 test/unit/pdftops_test.js diff --git a/src/util/pdftops.js b/src/util/pdftops.js new file mode 100644 index 00000000..f55139e4 --- /dev/null +++ b/src/util/pdftops.js @@ -0,0 +1,64 @@ +const fs = require('fs') +const path = require('path') +const childProcess = require('child_process') +const parallel = require('run-parallel') +const series = require('run-series') +const uuid = require('uuid/v4') + +const PATH_TO_BUILD = path.join(__dirname, '..', '..', 'build') + +/** Node wrapper for pdftops + * + * See: + * - https://linux.die.net/man/1/pdftops + */ +class Pdftops { + constructor (pathToPdftops) { + this.cmdBase = pathToPdftops || 'pdftops' + } + + /** Convert PDF to EPS + * + * @param {buffer} pdf : pdf data buffer + * @param {object} opts + * - id {string} + * @param {function} cb + * - err {null || error} + * - result {buffer} + */ + pdf2eps (pdf, opts, cb) { + const id = opts.id || uuid() + const inPath = path.join(PATH_TO_BUILD, id + '-pdf') + const outPath = path.join(PATH_TO_BUILD, id + '-eps') + const cmd = `${this.cmdBase} -eps ${inPath} ${outPath}` + + const destroyTmpFiles = (cb) => parallel([ + (cb) => fs.unlink(inPath, cb), + (cb) => fs.unlink(outPath, cb) + ], cb) + + series([ + (cb) => fs.writeFile(inPath, pdf, 'utf-8', cb), + (cb) => childProcess.exec(cmd, cb), + (cb) => fs.readFile(outPath, cb) + ], (err, result) => { + destroyTmpFiles(() => { + cb(err, result[2]) + }) + }) + } + + /** Is pdftops installed? + * @return {boolean} + */ + static isPdftopsInstalled () { + try { + childProcess.execSync(`${this.cmdBase} -v`, {stdio: 'ignore'}) + } catch (e) { + return false + } + return true + } +} + +module.exports = Pdftops diff --git a/test/unit/pdftops_test.js b/test/unit/pdftops_test.js new file mode 100644 index 00000000..a58b6337 --- /dev/null +++ b/test/unit/pdftops_test.js @@ -0,0 +1,82 @@ +const tap = require('tap') +const sinon = require('sinon') +const path = require('path') +const fs = require('fs') +const childProcess = require('child_process') +const Pdftops = require('../../src/util/pdftops') + +const { paths } = require('../common') +const PDF_MOCK = fs.readFileSync(path.join(paths.build, '20.pdf')) + +tap.test('pdftops.pdf2eps', t => { + t.test('should convert pdf to eps', t => { + const pdftops = new Pdftops() + const outPath = path.join(paths.build, 'pdftops-test.eps') + + pdftops.pdf2eps(PDF_MOCK, {}, (err, result) => { + if (err) t.fail(err) + t.type(result, Buffer) + + fs.writeFile(outPath, result, (err) => { + if (err) t.fail(err) + + const size = fs.statSync(outPath).size + t.ok(size > 1e6, 'min pdf file size') + t.ok(size < 2e6, 'max pdf file size') + t.end() + }) + }) + }) + + t.test('should remove tmp files after conversion', t => { + const pdftops = new Pdftops() + const tmpOutPath = path.join(paths.build, 'tmp-eps') + const tmpSvgPath = path.join(paths.build, 'tmp-pdf') + + pdftops.pdf2eps(PDF_MOCK, {id: 'tmp'}, (err, result) => { + if (err) t.fail(err) + + t.type(result, Buffer) + t.notOk(fs.existsSync(tmpOutPath), 'clears tmp eps file') + t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp pdf file') + t.end() + }) + }) + + t.test('should error out when pdftops command fails', t => { + const pdftops = new Pdftops('not gonna work') + + const tmpOutPath = path.join(paths.build, 'tmp-eps') + const tmpSvgPath = path.join(paths.build, 'tmp-pdf') + + pdftops.pdf2eps(PDF_MOCK, {id: 'tmp'}, (err) => { + t.throws(() => { throw err }, /Command failed/) + t.notOk(fs.existsSync(tmpOutPath), 'clears tmp eps file') + t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp pdf file') + t.end() + }) + }) + + t.end() +}) + +tap.test('isPdftopsInstalled', t => { + t.afterEach((done) => { + childProcess.execSync.restore() + done() + }) + + t.test('should return true when binary execute correctly', t => { + sinon.stub(childProcess, 'execSync').returns(true) + t.ok(Pdftops.isPdftopsInstalled()) + t.end() + }) + + t.test('should return false when binary does not execute correctly', t => { + sinon.stub(childProcess, 'execSync').throws() + t.notOk(Pdftops.isPdftopsInstalled()) + t.end() + }) + + t.end() +}) From b90d7fc65a6aac2e8c1b4c44450ff84480e27aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 24 Aug 2017 12:40:06 -0400 Subject: [PATCH 06/18] add pretest script --- package.json | 1 + test/common.js | 9 +++++++++ test/pretest.js | 17 +++++++++++++++++ test/unit/batik_test.js | 15 +++++++-------- test/unit/pdftops_test.js | 13 ++++++------- 5 files changed, 40 insertions(+), 15 deletions(-) create mode 100644 test/pretest.js diff --git a/package.json b/package.json index d96b0b94..4bb59446 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/common.js b/test/common.js index 64eea0c0..23a5b024 100644 --- a/test/common.js +++ b/test/common.js @@ -1,12 +1,15 @@ +const fs = require('fs') const path = require('path') const EventEmitter = require('events') const sinon = require('sinon') const paths = {} const urls = {} +const mocks = {} paths.root = path.join(__dirname, '..') paths.build = path.join(paths.root, 'build') +paths.bin = path.join(paths.root, 'bin') paths.batik = process.env.BATIK_RASTERIZER_PATH || path.join(paths.build, 'batik-1.7', 'batik-rasterizer.jar') paths.readme = path.join(paths.root, 'README.md') paths.pkg = path.join(paths.root, 'package.json') @@ -15,6 +18,11 @@ paths.glob = path.join(paths.root, 'src', 'util', '*') urls.dummy = 'http://dummy.url' urls.plotlyGraphMock = 'https://raw.githubusercontent.com/plotly/plotly.js/master/test/image/mocks/20.json' +try { + mocks.svg = fs.readFileSync(path.join(paths.build, 'test-mock.svg'), 'utf-8') + mocks.pdf = fs.readFileSync(path.join(paths.build, 'test-mock.pdf')) +} catch (e) {} + function createMockWindow (opts = {}) { const win = new EventEmitter() const webContents = new EventEmitter() @@ -39,6 +47,7 @@ function stubProp (obj, key, newVal) { module.exports = { paths: paths, urls: urls, + mocks: mocks, createMockWindow: createMockWindow, stubProp: stubProp } diff --git a/test/pretest.js b/test/pretest.js new file mode 100644 index 00000000..733fd905 --- /dev/null +++ b/test/pretest.js @@ -0,0 +1,17 @@ +const { execSync } = require('child_process') +const { paths } = require('./common') + +const mock = JSON.stringify({ + data: [{ + y: [1, 2, 1] + }], + layout: { + title: 'test mock' + } +}) + +execSync(`${paths.bin}/plotly-graph-exporter.js '${mock}' -f svg -d build -o test-mock`) +console.log('build/test-mock.svg created') + +execSync(`${paths.bin}/plotly-graph-exporter.js '${mock}' -f pdf -d build -o test-mock`) +console.log('build/test-mock.pdf created') diff --git a/test/unit/batik_test.js b/test/unit/batik_test.js index b5aecd63..6f3b4961 100644 --- a/test/unit/batik_test.js +++ b/test/unit/batik_test.js @@ -3,17 +3,16 @@ const sinon = require('sinon') const path = require('path') const fs = require('fs') const childProcess = require('child_process') -const Batik = require('../../src/util/batik') -const { paths } = require('../common') -const SVG_MOCK = fs.readFileSync(path.join(paths.build, '20.svg')) +const Batik = require('../../src/util/batik') +const { paths, mocks } = require('../common') tap.test('batik.svg2pdf', t => { t.test('should convert svg to pdf', t => { const batik = new Batik(paths.batik) const outPath = path.join(paths.build, 'batik-test.pdf') - batik.svg2pdf(SVG_MOCK, {}, (err, result) => { + batik.svg2pdf(mocks.svg, {}, (err, result) => { if (err) t.fail(err) t.type(result, Buffer) @@ -21,8 +20,8 @@ tap.test('batik.svg2pdf', t => { if (err) t.fail(err) const size = fs.statSync(outPath).size - t.ok(size > 5e4, 'min pdf file size') - t.ok(size < 7e4, 'max pdf file size') + t.ok(size > 5e3, 'min pdf file size') + t.ok(size < 1e4, 'max pdf file size') t.end() }) }) @@ -33,7 +32,7 @@ tap.test('batik.svg2pdf', t => { const tmpOutPath = path.join(paths.build, 'tmp-out') const tmpSvgPath = path.join(paths.build, 'tmp-svg') - batik.svg2pdf(SVG_MOCK, {id: 'tmp'}, (err, result) => { + batik.svg2pdf(mocks.svg, {id: 'tmp'}, (err, result) => { if (err) t.fail(err) t.type(result, Buffer) @@ -50,7 +49,7 @@ tap.test('batik.svg2pdf', t => { const tmpOutPath = path.join(paths.build, 'tmp-out') const tmpSvgPath = path.join(paths.build, 'tmp-svg') - batik.svg2pdf(SVG_MOCK, {id: 'tmp'}, (err) => { + batik.svg2pdf(mocks.svg, {id: 'tmp'}, (err) => { t.throws(() => { throw err }, /Command failed/) t.notOk(fs.existsSync(tmpOutPath), 'clears tmp out file') t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp svg file') diff --git a/test/unit/pdftops_test.js b/test/unit/pdftops_test.js index a58b6337..3cb09622 100644 --- a/test/unit/pdftops_test.js +++ b/test/unit/pdftops_test.js @@ -5,15 +5,14 @@ const fs = require('fs') const childProcess = require('child_process') const Pdftops = require('../../src/util/pdftops') -const { paths } = require('../common') -const PDF_MOCK = fs.readFileSync(path.join(paths.build, '20.pdf')) +const { paths, mocks } = require('../common') tap.test('pdftops.pdf2eps', t => { t.test('should convert pdf to eps', t => { const pdftops = new Pdftops() const outPath = path.join(paths.build, 'pdftops-test.eps') - pdftops.pdf2eps(PDF_MOCK, {}, (err, result) => { + pdftops.pdf2eps(mocks.pdf, {}, (err, result) => { if (err) t.fail(err) t.type(result, Buffer) @@ -21,8 +20,8 @@ tap.test('pdftops.pdf2eps', t => { if (err) t.fail(err) const size = fs.statSync(outPath).size - t.ok(size > 1e6, 'min pdf file size') - t.ok(size < 2e6, 'max pdf file size') + t.ok(size > 5e4, 'min pdf file size') + t.ok(size < 6e4, 'max pdf file size') t.end() }) }) @@ -33,7 +32,7 @@ tap.test('pdftops.pdf2eps', t => { const tmpOutPath = path.join(paths.build, 'tmp-eps') const tmpSvgPath = path.join(paths.build, 'tmp-pdf') - pdftops.pdf2eps(PDF_MOCK, {id: 'tmp'}, (err, result) => { + pdftops.pdf2eps(mocks.pdf, {id: 'tmp'}, (err, result) => { if (err) t.fail(err) t.type(result, Buffer) @@ -49,7 +48,7 @@ tap.test('pdftops.pdf2eps', t => { const tmpOutPath = path.join(paths.build, 'tmp-eps') const tmpSvgPath = path.join(paths.build, 'tmp-pdf') - pdftops.pdf2eps(PDF_MOCK, {id: 'tmp'}, (err) => { + pdftops.pdf2eps(mocks.pdf, {id: 'tmp'}, (err) => { t.throws(() => { throw err }, /Command failed/) t.notOk(fs.existsSync(tmpOutPath), 'clears tmp eps file') t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp pdf file') From 828d2a3245ec9c74bf8a684ba240c984a916cb7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 24 Aug 2017 12:40:27 -0400 Subject: [PATCH 07/18] scale PDF pageSize in render --- src/component/plotly-graph/render.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index 9f33686d..e4b71c15 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -121,6 +121,8 @@ function decodeSVG (imgData) { */ function toPDF (imgData, imgOpts, info) { const win = remote.getCurrentWindow() + const w1 = imgOpts.scale * imgOpts.width + const h1 = imgOpts.scale * imgOpts.height // TODO // - how to (robustly) get pixel to microns (for pageSize) conversion factor @@ -132,8 +134,8 @@ function toPDF (imgData, imgOpts, info) { marginsType: 1, printSelectionOnly: true, pageSize: { - width: (imgOpts.width) / 0.0035, - height: (imgOpts.height) / 0.0035 + width: w1 / 0.0035, + height: h1 / 0.0035 } } @@ -141,6 +143,9 @@ function toPDF (imgData, imgOpts, info) { const div = document.createElement('div') const img = document.createElement('img') + img.width = w1 + img.height = h1 + document.body.appendChild(div) div.appendChild(img) From 1c9193ec650ffffa7f90e638f06619576cade369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 24 Aug 2017 12:40:51 -0400 Subject: [PATCH 08/18] update plotly-graph convert with new batik + pdftops logic --- src/component/plotly-graph/convert.js | 69 +++++++++++++++++++-------- test/unit/plotly-graph_test.js | 61 ++++++++++++++++++----- 2 files changed, 96 insertions(+), 34 deletions(-) diff --git a/src/component/plotly-graph/convert.js b/src/component/plotly-graph/convert.js index f3ba0b85..c8427276 100644 --- a/src/component/plotly-graph/convert.js +++ b/src/component/plotly-graph/convert.js @@ -1,4 +1,5 @@ const Batik = require('../../util/batik') +const Pdftops = require('../../util/pdftops') const cst = require('./constants') /** plotly-graph convert @@ -24,40 +25,48 @@ function convert (info, opts, reply) { } let errorCode = null + let body + let bodyLength const done = () => { if (errorCode) { result.msg = cst.statusMsg[errorCode] + } else { + result.body = body + result.bodyLength = result.head['Content-Length'] = bodyLength } reply(errorCode, result) } - const toBuffer = () => { - const body = result.body = Buffer.from(imgData, 'base64') - result.bodyLength = result.head['Content-Length'] = body.length - return done() - } - - const convertSVG = () => { + const svg2pdf = (svg, cb) => { const batik = opts.batik instanceof Batik ? opts.batik : new Batik(opts.batik) - batik.convertSVG(imgData, {format: format}, (err, buf) => { + batik.svg2pdf(svg, {id: info.id}, (err, pdf) => { if (err) { errorCode = 530 result.error = err return done() } - - result.bodyLength = result.head['Content-Length'] = buf.length - result.body = buf - return done() + cb(pdf) }) } - // TODO - const pdf2eps = () => {} + 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 // - is the 'encoded' option still relevant? @@ -66,24 +75,42 @@ function convert (info, opts, reply) { case 'png': case 'jpeg': case 'webp': - return toBuffer() + 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': if (opts.batik) { - return convertSVG() + svg2pdf(imgData, (pdf) => { + body = pdf + bodyLength = body.length + return done() + }) } else { - return toBuffer() + body = Buffer.from(imgData, 'base64') + bodyLength = body.length + return done() } + break case 'eps': if (opts.batik) { - return convertSVG() + svg2pdf(imgData, (pdf) => pdf2eps(pdf, (eps) => { + body = eps + bodyLength = body.length + return done() + })) } else { - return pdf2eps() + pdf2eps(imgData, (eps) => { + body = eps + bodyLength = body.length + return done() + }) } + break } } diff --git a/test/unit/plotly-graph_test.js b/test/unit/plotly-graph_test.js index 200e66a9..8264abb8 100644 --- a/test/unit/plotly-graph_test.js +++ b/test/unit/plotly-graph_test.js @@ -4,7 +4,7 @@ const EventEmitter = require('events') const _module = require('../../src/component/plotly-graph') const remote = require('../../src/util/remote') -const { paths, createMockWindow } = require('../common') +const { paths, mocks, createMockWindow } = require('../common') tap.test('inject:', t => { const fn = _module.inject @@ -347,34 +347,69 @@ tap.test('convert:', t => { }) }) - t.test('should convert image data to pdf when *batik* option is set', t => { - const info = {imgData: '', format: 'pdf'} + t.test('when *batik* option is set', t => { const opts = {batik: paths.batik} - fn(info, opts, (errorCode, result) => { + t.test('should convert svg data to pdf', t => { + const info = {imgData: '', format: 'pdf'} + + fn(info, opts, (errorCode, result) => { + t.equal(errorCode, null) + t.equal(result.head['Content-Type'], 'application/pdf') + t.type(result.body, Buffer) + t.end() + }) + }) + + t.test('should convert svg data to eps', t => { + const info = {imgData: mocks.svg, format: 'eps'} + + fn(info, opts, (errorCode, result) => { + t.equal(errorCode, null) + t.equal(result.head['Content-Type'], 'application/postscript') + t.type(result.body, Buffer) + t.end() + }) + }) + + t.test('should pass batik errors', t => { + const info = {imgData: '', format: 'pdf'} + opts.batik = 'not-gonna-work' + + fn(info, opts, (errorCode, result) => { + t.equal(errorCode, 530) + t.match(result.msg, 'image conversion error') + t.match(result.error.message, 'Command failed') + t.end() + }) + }) + + t.end() + }) + + t.test('should convert pdf data to eps', t => { + const info = {imgData: mocks.pdf, format: 'eps'} + + fn(info, {}, (errorCode, result) => { t.equal(errorCode, null) - t.equal(result.head['Content-Type'], 'application/pdf') + t.equal(result.head['Content-Type'], 'application/postscript') t.type(result.body, Buffer) t.end() }) }) - t.test('should pass batik errors', t => { - const info = {imgData: '', format: 'pdf'} - const opts = {batik: 'not-gonna-work'} + t.test('should pass pdftops errors', t => { + const info = {imgData: mocks.pdf, format: 'eps'} + const opts = {pdftops: 'not gonna work'} fn(info, opts, (errorCode, result) => { t.equal(errorCode, 530) t.match(result.msg, 'image conversion error') + t.match(result.error.message, 'Command failed') t.end() }) }) - t.test('should image data to eps', t => { - // TODO - t.end() - }) - t.end() }) From 5992a8baea2ea53ef454c62ad1d4bbb5d9e1b797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Wed, 30 Aug 2017 12:03:30 -0400 Subject: [PATCH 09/18] improve printToPDF page size logic - use correct px-by-micron logic - add margin offset (that appears to work ok) --- src/component/plotly-graph/render.js | 18 +++++++++++++----- test/unit/plotly-graph_test.js | 5 ++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index e4b71c15..71bfc127 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -124,8 +124,16 @@ function toPDF (imgData, imgOpts, info) { const w1 = imgOpts.scale * imgOpts.width const h1 = imgOpts.scale * imgOpts.height + // printToPDF expects page size setting in micrometer (1e-6 m) + // - Px by micrometer factor is taken + // from https://www.translatorscafe.com/unit-converter/en/length/13-110/micrometer-pixel/ + // - Even under the `marginsType: 1` setting (meaning no margins), printToPDF still + // output small margins. We need to take this into consideration so that the output PDF + // does not span multiple pages. The offset value was found empirically via trial-and-error. + const pxByMicrometer = 0.00377957517575025 + const inducedMarginOffset = 18 + // TODO - // - how to (robustly) get pixel to microns (for pageSize) conversion factor // - this work great, except runner app can't get all pdf to generate // when parallelLimit > 1 ??? // + figure out why??? @@ -134,8 +142,8 @@ function toPDF (imgData, imgOpts, info) { marginsType: 1, printSelectionOnly: true, pageSize: { - width: w1 / 0.0035, - height: h1 / 0.0035 + width: (w1 + inducedMarginOffset) / pxByMicrometer, + height: (h1 + inducedMarginOffset) / pxByMicrometer } } @@ -143,8 +151,8 @@ function toPDF (imgData, imgOpts, info) { const div = document.createElement('div') const img = document.createElement('img') - img.width = w1 - img.height = h1 + div.style.width = img.width = w1 + div.style.height = img.height = h1 document.body.appendChild(div) div.appendChild(img) diff --git a/test/unit/plotly-graph_test.js b/test/unit/plotly-graph_test.js index 8264abb8..ec256af7 100644 --- a/test/unit/plotly-graph_test.js +++ b/test/unit/plotly-graph_test.js @@ -469,7 +469,10 @@ tap.test('render:', t => { } const mockElements = () => { - const div = {appendChild: sinon.stub()} + const div = { + appendChild: sinon.stub(), + style: {} + } document.createElement.withArgs('div').returns(div) const img = new EventEmitter() From 135cb1baa946529fef27a3371a2451add52be238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Aug 2017 10:52:30 -0400 Subject: [PATCH 10/18] new format=pdf strategy: - open new window for each pdf export which: + :hocho: the extraneous margin-left in printSelectionOnly exports + no longer freezes up when parallelLimit > 1 - we still need to add a few pixel to the export pageSize + but now hide those artificial margin by setting and exporting the graphs' bgColor. --- src/component/plotly-graph/render.js | 122 ++++++++++++++++----------- 1 file changed, 71 insertions(+), 51 deletions(-) diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index 71bfc127..b31de06e 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -38,13 +38,16 @@ function render (info, opts, sendToMain) { sendToMain(errorCode, result) } - // TODO - // - figure out if we still need this: - // https://github.com/plotly/streambed/blob/7311d4386d80d45999797e87992f43fb6ecf48a1/image_server/server_app/main.js#L224-L229 - const PDF_OR_EPS = (format === 'pdf' || format === 'eps') const PRINT_TO_PDF = PDF_OR_EPS && !opts.batik + // stash `paper_bgcolor` here in order to set the pdf window bg color + let bgColor + const pdfBackground = (gd, _bgColor) => { + if (!bgColor) bgColor = _bgColor + gd._fullLayout.paper_bgcolor = 'rgba(0,0,0,0)' + } + const imgOpts = { format: PDF_OR_EPS ? 'svg' : format, width: info.width, @@ -54,7 +57,9 @@ function render (info, opts, sendToMain) { // return image data w/o the leading 'data:image' spec imageDataOnly: !PRINT_TO_PDF, // blend jpeg background color as jpeg does not support transparency - setBackground: format === 'jpeg' ? 'opaque' : '' + setBackground: format === 'jpeg' ? 'opaque' + : PRINT_TO_PDF ? pdfBackground + : '' } let promise @@ -64,7 +69,7 @@ function render (info, opts, sendToMain) { .toImage({data: figure.data, layout: figure.layout, config: config}, imgOpts) .then((imgData) => { if (PRINT_TO_PDF) { - return toPDF(imgData, imgOpts) + return toPDF(imgData, imgOpts, bgColor) } else { return imgData } @@ -88,7 +93,7 @@ function render (info, opts, sendToMain) { case 'pdf': case 'eps': if (PRINT_TO_PDF) { - return toPDF(imgData, imgOpts, info) + return toPDF(imgData, imgOpts, bgColor) } else { return decodeSVG(imgData) } @@ -115,67 +120,82 @@ function decodeSVG (imgData) { return window.decodeURIComponent(imgData.replace(cst.imgPrefix.svg, '')) } -/** - * See https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentsprinttopdfoptions-callback - * for other available options - */ -function toPDF (imgData, imgOpts, info) { - const win = remote.getCurrentWindow() - const w1 = imgOpts.scale * imgOpts.width - const h1 = imgOpts.scale * imgOpts.height - - // printToPDF expects page size setting in micrometer (1e-6 m) - // - Px by micrometer factor is taken - // from https://www.translatorscafe.com/unit-converter/en/length/13-110/micrometer-pixel/ - // - Even under the `marginsType: 1` setting (meaning no margins), printToPDF still - // output small margins. We need to take this into consideration so that the output PDF - // does not span multiple pages. The offset value was found empirically via trial-and-error. +function toPDF (imgData, imgOpts, bgColor) { + const wPx = imgOpts.scale * imgOpts.width + const hPx = imgOpts.scale * imgOpts.height + const pxByMicrometer = 0.00377957517575025 - const inducedMarginOffset = 18 + const offset = 6 - // TODO - // - this work great, except runner app can't get all pdf to generate - // when parallelLimit > 1 ??? - // + figure out why??? - // + maybe restrict that in coerce-opts? + // See other available options: + // https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentsprinttopdfoptions-callback const printOpts = { + // no margins marginsType: 1, - printSelectionOnly: true, + // make bg (set to `paper_bgcolor` value) appear in export + printBackground: true, + // printToPDF expects page size setting in micrometer (1e-6 m) + // - Px by micrometer factor is taken from + // https://www.translatorscafe.com/unit-converter/en/length/13-110/micrometer-pixel/ + // - Even under the `marginsType: 1` setting (meaning no margins), printToPDF still + // outputs small margins. We need to take this into consideration so that the output PDF + // does not span multiple pages. The offset value was found empirically via trial-and-error. pageSize: { - width: (w1 + inducedMarginOffset) / pxByMicrometer, - height: (h1 + inducedMarginOffset) / pxByMicrometer + width: (wPx + offset) / pxByMicrometer, + height: (hPx + offset) / pxByMicrometer } } return new Promise((resolve, reject) => { - const div = document.createElement('div') - const img = document.createElement('img') - - div.style.width = img.width = w1 - div.style.height = img.height = h1 - - document.body.appendChild(div) - div.appendChild(img) + let win = remote.createBrowserWindow({ + width: wPx, + height: hPx + }) - img.addEventListener('load', () => { - window.getSelection().selectAllChildren(div) + win.on('closed', () => { + win = null + }) + const html = encodeURIComponent(` + + + + + + + `) + + // we can't set image src into html as chromium has a 2MB URL limit + // https://craignicol.wordpress.com/2016/07/19/excellent-export-and-the-chrome-url-limit/ + + win.loadURL(`data:text/html,${html}`) + + win.webContents.executeJavaScript(`new Promise((resolve, reject) => { + const img = document.body.firstChild + img.onload = resolve + img.onerror = reject + img.src = "${imgData}" + setTimeout(() => reject(new Error('too long to load image')), 2000) + })`) + .then(() => { win.webContents.printToPDF(printOpts, (err, pdfData) => { - document.body.removeChild(div) - if (err) { - return reject(new Error('electron print to PDF error')) + reject(err) + } else { + resolve(pdfData) } - return resolve(pdfData) + win.close() }) }) - - img.addEventListener('error', () => { - document.body.removeChild(div) - return reject(new Error('image failed to load')) + .catch((err) => { + win.close() + reject(err) }) - - img.src = imgData }) } From 649f97f2ffa9fd4c970754907c900331cc463373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Aug 2017 14:05:15 -0400 Subject: [PATCH 11/18] put pdf page image load timeout value in constants.js --- src/component/plotly-graph/constants.js | 5 ++++- src/component/plotly-graph/render.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/component/plotly-graph/constants.js b/src/component/plotly-graph/constants.js index bd837092..8fe4a38e 100644 --- a/src/component/plotly-graph/constants.js +++ b/src/component/plotly-graph/constants.js @@ -28,5 +28,8 @@ module.exports = { svg: /^data:image\/svg\+xml,/ }, - mathJaxConfigQuery: '?config=TeX-AMS-MML_SVG' + mathJaxConfigQuery: '?config=TeX-AMS-MML_SVG', + + // time [in ms] after which printToPDF errors when image isn't loaded + pdfPageLoadImgTimeout: 2000 } diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index b31de06e..6af79e5a 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -180,7 +180,7 @@ function toPDF (imgData, imgOpts, bgColor) { img.onload = resolve img.onerror = reject img.src = "${imgData}" - setTimeout(() => reject(new Error('too long to load image')), 2000) + setTimeout(() => reject(new Error('too long to load image')), ${cst.pdfPageLoadImgTimeout}) })`) .then(() => { win.webContents.printToPDF(printOpts, (err, pdfData) => { @@ -193,8 +193,8 @@ function toPDF (imgData, imgOpts, bgColor) { }) }) .catch((err) => { - win.close() reject(err) + win.close() }) }) } From 23cfc95566292c1c3cc98480f24f5bd12691ca2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Aug 2017 14:05:56 -0400 Subject: [PATCH 12/18] add plotGlPixelRatio to improve look of gl3d grid lines --- src/component/plotly-graph/constants.js | 3 +++ src/component/plotly-graph/render.js | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/component/plotly-graph/constants.js b/src/component/plotly-graph/constants.js index 8fe4a38e..eb19c2cf 100644 --- a/src/component/plotly-graph/constants.js +++ b/src/component/plotly-graph/constants.js @@ -30,6 +30,9 @@ module.exports = { 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 } diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index 6af79e5a..a92a7c0e 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -23,10 +23,10 @@ function render (info, opts, sendToMain) { const figure = info.figure const format = info.format - const config = Object.assign({}, - {mapboxAccessToken: opts.mapboxAccessToken || ''}, - figure.config - ) + const config = Object.assign({ + mapboxAccessToken: opts.mapboxAccessToken || '', + plotGlPixelRatio: cst.plotGlPixelRatio + }, figure.config) const result = {} let errorCode = null From acb007b89cf8733c351c9f86e1e599dcd7101710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Aug 2017 16:15:44 -0400 Subject: [PATCH 13/18] fixup tests --- src/component/plotly-graph/render.js | 2 +- test/common.js | 9 ++- test/unit/pdftops_test.js | 4 +- test/unit/plotly-graph_test.js | 87 +++++++++++----------------- 4 files changed, 43 insertions(+), 59 deletions(-) diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index a92a7c0e..4456e16b 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -156,7 +156,7 @@ function toPDF (imgData, imgOpts, bgColor) { win = null }) - const html = encodeURIComponent(` + const html = window.encodeURIComponent(` diff --git a/test/common.js b/test/common.js index 23a5b024..525a3856 100644 --- a/test/common.js +++ b/test/common.js @@ -27,12 +27,17 @@ function createMockWindow (opts = {}) { const win = new EventEmitter() const webContents = new EventEmitter() + webContents.executeJavaScript = sinon.stub() webContents.printToPDF = sinon.stub() Object.assign(win, opts, { webContents: webContents, - loadURL: () => { webContents.emit('did-finish-load') }, - close: sinon.stub() + loadURL: sinon.stub().callsFake(() => { + webContents.emit('did-finish-load') + }), + close: sinon.stub().callsFake(() => { + win.emit('closed') + }) }) return win diff --git a/test/unit/pdftops_test.js b/test/unit/pdftops_test.js index 3cb09622..db11b27d 100644 --- a/test/unit/pdftops_test.js +++ b/test/unit/pdftops_test.js @@ -20,8 +20,8 @@ tap.test('pdftops.pdf2eps', t => { if (err) t.fail(err) const size = fs.statSync(outPath).size - t.ok(size > 5e4, 'min pdf file size') - t.ok(size < 6e4, 'max pdf file size') + t.ok(size > 3e5, 'min pdf file size') + t.ok(size < 4e5, 'max pdf file size') t.end() }) }) diff --git a/test/unit/plotly-graph_test.js b/test/unit/plotly-graph_test.js index ec256af7..96e6e12f 100644 --- a/test/unit/plotly-graph_test.js +++ b/test/unit/plotly-graph_test.js @@ -1,6 +1,5 @@ const tap = require('tap') const sinon = require('sinon') -const EventEmitter = require('events') const _module = require('../../src/component/plotly-graph') const remote = require('../../src/util/remote') @@ -459,39 +458,17 @@ tap.test('render:', t => { } const mockBrowser = () => { - document.body = { - appendChild: sinon.stub(), - removeChild: sinon.stub() - } document.createElement = sinon.stub() - window.getSelection = sinon.stub().returns({selectAllChildren: sinon.stub()}) window.decodeURIComponent = sinon.stub().returns('decoded image data') - } - - const mockElements = () => { - const div = { - appendChild: sinon.stub(), - style: {} - } - document.createElement.withArgs('div').returns(div) - - const img = new EventEmitter() - document.createElement.withArgs('img').returns(img) - img.addEventListener = img.addListener - - return { - div: div, - img: img - } + window.encodeURIComponent = sinon.stub().returns('encoded image data') } const mockWindow = () => { const win = createMockWindow() - sinon.stub(remote, 'getCurrentWindow').returns(win) - + sinon.stub(remote, 'createBrowserWindow').returns(win) return { win: win, - restore: () => remote.getCurrentWindow.restore() + restore: () => remote.createBrowserWindow.restore() } } @@ -509,21 +486,21 @@ tap.test('render:', t => { t.test('(format pdf)', t => { mock130() mockBrowser() - const {img} = mockElements() const {win, restore} = mockWindow() + win.webContents.executeJavaScript.returns(new Promise(resolve => resolve())) win.webContents.printToPDF.yields(null, 'pdf data') fn({format: 'pdf'}, {}, (errorCode, result) => { t.equal(errorCode, null) t.equal(result.imgData, 'pdf data') - t.equal(document.createElement.callCount, 2, 'createElement calls') - t.equal(document.body.appendChild.callCount, 1, 'body.appendChild calls') - t.equal(document.body.removeChild.callCount, 1, 'body.removeChild calls') + t.equal(window.encodeURIComponent.callCount, 1, 'encodeURIComponent calls') + t.equal(win.webContents.executeJavaScript.callCount, 1, 'executeJavaScript calls') + t.equal(win.webContents.printToPDF.callCount, 1, 'printToPDF calls') + t.ok(win.close.calledOnce) restore() t.end() }) - setTimeout(() => img.emit('load')) }) t.end() @@ -551,6 +528,7 @@ tap.test('render:', t => { fn({format: 'svg'}, {}, (errorCode, result) => { t.equal(result.imgData, 'decoded image data') t.ok(document.createElement.calledOnce) + t.ok(window.decodeURIComponent.calledOnce) t.ok(Plotly.newPlot.calledOnce) t.ok(Plotly.toImage.calledOnce) t.ok(Plotly.purge.calledOnce) @@ -561,38 +539,39 @@ tap.test('render:', t => { t.test('(format pdf)', t => { mock110() mockBrowser() - const {img} = mockElements() const {win, restore} = mockWindow() + win.webContents.executeJavaScript.returns(new Promise(resolve => resolve())) win.webContents.printToPDF.yields(null, 'pdf data') fn({format: 'pdf'}, {}, (errorCode, result) => { t.equal(errorCode, null) t.equal(result.imgData, 'pdf data') - t.equal(document.createElement.callCount, 3, 'createElement calls') - t.equal(document.body.appendChild.callCount, 1, 'body.appendChild calls') - t.equal(document.body.removeChild.callCount, 1, 'body.removeChild calls') + t.equal(document.createElement.callCount, 1, 'createElement calls') + t.equal(window.encodeURIComponent.callCount, 1, 'encodeURIComponent calls') + t.equal(win.webContents.executeJavaScript.callCount, 1, 'executeJavaScript calls') + t.equal(win.webContents.printToPDF.callCount, 1, 'printToPDF calls') + t.doesNotThrow(() => { + const gd = { + _fullLayout: {paper_bgcolor: 'some color'} + } + Plotly.toImage.args[0][1].setBackground(gd, 'some other color') + }, 'custom setBackground function') restore() t.end() }) - setTimeout(() => img.emit('load')) }) t.test('(format pdf with batik support)', t => { mock110() mockBrowser() - const {img} = mockElements() - const {win, restore} = mockWindow() - win.webContents.printToPDF.yields(null, 'pdf data') fn({format: 'pdf'}, {batik: 'path-to-batik'}, (errorCode, result) => { t.equal(errorCode, null) t.equal(result.imgData, 'decoded image data') - - restore() + t.equal(window.decodeURIComponent.callCount, 1, 'decodeURIComponent calls') t.end() }) - setTimeout(() => img.emit('load')) }) t.end() @@ -619,39 +598,39 @@ tap.test('render:', t => { }) }) - t.test('should return error code on printToPDF errors', t => { + t.test('should return error code on executeJavaScript errors', t => { mock130() mockBrowser() - const {img} = mockElements() const {win, restore} = mockWindow() - win.webContents.printToPDF.yields(new Error('oops')) + win.webContents.executeJavaScript.returns(new Promise((resolve, reject) => { + reject(new Error('oh no!')) + })) fn({format: 'pdf'}, {}, (errorCode, result) => { t.equal(errorCode, 525) - t.match(result.error, /electron print to PDF error/, 'error') - t.ok(document.body.removeChild.calledOnce) + t.match(result.error, /oh no/, 'error') + t.ok(win.close.calledOnce) restore() t.end() }) - setTimeout(() => img.emit('load')) }) - t.test('should return error code on createElement(\'img\') errors', t => { + t.test('should return error code on printToPDF errors', t => { mock130() mockBrowser() - const {img} = mockElements() - const {restore} = mockWindow() + const {win, restore} = mockWindow() + win.webContents.executeJavaScript.returns(new Promise(resolve => resolve())) + win.webContents.printToPDF.yields(new Error('oops')) fn({format: 'pdf'}, {}, (errorCode, result) => { t.equal(errorCode, 525) - t.match(result.error, /image failed to load/, 'error') - t.ok(document.body.removeChild.calledOnce) + t.match(result.error, /oops/, 'error') + t.ok(win.close.calledOnce) restore() t.end() }) - setTimeout(() => img.emit('error')) }) t.test('should generate svg for format pdf and eps', t => { From 96b1fbc94e803cbf0a91464f0d2c148fb6d1a9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Aug 2017 16:47:43 -0400 Subject: [PATCH 14/18] :hocho: batik as `printToPDF` :trophy: --- bin/args.js | 15 +--- bin/plotly-export-server_electron.js | 23 +----- bin/plotly-graph-exporter_electron.js | 1 - src/component/plotly-dashboard/render.js | 1 - src/component/plotly-graph/convert.js | 47 ++---------- src/component/plotly-graph/render.js | 6 +- src/util/batik.js | 95 ----------------------- test/common.js | 1 - test/unit/batik_test.js | 97 ------------------------ test/unit/plotly-graph_test.js | 48 ------------ 10 files changed, 11 insertions(+), 323 deletions(-) delete mode 100644 src/util/batik.js delete mode 100644 test/unit/batik_test.js diff --git a/bin/args.js b/bin/args.js index 0e187a05..7b176bcf 100644 --- a/bin/args.js +++ b/bin/args.js @@ -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'], @@ -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 = { @@ -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 = { @@ -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. @@ -219,9 +213,6 @@ exports.getServerHelpMsg = function () { --mathjax ${formatAliases('mathjax')} ${DESCRIPTION.mathjax} - --batik - ${DESCRIPTION.batik} - --debug ${DESCRIPTION.debug} ` diff --git a/bin/plotly-export-server_electron.js b/bin/plotly-export-server_electron.js index 3e2a7a1e..44e58b47 100644 --- a/bin/plotly-export-server_electron.js +++ b/bin/plotly-export-server_electron.js @@ -1,5 +1,4 @@ const plotlyExporter = require('../') -const Batik = require('../src/util/batik') const { getServerArgs, getServerHelpMsg } = require('./args') const pkg = require('../package.json') @@ -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 = { diff --git a/bin/plotly-graph-exporter_electron.js b/bin/plotly-graph-exporter_electron.js index 802cbffd..42fb2e18 100644 --- a/bin/plotly-graph-exporter_electron.js +++ b/bin/plotly-graph-exporter_electron.js @@ -58,7 +58,6 @@ getStdin().then((txt) => { mapboxAccessToken: argv['mapbox-access-token'], mathjax: argv.mathjax, topojson: argv.topojson, - batik: argv.batik, format: argv.format, scale: argv.scale, width: argv.width, diff --git a/src/component/plotly-dashboard/render.js b/src/component/plotly-dashboard/render.js index 5913c924..6e01a910 100644 --- a/src/component/plotly-dashboard/render.js +++ b/src/component/plotly-dashboard/render.js @@ -39,7 +39,6 @@ function render (info, opts, sendToMain) { // - 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(() => { diff --git a/src/component/plotly-graph/convert.js b/src/component/plotly-graph/convert.js index c8427276..64824c51 100644 --- a/src/component/plotly-graph/convert.js +++ b/src/component/plotly-graph/convert.js @@ -1,4 +1,3 @@ -const Batik = require('../../util/batik') const Pdftops = require('../../util/pdftops') const cst = require('./constants') @@ -8,7 +7,6 @@ const cst = require('./constants') * - format {string} (from parse) * - imgData {string} (from render) * @param {object} opts : component options - * - batik {string or instance of Batik} * @param {function} reply * - errorCode {number or null} * - result {object} @@ -38,21 +36,6 @@ function convert (info, opts, reply) { reply(errorCode, result) } - const svg2pdf = (svg, cb) => { - const batik = opts.batik instanceof Batik - ? opts.batik - : new Batik(opts.batik) - - batik.svg2pdf(svg, {id: info.id}, (err, pdf) => { - if (err) { - errorCode = 530 - result.error = err - return done() - } - cb(pdf) - }) - } - const pdf2eps = (pdf, cb) => { const pdftops = opts.pdftops instanceof Pdftops ? opts.pdftops @@ -75,6 +58,7 @@ function convert (info, opts, reply) { case 'png': case 'jpeg': case 'webp': + case 'pdf': body = Buffer.from(imgData, 'base64') bodyLength = body.length return done() @@ -83,33 +67,12 @@ function convert (info, opts, reply) { body = imgData bodyLength = encodeURI(imgData).split(/%..|./).length - 1 return done() - case 'pdf': - if (opts.batik) { - svg2pdf(imgData, (pdf) => { - body = pdf - bodyLength = body.length - return done() - }) - } else { - body = Buffer.from(imgData, 'base64') + case 'eps': + pdf2eps(imgData, (eps) => { + body = eps bodyLength = body.length return done() - } - break - case 'eps': - if (opts.batik) { - svg2pdf(imgData, (pdf) => pdf2eps(pdf, (eps) => { - body = eps - bodyLength = body.length - return done() - })) - } else { - pdf2eps(imgData, (eps) => { - body = eps - bodyLength = body.length - return done() - }) - } + }) break } } diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index 4456e16b..0a6f4a05 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -13,7 +13,6 @@ const cst = require('./constants') * - scale * @param {object} opts : component options * - mapboxAccessToken - * - batik * @param {function} sendToMain * - errorCode * - result @@ -38,8 +37,7 @@ function render (info, opts, sendToMain) { sendToMain(errorCode, result) } - const PDF_OR_EPS = (format === 'pdf' || format === 'eps') - const PRINT_TO_PDF = PDF_OR_EPS && !opts.batik + const PRINT_TO_PDF = (format === 'pdf' || format === 'eps') // stash `paper_bgcolor` here in order to set the pdf window bg color let bgColor @@ -49,7 +47,7 @@ function render (info, opts, sendToMain) { } const imgOpts = { - format: PDF_OR_EPS ? 'svg' : format, + format: PRINT_TO_PDF ? 'svg' : format, width: info.width, height: info.height, // works as of https://github.com/plotly/plotly.js/compare/to-image-scale diff --git a/src/util/batik.js b/src/util/batik.js deleted file mode 100644 index 820ce3bb..00000000 --- a/src/util/batik.js +++ /dev/null @@ -1,95 +0,0 @@ -const fs = require('fs') -const path = require('path') -const childProcess = require('child_process') -const parallel = require('run-parallel') -const series = require('run-series') -const uuid = require('uuid/v4') - -const PATH_TO_BUILD = path.join(__dirname, '..', '..', 'build') - -/** Node wrapper for Batik - * - * See: - * - https://xmlgraphics.apache.org/batik/tools/rasterizer.html - * - http://archive.apache.org/dist/xmlgraphics/batik/ - */ -class Batik { - /** Create batik rasterizer instance - * - * @param {string} batikJar : path to batik rasterizer jar file - */ - constructor (batikJar) { - this.batikJar = path.resolve(batikJar) - this.javaBase = 'java -jar -XX:+UseParallelGC -server' - this.batikBase = `${this.javaBase} ${this.batikJar}` - } - - /** Convert svg to pdf - * - * @param {string} svg : svg string to convert - * @param {object} opts : - * - id {string} - * @param {function} cb - * - err {null || error} - * - result {buffer} - */ - svg2pdf (svg, opts, cb) { - const id = opts.id || uuid() - const inPath = path.join(PATH_TO_BUILD, id + '-svg') - const outPath = path.join(PATH_TO_BUILD, id + '-out') - const cmd = `${this.batikBase} -m application/pdf -d ${outPath} ${inPath}` - - // TODO old batik wrapper had: - // - // svg = svg.replace(/(font-family: )('Courier New')/g, '$1\'Liberation mono\'') - // .replace(/(font-family: )('Times New Roman')/g, '$1\'Liberation Serif\'') - // .replace(/(font-family: )(Arial)/g, '$1\'Liberation Sans\'') - // .replace(/(font-family: )(Balto)/g, '$1\'Balto Book\'') - // .replace(/(font-family: )(Tahoma)/g, '$1\'Liberation Sans\''); - // - // find out why, and check if it's still needed. - - // TODO do we still need this `\ufeff` addition? - - const createTmpFiles = (cb) => parallel([ - (cb) => fs.writeFile(inPath, '\ufeff' + svg, cb), - (cb) => fs.writeFile(outPath, '', cb) - ], cb) - - const destroyTmpFiles = (cb) => parallel([ - (cb) => fs.unlink(inPath, cb), - (cb) => fs.unlink(outPath, cb) - ], cb) - - series([ - createTmpFiles, - (cb) => childProcess.exec(cmd, cb), - (cb) => fs.readFile(outPath, cb) - ], (err, result) => { - destroyTmpFiles(() => { - cb(err, result[2]) - }) - }) - } - - /** Does batik-rasterizer jar file exist? - * @return {boolean} - */ - doesBatikJarExist () { - return fs.existsSync(this.batikJar) - } - - /** Is java installed? - * @return {boolean} - */ - static isJavaInstalled () { - try { - childProcess.execSync('java -version', {stdio: 'ignore'}) - } catch (e) { - return false - } - return true - } -} - -module.exports = Batik diff --git a/test/common.js b/test/common.js index 525a3856..68d20060 100644 --- a/test/common.js +++ b/test/common.js @@ -10,7 +10,6 @@ const mocks = {} paths.root = path.join(__dirname, '..') paths.build = path.join(paths.root, 'build') paths.bin = path.join(paths.root, 'bin') -paths.batik = process.env.BATIK_RASTERIZER_PATH || path.join(paths.build, 'batik-1.7', 'batik-rasterizer.jar') paths.readme = path.join(paths.root, 'README.md') paths.pkg = path.join(paths.root, 'package.json') paths.glob = path.join(paths.root, 'src', 'util', '*') diff --git a/test/unit/batik_test.js b/test/unit/batik_test.js deleted file mode 100644 index 6f3b4961..00000000 --- a/test/unit/batik_test.js +++ /dev/null @@ -1,97 +0,0 @@ -const tap = require('tap') -const sinon = require('sinon') -const path = require('path') -const fs = require('fs') -const childProcess = require('child_process') - -const Batik = require('../../src/util/batik') -const { paths, mocks } = require('../common') - -tap.test('batik.svg2pdf', t => { - t.test('should convert svg to pdf', t => { - const batik = new Batik(paths.batik) - const outPath = path.join(paths.build, 'batik-test.pdf') - - batik.svg2pdf(mocks.svg, {}, (err, result) => { - if (err) t.fail(err) - t.type(result, Buffer) - - fs.writeFile(outPath, result, (err) => { - if (err) t.fail(err) - - const size = fs.statSync(outPath).size - t.ok(size > 5e3, 'min pdf file size') - t.ok(size < 1e4, 'max pdf file size') - t.end() - }) - }) - }) - - t.test('should remove tmp files after conversion', t => { - const batik = new Batik(paths.batik) - const tmpOutPath = path.join(paths.build, 'tmp-out') - const tmpSvgPath = path.join(paths.build, 'tmp-svg') - - batik.svg2pdf(mocks.svg, {id: 'tmp'}, (err, result) => { - if (err) t.fail(err) - - t.type(result, Buffer) - t.notOk(fs.existsSync(tmpOutPath), 'clears tmp out file') - t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp svg file') - t.end() - }) - }) - - t.test('should error out when batik command fails', t => { - const batik = new Batik(paths.batik) - batik.batikBase = 'not gonna work' - - const tmpOutPath = path.join(paths.build, 'tmp-out') - const tmpSvgPath = path.join(paths.build, 'tmp-svg') - - batik.svg2pdf(mocks.svg, {id: 'tmp'}, (err) => { - t.throws(() => { throw err }, /Command failed/) - t.notOk(fs.existsSync(tmpOutPath), 'clears tmp out file') - t.notOk(fs.existsSync(tmpSvgPath), 'clears tmp svg file') - t.end() - }) - }) - - t.end() -}) - -tap.test('doesBatikJarExist:', t => { - t.test('should return true when it does exists', t => { - const batik = new Batik(paths.batik) - t.ok(batik.doesBatikJarExist()) - t.end() - }) - - t.test('should return false when it does not exists', t => { - const batik = new Batik('not gonna work') - t.notOk(batik.doesBatikJarExist()) - t.end() - }) - t.end() -}) - -tap.test('isJavaInstalled', t => { - t.afterEach((done) => { - childProcess.execSync.restore() - done() - }) - - t.test('should return true when binary execute correctly', t => { - sinon.stub(childProcess, 'execSync').returns(true) - t.ok(Batik.isJavaInstalled()) - t.end() - }) - - t.test('should return false when binary does not execute correctly', t => { - sinon.stub(childProcess, 'execSync').throws() - t.notOk(Batik.isJavaInstalled()) - t.end() - }) - - t.end() -}) diff --git a/test/unit/plotly-graph_test.js b/test/unit/plotly-graph_test.js index 96e6e12f..49fa1ee0 100644 --- a/test/unit/plotly-graph_test.js +++ b/test/unit/plotly-graph_test.js @@ -346,44 +346,8 @@ tap.test('convert:', t => { }) }) - t.test('when *batik* option is set', t => { - const opts = {batik: paths.batik} - t.test('should convert svg data to pdf', t => { - const info = {imgData: '', format: 'pdf'} - - fn(info, opts, (errorCode, result) => { - t.equal(errorCode, null) - t.equal(result.head['Content-Type'], 'application/pdf') - t.type(result.body, Buffer) - t.end() - }) - }) - - t.test('should convert svg data to eps', t => { - const info = {imgData: mocks.svg, format: 'eps'} - - fn(info, opts, (errorCode, result) => { - t.equal(errorCode, null) - t.equal(result.head['Content-Type'], 'application/postscript') - t.type(result.body, Buffer) - t.end() - }) - }) - - t.test('should pass batik errors', t => { - const info = {imgData: '', format: 'pdf'} - opts.batik = 'not-gonna-work' - - fn(info, opts, (errorCode, result) => { - t.equal(errorCode, 530) - t.match(result.msg, 'image conversion error') - t.match(result.error.message, 'Command failed') - t.end() - }) }) - - t.end() }) t.test('should convert pdf data to eps', t => { @@ -562,18 +526,6 @@ tap.test('render:', t => { }) }) - t.test('(format pdf with batik support)', t => { - mock110() - mockBrowser() - - fn({format: 'pdf'}, {batik: 'path-to-batik'}, (errorCode, result) => { - t.equal(errorCode, null) - t.equal(result.imgData, 'decoded image data') - t.equal(window.decodeURIComponent.callCount, 1, 'decodeURIComponent calls') - t.end() - }) - }) - t.end() }) From f8708a2a5049c14c324943da0203f479ad1e8fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Aug 2017 16:48:40 -0400 Subject: [PATCH 15/18] add test convert + Pdftops instance test --- src/component/plotly-graph/convert.js | 12 +++++++----- test/unit/plotly-graph_test.js | 13 +++++++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/component/plotly-graph/convert.js b/src/component/plotly-graph/convert.js index 64824c51..b9e885d4 100644 --- a/src/component/plotly-graph/convert.js +++ b/src/component/plotly-graph/convert.js @@ -7,6 +7,7 @@ const cst = require('./constants') * - format {string} (from parse) * - imgData {string} (from render) * @param {object} opts : component options + * - pdftops {string or instance of Pdftops) * @param {function} reply * - errorCode {number or null} * - result {object} @@ -18,10 +19,7 @@ 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 @@ -31,7 +29,11 @@ function convert (info, opts, reply) { result.msg = cst.statusMsg[errorCode] } else { result.body = body - result.bodyLength = result.head['Content-Length'] = bodyLength + result.bodyLength = bodyLength + result.head = { + 'Content-Type': cst.contentFormat[format], + 'Content-Length': bodyLength + } } reply(errorCode, result) } diff --git a/test/unit/plotly-graph_test.js b/test/unit/plotly-graph_test.js index 49fa1ee0..75886762 100644 --- a/test/unit/plotly-graph_test.js +++ b/test/unit/plotly-graph_test.js @@ -1,5 +1,6 @@ const tap = require('tap') const sinon = require('sinon') +const Pdftops = require('../../src/util/pdftops') const _module = require('../../src/component/plotly-graph') const remote = require('../../src/util/remote') @@ -346,14 +347,22 @@ tap.test('convert:', t => { }) }) + t.test('should convert pdf data to eps', t => { + const info = {imgData: mocks.pdf, format: 'eps'} + fn(info, {}, (errorCode, result) => { + t.equal(errorCode, null) + t.equal(result.head['Content-Type'], 'application/postscript') + t.type(result.body, Buffer) + t.end() }) }) - t.test('should convert pdf data to eps', t => { + t.test('should convert pdf data to eps (while passing instance of Pdftops)', t => { const info = {imgData: mocks.pdf, format: 'eps'} + const opts = {pdftops: new Pdftops()} - fn(info, {}, (errorCode, result) => { + fn(info, opts, (errorCode, result) => { t.equal(errorCode, null) t.equal(result.head['Content-Type'], 'application/postscript') t.type(result.body, Buffer) From 11b67c3a218dd5f4ac7c43eb6a5f65aca10907ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Aug 2017 17:46:29 -0400 Subject: [PATCH 16/18] add circleci 2.0 config - :hocho: plotly-graph-exporter stdout msg test (for now, this fails on CI) --- .circleci/config.yml | 33 +++++++++++++++++++ .../integration/plotly-graph-exporter_test.js | 21 ------------ 2 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..2b6413e9 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,33 @@ +version: 2 + +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 diff --git a/test/integration/plotly-graph-exporter_test.js b/test/integration/plotly-graph-exporter_test.js index 4a582624..7cc5e47e 100644 --- a/test/integration/plotly-graph-exporter_test.js +++ b/test/integration/plotly-graph-exporter_test.js @@ -52,24 +52,3 @@ tap.test('should print help message', t => { t.end() }) - -tap.test('should print export info on success', t => { - const input = '{"data":[{"y":[1,2,1]}],"layout":{"title":"tester"}}' - const subprocess = _spawn(t, input) - - const matches = [ - /^exported fig/, - /done with code 0/ - ] - - let i = 0 - - subprocess.stdout.on('data', d => { - t.match(d.toString(), matches[i], `msg ${i}`) - i++ - }) - - subprocess.stderr.on('data', d => { - t.fail(d, 'unwanted msg to stderr') - }) -}) From 47614f9e798c994d0979499219204bb22ff9513c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Etienne=20T=C3=A9treault-Pinard?= Date: Thu, 31 Aug 2017 18:25:02 -0400 Subject: [PATCH 17/18] :books: --- src/component/plotly-graph/render.js | 3 ++- src/util/pdftops.js | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/component/plotly-graph/render.js b/src/component/plotly-graph/render.js index 0a6f4a05..a8ec7e63 100644 --- a/src/component/plotly-graph/render.js +++ b/src/component/plotly-graph/render.js @@ -158,7 +158,8 @@ function toPDF (imgData, imgOpts, bgColor) { -