@gradio/imageeditor 0.4.11 → 0.6.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 +67 -0
- package/ImageEditor.stories.svelte +6 -2
- package/Index.svelte +42 -13
- package/package.json +11 -8
- package/shared/Controls.svelte +2 -2
- package/shared/ImageEditor.svelte +35 -12
- package/shared/InteractiveImageEditor.svelte +97 -20
- package/shared/layers/Layers.svelte +113 -56
- package/shared/layers/utils.ts +44 -4
- package/shared/tools/Brush.svelte +42 -51
- package/shared/tools/{BrushColor.svelte → BrushOptions.svelte} +73 -32
- package/shared/tools/BrushSize.svelte +13 -20
- package/shared/tools/ColorSwatch.svelte +66 -34
- package/shared/tools/Crop.svelte +12 -17
- package/shared/tools/Cropper.svelte +2 -0
- package/shared/tools/Handle.svelte +1 -1
- package/shared/tools/Sources.svelte +45 -14
- package/shared/tools/Tools.svelte +42 -57
- package/shared/tools/crop.ts +9 -4
- package/shared/utils/events.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# @gradio/imageeditor
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Highlights
|
|
6
|
+
|
|
7
|
+
#### Setting File Upload Limits ([#7909](https://github.com/gradio-app/gradio/pull/7909) [`2afca65`](https://github.com/gradio-app/gradio/commit/2afca6541912b37dc84f447c7ad4af21607d7c72))
|
|
8
|
+
|
|
9
|
+
We have added a `max_file_size` size parameter to `launch()` that limits to size of files uploaded to the server. This limit applies to each individual file. This parameter can be specified as a string or an integer (corresponding to the size in bytes).
|
|
10
|
+
|
|
11
|
+
The following code snippet sets a max file size of 5 megabytes.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import gradio as gr
|
|
15
|
+
|
|
16
|
+
demo = gr.Interface(lambda x: x, "image", "image")
|
|
17
|
+
|
|
18
|
+
demo.launch(max_file_size="5mb")
|
|
19
|
+
# or
|
|
20
|
+
demo.launch(max_file_size=5 * gr.FileSize.MB)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
#### Error states can now be cleared
|
|
27
|
+
|
|
28
|
+
When a component encounters an error, the error state shown in the UI can now be cleared by clicking on the `x` icon in the top right of the component. This applies to all types of errors, whether it's raised in the UI or the server.
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
Thanks @freddyaboulton!
|
|
33
|
+
|
|
34
|
+
### Fixes
|
|
35
|
+
|
|
36
|
+
- [#8066](https://github.com/gradio-app/gradio/pull/8066) [`624f9b9`](https://github.com/gradio-app/gradio/commit/624f9b9477f74a581a6c14119234f9efdfcda398) - make gradio dev tools a local dependency rather than bundling. Thanks @pngwn!
|
|
37
|
+
|
|
38
|
+
### Dependency updates
|
|
39
|
+
|
|
40
|
+
- @gradio/atoms@0.7.1
|
|
41
|
+
- @gradio/client@0.17.0
|
|
42
|
+
- @gradio/image@0.10.0
|
|
43
|
+
- @gradio/statustracker@0.5.0
|
|
44
|
+
- @gradio/upload@0.9.0
|
|
45
|
+
- @gradio/utils@0.4.0
|
|
46
|
+
|
|
47
|
+
## 0.5.0
|
|
48
|
+
|
|
49
|
+
### Features
|
|
50
|
+
|
|
51
|
+
- [#8042](https://github.com/gradio-app/gradio/pull/8042) [`92139f3`](https://github.com/gradio-app/gradio/commit/92139f3d7d6b832b649ff1f6c10c87e6fb522cde) - refresh the `ImageEditor` UI. Thanks @pngwn!
|
|
52
|
+
- [#8059](https://github.com/gradio-app/gradio/pull/8059) [`074ce38`](https://github.com/gradio-app/gradio/commit/074ce3805ada6ba829c17da3af5f186d9f5f9865) - ensure the `ImageEditor` works correctly with layers and `change` events. Thanks @pngwn!
|
|
53
|
+
- [#7845](https://github.com/gradio-app/gradio/pull/7845) [`dbb7373`](https://github.com/gradio-app/gradio/commit/dbb7373dde69d4ed2741942b5a1898f8620cec24) - ensure `ImageEditor` events work as expected. Thanks @pngwn!
|
|
54
|
+
|
|
55
|
+
### Fixes
|
|
56
|
+
|
|
57
|
+
- [#8046](https://github.com/gradio-app/gradio/pull/8046) [`d6c289b`](https://github.com/gradio-app/gradio/commit/d6c289b34668f0011676737ee8edb4cb62b0620a) - round [x, y, w, h] before cropping to avoid unexpected interpolation on pixel values. Thanks @ernestchu!
|
|
58
|
+
- [#7959](https://github.com/gradio-app/gradio/pull/7959) [`2a5cb97`](https://github.com/gradio-app/gradio/commit/2a5cb978240960fd8b150a1eae6ae8328a5c0c59) - ensure `ImageEditor` always draws at the correct position. Thanks @hrrbay!
|
|
59
|
+
|
|
60
|
+
### Dependency updates
|
|
61
|
+
|
|
62
|
+
- @gradio/utils@0.3.2
|
|
63
|
+
- @gradio/statustracker@0.4.12
|
|
64
|
+
- @gradio/client@0.16.0
|
|
65
|
+
- @gradio/upload@0.8.5
|
|
66
|
+
- @gradio/atoms@0.7.0
|
|
67
|
+
- @gradio/icons@0.4.0
|
|
68
|
+
- @gradio/image@0.9.12
|
|
69
|
+
|
|
3
70
|
## 0.4.11
|
|
4
71
|
|
|
5
72
|
### Fixes
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
class="image-container"
|
|
27
27
|
style="width: 500px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
|
|
28
28
|
>
|
|
29
|
-
<ImageEditor
|
|
29
|
+
<ImageEditor
|
|
30
|
+
i18n={get(format)}
|
|
31
|
+
{...args}
|
|
32
|
+
server={{ accept_blobs: () => {} }}
|
|
33
|
+
/>
|
|
30
34
|
</div>
|
|
31
35
|
</Template>
|
|
32
36
|
|
|
@@ -118,7 +122,7 @@
|
|
|
118
122
|
coords: { clientX: 100, clientY: 100 }
|
|
119
123
|
});
|
|
120
124
|
|
|
121
|
-
await userEvent.click(canvas.getByLabelText("
|
|
125
|
+
await userEvent.click(canvas.getByLabelText("Draw button"));
|
|
122
126
|
|
|
123
127
|
var availableColors = document.querySelectorAll(
|
|
124
128
|
"button.color:not(.empty):not(.selected):not(.hidden)"
|
package/Index.svelte
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svelte:options accessors={true} />
|
|
1
|
+
<svelte:options accessors={true} immutable={true} />
|
|
2
2
|
|
|
3
3
|
<script lang="ts">
|
|
4
4
|
import type { Brush, Eraser } from "./shared/tools/Brush.svelte";
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { Block } from "@gradio/atoms";
|
|
14
14
|
import { StatusTracker } from "@gradio/statustracker";
|
|
15
15
|
import type { LoadingStatus } from "@gradio/statustracker";
|
|
16
|
+
import { tick } from "svelte";
|
|
16
17
|
|
|
17
18
|
export let elem_id = "";
|
|
18
19
|
export let elem_classes: string[] = [];
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
export let show_label: boolean;
|
|
27
28
|
export let show_download_button: boolean;
|
|
28
29
|
export let root: string;
|
|
30
|
+
export let value_is_output = false;
|
|
29
31
|
|
|
30
32
|
export let height: number | undefined;
|
|
31
33
|
export let width: number | undefined;
|
|
@@ -47,30 +49,37 @@
|
|
|
47
49
|
export let eraser: Eraser;
|
|
48
50
|
export let crop_size: [number, number] | `${string}:${string}` | null = null;
|
|
49
51
|
export let transforms: "crop"[] = ["crop"];
|
|
50
|
-
|
|
52
|
+
export let layers = true;
|
|
51
53
|
export let attached_events: string[] = [];
|
|
54
|
+
export let server: {
|
|
55
|
+
accept_blobs: (a: any) => void;
|
|
56
|
+
};
|
|
52
57
|
|
|
53
58
|
export let gradio: Gradio<{
|
|
54
59
|
change: never;
|
|
55
60
|
error: string;
|
|
56
61
|
input: never;
|
|
57
62
|
edit: never;
|
|
58
|
-
stream: never;
|
|
59
63
|
drag: never;
|
|
64
|
+
apply: never;
|
|
60
65
|
upload: never;
|
|
61
66
|
clear: never;
|
|
62
67
|
select: SelectData;
|
|
63
68
|
share: ShareData;
|
|
69
|
+
clear_status: LoadingStatus;
|
|
64
70
|
}>;
|
|
65
71
|
|
|
66
72
|
let editor_instance: InteractiveImageEditor;
|
|
73
|
+
let image_id: null | string = null;
|
|
74
|
+
|
|
75
|
+
export async function get_value(): Promise<ImageBlobs | { id: string }> {
|
|
76
|
+
if (image_id) {
|
|
77
|
+
const val = { id: image_id };
|
|
78
|
+
image_id = null;
|
|
79
|
+
return val;
|
|
80
|
+
}
|
|
67
81
|
|
|
68
|
-
export async function get_value(): Promise<ImageBlobs> {
|
|
69
|
-
// @ts-ignore
|
|
70
|
-
loading_status = { status: "pending" };
|
|
71
82
|
const blobs = await editor_instance.get_data();
|
|
72
|
-
// @ts-ignore
|
|
73
|
-
loading_status = { status: "complete" };
|
|
74
83
|
|
|
75
84
|
return blobs;
|
|
76
85
|
}
|
|
@@ -79,7 +88,15 @@
|
|
|
79
88
|
|
|
80
89
|
$: value && handle_change();
|
|
81
90
|
|
|
82
|
-
function
|
|
91
|
+
function wait_for_next_frame(): Promise<void> {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function handle_change(): Promise<void> {
|
|
98
|
+
await wait_for_next_frame();
|
|
99
|
+
|
|
83
100
|
if (
|
|
84
101
|
value &&
|
|
85
102
|
(value.background || value.layers?.length || value.composite)
|
|
@@ -89,8 +106,15 @@
|
|
|
89
106
|
}
|
|
90
107
|
|
|
91
108
|
function handle_save(): void {
|
|
109
|
+
gradio.dispatch("apply");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handle_history_change(): void {
|
|
92
113
|
gradio.dispatch("change");
|
|
93
|
-
|
|
114
|
+
if (!value_is_output) {
|
|
115
|
+
gradio.dispatch("input");
|
|
116
|
+
tick().then((_) => (value_is_output = false));
|
|
117
|
+
}
|
|
94
118
|
}
|
|
95
119
|
</script>
|
|
96
120
|
|
|
@@ -113,6 +137,7 @@
|
|
|
113
137
|
autoscroll={gradio.autoscroll}
|
|
114
138
|
i18n={gradio.i18n}
|
|
115
139
|
{...loading_status}
|
|
140
|
+
on:clear_status={() => gradio.dispatch("clear_status", loading_status)}
|
|
116
141
|
/>
|
|
117
142
|
<StaticImage
|
|
118
143
|
on:select={({ detail }) => gradio.dispatch("select", detail)}
|
|
@@ -146,9 +171,12 @@
|
|
|
146
171
|
autoscroll={gradio.autoscroll}
|
|
147
172
|
i18n={gradio.i18n}
|
|
148
173
|
{...loading_status}
|
|
174
|
+
on:clear_status={() => gradio.dispatch("clear_status", loading_status)}
|
|
149
175
|
/>
|
|
150
176
|
|
|
151
177
|
<InteractiveImageEditor
|
|
178
|
+
on:change={() => handle_history_change()}
|
|
179
|
+
bind:image_id
|
|
152
180
|
{crop_size}
|
|
153
181
|
{value}
|
|
154
182
|
bind:this={editor_instance}
|
|
@@ -159,10 +187,8 @@
|
|
|
159
187
|
on:save={(e) => handle_save()}
|
|
160
188
|
on:edit={() => gradio.dispatch("edit")}
|
|
161
189
|
on:clear={() => gradio.dispatch("clear")}
|
|
162
|
-
on:stream={() => gradio.dispatch("stream")}
|
|
163
190
|
on:drag={({ detail }) => (dragging = detail)}
|
|
164
191
|
on:upload={() => gradio.dispatch("upload")}
|
|
165
|
-
on:select={({ detail }) => gradio.dispatch("select", detail)}
|
|
166
192
|
on:share={({ detail }) => gradio.dispatch("share", detail)}
|
|
167
193
|
on:error={({ detail }) => {
|
|
168
194
|
loading_status = loading_status || {};
|
|
@@ -172,9 +198,12 @@
|
|
|
172
198
|
on:error
|
|
173
199
|
{brush}
|
|
174
200
|
{eraser}
|
|
175
|
-
changeable={attached_events.includes("
|
|
201
|
+
changeable={attached_events.includes("apply")}
|
|
176
202
|
i18n={gradio.i18n}
|
|
177
203
|
{transforms}
|
|
204
|
+
accept_blobs={server.accept_blobs}
|
|
205
|
+
{layers}
|
|
206
|
+
status={loading_status?.status}
|
|
178
207
|
></InteractiveImageEditor>
|
|
179
208
|
</Block>
|
|
180
209
|
{/if}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradio/imageeditor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Gradio UI packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
@@ -10,14 +10,17 @@
|
|
|
10
10
|
"@types/tinycolor2": "^1.4.6",
|
|
11
11
|
"pixi.js": "^7.3.2",
|
|
12
12
|
"tinycolor2": "^1.6.0",
|
|
13
|
-
"@gradio/atoms": "^0.
|
|
14
|
-
"@gradio/
|
|
15
|
-
"@gradio/
|
|
16
|
-
"@gradio/statustracker": "^0.
|
|
17
|
-
"@gradio/
|
|
18
|
-
"@gradio/
|
|
13
|
+
"@gradio/atoms": "^0.7.1",
|
|
14
|
+
"@gradio/client": "^0.17.0",
|
|
15
|
+
"@gradio/icons": "^0.4.0",
|
|
16
|
+
"@gradio/statustracker": "^0.5.0",
|
|
17
|
+
"@gradio/image": "^0.10.0",
|
|
18
|
+
"@gradio/upload": "^0.9.0",
|
|
19
19
|
"@gradio/wasm": "^0.10.0",
|
|
20
|
-
"@gradio/
|
|
20
|
+
"@gradio/utils": "^0.4.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@gradio/preview": "^0.8.0"
|
|
21
24
|
},
|
|
22
25
|
"main_changeset": true,
|
|
23
26
|
"main": "./Index.svelte",
|
package/shared/Controls.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { createEventDispatcher } from "svelte";
|
|
3
3
|
import { IconButton } from "@gradio/atoms";
|
|
4
|
-
import { Clear, Undo, Redo, Check } from "@gradio/icons";
|
|
4
|
+
import { Clear, Undo, Redo, Check, Trash } from "@gradio/icons";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Can the current image be undone?
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
}}
|
|
57
57
|
/>
|
|
58
58
|
<IconButton
|
|
59
|
-
Icon={
|
|
59
|
+
Icon={Trash}
|
|
60
60
|
label="Clear canvas"
|
|
61
61
|
on:click={(event) => {
|
|
62
62
|
dispatch("remove_image");
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
child_bottom: number;
|
|
29
29
|
}>;
|
|
30
30
|
active_tool: Writable<tool>;
|
|
31
|
+
toolbar_box: Writable<DOMRect>;
|
|
31
32
|
crop: Writable<[number, number, number, number]>;
|
|
32
33
|
position_spring: Spring<{
|
|
33
34
|
x: number;
|
|
@@ -69,10 +70,12 @@
|
|
|
69
70
|
const dispatch = createEventDispatcher<{
|
|
70
71
|
clear?: never;
|
|
71
72
|
save: void;
|
|
73
|
+
change: void;
|
|
72
74
|
}>();
|
|
73
75
|
export let crop_constraint = false;
|
|
74
76
|
|
|
75
77
|
let dimensions = writable(crop_size);
|
|
78
|
+
export let height = 0;
|
|
76
79
|
|
|
77
80
|
let editor_box: EditorContext["editor_box"] = writable({
|
|
78
81
|
parent_width: 0,
|
|
@@ -89,6 +92,8 @@
|
|
|
89
92
|
child_bottom: 0
|
|
90
93
|
});
|
|
91
94
|
|
|
95
|
+
$: height = $editor_box.child_height;
|
|
96
|
+
|
|
92
97
|
const crop = writable<[number, number, number, number]>([0, 0, 1, 1]);
|
|
93
98
|
const position_spring = spring(
|
|
94
99
|
{ x: 0, y: 0 },
|
|
@@ -103,6 +108,8 @@
|
|
|
103
108
|
|
|
104
109
|
const { can_redo, can_undo, current_history } = CommandManager;
|
|
105
110
|
|
|
111
|
+
$: $current_history.previous, dispatch("change");
|
|
112
|
+
|
|
106
113
|
$: {
|
|
107
114
|
history = !!$current_history.previous || $active_tool !== "bg";
|
|
108
115
|
}
|
|
@@ -114,6 +121,7 @@
|
|
|
114
121
|
PartialRecord<context_type, (dimensions?: typeof $dimensions) => void>
|
|
115
122
|
> = writable({});
|
|
116
123
|
const contexts: Writable<context_type[]> = writable([]);
|
|
124
|
+
const toolbar_box: Writable<DOMRect> = writable(new DOMRect());
|
|
117
125
|
|
|
118
126
|
const sort_order = ["bg", "layers", "crop", "draw", "erase"] as const;
|
|
119
127
|
const editor_context = setContext<EditorContext>(EDITOR_KEY, {
|
|
@@ -121,6 +129,7 @@
|
|
|
121
129
|
current_layer: writable(null),
|
|
122
130
|
dimensions,
|
|
123
131
|
editor_box,
|
|
132
|
+
toolbar_box,
|
|
124
133
|
active_tool,
|
|
125
134
|
crop,
|
|
126
135
|
position_spring,
|
|
@@ -238,10 +247,10 @@
|
|
|
238
247
|
return $pixi?.get_blobs(
|
|
239
248
|
$pixi.get_layers(),
|
|
240
249
|
new Rectangle(
|
|
241
|
-
l * $dimensions[0],
|
|
242
|
-
t * $dimensions[1],
|
|
243
|
-
w * $dimensions[0],
|
|
244
|
-
h * $dimensions[1]
|
|
250
|
+
Math.round(l * $dimensions[0]),
|
|
251
|
+
Math.round(t * $dimensions[1]),
|
|
252
|
+
Math.round(w * $dimensions[0]),
|
|
253
|
+
Math.round(h * $dimensions[1])
|
|
245
254
|
),
|
|
246
255
|
$dimensions
|
|
247
256
|
);
|
|
@@ -331,39 +340,53 @@
|
|
|
331
340
|
style:transform="translate({$position_spring.x}px, {$position_spring.y}px)"
|
|
332
341
|
></div>
|
|
333
342
|
</div>
|
|
334
|
-
<
|
|
343
|
+
<div class="tools-wrap">
|
|
344
|
+
<slot />
|
|
345
|
+
</div>
|
|
335
346
|
<div
|
|
336
347
|
class="border"
|
|
337
|
-
style:width="{$crop[2] * $editor_box.child_width}px"
|
|
338
|
-
style:height="{$crop[3] * $editor_box.child_height}px"
|
|
348
|
+
style:width="{$crop[2] * $editor_box.child_width + 1}px"
|
|
349
|
+
style:height="{$crop[3] * $editor_box.child_height + 1}px"
|
|
339
350
|
style:top="{$crop[1] * $editor_box.child_height +
|
|
340
|
-
($editor_box.child_top - $editor_box.parent_top)
|
|
351
|
+
($editor_box.child_top - $editor_box.parent_top) -
|
|
352
|
+
0.5}px"
|
|
341
353
|
style:left="{$crop[0] * $editor_box.child_width +
|
|
342
|
-
($editor_box.child_left - $editor_box.parent_left)
|
|
354
|
+
($editor_box.child_left - $editor_box.parent_left) -
|
|
355
|
+
0.5}px"
|
|
343
356
|
></div>
|
|
344
357
|
</div>
|
|
345
358
|
|
|
346
359
|
<style>
|
|
347
360
|
.wrap {
|
|
361
|
+
display: flex;
|
|
348
362
|
width: 100%;
|
|
349
363
|
height: 100%;
|
|
350
364
|
position: relative;
|
|
351
|
-
display: flex;
|
|
352
365
|
justify-content: center;
|
|
353
|
-
align-items:
|
|
366
|
+
align-items: flex-start;
|
|
354
367
|
}
|
|
355
368
|
.border {
|
|
356
369
|
position: absolute;
|
|
357
370
|
border: var(--block-border-color) 1px solid;
|
|
358
371
|
pointer-events: none;
|
|
372
|
+
border-radius: var(--radius-md);
|
|
359
373
|
}
|
|
360
374
|
|
|
361
375
|
.stage-wrap {
|
|
362
376
|
margin: var(--size-8);
|
|
363
377
|
margin-bottom: var(--size-1);
|
|
378
|
+
border-radius: var(--radius-md);
|
|
379
|
+
overflow: hidden;
|
|
364
380
|
}
|
|
365
381
|
|
|
366
|
-
.
|
|
382
|
+
.tools-wrap {
|
|
383
|
+
display: flex;
|
|
384
|
+
justify-content: center;
|
|
385
|
+
align-items: center;
|
|
386
|
+
padding: 0 var(--spacing-xl) 0 0;
|
|
387
|
+
border: 1px solid var(--block-border-color);
|
|
388
|
+
border-radius: var(--radius-sm);
|
|
389
|
+
margin: var(--spacing-xxl) 0 var(--spacing-xxl) 0;
|
|
367
390
|
}
|
|
368
391
|
|
|
369
392
|
.image-container {
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
import { type Brush as IBrush } from "./tools/Brush.svelte";
|
|
23
23
|
import { type Eraser } from "./tools/Brush.svelte";
|
|
24
24
|
|
|
25
|
-
export let brush: IBrush | null;
|
|
26
|
-
export let eraser: Eraser | null;
|
|
27
25
|
import { Tools, Crop, Brush, Sources } from "./tools";
|
|
28
26
|
import { BlockLabel } from "@gradio/atoms";
|
|
29
27
|
import { Image as ImageIcon } from "@gradio/icons";
|
|
30
28
|
|
|
29
|
+
export let brush: IBrush | null;
|
|
30
|
+
export let eraser: Eraser | null;
|
|
31
31
|
export let sources: ("clipboard" | "webcam" | "upload")[];
|
|
32
32
|
export let crop_size: [number, number] | `${string}:${string}` | null = null;
|
|
33
33
|
export let i18n: I18nFormatter;
|
|
@@ -41,9 +41,14 @@
|
|
|
41
41
|
composite: null
|
|
42
42
|
};
|
|
43
43
|
export let transforms: "crop"[] = ["crop"];
|
|
44
|
+
export let layers: boolean;
|
|
45
|
+
export let accept_blobs: (a: any) => void;
|
|
46
|
+
export let status: "pending" | "complete" | "error" = "complete";
|
|
44
47
|
|
|
45
48
|
const dispatch = createEventDispatcher<{
|
|
46
49
|
clear?: never;
|
|
50
|
+
upload?: never;
|
|
51
|
+
change?: never;
|
|
47
52
|
}>();
|
|
48
53
|
|
|
49
54
|
let editor: ImageEditor;
|
|
@@ -56,6 +61,8 @@
|
|
|
56
61
|
return !!o;
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
$: if (bg) dispatch("upload");
|
|
65
|
+
|
|
59
66
|
export async function get_data(): Promise<ImageBlobs> {
|
|
60
67
|
const blobs = await editor.get_blobs();
|
|
61
68
|
|
|
@@ -107,11 +114,83 @@
|
|
|
107
114
|
let bg = false;
|
|
108
115
|
let history = false;
|
|
109
116
|
|
|
117
|
+
export let image_id: null | string = null;
|
|
118
|
+
|
|
110
119
|
$: editor &&
|
|
111
120
|
editor.set_tool &&
|
|
112
121
|
(sources && sources.length
|
|
113
122
|
? editor.set_tool("bg")
|
|
114
123
|
: editor.set_tool("draw"));
|
|
124
|
+
|
|
125
|
+
type BinaryImages = [string, string, File, number | null][];
|
|
126
|
+
|
|
127
|
+
function nextframe(): Promise<void> {
|
|
128
|
+
return new Promise((resolve) => setTimeout(() => resolve(), 30));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let uploading = false;
|
|
132
|
+
let pending = false;
|
|
133
|
+
async function handle_change(e: CustomEvent<Blob | any>): Promise<void> {
|
|
134
|
+
if (uploading) {
|
|
135
|
+
pending = true;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
uploading = true;
|
|
140
|
+
|
|
141
|
+
await nextframe();
|
|
142
|
+
const blobs = await editor.get_blobs();
|
|
143
|
+
|
|
144
|
+
const images: BinaryImages = [];
|
|
145
|
+
|
|
146
|
+
let id = Math.random().toString(36).substring(2);
|
|
147
|
+
|
|
148
|
+
if (blobs.background)
|
|
149
|
+
images.push([
|
|
150
|
+
id,
|
|
151
|
+
"background",
|
|
152
|
+
new File([blobs.background], "background.png"),
|
|
153
|
+
null
|
|
154
|
+
]);
|
|
155
|
+
if (blobs.composite)
|
|
156
|
+
images.push([
|
|
157
|
+
id,
|
|
158
|
+
"composite",
|
|
159
|
+
new File([blobs.composite], "composite.png"),
|
|
160
|
+
null
|
|
161
|
+
]);
|
|
162
|
+
blobs.layers.forEach((layer, i) => {
|
|
163
|
+
if (layer)
|
|
164
|
+
images.push([
|
|
165
|
+
id as string,
|
|
166
|
+
`layer`,
|
|
167
|
+
new File([layer], `layer_${i}.png`),
|
|
168
|
+
i
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await Promise.all(
|
|
173
|
+
images.map(async ([image_id, type, data, index]) => {
|
|
174
|
+
return accept_blobs({
|
|
175
|
+
binary: true,
|
|
176
|
+
data: { file: data, id: image_id, type, index }
|
|
177
|
+
});
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
image_id = id;
|
|
181
|
+
dispatch("change");
|
|
182
|
+
|
|
183
|
+
await nextframe();
|
|
184
|
+
uploading = false;
|
|
185
|
+
if (pending) {
|
|
186
|
+
pending = false;
|
|
187
|
+
uploading = false;
|
|
188
|
+
handle_change(e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let active_mode: "webcam" | "color" | null = null;
|
|
193
|
+
let editor_height = 0;
|
|
115
194
|
</script>
|
|
116
195
|
|
|
117
196
|
<BlockLabel
|
|
@@ -121,8 +200,10 @@
|
|
|
121
200
|
/>
|
|
122
201
|
<ImageEditor
|
|
123
202
|
bind:this={editor}
|
|
203
|
+
bind:height={editor_height}
|
|
124
204
|
{changeable}
|
|
125
205
|
on:save
|
|
206
|
+
on:change={handle_change}
|
|
126
207
|
on:clear={() => dispatch("clear")}
|
|
127
208
|
bind:history
|
|
128
209
|
bind:bg
|
|
@@ -130,15 +211,17 @@
|
|
|
130
211
|
crop_constraint={!!crop_constraint}
|
|
131
212
|
>
|
|
132
213
|
<Tools {i18n}>
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
214
|
+
<Layers layer_files={value?.layers || null} enable_layers={layers} />
|
|
215
|
+
|
|
216
|
+
<Sources
|
|
217
|
+
{i18n}
|
|
218
|
+
{root}
|
|
219
|
+
{sources}
|
|
220
|
+
bind:bg
|
|
221
|
+
bind:active_mode
|
|
222
|
+
background_file={value?.background || value?.composite || null}
|
|
223
|
+
></Sources>
|
|
224
|
+
|
|
142
225
|
{#if transforms.includes("crop")}
|
|
143
226
|
<Crop {crop_constraint} />
|
|
144
227
|
{/if}
|
|
@@ -157,10 +240,8 @@
|
|
|
157
240
|
{/if}
|
|
158
241
|
</Tools>
|
|
159
242
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{#if !bg && !history}
|
|
163
|
-
<div class="empty wrap">
|
|
243
|
+
{#if !bg && !history && active_mode !== "webcam" && status !== "error"}
|
|
244
|
+
<div class="empty wrap" style:height={`${editor_height}px`}>
|
|
164
245
|
{#if sources && sources.length}
|
|
165
246
|
<div>Upload an image</div>
|
|
166
247
|
{/if}
|
|
@@ -181,7 +262,6 @@
|
|
|
181
262
|
flex-direction: column;
|
|
182
263
|
justify-content: center;
|
|
183
264
|
align-items: center;
|
|
184
|
-
|
|
185
265
|
position: absolute;
|
|
186
266
|
height: 100%;
|
|
187
267
|
width: 100%;
|
|
@@ -191,6 +271,7 @@
|
|
|
191
271
|
z-index: var(--layer-top);
|
|
192
272
|
text-align: center;
|
|
193
273
|
color: var(--body-text-color);
|
|
274
|
+
top: var(--size-8);
|
|
194
275
|
}
|
|
195
276
|
|
|
196
277
|
.wrap {
|
|
@@ -198,14 +279,10 @@
|
|
|
198
279
|
flex-direction: column;
|
|
199
280
|
justify-content: center;
|
|
200
281
|
align-items: center;
|
|
201
|
-
min-height: var(--size-60);
|
|
202
282
|
color: var(--block-label-text-color);
|
|
203
283
|
line-height: var(--line-md);
|
|
204
|
-
height: 100%;
|
|
205
|
-
padding-top: var(--size-3);
|
|
206
284
|
font-size: var(--text-lg);
|
|
207
285
|
pointer-events: none;
|
|
208
|
-
transform: translateY(-30px);
|
|
209
286
|
}
|
|
210
287
|
|
|
211
288
|
.or {
|