From aae8c84bbfea32ba0a8cf5d87cc1173981a86654 Mon Sep 17 00:00:00 2001 From: Guillaume Coutable Date: Thu, 16 Jan 2025 15:48:42 +0100 Subject: [PATCH] [4430] Add the PNG export of a diagram Bug: https://github.com/eclipse-sirius/sirius-web/issues/4430 Signed-off-by: Guillaume Coutable --- CHANGELOG.adoc | 1 + .../src/renderer/panel/DiagramPanel.tsx | 44 ++++++++++++++--- .../src/renderer/panel/useExportToImage.tsx | 49 ++++++++++++++++--- .../renderer/panel/useExportToImage.types.ts | 5 +- 4 files changed, 81 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index faff5982d9..8bd0b265c7 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -64,6 +64,7 @@ Some log messages have been updated in order to provide more information and mak The configuration property `sirius.web.graphql.tracing` has also been added to active the tracing mode of the GraphQL API. It can be activated using `sirius.web.graphql.tracing=true` since it is not enabled by default to not have any impact on the performance of the application. Some additional log has also been contributed on the frontend in order to view more easily the order and time of the GraphQL requests and responses. +- https://github.com/eclipse-sirius/sirius-web/issues/4430[#4430] [diagram] Add the PNG export of a diagram. === Improvements diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/DiagramPanel.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/DiagramPanel.tsx index 8c3e555115..633a73b770 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/DiagramPanel.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/DiagramPanel.tsx @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023, 2024 Obeo. + * Copyright (c) 2023, 2025 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -19,21 +19,26 @@ import FullscreenExitIcon from '@mui/icons-material/FullscreenExit'; import GridOffIcon from '@mui/icons-material/GridOff'; import GridOnIcon from '@mui/icons-material/GridOn'; import ImageIcon from '@mui/icons-material/Image'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import ShareIcon from '@mui/icons-material/Share'; import TonalityIcon from '@mui/icons-material/Tonality'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import ZoomInIcon from '@mui/icons-material/ZoomIn'; import ZoomOutIcon from '@mui/icons-material/ZoomOut'; +import { ListItemText, MenuItem } from '@mui/material'; import CircularProgress from '@mui/material/CircularProgress'; import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; import Paper from '@mui/material/Paper'; import Tooltip from '@mui/material/Tooltip'; import { Edge, Node, Panel, useNodesInitialized, useReactFlow } from '@xyflow/react'; -import { memo, useContext, useEffect, useState } from 'react'; +import { memo, useContext, useEffect, useRef, useState } from 'react'; import { DiagramContext } from '../../contexts/DiagramContext'; import { DiagramContextValue } from '../../contexts/DiagramContext.types'; import { HelperLinesIcon } from '../../icons/HelperLinesIcon'; import { HelperLinesIconOff } from '../../icons/HelperLinesIconOff'; +import { SmartEdgeIcon } from '../../icons/SmartEdgeIcon'; +import { SmoothStepEdgeIcon } from '../../icons/SmoothStepEdgeIcon'; import { UnpinIcon } from '../../icons/UnpinIcon'; import { EdgeData, NodeData } from '../DiagramRenderer.types'; import { useFadeDiagramElements } from '../fade/useFadeDiagramElements'; @@ -44,8 +49,6 @@ import { usePinDiagramElements } from '../pin/usePinDiagramElements'; import { DiagramPanelActionProps, DiagramPanelProps, DiagramPanelState } from './DiagramPanel.types'; import { diagramPanelActionExtensionPoint } from './DiagramPanelExtensionPoints'; import { useExportToImage } from './useExportToImage'; -import { SmartEdgeIcon } from '../../icons/SmartEdgeIcon'; -import { SmoothStepEdgeIcon } from '../../icons/SmoothStepEdgeIcon'; export const DiagramPanel = memo( ({ @@ -103,7 +106,17 @@ export const DiagramPanel = memo( const onUnhideAll = () => hideDiagramElements([...getAllElementsIds()], false); const onUnpinAll = () => pinDiagramElements([...getAllElementsIds()], false); - const { exportToImage } = useExportToImage(); + const [exportImageMenuOpen, setExportImageMenuOpen] = useState(false); + const anchorExportImageMenuRef = useRef(null); + const handleExportImageMenuToggle = () => { + setExportImageMenuOpen((prevState) => !prevState); + }; + + const onCloseExportImageMenu = () => { + setExportImageMenuOpen(false); + }; + + const { exportToSVG, exportToPNG } = useExportToImage(); const { editingContextId, diagramId } = useContext(DiagramContext); return ( @@ -150,10 +163,12 @@ export const DiagramPanel = memo( + aria-label="export to image" + onClick={handleExportImageMenuToggle} + data-testid="export-diagram-to-image" + ref={anchorExportImageMenuRef}> + {snapToGrid ? ( @@ -287,6 +302,19 @@ export const DiagramPanel = memo( onClose={handleCloseDialog} /> ) : null} + + + + + + + + ); } diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/useExportToImage.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/useExportToImage.tsx index 7f093aa928..4864d29b72 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/useExportToImage.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/useExportToImage.tsx @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023, 2024 Obeo. + * Copyright (c) 2023, 2025 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -20,14 +20,14 @@ import { getViewportForBounds, useReactFlow, } from '@xyflow/react'; -import { toSvg } from 'html-to-image'; +import { toPng, toSvg } from 'html-to-image'; import { useCallback } from 'react'; import { EdgeData, NodeData } from '../DiagramRenderer.types'; import { UseExportToImage } from './useExportToImage.types'; -const downloadImage = (dataUrl: string) => { +const downloadImage = (dataUrl: string, fileName: string) => { const a: HTMLAnchorElement = document.createElement('a'); - a.setAttribute('download', 'diagram.svg'); + a.setAttribute('download', fileName); a.setAttribute('href', dataUrl); a.click(); }; @@ -35,12 +35,12 @@ const downloadImage = (dataUrl: string) => { export const useExportToImage = (): UseExportToImage => { const reactFlow = useReactFlow, Edge>(); - const exportToImage = useCallback(() => { + const exportToSVG = useCallback(() => { const nodesBounds: Rect = getNodesBounds(reactFlow.getNodes()); const imageWidth: number = nodesBounds.width; const imageHeight: number = nodesBounds.height; - const viewport: Viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2, 2); + const viewport: Viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2, 0.05); const edges: HTMLElement | null = document.querySelector('.react-flow__edges'); const edgeMarkersDefs: HTMLElement | null = document.getElementById('edge-markers-defs'); @@ -61,9 +61,42 @@ export const useExportToImage = (): UseExportToImage => { transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, }, }) - .then(downloadImage) + .then((dataUrl) => downloadImage(dataUrl, 'diagram.svg')) .finally(() => edges.removeChild(clonedEdgeMarkersDefs)); } }, []); - return { exportToImage }; + + const exportToPNG = useCallback(() => { + const nodesBounds: Rect = getNodesBounds(reactFlow.getNodes()); + const imageWidth: number = nodesBounds.width; + const imageHeight: number = nodesBounds.height; + + const viewport: Viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2, 0.05); + + const edges: HTMLElement | null = document.querySelector('.react-flow__edges'); + const edgeMarkersDefs: HTMLElement | null = document.getElementById('edge-markers-defs'); + + const reactFlowNodeContainer: HTMLElement | null = document.querySelector('.react-flow__viewport'); + + if (reactFlowNodeContainer && edges && edgeMarkersDefs) { + const clonedEdgeMarkersDefs: Node = edgeMarkersDefs.cloneNode(true); + edges.insertBefore(clonedEdgeMarkersDefs, edges.firstChild); + + toPng(reactFlowNodeContainer, { + backgroundColor: '#ffffff', + width: imageWidth, + height: imageHeight, + style: { + width: imageWidth.toString(), + height: imageHeight.toString(), + transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, + }, + pixelRatio: 2, + }) + .then((dataUrl) => downloadImage(dataUrl, 'diagram.png')) + .finally(() => edges.removeChild(clonedEdgeMarkersDefs)); + } + }, []); + + return { exportToSVG, exportToPNG }; }; diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/useExportToImage.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/useExportToImage.types.ts index 353a57dfb7..42248c6b7f 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/useExportToImage.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/panel/useExportToImage.types.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023, 2024 Obeo. + * Copyright (c) 2023, 2025 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -12,5 +12,6 @@ *******************************************************************************/ export interface UseExportToImage { - exportToImage: () => void; + exportToSVG: () => void; + exportToPNG: () => void; }