@flowgram-vue/export-plugin 0.2.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/LICENSE +22 -0
- package/dist/index.cjs +502 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +499 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/src/constant.ts +20 -0
- package/src/create-plugin.ts +23 -0
- package/src/download-service/index.ts +7 -0
- package/src/download-service/service.ts +139 -0
- package/src/download-service/type.ts +15 -0
- package/src/env.d.ts +9 -0
- package/src/export-image-service/constant.ts +151 -0
- package/src/export-image-service/index.ts +6 -0
- package/src/export-image-service/service.ts +249 -0
- package/src/export-image-service/type.ts +27 -0
- package/src/export-image-service/utils.ts +32 -0
- package/src/index.ts +9 -0
- package/src/type.ts +8 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { inject, injectable } from 'inversify';
|
|
7
|
+
import { FlowDocument } from '@flowgram-vue/document';
|
|
8
|
+
|
|
9
|
+
import { getWorkflowRect } from './utils';
|
|
10
|
+
import { type IFlowExportImageService, type ExportImageOptions } from './type';
|
|
11
|
+
import {
|
|
12
|
+
IN_SAFARI,
|
|
13
|
+
IN_FIREFOX,
|
|
14
|
+
EXPORT_IMAGE_WATERMARK_SVG,
|
|
15
|
+
EXPORT_IMAGE_STYLE_PROPERTIES,
|
|
16
|
+
} from './constant';
|
|
17
|
+
import { FlowDownloadFormat } from '../constant';
|
|
18
|
+
|
|
19
|
+
const PADDING_X = 58;
|
|
20
|
+
const PADDING_Y = 138;
|
|
21
|
+
|
|
22
|
+
@injectable()
|
|
23
|
+
export class FlowExportImageService implements IFlowExportImageService {
|
|
24
|
+
private modernScreenshot: any;
|
|
25
|
+
|
|
26
|
+
@inject(FlowDocument)
|
|
27
|
+
private document: FlowDocument;
|
|
28
|
+
|
|
29
|
+
public async export(options: ExportImageOptions): Promise<string | undefined> {
|
|
30
|
+
try {
|
|
31
|
+
const imgUrl = await this.doExport(options);
|
|
32
|
+
return imgUrl;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error('Export image failed:', e);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async loadModernScreenshot() {
|
|
40
|
+
if (this.modernScreenshot) {
|
|
41
|
+
return this.modernScreenshot;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const modernScreenshot = await import('modern-screenshot');
|
|
45
|
+
this.modernScreenshot = modernScreenshot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async doExport(exportOptions: ExportImageOptions): Promise<string | undefined> {
|
|
49
|
+
if (this.document.layout.name.includes('fixed-layout')) {
|
|
50
|
+
return await this.doFixedExport(exportOptions);
|
|
51
|
+
}
|
|
52
|
+
return await this.doFreeExport(exportOptions);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async doFreeExport(exportOptions: ExportImageOptions): Promise<string | undefined> {
|
|
56
|
+
const { format } = exportOptions;
|
|
57
|
+
// const el = this.stackingContextManager.node as HTMLElement;
|
|
58
|
+
const renderLayer = window.document.querySelector('.gedit-flow-render-layer') as HTMLElement;
|
|
59
|
+
|
|
60
|
+
if (!renderLayer) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { width, height, x, y } = getWorkflowRect(this.document);
|
|
65
|
+
|
|
66
|
+
await this.loadModernScreenshot();
|
|
67
|
+
const { domToPng, domToForeignObjectSvg, domToJpeg } = this.modernScreenshot;
|
|
68
|
+
let imgUrl: string;
|
|
69
|
+
const options = {
|
|
70
|
+
scale: 2,
|
|
71
|
+
includeStyleProperties: IN_SAFARI || IN_FIREFOX ? EXPORT_IMAGE_STYLE_PROPERTIES : undefined,
|
|
72
|
+
width: width + PADDING_X * 2,
|
|
73
|
+
height: height + PADDING_Y * 2,
|
|
74
|
+
onCloneEachNode: (cloned: HTMLElement) => {
|
|
75
|
+
this.handleFreeClone(cloned, { width, height, x, y, options: exportOptions });
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
switch (format) {
|
|
79
|
+
case FlowDownloadFormat.PNG:
|
|
80
|
+
imgUrl = await domToPng(renderLayer, options);
|
|
81
|
+
break;
|
|
82
|
+
case FlowDownloadFormat.SVG: {
|
|
83
|
+
const svg = await domToForeignObjectSvg(renderLayer, options);
|
|
84
|
+
imgUrl = await this.svgToDataURL(svg);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case FlowDownloadFormat.JPEG:
|
|
88
|
+
imgUrl = await domToJpeg(renderLayer, options);
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
imgUrl = await domToPng(renderLayer, options);
|
|
92
|
+
}
|
|
93
|
+
return imgUrl;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async doFixedExport(exportOptions: ExportImageOptions): Promise<string | undefined> {
|
|
97
|
+
const { format } = exportOptions;
|
|
98
|
+
|
|
99
|
+
const el = window.document.querySelector('.gedit-flow-nodes-layer') as HTMLElement;
|
|
100
|
+
|
|
101
|
+
if (!el) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { width, height, x, y } = getWorkflowRect(this.document);
|
|
106
|
+
await this.loadModernScreenshot();
|
|
107
|
+
const { domToPng, domToForeignObjectSvg, domToJpeg } = this.modernScreenshot;
|
|
108
|
+
let imgUrl: string;
|
|
109
|
+
const options = {
|
|
110
|
+
scale: 2,
|
|
111
|
+
includeStyleProperties: IN_SAFARI || IN_FIREFOX ? EXPORT_IMAGE_STYLE_PROPERTIES : undefined,
|
|
112
|
+
width: width + PADDING_X * 2,
|
|
113
|
+
height: height + PADDING_Y * 2,
|
|
114
|
+
onCloneEachNode: (cloned: HTMLElement) => {
|
|
115
|
+
this.handleFixedClone(cloned, { width, height, x, y, options: exportOptions });
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
switch (format) {
|
|
119
|
+
case FlowDownloadFormat.PNG:
|
|
120
|
+
imgUrl = await domToPng(el, options);
|
|
121
|
+
break;
|
|
122
|
+
case FlowDownloadFormat.SVG: {
|
|
123
|
+
const svg = await domToForeignObjectSvg(el, options);
|
|
124
|
+
imgUrl = await this.svgToDataURL(svg);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case FlowDownloadFormat.JPEG:
|
|
128
|
+
imgUrl = await domToJpeg(el, options);
|
|
129
|
+
break;
|
|
130
|
+
default:
|
|
131
|
+
imgUrl = await domToPng(el, options);
|
|
132
|
+
}
|
|
133
|
+
return imgUrl;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async svgToDataURL(svg: SVGElement): Promise<string> {
|
|
137
|
+
return Promise.resolve()
|
|
138
|
+
.then(() => new XMLSerializer().serializeToString(svg))
|
|
139
|
+
.then(encodeURIComponent)
|
|
140
|
+
.then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 处理克隆节点
|
|
144
|
+
private handleFreeClone(
|
|
145
|
+
cloned: HTMLElement,
|
|
146
|
+
{
|
|
147
|
+
width,
|
|
148
|
+
height,
|
|
149
|
+
x,
|
|
150
|
+
y,
|
|
151
|
+
options,
|
|
152
|
+
}: { width: number; height: number; x: number; y: number; options: ExportImageOptions }
|
|
153
|
+
) {
|
|
154
|
+
if (
|
|
155
|
+
cloned?.classList?.contains('gedit-flow-activity-node') ||
|
|
156
|
+
cloned?.classList?.contains('gedit-flow-activity-line')
|
|
157
|
+
) {
|
|
158
|
+
this.handlePosition(cloned, x, y);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (cloned?.classList?.contains('gedit-flow-render-layer')) {
|
|
162
|
+
this.handleCanvas(cloned, width, height, options);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.handleTextareaValue(cloned);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private handleTextareaValue(cloned: HTMLElement) {
|
|
169
|
+
if (cloned.tagName !== 'TEXTAREA') {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const textarea = cloned as HTMLTextAreaElement;
|
|
173
|
+
const value = textarea.getAttribute('value') || textarea.value || '';
|
|
174
|
+
textarea.textContent = value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 处理克隆节点
|
|
178
|
+
private handleFixedClone(
|
|
179
|
+
cloned: HTMLElement,
|
|
180
|
+
{
|
|
181
|
+
width,
|
|
182
|
+
height,
|
|
183
|
+
x,
|
|
184
|
+
y,
|
|
185
|
+
options,
|
|
186
|
+
}: { width: number; height: number; x: number; y: number; options: ExportImageOptions }
|
|
187
|
+
) {
|
|
188
|
+
if (
|
|
189
|
+
cloned?.classList?.contains('gedit-flow-activity-node') ||
|
|
190
|
+
cloned?.classList?.contains('gedit-flow-activity-line')
|
|
191
|
+
) {
|
|
192
|
+
this.handlePosition(cloned, x, y);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (cloned?.classList?.contains('gedit-flow-nodes-layer')) {
|
|
196
|
+
const linesLayer = window.document
|
|
197
|
+
.querySelector('.gedit-flow-lines-layer')
|
|
198
|
+
?.cloneNode(true) as HTMLElement;
|
|
199
|
+
this.handleLines(linesLayer, width, height);
|
|
200
|
+
cloned.appendChild(linesLayer);
|
|
201
|
+
this.handleCanvas(cloned, width, height, options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.handleTextareaValue(cloned);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 处理节点位置
|
|
208
|
+
private handlePosition(cloned: HTMLElement, x: number, y: number) {
|
|
209
|
+
cloned.style.transform = `translate(${-x + PADDING_X}px, ${-y + PADDING_Y}px)`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 处理画布
|
|
213
|
+
private handleLines(cloned: HTMLElement, width: number, height: number) {
|
|
214
|
+
cloned.style.position = 'absolute';
|
|
215
|
+
cloned.style.width = `${width}px`;
|
|
216
|
+
cloned.style.height = `${height}px`;
|
|
217
|
+
cloned.style.left = `${width / 2 - PADDING_X}px`;
|
|
218
|
+
cloned.style.top = `${PADDING_Y}px`;
|
|
219
|
+
cloned.style.transform = 'none';
|
|
220
|
+
cloned.style.backgroundColor = 'transparent';
|
|
221
|
+
cloned.querySelector('.flow-lines-container')!.setAttribute('viewBox', `0 0 1000 1000`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 处理画布
|
|
225
|
+
private handleCanvas(
|
|
226
|
+
cloned: HTMLElement,
|
|
227
|
+
width: number,
|
|
228
|
+
height: number,
|
|
229
|
+
options: ExportImageOptions
|
|
230
|
+
) {
|
|
231
|
+
cloned.style.width = `${width + PADDING_X * 2}px`;
|
|
232
|
+
cloned.style.height = `${height + PADDING_Y * 2}px`;
|
|
233
|
+
cloned.style.transform = 'none';
|
|
234
|
+
cloned.style.backgroundColor = '#ECECEE';
|
|
235
|
+
this.handleWaterMark(cloned, options);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 添加水印节点
|
|
239
|
+
private handleWaterMark(element: HTMLElement, options: ExportImageOptions) {
|
|
240
|
+
const watermarkNode = document.createElement('div');
|
|
241
|
+
// 水印svg
|
|
242
|
+
watermarkNode.innerHTML = options?.watermarkSVG ?? EXPORT_IMAGE_WATERMARK_SVG;
|
|
243
|
+
watermarkNode.style.position = 'absolute';
|
|
244
|
+
watermarkNode.style.bottom = '32px';
|
|
245
|
+
watermarkNode.style.right = '32px';
|
|
246
|
+
watermarkNode.style.zIndex = '999999';
|
|
247
|
+
element.appendChild(watermarkNode);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FlowDownloadFormat } from '../constant';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 导出图片服务
|
|
10
|
+
*/
|
|
11
|
+
export interface IFlowExportImageService {
|
|
12
|
+
/**
|
|
13
|
+
* 导出
|
|
14
|
+
*/
|
|
15
|
+
export: (options: ExportImageOptions) => Promise<string | undefined>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 导出图片选项
|
|
20
|
+
*/
|
|
21
|
+
export interface ExportImageOptions {
|
|
22
|
+
/**
|
|
23
|
+
* 导出的格式
|
|
24
|
+
*/
|
|
25
|
+
format: FlowDownloadFormat;
|
|
26
|
+
watermarkSVG?: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FlowDocument, FlowNodeEntity } from '@flowgram-vue/document';
|
|
7
|
+
import { TransformData } from '@flowgram-vue/core';
|
|
8
|
+
|
|
9
|
+
const getNodesRect = (nodes: FlowNodeEntity[]) => {
|
|
10
|
+
const rects = nodes
|
|
11
|
+
.map((node) => node.getData<TransformData>(TransformData)?.bounds)
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
const x1 = Math.min(...rects.map((rect) => rect.x));
|
|
14
|
+
const x2 = Math.max(...rects.map((rect) => rect.x + rect.width));
|
|
15
|
+
const y1 = Math.min(...rects.map((rect) => rect.y));
|
|
16
|
+
const y2 = Math.max(...rects.map((rect) => rect.y + rect.height));
|
|
17
|
+
|
|
18
|
+
const width = x2 - x1;
|
|
19
|
+
const height = y2 - y1;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
width,
|
|
23
|
+
height,
|
|
24
|
+
x: x1,
|
|
25
|
+
y: y1,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 获取流程所有节点矩形坐标
|
|
31
|
+
*/
|
|
32
|
+
export const getWorkflowRect = (document: FlowDocument) => getNodesRect(document.getAllNodes());
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { createDownloadPlugin } from './create-plugin';
|
|
7
|
+
export { FlowDownloadService, type DownloadServiceOptions } from './download-service';
|
|
8
|
+
export { type CreateDownloadPluginOptions } from './type';
|
|
9
|
+
export { FlowDownloadFormat } from './constant';
|
package/src/type.ts
ADDED