@gradio/imageeditor 0.18.2-dev.0 → 0.18.2-dev.2

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/Index.svelte +122 -159
  3. package/InteractiveImageEditor.svelte +1 -0
  4. package/dist/Example.svelte +7 -4
  5. package/dist/Example.svelte.d.ts +20 -18
  6. package/dist/Index.svelte +152 -136
  7. package/dist/Index.svelte.d.ts +18 -202
  8. package/dist/InteractiveImageEditor.svelte +205 -148
  9. package/dist/InteractiveImageEditor.svelte.d.ts +55 -51
  10. package/dist/shared/Anchor.svelte +210 -136
  11. package/dist/shared/Anchor.svelte.d.ts +18 -16
  12. package/dist/shared/Controls.svelte +55 -36
  13. package/dist/shared/Controls.svelte.d.ts +35 -33
  14. package/dist/shared/IconButton.svelte +19 -17
  15. package/dist/shared/IconButton.svelte.d.ts +34 -32
  16. package/dist/shared/ImageEditor.svelte +673 -466
  17. package/dist/shared/ImageEditor.svelte.d.ts +4 -79
  18. package/dist/shared/Layers.svelte +52 -30
  19. package/dist/shared/Layers.svelte.d.ts +39 -36
  20. package/dist/shared/Resize.svelte +42 -21
  21. package/dist/shared/Resize.svelte.d.ts +29 -27
  22. package/dist/shared/SecondaryToolbar.svelte +17 -5
  23. package/dist/shared/SecondaryToolbar.svelte.d.ts +38 -36
  24. package/dist/shared/Toolbar.svelte +123 -69
  25. package/dist/shared/Toolbar.svelte.d.ts +40 -38
  26. package/dist/shared/brush/BrushOptions.svelte +119 -84
  27. package/dist/shared/brush/BrushOptions.svelte.d.ts +29 -27
  28. package/dist/shared/brush/BrushSize.svelte +12 -7
  29. package/dist/shared/brush/BrushSize.svelte.d.ts +23 -20
  30. package/dist/shared/brush/ColorField.svelte +49 -33
  31. package/dist/shared/brush/ColorField.svelte.d.ts +23 -21
  32. package/dist/shared/brush/ColorPicker.svelte +152 -108
  33. package/dist/shared/brush/ColorPicker.svelte.d.ts +19 -17
  34. package/dist/shared/brush/ColorSwatch.svelte +78 -46
  35. package/dist/shared/brush/ColorSwatch.svelte.d.ts +34 -32
  36. package/dist/types.d.ts +40 -0
  37. package/dist/types.js +25 -0
  38. package/package.json +12 -12
  39. package/shared/ImageEditor.svelte +1 -3
  40. package/shared/brush/BrushOptions.svelte +1 -1
  41. package/shared/brush/ColorPicker.svelte +2 -1
  42. package/types.ts +74 -0
@@ -1,471 +1,678 @@
1
- <script context="module">export const EDITOR_KEY = Symbol("editor");
1
+ <script lang="ts" context="module">
2
+ import type { Tool, Subtool } from "./Toolbar.svelte";
3
+
4
+ export const EDITOR_KEY = Symbol("editor");
5
+ export type context_type = "bg" | "layers" | "crop" | "draw" | "erase";
2
6
  </script>
3
7
 
