mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Moves image uploading to HTTP
- It all seems to work fine - A lot of cleanup is still needed - Logging needs to be added - May need types to be reviewed
This commit is contained in:
parent
b049bbc64e
commit
4382cd0b91
@ -46,6 +46,13 @@ class InvokeAIWebServer:
|
|||||||
self.esrgan = esrgan
|
self.esrgan = esrgan
|
||||||
|
|
||||||
self.canceled = Event()
|
self.canceled = Event()
|
||||||
|
self.ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"}
|
||||||
|
|
||||||
|
def allowed_file(self, filename: str) -> bool:
|
||||||
|
return (
|
||||||
|
"." in filename
|
||||||
|
and filename.rsplit(".", 1)[1].lower() in self.ALLOWED_EXTENSIONS
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.setup_app()
|
self.setup_app()
|
||||||
@ -98,41 +105,70 @@ class InvokeAIWebServer:
|
|||||||
return send_from_directory(self.app.static_folder, "index.html")
|
return send_from_directory(self.app.static_folder, "index.html")
|
||||||
|
|
||||||
@self.app.route("/upload", methods=["POST"])
|
@self.app.route("/upload", methods=["POST"])
|
||||||
def upload_base64_file():
|
def upload():
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
# check if the post request has the file part
|
||||||
dataURL = data["dataURL"]
|
if "file" not in request.files:
|
||||||
name = data["name"]
|
return "No file part", 400
|
||||||
|
file = request.files["file"]
|
||||||
|
|
||||||
print(f'>> Image upload requested "{name}"')
|
# If the user does not select a file, the browser submits an
|
||||||
|
# empty file without a filename.
|
||||||
|
if file.filename == "":
|
||||||
|
return "No selected file", 400
|
||||||
|
|
||||||
if dataURL is not None:
|
kind = request.form["kind"]
|
||||||
bytes = dataURL_to_bytes(dataURL)
|
|
||||||
|
|
||||||
file_path = self.save_file_unique_uuid_name(
|
if kind == "init":
|
||||||
bytes=bytes, name=name, path=self.result_path
|
path = self.init_image_path
|
||||||
|
elif kind == "temp":
|
||||||
|
path = self.temp_image_path
|
||||||
|
elif kind == "result":
|
||||||
|
path = self.result_path
|
||||||
|
elif kind == "mask":
|
||||||
|
path = self.mask_image_path
|
||||||
|
else:
|
||||||
|
return f"Invalid upload kind: {kind}", 400
|
||||||
|
|
||||||
|
if not self.allowed_file(file.filename):
|
||||||
|
return (
|
||||||
|
f'Invalid file type, must be one of: {", ".join(self.ALLOWED_EXTENSIONS)}',
|
||||||
|
400,
|
||||||
)
|
)
|
||||||
|
|
||||||
mtime = os.path.getmtime(file_path)
|
secured_filename = secure_filename(file.filename)
|
||||||
(width, height) = Image.open(file_path).size
|
|
||||||
|
|
||||||
response = {
|
uuid = uuid4().hex
|
||||||
|
truncated_uuid = uuid[:8]
|
||||||
|
|
||||||
|
split = os.path.splitext(secured_filename)
|
||||||
|
name = f"{split[0]}.{truncated_uuid}{split[1]}"
|
||||||
|
|
||||||
|
file_path = os.path.join(path, name)
|
||||||
|
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
mtime = os.path.getmtime(file_path)
|
||||||
|
(width, height) = Image.open(file_path).size
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"image": {
|
||||||
"url": self.get_url_from_image_path(file_path),
|
"url": self.get_url_from_image_path(file_path),
|
||||||
"mtime": mtime,
|
"mtime": mtime,
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
"category": "result",
|
},
|
||||||
"destination": "outpainting_merge",
|
}
|
||||||
}
|
|
||||||
return response
|
return response, 200
|
||||||
else:
|
|
||||||
return "No dataURL provided"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.socketio.emit("error", {"message": (str(e))})
|
self.socketio.emit("error", {"message": (str(e))})
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
return "Error uploading file", 500
|
||||||
|
|
||||||
self.load_socketio_listeners(self.socketio)
|
self.load_socketio_listeners(self.socketio)
|
||||||
|
|
||||||
@ -177,6 +213,7 @@ class InvokeAIWebServer:
|
|||||||
self.init_image_url = "outputs/init-images/"
|
self.init_image_url = "outputs/init-images/"
|
||||||
self.mask_image_url = "outputs/mask-images/"
|
self.mask_image_url = "outputs/mask-images/"
|
||||||
self.intermediate_url = "outputs/intermediates/"
|
self.intermediate_url = "outputs/intermediates/"
|
||||||
|
self.temp_image_url = "outputs/temp-images/"
|
||||||
# location for "finished" images
|
# location for "finished" images
|
||||||
self.result_path = args.outdir
|
self.result_path = args.outdir
|
||||||
# temporary path for intermediates
|
# temporary path for intermediates
|
||||||
@ -184,6 +221,8 @@ class InvokeAIWebServer:
|
|||||||
# path for user-uploaded init images and masks
|
# path for user-uploaded init images and masks
|
||||||
self.init_image_path = os.path.join(self.result_path, "init-images/")
|
self.init_image_path = os.path.join(self.result_path, "init-images/")
|
||||||
self.mask_image_path = os.path.join(self.result_path, "mask-images/")
|
self.mask_image_path = os.path.join(self.result_path, "mask-images/")
|
||||||
|
# path for temp images e.g. gallery generations which are not committed
|
||||||
|
self.temp_image_path = os.path.join(self.result_path, "temp-images/")
|
||||||
# txt log
|
# txt log
|
||||||
self.log_path = os.path.join(self.result_path, "invoke_log.txt")
|
self.log_path = os.path.join(self.result_path, "invoke_log.txt")
|
||||||
# make all output paths
|
# make all output paths
|
||||||
@ -194,6 +233,7 @@ class InvokeAIWebServer:
|
|||||||
self.intermediate_path,
|
self.intermediate_path,
|
||||||
self.init_image_path,
|
self.init_image_path,
|
||||||
self.mask_image_path,
|
self.mask_image_path,
|
||||||
|
self.temp_image_path,
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -517,59 +557,6 @@ class InvokeAIWebServer:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
# TODO: I think this needs a safety mechanism.
|
|
||||||
@socketio.on("uploadImage")
|
|
||||||
def handle_upload_image(bytes, name, destination):
|
|
||||||
try:
|
|
||||||
print(f'>> Image upload requested "{name}"')
|
|
||||||
file_path = self.save_file_unique_uuid_name(
|
|
||||||
bytes=bytes, name=name, path=self.init_image_path
|
|
||||||
)
|
|
||||||
mtime = os.path.getmtime(file_path)
|
|
||||||
(width, height) = Image.open(file_path).size
|
|
||||||
|
|
||||||
socketio.emit(
|
|
||||||
"imageUploaded",
|
|
||||||
{
|
|
||||||
"url": self.get_url_from_image_path(file_path),
|
|
||||||
"mtime": mtime,
|
|
||||||
"width": width,
|
|
||||||
"height": height,
|
|
||||||
"category": "user",
|
|
||||||
"destination": destination,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.socketio.emit("error", {"message": (str(e))})
|
|
||||||
print("\n")
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
print("\n")
|
|
||||||
|
|
||||||
# TODO: I think this needs a safety mechanism.
|
|
||||||
@socketio.on("uploadOutpaintingMergeImage")
|
|
||||||
def handle_upload_outpainting_merge_image(dataURL, name):
|
|
||||||
try:
|
|
||||||
print(f'>> Outpainting merge image upload requested "{name}"')
|
|
||||||
|
|
||||||
image = dataURL_to_image(dataURL)
|
|
||||||
file_name = self.make_unique_init_image_filename(name)
|
|
||||||
file_path = os.path.join(self.result_path, file_name)
|
|
||||||
image.save(file_path)
|
|
||||||
|
|
||||||
socketio.emit(
|
|
||||||
"outpaintingMergeImageUploaded",
|
|
||||||
{
|
|
||||||
"url": self.get_url_from_image_path(file_path),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.socketio.emit("error", {"message": (str(e))})
|
|
||||||
print("\n")
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
print("\n")
|
|
||||||
|
|
||||||
# App Functions
|
# App Functions
|
||||||
def get_system_config(self):
|
def get_system_config(self):
|
||||||
model_list = self.generate.model_cache.list_models()
|
model_list = self.generate.model_cache.list_models()
|
||||||
@ -1247,6 +1234,10 @@ class InvokeAIWebServer:
|
|||||||
return os.path.abspath(
|
return os.path.abspath(
|
||||||
os.path.join(self.intermediate_path, os.path.basename(url))
|
os.path.join(self.intermediate_path, os.path.basename(url))
|
||||||
)
|
)
|
||||||
|
elif "temp-images" in url:
|
||||||
|
return os.path.abspath(
|
||||||
|
os.path.join(self.temp_image_path, os.path.basename(url))
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return os.path.abspath(
|
return os.path.abspath(
|
||||||
os.path.join(self.result_path, os.path.basename(url))
|
os.path.join(self.result_path, os.path.basename(url))
|
||||||
@ -1267,6 +1258,8 @@ class InvokeAIWebServer:
|
|||||||
return os.path.join(self.mask_image_url, os.path.basename(path))
|
return os.path.join(self.mask_image_url, os.path.basename(path))
|
||||||
elif "intermediates" in path:
|
elif "intermediates" in path:
|
||||||
return os.path.join(self.intermediate_url, os.path.basename(path))
|
return os.path.join(self.intermediate_url, os.path.basename(path))
|
||||||
|
elif "temp-images" in path:
|
||||||
|
return os.path.join(self.temp_image_url, os.path.basename(path))
|
||||||
else:
|
else:
|
||||||
return os.path.join(self.result_url, os.path.basename(path))
|
return os.path.join(self.result_url, os.path.basename(path))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
11
frontend/src/app/invokeai.d.ts
vendored
11
frontend/src/app/invokeai.d.ts
vendored
@ -118,7 +118,7 @@ export declare type Image = {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
category: GalleryCategory;
|
category: GalleryCategory;
|
||||||
isBase64: boolean;
|
isBase64?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// GalleryImages is an array of Image.
|
// GalleryImages is an array of Image.
|
||||||
@ -178,8 +178,8 @@ export declare type ImageResultResponse = Omit<Image, 'uuid'> & {
|
|||||||
generationMode: InvokeTabName;
|
generationMode: InvokeTabName;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ImageUploadResponse = Omit<Image, 'uuid' | 'metadata'> & {
|
export declare type ImageUploadResponse = {
|
||||||
destination: 'img2img' | 'inpainting' | 'outpainting' | 'outpainting_merge';
|
image: Omit<Image, 'uuid' | 'metadata' | 'category'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ErrorResponse = {
|
export declare type ErrorResponse = {
|
||||||
@ -203,11 +203,6 @@ export declare type ImageUrlResponse = {
|
|||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare type ImageUploadDestination =
|
|
||||||
| 'img2img'
|
|
||||||
| 'inpainting'
|
|
||||||
| 'outpainting_merge';
|
|
||||||
|
|
||||||
export declare type UploadImagePayload = {
|
export declare type UploadImagePayload = {
|
||||||
file: File;
|
file: File;
|
||||||
destination?: ImageUploadDestination;
|
destination?: ImageUploadDestination;
|
||||||
|
@ -330,41 +330,41 @@ const makeSocketIOListeners = (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onImageUploaded: (data: InvokeAI.ImageUploadResponse) => {
|
// onImageUploaded: (data: InvokeAI.ImageUploadResponse) => {
|
||||||
const { destination, ...rest } = data;
|
// const { origin, image, kind } = data;
|
||||||
const image = {
|
// const newImage = {
|
||||||
uuid: uuidv4(),
|
// uuid: uuidv4(),
|
||||||
...rest,
|
// ...image,
|
||||||
};
|
// };
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
dispatch(addImage({ image, category: 'user' }));
|
// dispatch(addImage({ image: newImage, category: 'user' }));
|
||||||
|
|
||||||
switch (destination) {
|
// switch (origin) {
|
||||||
case 'img2img': {
|
// case 'img2img': {
|
||||||
dispatch(setInitialImage(image));
|
// dispatch(setInitialImage(newImage));
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
case 'inpainting': {
|
// case 'inpainting': {
|
||||||
dispatch(setImageToInpaint(image));
|
// dispatch(setImageToInpaint(newImage));
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
default: {
|
// default: {
|
||||||
dispatch(setCurrentImage(image));
|
// dispatch(setCurrentImage(newImage));
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
dispatch(
|
// dispatch(
|
||||||
addLogEntry({
|
// addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
// timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
message: `Image uploaded: ${data.url}`,
|
// message: `Image uploaded: ${image.url}`,
|
||||||
})
|
// })
|
||||||
);
|
// );
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
console.error(e);
|
// console.error(e);
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
/**
|
/**
|
||||||
* Callback to run when we receive a 'maskImageUploaded' event.
|
* Callback to run when we receive a 'maskImageUploaded' event.
|
||||||
*/
|
*/
|
||||||
|
@ -43,7 +43,7 @@ export const socketioMiddleware = () => {
|
|||||||
onGalleryImages,
|
onGalleryImages,
|
||||||
onProcessingCanceled,
|
onProcessingCanceled,
|
||||||
onImageDeleted,
|
onImageDeleted,
|
||||||
onImageUploaded,
|
// onImageUploaded,
|
||||||
onMaskImageUploaded,
|
onMaskImageUploaded,
|
||||||
onSystemConfig,
|
onSystemConfig,
|
||||||
onModelChanged,
|
onModelChanged,
|
||||||
@ -104,9 +104,9 @@ export const socketioMiddleware = () => {
|
|||||||
onImageDeleted(data);
|
onImageDeleted(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socketio.on('imageUploaded', (data: InvokeAI.ImageUploadResponse) => {
|
// socketio.on('imageUploaded', (data: InvokeAI.ImageUploadResponse) => {
|
||||||
onImageUploaded(data);
|
// onImageUploaded(data);
|
||||||
});
|
// });
|
||||||
|
|
||||||
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
socketio.on('maskImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
|
||||||
onMaskImageUploaded(data);
|
onMaskImageUploaded(data);
|
||||||
|
@ -8,12 +8,13 @@ import {
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store';
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { uploadImage } from 'app/socketio/actions';
|
// import { uploadImage } from 'app/socketio/actions';
|
||||||
import { ImageUploadDestination, UploadImagePayload } from 'app/invokeai';
|
import { UploadImagePayload } from 'app/invokeai';
|
||||||
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||||
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
import { tabDict } from 'features/tabs/InvokeTabs';
|
import { tabDict } from 'features/tabs/InvokeTabs';
|
||||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||||
|
import { uploadImage } from 'features/gallery/util/uploadImage';
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
type ImageUploaderProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -44,15 +45,12 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fileAcceptedCallback = useCallback(
|
const fileAcceptedCallback = useCallback(
|
||||||
(file: File) => {
|
async (file: File) => {
|
||||||
setIsHandlingUpload(true);
|
// setIsHandlingUpload(true);
|
||||||
const payload: UploadImagePayload = { file };
|
|
||||||
if (['img2img', 'inpainting', 'outpainting'].includes(activeTabName)) {
|
dispatch(uploadImage({ imageFile: file }));
|
||||||
payload.destination = activeTabName as ImageUploadDestination;
|
|
||||||
}
|
|
||||||
dispatch(uploadImage(payload));
|
|
||||||
},
|
},
|
||||||
[dispatch, activeTabName]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
@ -124,12 +122,12 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: UploadImagePayload = { file };
|
// const payload: UploadImagePayload = { file };
|
||||||
if (['img2img', 'inpainting'].includes(activeTabName)) {
|
// if (['img2img', 'inpainting'].includes(activeTabName)) {
|
||||||
payload.destination = activeTabName as ImageUploadDestination;
|
// payload.destination = activeTabName as ImageUploadDestination;
|
||||||
}
|
// }
|
||||||
|
|
||||||
dispatch(uploadImage(payload));
|
// dispatch(uploadImage(payload));
|
||||||
};
|
};
|
||||||
document.addEventListener('paste', pasteImageListener);
|
document.addEventListener('paste', pasteImageListener);
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -197,11 +197,11 @@ const IAICanvas = () => {
|
|||||||
listening={false}
|
listening={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isStaging && <IAICanvasStagingArea />}
|
<IAICanvasStagingArea visible={isStaging} />
|
||||||
{shouldShowIntermediates && <IAICanvasIntermediateImage />}
|
{shouldShowIntermediates && <IAICanvasIntermediateImage />}
|
||||||
{!isStaging && (
|
<IAICanvasBoundingBox
|
||||||
<IAICanvasBoundingBox visible={shouldShowBoundingBox} />
|
visible={shouldShowBoundingBox && !isStaging}
|
||||||
)}
|
/>
|
||||||
</Layer>
|
</Layer>
|
||||||
</Stage>
|
</Stage>
|
||||||
{isOnOutpaintingTab && <IAICanvasStatusText />}
|
{isOnOutpaintingTab && <IAICanvasStatusText />}
|
||||||
|
@ -2,13 +2,7 @@ import { GroupConfig } from 'konva/lib/Group';
|
|||||||
import { Group, Line } from 'react-konva';
|
import { Group, Line } from 'react-konva';
|
||||||
import { useAppSelector } from 'app/store';
|
import { useAppSelector } from 'app/store';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import {
|
import { currentCanvasSelector, isCanvasMaskLine } from './canvasSlice';
|
||||||
currentCanvasSelector,
|
|
||||||
GenericCanvasState,
|
|
||||||
InpaintingCanvasState,
|
|
||||||
isCanvasMaskLine,
|
|
||||||
OutpaintingCanvasState,
|
|
||||||
} from './canvasSlice';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
export const canvasLinesSelector = createSelector(
|
export const canvasLinesSelector = createSelector(
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
isStagingSelector,
|
isStagingSelector,
|
||||||
resetCanvas,
|
resetCanvas,
|
||||||
setTool,
|
setTool,
|
||||||
uploadOutpaintingMergedImage,
|
|
||||||
} from './canvasSlice';
|
} from './canvasSlice';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store';
|
import { useAppDispatch, useAppSelector } from 'app/store';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@ -26,6 +25,7 @@ import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
|||||||
import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover';
|
import IAICanvasEraserButtonPopover from './IAICanvasEraserButtonPopover';
|
||||||
import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover';
|
import IAICanvasBrushButtonPopover from './IAICanvasBrushButtonPopover';
|
||||||
import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover';
|
import IAICanvasMaskButtonPopover from './IAICanvasMaskButtonPopover';
|
||||||
|
import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas';
|
||||||
|
|
||||||
export const canvasControlsSelector = createSelector(
|
export const canvasControlsSelector = createSelector(
|
||||||
[currentCanvasSelector, isStagingSelector],
|
[currentCanvasSelector, isStagingSelector],
|
||||||
@ -68,13 +68,23 @@ const IAICanvasOutpaintingControls = () => {
|
|||||||
tooltip="Merge Visible"
|
tooltip="Merge Visible"
|
||||||
icon={<FaLayerGroup />}
|
icon={<FaLayerGroup />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch(uploadOutpaintingMergedImage(canvasImageLayerRef));
|
dispatch(
|
||||||
|
mergeAndUploadCanvas({
|
||||||
|
canvasImageLayerRef,
|
||||||
|
saveToGallery: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Save Selection to Gallery"
|
aria-label="Save to Gallery"
|
||||||
tooltip="Save Selection to Gallery"
|
tooltip="Save to Gallery"
|
||||||
icon={<FaSave />}
|
icon={<FaSave />}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(
|
||||||
|
mergeAndUploadCanvas({ canvasImageLayerRef, saveToGallery: true })
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label="Copy Selection"
|
aria-label="Copy Selection"
|
||||||
|
118
frontend/src/features/canvas/canvasReducers.ts
Normal file
118
frontend/src/features/canvas/canvasReducers.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
import { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { CanvasState, Dimensions, initialLayerState } from './canvasSlice';
|
||||||
|
import { Vector2d } from 'konva/lib/types';
|
||||||
|
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
|
|
||||||
|
export const setImageToInpaint_reducer = (
|
||||||
|
state: CanvasState,
|
||||||
|
image: InvokeAI.Image
|
||||||
|
// action: PayloadAction<InvokeAI.Image>
|
||||||
|
) => {
|
||||||
|
const { width: canvasWidth, height: canvasHeight } =
|
||||||
|
state.inpainting.stageDimensions;
|
||||||
|
const { width, height } = state.inpainting.boundingBoxDimensions;
|
||||||
|
const { x, y } = state.inpainting.boundingBoxCoordinates;
|
||||||
|
|
||||||
|
const maxWidth = Math.min(image.width, canvasWidth);
|
||||||
|
const maxHeight = Math.min(image.height, canvasHeight);
|
||||||
|
|
||||||
|
const newCoordinates: Vector2d = { x, y };
|
||||||
|
const newDimensions: Dimensions = { width, height };
|
||||||
|
|
||||||
|
if (width + x > maxWidth) {
|
||||||
|
// Bounding box at least needs to be translated
|
||||||
|
if (width > maxWidth) {
|
||||||
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.width = roundDownToMultiple(maxWidth, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.x = maxWidth - newDimensions.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height + y > maxHeight) {
|
||||||
|
// Bounding box at least needs to be translated
|
||||||
|
if (height > maxHeight) {
|
||||||
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.height = roundDownToMultiple(maxHeight, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.y = maxHeight - newDimensions.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.inpainting.boundingBoxDimensions = newDimensions;
|
||||||
|
state.inpainting.boundingBoxCoordinates = newCoordinates;
|
||||||
|
|
||||||
|
state.inpainting.pastLayerStates.push(state.inpainting.layerState);
|
||||||
|
|
||||||
|
state.inpainting.layerState = {
|
||||||
|
...initialLayerState,
|
||||||
|
objects: [
|
||||||
|
{
|
||||||
|
kind: 'image',
|
||||||
|
layer: 'base',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
image: image,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
state.outpainting.futureLayerStates = [];
|
||||||
|
state.doesCanvasNeedScaling = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setImageToOutpaint_reducer = (
|
||||||
|
state: CanvasState,
|
||||||
|
image: InvokeAI.Image
|
||||||
|
) => {
|
||||||
|
const { width: canvasWidth, height: canvasHeight } =
|
||||||
|
state.outpainting.stageDimensions;
|
||||||
|
const { width, height } = state.outpainting.boundingBoxDimensions;
|
||||||
|
const { x, y } = state.outpainting.boundingBoxCoordinates;
|
||||||
|
|
||||||
|
const maxWidth = Math.min(image.width, canvasWidth);
|
||||||
|
const maxHeight = Math.min(image.height, canvasHeight);
|
||||||
|
|
||||||
|
const newCoordinates: Vector2d = { x, y };
|
||||||
|
const newDimensions: Dimensions = { width, height };
|
||||||
|
|
||||||
|
if (width + x > maxWidth) {
|
||||||
|
// Bounding box at least needs to be translated
|
||||||
|
if (width > maxWidth) {
|
||||||
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.width = roundDownToMultiple(maxWidth, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.x = maxWidth - newDimensions.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height + y > maxHeight) {
|
||||||
|
// Bounding box at least needs to be translated
|
||||||
|
if (height > maxHeight) {
|
||||||
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.height = roundDownToMultiple(maxHeight, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.y = maxHeight - newDimensions.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.outpainting.boundingBoxDimensions = newDimensions;
|
||||||
|
state.outpainting.boundingBoxCoordinates = newCoordinates;
|
||||||
|
|
||||||
|
state.outpainting.pastLayerStates.push(state.outpainting.layerState);
|
||||||
|
state.outpainting.layerState = {
|
||||||
|
...initialLayerState,
|
||||||
|
objects: [
|
||||||
|
{
|
||||||
|
kind: 'image',
|
||||||
|
layer: 'base',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
image: image,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
state.outpainting.futureLayerStates = [];
|
||||||
|
state.doesCanvasNeedScaling = true;
|
||||||
|
};
|
@ -13,6 +13,14 @@ import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
|
|||||||
import { RootState } from 'app/store';
|
import { RootState } from 'app/store';
|
||||||
import { MutableRefObject } from 'react';
|
import { MutableRefObject } from 'react';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
|
import { tabMap } from 'features/tabs/InvokeTabs';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
import { mergeAndUploadCanvas } from './util/mergeAndUploadCanvas';
|
||||||
|
import { uploadImage } from 'features/gallery/util/uploadImage';
|
||||||
|
import {
|
||||||
|
setImageToInpaint_reducer,
|
||||||
|
setImageToOutpaint_reducer,
|
||||||
|
} from './canvasReducers';
|
||||||
|
|
||||||
export interface GenericCanvasState {
|
export interface GenericCanvasState {
|
||||||
tool: CanvasTool;
|
tool: CanvasTool;
|
||||||
@ -75,6 +83,8 @@ export type CanvasImage = {
|
|||||||
layer: 'base';
|
layer: 'base';
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
image: InvokeAI.Image;
|
image: InvokeAI.Image;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -137,7 +147,7 @@ export interface CanvasState {
|
|||||||
outpainting: OutpaintingCanvasState;
|
outpainting: OutpaintingCanvasState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialLayerState: CanvasLayerState = {
|
export const initialLayerState: CanvasLayerState = {
|
||||||
objects: [],
|
objects: [],
|
||||||
stagingArea: {
|
stagingArea: {
|
||||||
x: -1,
|
x: -1,
|
||||||
@ -283,104 +293,10 @@ export const canvasSlice = createSlice({
|
|||||||
// state.inpainting.imageToInpaint = undefined;
|
// state.inpainting.imageToInpaint = undefined;
|
||||||
},
|
},
|
||||||
setImageToOutpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
setImageToOutpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||||
const { width: canvasWidth, height: canvasHeight } =
|
setImageToOutpaint_reducer(state, action.payload);
|
||||||
state.outpainting.stageDimensions;
|
|
||||||
const { width, height } = state.outpainting.boundingBoxDimensions;
|
|
||||||
const { x, y } = state.outpainting.boundingBoxCoordinates;
|
|
||||||
|
|
||||||
const maxWidth = Math.min(action.payload.width, canvasWidth);
|
|
||||||
const maxHeight = Math.min(action.payload.height, canvasHeight);
|
|
||||||
|
|
||||||
const newCoordinates: Vector2d = { x, y };
|
|
||||||
const newDimensions: Dimensions = { width, height };
|
|
||||||
|
|
||||||
if (width + x > maxWidth) {
|
|
||||||
// Bounding box at least needs to be translated
|
|
||||||
if (width > maxWidth) {
|
|
||||||
// Bounding box also needs to be resized
|
|
||||||
newDimensions.width = roundDownToMultiple(maxWidth, 64);
|
|
||||||
}
|
|
||||||
newCoordinates.x = maxWidth - newDimensions.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (height + y > maxHeight) {
|
|
||||||
// Bounding box at least needs to be translated
|
|
||||||
if (height > maxHeight) {
|
|
||||||
// Bounding box also needs to be resized
|
|
||||||
newDimensions.height = roundDownToMultiple(maxHeight, 64);
|
|
||||||
}
|
|
||||||
newCoordinates.y = maxHeight - newDimensions.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.outpainting.boundingBoxDimensions = newDimensions;
|
|
||||||
state.outpainting.boundingBoxCoordinates = newCoordinates;
|
|
||||||
|
|
||||||
state.outpainting.pastLayerStates.push(state.outpainting.layerState);
|
|
||||||
state.outpainting.layerState = {
|
|
||||||
...initialLayerState,
|
|
||||||
objects: [
|
|
||||||
{
|
|
||||||
kind: 'image',
|
|
||||||
layer: 'base',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
image: action.payload,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
state.outpainting.futureLayerStates = [];
|
|
||||||
state.doesCanvasNeedScaling = true;
|
|
||||||
},
|
},
|
||||||
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||||
const { width: canvasWidth, height: canvasHeight } =
|
setImageToInpaint_reducer(state, action.payload);
|
||||||
state.inpainting.stageDimensions;
|
|
||||||
const { width, height } = state.inpainting.boundingBoxDimensions;
|
|
||||||
const { x, y } = state.inpainting.boundingBoxCoordinates;
|
|
||||||
|
|
||||||
const maxWidth = Math.min(action.payload.width, canvasWidth);
|
|
||||||
const maxHeight = Math.min(action.payload.height, canvasHeight);
|
|
||||||
|
|
||||||
const newCoordinates: Vector2d = { x, y };
|
|
||||||
const newDimensions: Dimensions = { width, height };
|
|
||||||
|
|
||||||
if (width + x > maxWidth) {
|
|
||||||
// Bounding box at least needs to be translated
|
|
||||||
if (width > maxWidth) {
|
|
||||||
// Bounding box also needs to be resized
|
|
||||||
newDimensions.width = roundDownToMultiple(maxWidth, 64);
|
|
||||||
}
|
|
||||||
newCoordinates.x = maxWidth - newDimensions.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (height + y > maxHeight) {
|
|
||||||
// Bounding box at least needs to be translated
|
|
||||||
if (height > maxHeight) {
|
|
||||||
// Bounding box also needs to be resized
|
|
||||||
newDimensions.height = roundDownToMultiple(maxHeight, 64);
|
|
||||||
}
|
|
||||||
newCoordinates.y = maxHeight - newDimensions.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.inpainting.boundingBoxDimensions = newDimensions;
|
|
||||||
state.inpainting.boundingBoxCoordinates = newCoordinates;
|
|
||||||
|
|
||||||
state.inpainting.pastLayerStates.push(state.inpainting.layerState);
|
|
||||||
|
|
||||||
state.inpainting.layerState = {
|
|
||||||
...initialLayerState,
|
|
||||||
objects: [
|
|
||||||
{
|
|
||||||
kind: 'image',
|
|
||||||
layer: 'base',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
image: action.payload,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
state.outpainting.futureLayerStates = [];
|
|
||||||
state.doesCanvasNeedScaling = true;
|
|
||||||
},
|
},
|
||||||
setStageDimensions: (state, action: PayloadAction<Dimensions>) => {
|
setStageDimensions: (state, action: PayloadAction<Dimensions>) => {
|
||||||
state[state.currentCanvas].stageDimensions = action.payload;
|
state[state.currentCanvas].stageDimensions = action.payload;
|
||||||
@ -568,8 +484,7 @@ export const canvasSlice = createSlice({
|
|||||||
currentCanvas.layerState.stagingArea.images.push({
|
currentCanvas.layerState.stagingArea.images.push({
|
||||||
kind: 'image',
|
kind: 'image',
|
||||||
layer: 'base',
|
layer: 'base',
|
||||||
x: boundingBox.x,
|
...boundingBox,
|
||||||
y: boundingBox.y,
|
|
||||||
image,
|
image,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -705,14 +620,8 @@ export const canvasSlice = createSlice({
|
|||||||
currentCanvas.pastLayerStates.shift();
|
currentCanvas.pastLayerStates.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { x, y, image } = images[selectedImageIndex];
|
|
||||||
|
|
||||||
currentCanvas.layerState.objects.push({
|
currentCanvas.layerState.objects.push({
|
||||||
kind: 'image',
|
...images[selectedImageIndex],
|
||||||
layer: 'base',
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
image,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
currentCanvas.layerState.stagingArea = {
|
currentCanvas.layerState.stagingArea = {
|
||||||
@ -723,20 +632,39 @@ export const canvasSlice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(uploadOutpaintingMergedImage.fulfilled, (state, action) => {
|
builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => {
|
||||||
if (!action.payload) return;
|
if (!action.payload) return;
|
||||||
state.outpainting.pastLayerStates.push({
|
const { image, kind, boundingBox } = action.payload;
|
||||||
...state.outpainting.layerState,
|
|
||||||
});
|
|
||||||
state.outpainting.futureLayerStates = [];
|
|
||||||
|
|
||||||
state.outpainting.layerState.objects = [
|
if (kind === 'temp_merged_canvas') {
|
||||||
{
|
state.outpainting.pastLayerStates.push({
|
||||||
kind: 'image',
|
...state.outpainting.layerState,
|
||||||
layer: 'base',
|
});
|
||||||
...action.payload,
|
|
||||||
},
|
state.outpainting.futureLayerStates = [];
|
||||||
];
|
|
||||||
|
state.outpainting.layerState.objects = [
|
||||||
|
{
|
||||||
|
kind: 'image',
|
||||||
|
layer: 'base',
|
||||||
|
...boundingBox,
|
||||||
|
image,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(uploadImage.fulfilled, (state, action) => {
|
||||||
|
if (!action.payload) return;
|
||||||
|
const { image, kind, activeTabName } = action.payload;
|
||||||
|
|
||||||
|
if (kind !== 'init') return;
|
||||||
|
|
||||||
|
if (activeTabName === 'inpainting') {
|
||||||
|
setImageToInpaint_reducer(state, image);
|
||||||
|
} else if (activeTabName === 'outpainting') {
|
||||||
|
setImageToOutpaint_reducer(state, image);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -799,66 +727,6 @@ export const {
|
|||||||
|
|
||||||
export default canvasSlice.reducer;
|
export default canvasSlice.reducer;
|
||||||
|
|
||||||
export const uploadOutpaintingMergedImage = createAsyncThunk(
|
|
||||||
'canvas/uploadOutpaintingMergedImage',
|
|
||||||
async (
|
|
||||||
canvasImageLayerRef: MutableRefObject<Konva.Layer | null>,
|
|
||||||
thunkAPI
|
|
||||||
) => {
|
|
||||||
const { getState } = thunkAPI;
|
|
||||||
|
|
||||||
const state = getState() as RootState;
|
|
||||||
const stageScale = state.canvas.outpainting.stageScale;
|
|
||||||
|
|
||||||
if (!canvasImageLayerRef.current) return;
|
|
||||||
const tempScale = canvasImageLayerRef.current.scale();
|
|
||||||
|
|
||||||
const { x: relativeX, y: relativeY } =
|
|
||||||
canvasImageLayerRef.current.getClientRect({
|
|
||||||
relativeTo: canvasImageLayerRef.current.getParent(),
|
|
||||||
});
|
|
||||||
|
|
||||||
canvasImageLayerRef.current.scale({
|
|
||||||
x: 1 / stageScale,
|
|
||||||
y: 1 / stageScale,
|
|
||||||
});
|
|
||||||
|
|
||||||
const clientRect = canvasImageLayerRef.current.getClientRect();
|
|
||||||
|
|
||||||
const imageDataURL = canvasImageLayerRef.current.toDataURL(clientRect);
|
|
||||||
|
|
||||||
canvasImageLayerRef.current.scale(tempScale);
|
|
||||||
|
|
||||||
if (!imageDataURL) return;
|
|
||||||
|
|
||||||
const response = await fetch(window.location.origin + '/upload', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
dataURL: imageDataURL,
|
|
||||||
name: 'outpaintingmerge.png',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = (await response.json()) as InvokeAI.ImageUploadResponse;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { destination, ...rest } = data;
|
|
||||||
const image = {
|
|
||||||
uuid: uuidv4(),
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
image,
|
|
||||||
x: relativeX,
|
|
||||||
y: relativeY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const currentCanvasSelector = (state: RootState): BaseCanvasState =>
|
export const currentCanvasSelector = (state: RootState): BaseCanvasState =>
|
||||||
state.canvas[state.canvas.currentCanvas];
|
state.canvas[state.canvas.currentCanvas];
|
||||||
|
|
||||||
|
26
frontend/src/features/canvas/util/layerToBlob.ts
Normal file
26
frontend/src/features/canvas/util/layerToBlob.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import Konva from 'konva';
|
||||||
|
|
||||||
|
const layerToBlob = async (layer: Konva.Layer, stageScale: number) => {
|
||||||
|
const tempScale = layer.scale();
|
||||||
|
|
||||||
|
const { x: relativeX, y: relativeY } = layer.getClientRect({
|
||||||
|
relativeTo: layer.getParent(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scale the canvas before getting it as a Blob
|
||||||
|
layer.scale({
|
||||||
|
x: 1 / stageScale,
|
||||||
|
y: 1 / stageScale,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientRect = layer.getClientRect();
|
||||||
|
|
||||||
|
const blob = await layer.toBlob(clientRect);
|
||||||
|
|
||||||
|
// Unscale the canvas
|
||||||
|
layer.scale(tempScale);
|
||||||
|
|
||||||
|
return { blob, relativeX, relativeY };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default layerToBlob;
|
64
frontend/src/features/canvas/util/mergeAndUploadCanvas.ts
Normal file
64
frontend/src/features/canvas/util/mergeAndUploadCanvas.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { RootState } from 'app/store';
|
||||||
|
import Konva from 'konva';
|
||||||
|
import { MutableRefObject } from 'react';
|
||||||
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import layerToBlob from './layerToBlob';
|
||||||
|
|
||||||
|
export const mergeAndUploadCanvas = createAsyncThunk(
|
||||||
|
'canvas/mergeAndUploadCanvas',
|
||||||
|
async (
|
||||||
|
args: {
|
||||||
|
canvasImageLayerRef: MutableRefObject<Konva.Layer | null>;
|
||||||
|
saveToGallery: boolean;
|
||||||
|
},
|
||||||
|
thunkAPI
|
||||||
|
) => {
|
||||||
|
const { canvasImageLayerRef, saveToGallery } = args;
|
||||||
|
|
||||||
|
const { getState } = thunkAPI;
|
||||||
|
|
||||||
|
const state = getState() as RootState;
|
||||||
|
|
||||||
|
const stageScale = state.canvas[state.canvas.currentCanvas].stageScale;
|
||||||
|
|
||||||
|
if (!canvasImageLayerRef.current) return;
|
||||||
|
|
||||||
|
const { blob, relativeX, relativeY } = await layerToBlob(
|
||||||
|
canvasImageLayerRef.current,
|
||||||
|
stageScale
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!blob) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('file', blob as Blob, 'merged_canvas.png');
|
||||||
|
formData.append('kind', saveToGallery ? 'result' : 'temp');
|
||||||
|
|
||||||
|
const response = await fetch(window.location.origin + '/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { image } = (await response.json()) as InvokeAI.ImageUploadResponse;
|
||||||
|
|
||||||
|
const newImage: InvokeAI.Image = {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
category: saveToGallery ? 'result' : 'user',
|
||||||
|
...image,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
image: newImage,
|
||||||
|
kind: saveToGallery ? 'merged_canvas' : 'temp_merged_canvas',
|
||||||
|
boundingBox: {
|
||||||
|
x: relativeX,
|
||||||
|
y: relativeY,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
@ -4,6 +4,10 @@ import _, { clamp } from 'lodash';
|
|||||||
import * as InvokeAI from 'app/invokeai';
|
import * as InvokeAI from 'app/invokeai';
|
||||||
import { IRect } from 'konva/lib/types';
|
import { IRect } from 'konva/lib/types';
|
||||||
import { InvokeTabName } from 'features/tabs/InvokeTabs';
|
import { InvokeTabName } from 'features/tabs/InvokeTabs';
|
||||||
|
import { mergeAndUploadCanvas } from 'features/canvas/util/mergeAndUploadCanvas';
|
||||||
|
import { uploadImage } from './util/uploadImage';
|
||||||
|
import { setInitialImage } from 'features/options/optionsSlice';
|
||||||
|
import { setImageToInpaint } from 'features/canvas/canvasSlice';
|
||||||
|
|
||||||
export type GalleryCategory = 'user' | 'result';
|
export type GalleryCategory = 'user' | 'result';
|
||||||
|
|
||||||
@ -25,7 +29,10 @@ export type Gallery = {
|
|||||||
export interface GalleryState {
|
export interface GalleryState {
|
||||||
currentImage?: InvokeAI.Image;
|
currentImage?: InvokeAI.Image;
|
||||||
currentImageUuid: string;
|
currentImageUuid: string;
|
||||||
intermediateImage?: InvokeAI.Image & { boundingBox?: IRect; generationMode?: InvokeTabName };
|
intermediateImage?: InvokeAI.Image & {
|
||||||
|
boundingBox?: IRect;
|
||||||
|
generationMode?: InvokeTabName;
|
||||||
|
};
|
||||||
shouldPinGallery: boolean;
|
shouldPinGallery: boolean;
|
||||||
shouldShowGallery: boolean;
|
shouldShowGallery: boolean;
|
||||||
galleryScrollPosition: number;
|
galleryScrollPosition: number;
|
||||||
@ -261,6 +268,46 @@ export const gallerySlice = createSlice({
|
|||||||
state.galleryWidth = action.payload;
|
state.galleryWidth = action.payload;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder.addCase(mergeAndUploadCanvas.fulfilled, (state, action) => {
|
||||||
|
if (!action.payload) return;
|
||||||
|
const { image, kind, boundingBox } = action.payload;
|
||||||
|
|
||||||
|
if (kind === 'merged_canvas') {
|
||||||
|
const { uuid, url, mtime } = image;
|
||||||
|
|
||||||
|
state.categories.result.images.unshift(image);
|
||||||
|
|
||||||
|
if (state.shouldAutoSwitchToNewImages) {
|
||||||
|
state.currentImageUuid = uuid;
|
||||||
|
state.currentImage = image;
|
||||||
|
state.currentCategory = 'result';
|
||||||
|
}
|
||||||
|
|
||||||
|
state.intermediateImage = undefined;
|
||||||
|
state.categories.result.latest_mtime = mtime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.addCase(uploadImage.fulfilled, (state, action) => {
|
||||||
|
if (!action.payload) return;
|
||||||
|
const { image, kind } = action.payload;
|
||||||
|
|
||||||
|
if (kind === 'init') {
|
||||||
|
const { uuid, mtime } = image;
|
||||||
|
|
||||||
|
state.categories.result.images.unshift(image);
|
||||||
|
|
||||||
|
if (state.shouldAutoSwitchToNewImages) {
|
||||||
|
state.currentImageUuid = uuid;
|
||||||
|
state.currentImage = image;
|
||||||
|
state.currentCategory = 'user';
|
||||||
|
}
|
||||||
|
|
||||||
|
state.intermediateImage = undefined;
|
||||||
|
state.categories.result.latest_mtime = mtime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
47
frontend/src/features/gallery/util/uploadImage.ts
Normal file
47
frontend/src/features/gallery/util/uploadImage.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { RootState } from 'app/store';
|
||||||
|
import * as InvokeAI from 'app/invokeai';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { activeTabNameSelector } from 'features/options/optionsSelectors';
|
||||||
|
|
||||||
|
export const uploadImage = createAsyncThunk(
|
||||||
|
'gallery/uploadImage',
|
||||||
|
async (
|
||||||
|
args: {
|
||||||
|
imageFile: File;
|
||||||
|
},
|
||||||
|
thunkAPI
|
||||||
|
) => {
|
||||||
|
const { imageFile } = args;
|
||||||
|
|
||||||
|
const { getState } = thunkAPI;
|
||||||
|
|
||||||
|
const state = getState() as RootState;
|
||||||
|
|
||||||
|
const activeTabName = activeTabNameSelector(state);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('file', imageFile, imageFile.name);
|
||||||
|
formData.append('kind', 'init');
|
||||||
|
|
||||||
|
const response = await fetch(window.location.origin + '/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { image } = (await response.json()) as InvokeAI.ImageUploadResponse;
|
||||||
|
|
||||||
|
const newImage: InvokeAI.Image = {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
category: 'user',
|
||||||
|
...image,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
image: newImage,
|
||||||
|
kind: 'init',
|
||||||
|
activeTabName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
@ -5,6 +5,7 @@ import promptToString from 'common/util/promptToString';
|
|||||||
import { seedWeightsToString } from 'common/util/seedWeightPairs';
|
import { seedWeightsToString } from 'common/util/seedWeightPairs';
|
||||||
import { FACETOOL_TYPES } from 'app/constants';
|
import { FACETOOL_TYPES } from 'app/constants';
|
||||||
import { InvokeTabName, tabMap } from 'features/tabs/InvokeTabs';
|
import { InvokeTabName, tabMap } from 'features/tabs/InvokeTabs';
|
||||||
|
import { uploadImage } from 'features/gallery/util/uploadImage';
|
||||||
|
|
||||||
export type UpscalingLevel = 2 | 4;
|
export type UpscalingLevel = 2 | 4;
|
||||||
|
|
||||||
@ -361,6 +362,16 @@ export const optionsSlice = createSlice({
|
|||||||
state.isLightBoxOpen = action.payload;
|
state.isLightBoxOpen = action.payload;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder.addCase(uploadImage.fulfilled, (state, action) => {
|
||||||
|
if (!action.payload) return;
|
||||||
|
const { image, kind, activeTabName } = action.payload;
|
||||||
|
|
||||||
|
if (kind === 'init' && activeTabName === 'img2img') {
|
||||||
|
state.initialImage = image;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
Loading…
Reference in New Issue
Block a user