From 48b1a36e32b4fdf2b7bbecba2d9606e095734a04 Mon Sep 17 00:00:00 2001 From: Dymonelewis <120695700@qq.com> Date: Mon, 24 Mar 2025 15:40:43 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(snapshot):=20=E5=BF=AB=E7=85=A7?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81=20base64=20=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=92=8C=E5=B1=80=E9=83=A8=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/extensions/snapshot/index.tsx | 34 ++++++------ packages/extension/src/style/raw.ts | 4 ++ .../extension/src/tools/snapshot/index.ts | 54 +++++++++++++++++-- 3 files changed, 72 insertions(+), 20 deletions(-) diff --git a/examples/feature-examples/src/pages/extensions/snapshot/index.tsx b/examples/feature-examples/src/pages/extensions/snapshot/index.tsx index bc0b3645e..a2a9f854c 100644 --- a/examples/feature-examples/src/pages/extensions/snapshot/index.tsx +++ b/examples/feature-examples/src/pages/extensions/snapshot/index.tsx @@ -177,25 +177,25 @@ export default function SnapshotExample() { } // 预览 base64 - const previewBase64 = () => { + const previewBase64 = async () => { if (lfRef.current) { setBlobData('') - lfRef.current - .getSnapshotBase64(backgroundColor) - .then( - ({ - data, - width, - height, - }: { - data: string - width: number - height: number - }) => { - setBase64Data(data) - console.log('width, height ', width, height) - }, - ) + const params: ToImageOptions = { + fileType, + backgroundColor, + partial, + width, + height, + padding, + quality, + } + const result = await lfRef.current.getSnapshotBase64( + 'white', + 'png', + params, + ) + setBase64Data(result.data) + console.log('width, height ', result) } } diff --git a/packages/extension/src/style/raw.ts b/packages/extension/src/style/raw.ts index e83c0deb0..674e3ec69 100644 --- a/packages/extension/src/style/raw.ts +++ b/packages/extension/src/style/raw.ts @@ -193,6 +193,10 @@ export const content = `@import url('medium-editor/dist/css/medium-editor.min.cs background: #eaedf2; border: 1px solid #93a3b4; } +.lf-mini-map .lf-graph { + width: 100% !important; + height: 100% !important; +} .lf-mini-map-graph { position: relative; overflow: hidden; diff --git a/packages/extension/src/tools/snapshot/index.ts b/packages/extension/src/tools/snapshot/index.ts index 32ebc91e6..8aac378f4 100644 --- a/packages/extension/src/tools/snapshot/index.ts +++ b/packages/extension/src/tools/snapshot/index.ts @@ -77,7 +77,8 @@ export class Snapshot { lf.getSnapshotBase64 = async ( backgroundColor?: string, fileType?: string, - ) => await this.getSnapshotBase64(backgroundColor, fileType) + toImageOptions?: ToImageOptions, + ) => await this.getSnapshotBase64(backgroundColor, fileType, toImageOptions) } /** @@ -209,19 +210,66 @@ export class Snapshot { * 获取base64对象 * @param backgroundColor * @param fileType + * @param toImageOptions * @returns */ async getSnapshotBase64( backgroundColor?: string, fileType?: string, + toImageOptions?: ToImageOptions, + ): Promise { + const curPartial = this.lf.graphModel.getPartial() + const { partial = curPartial } = toImageOptions ?? {} + // 获取流程图配置 + const editConfig = this.lf.getEditConfig() + // 开启静默模式 + this.lf.updateEditConfig({ + isSilentMode: true, + stopScrollGraph: true, + stopMoveGraph: true, + }) + + let result: SnapshotResponse | undefined + // 处理局部渲染模式 + if (curPartial !== partial) { + this.lf.graphModel.setPartial(partial) + await new Promise((resolve) => { + this.lf.graphModel.eventCenter.once('graph:updated', async () => { + result = await this._getSnapshotBase64( + backgroundColor, + fileType, + toImageOptions, + ) + // 恢复原来渲染模式 + this.lf.graphModel.setPartial(curPartial) + resolve() + }) + }) + } else { + result = await this._getSnapshotBase64( + backgroundColor, + fileType, + toImageOptions, + ) + } + + // 恢复原来配置 + this.lf.updateEditConfig(editConfig) + return result! + } + + // 内部方法处理实际的base64转换 + private async _getSnapshotBase64( + backgroundColor?: string, + fileType?: string, + toImageOptions?: ToImageOptions, ): Promise { const svg = this.getSvgRootElement(this.lf) await updateImageSource(svg as SVGElement) return new Promise((resolve) => { - this.getCanvasData(svg, { backgroundColor }).then( + this.getCanvasData(svg, { backgroundColor, ...toImageOptions }).then( (canvas: HTMLCanvasElement) => { const base64 = canvas.toDataURL(`image/${fileType ?? 'png'}`) - // 输出图片数据以及图片宽高 resolve({ data: base64, width: canvas.width, From 6833ad1ab4601f097b660bca304857b4e2b21980 Mon Sep 17 00:00:00 2001 From: Dymonelewis <120695700@qq.com> Date: Mon, 24 Mar 2025 16:00:20 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(snapshot):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=9B=B8=E5=AF=B9=E8=B7=AF=E5=BE=84=E5=9B=BE=E7=89=87=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature-examples/public/images/test.jpeg | Bin 0 -> 3333 bytes .../src/pages/extensions/snapshot/data.ts | 10 + .../pages/extensions/snapshot/imageNode.ts | 37 +- .../src/pages/extensions/snapshot/index.tsx | 9 +- .../extension/src/tools/snapshot/index.ts | 401 +++++++++++------- 5 files changed, 299 insertions(+), 158 deletions(-) create mode 100755 examples/feature-examples/public/images/test.jpeg diff --git a/examples/feature-examples/public/images/test.jpeg b/examples/feature-examples/public/images/test.jpeg new file mode 100755 index 0000000000000000000000000000000000000000..540ea5931acc7513f2e1d1af803d0d1f34b69d64 GIT binary patch literal 3333 zcmbW2c{o&U8^E72!$`81%FAR)qGZXdcgjps)^{Q;N{pmlBuU6NmZFq3sbnc4RF)}g zWNj?TTe80BHoW;@||ZN2An$h%5>%i$a#;XeZr#3%=7c`|R92X8~SZTJ{qD zm4CE^_y6c6EA$e*_17W^hSdVNdddXH6^A4eg=T zCognKtkKv$x{nf(&`x7Q#r1Lz-M&}fVp27?h37ZV~bA-;^nDv2Mg z`ib-p8BU4`wO%N{(ol@kk)cYM42LlxgsS+Y8FBPw#0`(GxV5*%A7@@5Hg3Vr1&L2xz0;#zBX(usl4R2St$5NjMro>RQ`uM=1UyKbJVn zJsq%q;99-W*YW*z>~B%o2zY7Y8s_TjOUUvUM9{ zZ$>pITcXrTxptLCLruIxOHE>#Vq1jFFnJR99QGMes(zRy$muy6wtxWYnN~PWw~&o) z2_%a^Bj@1i4#7eGN->2CsKuyY+zJ@ZdYtJ_S20(#5yWd0y<}*u6`?fK)W)c%|~T z+c`TY=OODG$J@T_NHA_!&(YAwaEm#txo_bC>B`@PC}W>0q+gnf3J#xNr7z}J4~%64rHd!79ZOxP@{x( zPgfSvU1&;y4bO>l2#7$ydxtK97F#*yO07w;*8*TyzZ{I{&9wAcs$2xcO?WrBkJDb}X zSty?5W!$yakn>gL_N!L0}X-Zzxtw&*fO?o{+iy>OkF(cxBH3;}fQl3fWOw}J9A zs2*SbE+bE-BEa)Gdt(vj>&)kg_Rkhyk_;C@hu!>Vjm=Ty!To)_b#BbU2APf1j#6pt zSXI!xmT5%md zJ6b@9Pm65~mJ!(8DSC`-QMMOS#b>r8h7W9|6{MaqV*P3OO4!%b^w7tbA>d$%CB_@q-RaGCB zx|i?C;fir&+Z4>c?-r#*Z5&Ss*Wj5W;7TeqwZFV?dzhjPcTHUAqdz%M&HRKdhD#+o zum=+1sh8wUa1%A+rR-_u<67@0=u+-<0>@e15&=Gf`;X8p=@o)^M9Err#`RkD(R-{@G`G z_;!yXt|V{1fPjnjf$SKT^hgpWXm~$_yS|rBp+8ii1g2=JPMRCSxPlNc*Qp%TVGC`$ zI**f^k|332W<&}Wzbb?qjgisti2yHe+m>)bCD)wFNUgY6^i90_>f8R-uOA&93+_sT z8&++b*qhXb*aSWJv5{WBix{juXM*3jNtF?tH2K)kwaxR$~ zQ!88WiDEinNHpV&ST=S1VxytQyM5@1CokF0S~H*V8r3y20<%d^2{2 z(-}uppy+eAdJu3>RkXjxeda;hpTFHsW+<7~+}SOicHVa3t?hmp)N$gvUGCW)WFy#$ zeYNAj)vz^;$#L`hL4@%S)Sejx4B~~S(q+9W7Mfk<3@0&kdZ^9l`b< ze_6BF(VbIXAJ*SWth*7RTZdgesQ@Omx7|s)6G=pR`F3;@FXjwAcVui>zFgVRscYJ` zp`W+yysmo11?-^rQoC2mRAflvJ##-7*WN=YC_fIWVr*+!g{RY+FD5{RO&6cmUR;t& zL_kBvI08~%FC8C60F5Zl`=h&u1eGL<-Hp~uPT~9C1?O&>rCx0OMha=cM`~AL#{?{Q z=c9uzrrYJ4O9$Wn-Yy@1S;yUD{y{wpg!}VO@iVD&bvqBJV7l{#$HQYHU3ZrWditgk zb#bYTdY*qbe2p_j5B9&4UQB&oVd7{@_$C*qu(*3=tA#1OJ~t;)gOTfID_DOTI0!o@ Sf>|EX*ubW1mH1^MAN~bzrZ3q5 literal 0 HcmV?d00001 diff --git a/examples/feature-examples/src/pages/extensions/snapshot/data.ts b/examples/feature-examples/src/pages/extensions/snapshot/data.ts index 6f4942ca1..7f71f83bb 100644 --- a/examples/feature-examples/src/pages/extensions/snapshot/data.ts +++ b/examples/feature-examples/src/pages/extensions/snapshot/data.ts @@ -38,6 +38,16 @@ export default { y: 100, text: '菱形', }, + { + id: 'test_image_1', + type: 'test-image', + x: 300, + y: 200, + properties: { + width: 82, + height: 96, + }, + }, ], edges: [ { diff --git a/examples/feature-examples/src/pages/extensions/snapshot/imageNode.ts b/examples/feature-examples/src/pages/extensions/snapshot/imageNode.ts index bd98f5fdb..1cf1c227c 100644 --- a/examples/feature-examples/src/pages/extensions/snapshot/imageNode.ts +++ b/examples/feature-examples/src/pages/extensions/snapshot/imageNode.ts @@ -28,8 +28,43 @@ class ImageNode extends RectNode { } } -export default { +class TestImageModel extends RectNodeModel { + initNodeData(data: any) { + super.initNodeData(data) + this.width = 100 + this.height = 75 + } +} + +class TestImageNode extends RectNode { + getImageHref() { + return '/images/test.jpeg' + } + getShape() { + const { x, y, width, height } = this.props.model + const href = this.getImageHref() + const attrs = { + x: x - (1 / 2) * width, + y: y - (1 / 2) * height, + width, + height, + href, + preserveAspectRatio: 'none meet', + } + return h('g', {}, [h('image', { ...attrs })]) + } +} + +const defaultImageNode = { type: 'image', view: ImageNode, model: ImageModel, } + +export const testImage = { + type: 'test-image', + view: TestImageNode, + model: TestImageModel, +} + +export default defaultImageNode diff --git a/examples/feature-examples/src/pages/extensions/snapshot/index.tsx b/examples/feature-examples/src/pages/extensions/snapshot/index.tsx index a2a9f854c..ebff14fd0 100644 --- a/examples/feature-examples/src/pages/extensions/snapshot/index.tsx +++ b/examples/feature-examples/src/pages/extensions/snapshot/index.tsx @@ -15,8 +15,8 @@ import { InputNumber, Switch, } from 'antd' -import ImageNode from './imageNode' import CustomHtml from '@/components/nodes/custom-html/Html' +import ImageNode, { testImage } from './imageNode' import data from './data' import { circle as circleSvgUrl, rect as rectSvgUrl } from './svg' @@ -89,6 +89,7 @@ export default function SnapshotExample() { }) lf.register(CustomHtml) lf.register(ImageNode) + lf.register(testImage) lf.setPatternItems([ { @@ -103,6 +104,12 @@ export default function SnapshotExample() { text: 'circle', icon: rectSvgUrl, }, + { + type: 'test-image', + label: 'Test Image', + text: 'Test Image', + icon: rectSvgUrl, + }, ]) lf.on('custom:button-click', (model: any) => { diff --git a/packages/extension/src/tools/snapshot/index.ts b/packages/extension/src/tools/snapshot/index.ts index 8aac378f4..9dddef8b6 100644 --- a/packages/extension/src/tools/snapshot/index.ts +++ b/packages/extension/src/tools/snapshot/index.ts @@ -176,139 +176,134 @@ export class Snapshot { } /** - * 下载图片 - * @param fileName - * @param toImageOptions + * 将图片转换为base64格式 + * @param url - 图片URL + * @returns Promise - base64字符串 */ - private async snapshot(fileName?: string, toImageOptions?: ToImageOptions) { - const { fileType = 'png', quality } = toImageOptions ?? {} - this.fileName = `${fileName ?? `logic-flow.${Date.now()}`}.${fileType}` - const svg = this.getSvgRootElement(this.lf) - await updateImageSource(svg as SVGElement) - if (fileType === 'svg') { - const copy = this.cloneSvg(svg) - const svgString = new XMLSerializer().serializeToString(copy) - const blob = new Blob([svgString], { - type: 'image/svg+xml;charset=utf-8', - }) - const url = URL.createObjectURL(blob) - this.triggerDownload(url) - } else { - this.getCanvasData(svg, toImageOptions ?? {}).then( - (canvas: HTMLCanvasElement) => { - // canvas元素 => base64 url image/octet-stream: 确保所有浏览器都能正常下载 - const imgUrl = canvas - .toDataURL(`image/${fileType}`, quality) - .replace(`image/${fileType}`, 'image/octet-stream') - this.triggerDownload(imgUrl) - }, - ) - } + private async convertImageToBase64(url: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image() + img.crossOrigin = 'anonymous' // 处理跨域问题 + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = img.width + canvas.height = img.height + const ctx = canvas.getContext('2d') + ctx?.drawImage(img, 0, 0) + const base64 = canvas.toDataURL('image/png') + resolve(base64) + } + img.onerror = () => { + reject(new Error(`Failed to load image: ${url}`)) + } + img.src = url + }) } /** - * 获取base64对象 - * @param backgroundColor - * @param fileType - * @param toImageOptions - * @returns + * 检查URL是否为相对路径 + * @param url - 要检查的URL + * @returns boolean - 是否为相对路径 */ - async getSnapshotBase64( - backgroundColor?: string, - fileType?: string, - toImageOptions?: ToImageOptions, - ): Promise { - const curPartial = this.lf.graphModel.getPartial() - const { partial = curPartial } = toImageOptions ?? {} - // 获取流程图配置 - const editConfig = this.lf.getEditConfig() - // 开启静默模式 - this.lf.updateEditConfig({ - isSilentMode: true, - stopScrollGraph: true, - stopMoveGraph: true, - }) + private isRelativePath(url: string): boolean { + return ( + !url.startsWith('data:') && + !url.startsWith('http://') && + !url.startsWith('https://') && + !url.startsWith('//') + ) + } - let result: SnapshotResponse | undefined - // 处理局部渲染模式 - if (curPartial !== partial) { - this.lf.graphModel.setPartial(partial) - await new Promise((resolve) => { - this.lf.graphModel.eventCenter.once('graph:updated', async () => { - result = await this._getSnapshotBase64( - backgroundColor, - fileType, - toImageOptions, - ) - // 恢复原来渲染模式 - this.lf.graphModel.setPartial(curPartial) - resolve() - }) - }) - } else { - result = await this._getSnapshotBase64( - backgroundColor, - fileType, - toImageOptions, - ) + /** + * 处理SVG中的图片元素 + * @param element - SVG元素 + */ + private async processImages(element: Element): Promise { + // 处理image元素 + const images = element.getElementsByTagName('image') + for (let i = 0; i < images.length; i++) { + const image = images[i] + const href = + image.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || + image.getAttribute('href') + if (href && this.isRelativePath(href)) { + try { + const base64 = await this.convertImageToBase64(href) + image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', base64) + image.setAttribute('href', base64) + } catch (error) { + console.warn(`Failed to convert image to base64: ${href}`, error) + } + } } - // 恢复原来配置 - this.lf.updateEditConfig(editConfig) - return result! - } - - // 内部方法处理实际的base64转换 - private async _getSnapshotBase64( - backgroundColor?: string, - fileType?: string, - toImageOptions?: ToImageOptions, - ): Promise { - const svg = this.getSvgRootElement(this.lf) - await updateImageSource(svg as SVGElement) - return new Promise((resolve) => { - this.getCanvasData(svg, { backgroundColor, ...toImageOptions }).then( - (canvas: HTMLCanvasElement) => { - const base64 = canvas.toDataURL(`image/${fileType ?? 'png'}`) - resolve({ - data: base64, - width: canvas.width, - height: canvas.height, - }) - }, - ) - }) + // 处理foreignObject中的img元素 + const foreignObjects = element.getElementsByTagName('foreignObject') + for (let i = 0; i < foreignObjects.length; i++) { + const foreignObject = foreignObjects[i] + const images = foreignObject.getElementsByTagName('img') + for (let j = 0; j < images.length; j++) { + const image = images[j] + const src = image.getAttribute('src') + if (src && this.isRelativePath(src)) { + try { + const base64 = await this.convertImageToBase64(src) + image.setAttribute('src', base64) + } catch (error) { + console.warn(`Failed to convert image to base64: ${src}`, error) + } + } + } + } } /** - * 获取Blob对象 - * @param backgroundColor - * @param fileType + * 克隆并处理画布节点 + * @param svg * @returns */ - async getSnapshotBlob( - backgroundColor?: string, - fileType?: string, - ): Promise { - const svg = this.getSvgRootElement(this.lf) - await updateImageSource(svg as SVGElement) - return new Promise((resolve) => { - this.getCanvasData(svg, { backgroundColor }).then( - (canvas: HTMLCanvasElement) => { - canvas.toBlob( - (blob) => { - // 输出图片数据以及图片宽高 - resolve({ - data: blob!, - width: canvas.width, - height: canvas.height, - }) - }, - `image/${fileType ?? 'png'}`, - ) - }, - ) - }) + private async cloneSvg( + svg: Element, + addStyle: boolean = true, + ): Promise { + const copy = svg.cloneNode(true) as Element + const graph = copy.lastChild as Element + let childLength = graph?.childNodes?.length + if (childLength) { + for (let i = 0; i < childLength; i++) { + const lfLayer = graph?.childNodes[i] as SVGGraphicsElement + // 只保留包含节点和边的基础图层进行下载,其他图层删除 + const layerClassList = + lfLayer.classList && Array.from(lfLayer.classList) + if (layerClassList && layerClassList.indexOf('lf-base') < 0) { + graph?.removeChild(graph.childNodes[i]) + childLength-- + i-- + } else { + // 删除锚点 + const lfBase = graph?.childNodes[i] + lfBase && + lfBase.childNodes.forEach((item) => { + const element = item as SVGGraphicsElement + this.removeAnchor(element.firstChild!) + this.removeRotateControl(element.firstChild!) + }) + } + } + } + + // 处理图片路径 + await this.processImages(copy) + + // 设置css样式 + if (addStyle) { + const style = document.createElement('style') + style.innerHTML = this.getClassRules() + const foreignObject = document.createElement('foreignObject') + foreignObject.appendChild(style) + copy.appendChild(foreignObject) + } + return copy } /** @@ -350,7 +345,7 @@ export class Snapshot { toImageOptions: ToImageOptions, ): Promise { const { width, height, backgroundColor, padding = 40 } = toImageOptions - const copy = this.cloneSvg(svg, false) + const copy = await this.cloneSvg(svg, false) let dpr = window.devicePixelRatio || 1 if (dpr < 1) { @@ -469,45 +464,139 @@ export class Snapshot { } /** - * 克隆并处理画布节点 - * @param svg + * 获取Blob对象 + * @param backgroundColor + * @param fileType * @returns */ - private cloneSvg(svg: Element, addStyle: boolean = true): Node { - const copy = svg.cloneNode(true) - const graph = copy.lastChild - let childLength = graph?.childNodes?.length - if (childLength) { - for (let i = 0; i < childLength; i++) { - const lfLayer = graph?.childNodes[i] as SVGGraphicsElement - // 只保留包含节点和边的基础图层进行下载,其他图层删除 - const layerClassList = - lfLayer.classList && Array.from(lfLayer.classList) - if (layerClassList && layerClassList.indexOf('lf-base') < 0) { - graph?.removeChild(graph.childNodes[i]) - childLength-- - i-- - } else { - // 删除锚点 - const lfBase = graph?.childNodes[i] - lfBase && - lfBase.childNodes.forEach((item) => { - const element = item as SVGGraphicsElement - this.removeAnchor(element.firstChild!) - this.removeRotateControl(element.firstChild!) - }) - } - } + async getSnapshotBlob( + backgroundColor?: string, + fileType?: string, + ): Promise { + const svg = this.getSvgRootElement(this.lf) + await updateImageSource(svg as SVGElement) + return new Promise((resolve) => { + this.getCanvasData(svg, { backgroundColor }).then( + (canvas: HTMLCanvasElement) => { + canvas.toBlob( + (blob) => { + // 输出图片数据以及图片宽高 + resolve({ + data: blob!, + width: canvas.width, + height: canvas.height, + }) + }, + `image/${fileType ?? 'png'}`, + ) + }, + ) + }) + } + + /** + * 获取base64对象 + * @param backgroundColor + * @param fileType + * @param toImageOptions + * @returns + */ + async getSnapshotBase64( + backgroundColor?: string, + fileType?: string, + toImageOptions?: ToImageOptions, + ): Promise { + const curPartial = this.lf.graphModel.getPartial() + const { partial = curPartial } = toImageOptions ?? {} + // 获取流程图配置 + const editConfig = this.lf.getEditConfig() + // 开启静默模式 + this.lf.updateEditConfig({ + isSilentMode: true, + stopScrollGraph: true, + stopMoveGraph: true, + }) + + let result: SnapshotResponse | undefined + // 处理局部渲染模式 + if (curPartial !== partial) { + this.lf.graphModel.setPartial(partial) + await new Promise((resolve) => { + this.lf.graphModel.eventCenter.once('graph:updated', async () => { + result = await this._getSnapshotBase64( + backgroundColor, + fileType, + toImageOptions, + ) + // 恢复原来渲染模式 + this.lf.graphModel.setPartial(curPartial) + resolve() + }) + }) + } else { + result = await this._getSnapshotBase64( + backgroundColor, + fileType, + toImageOptions, + ) } - // 设置css样式 - if (addStyle) { - const style = document.createElement('style') - style.innerHTML = this.getClassRules() - const foreignObject = document.createElement('foreignObject') - foreignObject.appendChild(style) - copy.appendChild(foreignObject) + + // 恢复原来配置 + this.lf.updateEditConfig(editConfig) + return result! + } + + // 内部方法处理实际的base64转换 + private async _getSnapshotBase64( + backgroundColor?: string, + fileType?: string, + toImageOptions?: ToImageOptions, + ): Promise { + const svg = this.getSvgRootElement(this.lf) + await updateImageSource(svg as SVGElement) + return new Promise((resolve) => { + this.getCanvasData(svg, { backgroundColor, ...toImageOptions }).then( + (canvas: HTMLCanvasElement) => { + const base64 = canvas.toDataURL(`image/${fileType ?? 'png'}`) + resolve({ + data: base64, + width: canvas.width, + height: canvas.height, + }) + }, + ) + }) + } + + /** + * 下载图片 + * @param fileName + * @param toImageOptions + */ + private async snapshot(fileName?: string, toImageOptions?: ToImageOptions) { + const { fileType = 'png', quality } = toImageOptions ?? {} + this.fileName = `${fileName ?? `logic-flow.${Date.now()}`}.${fileType}` + const svg = this.getSvgRootElement(this.lf) + await updateImageSource(svg as SVGElement) + if (fileType === 'svg') { + const copy = await this.cloneSvg(svg) + const svgString = new XMLSerializer().serializeToString(copy) + const blob = new Blob([svgString], { + type: 'image/svg+xml;charset=utf-8', + }) + const url = URL.createObjectURL(blob) + this.triggerDownload(url) + } else { + this.getCanvasData(svg, toImageOptions ?? {}).then( + (canvas: HTMLCanvasElement) => { + // canvas元素 => base64 url image/octet-stream: 确保所有浏览器都能正常下载 + const imgUrl = canvas + .toDataURL(`image/${fileType}`, quality) + .replace(`image/${fileType}`, 'image/octet-stream') + this.triggerDownload(imgUrl) + }, + ) } - return copy } } From ffcfce3d7e87eb124172d2abc48e99252c32df4b Mon Sep 17 00:00:00 2001 From: Dymonelewis <120695700@qq.com> Date: Mon, 28 Apr 2025 15:02:07 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(extension):=20=E4=BC=98=E5=8C=96snapsho?= =?UTF-8?q?t=E6=8F=92=E4=BB=B6,=E5=AE=9E=E7=8E=B0snapshotBase64=E5=92=8Csn?= =?UTF-8?q?apshotBlob=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=8F=82=E6=95=B0=20&=20=E4=BC=98=E5=8C=96sn?= =?UTF-8?q?apshot=E6=96=87=E6=A1=A3=E5=92=8Cdemo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/extensions/snapshot/index.tsx | 21 +- .../extension/src/tools/snapshot/index.ts | 294 ++++++++++-------- .../docs/tutorial/extension/snapshot.en.md | 260 +++++++++++----- .../docs/tutorial/extension/snapshot.zh.md | 257 ++++++++++----- .../extension/native/demo/snapshot.tsx | 24 +- sites/docs/public/test.jpeg | Bin 0 -> 3333 bytes .../advanced/node/htmlNode/index.less | 4 +- .../src/tutorial/extension/snapshot/data.ts | 20 +- .../tutorial/extension/snapshot/imageNode.ts | 34 ++ .../src/tutorial/extension/snapshot/index.tsx | 31 +- 10 files changed, 634 insertions(+), 311 deletions(-) create mode 100755 sites/docs/public/test.jpeg diff --git a/examples/feature-examples/src/pages/extensions/snapshot/index.tsx b/examples/feature-examples/src/pages/extensions/snapshot/index.tsx index ebff14fd0..673477235 100644 --- a/examples/feature-examples/src/pages/extensions/snapshot/index.tsx +++ b/examples/feature-examples/src/pages/extensions/snapshot/index.tsx @@ -163,9 +163,18 @@ export default function SnapshotExample() { // 预览 blob const previewBlob = () => { if (lfRef.current) { - setBase64Data('') + setBlobData('') + const params: ToImageOptions = { + fileType, + backgroundColor, + partial, + width, + height, + padding, + quality, + } lfRef.current - .getSnapshotBlob(backgroundColor, fileType) + .getSnapshotBlob(backgroundColor, fileType, params) .then( ({ data, @@ -186,7 +195,7 @@ export default function SnapshotExample() { // 预览 base64 const previewBase64 = async () => { if (lfRef.current) { - setBlobData('') + setBase64Data('') const params: ToImageOptions = { fileType, backgroundColor, @@ -310,17 +319,17 @@ export default function SnapshotExample() {
- + {blobData && ( <>

blobData

- + )} {base64Data && ( <>

base64Data

- + )} diff --git a/packages/extension/src/tools/snapshot/index.ts b/packages/extension/src/tools/snapshot/index.ts index 9dddef8b6..107c8545d 100644 --- a/packages/extension/src/tools/snapshot/index.ts +++ b/packages/extension/src/tools/snapshot/index.ts @@ -70,12 +70,15 @@ export class Snapshot { ) => await this.getSnapshot(fileName, toImageOptions) /* 获取Blob对象 */ - lf.getSnapshotBlob = async (backgroundColor?: string, fileType?: string) => - await this.getSnapshotBlob(backgroundColor, fileType) + lf.getSnapshotBlob = async ( + backgroundColor?: string, // 兼容老的使用方式 + fileType?: string, + toImageOptions?: ToImageOptions, + ) => await this.getSnapshotBlob(backgroundColor, fileType, toImageOptions) /* 获取Base64对象 */ lf.getSnapshotBase64 = async ( - backgroundColor?: string, + backgroundColor?: string, // 兼容老的使用方式 fileType?: string, toImageOptions?: ToImageOptions, ) => await this.getSnapshotBase64(backgroundColor, fileType, toImageOptions) @@ -144,37 +147,6 @@ export class Snapshot { } } - /** - * 导出画布:导出前的处理画布工作,局部渲染模式处理、静默模式处理 - * @param fileName - * @param toImageOptions - */ - async getSnapshot(fileName?: string, toImageOptions?: ToImageOptions) { - const curPartial = this.lf.graphModel.getPartial() - const { partial = curPartial } = toImageOptions ?? {} - // 获取流程图配置 - const editConfig = this.lf.getEditConfig() - // 开启静默模式:如果元素多的话 避免用户交互 感知卡顿 - this.lf.updateEditConfig({ - isSilentMode: true, - stopScrollGraph: true, - stopMoveGraph: true, - }) - // 画布当前渲染模式和用户导出渲染模式不一致时,需要更新画布 - if (curPartial !== partial) { - this.lf.graphModel.setPartial(partial) - this.lf.graphModel.eventCenter.once('graph:updated', async () => { - await this.snapshot(fileName, toImageOptions) - // 恢复原来渲染模式 - this.lf.graphModel.setPartial(curPartial) - }) - } else { - await this.snapshot(fileName, toImageOptions) - } - // 恢复原来配置 - this.lf.updateEditConfig(editConfig) - } - /** * 将图片转换为base64格式 * @param url - 图片URL @@ -378,21 +350,31 @@ export class Snapshot { const { transformModel } = graphModel const { SCALE_X, SCALE_Y, TRANSLATE_X, TRANSLATE_Y } = transformModel + // 计算实际宽高,考虑缩放因素 + // 在宽画布情况下,getBoundingClientRect可能无法获取到所有元素的边界 + // 因此我们添加一个安全系数来确保能够容纳所有元素 + const safetyFactor = 1.1 // 安全系数,增加20%的空间 + const actualWidth = (bbox.width / SCALE_X) * safetyFactor + const actualHeight = (bbox.height / SCALE_Y) * safetyFactor + // 将导出区域移动到左上角,canvas 绘制的时候是从左上角开始绘制的 + // 在transform矩阵中加入padding值,确保左侧元素不会被截断 ;(copy.lastChild as SVGElement).style.transform = `matrix(1, 0, 0, 1, ${ - (-offsetX + TRANSLATE_X) * (1 / SCALE_X) - }, ${(-offsetY + TRANSLATE_Y) * (1 / SCALE_Y)})` + (-offsetX + TRANSLATE_X) * (1 / SCALE_X) + padding / dpr + }, ${(-offsetY + TRANSLATE_Y) * (1 / SCALE_Y) + padding / dpr})` - // 包含所有元素的最小宽高 - const bboxWidth = Math.ceil(bbox.width / SCALE_X) - const bboxHeight = Math.ceil(bbox.height / SCALE_Y) + // 包含所有元素的最小宽高,确保足够大以容纳所有元素 + const bboxWidth = Math.ceil(actualWidth) + const bboxHeight = Math.ceil(actualHeight) const canvas = document.createElement('canvas') canvas.style.width = `${bboxWidth}px` canvas.style.height = `${bboxHeight}px` // 宽高值 默认加padding 40,保证图形不会紧贴着下载图片 - canvas.width = bboxWidth * dpr + padding * 2 - canvas.height = bboxHeight * dpr + padding * 2 + // 为宽画布添加额外的安全边距,确保不会裁剪 + const safetyMargin = 40 // 额外的安全边距 + canvas.width = bboxWidth * dpr + padding * 2 + safetyMargin + canvas.height = bboxHeight * dpr + padding * 2 + safetyMargin const ctx = canvas.getContext('2d') if (ctx) { // 清空canvas @@ -430,19 +412,22 @@ export class Snapshot { ? copyCanvas(canvas, width, height).height : canvas.height, }).then((imageBitmap) => { - ctx?.drawImage(imageBitmap, padding / dpr, padding / dpr) + // 由于在transform矩阵中已经考虑了padding,这里不再需要额外的padding偏移 + ctx?.drawImage(imageBitmap, 0, 0) resolve( width && height ? copyCanvas(canvas, width, height) : canvas, ) }) } else { - ctx?.drawImage(img, padding / dpr, padding / dpr) + // 由于在transform矩阵中已经考虑了padding,这里不再需要额外的padding偏移 + ctx?.drawImage(img, 0, 0) resolve( width && height ? copyCanvas(canvas, width, height) : canvas, ) } } catch (e) { - ctx?.drawImage(img, padding / dpr, padding / dpr) + // 由于在transform矩阵中已经考虑了padding,这里不再需要额外的padding偏移 + ctx?.drawImage(img, 0, 0) resolve(width && height ? copyCanvas(canvas, width, height) : canvas) } } @@ -464,108 +449,65 @@ export class Snapshot { } /** - * 获取Blob对象 - * @param backgroundColor - * @param fileType - * @returns - */ - async getSnapshotBlob( - backgroundColor?: string, - fileType?: string, - ): Promise { - const svg = this.getSvgRootElement(this.lf) - await updateImageSource(svg as SVGElement) - return new Promise((resolve) => { - this.getCanvasData(svg, { backgroundColor }).then( - (canvas: HTMLCanvasElement) => { - canvas.toBlob( - (blob) => { - // 输出图片数据以及图片宽高 - resolve({ - data: blob!, - width: canvas.width, - height: canvas.height, - }) - }, - `image/${fileType ?? 'png'}`, - ) - }, - ) - }) - } - - /** - * 获取base64对象 - * @param backgroundColor - * @param fileType - * @param toImageOptions - * @returns + * 封装导出前的通用处理逻辑:局部渲染模式处理、静默模式处理 + * @param callback 实际执行的导出操作回调函数 + * @param toImageOptions 导出图片选项 + * @returns 返回回调函数的执行结果 */ - async getSnapshotBase64( - backgroundColor?: string, - fileType?: string, + private async withExportPreparation( + callback: () => Promise, toImageOptions?: ToImageOptions, - ): Promise { + ): Promise { + // 获取当前局部渲染状态 const curPartial = this.lf.graphModel.getPartial() const { partial = curPartial } = toImageOptions ?? {} // 获取流程图配置 const editConfig = this.lf.getEditConfig() - // 开启静默模式 + + // 开启静默模式:如果元素多的话 避免用户交互 感知卡顿 this.lf.updateEditConfig({ isSilentMode: true, stopScrollGraph: true, stopMoveGraph: true, }) - let result: SnapshotResponse | undefined - // 处理局部渲染模式 - if (curPartial !== partial) { - this.lf.graphModel.setPartial(partial) - await new Promise((resolve) => { - this.lf.graphModel.eventCenter.once('graph:updated', async () => { - result = await this._getSnapshotBase64( - backgroundColor, - fileType, - toImageOptions, - ) - // 恢复原来渲染模式 - this.lf.graphModel.setPartial(curPartial) - resolve() + let result: T + + try { + // 如果画布的渲染模式与导出渲染模式不一致,则切换渲染模式 + if (curPartial !== partial) { + this.lf.graphModel.setPartial(partial) + // 等待画布更新完成 + result = await new Promise((resolve) => { + this.lf.graphModel.eventCenter.once('graph:updated', async () => { + const callbackResult = await callback() + // 恢复原来渲染模式 + this.lf.graphModel.setPartial(curPartial) + resolve(callbackResult) + }) }) - }) - } else { - result = await this._getSnapshotBase64( - backgroundColor, - fileType, - toImageOptions, - ) + } else { + // 直接执行回调 + result = await callback() + } + } finally { + // 恢复原来配置 + this.lf.updateEditConfig(editConfig) } - // 恢复原来配置 - this.lf.updateEditConfig(editConfig) - return result! + return result } - // 内部方法处理实际的base64转换 - private async _getSnapshotBase64( - backgroundColor?: string, - fileType?: string, - toImageOptions?: ToImageOptions, - ): Promise { - const svg = this.getSvgRootElement(this.lf) - await updateImageSource(svg as SVGElement) - return new Promise((resolve) => { - this.getCanvasData(svg, { backgroundColor, ...toImageOptions }).then( - (canvas: HTMLCanvasElement) => { - const base64 = canvas.toDataURL(`image/${fileType ?? 'png'}`) - resolve({ - data: base64, - width: canvas.width, - height: canvas.height, - }) - }, - ) - }) + /** + * 导出画布:导出前的处理画布工作,局部渲染模式处理、静默模式处理 + * @param fileName + * @param toImageOptions + */ + async getSnapshot(fileName?: string, toImageOptions?: ToImageOptions) { + await this.withExportPreparation( + () => this.snapshot(fileName, toImageOptions), + toImageOptions, + ) } /** @@ -598,6 +540,100 @@ export class Snapshot { ) } } + + /** + * 获取Blob对象 + * @param fileType + * @param toImageOptions + * @returns + */ + async getSnapshotBlob( + backgroundColor?: string, + fileType?: string, + toImageOptions?: ToImageOptions, + ): Promise { + return await this.withExportPreparation( + () => this.snapshotBlob(toImageOptions, fileType, backgroundColor), + toImageOptions, + ) + } + + // 内部方法处理blob转换 + private async snapshotBlob( + toImageOptions?: ToImageOptions, + baseFileType?: string, + backgroundColor?: string, + ): Promise { + const { fileType = baseFileType } = toImageOptions ?? {} + const svg = this.getSvgRootElement(this.lf) + await updateImageSource(svg as SVGElement) + return new Promise((resolve) => { + this.getCanvasData(svg, { + backgroundColor, + ...(toImageOptions ?? {}), + }).then((canvas: HTMLCanvasElement) => { + canvas.toBlob( + (blob) => { + // 输出图片数据以及图片宽高 + resolve({ + data: blob!, + width: canvas.width, + height: canvas.height, + }) + }, + `image/${fileType ?? 'png'}`, + ) + }) + }) + } + + /** + * 获取base64对象 + * @param backgroundColor + * @param fileType + * @param toImageOptions + * @returns + */ + async getSnapshotBase64( + backgroundColor?: string, + fileType?: string, + toImageOptions?: ToImageOptions, + ): Promise { + console.log( + 'getSnapshotBase64---------------', + backgroundColor, + fileType, + toImageOptions, + ) + return await this.withExportPreparation( + () => this._getSnapshotBase64(backgroundColor, fileType, toImageOptions), + toImageOptions, + ) + } + + // 内部方法处理实际的base64转换 + private async _getSnapshotBase64( + backgroundColor?: string, + baseFileType?: string, + toImageOptions?: ToImageOptions, + ): Promise { + const { fileType = baseFileType } = toImageOptions ?? {} + const svg = this.getSvgRootElement(this.lf) + await updateImageSource(svg as SVGElement) + return new Promise((resolve) => { + this.getCanvasData(svg, { + backgroundColor, + ...(toImageOptions ?? {}), + }).then((canvas: HTMLCanvasElement) => { + const base64 = canvas.toDataURL(`image/${fileType ?? 'png'}`) + resolve({ + data: base64, + width: canvas.width, + height: canvas.height, + }) + }) + }) + } } export default Snapshot diff --git a/sites/docs/docs/tutorial/extension/snapshot.en.md b/sites/docs/docs/tutorial/extension/snapshot.en.md index 30f34247d..c795408d7 100644 --- a/sites/docs/docs/tutorial/extension/snapshot.en.md +++ b/sites/docs/docs/tutorial/extension/snapshot.en.md @@ -9,62 +9,76 @@ toc: content tag: Optimization --- -We often need to export the canvas content as an image. We provide an independent plug-in package `Snapshot` to use this function. +We often need to export the canvas content as an image. LogicFlow provides an independent plug-in package `Snapshot` to support exporting the canvas as an image. -## Demonstration +## Usage - - -## usage +### Registration -### 1. register - -There are two registration methods, global registration and local registration. The difference is that global registration is available for every `lf` instance. +Like other LogicFlow plugins, Snapshot supports both global and local registration: ```tsx | pure import LogicFlow from "@logicflow/core"; import { Snapshot } from "@logicflow/extension"; -// Global Registration +// Global Registration: Available for all LogicFlow instances LogicFlow.use(Snapshot); -// Partial Registration +// Local Registration: Only available for the current instance const lf = new LogicFlow({ ...config, plugins: [Snapshot] }); - ``` -### 2. use +### Basic Usage -After registration, the `getSnapshot()` method will be mounted on the `lf` instance and called through the `lf.getSnapshot()` method. +After registering the plugin, you can directly call the export method through the LogicFlow instance: ```tsx | pure +// Export as PNG image and download +lf.getSnapshot('Flowchart'); +``` -// Can be triggered in any way, and then the drawn graphics are downloaded to the local disk -document.getElementById("button").addEventListener("click", () => { - lf.getSnapshot(); +## Features - // or version 1.1.13 - // lf.extension.snapshot.getSnapshot() -}); +In version 2.0, we have comprehensively upgraded the export functionality: -``` +- **Multiple Format Support**: PNG, JPEG, SVG, and other formats +- **Custom Background and Padding**: Adjust image effects according to requirements +- **Partial Rendering**: Option to export only the visible area, improving efficiency +- **Custom Styles**: Support for adding CSS styles to ensure consistent export image style -It is worth mentioning that the images captured and downloaded through this plug-in will not be affected by offset or scaling. +### Configuration Options -## Customize CSS +The export method supports the `toImageOptions` parameter with the following configuration options: -When custom elements need to add additional CSS styles when exporting images, you can do it as follows: +| Property Name | Type | Default Value | Description | +| ------------- | ---- | ------------- | ----------- | +| fileType | string | png | Export format: `png`, `webp`, `jpeg`, `svg` | +| width | number | - | Image width (may cause image stretching) | +| height | number | - | Image height (may cause image stretching) | +| backgroundColor | string | - | Background color, transparent by default | +| quality | number | 0.92 | Image quality, only effective for `jpeg` and `webp`, value range 0-1 | +| padding | number | 40 | Inner margin, in pixels | +| partial | boolean | false | Whether to export only the visible area | -In order to keep the images generated by the flowchart consistent with the effects on the canvas, the `snapshot` plugin will load all `css` rules of the current page into the exported images by default, but this may cause errors due to cross-domain CSS files, refer to issue575. You can modify useGlobalRules to prohibit loading all CSS rules, and then use the `customCssRules` property to customize and add CSS styles. +:::warning{title=Notes} +- SVG format does not support `width`, `height`, `backgroundColor`, `padding` attributes +- Custom width and height may cause image stretching, also affecting padding +- During export, the canvas will automatically handle wide canvas situations, adding safety factors and extra margins +- During export, silent mode will be automatically enabled, disabling canvas interaction +- Automatically converts relative path images in SVG to Base64 encoding 2.0.14 New +::: -```tsx | pure +### Custom CSS Styles + +To keep the exported image consistent with the canvas effect, the plugin loads all CSS rules of the page by default. If you encounter cross-domain issues, you can: -// CSS style is enabled by default -lf.extension.snapshot.useGlobalRules = true -// Will not overwrite css styles, will be superimposed, customCssRules has a high priority +```tsx | pure +// Disable global CSS rules +lf.extension.snapshot.useGlobalRules = false; +// Add custom styles (higher priority) lf.extension.snapshot.customCssRules = ` .uml-wrapper { line-height: 1.2; @@ -72,88 +86,166 @@ lf.extension.snapshot.customCssRules = ` color: blue; } ` - ``` -## API - -### lf.getSnapshot(...) - -Export the image. - -```ts - -getSnapshot(fileName?: string, toImageOptions?: ToImageOptions) : Promise +## API Reference +### getSnapshot +Export image and download +```tsx | pure +lf.getSnapshot(name: string, toImageOptions?: ToImageOptions) ``` -`fileName` is the file name. If it is not filled in, the default value is `logic-flow.${current timestamp}`. `ToImageOptions` is described as follows: - -| Property Name | Type | Default Value | Required | Description | -| --------- | -------- | -------------------------- | -------- | ----------------------------------------------------------------- | -| fileType | string | png | | The format of the exported image. The optional values ​​are: `png`, `webp`, `jpeg`, `svg`. The default value is `png` | -| width | number | - | | The width of the exported image. Usually, it does not need to be set. Setting it may stretch the image. | -| height | numebr | - | | The height of the exported image. Usually, it does not need to be set. Setting it may stretch the image. | -| backgroundColor | string | - | | The background color of the exported image. The default value is transparent | -| quality | number | - | | The quality of the exported image. When the image format is specified as `jpeg` or `webp`, the image quality can be selected from the range of 0 to 1. If it exceeds the value range, the default value of 0.92 will be used. This parameter will be ignored when exporting to other formats. | -| padding | number | 40 | | The inner margin of the exported image, that is, the distance between the border of the element content area and the border of the image, in pixels, the default is 40 | -| partial | boolean | - | | Whether to enable partial rendering when exporting images, `false`: all elements on the canvas will be exported, `true`: only visible elements in the screen area will be exported | - -Note: -- `svg` currently does not support `width`, `height`, `backgroundColor`, `padding` attributes. -- After customizing the width and height, the graphics may be stretched, and `padding` will also be stretched, resulting in inaccuracy. -- The `toImageOptions` option is supported in `2.0.0` version. - -### lf.getSnapshotBlob(...) +### getSnapshotBlob +Get Blob object +```tsx | pure +lf.getSnapshotBlob(backgroundColor?: string, fileType?: string): Promise<{ data: Blob; width: number; height: number }> +// Supported syntax after version 2.0.14👇🏻 +lf.getSnapshotBlob( + backgroundColor?: string, // Compatible with old syntax, will be used as fallback for toImageOptions.backgroundColor + fileType?: string, // Compatible with old syntax, will be used as fallback for toImageOptions.fileType + toImageOptions?: ToImageOptions // New parameter +) +``` -In addition to supporting image type export, `snapshot` also supports downloading Blob file objects and Base64 text encoding +### getSnapshotBase64 +Get Base64 string +```tsx | pure +lf.getSnapshotBase64(backgroundColor?: string, fileType?: string): Promise<{ data: string; width: number; height: number }> +// Supported syntax after version 2.0.14👇🏻 +lf.getSnapshotBase64( + backgroundColor?: string, // Compatible with old syntax, will be used as fallback for toImageOptions.backgroundColor + fileType?: string, // Compatible with old syntax, will be used as fallback for toImageOptions.fileType + toImageOptions?: ToImageOptions // New parameter +) +``` -Get a Blob object. +## Usage Examples -```ts +### Demonstration -async getSnapshotBlob(backgroundColor?: string, fileType?: string) : Promise + -// example -const { data : blob } = await lf.getSnapshotBlob() -console.log(blob) +### Code Examples +**Basic Usage: Export as PNG image and download** +```tsx | pure +lf.getSnapshot('Flowchart'); ``` -`backgroundColor`: background, transparent by default if not filled. - -`fileType`: file type, png by default if not filled. - -`SnapshotResponse`: return object. +**Advanced Usage: Specify format, background color, and other options** +```tsx | pure +lf.getSnapshot('Flowchart', { + fileType: 'png', // Options: 'png', 'webp', 'jpeg', 'svg' + backgroundColor: '#f5f5f5', + padding: 30, // Inner margin, in pixels + partial: false, // false: export all elements, true: only export visible area + quality: 0.92 // Effective for jpeg and webp formats, value range 0-1 +}) +``` +**Export as SVG format** ```tsx | pure +lf.getSnapshot('Flowchart', { + fileType:'svg' + // Note: svg format does not support width, height, backgroundColor, padding attributes +}); +``` -export type SnapshotResponse = { - data: Blob | string // Blob object or Base64 encoded text - width: number // Image width - height: number // Image height +**Get Blob object for further processing** +```tsx | pure +const { data: blob, width, height } = await lf.getSnapshotBlob({ + fileType: 'jpeg', + backgroundColor: '#ffffff', + quality: 0.8 +}) +// Use Blob object to create temporary URL (e.g., for preview) +const blobUrl = URL.createObjectURL(blob); +try { + // Use blobUrl, e.g., set as image source + document.getElementById('preview').src = blobUrl; +} finally { + // Release URL after use + URL.revokeObjectURL(blobUrl); } - ``` -### lf.getSnapshotBase64(...) +**Get Base64 string for further processing** +```tsx | pure +const { data: base64 } = await lf.getSnapshotBase64({ + fileType: 'png', + partial: true // Only export visible area +}); +// Use Base64 directly for img tag +document.getElementById('preview').src = base64; +``` -Get the `Base64 text encoding` of the text. +**Custom CSS Styles** +```tsx | pure +lf.extension.snapshot.useGlobalRules = false; // Disable global CSS rules to avoid cross-domain issues +lf.extension.snapshot.customCssRules = ` + .node-container { border: 2px solid blue; } + .edge-text { font-weight: bold; } + .lf-node-text { font-size: 14px; font-weight: bold; } +`; +``` +**Using in Components** +```tsx | pure +const downloadSnapshot = async () => { + // Export as image and download + await lf.getSnapshot('Flowchart', { + fileType: 'png', + backgroundColor: '#ffffff', + padding: 40 + }); +}; +``` -```ts +**Using in Button Click Events** +```tsx | pure +// Using in button click events +document.getElementById('download-btn').addEventListener('click', async () => { + // Show loading state + showLoading(); + try { + // Export image (will automatically apply silent mode and other optimizations) + await lf.getSnapshot('Flowchart'); + } finally { + // Hide loading state + hideLoading(); + } +}); +``` -async getSnapshotBase64(backgroundColor?: string, fileType?: string) : Promise +**Export and Upload to Server** -// example -const { data : base64 } = await lf.getSnapshotBlob() -console.log(base64) +```tsx | pure +// Export as Blob and upload to server +async function exportAndUpload() { + const { data: blob } = await lf.getSnapshotBlob({ + fileType: 'png', + backgroundColor: '#ffffff' + }); + + const formData = new FormData(); + formData.append('file', blob, 'flowchart.png'); + + try { + const response = await fetch('/api/upload', { + method: 'POST', + body: formData + }); + const result = await response.json(); + console.log('Upload successful:', result); + } catch (error) { + console.error('Upload failed:', error); + } +} ``` -## Other export types - -### xml +## Other Export Types -1.0.7 New +### xml 1.0.7 New The default data generated by LogicFlow is in json format. Some process engines may require the front end to provide data in xml format. `@logicflow/extension` provides two plugins, `lfJson2Xml` and `lfXml2Json`, for converting json and xml to each other. diff --git a/sites/docs/docs/tutorial/extension/snapshot.zh.md b/sites/docs/docs/tutorial/extension/snapshot.zh.md index 05eda4649..3665570b6 100644 --- a/sites/docs/docs/tutorial/extension/snapshot.zh.md +++ b/sites/docs/docs/tutorial/extension/snapshot.zh.md @@ -9,62 +9,75 @@ toc: content tag: 优化 --- -我们经常需要将画布内容通过图片的形式导出来,我们提供了一个独立的插件包 `Snapshot` 来使用这个功能。 - -## 演示 - - - +我们常常有需要将画布内容以图片的形式导出来的情况,因此LogicFlow提供了一个独立的插件包 `Snapshot` 以支持用户将画布导出为图片。 ## 用法 -### 1. 注册 +### 注册插件 -两种注册方式,全局注册和局部注册,区别是全局注册每一个`lf`实例都可以使用。 +与其他LogicFlow插件一样,Snapshot支持全局注册和局部注册两种方式: ```tsx | pure import LogicFlow from "@logicflow/core"; import { Snapshot } from "@logicflow/extension"; -// 全局注册 +// 全局注册:所有LogicFlow实例都能使用 LogicFlow.use(Snapshot); -// 局部注册 +// 局部注册:仅当前实例可用 const lf = new LogicFlow({ ...config, plugins: [Snapshot] }); - ``` -### 2. 使用 +### 基本用法 -注册后,`lf`实例身上将被挂载`getSnapshot()`方法,通过`lf.getSnapshot()`方法调用。 +注册插件后,您可以直接通过LogicFlow实例调用导出方法: ```tsx | pure +// 导出为PNG图片并下载 +lf.getSnapshot('流程图'); +``` -// 可以使用任意方式触发,然后将绘制的图形下载到本地磁盘上 -document.getElementById("button").addEventListener("click", () => { - lf.getSnapshot(); +## 功能特性 - // 或者 1.1.13版本 - // lf.extension.snapshot.getSnapshot() -}); +在2.0版本中,我们对导出功能进行了全面升级: -``` +- **多格式支持**:PNG、JPEG、SVG等多种格式 +- **自定义背景和边距**:根据需求调整图片效果 +- **局部渲染**:可选择只导出可见区域,提高效率 +- **自定义样式**:支持添加CSS样式,确保导出图片风格一致 -值得一提的是:通过此插件截取下载的图片不会因为偏移、缩放受到影响。 +### 配置选项 -## 自定义设置 css +导出方法支持`toImageOptions`参数,提供以下配置项: -当自定义元素在导出图片时需要额外添加 css 样式时,可以用如下方式实现: +| 属性名 | 类型 | 默认值 | 描述 | +| --------------- | ------- | ------ | ----------------------------------------- | +| fileType | string | png | 导出格式:`png`、`webp`、`jpeg`、`svg` | +| width | number | - | 图片宽度(可能导致图形拉伸) | +| height | number | - | 图片高度(可能导致图形拉伸) | +| backgroundColor | string | - | 背景色,默认透明 | +| quality | number | 0.92 | 图片质量,仅对`jpeg`和`webp`有效,取值0-1 | +| padding | number | 40 | 内边距,单位像素 | +| partial | boolean | false | 是否只导出可见区域 | -为了保持流程图生成的图片与画布上效果一致,`snapshot`插件默认会将当前页面所有的 `css` 规则都加载到导出图片中, 但是可能会因为 css 文件跨域引起报错,参考 issue575。可以修改useGlobalRules来禁止加载所有 css 规则,然后通过`customCssRules`属性来自定义增加css样式。 +:::warning{title=注意事项} +- 导出SVG格式的图片时不支持`width`、`height`、`backgroundColor`、`padding`属性 +- 自定义宽高可能导致图形拉伸,同时影响内边距 +- 导出时会自动处理宽画布情况,添加安全系数和额外边距 +- 导出过程中会自动开启静默模式,禁用画布交互 +- 自动将SVG中的相对路径图片转换为Base64编码2.0.14新增 +::: -```tsx | pure +### 自定义CSS样式 -// 默认开启css样式 -lf.extension.snapshot.useGlobalRules = true -// 不会覆盖css样式,会叠加,customCssRules优先级高 +为保持导出图片与画布效果一致,插件默认加载页面所有CSS规则。如遇跨域问题,可以: + +```tsx | pure +// 禁用全局CSS规则 +lf.extension.snapshot.useGlobalRules = false; +// 添加自定义样式(优先级高) lf.extension.snapshot.customCssRules = ` .uml-wrapper { line-height: 1.2; @@ -72,88 +85,166 @@ lf.extension.snapshot.customCssRules = ` color: blue; } ` - ``` -## API - -### lf.getSnapshot(...) - -导出图片。 - -```ts - -getSnapshot(fileName?: string, toImageOptions?: ToImageOptions) : Promise +## API参考 +### getSnapshot +导出图片并下载 +```tsx | pure +lf.getSnapshot(name: string, toImageOptions?: ToImageOptions) ``` -`fileName` 为文件名称,不填为默认为`logic-flow.当前时间戳`,`ToImageOptions` 描述如下: - -| 属性名 | 类型 | 默认值 | 必填 | 描述 | -| --------- | -------- | -------------------------- | -------- | ----------------------------------------------------------------- | -| fileType | string | png | | 导出图片的格式,可选值为:`png`、`webp`、`jpeg`、`svg`,默认值为 `png` | -| width | number | - | | 导出图片的宽度,通常无需设置,设置后可能会拉伸图形 | -| height | numebr | - | | 导出图片的高度,通常无需设置,设置后可能会拉伸图形 | -| backgroundColor | string | - | | 导出图片的背景色,默认为透明 | -| quality | number | - | | 导出图片的质量。在指定图片格式为 `jpeg` 或 `webp` 的情况下,可以从 0 到 1 的区间内选择图片的质量,如果超出取值范围,将会使用默认值 0.92。导出为其他格式的图片时,该参数会被忽略 | -| padding | number | 40 | | 导出图片的内边距,即元素内容所在区域边界与图片边界的距离,单位为像素,默认为 40 | -| partial | boolean | - | | 导出图片时是否开启局部渲染,`false`:将导出画布上所有的元素,`true`:只导出画面区域内的可见元素 | - -注意: -- `svg`目前暂不支持`width`,`height`, `backgroundColor`, `padding` 属性。 -- 自定义宽高后,可能会拉伸图形,这时候`padding`也会被拉伸导致不准确。 -- `2.0.0`版本才开始支持`toImageOptions`选项。 - -### lf.getSnapshotBlob(...) +### getSnapshotBlob +获取Blob对象 +```tsx | pure +lf.getSnapshotBlob(backgroundColor?: string, fileType?: string): Promise<{ data: Blob; width: number; height: number }> +// 2.0.14版本后支持的写法👇🏻 +lf.getSnapshotBlob( + backgroundColor?: string, // 兼容老写法,传入后会作为toImageOptions.backgroundColor的兜底配置 + fileType?: string, // 兼容老写法,传入后会作为toImageOptions.fileType的兜底配置 + toImageOptions?: ToImageOptions // 新增参数 +) +``` -`snapshot` 除了支持图片类型导出,还支持下载 Blob文件对象 Base64文本编码 +### getSnapshotBase64 +获取Base64字符串 +```tsx | pure +lf.getSnapshotBase64(backgroundColor?: string, fileType?: string): Promise<{ data: string; width: number; height: number }> +// 2.0.14版本后支持的写法👇🏻 +lf.getSnapshotBase64( + backgroundColor?: string, // 兼容老写法,传入后会作为toImageOptions.backgroundColor的兜底配置 + fileType?: string, // 兼容老写法,传入后会作为toImageOptions.fileType的兜底配置 + toImageOptions?: ToImageOptions // 新增参数 +) +``` -获取`Blob`对象。 +## 使用示例 -```ts +### 功能演示 -async getSnapshotBlob(backgroundColor?: string, fileType?: string) : Promise + -// example -const { data : blob } = await lf.getSnapshotBlob() -console.log(blob) +### 代码示例 +**基本用法:导出为PNG图片并下载** +```tsx | pure +lf.getSnapshot('流程图'); ``` -`backgroundColor`: 背景,不填默认为透明。 - -`fileType`: 文件类型,不填默认为png。 - -`SnapshotResponse`: 返回对象。 +**高级用法:指定格式、背景色和其他选项** +```tsx | pure +lf.getSnapshot('流程图', { + fileType: 'png', // 可选:'png'、'webp'、'jpeg'、'svg' + backgroundColor: '#f5f5f5', + padding: 30, // 内边距,单位为像素 + partial: false, // false: 导出所有元素,true: 只导出可见区域 + quality: 0.92 // 对jpeg和webp格式有效,取值范围0-1 +}) +``` +**导出为SVG格式** ```tsx | pure +lf.getSnapshot('流程图', { + fileType:'svg' + // 注意:svg格式暂不支持width、height、backgroundColor、padding属性 +}); +``` -export type SnapshotResponse = { - data: Blob | string // Blob对象 或 Base64文本编码文本 - width: number // 图片宽度 - height: number // 图片高度 +**获取Blob对象用于进一步处理** +```tsx | pure +const { data: blob, width, height } = await lf.getSnapshotBlob({ + fileType: 'jpeg', + backgroundColor: '#ffffff', + quality: 0.8 +}) +// 使用Blob对象创建临时URL(例如预览) +const blobUrl = URL.createObjectURL(blob); +try { + // 使用blobUrl,例如设置为图片源 + document.getElementById('preview').src = blobUrl; +} finally { + // 使用完毕后释放URL + URL.revokeObjectURL(blobUrl); } - ``` -### lf.getSnapshotBase64(...) +**获取Base64字符串用于进一步处理** +```tsx | pure +const { data: base64 } = await lf.getSnapshotBase64({ + fileType: 'png', + partial: true // 只导出可见区域 +}); +// 将Base64直接用于img标签 +document.getElementById('preview').src = base64; +``` -获取`Base64文本编码`文本。 +**自定义CSS样式** +```tsx | pure +lf.extension.snapshot.useGlobalRules = false; // 禁用全局CSS规则,避免跨域问题 +lf.extension.snapshot.customCssRules = ` + .node-container { border: 2px solid blue; } + .edge-text { font-weight: bold; } + .lf-node-text { font-size: 14px; font-weight: bold; } +`; +``` +**在组件组件中使用** +```tsx | pure +const downloadSnapshot = async () => { + // 导出为图片并下载 + await lf.getSnapshot('流程图', { + fileType: 'png', + backgroundColor: '#ffffff', + padding: 40 + }); +}; +``` -```ts +**在按钮点击事件中使用** +```tsx | pure +// 在按钮点击事件中使用 +document.getElementById('download-btn').addEventListener('click', async () => { + // 显示加载状态 + showLoading(); + try { + // 导出图片(会自动应用静默模式和其他优化) + await lf.getSnapshot('流程图'); + } finally { + // 隐藏加载状态 + hideLoading(); + } +}); +``` -async getSnapshotBase64(backgroundColor?: string, fileType?: string) : Promise +**导出并上传到服务器** -// example -const { data : base64 } = await lf.getSnapshotBlob() -console.log(base64) +```tsx | pure +// 导出为Blob并上传到服务器 +async function exportAndUpload() { + const { data: blob } = await lf.getSnapshotBlob({ + fileType: 'png', + backgroundColor: '#ffffff' + }); + + const formData = new FormData(); + formData.append('file', blob, 'flowchart.png'); + + try { + const response = await fetch('/api/upload', { + method: 'POST', + body: formData + }); + const result = await response.json(); + console.log('上传成功:', result); + } catch (error) { + console.error('上传失败:', error); + } +} ``` ## 其他导出类型 -### xml - -1.0.7 新增 +### xml 1.0.7 新增 LogicFlow 默认生成的数据是 json 格式,可能会有一些流程引擎需要前端提供 xml 格式数据。`@logicflow/extension`提供了`lfJson2Xml`和`lfXml2Json`两个插件,用于将 json 和 xml 进行互相转换。 diff --git a/sites/docs/examples/extension/native/demo/snapshot.tsx b/sites/docs/examples/extension/native/demo/snapshot.tsx index 239f31d6e..d6095a1a9 100644 --- a/sites/docs/examples/extension/native/demo/snapshot.tsx +++ b/sites/docs/examples/extension/native/demo/snapshot.tsx @@ -117,7 +117,7 @@ const data = { id: 'uml_1', properties: { name: 'hello', - body: '哈哈哈哈', + body: '哈哈哈哈111', }, }, { @@ -296,8 +296,17 @@ const SnapshotExample: React.FC = () => { const previewBlob = () => { if (lfRef.current) { setBase64Data(''); + const params: ToImageOptions = { + fileType, + backgroundColor, + partial, + width, + height, + padding, + quality, + }; lfRef.current - .getSnapshotBlob(backgroundColor, fileType) + .getSnapshotBlob(backgroundColor, fileType, params) .then( ({ data, @@ -319,8 +328,17 @@ const SnapshotExample: React.FC = () => { const previewBase64 = () => { if (lfRef.current) { setBlobData(''); + const params: ToImageOptions = { + fileType, + backgroundColor, + partial, + width, + height, + padding, + quality, + }; lfRef.current - .getSnapshotBase64(backgroundColor) + .getSnapshotBase64(backgroundColor, fileType, params) .then( ({ data, diff --git a/sites/docs/public/test.jpeg b/sites/docs/public/test.jpeg new file mode 100755 index 0000000000000000000000000000000000000000..540ea5931acc7513f2e1d1af803d0d1f34b69d64 GIT binary patch literal 3333 zcmbW2c{o&U8^E72!$`81%FAR)qGZXdcgjps)^{Q;N{pmlBuU6NmZFq3sbnc4RF)}g zWNj?TTe80BHoW;@||ZN2An$h%5>%i$a#;XeZr#3%=7c`|R92X8~SZTJ{qD zm4CE^_y6c6EA$e*_17W^hSdVNdddXH6^A4eg=T zCognKtkKv$x{nf(&`x7Q#r1Lz-M&}fVp27?h37ZV~bA-;^nDv2Mg z`ib-p8BU4`wO%N{(ol@kk)cYM42LlxgsS+Y8FBPw#0`(GxV5*%A7@@5Hg3Vr1&L2xz0;#zBX(usl4R2St$5NjMro>RQ`uM=1UyKbJVn zJsq%q;99-W*YW*z>~B%o2zY7Y8s_TjOUUvUM9{ zZ$>pITcXrTxptLCLruIxOHE>#Vq1jFFnJR99QGMes(zRy$muy6wtxWYnN~PWw~&o) z2_%a^Bj@1i4#7eGN->2CsKuyY+zJ@ZdYtJ_S20(#5yWd0y<}*u6`?fK)W)c%|~T z+c`TY=OODG$J@T_NHA_!&(YAwaEm#txo_bC>B`@PC}W>0q+gnf3J#xNr7z}J4~%64rHd!79ZOxP@{x( zPgfSvU1&;y4bO>l2#7$ydxtK97F#*yO07w;*8*TyzZ{I{&9wAcs$2xcO?WrBkJDb}X zSty?5W!$yakn>gL_N!L0}X-Zzxtw&*fO?o{+iy>OkF(cxBH3;}fQl3fWOw}J9A zs2*SbE+bE-BEa)Gdt(vj>&)kg_Rkhyk_;C@hu!>Vjm=Ty!To)_b#BbU2APf1j#6pt zSXI!xmT5%md zJ6b@9Pm65~mJ!(8DSC`-QMMOS#b>r8h7W9|6{MaqV*P3OO4!%b^w7tbA>d$%CB_@q-RaGCB zx|i?C;fir&+Z4>c?-r#*Z5&Ss*Wj5W;7TeqwZFV?dzhjPcTHUAqdz%M&HRKdhD#+o zum=+1sh8wUa1%A+rR-_u<67@0=u+-<0>@e15&=Gf`;X8p=@o)^M9Err#`RkD(R-{@G`G z_;!yXt|V{1fPjnjf$SKT^hgpWXm~$_yS|rBp+8ii1g2=JPMRCSxPlNc*Qp%TVGC`$ zI**f^k|332W<&}Wzbb?qjgisti2yHe+m>)bCD)wFNUgY6^i90_>f8R-uOA&93+_sT z8&++b*qhXb*aSWJv5{WBix{juXM*3jNtF?tH2K)kwaxR$~ zQ!88WiDEinNHpV&ST=S1VxytQyM5@1CokF0S~H*V8r3y20<%d^2{2 z(-}uppy+eAdJu3>RkXjxeda;hpTFHsW+<7~+}SOicHVa3t?hmp)N$gvUGCW)WFy#$ zeYNAj)vz^;$#L`hL4@%S)Sejx4B~~S(q+9W7Mfk<3@0&kdZ^9l`b< ze_6BF(VbIXAJ*SWth*7RTZdgesQ@Omx7|s)6G=pR`F3;@FXjwAcVui>zFgVRscYJ` zp`W+yysmo11?-^rQoC2mRAflvJ##-7*WN=YC_fIWVr*+!g{RY+FD5{RO&6cmUR;t& zL_kBvI08~%FC8C60F5Zl`=h&u1eGL<-Hp~uPT~9C1?O&>rCx0OMha=cM`~AL#{?{Q z=c9uzrrYJ4O9$Wn-Yy@1S;yUD{y{wpg!}VO@iVD&bvqBJV7l{#$HQYHU3ZrWditgk zb#bYTdY*qbe2p_j5B9&4UQB&oVd7{@_$C*qu(*3=tA#1OJ~t;)gOTfID_DOTI0!o@ Sf>|EX*ubW1mH1^MAN~bzrZ3q5 literal 0 HcmV?d00001 diff --git a/sites/docs/src/tutorial/advanced/node/htmlNode/index.less b/sites/docs/src/tutorial/advanced/node/htmlNode/index.less index 1beb59996..81933cb3a 100644 --- a/sites/docs/src/tutorial/advanced/node/htmlNode/index.less +++ b/sites/docs/src/tutorial/advanced/node/htmlNode/index.less @@ -2,8 +2,8 @@ box-sizing: border-box; width: 100%; height: 100%; - background: #68fce2; - border: 2px solid #838382; + background: #efdbff; + border: 2px solid #9254de; border-radius: 10px; } diff --git a/sites/docs/src/tutorial/extension/snapshot/data.ts b/sites/docs/src/tutorial/extension/snapshot/data.ts index 882d9daed..1e953ac5f 100644 --- a/sites/docs/src/tutorial/extension/snapshot/data.ts +++ b/sites/docs/src/tutorial/extension/snapshot/data.ts @@ -7,7 +7,7 @@ export default { id: 'uml_1', properties: { name: 'hello', - body: '哈哈哈哈', + body: '哈哈哈哈111', }, }, { @@ -38,6 +38,24 @@ export default { y: 100, text: '菱形', }, + { + id: 'test_image_1', + type: 'test-image', + x: 300, + y: 200, + text: { + x: 420, + y: 200, + value: '通过相对路径引入的图片\n实际这张图片放在了\n项目的public目录下', + }, + properties: { + width: 82, + height: 96, + textStyle: { + textAlign: 'left', + }, + }, + }, ], edges: [ { diff --git a/sites/docs/src/tutorial/extension/snapshot/imageNode.ts b/sites/docs/src/tutorial/extension/snapshot/imageNode.ts index bd818328c..85ca5aebf 100644 --- a/sites/docs/src/tutorial/extension/snapshot/imageNode.ts +++ b/sites/docs/src/tutorial/extension/snapshot/imageNode.ts @@ -28,6 +28,40 @@ class ImageNode extends RectNode { } } +class TestImageModel extends RectNodeModel { + initNodeData(data: any) { + super.initNodeData(data); + this.width = 100; + this.height = 75; + } +} + +class TestImageNode extends RectNode { + getImageHref() { + console.log('getImageHref'); + return '/test.jpeg'; + } + getShape() { + const { x, y, width, height } = this.props.model; + const href = this.getImageHref(); + const attrs = { + x: x - (1 / 2) * width, + y: y - (1 / 2) * height, + width, + height, + href, + preserveAspectRatio: 'none meet', + }; + return h('g', {}, [h('image', { ...attrs })]); + } +} + +export const testImage = { + type: 'test-image', + view: TestImageNode, + model: TestImageModel, +}; + export default { type: 'image', view: ImageNode, diff --git a/sites/docs/src/tutorial/extension/snapshot/index.tsx b/sites/docs/src/tutorial/extension/snapshot/index.tsx index 4c8cb6d49..dfd5eb6a1 100644 --- a/sites/docs/src/tutorial/extension/snapshot/index.tsx +++ b/sites/docs/src/tutorial/extension/snapshot/index.tsx @@ -15,7 +15,7 @@ import { InputNumber, Switch, } from 'antd'; -import ImageNode from './imageNode'; +import ImageNode, { testImage } from './imageNode'; import CustomHtml from '../../components/nodes/custom-html/Html'; import data from './data'; import { circle as circleSvgUrl, rect as rectSvgUrl } from './svg'; @@ -89,6 +89,7 @@ export default function SnapshotExample() { }); lf.register(CustomHtml); lf.register(ImageNode); + lf.register(testImage); lf.setPatternItems([ { @@ -103,6 +104,12 @@ export default function SnapshotExample() { text: 'circle', icon: rectSvgUrl, }, + { + type: 'test-image', + label: 'Test Image', + text: 'Test Image', + icon: rectSvgUrl, + }, ]); lf.on('custom:button-click', (model: any) => { @@ -149,8 +156,17 @@ export default function SnapshotExample() { const previewBlob = () => { if (lfRef.current) { setBase64Data(''); + const params: ToImageOptions = { + fileType, + backgroundColor, + partial, + width, + height, + padding, + quality, + }; lfRef.current - .getSnapshotBlob(backgroundColor, fileType) + .getSnapshotBlob(backgroundColor, fileType, params) .then( ({ data, @@ -172,8 +188,17 @@ export default function SnapshotExample() { const previewBase64 = () => { if (lfRef.current) { setBlobData(''); + const params: ToImageOptions = { + fileType, + backgroundColor, + partial, + width, + height, + padding, + quality, + }; lfRef.current - .getSnapshotBase64(backgroundColor) + .getSnapshotBase64(backgroundColor, '', params) .then( ({ data,