Skip to content

Commit 22b47dc

Browse files
committed
allow developers to control layers more granularly
1 parent 875b2aa commit 22b47dc

File tree

9 files changed

+169
-192
lines changed

9 files changed

+169
-192
lines changed

demo/image_editor/run.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ def update_editor_settings(
236236
border_region=100,
237237
canvas_size=[600, 600],
238238
type="numpy",
239+
layers=gr.LayerOptions(
240+
allow_additional_layers=False, layers=["Mask", "Mask 2"]
241+
),
239242
)
240243
im_preview = gr.Image(label="Preview")
241244

gradio/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
component,
6565
)
6666
from gradio.components.audio import WaveformOptions
67-
from gradio.components.image_editor import Brush, Eraser
67+
from gradio.components.image_editor import Brush, Eraser, LayerOptions
6868
from gradio.data_classes import FileData
6969
from gradio.events import (
7070
CopyData,
@@ -175,6 +175,7 @@
175175
"Json",
176176
"KeyUpData",
177177
"Label",
178+
"LayerOptions",
178179
"LikeData",
179180
"LinePlot",
180181
"List",

gradio/components/image_editor.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,25 @@ def __post_init__(self):
117117
)
118118

119119

120+
@document()
121+
@dataclasses.dataclass
122+
class LayerOptions:
123+
"""
124+
A dataclass for specifying options for the layer tool in the ImageEditor component. An instance of this class can be passed to the `layers` parameter of `gr.ImageEditor`.
125+
Parameters:
126+
allow_additional_layers: If True, users can add additional layers to the image. If False, the add layer button will not be shown.
127+
layers: A list of layers to make available to the user when using the layer tool. One layer must be provided, if the length of the list is 0 then a layer will be generated automatically.
128+
"""
129+
130+
allow_additional_layers: bool = True
131+
layers: list[str] | None = None
132+
disabled: bool = False
133+
134+
def __post_init__(self):
135+
if self.layers is None or len(self.layers) == 0:
136+
self.layers = ["Layer 1"]
137+
138+
120139
@document()
121140
class ImageEditor(Component):
122141
"""
@@ -215,7 +234,7 @@ def __init__(
215234
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).
216235
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).
217236
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.
218-
layers: If True, will allow users to add layers to the image. If False, the layers option will be hidden.
237+
layers: The options for the layer tool in the image editor. Can be a boolean or an instance of the `gr.LayerOptions` class. If True, will allow users to add layers to the image. If False, the layers option will be hidden. If an instance of `gr.LayerOptions`, it will be used to configure the layer tool. [See `gr.LayerOptions` docs](#layer-options).
219238
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.
220239
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.
221240
show_fullscreen_button: If True, will display button to view image in fullscreen mode.
@@ -275,7 +294,11 @@ def __init__(
275294
self.brush = Brush() if brush is None else brush
276295
self.blob_storage: dict[str, EditorDataBlobs] = {}
277296
self.format = format
278-
self.layers = layers
297+
self.layers = (
298+
LayerOptions()
299+
if layers is True
300+
else LayerOptions(disabled=True) if layers is False else layers
301+
)
279302
self.canvas_size = canvas_size
280303
self.fixed_canvas = fixed_canvas
281304
self.show_fullscreen_button = show_fullscreen_button

js/imageeditor/Index.svelte

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import { StatusTracker } from "@gradio/statustracker";
1212
import type { LoadingStatus } from "@gradio/statustracker";
1313
import { tick } from "svelte";
14-
14+
import type { LayerOptions } from "./shared/types";
1515
export let elem_id = "";
1616
export let elem_classes: string[] = [];
1717
export let visible = true;
@@ -45,7 +45,7 @@
4545
export let brush: Brush;
4646
export let eraser: Eraser;
4747
export let transforms: "crop"[] = ["crop"];
48-
export let layers = true;
48+
export let layers: LayerOptions;
4949
export let attached_events: string[] = [];
5050
export let server: {
5151
accept_blobs: (a: any) => void;
@@ -54,7 +54,6 @@
5454
export let fixed_canvas = false;
5555
export let show_fullscreen_button = true;
5656
export let full_history: any = null;
57-
5857
export let gradio: Gradio<{
5958
change: never;
6059
error: string;
@@ -86,7 +85,6 @@
8685
}
8786
8887
let dragging: boolean;
89-
9088
$: value && handle_change();
9189
const is_browser = typeof window !== "undefined";
9290
const raf = is_browser

js/imageeditor/InteractiveImageEditor.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import { Image as ImageIcon } from "@gradio/icons";
2929
import { inject } from "./shared/utils/parse_placeholder";
3030
// import Sources from "./shared/image/Sources.svelte";
31-
31+
import { type LayerOptions } from "./shared/types";
3232
export let brush: IBrush;
3333
export let eraser: Eraser;
3434
export let sources: ("clipboard" | "webcam" | "upload")[];
@@ -43,7 +43,7 @@
4343
composite: null,
4444
};
4545
// export let transforms: "crop"[] = ["crop"];
46-
export let layers: boolean;
46+
export let layers: LayerOptions;
4747
export let transforms: "crop"[] = ["crop"];
4848
export let accept_blobs: (a: any) => void;
4949
export let status:
@@ -60,7 +60,6 @@
6060
export let dragging: boolean;
6161
export let placeholder: string | undefined = undefined;
6262
export let border_region: number;
63-
6463
export let full_history: CommandNode | null = null;
6564
6665
const dispatch = createEventDispatcher<{
@@ -244,6 +243,7 @@
244243
eraser_options={eraser}
245244
{fixed_canvas}
246245
{border_region}
246+
layer_options={layers}
247247
>
248248
{#if !background_image && current_tool === "image"}
249249
<div class="empty wrap">

js/imageeditor/shared/ImageEditor.svelte

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import { create_drag } from "@gradio/upload";
3333
import Layers from "./Layers.svelte";
3434
import { Check } from "@gradio/icons";
35-
35+
import { type LayerOptions } from "./types";
3636
const { drag, open_file_upload } = create_drag();
3737
3838
interface WeirdTypeData {
@@ -78,6 +78,8 @@
7878
export let layers: WeirdTypeData[];
7979
export let background: WeirdTypeData;
8080
export let border_region: number;
81+
export let layer_options: LayerOptions;
82+
8183
/**
8284
* Gets the image blobs from the editor
8385
* @returns {Promise<ImageBlobs>} Object containing background, layers, and composite image blobs
@@ -96,7 +98,7 @@
9698
* @param {Blob | File} image - The image to add
9799
*/
98100
export function add_image(image: Blob | File): void {
99-
editor.add_image({ image, border_region });
101+
editor.add_image({ image });
100102
}
101103
102104
let pending_bg: Promise<void>;
@@ -201,6 +203,7 @@
201203
tools: ["image", zoom, new ResizeTool(), brush],
202204
fixed_canvas,
203205
border_region,
206+
layer_options,
204207
});
205208
206209
crop_zoom = new ZoomTool();
@@ -413,12 +416,10 @@
413416
$: add_layers_from_url(layers);
414417
415418
async function handle_crop_confirm(): Promise<void> {
416-
const { image, width, height, x, y, original_dimensions } =
417-
await crop.get_crop_bounds();
419+
const { image, x, y, original_dimensions } = await crop.get_crop_bounds();
418420
if (!image) return;
419421
editor.add_image({
420422
image,
421-
border_region,
422423
resize: false,
423424
crop_offset: { x, y },
424425
original_dimensions,
@@ -517,8 +518,9 @@
517518
</div>
518519
{/if}
519520

520-
{#if current_subtool !== "crop"}
521+
{#if current_subtool !== "crop" && !layer_options.disabled}
521522
<Layers
523+
enable_additional_layers={layer_options.allow_additional_layers}
522524
layers={editor.layers}
523525
on:new_layer={() => {
524526
editor.add_layer();

js/imageeditor/shared/Layers.svelte

Lines changed: 17 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -19,125 +19,10 @@
1919
2020
export let layers: Writable<{
2121
active_layer: string;
22-
layers: { name: string; id: string }[];
22+
layers: { name: string; id: string; user_created: boolean }[];
2323
}>;
24-
// let show_layers = false;
2524
26-
// export let layer_files: (FileData | null)[] | null = [];
27-
// export let enable_layers = true;
28-
29-
// const {
30-
// pixi,
31-
// current_layer,
32-
// dimensions,
33-
// register_context,
34-
// command_manager,
35-
// current_history
36-
// } = getContext<EditorContext>(EDITOR_KEY);
37-
38-
// const { can_undo } = command_manager;
39-
40-
// const LayerManager = layer_manager();
41-
// const manager_current_layer = LayerManager.active_layer;
42-
// const layers = LayerManager.layers;
43-
44-
// $: current_layer.set($manager_current_layer);
45-
46-
// register_context("layers", {
47-
// init_fn: () => {
48-
// new_layer();
49-
// },
50-
// reset_fn: () => {
51-
// LayerManager.reset();
52-
// }
53-
// });
54-
55-
// async function validate_layers(): Promise<void> {
56-
// let invalid = $layers.some(
57-
// (layer) =>
58-
// layer.composite.texture?.width != $dimensions[0] ||
59-
// layer.composite.texture?.height != $dimensions[1]
60-
// );
61-
// if (invalid) {
62-
// LayerManager.reset();
63-
// if (!layer_files || layer_files.length == 0) new_layer();
64-
// else render_layer_files(layer_files);
65-
// }
66-
// }
67-
// $: $dimensions, validate_layers();
68-
69-
// async function new_layer(): Promise<void> {
70-
// if (!$pixi) return;
71-
72-
// const new_layer = LayerManager.add_layer(
73-
// $pixi.layer_container,
74-
// $pixi.renderer,
75-
// ...$dimensions
76-
// );
77-
78-
// if ($can_undo || $layers.length > 0) {
79-
// command_manager.execute(new_layer);
80-
// } else {
81-
// new_layer.execute();
82-
// }
83-
// }
84-
85-
// $: render_layer_files(layer_files);
86-
87-
// function is_not_null<T>(x: T | null): x is T {
88-
// return x !== null;
89-
// }
90-
91-
// async function render_layer_files(
92-
// _layer_files: typeof layer_files
93-
// ): Promise<void> {
94-
// await tick();
95-
// if (!_layer_files || _layer_files.length == 0) {
96-
// LayerManager.reset();
97-
// new_layer();
98-
// return;
99-
// }
100-
// if (!$pixi) return;
101-
102-
// const fetch_promises = await Promise.all(
103-
// _layer_files.map((f) => {
104-
// if (!f || !f.url) return null;
105-
106-
// return fetch(f.url);
107-
// })
108-
// );
109-
110-
// const blobs = await Promise.all(
111-
// fetch_promises.map((p) => {
112-
// if (!p) return null;
113-
// return p.blob();
114-
// })
115-
// );
116-
117-
// LayerManager.reset();
118-
119-
// for (const blob of blobs.filter(is_not_null)) {
120-
// const new_layer = await LayerManager.add_layer_from_blob(
121-
// $pixi.layer_container,
122-
// $pixi.renderer,
123-
// blob,
124-
// $pixi.view
125-
// );
126-
127-
// if ($can_undo && $layers.length === 0) {
128-
// command_manager.execute(new_layer);
129-
// } else {
130-
// new_layer.execute();
131-
// }
132-
// }
133-
// }
134-
135-
// onMount(async () => {
136-
// await tick();
137-
// if (!$pixi) return;
138-
139-
// $pixi = { ...$pixi!, get_layers: LayerManager.get_layers };
140-
// });
25+
export let enable_additional_layers = true;
14126
14227
export let enable_layers = true;
14328
export let show_layers = false;
@@ -176,7 +61,7 @@
17661
</button>
17762
{#if show_layers}
17863
<ul>
179-
{#each $layers.layers as { id, name }, i (i)}
64+
{#each $layers.layers as { id, name, user_created }, i (i)}
18065
<li>
18166
<button
18267
class:selected_layer={$layers.active_layer === id}
@@ -205,7 +90,7 @@
20590
size="x-small"
20691
/>
20792
{/if}
208-
{#if $layers.layers.length > 1}
93+
{#if $layers.layers.length > 1 && user_created}
20994
<IconButton
21095
on:click={(e) => {
21196
e.stopPropagation();
@@ -219,17 +104,19 @@
219104
{/if}
220105
</li>
221106
{/each}
222-
<li class="add-layer">
223-
<IconButton
224-
Icon={Plus}
225-
aria-label="Add Layer"
226-
on:click={(e) => {
227-
e.stopPropagation();
228-
new_layer();
229-
}}
230-
size="x-small"
231-
/>
232-
</li>
107+
{#if enable_additional_layers}
108+
<li class="add-layer">
109+
<IconButton
110+
Icon={Plus}
111+
aria-label="Add Layer"
112+
on:click={(e) => {
113+
e.stopPropagation();
114+
new_layer();
115+
}}
116+
size="x-small"
117+
/>
118+
</li>
119+
{/if}
233120
</ul>
234121
{/if}
235122
</div>

0 commit comments

Comments
 (0)