mirror of
https://github.com/gradio-app/gradio.git
synced 2025-01-18 10:44:33 +08:00
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:
parent
527abb2ca7
commit
8464481e22
@ -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=[
|
||||
|
@ -17,6 +17,7 @@ from gradio.components import (
|
||||
Dataframe,
|
||||
Dropdown,
|
||||
File,
|
||||
Gallery,
|
||||
HighlightedText,
|
||||
Image,
|
||||
KeyValues,
|
||||
|
@ -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 = [
|
||||
|
138
ui/packages/app/src/components/Gallery/Gallery.svelte
Normal file
138
ui/packages/app/src/components/Gallery/Gallery.svelte
Normal 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>
|
31
ui/packages/app/src/components/Gallery/clear.svg
Normal file
31
ui/packages/app/src/components/Gallery/clear.svg
Normal 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 |
1
ui/packages/app/src/components/Gallery/image.svg
Normal file
1
ui/packages/app/src/components/Gallery/image.svg
Normal 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 |
2
ui/packages/app/src/components/Gallery/index.ts
Normal file
2
ui/packages/app/src/components/Gallery/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as Component } from "./Gallery.svelte";
|
||||
export const modes = ["static"];
|
@ -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"),
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user