image gallery component + img css (#1140)

* first commit

* format

* first commit

* Gallery component (#1142)

* gallery

* ui update

* scroll gallery items, keeping them centered

* fix keyboard event

Co-authored-by: Ali Abid <aliabid94@gmail.com>
Co-authored-by: Victor Muštar <victor.mustar@gmail.com>
Co-authored-by: pngwn <hello@pngwn.io>

* formatting

* formatting

Co-authored-by: Ali Abid <aliabid94@gmail.com>
Co-authored-by: pngwn <hello@pngwn.io>
Co-authored-by: Victor Muštar <victor.mustar@gmail.com>
This commit is contained in:
aliabid94 2022-05-03 14:49:51 -07:00 committed by GitHub
parent 527abb2ca7
commit 8464481e22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 241 additions and 19 deletions

View File

@ -7,30 +7,34 @@ import time
import gradio as gr
def fake_gan(*args):
def fake_gan(count, *args):
time.sleep(1)
image = random.choice(
[
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80",
"https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=386&q=80",
"https://images.unsplash.com/photo-1542909168-82c3e7fdca5c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8aHVtYW4lMjBmYWNlfGVufDB8fDB8fA%3D%3D&w=1000&q=80",
"https://images.unsplash.com/photo-1546456073-92b9f0a8d413?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80",
"https://images.unsplash.com/photo-1601412436009-d964bd02edbc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=464&q=80",
]
)
return image
images = [
random.choice(
[
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80",
"https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=386&q=80",
"https://images.unsplash.com/photo-1542909168-82c3e7fdca5c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8aHVtYW4lMjBmYWNlfGVufDB8fDB8fA%3D%3D&w=1000&q=80",
"https://images.unsplash.com/photo-1546456073-92b9f0a8d413?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80",
"https://images.unsplash.com/photo-1601412436009-d964bd02edbc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=464&q=80",
]
)
for _ in range(int(count))
]
return images
demo = gr.Interface(
fn=fake_gan,
inputs=[
gr.Number(label="Generation Count"),
gr.Image(label="Initial Image (optional)"),
gr.Slider(25, minimum=0, maximum=50, label="TV_scale (for smoothness)"),
gr.Slider(25, minimum=0, maximum=50, label="Range_Scale (out of range RBG)"),
gr.Number(label="Seed"),
gr.Number(label="Respacing"),
],
outputs=gr.Image(label="Generated Image"),
outputs=gr.Gallery(label="Generated Images"),
title="FD-GAN",
description="This is a fake demo of a GAN. In reality, the images are randomly chosen from Unsplash.",
examples=[

View File

@ -17,6 +17,7 @@ from gradio.components import (
Dataframe,
Dropdown,
File,
Gallery,
HighlightedText,
Image,
KeyValues,

View File

@ -1136,7 +1136,7 @@ class Image(IOComponent):
self.test_input = deepcopy(media_data.BASE64_IMAGE)
self.interpret_by_tokens = True
super().__init__(
label=label, requires_permissions=requires_permissions, **kwargs
label=label, requires_permissions=requires_permissions, css=css, **kwargs
)
def get_template_context(self):
@ -2712,6 +2712,44 @@ class HTML(IOComponent):
)
class Gallery(IOComponent):
def __init__(
self,
*,
label: Optional[str] = None,
**kwargs,
):
super().__init__(label=label, **kwargs)
def get_template_context(self):
return {
**super().get_template_context(),
}
def postprocess(self, y):
"""
Parameters:
y (List[Union[numpy.array, PIL.Image, str]]): list of images
Returns:
(str): list of base64 url data for images
"""
output = []
for img in y:
if isinstance(img, np.ndarray):
img = processing_utils.encode_array_to_base64(img)
elif isinstance(img, PIL.Image.Image):
img = np.array(img)
img = processing_utils.encode_array_to_base64(img)
elif isinstance(img, str):
img = processing_utils.encode_url_or_file_to_base64(img)
else:
raise ValueError(
"Unknown type. Please choose from: 'numpy', 'pil', 'file'."
)
output.append(img)
return output
class Carousel(IOComponent):
"""
Component displays a set of output components that can be scrolled through.
@ -2721,7 +2759,6 @@ class Carousel(IOComponent):
def __init__(
self,
default_value="",
*,
components: Component | List[Component],
label: Optional[str] = None,
@ -2730,11 +2767,9 @@ class Carousel(IOComponent):
):
"""
Parameters:
default_value (str): IGNORED
components (Union[List[OutputComponent], OutputComponent]): Classes of component(s) that will be scrolled through.
label (str): component name in interface.
"""
# TODO: Shall we havea default value in carousel?
if not isinstance(components, list):
components = [components]
self.components = [

View File

@ -0,0 +1,138 @@
<script lang="ts">
import { Block, BlockLabel } from "@gradio/atoms";
import { ModifyUpload } from "@gradio/upload";
import { tick } from "svelte";
import { Component as StatusTracker } from "../StatusTracker/";
import image_icon from "./image.svg";
export let loading_status: "complete" | "pending" | "error";
export let show_label: boolean;
export let label: string;
export let value: Array<string> | null = null;
export let style: string = "";
let selected_image: number | null = null;
$: 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;
}
}
$: 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"
});
}
</script>
<svelte:window />
<Block variant="solid" color="grey" padding={false}>
<StatusTracker tracked_status={loading_status} />
<BlockLabel {show_label} image={image_icon} label={label || "Gallery"} />
{#if value === null}
<div class="min-h-[16rem] flex justify-center items-center">
<img src={image_icon} alt="" class="h-6 opacity-20" />
</div>
{:else}
{#if selected_image !== null}
<div
on:keydown={on_keydown}
class="absolute inset-0 z-10 flex flex-col bg-white/90 backdrop-blur min-h-[350px] xl:min-h-[450px] max-h-[55vh]"
>
<ModifyUpload on:clear={() => (selected_image = null)} />
<img
on:click={() => (selected_image = next)}
class="w-full object-contain h-[calc(100%-50px)]"
src={value[selected_image]}
alt=""
/>
<div
bind:this={container}
class="absolute h-[50px] bg-white overflow-x-scroll scroll-hide w-full bottom-0 flex gap-1.5 items-center py-2 text-sm px-3"
>
{#each value as image, i}
<button
bind:this={el[i]}
on:click={() => (selected_image = i)}
class="gallery-item !flex-none !h-7 !w-7 transition-all duration-75 {selected_image ===
i
? '!ring-2 !ring-orange-500 hover:!ring-orange-500'
: 'scale-90 transform'}"
>
<img
alt=""
class="h-full w-full overflow-hidden object-contain"
src={image}
/>
</button>
{/each}
</div>
</div>
{/if}
<div
class="overflow-y-auto h-full p-2 min-h-[350px] xl:min-h-[450px] max-h-[55vh]"
>
<div class="pt-6 grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2">
{#each value as image, i}
<button class="gallery-item" on:click={() => (selected_image = i)}>
<img
alt=""
class="h-full w-full overflow-hidden object-contain"
src={image}
/>
</button>
{:else}
<p>Empty</p>
{/each}
</div>
</div>
{/if}
</Block>
<style lang="postcss">
.gallery-item {
@apply rounded shadow-sm relative aspect-square h-full hover:brightness-110 focus:ring-blue-500 focus:ring-2 ring-1 ring-gray-200 hover:ring hover:ring-orange-300 w-full overflow-hidden bg-gray-100 object-fill outline-none;
}
</style>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
width="100%"
height="100%"
viewBox="0 0 24 24"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
"
>
<g
transform="matrix(1.14096,-0.140958,-0.140958,1.14096,-0.0559523,0.0559523)"
>
<path
d="M18,6L6.087,17.913"
style="fill: none; fill-rule: nonzero; stroke: black; stroke-width: 2px"
/>
</g>
<path
d="M4.364,4.364L19.636,19.636"
style="fill: none; fill-rule: nonzero; stroke: black; stroke-width: 2px"
/>
</svg>

After

Width:  |  Height:  |  Size: 805 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather feather-image"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@ -0,0 +1,2 @@
export { default as Component } from "./Gallery.svelte";
export const modes = ["static"];

View File

@ -13,6 +13,7 @@ export const component_map: Record<string, any> = {
file: () => import("./File"),
statustracker: () => import("./StatusTracker"),
highlightedtext: () => import("./HighlightedText"),
gallery: () => import("./Gallery"),
html: () => import("./HTML"),
image: () => import("./Image"),
interpretation: () => import("./Interpretation"),

View File

@ -102,9 +102,9 @@
editable
/>
<img class="w-full h-full object-contain" src={value} alt="" />
<img class="w-full h-full object-contain" src={value} {style} alt="" />
{:else}
<img class="w-full h-full object-contain" src={value} alt="" />
<img class="w-full h-full object-contain" src={value} {style} alt="" />
{/if}
</div>

View File

@ -22,5 +22,5 @@
<img src={image_icon} alt="" class="h-6 opacity-20" />
</div>
{:else}
<img class="w-full h-full object-contain" src={value} alt="" />
<img class="w-full h-full object-contain" src={value} {style} alt="" />
{/if}

View File

@ -9,6 +9,15 @@
.gr-form-gap > .form + .form {
@apply mt-0 border-t-0;
}
.scroll-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scroll-hide::-webkit-scrollbar {
display: none;
}
}
.cropper-container {