4
- <script>import { onMount, createEventDispatcher, tick } from "svelte";
5
- import { get } from "svelte/store";
6
- import Toolbar, {} from "./Toolbar.svelte";
7
- import { CropTool } from "./crop/crop";
8
- import { ResizeTool } from "./resize/resize";
9
- import { Webcam } from "@gradio/image";
10
- import tinycolor, {} from "tinycolor2";
11
- import { ZoomTool } from "./zoom/zoom";
12
- import {} from "./core/commands";
13
- import { ImageEditor } from "./core/editor";
14
- import {} from "./brush/types";
15
- import { BrushTool } from "./brush/brush";
16
- import { create_drag } from "@gradio/upload";
17
- import SecondaryToolbar from "./SecondaryToolbar.svelte";
18
- import { Check } from "@gradio/icons";
19
- import {} from "./types";
20
- import Controls from "./Controls.svelte";
21
- import IconButton from "./IconButton.svelte";
22
- const { drag, open_file_upload } = create_drag();
23
- const dispatch = createEventDispatcher();
24
- export const antialias = true;
25
- export let changeable = false;
26
- export let sources = ["upload", "webcam", "clipboard"];
27
- export let transforms = ["crop", "resize"];
28
- export let canvas_size;
29
- export let is_dragging = false;
30
- export let background_image = false;
31
- export let brush_options;
32
- export let eraser_options;
33
- export let fixed_canvas = false;
34
- export let root;
35
- export let i18n;
36
- export let upload;
37
- export let composite;
38
- export let layers;
39
- export let background;
40
- export let border_region = 0;
41
- export let layer_options;
42
- export let current_tool;
43
- export let webcam_options;
44
- export let show_download_button = false;
45
- export let theme_mode;
46
- export let full_history = null;
47
- let pixi_target;
48
- let pixi_target_crop;
49
- $: if (layer_options) {
50
- if (check_if_should_init()) {
51
- editor.set_layer_options(layer_options);
52
- refresh_tools();
53
- }
54
- }
55
- function refresh_tools() {
56
- if (!editor || !ready) return;
57
- editor.set_tool(current_tool);
58
- editor.set_subtool(current_subtool);
59
- }
60
- $: if (editor && ready && editor.layers) {
61
- const current_layers = get(editor.layers);
62
- if (current_layers.layers.length > 0 && !current_layers.active_layer) {
63
- refresh_tools_for_layer_changes(current_layers);
64
- }
65
- }
66
- function refresh_tools_for_layer_changes(current_layers) {
67
- if (!editor || !ready) return;
68
- if (current_layers.layers.length > 0 && !current_layers.active_layer) {
69
- editor.set_layer(current_layers.layers[0].id);
70
- }
71
- if (current_tool) {
72
- editor.set_tool(current_tool);
73
- if (current_subtool) {
74
- editor.set_subtool(current_subtool);
75
- }
76
- }
77
- if (brush && (current_tool === "draw" || current_tool === "erase")) {
78
- brush.set_tool(current_tool, current_subtool);
79
- }
80
- }
81
- function check_if_should_init() {
82
- return layer_options && editor && ready;
83
- }
84
- export let has_drawn = false;
85
- export async function get_blobs() {
86
- if (!editor) return { background: null, layers: [], composite: null };
87
- if (!background_image && !has_drawn && !layers.length)
88
- return { background: null, layers: [], composite: null };
89
- const blobs = await editor.get_blobs();
90
- return blobs;
91
- }
92
- let editor;
93
- export function add_image(image) {
94
- editor.add_image({ image });
95
- }
96
- let pending_bg;
97
- export async function add_image_from_url(source) {
98
- if (!editor || !source || !check_if_should_init()) return;
99
- let url;
100
- if (typeof source === "string") {
101
- url = source;
102
- } else if (source?.meta?._type === "gradio.FileData" && source?.url) {
103
- url = source.url;
104
- } else {
105
- console.warn("Invalid source provided to add_image_from_url:", source);
106
- return;
107
- }
108
- try {
109
- pending_bg = editor.add_image_from_url(url);
110
- let pending_crop = crop.add_image_from_url(url);
111
- await Promise.all([pending_bg, pending_crop]);
112
- crop.set_tool("image");
113
- crop.set_subtool("crop");
114
- background_image = true;
115
- zoom.set_zoom("fit");
116
- dispatch("upload");
117
- dispatch("input");
118
- } catch (error) {
119
- console.error("Error adding image from URL:", error);
120
- }
121
- }
122
- export async function add_layers_from_url(source) {
123
- if (!editor || !source.length || !check_if_should_init()) return;
124
- if (Array.isArray(source) && source.every((item) => item?.meta?._type === "gradio.FileData")) {
125
- try {
126
- await pending_bg;
127
- await editor.add_layers_from_url(source.map((item) => item.url));
128
- dispatch("change");
129
- dispatch("input");
130
- } catch (error) {
131
- console.error("Error adding layer from URL:", error);
132
- }
133
- }
134
- }
135
- let brush;
136
- let zoom;
137
- let zoom_level = 1;
138
- let ready = false;
139
- let mounted = false;
140
- let min_zoom = true;
141
- let last_dimensions = { width: 0, height: 0 };
142
- async function handle_visibility_change() {
143
- if (!editor || !ready || !zoom) return;
144
- await tick();
145
- const is_visible = pixi_target.offsetParent !== null;
146
- if (is_visible) {
147
- const current_dimensions = pixi_target.getBoundingClientRect();
148
- if (current_dimensions.width !== last_dimensions.width || current_dimensions.height !== last_dimensions.height) {
149
- zoom.set_zoom("fit");
150
- last_dimensions = {
151
- width: current_dimensions.width,
152
- height: current_dimensions.height
153
- };
154
- }
155
- }
156
- }
157
- onMount(() => {
158
- let intersection_observer;
159
- let resize_observer;
160
- init_image_editor().then(() => {
161
- mounted = true;
162
- intersection_observer = new IntersectionObserver(() => {
163
- handle_visibility_change();
164
- });
165
- resize_observer = new ResizeObserver(() => {
166
- handle_visibility_change();
167
- });
168
- intersection_observer.observe(pixi_target);
169
- resize_observer.observe(pixi_target);
170
- setTimeout(() => {
171
- if (full_history && editor) {
172
- editor.command_manager.replay(full_history, editor.context).then(() => {
173
- refresh_tools_after_history();
174
- });
175
- }
176
- }, 0);
177
- });
178
- if (typeof window !== "undefined") {
179
- window.editor = editor;
180
- }
181
- return () => {
182
- if (intersection_observer) {
183
- intersection_observer.disconnect();
184
- }
185
- if (resize_observer) {
186
- resize_observer.disconnect();
187
- }
188
- if (editor) {
189
- editor.destroy();
190
- }
191
- };
192
- });
193
- function refresh_tools_after_history() {
194
- if (!editor || !ready) return;
195
- const current_layers = get(editor.layers);
196
- if (current_layers.layers.length > 0 && !current_layers.active_layer) {
197
- editor.set_layer(current_layers.layers[0].id);
198
- }
199
- if (current_tool) {
200
- editor.set_tool(current_tool);
201
- if (current_subtool) {
202
- editor.set_subtool(current_subtool);
203
- }
204
- }
205
- if (brush && (current_tool === "draw" || current_tool === "erase")) {
206
- brush.set_tool(current_tool, current_subtool);
207
- }
208
- full_history = editor.command_manager.history;
209
- }
210
- let crop;
211
- let crop_zoom;
212
- export let can_undo = false;
213
- let can_redo = false;
214
- async function init_image_editor() {
215
- brush = new BrushTool();
216
- zoom = new ZoomTool();
217
- editor = new ImageEditor({
218
- target_element: pixi_target,
219
- width: canvas_size[0],
220
- height: canvas_size[1],
221
- tools: ["image", zoom, new ResizeTool(), brush],
222
- fixed_canvas,
223
- border_region,
224
- layer_options,
225
- theme_mode
226
- });
227
- brush.on("change", () => {
228
- has_drawn = true;
229
- });
230
- crop_zoom = new ZoomTool();
231
- crop = new ImageEditor({
232
- target_element: pixi_target_crop,
233
- width: canvas_size[0],
234
- height: canvas_size[1],
235
- tools: ["image", crop_zoom, new CropTool()],
236
- dark: true,
237
- fixed_canvas: false,
238
- border_region: 0,
239
- pad_bottom: 40
240
- });
241
- editor.scale.subscribe((_scale) => {
242
- zoom_level = _scale;
243
- });
244
- editor.min_zoom.subscribe((is_min_zoom) => {
245
- min_zoom = is_min_zoom;
246
- });
247
- editor.dimensions.subscribe((dimensions) => {
248
- last_dimensions = { ...dimensions };
249
- });
250
- editor.command_manager.current_history.subscribe((history) => {
251
- can_undo = history.previous !== null;
252
- can_redo = history.next !== null;
253
- });
254
- await Promise.all([editor.ready, crop.ready]).then(() => {
255
- handle_tool_change({ tool: "image" });
256
- ready = true;
257
- if (sources.length > 0) {
258
- handle_tool_change({ tool: "image" });
259
- } else {
260
- handle_tool_change({ tool: "draw" });
261
- }
262
- crop.set_subtool("crop");
263
- });
264
- editor.on("change", () => {
265
- dispatch("change");
266
- full_history = editor.command_manager.history;
267
- });
268
- if (background || layers.length > 0) {
269
- if (background) {
270
- await add_image_from_url(background);
271
- }
272
- if (layers.length > 0) {
273
- await add_layers_from_url(layers);
274
- }
275
- handle_tool_change({ tool: "draw" });
276
- } else if (composite) {
277
- await add_image_from_url(composite);
278
- handle_tool_change({ tool: "draw" });
279
- }
280
- refresh_tools();
281
- }
282
- $: if (background == null && layers.length == 0 && composite == null && editor && ready) {
283
- editor.reset_canvas();
284
- zoom.set_zoom("fit");
285
- handle_tool_change({ tool: "image" });
286
- background_image = false;
287
- has_drawn = false;
288
- }
289
- $: current_tool === "image" && current_subtool === "crop" && crop_zoom.set_zoom("fit");
290
- async function handle_files(files) {
291
- if (files == null) return;
292
- if (!sources.includes("upload")) return;
293
- editor.reset_canvas();
294
- const _file = Array.isArray(files) ? files[0] : files;
295
- await editor.add_image({ image: _file });
296
- await crop.add_image({ image: _file });
297
- crop.reset();
298
- background_image = true;
299
- zoom.set_zoom("fit");
300
- handle_tool_change({ tool: "draw" });
301
- dispatch("upload");
302
- dispatch("input");
303
- dispatch("change");
304
- }
305
- $: background_image = can_undo && editor.command_manager.contains("AddImage");
306
- function handle_tool_change({ tool }) {
307
- editor.set_tool(tool);
308
- current_tool = tool;
309
- if (tool === "image") {
310
- crop.set_tool("image");
311
- crop.set_subtool("crop");
312
- }
313
- }
314
- function handle_subtool_change({
315
- tool,
316
- subtool
317
- }) {
318
- editor.set_subtool(subtool);
319
- current_subtool = subtool;
320
- if (subtool === null) {
321
- return;
322
- }
323
- if (tool === "draw") {
324
- if (subtool === "size") {
325
- brush_size_visible = true;
326
- } else if (subtool === "color") {
327
- brush_color_visible = true;
328
- }
329
- }
330
- if (tool === "erase" && subtool === "size") {
331
- eraser_size_visible = true;
332
- }
333
- if (tool === "image" && subtool === "paste") {
334
- process_clipboard();
335
- }
336
- if (tool === "image" && subtool === "upload") {
337
- tick().then(() => {
338
- disable_click = false;
339
- open_file_upload();
340
- });
341
- }
342
- }
343
- let eraser_size_visible = false;
344
- let selected_color;
345
- let selected_size;
346
- let selected_opacity = 1;
347
- let selected_eraser_size;
348
- $: {
349
- if (brush_options) {
350
- update_brush_options();
351
- }
352
- if (eraser_options) {
353
- update_eraser_options();
354
- }
355
- }
356
- function update_brush_options() {
357
- const default_color = brush_options.default_color === "auto" ? brush_options.colors[0] : brush_options.default_color;
358
- if (Array.isArray(default_color)) {
359
- selected_color = default_color[0];
360
- selected_opacity = default_color[1];
361
- } else {
362
- selected_color = default_color;
363
- const color = tinycolor(default_color);
364
- if (color.getAlpha() < 1) {
365
- selected_opacity = color.getAlpha();
366
- } else {
367
- selected_opacity = 1;
368
- }
369
- }
370
- selected_size = typeof brush_options.default_size === "number" ? brush_options.default_size : 25;
371
- }
372
- function update_eraser_options() {
373
- selected_eraser_size = eraser_options.default_size === "auto" ? 25 : eraser_options.default_size;
374
- }
375
- let brush_size_visible = false;
376
- let brush_color_visible = false;
377
- $: brush?.set_brush_color(
378
- (() => {
379
- let color_value;
380
- if (selected_color === "auto") {
381
- const default_color = brush_options.colors.find(
382
- (color) => Array.isArray(color) ? color[0] === brush_options.default_color : color === brush_options.default_color
383
- ) || brush_options.colors[0];
384
- color_value = Array.isArray(default_color) ? default_color[0] : default_color;
385
- } else {
386
- color_value = selected_color;
387
- }
388
- return color_value;
389
- })()
390
- );
391
- $: brush?.set_brush_size(
392
- typeof selected_size === "number" ? selected_size : 25
393
- );
394
- $: brush?.set_eraser_size(
395
- typeof selected_eraser_size === "number" ? selected_eraser_size : 25
396
- );
397
- $: disable_click = current_tool !== "image" || current_tool === "image" && background_image || current_tool === "image" && current_subtool === "webcam" || !sources.includes("upload");
398
- let current_subtool = null;
399
- let preview = false;
400
- $: brush?.preview_brush(preview);
401
- $: brush?.set_brush_opacity(selected_opacity);
402
- function handle_zoom_change(zoom_level2) {
403
- zoom.set_zoom(zoom_level2);
404
- }
405
- function zoom_in_out(direction) {
406
- zoom.set_zoom(
407
- direction === "in" ? zoom_level + (zoom_level < 1 ? 0.1 : zoom_level * 0.1) : zoom_level - (zoom_level < 1 ? 0.1 : zoom_level * 0.1)
408
- );
409
- }
410
- async function process_clipboard() {
411
- const items = await navigator.clipboard.read();
412
- for (let i = 0; i < items.length; i++) {
413
- const type = items[i].types.find((t) => t.startsWith("image/"));
414
- if (type) {
415
- const blob = await items[i].getType(type);
416
- handle_files(blob);
417
- }
418
- }
419
- }
420
- function handle_capture(e) {
421
- if (e.detail !== null) {
422
- handle_files(e.detail);
423
- }
424
- handle_subtool_change({ tool: current_tool, subtool: null });
425
- }
426
- function handle_save() {
427
- dispatch("save");
428
- }
429
- $: add_image_from_url(composite || background);
430
- $: add_layers_from_url(layers);
431
- async function handle_crop_confirm() {
432
- const { image } = await crop.get_crop_bounds();
433
- if (!image) return;
434
- await editor.add_image({
435
- image,
436
- resize: false
437
- });
438
- handle_subtool_change({ tool: "image", subtool: null });
439
- dispatch("change");
440
- dispatch("input");
441
- }
442
- async function handle_download() {
443
- const blobs = await editor.get_blobs();
444
- const blob = blobs.composite;
445
- if (!blob) {
446
- dispatch("download_error", "Unable to generate image to download.");
447
- return;
448
- }
449
- const url = URL.createObjectURL(blob);
450
- const link = document.createElement("a");
451
- link.href = url;
452
- link.download = "image.png";
453
- link.click();
454
- URL.revokeObjectURL(url);
455
- }
456
- function handle_undo() {
457
- editor.undo();
458
- }
459
- function handle_redo() {
460
- editor.redo();
461
- }
462
- let old_background = background;
463
- $: if (old_background !== background) {
464
- handle_tool_change({ tool: "draw" });
465
- handle_subtool_change({ tool: "draw", subtool: null });
466
- current_tool = "draw";
467
- old_background = background;
468
- }
8
+ <script lang="ts">
9
+ import { onMount, createEventDispatcher, tick } from "svelte";
10
+ import { get } from "svelte/store";
11
+ import Toolbar, { type Tool as ToolbarTool } from "./Toolbar.svelte";
12
+ import { CropTool } from "./crop/crop";
13
+ import { ResizeTool } from "./resize/resize";
14
+ import { Webcam } from "@gradio/image";
15
+ import type { I18nFormatter } from "@gradio/utils";
16
+ import type { Client, FileData } from "@gradio/client";
17
+ import tinycolor, { type ColorInput } from "tinycolor2";
18
+ import { ZoomTool } from "./zoom/zoom";
19
+ import { type CommandManager, type CommandNode } from "./core/commands";
20
+ import { ImageEditor } from "./core/editor";
21
+ import { type Brush, type Eraser } from "./brush/types";
22
+ import { BrushTool } from "./brush/brush";
23
+ import { create_drag } from "@gradio/upload";
24
+ import SecondaryToolbar from "./SecondaryToolbar.svelte";
25
+ import { Check } from "@gradio/icons";
26
+ import type { LayerOptions, Source, Transform, WebcamOptions } from "./types";
27
+
28
+ import { type ImageBlobs } from "./types";
29
+ import Controls from "./Controls.svelte";
30
+ import IconButton from "./IconButton.svelte";
31
+
32
+ const { drag, open_file_upload } = create_drag();
33
+ const dispatch = createEventDispatcher<{
34
+ clear?: never;
35
+ save: void;
36
+ change: void;
37
+ upload: void;
38
+ input: void;
39
+ download_error: string;
40
+ }>();
41
+
42
+ export const antialias = true;
43
+
44
+ export let changeable = false;
45
+ export let sources: Source[] = ["upload", "webcam", "clipboard"];
46
+ export let transforms: Transform[] = ["crop", "resize"];
47
+ export let canvas_size: [number, number];
48
+ export let is_dragging = false;
49
+ export let background_image = false;
50
+ export let brush_options: Brush;
51
+ export let eraser_options: Eraser;
52
+ export let fixed_canvas = false;
53
+ export let root: string;
54
+ export let i18n: I18nFormatter;
55
+ export let upload: Client["upload"];
56
+ export let composite: FileData | null;
57
+ export let layers: FileData[];
58
+ export let background: FileData | null;
59
+ export let border_region = 0;
60
+ export let layer_options: LayerOptions;
61
+ export let current_tool: ToolbarTool;
62
+ export let webcam_options: WebcamOptions;
63
+ export let show_download_button = false;
64
+ export let theme_mode: "dark" | "light";
65
+ export let full_history: CommandNode | null = null;
66
+
67
+ let pixi_target: HTMLDivElement;
68
+ let pixi_target_crop: HTMLDivElement;
69
+
70
+ $: if (layer_options) {
71
+ if (check_if_should_init()) {
72
+ editor.set_layer_options(layer_options);
73
+ refresh_tools();
74
+ }
75
+ }
76
+
77
+ function refresh_tools(): void {
78
+ if (!editor || !ready) return;
79
+ editor.set_tool(current_tool);
80
+ editor.set_subtool(current_subtool);
81
+ }
82
+
83
+ $: if (editor && ready && editor.layers) {
84
+ const current_layers = get(editor.layers);
85
+
86
+ if (current_layers.layers.length > 0 && !current_layers.active_layer) {
87
+ refresh_tools_for_layer_changes(current_layers);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Refreshes tools when layer state changes
93
+ */
94
+ function refresh_tools_for_layer_changes(current_layers: any): void {
95
+ if (!editor || !ready) return;
96
+
97
+ if (current_layers.layers.length > 0 && !current_layers.active_layer) {
98
+ editor.set_layer(current_layers.layers[0].id);
99
+ }
100
+
101
+ // reapply current tool to ensure it targets the correct layer
102
+ if (current_tool) {
103
+ editor.set_tool(current_tool);
104
+ if (current_subtool) {
105
+ editor.set_subtool(current_subtool);
106
+ }
107
+ }
108
+
109
+ if (brush && (current_tool === "draw" || current_tool === "erase")) {
110
+ brush.set_tool(current_tool, current_subtool);
111
+ }
112
+ }
113
+
114
+ function check_if_should_init(): boolean {
115
+ return layer_options && editor && ready;
116
+ }
117
+
118
+ export let has_drawn = false;
119
+
120
+ /**
121
+ * Gets the image blobs from the editor
122
+ * @returns {Promise<ImageBlobs>} Object containing background, layers, and composite image blobs
123
+ */
124
+ export async function get_blobs(): Promise<ImageBlobs> {
125
+ if (!editor) return { background: null, layers: [], composite: null };
126
+ if (!background_image && !has_drawn && !layers.length)
127
+ return { background: null, layers: [], composite: null };
128
+ const blobs = await editor.get_blobs();
129
+ return blobs;
130
+ }
131
+
132
+ let editor: ImageEditor;
133
+
134
+ /**
135
+ * Adds an image to the editor
136
+ * @param {Blob | File} image - The image to add
137
+ */
138
+ export function add_image(image: Blob | File): void {
139
+ editor.add_image({ image });
140
+ }
141
+
142
+ let pending_bg: Promise<void>;
143
+ /**
144
+ * Adds an image to the editor from a URL
145
+ * @param {string | FileData} source - The URL of the image or a FileData object
146
+ * @returns {Promise<void>}
147
+ */
148
+ export async function add_image_from_url(
149
+ source:
150
+ | string
151
+ | {
152
+ url: string;
153
+ meta: {
154
+ _type: string;
155
+ };
156
+ }
157
+ | any
158
+ ): Promise<void> {
159
+ if (!editor || !source || !check_if_should_init()) return;
160
+ let url: string;
161
+
162
+ if (typeof source === "string") {
163
+ url = source;
164
+ } else if (source?.meta?._type === "gradio.FileData" && source?.url) {
165
+ url = source.url;
166
+ } else {
167
+ console.warn("Invalid source provided to add_image_from_url:", source);
168
+ return;
169
+ }
170
+
171
+ try {
172
+ pending_bg = editor.add_image_from_url(url);
173
+ let pending_crop = crop.add_image_from_url(url);
174
+ await Promise.all([pending_bg, pending_crop]);
175
+ crop.set_tool("image");
176
+ crop.set_subtool("crop");
177
+ background_image = true;
178
+ zoom.set_zoom("fit");
179
+ dispatch("upload");
180
+ dispatch("input");
181
+ } catch (error) {
182
+ console.error("Error adding image from URL:", error);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Adds a new layer with an image loaded from a URL
188
+ * @param {string | FileData} source - The URL of the image or a FileData object
189
+ * @returns {Promise<string | null>} - The ID of the created layer, or null if failed
190
+ */
191
+ export async function add_layers_from_url(
192
+ source: FileData[] | any
193
+ ): Promise<void> {
194
+ if (!editor || !source.length || !check_if_should_init()) return;
195
+
196
+ if (
197
+ Array.isArray(source) &&
198
+ source.every((item) => item?.meta?._type === "gradio.FileData")
199
+ ) {
200
+ try {
201
+ await pending_bg;
202
+ await editor.add_layers_from_url(source.map((item) => item.url));
203
+ dispatch("change");
204
+ dispatch("input");
205
+ } catch (error) {
206
+ console.error("Error adding layer from URL:", error);
207
+ }
208
+ }
209
+ }
210
+
211
+ let brush: BrushTool;
212
+ let zoom: ZoomTool;
213
+ let zoom_level = 1;
214
+ let ready = false;
215
+ let mounted = false;
216
+ let min_zoom = true;
217
+
218
+ let last_dimensions = { width: 0, height: 0 };
219
+
220
+ /**
221
+ * Handles visibility changes and resets zoom if dimensions have changed
222
+ */
223
+ async function handle_visibility_change(): Promise<void> {
224
+ if (!editor || !ready || !zoom) return;
225
+ await tick();
226
+
227
+ const is_visible = pixi_target.offsetParent !== null;
228
+
229
+ if (is_visible) {
230
+ const current_dimensions = pixi_target.getBoundingClientRect();
231
+
232
+ if (
233
+ current_dimensions.width !== last_dimensions.width ||
234
+ current_dimensions.height !== last_dimensions.height
235
+ ) {
236
+ zoom.set_zoom("fit");
237
+
238
+ last_dimensions = {
239
+ width: current_dimensions.width,
240
+ height: current_dimensions.height
241
+ };
242
+ }
243
+ }
244
+ }
245
+
246
+ onMount(() => {
247
+ let intersection_observer: IntersectionObserver;
248
+ let resize_observer: ResizeObserver;
249
+
250
+ init_image_editor().then(() => {
251
+ mounted = true;
252
+ intersection_observer = new IntersectionObserver(() => {
253
+ handle_visibility_change();
254
+ });
255
+
256
+ resize_observer = new ResizeObserver(() => {
257
+ handle_visibility_change();
258
+ });
259
+
260
+ intersection_observer.observe(pixi_target);
261
+ resize_observer.observe(pixi_target);
262
+
263
+ setTimeout(() => {
264
+ if (full_history && editor) {
265
+ editor.command_manager
266
+ .replay(full_history, editor.context)
267
+ .then(() => {
268
+ refresh_tools_after_history();
269
+ });
270
+ }
271
+ }, 0);
272
+ });
273
+
274
+ if (typeof window !== "undefined") {
275
+ (window as any).editor = editor;
276
+ }
277
+
278
+ return () => {
279
+ if (intersection_observer) {
280
+ intersection_observer.disconnect();
281
+ }
282
+ if (resize_observer) {
283
+ resize_observer.disconnect();
284
+ }
285
+ if (editor) {
286
+ editor.destroy();
287
+ }
288
+ };
289
+ });
290
+
291
+ /**
292
+ * Refreshes tool state after history replay to ensure proper layer targeting
293
+ */
294
+ function refresh_tools_after_history(): void {
295
+ if (!editor || !ready) return;
296
+
297
+ const current_layers = get(editor.layers);
298
+
299
+ if (current_layers.layers.length > 0 && !current_layers.active_layer) {
300
+ editor.set_layer(current_layers.layers[0].id);
301
+ }
302
+
303
+ if (current_tool) {
304
+ editor.set_tool(current_tool);
305
+ if (current_subtool) {
306
+ editor.set_subtool(current_subtool);
307
+ }
308
+ }
309
+
310
+ if (brush && (current_tool === "draw" || current_tool === "erase")) {
311
+ brush.set_tool(current_tool, current_subtool);
312
+ }
313
+
314
+ full_history = editor.command_manager.history;
315
+ }
316
+
317
+ let crop: ImageEditor;
318
+ let crop_zoom: ZoomTool;
319
+ export let can_undo = false;
320
+ let can_redo = false;
321
+ async function init_image_editor(): Promise<void> {
322
+ brush = new BrushTool();
323
+ zoom = new ZoomTool();
324
+ editor = new ImageEditor({
325
+ target_element: pixi_target,
326
+ width: canvas_size[0],
327
+ height: canvas_size[1],
328
+ tools: ["image", zoom, new ResizeTool(), brush],
329
+ fixed_canvas,
330
+ border_region,
331
+ layer_options,
332
+ theme_mode
333
+ });
334
+
335
+ brush.on("change", () => {
336
+ has_drawn = true;
337
+ });
338
+
339
+ crop_zoom = new ZoomTool();
340
+
341
+ crop = new ImageEditor({
342
+ target_element: pixi_target_crop,
343
+ width: canvas_size[0],
344
+ height: canvas_size[1],
345
+ tools: ["image", crop_zoom, new CropTool()],
346
+ dark: true,
347
+ fixed_canvas: false,
348
+ border_region: 0,
349
+ pad_bottom: 40
350
+ });
351
+
352
+ editor.scale.subscribe((_scale) => {
353
+ zoom_level = _scale;
354
+ });
355
+
356
+ editor.min_zoom.subscribe((is_min_zoom) => {
357
+ min_zoom = is_min_zoom;
358
+ });
359
+
360
+ editor.dimensions.subscribe((dimensions) => {
361
+ last_dimensions = { ...dimensions };
362
+ });
363
+
364
+ editor.command_manager.current_history.subscribe((history) => {
365
+ can_undo = history.previous !== null;
366
+ can_redo = history.next !== null;
367
+ });
368
+
369
+ await Promise.all([editor.ready, crop.ready]).then(() => {
370
+ handle_tool_change({ tool: "image" });
371
+ ready = true;
372
+ if (sources.length > 0) {
373
+ handle_tool_change({ tool: "image" });
374
+ } else {
375
+ handle_tool_change({ tool: "draw" });
376
+ }
377
+ crop.set_subtool("crop");
378
+ });
379
+
380
+ editor.on("change", () => {
381
+ dispatch("change");
382
+ dispatch("input");
383
+ full_history = editor.command_manager.history;
384
+ });
385
+
386
+ if (background || layers.length > 0) {
387
+ if (background) {
388
+ await add_image_from_url(background);
389
+ }
390
+ if (layers.length > 0) {
391
+ await add_layers_from_url(layers);
392
+ }
393
+ handle_tool_change({ tool: "draw" });
394
+ } else if (composite) {
395
+ await add_image_from_url(composite);
396
+ handle_tool_change({ tool: "draw" });
397
+ }
398
+
399
+ refresh_tools();
400
+ }
401
+
402
+ $: if (
403
+ background == null &&
404
+ layers.length == 0 &&
405
+ composite == null &&
406
+ editor &&
407
+ ready
408
+ ) {
409
+ editor.reset_canvas();
410
+ zoom.set_zoom("fit");
411
+ handle_tool_change({ tool: "image" });
412
+ background_image = false;
413
+ has_drawn = false;
414
+ }
415
+
416
+ $: current_tool === "image" &&
417
+ current_subtool === "crop" &&
418
+ crop_zoom.set_zoom("fit");
419
+
420
+ /**
421
+ * Handles file uploads
422
+ * @param {File[]} files - The uploaded files
423
+ */
424
+ async function handle_files(
425
+ files: File[] | Blob[] | File | Blob | null
426
+ ): Promise<void> {
427
+ if (files == null) return;
428
+ if (!sources.includes("upload")) return;
429
+ editor.reset_canvas();
430
+ const _file = Array.isArray(files) ? files[0] : files;
431
+ await editor.add_image({ image: _file });
432
+ await crop.add_image({ image: _file });
433
+ crop.reset();
434
+
435
+ background_image = true;
436
+ zoom.set_zoom("fit");
437
+ handle_tool_change({ tool: "draw" });
438
+ dispatch("upload");
439
+ }
440
+
441
+ $: background_image = can_undo && editor.command_manager.contains("AddImage");
442
+
443
+ /**
444
+ * Handles tool change events
445
+ * @param {{ tool: ToolbarTool }} param0 - Object containing the selected tool
446
+ */
447
+ function handle_tool_change({ tool }: { tool: ToolbarTool }): void {
448
+ editor.set_tool(tool);
449
+ current_tool = tool;
450
+
451
+ if (tool === "image") {
452
+ crop.set_tool("image");
453
+ crop.set_subtool("crop");
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Handles subtool change events
459
+ * @param {{ tool: ToolbarTool, subtool: Subtool }} param0 - Object containing the selected tool and subtool
460
+ */
461
+ function handle_subtool_change({
462
+ tool,
463
+ subtool
464
+ }: {
465
+ tool: ToolbarTool;
466
+ subtool: Subtool | null;
467
+ }): void {
468
+ editor.set_subtool(subtool);
469
+ current_subtool = subtool;
470
+
471
+ if (subtool === null) {
472
+ return;
473
+ }
474
+
475
+ if (tool === "draw") {
476
+ if (subtool === "size") {
477
+ brush_size_visible = true;
478
+ } else if (subtool === "color") {
479
+ brush_color_visible = true;
480
+ }
481
+ }
482
+
483
+ if (tool === "erase" && subtool === "size") {
484
+ eraser_size_visible = true;
485
+ }
486
+
487
+ if (tool === "image" && subtool === "paste") {
488
+ process_clipboard();
489
+ }
490
+
491
+ if (tool === "image" && subtool === "upload") {
492
+ tick().then(() => {
493
+ disable_click = false;
494
+ open_file_upload();
495
+ });
496
+ }
497
+ }
498
+
499
+ let eraser_size_visible = false;
500
+ let selected_color: ColorInput | string;
501
+ let selected_size: number;
502
+ let selected_opacity = 1;
503
+ let selected_eraser_size: number;
504
+
505
+ $: {
506
+ if (brush_options) {
507
+ update_brush_options();
508
+ }
509
+
510
+ if (eraser_options) {
511
+ update_eraser_options();
512
+ }
513
+ }
514
+
515
+ function update_brush_options(): void {
516
+ const default_color =
517
+ brush_options.default_color === "auto"
518
+ ? brush_options.colors[0]
519
+ : brush_options.default_color;
520
+
521
+ // color is already a tuple [color, opacity]
522
+ if (Array.isArray(default_color)) {
523
+ selected_color = default_color[0];
524
+ selected_opacity = default_color[1];
525
+ } else {
526
+ selected_color = default_color;
527
+
528
+ // color is a string, check if it has opacity info
529
+ const color = tinycolor(default_color);
530
+ if (color.getAlpha() < 1) {
531
+ selected_opacity = color.getAlpha();
532
+ } else {
533
+ selected_opacity = 1;
534
+ }
535
+ }
536
+
537
+ selected_size =
538
+ typeof brush_options.default_size === "number"
539
+ ? brush_options.default_size
540
+ : 25;
541
+ }
542
+
543
+ function update_eraser_options(): void {
544
+ selected_eraser_size =
545
+ eraser_options.default_size === "auto" ? 25 : eraser_options.default_size;
546
+ }
547
+
548
+ let brush_size_visible = false;
549
+
550
+ let brush_color_visible = false;
551
+
552
+ $: brush?.set_brush_color(
553
+ (() => {
554
+ let color_value;
555
+ if (selected_color === "auto") {
556
+ const default_color =
557
+ brush_options.colors.find((color) =>
558
+ Array.isArray(color)
559
+ ? color[0] === brush_options.default_color
560
+ : color === brush_options.default_color
561
+ ) || brush_options.colors[0];
562
+
563
+ color_value = Array.isArray(default_color)
564
+ ? default_color[0]
565
+ : default_color;
566
+ } else {
567
+ color_value = selected_color;
568
+ }
569
+ return color_value;
570
+ })()
571
+ );
572
+
573
+ $: brush?.set_brush_size(
574
+ typeof selected_size === "number" ? selected_size : 25
575
+ );
576
+
577
+ $: brush?.set_eraser_size(
578
+ typeof selected_eraser_size === "number" ? selected_eraser_size : 25
579
+ );
580
+
581
+ $: disable_click =
582
+ current_tool !== "image" ||
583
+ (current_tool === "image" && background_image) ||
584
+ (current_tool === "image" && current_subtool === "webcam") ||
585
+ !sources.includes("upload");
586
+
587
+ let current_subtool: Subtool | null = null;
588
+ let preview = false;
589
+ $: brush?.preview_brush(preview);
590
+ $: brush?.set_brush_opacity(selected_opacity);
591
+
592
+ function handle_zoom_change(zoom_level: number | "fit"): void {
593
+ zoom.set_zoom(zoom_level);
594
+ }
595
+
596
+ function zoom_in_out(direction: "in" | "out"): void {
597
+ zoom.set_zoom(
598
+ direction === "in"
599
+ ? zoom_level + (zoom_level < 1 ? 0.1 : zoom_level * 0.1)
600
+ : zoom_level - (zoom_level < 1 ? 0.1 : zoom_level * 0.1)
601
+ );
602
+ }
603
+
604
+ async function process_clipboard(): Promise<void> {
605
+ const items = await navigator.clipboard.read();
606
+
607
+ for (let i = 0; i < items.length; i++) {
608
+ const type = items[i].types.find((t) => t.startsWith("image/"));
609
+ if (type) {
610
+ const blob = await items[i].getType(type);
611
+
612
+ handle_files(blob);
613
+ }
614
+ }
615
+ }
616
+
617
+ function handle_capture(e: CustomEvent): void {
618
+ if (e.detail !== null) {
619
+ handle_files(e.detail as Blob);
620
+ }
621
+ handle_subtool_change({ tool: current_tool, subtool: null });
622
+ }
623
+
624
+ function handle_save(): void {
625
+ dispatch("save");
626
+ }
627
+
628
+ $: add_image_from_url(composite || background);
629
+ $: add_layers_from_url(layers);
630
+
631
+ async function handle_crop_confirm(): Promise<void> {
632
+ const { image } = await crop.get_crop_bounds();
633
+ if (!image) return;
634
+
635
+ await editor.add_image({
636
+ image,
637
+ resize: false
638
+ });
639
+ handle_subtool_change({ tool: "image", subtool: null });
640
+ dispatch("change");
641
+ dispatch("input");
642
+ }
643
+
644
+ async function handle_download(): Promise<void> {
645
+ const blobs = await editor.get_blobs();
646
+
647
+ const blob = blobs.composite;
648
+ if (!blob) {
649
+ dispatch("download_error", "Unable to generate image to download.");
650
+ return;
651
+ }
652
+ const url = URL.createObjectURL(blob);
653
+ const link = document.createElement("a");
654
+ link.href = url;
655
+ link.download = "image.png";
656
+ link.click();
657
+ URL.revokeObjectURL(url);
658
+ }
659
+
660
+ function handle_undo(): void {
661
+ editor.undo();
662
+ }
663
+
664
+ function handle_redo(): void {
665
+ editor.redo();
666
+ }
667
+
668
+ let old_background = background;
669
+
670
+ $: if (old_background !== background) {
671
+ handle_tool_change({ tool: "draw" });
672
+ handle_subtool_change({ tool: "draw", subtool: null });
673
+ current_tool = "draw";
674
+ old_background = background;
675
+ }
469
676
  </script>
470
677
 
471
678
  <div