@gradio/image 0.26.3 → 0.27.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @gradio/image
2
2
 
3
+ ## 0.27.0
4
+
5
+ ### Features
6
+
7
+ - [#13526](https://github.com/gradio-app/gradio/pull/13526) [`53cb4ca`](https://github.com/gradio-app/gradio/commit/53cb4cae1ec3521e9170d12867253516413ba37a) - Run `pnpm lint` and `pnpm ts:check` on CI. Thanks @abidlabs!
8
+
9
+ ### Fixes
10
+
11
+ - [#13532](https://github.com/gradio-app/gradio/pull/13532) [`0a933b4`](https://github.com/gradio-app/gradio/commit/0a933b428f5b2158cb8c764f38abf0da2312d58a) - Fix `gr.SelectData` coordinates when the image in `gr.Image` does not fill its container. Thanks @hysts!
12
+ - [#13533](https://github.com/gradio-app/gradio/pull/13533) [`e5ec1ca`](https://github.com/gradio-app/gradio/commit/e5ec1ca66c45e7e3a057b75ac2b8b86660b2827b) - Fix fullscreen button not working in ImageSlider, interactive Image, native plots, and AnnotatedImage. Thanks @hysts!
13
+
14
+ ### Dependency updates
15
+
16
+ - @gradio/atoms@0.25.0
17
+ - @gradio/statustracker@0.15.0
18
+ - @gradio/utils@0.13.0
19
+ - @gradio/client@2.3.0
20
+ - @gradio/upload@0.18.0
21
+
22
+ ## 0.26.3
23
+
24
+ ### Dependency updates
25
+
26
+ - @gradio/client@2.2.2
27
+
28
+ ## 0.26.3
29
+
30
+ ### Dependency updates
31
+
32
+ - @gradio/client@2.2.1
33
+ - @gradio/upload@0.17.10
34
+
3
35
  ## 0.26.3
4
36
 
5
37
  ### Dependency updates
@@ -1,7 +1,6 @@
1
1
  <script module>
2
2
  import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import StaticImage from "./Index.svelte";
4
- import { userEvent, within, expect } from "storybook/test";
5
4
  import { allModes } from "../storybook/modes";
6
5
  import { wrapProps } from "../storybook/wrapProps";
7
6
 
@@ -37,13 +36,6 @@
37
36
  buttons: ["fullscreen", "download"],
38
37
  webcam_options: { mirror: true, constraints: null }
39
38
  }}
40
- play={async ({ canvasElement }) => {
41
- const canvas = within(canvasElement);
42
- const expand_btn = await canvas.findByRole("button", {
43
- name: "Fullscreen"
44
- });
45
- expect(expand_btn).toBeTruthy();
46
- }}
47
39
  >
48
40
  {#snippet template(args)}
49
41
  <div
@@ -146,41 +138,6 @@
146
138
  {/snippet}
147
139
  </Story>
148
140
 
149
- <Story
150
- name="interactive with upload, clipboard, and webcam"
151
- args={{
152
- sources: ["upload", "clipboard", "webcam"],
153
- value: {
154
- path: cheetah,
155
- url: cheetah,
156
- orig_name: "cheetah.jpg"
157
- },
158
- show_label: false,
159
- interactive: true,
160
- placeholder: md,
161
- buttons: [],
162
- webcam_options: { mirror: true, constraints: null }
163
- }}
164
- parameters={{ chromatic: { disableSnapshot: true } }}
165
- play={async ({ canvasElement }) => {
166
- const canvas = within(canvasElement);
167
- const webcamButton = await canvas.findByLabelText("Capture from camera");
168
- userEvent.click(webcamButton);
169
- userEvent.click(await canvas.findByTitle("grant webcam access"));
170
- userEvent.click(await canvas.findByLabelText("Upload file"));
171
- userEvent.click(await canvas.findByLabelText("Paste from clipboard"));
172
- }}
173
- >
174
- {#snippet template(args)}
175
- <div
176
- class="image-container"
177
- style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
178
- >
179
- <StaticImage {...wrapProps(args)} />
180
- </div>
181
- {/snippet}
182
- </Story>
183
-
184
141
  <Story
185
142
  name="interactive with webcam"
186
143
  args={{
@@ -217,28 +174,3 @@
217
174
  </div>
218
175
  {/snippet}
219
176
  </Story>
220
-
221
- <Story
222
- name="interactive webcam with streaming"
223
- args={{
224
- sources: ["webcam"],
225
- interactive: true,
226
- value: {
227
- path: cheetah,
228
- url: cheetah,
229
- orig_name: "cheetah.jpg"
230
- },
231
- streaming: true,
232
- buttons: ["download"],
233
- webcam_options: { mirror: true, constraints: null }
234
- }}
235
- >
236
- {#snippet template(args)}
237
- <div
238
- class="image-container"
239
- style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
240
- >
241
- <StaticImage {...wrapProps(args)} />
242
- </div>
243
- {/snippet}
244
- </Story>
package/Image.test.ts CHANGED
@@ -326,6 +326,25 @@ describe("Props: buttons (static mode)", () => {
326
326
  expect(getByLabelText("Fullscreen")).toBeTruthy();
327
327
  });
328
328
 
329
+ test("clicking the fullscreen button toggles fullscreen in interactive mode", async () => {
330
+ const { getByLabelText } = await render(Image, {
331
+ ...default_props,
332
+ interactive: true,
333
+ value: fake_value,
334
+ buttons: ["fullscreen"]
335
+ });
336
+
337
+ await fireEvent.click(getByLabelText("Fullscreen"));
338
+ await waitFor(() => {
339
+ expect(getByLabelText("Exit fullscreen mode")).toBeVisible();
340
+ });
341
+
342
+ await fireEvent.click(getByLabelText("Exit fullscreen mode"));
343
+ await waitFor(() => {
344
+ expect(getByLabelText("Fullscreen")).toBeVisible();
345
+ });
346
+ });
347
+
329
348
  test("empty buttons array shows no action buttons", async () => {
330
349
  const { queryByLabelText } = await render(Image, {
331
350
  ...default_props,
@@ -635,6 +654,52 @@ describe("get_coordinates_of_clicked_image", () => {
635
654
  expect(result).toEqual([100, 200]);
636
655
  });
637
656
 
657
+ test("handles image shown at natural size with empty space on both axes", () => {
658
+ // `object-fit: scale-down` never upscales: a 100x100 natural image in
659
+ // a 400x300 box is drawn at natural size, centered at offset (150, 100).
660
+ // Click at (150, 100) = top-left corner of the drawn image.
661
+ const evt = make_mock_event(
662
+ 150,
663
+ 100,
664
+ {
665
+ naturalWidth: 100,
666
+ naturalHeight: 100
667
+ },
668
+ {
669
+ left: 0,
670
+ top: 0,
671
+ width: 400,
672
+ height: 300
673
+ }
674
+ );
675
+
676
+ const result = get_coordinates_of_clicked_image(evt);
677
+ expect(result).toEqual([0, 0]);
678
+ });
679
+
680
+ test("returns null when click is in the empty space around a natural-size image", () => {
681
+ // 100x100 natural image in a 1920x1080 box: drawn at natural size,
682
+ // centered at offset (910, 490). A click at (500, 540) is inside the
683
+ // box but left of the drawn image, so no image pixel is under it.
684
+ const evt = make_mock_event(
685
+ 500,
686
+ 540,
687
+ {
688
+ naturalWidth: 100,
689
+ naturalHeight: 100
690
+ },
691
+ {
692
+ left: 0,
693
+ top: 0,
694
+ width: 1920,
695
+ height: 1080
696
+ }
697
+ );
698
+
699
+ const result = get_coordinates_of_clicked_image(evt);
700
+ expect(result).toBeNull();
701
+ });
702
+
638
703
  test("returns [NaN, NaN] when currentTarget is not an Element", () => {
639
704
  const evt = {
640
705
  currentTarget: {},
@@ -24,14 +24,3 @@
24
24
  <Image {...args} />
25
25
  {/snippet}
26
26
  </Story>
27
-
28
- <Story
29
- name="Null"
30
- args={{
31
- value: null
32
- }}
33
- >
34
- {#snippet template(args)}
35
- <Image {...args} />
36
- {/snippet}
37
- </Story>
package/Index.svelte CHANGED
@@ -100,7 +100,7 @@
100
100
  width={gradio.props.width}
101
101
  allow_overflow={false}
102
102
  container={gradio.shared.container}
103
- scale{gradio.shared.scale}
103
+ scale={gradio.shared.scale}
104
104
  min_width={gradio.shared.min_width}
105
105
  bind:fullscreen
106
106
  >
package/dist/Index.svelte CHANGED
@@ -100,7 +100,7 @@
100
100
  width={gradio.props.width}
101
101
  allow_overflow={false}
102
102
  container={gradio.shared.container}
103
- scale{gradio.shared.scale}
103
+ scale={gradio.shared.scale}
104
104
  min_width={gradio.shared.min_width}
105
105
  bind:fullscreen
106
106
  >
@@ -1,23 +1,26 @@
1
1
  <script lang="ts">
2
2
  let {
3
- src,
4
- restProps,
3
+ src = "",
4
+ restProps = {},
5
5
  data_testid,
6
- class_names
6
+ class_names = [],
7
+ ...imgProps
7
8
  }: {
8
- src: string;
9
- restProps: object;
10
- data_testid: string;
11
- class_names: string[];
9
+ src?: string;
10
+ restProps?: Record<string, any>;
11
+ data_testid?: string;
12
+ class_names?: string[];
13
+ [key: string]: any;
12
14
  } = $props();
13
15
  </script>
14
16
 
15
17
  <!-- svelte-ignore a11y-missing-attribute -->
16
18
  <img
17
19
  {src}
18
- class={(class_names || []).join(" ")}
20
+ class={class_names.join(" ")}
19
21
  data-testid={data_testid}
20
22
  {...restProps}
23
+ {...imgProps}
21
24
  on:load
22
25
  />
23
26
 
@@ -1,8 +1,9 @@
1
1
  type $$ComponentProps = {
2
- src: string;
3
- restProps: object;
4
- data_testid: string;
5
- class_names: string[];
2
+ src?: string;
3
+ restProps?: Record<string, any>;
4
+ data_testid?: string;
5
+ class_names?: string[];
6
+ [key: string]: any;
6
7
  };
7
8
  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> {
8
9
  new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
@@ -121,6 +121,7 @@
121
121
  upload?: never;
122
122
  select: SelectData;
123
123
  end_stream: never;
124
+ fullscreen: boolean;
124
125
  }>();
125
126
 
126
127
  export let dragging = false;
@@ -184,7 +185,13 @@
184
185
  <IconButtonWrapper>
185
186
  {#if value?.url && !active_streaming}
186
187
  {#if show_fullscreen_button}
187
- <FullscreenButton {fullscreen} on:fullscreen />
188
+ <FullscreenButton
189
+ {fullscreen}
190
+ onclick={(is_fullscreen) => {
191
+ fullscreen = is_fullscreen;
192
+ dispatch("fullscreen", is_fullscreen);
193
+ }}
194
+ />
188
195
  {/if}
189
196
  <IconButton
190
197
  Icon={Clear}
@@ -257,7 +264,7 @@
257
264
  {sources}
258
265
  bind:active_source
259
266
  {handle_clear}
260
- handle_select={handle_select_source}
267
+ handle_select={(source) => handle_select_source(source as source_type)}
261
268
  />
262
269
  {/if}
263
270
  </div>
@@ -46,7 +46,6 @@ declare const ImageUploader: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_P
46
46
  }, {
47
47
  default: {};
48
48
  }>, {
49
- fullscreen: any;
50
49
  error: CustomEvent<string>;
51
50
  drag: CustomEvent<any>;
52
51
  close_stream: CustomEvent<undefined>;
@@ -56,6 +55,7 @@ declare const ImageUploader: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_P
56
55
  upload?: CustomEvent<undefined> | undefined;
57
56
  select: CustomEvent<SelectData>;
58
57
  end_stream: CustomEvent<never>;
58
+ fullscreen: CustomEvent<boolean>;
59
59
  } & {
60
60
  [evt: string]: CustomEvent<any>;
61
61
  }, {
@@ -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
  export interface Base64File {
@@ -7,20 +7,13 @@ export const get_coordinates_of_clicked_image = (evt) => {
7
7
  return [NaN, NaN];
8
8
  }
9
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
- }
10
+ // The image is rendered with `object-fit: scale-down`: centered, never
11
+ // scaled above its natural size, so empty space can appear on both axes.
12
+ const scale = Math.min(imageRect.width / image.naturalWidth, imageRect.height / image.naturalHeight, 1);
13
+ const x_offset = (imageRect.width - image.naturalWidth * scale) / 2;
14
+ const y_offset = (imageRect.height - image.naturalHeight * scale) / 2;
15
+ const x = Math.round((evt.clientX - imageRect.left - x_offset) / scale);
16
+ const y = Math.round((evt.clientY - imageRect.top - y_offset) / scale);
24
17
  if (x < 0 || x >= image.naturalWidth || y < 0 || y >= image.naturalHeight) {
25
18
  return null;
26
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/image",
3
- "version": "0.26.3",
3
+ "version": "0.27.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,12 +10,12 @@
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.24.0",
13
+ "@gradio/atoms": "^0.25.0",
14
+ "@gradio/client": "^2.3.0",
14
15
  "@gradio/icons": "^0.15.1",
15
- "@gradio/statustracker": "^0.14.1",
16
- "@gradio/upload": "^0.17.9",
17
- "@gradio/utils": "^0.12.2",
18
- "@gradio/client": "^2.2.0"
16
+ "@gradio/statustracker": "^0.15.0",
17
+ "@gradio/utils": "^0.13.0",
18
+ "@gradio/upload": "^0.18.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@gradio/preview": "^0.16.2"
@@ -1,23 +1,26 @@
1
1
  <script lang="ts">
2
2
  let {
3
- src,
4
- restProps,
3
+ src = "",
4
+ restProps = {},
5
5
  data_testid,
6
- class_names
6
+ class_names = [],
7
+ ...imgProps
7
8
  }: {
8
- src: string;
9
- restProps: object;
10
- data_testid: string;
11
- class_names: string[];
9
+ src?: string;
10
+ restProps?: Record<string, any>;
11
+ data_testid?: string;
12
+ class_names?: string[];
13
+ [key: string]: any;
12
14
  } = $props();
13
15
  </script>
14
16
 
15
17
  <!-- svelte-ignore a11y-missing-attribute -->
16
18
  <img
17
19
  {src}
18
- class={(class_names || []).join(" ")}
20
+ class={class_names.join(" ")}
19
21
  data-testid={data_testid}
20
22
  {...restProps}
23
+ {...imgProps}
21
24
  on:load
22
25
  />
23
26
 
@@ -121,6 +121,7 @@
121
121
  upload?: never;
122
122
  select: SelectData;
123
123
  end_stream: never;
124
+ fullscreen: boolean;
124
125
  }>();
125
126
 
126
127
  export let dragging = false;
@@ -184,7 +185,13 @@
184
185
  <IconButtonWrapper>
185
186
  {#if value?.url && !active_streaming}
186
187
  {#if show_fullscreen_button}
187
- <FullscreenButton {fullscreen} on:fullscreen />
188
+ <FullscreenButton
189
+ {fullscreen}
190
+ onclick={(is_fullscreen) => {
191
+ fullscreen = is_fullscreen;
192
+ dispatch("fullscreen", is_fullscreen);
193
+ }}
194
+ />
188
195
  {/if}
189
196
  <IconButton
190
197
  Icon={Clear}
@@ -257,7 +264,7 @@
257
264
  {sources}
258
265
  bind:active_source
259
266
  {handle_clear}
260
- handle_select={handle_select_source}
267
+ handle_select={(source) => handle_select_source(source as source_type)}
261
268
  />
262
269
  {/if}
263
270
  </div>
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
  }