@gradio/image 0.14.0 → 0.16.0-beta.1

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,32 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { SelectData } from "@gradio/utils";
3
+ import { type FileData } from "@gradio/client";
4
+ import type { I18nFormatter } from "@gradio/utils";
5
+ declare const __propDef: {
6
+ props: {
7
+ value: null | FileData;
8
+ label?: string | undefined;
9
+ show_label: boolean;
10
+ show_download_button?: boolean | undefined;
11
+ selectable?: boolean | undefined;
12
+ show_share_button?: boolean | undefined;
13
+ i18n: I18nFormatter;
14
+ show_fullscreen_button?: boolean | undefined;
15
+ };
16
+ events: {
17
+ share: CustomEvent<import("@gradio/utils").ShareData>;
18
+ error: CustomEvent<string>;
19
+ load: CustomEvent<void | undefined> | undefined;
20
+ change: CustomEvent<string>;
21
+ select: CustomEvent<SelectData>;
22
+ } & {
23
+ [evt: string]: CustomEvent<any>;
24
+ };
25
+ slots: {};
26
+ };
27
+ export type ImagePreviewProps = typeof __propDef.props;
28
+ export type ImagePreviewEvents = typeof __propDef.events;
29
+ export type ImagePreviewSlots = typeof __propDef.slots;
30
+ export default class ImagePreview extends SvelteComponent<ImagePreviewProps, ImagePreviewEvents, ImagePreviewSlots> {
31
+ }
32
+ export {};
@@ -0,0 +1,198 @@
1
+ <script>import { createEventDispatcher, tick } from "svelte";
2
+ import { BlockLabel } from "@gradio/atoms";
3
+ import { Image as ImageIcon } from "@gradio/icons";
4
+ import {
5
+ } from "@gradio/utils";
6
+ import { get_coordinates_of_clicked_image } from "./utils";
7
+ import Webcam from "./Webcam.svelte";
8
+ import { Upload } from "@gradio/upload";
9
+ import { FileData } from "@gradio/client";
10
+ import ClearImage from "./ClearImage.svelte";
11
+ import { SelectSource } from "@gradio/atoms";
12
+ import Image from "./Image.svelte";
13
+ export let value;
14
+ export let label = void 0;
15
+ export let show_label;
16
+ export let sources = ["upload", "clipboard", "webcam"];
17
+ export let streaming = false;
18
+ export let pending = false;
19
+ export let mirror_webcam;
20
+ export let selectable = false;
21
+ export let root;
22
+ export let i18n;
23
+ export let max_file_size = null;
24
+ export let upload;
25
+ export let stream_handler;
26
+ export let stream_every;
27
+ export let modify_stream;
28
+ export let set_time_limit;
29
+ let upload_input;
30
+ let uploading = false;
31
+ export let active_source = null;
32
+ function handle_upload({ detail }) {
33
+ if (!streaming) {
34
+ value = detail;
35
+ dispatch("upload");
36
+ }
37
+ }
38
+ function handle_clear() {
39
+ value = null;
40
+ dispatch("clear");
41
+ dispatch("change", null);
42
+ }
43
+ async function handle_save(img_blob, event) {
44
+ pending = true;
45
+ const f = await upload_input.load_files([
46
+ new File([img_blob], `image/${streaming ? "jpeg" : "png"}`)
47
+ ]);
48
+ if (event === "change" || event === "upload") {
49
+ value = f?.[0] || null;
50
+ await tick();
51
+ dispatch("change");
52
+ } else {
53
+ dispatch("stream", { value: f?.[0] || null, is_value_data: true });
54
+ }
55
+ pending = false;
56
+ }
57
+ $:
58
+ active_streaming = streaming && active_source === "webcam";
59
+ $:
60
+ if (uploading && !active_streaming)
61
+ value = null;
62
+ const dispatch = createEventDispatcher();
63
+ export let dragging = false;
64
+ $:
65
+ dispatch("drag", dragging);
66
+ function handle_click(evt) {
67
+ let coordinates = get_coordinates_of_clicked_image(evt);
68
+ if (coordinates) {
69
+ dispatch("select", { index: coordinates, value: null });
70
+ }
71
+ }
72
+ $:
73
+ if (!active_source && sources) {
74
+ active_source = sources[0];
75
+ }
76
+ async function handle_select_source(source) {
77
+ switch (source) {
78
+ case "clipboard":
79
+ upload_input.paste_clipboard();
80
+ break;
81
+ default:
82
+ break;
83
+ }
84
+ }
85
+ </script>
86
+
87
+ <BlockLabel {show_label} Icon={ImageIcon} label={label || "Image"} />
88
+
89
+ <div data-testid="image" class="image-container">
90
+ {#if value?.url && !active_streaming}
91
+ <ClearImage
92
+ on:remove_image={() => {
93
+ value = null;
94
+ dispatch("clear");
95
+ }}
96
+ />
97
+ {/if}
98
+ <div
99
+ class="upload-container"
100
+ class:reduced-height={sources.length > 1}
101
+ style:width={value ? "auto" : "100%"}
102
+ >
103
+ <Upload
104
+ hidden={value !== null || active_source === "webcam"}
105
+ bind:this={upload_input}
106
+ bind:uploading
107
+ bind:dragging
108
+ filetype={active_source === "clipboard" ? "clipboard" : "image/*"}
109
+ on:load={handle_upload}
110
+ on:error
111
+ {root}
112
+ {max_file_size}
113
+ disable_click={!sources.includes("upload") || value !== null}
114
+ {upload}
115
+ {stream_handler}
116
+ >
117
+ {#if value === null}
118
+ <slot />
119
+ {/if}
120
+ </Upload>
121
+ {#if active_source === "webcam" && (streaming || (!streaming && !value))}
122
+ <Webcam
123
+ {root}
124
+ {value}
125
+ on:capture={(e) => handle_save(e.detail, "change")}
126
+ on:stream={(e) => handle_save(e.detail, "stream")}
127
+ on:error
128
+ on:drag
129
+ on:upload={(e) => handle_save(e.detail, "upload")}
130
+ on:close_stream
131
+ {mirror_webcam}
132
+ {stream_every}
133
+ {streaming}
134
+ mode="image"
135
+ include_audio={false}
136
+ {i18n}
137
+ {upload}
138
+ bind:modify_stream
139
+ bind:set_time_limit
140
+ />
141
+ {:else if value !== null && !streaming}
142
+ <!-- svelte-ignore a11y-click-events-have-key-events-->
143
+ <!-- svelte-ignore a11y-no-static-element-interactions-->
144
+ <div class:selectable class="image-frame" on:click={handle_click}>
145
+ <Image src={value.url} alt={value.alt_text} />
146
+ </div>
147
+ {/if}
148
+ </div>
149
+ {#if sources.length > 1 || sources.includes("clipboard")}
150
+ <SelectSource
151
+ {sources}
152
+ bind:active_source
153
+ {handle_clear}
154
+ handle_select={handle_select_source}
155
+ />
156
+ {/if}
157
+ </div>
158
+
159
+ <style>
160
+ .image-frame :global(img) {
161
+ width: var(--size-full);
162
+ height: var(--size-full);
163
+ object-fit: scale-down;
164
+ }
165
+
166
+ .image-frame {
167
+ object-fit: cover;
168
+ width: 100%;
169
+ height: 100%;
170
+ }
171
+
172
+ .upload-container {
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+
177
+ height: 100%;
178
+ flex-shrink: 1;
179
+ max-height: 100%;
180
+ }
181
+
182
+ .reduced-height {
183
+ height: calc(100% - var(--size-10));
184
+ }
185
+
186
+ .image-container {
187
+ display: flex;
188
+ height: 100%;
189
+ flex-direction: column;
190
+ justify-content: center;
191
+ align-items: center;
192
+ max-height: 100%;
193
+ }
194
+
195
+ .selectable {
196
+ cursor: crosshair;
197
+ }
198
+ </style>
@@ -0,0 +1,47 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import { type SelectData, type I18nFormatter, type ValueData } from "@gradio/utils";
3
+ import { FileData, type Client } from "@gradio/client";
4
+ declare const __propDef: {
5
+ props: {
6
+ value: null | FileData;
7
+ label?: string | undefined;
8
+ show_label: boolean;
9
+ sources?: ("upload" | "clipboard" | "microphone" | "webcam" | null)[] | undefined;
10
+ streaming?: boolean | undefined;
11
+ pending?: boolean | undefined;
12
+ mirror_webcam: boolean;
13
+ selectable?: boolean | undefined;
14
+ root: string;
15
+ i18n: I18nFormatter;
16
+ max_file_size?: (number | null) | undefined;
17
+ upload: Client["upload"];
18
+ stream_handler: Client["stream"];
19
+ stream_every: number;
20
+ modify_stream: (state: "open" | "closed" | "waiting") => void;
21
+ set_time_limit: (arg0: number) => void;
22
+ active_source?: ("upload" | "clipboard" | "microphone" | "webcam" | null) | undefined;
23
+ dragging?: boolean | undefined;
24
+ };
25
+ events: {
26
+ error: CustomEvent<string> | CustomEvent<any>;
27
+ drag: CustomEvent<any>;
28
+ close_stream: CustomEvent<undefined>;
29
+ change?: CustomEvent<undefined> | undefined;
30
+ stream: CustomEvent<ValueData>;
31
+ clear?: CustomEvent<undefined> | undefined;
32
+ upload?: CustomEvent<undefined> | undefined;
33
+ select: CustomEvent<SelectData>;
34
+ end_stream: CustomEvent<never>;
35
+ } & {
36
+ [evt: string]: CustomEvent<any>;
37
+ };
38
+ slots: {
39
+ default: {};
40
+ };
41
+ };
42
+ export type ImageUploaderProps = typeof __propDef.props;
43
+ export type ImageUploaderEvents = typeof __propDef.events;
44
+ export type ImageUploaderSlots = typeof __propDef.slots;
45
+ export default class ImageUploader extends SvelteComponent<ImageUploaderProps, ImageUploaderEvents, ImageUploaderSlots> {
46
+ }
47
+ export {};
@@ -0,0 +1,431 @@
1
+ <script>import { createEventDispatcher, onMount } from "svelte";
2
+ import {
3
+ Camera,
4
+ Circle,
5
+ Square,
6
+ DropdownArrow,
7
+ Spinner
8
+ } from "@gradio/icons";
9
+ import { StreamingBar } from "@gradio/statustracker";
10
+ import { prepare_files } from "@gradio/client";
11
+ import WebcamPermissions from "./WebcamPermissions.svelte";
12
+ import { fade } from "svelte/transition";
13
+ import {
14
+ get_devices,
15
+ get_video_stream,
16
+ set_available_devices
17
+ } from "./stream_utils";
18
+ let video_source;
19
+ let available_video_devices = [];
20
+ let selected_device = null;
21
+ let time_limit = null;
22
+ let stream_state = "closed";
23
+ export const modify_stream = (state) => {
24
+ if (state === "closed") {
25
+ time_limit = null;
26
+ stream_state = "closed";
27
+ value = null;
28
+ } else if (state === "waiting") {
29
+ stream_state = "waiting";
30
+ } else {
31
+ stream_state = "open";
32
+ }
33
+ };
34
+ export const set_time_limit = (time) => {
35
+ if (recording)
36
+ time_limit = time;
37
+ };
38
+ let canvas;
39
+ export let streaming = false;
40
+ export let pending = false;
41
+ export let root = "";
42
+ export let stream_every = 1;
43
+ export let mode = "image";
44
+ export let mirror_webcam;
45
+ export let include_audio;
46
+ export let i18n;
47
+ export let upload;
48
+ export let value = null;
49
+ const dispatch = createEventDispatcher();
50
+ onMount(() => canvas = document.createElement("canvas"));
51
+ const handle_device_change = async (event) => {
52
+ const target = event.target;
53
+ const device_id = target.value;
54
+ await get_video_stream(include_audio, video_source, device_id).then(
55
+ async (local_stream) => {
56
+ stream = local_stream;
57
+ selected_device = available_video_devices.find(
58
+ (device) => device.deviceId === device_id
59
+ ) || null;
60
+ options_open = false;
61
+ }
62
+ );
63
+ };
64
+ async function access_webcam() {
65
+ try {
66
+ get_video_stream(include_audio, video_source).then(async (local_stream) => {
67
+ webcam_accessed = true;
68
+ available_video_devices = await get_devices();
69
+ stream = local_stream;
70
+ }).then(() => set_available_devices(available_video_devices)).then((devices) => {
71
+ available_video_devices = devices;
72
+ const used_devices = stream.getTracks().map((track) => track.getSettings()?.deviceId)[0];
73
+ selected_device = used_devices ? devices.find((device) => device.deviceId === used_devices) || available_video_devices[0] : available_video_devices[0];
74
+ });
75
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
76
+ dispatch("error", i18n("image.no_webcam_support"));
77
+ }
78
+ } catch (err) {
79
+ if (err instanceof DOMException && err.name == "NotAllowedError") {
80
+ dispatch("error", i18n("image.allow_webcam_access"));
81
+ } else {
82
+ throw err;
83
+ }
84
+ }
85
+ }
86
+ function take_picture() {
87
+ var context = canvas.getContext("2d");
88
+ if ((!streaming || streaming && recording) && video_source.videoWidth && video_source.videoHeight) {
89
+ canvas.width = video_source.videoWidth;
90
+ canvas.height = video_source.videoHeight;
91
+ context.drawImage(
92
+ video_source,
93
+ 0,
94
+ 0,
95
+ video_source.videoWidth,
96
+ video_source.videoHeight
97
+ );
98
+ if (mirror_webcam) {
99
+ context.scale(-1, 1);
100
+ context.drawImage(video_source, -video_source.videoWidth, 0);
101
+ }
102
+ if (streaming && (!recording || stream_state === "waiting")) {
103
+ return;
104
+ }
105
+ canvas.toBlob(
106
+ (blob) => {
107
+ dispatch(streaming ? "stream" : "capture", blob);
108
+ },
109
+ `image/${streaming ? "jpeg" : "png"}`,
110
+ 0.8
111
+ );
112
+ }
113
+ }
114
+ let recording = false;
115
+ let recorded_blobs = [];
116
+ let stream;
117
+ let mimeType;
118
+ let media_recorder;
119
+ function take_recording() {
120
+ if (recording) {
121
+ media_recorder.stop();
122
+ let video_blob = new Blob(recorded_blobs, { type: mimeType });
123
+ let ReaderObj = new FileReader();
124
+ ReaderObj.onload = async function(e) {
125
+ if (e.target) {
126
+ let _video_blob = new File(
127
+ [video_blob],
128
+ "sample." + mimeType.substring(6)
129
+ );
130
+ const val = await prepare_files([_video_blob]);
131
+ let val_ = ((await upload(val, root))?.filter(Boolean))[0];
132
+ dispatch("capture", val_);
133
+ dispatch("stop_recording");
134
+ }
135
+ };
136
+ ReaderObj.readAsDataURL(video_blob);
137
+ } else {
138
+ dispatch("start_recording");
139
+ recorded_blobs = [];
140
+ let validMimeTypes = ["video/webm", "video/mp4"];
141
+ for (let validMimeType of validMimeTypes) {
142
+ if (MediaRecorder.isTypeSupported(validMimeType)) {
143
+ mimeType = validMimeType;
144
+ break;
145
+ }
146
+ }
147
+ if (mimeType === null) {
148
+ console.error("No supported MediaRecorder mimeType");
149
+ return;
150
+ }
151
+ media_recorder = new MediaRecorder(stream, {
152
+ mimeType
153
+ });
154
+ media_recorder.addEventListener("dataavailable", function(e) {
155
+ recorded_blobs.push(e.data);
156
+ });
157
+ media_recorder.start(200);
158
+ }
159
+ recording = !recording;
160
+ }
161
+ let webcam_accessed = false;
162
+ function record_video_or_photo() {
163
+ if (mode === "image" && streaming) {
164
+ recording = !recording;
165
+ }
166
+ if (mode === "image") {
167
+ take_picture();
168
+ } else {
169
+ take_recording();
170
+ }
171
+ if (!recording && stream) {
172
+ dispatch("close_stream");
173
+ stream.getTracks().forEach((track) => track.stop());
174
+ video_source.srcObject = null;
175
+ webcam_accessed = false;
176
+ window.setTimeout(() => {
177
+ value = null;
178
+ }, 500);
179
+ value = null;
180
+ }
181
+ }
182
+ if (streaming && mode === "image") {
183
+ window.setInterval(() => {
184
+ if (video_source && !pending) {
185
+ take_picture();
186
+ }
187
+ }, stream_every * 1e3);
188
+ }
189
+ let options_open = false;
190
+ export function click_outside(node, cb) {
191
+ const handle_click = (event) => {
192
+ if (node && !node.contains(event.target) && !event.defaultPrevented) {
193
+ cb(event);
194
+ }
195
+ };
196
+ document.addEventListener("click", handle_click, true);
197
+ return {
198
+ destroy() {
199
+ document.removeEventListener("click", handle_click, true);
200
+ }
201
+ };
202
+ }
203
+ function handle_click_outside(event) {
204
+ event.preventDefault();
205
+ event.stopPropagation();
206
+ options_open = false;
207
+ }
208
+ </script>
209
+
210
+ <div class="wrap">
211
+ <StreamingBar {time_limit} />
212
+ <!-- svelte-ignore a11y-media-has-caption -->
213
+ <!-- need to suppress for video streaming https://github.com/sveltejs/svelte/issues/5967 -->
214
+ <video
215
+ bind:this={video_source}
216
+ class:flip={mirror_webcam}
217
+ class:hide={!webcam_accessed || (webcam_accessed && !!value)}
218
+ />
219
+ <!-- svelte-ignore a11y-missing-attribute -->
220
+ <img
221
+ src={value?.url}
222
+ class:hide={!webcam_accessed || (webcam_accessed && !value)}
223
+ />
224
+ {#if !webcam_accessed}
225
+ <div
226
+ in:fade={{ delay: 100, duration: 200 }}
227
+ title="grant webcam access"
228
+ style="height: 100%"
229
+ >
230
+ <WebcamPermissions on:click={async () => access_webcam()} />
231
+ </div>
232
+ {:else}
233
+ <div class="button-wrap">
234
+ <button
235
+ on:click={record_video_or_photo}
236
+ aria-label={mode === "image" ? "capture photo" : "start recording"}
237
+ >
238
+ {#if mode === "video" || streaming}
239
+ {#if streaming && stream_state === "waiting"}
240
+ <div class="icon-with-text" style="width:var(--size-24);">
241
+ <div class="icon color-primary" title="spinner">
242
+ <Spinner />
243
+ </div>
244
+ {i18n("audio.waiting")}
245
+ </div>
246
+ {:else if (streaming && stream_state === "open") || (!streaming && recording)}
247
+ <div class="icon-with-text">
248
+ <div class="icon color-primary" title="stop recording">
249
+ <Square />
250
+ </div>
251
+ {i18n("audio.stop")}
252
+ </div>
253
+ {:else}
254
+ <div class="icon-with-text">
255
+ <div class="icon color-primary" title="start recording">
256
+ <Circle />
257
+ </div>
258
+ {i18n("audio.record")}
259
+ </div>
260
+ {/if}
261
+ {:else}
262
+ <div class="icon" title="capture photo">
263
+ <Camera />
264
+ </div>
265
+ {/if}
266
+ </button>
267
+ {#if !recording}
268
+ <button
269
+ class="icon"
270
+ on:click={() => (options_open = true)}
271
+ aria-label="select input source"
272
+ >
273
+ <DropdownArrow />
274
+ </button>
275
+ {/if}
276
+ </div>
277
+ {#if options_open && selected_device}
278
+ <select
279
+ class="select-wrap"
280
+ aria-label="select source"
281
+ use:click_outside={handle_click_outside}
282
+ on:change={handle_device_change}
283
+ >
284
+ <button
285
+ class="inset-icon"
286
+ on:click|stopPropagation={() => (options_open = false)}
287
+ >
288
+ <DropdownArrow />
289
+ </button>
290
+ {#if available_video_devices.length === 0}
291
+ <option value="">{i18n("common.no_devices")}</option>
292
+ {:else}
293
+ {#each available_video_devices as device}
294
+ <option
295
+ value={device.deviceId}
296
+ selected={selected_device.deviceId === device.deviceId}
297
+ >
298
+ {device.label}
299
+ </option>
300
+ {/each}
301
+ {/if}
302
+ </select>
303
+ {/if}
304
+ {/if}
305
+ </div>
306
+
307
+ <style>
308
+ .wrap {
309
+ position: relative;
310
+ width: var(--size-full);
311
+ height: var(--size-full);
312
+ }
313
+
314
+ .hide {
315
+ display: none;
316
+ }
317
+
318
+ video {
319
+ width: var(--size-full);
320
+ height: var(--size-full);
321
+ object-fit: cover;
322
+ }
323
+
324
+ .button-wrap {
325
+ position: absolute;
326
+ background-color: var(--block-background-fill);
327
+ border: 1px solid var(--border-color-primary);
328
+ border-radius: var(--radius-xl);
329
+ padding: var(--size-1-5);
330
+ display: flex;
331
+ bottom: var(--size-2);
332
+ left: 50%;
333
+ transform: translate(-50%, 0);
334
+ box-shadow: var(--shadow-drop-lg);
335
+ border-radius: var(--radius-xl);
336
+ line-height: var(--size-3);
337
+ color: var(--button-secondary-text-color);
338
+ }
339
+
340
+ .icon-with-text {
341
+ width: var(--size-20);
342
+ align-items: center;
343
+ margin: 0 var(--spacing-xl);
344
+ display: flex;
345
+ justify-content: space-evenly;
346
+ }
347
+
348
+ @media (--screen-md) {
349
+ button {
350
+ bottom: var(--size-4);
351
+ }
352
+ }
353
+
354
+ @media (--screen-xl) {
355
+ button {
356
+ bottom: var(--size-8);
357
+ }
358
+ }
359
+
360
+ .icon {
361
+ width: 18px;
362
+ height: 18px;
363
+ display: flex;
364
+ justify-content: space-between;
365
+ align-items: center;
366
+ }
367
+
368
+ .color-primary {
369
+ fill: var(--primary-600);
370
+ stroke: var(--primary-600);
371
+ }
372
+
373
+ .flip {
374
+ transform: scaleX(-1);
375
+ }
376
+
377
+ .select-wrap {
378
+ -webkit-appearance: none;
379
+ -moz-appearance: none;
380
+ appearance: none;
381
+ color: var(--button-secondary-text-color);
382
+ background-color: transparent;
383
+ width: 95%;
384
+ font-size: var(--text-md);
385
+ position: absolute;
386
+ bottom: var(--size-2);
387
+ background-color: var(--block-background-fill);
388
+ box-shadow: var(--shadow-drop-lg);
389
+ border-radius: var(--radius-xl);
390
+ z-index: var(--layer-top);
391
+ border: 1px solid var(--border-color-primary);
392
+ text-align: left;
393
+ line-height: var(--size-4);
394
+ white-space: nowrap;
395
+ text-overflow: ellipsis;
396
+ left: 50%;
397
+ transform: translate(-50%, 0);
398
+ max-width: var(--size-52);
399
+ }
400
+
401
+ .select-wrap > option {
402
+ padding: 0.25rem 0.5rem;
403
+ border-bottom: 1px solid var(--border-color-accent);
404
+ padding-right: var(--size-8);
405
+ text-overflow: ellipsis;
406
+ overflow: hidden;
407
+ }
408
+
409
+ .select-wrap > option:hover {
410
+ background-color: var(--color-accent);
411
+ }
412
+
413
+ .select-wrap > option:last-child {
414
+ border: none;
415
+ }
416
+
417
+ .inset-icon {
418
+ position: absolute;
419
+ top: 5px;
420
+ right: -6.5px;
421
+ width: var(--size-10);
422
+ height: var(--size-5);
423
+ opacity: 0.8;
424
+ }
425
+
426
+ @media (--screen-md) {
427
+ .wrap {
428
+ font-size: var(--text-lg);
429
+ }
430
+ }
431
+ </style>