diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts index 60fdd6fb72..78db378a04 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts @@ -245,6 +245,7 @@ export const { refImageIPAdapterBeginEndStepPctChanged, refImageFLUXReduxImageInfluenceChanged, refImageIsEnabledToggled, + refImageRecalled, } = refImagesSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 9bff95d850..344938dbd7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -300,10 +300,9 @@ const zCanvasEntityBase = z.object({ isLocked: z.boolean(), }); -const zRefImageState = z.object({ +export const zRefImageState = z.object({ id: zId, isEnabled: z.boolean().default(true), - // This should be named `referenceImage` but we need to keep it as `ipAdapter` for backwards compatibility config: z.discriminatedUnion('type', [ zIPAdapterConfig, zFLUXReduxConfig, @@ -579,7 +578,7 @@ const zCanvasState = z.object({ }); export type CanvasState = z.infer; -const zRefImagesState = z.object({ +export const zRefImagesState = z.object({ selectedEntityId: z.string().nullable().default(null), isPanelOpen: z.boolean().default(false), entities: z.array(zRefImageState).default(() => []), @@ -594,6 +593,11 @@ export const getInitialRefImagesState = () => deepClone(INITIAL_REF_IMAGES_STATE const CANVAS_INITIAL_STATE = zCanvasState.parse({}); export const getInitialCanvasState = () => deepClone(CANVAS_INITIAL_STATE); +export const zCanvasReferenceImageState_OLD = zCanvasEntityBase.extend({ + type: z.literal('reference_image'), + ipAdapter: z.discriminatedUnion('type', [zIPAdapterConfig, zFLUXReduxConfig, zChatGPT4oReferenceImageConfig]), +}); + export const zCanvasMetadata = z.object({ inpaintMasks: z.array(zCanvasInpaintMaskState), rasterLayers: z.array(zCanvasRasterLayerState), diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index 10fe9d1a61..6e9509c330 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -56,6 +56,7 @@ const ImageMetadataActions = (props: Props) => { + ); diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index 9acecd1023..3e085db300 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -32,7 +32,9 @@ import { shouldConcatPromptsChanged, vaeSelected, } from 'features/controlLayers/store/paramsSlice'; -import { type CanvasMetadata, type LoRA, zCanvasMetadata } from 'features/controlLayers/store/types'; +import { refImageRecalled } from 'features/controlLayers/store/refImagesSlice'; +import type { CanvasMetadata, LoRA, RefImageState } from 'features/controlLayers/store/types'; +import { zCanvasMetadata, zCanvasReferenceImageState_OLD, zRefImageState } from 'features/controlLayers/store/types'; import type { ModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { zModelIdentifier } from 'features/nodes/types/v2/common'; @@ -768,6 +770,50 @@ const CanvasLayers: SingleMetadataHandler = { }; //#endregion CanvasLayers +//#region RefImages +const RefImages: CollectionMetadataHandler = { + [CollectionMetadataKey]: true, + type: 'RefImages', + parse: async (metadata) => { + try { + // First attempt to parse from the v6 slot + const raw = getProperty(metadata, 'ref_images'); + // This validator fetches all referenced images. If any do not exist, validation fails. The logic for this is in + // the zImageWithDims schema. + const parsed = await z.array(zRefImageState).parseAsync(raw); + return Promise.resolve(parsed); + } catch { + // Fall back to extracting from canvas metadata] + const raw = getProperty(metadata, 'canvas_v2_metadata.referenceImages.entities'); + // This validator fetches all referenced images. If any do not exist, validation fails. The logic for this is in + // the zImageWithDims schema. + const oldParsed = await z.array(zCanvasReferenceImageState_OLD).parseAsync(raw); + const parsed: RefImageState[] = oldParsed.map(({ id, ipAdapter, isEnabled }) => ({ + id, + config: ipAdapter, + isEnabled, + })); + return parsed; + } + }, + recall: (value, store) => { + for (const data of value) { + store.dispatch(refImageRecalled({ data: { ...data, id: getPrefixedId('reference_image') } })); + } + }, + recallOne: (data, store) => { + store.dispatch(refImageRecalled({ data: { ...data, id: getPrefixedId('reference_image') } })); + }, + LabelComponent: () => , + ValueComponent: ({ value }: CollectionMetadataValueProps) => { + if (value.config.model) { + return ; + } + return ; + }, +}; +//#endregion RefImages + export const MetadataHandlers = { CreatedBy, GenerationMode, @@ -797,8 +843,8 @@ export const MetadataHandlers = { VAEModel, LoRAs, CanvasLayers, - // TODO: - // Ref images + RefImages, + // TODO: These had parsers in the prev implementation, but they were never actually used? // controlNet: parseControlNet, // controlNets: parseAllControlNets, // t2iAdapter: parseT2IAdapter, @@ -808,9 +854,6 @@ export const MetadataHandlers = { // controlNetToControlLayer: parseControlNetToControlAdapterLayer, // t2iAdapterToControlAdapterLayer: parseT2IAdapterToControlAdapterLayer, // ipAdapterToIPAdapterLayer: parseIPAdapterToIPAdapterLayer, - // layer: parseLayer, - // layers: parseLayers, - // canvasV2Metadata: parseCanvasV2Metadata, } as const; const successToast = (parameter: ReactNode) => {