bpmn-js-copy-as-image 0.3.0 → 0.4.1

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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Canvg } from 'canvg';
1
+ import { svgToImage } from '@bpmn-io/svg-to-image';
2
2
 
3
3
  /**
4
4
  * Flatten array, one level deep.
@@ -139,120 +139,6 @@ function getBBox(elements, stopRecursion) {
139
139
  };
140
140
  }
141
141
 
142
- /**
143
- * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
144
- * under one or more contributor license agreements. See the NOTICE file
145
- * distributed with this work for additional information regarding copyright
146
- * ownership.
147
- *
148
- * Camunda licenses this file to you under the MIT; you may not use this file
149
- * except in compliance with the MIT License.
150
- */
151
-
152
-
153
- // list of defined encodings
154
- const ENCODINGS = [
155
- 'image/png',
156
- 'image/jpeg'
157
- ];
158
-
159
- const OUTPUT_FORMATS = [
160
- 'dataUrl',
161
- 'blob'
162
- ];
163
-
164
- const INITIAL_SCALE = 3;
165
- const FINAL_SCALE = 1;
166
- const SCALE_STEP = 1;
167
-
168
- const DATA_URL_REGEX = /^data:((?:\w+\/(?:(?!;).)+)?)((?:;[\w\W]*?[^;])*),(.+)$/;
169
-
170
- /**
171
- * Generate an image from SVG markup.
172
- *
173
- * @param {string} svg
174
- * @param {{ imageType?: 'png'|'jpeg', outputFormat?: 'dataUrl'|'blob' }} [options]
175
- *
176
- * @returns {Promise<string|Blob>}
177
- */
178
- async function generateImageFromSvg(svg, options = {}) {
179
- const { imageType = 'png', outputFormat = 'dataUrl' } = options;
180
- const encoding = 'image/' + imageType;
181
-
182
- if (OUTPUT_FORMATS.indexOf(outputFormat) === -1) {
183
- throw new Error('<' + outputFormat + '> is not supported output format for converting svg to image');
184
- }
185
-
186
- if (ENCODINGS.indexOf(encoding) === -1) {
187
- throw new Error('<' + imageType + '> is not supported type for converting svg to image');
188
- }
189
-
190
- const initialSVG = svg;
191
-
192
- for (let scale = INITIAL_SCALE; scale >= FINAL_SCALE; scale -= SCALE_STEP) {
193
- try {
194
- let canvas = document.createElement('canvas');
195
-
196
- svg = scaleSvg(initialSVG, scale);
197
-
198
- const context = canvas.getContext('2d');
199
-
200
- const canvg = Canvg.fromString(context, svg);
201
- await canvg.render();
202
-
203
- // make the background white for every format
204
- context.globalCompositeOperation = 'destination-over';
205
- context.fillStyle = 'white';
206
-
207
- context.fillRect(0, 0, canvas.width, canvas.height);
208
-
209
- if (outputFormat === 'dataUrl') {
210
- const dataUrl = canvas.toDataURL(encoding);
211
-
212
- if (DATA_URL_REGEX.test(dataUrl)) {
213
- return dataUrl;
214
- }
215
- } else {
216
- const blob = await new Promise(resolve => {
217
- canvas.toBlob(result => resolve(result), encoding);
218
- });
219
-
220
- if (blob) {
221
- return blob;
222
- }
223
- }
224
- } catch (error) {
225
-
226
- // If rendering or export fails for this scale, try again with a smaller scale.
227
- continue;
228
- }
229
- }
230
-
231
- throw new Error('Error happened generating image. Diagram size is too big.');
232
- }
233
-
234
- function scaleSvg(svg, scale) {
235
- return svg
236
- .replace(/width="([^"]+)"/, function(match, widthStr) {
237
- const width = parseFloat(widthStr);
238
-
239
- if (Number.isNaN(width)) {
240
- return match;
241
- }
242
-
243
- return `width="${width * scale}"`;
244
- })
245
- .replace(/height="([^"]+)"/, function(match, heightStr) {
246
- const height = parseFloat(heightStr);
247
-
248
- if (Number.isNaN(height)) {
249
- return match;
250
- }
251
-
252
- return `height="${height * scale}"`;
253
- });
254
- }
255
-
256
142
  const PADDING = {
257
143
  x: 6,
258
144
  y: 6
@@ -305,7 +191,7 @@ class ElementsRenderer {
305
191
  */
306
192
  async renderAsPNG(elements) {
307
193
  const svg = await this.renderAsSVG(elements);
308
- return generateImageFromSvg(svg, { imageType: 'png', outputFormat: 'blob' });
194
+ return svgToImage(svg, { imageType: 'png', outputFormat: 'blob' });
309
195
  }
310
196
 
311
197
  async renderAsSVG(elements) {
@@ -481,4 +367,4 @@ const CopyAsImageEditorActionsModule = {
481
367
  copyAsImageEditorActions: [ 'type', CopyAsImageEditorActions ]
482
368
  };
483
369
 
484
- export { CopyAsImageContextPadModule, CopyAsImageEditorActionsModule, CopyAsImageModule, ElementsRendererModule, generateImageFromSvg };
370
+ export { CopyAsImageContextPadModule, CopyAsImageEditorActionsModule, CopyAsImageModule, ElementsRendererModule };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bpmn-js-copy-as-image",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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,6 @@
79
79
  "webpack": "^5.105.0"
80
80
  },
81
81
  "dependencies": {
82
- "canvg": "^4.0.3"
82
+ "@bpmn-io/svg-to-image": "^1.0.0"
83
83
  }
84
84
  }
package/dist/index.es.js DELETED
@@ -1,353 +0,0 @@
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 };