@gradio/imageeditor 0.13.0 → 0.14.1

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 (36) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/ImageEditor.stories.svelte +9 -1
  3. package/Index.svelte +13 -1
  4. package/InteractiveImageEditor.svelte +10 -1
  5. package/README.md +2 -0
  6. package/dist/Index.svelte +6 -0
  7. package/dist/Index.svelte.d.ts +9 -1
  8. package/dist/InteractiveImageEditor.svelte +7 -0
  9. package/dist/InteractiveImageEditor.svelte.d.ts +5 -1
  10. package/dist/shared/Controls.svelte +13 -1
  11. package/dist/shared/Controls.svelte.d.ts +2 -0
  12. package/dist/shared/ImageEditor.svelte +29 -3
  13. package/dist/shared/ImageEditor.svelte.d.ts +5 -2
  14. package/dist/shared/Layers.svelte +29 -2
  15. package/dist/shared/Layers.svelte.d.ts +2 -0
  16. package/dist/shared/SecondaryToolbar.svelte +1 -0
  17. package/dist/shared/SecondaryToolbar.svelte.d.ts +2 -0
  18. package/dist/shared/brush/brush-textures.d.ts +0 -6
  19. package/dist/shared/brush/brush-textures.js +0 -19
  20. package/dist/shared/brush/brush.js +3 -2
  21. package/dist/shared/core/editor.d.ts +15 -6
  22. package/dist/shared/core/editor.js +76 -33
  23. package/dist/shared/image/image.js +2 -4
  24. package/dist/shared/types.d.ts +4 -0
  25. package/dist/shared/zoom/zoom.js +1 -1
  26. package/package.json +9 -9
  27. package/shared/Controls.svelte +14 -40
  28. package/shared/ImageEditor.svelte +33 -4
  29. package/shared/Layers.svelte +36 -3
  30. package/shared/SecondaryToolbar.svelte +7 -1
  31. package/shared/brush/brush-textures.ts +0 -23
  32. package/shared/brush/brush.ts +6 -2
  33. package/shared/core/editor.ts +102 -44
  34. package/shared/image/image.ts +2 -4
  35. package/shared/types.ts +5 -0
  36. package/shared/zoom/zoom.ts +1 -1
@@ -4,7 +4,12 @@
4
4
 
5
5
  export let layers: Writable<{
6
6
  active_layer: string;
7
- layers: { name: string; id: string; user_created: boolean }[];
7
+ layers: {
8
+ name: string;
9
+ id: string;
10
+ user_created: boolean;
11
+ visible: boolean;
12
+ }[];
8
13
  }>;
9
14
 
10
15
  export let enable_additional_layers = true;
@@ -24,6 +29,7 @@
24
29
  on:change_layer
25
30
  on:move_layer
26
31
  on:delete_layer
32
+ on:toggle_layer_visibility
27
33
  />
28
34
  {/if}
29
35
  </div>
@@ -5,7 +5,6 @@ import {
5
5
  Graphics,
6
6
  Application
7
7
  } from "pixi.js";
8
- import { safe_destroy, safe_remove_child } from "./brush-utils";
9
8
  import { type ImageEditorContext } from "../core/editor";
10
9
 
11
10
  /**
@@ -26,22 +25,12 @@ export class BrushTextures {
26
25
  // Track if a new stroke is starting
27
26
  private is_new_stroke = true;
28
27
 
29
- // Current scale factor
30
- private scale = 1;
31
-
32
28
  // Track the current drawing opacity
33
29
  private current_opacity = 1.0;
34
30
 
35
31
  // Track current drawing mode
36
32
  private current_mode: "draw" | "erase" = "draw";
37
33
 
38
- // Store last drawing parameters for eraser recreation
39
- private last_from_x: number | null = null;
40
- private last_from_y: number | null = null;
41
- private last_to_x: number | null = null;
42
- private last_to_y: number | null = null;
43
- private last_size: number | null = null;
44
-
45
34
  constructor(
46
35
  private image_editor_context: ImageEditorContext,
47
36
  private app: Application
@@ -50,11 +39,6 @@ export class BrushTextures {
50
39
  width: this.image_editor_context.image_container.width,
51
40
  height: this.image_editor_context.image_container.height
52
41
  };
53
-
54
- // Subscribe to scale changes
55
- this.image_editor_context.scale.subscribe((scale) => {
56
- this.scale = scale;
57
- });
58
42
  }
59
43
 
60
44
  /**
@@ -336,13 +320,6 @@ export class BrushTextures {
336
320
  this.current_opacity =
337
321
  mode === "draw" ? Math.min(Math.max(opacity, 0), 1) : 0.5;
338
322
 
339
- // Store parameters for potential eraser recreation
340
- this.last_from_x = from_x;
341
- this.last_from_y = from_y;
342
- this.last_to_x = to_x;
343
- this.last_to_y = to_y;
344
- this.last_size = size;
345
-
346
323
  // Apply scale to brush size for consistent rendering
347
324
  const scaled_size = size;
348
325
 
@@ -213,8 +213,12 @@ export class BrushTool implements Tool {
213
213
  * @private
214
214
  */
