From 48edb6e02301793420fd4340ebab571be5e27bdf Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 30 Aug 2024 21:32:44 +1000
Subject: [PATCH] feat(ui): add save to gallery button
---
invokeai/frontend/web/public/locales/en.json | 4 ++
.../components/ControlLayersToolbar.tsx | 2 +
.../components/SaveToGalleryButton.tsx | 53 +++++++++++++++++++
.../konva/CanvasCompositorModule.ts | 23 ++++----
.../controlLayers/konva/CanvasStageModule.ts | 23 ++++----
5 files changed, 83 insertions(+), 22 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SaveToGalleryButton.tsx
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 7cd0c29fae..d35da423bb 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1654,6 +1654,10 @@
"storeNotInitialized": "Store is not initialized"
},
"controlLayers": {
+ "saveCanvasToGallery": "Save Canvas To Gallery",
+ "saveBboxToGallery": "Save Bbox To Gallery",
+ "savedToGalleryOk": "Saved to Gallery",
+ "savedToGalleryError": "Error saving to gallery",
"clearHistory": "Clear History",
"generateMode": "Generate",
"generateModeDesc": "Create individual images. Generated images are added directly to the gallery.",
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx
index 19ffcc25d7..70c3bc883f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx
@@ -2,6 +2,7 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton';
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
+import { SaveToGalleryButton } from 'features/controlLayers/components/SaveToGalleryButton';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
@@ -27,6 +28,7 @@ export const ControlLayersToolbar = memo(() => {
+
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SaveToGalleryButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SaveToGalleryButton.tsx
new file mode 100644
index 0000000000..afb4fec35a
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SaveToGalleryButton.tsx
@@ -0,0 +1,53 @@
+import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
+import { logger } from 'app/logging/logger';
+import { buildUseBoolean } from 'common/hooks/useBoolean';
+import { isOk, withResultAsync } from 'common/util/result';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { toast } from 'features/toast/toast';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiFloppyDiskBold } from 'react-icons/pi';
+import { serializeError } from 'serialize-error';
+
+const log = logger('canvas');
+
+const [useIsSaving] = buildUseBoolean(false);
+
+export const SaveToGalleryButton = memo(() => {
+ const { t } = useTranslation();
+ const shift = useShiftModifier();
+ const canvasManager = useCanvasManager();
+ const isSaving = useIsSaving();
+
+ const onClick = useCallback(async () => {
+ isSaving.setTrue();
+
+ const rect = shift ? canvasManager.stateApi.getBbox().rect : canvasManager.stage.getVisibleRect();
+
+ const result = await withResultAsync(() =>
+ canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, true)
+ );
+
+ if (isOk(result)) {
+ toast({ title: t('controlLayers.savedToGalleryOk') });
+ } else {
+ log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery');
+ toast({ title: t('controlLayers.savedToGalleryError'), status: 'error' });
+ }
+
+ isSaving.setFalse();
+ }, [canvasManager.compositor, canvasManager.stage, canvasManager.stateApi, isSaving, shift, t]);
+
+ return (
+ }
+ isLoading={isSaving.isTrue}
+ aria-label={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
+ tooltip={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
+ />
+ );
+});
+
+SaveToGalleryButton.displayName = 'SaveToGalleryButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts
index 3302e783f2..a371c2eff0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts
@@ -147,6 +147,19 @@ export class CanvasCompositorModule extends CanvasModuleABC {
return stableHash(data);
};
+ rasterizeAndUploadCompositeRasterLayer = async (rect: Rect, saveToGallery: boolean) => {
+ this.log.trace({ rect }, 'Rasterizing composite raster layer');
+
+ const canvas = this.getCompositeRasterLayerCanvas(rect);
+ const blob = await canvasToBlob(canvas);
+
+ if (this.manager._isDebugging) {
+ previewBlob(blob, 'Composite raster layer canvas');
+ }
+
+ return uploadImage(blob, 'composite-raster-layer.png', 'general', !saveToGallery);
+ };
+
getCompositeRasterLayerImageDTO = async (rect: Rect): Promise => {
let imageDTO: ImageDTO | null = null;
@@ -161,15 +174,7 @@ export class CanvasCompositorModule extends CanvasModuleABC {
}
}
- this.log.trace({ rect }, 'Rasterizing composite raster layer');
-
- const canvas = this.getCompositeRasterLayerCanvas(rect);
- const blob = await canvasToBlob(canvas);
- if (this.manager._isDebugging) {
- previewBlob(blob, 'Composite raster layer canvas');
- }
-
- imageDTO = await uploadImage(blob, 'composite-raster-layer.png', 'general', true);
+ imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, false);
this.manager.cache.imageNameCache.set(hash, imageDTO.image_name);
return imageDTO;
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts
index c8e65d88a0..beb77089c1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts
@@ -85,26 +85,23 @@ export class CanvasStageModule extends CanvasModuleABC {
}
}
- const rectUnion = getRectUnion(...rects);
-
- if (rectUnion.width === 0 || rectUnion.height === 0) {
- // fall back to the bbox if there is no content
- return this.manager.stateApi.getBbox().rect;
- } else {
- return rectUnion;
- }
+ return getRectUnion(...rects);
};
fitBboxToStage = () => {
- this.log.trace('Fitting bbox to stage');
- const bbox = this.manager.stateApi.getBbox();
- this.fitRect(bbox.rect);
+ const { rect } = this.manager.stateApi.getBbox();
+ this.log.trace({ rect }, 'Fitting bbox to stage');
+ this.fitRect(rect);
};
fitLayersToStage() {
- this.log.trace('Fitting layers to stage');
const rect = this.getVisibleRect();
- this.fitRect(rect);
+ if (rect.width === 0 || rect.height === 0) {
+ this.fitBboxToStage();
+ } else {
+ this.log.trace({ rect }, 'Fitting layers to stage');
+ this.fitRect(rect);
+ }
}
fitRect = (rect: Rect) => {