Image width/height for gallery component, don't save ImageCache props

This commit is contained in:
space-nuko
2023-05-08 19:48:40 -05:00
parent 92fbe1ea6b
commit 37701f6a54
8 changed files with 1356 additions and 830 deletions

View File

@@ -0,0 +1,396 @@
<script lang="ts">
import { BlockLabel, Empty } from "@gradio/atoms";
import { ModifyUpload } from "@gradio/upload";
import type { SelectData } from "@gradio/utils";
import { createEventDispatcher } from "svelte";
import { tick } from "svelte";
import type { Styles } from "@gradio/utils";
import { get_styles } from "@gradio/utils";
import { Image } from "@gradio/icons";
import type { FileData } from "@gradio/upload";
import { normalise_file } from "@gradio/upload";
export let show_label: boolean = true;
export let label: string;
export let root: string = "";
export let root_url: null | string = null;
export let value: Array<string> | Array<FileData> | null = null;
export let style: Styles = {
grid_cols: [2],
object_fit: "cover",
height: "auto"
};
export let imageWidth: number = 1;
export let imageHeight: number = 1;
const dispatch = createEventDispatcher<{
select: SelectData;
}>();
// tracks whether the value of the gallery was reset
let was_reset: boolean = true;
$: if (selected_image == null || was_reset) {
imageWidth = 1;
imageHeight = 1;
}
$: was_reset = value == null || value.length == 0 ? true : was_reset;
$: _value =
value === null
? null
: value.map((img) =>
Array.isArray(img)
? [normalise_file(img[0], root, root_url), img[1]]
: [normalise_file(img, root, root_url), null]
);
let prevValue: string[] | FileData[] | null = value;
let selected_image: number | null = null;
let old_selected_image: number | null = null;
$: if (prevValue !== value) {
// When value is falsy (clear button or first load),
// style.preview determines the selected image
if (was_reset) {
selected_image = style.preview && value?.length ? 0 : null;
was_reset = false;
// Otherwise we keep the selected_image the same if the
// gallery has at least as many elements as it did before
} else {
selected_image =
selected_image !== null &&
value !== null &&
selected_image < value.length
? selected_image
: null;
}
prevValue = value;
}
$: previous =
((selected_image ?? 0) + (_value?.length ?? 0) - 1) % (_value?.length ?? 0);
$: next = ((selected_image ?? 0) + 1) % (_value?.length ?? 0);
function on_keydown(e: KeyboardEvent) {
switch (e.code) {
case "Escape":
e.preventDefault();
selected_image = null;
break;
case "ArrowLeft":
e.preventDefault();
selected_image = previous;
break;
case "ArrowRight":
e.preventDefault();
selected_image = next;
break;
default:
break;
}
}
$: {
if (selected_image !== old_selected_image) {
old_selected_image = selected_image;
if (selected_image !== null) {
dispatch("select", {
index: selected_image,
value: _value?.[selected_image][1]
});
}
}
}
$: scroll_to_img(selected_image);
let el: Array<HTMLButtonElement> = [];
let container: HTMLDivElement;
async function scroll_to_img(index: number | null) {
if (typeof index !== "number") return;
await tick();
el[index].focus();
const { left: container_left, width: container_width } =
container.getBoundingClientRect();
const { left, width } = el[index].getBoundingClientRect();
const relative_left = left - container_left;
const pos =
relative_left + width / 2 - container_width / 2 + container.scrollLeft;
container.scrollTo({
left: pos < 0 ? 0 : pos,
behavior: "smooth"
});
}
$: can_zoom = window_height >= height;
function add_height_to_styles(style: Styles): string {
styles = get_styles(style, ["grid_cols", "grid_rows", "object_fit"]).styles;
return styles + ` height: ${style.height}`;
}
$: styles = add_height_to_styles(style);
let height = 0;
let window_height = 0;
</script>
<svelte:window bind:innerHeight={window_height} />
{#if show_label}
<BlockLabel
{show_label}
Icon={Image}
label={label || "Gallery"}
disable={typeof style.container === "boolean" && !style.container}
/>
{/if}
{#if value === null || _value === null || _value.length === 0}
<Empty size="large" unpadded_box={true}><Image /></Empty>
{:else}
{#if selected_image !== null}
<div
on:keydown={on_keydown}
class="preview"
class:fixed-height={style.height !== "auto"}
>
<ModifyUpload on:clear={() => (selected_image = null)} />
<img
on:click={() => (selected_image = next)}
src={_value[selected_image][0].data}
alt={_value[selected_image][1] || ""}
title={_value[selected_image][1] || null}
class:with-caption={!!_value[selected_image][1]}
style="height: calc(100% - {_value[selected_image][1]
? '80px'
: '60px'})"
bind:naturalWidth={imageWidth}
bind:naturalHeight={imageHeight}
/>
{#if _value[selected_image][1]}
<div class="caption">
{_value[selected_image][1]}
</div>
{/if}
<div bind:this={container} class="thumbnails scroll-hide">
{#each _value as image, i}
<button
bind:this={el[i]}
on:click={() => (selected_image = i)}
class="thumbnail-item thumbnail-small"
class:selected={selected_image === i}
>
<img
src={image[0].data}
title={image[1] || null}
alt={image[1] || null}
/>
</button>
{/each}
</div>
</div>
{/if}
<div
bind:clientHeight={height}
class="grid-wrap"
class:fixed-height={!style.height || style.height == "auto"}
>
<div class="grid-container" style={styles} class:pt-6={show_label}>
{#each _value as [image, caption], i}
<button
class="thumbnail-item thumbnail-lg"
class:selected={selected_image === i}
on:click={() => (selected_image = can_zoom ? i : selected_image)}
>
<img
alt={caption || ""}
src={typeof image === "string" ? image : image.data}
/>
{#if caption}
<div class="caption-label">
{caption}
</div>
{/if}
</button>
{/each}
</div>
</div>
{/if}
<style lang="postcss">
.preview {
display: flex;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
flex-direction: column;
z-index: var(--layer-2);
backdrop-filter: blur(8px);
background: var(--background-fill-primary);
height: var(--size-full);
}
.fixed-height {
min-height: var(--size-80);
max-height: 55vh;
}
@media (--screen-xl) {
.fixed-height {
min-height: 450px;
}
}
.preview img {
width: var(--size-full);
height: calc(var(--size-full) - 60px);
object-fit: contain;
}
.preview img.with-caption {
height: calc(var(--size-full) - 80px);
}
.caption {
padding: var(--size-2) var(--size-3);
overflow: hidden;
color: var(--block-label-text-color);
font-weight: var(--weight-semibold);
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.thumbnails {
display: flex;
position: absolute;
bottom: 0;
justify-content: center;
align-items: center;
gap: var(--spacing-lg);
width: var(--size-full);
height: var(--size-14);
overflow-x: scroll;
}
.thumbnail-item {
--ring-color: transparent;
position: relative;
outline: none;
box-shadow: 0 0 0 2px var(--ring-color), var(--shadow-drop);
border: 1px solid var(--border-color-primary);
border-radius: var(--button-small-radius);
background: var(--background-fill-secondary);
aspect-ratio: var(--ratio-square);
width: var(--size-full);
height: var(--size-full);
overflow: clip;
}
.thumbnail-item:hover {
--ring-color: var(--border-color-accent);
filter: brightness(1.1);
border-color: var(--border-color-accent);
}
.thumbnail-small {
flex: none;
transform: scale(0.9);
transition: 0.075s;
width: var(--size-9);
height: var(--size-9);
}
.thumbnail-small.selected {
--ring-color: var(--color-accent);
transform: scale(1);
border-color: var(--color-accent);
}
.thumbnail-small > img {
width: var(--size-full);
height: var(--size-full);
overflow: hidden;
object-fit: var(--object-fit);
}
.grid-wrap {
padding: var(--size-2);
height: var(--size-full);
overflow-y: auto;
}
.grid-container {
display: grid;
grid-template-rows: var(--grid-rows);
grid-template-columns: var(--grid-cols);
gap: var(--spacing-lg);
}
@media (--screen-sm) {
.grid-container {
grid-template-columns: var(--sm-grid-cols);
}
}
@media (--screen-md) {
.grid-container {
grid-template-columns: var(--md-grid-cols);
}
}
@media (--screen-lg) {
.grid-container {
grid-template-columns: var(--lg-grid-cols);
}
}
@media (--screen-xl) {
.grid-container {
grid-template-columns: var(--xl-grid-cols);
}
}
@media (--screen-xxl) {
.grid-container {
grid-template-columns: var(--2xl-grid-cols);
}
}
.thumbnail-lg > img {
width: var(--size-full);
height: var(--size-full);
overflow: hidden;
object-fit: var(--object-fit);
}
.thumbnail-lg:hover .caption-label {
opacity: 0.5;
}
.caption-label {
position: absolute;
right: var(--block-label-margin);
bottom: var(--block-label-margin);
z-index: var(--layer-1);
border-top: 1px solid var(--border-color-primary);
border-left: 1px solid var(--border-color-primary);
border-radius: var(--block-label-radius);
background: var(--background-fill-secondary);
padding: var(--block-label-padding);
max-width: 80%;
overflow: hidden;
font-size: var(--block-label-text-size);
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1 @@
export { default as Gallery } from "./Gallery.svelte"

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { SelectData } from "@gradio/utils";
import { BlockLabel, Empty, IconButton } from "@gradio/atoms";
import { Download } from "@gradio/icons";
import { get_coordinates_of_clicked_image } from "./utils";
import { Image } from "@gradio/icons";
export let value: null | string;
export let label: string | undefined = undefined;
export let show_label: boolean;
export let selectable: boolean = false;
export let imageWidth: number = 1;
export let imageHeight: number = 1;
let imageElem: HTMLImageElement | null = null;
const dispatch = createEventDispatcher<{
change: string;
select: SelectData;
}>();
$: value && dispatch("change", value);
$: if (value == null || !imageElem) {
imageWidth = 1;
imageHeight = 1;
}
const handle_click = (evt: MouseEvent) => {
let coordinates = get_coordinates_of_clicked_image(evt);
if (coordinates) {
dispatch("select", { index: coordinates, value: null });
}
};
</script>
<BlockLabel {show_label} Icon={Image} label={label || "Image"} />
{#if value === null}
<Empty size="large" unpadded_box={true}><Image /></Empty>
{:else}
<div class="download">
<a
href={value}
target={window.__is_colab__ ? "_blank" : null}
download={"image"}
>
<IconButton Icon={Download} label="Download" />
</a>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<img src={value} alt="" class:selectable on:click={handle_click} bind:naturalWidth={imageWidth} bind:naturalHeight={imageHeight} />
{/if}
<style>
img {
width: var(--size-full);
height: var(--size-full);
object-fit: contain;
}
.selectable {
cursor: crosshair;
}
.download {
position: absolute;
top: 6px;
right: 6px;
}
</style>

View File

@@ -0,0 +1 @@
export { default as StaticImage } from "./StaticImage.svelte"

View File

@@ -0,0 +1,26 @@
export const get_coordinates_of_clicked_image = (
evt: MouseEvent
): [number, number] | null => {
let image = evt.currentTarget as HTMLImageElement;
const imageRect = image.getBoundingClientRect();
const xScale = image.naturalWidth / imageRect.width;
const yScale = image.naturalHeight / imageRect.height;
if (xScale > yScale) {
const displayed_width = imageRect.width;
const displayed_height = image.naturalHeight / xScale;
const y_offset = (imageRect.height - displayed_height) / 2;
var x = Math.round((evt.clientX - imageRect.left) * xScale);
var y = Math.round((evt.clientY - imageRect.top - y_offset) * xScale);
} else {
const displayed_width = image.naturalWidth / yScale;
const displayed_height = imageRect.height;
const x_offset = (imageRect.width - displayed_width) / 2;
var x = Math.round((evt.clientX - imageRect.left - x_offset) * yScale);
var y = Math.round((evt.clientY - imageRect.top) * yScale);
}
if (x < 0 || x >= image.naturalWidth || y < 0 || y >= image.naturalHeight) {
return null;
}
return [x, y];
};

View File

@@ -1,4 +1,4 @@
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core"; import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { GalleryOutput } from "./ComfyWidgetNodes"; import type { GalleryOutput } from "./ComfyWidgetNodes";
@@ -47,6 +47,8 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } } { name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
] ]
override saveUserState = false;
private _uploadPromise: Promise<void> | null = null; private _uploadPromise: Promise<void> | null = null;
stateWidget: ITextWidget; stateWidget: ITextWidget;
@@ -124,6 +126,14 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
this.setOutputData(1, state) this.setOutputData(1, state)
} }
override stripUserState(o: SerializedLGraphNode) {
super.stripUserState(o);
o.properties.images = null
o.properties.index = 0
o.properties.filenames = {}
o.properties.genNumber = 0
}
private setIndex(newIndex: number, force: boolean = false) { private setIndex(newIndex: number, force: boolean = false) {
if (newIndex === this.properties.index && !force) if (newIndex === this.properties.index && !force)
return; return;

View File

@@ -534,7 +534,9 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } }, { name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } },
{ name: "clear", type: BuiltInSlotType.ACTION } { name: "clear", type: BuiltInSlotType.ACTION }
], ],
{ name: "selected_index", type: "number" } outputs: [
{ name: "selected_index", type: "number" },
{ name: "width", type: "number" },
{ name: "height", type: "number" }, { name: "height", type: "number" },
] ]
} }
@@ -562,8 +564,12 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
this.modeWidget.value = value; this.modeWidget.value = value;
} }
} }
imageSize: Vector2 = [1, 1]
override onExecute() { override onExecute() {
this.setOutputData(0, this.properties.index)
this.setOutputData(1, this.imageSize[0])
this.setOutputData(2, this.imageSize[1]) this.setOutputData(2, this.imageSize[1])
} }

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import { Block, BlockLabel, Empty } from "@gradio/atoms"; import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { Gallery } from "@gradio/gallery"; import { Gallery } from "$lib/components/gradio/gallery";
import { Image } from "@gradio/icons"; import { Image } from "@gradio/icons";
import { StaticImage } from "@gradio/image"; import { StaticImage } from "$lib/components/gradio/image";
import type { Styles } from "@gradio/utils"; import type { Styles } from "@gradio/utils";
import type { WidgetLayout } from "$lib/stores/layoutState"; import type { WidgetLayout } from "$lib/stores/layoutState";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
@@ -19,6 +19,8 @@
let nodeValue: Writable<GradioFileData[]> | null = null; let nodeValue: Writable<GradioFileData[]> | null = null;
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
let option: number | null = null; let option: number | null = null;
let imageWidth: number = 1;
let imageHeight: number = 1;
$: widget && setNodeValue(widget); $: widget && setNodeValue(widget);
@@ -30,7 +32,7 @@
if ($nodeValue != null) { if ($nodeValue != null) {
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) { if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
node.setProperty("index", clamp(node.properties.index, 0, $nodeValue)) node.setProperty("index", clamp(node.properties.index, 0, $nodeValue.length))
} }
} }
} }
@@ -42,6 +44,15 @@
} }
let element: HTMLDivElement; let element: HTMLDivElement;
$: if (node) {
if (imageWidth > 1 || imageHeight > 1) {
node.imageSize = [imageWidth, imageHeight]
}
else {
node.imageSize = [1, 1]
}
}
let mobileLightbox = null; let mobileLightbox = null;
function showMobileLightbox(event: Event) { function showMobileLightbox(event: Event) {
@@ -133,6 +144,8 @@
value={$nodeValue[$nodeValue.length-1].data} value={$nodeValue[$nodeValue.length-1].data}
show_label={widget.attrs.title != ""} show_label={widget.attrs.title != ""}
label={widget.attrs.title} label={widget.attrs.title}
bind:imageWidth
bind:imageHeight
/> />
{:else} {:else}
<Empty size="large" unpadded_box={true}><Image /></Empty> <Empty size="large" unpadded_box={true}><Image /></Empty>
@@ -151,6 +164,8 @@
root={""} root={""}
root_url={""} root_url={""}
on:select={onSelect} on:select={onSelect}
bind:imageWidth
bind:imageHeight
/> />
</div> </div>
</Block> </Block>