Skip to content

Commit

Permalink
feat(client): add image export (svg, png, jpeg)
Browse files Browse the repository at this point in the history
Closes #908
Related to #866
  • Loading branch information
philippfromme authored and nikku committed Sep 24, 2018
1 parent 406610f commit b33a755
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 22 deletions.
1 change: 0 additions & 1 deletion app/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ renderer.on('client-config:get', function(...args) {
});

renderer.on('file:save-as', function(diagramFile, done) {

saveCallback(fileSystem.saveAs, diagramFile, done);
});

Expand Down
42 changes: 41 additions & 1 deletion client/src/app/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,42 @@ export class App extends Component {
this.props.globals.backend.sendUpdateMenu(state);
}

async exportAs(tab) {
const filters = [{
name: 'PNG Image',
extensions: [ 'png' ]
}, {
name: 'JPEG Image',
extensions: [ 'jpeg' ]
}, {
name: 'SVG Image',
extensions: [ 'svg' ]
}];

const suggestedFile = await this.askExportAs(tab, filters);

if (!suggestedFile) {
throw new Error('user canceled');
}

const {
fileType
} = suggestedFile;

const contents = await this.tabRef.current.triggerAction('export-as', {
fileType
});

this.props.globals.fileSystem.writeFile({
...suggestedFile,
contents
});
}

askExportAs = (file, filters) => {
return this.props.globals.dialog.askExportAs(file, filters);
}

