@gradio/imageeditor 0.4.11 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@
4
4
  export let selected_color: string;
5
5
  export let colors: string[];
6
6
  export let user_colors: (string | null)[] | null = [];
7
+ import { Palette } from "@gradio/icons";
7
8
 
8
9
  export let show_empty = false;
9
10
  export let current_mode: "hex" | "rgb" | "hsl" = "hex";
@@ -50,44 +51,75 @@
50
51
  }
51
52
  </script>
52
53
 
53
- {#if user_colors}
54
- <div class="swatch">
55
- {#each user_colors as color, i}
56
- <button
57
- on:click={() => handle_select("edit", { index: i, color })}
58
- class="color"
59
- class:empty={color === null}
60
- style="background-color: {color}"
61
- class:selected={`edit-${i}` === current_index}
62
- ></button>
63
- {/each}
64
-
65
- <button
66
- on:click={handle_picker_click}
67
- class="color colorpicker"
68
- class:hidden={!color_picker}
69
- ></button>
70
- </div>
54
+ {#if !color_picker}
55
+ <span class:lg={user_colors}></span>
71
56
  {/if}
72
- <menu class="swatch">
73
- {#each _colors as color, i}
74
- <button
75
- on:click={() => handle_select("select", { index: i, color })}
76
- class="color"
77
- class:empty={color === null}
78
- style="background-color: {color}"
79
- class:selected={`select-${i}` === current_index}
80
- ></button>
81
- {/each}
82
- </menu>
57
+
58
+ <div class="swatch-wrap">
59
+ <span class="icon-wrap">
60
+ <Palette />
61
+ </span>
62
+ <div>
63
+ {#if user_colors}
64
+ <div class="swatch">
65
+ {#each user_colors as color, i}
66
+ <button
67
+ on:click={() => handle_select("edit", { index: i, color })}
68
+ class="color"
69
+ class:empty={color === null}
70
+ style="background-color: {color}"
71
+ class:selected={`edit-${i}` === current_index}
72
+ ></button>
73
+ {/each}
74
+
75
+ <button
76
+ on:click={handle_picker_click}
77
+ class="color colorpicker"
78
+ class:hidden={!color_picker}
79
+ ></button>
80
+ </div>
81
+ {/if}
82
+ <menu class="swatch">
83
+ {#each _colors as color, i}
84
+ <button
85
+ on:click={() => handle_select("select", { index: i, color })}
86
+ class="color"
87
+ class:empty={color === null}
88
+ style="background-color: {color}"
89
+ class:selected={`select-${i}` === current_index}
90
+ ></button>
91
+ {/each}
92
+ </menu>
93
+ </div>
94
+ </div>
83
95
 
84
96
  <style>
97
+ .icon-wrap {
98
+ width: 18px;
99
+ margin-top: 6px;
100
+
101
+ margin-left: var(--size-4);
102
+ }
103
+
104
+ .swatch-wrap {
105
+ display: flex;
106
+ justify-content: space-between;
107
+ align-items: flex-start;
108
+ }
109
+ span {
110
+ margin-top: 0px;
111
+ }
112
+
113
+ span.lg {
114
+ margin-top: var(--spacing-xl);
115
+ }
85
116
  .swatch {
86
117
  display: flex;
87
- width: 100%;
88
- gap: var(--size-2);
89
- justify-content: center;
90
- margin-bottom: var(--size-1);
118
+ gap: var(--size-2-5);
119
+ justify-content: space-around;
120
+ margin-bottom: var(--size-2);
121
+ margin-right: var(--size-6);
122
+ padding-left: var(--size-2);
91
123
  }
92
124
 
93
125
  .empty {
@@ -96,7 +128,7 @@
96
128
  display: flex;
97
129
  justify-content: center;
98
130
  align-items: center;
99
- padding-top: 1px;
131
+ padding-top: 4px;
100
132
  text-align: center;
101
133
  font-size: var(--scale-0);
102
134
  cursor: pointer;
@@ -68,14 +68,17 @@
68
68
  t_p = y_offset / $editor_box.child_height;
69
69
 
70
70
  c = crop_canvas($pixi!.renderer, $pixi!.mask_container, crop, 0.2);
71
- c.start(...$dimensions, current_crop);
72
-
73
- c.continue([
74
- l_p * $dimensions[0],
75
- t_p * $dimensions[1],
76
- w_p * $dimensions[0],
77
- h_p * $dimensions[1]
78
- ]);
71
+ c.start(...$dimensions, current_crop, false);
72
+
73
+ c.continue(
74
+ [
75
+ l_p * $dimensions[0],
76
+ t_p * $dimensions[1],
77
+ w_p * $dimensions[0],
78
+ h_p * $dimensions[1]
79
+ ],
80
+ false
81
+ );
79
82
  c.stop();
80
83
 
81
84
  c.execute();
@@ -167,12 +170,7 @@
167
170
 
168
171
  $: $editor_box && get_measurements();
169
172
 
170
- onMount(() =>
171
- register_tool("crop", {
172
- default: "crop",
173
- options: []
174
- })
175
- );
173
+ onMount(() => register_tool("crop"));
176
174
  </script>
177
175
 
178
176
  {#if $active_tool === "crop" && measured}
@@ -188,6 +186,3 @@
188
186
  on:crop_end={({ detail }) => handle_crop("stop", detail)}
189
187
  />
190
188
  {/if}
191
-
192
- <style>
193
- </style>
@@ -332,6 +332,7 @@
332
332
  left: 0;
333
333
  width: 100%;
334
334
  height: 100%;
335
+
335
336
  /* transform: translateY(-20px); */
336
337
  }
337
338
 
@@ -340,5 +341,6 @@
340
341
  width: 100%;
341
342
  height: 100%;
342
343
  border: 1px solid black;
344
+ z-index: var(--layer-top);
343
345
  }
344
346
  </style>
@@ -122,7 +122,7 @@
122
122
  --hitbox-corner-offset: -25px;
123
123
  --handle-corner-offset: calc(50% - 5px);
124
124
  --handle-mid-offset: calc(50% - 2px);
125
- z-index: 1;
125
+ z-index: var(--layer-top);
126
126
  }
127
127
 
128
128
  /* hitbox positions */
@@ -1,15 +1,16 @@
1
1
  <script lang="ts">
2
- import { getContext, onMount, tick } from "svelte";
2
+ import { getContext, onMount, tick, createEventDispatcher } from "svelte";
3
3
  import { type ToolContext, TOOL_KEY } from "./Tools.svelte";
4
4
  import { type EditorContext, EDITOR_KEY } from "../ImageEditor.svelte";
5
5
  import {
6
- Upload as UploadIcon,
6
+ Image as ImageIcon,
7
7
  Webcam as WebcamIcon,
8
8
  ImagePaste
9
9
  } from "@gradio/icons";
10
10
  import { Upload } from "@gradio/upload";
11
11
  import { Webcam } from "@gradio/image";
12
12
  import { type I18nFormatter } from "@gradio/utils";
13
+ import { IconButton } from "@gradio/atoms";
13
14
 
14
15
  import { add_bg_color, add_bg_image } from "./sources";
15
16
  import type { FileData } from "@gradio/client";
@@ -24,21 +25,27 @@
24
25
  export let mirror_webcam = true;
25
26
  export let i18n: I18nFormatter;
26
27
 
27
- const { active_tool, register_tool } = getContext<ToolContext>(TOOL_KEY);
28
+ const { active_tool } = getContext<ToolContext>(TOOL_KEY);
28
29
  const { pixi, dimensions, register_context, reset, editor_box } =
29
30
  getContext<EditorContext>(EDITOR_KEY);
30
31
 
31
- let active_mode: "webcam" | "color" | null = null;
32
+ export let active_mode: "webcam" | "color" | null = null;
32
33
  let background: Blob | File | null;
33
34
 
35
+ const dispatch = createEventDispatcher<{
36
+ upload: never;
37
+ }>();
38
+
34
39
  const sources_meta = {
35
40
  upload: {
36
- icon: UploadIcon,
41
+ icon: ImageIcon,
37
42
  label: "Upload",
38
43
  order: 0,
39
44
  id: "bg_upload",
40
45
  cb() {
41
46
  upload.open_file_upload();
47
+
48
+ $active_tool = "bg";
42
49
  }
43
50
  },
44
51
  webcam: {
@@ -48,6 +55,7 @@
48
55
  id: "bg_webcam",
49
56
  cb() {
50
57
  active_mode = "webcam";
58
+ $active_tool = "bg";
51
59
  }
52
60
  },
53
61
  clipboard: {
@@ -57,6 +65,7 @@
57
65
  id: "bg_clipboard",
58
66
  cb() {
59
67
  process_clipboard();
68
+ $active_tool = null;
60
69
  }
61
70
  }
62
71
  } as const;
@@ -148,18 +157,25 @@
148
157
  },
149
158
  reset_fn: () => {}
150
159
  });
151
-
152
- onMount(() => {
153
- return register_tool("bg", {
154
- default: "bg_upload",
155
- options: sources_list || []
156
- });
157
- });
158
160
  </script>
159
161
 
160
162
  <svelte:window on:keydown={handle_key} />
161
163
 
162
- {#if $active_tool === "bg"}
164
+ {#if sources.length}
165
+ <div class="source-wrap">
166
+ {#each sources_list as { icon, label, id, cb } (id)}
167
+ <IconButton
168
+ Icon={icon}
169
+ size="medium"
170
+ padded={false}
171
+ label={label + " button"}
172
+ hasPopup={true}
173
+ transparent={true}
174
+ on:click={cb}
175
+ />
176
+ {/each}
177
+ <span class="sep"></span>
178
+ </div>
163
179
  <div class="upload-container">
164
180
  <Upload
165
181
  hidden={true}
@@ -203,7 +219,6 @@
203
219
  width: 100%;
204
220
  left: 0;
205
221
  right: 0;
206
- background-color: rgba(0, 0, 0, 0.9);
207
222
  margin: auto;
208
223
  z-index: var(--layer-top);
209
224
  display: flex;
@@ -213,4 +228,20 @@
213
228
  .modal-inner {
214
229
  width: 100%;
215
230
  }
231
+
232
+ .sep {
233
+ height: 12px;
234
+ background-color: var(--block-border-color);
235
+ width: 1px;
236
+ display: block;
237
+ margin-left: var(--spacing-xl);
238
+ }
239
+
240
+ .source-wrap {
241
+ display: flex;
242
+ justify-content: center;
243
+ align-items: center;
244
+ margin-left: var(--spacing-lg);
245
+ height: 100%;
246
+ }
216
247
  </style>
@@ -28,18 +28,16 @@
28
28
  }
29
29
 
30
30
  export interface ToolContext {
31
- register_tool: (type: tool, meta: ToolMeta) => () => void;
31
+ register_tool: (type: tool, opts?: { cb: () => void }) => () => void;
32
32
  active_tool: {
33
+ set: (tool: tool) => void;
33
34
  subscribe(
34
35
  this: void,
35
36
  run: Subscriber<tool | null>,
36
37
  invalidate?: Invalidator<tool | null>
37
38
  ): Unsubscriber;
38
39
  };
39
- activate_subtool: (
40
- sub_tool: upload_tool | transform_tool | brush_tool | eraser_tool | null,
41
- cb?: (...args: any[]) => void
42
- ) => void;
40
+
43
41
  current_color: Writable<string>;
44
42
  }
45
43
  </script>
@@ -52,28 +50,22 @@
52
50
  import { Image, Crop, Brush, Erase } from "@gradio/icons";
53
51
  import { type I18nFormatter } from "@gradio/utils";
54
52
 
55
- const { current_history, active_tool } =
53
+ const { active_tool, toolbar_box, editor_box } =
56
54
  getContext<EditorContext>(EDITOR_KEY);
57
55
 
58
56
  export let i18n: I18nFormatter;
59
57
 
60
58
  let tools: tool[] = [];
61
59
 
62
- const metas: Record<tool, ToolMeta | null> = {
63
- draw: null,
64
- erase: null,
65
- crop: null,
66
- bg: null
67
- };
68
-
69
- $: sub_menu = $active_tool && metas[$active_tool];
60
+ const cbs: Record<string, () => void> = {};
70
61
  let current_color = writable("#000000");
71
- let sub_tool: upload_tool | transform_tool | brush_tool | eraser_tool | null;
72
62
  const tool_context: ToolContext = {
73
63
  current_color,
74
- register_tool: (type: tool, meta: ToolMeta) => {
64
+ register_tool: (type: tool, opts?: { cb: () => void }) => {
75
65
  tools = [...tools, type];
76
- metas[type] = meta;
66
+ if (opts?.cb) {
67
+ cbs[type] = opts.cb;
68
+ }
77
69
 
78
70
  return () => {
79
71
  tools = tools.filter((tool) => tool !== type);
@@ -81,15 +73,8 @@
81
73
  },
82
74
 
83
75
  active_tool: {
84
- subscribe: active_tool.subscribe
85
- },
86
-
87
- activate_subtool: (
88
- _sub_tool: upload_tool | transform_tool | brush_tool | eraser_tool | null,
89
- cb?: (...args: any[]) => void
90
- ) => {
91
- sub_tool = _sub_tool;
92
- if (cb) cb();
76
+ subscribe: active_tool.subscribe,
77
+ set: active_tool.set
93
78
  }
94
79
  };
95
80
 
@@ -103,11 +88,6 @@
103
88
  icon: typeof Image;
104
89
  }
105
90
  > = {
106
- bg: {
107
- order: 0,
108
- label: i18n("Image"),
109
- icon: Image
110
- },
111
91
  crop: {
112
92
  order: 1,
113
93
  label: i18n("Transform"),
@@ -122,44 +102,49 @@
122
102
  order: 2,
123
103
  label: i18n("Erase"),
124
104
  icon: Erase
105
+ },
106
+ bg: {
107
+ order: 0,
108
+ label: i18n("Background"),
109
+ icon: Image
125
110
  }
126
111
  } as const;
112
+
113
+ let toolbar_width: number;
114
+ let toolbar_wrap: HTMLDivElement;
115
+
116
+ $: toolbar_width, $editor_box, get_dimensions();
117
+
118
+ function get_dimensions(): void {
119
+ if (!toolbar_wrap) return;
120
+ $toolbar_box = toolbar_wrap.getBoundingClientRect();
121
+ }
122
+
123
+ function handle_click(e: Event, tool: tool): void {
124
+ e.stopPropagation();
125
+ $active_tool = tool;
126
+ cbs[tool] && cbs[tool]();
127
+ }
127
128
  </script>
128
129
 
129
130
  <slot />
130
131
 
131
- <div class="toolbar-wrap">
132
- <Toolbar show_border={false}>
133
- {#if sub_menu}
134
- {#each sub_menu.options as meta (meta.id)}
135
- <IconButton
136
- highlight={sub_tool === meta.id && meta.id !== "brush_size"}
137
- color={$active_tool === "draw" && meta.id === "brush_size"
138
- ? $current_color
139
- : undefined}
140
- on:click={() => tool_context.activate_subtool(meta.id, meta.cb)}
141
- Icon={meta.icon}
142
- size="large"
143
- padded={false}
144
- label={meta.label + " button"}
145
- hasPopup={true}
146
- transparent={true}
147
- />
148
- {/each}
149
- {/if}
150
- </Toolbar>
151
-
132
+ <div
133
+ class="toolbar-wrap"
134
+ bind:clientWidth={toolbar_width}
135
+ bind:this={toolbar_wrap}
136
+ >
152
137
  <Toolbar show_border={false}>
153
138
  {#each tools as tool (tool)}
154
139
  <IconButton
155
- disabled={tool === "bg" && !!$current_history.previous}
156
140
  highlight={$active_tool === tool}
157
- on:click={() => ($active_tool = tool)}
141
+ on:click={(e) => handle_click(e, tool)}
158
142
  Icon={tools_meta[tool].icon}
159
- size="large"
143
+ size="medium"
160
144
  padded={false}
161
145
  label={tools_meta[tool].label + " button"}
162
146
  transparent={true}
147
+ offset={tool === "draw" ? -2 : tool === "erase" ? -6 : 0}
163
148
  />
164
149
  {/each}
165
150
  </Toolbar>
@@ -168,9 +153,9 @@
168
153
  <style>
169
154
  .toolbar-wrap {
170
155
  display: flex;
171
- flex-direction: column;
172
156
  justify-content: center;
173
157
  align-items: center;
174
- width: 100%;
158
+ margin-left: var(--spacing-xl);
159
+ height: 100%;
175
160
  }
176
161
  </style>
@@ -14,10 +14,14 @@ export interface CropCommand extends Command {
14
14
  width: number,
15
15
  height: number,
16
16
  previous_crop: [number, number, number, number],
17
+ preview?: boolean,
17
18
  set_previous?: boolean
18
19
  ) => void;
19
20
  stop: () => number;
20
- continue: (crop_size: [number, number, number, number]) => void;
21
+ continue: (
22
+ crop_size: [number, number, number, number],
23
+ preview?: boolean
24
+ ) => void;
21
25
  }
22
26
 
23
27
  export function crop_canvas(
@@ -74,6 +78,7 @@ export function crop_canvas(
74
78
  _width: number,
75
79
  _height: number,
76
80
  _previous_crop: [number, number, number, number],
81
+ _preview = true,
77
82
  set_previous = true
78
83
  ) => {
79
84
  clean = false;
@@ -82,7 +87,7 @@ export function crop_canvas(
82
87
  height: _height
83
88
  });
84
89
 
85
- crop_mask(_width, _height, _previous_crop, true);
90
+ crop_mask(_width, _height, _previous_crop, _preview);
86
91
  sprite = new Sprite(text);
87
92
  mask_container.mask = sprite;
88
93
  width = _width;
@@ -90,10 +95,10 @@ export function crop_canvas(
90
95
  if (set_previous)
91
96
  previous_crop = JSON.parse(JSON.stringify(_previous_crop));
92
97
  },
93
- continue: (crop_size: [number, number, number, number]) => {
98
+ continue: (crop_size: [number, number, number, number], preview = true) => {
94
99
  final_crop = JSON.parse(JSON.stringify(crop_size));
95
100
  if (spring_value === 0.2) {
96
- crop_mask(width, height, final_crop, true);
101
+ crop_mask(width, height, final_crop, preview);
97
102
  } else {
98
103
  alpha_spring.set(0.2);
99
104
  }
@@ -1,3 +1,5 @@
1
+ import { tick } from "svelte";
2
+
1
3
  /**
2
4
  * Svelte action to handle clicks outside of a DOM node
3
5
  * @param node DOM node to check the click is outside of