From bd028acdae678e38aa83ce03424e21f43f81c95c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:56:54 +1000 Subject: [PATCH] feat(ui): support embedding metadata when rasterizing composite layer - Allow `uploadImage` util to accept `metadata` to embed in the image - Update compositor to support `metadata` field when uploading rasterized composite layer --- .../common/CanvasEntityMergeVisibleButton.tsx | 2 +- .../controlLayers/hooks/saveCanvasHooks.ts | 23 ++++++++++++++++--- .../konva/CanvasCompositorModule.ts | 15 ++++++++---- .../web/src/services/api/endpoints/images.ts | 17 ++++++++++---- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx index 7528f2c4a8..1457cef836 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMergeVisibleButton.tsx @@ -30,7 +30,7 @@ export const CanvasEntityMergeVisibleButton = memo(({ type }: Props) => { if (type === 'raster_layer') { const rect = canvasManager.stage.getVisibleRect('raster_layer'); const result = await withResultAsync(() => - canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, false) + canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, { is_intermediate: true }) ); if (result.isOk()) { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts index 096d090a77..d2502a7c84 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/saveCanvasHooks.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; +import type { SerializableObject } from 'common/types'; import { deepClone } from 'common/util/deepClone'; import { withResultAsync } from 'common/util/result'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; @@ -14,6 +15,7 @@ import { rgAdded, rgIPAdapterImageChanged, } from 'features/controlLayers/store/canvasSlice'; +import { selectCanvasMetadata } from 'features/controlLayers/store/selectors'; import type { CanvasControlLayerState, CanvasEntityIdentifier, @@ -38,10 +40,12 @@ type UseSaveCanvasArg = { toastOk: string; toastError: string; onSave?: (imageDTO: ImageDTO, rect: Rect) => void; + withMetadata?: boolean; }; -const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave }: UseSaveCanvasArg) => { +const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave, withMetadata }: UseSaveCanvasArg) => { const { t } = useTranslation(); + const store = useAppStore(); const canvasManager = useCanvasManager(); @@ -58,8 +62,17 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave }: U return; } + let metadata: SerializableObject | undefined = undefined; + + if (withMetadata) { + metadata = selectCanvasMetadata(store.getState()); + } + const result = await withResultAsync(() => - canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, saveToGallery) + canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, { + is_intermediate: !saveToGallery, + metadata, + }) ); if (result.isOk()) { @@ -78,9 +91,11 @@ const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave }: U onSave, region, saveToGallery, + store, t, toastError, toastOk, + withMetadata, ]); return saveCanvas; @@ -94,6 +109,7 @@ export const useSaveCanvasToGallery = () => { saveToGallery: true, toastOk: t('controlLayers.savedToGalleryOk'), toastError: t('controlLayers.savedToGalleryError'), + withMetadata: true, }), [t] ); @@ -109,6 +125,7 @@ export const useSaveBboxToGallery = () => { saveToGallery: true, toastOk: t('controlLayers.savedToGalleryOk'), toastError: t('controlLayers.savedToGalleryError'), + withMetadata: true, }), [t] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index 5e4ed2dd49..cc457a4f15 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -13,6 +13,7 @@ import type { GenerationMode, Rect } from 'features/controlLayers/store/types'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; import { atom, computed } from 'nanostores'; import type { Logger } from 'roarr'; +import type { UploadOptions } from 'services/api/endpoints/images'; import { getImageDTO, uploadImage } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import stableHash from 'stable-hash'; @@ -149,10 +150,13 @@ export class CanvasCompositorModule extends CanvasModuleBase { * If the hash of the composite raster layer is found in the cache, the cached image DTO is returned. * * @param rect The region to include in the rasterized image - * @param saveToGallery Whether to save the image to the gallery or just return the uploaded image DTO + * @param options Options for uploading the image * @returns A promise that resolves to the uploaded image DTO */ - rasterizeAndUploadCompositeRasterLayer = async (rect: Rect, saveToGallery: boolean): Promise => { + rasterizeAndUploadCompositeRasterLayer = async ( + rect: Rect, + options: Pick + ): Promise => { this.log.trace({ rect }, 'Rasterizing composite raster layer'); assert(rect.width > 0 && rect.height > 0, 'Unable to rasterize empty rect'); @@ -178,8 +182,9 @@ export class CanvasCompositorModule extends CanvasModuleBase { blob, fileName: 'composite-raster-layer.png', image_category: 'general', - is_intermediate: !saveToGallery, - board_id: saveToGallery ? selectAutoAddBoardId(this.manager.store.getState()) : undefined, + is_intermediate: options.is_intermediate, + board_id: options.is_intermediate ? undefined : selectAutoAddBoardId(this.manager.store.getState()), + metadata: options.metadata, }) ); this.$isUploading.set(false); @@ -212,7 +217,7 @@ export class CanvasCompositorModule extends CanvasModuleBase { } } - imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, false); + imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, { is_intermediate: true }); this.manager.cache.imageNameCache.set(hash, imageDTO.image_name); return imageDTO; }; diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 5c39e2ee49..90c076f283 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -587,20 +587,29 @@ export const getImageDTO = async (image_name: string, forceRefetch?: boolean): P } }; -export const uploadImage = async (arg: { +export type UploadOptions = { blob: Blob; fileName: string; image_category: ImageCategory; is_intermediate: boolean; crop_visible?: boolean; board_id?: BoardId; -}): Promise => { - const { blob, fileName, image_category, is_intermediate, crop_visible = false, board_id } = arg; + metadata?: SerializableObject; +}; +export const uploadImage = async (arg: UploadOptions): Promise => { + const { blob, fileName, image_category, is_intermediate, crop_visible = false, board_id, metadata } = arg; const { dispatch } = getStore(); const file = new File([blob], fileName, { type: 'image/png' }); const req = dispatch( - imagesApi.endpoints.uploadImage.initiate({ file, image_category, is_intermediate, crop_visible, board_id }) + imagesApi.endpoints.uploadImage.initiate({ + file, + image_category, + is_intermediate, + crop_visible, + board_id, + metadata, + }) ); req.reset(); return await req.unwrap();