@gradio/image 0.26.3 → 0.28.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.
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher, tick } from "svelte";
2
+ import { tick, type Snippet } from "svelte";
3
3
  import { BlockLabel, IconButtonWrapper, IconButton } from "@gradio/atoms";
4
4
  import { Clear, Image as ImageIcon } from "@gradio/icons";
5
5
  import { FullscreenButton } from "@gradio/atoms";
@@ -17,36 +17,80 @@
17
17
  import Image from "./Image.svelte";
18
18
  import type { Base64File, WebcamOptions } from "./types";
19
19
 
20
- export let value: null | FileData | Base64File = null;
21
- export let label: string | undefined = undefined;
22
- export let show_label: boolean;
23
-
24
20
  type source_type = "upload" | "webcam" | "clipboard" | "microphone" | null;
25
21
 
26
- export let sources: source_type[] = ["upload", "clipboard", "webcam"];
27
- export let streaming = false;
28
- export let pending = false;
29
- export let webcam_options: WebcamOptions;
30
- export let selectable = false;
31
- export let root: string;
32
- export let i18n: I18nFormatter;
33
- export let max_file_size: number | null = null;
34
- export let upload: Client["upload"];
35
- export let stream_handler: Client["stream"];
36
- export let stream_every: number;
37
- export let time_limit: number;
38
- export let show_fullscreen_button = true;
39
- export let stream_state: "open" | "waiting" | "closed" = "closed";
40
- export let upload_promise: Promise<any> | null = null;
41
- export let onerror: ((error: string) => void) | undefined = undefined;
22
+ let {
23
+ value = $bindable<null | FileData | Base64File>(null),
24
+ label = undefined,
25
+ show_label,
26
+ sources = ["upload", "clipboard", "webcam"],
27
+ streaming = false,
28
+ pending = $bindable(false),
29
+ webcam_options,
30
+ selectable = false,
31
+ root,
32
+ i18n,
33
+ max_file_size = null,
34
+ upload,
35
+ stream_handler,
36
+ stream_every,
37
+ time_limit,
38
+ show_fullscreen_button = true,
39
+ stream_state = "closed",
40
+ upload_promise = $bindable<Promise<any> | null>(null),
41
+ onerror,
42
+ uploading = $bindable(false),
43
+ active_source = $bindable<source_type>(null),
44
+ fullscreen = $bindable(false),
45
+ dragging = $bindable(false),
46
+ onchange,
47
+ onstream,
48
+ onclear,
49
+ ondrag,
50
+ onupload,
51
+ onselect,
52
+ onfullscreen,
53
+ onclose_stream,
54
+ children
55
+ }: {
56
+ value?: null | FileData | Base64File;
57
+ label?: string;
58
+ show_label: boolean;
59
+ sources?: source_type[];
60
+ streaming?: boolean;
61
+ pending?: boolean;
62
+ webcam_options: WebcamOptions;
63
+ selectable?: boolean;
64
+ root: string;
65
+ i18n: I18nFormatter;
66
+ max_file_size?: number | null;
67
+ upload: Client["upload"];
68
+ stream_handler: Client["stream"];
69
+ stream_every: number;
70
+ time_limit: number;
71
+ show_fullscreen_button?: boolean;
72
+ stream_state?: "open" | "waiting" | "closed";
73
+ upload_promise?: Promise<any> | null;
74
+ onerror?: (error: string) => void;
75
+ uploading?: boolean;
76
+ active_source?: source_type;
77
+ fullscreen?: boolean;
78
+ dragging?: boolean;
79
+ onchange?: (value?: null | FileData | Base64File) => void;
80
+ onstream?: (value: ValueData) => void;
81
+ onclear?: () => void;
82
+ ondrag?: (dragging: boolean) => void;
83
+ onupload?: () => void;
84
+ onselect?: (value: SelectData) => void;
85
+ onfullscreen?: (fullscreen: boolean) => void;
86
+ onclose_stream?: () => void;
87
+ children?: Snippet;
88
+ } = $props();
42
89
 
