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 <dan.rukas@gmail.com>
This commit is contained in:
Bill Wallace 2026-02-18 19:03:17 -05:00 committed by GitHub
parent c85ca0e177
commit 53c67f3612
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 73 additions and 5 deletions

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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'; import { useTranslation } from 'react-i18next';
const MAX_TEXTURE_SIZE = 10000; const MAX_TEXTURE_SIZE = 10000;
const DEFAULT_FILENAME = 'image'; const DEFAULT_FILENAME = 'image';
@ -16,6 +16,7 @@ interface ViewportDownloadFormNewProps {
onEnableViewport: (element: HTMLElement) => void; onEnableViewport: (element: HTMLElement) => void;
onDisableViewport: () => void; onDisableViewport: () => void;
onDownload: (filename: string, fileType: string) => void; onDownload: (filename: string, fileType: string) => void;
onCopyToClipboard: () => void;
warningState: { enabled: boolean; value: string }; warningState: { enabled: boolean; value: string };
} }
@ -32,6 +33,7 @@ function ViewportDownloadFormNew({
onEnableViewport, onEnableViewport,
onDisableViewport, onDisableViewport,
onDownload, onDownload,
onCopyToClipboard,
}: ViewportDownloadFormNewProps) { }: ViewportDownloadFormNewProps) {
const [viewportElement, setViewportElement] = useState<HTMLElement | null>(null); const [viewportElement, setViewportElement] = useState<HTMLElement | null>(null);
const [showWarningMessage, setShowWarningMessage] = useState(true); const [showWarningMessage, setShowWarningMessage] = useState(true);
@ -138,13 +140,27 @@ function ViewportDownloadFormNew({
<FooterAction.Secondary onClick={onClose}> <FooterAction.Secondary onClick={onClose}>
{t('Common:Cancel')} {t('Common:Cancel')}
</FooterAction.Secondary> </FooterAction.Secondary>
<FooterAction.Secondary
onClick={async () => {
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')}
</FooterAction.Secondary>
<FooterAction.Primary <FooterAction.Primary
onClick={() => { onClick={() => {
onDownload(filename || DEFAULT_FILENAME, fileType); onDownload(filename || DEFAULT_FILENAME, fileType);
onClose(); onClose();
}} }}
> >
{t('Common:Save')} {t('Save Image')}
</FooterAction.Primary> </FooterAction.Primary>
</FooterAction.Right> </FooterAction.Right>
</FooterAction> </FooterAction>

View File

@ -118,16 +118,20 @@ const CornerstoneViewportDownloadForm = ({
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID); const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
try { try {
const properties = viewport.getProperties();
if (downloadViewport instanceof StackViewport) { if (downloadViewport instanceof StackViewport) {
const imageId = viewport.getCurrentImageId(); const imageId = viewport.getCurrentImageId();
const properties = viewport.getProperties();
await downloadViewport.setStack([imageId]); await downloadViewport.setStack([imageId]);
downloadViewport.setProperties(properties); downloadViewport.setProperties(properties);
} else if (downloadViewport instanceof BaseVolumeViewport) { } else if (downloadViewport instanceof BaseVolumeViewport) {
const volumeIds = viewport.getAllVolumeIds(); 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) { if (segmentationRepresentations?.length) {
segmentationRepresentations.forEach(segRepresentation => { segmentationRepresentations.forEach(segRepresentation => {
@ -230,6 +234,48 @@ const CornerstoneViewportDownloadForm = ({
downloadUrl(canvas.toDataURL(`image/${fileType}`, 1.0), { filename }); 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<Blob>((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( const ViewportDownloadFormNew = customizationService.getCustomization(
'ohif.captureViewportModal' 'ohif.captureViewportModal'
); );
@ -247,6 +293,7 @@ const CornerstoneViewportDownloadForm = ({
onEnableViewport={handleEnableViewport} onEnableViewport={handleEnableViewport}
onDisableViewport={handleDisableViewport} onDisableViewport={handleDisableViewport}
onDownload={handleDownload} onDownload={handleDownload}
onCopyToClipboard={handleCopyToClipboard}
warningState={warningState} warningState={warningState}
/> />
); );

View File

@ -1,9 +1,13 @@
{ {
"Copy to Clipboard": "Copy Image",
"File name": "File name", "File name": "File name",
"Image size": "Image size", "Image size": "Image size",
"Image size in pixels": "Image size in pixels", "Image size in pixels": "Image size in pixels",
"Include annotations": "Include annotations", "Include annotations": "Include annotations",
"Include warning message": "Include warning message", "Include warning message": "Include warning message",
"Width": "Width", "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"
} }

View File

@ -1,5 +1,6 @@
{ {
"Capture": "Capture", "Capture": "Capture",
"Copy to Clipboard": "Copier dans le presse-papiers",
"Cancel": "$t(Common:Cancel)", "Cancel": "$t(Common:Cancel)",
"Download": "$t(Common:Download)", "Download": "$t(Common:Download)",
"Image size": "Taille de l'image", "Image size": "Taille de l'image",