diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
index abb17d1eec..4afe023fbb 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -15,7 +15,9 @@ import { addDeleteBoardAndImagesFulfilledListener } from './listeners/boardAndIm
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
+import { addCanvasImageToControlNetListener } from './listeners/canvasImageToControlNet';
import { addCanvasMaskSavedToGalleryListener } from './listeners/canvasMaskSavedToGallery';
+import { addCanvasMaskToControlNetListener } from './listeners/canvasMaskToControlNet';
import { addCanvasMergedListener } from './listeners/canvasMerged';
import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGallery';
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
@@ -41,6 +43,8 @@ import {
addImageUploadedFulfilledListener,
addImageUploadedRejectedListener,
} from './listeners/imageUploaded';
+import { addImagesStarredListener } from './listeners/imagesStarred';
+import { addImagesUnstarredListener } from './listeners/imagesUnstarred';
import { addInitialImageSelectedListener } from './listeners/initialImageSelected';
import { addModelSelectedListener } from './listeners/modelSelected';
import { addModelsLoadedListener } from './listeners/modelsLoaded';
@@ -80,8 +84,6 @@ import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage';
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
-import { addImagesStarredListener } from './listeners/imagesStarred';
-import { addImagesUnstarredListener } from './listeners/imagesUnstarred';
export const listenerMiddleware = createListenerMiddleware();
@@ -137,6 +139,8 @@ addSessionReadyToInvokeListener();
// Canvas actions
addCanvasSavedToGalleryListener();
addCanvasMaskSavedToGalleryListener();
+addCanvasImageToControlNetListener();
+addCanvasMaskToControlNetListener();
addCanvasDownloadedAsImageListener();
addCanvasCopiedToClipboardListener();
addCanvasMergedListener();
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts
new file mode 100644
index 0000000000..fb411a6e25
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts
@@ -0,0 +1,58 @@
+import { logger } from 'app/logging/logger';
+import { canvasImageToControlNet } from 'features/canvas/store/actions';
+import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
+import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
+import { addToast } from 'features/system/store/systemSlice';
+import { imagesApi } from 'services/api/endpoints/images';
+import { startAppListening } from '..';
+
+export const addCanvasImageToControlNetListener = () => {
+ startAppListening({
+ actionCreator: canvasImageToControlNet,
+ effect: async (action, { dispatch, getState }) => {
+ const log = logger('canvas');
+ const state = getState();
+
+ const blob = await getBaseLayerBlob(state);
+
+ if (!blob) {
+ log.error('Problem getting base layer blob');
+ dispatch(
+ addToast({
+ title: 'Problem Saving Canvas',
+ description: 'Unable to export base layer',
+ status: 'error',
+ })
+ );
+ return;
+ }
+
+ const { autoAddBoardId } = state.gallery;
+
+ const imageDTO = await dispatch(
+ imagesApi.endpoints.uploadImage.initiate({
+ file: new File([blob], 'savedCanvas.png', {
+ type: 'image/png',
+ }),
+ image_category: 'mask',
+ is_intermediate: false,
+ board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
+ crop_visible: true,
+ postUploadAction: {
+ type: 'TOAST',
+ toastOptions: { title: 'Canvas Sent to ControlNet & Assets' },
+ },
+ })
+ ).unwrap();
+
+ const { image_name } = imageDTO;
+
+ dispatch(
+ controlNetImageChanged({
+ controlNetId: action.payload.controlNet.controlNetId,
+ controlImage: image_name,
+ })
+ );
+ },
+ });
+};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts
new file mode 100644
index 0000000000..6c97259f02
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts
@@ -0,0 +1,70 @@
+import { logger } from 'app/logging/logger';
+import { canvasMaskToControlNet } from 'features/canvas/store/actions';
+import { getCanvasData } from 'features/canvas/util/getCanvasData';
+import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
+import { addToast } from 'features/system/store/systemSlice';
+import { imagesApi } from 'services/api/endpoints/images';
+import { startAppListening } from '..';
+
+export const addCanvasMaskToControlNetListener = () => {
+ startAppListening({
+ actionCreator: canvasMaskToControlNet,
+ effect: async (action, { dispatch, getState }) => {
+ const log = logger('canvas');
+ const state = getState();
+
+ const canvasBlobsAndImageData = await getCanvasData(
+ state.canvas.layerState,
+ state.canvas.boundingBoxCoordinates,
+ state.canvas.boundingBoxDimensions,
+ state.canvas.isMaskEnabled,
+ state.canvas.shouldPreserveMaskedArea
+ );
+
+ if (!canvasBlobsAndImageData) {
+ return;
+ }
+
+ const { maskBlob } = canvasBlobsAndImageData;
+
+ if (!maskBlob) {
+ log.error('Problem getting mask layer blob');
+ dispatch(
+ addToast({
+ title: 'Problem Importing Mask',
+ description: 'Unable to export mask',
+ status: 'error',
+ })
+ );
+ return;
+ }
+
+ const { autoAddBoardId } = state.gallery;
+
+ const imageDTO = await dispatch(
+ imagesApi.endpoints.uploadImage.initiate({
+ file: new File([maskBlob], 'canvasMaskImage.png', {
+ type: 'image/png',
+ }),
+ image_category: 'mask',
+ is_intermediate: false,
+ board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
+ crop_visible: true,
+ postUploadAction: {
+ type: 'TOAST',
+ toastOptions: { title: 'Mask Sent to ControlNet & Assets' },
+ },
+ })
+ ).unwrap();
+
+ const { image_name } = imageDTO;
+
+ dispatch(
+ controlNetImageChanged({
+ controlNetId: action.payload.controlNet.controlNetId,
+ controlImage: image_name,
+ })
+ );
+ },
+ });
+};
diff --git a/invokeai/frontend/web/src/features/canvas/store/actions.ts b/invokeai/frontend/web/src/features/canvas/store/actions.ts
index b4efa76e42..85e0a7b406 100644
--- a/invokeai/frontend/web/src/features/canvas/store/actions.ts
+++ b/invokeai/frontend/web/src/features/canvas/store/actions.ts
@@ -1,4 +1,5 @@
import { createAction } from '@reduxjs/toolkit';
+import { ControlNetConfig } from 'features/controlNet/store/controlNetSlice';
import { ImageDTO } from 'services/api/types';
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
@@ -20,3 +21,11 @@ export const canvasMerged = createAction('canvas/canvasMerged');
export const stagingAreaImageSaved = createAction<{ imageDTO: ImageDTO }>(
'canvas/stagingAreaImageSaved'
);
+
+export const canvasMaskToControlNet = createAction<{
+ controlNet: ControlNetConfig;
+}>('canvas/canvasMaskToControlNet');
+
+export const canvasImageToControlNet = createAction<{
+ controlNet: ControlNetConfig;
+}>('canvas/canvasImageToControlNet');
diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx
index de9995c577..1f70542494 100644
--- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx
+++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx
@@ -17,11 +17,13 @@ import { stateSelector } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIIconButton from 'common/components/IAIIconButton';
import IAISwitch from 'common/components/IAISwitch';
+import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useToggle } from 'react-use';
import { v4 as uuidv4 } from 'uuid';
import ControlNetImagePreview from './ControlNetImagePreview';
import ControlNetProcessorComponent from './ControlNetProcessorComponent';
import ParamControlNetShouldAutoConfig from './ParamControlNetShouldAutoConfig';
+import ControlNetCanvasImageImports from './imports/ControlNetCanvasImageImports';
import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd';
import ParamControlNetControlMode from './parameters/ParamControlNetControlMode';
import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect';
@@ -36,6 +38,8 @@ const ControlNet = (props: ControlNetProps) => {
const { controlNetId } = controlNet;
const dispatch = useAppDispatch();
+ const activeTabName = useAppSelector(activeTabNameSelector);
+
const selector = createSelector(
stateSelector,
({ controlNet }) => {
@@ -108,6 +112,9 @@ const ControlNet = (props: ControlNetProps) => {
>
+ {activeTabName === 'unifiedCanvas' && (
+
+ )}
{
/>
)}
+
{
+ ({ controlNet, gallery }) => {
const { pendingControlImages } = controlNet;
+ const { autoAddBoardId } = gallery;
return {
pendingControlImages,
+ autoAddBoardId,
};
},
defaultSelectorOptions
@@ -47,7 +53,7 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
const dispatch = useAppDispatch();
- const { pendingControlImages } = useAppSelector(selector);
+ const { pendingControlImages, autoAddBoardId } = useAppSelector(selector);
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
@@ -59,9 +65,26 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
processedControlImageName ?? skipToken
);
+ const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
+ const [addToBoard] = useAddImageToBoardMutation();
+
const handleResetControlImage = useCallback(() => {
dispatch(controlNetImageChanged({ controlNetId, controlImage: null }));
}, [controlNetId, dispatch]);
+
+ const handleSaveControlImage = useCallback(() => {
+ if (!processedControlImage) {
+ return;
+ }
+
+ changeIsIntermediate({
+ imageDTO: processedControlImage,
+ is_intermediate: false,
+ });
+
+ addToBoard({ imageDTO: processedControlImage, board_id: autoAddBoardId });
+ }, [processedControlImage, autoAddBoardId, changeIsIntermediate, addToBoard]);
+
const handleMouseEnter = useCallback(() => {
setIsMouseOverImage(true);
}, []);
@@ -122,11 +145,19 @@ const ControlNetImagePreview = ({ isSmall, controlNet }: Props) => {
isDropDisabled={shouldShowProcessedImage || !isEnabled}
postUploadAction={postUploadAction}
>
- : undefined}
- tooltip="Reset Control Image"
- />
+ <>
+ : undefined}
+ tooltip="Reset Control Image"
+ />
+ : undefined}
+ tooltip="Save Control Image"
+ styleOverrides={{ marginTop: 6 }}
+ />
+ >
{
+ const { controlNet } = props;
+ const dispatch = useAppDispatch();
+
+ const handleImportImageFromCanvas = useCallback(() => {
+ dispatch(canvasImageToControlNet({ controlNet }));
+ }, [controlNet, dispatch]);
+
+ const handleImportMaskFromCanvas = useCallback(() => {
+ dispatch(canvasMaskToControlNet({ controlNet }));
+ }, [controlNet, dispatch]);
+
+ return (
+
+ }
+ tooltip="Import Image From Canvas"
+ aria-label="Import Image From Canvas"
+ onClick={handleImportImageFromCanvas}
+ />
+ }
+ tooltip="Import Mask From Canvas"
+ aria-label="Import Mask From Canvas"
+ onClick={handleImportMaskFromCanvas}
+ />
+
+ );
+};
+
+export default memo(ControlNetCanvasImageImports);