@gradio/image 0.3.0-beta.7 → 0.3.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.
@@ -0,0 +1,224 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher, tick } from "svelte";
3
+ import { BlockLabel } from "@gradio/atoms";
4
+ import { Image } from "@gradio/icons";
5
+ import type { SelectData, I18nFormatter } from "@gradio/utils";
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
+ import Webcam from "./Webcam.svelte";
13
+ import { Toolbar, IconButton } from "@gradio/atoms";
14
+
15
+ import { Upload } from "@gradio/upload";
16
+ import { type FileData, normalise_file } from "@gradio/client";
17
+ import ClearImage from "./ClearImage.svelte";
18
+
19
+ export let value: null | FileData;
20
+ export let label: string | undefined = undefined;
21
+ export let show_label: boolean;
22
+
23
+ export let sources: ("clipboard" | "webcam" | "upload")[] = [
24
+ "upload",
25
+ "clipboard",
26
+ "webcam"
27
+ ];
28
+ export let streaming = false;
29
+ export let pending = false;
30
+ export let mirror_webcam: boolean;
31
+ export let selectable = false;
32
+ export let root: string;
33
+ export let i18n: I18nFormatter;
34
+
35
+ let upload: Upload;
36
+ export let active_tool: "webcam" | null = null;
37
+
38
+ function handle_upload({ detail }: CustomEvent<FileData>): void {
39
+ value = normalise_file(detail, root, null);
40
+ }
41
+
42
+ async function handle_save(img_blob: Blob | any): Promise<void> {
43
+ pending = true;
44
+ const f = await upload.load_files([new File([img_blob], `webcam.png`)]);
45
+
46
+ value = f?.[0] || null;
47
+ if (!streaming) active_tool = null;
48
+
49
+ await tick();
50
+
51
+ dispatch(streaming ? "stream" : "change");
52
+ pending = false;
53
+ }
54
+
55
+ $: value && !value.url && (value = normalise_file(value, root, null));
56
+
57
+ const dispatch = createEventDispatcher<{
58
+ change?: never;
59
+ stream?: never;
60
+ clear?: never;
61
+ drag: boolean;
62
+ upload?: never;
63
+ select: SelectData;
64
+ }>();
65
+
66
+ let dragging = false;
67
+
68
+ $: dispatch("drag", dragging);
69
+
70
+ function handle_click(evt: MouseEvent): void {
71
+ let coordinates = get_coordinates_of_clicked_image(evt);
72
+ if (coordinates) {
73
+ dispatch("select", { index: coordinates, value: null });
74
+ }
75
+ }
76
+
77
+ const sources_meta = {
78
+ upload: {
79
+ icon: UploadIcon,
80
+ label: i18n("Upload"),
81
+ order: 0
82
+ },
83
+ webcam: {
84
+ icon: WebcamIcon,
85
+ label: i18n("Webcam"),
86
+ order: 1
87
+ },
88
+ clipboard: {
89
+ icon: ImagePaste,
90
+ label: i18n("Paste"),
91
+ order: 2
92
+ }
93
+ };
94
+
95
+ $: sources_list = sources.sort(
96
+ (a, b) => sources_meta[a].order - sources_meta[b].order
97
+ );
98
+
99
+ $: {
100
+ if (sources.length === 1 && sources[0] === "webcam") {
101
+ active_tool = "webcam";
102
+ }
103
+ }
104
+
105
+ async function handle_toolbar(
106
+ source: (typeof sources)[number]
107
+ ): Promise<void> {
108
+ switch (source) {
109
+ case "clipboard":
110
+ navigator.clipboard.read().then(async (items) => {
111
+ for (let i = 0; i < items.length; i++) {
112
+ const type = items[i].types.find((t) => t.startsWith("image/"));
113
+ if (type) {
114
+ items[i].getType(type).then(async (blob) => {
115
+ const f = await upload.load_files([
116
+ new File([blob], `clipboard.${type.replace("image/", "")}`)
117
+ ]);
118
+ f;
119
+ value = f?.[0] || null;
120
+ });
121
+ break;
122
+ }
123
+ }
124
+ });
125
+ break;
126
+ case "webcam":
127
+ active_tool = "webcam";
128
+ break;
129
+ case "upload":
130
+ upload.open_file_upload();
131
+ break;
132
+ default:
133
+ break;
134
+ }
135
+ }
136
+ </script>
137
+
138
+ <BlockLabel {show_label} Icon={Image} label={label || "Image"} />
139
+
140
+ <div data-testid="image" class="image-container">
141
+ {#if value?.url}
142
+ <ClearImage on:remove_image={() => (value = null)} />
143
+ {/if}
144
+ <div class="upload-container">
145
+ <Upload
146
+ hidden={value !== null || active_tool === "webcam"}
147
+ bind:this={upload}
148
+ bind:dragging
149
+ filetype="image/*"
150
+ on:load={handle_upload}
151
+ on:error
152
+ {root}
153
+ disable_click={!sources.includes("upload")}
154
+ >
155
+ {#if value === null && !active_tool}
156
+ <slot />
157
+ {/if}
158
+ </Upload>
159
+ {#if active_tool === "webcam"}
160
+ <Webcam
161
+ on:capture={(e) => handle_save(e.detail)}
162
+ on:stream={(e) => handle_save(e.detail)}
163
+ on:error
164
+ on:drag
165
+ on:upload={(e) => handle_save(e.detail)}
166
+ {mirror_webcam}
167
+ {streaming}
168
+ mode="image"
169
+ include_audio={false}
170
+ {i18n}
171
+ />
172
+ {:else if value !== null && !streaming}
173
+ <!-- svelte-ignore a11y-click-events-have-key-events-->
174
+ <!-- svelte-ignore a11y-no-noninteractive-element-interactions-->
175
+ <img
176
+ src={value.url}
177
+ alt={value.alt_text}
178
+ on:click={handle_click}
179
+ class:selectable
180
+ />
181
+ {/if}
182
+ </div>
183
+ {#if sources.length > 1 || sources.includes("clipboard")}
184
+ <Toolbar show_border={!value?.url}>
185
+ {#each sources_list as source}
186
+ <IconButton
187
+ on:click={() => handle_toolbar(source)}
188
+ Icon={sources_meta[source].icon}
189
+ size="large"
190
+ padded={false}
191
+ />
192
+ {/each}
193
+ </Toolbar>
194
+ {/if}
195
+ </div>
196
+
197
+ <style>
198
+ /* .image-container {
199
+ height: auto;
200
+ } */
201
+ img {
202
+ width: var(--size-full);
203
+ height: var(--size-full);
204
+ }
205
+
206
+ .upload-container {
207
+ height: 100%;
208
+ flex-shrink: 1;
209
+ max-height: 100%;
210
+ }
211
+
212
+ .image-container {
213
+ display: flex;
214
+ height: 100%;
215
+ flex-direction: column;
216
+ justify-content: center;
217
+ align-items: center;
218
+ max-height: 100%;
219
+ }
220
+
221
+ .selectable {
222
+ cursor: crosshair;
223
+ }
224
+ </style>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher, onMount } from "svelte";
3
- import { Camera, Circle, Square } from "@gradio/icons";
4
- import type { I18nFormatter } from "js/utils/src";
2
+ import { createEventDispatcher, onMount, tick } from "svelte";
3
+ import { Camera, Circle, Square, DropdownArrow } from "@gradio/icons";
4
+ import type { I18nFormatter } from "@gradio/utils";
5
5
 
6
6
  let video_source: HTMLVideoElement;
7
7
  let canvas: HTMLCanvasElement;
@@ -22,7 +22,7 @@
22
22
  is_example?: boolean;
23
23
  is_file: boolean;
24
24
  }
25
- | string;
25
+ | Blob;
26
26
  error: string;
27
27
  start_recording: undefined;
28
28
  stop_recording: undefined;
@@ -30,11 +30,15 @@
30
30
 
31
31
  onMount(() => (canvas = document.createElement("canvas")));
32
32
 
33
- async function access_webcam(): Promise<void> {
33
+ async function access_webcam(device_id?: string): Promise<void> {
34
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
35
+ dispatch("error", i18n("image.no_webcam_support"));
36
+ return;
37
+ }
34
38
  try {
35
39
  stream = await navigator.mediaDevices.getUserMedia({
36
- video: true,
37
- audio: include_audio,
40
+ video: device_id ? { deviceId: { exact: device_id } } : true,
41
+ audio: include_audio
38
42
  });
39
43
  video_source.srcObject = stream;
40
44
  video_source.muted = true;
@@ -62,8 +66,18 @@
62
66
  video_source.videoHeight
63
67
  );
64
68
 
65
- var data = canvas.toDataURL("image/png");
66
- dispatch(streaming ? "stream" : "capture", data);
69
+ if (mirror_webcam) {
70
+ context.scale(-1, 1);
71
+ context.drawImage(video_source, -video_source.videoWidth, 0);
72
+ }
73
+
74
+ canvas.toBlob(
75
+ (blob) => {
76
+ dispatch(streaming ? "stream" : "capture", blob);
77
+ },
78
+ "image/png",
79
+ 0.8
80
+ );
67
81
  }
68
82
  }
69
83
 
@@ -105,7 +119,7 @@
105
119
  return;
106
120
  }
107
121
  media_recorder = new MediaRecorder(stream, {
108
- mimeType: mimeType,
122
+ mimeType: mimeType
109
123
  });
110
124
  media_recorder.addEventListener("dataavailable", function (e) {
111
125
  recorded_blobs.push(e.data);
@@ -124,6 +138,46 @@
124
138
  }
125
139
  }, 500);
