@gradio/image 0.15.0 → 0.15.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,371 @@
1
+ <script>import { createEventDispatcher, onMount } from "svelte";
2
+ import { Camera, Circle, Square, DropdownArrow } from "@gradio/icons";
3
+ import { prepare_files } from "@gradio/client";
4
+ import WebcamPermissions from "./WebcamPermissions.svelte";
5
+ import { fade } from "svelte/transition";
6
+ import {
7
+ get_devices,
8
+ get_video_stream,
9
+ set_available_devices
10
+ } from "./stream_utils";
11
+ let video_source;
12
+ let available_video_devices = [];
13
+ let selected_device = null;
14
+ let canvas;
15
+ export let streaming = false;
16
+ export let pending = false;
17
+ export let root = "";
18
+ export let mode = "image";
19
+ export let mirror_webcam;
20
+ export let include_audio;
21
+ export let i18n;
22
+ export let upload;
23
+ const dispatch = createEventDispatcher();
24
+ onMount(() => canvas = document.createElement("canvas"));
25
+ const handle_device_change = async (event) => {
26
+ const target = event.target;
27
+ const device_id = target.value;
28
+ await get_video_stream(include_audio, video_source, device_id).then(
29
+ async (local_stream) => {
30
+ stream = local_stream;
31
+ selected_device = available_video_devices.find(
32
+ (device) => device.deviceId === device_id
33
+ ) || null;
34
+ options_open = false;
35
+ }
36
+ );
37
+ };
38
+ async function access_webcam() {
39
+ try {
40
+ get_video_stream(include_audio, video_source).then(async (local_stream) => {
41
+ webcam_accessed = true;
42
+ available_video_devices = await get_devices();
43
+ stream = local_stream;
44
+ }).then(() => set_available_devices(available_video_devices)).then((devices) => {
45
+ available_video_devices = devices;
46
+ const used_devices = stream.getTracks().map((track) => track.getSettings()?.deviceId)[0];
47
+ selected_device = used_devices ? devices.find((device) => device.deviceId === used_devices) || available_video_devices[0] : available_video_devices[0];
48
+ });
49
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
50
+ dispatch("error", i18n("image.no_webcam_support"));
51
+ }
52
+ } catch (err) {
53
+ if (err instanceof DOMException && err.name == "NotAllowedError") {
54
+ dispatch("error", i18n("image.allow_webcam_access"));
55
+ } else {
56
+ throw err;
57
+ }
58
+ }
59
+ }
60
+ function take_picture() {
61
+ var context = canvas.getContext("2d");
62
+ if ((!streaming || streaming && recording) && video_source.videoWidth && video_source.videoHeight) {
63
+ canvas.width = video_source.videoWidth;
64
+ canvas.height = video_source.videoHeight;
65
+ context.drawImage(
66
+ video_source,
67
+ 0,
68
+ 0,
69
+ video_source.videoWidth,
70
+ video_source.videoHeight
71
+ );
72
+ if (mirror_webcam) {
73
+ context.scale(-1, 1);
74
+ context.drawImage(video_source, -video_source.videoWidth, 0);
75
+ }
76
+ canvas.toBlob(
77
+ (blob) => {
78
+ dispatch(streaming ? "stream" : "capture", blob);
79
+ },
80
+ "image/png",
81
+ 0.8
82
+ );
83
+ }
84
+ }
85
+ let recording = false;
86
+ let recorded_blobs = [];
87
+ let stream;
88
+ let mimeType;
89
+ let media_recorder;
90
+ function take_recording() {
91
+ if (recording) {
92
+ media_recorder.stop();
93
+ let video_blob = new Blob(recorded_blobs, { type: mimeType });
94
+ let ReaderObj = new FileReader();
95
+ ReaderObj.onload = async function(e) {
96
+ if (e.target) {
97
+ let _video_blob = new File(
98
+ [video_blob],
99
+ "sample." + mimeType.substring(6)
100
+ );
101
+ const val = await prepare_files([_video_blob]);
102
+ let value = ((await upload(val, root))?.filter(Boolean))[0];
103
+ dispatch("capture", value);
104
+ dispatch("stop_recording");
105
+ }
106
+ };
107
+ ReaderObj.readAsDataURL(video_blob);
108
+ } else {
109
+ dispatch("start_recording");
110
+ recorded_blobs = [];
111
+ let validMimeTypes = ["video/webm", "video/mp4"];
112
+ for (let validMimeType of validMimeTypes) {
113
+ if (MediaRecorder.isTypeSupported(validMimeType)) {
114
+ mimeType = validMimeType;
115
+ break;
116
+ }
117
+ }
118
+ if (mimeType === null) {
119
+ console.error("No supported MediaRecorder mimeType");
120
+ return;
121
+ }
122
+ media_recorder = new MediaRecorder(stream, {
123
+ mimeType
124
+ });
125
+ media_recorder.addEventListener("dataavailable", function(e) {
126
+ recorded_blobs.push(e.data);
127
+ });
128
+ media_recorder.start(200);
129
+ }
130
+ recording = !recording;
131
+ }
132
+ let webcam_accessed = false;
133
+ function record_video_or_photo() {
134
+ if (mode === "image" && streaming) {
135
+ recording = !recording;
136
+ }
137
+ if (mode === "image") {
138
+ take_picture();
139
+ } else {
140
+ take_recording();
141
+ }
142
+ if (!recording && stream) {
143
+ stream.getTracks().forEach((track) => track.stop());
144
+ video_source.srcObject = null;
145
+ webcam_accessed = false;
146
+ }
147
+ }
148
+ if (streaming && mode === "image") {
149
+ window.setInterval(() => {
150
+ if (video_source && !pending) {
151
+ take_picture();
152
+ }
153
+ }, 500);
154
+ }
155
+ let options_open = false;
156
+ export function click_outside(node, cb) {
157
+ const handle_click = (event) => {
158
+ if (node && !node.contains(event.target) && !event.defaultPrevented) {
159
+ cb(event);
160
+ }
161
+ };
162
+ document.addEventListener("click", handle_click, true);
163
+ return {
164
+ destroy() {
165
+ document.removeEventListener("click", handle_click, true);
166
+ }
167
+ };
168
+ }
169
+ function handle_click_outside(event) {
170
+ event.preventDefault();
171
+ event.stopPropagation();
172
+ options_open = false;
173
+ }
174
+ </script>
175
+
176
+ <div class="wrap">
177
+ <!-- svelte-ignore a11y-media-has-caption -->
178
+ <!-- need to suppress for video streaming https://github.com/sveltejs/svelte/issues/5967 -->
179
+ <video
180
+ bind:this={video_source}
181
+ class:flip={mirror_webcam}
182
+ class:hide={!webcam_accessed}
183
+ />
184
+ {#if !webcam_accessed}
185
+ <div
186
+ in:fade={{ delay: 100, duration: 200 }}
187
+ title="grant webcam access"
188
+ style="height: 100%"
189
+ >
190
+ <WebcamPermissions on:click={async () => access_webcam()} />
191
+ </div>
192
+ {:else}
193
+ <div class="button-wrap">
194
+ <button
195
+ on:click={record_video_or_photo}
196
+ aria-label={mode === "image" ? "capture photo" : "start recording"}
197
+ >
198
+ {#if mode === "video" || streaming}
199
+ {#if recording}
200
+ <div class="icon red" title="stop recording">
201
+ <Square />
202
+ </div>
203
+ {:else}
204
+ <div class="icon red" title="start recording">
205
+ <Circle />
206
+ </div>
207
+ {/if}
208
+ {:else}
209
+ <div class="icon" title="capture photo">
210
+ <Camera />
211
+ </div>
212
+ {/if}
213
+ </button>
214
+ {#if !recording}
215
+ <button
216
+ class="icon"
217
+ on:click={() => (options_open = true)}
218
+ aria-label="select input source"
219
+ >
220
+ <DropdownArrow />
221
+ </button>
222
+ {/if}
223
+ </div>
224
+ {#if options_open && selected_device}
225
+ <select
226
+ class="select-wrap"
227
+ aria-label="select source"
228
+ use:click_outside={handle_click_outside}
229
+ on:change={handle_device_change}
230
+ >
231
+ <button
232
+ class="inset-icon"
233
+ on:click|stopPropagation={() => (options_open = false)}
234
+ >
235
+ <DropdownArrow />
236
+ </button>
237
+ {#if available_video_devices.length === 0}
238
+ <option value="">{i18n("common.no_devices")}</option>
239
+ {:else}
240
+ {#each available_video_devices as device}
241
+ <option
242
+ value={device.deviceId}
243
+ selected={selected_device.deviceId === device.deviceId}
244
+ >
245
+ {device.label}
246
+ </option>
247
+ {/each}
248
+ {/if}
249
+ </select>
250
+ {/if}
251
+ {/if}
252
+ </div>
253
+
254
+ <style>
255
+ .wrap {
256
+ position: relative;
257
+ width: var(--size-full);
258
+ height: var(--size-full);
259
+ }
260
+
261
+ .hide {
262
+ display: none;
263
+ }
264
+
265
+ video {
266
+ width: var(--size-full);
267
+ height: var(--size-full);
268
+ object-fit: cover;
269
+ }
270
+
271
+ .button-wrap {
272
+ position: absolute;
273
+ background-color: var(--block-background-fill);
274
+ border: 1px solid var(--border-color-primary);
275
+ border-radius: var(--radius-xl);
276
+ padding: var(--size-1-5);
277
+ display: flex;
278
+ bottom: var(--size-2);
279
+ left: 50%;
280
+ transform: translate(-50%, 0);
281
+ box-shadow: var(--shadow-drop-lg);
282
+ border-radius: var(--radius-xl);
283
+ line-height: var(--size-3);
284
+ color: var(--button-secondary-text-color);
285
+ }
286
+
287
+ @media (--screen-md) {
288
+ button {
289
+ bottom: var(--size-4);
290
+ }
291
+ }
292
+
293
+ @media (--screen-xl) {
294
+ button {
295
+ bottom: var(--size-8);
296
+ }
297
+ }
298
+
299
+ .icon {
300
+ opacity: 0.8;
301
+ width: 18px;
302
+ height: 18px;
303
+ display: flex;
304
+ justify-content: space-between;
305
+ align-items: center;
306
+ }
307
+
308
+ .red {
309
+ fill: red;
310
+ stroke: red;
311
+ }
312
+
313
+ .flip {
314
+ transform: scaleX(-1);
315
+ }
316
+
317
+ .select-wrap {
318
+ -webkit-appearance: none;
319
+ -moz-appearance: none;
320
+ appearance: none;
321
+ color: var(--button-secondary-text-color);
322
+ background-color: transparent;
323
+ width: 95%;
324
+ font-size: var(--text-md);
325
+ position: absolute;
326
+ bottom: var(--size-2);
327
+ background-color: var(--block-background-fill);
328
+ box-shadow: var(--shadow-drop-lg);
329
+ border-radius: var(--radius-xl);
330
+ z-index: var(--layer-top);
331
+ border: 1px solid var(--border-color-primary);
332
+ text-align: left;
333
+ line-height: var(--size-4);
334
+ white-space: nowrap;
335
+ text-overflow: ellipsis;
336
+ left: 50%;
337
+ transform: translate(-50%, 0);
338
+ max-width: var(--size-52);
339
+ }
340
+
341
+ .select-wrap > option {
342
+ padding: 0.25rem 0.5rem;
343
+ border-bottom: 1px solid var(--border-color-accent);
344
+ padding-right: var(--size-8);
345
+ text-overflow: ellipsis;
346
+ overflow: hidden;
347
+ }
348
+
349
+ .select-wrap > option:hover {
350
+ background-color: var(--color-accent);
351
+ }
352
+
353
+ .select-wrap > option:last-child {
354
+ border: none;
355
+ }
356
+
357
+ .inset-icon {
358
+ position: absolute;
359
+ top: 5px;
360
+ right: -6.5px;
361
+ width: var(--size-10);
362
+ height: var(--size-5);
363
+ opacity: 0.8;
364
+ }
365
+
366
+ @media (--screen-md) {
367
+ .wrap {
368
+ font-size: var(--text-lg);
369
+ }
370
+ }
371
+ </style>
@@ -0,0 +1,33 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { I18nFormatter } from "@gradio/utils";
3
+ import { type FileData, type Client } from "@gradio/client";
4
+ declare const __propDef: {
5
+ props: {
6
+ streaming?: boolean | undefined;
7
+ pending?: boolean | undefined;
8
+ root?: string | undefined;
9
+ mode?: ("image" | "video") | undefined;
10
+ mirror_webcam: boolean;
11
+ include_audio: boolean;
12
+ i18n: I18nFormatter;
13
+ upload: Client["upload"];
14
+ click_outside?: ((node: Node, cb: any) => any) | undefined;
15
+ };
16
+ events: {
17
+ stream: CustomEvent<undefined>;
18
+ capture: CustomEvent<Blob | FileData | null>;
19
+ error: CustomEvent<string>;
20
+ start_recording: CustomEvent<undefined>;
21
+ stop_recording: CustomEvent<undefined>;
22
+ } & {
23
+ [evt: string]: CustomEvent<any>;
24
+ };
25
+ slots: {};
26
+ };
27
+ export type WebcamProps = typeof __propDef.props;
28
+ export type WebcamEvents = typeof __propDef.events;
29
+ export type WebcamSlots = typeof __propDef.slots;
30
+ export default class Webcam extends SvelteComponent<WebcamProps, WebcamEvents, WebcamSlots> {
31
+ get click_outside(): (node: Node, cb: any) => any;
32
+ }
33
+ export {};
@@ -0,0 +1,42 @@
1
+ <script>import { Webcam } from "@gradio/icons";
2
+ import { createEventDispatcher } from "svelte";
3
+ const dispatch = createEventDispatcher();
4
+ </script>
5
+
6
+ <button style:height="100%" on:click={() => dispatch("click")}>
7
+ <div class="wrap">
8
+ <span class="icon-wrap">
9
+ <Webcam />
10
+ </span>
11
+ {"Click to Access Webcam"}
12
+ </div>
13
+ </button>
14
+
15
+ <style>
16
+ button {
17
+ cursor: pointer;
18
+ width: var(--size-full);
19
+ }
20
+
21
+ .wrap {
22
+ display: flex;
23
+ flex-direction: column;
24
+ justify-content: center;
25
+ align-items: center;
26
+ min-height: var(--size-60);
27
+ color: var(--block-label-text-color);
28
+ height: 100%;
29
+ padding-top: var(--size-3);
30
+ }
31
+
32
+ .icon-wrap {
33
+ width: 30px;
34
+ margin-bottom: var(--spacing-lg);
35
+ }
36
+
37
+ @media (--screen-md) {
38
+ .wrap {
39
+ font-size: var(--text-lg);
40
+ }
41
+ }
42
+ </style>
@@ -0,0 +1,18 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ [x: string]: never;
5
+ };
6
+ events: {
7
+ click: CustomEvent<undefined>;
8
+ } & {
9
+ [evt: string]: CustomEvent<any>;
10
+ };
11
+ slots: {};
12
+ };
13
+ export type WebcamPermissionsProps = typeof __propDef.props;
14
+ export type WebcamPermissionsEvents = typeof __propDef.events;
15
+ export type WebcamPermissionsSlots = typeof __propDef.slots;
16
+ export default class WebcamPermissions extends SvelteComponent<WebcamPermissionsProps, WebcamPermissionsEvents, WebcamPermissionsSlots> {
17
+ }
18
+ export {};
@@ -0,0 +1,2 @@
1
+ export { default as Image } from "./Image.svelte";
2
+ export { default as StaticImage } from "./ImagePreview.svelte";
@@ -0,0 +1,2 @@
1
+ export { default as Image } from "./Image.svelte";
2
+ export { default as StaticImage } from "./ImagePreview.svelte";
@@ -0,0 +1,5 @@
1
+ export declare function get_devices(): Promise<MediaDeviceInfo[]>;
2
+ export declare function handle_error(error: string): void;
3
+ export declare function set_local_stream(local_stream: MediaStream | null, video_source: HTMLVideoElement): void;
4
+ export declare function get_video_stream(include_audio: boolean, video_source: HTMLVideoElement, device_id?: string): Promise<MediaStream>;
5
+ export declare function set_available_devices(devices: MediaDeviceInfo[]): MediaDeviceInfo[];
@@ -0,0 +1,31 @@
1
+ export function get_devices() {
2
+ return navigator.mediaDevices.enumerateDevices();
3
+ }
4
+ export function handle_error(error) {
5
+ throw new Error(error);
6
+ }
7
+ export function set_local_stream(local_stream, video_source) {
8
+ video_source.srcObject = local_stream;
9
+ video_source.muted = true;
10
+ video_source.play();
11
+ }
12
+ export async function get_video_stream(include_audio, video_source, device_id) {
13
+ const size = {
14
+ width: { ideal: 1920 },
15
+ height: { ideal: 1440 }
16
+ };
17
+ const constraints = {
18
+ video: device_id ? { deviceId: { exact: device_id }, ...size } : size,
19
+ audio: include_audio
20
+ };
21
+ return navigator.mediaDevices
22
+ .getUserMedia(constraints)
23
+ .then((local_stream) => {
24
+ set_local_stream(local_stream, video_source);
25
+ return local_stream;
26
+ });
27
+ }
28
+ export function set_available_devices(devices) {
29
+ const cameras = devices.filter((device) => device.kind === "videoinput");
30
+ return cameras;
31
+ }
@@ -0,0 +1 @@
1
+ export declare const get_coordinates_of_clicked_image: (evt: MouseEvent) => [number, number] | null;
@@ -0,0 +1,28 @@
1
+ export const get_coordinates_of_clicked_image = (evt) => {
2
+ let image;
3
+ if (evt.currentTarget instanceof Element) {
4
+ image = evt.currentTarget.querySelector("img");
5
+ }
6
+ else {
7
+ return [NaN, NaN];
8
+ }
9
+ const imageRect = image.getBoundingClientRect();
10
+ const xScale = image.naturalWidth / imageRect.width;
11
+ const yScale = image.naturalHeight / imageRect.height;
12
+ if (xScale > yScale) {
13
+ const displayed_height = image.naturalHeight / xScale;
14
+ const y_offset = (imageRect.height - displayed_height) / 2;
15
+ var x = Math.round((evt.clientX - imageRect.left) * xScale);
16
+ var y = Math.round((evt.clientY - imageRect.top - y_offset) * xScale);
17
+ }
18
+ else {
19
+ const displayed_width = image.naturalWidth / yScale;
20
+ const x_offset = (imageRect.width - displayed_width) / 2;
21
+ var x = Math.round((evt.clientX - imageRect.left - x_offset) * yScale);
22
+ var y = Math.round((evt.clientY - imageRect.top) * yScale);
23
+ }
24
+ if (x < 0 || x >= image.naturalWidth || y < 0 || y >= image.naturalHeight) {
25
+ return null;
26
+ }
27
+ return [x, y];
28
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/image",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,25 +10,44 @@
10
10
  "cropperjs": "^1.5.12",
11
11
  "lazy-brush": "^1.0.1",
12
12
  "resize-observer-polyfill": "^1.5.1",
13
- "@gradio/atoms": "^0.8.0",
14
- "@gradio/icons": "^0.7.1",
15
- "@gradio/statustracker": "^0.7.5",
16
- "@gradio/client": "^1.5.1",
17
- "@gradio/utils": "^0.6.0",
18
- "@gradio/wasm": "^0.13.0",
19
- "@gradio/upload": "^0.12.3"
13
+ "@gradio/atoms": "^0.8.1",
14
+ "@gradio/client": "^1.5.2",
15
+ "@gradio/icons": "^0.7.2",
16
+ "@gradio/statustracker": "^0.7.6",
17
+ "@gradio/utils": "^0.6.1",
18
+ "@gradio/upload": "^0.12.4",
19
+ "@gradio/wasm": "^0.13.1"
20
20
  },
21
21
  "devDependencies": {
22
- "@gradio/preview": "^0.11.0"
22
+ "@gradio/preview": "^0.11.1"
23
23
  },
24
24
  "main_changeset": true,
25
25
  "main": "./Index.svelte",
26
26
  "exports": {
27
- ".": "./Index.svelte",
28
- "./shared": "./shared/index.ts",
29
- "./example": "./Example.svelte",
30
- "./base": "./shared/ImagePreview.svelte",
31
- "./package.json": "./package.json"
27
+ "./package.json": "./package.json",
28
+ ".": {
29
+ "gradio": "./Index.svelte",
30
+ "svelte": "./dist/Index.svelte",
31
+ "types": "./dist/Index.svelte.d.ts"
32
+ },
33
+ "./example": {
34
+ "gradio": "./Example.svelte",
35
+ "svelte": "./dist/Example.svelte",
36
+ "types": "./dist/Example.svelte.d.ts"
37
+ },
38
+ "./base": {
39
+ "gradio": "./shared/ImagePreview.svelte",
40
+ "svelte": "./dist/shared/ImagePreview.svelte",
41
+ "types": "./dist/shared/ImagePreview.svelte.d.ts"
42
+ },
43
+ "./shared": {
44
+ "gradio": "./shared/index.ts",
45
+ "svelte": "./dist/shared/index.js",
46
+ "types": "./dist/shared/index.d.ts"
47
+ }
48
+ },
49
+ "peerDependencies": {
50
+ "svelte": "^4.0.0"
32
51
  },
33
52
  "repository": {
34
53
  "type": "git",