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 +78 -0
- package/package.json +51 -0
- package/src/CopyAsImageContextPadProvider.js +43 -0
- package/src/ElementsRenderer.js +89 -0
- package/src/index.js +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# bpmn-js-copy-as-image
|
|
2
|
+
|
|
3
|
+
[](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
|
+

|
|
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
|
+
};
|