@gradio/image 0.5.2 → 0.5.4

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,17 @@
1
1
  # @gradio/image
2
2
 
3
+ ## 0.5.4
4
+
5
+ ### Fixes
6
+
7
+ - [#6865](https://github.com/gradio-app/gradio/pull/6865) [`15c97c6`](https://github.com/gradio-app/gradio/commit/15c97c6d346c475141d20615b5a865e9c44bdc76) - Fix webcam when `streaming=True`. Thanks [@hannahblair](https://github.com/hannahblair)!
8
+
9
+ ## 0.5.3
10
+
11
+ ### Fixes
12
+
13
+ - [#6766](https://github.com/gradio-app/gradio/pull/6766) [`73268ee`](https://github.com/gradio-app/gradio/commit/73268ee2e39f23ebdd1e927cb49b8d79c4b9a144) - Improve source selection UX. Thanks [@hannahblair](https://github.com/hannahblair)!
14
+
3
15
  ## 0.5.2
4
16
 
5
17
  ### Patch Changes
@@ -294,4 +306,4 @@ Thanks [@pngwn](https://github.com/pngwn)!
294
306
 
295
307
  ### Features
296
308
 
297
- - [#4979](https://github.com/gradio-app/gradio/pull/4979) [`44ac8ad0`](https://github.com/gradio-app/gradio/commit/44ac8ad08d82ea12c503dde5c78f999eb0452de2) - Allow setting sketch color default. Thanks [@aliabid94](https://github.com/aliabid94)!
309
+ - [#4979](https://github.com/gradio-app/gradio/pull/4979) [`44ac8ad0`](https://github.com/gradio-app/gradio/commit/44ac8ad08d82ea12c503dde5c78f999eb0452de2) - Allow setting sketch color default. Thanks [@aliabid94](https://github.com/aliabid94)!
@@ -1,25 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
3
3
  import StaticImage from "./Index.svelte";
4
+ import { userEvent, within } from "@storybook/testing-library";
4
5
  </script>
5
6
 
6
- <Meta
7
- title="Components/Image"
8
- component={Image}
9
- argTypes={{
10
- value: {
11
- control: "object",
12
- description: "The image URL or file to display",
13
- name: "value"
14
- },
15
- show_download_button: {
16
- options: [true, false],
17
- description: "If false, the download button will not be visible",
18
- control: { type: "boolean" },
19
- defaultValue: true
20
- }
21
- }}
22
- />
7
+ <Meta title="Components/Image" component={Image} />
23
8
 
24
9
  <Template let:args>
25
10
  <div
@@ -31,7 +16,7 @@
31
16
  </Template>
32
17
 
33
18
  <Story
34
- name="Static Image with label and download button"
19
+ name="static with label and download button"
35
20
  args={{
36
21
  value: {
37
22
  path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
@@ -44,7 +29,7 @@
44
29
  />
45
30
 
46
31
  <Story
47
- name="Static Image with no label or download button"
32
+ name="static with no label or download button"
48
33
  args={{
49
34
  value: {
50
35
  path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
@@ -55,3 +40,62 @@
55
40
  show_download_button: false
56
41
  }}
57
42
  />
43
+
44
+ <Story
45
+ name="interactive with upload, clipboard, and webcam"
46
+ args={{
47
+ sources: ["upload", "clipboard", "webcam"],
48
+ value: {
49
+ path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
50
+ url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
51
+ orig_name: "cheetah.jpg"
52
+ },
53
+ show_label: false,
54
+ show_download_button: false,
55
+ interactive: true
56
+ }}
57
+ play={async ({ canvasElement }) => {
58
+ const canvas = within(canvasElement);
59
+
60
+ const webcamButton = await canvas.findByLabelText("Capture from camera");
61
+ userEvent.click(webcamButton);
62
+
63
+ userEvent.click(await canvas.findByTitle("select video source"));
64
+ userEvent.click(await canvas.findByLabelText("select source"));
65
+ userEvent.click(await canvas.findByLabelText("Upload file"));
66
+ userEvent.click(await canvas.findByLabelText("Paste from clipboard"));
67
+ }}
68
+ />
69
+
70
+ <Story
71
+ name="interactive with webcam"
72
+ args={{
73
+ sources: ["webcam"],
74
+ show_download_button: true,
75
+ interactive: true
76
+ }}
77
+ />
78
+
79
+ <Story
80
+ name="interactive with clipboard"
81
+ args={{
82
+ sources: ["clipboard"],
83
+ show_download_button: true,
84
+ interactive: true
85
+ }}
86
+ />
87
+
88
+ <Story
89
+ name="interactive webcam with streaming"
90
+ args={{
91
+ sources: ["webcam"],
92
+ show_download_button: true,
93
+ interactive: true,
94
+ value: {
95
+ path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
96
+ url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
97
+ orig_name: "cheetah.jpg"
98
+ },
99
+ streaming: true
100
+ }}
101
+ />
package/Index.svelte CHANGED
@@ -20,6 +20,8 @@
20
20
  import type { LoadingStatus } from "@gradio/statustracker";
21
21
  import { normalise_file } from "@gradio/client";
22
22
 
23
+ type sources = "upload" | "webcam" | "clipboard" | null;
24
+
23
25
  export let elem_id = "";
24
26
  export let elem_classes: string[] = [];
25
27
  export let visible = true;
@@ -66,7 +68,7 @@
66
68
  $: url && gradio.dispatch("change");
67
69
 
68
70
  let dragging: boolean;
69
- let active_tool: null | "webcam" = null;
71
+ let active_source: sources = null;
70
72
  </script>
71
73
 
72
74
  {#if !interactive}
@@ -124,7 +126,7 @@
124
126
  />
125
127
 
126
128
  <ImageUploader
127
- bind:active_tool
129
+ bind:active_source
128
130
  bind:value
129
131
  selectable={_selectable}
130
132
  {root}
@@ -144,8 +146,6 @@
144
146
  loading_status.status = "error";
145
147
  gradio.dispatch("error", detail);
146
148
  }}
147
- on:click={() => gradio.dispatch("error", "bad thing happened")}
148
- on:error
149
149
  {label}
150
150
  {show_label}
151
151
  {pending}
@@ -153,8 +153,10 @@
153
153
  {mirror_webcam}
154
154
  i18n={gradio.i18n}
155
155
  >
156
- {#if sources.includes("upload")}
157
- <UploadText i18n={gradio.i18n} type="image" mode="short" />
156
+ {#if active_source === "upload" || !active_source}
157
+ <UploadText i18n={gradio.i18n} type="image" />
158
+ {:else if active_source === "clipboard"}
159
+ <UploadText i18n={gradio.i18n} type="clipboard" mode="short" />
158
160
  {:else}
159
161
  <Empty unpadded_box={true} size="large"><Image /></Empty>
160
162
  {/if}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/image",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,12 +10,12 @@
10
10
  "cropperjs": "^1.5.12",
11
11
  "lazy-brush": "^1.0.1",
12
12
  "resize-observer-polyfill": "^1.5.1",
13
- "@gradio/client": "^0.9.2",
14
- "@gradio/atoms": "^0.4.0",
15
- "@gradio/statustracker": "^0.4.2",
16
- "@gradio/upload": "^0.5.5",
17
- "@gradio/utils": "^0.2.0",
13
+ "@gradio/atoms": "^0.4.1",
14
+ "@gradio/client": "^0.9.4",
18
15
  "@gradio/icons": "^0.3.2",
16
+ "@gradio/statustracker": "^0.4.3",
17
+ "@gradio/utils": "^0.2.0",
18
+ "@gradio/upload": "^0.5.7",
19
19
  "@gradio/wasm": "^0.4.0"
20
20
  },
21
21
  "main_changeset": true,
@@ -4,28 +4,21 @@
4
4
  import { Image as ImageIcon } from "@gradio/icons";
5
5
  import type { SelectData, I18nFormatter } from "@gradio/utils";
6
6
  import { get_coordinates_of_clicked_image } from "./utils";
7
- import {
8
- Webcam as WebcamIcon,
9
- ImagePaste,
10
- Upload as UploadIcon
11
- } from "@gradio/icons";
12
7
  import Webcam from "./Webcam.svelte";
13
- import { Toolbar, IconButton } from "@gradio/atoms";
14
8
 
15
9
  import { Upload } from "@gradio/upload";
16
10
  import { type FileData, normalise_file } from "@gradio/client";
17
11
  import ClearImage from "./ClearImage.svelte";
12
+ import { SelectSource } from "@gradio/atoms";
18
13
  import Image from "./Image.svelte";
19
14
 
20
15
  export let value: null | FileData;
21
16
  export let label: string | undefined = undefined;
22
17
  export let show_label: boolean;
23
18
 
24
- export let sources: ("clipboard" | "webcam" | "upload")[] = [
25
- "upload",
26
- "clipboard",
27
- "webcam"
28
- ];
19
+ type source_type = "upload" | "webcam" | "clipboard" | "microphone" | null;
20
+
21
+ export let sources: source_type[] = ["upload", "clipboard", "webcam"];
29
22
  export let streaming = false;
30
23
  export let pending = false;
31
24
  export let mirror_webcam: boolean;
@@ -35,19 +28,24 @@
35
28
 
36
29
  let upload: Upload;
37
30
  let uploading = false;
38
- export let active_tool: "webcam" | null = null;
31
+ export let active_source: source_type = null;
39
32
 
40
33
  function handle_upload({ detail }: CustomEvent<FileData>): void {
41
34
  value = normalise_file(detail, root, null);
42
35
  dispatch("upload");
43
36
  }
44
37
 
38
+ function handle_clear(): void {
39
+ value = null;
40
+ dispatch("clear");
41
+ dispatch("change", null);
42
+ }
43
+
45
44
  async function handle_save(img_blob: Blob | any): Promise<void> {
46
45
  pending = true;
47
46
  const f = await upload.load_files([new File([img_blob], `webcam.png`)]);
48
47
 
49
48
  value = f?.[0] || null;
50
- if (!streaming) active_tool = null;
51
49
 
52
50
  await tick();
53
51
 
@@ -55,7 +53,7 @@
55
53
  pending = false;
56
54
  }
57
55
 
58
- $: active_streaming = streaming && active_tool === "webcam";
56
+ $: active_streaming = streaming && active_source === "webcam";
59
57
  $: if (uploading && !active_streaming) value = null;
60
58
 
61
59
  $: value && !value.url && (value = normalise_file(value, root, null));
@@ -80,61 +78,16 @@
80
78
  }
81
79
  }
82
80
 
83
- const sources_meta = {
84
- upload: {
85
- icon: UploadIcon,
86
- label: i18n("Upload"),
87
- order: 0
88
- },
89
- webcam: {
90
- icon: WebcamIcon,
91
- label: i18n("Webcam"),
92
- order: 1
93
- },
94
- clipboard: {
95
- icon: ImagePaste,
96
- label: i18n("Paste"),
97
- order: 2
98
- }
99
- };
100
-
101
- $: sources_list = sources.sort(
102
- (a, b) => sources_meta[a].order - sources_meta[b].order
103
- );
104
-
105
- $: {
106
- if (sources.length === 1 && sources[0] === "webcam") {
107
- active_tool = "webcam";
108
- }
81
+ $: if (!active_source && sources) {
82
+ active_source = sources[0];
109
83
  }
110
84
 
111
- async function handle_toolbar(
85
+ async function handle_select_source(
112
86
  source: (typeof sources)[number]
113
87
  ): Promise<void> {
114
88
  switch (source) {
115
89
  case "clipboard":
116
- navigator.clipboard.read().then(async (items) => {
117
- for (let i = 0; i < items.length; i++) {
118
- const type = items[i].types.find((t) => t.startsWith("image/"));
119
- if (type) {
120
- value = null;
121
- items[i].getType(type).then(async (blob) => {
122
- const f = await upload.load_files([
123
- new File([blob], `clipboard.${type.replace("image/", "")}`)
124
- ]);
125
- f;
126
- value = f?.[0] || null;
127
- });
128
- break;
129
- }
130
- }
131
- });
132
- break;
133
- case "webcam":
134
- active_tool = "webcam";
135
- break;
136
- case "upload":
137
- upload.open_file_upload();
90
+ upload.paste_clipboard();
138
91
  break;
139
92
  default:
140
93
  break;
@@ -155,21 +108,21 @@
155
108
  {/if}
156
109
  <div class="upload-container">
157
110
  <Upload
158
- hidden={value !== null || active_tool === "webcam"}
111
+ hidden={value !== null || active_source === "webcam"}
159
112
  bind:this={upload}
160
113
  bind:uploading
161
114
  bind:dragging
162
- filetype="image/*"
115
+ filetype={active_source === "clipboard" ? "clipboard" : "image/*"}
163
116
  on:load={handle_upload}
164
117
  on:error
165
118
  {root}
166
119
  disable_click={!sources.includes("upload")}
167
120
  >
168
- {#if value === null && !active_tool}
121
+ {#if value === null}
169
122
  <slot />
170
123
  {/if}
171
124
  </Upload>
172
- {#if active_tool === "webcam"}
125
+ {#if active_source === "webcam" && (streaming || (!streaming && !value))}
173
126
  <Webcam
174
127
  on:capture={(e) => handle_save(e.detail)}
175
128
  on:stream={(e) => handle_save(e.detail)}
@@ -191,17 +144,12 @@
191
144
  {/if}
192
145
  </div>
193
146
  {#if sources.length > 1 || sources.includes("clipboard")}
194
- <Toolbar show_border={!value?.url}>
195
- {#each sources_list as source}
196
- <IconButton
197
- on:click={() => handle_toolbar(source)}
198
- Icon={sources_meta[source].icon}
199
- size="large"
200
- label="{source}-image-toolbar-btn"
201
- padded={false}
202
- />
203
- {/each}
204
- </Toolbar>
147
+ <SelectSource
148
+ {sources}
149
+ bind:active_source
150
+ {handle_clear}
151
+ handle_select={handle_select_source}
152
+ />
205
153
  {/if}
206
154
  </div>
207
155
 
@@ -209,6 +157,13 @@
209
157
  .image-frame :global(img) {
210
158
  width: var(--size-full);
211
159
  height: var(--size-full);
160
+ object-fit: cover;
161
+ }
162
+
163
+ .image-frame {
164
+ object-fit: cover;
165
+ width: 100%;
166
+ height: 100%;
212
167
  }
213
168
 
214
169
  .upload-container {
@@ -182,7 +182,7 @@
182
182
  <!-- need to suppress for video streaming https://github.com/sveltejs/svelte/issues/5967 -->
183
183
  <video bind:this={video_source} class:flip={mirror_webcam} />
184
184
  {#if !streaming}
185
- <div class:capture={!recording} class="button-wrap">
185
+ <div class="button-wrap">
186
186
  <button
187
187
  on:click={mode === "image" ? take_picture : take_recording}
188
188
  aria-label={mode === "image" ? "capture photo" : "start recording"}
@@ -212,29 +212,32 @@
212
212
  <div class="icon" title="select video source">
213
213
  <DropdownArrow />
214
214
  </div>
215
-
216
- {#if options_open}
217
- <div class="select-wrap" use:click_outside={handle_click_outside}>
218
- <!-- svelte-ignore a11y-click-events-have-key-events-->
219
- <!-- svelte-ignore a11y-no-static-element-interactions-->
220
- <span
221
- class="inset-icon"
222
- on:click|stopPropagation={() => (options_open = false)}
223
- >
224
- <DropdownArrow />
225
- </span>
226
- {#each video_sources as source}
227
- <!-- svelte-ignore a11y-click-events-have-key-events-->
228
- <!-- svelte-ignore a11y-no-static-element-interactions-->
229
- <div on:click={() => selectVideoSource(source.deviceId)}>
230
- {source.label}
231
- </div>
232
- {/each}
233
- </div>
234
- {/if}
235
215
  </button>
236
216
  {/if}
237
217
  </div>
218
+ {#if options_open}
219
+ <select
220
+ class="select-wrap"
221
+ aria-label="select source"
222
+ use:click_outside={handle_click_outside}
223
+ >
224
+ <button
225
+ class="inset-icon"
226
+ on:click|stopPropagation={() => (options_open = false)}
227
+ >
228
+ <DropdownArrow />
229
+ </button>
230
+ {#if video_sources.length === 0}
231
+ <option value="">{i18n("common.no_devices")}</option>
232
+ {:else}
233
+ {#each video_sources as source}
234
+ <option on:click={() => selectVideoSource(source.deviceId)}>
235
+ {source.label}
236
+ </option>
237
+ {/each}
238
+ {/if}
239
+ </select>
240
+ {/if}
238
241
  {/if}
239
242
  </div>
240
243
 
@@ -243,36 +246,28 @@
243
246
  position: relative;
244
247
  width: var(--size-full);
245
248
  height: var(--size-full);
246
- min-height: var(--size-60);
247
249
  }
248
250
 
249
251
  video {
250
252
  width: var(--size-full);
251
253
  height: var(--size-full);
254
+ object-fit: cover;
252
255
  }
253
256
 
254
257
  .button-wrap {
255
- display: flex;
256
258
  position: absolute;
257
- right: 0px;
259
+ background-color: var(--block-background-fill);
260
+ border: 1px solid var(--border-color-primary);
261
+ border-radius: var(--radius-xl);
262
+ padding: var(--size-1-5);
263
+ display: flex;
258
264
  bottom: var(--size-2);
259
- left: 0px;
260
- justify-content: center;
261
- align-items: center;
262
- margin: auto;
265
+ left: 50%;
266
+ transform: translate(-50%, 0);
263
267
  box-shadow: var(--shadow-drop-lg);
264
268
  border-radius: var(--radius-xl);
265
- background-color: rgba(0, 0, 0, 0.9);
266
- width: var(--size-10);
267
- height: var(--size-8);
268
- padding: var(--size-2-5);
269
- padding-right: var(--size-1);
270
- z-index: var(--layer-3);
271
- }
272
-
273
- .capture {
274
- width: var(--size-14);
275
- transform: translateX(var(--size-2-5));
269
+ line-height: var(--size-3);
270
+ color: var(--button-secondary-text-color);
276
271
  }
277
272
 
278
273
  @media (--screen-md) {
@@ -289,9 +284,8 @@
289
284
 
290
285
  .icon {
291
286
  opacity: 0.8;
292
- width: 100%;
293
- height: 100%;
294
- color: white;
287
+ width: 18px;
288
+ height: 18px;
295
289
  display: flex;
296
290
  justify-content: space-between;
297
291
  align-items: center;
@@ -310,35 +304,39 @@
310
304
  -webkit-appearance: none;
311
305
  -moz-appearance: none;
312
306
  appearance: none;
307
+ color: var(--button-secondary-text-color);
313
308
  background-color: transparent;
314
- border: none;
315
- width: auto;
316
- font-size: 1rem;
317
- /* padding: 0.5rem; */
318
- width: max-content;
309
+ width: 95%;
310
+ font-size: var(--text-md);
319
311
  position: absolute;
320
- top: 0;
321
- right: 0;
312
+ bottom: var(--size-2);
322
313
  background-color: var(--block-background-fill);
323
314
  box-shadow: var(--shadow-drop-lg);
324
315
  border-radius: var(--radius-xl);
325
316
  z-index: var(--layer-top);
326
- border: 1px solid var(--border-color-accent);
317
+ border: 1px solid var(--border-color-primary);
327
318
  text-align: left;
328
- overflow: hidden;
319
+ line-height: var(--size-4);
320
+ white-space: nowrap;
321
+ text-overflow: ellipsis;
322
+ left: 50%;
323
+ transform: translate(-50%, 0);
324
+ max-width: var(--size-52);
329
325
  }
330
326
 
331
- .select-wrap > div {
327
+ .select-wrap > option {
332
328
  padding: 0.25rem 0.5rem;
333
329
  border-bottom: 1px solid var(--border-color-accent);
334
330
  padding-right: var(--size-8);
331
+ text-overflow: ellipsis;
332
+ overflow: hidden;
335
333
  }
336
334
 
337
- .select-wrap > div:hover {
335
+ .select-wrap > option:hover {
338
336
  background-color: var(--color-accent);
339
337
  }
340
338
 
341
- .select-wrap > div:last-child {
339
+ .select-wrap > option:last-child {
342
340
  border: none;
343
341
  }
344
342