@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
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
+
3
26
  ## 0.4.11
4
27
 
5
28
  ### Fixes
@@ -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.11",
3
+ "version": "0.5.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,14 +10,14 @@
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.2",
14
- "@gradio/icons": "^0.3.4",
15
- "@gradio/image": "^0.9.11",
16
- "@gradio/statustracker": "^0.4.11",
17
- "@gradio/client": "^0.15.1",
18
- "@gradio/utils": "^0.3.1",
19
- "@gradio/wasm": "^0.10.0",
20
- "@gradio/upload": "^0.8.4"
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
+ "@gradio/wasm": "^0.10.0"
21
21
  },
22
22
  "main_changeset": true,
23
23
  "main": "./Index.svelte",
@@ -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;
@@ -69,10 +70,12 @@
69
70
  const dispatch = createEventDispatcher<{
70
71
  clear?: never;
71
72
  save: void;
73
+ change: void;
72
74
  }>();
73
75
  export let crop_constraint = false;
74
76
 
75
77
  let dimensions = writable(crop_size);
78
+ export let height = 0;
76
79
 
77
80
  let editor_box: EditorContext["editor_box"] = writable({
78
81
  parent_width: 0,
@@ -89,6 +92,8 @@
89
92
  child_bottom: 0
90
93
  });
91
94
 
95
+ $: height = $editor_box.child_height;
96
+
92
97
  const crop = writable<[number, number, number, number]>([0, 0, 1, 1]);
93
98
  const position_spring = spring(
94
99
  { x: 0, y: 0 },
@@ -103,6 +108,8 @@
103
108
 
104
109
  const { can_redo, can_undo, current_history } = CommandManager;
105
110
 
111
+ $: $current_history.previous, dispatch("change");
112
+
106
113
  $: {
107
114
  history = !!$current_history.previous || $active_tool !== "bg";
108
115
  }
@@ -114,6 +121,7 @@
114
121
  PartialRecord<context_type, (dimensions?: typeof $dimensions) => void>
115
122
  > = writable({});
116
123
  const contexts: Writable<context_type[]> = writable([]);
124
+ const toolbar_box: Writable<DOMRect> = writable(new DOMRect());
117
125
 
118
126
  const sort_order = ["bg", "layers", "crop", "draw", "erase"] as const;
119
127
  const editor_context = setContext<EditorContext>(EDITOR_KEY, {
@@ -121,6 +129,7 @@
121
129
  current_layer: writable(null),
122
130
  dimensions,
123
131
  editor_box,
132
+ toolbar_box,
124
133
  active_tool,
125
134
  crop,
126
135
  position_spring,
@@ -238,10 +247,10 @@
238
247
  return $pixi?.get_blobs(
239
248
  $pixi.get_layers(),
240
249
  new Rectangle(
241
- l * $dimensions[0],
242
- t * $dimensions[1],
243
- w * $dimensions[0],
244
- 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])
245
254
  ),
246
255
  $dimensions
247
256
  );
@@ -331,39 +340,53 @@
331
340
  style:transform="translate({$position_spring.x}px, {$position_spring.y}px)"
332
341
  ></div>
333
342
  </div>
334
- <slot />
343
+ <div class="tools-wrap">
344
+ <slot />
345
+ </div>
335
346
  <div
336
347
  class="border"
337
- style:width="{$crop[2] * $editor_box.child_width}px"
338
- 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"
339
350
  style:top="{$crop[1] * $editor_box.child_height +
340
- ($editor_box.child_top - $editor_box.parent_top)}px"
351
+ ($editor_box.child_top - $editor_box.parent_top) -
352
+ 0.5}px"
341
353
  style:left="{$crop[0] * $editor_box.child_width +
342
- ($editor_box.child_left - $editor_box.parent_left)}px"
354
+ ($editor_box.child_left - $editor_box.parent_left) -
355
+ 0.5}px"
343
356
  ></div>
344
357
  </div>
345
358
 
346
359
  <style>
347
360
  .wrap {
361
+ display: flex;
348
362
  width: 100%;
349
363
  height: 100%;
350
364
  position: relative;
351
- display: flex;
352
365
  justify-content: center;
353
- align-items: center;
366
+ align-items: flex-start;
354
367
  }
355
368
  .border {
356
369
  position: absolute;
357
370
  border: var(--block-border-color) 1px solid;
358
371
  pointer-events: none;
372
+ border-radius: var(--radius-md);
359
373
  }
360
374
 
