Implements thumbnails for gallery

- Thumbnails are saved whenever an image is saved, and when gallery requests images from server
- Thumbnails saved at original image aspect ratio with width of 128px as WEBP
- If the thumbnail property of an image is unavailable for whatever reason, the image's full size URL is used instead
This commit is contained in:
psychedelicious 2022-11-20 00:04:06 +11:00 committed by blessedcoolant
parent a3308c853d
commit 50a67a7172
5 changed files with 62 additions and 20 deletions

View File

@ -172,8 +172,11 @@ class InvokeAIWebServer:
(width, height) = pil_image.size
thumbnail_path = save_thumbnail(pil_image, file_path)
response = {
"url": self.get_url_from_image_path(file_path),
"thumbnail": self.get_url_from_image_path(thumbnail_path),
"mtime": mtime,
"width": width,
"height": height,
@ -306,6 +309,7 @@ class InvokeAIWebServer:
)
paths = []
for ext in ("*.png", "*.jpg", "*.jpeg"):
paths.extend(glob.glob(os.path.join(base_path, ext)))
@ -329,11 +333,15 @@ class InvokeAIWebServer:
else:
sd_metadata = {}
(width, height) = Image.open(path).size
pil_image = Image.open(path)
(width, height) = pil_image.size
thumbnail_path = save_thumbnail(pil_image, path)
image_array.append(
{
"url": self.get_url_from_image_path(path),
"thumbnail": self.get_url_from_image_path(thumbnail_path),
"mtime": os.path.getmtime(path),
"metadata": sd_metadata,
"width": width,
@ -389,11 +397,15 @@ class InvokeAIWebServer:
else:
sd_metadata = {}
(width, height) = Image.open(path).size
pil_image = Image.open(path)
(width, height) = pil_image.size
thumbnail_path = save_thumbnail(pil_image, path)
image_array.append(
{
"url": self.get_url_from_image_path(path),
"thumbnail": self.get_url_from_image_path(thumbnail_path),
"mtime": os.path.getmtime(path),
"metadata": sd_metadata,
"width": width,
@ -537,6 +549,8 @@ class InvokeAIWebServer:
postprocessing=postprocessing_parameters["type"],
)
thumbnail_path = save_thumbnail(image, path)
self.write_log_message(
f'[Postprocessed] "{original_image_path}" > "{path}": {postprocessing_parameters}'
)
@ -549,6 +563,7 @@ class InvokeAIWebServer:
"postprocessingResult",
{
"url": self.get_url_from_image_path(path),
"thumbnail": self.get_url_from_image_path(thumbnail_path),
"mtime": os.path.getmtime(path),
"metadata": metadata,
"width": width,
@ -575,8 +590,12 @@ class InvokeAIWebServer:
from send2trash import send2trash
path = self.get_image_path_from_url(url)
base = os.path.splitext(path)[0]
thumbnail_path = base + ".webp"
send2trash(path)
send2trash(thumbnail_path)
socketio.emit(
"imageDeleted",
{"url": url, "uuid": uuid, "category": category},
@ -949,6 +968,8 @@ class InvokeAIWebServer:
postprocessing=postprocessing,
)
thumbnail_path = save_thumbnail(image, path)
print(f'>> Image generated: "{path}"')
self.write_log_message(f'[Generated] "{path}": {command}')
@ -966,6 +987,7 @@ class InvokeAIWebServer:
"generationResult",
{
"url": self.get_url_from_image_path(path),
"thumbnail": self.get_url_from_image_path(thumbnail_path),
"mtime": os.path.getmtime(path),
"metadata": metadata,
"width": width,
@ -1457,3 +1479,30 @@ def paste_image_into_bounding_box(
bounds = (x, y, x + width, y + height)
im.paste(donor_image, bounds)
return recipient_image
"""
Saves a thumbnail of an image, returning its path.
"""
def save_thumbnail(
image: ImageType,
path: str,
size: int = 128,
) -> str:
base = os.path.splitext(path)[0]
thumbnail_path = base + ".webp"
if os.path.exists(thumbnail_path):
return thumbnail_path
thumbnail_width = size
thumbnail_height = round(size * (image.height / image.width))
image_copy = image.copy()
image_copy.thumbnail(size=(thumbnail_width, thumbnail_height))
image_copy.save(thumbnail_path, "WEBP")
return thumbnail_path

View File

@ -113,6 +113,7 @@ export declare type Metadata = SystemConfig & {
export declare type Image = {
uuid: string;
url: string;
thumbnail: string;
mtime: number;
metadata?: Metadata;
width: number;
@ -184,6 +185,7 @@ export declare type ImageUploadResponse = {
mtime: number;
width: number;
height: number;
thumbnail: string;
// bbox: [number, number, number, number];
};

View File

@ -69,16 +69,14 @@ export const mergeAndUploadCanvas =
body: formData,
});
const { url, mtime, width, height } =
(await response.json()) as InvokeAI.ImageUploadResponse;
const image = (await response.json()) as InvokeAI.ImageUploadResponse;
const { url, width, height } = image;
const newImage: InvokeAI.Image = {
uuid: uuidv4(),
url,
mtime,
category: shouldSaveToGallery ? 'result' : 'user',
width: width,
height: height,
...image,
};
if (shouldDownload) {

View File

@ -23,13 +23,10 @@ import {
import * as InvokeAI from 'app/invokeai';
import * as ContextMenu from '@radix-ui/react-context-menu';
import {
resetCanvasView,
resizeAndScaleCanvas,
setDoesCanvasNeedScaling,
setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice';
import { hoverableImageSelector } from 'features/gallery/store/gallerySliceSelectors';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
interface HoverableImageProps {
image: InvokeAI.Image;
@ -54,7 +51,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
isLightBoxOpen,
} = useAppSelector(hoverableImageSelector);
const { image, isSelected } = props;
const { url, uuid, metadata } = image;
const { url, thumbnail, uuid, metadata } = image;
const [isHovered, setIsHovered] = useState<boolean>(false);
@ -173,7 +170,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
className="hoverable-image-image"
objectFit={galleryImageObjectFit}
rounded={'md'}
src={url}
src={thumbnail || url}
loading={'lazy'}
/>
<div className="hoverable-image-content" onClick={handleSelectImage}>

View File

@ -37,16 +37,12 @@ export const uploadImage =
body: formData,
});
const { url, mtime, width, height } =
(await response.json()) as InvokeAI.ImageUploadResponse;
const image = (await response.json()) as InvokeAI.ImageUploadResponse;
console.log(image)
const newImage: InvokeAI.Image = {
uuid: uuidv4(),
url,
mtime,
category: 'user',
width: width,
height: height,
...image,
};
dispatch(addImage({ image: newImage, category: 'user' }));