bpmn-js-copy-as-image 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # bpmn-js-copy-as-image
2
+
3
+ [![CI](https://github.com/barmac/bpmn-js-copy-as-image/actions/workflows/CI.yml/badge.svg)](https://github.com/barmac/bpmn-js-copy-as-image/actions/workflows/CI.yml)
4
+
5
+ This project allows to capture elements as PNG or SVG programmatically, and to copy rendered PNG to the clipboard via context pad.
6
+
7
+ ![pasted screenshot](./resources/screenshot.png)
8
+
9
+ ## Features
10
+
11
+ * copy selected elements as PNG via the context pad
12
+ * render elements as PNG
13
+ * render elements as SVG
14
+
15
+ ## Usage
16
+
17
+ The project exposes 2 modules which can be used with [bpmn-js](https://github.com/bpmn-io/bpmn-js).
18
+
19
+ ### Context pad extension
20
+
21
+ To use the context pad extension, import the `CopyAsImageModule`:
22
+
23
+ ```javascript
24
+ import BpmnModeler from 'bpmn-js/lib/Modeler';
25
+ import { CopyAsImageModule } from 'bpmn-js-copy-as-image';
26
+
27
+ const modeler = new BpmnModeler({
28
+ container: '#container',
29
+ additionalModules: [
30
+ CopyAsImageModule
31
+ ]
32
+ });
33
+
34
+ await modeler.importXML(/* ... */);
35
+ ```
36
+
37
+ ### Programmatic API only
38
+
39
+ The programmatic API is included in the context pad extension. If you want to use only the former,
40
+ import only the `ElementsRendererModule`:
41
+
42
+
43
+ ```javascript
44
+ import BpmnModeler from 'bpmn-js/lib/Modeler';
45
+ import { ElementsRendererModule } from 'bpmn-js-copy-as-image';
46
+
47
+ const modeler = new BpmnModeler({
48
+ container: '#container',
49
+ additionalModules: [
50
+ ElementsRendererModule
51
+ ]
52
+ });
53
+
54
+ await modeler.importXML(/* ... */);
55
+
56
+ const elementsRenderer = modeler.get('elementsRenderer');
57
+ const png = await elementsRenderer.renderAsPNG([ 'Task_1' ]);
58
+ ```
59
+
60
+ ## Demo
61
+
62
+ ```
63
+ # install dependencies
64
+ npm install
65
+
66
+ # run in browser
67
+ npm start
68
+ ```
69
+
70
+ Go to http://localhost:9876/debug.html.
71
+
72
+ ## Credits
73
+
74
+ The project was built on top of @nikku's [native copy and paste example](https://github.com/nikku/bpmn-js-native-copy-paste).
75
+
76
+ ## License
77
+
78
+ MIT
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "bpmn-js-copy-as-image",
3
+ "version": "0.1.0",
4
+ "description": "A bpmn-js extension which allows to render selected elements as images",
5
+ "main": "src/index.js",
6
+ "files": [
7
+ "src"
8
+ ],
9
+ "scripts": {
10
+ "all": "npm test",
11
+ "start": "crosse-env TEST_BROWSERS=Debug npm run dev",
12
+ "dev": "karma start",
13
+ "test": "karma start --single-run --no-auto-watch"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/barmac/bpmn-js-copy-as-image.git"
18
+ },
19
+ "keywords": [
20
+ "bpmn-js"
21
+ ],
22
+ "author": "Maciej Barelkowski <maciej.barelkowski@camunda.com>",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/barmac/bpmn-js-copy-as-image/issues"
26
+ },
27
+ "homepage": "https://github.com/barmac/bpmn-js-copy-as-image#readme",
28
+ "devDependencies": {
29
+ "bpmn-js": "^9.4.0",
30
+ "chai": "^4.3.6",
31
+ "cross-env": "^7.0.3",
32
+ "downloadjs": "^1.4.7",
33
+ "file-drops": "^0.4.0",
34
+ "file-open": "^0.1.1",
35
+ "karma": "^6.4.0",
36
+ "karma-chrome-launcher": "^3.1.1",
37
+ "karma-debug-launcher": "0.0.5",
38
+ "karma-mocha": "^2.0.1",
39
+ "karma-sinon-chai": "^2.0.2",
40
+ "karma-webpack": "^5.0.0",
41
+ "mocha": "^10.0.0",
42
+ "puppeteer": "^16.2.0",
43
+ "sinon": "^14.0.0",
44
+ "sinon-chai": "^3.7.0",
45
+ "webpack": "^5.74.0"
46
+ },
47
+ "dependencies": {
48
+ "canvg": "^4.0.1",
49
+ "diagram-js": "^8.9.0"
50
+ }
51
+ }
@@ -0,0 +1,43 @@
1
+ export class CopyAsImageContextPadProvider {
2
+ constructor(elementsRenderer, contextPad) {
3
+ this._elementsRenderer = elementsRenderer;
4
+ this._contextPad = contextPad;
5
+
6
+ contextPad.registerProvider(this);
7
+ }
8
+
9
+ getContextPadEntries(element) {
10
+ return this._getEntries(element);
11
+ }
12
+
13
+ getMultiElementContextPadEntries(elements) {
14
+ return this._getEntries(elements);
15
+ }
16
+
17
+ _getEntries(elementOrElements) {
18
+ const elementsRenderer = this._elementsRenderer;
19
+ const contextPad = this._contextPad;
20
+
21
+ return {
22
+ 'copy-as-png': {
23
+ title: 'Copy as PNG',
24
+ imageUrl: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='30' width='30'%3E%3Ctext x='0' y='15'%3EPNG%3C/text%3E%3C/svg%3E",
25
+ action: {
26
+ async click() {
27
+ const png = await elementsRenderer.renderAsPNG(elementOrElements);
28
+
29
+ await navigator.clipboard.write([
30
+ new ClipboardItem({
31
+ 'image/png': png
32
+ })
33
+ ]);
34
+
35
+ contextPad.close();
36
+ }
37
+ }
38
+ }
39
+ };
40
+ }
41
+ }
42
+
43
+ CopyAsImageContextPadProvider.$inject = [ 'elementsRenderer', 'contextPad' ];
@@ -0,0 +1,89 @@
1
+ import { Canvg } from 'canvg'
2
+ import { getBBox } from 'diagram-js/lib/util/Elements';
3
+
4
+ const PADDING = {
5
+ x: 6,
6
+ y: 6
7
+ }
8
+
9
+ export class ElementsRenderer {
10
+ constructor(bpmnjs, elementRegistry) {
11
+ this._bpmnjs = bpmnjs;
12
+ }
13
+
14
+ /**
15
+ * Render passed elements as PNG.
16
+ *
17
+ * @param {Array<string|object>} elements - elements to render
18
+ * @returns {Promise<Blob>}
19
+ */
20
+ async renderAsPNG(elements) {
21
+ const svg = await this.renderAsSVG(elements);
22
+
23
+ const canvas = document.createElement('canvas');
24
+ const ctx = canvas.getContext('2d');
25
+ const canvg = Canvg.fromString(ctx, svg);
26
+
27
+ await canvg.render();
28
+
29
+ ctx.globalCompositeOperation = 'destination-over';
30
+ ctx.fillStyle = 'white';
31
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
32
+
33
+ const png = await new Promise(resolve => {
34
+ canvas.toBlob(blob => resolve(blob), 'image/png');
35
+ });
36
+
37
+ return png;
38
+ }
39
+
40
+ async renderAsSVG(elements) {
41
+ if (!Array.isArray(elements)) {
42
+ elements = [ elements ];
43
+ }
44
+
45
+ // gather elements ids
46
+ const ids = elements.map(element => {
47
+ if (typeof element !== 'string') {
48
+ return element.id;
49
+ }
50
+
51
+ return element;
52
+ });
53
+
54
+ // save the diagram as svg and parse document
55
+ const { svg } = await this._bpmnjs.saveSVG();
56
+ const svgDoc = new DOMParser().parseFromString(svg, 'image/svg+xml');
57
+
58
+ // remove visuals of elements we don't want to render
59
+ const gfx = svgDoc.querySelectorAll('svg > .djs-group [data-element-id]')
60
+ gfx.forEach(element => {
61
+ if (!ids.includes(element.dataset.elementId)) {
62
+ element.querySelector('.djs-visual').remove();
63
+ }
64
+ });
65
+
66
+ // adjust svg viewbox with padding to account for arrow markers
67
+ const bbox = this._getBBox(elements);
68
+ svgDoc.documentElement.setAttribute('viewBox',
69
+ `${bbox.x - PADDING.x} ${bbox.y - PADDING.y} ${bbox.width + PADDING.x * 2} ${bbox.height + PADDING.y * 2}`);
70
+
71
+ const serialized = new XMLSerializer().serializeToString(svgDoc);
72
+
73
+ return serialized;
74
+ }
75
+
76
+ _getBBox(elementsOrIds) {
77
+ const elements = elementsOrIds.map(elementOrId => {
78
+ if (typeof elementOrId !== 'string') {
79
+ return elementOrId;
80
+ }
81
+
82
+ return this._elementRegistry.get(elementOrId);
83
+ });
84
+
85
+ return getBBox(elements);
86
+ }
87
+ }
88
+
89
+ ElementsRenderer.$inject = [ 'bpmnjs' ];
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { ElementsRenderer } from './ElementsRenderer';
2
+ import { CopyAsImageContextPadProvider } from './CopyAsImageContextPadProvider';
3
+
4
+
5
+ export const ElementsRendererModule = {
6
+ elementsRenderer: [ 'type', ElementsRenderer ]
7
+ };
8
+
9
+ export const CopyAsImageModule = {
10
+ __depends__: [ ElementsRendererModule ],
11
+ __init__: [ 'copyAsImageContextPadProvider' ],
12
+ copyAsImageContextPadProvider: [ 'type', CopyAsImageContextPadProvider ],
13
+ };