@gradio/imageeditor 0.4.10 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @gradio/imageeditor
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Features
6
+
7
+ - [#8042](https://github.com/gradio-app/gradio/pull/8042) [`92139f3`](https://github.com/gradio-app/gradio/commit/92139f3d7d6b832b649ff1f6c10c87e6fb522cde) - refresh the `ImageEditor` UI. Thanks @pngwn!
8
+ - [#8059](https://github.com/gradio-app/gradio/pull/8059) [`074ce38`](https://github.com/gradio-app/gradio/commit/074ce3805ada6ba829c17da3af5f186d9f5f9865) - ensure the `ImageEditor` works correctly with layers and `change` events. Thanks @pngwn!
9
+ - [#7845](https://github.com/gradio-app/gradio/pull/7845) [`dbb7373`](https://github.com/gradio-app/gradio/commit/dbb7373dde69d4ed2741942b5a1898f8620cec24) - ensure `ImageEditor` events work as expected. Thanks @pngwn!
10
+
11
+ ### Fixes
12
+
13
+ - [#8046](https://github.com/gradio-app/gradio/pull/8046) [`d6c289b`](https://github.com/gradio-app/gradio/commit/d6c289b34668f0011676737ee8edb4cb62b0620a) - round [x, y, w, h] before cropping to avoid unexpected interpolation on pixel values. Thanks @ernestchu!
14
+ - [#7959](https://github.com/gradio-app/gradio/pull/7959) [`2a5cb97`](https://github.com/gradio-app/gradio/commit/2a5cb978240960fd8b150a1eae6ae8328a5c0c59) - ensure `ImageEditor` always draws at the correct position. Thanks @hrrbay!
15
+
16
+ ### Dependency updates
17
+
18
+ - @gradio/utils@0.3.2
19
+ - @gradio/statustracker@0.4.12
20
+ - @gradio/client@0.16.0
21
+ - @gradio/upload@0.8.5
22
+ - @gradio/atoms@0.7.0
23
+ - @gradio/icons@0.4.0
24
+ - @gradio/image@0.9.12
25
+
26
+ ## 0.4.11
27
+
28
+ ### Fixes
29
+
30
+ - [#7817](https://github.com/gradio-app/gradio/pull/7817) [`867ff16`](https://github.com/gradio-app/gradio/commit/867ff16cd47ceb5bbf076503538a262b6fe64638) - Trigger the "clear" event of Image Editor. Thanks @uebian!
31
+
32
+ ### Dependency updates
33
+
34
+ - @gradio/utils@0.3.1
35
+ - @gradio/atoms@0.6.2
36
+ - @gradio/statustracker@0.4.11
37
+ - @gradio/upload@0.8.4
38
+ - @gradio/image@0.9.11
39
+ - @gradio/client@0.15.1
40
+
3
41
  ## 0.4.10
4
42
 
5
43
  ### Dependency updates
@@ -26,7 +26,11 @@
26
26
  class="image-container"
27
27
  style="width: 500px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
28
28
  >
29
- <ImageEditor i18n={get(format)} {...args} />
29
+ <ImageEditor
30
+ i18n={get(format)}
31
+ {...args}
32
+ server={{ accept_blobs: () => {} }}
33
+ />
30
34
  </div>
31
35
  </Template>
32
36
 
@@ -118,7 +122,7 @@
118
122
  coords: { clientX: 100, clientY: 100 }
119
123
  });
120
124
 
121
- await userEvent.click(canvas.getByLabelText("Color button"));
125
+ await userEvent.click(canvas.getByLabelText("Draw button"));
122
126
 
123
127
  var availableColors = document.querySelectorAll(
124
128
  "button.color:not(.empty):not(.selected):not(.hidden)"
package/Index.svelte CHANGED
@@ -1,4 +1,4 @@
1
- <svelte:options accessors={true} />
1
+ <svelte:options accessors={true} immutable={true} />
2
2
 
3
3
  <script lang="ts">
4
4
  import type { Brush, Eraser } from "./shared/tools/Brush.svelte";
@@ -13,6 +13,7 @@
13
13
  import { Block } from "@gradio/atoms";
14
14
  import { StatusTracker } from "@gradio/statustracker";
15
15
  import type { LoadingStatus } from "@gradio/statustracker";
16
+ import { tick } from "svelte";
16
17
 
17
18
  export let elem_id = "";
18
19
  export let elem_classes: string[] = [];
@@ -26,6 +27,7 @@
26
27
  export let show_label: boolean;
27
28
  export let show_download_button: boolean;
28
29
  export let root: string;
30
+ export let value_is_output = false;
29
31
 
30
32
  export let height: number | undefined;
31
33
  export let width: number | undefined;
@@ -47,16 +49,19 @@
47
49
  export let eraser: Eraser;
48
50
  export let crop_size: [number, number] | `${string}:${string}` | null = null;
49
51
  export let transforms: "crop"[] = ["crop"];
50
-
52
+ export let layers = true;
51
53
  export let attached_events: string[] = [];
54
+ export let server: {
55
+ accept_blobs: (a: any) => void;
56
+ };
52
57
 
53
58
  export let gradio: Gradio<{
54
59
  change: never;
55
60
  error: string;
56
61
  input: never;
57
62
  edit: never;
58
- stream: never;
59
63
  drag: never;
64
+ apply: never;
60
65
  upload: never;
61
66
  clear: never;
62
67
  select: SelectData;
@@ -64,13 +69,16 @@
64
69
  }>;
65
70
 
66
71
  let editor_instance: InteractiveImageEditor;
72
+ let image_id: null | string = null;
73
+
74
+ export async function get_value(): Promise<ImageBlobs | { id: string }> {
75
+ if (image_id) {
76
+ const val = { id: image_id };
77
+ image_id = null;
78
+ return val;
79
+ }
67
80
 
68
- export async function get_value(): Promise<ImageBlobs> {
69
- // @ts-ignore
70
- loading_status = { status: "pending" };
71
81
  const blobs = await editor_instance.get_data();
72
- // @ts-ignore
73
- loading_status = { status: "complete" };
74
82
 
75
83
  return blobs;
76
84
  }
@@ -79,7 +87,15 @@
79
87
 
80
88
  $: value && handle_change();
81
89
 
82
- function handle_change(): void {
90
+ function wait_for_next_frame(): Promise<void> {
91
+ return new Promise((resolve) => {
92
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
93
+ });
94
+ }
95
+
96
+ async function handle_change(): Promise<void> {
97
+ await wait_for_next_frame();
98
+
83
99
  if (
84
100
  value &&
85
101
  (value.background || value.layers?.length || value.composite)
@@ -89,8 +105,15 @@
89
105
  }
90
106
 
91
107
  function handle_save(): void {
108
+ gradio.dispatch("apply");
109
+ }
110
+
111
+ function handle_history_change(): void {
92
112
  gradio.dispatch("change");
93
- gradio.dispatch("input");
113
+ if (!value_is_output) {
114
+ gradio.dispatch("input");
115
+ tick().then((_) => (value_is_output = false));
116
+ }
94
117
  }
95
118
  </script>
96
119
 
@@ -149,6 +172,8 @@
149
172
  />
150
173
 
151
174
  <InteractiveImageEditor
175
+ on:change={() => handle_history_change()}
176
+ bind:image_id
152
177
  {crop_size}
153
178
  {value}
154
179
  bind:this={editor_instance}
@@ -159,10 +184,8 @@
159
184
  on:save={(e) => handle_save()}
160
185
  on:edit={() => gradio.dispatch("edit")}
161
186
  on:clear={() => gradio.dispatch("clear")}
162
- on:stream={() => gradio.dispatch("stream")}
163
187
  on:drag={({ detail }) => (dragging = detail)}
164
188
  on:upload={() => gradio.dispatch("upload")}
165
- on:select={({ detail }) => gradio.dispatch("select", detail)}
166
189
  on:share={({ detail }) => gradio.dispatch("share", detail)}
167
190
  on:error={({ detail }) => {
168
191
  loading_status = loading_status || {};
@@ -172,9 +195,12 @@
172
195
  on:error
173
196
  {brush}
174
197
  {eraser}
175
- changeable={attached_events.includes("change")}
198
+ changeable={attached_events.includes("apply")}
176
199
  i18n={gradio.i18n}
177
200
  {transforms}
201
+ accept_blobs={server.accept_blobs}
202
+ {layers}
203
+ status={loading_status?.status}
178
204
  ></InteractiveImageEditor>
179
205
  </Block>
180
206
  {/if}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/imageeditor",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,13 +10,13 @@
10
10
  "@types/tinycolor2": "^1.4.6",
11
11
  "pixi.js": "^7.3.2",
12
12
  "tinycolor2": "^1.6.0",
13
- "@gradio/atoms": "^0.6.1",
14
- "@gradio/client": "^0.15.0",
15
- "@gradio/statustracker": "^0.4.10",
16
- "@gradio/icons": "^0.3.4",
17
- "@gradio/upload": "^0.8.3",
18
- "@gradio/image": "^0.9.10",
19
- "@gradio/utils": "^0.3.0",
13
+ "@gradio/atoms": "^0.7.0",
14
+ "@gradio/client": "^0.16.0",
15
+ "@gradio/icons": "^0.4.0",
16
+ "@gradio/image": "^0.9.12",
17
+ "@gradio/statustracker": "^0.4.12",
18
+ "@gradio/upload": "^0.8.5",
19
+ "@gradio/utils": "^0.3.2",
20
20
  "@gradio/wasm": "^0.10.0"
21
21
  },
22
22
  "main_changeset": true,
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher } from "svelte";
3
3
  import { IconButton } from "@gradio/atoms";
4
- import { Clear, Undo, Redo, Check } from "@gradio/icons";
4
+ import { Clear, Undo, Redo, Check, Trash } from "@gradio/icons";
5
5
 
6
6
  /**
7
7
  * Can the current image be undone?
@@ -56,7 +56,7 @@
56
56
  }}
57
57
  />
58
58
  <IconButton
59
- Icon={Clear}
59
+ Icon={Trash}
60
60
  label="Clear canvas"
61
61
  on:click={(event) => {
62
62
  dispatch("remove_image");
@@ -28,6 +28,7 @@
28
28
  child_bottom: number;
29
29
  }>;
30
30
  active_tool: Writable<tool>;
31
+ toolbar_box: Writable<DOMRect>;
31
32
  crop: Writable<[number, number, number, number]>;
32
33
  position_spring: Spring<{
33
34
  x: number;
@@ -67,11 +68,14 @@
67
68
  export let bg = false;
68
69
  export let sources: ("clipboard" | "webcam" | "upload")[];
69
70
  const dispatch = createEventDispatcher<{
71
+ clear?: never;
70
72
  save: void;
73
+ change: void;
71
74
  }>();
72
75
  export let crop_constraint = false;
73
76
 
74
77
  let dimensions = writable(crop_size);
78
+ export let height = 0;
75
79
 
76
80
  let editor_box: EditorContext["editor_box"] = writable({
77
81
  parent_width: 0,
@@ -88,6 +92,8 @@
88
92
  child_bottom: 0
89
93
  });
90
94
 
95
+ $: height = $editor_box.child_height;
96
+
91
97
  const crop = writable<[number, number, number, number]>([0, 0, 1, 1]);
92
98
  const position_spring = spring(
93
99
  { x: 0, y: 0 },
@@ -102,6 +108,8 @@
102
108
 
103
109
  const { can_redo, can_undo, current_history } = CommandManager;
104
110
 
111
+ $: $current_history.previous, dispatch("change");
112
+
105
113
  $: {
106
114
  history = !!$current_history.previous || $active_tool !== "bg";
107
115
  }
@@ -113,6 +121,7 @@
113
121
  PartialRecord<context_type, (dimensions?: typeof $dimensions) => void>
114
122
  > = writable({});
115
123
  const contexts: Writable<context_type[]> = writable([]);
124
+ const toolbar_box: Writable<DOMRect> = writable(new DOMRect());
116
125
 
117
126
  const sort_order = ["bg", "layers", "crop", "draw", "erase"] as const;
118
127
  const editor_context = setContext<EditorContext>(EDITOR_KEY, {
@@ -120,6 +129,7 @@
120
129
  current_layer: writable(null),
121
130
  dimensions,
122
131
  editor_box,
132
+ toolbar_box,
123
133
  active_tool,
124
134
  crop,
125
135
  position_spring,
@@ -237,10 +247,10 @@
237
247
  return $pixi?.get_blobs(
238
248
  $pixi.get_layers(),
239
249
  new Rectangle(
240
- l * $dimensions[0],
241
- t * $dimensions[1],
242
- w * $dimensions[0],
243
- h * $dimensions[1]
250
+ Math.round(l * $dimensions[0]),
251
+ Math.round(t * $dimensions[1]),
252
+ Math.round(w * $dimensions[0]),
253
+ Math.round(h * $dimensions[1])
244
254
  ),
245
255
  $dimensions
246
256
  );
@@ -256,6 +266,7 @@
256
266
  } else {
257
267
  set_tool("bg");
258
268
  }
269
+ dispatch("clear");
259
270
  }
260
271
 
261
272
  onMount(() => {
@@ -329,39 +340,53 @@
329
340
  style:transform="translate({$position_spring.x}px, {$position_spring.y}px)"
330
341
  ></div>
331
342
  </div>
332
- <slot />
343
+ <div class="tools-wrap">
344
+ <slot />
345
+ </div>
333
346
  <div
334
347
  class="border"
335
- style:width="{$crop[2] * $editor_box.child_width}px"
336
- style:height="{$crop[3] * $editor_box.child_height}px"
348
+ style:width="{$crop[2] * $editor_box.child_width + 1}px"
349
+ style:height="{$crop[3] * $editor_box.child_height + 1}px"
337
350
  style:top="{$crop[1] * $editor_box.child_height +
338
- ($editor_box.child_top - $editor_box.parent_top)}px"
351
+ ($editor_box.child_top - $editor_box.parent_top) -
352
+ 0.5}px"
339
353
  style:left="{$crop[0] * $editor_box.child_width +
340
- ($editor_box.child_left - $editor_box.parent_left)}px"
354
+ ($editor_box.child_left - $editor_box.parent_left) -
355
+ 0.5}px"
341
356
  ></div>
342
357
  </div>
343
358
 
344
359
  <style>
345
360
  .wrap {
361
+ display: flex;
346
362
  width: 100%;
347
363
  height: 100%;
348
364
  position: relative;
349
- display: flex;
350
365
  justify-content: center;
351
- align-items: center;
366
+ align-items: flex-start;
352
367
  }
353
368
  .border {
354
369
  position: absolute;
355
370
  border: var(--block-border-color) 1px solid;
356
371
  pointer-events: none;
372
+ border-radius: var(--radius-md);
357
373
  }
358
374
 
359
375
  .stage-wrap {
360
376
  margin: var(--size-8);
361
377
  margin-bottom: var(--size-1);
378
+ border-radius: var(--radius-md);
379
+ overflow: hidden;
362
380
  }
363
381
 
364
- .bg {
382
+ .tools-wrap {
383
+ display: flex;
384
+ justify-content: center;
385
+ align-items: center;
386
+ padding: 0 var(--spacing-xl) 0 0;
387
+ border: 1px solid var(--block-border-color);
388
+ border-radius: var(--radius-sm);
389
+ margin: var(--spacing-xxl) 0 var(--spacing-xxl) 0;
365
390
  }
366
391
 
367
392
  .image-container {
@@ -13,6 +13,7 @@
13
13
  </script>
14
14
 
15
15
  <script lang="ts">
16
+ import { createEventDispatcher } from "svelte";
16
17
  import { type I18nFormatter } from "@gradio/utils";
17
18
  import { prepare_files, upload, type FileData } from "@gradio/client";
18
19
 
@@ -21,12 +22,12 @@
21
22
  import { type Brush as IBrush } from "./tools/Brush.svelte";
22
23
  import { type Eraser } from "./tools/Brush.svelte";
23
24
 
24
- export let brush: IBrush | null;
25
- export let eraser: Eraser | null;
26
25
  import { Tools, Crop, Brush, Sources } from "./tools";
27
26
  import { BlockLabel } from "@gradio/atoms";
28
27
  import { Image as ImageIcon } from "@gradio/icons";
29
28
 
29
+ export let brush: IBrush | null;
30
+ export let eraser: Eraser | null;
30
31
  export let sources: ("clipboard" | "webcam" | "upload")[];
31
32
  export let crop_size: [number, number] | `${string}:${string}` | null = null;
32
33
  export let i18n: I18nFormatter;
@@ -40,6 +41,15 @@
40
41
  composite: null
41
42
  };
42
43
  export let transforms: "crop"[] = ["crop"];
44
+ export let layers: boolean;
45
+ export let accept_blobs: (a: any) => void;
46
+ export let status: "pending" | "complete" | "error" = "complete";
47
+
48
+ const dispatch = createEventDispatcher<{
49
+ clear?: never;
50
+ upload?: never;
51
+ change?: never;
52
+ }>();
43
53
 
44
54
  let editor: ImageEditor;
45
55
 
@@ -51,6 +61,8 @@
51
61
  return !!o;
52
62
  }
53
63
 
64
+ $: if (bg) dispatch("upload");
65
+
54
66
  export async function get_data(): Promise<ImageBlobs> {
55
67
  const blobs = await editor.get_blobs();
56
68
 
@@ -102,11 +114,83 @@
102
114
  let bg = false;
103
115
  let history = false;
104
116
 
117
+ export let image_id: null | string = null;
118
+
105
119
  $: editor &&
106
120
  editor.set_tool &&
107
121
  (sources && sources.length
108
122
  ? editor.set_tool("bg")
109
123
  : editor.set_tool("draw"));
124
+
125
+ type BinaryImages = [string, string, File, number | null][];
126
+
127
+ function nextframe(): Promise<void> {
128
+ return new Promise((resolve) => setTimeout(() => resolve(), 30));
129
+ }
130
+
131
+ let uploading = false;
132
+ let pending = false;
133
+ async function handle_change(e: CustomEvent<Blob | any>): Promise<void> {
134
+ if (uploading) {
135
+ pending = true;
136
+ return;
137
+ }
138
+
139
+ uploading = true;
140
+
141
+ await nextframe();
142
+ const blobs = await editor.get_blobs();
143
+
144
+ const images: BinaryImages = [];
145
+
146
+ let id = Math.random().toString(36).substring(2);
147
+
148
+ if (blobs.background)
149
+ images.push([
150
+ id,
151
+ "background",
152
+ new File([blobs.background], "background.png"),
153
+ null
154
+ ]);
155
+ if (blobs.composite)
156
+ images.push([
157
+ id,
158
+ "composite",
159
+ new File([blobs.composite], "composite.png"),
160
+ null
161
+ ]);
162
+ blobs.layers.forEach((layer, i) => {
163
+ if (layer)
164
+ images.push([
165
+ id as string,
166
+ `layer`,
167
+ new File([layer], `layer_${i}.png`),
168
+ i
169
+ ]);
170
+ });
171
+
172
+ await Promise.all(
173
+ images.map(async ([image_id, type, data, index]) => {
174
+ return accept_blobs({
175
+ binary: true,
176
+ data: { file: data, id: image_id, type, index }
177
+ });
178
+ })
179
+ );
180
+ image_id = id;
181
+ dispatch("change");
182
+
183
+ await nextframe();
184
+ uploading = false;
185
+ if (pending) {
186
+ pending = false;
187
+ uploading = false;
188
+ handle_change(e);
189
+ }
190
+ }
191
+
192
+ let active_mode: "webcam" | "color" | null = null;
193
+ let editor_height = 0;
110
194
  </script>
111
195
 
112
196
  <BlockLabel
@@ -116,23 +200,28 @@
116
200
  />
117
201
  <ImageEditor
118
202
  bind:this={editor}
203
+ bind:height={editor_height}
119
204
  {changeable}
120
205
  on:save
206
+ on:change={handle_change}
207
+ on:clear={() => dispatch("clear")}
121
208
  bind:history
122
209
  bind:bg
123
210
  {sources}
124
211
  crop_constraint={!!crop_constraint}
125
212
  >
126
213
  <Tools {i18n}>
127
- {#if sources && sources.length}
128
- <Sources
129
- {i18n}
130
- {root}
131
- {sources}
132
- bind:bg
133
- background_file={value?.background || null}
134
- ></Sources>
135
- {/if}
214
+ <Layers layer_files={value?.layers || null} enable_layers={layers} />
215
+
216
+ <Sources
217
+ {i18n}
218
+ {root}
219
+ {sources}
220
+ bind:bg
221
+ bind:active_mode
222
+ background_file={value?.background || value?.composite || null}
223
+ ></Sources>
224
+
136
225
  {#if transforms.includes("crop")}
137
226
  <Crop {crop_constraint} />
138
227
  {/if}
@@ -151,10 +240,8 @@
151
240
  {/if}
152
241
  </Tools>
153
242
 
154
- <Layers layer_files={value?.layers || null} />
155
-
156
- {#if !bg && !history}
157
- <div class="empty wrap">
243
+ {#if !bg && !history && active_mode !== "webcam" && status !== "error"}
244
+ <div class="empty wrap" style:height={`${editor_height}px`}>
158
245
  {#if sources && sources.length}
159
246
  <div>Upload an image</div>
160
247
  {/if}
@@ -175,7 +262,6 @@
175
262
  flex-direction: column;
176
263
  justify-content: center;
177
264
  align-items: center;
178
-
179
265
  position: absolute;
180
266
  height: 100%;
181
267
  width: 100%;
@@ -185,6 +271,7 @@
185
271
  z-index: var(--layer-top);
186
272
  text-align: center;
187
273
  color: var(--body-text-color);
274
+ top: var(--size-8);
188
275
  }
189
276
 
190
277
  .wrap {
@@ -192,14 +279,10 @@
192
279
  flex-direction: column;
193
280
  justify-content: center;
194
281
  align-items: center;
195
- min-height: var(--size-60);
196
282
  color: var(--block-label-text-color);
197
283
  line-height: var(--line-md);
198
- height: 100%;
199
- padding-top: var(--size-3);
200
284
  font-size: var(--text-lg);
201
285
  pointer-events: none;
202
- transform: translateY(-30px);
203
286
  }
204
287
 
205
288
  .or {