Skip to content

Commit

Permalink
Fix ImageEditor Size Issues (#10357)
Browse files Browse the repository at this point in the history
* Fix

* Add test

* Fix

* Format

* add changeset

* rm for now

* Revert "rm for now"

This reverts commit 3cfbe3c.

* rm for now

* tests

* fix tests

* dynamic canvas

* add code

* fix

* Add code

* add code

* crop size deprecation

* fix crop_size

* add code

* typecheck

* fix story

---------

Co-authored-by: gradio-pr-bot <[email protected]>
Co-authored-by: Suzana Ilic <[email protected]>
  • Loading branch information
3 people authored Jan 22, 2025
1 parent 812f2d8 commit 43e7cce
Show file tree
Hide file tree
Showing 15 changed files with 269 additions and 67 deletions.
6 changes: 6 additions & 0 deletions .changeset/calm-lions-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gradio/imageeditor": patch
"gradio": patch
---

fix:Fix ImageEditor Size Issues
1 change: 1 addition & 0 deletions demo/image_editor_canvas_size/run.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_editor_canvas_size"]}, {"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", " image = gr.ImageEditor(label=\"Default Canvas. Not fixed\", elem_id=\"default\")\n", " with gr.Column():\n", " custom_canvas = gr.ImageEditor(label=\"Custom Canvas, not fixed\", canvas_size=(300, 300),\n", " elem_id=\"small\")\n", " with gr.Column():\n", " custom_canvas_fixed = gr.ImageEditor(label=\"Custom Canvas,fixed\", canvas_size=(500, 500), fixed_canvas=True,\n", " elem_id=\"fixed\")\n", " with gr.Column():\n", " width = gr.Number(label=\"Width\")\n", " height = gr.Number(label=\"Height\")\n", "\n", " image.change(lambda x: x[\"composite\"].shape, outputs=[height, width], inputs=image)\n", " custom_canvas.change(lambda x: x[\"composite\"].shape, outputs=[height, width], inputs=custom_canvas)\n", " custom_canvas_fixed.change(lambda x: x[\"composite\"].shape, outputs=[height, width], inputs=custom_canvas_fixed)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
22 changes: 22 additions & 0 deletions demo/image_editor_canvas_size/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import gradio as gr

with gr.Blocks() as demo:
with gr.Row():
with gr.Column():
image = gr.ImageEditor(label="Default Canvas. Not fixed", elem_id="default")
with gr.Column():
custom_canvas = gr.ImageEditor(label="Custom Canvas, not fixed", canvas_size=(300, 300),
elem_id="small")
with gr.Column():
custom_canvas_fixed = gr.ImageEditor(label="Custom Canvas,fixed", canvas_size=(500, 500), fixed_canvas=True,
elem_id="fixed")
with gr.Column():
width = gr.Number(label="Width")
height = gr.Number(label="Height")

image.change(lambda x: x["composite"].shape, outputs=[height, width], inputs=image)
custom_canvas.change(lambda x: x["composite"].shape, outputs=[height, width], inputs=custom_canvas)
custom_canvas_fixed.change(lambda x: x["composite"].shape, outputs=[height, width], inputs=custom_canvas_fixed)

if __name__ == "__main__":
demo.launch()
35 changes: 25 additions & 10 deletions gradio/components/image_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,15 @@ def __init__(
brush: Brush | None | Literal[False] = None,
format: str = "webp",
layers: bool = True,
canvas_size: tuple[int, int] | None = None,
canvas_size: tuple[int, int] = (800, 800),
fixed_canvas: bool = False,
show_fullscreen_button: bool = True,
):
"""
Parameters:
value: Optional initial image(s) to populate the image editor. Should be a dictionary with keys: `background`, `layers`, and `composite`. The values corresponding to `background` and `composite` should be images or None, while `layers` should be a list of images. Images can be of type PIL.Image, np.array, or str filepath/URL. Or, the value can be a callable, in which case the function will be called whenever the app loads to set the initial value of the component.
height: The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image files or numpy arrays, but will affect the displayed images.
width: The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image files or numpy arrays, but will affect the displayed images.
height: The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image files or numpy arrays, but will affect the displayed images. Beware of conflicting values with the canvas_size paramter. If the canvas_size is larger than the height, the editing canvas will not fit in the component.
width: The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image files or numpy arrays, but will affect the displayed images. Beware of conflicting values with the canvas_size paramter. If the canvas_size is larger than the height, the editing canvas will not fit in the component.
image_mode: "RGB" if color, or "L" if black and white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning.
sources: List of sources that can be used to set the background image. "upload" creates a box where user can drop an image file, "webcam" allows user to take snapshot from their webcam, "clipboard" allows users to paste an image from the clipboard.
type: The format the images are converted to before being passed into the prediction function. "numpy" converts the images to numpy arrays with shape (height, width, 3) and values from 0 to 255, "pil" converts the images to PIL image objects, "filepath" passes images as str filepaths to temporary copies of the images.
Expand All @@ -206,13 +207,14 @@ def __init__(
placeholder: Custom text for the upload area. Overrides default upload messages when provided. Accepts new lines and `#` to designate a heading.
mirror_webcam: If True webcam will be mirrored. Default is True.
show_share_button: If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.
crop_size: The size of the crop box in pixels. If a tuple, the first value is the width and the second value is the height. If a string, the value must be a ratio in the form `width:height` (e.g. "16:9").
crop_size: Deprecated. Used to set the `canvas_size` parameter.
transforms: The transforms tools to make available to users. "crop" allows the user to crop the image.
eraser: The options for the eraser tool in the image editor. Should be an instance of the `gr.Eraser` class, or None to use the default settings. Can also be False to hide the eraser tool. [See `gr.Eraser` docs](#eraser).
brush: The options for the brush tool in the image editor. Should be an instance of the `gr.Brush` class, or None to use the default settings. Can also be False to hide the brush tool, which will also hide the eraser tool. [See `gr.Brush` docs](#brush).
format: Format to save image if it does not already have a valid format (e.g. if the image is being returned to the frontend as a numpy array or PIL Image). The format should be supported by the PIL library. This parameter has no effect on SVG files.
layers: If True, will allow users to add layers to the image. If False, the layers option will be hidden.
canvas_size: The size of the default canvas in pixels. If a tuple, the first value is the width and the second value is the height. If None, the canvas size will be the same as the background image or 800 x 600 if no background image is provided.
canvas_size: The size of the canvas in pixels. The first value is the width and the second value is the height. If its set, uploaded images will be rescaled to fit the canvas size while preserving the aspect ratio. The canvas size will always change to match the size of an uploaded image unless fixed_canvas is set to True.
fixed_canvas: If True, the canvas size will not change based on the size of the background image and the image will be rescaled to fit (while preserving the aspect ratio) and placed in the center of the canvas.
show_fullscreen_button: If True, will display button to view image in fullscreen mode.
"""
self._selectable = _selectable
Expand Down Expand Up @@ -247,14 +249,31 @@ def __init__(
else show_share_button
)

self.crop_size = crop_size
if crop_size is not None:
warnings.warn(
"`crop_size` parameter is deprecated. Please use `canvas_size` instead."
)
if isinstance(crop_size, str):
# convert ratio to tuple
proportion = [
int(crop_size.split(":")[0]),
int(crop_size.split(":")[1]),
]
ratio = proportion[0] / proportion[1]
canvas_size = (
(int(800 * ratio), 800) if ratio > 1 else (800, int(800 / ratio))
)
else:
canvas_size = (int(crop_size[0]), int(crop_size[1]))

self.transforms = transforms
self.eraser = Eraser() if eraser is None else eraser
self.brush = Brush() if brush is None else brush
self.blob_storage: dict[str, EditorDataBlobs] = {}
self.format = format
self.layers = layers
self.canvas_size = canvas_size
self.fixed_canvas = fixed_canvas
self.show_fullscreen_button = show_fullscreen_button
self.placeholder = placeholder

Expand Down Expand Up @@ -301,10 +320,6 @@ def convert_and_format_image(
with warnings.catch_warnings():
warnings.simplefilter("ignore")
im = im.convert(self.image_mode)
if self.crop_size and not isinstance(self.crop_size, str):
im = image_utils.crop_scale(
im, int(self.crop_size[0]), int(self.crop_size[1])
)
return image_utils.format_image(
im,
cast(Literal["numpy", "pil", "filepath"], self.type),
Expand Down
12 changes: 9 additions & 3 deletions gradio/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ def __init__(
brush: Brush | None = None,
format: str = "webp",
layers: bool = True,
canvas_size: tuple[int, int] | None = None,
canvas_size: tuple[int, int] = (800, 800),
fixed_canvas: bool = False,
show_fullscreen_button: bool = True,
):
if not brush:
Expand Down Expand Up @@ -170,6 +171,7 @@ def __init__(
layers=layers,
canvas_size=canvas_size,
show_fullscreen_button=show_fullscreen_button,
fixed_canvas=fixed_canvas,
)


Expand Down Expand Up @@ -217,7 +219,8 @@ def __init__(
brush: Brush | None = None,
format: str = "webp",
layers: bool = True,
canvas_size: tuple[int, int] | None = None,
canvas_size: tuple[int, int] = (800, 800),
fixed_canvas: bool = False,
show_fullscreen_button: bool = True,
placeholder: str | None = None,
):
Expand Down Expand Up @@ -254,6 +257,7 @@ def __init__(
canvas_size=canvas_size,
show_fullscreen_button=show_fullscreen_button,
placeholder=placeholder,
fixed_canvas=fixed_canvas,
)


Expand Down Expand Up @@ -306,7 +310,8 @@ def __init__(
brush: Brush | None = None,
format: str = "webp",
layers: bool = True,
canvas_size: tuple[int, int] | None = None,
canvas_size: tuple[int, int] = (800, 800),
fixed_canvas: bool = False,
show_fullscreen_button: bool = True,
):
if not brush:
Expand Down Expand Up @@ -344,6 +349,7 @@ def __init__(
layers=layers,
canvas_size=canvas_size,
show_fullscreen_button=show_fullscreen_button,
fixed_canvas=fixed_canvas,
)


Expand Down
2 changes: 2 additions & 0 deletions js/imageeditor/ImageEditor.stories.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
interactive: "true",
label: "Image Editor",
show_label: true,
canvas_size: [800, 600],
brush: {
default_size: "auto",
colors: ["#ff0000", "#00ff00", "#0000ff"],
Expand Down Expand Up @@ -216,6 +217,7 @@
type: "pil",
placeholder: "Upload an image of a cat",
sources: ["upload", "webcam"],
canvas_size: [800, 800],
interactive: "true",
brush: {
default_size: "auto",
Expand Down
23 changes: 19 additions & 4 deletions js/imageeditor/Index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
export let root: string;
export let value_is_output = false;
export let height: number | undefined = 450;
export let height: number | undefined;
export let width: number | undefined;
export let _selectable = false;
Expand All @@ -55,7 +55,8 @@
export let server: {
accept_blobs: (a: any) => void;
};
export let canvas_size: [number, number] | undefined;
export let canvas_size: [number, number];
export let fixed_canvas = false;
export let show_fullscreen_button = true;
export let full_history: any = null;
Expand Down Expand Up @@ -125,6 +126,18 @@
}
}
let dynamic_height: number | undefined = undefined;
// In case no height given, pick a height large enough for the entire canvas
// in pixi.ts, the max-height of the canvas is canvas height / pixel ratio
let safe_height_initial = Math.max(
canvas_size[1] / (is_browser ? window.devicePixelRatio : 1),
250
);
$: safe_height = Math.max((dynamic_height ?? safe_height_initial) + 100, 250);
$: has_value = value?.background || value?.layers?.length || value?.composite;
</script>

Expand All @@ -136,7 +149,7 @@
padding={false}
{elem_id}
{elem_classes}
height={height || undefined}
{height}
{width}
allow_overflow={false}
{container}
Expand Down Expand Up @@ -171,7 +184,7 @@
padding={false}
{elem_id}
{elem_classes}
height={height || undefined}
height={height || safe_height}
{width}
allow_overflow={false}
{container}
Expand All @@ -194,11 +207,13 @@
{crop_size}
{value}
bind:this={editor_instance}
bind:dynamic_height
{root}
{sources}
{label}
{show_label}
{height}
{fixed_canvas}
on:save={(e) => handle_save()}
on:edit={() => gradio.dispatch("edit")}
on:clear={() => gradio.dispatch("clear")}
Expand Down
6 changes: 6 additions & 0 deletions js/imageeditor/shared/ImageEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@
child_bottom: 0
});
export let canvas_height = undefined;
$: height = $editor_box.child_height;
$: canvas_height = $crop[3] * $editor_box.child_height + 1;
const crop = writable<[number, number, number, number]>([0, 0, 1, 1]);
const position_spring = spring(
Expand Down Expand Up @@ -408,6 +411,9 @@
.container {
position: relative;
margin: var(--spacing-md);
/* in case the canvas_size is really small */
/* set min-height so that upload text does not cover the toolbar */
min-height: 100px;
}
.no-border {
Expand Down
14 changes: 11 additions & 3 deletions js/imageeditor/shared/InteractiveImageEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@
| "error"
| "generating"
| "streaming" = "complete";
export let canvas_size: [number, number] | undefined;
export let canvas_size: [number, number];
export let fixed_canvas = false;
export let realtime: boolean;
export let upload: Client["upload"];
export let stream_handler: Client["stream"];
export let dragging: boolean;
export let placeholder: string | undefined = undefined;
export let height = 450;
export let dynamic_height: number | undefined = undefined;
export let height;
export let full_history: CommandNode | null = null;
const dispatch = createEventDispatcher<{
Expand Down Expand Up @@ -207,6 +209,10 @@
let active_mode: "webcam" | "color" | null = null;
let editor_height = height - 100;
let _dynamic_height: number;
$: dynamic_height = _dynamic_height;
$: [heading, paragraph] = placeholder ? inject(placeholder) : [false, false];
</script>

Expand All @@ -221,6 +227,7 @@
crop_size={Array.isArray(crop_size) ? crop_size : undefined}
bind:this={editor}
bind:height={editor_height}
bind:canvas_height={_dynamic_height}
parent_height={height}
{changeable}
on:save
Expand All @@ -242,10 +249,11 @@
{sources}
{upload}
{stream_handler}
{canvas_size}
bind:bg
bind:active_mode
background_file={value?.background || value?.composite || null}
max_height={height}
{fixed_canvas}
></Sources>

{#if transforms.includes("crop")}
Expand Down
13 changes: 9 additions & 4 deletions js/imageeditor/shared/tools/Sources.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
export let upload: Client["upload"];
export let stream_handler: Client["stream"];
export let dragging: boolean;
export let max_height: number;
export let canvas_size: [number, number];
export let fixed_canvas = false;
const { active_tool } = getContext<ToolContext>(TOOL_KEY);
const { pixi, dimensions, register_context, reset, editor_box } =
Expand Down Expand Up @@ -110,7 +111,8 @@
$pixi.renderer,
background,
$pixi.resize,
max_height
canvas_size,
fixed_canvas
);
$dimensions = await add_image.start();
Expand Down Expand Up @@ -187,8 +189,11 @@
class:click-disabled={!!bg ||
active_mode === "webcam" ||
$active_tool !== "bg"}
style:height="{$editor_box.child_height +
($editor_box.child_top - $editor_box.parent_top)}px"
style:height="{Math.max(
$editor_box.child_height +
($editor_box.child_top - $editor_box.parent_top),
100
)}px"
>
<Upload
hidden={bg || active_mode === "webcam" || $active_tool !== "bg"}
Expand Down
Loading

0 comments on commit 43e7cce

Please sign in to comment.