215
215
  private on_pointer_down(event: FederatedPointerEvent): void {
216
- // Don't draw if not over image
217
- if (this.current_tool !== "erase" && this.current_tool !== "draw") {
216
+ const current_layer =
217
+ this.image_editor_context.layer_manager.get_active_layer();
218
+ if (
219
+ !current_layer?.visible ||
220
+ (this.current_tool !== "erase" && this.current_tool !== "draw")
221
+ ) {
218
222
  return;
219
223
  }
220
224
 
@@ -79,12 +79,19 @@ export class LayerManager {
79
79
  id: string;
80
80
  container: Container;
81
81
  user_created: boolean;
82
+ visible: boolean;
82
83
  }[] = [];
83
84
  private active_layer: Container | null = null;
85
+ private active_layer_id: string | null = null;
84
86
  private draw_textures: Map<Container, RenderTexture> = new Map();
85
87
  layer_store: Writable<{
86
88
  active_layer: string;
87
- layers: { name: string; id: string; user_created: boolean }[];
89
+ layers: {
90
+ name: string;
91
+ id: string;
92
+ user_created: boolean;
93
+ visible: boolean;
94
+ }[];
88
95
  }> = writable({
89
96
  active_layer: "",
90
97
 
@@ -113,6 +120,18 @@ export class LayerManager {
113
120
  this.layer_options = layer_options;
114
121
  }
115
122
 
123
+ toggle_layer_visibility(id: string): void {
124
+ const layer = this.layers.find((l) => l.id === id);
125
+ if (layer) {
126
+ layer.container.visible = !layer.container.visible;
127
+ layer.visible = layer.container.visible;
128
+ this.layer_store.update((state) => ({
129
+ active_layer: state.active_layer,
130
+ layers: this.layers
131
+ }));
132
+ }
133
+ }
134
+
116
135
  create_background_layer(width: number, height: number): Container {
117
136
  if (this.background_layer) {
118
137
  this.background_layer.destroy();
@@ -277,22 +296,27 @@ export class LayerManager {
277
296
  width,
278
297
  height,
279
298
  layer_name,
280
- user_created
299
+ user_created,
300
+ layer_id = undefined,
301
+ make_active = false
281
302
  }: {
282
303
  width: number;
283
304
  height: number;
284
305
  layer_name?: string;
285
306
  user_created: boolean;
307
+ layer_id?: string;
308
+ make_active?: boolean;
286
309
  }): Container {
287
310
  const layer = new Container();
288
- const layer_id = Math.random().toString(36).substring(2, 15);
311
+ const _layer_id = layer_id || Math.random().toString(36).substring(2, 15);
289
312
  const _layer_name = layer_name || `Layer ${this.layers.length + 1}`;
290
313
 
291
314
  this.layers.push({
292
315
  name: _layer_name,
293
- id: layer_id,
316
+ id: _layer_id,
294
317
  container: layer,
295
- user_created
318
+ user_created,
319
+ visible: true
296
320
  });
297
321
 
298
322
  this.image_container.addChild(layer);
@@ -325,9 +349,12 @@ export class LayerManager {
325
349
  this.draw_textures.set(layer, draw_texture);
326
350
 
327
351
  this.update_layer_order();
328
- this.active_layer = layer;
352
+ if (make_active) {
353
+ this.set_active_layer(_layer_id);
354
+ }
355
+
329
356
  this.layer_store.set({
330
- active_layer: layer_id,
357
+ active_layer: this.active_layer_id || "",
331
358
  layers: this.layers
332
359
  });
333
360
  return layer;
@@ -436,6 +463,7 @@ export class LayerManager {
436
463
  this.layers.find((l) => l.id === id)?.container ||
437
464
  this.layers[0]?.container ||
438
465
  null;
466
+ this.active_layer_id = id;
439
467
  this.layer_store.set({
440
468
  active_layer: id,
441
469
  layers: this.layers
@@ -469,8 +497,9 @@ export class LayerManager {
469
497
  }
470
498
  this.layers[index].container.destroy();
471
499
  if (this.active_layer === this.layers[index].container) {
472
- this.active_layer =
473
- this.layers[Math.max(0, index - 1)]?.container || null;
500
+ const new_active_layer = this.layers[Math.max(0, index - 1)] || null;
501
+ this.active_layer = new_active_layer?.container || null;
502
+ this.active_layer_id = new_active_layer?.id || null;
474
503
  }
475
504
  this.layers = this.layers.filter((l) => l.id !== id);
476
505
 
@@ -849,21 +878,34 @@ export class LayerManager {
849
878
 
850
879
  reset_layers(width: number, height: number, persist = false): void {
851
880
  const _layers_to_recreate = persist
852
- ? this.layers.map((layer) => layer.name)
853
- : this.layer_options.layers;
881
+ ? this.layers.map((layer) => [layer.name, layer.id])
882
+ : this.layer_options.layers.map((layer) => [layer, undefined]);
854
883
 
855
884
  this.layers.forEach((layer) => {
856
885
  this.delete_layer(layer.id);
857
886
  });
858
887
 
859
- for (const layer of _layers_to_recreate) {
888
+ for (const [layer_name, layer_id] of _layers_to_recreate) {
860
889
  this.create_layer({
861
890
  width,
862
891
  height,
863
- layer_name: layer,
864
- user_created: this.layer_options.layers.includes(layer) ? false : true
892
+ layer_name: layer_name,
893
+ user_created: this.layer_options.layers.find((l) => l === layer_name)
894
+ ? false
895
+ : true,
896
+ layer_id: layer_id
865
897
  });
866
898
  }
899
+
900
+ if (!persist) {
901
+ this.active_layer = this.layers[0].container;
902
+ this.active_layer_id = this.layers[0].id;
903
+ }
904
+
905
+ this.layer_store.update((state) => ({
906
+ active_layer: this.active_layer_id || this.layers[0].id,
907
+ layers: this.layers
908
+ }));
867
909
  }
868
910
 
869
911
  init_layers(width: number, height: number): void {
@@ -880,8 +922,9 @@ export class LayerManager {
880
922
  }
881
923
 
882
924
  this.active_layer = this.layers[0].container;
925
+ this.active_layer_id = this.layers[0].id;
883
926
  this.layer_store.update((_layers) => ({
884
- active_layer: _layers.layers[0].id,
927
+ active_layer: this.layers[0].id,
885
928
  layers: this.layers
886
929
  }));
887
930
  }
@@ -899,6 +942,7 @@ interface ImageEditorOptions {
899
942
  border_region?: number;
900
943
  layer_options?: LayerOptions;
901
944
  pad_bottom?: number;
945
+ theme_mode?: "dark" | "light";
902
946
  }
903
947
 
904
948
  const core_tool_map = {
@@ -922,8 +966,8 @@ export interface ImageEditorContext {
922
966
  scale?: number;
923
967
  position?: { x: number; y: number };
924
968
  animate?: boolean;
925
- }) => void;
926
- execute_command: (command: Command) => void;
969
+ }) => Promise<void>;
970
+ execute_command: (command: Command) => Promise<void> | void;
927
971
  resize_canvas: (width: number, height: number) => void;
928
972
  reset: () => void;
929
973
  set_background_image: (image: Sprite) => void;
@@ -1034,7 +1078,12 @@ export class ImageEditor {
1034
1078
  };
1035
1079
  layers: Writable<{
1036
1080
  active_layer: string;
1037
- layers: { name: string; id: string; user_created: boolean }[];
1081
+ layers: {
1082
+ name: string;
1083
+ id: string;
1084
+ user_created: boolean;
1085
+ visible: boolean;
1086
+ }[];
1038
1087
  }> = writable({
1039
1088
  active_layer: "",
1040
1089
  layers: []
@@ -1051,14 +1100,15 @@ export class ImageEditor {
1051
1100
  private overlay_container!: Container;
1052
1101
  private overlay_graphics!: Graphics;
1053
1102
  private pad_bottom: number;
1103
+ private theme_mode: "dark" | "light";
1054
1104
  constructor(options: ImageEditorOptions) {
1055
1105
  this.pad_bottom = options.pad_bottom || 0;
1056
1106
  this.dark = options.dark || false;
1107
+ this.theme_mode = options.theme_mode || "dark";
1057
1108
  this.target_element = options.target_element;
1058
1109
  this.width = options.width;
1059
1110
  this.height = options.height;
1060
1111
  this.command_manager = new CommandManager();
1061
- // this.layers = writable([]);
1062
1112
  this.ready = new Promise((resolve) => {
1063
1113
  this.ready_resolve = resolve;
1064
1114
  });
@@ -1142,7 +1192,7 @@ export class ImageEditor {
1142
1192
  width: container_box.width,
1143
1193
  height: container_box.height,
1144
1194
  backgroundAlpha: this.dark ? 0 : 1,
1145
- backgroundColor: this.dark ? 0x333333 : 0xffffff,
1195
+ backgroundColor: this.theme_mode === "dark" ? "#27272a" : "#ffffff",
1146
1196
  resolution: window.devicePixelRatio,
1147
1197
  autoDensity: true,
1148
1198
  antialias: true,
@@ -1385,12 +1435,6 @@ export class ImageEditor {
1385
1435
  this.layers = this.layer_manager.layer_store;
1386
1436
  }
1387
1437
 
1388
- // private resize_image(width: number, height: number): void {
1389
- // // Implement image resizing logic here
1390
- // this.image_container.width = width;
1391
- // this.image_container.height = height;
1392
- // }
1393
-
1394
1438
  resize_canvas(width: number, height: number): void {
1395
1439
  if (this.app.renderer) {
1396
1440
  this.app.renderer.resize(width, height);
@@ -1417,33 +1461,42 @@ export class ImageEditor {
1417
1461
  }
1418
1462
  }
1419
1463
 
1420
- set_image_properties(properties: {
1464
+ async set_image_properties(properties: {
1421
1465
  width?: number;
1422
1466
  height?: number;
1423
1467
  scale?: number;
1424
1468
  position?: { x: number; y: number };
1425
1469
  animate?: boolean;
1426
- }): void {
1470
+ }): Promise<void> {
1427
1471
  let hard =
1428
1472
  typeof properties.animate !== "undefined" ? !properties.animate : true;
1429
1473
  if (properties.position) {
1430
- this.position.set(properties.position, { hard });
1474
+ const pos = this.position.set(properties.position, { hard });
1475
+ if (hard) {
1476
+ await pos;
1477
+ }
1431
1478
  }
1432
1479
  if (properties.scale) {
1433
- this.scale.set(properties.scale, { hard });
1480
+ const scale = this.scale.set(properties.scale, { hard });
1481
+ if (hard) {
1482
+ await scale;
1483
+ }
1434
1484
  }
1435
1485
  if (properties.width && properties.height) {
1436
1486
  this.width = properties.width;
1437
1487
  this.height = properties.height;
1438
- this.dimensions.set(
1488
+ const dimensions = this.dimensions.set(
1439
1489
  { width: properties.width, height: properties.height },
1440
1490
  { hard }
1441
1491
  );
1492
+ if (hard) {
1493
+ await dimensions;
1494
+ }
1442
1495
  }
1443
1496
  }
1444
1497
 
1445
- execute_command(command: Command): void {
1446
- this.command_manager.execute(command);
1498
+ async execute_command(command: Command): Promise<void> {
1499
+ await this.command_manager.execute(command);
1447
1500
  }
1448
1501
 
1449
1502
  undo(): void {
@@ -1463,15 +1516,12 @@ export class ImageEditor {
1463
1516
  image: Blob | File;
1464
1517
  resize?: boolean;
1465
1518
  }): Promise<void> {
1466
- const image_command = new AddImageCommand(
1467
- this.context,
1519
+ const image_tool = this.tools.get("image") as ImageTool;
1520
+ await image_tool.add_image({
1468
1521
  image,
1469
- this.fixed_canvas,
1470
- this.border_region
1471
- );
1472
-
1473
- await image_command.start();
1474
- this.context.execute_command(image_command);
1522
+ fixed_canvas: this.fixed_canvas,
1523
+ border_region: this.border_region
1524
+ });
1475
1525
  }
1476
1526
 
1477
1527
  /**
@@ -1517,14 +1567,14 @@ export class ImageEditor {
1517
1567
  this.background_image = image;
1518
1568
  }
1519
1569
 
1520
- reset_canvas(): void {
1570
+ async reset_canvas(): Promise<void> {
1521
1571
  this.layer_manager.reset_layers(this.width, this.height);
1522
1572
  // Clear background image
1523
1573
  this.background_image = undefined;
1524
1574
  this.background_image_present.set(false);
1525
1575
 
1526
1576
  // Reset position, scale and dimensions to default values
1527
- this.set_image_properties({
1577
+ await this.set_image_properties({
1528
1578
  width: this.width,
1529
1579
  height: this.height,
1530
1580
  scale: 1,
@@ -1559,7 +1609,9 @@ export class ImageEditor {
1559
1609
  width: this.width,
1560
1610
  height: this.height,
1561
1611
  layer_name: undefined,
1562
- user_created: true
1612
+ user_created: true,
1613
+ layer_id: undefined,
1614
+ make_active: true
1563
1615
  });
1564
1616
  this.notify("change");
1565
1617
  }
@@ -1586,6 +1638,8 @@ export class ImageEditor {
1586
1638
  await this.layer_manager.add_layer_from_url(url);
1587
1639
  }
1588
1640
 
1641
+ this.layer_manager.set_active_layer(_layers[0].id);
1642
+
1589
1643
  // Update resize tool if present with border region
1590
1644
  const resize_tool = this.tools.get("resize") as any;
1591
1645
  if (resize_tool && typeof resize_tool.set_border_region === "function") {
@@ -1727,4 +1781,8 @@ export class ImageEditor {
1727
1781
  this.height
1728
1782
  );
1729
1783
  }
1784
+
1785
+ toggle_layer_visibility(id: string): void {
1786
+ this.layer_manager.toggle_layer_visibility(id);
1787
+ }
1730
1788
  }
@@ -52,8 +52,7 @@ export class ImageTool implements Tool {
52
52
  border_region
53
53
  );
54
54
 
55
- await image_command.start();
56
- this.context.execute_command(image_command);
55
+ await this.context.execute_command(image_command);
57
56
  }
58
57
 
59
58
  set_tool(tool: ToolbarTool, subtool: Subtool): void {
@@ -162,7 +161,7 @@ export class AddImageCommand implements BgImageCommand {
162
161
  const [width, height] = await this.start();
163
162
 
164
163
  // Update image properties with the original dimensions and center in viewport
165
- this.context.set_image_properties({
164
+ await this.context.set_image_properties({
166
165
  scale: 1, // Start at 1:1 scale
167
166
  position: {
168
167
  x: this.context.app.screen.width / 2,
@@ -196,7 +195,6 @@ export class AddImageCommand implements BgImageCommand {
196
195
  if (this.sprite) {
197
196
  this.sprite.destroy();
198
197
  }
199
- // The background layer will be automatically cleaned up when a new image is added
200
198
  }
201
199
  }
202
200
 
package/shared/types.ts CHANGED
@@ -12,3 +12,8 @@ export interface LayerOptions {
12
12
 
13
13
  export type Source = "upload" | "webcam" | "clipboard";
14
14
  export type Transform = "crop" | "resize";
15
+
16
+ export interface WebcamOptions {
17
+ mirror: boolean;
18
+ constraints: Record<string, any>;
19
+ }
@@ -142,7 +142,7 @@ export class ZoomTool implements Tool {
142
142
  this.setup_event_listeners();
143
143
 
144
144
  // Update context with initial values
145
- this.image_editor_context.set_image_properties({
145
+ await this.image_editor_context.set_image_properties({
146
146
  scale: min_zoom,
147
147
  position: { x: initial_x, y: initial_y }
148
148
  });