@gradio/imageslider 0.2.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,165 @@
1
+ <script lang="ts">
2
+ import type { HTMLImgAttributes } from "svelte/elements";
3
+ import { createEventDispatcher, onMount, tick } from "svelte";
4
+ interface Props extends HTMLImgAttributes {
5
+ "data-testid"?: string;
6
+ fixed?: boolean;
7
+ transform?: string;
8
+ img_el?: HTMLImageElement;
9
+ hidden?: boolean;
10
+ variant?: "preview" | "upload";
11
+ max_height?: number;
12
+ fullscreen?: boolean;
13
+ }
14
+ type $$Props = Props;
15
+
16
+ import { resolve_wasm_src } from "@gradio/wasm/svelte";
17
+
18
+ export let src: HTMLImgAttributes["src"] = undefined;
19
+ export let fullscreen = false;
20
+
21
+ let resolved_src: typeof src;
22
+
23
+ export let fixed = false;
24
+ export let transform = "translate(0px, 0px) scale(1)";
25
+ export let img_el: HTMLImageElement | null = null;
26
+ export let hidden = false;
27
+ export let variant = "upload";
28
+ export let max_height = 500;
29
+ // The `src` prop can be updated before the Promise from `resolve_wasm_src` is resolved.
30
+ // In such a case, the resolved value for the old `src` has to be discarded,
31
+ // This variable `latest_src` is used to pick up only the value resolved for the latest `src` prop.
32
+ let latest_src: typeof src;
33
+ $: {
34
+ // In normal (non-Wasm) Gradio, the `<img>` element should be rendered with the passed `src` props immediately
35
+ // without waiting for `resolve_wasm_src()` to resolve.
36
+ // If it waits, a blank image is displayed until the async task finishes
37
+ // and it leads to undesirable flickering.
38
+ // So set `src` to `resolved_src` here.
39
+ resolved_src = src;
40
+
41
+ latest_src = src;
42
+ const resolving_src = src;
43
+ resolve_wasm_src(resolving_src).then((s) => {
44
+ if (latest_src === resolving_src) {
45
+ resolved_src = s;
46
+ }
47
+ });
48
+ }
49
+
50
+ const dispatch = createEventDispatcher<{
51
+ load: {
52
+ top: number;
53
+ left: number;
54
+ width: number;
55
+ height: number;
56
+ };
57
+ }>();
58
+
59
+ function get_image_size(img: HTMLImageElement | null): {
60
+ top: number;
61
+ left: number;
62
+ width: number;
63
+ height: number;
64
+ } {
65
+ if (!img) return { top: 0, left: 0, width: 0, height: 0 };
66
+ const container = img.parentElement?.getBoundingClientRect();
67
+
68
+ if (!container) return { top: 0, left: 0, width: 0, height: 0 };
69
+
70
+ const naturalAspect = img.naturalWidth / img.naturalHeight;
71
+ const containerAspect = container.width / container.height;
72
+ let displayedWidth, displayedHeight;
73
+
74
+ if (naturalAspect > containerAspect) {
75
+ displayedWidth = container.width;
76
+ displayedHeight = container.width / naturalAspect;
77
+ } else {
78
+ displayedHeight = container.height;
79
+ displayedWidth = container.height * naturalAspect;
80
+ }
81
+
82
+ const offsetX = (container.width - displayedWidth) / 2;
83
+ const offsetY = (container.height - displayedHeight) / 2;
84
+
85
+ return {
86
+ top: offsetY,
87
+ left: offsetX,
88
+ width: displayedWidth,
89
+ height: displayedHeight
90
+ };
91
+ }
92
+
93
+ onMount(() => {
94
+ const resizer = new ResizeObserver(async (entries) => {
95
+ for (const entry of entries) {
96
+ await tick();
97
+ dispatch("load", get_image_size(img_el));
98
+ }
99
+ });
100
+
101
+ resizer.observe(img_el!);
102
+
103
+ return () => {
104
+ resizer.disconnect();
105
+ };
106
+ });
107
+ </script>
108
+
109
+ <!-- svelte-ignore a11y-missing-attribute -->
110
+ <img
111
+ src={resolved_src}
112
+ {...$$restProps}
113
+ class:fixed
114
+ style:transform
115
+ bind:this={img_el}
116
+ class:hidden
117
+ class:preview={variant === "preview"}
118
+ class:slider={variant === "upload"}
119
+ style:max-height={max_height && !fullscreen ? `${max_height}px` : null}
120
+ class:fullscreen
121
+ class:small={!fullscreen}
122
+ on:load={() => dispatch("load", get_image_size(img_el))}
123
+ />
124
+
125
+ <style>
126
+ .preview {
127
+ object-fit: contain;
128
+ width: 100%;
129
+ transform-origin: top left;
130
+ margin: auto;
131
+ }
132
+
133
+ .small {
134
+ max-height: 500px;
135
+ }
136
+
137
+ .upload {
138
+ object-fit: contain;
139
+ max-height: 500px;
140
+ }
141
+
142
+ .fixed {
143
+ position: absolute;
144
+ top: 0;
145
+ left: 0;
146
+ right: 0;
147
+ bottom: 0;
148
+ }
149
+
150
+ .fullscreen {
151
+ width: 100%;
152
+ height: 100%;
153
+ }
154
+
155
+ :global(.image-container:fullscreen) img {
156
+ width: 100%;
157
+ height: 100%;
158
+ max-height: none;
159
+ max-width: none;
160
+ }
161
+
162
+ .hidden {
163
+ opacity: 0;
164
+ }
165
+ </style>
@@ -0,0 +1,200 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { drag } from "d3-drag";
4
+ import { select } from "d3-selection";
5
+
6
+ function clamp(value: number, min: number, max: number): number {
7
+ return Math.min(Math.max(value, min), max);
8
+ }
9
+
10
+ export let position = 0.5;
11
+ export let disabled = false;
12
+
13
+ export let slider_color = "var(--border-color-primary)";
14
+ export let image_size: {
15
+ top: number;
16
+ left: number;
17
+ width: number;
18
+ height: number;
19
+ } = { top: 0, left: 0, width: 0, height: 0 };
20
+ export let el: HTMLDivElement | undefined = undefined;
21
+ export let parent_el: HTMLDivElement | undefined = undefined;
22
+ let inner: Element;
23
+ let px = 0;
24
+ let active = false;
25
+ let container_width = 0;
26
+
27
+ function set_position(width: number): void {
28
+ container_width = parent_el?.getBoundingClientRect().width || 0;
29
+ if (width === 0) {
30
+ image_size.width = el?.getBoundingClientRect().width || 0;
31
+ }
32
+
33
+ px = clamp(
34
+ image_size.width * position + image_size.left,
35
+ 0,
36
+ container_width
37
+ );
38
+ }
39
+
40
+ function round(n: number, points: number): number {
41
+ const mod = Math.pow(10, points);
42
+ return Math.round((n + Number.EPSILON) * mod) / mod;
43
+ }
44
+
45
+ function update_position(x: number): void {
46
+ px = clamp(x, 0, container_width);
47
+ position = round((x - image_size.left) / image_size.width, 5);
48
+ }
49
+
50
+ function drag_start(event: any): void {
51
+ if (disabled) return;
52
+ active = true;
53
+ update_position(event.x);
54
+ }
55
+
56
+ function drag_move(event: any): void {
57
+ if (disabled) return;
58
+ update_position(event.x);
59
+ }
60
+
61
+ function drag_end(): void {
62
+ if (disabled) return;
63
+ active = false;
64
+ }
65
+
66
+ function update_position_from_pc(pc: number): void {
67
+ px = clamp(image_size.width * pc + image_size.left, 0, container_width);
68
+ }
69
+
70
+ $: set_position(image_size.width);
71
+ $: update_position_from_pc(position);
72
+
73
+ onMount(() => {
74
+ set_position(image_size.width);
75
+ const drag_handler = drag()
76
+ .on("start", drag_start)
77
+ .on("drag", drag_move)
78
+ .on("end", drag_end);
79
+ select(inner).call(drag_handler);
80
+ });
81
+ </script>
82
+
83
+ <svelte:window on:resize={() => set_position(image_size.width)} />
84
+
85
+ <div class="wrap" role="none" bind:this={parent_el}>
86
+ <div class="content" bind:this={el}>
87
+ <slot />
88
+ </div>
89
+ <div
90
+ class="outer"
91
+ class:disabled
92
+ bind:this={inner}
93
+ role="none"
94
+ style="transform: translateX({px}px)"
95
+ class:grab={active}
96
+ >
97
+ <span class="icon-wrap" class:active class:disabled
98
+ ><span class="icon left">◢</span><span
99
+ class="icon center"
100
+ style:--color={slider_color}
101
+ ></span><span class="icon right">◢</span></span
102
+ >
103
+ <div class="inner" style:--color={slider_color}></div>
104
+ </div>
105
+ </div>
106
+
107
+ <style>
108
+ .wrap {
109
+ position: relative;
110
+ width: 100%;
111
+ height: 100%;
112
+ z-index: var(--layer-1);
113
+ overflow: hidden;
114
+ }
115
+
116
+ .icon-wrap {
117
+ display: block;
118
+ position: absolute;
119
+ top: 50%;
120
+ transform: translate(-20.5px, -50%);
121
+ left: 10px;
122
+ width: 40px;
123
+ transition: 0.2s;
124
+ color: var(--body-text-color);
125
+ height: 30px;
126
+ border-radius: 5px;
127
+ background-color: var(--color-accent);
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ z-index: var(--layer-3);
132
+ box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.3);
133
+ font-size: 12px;
134
+ }
135
+
136
+ .icon.left {
137
+ transform: rotate(135deg);
138
+ text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1);
139
+ }
140
+
141
+ .icon.right {
142
+ transform: rotate(-45deg);
143
+ text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1);
144
+ }
145
+
146
+ .icon.center {
147
+ display: block;
148
+ width: 1px;
149
+ height: 100%;
150
+ background-color: var(--color);
151
+ opacity: 0.1;
152
+ }
153
+
154
+ .icon-wrap.active {
155
+ opacity: 0;
156
+ }
157
+
158
+ .icon-wrap.disabled {
159
+ opacity: 0;
160
+ }
161
+
162
+ .outer {
163
+ width: 20px;
164
+ height: 100%;
165
+ position: absolute;
166
+ cursor: grab;
167
+ position: absolute;
168
+ top: 0;
169
+ left: -10px;
170
+ pointer-events: auto;
171
+ z-index: var(--layer-2);
172
+ }
173
+ .grab {
174
+ cursor: grabbing;
175
+ }
176
+
177
+ .inner {
178
+ width: 1px;
179
+ height: 100%;
180
+ background: var(--color);
181
+ position: absolute;
182
+ left: calc((100% - 2px) / 2);
183
+ }
184
+
185
+ .disabled {
186
+ cursor: auto;
187
+ }
188
+
189
+ .disabled .inner {
190
+ box-shadow: none;
191
+ }
192
+
193
+ .content {
194
+ width: 100%;
195
+ height: 100%;
196
+ display: flex;
197
+ justify-content: center;
198
+ align-items: center;
199
+ }
200
+ </style>
@@ -0,0 +1,229 @@
1
+ <script lang="ts">
2
+ import Slider from "./Slider.svelte";
3
+ import ImageEl from "./ImageEl.svelte";
4
+ import {
5
+ BlockLabel,
6
+ Empty,
7
+ IconButton,
8
+ IconButtonWrapper,
9
+ FullscreenButton
10
+ } from "@gradio/atoms";
11
+ import { Image, Download, Undo, Clear } from "@gradio/icons";
12
+ import { type FileData } from "@gradio/client";
13
+ import type { I18nFormatter } from "@gradio/utils";
14
+ import { DownloadLink } from "@gradio/wasm/svelte";
15
+ import { ZoomableImage } from "./zoom";
16
+ import { onMount } from "svelte";
17
+ import { tweened, type Tweened } from "svelte/motion";
18
+ import { createEventDispatcher } from "svelte";
19
+
20
+ export let value: [null | FileData, null | FileData] = [null, null];
21
+ export let label: string | undefined = undefined;
22
+ export let show_download_button = true;
23
+ export let show_label: boolean;
24
+ export let i18n: I18nFormatter;
25
+ export let position: number;
26
+ export let layer_images = true;
27
+ export let show_single = false;
28
+ export let slider_color: string;
29
+ export let show_fullscreen_button = true;
30
+ export let el_width = 0;
31
+ export let max_height: number;
32
+ export let interactive = true;
33
+ const dispatch = createEventDispatcher<{ clear: void }>();
34
+
35
+ let img: HTMLImageElement;
36
+ let slider_wrap: HTMLDivElement;
37
+ let image_container: HTMLDivElement;
38
+
39
+ let transform: Tweened<{ x: number; y: number; z: number }> = tweened(
40
+ { x: 0, y: 0, z: 1 },
41
+ {
42
+ duration: 75
43
+ }
44
+ );
45
+ let parent_el: HTMLDivElement;
46
+
47
+ $: coords_at_viewport = get_coords_at_viewport(
48
+ position,
49
+ viewport_width,
50
+ image_size.width,
51
+ image_size.left,
52
+ $transform.x,
53
+ $transform.z
54
+ );
55
+ $: style = layer_images
56
+ ? `clip-path: inset(0 0 0 ${coords_at_viewport * 100}%)`
57
+ : "";
58
+
59
+ function get_coords_at_viewport(
60
+ viewport_percent_x: number, // 0-1
61
+ viewportWidth: number,
62
+ image_width: number,
63
+ img_offset_x: number,
64
+ tx: number, // image translation x (in pixels)
65
+ scale: number // image scale (uniform)
66
+ ): number {
67
+ const px_relative_to_image = viewport_percent_x * image_width;
68
+ const pixel_position = px_relative_to_image + img_offset_x;
69
+
70
+ const normalised_position = (pixel_position - tx) / scale;
71
+ const percent_position = normalised_position / viewportWidth;
72
+
73
+ return percent_position;
74
+ }
75
+
76
+ let img_width = 0;
77
+ let viewport_width = 0;
78
+
79
+ let zoomable_image: ZoomableImage | null = null;
80
+ let observer: ResizeObserver | null = null;
81
+
82
+ function init_image(
83
+ img: HTMLImageElement,
84
+ slider_wrap: HTMLDivElement
85
+ ): void {
86
+ if (!img || !slider_wrap) return;
87
+ zoomable_image?.destroy();
88
+ observer?.disconnect();
89
+ img_width = img?.getBoundingClientRect().width || 0;
90
+ viewport_width = slider_wrap?.getBoundingClientRect().width || 0;
91
+ zoomable_image = new ZoomableImage(slider_wrap, img);
92
+ zoomable_image.subscribe(({ x, y, scale }) => {
93
+ transform.set({ x, y, z: scale });
94
+ });
95
+
96
+ observer = new ResizeObserver((entries) => {
97
+ for (const entry of entries) {
98
+ if (entry.target === slider_wrap) {
99
+ viewport_width = entry.contentRect.width;
100
+ }
101
+
102
+ if (entry.target === img) {
103
+ img_width = entry.contentRect.width;
104
+ }
105
+ }
106
+ });
107
+ observer.observe(slider_wrap);
108
+ observer.observe(img);
109
+ }
110
+
111
+ $: init_image(img, slider_wrap);
112
+
113
+ onMount(() => {
114
+ return () => {
115
+ zoomable_image?.destroy();
116
+ observer?.disconnect();
117
+ };
118
+ });
119
+
120
+ let is_full_screen = false;
121
+ let slider_wrap_parent: HTMLDivElement;
122
+
123
+ let image_size: { top: number; left: number; width: number; height: number } =
124
+ { top: 0, left: 0, width: 0, height: 0 };
125
+
126
+ function handle_image_load(event: CustomEvent): void {
127
+ image_size = event.detail;
128
+ }
129
+ </script>
130
+
131
+ <BlockLabel {show_label} Icon={Image} label={label || i18n("image.image")} />
132
+ {#if (value === null || value[0] === null || value[1] === null) && !show_single}
133
+ <Empty unpadded_box={true} size="large"><Image /></Empty>
134
+ {:else}
135
+ <div class="image-container" bind:this={image_container}>
136
+ <IconButtonWrapper>
137
+ <IconButton
138
+ Icon={Undo}
139
+ label={i18n("common.undo")}
140
+ disabled={$transform.z === 1}
141
+ on:click={() => zoomable_image?.reset_zoom()}
142
+ />
143
+ {#if show_fullscreen_button}
144
+ <FullscreenButton container={image_container} bind:is_full_screen />
145
+ {/if}
146
+
147
+ {#if show_download_button}
148
+ <DownloadLink
149
+ href={value[1]?.url}
150
+ download={value[1]?.orig_name || "image"}
151
+ >
152
+ <IconButton Icon={Download} label={i18n("common.download")} />
153
+ </DownloadLink>
154
+ {/if}
155
+ {#if interactive}
156
+ <IconButton
157
+ Icon={Clear}
158
+ label="Remove Image"
159
+ on:click={(event) => {
160
+ value = [null, null];
161
+ dispatch("clear");
162
+ event.stopPropagation();
163
+ }}
164
+ />
165
+ {/if}
166
+ </IconButtonWrapper>
167
+ <div
168
+ class="slider-wrap"
169
+ bind:this={slider_wrap_parent}
170
+ bind:clientWidth={el_width}
171
+ class:limit_height={!is_full_screen}
172
+ >
173
+ <Slider
174
+ bind:position
175
+ {slider_color}
176
+ bind:el={slider_wrap}
177
+ bind:parent_el
178
+ {image_size}
179
+ >
180
+ <ImageEl
181
+ src={value?.[0]?.url}
182
+ alt=""
183
+ loading="lazy"
184
+ bind:img_el={img}
185
+ variant="preview"
186
+ transform="translate({$transform.x}px, {$transform.y}px) scale({$transform.z})"
187
+ fullscreen={is_full_screen}
188
+ {max_height}
189
+ on:load={handle_image_load}
190
+ />
191
+ <ImageEl
192
+ variant="preview"
193
+ fixed={layer_images}
194
+ hidden={!value?.[1]?.url}
195
+ src={value?.[1]?.url}
196
+ alt=""
197
+ loading="lazy"
198
+ {style}
199
+ transform="translate({$transform.x}px, {$transform.y}px) scale({$transform.z})"
200
+ fullscreen={is_full_screen}
201
+ {max_height}
202
+ on:load={handle_image_load}
203
+ />
204
+ </Slider>
205
+ </div>
206
+ </div>
207
+ {/if}
208
+
209
+ <style>
210
+ .slider-wrap {
211
+ user-select: none;
212
+ height: 100%;
213
+ width: 100%;
214
+ position: relative;
215
+ display: flex;
216
+ align-items: center;
217
+ justify-content: center;
218
+ }
219
+
220
+ .limit_height :global(img) {
221
+ max-height: 500px;
222
+ }
223
+
224
+ .image-container {
225
+ height: 100%;
226
+ position: relative;
227
+ min-width: var(--size-20);
228
+ }
229
+ </style>
@@ -0,0 +1,46 @@
1
+ <svelte:options accessors={true} />
2
+
3
+ <script lang="ts">
4
+ import type { I18nFormatter } from "@gradio/utils";
5
+ import Image from "./Image.svelte";
6
+ import { type Client } from "@gradio/client";
7
+
8
+ import type { FileData } from "@gradio/client";
9
+
10
+ export let value: [FileData | null, FileData | null] = [null, null];
11
+ export let upload: Client["upload"];
12
+ export let stream_handler: Client["stream"];
13
+ export let label: string;
14
+ export let show_label: boolean;
15
+ export let i18n: I18nFormatter;
16
+ export let root: string;
17
+ export let upload_count = 1;
18
+ export let dragging: boolean;
19
+ export let max_height: number;
20
+ export let max_file_size: number | null = null;
21
+ </script>
22
+
23
+ <Image
24
+ slider_color="var(--border-color-primary)"
25
+ position={0.5}
26
+ bind:value
27
+ bind:dragging
28
+ {root}
29
+ on:edit
30
+ on:clear
31
+ on:stream
32
+ on:drag={({ detail }) => (dragging = detail)}
33
+ on:upload
34
+ on:select
35
+ on:share
36
+ {label}
37
+ {show_label}
38
+ {upload_count}
39
+ {stream_handler}
40
+ {upload}
41
+ {max_file_size}
42
+ {max_height}
43
+ {i18n}
44
+ >
45
+ <slot />
46
+ </Image>