diff --git a/.changeset/calm-lions-leave.md b/.changeset/calm-lions-leave.md new file mode 100644 index 0000000000000..ccf8c6f166e15 --- /dev/null +++ b/.changeset/calm-lions-leave.md @@ -0,0 +1,6 @@ +--- +"@gradio/imageeditor": patch +"gradio": patch +--- + +fix:Fix ImageEditor Size Issues diff --git a/demo/image_editor_canvas_size/run.ipynb b/demo/image_editor_canvas_size/run.ipynb new file mode 100644 index 0000000000000..43f38d527fa2b --- /dev/null +++ b/demo/image_editor_canvas_size/run.ipynb @@ -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} \ No newline at end of file diff --git a/demo/image_editor_canvas_size/run.py b/demo/image_editor_canvas_size/run.py new file mode 100644 index 0000000000000..8aec553ade3d7 --- /dev/null +++ b/demo/image_editor_canvas_size/run.py @@ -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() \ No newline at end of file diff --git a/gradio/components/image_editor.py b/gradio/components/image_editor.py index 5b286b5840666..7e21291c3c774 100644 --- a/gradio/components/image_editor.py +++ b/gradio/components/image_editor.py @@ -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. @@ -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 @@ -247,7 +249,23 @@ 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 @@ -255,6 +273,7 @@ def __init__( 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 @@ -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), diff --git a/gradio/templates.py b/gradio/templates.py index ea3a542c56ea0..f3e1a1d7af5bf 100644 --- a/gradio/templates.py +++ b/gradio/templates.py @@ -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: @@ -170,6 +171,7 @@ def __init__( layers=layers, canvas_size=canvas_size, show_fullscreen_button=show_fullscreen_button, + fixed_canvas=fixed_canvas, ) @@ -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, ): @@ -254,6 +257,7 @@ def __init__( canvas_size=canvas_size, show_fullscreen_button=show_fullscreen_button, placeholder=placeholder, + fixed_canvas=fixed_canvas, ) @@ -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: @@ -344,6 +349,7 @@ def __init__( layers=layers, canvas_size=canvas_size, show_fullscreen_button=show_fullscreen_button, + fixed_canvas=fixed_canvas, ) diff --git a/js/imageeditor/ImageEditor.stories.svelte b/js/imageeditor/ImageEditor.stories.svelte index 7a6fed1ec1823..bbb51e9f7f08d 100644 --- a/js/imageeditor/ImageEditor.stories.svelte +++ b/js/imageeditor/ImageEditor.stories.svelte @@ -42,6 +42,7 @@ interactive: "true", label: "Image Editor", show_label: true, + canvas_size: [800, 600], brush: { default_size: "auto", colors: ["#ff0000", "#00ff00", "#0000ff"], @@ -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", diff --git a/js/imageeditor/Index.svelte b/js/imageeditor/Index.svelte index d50361ea18972..428a1ef0ba0b4 100644 --- a/js/imageeditor/Index.svelte +++ b/js/imageeditor/Index.svelte @@ -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; @@ -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; @@ -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; @@ -136,7 +149,7 @@ padding={false} {elem_id} {elem_classes} - height={height || undefined} + {height} {width} allow_overflow={false} {container} @@ -171,7 +184,7 @@ padding={false} {elem_id} {elem_classes} - height={height || undefined} + height={height || safe_height} {width} allow_overflow={false} {container} @@ -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")} diff --git a/js/imageeditor/shared/ImageEditor.svelte b/js/imageeditor/shared/ImageEditor.svelte index 387feb4eada71..3c9f62e2bba32 100644 --- a/js/imageeditor/shared/ImageEditor.svelte +++ b/js/imageeditor/shared/ImageEditor.svelte @@ -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( @@ -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 { diff --git a/js/imageeditor/shared/InteractiveImageEditor.svelte b/js/imageeditor/shared/InteractiveImageEditor.svelte index 2461d6121ff88..8f23e0db7af9e 100644 --- a/js/imageeditor/shared/InteractiveImageEditor.svelte +++ b/js/imageeditor/shared/InteractiveImageEditor.svelte @@ -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<{ @@ -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]; @@ -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 @@ -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} > {#if transforms.includes("crop")} diff --git a/js/imageeditor/shared/tools/Sources.svelte b/js/imageeditor/shared/tools/Sources.svelte index 54fa74ce180e6..2162ef3363b8d 100644 --- a/js/imageeditor/shared/tools/Sources.svelte +++ b/js/imageeditor/shared/tools/Sources.svelte @@ -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(TOOL_KEY); const { pixi, dimensions, register_context, reset, editor_box } = @@ -110,7 +111,8 @@ $pixi.renderer, background, $pixi.resize, - max_height + canvas_size, + fixed_canvas ); $dimensions = await add_image.start(); @@ -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" >