mirror of
https://github.com/gradio-app/gradio.git
synced 2024-11-21 01:01:05 +08:00
Image Fixes (#6441)
* Fix + tests * Rest of code lol * add changeset * lint * lint + comments * bind to upload * add changeset * Update breezy-foxes-search.md --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
parent
d92de491bf
commit
2f805a7dd3
7
.changeset/breezy-foxes-search.md
Normal file
7
.changeset/breezy-foxes-search.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
"@gradio/image": patch
|
||||
"@gradio/upload": patch
|
||||
"gradio": patch
|
||||
---
|
||||
|
||||
fix:Small but important bugfixes for gr.Image: The upload event was not triggering at all. The paste-from-clipboard was not triggering an upload event. The clear button was not triggering a change event. The change event was triggering infinitely. Uploaded images were not preserving their original names. Uploading a new image should clear out the previous image.
|
@ -1,7 +1,12 @@
|
||||
export default {
|
||||
use: {
|
||||
screenshot: "only-on-failure",
|
||||
trace: "retain-on-failure"
|
||||
trace: "retain-on-failure",
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
bypassCSP: true,
|
||||
launchOptions: {
|
||||
args: ["--disable-web-security"]
|
||||
}
|
||||
},
|
||||
testMatch: /.*.spec.ts/,
|
||||
testDir: "..",
|
||||
|
1
demo/image_component_events/run.ipynb
Normal file
1
demo/image_component_events/run.ipynb
Normal file
@ -0,0 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_component_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " with gr.Column():\n", " input_img = gr.Image(type=\"filepath\", label=\"Input Image\", sources=[\"upload\", \"clipboard\"])\n", " with gr.Column():\n", " output_img = gr.Image(type=\"filepath\", label=\"Output Image\", sources=[\"upload\", \"clipboard\"])\n", " with gr.Column():\n", " num_change = gr.Number(label=\"# Change Events\", value=0)\n", " num_load = gr.Number(label=\"# Upload Events\", value=0)\n", " num_change_o = gr.Number(label=\"# Change Events Output\", value=0)\n", " num_clear = gr.Number(label=\"# Clear Events\", value=0)\n", " input_img.upload(lambda s, n: (s, n + 1), [input_img, num_load], [output_img, num_load])\n", " input_img.change(lambda n: n + 1, num_change, num_change)\n", " input_img.clear(lambda n: n + 1, num_clear, num_clear)\n", " output_img.change(lambda n: n + 1, num_change_o, num_change_o)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
20
demo/image_component_events/run.py
Normal file
20
demo/image_component_events/run.py
Normal file
@ -0,0 +1,20 @@
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
input_img = gr.Image(type="filepath", label="Input Image", sources=["upload", "clipboard"])
|
||||
with gr.Column():
|
||||
output_img = gr.Image(type="filepath", label="Output Image", sources=["upload", "clipboard"])
|
||||
with gr.Column():
|
||||
num_change = gr.Number(label="# Change Events", value=0)
|
||||
num_load = gr.Number(label="# Upload Events", value=0)
|
||||
num_change_o = gr.Number(label="# Change Events Output", value=0)
|
||||
num_clear = gr.Number(label="# Clear Events", value=0)
|
||||
input_img.upload(lambda s, n: (s, n + 1), [input_img, num_load], [output_img, num_load])
|
||||
input_img.change(lambda n: n + 1, num_change, num_change)
|
||||
input_img.clear(lambda n: n + 1, num_clear, num_clear)
|
||||
output_img.change(lambda n: n + 1, num_change_o, num_change_o)
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
@ -147,12 +147,20 @@ class Image(StreamingInput, Component):
|
||||
) -> np.ndarray | _Image.Image | str | None:
|
||||
if payload is None:
|
||||
return payload
|
||||
if payload.orig_name:
|
||||
p = Path(payload.orig_name)
|
||||
name = p.stem
|
||||
else:
|
||||
name = "image"
|
||||
im = _Image.open(payload.path)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
im = im.convert(self.image_mode)
|
||||
return image_utils.format_image(
|
||||
im, cast(Literal["numpy", "pil", "filepath"], self.type), self.GRADIO_CACHE
|
||||
im,
|
||||
cast(Literal["numpy", "pil", "filepath"], self.type),
|
||||
self.GRADIO_CACHE,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def postprocess(
|
||||
@ -160,7 +168,9 @@ class Image(StreamingInput, Component):
|
||||
) -> FileData | None:
|
||||
if value is None:
|
||||
return None
|
||||
return FileData(path=image_utils.save_image(value, self.GRADIO_CACHE))
|
||||
saved = image_utils.save_image(value, self.GRADIO_CACHE)
|
||||
orig_name = Path(saved).name if Path(saved).exists() else None
|
||||
return FileData(path=saved, orig_name=orig_name)
|
||||
|
||||
def check_streamable(self):
|
||||
if self.streaming and self.sources != ["webcam"]:
|
||||
|
@ -15,6 +15,7 @@ def format_image(
|
||||
im: _Image.Image | None,
|
||||
type: Literal["numpy", "pil", "filepath"],
|
||||
cache_dir: str,
|
||||
name: str = "image",
|
||||
) -> np.ndarray | _Image.Image | str | None:
|
||||
"""Helper method to format an image based on self.type"""
|
||||
if im is None:
|
||||
@ -26,7 +27,7 @@ def format_image(
|
||||
return np.array(im)
|
||||
elif type == "filepath":
|
||||
path = processing_utils.save_pil_to_cache(
|
||||
im, cache_dir=cache_dir, format=fmt or "png" # type: ignore
|
||||
im, cache_dir=cache_dir, name=name, format=fmt or "png" # type: ignore
|
||||
)
|
||||
return path
|
||||
else:
|
||||
|
@ -141,12 +141,15 @@ def hash_base64(base64_encoding: str, chunk_num_blocks: int = 128) -> str:
|
||||
|
||||
|
||||
def save_pil_to_cache(
|
||||
img: Image.Image, cache_dir: str, format: Literal["png", "jpg"] = "png"
|
||||
img: Image.Image,
|
||||
cache_dir: str,
|
||||
name: str = "image",
|
||||
format: Literal["png", "jpg"] = "png",
|
||||
) -> str:
|
||||
bytes_data = encode_pil_to_bytes(img, format)
|
||||
temp_dir = Path(cache_dir) / hash_bytes(bytes_data)
|
||||
temp_dir.mkdir(exist_ok=True, parents=True)
|
||||
filename = str((temp_dir / f"image.{format}").resolve())
|
||||
filename = str((temp_dir / f"{name}.{format}").resolve())
|
||||
img.save(filename, pnginfo=get_pil_metadata(img))
|
||||
return filename
|
||||
|
||||
|
61
js/app/test/image_component_events.spec.ts
Normal file
61
js/app/test/image_component_events.spec.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { test, expect, drag_and_drop_file } from "@gradio/tootils";
|
||||
import fs from "fs";
|
||||
|
||||
test("Image click-to-upload uploads image successfuly. Clear button dispatches event correctly. Downloading the file works and has the correct name.", async ({
|
||||
page
|
||||
}) => {
|
||||
await page.getByRole("button", { name: "Drop Image Here" }).click();
|
||||
const uploader = await page.locator("input[type=file]");
|
||||
await Promise.all([
|
||||
uploader.setInputFiles(["./test/files/cheetah1.jpg"]),
|
||||
page.waitForResponse("**/upload?*?*")
|
||||
]);
|
||||
|
||||
await expect(page.getByLabel("# Change Events").first()).toHaveValue("1");
|
||||
await expect(await page.getByLabel("# Upload Events")).toHaveValue("1");
|
||||
await expect(await page.getByLabel("# Change Events Output")).toHaveValue(
|
||||
"1"
|
||||
);
|
||||
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await page.getByLabel("Download").click();
|
||||
const download = await downloadPromise;
|
||||
// Automatically convert to png in the backend since PIL is very picky
|
||||
await expect(download.suggestedFilename()).toBe("cheetah1.png");
|
||||
|
||||
await page.getByLabel("Remove Image").click();
|
||||
await expect(page.getByLabel("# Clear Events")).toHaveValue("1");
|
||||
await expect(page.getByLabel("# Change Events").first()).toHaveValue("2");
|
||||
});
|
||||
|
||||
test("Image drag-to-upload uploads image successfuly.", async ({ page }) => {
|
||||
await drag_and_drop_file(
|
||||
page,
|
||||
"input[type=file]",
|
||||
"./test/files/cheetah1.jpg",
|
||||
"cheetag1.jpg",
|
||||
"image/*"
|
||||
);
|
||||
await page.waitForResponse("**/upload?*");
|
||||
await expect(page.getByLabel("# Change Events").first()).toHaveValue("1");
|
||||
await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
|
||||
});
|
||||
|
||||
test("Image copy from clipboard dispatches upload event.", async ({ page }) => {
|
||||
// Need to make request from inside browser for blob to be formatted correctly
|
||||
// tried lots of different things
|
||||
await page.evaluate(async () => {
|
||||
const blob = await (
|
||||
await fetch(
|
||||
`https://gradio-builds.s3.amazonaws.com/assets/PDFDisplay.png`
|
||||
)
|
||||
).blob();
|
||||
navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
||||
});
|
||||
|
||||
await page.pause();
|
||||
await page.getByLabel("clipboard-image-toolbar-btn").click();
|
||||
await page.pause();
|
||||
await expect(page.getByLabel("# Change Events").first()).toHaveValue("1");
|
||||
await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
|
||||
});
|
@ -61,7 +61,9 @@
|
||||
share: ShareData;
|
||||
}>;
|
||||
|
||||
$: value?.url && gradio.dispatch("change");
|
||||
$: url = _value?.url;
|
||||
$: url && gradio.dispatch("change");
|
||||
|
||||
let dragging: boolean;
|
||||
let active_tool: null | "webcam" = null;
|
||||
</script>
|
||||
@ -127,7 +129,10 @@
|
||||
{root}
|
||||
{sources}
|
||||
on:edit={() => gradio.dispatch("edit")}
|
||||
on:clear={() => gradio.dispatch("clear")}
|
||||
on:clear={() => {
|
||||
gradio.dispatch("clear");
|
||||
gradio.dispatch("change");
|
||||
}}
|
||||
on:stream={() => gradio.dispatch("stream")}
|
||||
on:drag={({ detail }) => (dragging = detail)}
|
||||
on:upload={() => gradio.dispatch("upload")}
|
||||
|
@ -40,7 +40,7 @@
|
||||
<a
|
||||
href={value.url}
|
||||
target={window.__is_colab__ ? "_blank" : null}
|
||||
download={"image"}
|
||||
download={value.orig_name || "image"}
|
||||
>
|
||||
<IconButton Icon={Download} label={i18n("common.download")} />
|
||||
</a>
|
||||
|
@ -33,10 +33,12 @@
|
||||
export let i18n: I18nFormatter;
|
||||
|
||||
let upload: Upload;
|
||||
let uploading = false;
|
||||
export let active_tool: "webcam" | null = null;
|
||||
|
||||
function handle_upload({ detail }: CustomEvent<FileData>): void {
|
||||
value = normalise_file(detail, root, null);
|
||||
dispatch("upload");
|
||||
}
|
||||
|
||||
async function handle_save(img_blob: Blob | any): Promise<void> {
|
||||
@ -52,6 +54,8 @@
|
||||
pending = false;
|
||||
}
|
||||
|
||||
$: if (uploading) value = null;
|
||||
|
||||
$: value && !value.url && (value = normalise_file(value, root, null));
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@ -111,6 +115,7 @@
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const type = items[i].types.find((t) => t.startsWith("image/"));
|
||||
if (type) {
|
||||
value = null;
|
||||
items[i].getType(type).then(async (blob) => {
|
||||
const f = await upload.load_files([
|
||||
new File([blob], `clipboard.${type.replace("image/", "")}`)
|
||||
@ -139,12 +144,18 @@
|
||||
|
||||
<div data-testid="image" class="image-container">
|
||||
{#if value?.url}
|
||||
<ClearImage on:remove_image={() => (value = null)} />
|
||||
<ClearImage
|
||||
on:remove_image={() => {
|
||||
value = null;
|
||||
dispatch("clear");
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="upload-container">
|
||||
<Upload
|
||||
hidden={value !== null || active_tool === "webcam"}
|
||||
bind:this={upload}
|
||||
bind:uploading
|
||||
bind:dragging
|
||||
filetype="image/*"
|
||||
on:load={handle_upload}
|
||||
@ -187,6 +198,7 @@
|
||||
on:click={() => handle_toolbar(source)}
|
||||
Icon={sources_meta[source].icon}
|
||||
size="large"
|
||||
label="{source}-image-toolbar-btn"
|
||||
padded={false}
|
||||
/>
|
||||
{/each}
|
||||
|
@ -15,8 +15,8 @@
|
||||
export let root: string;
|
||||
export let hidden = false;
|
||||
export let include_sources = false;
|
||||
export let uploading = false;
|
||||
|
||||
let uploading = false;
|
||||
let upload_id: string;
|
||||
let file_data: FileData[];
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user