From 53c67f361234a0b4160c1b11fafcb143a0723336 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 18 Feb 2026 19:03:17 -0500 Subject: [PATCH] fix: Add copy to clipboard option for capture (#5720) * Add copy to clipboard option for capture * PR comments * Added toast feedback and updated labels --------- Co-authored-by: Dan Rukas --- .../captureViewportModalCustomization.tsx | 20 +++++++- .../utils/CornerstoneViewportDownloadForm.tsx | 51 ++++++++++++++++++- .../locales/en-US/CaptureViewportModal.json | 6 ++- .../src/locales/fr/CaptureViewportModal.json | 1 + 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/extensions/cornerstone/src/customizations/captureViewportModalCustomization.tsx b/extensions/cornerstone/src/customizations/captureViewportModalCustomization.tsx index 2d05a71df..c35c47d19 100644 --- a/extensions/cornerstone/src/customizations/captureViewportModalCustomization.tsx +++ b/extensions/cornerstone/src/customizations/captureViewportModalCustomization.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { ImageModal, FooterAction } from '@ohif/ui-next'; +import { ImageModal, FooterAction, toast } from '@ohif/ui-next'; import { useTranslation } from 'react-i18next'; const MAX_TEXTURE_SIZE = 10000; const DEFAULT_FILENAME = 'image'; @@ -16,6 +16,7 @@ interface ViewportDownloadFormNewProps { onEnableViewport: (element: HTMLElement) => void; onDisableViewport: () => void; onDownload: (filename: string, fileType: string) => void; + onCopyToClipboard: () => void; warningState: { enabled: boolean; value: string }; } @@ -32,6 +33,7 @@ function ViewportDownloadFormNew({ onEnableViewport, onDisableViewport, onDownload, + onCopyToClipboard, }: ViewportDownloadFormNewProps) { const [viewportElement, setViewportElement] = useState(null); const [showWarningMessage, setShowWarningMessage] = useState(true); @@ -138,13 +140,27 @@ function ViewportDownloadFormNew({ {t('Common:Cancel')} + { + try { + await onCopyToClipboard(); + toast.success(t('Image copied to clipboard')); + onClose(); + } catch (error) { + toast.error(t('Failed to copy image to clipboard')); + console.error('Failed to copy to clipboard:', error); + } + }} + > + {t('Copy to Clipboard')} + { onDownload(filename || DEFAULT_FILENAME, fileType); onClose(); }} > - {t('Common:Save')} + {t('Save Image')} diff --git a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx index 81e70c19f..b236910c0 100644 --- a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx +++ b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx @@ -118,16 +118,20 @@ const CornerstoneViewportDownloadForm = ({ const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID); try { + const properties = viewport.getProperties(); if (downloadViewport instanceof StackViewport) { const imageId = viewport.getCurrentImageId(); - const properties = viewport.getProperties(); await downloadViewport.setStack([imageId]); downloadViewport.setProperties(properties); } else if (downloadViewport instanceof BaseVolumeViewport) { const volumeIds = viewport.getAllVolumeIds(); - downloadViewport.setVolumes([{ volumeId: volumeIds[0] }]); + await downloadViewport.setVolumes([{ volumeId: volumeIds[0] }]); } + downloadViewport.setProperties(properties); + const viewRef = viewport.getViewReference(); + downloadViewport.setViewReference(viewRef); + downloadViewport.render(); if (segmentationRepresentations?.length) { segmentationRepresentations.forEach(segRepresentation => { @@ -230,6 +234,48 @@ const CornerstoneViewportDownloadForm = ({ downloadUrl(canvas.toDataURL(`image/${fileType}`, 1.0), { filename }); }; + const handleCopyToClipboard = async () => { + const divForDownloadViewport = document.querySelector( + `div[data-viewport-uid="${VIEWPORT_ID}"]` + ); + + if (!divForDownloadViewport) { + console.debug('No viewport found for copy'); + return; + } + + try { + const canvas = await html2canvas(divForDownloadViewport as HTMLElement); + + // Clipboard API only supports PNG format in most browsers + const blob = await new Promise((resolve, reject) => { + canvas.toBlob( + blob => { + if (blob) { + resolve(blob); + } else { + reject(new Error('Failed to create blob from canvas')); + } + }, + 'image/png', + 1.0 + ); + }); + + // Copy to clipboard using the Clipboard API + await navigator.clipboard.write([ + new ClipboardItem({ + 'image/png': blob, + }), + ]); + + console.log('Image copied to clipboard successfully'); + } catch (error) { + console.error('Failed to copy image to clipboard:', error); + throw error; + } + }; + const ViewportDownloadFormNew = customizationService.getCustomization( 'ohif.captureViewportModal' ); @@ -247,6 +293,7 @@ const CornerstoneViewportDownloadForm = ({ onEnableViewport={handleEnableViewport} onDisableViewport={handleDisableViewport} onDownload={handleDownload} + onCopyToClipboard={handleCopyToClipboard} warningState={warningState} /> ); diff --git a/platform/i18n/src/locales/en-US/CaptureViewportModal.json b/platform/i18n/src/locales/en-US/CaptureViewportModal.json index 1a321517c..f762157b6 100644 --- a/platform/i18n/src/locales/en-US/CaptureViewportModal.json +++ b/platform/i18n/src/locales/en-US/CaptureViewportModal.json @@ -1,9 +1,13 @@ { + "Copy to Clipboard": "Copy Image", "File name": "File name", "Image size": "Image size", "Image size in pixels": "Image size in pixels", "Include annotations": "Include annotations", "Include warning message": "Include warning message", "Width": "Width", - "Height": "Height" + "Height": "Height", + "Save Image": "Save Image", + "Image copied to clipboard": "Image copied to clipboard", + "Failed to copy image to clipboard": "Failed to copy image to clipboard" } diff --git a/platform/i18n/src/locales/fr/CaptureViewportModal.json b/platform/i18n/src/locales/fr/CaptureViewportModal.json index 48e669ae0..27085aec8 100644 --- a/platform/i18n/src/locales/fr/CaptureViewportModal.json +++ b/platform/i18n/src/locales/fr/CaptureViewportModal.json @@ -1,5 +1,6 @@ { "Capture": "Capture", + "Copy to Clipboard": "Copier dans le presse-papiers", "Cancel": "$t(Common:Cancel)", "Download": "$t(Common:Download)", "Image size": "Taille de l'image",