@gradio/imageeditor 0.20.2 → 0.21.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 +20 -0
- package/ImageEditor.test.ts +587 -1
- package/ImageEditorTestWrapper.svelte +56 -25
- package/dist/ImageEditorTestWrapper.svelte +56 -25
- package/dist/ImageEditorTestWrapper.svelte.d.ts +2 -1
- package/dist/shared/resize/resize.js +10 -1
- package/package.json +8 -8
- package/shared/resize/resize.ts +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @gradio/imageeditor
|
|
2
2
|
|
|
3
|
+
## 0.21.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- [`96a79b1`](https://github.com/gradio-app/gradio/commit/96a79b19c8b4a0634c9ab4a969460c0ad55ddf77) - Refactor the remaining component unit tests and remove components.test.ts. Thanks @abidlabs!
|
|
8
|
+
|
|
9
|
+
### Dependency updates
|
|
10
|
+
|
|
11
|
+
- @gradio/upload@0.19.0
|
|
12
|
+
- @gradio/statustracker@0.15.4
|
|
13
|
+
- @gradio/client@2.7.1
|
|
14
|
+
- @gradio/image@0.28.4
|
|
15
|
+
|
|
16
|
+
## 0.20.2
|
|
17
|
+
|
|
18
|
+
### Dependency updates
|
|
19
|
+
|
|
20
|
+
- @gradio/utils@0.14.2
|
|
21
|
+
- @gradio/client@2.7.0
|
|
22
|
+
|
|
3
23
|
## 0.20.2
|
|
4
24
|
|
|
5
25
|
### Fixes
|
package/ImageEditor.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
cleanup,
|
|
4
4
|
fireEvent,
|
|
@@ -127,3 +127,589 @@ describe("Internationalization", () => {
|
|
|
127
127
|
expect(getByRole("button", { name: "Añadir capa" })).toBeVisible();
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
describe("Static mode", () => {
|
|
132
|
+
afterEach(() => cleanup());
|
|
133
|
+
|
|
134
|
+
const static_props = { ...default_props, interactive: false };
|
|
135
|
+
|
|
136
|
+
test("renders the composite image", async () => {
|
|
137
|
+
const { getByRole } = await render(ImageEditor, {
|
|
138
|
+
...static_props,
|
|
139
|
+
value: { background: null, layers: [], composite: TEST_PNG }
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// The static branch renders the composite through StaticImage without any
|
|
143
|
+
// alt text, so the <img> is exposed as decorative rather than role="img".
|
|
144
|
+
await waitFor(() => expect(getByRole("presentation")).toBeVisible());
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("renders no editing toolbar", async () => {
|
|
148
|
+
const { queryByRole } = await render(ImageEditor, {
|
|
149
|
+
...static_props,
|
|
150
|
+
value: { background: null, layers: [], composite: TEST_PNG }
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(
|
|
154
|
+
queryByRole("button", { name: "image_editor.brush" })
|
|
155
|
+
).not.toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("renders the label", async () => {
|
|
159
|
+
const { getByText } = await render(ImageEditor, {
|
|
160
|
+
...static_props,
|
|
161
|
+
label: "Sketch pad",
|
|
162
|
+
show_label: true,
|
|
163
|
+
value: { background: null, layers: [], composite: TEST_PNG }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await waitFor(() => expect(getByText("Sketch pad")).toBeVisible());
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("renders an empty component when there is no value", async () => {
|
|
170
|
+
const { queryByRole } = await render(ImageEditor, {
|
|
171
|
+
...static_props,
|
|
172
|
+
value: null
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(queryByRole("presentation")).not.toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("clear_status is dispatched when the error status is dismissed", async () => {
|
|
179
|
+
const { listen, getByLabelText } = await render(ImageEditor, {
|
|
180
|
+
...static_props,
|
|
181
|
+
loading_status: {
|
|
182
|
+
status: "error",
|
|
183
|
+
queue_position: null,
|
|
184
|
+
queue_size: null,
|
|
185
|
+
eta: null,
|
|
186
|
+
message: "it broke",
|
|
187
|
+
show_progress: "full",
|
|
188
|
+
scroll_to_output: false,
|
|
189
|
+
visible: true,
|
|
190
|
+
fn_index: 0
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const clear_status = listen("clear_status");
|
|
194
|
+
|
|
195
|
+
await fireEvent.click(getByLabelText("common.clear"));
|
|
196
|
+
|
|
197
|
+
expect(clear_status).toHaveBeenCalledTimes(1);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("Interactive mode", () => {
|
|
202
|
+
afterEach(() => cleanup());
|
|
203
|
+
|
|
204
|
+
async function render_editor(
|
|
205
|
+
overrides: Record<string, any> = {}
|
|
206
|
+
): Promise<Awaited<ReturnType<typeof render>>> {
|
|
207
|
+
const view = await render(ImageEditor, {
|
|
208
|
+
...default_props,
|
|
209
|
+
client: mock_client(),
|
|
210
|
+
...overrides
|
|
211
|
+
});
|
|
212
|
+
await waitFor(
|
|
213
|
+
() => {
|
|
214
|
+
expect(
|
|
215
|
+
view.getByRole("button", { name: "image_editor.pan" })
|
|
216
|
+
).toBeVisible();
|
|
217
|
+
},
|
|
218
|
+
{ timeout: 5000 }
|
|
219
|
+
);
|
|
220
|
+
return view;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
test("renders the canvas controls", async () => {
|
|
224
|
+
const { getByRole } = await render_editor();
|
|
225
|
+
|
|
226
|
+
expect(getByRole("button", { name: "image_editor.pan" })).toBeVisible();
|
|
227
|
+
expect(
|
|
228
|
+
getByRole("button", { name: "image_editor.zoom_out" })
|
|
229
|
+
).toBeVisible();
|
|
230
|
+
expect(getByRole("button", { name: "common.undo" })).toBeVisible();
|
|
231
|
+
expect(
|
|
232
|
+
getByRole("button", { name: "image_editor.clear_canvas" })
|
|
233
|
+
).toBeVisible();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("renders the drawing tools", async () => {
|
|
237
|
+
const { getByRole } = await render_editor();
|
|
238
|
+
|
|
239
|
+
expect(getByRole("button", { name: "image_editor.brush" })).toBeVisible();
|
|
240
|
+
expect(getByRole("button", { name: "image_editor.erase" })).toBeVisible();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("undo and redo start disabled on a fresh canvas", async () => {
|
|
244
|
+
const { getByRole } = await render_editor();
|
|
245
|
+
|
|
246
|
+
expect(getByRole("button", { name: "common.undo" })).toBeDisabled();
|
|
247
|
+
expect(getByRole("button", { name: "image_editor.redo" })).toBeDisabled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("renders the label", async () => {
|
|
251
|
+
const { getByText } = await render_editor({
|
|
252
|
+
label: "Sketch pad",
|
|
253
|
+
show_label: true
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(getByText("Sketch pad")).toBeVisible();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("the download button is shown by default", async () => {
|
|
260
|
+
const { getByRole } = await render_editor({ buttons: null });
|
|
261
|
+
|
|
262
|
+
expect(getByRole("button", { name: "common.download" })).toBeVisible();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("an empty buttons list hides the download button", async () => {
|
|
266
|
+
const { queryByRole } = await render_editor({ buttons: [] });
|
|
267
|
+
|
|
268
|
+
expect(
|
|
269
|
+
queryByRole("button", { name: "common.download" })
|
|
270
|
+
).not.toBeInTheDocument();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("buttons: ['download'] shows the download button", async () => {
|
|
274
|
+
const { getByRole } = await render_editor({ buttons: ["download"] });
|
|
275
|
+
|
|
276
|
+
expect(getByRole("button", { name: "common.download" })).toBeVisible();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("the save button only appears when an apply listener is attached", async () => {
|
|
280
|
+
const { queryByRole } = await render_editor({ attached_events: [] });
|
|
281
|
+
|
|
282
|
+
expect(
|
|
283
|
+
queryByRole("button", { name: "image_editor.save_changes" })
|
|
284
|
+
).not.toBeInTheDocument();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("attaching an apply listener shows the save button", async () => {
|
|
288
|
+
const { getByRole } = await render_editor({ attached_events: ["apply"] });
|
|
289
|
+
|
|
290
|
+
expect(
|
|
291
|
+
getByRole("button", { name: "image_editor.save_changes" })
|
|
292
|
+
).toBeVisible();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("Toolbar: sources", () => {
|
|
297
|
+
afterEach(() => cleanup());
|
|
298
|
+
|
|
299
|
+
test("all three sources render their own button", async () => {
|
|
300
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
301
|
+
kind: "toolbar",
|
|
302
|
+
sources: ["upload", "clipboard", "webcam"]
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(getByRole("button", { name: "image_editor.upload" })).toBeVisible();
|
|
306
|
+
expect(getByRole("button", { name: "image_editor.paste" })).toBeVisible();
|
|
307
|
+
expect(getByRole("button", { name: "image_editor.webcam" })).toBeVisible();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("only the listed sources are offered", async () => {
|
|
311
|
+
const { getByRole, queryByRole } = await render(ImageEditorTestWrapper, {
|
|
312
|
+
kind: "toolbar",
|
|
313
|
+
sources: ["upload"]
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(getByRole("button", { name: "image_editor.upload" })).toBeVisible();
|
|
317
|
+
expect(
|
|
318
|
+
queryByRole("button", { name: "image_editor.webcam" })
|
|
319
|
+
).not.toBeInTheDocument();
|
|
320
|
+
expect(
|
|
321
|
+
queryByRole("button", { name: "image_editor.paste" })
|
|
322
|
+
).not.toBeInTheDocument();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("no sources and no transforms hides the image tool entirely", async () => {
|
|
326
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
327
|
+
kind: "toolbar",
|
|
328
|
+
sources: [],
|
|
329
|
+
transforms: []
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(
|
|
333
|
+
queryByRole("button", { name: "image.image" })
|
|
334
|
+
).not.toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("a transform keeps the image tool available once there is a background", async () => {
|
|
338
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
339
|
+
kind: "toolbar",
|
|
340
|
+
sources: [],
|
|
341
|
+
transforms: ["crop"],
|
|
342
|
+
background: true
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(getByRole("button", { name: "image.image" })).toBeVisible();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("Toolbar: transforms", () => {
|
|
350
|
+
afterEach(() => cleanup());
|
|
351
|
+
|
|
352
|
+
test("crop and resize are offered for an image with a background", async () => {
|
|
353
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
354
|
+
kind: "toolbar",
|
|
355
|
+
transforms: ["crop", "resize"],
|
|
356
|
+
background: true
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(getByRole("button", { name: "image_editor.crop" })).toBeVisible();
|
|
360
|
+
expect(getByRole("button", { name: "image_editor.resize" })).toBeVisible();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("only the listed transforms are offered", async () => {
|
|
364
|
+
const { getByRole, queryByRole } = await render(ImageEditorTestWrapper, {
|
|
365
|
+
kind: "toolbar",
|
|
366
|
+
transforms: ["crop"],
|
|
367
|
+
background: true
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(getByRole("button", { name: "image_editor.crop" })).toBeVisible();
|
|
371
|
+
expect(
|
|
372
|
+
queryByRole("button", { name: "image_editor.resize" })
|
|
373
|
+
).not.toBeInTheDocument();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("transforms are not offered until there is a background to transform", async () => {
|
|
377
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
378
|
+
kind: "toolbar",
|
|
379
|
+
transforms: ["crop", "resize"],
|
|
380
|
+
background: false
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(
|
|
384
|
+
queryByRole("button", { name: "image_editor.crop" })
|
|
385
|
+
).not.toBeInTheDocument();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("Toolbar: brush and eraser", () => {
|
|
390
|
+
afterEach(() => cleanup());
|
|
391
|
+
|
|
392
|
+
test("brush: false removes the brush tool", async () => {
|
|
393
|
+
const { queryByRole, getByRole } = await render(ImageEditorTestWrapper, {
|
|
394
|
+
kind: "toolbar",
|
|
395
|
+
brush_options: false
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(
|
|
399
|
+
queryByRole("button", { name: "image_editor.brush" })
|
|
400
|
+
).not.toBeInTheDocument();
|
|
401
|
+
expect(getByRole("button", { name: "image_editor.erase" })).toBeVisible();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("eraser: false removes the eraser tool", async () => {
|
|
405
|
+
const { queryByRole, getByRole } = await render(ImageEditorTestWrapper, {
|
|
406
|
+
kind: "toolbar",
|
|
407
|
+
eraser_options: false
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
expect(
|
|
411
|
+
queryByRole("button", { name: "image_editor.erase" })
|
|
412
|
+
).not.toBeInTheDocument();
|
|
413
|
+
expect(getByRole("button", { name: "image_editor.brush" })).toBeVisible();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("clicking the brush asks the editor to switch to the draw tool", async () => {
|
|
417
|
+
const ontool_change = vi.fn();
|
|
418
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
419
|
+
kind: "toolbar",
|
|
420
|
+
ontool_change
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
await fireEvent.click(getByRole("button", { name: "image_editor.brush" }));
|
|
424
|
+
|
|
425
|
+
expect(ontool_change).toHaveBeenCalledWith({ tool: "draw" });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("clicking the eraser asks the editor to switch to the erase tool", async () => {
|
|
429
|
+
const ontool_change = vi.fn();
|
|
430
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
431
|
+
kind: "toolbar",
|
|
432
|
+
ontool_change
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
await fireEvent.click(getByRole("button", { name: "image_editor.erase" }));
|
|
436
|
+
|
|
437
|
+
expect(ontool_change).toHaveBeenCalledWith({ tool: "erase" });
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("the draw tool offers colour and size controls", async () => {
|
|
441
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
442
|
+
kind: "toolbar",
|
|
443
|
+
tool: "draw"
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(getByRole("button", { name: "image_editor.color" })).toBeVisible();
|
|
447
|
+
expect(
|
|
448
|
+
getByRole("button", { name: "image_editor.brush_size" })
|
|
449
|
+
).toBeVisible();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("the erase tool offers a size control", async () => {
|
|
453
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
454
|
+
kind: "toolbar",
|
|
455
|
+
tool: "erase"
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(
|
|
459
|
+
getByRole("button", { name: "image_editor.eraser_size" })
|
|
460
|
+
).toBeVisible();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("choosing the brush size sub-tool is reported to the editor", async () => {
|
|
464
|
+
const onsubtool_change = vi.fn();
|
|
465
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
466
|
+
kind: "toolbar",
|
|
467
|
+
tool: "draw",
|
|
468
|
+
onsubtool_change
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await fireEvent.click(
|
|
472
|
+
getByRole("button", { name: "image_editor.brush_size" })
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
expect(onsubtool_change).toHaveBeenCalledWith({
|
|
476
|
+
tool: "draw",
|
|
477
|
+
subtool: "size"
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe("Controls", () => {
|
|
483
|
+
afterEach(() => cleanup());
|
|
484
|
+
|
|
485
|
+
test("undo is disabled until there is something to undo", async () => {
|
|
486
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
487
|
+
kind: "controls",
|
|
488
|
+
can_undo: false
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
expect(getByRole("button", { name: "common.undo" })).toBeDisabled();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("undo is enabled when there is something to undo", async () => {
|
|
495
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
496
|
+
kind: "controls",
|
|
497
|
+
can_undo: true
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(getByRole("button", { name: "common.undo" })).toBeEnabled();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("clicking undo calls back", async () => {
|
|
504
|
+
const onundo = vi.fn();
|
|
505
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
506
|
+
kind: "controls",
|
|
507
|
+
can_undo: true,
|
|
508
|
+
onundo
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await fireEvent.click(getByRole("button", { name: "common.undo" }));
|
|
512
|
+
|
|
513
|
+
expect(onundo).toHaveBeenCalledTimes(1);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("clicking redo calls back", async () => {
|
|
517
|
+
const onredo = vi.fn();
|
|
518
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
519
|
+
kind: "controls",
|
|
520
|
+
can_redo: true,
|
|
521
|
+
onredo
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
await fireEvent.click(getByRole("button", { name: "image_editor.redo" }));
|
|
525
|
+
|
|
526
|
+
expect(onredo).toHaveBeenCalledTimes(1);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("clicking clear calls back", async () => {
|
|
530
|
+
const onremove_image = vi.fn();
|
|
531
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
532
|
+
kind: "controls",
|
|
533
|
+
onremove_image
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
await fireEvent.click(
|
|
537
|
+
getByRole("button", { name: "image_editor.clear_canvas" })
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
expect(onremove_image).toHaveBeenCalledTimes(1);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("enable_download: false removes the download button", async () => {
|
|
544
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
545
|
+
kind: "controls",
|
|
546
|
+
enable_download: false
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
expect(
|
|
550
|
+
queryByRole("button", { name: "common.download" })
|
|
551
|
+
).not.toBeInTheDocument();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("changeable: false removes the save button", async () => {
|
|
555
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
556
|
+
kind: "controls",
|
|
557
|
+
changeable: false
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
expect(
|
|
561
|
+
queryByRole("button", { name: "image_editor.save_changes" })
|
|
562
|
+
).not.toBeInTheDocument();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("save is disabled until there are changes to save", async () => {
|
|
566
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
567
|
+
kind: "controls",
|
|
568
|
+
changeable: true,
|
|
569
|
+
can_save: false
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(
|
|
573
|
+
getByRole("button", { name: "image_editor.save_changes" })
|
|
574
|
+
).toBeDisabled();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("clicking save calls back", async () => {
|
|
578
|
+
const onsave = vi.fn();
|
|
579
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
580
|
+
kind: "controls",
|
|
581
|
+
changeable: true,
|
|
582
|
+
can_save: true,
|
|
583
|
+
onsave
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
await fireEvent.click(
|
|
587
|
+
getByRole("button", { name: "image_editor.save_changes" })
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
expect(onsave).toHaveBeenCalledTimes(1);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("clicking zoom out calls back", async () => {
|
|
594
|
+
const onzoom_out = vi.fn();
|
|
595
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
596
|
+
kind: "controls",
|
|
597
|
+
onzoom_out
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
await fireEvent.click(
|
|
601
|
+
getByRole("button", { name: "image_editor.zoom_out" })
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
expect(onzoom_out).toHaveBeenCalledTimes(1);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("clicking pan calls back", async () => {
|
|
608
|
+
const onpan = vi.fn();
|
|
609
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
610
|
+
kind: "controls",
|
|
611
|
+
onpan
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
await fireEvent.click(getByRole("button", { name: "image_editor.pan" }));
|
|
615
|
+
|
|
616
|
+
expect(onpan).toHaveBeenCalledTimes(1);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
describe("Layers", () => {
|
|
621
|
+
afterEach(() => cleanup());
|
|
622
|
+
|
|
623
|
+
const named_layers = {
|
|
624
|
+
active_layer: "layer-1",
|
|
625
|
+
layers: [
|
|
626
|
+
{ name: "Background", id: "layer-1", user_created: false, visible: true },
|
|
627
|
+
{ name: "Sketch", id: "layer-2", user_created: true, visible: true }
|
|
628
|
+
]
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
test("the active layer is named in the toolbar", async () => {
|
|
632
|
+
const { getByText } = await render(ImageEditorTestWrapper, {
|
|
633
|
+
kind: "layers",
|
|
634
|
+
layer_store: named_layers
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
expect(getByText("Background")).toBeVisible();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("opening the layer panel lists every layer", async () => {
|
|
641
|
+
const { getByRole, getAllByText } = await render(ImageEditorTestWrapper, {
|
|
642
|
+
kind: "layers",
|
|
643
|
+
layer_store: named_layers
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
await fireEvent.click(
|
|
647
|
+
getByRole("button", { name: "image_editor.show_layers" })
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
expect(getAllByText("Sketch").length).toBeGreaterThan(0);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("auto-generated layer names are translated", async () => {
|
|
654
|
+
const { getByText } = await render(ImageEditorTestWrapper, {
|
|
655
|
+
kind: "layers",
|
|
656
|
+
i18n: spanish_i18n
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
expect(getByText("Capa 1")).toBeVisible();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("a user-supplied layer name is shown verbatim", async () => {
|
|
663
|
+
const { getByText } = await render(ImageEditorTestWrapper, {
|
|
664
|
+
kind: "layers",
|
|
665
|
+
i18n: spanish_i18n,
|
|
666
|
+
layer_store: named_layers
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(getByText("Background")).toBeVisible();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("a new layer can be added when additional layers are allowed", async () => {
|
|
673
|
+
const onnew_layer = vi.fn();
|
|
674
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
675
|
+
kind: "layers",
|
|
676
|
+
enable_additional_layers: true,
|
|
677
|
+
onnew_layer
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
await fireEvent.click(
|
|
681
|
+
getByRole("button", { name: "image_editor.show_layers" })
|
|
682
|
+
);
|
|
683
|
+
await fireEvent.click(
|
|
684
|
+
getByRole("button", { name: "image_editor.add_layer" })
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
expect(onnew_layer).toHaveBeenCalledTimes(1);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("the add layer button is hidden when additional layers are disallowed", async () => {
|
|
691
|
+
const { getByRole, queryByRole } = await render(ImageEditorTestWrapper, {
|
|
692
|
+
kind: "layers",
|
|
693
|
+
enable_additional_layers: false
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
await fireEvent.click(
|
|
697
|
+
getByRole("button", { name: "image_editor.show_layers" })
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
expect(
|
|
701
|
+
queryByRole("button", { name: "image_editor.add_layer" })
|
|
702
|
+
).not.toBeInTheDocument();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("enable_layers: false renders no layer controls at all", async () => {
|
|
706
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
707
|
+
kind: "layers",
|
|
708
|
+
enable_layers: false
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
expect(
|
|
712
|
+
queryByRole("button", { name: "image_editor.show_layers" })
|
|
713
|
+
).not.toBeInTheDocument();
|
|
714
|
+
});
|
|
715
|
+
});
|
|
@@ -10,41 +10,72 @@
|
|
|
10
10
|
}: {
|
|
11
11
|
props: {
|
|
12
12
|
kind: "controls" | "toolbar" | "layers";
|
|
13
|
-
i18n
|
|
13
|
+
i18n?: I18nFormatter;
|
|
14
|
+
[key: string]: any;
|
|
14
15
|
};
|
|
15
16
|
} = $props();
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
]
|
|
22
|
-
|
|
18
|
+
const identity: I18nFormatter = (s: string) => s;
|
|
19
|
+
|
|
20
|
+
const default_brush = {
|
|
21
|
+
default_size: "auto" as const,
|
|
22
|
+
colors: ["#000000"],
|
|
23
|
+
default_color: "#000000",
|
|
24
|
+
color_mode: "defaults" as const
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const layers = writable(
|
|
28
|
+
props.layer_store ?? {
|
|
29
|
+
active_layer: "layer-1",
|
|
30
|
+
layers: [
|
|
31
|
+
{ name: "Layer 1", id: "layer-1", user_created: false, visible: true }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
);
|
|
23
35
|
</script>
|
|
24
36
|
|
|
25
37
|
{#if props.kind === "controls"}
|
|
26
38
|
<Controls
|
|
27
|
-
i18n={props.i18n}
|
|
28
|
-
tool="image"
|
|
29
|
-
can_undo={false}
|
|
30
|
-
can_redo={false}
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
i18n={props.i18n ?? identity}
|
|
40
|
+
tool={props.tool ?? "image"}
|
|
41
|
+
can_undo={props.can_undo ?? false}
|
|
42
|
+
can_redo={props.can_redo ?? false}
|
|
43
|
+
can_save={props.can_save ?? false}
|
|
44
|
+
enable_download={props.enable_download ?? true}
|
|
45
|
+
changeable={props.changeable ?? true}
|
|
46
|
+
current_zoom={props.current_zoom ?? 1}
|
|
47
|
+
onundo={props.onundo}
|
|
48
|
+
onredo={props.onredo}
|
|
49
|
+
onsave={props.onsave}
|
|
50
|
+
onremove_image={props.onremove_image}
|
|
51
|
+
ondownload={props.ondownload}
|
|
52
|
+
onzoom_in={props.onzoom_in}
|
|
53
|
+
onzoom_out={props.onzoom_out}
|
|
54
|
+
onpan={props.onpan}
|
|
33
55
|
/>
|
|
34
56
|
{:else if props.kind === "toolbar"}
|
|
35
57
|
<Toolbar
|
|
36
|
-
i18n={props.i18n}
|
|
37
|
-
brush_options={
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
i18n={props.i18n ?? identity}
|
|
59
|
+
brush_options={props.brush_options === undefined
|
|
60
|
+
? default_brush
|
|
61
|
+
: props.brush_options}
|
|
62
|
+
eraser_options={props.eraser_options === undefined
|
|
63
|
+
? { default_size: "auto" }
|
|
64
|
+
: props.eraser_options}
|
|
65
|
+
sources={props.sources ?? ["upload", "clipboard", "webcam"]}
|
|
66
|
+
transforms={props.transforms ?? []}
|
|
67
|
+
background={props.background ?? false}
|
|
68
|
+
tool={props.tool ?? "image"}
|
|
69
|
+
subtool={props.subtool ?? null}
|
|
70
|
+
ontool_change={props.ontool_change}
|
|
71
|
+
onsubtool_change={props.onsubtool_change}
|
|
47
72
|
/>
|
|
48
73
|
{:else}
|
|
49
|
-
<SecondaryToolbar
|
|
74
|
+
<SecondaryToolbar
|
|
75
|
+
i18n={props.i18n ?? identity}
|
|
76
|
+
{layers}
|
|
77
|
+
enable_layers={props.enable_layers ?? true}
|
|
78
|
+
enable_additional_layers={props.enable_additional_layers ?? true}
|
|
79
|
+
onnew_layer={props.onnew_layer}
|
|
80
|
+
/>
|
|
50
81
|
{/if}
|
|
@@ -10,41 +10,72 @@
|
|
|
10
10
|
}: {
|
|
11
11
|
props: {
|
|
12
12
|
kind: "controls" | "toolbar" | "layers";
|
|
13
|
-
i18n
|
|
13
|
+
i18n?: I18nFormatter;
|
|
14
|
+
[key: string]: any;
|
|
14
15
|
};
|
|
15
16
|
} = $props();
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
]
|
|
22
|
-
|
|
18
|
+
const identity: I18nFormatter = (s: string) => s;
|
|
19
|
+
|
|
20
|
+
const default_brush = {
|
|
21
|
+
default_size: "auto" as const,
|
|
22
|
+
colors: ["#000000"],
|
|
23
|
+
default_color: "#000000",
|
|
24
|
+
color_mode: "defaults" as const
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const layers = writable(
|
|
28
|
+
props.layer_store ?? {
|
|
29
|
+
active_layer: "layer-1",
|
|
30
|
+
layers: [
|
|
31
|
+
{ name: "Layer 1", id: "layer-1", user_created: false, visible: true }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
);
|
|
23
35
|
</script>
|
|
24
36
|
|
|
25
37
|
{#if props.kind === "controls"}
|
|
26
38
|
<Controls
|
|
27
|
-
i18n={props.i18n}
|
|
28
|
-
tool="image"
|
|
29
|
-
can_undo={false}
|
|
30
|
-
can_redo={false}
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
i18n={props.i18n ?? identity}
|
|
40
|
+
tool={props.tool ?? "image"}
|
|
41
|
+
can_undo={props.can_undo ?? false}
|
|
42
|
+
can_redo={props.can_redo ?? false}
|
|
43
|
+
can_save={props.can_save ?? false}
|
|
44
|
+
enable_download={props.enable_download ?? true}
|
|
45
|
+
changeable={props.changeable ?? true}
|
|
46
|
+
current_zoom={props.current_zoom ?? 1}
|
|
47
|
+
onundo={props.onundo}
|
|
48
|
+
onredo={props.onredo}
|
|
49
|
+
onsave={props.onsave}
|
|
50
|
+
onremove_image={props.onremove_image}
|
|
51
|
+
ondownload={props.ondownload}
|
|
52
|
+
onzoom_in={props.onzoom_in}
|
|
53
|
+
onzoom_out={props.onzoom_out}
|
|
54
|
+
onpan={props.onpan}
|
|
33
55
|
/>
|
|
34
56
|
{:else if props.kind === "toolbar"}
|
|
35
57
|
<Toolbar
|
|
36
|
-
i18n={props.i18n}
|
|
37
|
-
brush_options={
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
i18n={props.i18n ?? identity}
|
|
59
|
+
brush_options={props.brush_options === undefined
|
|
60
|
+
? default_brush
|
|
61
|
+
: props.brush_options}
|
|
62
|
+
eraser_options={props.eraser_options === undefined
|
|
63
|
+
? { default_size: "auto" }
|
|
64
|
+
: props.eraser_options}
|
|
65
|
+
sources={props.sources ?? ["upload", "clipboard", "webcam"]}
|
|
66
|
+
transforms={props.transforms ?? []}
|
|
67
|
+
background={props.background ?? false}
|
|
68
|
+
tool={props.tool ?? "image"}
|
|
69
|
+
subtool={props.subtool ?? null}
|
|
70
|
+
ontool_change={props.ontool_change}
|
|
71
|
+
onsubtool_change={props.onsubtool_change}
|
|
47
72
|
/>
|
|
48
73
|
{:else}
|
|
49
|
-
<SecondaryToolbar
|
|
74
|
+
<SecondaryToolbar
|
|
75
|
+
i18n={props.i18n ?? identity}
|
|
76
|
+
{layers}
|
|
77
|
+
enable_layers={props.enable_layers ?? true}
|
|
78
|
+
enable_additional_layers={props.enable_additional_layers ?? true}
|
|
79
|
+
onnew_layer={props.onnew_layer}
|
|
80
|
+
/>
|
|
50
81
|
{/if}
|
|
@@ -2,7 +2,8 @@ import type { I18nFormatter } from "@gradio/utils";
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
props: {
|
|
4
4
|
kind: "controls" | "toolbar" | "layers";
|
|
5
|
-
i18n
|
|
5
|
+
i18n?: I18nFormatter;
|
|
6
|
+
[key: string]: any;
|
|
6
7
|
};
|
|
7
8
|
};
|
|
8
9
|
declare const ImageEditorTestWrapper: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -789,7 +789,16 @@ export class ResizeTool {
|
|
|
789
789
|
* Cleans up DOM event listeners
|
|
790
790
|
*/
|
|
791
791
|
cleanup_dom_event_listeners() {
|
|
792
|
-
|
|
792
|
+
// Pixi's `canvas` getter dereferences the renderer, which is null once the
|
|
793
|
+
// app has been destroyed — so optional chaining on `app` is not enough when
|
|
794
|
+
// the editor is torn down before this cleanup runs.
|
|
795
|
+
let canvas;
|
|
796
|
+
try {
|
|
797
|
+
canvas = this.image_editor_context?.app?.canvas;
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
canvas = undefined;
|
|
801
|
+
}
|
|
793
802
|
if (this.dom_mousedown_handler) {
|
|
794
803
|
canvas?.removeEventListener("mousedown", this.dom_mousedown_handler);
|
|
795
804
|
this.dom_mousedown_handler = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradio/imageeditor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Gradio UI packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"pixi-filters": "^6.1.4",
|
|
12
12
|
"pixi.js": "^8.14.0",
|
|
13
13
|
"tinycolor2": "^1.6.0",
|
|
14
|
-
"@gradio/client": "^2.6.0",
|
|
15
|
-
"@gradio/image": "^0.28.3",
|
|
16
|
-
"@gradio/icons": "^0.16.0",
|
|
17
|
-
"@gradio/statustracker": "^0.15.3",
|
|
18
|
-
"@gradio/utils": "^0.14.1",
|
|
19
14
|
"@gradio/atoms": "^0.26.2",
|
|
20
|
-
"@gradio/
|
|
15
|
+
"@gradio/icons": "^0.16.0",
|
|
16
|
+
"@gradio/image": "^0.28.4",
|
|
17
|
+
"@gradio/upload": "^0.19.0",
|
|
18
|
+
"@gradio/statustracker": "^0.15.4",
|
|
19
|
+
"@gradio/client": "^2.7.1",
|
|
20
|
+
"@gradio/utils": "^0.14.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@gradio/preview": "^0.17.
|
|
23
|
+
"@gradio/preview": "^0.17.1"
|
|
24
24
|
},
|
|
25
25
|
"main_changeset": true,
|
|
26
26
|
"main": "./Index.svelte",
|
package/shared/resize/resize.ts
CHANGED
|
@@ -1078,7 +1078,15 @@ export class ResizeTool implements Tool {
|
|
|
1078
1078
|
* Cleans up DOM event listeners
|
|
1079
1079
|
*/
|
|
1080
1080
|
private cleanup_dom_event_listeners(): void {
|
|
1081
|
-
|
|
1081
|
+
// Pixi's `canvas` getter dereferences the renderer, which is null once the
|
|
1082
|
+
// app has been destroyed — so optional chaining on `app` is not enough when
|
|
1083
|
+
// the editor is torn down before this cleanup runs.
|
|
1084
|
+
let canvas: HTMLCanvasElement | undefined;
|
|
1085
|
+
try {
|
|
1086
|
+
canvas = this.image_editor_context?.app?.canvas;
|
|
1087
|
+
} catch {
|
|
1088
|
+
canvas = undefined;
|
|
1089
|
+
}
|
|
1082
1090
|
|
|
1083
1091
|
if (this.dom_mousedown_handler) {
|
|
1084
1092
|
canvas?.removeEventListener("mousedown", this.dom_mousedown_handler);
|