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:
Freddy Boulton 2023-11-16 09:45:48 -05:00 committed by GitHub
parent d92de491bf
commit 2f805a7dd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 136 additions and 11 deletions

View 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.

View File

@ -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: "..",

View 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}

View 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()

View File

@ -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"]:

View File

@ -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:

View File

@ -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

View 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");
});

View File

@ -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")}

View File

@ -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>

View File

@ -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}

View File

@ -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[];