126
140
  }
141
+
142
+ async function select_source(): Promise<void> {
143
+ const devices = await navigator.mediaDevices.enumerateDevices();
144
+ video_sources = devices.filter((device) => device.kind === "videoinput");
145
+ options_open = true;
146
+ }
147
+
148
+ let video_sources: MediaDeviceInfo[] = [];
149
+ async function selectVideoSource(device_id: string): Promise<void> {
150
+ await access_webcam(device_id);
151
+ options_open = false;
152
+ }
153
+
154
+ let options_open = false;
155
+
156
+ export function click_outside(node: Node, cb: any): any {
157
+ const handle_click = (event: MouseEvent): void => {
158
+ if (
159
+ node &&
160
+ !node.contains(event.target as Node) &&
161
+ !event.defaultPrevented
162
+ ) {
163
+ cb(event);
164
+ }
165
+ };
166
+
167
+ document.addEventListener("click", handle_click, true);
168
+
169
+ return {
170
+ destroy() {
171
+ document.removeEventListener("click", handle_click, true);
172
+ }
173
+ };
174
+ }
175
+
176
+ function handle_click_outside(event: MouseEvent): void {
177
+ event.preventDefault();
178
+ event.stopPropagation();
179
+ options_open = false;
180
+ }
127
181
  </script>