43
90
  let upload_input: Upload;
44
- export let uploading = false;
45
- export let active_source: source_type = null;
46
- export let fullscreen = false;
47
91
 
48
- let files: FileData[] = [];
49
- let upload_id: string;
92
+ let files = $state<FileData[]>([]);
93
+ let upload_id = $state("");
50
94
 
51
95
  async function handle_upload(detail: FileData): Promise<void> {
52
96
  if (!streaming) {
@@ -62,14 +106,14 @@
62
106
  }
63
107
 
64
108
  await tick();
65
- dispatch("upload");
109
+ onupload?.();
66
110
  }
67
111
  }
68
112
 
69
113
  function handle_clear(): void {
70
114
  value = null;
71
- dispatch("clear");
72
- dispatch("change", null);
115
+ onclear?.();
116
+ onchange?.(null);
73
117
  }
74
118
 
75
119
  function handle_remove_image_click(event: MouseEvent): void {
@@ -82,7 +126,7 @@
82
126
  event: "change" | "stream" | "upload"
83
127
  ): Promise<void> {
84
128
  if (event === "stream") {
85
- dispatch("stream", {
129
+ onstream?.({
86
130
  value: { url: img_blob } as Base64File,
87
131
  is_value_data: true
88
132
  });
@@ -105,38 +149,31 @@
105
149
  if (event === "change" || event === "upload") {
106
150
  value = f?.[0] || null;
107
151
  await tick();
108
- dispatch("change");
152
+ onchange?.();
109
153
  }
110
154
  pending = false;
111
155
  }
112
156
 
113
- $: active_streaming = streaming && active_source === "webcam";
114
- $: if (uploading && !active_streaming) value = null;
115
-
116
- const dispatch = createEventDispatcher<{
117
- change?: never;
118
- stream: ValueData;
119
- clear?: never;
120
- drag: boolean;
121
- upload?: never;
122
- select: SelectData;
123
- end_stream: never;
124
- }>();
125
-
126
- export let dragging = false;
127
-
128
- $: dispatch("drag", dragging);
157
+ let active_streaming = $derived(streaming && active_source === "webcam");
158
+ $effect(() => {
159
+ if (uploading && !active_streaming) value = null;
160
+ });
161
+ $effect(() => {
162
+ ondrag?.(dragging);
163
+ });
129
164
 
130
165
  function handle_click(evt: MouseEvent): void {
131
166
  let coordinates = get_coordinates_of_clicked_image(evt);
132
167
  if (coordinates) {
133
- dispatch("select", { index: coordinates, value: null });
168
+ onselect?.({ index: coordinates, value: null });
134
169
  }
135
170
  }
136
171
 
137
- $: if (!active_source && sources) {
138
- active_source = sources[0];
139
- }
172
+ $effect(() => {
173
+ if (!active_source && sources) {
174
+ active_source = sources[0];
175
+ }
176
+ });
140
177
 
141
178
  async function handle_select_source(
142
179
  source: (typeof sources)[number]
@@ -184,7 +221,13 @@
184
221
  <IconButtonWrapper>
185
222
  {#if value?.url && !active_streaming}
186
223
  {#if show_fullscreen_button}
187
- <FullscreenButton {fullscreen} on:fullscreen />
224
+ <FullscreenButton
225
+ {fullscreen}
226
+ onclick={(is_fullscreen) => {
227
+ fullscreen = is_fullscreen;
228
+ onfullscreen?.(is_fullscreen);
229
+ }}
230
+ />
188
231
  {/if}
189
232
  <IconButton
190
233
  Icon={Clear}
@@ -198,8 +241,8 @@
198
241
  class="upload-container"
199
242
  class:reduced-height={sources.length > 1}
200
243
  style:width={value ? "auto" : "100%"}
201
- on:dragover={on_drag_over}
202
- on:drop={on_drop}
244
+ ondragover={on_drag_over}
245
+ ondrop={on_drop}
203
246
  >
204
247
  <Upload
205
248
  bind:upload_promise
@@ -218,7 +261,7 @@
218
261
  aria_label={i18n("image.drop_to_upload")}
219
262
  >
220
263
  {#if value === null}
221
- <slot />
264
+ {#if children}{@render children()}{/if}
222
265
  {/if}
223
266
  </Upload>
224
267
  {#if active_source === "webcam" && !streaming && pending}
@@ -227,12 +270,10 @@
227
270
  <Webcam
228
271
  {root}
229
272
  {value}
230
- on:capture={(e) => handle_save(e.detail, "change")}
231
- on:stream={(e) => handle_save(e.detail, "stream")}
232
- on:error
233
- on:drag
234
- on:upload={(e) => handle_save(e.detail, "upload")}
235
- on:close_stream
273
+ oncapture={(detail) => handle_save(detail, "change")}
274
+ onstream={(detail) => handle_save(detail, "stream")}
275
+ {onerror}
276
+ {onclose_stream}
236
277
  {stream_state}
237
278
  mirror_webcam={webcam_options.mirror}
238
279
  {stream_every}
@@ -247,7 +288,7 @@
247
288
  {:else if value !== null && !streaming}
248
289
  <!-- svelte-ignore a11y-click-events-have-key-events-->
249
290
  <!-- svelte-ignore a11y-no-static-element-interactions-->
250
- <div class:selectable class="image-frame" on:click={handle_click}>
291
+ <div class:selectable class="image-frame" onclick={handle_click}>
251
292
  <Image src={value.url} restProps={{ alt: value.alt_text }} />
252
293
  </div>
253
294
  {/if}
@@ -257,7 +298,7 @@
257
298
  {sources}
258
299
  bind:active_source
259
300
  {handle_clear}
260
- handle_select={handle_select_source}
301
+ handle_select={(source) => handle_select_source(source as source_type)}
261
302
  />
262
303
  {/if}
263
304
  </div>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher, onDestroy, onMount } from "svelte";
2
+ import { onDestroy, onMount } from "svelte";
3
3
  import {
4
4
  Camera,
5
5
  Circle,
@@ -20,33 +20,51 @@
20
20
  import type { Base64File } from "./types";
21
21
 
22
22
  let video_source: HTMLVideoElement;
23
- let available_video_devices: MediaDeviceInfo[] = [];
24
- let selected_device: MediaDeviceInfo | null = null;
25
-
26
- export let stream_state: "open" | "waiting" | "closed" = "closed";
23
+ let available_video_devices = $state<MediaDeviceInfo[]>([]);
24
+ let selected_device = $state<MediaDeviceInfo | null>(null);
27
25
 
28
26
  let canvas: HTMLCanvasElement;
29
- export let streaming = false;
30
- export let pending = false;
31
- export let root = "";
32
- export let stream_every = 1;
33
-
34
- export let mode: "image" | "video" = "image";
35
- export let mirror_webcam: boolean;
36
- export let include_audio: boolean;
37
- export let webcam_constraints: { [key: string]: any } | null = null;
38
- export let i18n: I18nFormatter;
39
- export let upload: Client["upload"];
40
- export let value: FileData | null | Base64File = null;
41
- export let time_limit: number | null = null;
42
- const dispatch = createEventDispatcher<{
43
- stream: Blob | string;
44
- capture: FileData | Blob | null;
45
- error: string;
46
- start_recording: undefined;
47
- stop_recording: undefined;
48
- close_stream: undefined;
49
- }>();
27
+ let {
28
+ stream_state = "closed",
29
+ streaming = false,
30
+ pending = false,
31
+ root = "",
32
+ stream_every = 1,
33
+ mode = "image",
34
+ mirror_webcam,
35
+ include_audio,
36
+ webcam_constraints = null,
37
+ i18n,
38
+ upload,
39
+ value = null,
40
+ time_limit = null,
41
+ onstream,
42
+ oncapture,
43
+ onerror,
44
+ onstart_recording,
45
+ onstop_recording,
46
+ onclose_stream
47
+ }: {
48
+ stream_state?: "open" | "waiting" | "closed";
49
+ streaming?: boolean;
50
+ pending?: boolean;
51
+ root?: string;
52
+ stream_every?: number;
53
+ mode?: "image" | "video";
54
+ mirror_webcam: boolean;
55
+ include_audio: boolean;
56
+ webcam_constraints?: { [key: string]: any } | null;
57
+ i18n: I18nFormatter;
58
+ upload: Client["upload"];
59
+ value?: FileData | null | Base64File;
60
+ time_limit?: number | null;
61
+ onstream?: (value: Blob | string) => void;
62
+ oncapture?: (value: FileData | Blob | null) => void;
63
+ onerror?: (error: string) => void;
64
+ onstart_recording?: () => void;
65
+ onstop_recording?: () => void;
66
+ onclose_stream?: () => void;
67
+ } = $props();
50
68
 
51
69
  onMount(() => {
52
70
  canvas = document.createElement("canvas");
@@ -101,11 +119,11 @@
101
119
  });
102
120
 
103
121
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
104
- dispatch("error", i18n("image.no_webcam_support"));
122
+ onerror?.(i18n("image.no_webcam_support"));
105
123
  }
106
124
  } catch (err) {
107
125
  if (err instanceof DOMException && err.name == "NotAllowedError") {
108
- dispatch("error", i18n("image.allow_webcam_access"));
126
+ onerror?.(i18n("image.allow_webcam_access"));
109
127
  } else {
110
128
  throw err;
111
129
  }
@@ -139,13 +157,17 @@
139
157
  }
140
158
  if (streaming) {
141
159
  const image_data = canvas.toDataURL("image/jpeg");
142
- dispatch("stream", image_data);
160
+ onstream?.(image_data);
143
161
  return;
144
162
  }
145
163
 
146
164
  canvas.toBlob(
147
165
  (blob) => {
148
- dispatch(streaming ? "stream" : "capture", blob);
166
+ if (streaming) {
167
+ onstream?.(blob as Blob);
168
+ } else {
169
+ oncapture?.(blob);
170
+ }
149
171
  },
150
172
  `image/${streaming ? "jpeg" : "png"}`,
151
173
  0.8
@@ -153,7 +175,7 @@
153
175
  }
154
176
  }
155
177
 
156
- let recording = false;
178
+ let recording = $state(false);
157
179
  let recorded_blobs: BlobPart[] = [];
158
180
  let stream: MediaStream;
159
181
  let mimeType: string;
@@ -174,13 +196,13 @@
174
196
  let val_ = (
175
197
  (await upload(val, root))?.filter(Boolean) as FileData[]
176
198
  )[0];
177
- dispatch("capture", val_);
178
- dispatch("stop_recording");
199
+ oncapture?.(val_);
200
+ onstop_recording?.();
179
201
  }
180
202
  };
181
203
  ReaderObj.readAsDataURL(video_blob);
182
204
  } else if (typeof MediaRecorder !== "undefined") {
183
- dispatch("start_recording");
205
+ onstart_recording?.();
184
206
  recorded_blobs = [];
185
207
  let validMimeTypes = ["video/webm", "video/mp4"];
186
208
  for (let validMimeType of validMimeTypes) {
@@ -204,7 +226,7 @@
204
226
  recording = !recording;
205
227
  }
206
228
 
207
- let webcam_accessed = false;
229
+ let webcam_accessed = $state(false);
208
230
 
209
231
  function record_video_or_photo({
210
232
  destroy
@@ -222,11 +244,11 @@
222
244
  }
223
245
 
224
246
  if (!recording && stream) {
225
- dispatch("close_stream");
247
+ onclose_stream?.();
226
248
  }
227
249
  }
228
250
 
229
- let options_open = false;
251
+ let options_open = $state(false);
230
252
 
231
253
  export function click_outside(node: Node, cb: any): any {
232
254
  const handle_click = (event: MouseEvent): void => {
@@ -281,12 +303,12 @@
281
303
  title="grant webcam access"
282
304
  style="height: 100%"
283
305
  >
284
- <WebcamPermissions on:click={async () => access_webcam()} />
306
+ <WebcamPermissions onclick={async () => access_webcam()} />
285
307
  </div>
286
308
  {:else}
287
309
  <div class="button-wrap">
288
310
  <button
289
- on:click={() => record_video_or_photo()}
311
+ onclick={() => record_video_or_photo()}
290
312
  aria-label={mode === "image" ? "capture photo" : "start recording"}
291
313
  >
292
314
  {#if mode === "video" || streaming}
@@ -321,7 +343,7 @@
321
343
  {#if !recording}
322
344
  <button
323
345
  class="icon"
324
- on:click={() => (options_open = true)}
346
+ onclick={() => (options_open = true)}
325
347
  aria-label="select input source"
326
348
  >
327
349
  <DropdownArrow />
@@ -333,14 +355,8 @@
333
355
  class="select-wrap"
334
356
  aria-label="select source"
335
357
  use:click_outside={handle_click_outside}
336
- on:change={handle_device_change}
358
+ onchange={handle_device_change}
337
359
  >
338
- <!-- <button
339
- class="inset-icon"
340
- on:click|stopPropagation={() => (options_open = false)}
341
- >
342
- <DropdownArrow />
343
- </button> -->
344
360
  {#if available_video_devices.length === 0}
345
361
  <option value="">{i18n("common.no_devices")}</option>
346
362
  {:else}
@@ -1,13 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { Webcam } from "@gradio/icons";
3
- import { createEventDispatcher } from "svelte";
4
3
 
5
- const dispatch = createEventDispatcher<{
6
- click: undefined;
7
- }>();
4
+ let { onclick }: { onclick?: () => void } = $props();
8
5
  </script>
9
6
 
10
- <button style:height="100%" on:click={() => dispatch("click")}>
7
+ <button style:height="100%" {onclick}>
11
8
  <div class="wrap">
12
9
  <span class="icon-wrap">
13
10
  <Webcam />
package/shared/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { LoadingStatus } from "@gradio/statustracker";
1
+ import type { ILoadingStatus as LoadingStatus } from "@gradio/statustracker";
2
2
  import type { FileData } from "@gradio/client";
3
3
  import type { CustomButton } from "@gradio/utils";
4
4
 
package/shared/utils.ts CHANGED
@@ -9,19 +9,17 @@ export const get_coordinates_of_clicked_image = (
9
9
  }
10
10
 
11
11
  const imageRect = image.getBoundingClientRect();
12
- const xScale = image.naturalWidth / imageRect.width;
13
- const yScale = image.naturalHeight / imageRect.height;
14
- if (xScale > yScale) {
15
- const displayed_height = image.naturalHeight / xScale;
16
- const y_offset = (imageRect.height - displayed_height) / 2;
17
- var x = Math.round((evt.clientX - imageRect.left) * xScale);
18
- var y = Math.round((evt.clientY - imageRect.top - y_offset) * xScale);
19
- } else {
20
- const displayed_width = image.naturalWidth / yScale;
21
- const x_offset = (imageRect.width - displayed_width) / 2;
22
- var x = Math.round((evt.clientX - imageRect.left - x_offset) * yScale);
23
- var y = Math.round((evt.clientY - imageRect.top) * yScale);
24
- }
12
+ // The image is rendered with `object-fit: scale-down`: centered, never
13
+ // scaled above its natural size, so empty space can appear on both axes.
14
+ const scale = Math.min(
15
+ imageRect.width / image.naturalWidth,
16
+ imageRect.height / image.naturalHeight,
17
+ 1
18
+ );
19
+ const x_offset = (imageRect.width - image.naturalWidth * scale) / 2;
20
+ const y_offset = (imageRect.height - image.naturalHeight * scale) / 2;
21
+ const x = Math.round((evt.clientX - imageRect.left - x_offset) / scale);
22
+ const y = Math.round((evt.clientY - imageRect.top - y_offset) / scale);
25
23
  if (x < 0 || x >= image.naturalWidth || y < 0 || y >= image.naturalHeight) {
26
24
  return null;
27
25
  }