@gradio/image 0.24.0 → 0.25.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.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,52 @@
1
1
  # @gradio/image
2
+
3
+ ## 0.25.1
4
+
5
+ ### Fixes
6
+
7
+ - [#12800](https://github.com/gradio-app/gradio/pull/12800) [`7a1c321`](https://github.com/gradio-app/gradio/commit/7a1c321b6546ba05a353488f5133e8262c4a8a39) - Bump svelte/kit for security reasons. Thanks @freddyaboulton!
8
+ - [#12779](https://github.com/gradio-app/gradio/pull/12779) [`ea2d3e9`](https://github.com/gradio-app/gradio/commit/ea2d3e985a8b42d188e551f517c5825c00790628) - Migrate Audio + Upload + Atoms to Svelte 5. Thanks @dawoodkhan82!
9
+
10
+ ### Dependency updates
11
+
12
+ - @gradio/statustracker@0.12.2
13
+ - @gradio/atoms@0.20.1
14
+ - @gradio/utils@0.11.2
15
+ - @gradio/icons@0.15.1
16
+ - @gradio/upload@0.17.4
17
+ - @gradio/client@2.0.3
18
+
19
+ ## 0.25.0
20
+
21
+ ### Dependency updates
22
+
23
+ - @gradio/utils@0.11.1
24
+ - @gradio/client@2.0.2
25
+
26
+ ## 0.25.0
27
+
28
+ ### Features
29
+
30
+ - [#12539](https://github.com/gradio-app/gradio/pull/12539) [`f1d83fa`](https://github.com/gradio-app/gradio/commit/f1d83fac3d6e4bad60cf896a026fa2d572f26073) - Add ability to add custom buttons to components. Thanks @abidlabs!
31
+
32
+ ### Fixes
33
+
34
+ - [#12575](https://github.com/gradio-app/gradio/pull/12575) [`7498fac`](https://github.com/gradio-app/gradio/commit/7498fac6a1c575ff04920ad5178853843a3b270e) - Fix image buttons default value. Thanks @freddyaboulton!
35
+
36
+ ### Dependency updates
37
+
38
+ - @gradio/atoms@0.20.0
39
+ - @gradio/utils@0.11.0
40
+ - @gradio/client@2.0.1
41
+ - @gradio/statustracker@0.12.1
42
+ - @gradio/upload@0.17.3
43
+
44
+ ## 0.24.0
45
+
46
+ ### Dependency updates
47
+
48
+ - @gradio/utils@0.10.4
49
+
2
50
  ## 0.24.0
3
51
 
4
52
  ### Features
@@ -1,12 +1,14 @@
1
- <script context="module">
2
- import { Template, Story } from "@storybook/addon-svelte-csf";
1
+ <script module>
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import StaticImage from "./Index.svelte";
4
- import { userEvent, within } from "@storybook/test";
4
+ import { userEvent, within } from "storybook/test";
5
5
  import { allModes } from "../storybook/modes";
6
- import image_file_100x100 from "../storybook/test_files/image_100x100.webp";
7
- import image_file_100x1000 from "../storybook/test_files/image_100x100.webp";
6
+ import { wrapProps } from "../storybook/wrapProps";
8
7
 
9
- export const meta = {
8
+ const cheetah = "/cheetah.jpg";
9
+ const lion = "/lion.jpg";
10
+
11
+ const { Story } = defineMeta({
10
12
  title: "Components/Image",
11
13
  component: StaticImage,
12
14
  parameters: {
@@ -17,177 +19,224 @@
17
19
  }
18
20
  }
19
21
  }
20
- };
22
+ });
21
23
 
22
24
  let md = `# a heading! /n a new line! `;
23
25
  </script>
24
26
 
25
- <Template let:args>
26
- <div
27
- class="image-container"
28
- style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
29
- >
30
- <StaticImage {...args} />
31
- </div>
32
- </Template>
33
-
34
27
  <Story
35
28
  name="static with label, info and download button"
36
29
  args={{
37
30
  value: {
38
- path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
39
- url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
31
+ path: cheetah,
32
+ url: cheetah,
40
33
  orig_name: "cheetah.jpg"
41
34
  },
42
35
  show_label: true,
43
36
  placeholder: "This is a cheetah",
44
- show_download_button: true,
45
- webcam_options: {
46
- mirror: true,
47
- constraints: null
48
- }
37
+ buttons: ["fullscreen", "download"],
38
+ webcam_options: { mirror: true, constraints: null }
49
39
  }}
50
40
  play={async ({ canvasElement }) => {
51
41
  const canvas = within(canvasElement);
52
-
53
- const expand_btn = canvas.getByRole("button", {
54
- name: "Fullscreen"
55
- });
42
+ const expand_btn = canvas.getByRole("button", { name: "Fullscreen" });
56
43
  await userEvent.click(expand_btn);
57
44
  }}
58
- />
45
+ >
46
+ {#snippet template(args)}
47
+ <div
48
+ class="image-container"
49
+ style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
50
+ >
51
+ <StaticImage {...wrapProps(args)} />
52
+ </div>
53
+ {/snippet}
54
+ </Story>
59
55
 
60
56
  <Story
61
57
  name="static with no label or download button"
62
58
  args={{
63
59
  value: {
64
- path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
65
- url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
60
+ path: cheetah,
61
+ url: cheetah,
66
62
  orig_name: "cheetah.jpg"
67
63
  },
68
64
  show_label: false,
69
- show_download_button: false,
70
- webcam_options: {
71
- mirror: true,
72
- constraints: null
73
- }
65
+ buttons: [],
66
+ webcam_options: { mirror: true, constraints: null }
74
67
  }}
75
- />
68
+ >
69
+ {#snippet template(args)}
70
+ <div
71
+ class="image-container"
72
+ style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
73
+ >
74
+ <StaticImage {...wrapProps(args)} />
75
+ </div>
76
+ {/snippet}
77
+ </Story>
76
78
 
77
79
  <Story
78
80
  name="static with a vertically long image"
79
81
  args={{
80
82
  value: {
81
- path: image_file_100x1000,
82
- url: image_file_100x1000,
83
- orig_name: "image.webp"
83
+ path: lion,
84
+ url: lion,
85
+ orig_name: "lion.jpg"
84
86
  },
85
- webcam_options: {
86
- mirror: true,
87
- constraints: null
88
- }
87
+ buttons: [],
88
+ webcam_options: { mirror: true, constraints: null }
89
89
  }}
90
- />
90
+ >
91
+ {#snippet template(args)}
92
+ <div
93
+ class="image-container"
94
+ style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
95
+ >
96
+ <StaticImage {...wrapProps(args)} />
97
+ </div>
98
+ {/snippet}
99
+ </Story>
91
100
 
92
101
  <Story
93
102
  name="static with a vertically long image and a fixed height"
94
103
  args={{
95
104
  value: {
96
- path: image_file_100x1000,
97
- url: image_file_100x1000,
98
- orig_name: "image.webp"
105
+ path: lion,
106
+ url: lion,
107
+ orig_name: "lion.jpg"
99
108
  },
100
109
  height: "500px",
101
- webcam_options: {
102
- mirror: true,
103
- constraints: null
104
- }
110
+ buttons: [],
111
+ webcam_options: { mirror: true, constraints: null }
105
112
  }}
106
- />
113
+ >
114
+ {#snippet template(args)}
115
+ <div
116
+ class="image-container"
117
+ style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
118
+ >
119
+ <StaticImage {...wrapProps(args)} />
120
+ </div>
121
+ {/snippet}
122
+ </Story>
107
123
 
108
124
  <Story
109
125
  name="static with a small image and a fixed height"
110
126
  args={{
111
127
  value: {
112
- path: image_file_100x100,
113
- url: image_file_100x100,
114
- orig_name: "image.webp"
128
+ path: cheetah,
129
+ url: cheetah,
130
+ orig_name: "cheetah.jpg"
115
131
  },
116
132
  height: "500px",
117
- webcam_options: {
118
- mirror: true,
119
- constraints: null
120
- }
133
+ buttons: [],
134
+ webcam_options: { mirror: true, constraints: null }
121
135
  }}
122
- />
136
+ >
137
+ {#snippet template(args)}
138
+ <div
139
+ class="image-container"
140
+ style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
141
+ >
142
+ <StaticImage {...wrapProps(args)} />
143
+ </div>
144
+ {/snippet}
145
+ </Story>
123
146
 
124
147
  <Story
125
148
  name="interactive with upload, clipboard, and webcam"
126
149
  args={{
127
150
  sources: ["upload", "clipboard", "webcam"],
128
151
  value: {
129
- path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
130
- url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
152
+ path: cheetah,
153
+ url: cheetah,
131
154
  orig_name: "cheetah.jpg"
132
155
  },
133
156
  show_label: false,
134
- show_download_button: false,
135
157
  interactive: true,
136
158
  placeholder: md,
137
- webcam_options: {
138
- mirror: true,
139
- constraints: null
140
- }
159
+ buttons: [],
160
+ webcam_options: { mirror: true, constraints: null }
141
161
  }}
162
+ parameters={{ chromatic: { disableSnapshot: true } }}
142
163
  play={async ({ canvasElement }) => {
143
164
  const canvas = within(canvasElement);
144
-
145
165
  const webcamButton = await canvas.findByLabelText("Capture from camera");
146
166
  userEvent.click(webcamButton);
147
-
148
167
  userEvent.click(await canvas.findByTitle("grant webcam access"));
149
168
  userEvent.click(await canvas.findByLabelText("Upload file"));
150
169
  userEvent.click(await canvas.findByLabelText("Paste from clipboard"));
151
170
  }}
152
- />
171
+ >
172
+ {#snippet template(args)}
173
+ <div
174
+ class="image-container"
175
+ style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
176
+ >
177
+ <StaticImage {...wrapProps(args)} />
178
+ </div>
179
+ {/snippet}
180
+ </Story>
153
181
 
154
182
  <Story
155
183
  name="interactive with webcam"
156
184
  args={{
157
185
  sources: ["webcam"],
158
- show_download_button: true,
159
186
  interactive: true,
160
- webcam_options: {
161
- mirror: true,
162
- constraints: null
163
- }
187
+ buttons: ["download"],
188
+ webcam_options: { mirror: true, constraints: null }
164
189
  }}
165
- />
190
+ >
191
+ {#snippet template(args)}
192
+ <div
193
+ class="image-container"
194
+ style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
195
+ >
196
+ <StaticImage {...wrapProps(args)} />
197
+ </div>
198
+ {/snippet}
199
+ </Story>
166
200
 
167
201
  <Story
168
202
  name="interactive with clipboard"
169
203
  args={{
170
204
  sources: ["clipboard"],
171
- show_download_button: true,
172
- interactive: true
205
+ interactive: true,
206
+ buttons: ["download"]
173
207
  }}
174
- />
208
+ >
209
+ {#snippet template(args)}
210
+ <div
211
+ class="image-container"
212
+ style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
213
+ >
214
+ <StaticImage {...wrapProps(args)} />
215
+ </div>
216
+ {/snippet}
217
+ </Story>
175
218
 
176
219
  <Story
177
220
  name="interactive webcam with streaming"
178
221
  args={{
179
222
  sources: ["webcam"],
180
- show_download_button: true,
181
223
  interactive: true,
182
224
  value: {
183
- path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
184
- url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
225
+ path: cheetah,
226
+ url: cheetah,
185
227
  orig_name: "cheetah.jpg"
186
228
  },
187
229
  streaming: true,
188
- webcam_options: {
189
- mirror: true,
190
- constraints: null
191
- }
230
+ buttons: ["download"],
231
+ webcam_options: { mirror: true, constraints: null }
192
232
  }}
193
- />
233
+ >
234
+ {#snippet template(args)}
235
+ <div
236
+ class="image-container"
237
+ style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
238
+ >
239
+ <StaticImage {...wrapProps(args)} />
240
+ </div>
241
+ {/snippet}
242
+ </Story>
package/Image.test.ts CHANGED
@@ -29,10 +29,12 @@ describe("Image", () => {
29
29
  window.HTMLMediaElement.prototype.play = vi.fn();
30
30
  window.HTMLMediaElement.prototype.pause = vi.fn();
31
31
  });
32
- beforeEach(setupi18n);
32
+ beforeEach(async () => {
33
+ await setupi18n();
34
+ });
33
35
  afterEach(() => cleanup());
34
36
 
35
- test("image change event trigger fires when value is changed and only fires once", async () => {
37
+ test.skip("image change event trigger fires when value is changed and only fires once", async () => {
36
38
  const { component, listen } = await render(Image, {
37
39
  show_label: true,
38
40
  loading_status,
@@ -50,7 +52,8 @@ describe("Image", () => {
50
52
  // brush_color: "#000000",
51
53
  // brush_radius: 5,
52
54
  // mask_opacity: 0.5,
53
- interactive: true
55
+ interactive: true,
56
+ buttons: ["download", "share", "fullscreen"]
54
57
  });
55
58
 
56
59
  const mock = listen("change");
@@ -1,28 +1,37 @@
1
- <script>
2
- import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
1
+ <script module>
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import Image from "./Example.svelte";
4
- </script>
5
4
 
6
- <Meta title="Components/Image/Example" component={Image} />
5
+ const cheetah = "/cheetah.jpg";
7
6
 
8
- <Template let:args>
9
- <Image {...args} />
10
- </Template>
7
+ const { Story } = defineMeta({
8
+ title: "Components/Image/Example",
9
+ component: Image
10
+ });
11
+ </script>
11
12
 
12
13
  <Story
13
14
  name="Image file"
14
15
  args={{
15
16
  value: {
16
- path: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
17
- url: "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg",
17
+ path: cheetah,
18
+ url: cheetah,
18
19
  orig_name: "cheetah.jpg"
19
20
  }
20
21
  }}
21
- />
22
+ >
23
+ {#snippet template(args)}
24
+ <Image {...args} />
25
+ {/snippet}
26
+ </Story>
22
27
 
23
28
  <Story
24
29
  name="Null"
25
30
  args={{
26
31
  value: null
27
32
  }}
28
- />
33
+ >
34
+ {#snippet template(args)}
35
+ <Image {...args} />
36
+ {/snippet}
37
+ </Story>
package/Index.svelte CHANGED
@@ -119,6 +119,9 @@
119
119
  selectable={gradio.props._selectable}
120
120
  i18n={gradio.i18n}
121
121
  buttons={gradio.props.buttons}
122
+ on_custom_button_click={(id) => {
123
+ gradio.dispatch("custom_button_click", { id });
124
+ }}
122
125
  />
123
126
  </Block>
124
127
  {:else}
@@ -162,7 +165,9 @@
162
165
  {fullscreen}
163
166
  show_fullscreen_button={gradio.props.buttons === null
164
167
  ? true
165
- : gradio.props.buttons.includes("fullscreen")}
168
+ : gradio.props.buttons.some(
169
+ (btn) => typeof btn === "string" && btn === "fullscreen"
170
+ )}
166
171
  on:edit={() => gradio.dispatch("edit")}
167
172
  on:clear={() => {
168
173
  fullscreen = false;
@@ -180,7 +185,7 @@
180
185
  }}
181
186
  on:select={({ detail }) => gradio.dispatch("select", detail)}
182
187
  on:share={({ detail }) => gradio.dispatch("share", detail)}
183
- on:error={({ detail }) => {
188
+ onerror={(detail) => {
184
189
  gradio.shared.loading_status.status = "error";
185
190
  gradio.dispatch("error", detail);
186
191
  }}
package/dist/Index.svelte CHANGED
@@ -119,6 +119,9 @@
119
119
  selectable={gradio.props._selectable}
120
120
  i18n={gradio.i18n}
121
121
  buttons={gradio.props.buttons}
122
+ on_custom_button_click={(id) => {
123
+ gradio.dispatch("custom_button_click", { id });
124
+ }}
122
125
  />
123
126
  </Block>
124
127
  {:else}
@@ -162,7 +165,9 @@
162
165
  {fullscreen}
163
166
  show_fullscreen_button={gradio.props.buttons === null
164
167
  ? true
165
- : gradio.props.buttons.includes("fullscreen")}
168
+ : gradio.props.buttons.some(
169
+ (btn) => typeof btn === "string" && btn === "fullscreen"
170
+ )}
166
171
  on:edit={() => gradio.dispatch("edit")}
167
172
  on:clear={() => {
168
173
  fullscreen = false;
@@ -180,7 +185,7 @@
180
185
  }}
181
186
  on:select={({ detail }) => gradio.dispatch("select", detail)}
182
187
  on:share={({ detail }) => gradio.dispatch("share", detail)}
183
- on:error={({ detail }) => {
188
+ onerror={(detail) => {
184
189
  gradio.shared.loading_status.status = "error";
185
190
  gradio.dispatch("error", detail);
186
191
  }}
@@ -11,6 +11,7 @@
11
11
  FullscreenButton,
12
12
  DownloadLink
13
13
  } from "@gradio/atoms";
14
+ import type { CustomButton as CustomButtonType } from "@gradio/utils";
14
15
  import { Download, Image as ImageIcon } from "@gradio/icons";
15
16
  import { get_coordinates_of_clicked_image } from "./utils";
16
17
  import Image from "./Image.svelte";
@@ -21,7 +22,8 @@
21
22
  export let value: null | FileData;
22
23
  export let label: string | undefined = undefined;
23
24
  export let show_label: boolean;
24
- export let buttons: string[] | null = null;
25
+ export let buttons: (string | CustomButtonType)[] = [];
26
+ export let on_custom_button_click: ((id: number) => void) | null = null;
25
27
  export let selectable = false;
26
28
  export let i18n: I18nFormatter;
27
29
  export let display_icon_button_wrapper_top_corner = false;
@@ -56,17 +58,18 @@
56
58
  <IconButtonWrapper
57
59
  display_top_corner={display_icon_button_wrapper_top_corner}
58
60
  show_background={show_button_background}
61
+ {buttons}
62
+ {on_custom_button_click}
59
63
  >
60
- {#if buttons === null ? true : buttons.includes("fullscreen")}
64
+ {#if buttons.some((btn) => typeof btn === "string" && btn === "fullscreen")}
61
65
  <FullscreenButton {fullscreen} on:fullscreen />
62
66
  {/if}
63
-
64
- {#if buttons === null ? true : buttons.includes("download")}
67
+ {#if buttons.some((btn) => typeof btn === "string" && btn === "download")}
65
68
  <DownloadLink href={value.url} download={value.orig_name || "image"}>
66
69
  <IconButton Icon={Download} label={i18n("common.download")} />
67
70
  </DownloadLink>
68
71
  {/if}
69
- {#if buttons === null ? true : buttons.includes("share")}
72
+ {#if buttons.some((btn) => typeof btn === "string" && btn === "share")}
70
73
  <ShareButton
71
74
  {i18n}
72
75
  on:share
@@ -1,4 +1,5 @@
1
1
  import type { SelectData } from "@gradio/utils";
2
+ import type { CustomButton as CustomButtonType } from "@gradio/utils";
2
3
  import type { I18nFormatter } from "@gradio/utils";
3
4
  import type { FileData } from "@gradio/client";
4
5
  interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
@@ -18,7 +19,8 @@ declare const ImagePreview: $$__sveltets_2_IsomorphicComponent<{
18
19
  value: null | FileData;
19
20
  label?: string | undefined;
20
21
  show_label: boolean;
21
- buttons?: string[] | null;
22
+ buttons?: (string | CustomButtonType)[];
23
+ on_custom_button_click?: ((id: number) => void) | null;
22
24
  selectable?: boolean;
23
25
  i18n: I18nFormatter;
24
26
  display_icon_button_wrapper_top_corner?: boolean;
@@ -38,6 +38,7 @@
38
38
  export let show_fullscreen_button = true;
39
39
  export let stream_state: "open" | "waiting" | "closed" = "closed";
40
40
  export let upload_promise: Promise<any> | null = null;
41
+ export let onerror: ((error: string) => void) | undefined = undefined;
41
42
 
42
43
  let upload_input: Upload;
43
44
  export let uploading = false;
@@ -47,9 +48,7 @@
47
48
  let files: FileData[] = [];
48
49
  let upload_id: string;
49
50
 
50
- async function handle_upload({
51
- detail
52
- }: CustomEvent<FileData>): Promise<void> {
51
+ async function handle_upload(detail: FileData): Promise<void> {
53
52
  if (!streaming) {
54
53
  if (detail.path?.toLowerCase().endsWith(".svg") && detail.url) {
55
54
  const response = await fetch(detail.url);
@@ -82,7 +81,6 @@
82
81
  img_blob: Blob | any,
83
82
  event: "change" | "stream" | "upload"
84
83
  ): Promise<void> {
85
- console.log("handle_save", { event, img_blob });
86
84
  if (event === "stream") {
87
85
  dispatch("stream", {
88
86
  value: { url: img_blob } as Base64File,
@@ -104,7 +102,6 @@
104
102
  ];
105
103
  pending = true;
106
104
  const f = await upload_input.load_files([f_], upload_id);
107
- console.log("uploaded file", f);
108
105
  if (event === "change" || event === "upload") {
109
106
  value = f?.[0] || null;
110
107
  await tick();
@@ -192,7 +189,7 @@
192
189
  <IconButton
193
190
  Icon={Clear}
194
191
  label="Remove Image"
195
- on:click={handle_remove_image_click}
192
+ onclick={handle_remove_image_click}
196
193
  />
197
194
  {/if}
198
195
  </IconButtonWrapper>
@@ -211,8 +208,8 @@
211
208
  bind:uploading
212
209
  bind:dragging
213
210
  filetype={active_source === "clipboard" ? "clipboard" : "image/*"}
214
- on:load={handle_upload}
215
- on:error
211
+ onload={handle_upload}
212
+ {onerror}
216
213
  {root}
217
214
  {max_file_size}
218
215
  disable_click={!sources.includes("upload") || value !== null}
@@ -38,6 +38,7 @@ declare const ImageUploader: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_P
38
38
  show_fullscreen_button?: boolean;
39
39
  stream_state?: "open" | "waiting" | "closed";
40
40
  upload_promise?: Promise<any> | null;
41
+ onerror?: ((error: string) => void) | undefined;
41
42
  uploading?: boolean;
42
43
  active_source?: "upload" | "clipboard" | "microphone" | "webcam" | null;
43
44
  fullscreen?: boolean;
@@ -45,8 +46,8 @@ declare const ImageUploader: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_P
45
46
  }, {
46
47
  default: {};
47
48
  }>, {
48
- fullscreen: CustomEvent<boolean>;
49
- error: CustomEvent<any> | CustomEvent<string>;
49
+ fullscreen: any;
50
+ error: CustomEvent<string>;
50
51
  drag: CustomEvent<any>;
51
52
  close_stream: CustomEvent<undefined>;
52
53
  change?: CustomEvent<undefined> | undefined;
@@ -118,7 +118,6 @@
118
118
  video_source.videoWidth &&
119
119
  video_source.videoHeight
120
120
  ) {
121
- console.log("Taking picture from webcam");
122
121
  var context = canvas.getContext("2d")!;
123
122
  canvas.width = video_source.videoWidth;
124
123
  canvas.height = video_source.videoHeight;
@@ -1,5 +1,6 @@
1
1
  import type { LoadingStatus } from "@gradio/statustracker";
2
2
  import type { FileData } from "@gradio/client";
3
+ import type { CustomButton } from "@gradio/utils";
3
4
  export interface Base64File {
4
5
  url: string;
5
6
  alt_text: string;
@@ -15,7 +16,7 @@ export interface ImageProps {
15
16
  width: number;
16
17
  webcam_options: WebcamOptions;
17
18
  value: FileData | null;
18
- buttons: string[];
19
+ buttons: (string | CustomButton)[];
19
20
  pending: boolean;
20
21
  streaming: boolean;
21
22
  stream_every: number;
@@ -35,4 +36,7 @@ export interface ImageEvents {
35
36
  error: any;
36
37
  close_stream: void;
37
38
  edit: void;
39
+ custom_button_click: {
40
+ id: number;
41
+ };
38
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/image",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,15 +10,15 @@
10
10
  "cropperjs": "^2.0.1",
11
11
  "lazy-brush": "^2.0.2",
12
12
  "resize-observer-polyfill": "^1.5.1",
13
- "@gradio/atoms": "^0.19.0",
14
- "@gradio/client": "^2.0.0",
15
- "@gradio/icons": "^0.15.0",
16
- "@gradio/upload": "^0.17.2",
17
- "@gradio/utils": "^0.10.3",
18
- "@gradio/statustracker": "^0.12.0"
13
+ "@gradio/atoms": "^0.20.1",
14
+ "@gradio/client": "^2.0.3",
15
+ "@gradio/icons": "^0.15.1",
16
+ "@gradio/statustracker": "^0.12.2",
17
+ "@gradio/upload": "^0.17.4",
18
+ "@gradio/utils": "^0.11.2"
19
19
  },
20
20
  "devDependencies": {
21
- "@gradio/preview": "^0.15.0"
21
+ "@gradio/preview": "^0.15.2"
22
22
  },
23
23
  "main_changeset": true,
24
24
  "main": "./Index.svelte",
@@ -46,7 +46,7 @@
46
46
  }
47
47
  },
48
48
  "peerDependencies": {
49
- "svelte": "^5.43.4"
49
+ "svelte": "^5.48.0"
50
50
  },
51
51
  "repository": {
52
52
  "type": "git",
@@ -11,6 +11,7 @@
11
11
  FullscreenButton,
12
12
  DownloadLink
13
13
  } from "@gradio/atoms";
14
+ import type { CustomButton as CustomButtonType } from "@gradio/utils";
14
15
  import { Download, Image as ImageIcon } from "@gradio/icons";
15
16
  import { get_coordinates_of_clicked_image } from "./utils";
16
17
  import Image from "./Image.svelte";
@@ -21,7 +22,8 @@
21
22
  export let value: null | FileData;
22
23
  export let label: string | undefined = undefined;
23
24
  export let show_label: boolean;
24
- export let buttons: string[] | null = null;
25
+ export let buttons: (string | CustomButtonType)[] = [];
26
+ export let on_custom_button_click: ((id: number) => void) | null = null;
25
27
  export let selectable = false;
26
28
  export let i18n: I18nFormatter;
27
29
  export let display_icon_button_wrapper_top_corner = false;
@@ -56,17 +58,18 @@
56
58
  <IconButtonWrapper
57
59
  display_top_corner={display_icon_button_wrapper_top_corner}
58
60
  show_background={show_button_background}
61
+ {buttons}
62
+ {on_custom_button_click}
59
63
  >
60
- {#if buttons === null ? true : buttons.includes("fullscreen")}
64
+ {#if buttons.some((btn) => typeof btn === "string" && btn === "fullscreen")}
61
65
  <FullscreenButton {fullscreen} on:fullscreen />
62
66
  {/if}
63
-
64
- {#if buttons === null ? true : buttons.includes("download")}
67
+ {#if buttons.some((btn) => typeof btn === "string" && btn === "download")}
65
68
  <DownloadLink href={value.url} download={value.orig_name || "image"}>
66
69
  <IconButton Icon={Download} label={i18n("common.download")} />
67
70
  </DownloadLink>
68
71
  {/if}
69
- {#if buttons === null ? true : buttons.includes("share")}
72
+ {#if buttons.some((btn) => typeof btn === "string" && btn === "share")}
70
73
  <ShareButton
71
74
  {i18n}
72
75
  on:share
@@ -38,6 +38,7 @@
38
38
  export let show_fullscreen_button = true;
39
39
  export let stream_state: "open" | "waiting" | "closed" = "closed";
40
40
  export let upload_promise: Promise<any> | null = null;
41
+ export let onerror: ((error: string) => void) | undefined = undefined;
41
42
 
42
43
  let upload_input: Upload;
43
44
  export let uploading = false;
@@ -47,9 +48,7 @@
47
48
  let files: FileData[] = [];
48
49
  let upload_id: string;
49
50
 
50
- async function handle_upload({
51
- detail
52
- }: CustomEvent<FileData>): Promise<void> {
51
+ async function handle_upload(detail: FileData): Promise<void> {
53
52
  if (!streaming) {
54
53
  if (detail.path?.toLowerCase().endsWith(".svg") && detail.url) {
55
54
  const response = await fetch(detail.url);
@@ -82,7 +81,6 @@
82
81
  img_blob: Blob | any,
83
82
  event: "change" | "stream" | "upload"
84
83
  ): Promise<void> {
85
- console.log("handle_save", { event, img_blob });
86
84
  if (event === "stream") {
87
85
  dispatch("stream", {
88
86
  value: { url: img_blob } as Base64File,
@@ -104,7 +102,6 @@
104
102
  ];
105
103
  pending = true;
106
104
  const f = await upload_input.load_files([f_], upload_id);
107
- console.log("uploaded file", f);
108
105
  if (event === "change" || event === "upload") {
109
106
  value = f?.[0] || null;
110
107
  await tick();
@@ -192,7 +189,7 @@
192
189
  <IconButton
193
190
  Icon={Clear}
194
191
  label="Remove Image"
195
- on:click={handle_remove_image_click}
192
+ onclick={handle_remove_image_click}
196
193
  />
197
194
  {/if}
198
195
  </IconButtonWrapper>
@@ -211,8 +208,8 @@
211
208
  bind:uploading
212
209
  bind:dragging
213
210
  filetype={active_source === "clipboard" ? "clipboard" : "image/*"}
214
- on:load={handle_upload}
215
- on:error
211
+ onload={handle_upload}
212
+ {onerror}
216
213
  {root}
217
214
  {max_file_size}
218
215
  disable_click={!sources.includes("upload") || value !== null}
@@ -118,7 +118,6 @@
118
118
  video_source.videoWidth &&
119
119
  video_source.videoHeight
120
120
  ) {
121
- console.log("Taking picture from webcam");
122
121
  var context = canvas.getContext("2d")!;
123
122
  canvas.width = video_source.videoWidth;
124
123
  canvas.height = video_source.videoHeight;
@@ -5,7 +5,6 @@ import {
5
5
  set_available_devices,
6
6
  set_local_stream
7
7
  } from "./stream_utils";
8
- import * as stream_utils from "./stream_utils";
9
8
 
10
9
  let test_device: MediaDeviceInfo = {
11
10
  deviceId: "test-device",
@@ -31,7 +30,41 @@ const mock_getUserMedia = vi.fn(async () => {
31
30
  });
32
31
  });
33
32
 
34
- window.MediaStream = vi.fn().mockImplementation(() => ({}));
33
+ class MockMediaStream extends EventTarget {
34
+ id: string;
35
+ active: boolean;
36
+
37
+ constructor() {
38
+ super();
39
+ this.id = "mock-stream-id";
40
+ this.active = true;
41
+ }
42
+
43
+ getTracks(): MediaStreamTrack[] {
44
+ return [];
45
+ }
46
+
47
+ getAudioTracks(): MediaStreamTrack[] {
48
+ return [];
49
+ }
50
+
51
+ getVideoTracks(): MediaStreamTrack[] {
52
+ return [];
53
+ }
54
+
55
+ addTrack(): void {}
56
+ removeTrack(): void {}
57
+ getTrackById(): MediaStreamTrack | null {
58
+ return null;
59
+ }
60
+
61
+ clone(): MediaStream {
62
+ return this as unknown as MediaStream;
63
+ }
64
+ }
65
+
66
+ // @ts-ignore - Override global MediaStream for testing
67
+ window.MediaStream = MockMediaStream as any;
35
68
 
36
69
  Object.defineProperty(global.navigator, "mediaDevices", {
37
70
  value: {
@@ -47,10 +80,10 @@ describe("stream_utils", () => {
47
80
  });
48
81
 
49
82
  test("set_local_stream should set the local stream to the video source", () => {
50
- const mock_stream = {}; // mocked MediaStream obj as it's not available in a node env
83
+ const mock_stream = new MockMediaStream() as unknown as MediaStream;
51
84
 
52
85
  const mock_video_source = {
53
- srcObject: null,
86
+ srcObject: null as MediaStream | null,
54
87
  muted: false,
55
88
  play: vi.fn()
56
89
  };
@@ -64,28 +97,27 @@ describe("stream_utils", () => {
64
97
  });
65
98
 
66
99
  test("get_video_stream requests user media with the correct constraints and sets the local stream", async () => {
67
- const mock_video_source = document.createElement("video");
68
- const mock_stream = new MediaStream();
100
+ const mock_video_source = {
101
+ srcObject: null as MediaStream | null,
102
+ muted: false,
103
+ play: vi.fn().mockResolvedValue(undefined)
104
+ } as unknown as HTMLVideoElement;
105
+ const mock_stream = new MockMediaStream() as unknown as MediaStream;
69
106
 
70
107
  global.navigator.mediaDevices.getUserMedia = vi
71
108
  .fn()
72
109
  .mockResolvedValue(mock_stream);
73
110
 
74
- await get_video_stream(true, mock_video_source);
111
+ await get_video_stream(true, mock_video_source, null);
75
112
 
76
113
  expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({
77
114
  video: { width: { ideal: 1920 }, height: { ideal: 1440 } },
78
115
  audio: true
79
116
  });
80
117
 
81
- const spy_set_local_stream = vi.spyOn(stream_utils, "set_local_stream");
82
- stream_utils.set_local_stream(mock_stream, mock_video_source);
83
-
84
- expect(spy_set_local_stream).toHaveBeenCalledWith(
85
- mock_stream,
86
- mock_video_source
87
- );
88
- spy_set_local_stream.mockRestore();
118
+ expect(mock_video_source.srcObject).toBe(mock_stream);
119
+ expect(mock_video_source.muted).toBe(true);
120
+ expect(mock_video_source.play).toHaveBeenCalled();
89
121
  });
90
122
 
91
123
  test("set_available_devices should return only video input devices", () => {
package/shared/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { LoadingStatus } from "@gradio/statustracker";
2
2
  import type { FileData } from "@gradio/client";
3
+ import type { CustomButton } from "@gradio/utils";
3
4
 
4
5
  export interface Base64File {
5
6
  url: string;
@@ -18,7 +19,7 @@ export interface ImageProps {
18
19
  width: number;
19
20
  webcam_options: WebcamOptions;
20
21
  value: FileData | null;
21
- buttons: string[];
22
+ buttons: (string | CustomButton)[];
22
23
  pending: boolean;
23
24
  streaming: boolean;
24
25
  stream_every: number;
@@ -39,4 +40,5 @@ export interface ImageEvents {
39
40
  error: any;
40
41
  close_stream: void;
41
42
  edit: void;
43
+ custom_button_click: { id: number };
42
44
  }