diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 3df56c10ac..41d6dd3572 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1376,6 +1376,7 @@
"problemCopyingCanvasDesc": "Unable to export base layer",
"problemCopyingImage": "Unable to Copy Image",
"problemCopyingImageLink": "Unable to Copy Image Link",
+ "problemDownloadingImage": "Unable to Download Image",
"problemDownloadingCanvas": "Problem Downloading Canvas",
"problemDownloadingCanvasDesc": "Unable to export base layer",
"problemImportingMask": "Problem Importing Mask",
diff --git a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts
new file mode 100644
index 0000000000..5c75549eac
--- /dev/null
+++ b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts
@@ -0,0 +1,43 @@
+import { useAppToaster } from 'app/components/Toaster';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { useImageUrlToBlob } from './useImageUrlToBlob';
+
+export const useDownloadImage = () => {
+ const toaster = useAppToaster();
+ const { t } = useTranslation();
+ const imageUrlToBlob = useImageUrlToBlob();
+
+ const downloadImage = useCallback(
+ async (image_url: string, image_name: string) => {
+ try {
+ const blob = await imageUrlToBlob(image_url);
+
+ if (!blob) {
+ throw new Error('Unable to create Blob');
+ }
+
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.style.display = 'none';
+ a.href = url;
+ a.download = image_name;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ } catch (err) {
+ toaster({
+ title: t('toast.problemDownloadingImage'),
+ description: String(err),
+ status: 'error',
+ duration: 2500,
+ isClosable: true,
+ });
+ }
+ },
+ [t, toaster, imageUrlToBlob]
+ );
+
+ return { downloadImage };
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
index 4a9b9c6e8b..f4d9c7a840 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
@@ -4,6 +4,7 @@ import { useAppToaster } from 'app/components/Toaster';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
+import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
@@ -47,7 +48,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const toaster = useAppToaster();
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const customStarUi = useStore($customStarUI);
-
+ const { downloadImage } = useDownloadImage();
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(imageDTO?.image_name);
const { getAndLoadEmbeddedWorkflow, getAndLoadEmbeddedWorkflowResult } = useGetAndLoadEmbeddedWorkflow({});
@@ -143,6 +144,10 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
}
}, [unstarImages, imageDTO]);
+ const handleDownloadImage = useCallback(() => {
+ downloadImage(imageDTO.image_url, imageDTO.image_name);
+ }, [downloadImage, imageDTO.image_name, imageDTO.image_url]);
+
return (
<>
}>
@@ -153,14 +158,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
{t('parameters.copyImage')}
)}
- }
- w="100%"
- >
+ } onClickCapture={handleDownloadImage}>
{t('parameters.downloadImage')}