bpmn-js-copy-as-image 0.2.0 → 0.4.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/dist/index.es.js +353 -0
- package/dist/index.js +8 -18
- package/package.json +2 -1
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { Canvg } from 'canvg';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flatten array, one level deep.
|
|
5
|
+
*
|
|
6
|
+
* @template T
|
|
7
|
+
*
|
|
8
|
+
* @param {T[][] | T[] | null} [arr]
|
|
9
|
+
*
|
|
10
|
+
* @return {T[]}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const nativeToString = Object.prototype.toString;
|
|
14
|
+
const nativeHasOwnProperty = Object.prototype.hasOwnProperty;
|
|
15
|
+
|
|
16
|
+
function isUndefined(obj) {
|
|
17
|
+
return obj === undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isNil(obj) {
|
|
21
|
+
return obj == null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isArray(obj) {
|
|
25
|
+
return nativeToString.call(obj) === '[object Array]';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return true, if target owns a property with the given key.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} target
|
|
32
|
+
* @param {String} key
|
|
33
|
+
*
|
|
34
|
+
* @return {Boolean}
|
|
35
|
+
*/
|
|
36
|
+
function has(target, key) {
|
|
37
|
+
return !isNil(target) && nativeHasOwnProperty.call(target, key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Iterate over collection; returning something
|
|
43
|
+
* (non-undefined) will stop iteration.
|
|
44
|
+
*
|
|
45
|
+
* @template T
|
|
46
|
+
* @param {Collection<T>} collection
|
|
47
|
+
* @param { ((item: T, idx: number) => (boolean|void)) | ((item: T, key: string) => (boolean|void)) } iterator
|
|
48
|
+
*
|
|
49
|
+
* @return {T} return result that stopped the iteration
|
|
50
|
+
*/
|
|
51
|
+
function forEach(collection, iterator) {
|
|
52
|
+
|
|
53
|
+
let val,
|
|
54
|
+
result;
|
|
55
|
+
|
|
56
|
+
if (isUndefined(collection)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const convertKey = isArray(collection) ? toNum : identity;
|
|
61
|
+
|
|
62
|
+
for (let key in collection) {
|
|
63
|
+
|
|
64
|
+
if (has(collection, key)) {
|
|
65
|
+
val = collection[key];
|
|
66
|
+
|
|
67
|
+
result = iterator(val, convertKey(key));
|
|
68
|
+
|
|
69
|
+
if (result === false) {
|
|
70
|
+
return val;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
function identity(arg) {
|
|
78
|
+
return arg;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toNum(arg) {
|
|
82
|
+
return Number(arg);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns the surrounding bbox for all elements in
|
|
87
|
+
* the array or the element primitive.
|
|
88
|
+
*
|
|
89
|
+
* @param {Element|Element[]} elements
|
|
90
|
+
* @param {boolean} [stopRecursion=false]
|
|
91
|
+
*
|
|
92
|
+
* @return {Rect}
|
|
93
|
+
*/
|
|
94
|
+
function getBBox(elements, stopRecursion) {
|
|
95
|
+
|
|
96
|
+
stopRecursion = !!stopRecursion;
|
|
97
|
+
if (!isArray(elements)) {
|
|
98
|
+
elements = [ elements ];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
var minX,
|
|
102
|
+
minY,
|
|
103
|
+
maxX,
|
|
104
|
+
maxY;
|
|
105
|
+
|
|
106
|
+
forEach(elements, function(element) {
|
|
107
|
+
|
|
108
|
+
// If element is a connection the bbox must be computed first
|
|
109
|
+
var bbox = element;
|
|
110
|
+
if (element.waypoints && !stopRecursion) {
|
|
111
|
+
bbox = getBBox(element.waypoints, true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var x = bbox.x,
|
|
115
|
+
y = bbox.y,
|
|
116
|
+
height = bbox.height || 0,
|
|
117
|
+
width = bbox.width || 0;
|
|
118
|
+
|
|
119
|
+
if (x < minX || minX === undefined) {
|
|
120
|
+
minX = x;
|
|
121
|
+
}
|
|
122
|
+
if (y < minY || minY === undefined) {
|
|
123
|
+
minY = y;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if ((x + width) > maxX || maxX === undefined) {
|
|
127
|
+
maxX = x + width;
|
|
128
|
+
}
|
|
129
|
+
if ((y + height) > maxY || maxY === undefined) {
|
|
130
|
+
maxY = y + height;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
x: minX,
|
|
136
|
+
y: minY,
|
|
137
|
+
height: maxY - minY,
|
|
138
|
+
width: maxX - minX
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const PADDING = {
|
|
143
|
+
x: 6,
|
|
144
|
+
y: 6
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
class ElementsRenderer {
|
|
148
|
+
constructor(bpmnjs, elementRegistry, selection, copyPaste) {
|
|
149
|
+
this._bpmnjs = bpmnjs;
|
|
150
|
+
this._elementRegistry = elementRegistry;
|
|
151
|
+
this._selection = selection;
|
|
152
|
+
this._copyPaste = copyPaste;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Render current selection as PNG.
|
|
157
|
+
*
|
|
158
|
+
* @returns {Promise<Blob|null>}
|
|
159
|
+
*/
|
|
160
|
+
async renderSelectionAsPNG() {
|
|
161
|
+
const elements = this._selection.get();
|
|
162
|
+
|
|
163
|
+
if (!elements || !elements.length) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const tree = this._copyPaste.createTree(elements);
|
|
168
|
+
const ids = new Set();
|
|
169
|
+
|
|
170
|
+
Object.values(tree || {}).forEach(branch => {
|
|
171
|
+
branch.forEach(descriptor => ids.add(descriptor.id));
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!ids.size) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return this.renderAsPNG([ ...ids ]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Render passed elements as PNG.
|
|
183
|
+
*
|
|
184
|
+
* @param {Array<string|object>} elements - elements to render
|
|
185
|
+
* @returns {Promise<Blob>}
|
|
186
|
+
*/
|
|
187
|
+
async renderAsPNG(elements) {
|
|
188
|
+
const svg = await this.renderAsSVG(elements);
|
|
189
|
+
|
|
190
|
+
const canvas = document.createElement('canvas');
|
|
191
|
+
const ctx = canvas.getContext('2d');
|
|
192
|
+
const canvg = Canvg.fromString(ctx, svg);
|
|
193
|
+
|
|
194
|
+
await canvg.render();
|
|
195
|
+
|
|
196
|
+
ctx.globalCompositeOperation = 'destination-over';
|
|
197
|
+
ctx.fillStyle = 'white';
|
|
198
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
199
|
+
|
|
200
|
+
const png = await new Promise(resolve => {
|
|
201
|
+
canvas.toBlob(blob => resolve(blob), 'image/png');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return png;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async renderAsSVG(elements) {
|
|
208
|
+
if (!Array.isArray(elements)) {
|
|
209
|
+
elements = [ elements ];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// gather elements ids
|
|
213
|
+
const ids = elements.map(element => {
|
|
214
|
+
if (typeof element !== 'string') {
|
|
215
|
+
return element.id;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return element;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// save the diagram as svg and parse document
|
|
222
|
+
const { svg } = await this._bpmnjs.saveSVG();
|
|
223
|
+
const svgDoc = new DOMParser().parseFromString(svg, 'image/svg+xml');
|
|
224
|
+
|
|
225
|
+
// remove visuals of elements we don't want to render
|
|
226
|
+
const gfx = svgDoc.querySelectorAll('svg > .djs-group [data-element-id]');
|
|
227
|
+
gfx.forEach(element => {
|
|
228
|
+
if (!ids.includes(element.dataset.elementId)) {
|
|
229
|
+
element.querySelector('.djs-visual').remove();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// adjust svg viewbox with padding to account for arrow markers
|
|
234
|
+
const bbox = this._getBBox(elements);
|
|
235
|
+
svgDoc.documentElement.setAttribute('viewBox',
|
|
236
|
+
`${bbox.x - PADDING.x} ${bbox.y - PADDING.y} ${bbox.width + PADDING.x * 2} ${bbox.height + PADDING.y * 2}`);
|
|
237
|
+
|
|
238
|
+
const serialized = new XMLSerializer().serializeToString(svgDoc);
|
|
239
|
+
|
|
240
|
+
return serialized;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_getBBox(elementsOrIds) {
|
|
244
|
+
const elements = elementsOrIds.map(elementOrId => {
|
|
245
|
+
if (typeof elementOrId !== 'string') {
|
|
246
|
+
return elementOrId;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return this._elementRegistry.get(elementOrId);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return getBBox(elements);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
ElementsRenderer.$inject = [ 'bpmnjs', 'elementRegistry', 'selection', 'copyPaste' ];
|
|
257
|
+
|
|
258
|
+
class CopyAsImageContextPadProvider {
|
|
259
|
+
constructor(elementsRenderer, contextPad) {
|
|
260
|
+
this._elementsRenderer = elementsRenderer;
|
|
261
|
+
this._contextPad = contextPad;
|
|
262
|
+
|
|
263
|
+
contextPad.registerProvider(this);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getContextPadEntries(element) {
|
|
267
|
+
return this._getEntries(element);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
getMultiElementContextPadEntries(elements) {
|
|
271
|
+
return this._getEntries(elements);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_getEntries(elementOrElements) {
|
|
275
|
+
const elementsRenderer = this._elementsRenderer;
|
|
276
|
+
const contextPad = this._contextPad;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
'copy-as-png': {
|
|
280
|
+
title: 'Copy as PNG',
|
|
281
|
+
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",
|
|
282
|
+
action: {
|
|
283
|
+
async click() {
|
|
284
|
+
const png = await elementsRenderer.renderAsPNG(elementOrElements);
|
|
285
|
+
|
|
286
|
+
await navigator.clipboard.write([
|
|
287
|
+
new ClipboardItem({
|
|
288
|
+
'image/png': png
|
|
289
|
+
})
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
contextPad.close();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
CopyAsImageContextPadProvider.$inject = [ 'elementsRenderer', 'contextPad' ];
|
|
301
|
+
|
|
302
|
+
const HIGH_PRIORITY = 1500;
|
|
303
|
+
|
|
304
|
+
const KEY_C = [ 'c', 'C', 'KeyC' ];
|
|
305
|
+
|
|
306
|
+
class CopyAsImageKeyboard {
|
|
307
|
+
constructor(keyboard, elementsRenderer) {
|
|
308
|
+
this._elementsRenderer = elementsRenderer;
|
|
309
|
+
|
|
310
|
+
keyboard.addListener(HIGH_PRIORITY, event => {
|
|
311
|
+
const keyEvent = event.keyEvent;
|
|
312
|
+
|
|
313
|
+
if (keyboard.isCmd(keyEvent) && keyboard.isShift(keyEvent) && keyboard.isKey(KEY_C, keyEvent)) {
|
|
314
|
+
this._copySelectionAsImage().catch(() => {});
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async _copySelectionAsImage() {
|
|
321
|
+
const png = await this._elementsRenderer.renderSelectionAsPNG();
|
|
322
|
+
|
|
323
|
+
if (!png) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await navigator.clipboard.write([
|
|
328
|
+
new window.ClipboardItem({
|
|
329
|
+
'image/png': png
|
|
330
|
+
})
|
|
331
|
+
]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
CopyAsImageKeyboard.$inject = [ 'keyboard', 'elementsRenderer' ];
|
|
336
|
+
|
|
337
|
+
const ElementsRendererModule = {
|
|
338
|
+
elementsRenderer: [ 'type', ElementsRenderer ]
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const CopyAsImageModule = {
|
|
342
|
+
__depends__: [ ElementsRendererModule ],
|
|
343
|
+
__init__: [ 'copyAsImageKeyboard' ],
|
|
344
|
+
copyAsImageKeyboard: [ 'type', CopyAsImageKeyboard ]
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const CopyAsImageContextPad = {
|
|
348
|
+
__depends__: [ ElementsRendererModule ],
|
|
349
|
+
__init__: [ 'copyAsImageContextPadProvider' ],
|
|
350
|
+
copyAsImageContextPadProvider: [ 'type', CopyAsImageContextPadProvider ],
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export { CopyAsImageContextPad, CopyAsImageModule, ElementsRendererModule };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { svgToImage } from '@bpmn-io/svg-to-image';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Flatten array, one level deep.
|
|
@@ -191,22 +191,7 @@ class ElementsRenderer {
|
|
|
191
191
|
*/
|
|
192
192
|
async renderAsPNG(elements) {
|
|
193
193
|
const svg = await this.renderAsSVG(elements);
|
|
194
|
-
|
|
195
|
-
const canvas = document.createElement('canvas');
|
|
196
|
-
const ctx = canvas.getContext('2d');
|
|
197
|
-
const canvg = Canvg.fromString(ctx, svg);
|
|
198
|
-
|
|
199
|
-
await canvg.render();
|
|
200
|
-
|
|
201
|
-
ctx.globalCompositeOperation = 'destination-over';
|
|
202
|
-
ctx.fillStyle = 'white';
|
|
203
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
204
|
-
|
|
205
|
-
const png = await new Promise(resolve => {
|
|
206
|
-
canvas.toBlob(blob => resolve(blob), 'image/png');
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
return png;
|
|
194
|
+
return svgToImage(svg, { imageType: 'png', outputFormat: 'blob' });
|
|
210
195
|
}
|
|
211
196
|
|
|
212
197
|
async renderAsSVG(elements) {
|
|
@@ -237,8 +222,13 @@ class ElementsRenderer {
|
|
|
237
222
|
|
|
238
223
|
// adjust svg viewbox with padding to account for arrow markers
|
|
239
224
|
const bbox = this._getBBox(elements);
|
|
225
|
+
const paddedWidth = bbox.width + PADDING.x * 2;
|
|
226
|
+
const paddedHeight = bbox.height + PADDING.y * 2;
|
|
227
|
+
|
|
240
228
|
svgDoc.documentElement.setAttribute('viewBox',
|
|
241
|
-
`${bbox.x - PADDING.x} ${bbox.y - PADDING.y} ${
|
|
229
|
+
`${bbox.x - PADDING.x} ${bbox.y - PADDING.y} ${paddedWidth} ${paddedHeight}`);
|
|
230
|
+
svgDoc.documentElement.setAttribute('width', `${Math.max(1, Math.ceil(paddedWidth))}`);
|
|
231
|
+
svgDoc.documentElement.setAttribute('height', `${Math.max(1, Math.ceil(paddedHeight))}`);
|
|
242
232
|
|
|
243
233
|
const serialized = new XMLSerializer().serializeToString(svgDoc);
|
|
244
234
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bpmn-js-copy-as-image",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A bpmn-js extension which allows to render selected elements as images",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"webpack": "^5.105.0"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
+
"@bpmn-io/svg-to-image": "^1.0.0",
|
|
82
83
|
"canvg": "^4.0.3"
|
|
83
84
|
}
|
|
84
85
|
}
|