diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index f0ac55905e..4ed56fe1f1 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1081,6 +1081,7 @@
"importFailed": "Import Failed",
"importSuccessful": "Import Successful",
"invalidUpload": "Invalid Upload",
+ "layerCopiedToClipboard": "Layer Copied to Clipboard",
"loadedWithWarnings": "Workflow Loaded with Warnings",
"maskSavedAssets": "Mask Saved to Assets",
"maskSentControlnetAssets": "Mask Sent to ControlNet & Assets",
@@ -1101,6 +1102,7 @@
"problemCopyingCanvas": "Problem Copying Canvas",
"problemCopyingCanvasDesc": "Unable to export base layer",
"problemCopyingImage": "Unable to Copy Image",
+ "problemCopyingLayer": "Unable to Copy Layer",
"problemDownloadingImage": "Unable to Download Image",
"problemDownloadingCanvas": "Problem Downloading Canvas",
"problemDownloadingCanvasDesc": "Unable to export base layer",
@@ -1669,6 +1671,7 @@
"sendToGallery": "Send To Gallery",
"sendToGalleryDesc": "Pressing Invoke generates and saves a unique image to your gallery.",
"sendToCanvas": "Send To Canvas",
+ "copyToClipboard": "Copy to Clipboard",
"sendToCanvasDesc": "Pressing Invoke stages your work in progress on the canvas.",
"viewProgressInViewer": "View progress and outputs in the Image Viewer.",
"viewProgressOnCanvas": "View progress and stage outputs on the Canvas.",
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx
index 19c673fd69..4369a9925a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx
@@ -1,5 +1,6 @@
import { MenuGroup } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
+import { CanvasEntityMenuItemsCopy } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopy';
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
@@ -20,6 +21,7 @@ const CanvasContextMenuSelectedEntityMenuItemsContent = memo(() => {
{isFilterableEntityIdentifier(entityIdentifier) && }
{isTransformableEntityIdentifier(entityIdentifier) && }
+
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityCopyToClipboard.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityCopyToClipboard.tsx
new file mode 100644
index 0000000000..1d4fc22fdd
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityCopyToClipboard.tsx
@@ -0,0 +1,34 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
+import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
+import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiCopyBold } from 'react-icons/pi';
+
+export const CanvasEntityCopyToClipboard = memo(() => {
+ const { t } = useTranslation();
+ const entityIdentifier = useEntityIdentifierContext();
+ const adapter = useEntityAdapterSafe(entityIdentifier);
+ const isBusy = useCanvasIsBusy();
+ const copyLayerToClipboard = useCopyLayerToClipboard();
+ const onClick = useCallback(() => {
+ copyLayerToClipboard(adapter);
+ }, [copyLayerToClipboard, adapter]);
+
+ return (
+ }
+ onClick={onClick}
+ isDisabled={isBusy}
+ />
+ );
+});
+
+CanvasEntityCopyToClipboard.displayName = 'CanvasEntityCopyToClipboard';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx
index cc89cb02f6..fd4771b895 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx
@@ -6,11 +6,14 @@ import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/co
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { memo } from 'react';
+import { CanvasEntityCopyToClipboard } from './CanvasEntityCopyToClipboard';
+
export const CanvasEntityHeaderCommonActions = memo(() => {
const entityIdentifier = useEntityIdentifierContext();
return (
+
{entityIdentifier.type !== 'reference_image' && }
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsCopy.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsCopy.tsx
new file mode 100644
index 0000000000..040c439d87
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsCopy.tsx
@@ -0,0 +1,28 @@
+import { MenuItem } from '@invoke-ai/ui-library';
+import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
+import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard';
+import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiCopyBold } from 'react-icons/pi';
+
+export const CanvasEntityMenuItemsCopy = memo(() => {
+ const { t } = useTranslation();
+ const entityIdentifier = useEntityIdentifierContext();
+ const adapter = useEntityAdapterSafe(entityIdentifier);
+ const isInteractable = useIsEntityInteractable(entityIdentifier);
+ const copyLayerToClipboard = useCopyLayerToClipboard();
+
+ const onClick = useCallback(() => {
+ copyLayerToClipboard(adapter);
+ }, [copyLayerToClipboard, adapter]);
+
+ return (
+ } isDisabled={!isInteractable}>
+ {t('controlLayers.copyToClipboard')}
+
+ );
+});
+
+CanvasEntityMenuItemsCopy.displayName = 'CanvasEntityMenuItemsCopy';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCopyLayerToClipboard.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCopyLayerToClipboard.ts
new file mode 100644
index 0000000000..9221c3e2cb
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCopyLayerToClipboard.ts
@@ -0,0 +1,44 @@
+import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
+import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
+import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
+import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance';
+import { canvasToBlob } from 'features/controlLayers/konva/util';
+import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
+import { toast } from 'features/toast/toast';
+import { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+
+export const useCopyLayerToClipboard = () => {
+ const { t } = useTranslation();
+ const copyLayerToCipboard = useCallback(
+ async (
+ adapter:
+ | CanvasEntityAdapterRasterLayer
+ | CanvasEntityAdapterControlLayer
+ | CanvasEntityAdapterInpaintMask
+ | CanvasEntityAdapterRegionalGuidance
+ | null
+ ) => {
+ if (!adapter) {
+ return;
+ }
+ try {
+ const canvas = adapter.getCanvas();
+ const blob = await canvasToBlob(canvas);
+ copyBlobToClipboard(blob);
+ toast({
+ status: 'info',
+ title: t('toast.layerCopiedToClipboard'),
+ });
+ } catch (error) {
+ toast({
+ status: 'error',
+ title: t('toast.problemCopyingLayer'),
+ });
+ }
+ },
+ [t]
+ );
+
+ return copyLayerToCipboard;
+};
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index a6990a910a..944d8592e2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -299,11 +299,11 @@ export type EntityIdentifierPayload<
U extends CanvasEntityType = CanvasEntityType,
> = T extends void
? {
- entityIdentifier: CanvasEntityIdentifier;
- }
+ entityIdentifier: CanvasEntityIdentifier;
+ }
: {
- entityIdentifier: CanvasEntityIdentifier;
- } & T;
+ entityIdentifier: CanvasEntityIdentifier;
+ } & T;
export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>;
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{ brushLine: CanvasBrushLineState }>;