128
182
 
129
183
  <div class="wrap">
@@ -131,26 +185,59 @@
131
185
  <!-- need to suppress for video streaming https://github.com/sveltejs/svelte/issues/5967 -->
132
186
  <video bind:this={video_source} class:flip={mirror_webcam} />
133
187
  {#if !streaming}
134
- <button
135
- on:click={mode === "image" ? take_picture : take_recording}
136
- aria-label={mode === "image" ? "capture photo" : "start recording"}
137
- >
138
- {#if mode === "video"}
139
- {#if recording}
140
- <div class="icon" title="stop recording">
141
- <Square />
142
- </div>
188
+ <div class:capture={!recording} class="button-wrap">
189
+ <button
190
+ on:click={mode === "image" ? take_picture : take_recording}
191
+ aria-label={mode === "image" ? "capture photo" : "start recording"}
192
+ >
193
+ {#if mode === "video"}
194
+ {#if recording}
195
+ <div class="icon" title="stop recording">
196
+ <Square />
197
+ </div>
198
+ {:else}
199
+ <div class="icon" title="start recording">
200
+ <Circle />
201
+ </div>
202
+ {/if}
143
203
  {:else}
144
- <div class="icon" title="start recording">
145
- <Circle />
204
+ <div class="icon" title="capture photo">
205
+ <Camera />
146
206
  </div>
147
207
  {/if}
148
- {:else}
149
- <div class="icon" title="capture photo">
150
- <Camera />
151
- </div>
208
+ </button>
209
+
210
+ {#if !recording}
211
+ <button
212
+ on:click={select_source}
213
+ aria-label={mode === "image" ? "capture photo" : "start recording"}
214
+ >
215
+ <div class="icon" title="select video source">
216
+ <DropdownArrow />
217
+ </div>
218
+
219
+ {#if options_open}
220
+ <div class="select-wrap" use:click_outside={handle_click_outside}>
221
+ <!-- svelte-ignore a11y-click-events-have-key-events-->
222
+ <!-- svelte-ignore a11y-no-static-element-interactions-->
223
+ <span
224
+ class="inset-icon"
225
+ on:click|stopPropagation={() => (options_open = false)}
226
+ >
227
+ <DropdownArrow />
228
+ </span>
229
+ {#each video_sources as source}
230
+ <!-- svelte-ignore a11y-click-events-have-key-events-->
231
+ <!-- svelte-ignore a11y-no-static-element-interactions-->
232
+ <div on:click={() => selectVideoSource(source.deviceId)}>
233
+ {source.label}
234
+ </div>
235
+ {/each}
236
+ </div>
237
+ {/if}
238
+ </button>
152
239
  {/if}
153
- </button>
240
+ </div>
154
241
  {/if}
155
242
  </div>
156
243
 
@@ -167,7 +254,7 @@
167
254
  height: var(--size-full);
168
255
  }
169
256
 
170
- button {
257
+ .button-wrap {
171
258
  display: flex;
172
259
  position: absolute;
173
260
  right: 0px;
@@ -180,7 +267,15 @@
180
267
  border-radius: var(--radius-xl);
181
268
  background-color: rgba(0, 0, 0, 0.9);
182
269
  width: var(--size-10);
183
- height: var(--size-10);
270
+ height: var(--size-8);
271
+ padding: var(--size-2-5);
272
+ padding-right: var(--size-1);
273
+ z-index: var(--layer-3);
274
+ }
275
+
276
+ .capture {
277
+ width: var(--size-14);
278
+ transform: translateX(var(--size-2-5));
184
279
  }
185
280
 
186
281
  @media (--screen-md) {
@@ -197,12 +292,60 @@
197
292
 
198
293
  .icon {
199
294
  opacity: 0.8;
200
- width: 50%;
201
- height: 50%;
295
+ width: 100%;
296
+ height: 100%;
202
297
  color: white;
298
+ display: flex;
299
+ justify-content: space-between;
300
+ align-items: center;
203
301
  }
204
302
 
205
303
  .flip {
206
304
  transform: scaleX(-1);
207
305
  }
306
+
307
+ .select-wrap {
308
+ -webkit-appearance: none;
309
+ -moz-appearance: none;
310
+ appearance: none;
311
+ background-color: transparent;
312
+ border: none;
313
+ width: auto;
314
+ font-size: 1rem;
315
+ /* padding: 0.5rem; */
316
+ width: max-content;
317
+ position: absolute;
318
+ top: 0;
319
+ right: 0;
320
+ background-color: var(--block-background-fill);
321
+ box-shadow: var(--shadow-drop-lg);
322
+ border-radius: var(--radius-xl);
323
+ z-index: var(--layer-top);
324
+ border: 1px solid var(--border-color-accent);
325
+ text-align: left;
326
+ overflow: hidden;
327
+ }
328
+
329
+ .select-wrap > div {
330
+ padding: 0.25rem 0.5rem;
331
+ border-bottom: 1px solid var(--border-color-accent);
332
+ padding-right: var(--size-8);
333
+ }
334
+
335
+ .select-wrap > div:hover {
336
+ background-color: var(--color-accent);
337
+ }
338
+
339
+ .select-wrap > div:last-child {
340
+ border: none;
341
+ }
342
+
343
+ .inset-icon {
344
+ position: absolute;
345
+ top: 5px;
346
+ right: -6.5px;
347
+ width: var(--size-10);
348
+ height: var(--size-5);
349
+ opacity: 0.8;
350
+ }
208
351
  </style>
@@ -1,33 +0,0 @@
1
- <svelte:options accessors={true} />
2
-
3
- <script lang="ts">
4
- import Cropper from "cropperjs";
5
- import { onMount, createEventDispatcher } from "svelte";
6
-
7
- export let image: string;
8
- let el: HTMLImageElement;
9
-
10
- const dispatch = createEventDispatcher();
11
- let cropper: Cropper;
12
-
13
- export function destroy(): void {
14
- cropper.destroy();
15
- }
16
-
17
- export function create(): void {
18
- if (cropper) {
19
- destroy();
20
- }
21
- cropper = new Cropper(el, {
22
- autoCropArea: 1,
23
- cropend(): void {
24
- const image_data = cropper.getCroppedCanvas().toDataURL();
25
- dispatch("crop", image_data);
26
- }
27
- });
28
-
29
- dispatch("crop", image);
30
- }
31
- </script>
32
-
33
- <img src={image} bind:this={el} alt="" />