@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.
- package/CHANGELOG.md +57 -0
- package/Example.svelte +39 -0
- package/ImageEditor.stories.svelte +197 -0
- package/Index.svelte +181 -0
- package/LICENSE +201 -0
- package/README.md +1 -0
- package/package.json +29 -0
- package/shared/Controls.svelte +104 -0
- package/shared/ImageEditor.svelte +375 -0
- package/shared/InteractiveImageEditor.svelte +211 -0
- package/shared/layers/Layers.svelte +178 -0
- package/shared/layers/utils.ts +223 -0
- package/shared/tools/Brush.svelte +301 -0
- package/shared/tools/BrushColor.svelte +139 -0
- package/shared/tools/BrushSize.svelte +75 -0
- package/shared/tools/ColorField.svelte +172 -0
- package/shared/tools/ColorPicker.svelte +205 -0
- package/shared/tools/ColorSwatch.svelte +170 -0
- package/shared/tools/Crop.svelte +193 -0
- package/shared/tools/Cropper.svelte +344 -0
- package/shared/tools/Handle.svelte +289 -0
- package/shared/tools/Sources.svelte +215 -0
- package/shared/tools/Tools.svelte +176 -0
- package/shared/tools/brush.ts +247 -0
- package/shared/tools/crop.ts +210 -0
- package/shared/tools/index.ts +5 -0
- package/shared/tools/sources.ts +114 -0
- package/shared/tools/types.ts +1 -0
- package/shared/utils/commands.ts +162 -0
- package/shared/utils/events.ts +28 -0
- package/shared/utils/pixi.ts +296 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gradio/imageeditor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Gradio UI packages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "",
|
|
7
|
+
"license": "ISC",
|
|
8
|
+
"private": false,
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@types/tinycolor2": "^1.4.6",
|
|
11
|
+
"pixi.js": "^7.3.2",
|
|
12
|
+
"tinycolor2": "^1.6.0",
|
|
13
|
+
"@gradio/atoms": "^0.3.0",
|
|
14
|
+
"@gradio/icons": "^0.3.0",
|
|
15
|
+
"@gradio/client": "^0.8.1",
|
|
16
|
+
"@gradio/image": "^0.4.0",
|
|
17
|
+
"@gradio/upload": "^0.5.0",
|
|
18
|
+
"@gradio/statustracker": "^0.4.0",
|
|
19
|
+
"@gradio/wasm": "^0.3.0",
|
|
20
|
+
"@gradio/utils": "^0.2.0"
|
|
21
|
+
},
|
|
22
|
+
"main_changeset": true,
|
|
23
|
+
"main": "./Index.svelte",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./Index.svelte",
|
|
26
|
+
"./example": "./Example.svelte",
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from "svelte";
|
|
3
|
+
import { IconButton } from "@gradio/atoms";
|
|
4
|
+
import { Clear, Undo, Redo, Check } from "@gradio/icons";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Can the current image be undone?
|
|
8
|
+
*/
|
|
9
|
+
export let can_undo = false;
|
|
10
|
+
/**
|
|
11
|
+
* Can the current image be redone?
|
|
12
|
+
*/
|
|
13
|
+
export let can_redo = false;
|
|
14
|
+
|
|
15
|
+
export let can_save = false;
|
|
16
|
+
export let changeable = false;
|
|
17
|
+
|
|
18
|
+
const dispatch = createEventDispatcher<{
|
|
19
|
+
/**
|
|
20
|
+
* Remove the current image.
|
|
21
|
+
*/
|
|
22
|
+
remove_image: void;
|
|
23
|
+
/**
|
|
24
|
+
* Undo the last action.
|
|
25
|
+
*/
|
|
26
|
+
undo: void;
|
|
27
|
+
/**
|
|
28
|
+
* Redo the last action.
|
|
29
|
+
*/
|
|
30
|
+
redo: void;
|
|
31
|
+
/**
|
|
32
|
+
* Save the current image.
|
|
33
|
+
*/
|
|
34
|
+
save: void;
|
|
35
|
+
}>();
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div class="controls-wrap">
|
|
39
|
+
<div class="row-wrap">
|
|
40
|
+
<IconButton
|
|
41
|
+
disabled={!can_undo}
|
|
42
|
+
Icon={Undo}
|
|
43
|
+
label="Undo"
|
|
44
|
+
on:click={(event) => {
|
|
45
|
+
dispatch("undo");
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
<IconButton
|
|
50
|
+
disabled={!can_redo}
|
|
51
|
+
Icon={Redo}
|
|
52
|
+
label="Redo"
|
|
53
|
+
on:click={(event) => {
|
|
54
|
+
dispatch("redo");
|
|
55
|
+
event.stopPropagation();
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
<IconButton
|
|
59
|
+
Icon={Clear}
|
|
60
|
+
label="Clear canvas"
|
|
61
|
+
on:click={(event) => {
|
|
62
|
+
dispatch("remove_image");
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
{#if changeable}
|
|
68
|
+
<div class="row-wrap save">
|
|
69
|
+
<IconButton
|
|
70
|
+
disabled={!can_save}
|
|
71
|
+
Icon={Check}
|
|
72
|
+
label="Save changes"
|
|
73
|
+
on:click={(event) => {
|
|
74
|
+
dispatch("save");
|
|
75
|
+
event.stopPropagation();
|
|
76
|
+
}}
|
|
77
|
+
background={"var(--color-green-500)"}
|
|
78
|
+
color={"#fff"}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<style>
|
|
85
|
+
.controls-wrap {
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
gap: var(--spacing-lg);
|
|
89
|
+
position: absolute;
|
|
90
|
+
top: var(--size-2);
|
|
91
|
+
right: var(--size-2);
|
|
92
|
+
}
|
|
93
|
+
.row-wrap {
|
|
94
|
+
display: flex;
|
|
95
|
+
|
|
96
|
+
justify-content: flex-end;
|
|
97
|
+
gap: var(--spacing-sm);
|
|
98
|
+
z-index: var(--layer-5);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.save :global(svg) {
|
|
102
|
+
transform: translateY(1px);
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
<script lang="ts" context="module">
|
|
2
|
+
import type { Writable, Readable } from "svelte/store";
|
|
3
|
+
import type { Spring } from "svelte/motion";
|
|
4
|
+
import { type PixiApp } from "./utils/pixi";
|
|
5
|
+
import { type CommandManager } from "./utils/commands";
|
|
6
|
+
|
|
7
|
+
export const EDITOR_KEY = Symbol("editor");
|
|
8
|
+
export type context_type = "bg" | "layers" | "crop" | "draw" | "erase";
|
|
9
|
+
type PartialRecord<K extends keyof any, T> = Partial<Record<K, T>>;
|
|
10
|
+
import { type tool } from "./tools";
|
|
11
|
+
|
|
12
|
+
export interface EditorContext {
|
|
13
|
+
pixi: Writable<PixiApp | null>;
|
|
14
|
+
current_layer: Writable<LayerScene | null>;
|
|
15
|
+
dimensions: Writable<[number, number]>;
|
|
16
|
+
editor_box: Writable<{
|
|
17
|
+
parent_width: number;
|
|
18
|
+
parent_height: number;
|
|
19
|
+
parent_left: number;
|
|
20
|
+
parent_top: number;
|
|
21
|
+
parent_right: number;
|
|
22
|
+
parent_bottom: number;
|
|
23
|
+
child_width: number;
|
|
24
|
+
child_height: number;
|
|
25
|
+
child_left: number;
|
|
26
|
+
child_top: number;
|
|
27
|
+
child_right: number;
|
|
28
|
+
child_bottom: number;
|
|
29
|
+
}>;
|
|
30
|
+
active_tool: Writable<tool>;
|
|
31
|
+
crop: Writable<[number, number, number, number]>;
|
|
32
|
+
position_spring: Spring<{
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
}>;
|
|
36
|
+
command_manager: CommandManager;
|
|
37
|
+
current_history: CommandManager["current_history"];
|
|
38
|
+
register_context: (
|
|
39
|
+
type: context_type,
|
|
40
|
+
{
|
|
41
|
+
reset_fn,
|
|
42
|
+
init_fn
|
|
43
|
+
}: {
|
|
44
|
+
reset_fn?: () => void;
|
|
45
|
+
init_fn?: (dimensions?: [number, number]) => void;
|
|
46
|
+
}
|
|
47
|
+
) => void;
|
|
48
|
+
reset: (clear_image: boolean, dimensions: [number, number]) => void;
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<script lang="ts">
|
|
53
|
+
import { onMount, setContext, createEventDispatcher, tick } from "svelte";
|
|
54
|
+
import { writable } from "svelte/store";
|
|
55
|
+
import { spring } from "svelte/motion";
|
|
56
|
+
import { Rectangle } from "pixi.js";
|
|
57
|
+
|
|
58
|
+
import { command_manager } from "./utils/commands";
|
|
59
|
+
|
|
60
|
+
import { type LayerScene } from "./layers/utils";
|
|
61
|
+
import { create_pixi_app, type ImageBlobs } from "./utils/pixi";
|
|
62
|
+
import Controls from "./Controls.svelte";
|
|
63
|
+
export let antialias = true;
|
|
64
|
+
export let crop_size: [number, number] = [800, 600];
|
|
65
|
+
export let changeable = false;
|
|
66
|
+
export let history: boolean;
|
|
67
|
+
export let bg = false;
|
|
68
|
+
export let sources: ("clipboard" | "webcam" | "upload")[];
|
|
69
|
+
const dispatch = createEventDispatcher<{
|
|
70
|
+
save: void;
|
|
71
|
+
}>();
|
|
72
|
+
export let crop_constraint = false;
|
|
73
|
+
|
|
74
|
+
let dimensions = writable(crop_size);
|
|
75
|
+
|
|
76
|
+
let editor_box: EditorContext["editor_box"] = writable({
|
|
77
|
+
parent_width: 0,
|
|
78
|
+
parent_height: 0,
|
|
79
|
+
parent_top: 0,
|
|
80
|
+
parent_left: 0,
|
|
81
|
+
parent_right: 0,
|
|
82
|
+
parent_bottom: 0,
|
|
83
|
+
child_width: 0,
|
|
84
|
+
child_height: 0,
|
|
85
|
+
child_top: 0,
|
|
86
|
+
child_left: 0,
|
|
87
|
+
child_right: 0,
|
|
88
|
+
child_bottom: 0
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const crop = writable<[number, number, number, number]>([0, 0, 1, 1]);
|
|
92
|
+
const position_spring = spring(
|
|
93
|
+
{ x: 0, y: 0 },
|
|
94
|
+
{
|
|
95
|
+
stiffness: 0.1,
|
|
96
|
+
damping: 0.5
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
const pixi = writable<PixiApp | null>(null);
|
|
100
|
+
|
|
101
|
+
const CommandManager = command_manager();
|
|
102
|
+
|
|
103
|
+
const { can_redo, can_undo, current_history } = CommandManager;
|
|
104
|
+
|
|
105
|
+
$: {
|
|
106
|
+
history = !!$current_history.previous || $active_tool !== "bg";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const active_tool: Writable<tool> = writable("bg");
|
|
110
|
+
const reset_context: Writable<PartialRecord<context_type, () => void>> =
|
|
111
|
+
writable({});
|
|
112
|
+
const init_context: Writable<
|
|
113
|
+
PartialRecord<context_type, (dimensions?: typeof $dimensions) => void>
|
|
114
|
+
> = writable({});
|
|
115
|
+
const contexts: Writable<context_type[]> = writable([]);
|
|
116
|
+
|
|
117
|
+
const sort_order = ["bg", "layers", "crop", "draw", "erase"] as const;
|
|
118
|
+
const editor_context = setContext<EditorContext>(EDITOR_KEY, {
|
|
119
|
+
pixi,
|
|
120
|
+
current_layer: writable(null),
|
|
121
|
+
dimensions,
|
|
122
|
+
editor_box,
|
|
123
|
+
active_tool,
|
|
124
|
+
crop,
|
|
125
|
+
position_spring,
|
|
126
|
+
command_manager: CommandManager,
|
|
127
|
+
current_history,
|
|
128
|
+
register_context: (
|
|
129
|
+
type: context_type,
|
|
130
|
+
{
|
|
131
|
+
reset_fn,
|
|
132
|
+
init_fn
|
|
133
|
+
}: {
|
|
134
|
+
reset_fn?: () => void;
|
|
135
|
+
init_fn?: (dimensions?: [number, number]) => void;
|
|
136
|
+
}
|
|
137
|
+
) => {
|
|
138
|
+
contexts.update((c) => [...c, type]);
|
|
139
|
+
init_context.update((c) => ({ ...c, [type]: init_fn }));
|
|
140
|
+
reset_context.update((c) => ({ ...c, [type]: reset_fn }));
|
|
141
|
+
},
|
|
142
|
+
reset: (clear_image: boolean, dimensions: [number, number]) => {
|
|
143
|
+
bg = false;
|
|
144
|
+
|
|
145
|
+
const _sorted_contexts = $contexts.sort((a, b) => {
|
|
146
|
+
return sort_order.indexOf(a) - sort_order.indexOf(b);
|
|
147
|
+
});
|
|
148
|
+
for (const k of _sorted_contexts) {
|
|
149
|
+
if (k in $reset_context && typeof $reset_context[k] === "function") {
|
|
150
|
+
$reset_context[k]?.();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const k of _sorted_contexts) {
|
|
155
|
+
if (k in $init_context && typeof $init_context[k] === "function") {
|
|
156
|
+
if (k === "bg" && !clear_image) {
|
|
157
|
+
continue;
|
|
158
|
+
} else {
|
|
159
|
+
$init_context[k]?.(dimensions);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
CommandManager.reset();
|
|
163
|
+
$pixi?.resize?.(...dimensions);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
let pixi_target: HTMLDivElement;
|
|
169
|
+
|
|
170
|
+
let canvas_wrap: HTMLDivElement;
|
|
171
|
+
|
|
172
|
+
function get_dimensions(parent: HTMLDivElement, child: HTMLDivElement): void {
|
|
173
|
+
if (!parent || !child) return;
|
|
174
|
+
const {
|
|
175
|
+
width: parent_width,
|
|
176
|
+
height: parent_height,
|
|
177
|
+
top: parent_top,
|
|
178
|
+
left: parent_left,
|
|
179
|
+
right: parent_right,
|
|
180
|
+
bottom: parent_bottom
|
|
181
|
+
} = canvas_wrap.getBoundingClientRect();
|
|
182
|
+
const {
|
|
183
|
+
width: child_width,
|
|
184
|
+
height: child_height,
|
|
185
|
+
top: child_top,
|
|
186
|
+
left: child_left,
|
|
187
|
+
right: child_right,
|
|
188
|
+
bottom: child_bottom
|
|
189
|
+
} = child.getBoundingClientRect();
|
|
190
|
+
editor_box.set({
|
|
191
|
+
child_width,
|
|
192
|
+
child_height,
|
|
193
|
+
child_left,
|
|
194
|
+
child_right,
|
|
195
|
+
child_top,
|
|
196
|
+
child_bottom,
|
|
197
|
+
|
|
198
|
+
parent_width,
|
|
199
|
+
parent_height,
|
|
200
|
+
parent_left,
|
|
201
|
+
parent_right,
|
|
202
|
+
parent_top,
|
|
203
|
+
parent_bottom
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
$: if (crop_constraint && bg && !history) {
|
|
208
|
+
set_crop();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function set_crop(): void {
|
|
212
|
+
requestAnimationFrame(() => {
|
|
213
|
+
tick().then((v) => ($active_tool = "crop"));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function reposition_canvas(): void {
|
|
218
|
+
if (!$editor_box) return;
|
|
219
|
+
const [l, t, w, h] = $crop;
|
|
220
|
+
|
|
221
|
+
const cx = l * $editor_box.child_width;
|
|
222
|
+
const cy = t * $editor_box.child_height;
|
|
223
|
+
const cw = w * $editor_box.child_width;
|
|
224
|
+
const ch = h * $editor_box.child_height;
|
|
225
|
+
|
|
226
|
+
const x = 0.5 * $editor_box.child_width - cx - cw / 2;
|
|
227
|
+
const y = 0.5 * $editor_box.child_height - cy - ch / 2;
|
|
228
|
+
|
|
229
|
+
position_spring.set({ x, y });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function get_blobs(): Promise<ImageBlobs> {
|
|
233
|
+
if (!$pixi || !$pixi.get_layers)
|
|
234
|
+
return { background: null, layers: [], composite: null };
|
|
235
|
+
const [l, t, w, h] = $crop;
|
|
236
|
+
|
|
237
|
+
return $pixi?.get_blobs(
|
|
238
|
+
$pixi.get_layers(),
|
|
239
|
+
new Rectangle(
|
|
240
|
+
l * $dimensions[0],
|
|
241
|
+
t * $dimensions[1],
|
|
242
|
+
w * $dimensions[0],
|
|
243
|
+
h * $dimensions[1]
|
|
244
|
+
),
|
|
245
|
+
$dimensions
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
$: $crop && reposition_canvas();
|
|
250
|
+
$: $position_spring && get_dimensions(canvas_wrap, pixi_target);
|
|
251
|
+
|
|
252
|
+
export function handle_remove(): void {
|
|
253
|
+
editor_context.reset(true, $dimensions);
|
|
254
|
+
if (!sources.length) {
|
|
255
|
+
set_tool("draw");
|
|
256
|
+
} else {
|
|
257
|
+
set_tool("bg");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
onMount(() => {
|
|
262
|
+
const app = create_pixi_app(pixi_target, ...crop_size, antialias);
|
|
263
|
+
|
|
264
|
+
function resize(width: number, height: number): void {
|
|
265
|
+
app.resize(width, height);
|
|
266
|
+
dimensions.set([width, height]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
pixi.set({ ...app, resize });
|
|
270
|
+
|
|
271
|
+
const resizer = new ResizeObserver((entries) => {
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
get_dimensions(canvas_wrap, pixi_target);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
resizer.observe(canvas_wrap);
|
|
278
|
+
resizer.observe(pixi_target);
|
|
279
|
+
|
|
280
|
+
for (const k of $contexts) {
|
|
281
|
+
if (k in $init_context && typeof $init_context[k] === "function") {
|
|
282
|
+
$init_context[k]?.($dimensions);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
resize(...$dimensions);
|
|
287
|
+
|
|
288
|
+
return () => {
|
|
289
|
+
$pixi?.destroy();
|
|
290
|
+
resizer.disconnect();
|
|
291
|
+
for (const k of $contexts) {
|
|
292
|
+
if (k in $reset_context) {
|
|
293
|
+
$reset_context[k]?.();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
let saved_history: null | typeof $current_history = $current_history;
|
|
300
|
+
|
|
301
|
+
function handle_save(): void {
|
|
302
|
+
saved_history = $current_history;
|
|
303
|
+
dispatch("save");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function set_tool(tool: tool): void {
|
|
307
|
+
$active_tool = tool;
|
|
308
|
+
}
|
|
309
|
+
</script>
|
|
310
|
+
|
|
311
|
+
<svelte:window on:scroll={() => get_dimensions(canvas_wrap, pixi_target)} />
|
|
312
|
+
|
|
313
|
+
<div data-testid="image" class="image-container">
|
|
314
|
+
<Controls
|
|
315
|
+
can_undo={$can_undo}
|
|
316
|
+
can_redo={$can_redo}
|
|
317
|
+
can_save={saved_history !== $current_history}
|
|
318
|
+
{changeable}
|
|
319
|
+
on:undo={CommandManager.undo}
|
|
320
|
+
on:redo={CommandManager.redo}
|
|
321
|
+
on:remove_image={handle_remove}
|
|
322
|
+
on:save={handle_save}
|
|
323
|
+
/>
|
|
324
|
+
<div class="wrap" bind:this={canvas_wrap}>
|
|
325
|
+
<div
|
|
326
|
+
bind:this={pixi_target}
|
|
327
|
+
class="stage-wrap"
|
|
328
|
+
class:bg={!bg}
|
|
329
|
+
style:transform="translate({$position_spring.x}px, {$position_spring.y}px)"
|
|
330
|
+
></div>
|
|
331
|
+
</div>
|
|
332
|
+
<slot />
|
|
333
|
+
<div
|
|
334
|
+
class="border"
|
|
335
|
+
style:width="{$crop[2] * $editor_box.child_width}px"
|
|
336
|
+
style:height="{$crop[3] * $editor_box.child_height}px"
|
|
337
|
+
style:top="{$crop[1] * $editor_box.child_height +
|
|
338
|
+
($editor_box.child_top - $editor_box.parent_top)}px"
|
|
339
|
+
style:left="{$crop[0] * $editor_box.child_width +
|
|
340
|
+
($editor_box.child_left - $editor_box.parent_left)}px"
|
|
341
|
+
></div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<style>
|
|
345
|
+
.wrap {
|
|
346
|
+
width: 100%;
|
|
347
|
+
height: 100%;
|
|
348
|
+
position: relative;
|
|
349
|
+
display: flex;
|
|
350
|
+
justify-content: center;
|
|
351
|
+
align-items: center;
|
|
352
|
+
}
|
|
353
|
+
.border {
|
|
354
|
+
position: absolute;
|
|
355
|
+
border: var(--block-border-color) 1px solid;
|
|
356
|
+
pointer-events: none;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.stage-wrap {
|
|
360
|
+
margin: var(--size-8);
|
|
361
|
+
margin-bottom: var(--size-1);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.bg {
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.image-container {
|
|
368
|
+
display: flex;
|
|
369
|
+
height: 100%;
|
|
370
|
+
flex-direction: column;
|
|
371
|
+
justify-content: center;
|
|
372
|
+
align-items: center;
|
|
373
|
+
max-height: 100%;
|
|
374
|
+
}
|
|
375
|
+
</style>
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<script lang="ts" context="module">
|
|
2
|
+
export interface EditorData {
|
|
3
|
+
background: FileData | null;
|
|
4
|
+
layers: FileData[] | null;
|
|
5
|
+
composite: FileData | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ImageBlobs {
|
|
9
|
+
background: FileData | null;
|
|
10
|
+
layers: FileData[];
|
|
11
|
+
composite: FileData | null;
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { type I18nFormatter } from "@gradio/utils";
|
|
17
|
+
import {
|
|
18
|
+
prepare_files,
|
|
19
|
+
upload,
|
|
20
|
+
normalise_file,
|
|
21
|
+
type FileData
|
|
22
|
+
} from "@gradio/client";
|
|
23
|
+
|
|
24
|
+
import ImageEditor from "./ImageEditor.svelte";
|
|
25
|
+
import Layers from "./layers/Layers.svelte";
|
|
26
|
+
import { type Brush as IBrush } from "./tools/Brush.svelte";
|
|
27
|
+
import { type Eraser } from "./tools/Brush.svelte";
|
|
28
|
+
|
|
29
|
+
export let brush: IBrush | null;
|
|
30
|
+
export let eraser: Eraser | null;
|
|
31
|
+
import { Tools, Crop, Brush, Sources } from "./tools";
|
|
32
|
+
|
|
33
|
+
export let sources: ("clipboard" | "webcam" | "upload")[];
|
|
34
|
+
export let crop_size: [number, number] | `${string}:${string}` | null = null;
|
|
35
|
+
export let i18n: I18nFormatter;
|
|
36
|
+
export let root: string;
|
|
37
|
+
export let proxy_url: string;
|
|
38
|
+
export let changeable = false;
|
|
39
|
+
export let value: EditorData | null = {
|
|
40
|
+
background: null,
|
|
41
|
+
layers: [],
|
|
42
|
+
composite: null
|
|
43
|
+
};
|
|
44
|
+
export let transforms: "crop"[] = ["crop"];
|
|
45
|
+
|
|
46
|
+
let editor: ImageEditor;
|
|
47
|
+
|
|
48
|
+
function is_not_null(o: Blob | null): o is Blob {
|
|
49
|
+
return !!o;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function is_file_data(o: null | FileData): o is FileData {
|
|
53
|
+
return !!o;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function get_data(): Promise<ImageBlobs> {
|
|
57
|
+
const blobs = await editor.get_blobs();
|
|
58
|
+
|
|
59
|
+
const bg = blobs.background
|
|
60
|
+
? upload(
|
|
61
|
+
await prepare_files([new File([blobs.background], "background.png")]),
|
|
62
|
+
root
|
|
63
|
+
)
|
|
64
|
+
: Promise.resolve(null);
|
|
65
|
+
|
|
66
|
+
const layers = blobs.layers
|
|
67
|
+
.filter(is_not_null)
|
|
68
|
+
.map(async (blob, i) =>
|
|
69
|
+
upload(await prepare_files([new File([blob], `layer_${i}.png`)]), root)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const composite = blobs.composite
|
|
73
|
+
? upload(
|
|
74
|
+
await prepare_files([new File([blobs.composite], "composite.png")]),
|
|
75
|
+
root
|
|
76
|
+
)
|
|
77
|
+
: Promise.resolve(null);
|
|
78
|
+
|
|
79
|
+
const [background, composite_, ...layers_] = await Promise.all([
|
|
80
|
+
bg,
|
|
81
|
+
composite,
|
|
82
|
+
...layers
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
background: Array.isArray(background) ? background[0] : background,
|
|
87
|
+
layers: layers_
|
|
88
|
+
.flatMap((layer) => (Array.isArray(layer) ? layer : [layer]))
|
|
89
|
+
.filter(is_file_data),
|
|
90
|
+
composite: Array.isArray(composite_) ? composite_[0] : composite_
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handle_value(value: EditorData | null): void {
|
|
95
|
+
if (!editor) return;
|
|
96
|
+
if (value == null) {
|
|
97
|
+
editor.handle_remove();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
$: handle_value(value);
|
|
102
|
+
|
|
103
|
+
$: crop_constraint = crop_size;
|
|
104
|
+
let bg = false;
|
|
105
|
+
let history = false;
|
|
106
|
+
|
|
107
|
+
$: editor &&
|
|
108
|
+
editor.set_tool &&
|
|
109
|
+
(sources && sources.length
|
|
110
|
+
? editor.set_tool("bg")
|
|
111
|
+
: editor.set_tool("draw"));
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<ImageEditor
|
|
115
|
+
bind:this={editor}
|
|
116
|
+
{changeable}
|
|
117
|
+
on:save
|
|
118
|
+
bind:history
|
|
119
|
+
bind:bg
|
|
120
|
+
{sources}
|
|
121
|
+
crop_constraint={!!crop_constraint}
|
|
122
|
+
>
|
|
123
|
+
<Tools {i18n}>
|
|
124
|
+
{#if sources && sources.length}
|
|
125
|
+
<Sources
|
|
126
|
+
{i18n}
|
|
127
|
+
{root}
|
|
128
|
+
{sources}
|
|
129
|
+
bind:bg
|
|
130
|
+
background_file={normalise_file(
|
|
131
|
+
value?.background || null,
|
|
132
|
+
root,
|
|
133
|
+
proxy_url
|
|
134
|
+
)}
|
|
135
|
+
></Sources>
|
|
136
|
+
{/if}
|
|
137
|
+
{#if transforms.includes("crop")}
|
|
138
|
+
<Crop {crop_constraint} />
|
|
139
|
+
{/if}
|
|
140
|
+
{#if brush}
|
|
141
|
+
<Brush
|
|
142
|
+
color_mode={brush.color_mode}
|
|
143
|
+
default_color={brush.default_color}
|
|
144
|
+
default_size={brush.default_size}
|
|
145
|
+
colors={brush.colors}
|
|
146
|
+
mode="draw"
|
|
147
|
+
/>
|
|
148
|
+
{/if}
|
|
149
|
+
|
|
150
|
+
{#if brush && eraser}
|
|
151
|
+
<Brush default_size={eraser.default_size} mode="erase" />
|
|
152
|
+
{/if}
|
|
153
|
+
</Tools>
|
|
154
|
+
|
|
155
|
+
<Layers
|
|
156
|
+
layer_files={normalise_file(value?.layers || null, root, proxy_url)}
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
{#if !bg && !history}
|
|
160
|
+
<div class="empty wrap">
|
|
161
|
+
{#if sources && sources.length}
|
|
162
|
+
<div>Upload an image</div>
|
|
163
|
+
{/if}
|
|
164
|
+
|
|
165
|
+
{#if sources && sources.length && brush}
|
|
166
|
+
<div class="or">or</div>
|
|
167
|
+
{/if}
|
|
168
|
+
{#if brush}
|
|
169
|
+
<div>select the draw tool to start</div>
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
{/if}
|
|
173
|
+
</ImageEditor>
|
|
174
|
+
|
|
175
|
+
<style>
|
|
176
|
+
.empty {
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
align-items: center;
|
|
181
|
+
|
|
182
|
+
position: absolute;
|
|
183
|
+
height: 100%;
|
|
184
|
+
width: 100%;
|
|
185
|
+
left: 0;
|
|
186
|
+
right: 0;
|
|
187
|
+
margin: auto;
|
|
188
|
+
z-index: var(--layer-top);
|
|
189
|
+
text-align: center;
|
|
190
|
+
color: var(--body-text-color);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.wrap {
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
justify-content: center;
|
|
197
|
+
align-items: center;
|
|
198
|
+
min-height: var(--size-60);
|
|
199
|
+
color: var(--block-label-text-color);
|
|
200
|
+
line-height: var(--line-md);
|
|
201
|
+
height: 100%;
|
|
202
|
+
padding-top: var(--size-3);
|
|
203
|
+
font-size: var(--text-lg);
|
|
204
|
+
pointer-events: none;
|
|
205
|
+
transform: translateY(-30px);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.or {
|
|
209
|
+
color: var(--body-text-color-subdued);
|
|
210
|
+
}
|
|
211
|
+
</style>
|