361
375
  .stage-wrap {
362
376
  margin: var(--size-8);
363
377
  margin-bottom: var(--size-1);
378
+ border-radius: var(--radius-md);
379
+ overflow: hidden;
364
380
  }
365
381
 
366
- .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;
367
390
  }
368
391
 
369
392
  .image-container {
@@ -22,12 +22,12 @@
22
22
  import { type Brush as IBrush } from "./tools/Brush.svelte";
23
23
  import { type Eraser } from "./tools/Brush.svelte";
24
24
 
25
- export let brush: IBrush | null;
26
- export let eraser: Eraser | null;
27
25
  import { Tools, Crop, Brush, Sources } from "./tools";
28
26
  import { BlockLabel } from "@gradio/atoms";
29
27
  import { Image as ImageIcon } from "@gradio/icons";
30
28
 
29
+ export let brush: IBrush | null;
30
+ export let eraser: Eraser | null;
31
31
  export let sources: ("clipboard" | "webcam" | "upload")[];
32
32
  export let crop_size: [number, number] | `${string}:${string}` | null = null;
33
33
  export let i18n: I18nFormatter;
@@ -41,9 +41,14 @@
41
41
  composite: null
42
42
  };
43
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";
44
47
 
45
48
  const dispatch = createEventDispatcher<{
46
49
  clear?: never;
50
+ upload?: never;
51
+ change?: never;
47
52
  }>();
48
53
 
49
54
  let editor: ImageEditor;
@@ -56,6 +61,8 @@
56
61
  return !!o;
57
62
  }
58
63
 
64
+ $: if (bg) dispatch("upload");
65
+
59
66
  export async function get_data(): Promise<ImageBlobs> {
60
67
  const blobs = await editor.get_blobs();
61
68
 
@@ -107,11 +114,83 @@
107
114
  let bg = false;
108
115
  let history = false;
109
116
 
117
+ export let image_id: null | string = null;
118
+
110
119
  $: editor &&
111
120
  editor.set_tool &&
112
121
  (sources && sources.length
113
122
  ? editor.set_tool("bg")
114
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;
115
194
  </script>
116
195
 
117
196
  <BlockLabel
@@ -121,8 +200,10 @@
121
200
  />
122
201
  <ImageEditor
123
202
  bind:this={editor}
203
+ bind:height={editor_height}
124
204
  {changeable}
125
205
  on:save
206
+ on:change={handle_change}
126
207
  on:clear={() => dispatch("clear")}
127
208
  bind:history
128
209
  bind:bg
@@ -130,15 +211,17 @@
130
211
  crop_constraint={!!crop_constraint}
131
212
  >
132
213
  <Tools {i18n}>
133
- {#if sources && sources.length}
134
- <Sources
135
- {i18n}
136
- {root}
137
- {sources}
138
- bind:bg
139
- background_file={value?.background || null}
140
- ></Sources>
141
- {/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
+
142
225
  {#if transforms.includes("crop")}
143
226
  <Crop {crop_constraint} />
144
227
  {/if}
@@ -157,10 +240,8 @@
157
240
  {/if}
158
241
  </Tools>
159
242
 
160
- <Layers layer_files={value?.layers || null} />
161
-
162
- {#if !bg && !history}
163
- <div class="empty wrap">
243
+ {#if !bg && !history && active_mode !== "webcam" && status !== "error"}
244
+ <div class="empty wrap" style:height={`${editor_height}px`}>
164
245
  {#if sources && sources.length}
165
246
  <div>Upload an image</div>
166
247
  {/if}
@@ -181,7 +262,6 @@
181
262
  flex-direction: column;
182
263
  justify-content: center;
183
264
  align-items: center;
184
-
185
265
  position: absolute;
186
266
  height: 100%;
187
267
  width: 100%;
@@ -191,6 +271,7 @@
191
271
  z-index: var(--layer-top);
192
272
  text-align: center;
193
273
  color: var(--body-text-color);
274
+ top: var(--size-8);
194
275
  }
195
276
 
196
277
  .wrap {
@@ -198,14 +279,10 @@
198
279
  flex-direction: column;
199
280
  justify-content: center;
200
281
  align-items: center;
201
- min-height: var(--size-60);
202
282
  color: var(--block-label-text-color);
203
283
  line-height: var(--line-md);
204
- height: 100%;
205
- padding-top: var(--size-3);
206
284
  font-size: var(--text-lg);
207
285
  pointer-events: none;
208
- transform: translateY(-30px);
209
286
  }
210
287
 
211
288
  .or {