triggerAction = (action, options) => {

const {
Expand Down Expand Up @@ -633,6 +669,10 @@ export class App extends Component {
return this.updateMenu();
}

if (action === 'export-as') {
return this.exportAs(activeTab);
}

const tab = this.tabRef.current;

return tab.triggerAction(action, options);
Expand Down Expand Up @@ -733,7 +773,7 @@ export class App extends Component {
tabState.canExport && <Fill name="toolbar" group="export">
<Button
title="Export as image"
onClick={ this.composeAction('export-as-image') }
onClick={ this.composeAction('export-as') }
>
<Icon name="picture" />
</Button>
Expand Down
55 changes: 55 additions & 0 deletions client/src/app/__tests__/AppSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,61 @@ describe('<App>', function() {
});


describe('exporting', function() {

let askExportAsSpy;
let writeFileSpy;

let app;

beforeEach(function() {

// given
const dialog = new Dialog();
const fileSystem = new FileSystem();

dialog.setAskExportAsResponse(Promise.resolve({
fileType: 'svg',
name: 'foo.svg',
path: 'foo'
}));

askExportAsSpy = spy(dialog, 'askExportAs');
writeFileSpy = spy(fileSystem, 'writeFile');

const rendered = createApp({
globals: {
dialog,
fileSystem
}
}, mount);

app = rendered.app;
});


it.only('should export SVG', async function() {

// given
await app.createDiagram();

// when
await app.triggerAction('export-as');

// then
expect(askExportAsSpy).to.have.been.called;

expect(writeFileSpy).to.have.been.calledWith({
contents: 'CONTENTS',
fileType: 'svg',
name: 'foo.svg',
path: 'foo'
});
});

});


describe('loading', function() {

it('should support life-cycle', async function() {
Expand Down
14 changes: 13 additions & 1 deletion client/src/app/__tests__/mocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class FakeTab extends Component {
if (action === 'save') {
return 'CONTENTS';
}

if (action === 'export-as') {
return 'CONTENTS';
}
}

render() {
Expand Down Expand Up @@ -74,14 +78,22 @@ export class Dialog {
this.askSaveResponse = response;
}

setAskExportAsResponse(response) {
this.askExportAsResponse = response;
}

setOpenFileResponse(response) {
this.this.openFileResponse = response;
this.openFileResponse = response;
}

askSave() {
return this.askSaveResponse;
}

askExportAs() {
return this.askExportAsResponse;
}

openFile() {
return this.openFileResponse;
}
Expand Down
4 changes: 4 additions & 0 deletions client/src/app/tabs/MultiSheetTab.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ class MultiSheetTab extends CachedComponent {
});

return xml;
} else if (action === 'export-as') {
const { fileType } = options;

return await editor.exportAs(fileType);
}

return editor.triggerAction(action, options);
Expand Down
34 changes: 34 additions & 0 deletions client/src/app/tabs/bpmn/BpmnEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import { getBpmnEditMenu } from './getBpmnEditMenu';

import css from './BpmnEditor.less';

import { assign } from 'min-dash';

import generateImage from '../../util/generateImage';

const COLORS = [{
title: 'White',
fill: 'white',
Expand Down Expand Up @@ -271,6 +275,36 @@ export class BpmnEditor extends CachedComponent {
});
}

exportAs(type) {
const {
modeler
} = this.getCached();

return new Promise((resolve, reject) => {

modeler.saveSVG((err, svg) => {
let contents;

if (err) {
reject(err);
}

if (type !== 'svg') {
try {
contents = generateImage(type, svg);
} catch (err) {
return reject(err);
}
} else {
contents = svg;
}

resolve(contents);
});

});
}

triggerAction = (action, context) => {
const {
modeler
Expand Down
63 changes: 46 additions & 17 deletions client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,30 +135,59 @@ describe('<BpmnEditor>', function() {
});


it.skip('#getXML', function(done) {
const slotFillRoot = ReactDOM.render(
<SlotFillRoot>
<BpmnEditorWithCachedState id="foo" xml={ diagramXML } />
</SlotFillRoot>,
container
);
it('#getXML', async function() {
const bpmnEditor = renderBpmnEditor(diagramXML, container);

const bpmnEditor = findRenderedComponentWithType(slotFillRoot, BpmnEditor);
const xml = await bpmnEditor.getXML();

const {
modeler
} = bpmnEditor.getCached();
expect(xml).to.exist;
expect(xml).to.eql(diagramXML);
});

describe.only('#exportAs', function() {

modeler.on('import.done', async function() {
let bpmnEditor;

const xml = await bpmnEditor.getXML();
beforeEach(function() {
bpmnEditor = renderBpmnEditor(diagramXML, container);
});

expect(xml).to.exist;
expect(xml).to.eql(diagramXML);

done();
it('svg', async function() {
const contents = await bpmnEditor.exportAs('svg');

expect(contents).to.exist;
expect(contents).to.equal('<svg />');
});


it('png', async function() {
const contents = await bpmnEditor.exportAs('png');

expect(contents).to.exist;
expect(contents).to.contain('data:image/png');
});


it('jpeg', async function() {
const contents = await bpmnEditor.exportAs('jpeg');

expect(contents).to.exist;
expect(contents).to.contain('data:image/jpeg');
});

});

});

});

function renderBpmnEditor(xml, container) {
const slotFillRoot = ReactDOM.render(
<SlotFillRoot>
<BpmnEditorWithCachedState id="foo" xml={ diagramXML } />
</SlotFillRoot>,
container
);

return findRenderedComponentWithType(slotFillRoot, BpmnEditor);
}
34 changes: 34 additions & 0 deletions client/src/app/util/generateImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import canvg from 'canvg-browser';

// list of defined encodings
const ENCODINGS = [
'image/png',
'image/jpeg'
];


export default function generateImage(type, svg) {
const encoding = 'image/' + type;

let context,
canvas;

if (ENCODINGS.indexOf(encoding) === -1) {
throw new Error('<' + type + '> is an unknown type for converting svg to image');
}

canvas = document.createElement('canvas');

canvg(canvas, svg);

// make the background white for every format
context = canvas.getContext('2d');

context.globalCompositeOperation = 'destination-over';

context.fillStyle = 'white';

context.fillRect(0, 0, canvas.width, canvas.height);

return canvas.toDataURL(encoding);
}
4 changes: 4 additions & 0 deletions client/src/remote/Dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ export default class Dialog {
return this.backend.send('dialog:close-tab', file);
}

askExportAs(file, filters) {
return this.backend.send('file:export-as', file, filters);
}

}
8 changes: 6 additions & 2 deletions client/test/mocks/bpmn-js/Modeler.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ export default class Modeler {
done && done();
}

saveXML(done) {
done(this.xml);
saveXML(options, done) {
done(null, this.xml);
}

saveSVG(done) {
done(null, '<svg />');
}

attachTo() {}
Expand Down

0 comments on commit b33a755

Please sign in to comment.