@gradio/imageeditor 0.1.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,289 @@
1
+ <script lang="ts">
2
+ import { tick, createEventDispatcher } from "svelte";
3
+
4
+ export let type = "corner";
5
+ export let location: "tl" | "tr" | "bl" | "br" | "t" | "b" | "l" | "r";
6
+
7
+ export let x1 = 0;
8
+ export let y1 = 0;
9
+ export let x2 = 0;
10
+ export let y2 = 0;
11
+
12
+ let box: DOMRect;
13
+ let y_offset = 0;
14
+ let x_offset = 0;
15
+ const min = 100;
16
+
17
+ let active = false;
18
+
19
+ const dispatch = createEventDispatcher<{
20
+ change: {
21
+ top: number | undefined;
22
+ bottom: number | undefined;
23
+ left: number | undefined;
24
+ right: number | undefined;
25
+ };
26
+ }>();
27
+ export let dragging = false;
28
+
29
+ let el: HTMLDivElement;
30
+
31
+ function handle_mousedown(e: MouseEvent): void {
32
+ box = el.getBoundingClientRect();
33
+ active = true;
34
+ dragging = true;
35
+
36
+ if (["tl", "bl", "l"].includes(location)) x_offset = e.clientX - box.right;
37
+ if (["tl", "tr", "t"].includes(location)) y_offset = e.clientY - box.bottom;
38
+ if (["tr", "r", "br"].includes(location)) x_offset = e.clientX - box.left;
39
+ if (["br", "b", "bl"].includes(location)) y_offset = e.clientY - box.top;
40
+ }
41
+
42
+ function handle_mousemove(e: MouseEvent): void {
43
+ if (!active) return;
44
+ const top_pos = e.clientY - y_offset;
45
+ const bottom_pos = e.clientY - y_offset;
46
+ const left_pos = e.clientX - x_offset;
47
+ const right_pos = e.clientX - x_offset;
48
+
49
+ if (["tl", "bl", "l"].includes(location)) {
50
+ x1 = left_pos;
51
+ }
52
+
53
+ if (["tl", "tr", "t"].includes(location)) {
54
+ y1 = top_pos;
55
+ }
56
+
57
+ if (["tr", "r", "br"].includes(location)) {
58
+ x2 = right_pos;
59
+ }
60
+
61
+ if (["br", "b", "bl"].includes(location)) {
62
+ y2 = bottom_pos;
63
+ }
64
+ }
65
+
66
+ async function handle_mouseup(e: MouseEvent): Promise<void> {
67
+ await tick();
68
+ active = false;
69
+ dragging = false;
70
+ }
71
+
72
+ function make_transform(
73
+ _location: typeof location,
74
+ x1: number,
75
+ x2: number,
76
+ y1: number,
77
+ y2: number
78
+ ): string {
79
+ let t = undefined;
80
+ let l = undefined;
81
+ let r = undefined;
82
+ let b = undefined;
83
+
84
+ if (box) {
85
+ if (["tl", "l", "bl"].includes(_location)) {
86
+ l = x1;
87
+ }
88
+
89
+ if (["tl", "t", "tr"].includes(_location)) {
90
+ t = y1;
91
+ }
92
+
93
+ if (["tr", "r", "br"].includes(_location)) {
94
+ r = x2;
95
+ }
96
+
97
+ if (["br", "b", "bl"].includes(_location)) {
98
+ b = y2;
99
+ }
100
+ }
101
+
102
+ dispatch("change", { top: t, bottom: b, left: l, right: r });
103
+ return "";
104
+ }
105
+
106
+ $: transform = make_transform(location, x1, x2, y1, y2);
107
+ </script>
108
+
109
+ <svelte:window on:mousemove={handle_mousemove} on:mouseup={handle_mouseup} />
110
+
111
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
112
+ <div class="hitbox {location}" on:mousedown={handle_mousedown} style:transform>
113
+ <div class="handle {type} {location}" bind:this={el}></div>
114
+ </div>
115
+
116
+ <style>
117
+ .hitbox {
118
+ position: absolute;
119
+ width: 50px;
120
+ height: 50px;
121
+ --hitbox-mid-offset: calc(50% - 25px);
122
+ --hitbox-corner-offset: -25px;
123
+ --handle-corner-offset: calc(50% - 5px);
124
+ --handle-mid-offset: calc(50% - 2px);
125
+ z-index: 1;
126
+ }
127
+
128
+ /* hitbox positions */
129
+ .hitbox.tl,
130
+ .hitbox.bl,
131
+ .hitbox.l {
132
+ left: var(--hitbox-corner-offset);
133
+ }
134
+
135
+ .hitbox.tr,
136
+ .hitbox.br,
137
+ .hitbox.r {
138
+ right: var(--hitbox-corner-offset);
139
+ }
140
+
141
+ .hitbox.t,
142
+ .hitbox.tr,
143
+ .hitbox.tl {
144
+ top: var(--hitbox-corner-offset);
145
+ }
146
+
147
+ .hitbox.t,
148
+ .hitbox.b {
149
+ left: var(--hitbox-mid-offset);
150
+ }
151
+
152
+ .hitbox.l,
153
+ .hitbox.r {
154
+ top: var(--hitbox-mid-offset);
155
+ }
156
+
157
+ .hitbox.b,
158
+ .hitbox.br,
159
+ .hitbox.bl {
160
+ bottom: var(--hitbox-corner-offset);
161
+ }
162
+
163
+ /* hitbox cursors */
164
+
165
+ .hitbox.tl,
166
+ .hitbox.br {
167
+ cursor: nwse-resize;
168
+ }
169
+
170
+ .hitbox.bl,
171
+ .hitbox.tr {
172
+ cursor: nesw-resize;
173
+ }
174
+
175
+ .hitbox.l,
176
+ .hitbox.r {
177
+ cursor: ew-resize;
178
+ }
179
+
180
+ .hitbox.t,
181
+ .hitbox.b {
182
+ cursor: ns-resize;
183
+ }
184
+
185
+ /* Visual handles */
186
+ .handle {
187
+ position: absolute;
188
+ }
189
+
190
+ /* square to fill in corners */
191
+ .corner {
192
+ width: 4px;
193
+ height: 4px;
194
+ background: black;
195
+ }
196
+
197
+ /* handle segment positions */
198
+ .handle.tl,
199
+ .handle.bl,
200
+ .handle.l {
201
+ left: var(--handle-corner-offset);
202
+ }
203
+
204
+ .handle.tr,
205
+ .handle.r,
206
+ .handle.br {
207
+ right: var(--handle-corner-offset);
208
+ }
209
+
210
+ .handle.tr,
211
+ .handle.t,
212
+ .handle.tl {
213
+ top: var(--handle-corner-offset);
214
+ }
215
+
216
+ .handle.br,
217
+ .handle.b,
218
+ .handle.bl {
219
+ bottom: var(--handle-corner-offset);
220
+ }
221
+
222
+ .handle.l,
223
+ .handle.r {
224
+ bottom: var(--handle-mid-offset);
225
+ }
226
+
227
+ .handle.t,
228
+ .handle.b {
229
+ left: var(--handle-mid-offset);
230
+ }
231
+
232
+ .handle::before {
233
+ position: absolute;
234
+ content: "";
235
+ width: 20px;
236
+ height: 4px;
237
+ background: black;
238
+ }
239
+
240
+ .handle::after {
241
+ position: absolute;
242
+ content: "";
243
+ width: 4px;
244
+ height: 20px;
245
+ background: black;
246
+ }
247
+
248
+ .handle.t::before,
249
+ .handle.b::before {
250
+ width: 30px;
251
+ left: -14px;
252
+ }
253
+
254
+ .handle.r::after,
255
+ .handle.l::after {
256
+ height: 30px;
257
+ top: -13px;
258
+ }
259
+
260
+ .handle.l::before,
261
+ .handle.r::before {
262
+ content: none;
263
+ }
264
+
265
+ .handle.t::after,
266
+ .handle.b::after {
267
+ content: none;
268
+ }
269
+
270
+ .handle.tr::before,
271
+ .handle.br::before {
272
+ right: 4px;
273
+ }
274
+
275
+ .handle.tl::before,
276
+ .handle.bl::before {
277
+ left: 4px;
278
+ }
279
+
280
+ .handle.tl::after,
281
+ .handle.tr::after {
282
+ top: 4px;
283
+ }
284
+
285
+ .handle.bl::after,
286
+ .handle.br::after {
287
+ bottom: 4px;
288
+ }
289
+ </style>
@@ -0,0 +1,215 @@
1
+ <script lang="ts">
2
+ import { getContext, onMount, tick } from "svelte";
3
+ import { type ToolContext, TOOL_KEY } from "./Tools.svelte";
4
+ import { type EditorContext, EDITOR_KEY } from "../ImageEditor.svelte";
5
+ import {
6
+ Upload as UploadIcon,
7
+ Webcam as WebcamIcon,
8
+ ImagePaste
9
+ } from "@gradio/icons";
10
+ import { Upload } from "@gradio/upload";
11
+ import { Webcam } from "@gradio/image";
12
+ import { type I18nFormatter } from "@gradio/utils";
13
+
14
+ import { add_bg_color, add_bg_image } from "./sources";
15
+ import type { FileData, normalise_file } from "@gradio/client";
16
+
17
+ export let background_file: FileData | null;
18
+ export let root: string;
19
+ export let sources: ("upload" | "webcam" | "clipboard")[] = [
20
+ "upload",
21
+ "webcam",
22
+ "clipboard"
23
+ ];
24
+ export let mirror_webcam = true;
25
+ export let i18n: I18nFormatter;
26
+
27
+ const { active_tool, register_tool } = getContext<ToolContext>(TOOL_KEY);
28
+ const { pixi, dimensions, register_context, reset, editor_box } =
29
+ getContext<EditorContext>(EDITOR_KEY);
30
+
31
+ let active_mode: "webcam" | "color" | null = null;
32
+ let background: Blob | File | null;
33
+
34
+ const sources_meta = {
35
+ upload: {
36
+ icon: UploadIcon,
37
+ label: "Upload",
38
+ order: 0,
39
+ id: "bg_upload",
40
+ cb() {
41
+ upload.open_file_upload();
42
+ }
43
+ },
44
+ webcam: {
45
+ icon: WebcamIcon,
46
+ label: "Webcam",
47
+ order: 1,
48
+ id: "bg_webcam",
49
+ cb() {
50
+ active_mode = "webcam";
51
+ }
52
+ },
53
+ clipboard: {
54
+ icon: ImagePaste,
55
+ label: "Paste",
56
+ order: 2,
57
+ id: "bg_clipboard",
58
+ cb() {
59
+ process_clipboard();
60
+ }
61
+ }
62
+ } as const;
63
+
64
+ $: sources_list = sources
65
+ .map((src) => sources_meta[src])
66
+ .sort((a, b) => a.order - b.order);
67
+
68
+ let upload: Upload;
69
+
70
+ async function process_clipboard(): Promise<void> {
71
+ const items = await navigator.clipboard.read();
72
+
73
+ for (let i = 0; i < items.length; i++) {
74
+ const type = items[i].types.find((t) => t.startsWith("image/"));
75
+ if (type) {
76
+ const blob = await items[i].getType(type);
77
+
78
+ background = blob || null;
79
+ }
80
+ }
81
+ }
82
+
83
+ function handle_upload(e: CustomEvent<Blob>): void {
84
+ const file_data = e.detail;
85
+ background = file_data;
86
+ active_mode = null;
87
+ }
88
+
89
+ let should_reset = true;
90
+
91
+ async function set_background(): Promise<void> {
92
+ if (!$pixi) return;
93
+ if (background) {
94
+ const add_image = add_bg_image(
95
+ $pixi.background_container,
96
+ $pixi.renderer,
97
+ background,
98
+ $pixi.resize
99
+ );
100
+ $dimensions = await add_image.start();
101
+
102
+ if (should_reset) {
103
+ reset(false, $dimensions);
104
+ }
105
+
106
+ add_image.execute();
107
+
108
+ should_reset = true;
109
+
110
+ await tick();
111
+ bg = true;
112
+ }
113
+ }
114
+
115
+ async function process_bg_file(file: FileData | null): Promise<void> {
116
+ if (!file || !file.url) return;
117
+ should_reset = false;
118
+
119
+ const blob_res = await fetch(file.url);
120
+ const blob = await blob_res.blob();
121
+ background = blob;
122
+ }
123
+
124
+ function handle_key(e: KeyboardEvent): void {
125
+ if (e.key === "Escape") {
126
+ active_mode = null;
127
+ }
128
+ }
129
+
130
+ $: background && set_background();
131
+ $: process_bg_file(background_file);
132
+
133
+ export let bg = false;
134
+
135
+ register_context("bg", {
136
+ init_fn: () => {
137
+ if (!$pixi) return;
138
+
139
+ const add_image = add_bg_color(
140
+ $pixi.background_container,
141
+ $pixi.renderer,
142
+ "black",
143
+ ...$dimensions,
144
+ $pixi.resize
145
+ );
146
+ $dimensions = add_image.start();
147
+ add_image.execute();
148
+ },
149
+ reset_fn: () => {}
150
+ });
151
+
152
+ onMount(() => {
153
+ return register_tool("bg", {
154
+ default: "bg_upload",
155
+ options: sources_list || []
156
+ });
157
+ });
158
+ </script>
159
+
160
+ <svelte:window on:keydown={handle_key} />
161
+
162
+ {#if $active_tool === "bg"}
163
+ <div class="upload-container">
164
+ <Upload
165
+ hidden={true}
166
+ bind:this={upload}
167
+ filetype="image/*"
168
+ on:load={handle_upload}
169
+ on:error
170
+ {root}
171
+ disable_click={!sources.includes("upload")}
172
+ format="blob"
173
+ ></Upload>
174
+ {#if active_mode === "webcam"}
175
+ <div
176
+ class="modal"
177
+ style:max-width="{$editor_box.child_width}px"
178
+ style:max-height="{$editor_box.child_height}px"
179
+ style:top="{$editor_box.child_top - $editor_box.parent_top}px"
180
+ >
181
+ <div class="modal-inner">
182
+ <Webcam
183
+ on:capture={handle_upload}
184
+ on:error
185
+ on:drag
186
+ {mirror_webcam}
187
+ streaming={false}
188
+ mode="image"
189
+ include_audio={false}
190
+ {i18n}
191
+ />
192
+ </div>
193
+ </div>
194
+ {/if}
195
+ </div>
196
+ {/if}
197
+
198
+ <style>
199
+ .modal {
200
+ position: absolute;
201
+ height: 100%;
202
+ width: 100%;
203
+ left: 0;
204
+ right: 0;
205
+ background-color: rgba(0, 0, 0, 0.9);
206
+ margin: auto;
207
+ z-index: var(--layer-top);
208
+ display: flex;
209
+ align-items: center;
210
+ }
211
+
212
+ .modal-inner {
213
+ width: 100%;
214
+ }
215
+ </style>
@@ -0,0 +1,176 @@
1
+ <script lang="ts" context="module">
2
+ import type {
3
+ Subscriber,
4
+ Invalidator,
5
+ Unsubscriber,
6
+ Writable
7
+ } from "svelte/store";
8
+ import type { tool } from "../tools/";
9
+
10
+ export const TOOL_KEY = Symbol("tool");
11
+
12
+ type upload_tool = "bg_webcam" | "bg_upload" | "bg_clipboard" | "bg_color";
13
+ type transform_tool = "crop" | "rotate";
14
+ type brush_tool = "brush_color" | "brush_size";
15
+ type eraser_tool = "eraser_size";
16
+
17
+ export interface ToolOptions {
18
+ order: number;
19
+ label: string;
20
+ icon: typeof Image;
21
+ cb: (...args: any[]) => void;
22
+ id: upload_tool | transform_tool | brush_tool | eraser_tool;
23
+ }
24
+
25
+ export interface ToolMeta {
26
+ default: upload_tool | transform_tool | brush_tool | eraser_tool | null;
27
+ options: ToolOptions[];
28
+ }
29
+
30
+ export interface ToolContext {
31
+ register_tool: (type: tool, meta: ToolMeta) => () => void;
32
+ active_tool: {
33
+ subscribe(
34
+ this: void,
35
+ run: Subscriber<tool | null>,
36
+ invalidate?: Invalidator<tool | null>
37
+ ): Unsubscriber;
38
+ };
39
+ activate_subtool: (
40
+ sub_tool: upload_tool | transform_tool | brush_tool | eraser_tool | null,
41
+ cb?: (...args: any[]) => void
42
+ ) => void;
43
+ current_color: Writable<string>;
44
+ }
45
+ </script>
46
+
47
+ <script lang="ts">
48
+ import { Toolbar, IconButton } from "@gradio/atoms";
49
+ import { getContext, setContext } from "svelte";
50
+ import { writable } from "svelte/store";
51
+ import { EDITOR_KEY, type EditorContext } from "../ImageEditor.svelte";
52
+ import { Image, Crop, Brush, Erase } from "@gradio/icons";
53
+ import { type I18nFormatter } from "@gradio/utils";
54
+
55
+ const { current_history, active_tool } =
56
+ getContext<EditorContext>(EDITOR_KEY);
57
+
58
+ export let i18n: I18nFormatter;
59
+
60
+ let tools: tool[] = [];
61
+
62
+ const metas: Record<tool, ToolMeta | null> = {
63
+ draw: null,
64
+ erase: null,
65
+ crop: null,
66
+ bg: null
67
+ };
68
+
69
+ $: sub_menu = $active_tool && metas[$active_tool];
70
+ let current_color = writable("#000000");
71
+ let sub_tool: upload_tool | transform_tool | brush_tool | eraser_tool | null;
72
+ const tool_context: ToolContext = {
73
+ current_color,
74
+ register_tool: (type: tool, meta: ToolMeta) => {
75
+ tools = [...tools, type];
76
+ metas[type] = meta;
77
+
78
+ return () => {
79
+ tools = tools.filter((tool) => tool !== type);
80
+ };
81
+ },
82
+
83
+ active_tool: {
84
+ subscribe: active_tool.subscribe
85
+ },
86
+
87
+ activate_subtool: (
88
+ _sub_tool: upload_tool | transform_tool | brush_tool | eraser_tool | null,
89
+ cb?: (...args: any[]) => void
90
+ ) => {
91
+ sub_tool = _sub_tool;
92
+ if (cb) cb();
93
+ }
94
+ };
95
+
96
+ setContext<ToolContext>(TOOL_KEY, tool_context);
97
+
98
+ const tools_meta: Record<
99
+ tool,
100
+ {
101
+ order: number;
102
+ label: string;
103
+ icon: typeof Image;
104
+ }
105
+ > = {
106
+ bg: {
107
+ order: 0,
108
+ label: i18n("Image"),
109
+ icon: Image
110
+ },
111
+ crop: {
112
+ order: 1,
113
+ label: i18n("Transform"),
114
+ icon: Crop
115
+ },
116
+ draw: {
117
+ order: 2,
118
+ label: i18n("Draw"),
119
+ icon: Brush
120
+ },
121
+ erase: {
122
+ order: 2,
123
+ label: i18n("Erase"),
124
+ icon: Erase
125
+ }
126
+ } as const;
127
+ </script>
128
+
129
+ <slot />
130
+
131
+ <div class="toolbar-wrap">
132
+ <Toolbar show_border={false}>
133
+ {#if sub_menu}
134
+ {#each sub_menu.options as meta (meta.id)}
135
+ <IconButton
136
+ highlight={sub_tool === meta.id && meta.id !== "brush_size"}
137
+ color={$active_tool === "draw" && meta.id === "brush_size"
138
+ ? $current_color
139
+ : undefined}
140
+ on:click={() => tool_context.activate_subtool(meta.id, meta.cb)}
141
+ Icon={meta.icon}
142
+ size="large"
143
+ padded={false}
144
+ label={meta.label + " button"}
145
+ hasPopup={true}
146
+ transparent={true}
147
+ />
148
+ {/each}
149
+ {/if}
150
+ </Toolbar>
151
+
152
+ <Toolbar show_border={false}>
153
+ {#each tools as tool (tool)}
154
+ <IconButton
155
+ disabled={tool === "bg" && !!$current_history.previous}
156
+ highlight={$active_tool === tool}
157
+ on:click={() => ($active_tool = tool)}
158
+ Icon={tools_meta[tool].icon}
159
+ size="large"
160
+ padded={false}
161
+ label={tools_meta[tool].label + " button"}
162
+ transparent={true}
163
+ />
164
+ {/each}
165
+ </Toolbar>
166
+ </div>
167
+
168
+ <style>
169
+ .toolbar-wrap {
170
+ display: flex;
171
+ flex-direction: column;
172
+ justify-content: center;
173
+ align-items: center;
174
+ width: 100%;
175
+ }
176
+ </style>