Skip to content

Commit

Permalink
Image Fixes (#6441)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed Nov 16, 2023
1 parent d92de49 commit 2f805a7
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 11 deletions.
7 changes: 7 additions & 0 deletions .changeset/breezy-foxes-search.md
@@ -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.
7 changes: 6 additions & 1 deletion .config/playwright.config.js
@@ -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: "..",
Expand Down
1 change: 1 addition & 0 deletions demo/image_component_events/run.ipynb
@@ -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 changes: 20 additions & 0 deletions demo/image_component_events/run.py
@@ -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()
14 changes: 12 additions & 2 deletions gradio/components/image.py
Expand Up @@ -147,20 +147,30 @@ def preprocess(
) -> 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(
self, value: np.ndarray | _Image.Image | str | Path | None
) -> 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"]:
Expand Down
3 changes: 2 additions & 1 deletion gradio/image_utils.py
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
7 changes: 5 additions & 2 deletions gradio/processing_utils.py
Expand Up @@ -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

Expand Down
61 changes: 61 additions & 0 deletions js/app/test/image_component_events.spec.ts
@@ -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");
});
9 changes: 7 additions & 2 deletions js/image/Index.svelte
Expand Up @@ -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>
Expand Down Expand Up @@ -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")}
Expand Down
2 changes: 1 addition & 1 deletion js/image/shared/ImagePreview.svelte
Expand Up @@ -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>
Expand Down
14 changes: 13 additions & 1 deletion js/image/shared/ImageUploader.svelte
Expand Up @@ -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> {
Expand All @@ -52,6 +54,8 @@
pending = false;
}
$: if (uploading) value = null;
$: value && !value.url && (value = normalise_file(value, root, null));
const dispatch = createEventDispatcher<{
Expand Down Expand Up @@ -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/", "")}`)
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -187,6 +198,7 @@
on:click={() => handle_toolbar(source)}
Icon={sources_meta[source].icon}
size="large"
label="{source}-image-toolbar-btn"
padded={false}
/>
{/each}
Expand Down
2 changes: 1 addition & 1 deletion js/upload/src/Upload.svelte
Expand Up @@ -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[];
Expand Down

0 comments on commit 2f805a7

Please sign in to comment.