@avament-pub/image-editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,773 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { IMAGE_EDITOR_COLORS, IMAGE_EDITOR_DEFAULT_TEXT_SIZE, IMAGE_EDITOR_DEFAULT_TEXT_WIDTH, defaultResolveSrc, editedAttachmentFileName, isShapeDrawTool, rotateImageDataUrl, shapeFillAndStroke, } from "./image-editor-utils";
4
+ import { useLatestRef } from "./use-latest-ref";
5
+ const HISTORY_LIMIT = 40;
6
+ const HISTORY_PROPS = ["isBackground", "annotationId"];
7
+ function isBackgroundObject(obj) {
8
+ return Boolean(obj === null || obj === void 0 ? void 0 : obj.isBackground);
9
+ }
10
+ function newAnnotationId() {
11
+ return `ann-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
12
+ }
13
+ export function useImageEditor({ src, fileName, labels, resolveSrc, onSave, }) {
14
+ var _a;
15
+ const hostRef = React.useRef(null);
16
+ const canvasRef = React.useRef(null);
17
+ const textInputRef = React.useRef(null);
18
+ const fabricRef = React.useRef(null);
19
+ const drawingRef = React.useRef(false);
20
+ const startRef = React.useRef(null);
21
+ const draftObjectRef = React.useRef(null);
22
+ const historyRef = React.useRef([]);
23
+ const historyIndexRef = React.useRef(-1);
24
+ const applyingHistoryRef = React.useRef(false);
25
+ const viewportRef = React.useRef({ width: 0, height: 0, zoom: 1 });
26
+ const [tool, setTool] = React.useState("pen");
27
+ const [color, setColor] = React.useState(IMAGE_EDITOR_COLORS[0]);
28
+ const [strokeWidth, setStrokeWidth] = React.useState(4);
29
+ const [shapeStyle, setShapeStyle] = React.useState("outline");
30
+ const [cropDraft, setCropDraft] = React.useState(null);
31
+ const [ready, setReady] = React.useState(false);
32
+ const [busy, setBusy] = React.useState(false);
33
+ const [error, setError] = React.useState(null);
34
+ const [canUndo, setCanUndo] = React.useState(false);
35
+ const [canRedo, setCanRedo] = React.useState(false);
36
+ const [textEditor, setTextEditor] = React.useState(null);
37
+ const toolRef = useLatestRef(tool);
38
+ const colorRef = useLatestRef(color);
39
+ const strokeRef = useLatestRef(strokeWidth);
40
+ const shapeStyleRef = useLatestRef(shapeStyle);
41
+ const editingTextIdRef = useLatestRef((_a = textEditor === null || textEditor === void 0 ? void 0 : textEditor.id) !== null && _a !== void 0 ? _a : null);
42
+ const labelsRef = useLatestRef(labels);
43
+ const resolveSrcRef = useLatestRef(resolveSrc);
44
+ const syncHistoryButtons = React.useCallback(() => {
45
+ setCanUndo(historyIndexRef.current > 0);
46
+ setCanRedo(historyIndexRef.current < historyRef.current.length - 1);
47
+ }, []);
48
+ const markBackgroundObjects = React.useCallback((canvas) => {
49
+ canvas.getObjects().forEach((obj) => {
50
+ if (isBackgroundObject(obj)) {
51
+ obj.set({
52
+ selectable: false,
53
+ evented: false,
54
+ hasControls: false,
55
+ lockMovementX: true,
56
+ lockMovementY: true,
57
+ });
58
+ }
59
+ });
60
+ }, []);
61
+ const syncObjectInteractivity = React.useCallback((canvas, currentTool) => {
62
+ const allowSelect = currentTool === "select" ||
63
+ isShapeDrawTool(currentTool) ||
64
+ currentTool === "text";
65
+ canvas.selection = allowSelect;
66
+ canvas.skipTargetFind = currentTool === "pen" || currentTool === "crop";
67
+ canvas.defaultCursor =
68
+ currentTool === "eraser"
69
+ ? "cell"
70
+ : currentTool === "pen"
71
+ ? "crosshair"
72
+ : "default";
73
+ canvas.hoverCursor =
74
+ currentTool === "eraser" ? "cell" : allowSelect ? "move" : "crosshair";
75
+ canvas.getObjects().forEach((obj) => {
76
+ if (isBackgroundObject(obj)) {
77
+ obj.set({ selectable: false, evented: false });
78
+ return;
79
+ }
80
+ if (currentTool === "eraser") {
81
+ obj.set({
82
+ selectable: false,
83
+ evented: true,
84
+ hasControls: false,
85
+ });
86
+ return;
87
+ }
88
+ const interactive = allowSelect;
89
+ obj.set({
90
+ selectable: interactive,
91
+ evented: interactive,
92
+ hasControls: interactive,
93
+ });
94
+ });
95
+ canvas.requestRenderAll();
96
+ }, []);
97
+ const pushHistory = React.useCallback(() => {
98
+ const canvas = fabricRef.current;
99
+ if (!canvas || applyingHistoryRef.current)
100
+ return;
101
+ const json = JSON.stringify(canvas.toObject([...HISTORY_PROPS]));
102
+ const next = historyRef.current.slice(0, historyIndexRef.current + 1);
103
+ if (next[next.length - 1] === json) {
104
+ syncHistoryButtons();
105
+ return;
106
+ }
107
+ next.push(json);
108
+ while (next.length > HISTORY_LIMIT)
109
+ next.shift();
110
+ historyRef.current = next;
111
+ historyIndexRef.current = next.length - 1;
112
+ syncHistoryButtons();
113
+ }, [syncHistoryButtons]);
114
+ const findObjectByAnnotationId = React.useCallback((id) => {
115
+ var _a;
116
+ const canvas = fabricRef.current;
117
+ if (!canvas)
118
+ return null;
119
+ return ((_a = canvas.getObjects().find((obj) => {
120
+ return obj.annotationId === id;
121
+ })) !== null && _a !== void 0 ? _a : null);
122
+ }, []);
123
+ const applyHistory = React.useCallback(async (index) => {
124
+ const canvas = fabricRef.current;
125
+ const snapshot = historyRef.current[index];
126
+ if (!canvas || !snapshot)
127
+ return;
128
+ applyingHistoryRef.current = true;
129
+ drawingRef.current = false;
130
+ startRef.current = null;
131
+ draftObjectRef.current = null;
132
+ setCropDraft(null);
133
+ setTextEditor(null);
134
+ try {
135
+ await canvas.loadFromJSON(JSON.parse(snapshot));
136
+ const { width, height, zoom } = viewportRef.current;
137
+ if (width && height) {
138
+ canvas.setDimensions({ width, height });
139
+ canvas.setZoom(zoom || 1);
140
+ }
141
+ markBackgroundObjects(canvas);
142
+ syncObjectInteractivity(canvas, toolRef.current);
143
+ canvas.discardActiveObject();
144
+ canvas.requestRenderAll();
145
+ historyIndexRef.current = index;
146
+ syncHistoryButtons();
147
+ }
148
+ catch (_a) {
149
+ setError(labelsRef.current.loadFailed);
150
+ }
151
+ finally {
152
+ applyingHistoryRef.current = false;
153
+ }
154
+ }, [
155
+ labelsRef,
156
+ markBackgroundObjects,
157
+ syncHistoryButtons,
158
+ syncObjectInteractivity,
159
+ toolRef,
160
+ ]);
161
+ const fitCanvasToHost = React.useCallback((naturalW, naturalH) => {
162
+ const host = hostRef.current;
163
+ const canvas = fabricRef.current;
164
+ if (!host || !canvas)
165
+ return { width: naturalW, height: naturalH, zoom: 1 };
166
+ const maxW = Math.max(280, host.clientWidth - 8);
167
+ const maxH = Math.max(240, host.clientHeight - 8);
168
+ const zoom = Math.min(1, maxW / naturalW, maxH / naturalH);
169
+ const width = Math.max(1, Math.round(naturalW * zoom));
170
+ const height = Math.max(1, Math.round(naturalH * zoom));
171
+ canvas.setDimensions({ width, height });
172
+ canvas.setZoom(zoom);
173
+ viewportRef.current = { width, height, zoom };
174
+ return { width, height, zoom };
175
+ }, []);
176
+ const commitTextEditor = React.useCallback(() => {
177
+ var _a;
178
+ const editing = textEditor;
179
+ if (!editing)
180
+ return;
181
+ const obj = findObjectByAnnotationId(editing.id);
182
+ if (obj && typeof obj.set === "function") {
183
+ const nextText = editing.value.trim() || labelsRef.current.textPlaceholder;
184
+ obj.set("text", nextText);
185
+ (_a = fabricRef.current) === null || _a === void 0 ? void 0 : _a.requestRenderAll();
186
+ pushHistory();
187
+ }
188
+ setTextEditor(null);
189
+ }, [findObjectByAnnotationId, labelsRef, pushHistory, textEditor]);
190
+ React.useEffect(() => {
191
+ if (!textEditor)
192
+ return;
193
+ const id = window.setTimeout(() => { var _a; return (_a = textInputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, 0);
194
+ return () => window.clearTimeout(id);
195
+ }, [textEditor]);
196
+ React.useEffect(() => {
197
+ let disposed = false;
198
+ let fabricCanvas = null;
199
+ const boot = async () => {
200
+ var _a;
201
+ if (!canvasRef.current || !hostRef.current)
202
+ return;
203
+ setError(null);
204
+ setReady(false);
205
+ try {
206
+ const fabric = await import("fabric");
207
+ const { Canvas, FabricImage, PencilBrush, Line, Rect, Ellipse, Textbox, Triangle, Group, } = fabric;
208
+ const resolver = (_a = resolveSrcRef.current) !== null && _a !== void 0 ? _a : defaultResolveSrc;
209
+ const objectUrl = await resolver(src);
210
+ if (disposed || !canvasRef.current)
211
+ return;
212
+ const image = await FabricImage.fromURL(objectUrl);
213
+ if (disposed)
214
+ return;
215
+ const naturalW = image.width || 1;
216
+ const naturalH = image.height || 1;
217
+ fabricCanvas = new Canvas(canvasRef.current, {
218
+ selection: true,
219
+ preserveObjectStacking: true,
220
+ backgroundColor: "#0b1220",
221
+ });
222
+ fabricRef.current = fabricCanvas;
223
+ image.set({
224
+ left: 0,
225
+ top: 0,
226
+ originX: "left",
227
+ originY: "top",
228
+ selectable: false,
229
+ evented: false,
230
+ hasControls: false,
231
+ });
232
+ image.isBackground = true;
233
+ fabricCanvas.add(image);
234
+ fabricCanvas.sendObjectToBack(image);
235
+ fitCanvasToHost(naturalW, naturalH);
236
+ const brush = new PencilBrush(fabricCanvas);
237
+ brush.color = colorRef.current;
238
+ brush.width = strokeRef.current;
239
+ fabricCanvas.freeDrawingBrush = brush;
240
+ fabricCanvas.isDrawingMode = toolRef.current === "pen";
241
+ syncObjectInteractivity(fabricCanvas, toolRef.current);
242
+ const pointerFromEvent = (event) => {
243
+ if (!event || !fabricCanvas)
244
+ return null;
245
+ const point = fabricCanvas.getScenePoint(event);
246
+ return { x: point.x, y: point.y };
247
+ };
248
+ const clearDraft = () => {
249
+ if (draftObjectRef.current && fabricCanvas) {
250
+ fabricCanvas.remove(draftObjectRef.current);
251
+ draftObjectRef.current = null;
252
+ }
253
+ };
254
+ const createArrowGroup = (x1, y1, x2, y2) => {
255
+ const stroke = colorRef.current;
256
+ const width = strokeRef.current;
257
+ const angle = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
258
+ const head = Math.max(14, width * 4);
259
+ const line = new Line([x1, y1, x2, y2], {
260
+ stroke,
261
+ strokeWidth: width,
262
+ selectable: false,
263
+ evented: false,
264
+ });
265
+ const headShape = new Triangle({
266
+ left: x2,
267
+ top: y2,
268
+ originX: "center",
269
+ originY: "center",
270
+ width: head,
271
+ height: head,
272
+ fill: stroke,
273
+ angle: angle + 90,
274
+ selectable: false,
275
+ evented: false,
276
+ });
277
+ return new Group([line, headShape], {
278
+ selectable: false,
279
+ evented: false,
280
+ });
281
+ };
282
+ fabricCanvas.on("mouse:down", (opt) => {
283
+ var _a, _b;
284
+ if (!fabricCanvas || applyingHistoryRef.current)
285
+ return;
286
+ const currentTool = toolRef.current;
287
+ const target = opt.target;
288
+ if (currentTool === "eraser") {
289
+ if (target && !isBackgroundObject(target)) {
290
+ fabricCanvas.remove(target);
291
+ fabricCanvas.discardActiveObject();
292
+ fabricCanvas.requestRenderAll();
293
+ pushHistory();
294
+ }
295
+ return;
296
+ }
297
+ if (target &&
298
+ !isBackgroundObject(target) &&
299
+ (isShapeDrawTool(currentTool) ||
300
+ currentTool === "text" ||
301
+ currentTool === "select")) {
302
+ drawingRef.current = false;
303
+ if (currentTool === "text" &&
304
+ (target.type === "textbox" || target.type === "Textbox")) {
305
+ const id = (_a = target.annotationId) !== null && _a !== void 0 ? _a : newAnnotationId();
306
+ target.annotationId = id;
307
+ setTextEditor({
308
+ id,
309
+ value: String((_b = target.text) !== null && _b !== void 0 ? _b : ""),
310
+ });
311
+ }
312
+ return;
313
+ }
314
+ const p = pointerFromEvent(opt.e);
315
+ if (!p)
316
+ return;
317
+ if (currentTool === "select" || currentTool === "pen")
318
+ return;
319
+ if (currentTool === "text") {
320
+ const id = newAnnotationId();
321
+ const box = new Textbox(labelsRef.current.textPlaceholder, {
322
+ left: p.x,
323
+ top: p.y,
324
+ width: IMAGE_EDITOR_DEFAULT_TEXT_WIDTH,
325
+ fill: colorRef.current,
326
+ fontSize: IMAGE_EDITOR_DEFAULT_TEXT_SIZE,
327
+ editable: false,
328
+ selectable: true,
329
+ evented: true,
330
+ hasControls: true,
331
+ lockUniScaling: false,
332
+ objectCaching: false,
333
+ });
334
+ box.annotationId = id;
335
+ fabricCanvas.add(box);
336
+ fabricCanvas.setActiveObject(box);
337
+ fabricCanvas.requestRenderAll();
338
+ setTextEditor({
339
+ id,
340
+ value: labelsRef.current.textPlaceholder,
341
+ });
342
+ pushHistory();
343
+ return;
344
+ }
345
+ if (currentTool === "line" ||
346
+ currentTool === "arrow" ||
347
+ currentTool === "rect" ||
348
+ currentTool === "ellipse" ||
349
+ currentTool === "crop") {
350
+ drawingRef.current = true;
351
+ startRef.current = p;
352
+ clearDraft();
353
+ setCropDraft(null);
354
+ fabricCanvas.discardActiveObject();
355
+ }
356
+ });
357
+ fabricCanvas.on("mouse:move", (opt) => {
358
+ if (!fabricCanvas || !drawingRef.current || !startRef.current)
359
+ return;
360
+ const currentTool = toolRef.current;
361
+ const p = pointerFromEvent(opt.e);
362
+ if (!p)
363
+ return;
364
+ const start = startRef.current;
365
+ clearDraft();
366
+ let draft = null;
367
+ if (currentTool === "line") {
368
+ draft = new Line([start.x, start.y, p.x, p.y], {
369
+ stroke: colorRef.current,
370
+ strokeWidth: strokeRef.current,
371
+ selectable: false,
372
+ evented: false,
373
+ });
374
+ }
375
+ else if (currentTool === "arrow") {
376
+ draft = createArrowGroup(start.x, start.y, p.x, p.y);
377
+ }
378
+ else if (currentTool === "rect" || currentTool === "crop") {
379
+ const left = Math.min(start.x, p.x);
380
+ const top = Math.min(start.y, p.y);
381
+ const width = Math.abs(p.x - start.x);
382
+ const height = Math.abs(p.y - start.y);
383
+ const paint = currentTool === "crop"
384
+ ? {
385
+ fill: "rgba(12, 102, 228, 0.15)",
386
+ stroke: "#0c66e4",
387
+ }
388
+ : shapeFillAndStroke(colorRef.current, shapeStyleRef.current);
389
+ draft = new Rect({
390
+ left,
391
+ top,
392
+ width,
393
+ height,
394
+ fill: paint.fill,
395
+ stroke: paint.stroke,
396
+ strokeWidth: currentTool === "crop" ? 2 : strokeRef.current,
397
+ strokeDashArray: currentTool === "crop" ? [6, 4] : undefined,
398
+ selectable: false,
399
+ evented: false,
400
+ });
401
+ if (currentTool === "crop") {
402
+ setCropDraft({ x: left, y: top, w: width, h: height });
403
+ }
404
+ }
405
+ else if (currentTool === "ellipse") {
406
+ const left = Math.min(start.x, p.x);
407
+ const top = Math.min(start.y, p.y);
408
+ const width = Math.abs(p.x - start.x);
409
+ const height = Math.abs(p.y - start.y);
410
+ const paint = shapeFillAndStroke(colorRef.current, shapeStyleRef.current);
411
+ draft = new Ellipse({
412
+ left: left + width / 2,
413
+ top: top + height / 2,
414
+ rx: width / 2,
415
+ ry: height / 2,
416
+ fill: paint.fill,
417
+ stroke: paint.stroke,
418
+ strokeWidth: strokeRef.current,
419
+ selectable: false,
420
+ evented: false,
421
+ });
422
+ }
423
+ if (draft) {
424
+ draftObjectRef.current = draft;
425
+ fabricCanvas.add(draft);
426
+ fabricCanvas.requestRenderAll();
427
+ }
428
+ });
429
+ fabricCanvas.on("mouse:up", () => {
430
+ if (!fabricCanvas || !drawingRef.current)
431
+ return;
432
+ const currentTool = toolRef.current;
433
+ drawingRef.current = false;
434
+ startRef.current = null;
435
+ if (currentTool === "crop")
436
+ return;
437
+ if (draftObjectRef.current) {
438
+ const id = newAnnotationId();
439
+ draftObjectRef.current.annotationId = id;
440
+ draftObjectRef.current.set({
441
+ selectable: true,
442
+ evented: true,
443
+ hasControls: true,
444
+ });
445
+ fabricCanvas.setActiveObject(draftObjectRef.current);
446
+ draftObjectRef.current = null;
447
+ pushHistory();
448
+ }
449
+ });
450
+ fabricCanvas.on("path:created", (opt) => {
451
+ const path = opt.path;
452
+ if (path)
453
+ path.annotationId = newAnnotationId();
454
+ pushHistory();
455
+ });
456
+ fabricCanvas.on("object:modified", () => {
457
+ pushHistory();
458
+ });
459
+ fabricCanvas.on("mouse:dblclick", (opt) => {
460
+ var _a, _b;
461
+ const target = opt.target;
462
+ if (!target || isBackgroundObject(target))
463
+ return;
464
+ if (target.type !== "textbox" && target.type !== "Textbox")
465
+ return;
466
+ const id = (_a = target.annotationId) !== null && _a !== void 0 ? _a : newAnnotationId();
467
+ target.annotationId = id;
468
+ setTextEditor({
469
+ id,
470
+ value: String((_b = target.text) !== null && _b !== void 0 ? _b : ""),
471
+ });
472
+ });
473
+ historyRef.current = [
474
+ JSON.stringify(fabricCanvas.toObject([...HISTORY_PROPS])),
475
+ ];
476
+ historyIndexRef.current = 0;
477
+ syncHistoryButtons();
478
+ setReady(true);
479
+ }
480
+ catch (_b) {
481
+ if (!disposed) {
482
+ setError(labelsRef.current.loadFailed);
483
+ }
484
+ }
485
+ };
486
+ void boot();
487
+ return () => {
488
+ disposed = true;
489
+ fabricCanvas === null || fabricCanvas === void 0 ? void 0 : fabricCanvas.dispose();
490
+ fabricRef.current = null;
491
+ };
492
+ }, [
493
+ colorRef,
494
+ fitCanvasToHost,
495
+ labelsRef,
496
+ pushHistory,
497
+ resolveSrcRef,
498
+ shapeStyleRef,
499
+ src,
500
+ strokeRef,
501
+ syncHistoryButtons,
502
+ syncObjectInteractivity,
503
+ toolRef,
504
+ ]);
505
+ React.useEffect(() => {
506
+ const canvas = fabricRef.current;
507
+ if (!canvas)
508
+ return;
509
+ canvas.isDrawingMode = tool === "pen";
510
+ if (canvas.freeDrawingBrush) {
511
+ canvas.freeDrawingBrush.color = color;
512
+ canvas.freeDrawingBrush.width = strokeWidth;
513
+ }
514
+ syncObjectInteractivity(canvas, tool);
515
+ if (tool !== "crop") {
516
+ setCropDraft(null);
517
+ if (draftObjectRef.current) {
518
+ canvas.remove(draftObjectRef.current);
519
+ draftObjectRef.current = null;
520
+ canvas.requestRenderAll();
521
+ }
522
+ }
523
+ }, [tool, color, strokeWidth, syncObjectInteractivity]);
524
+ const handleToolChange = (next) => {
525
+ if (textEditor && next !== "text") {
526
+ commitTextEditor();
527
+ }
528
+ setTool(next);
529
+ };
530
+ const handleUndo = React.useCallback(async () => {
531
+ if (historyIndexRef.current <= 0)
532
+ return;
533
+ await applyHistory(historyIndexRef.current - 1);
534
+ }, [applyHistory]);
535
+ const handleRedo = React.useCallback(async () => {
536
+ if (historyIndexRef.current >= historyRef.current.length - 1)
537
+ return;
538
+ await applyHistory(historyIndexRef.current + 1);
539
+ }, [applyHistory]);
540
+ const handleDeleteSelected = React.useCallback(() => {
541
+ const canvas = fabricRef.current;
542
+ if (!canvas)
543
+ return;
544
+ const active = canvas.getActiveObjects();
545
+ if (!active.length)
546
+ return;
547
+ let removed = false;
548
+ active.forEach((obj) => {
549
+ if (isBackgroundObject(obj))
550
+ return;
551
+ canvas.remove(obj);
552
+ removed = true;
553
+ });
554
+ if (!removed)
555
+ return;
556
+ canvas.discardActiveObject();
557
+ canvas.requestRenderAll();
558
+ pushHistory();
559
+ }, [pushHistory]);
560
+ React.useEffect(() => {
561
+ if (!ready)
562
+ return;
563
+ const onKeyDown = (event) => {
564
+ if (editingTextIdRef.current)
565
+ return;
566
+ const target = event.target;
567
+ if (target &&
568
+ (target.tagName === "INPUT" ||
569
+ target.tagName === "TEXTAREA" ||
570
+ target.isContentEditable)) {
571
+ return;
572
+ }
573
+ if (busy)
574
+ return;
575
+ if (event.key === "Delete" || event.key === "Backspace") {
576
+ event.preventDefault();
577
+ handleDeleteSelected();
578
+ }
579
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "z") {
580
+ event.preventDefault();
581
+ if (event.shiftKey)
582
+ void handleRedo();
583
+ else
584
+ void handleUndo();
585
+ }
586
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "y") {
587
+ event.preventDefault();
588
+ void handleRedo();
589
+ }
590
+ };
591
+ window.addEventListener("keydown", onKeyDown);
592
+ return () => window.removeEventListener("keydown", onKeyDown);
593
+ }, [
594
+ busy,
595
+ editingTextIdRef,
596
+ handleDeleteSelected,
597
+ handleRedo,
598
+ handleUndo,
599
+ ready,
600
+ ]);
601
+ const applyCrop = async () => {
602
+ const canvas = fabricRef.current;
603
+ if (!canvas || !cropDraft)
604
+ return;
605
+ if (cropDraft.w < 4 || cropDraft.h < 4)
606
+ return;
607
+ setBusy(true);
608
+ try {
609
+ if (draftObjectRef.current) {
610
+ canvas.remove(draftObjectRef.current);
611
+ draftObjectRef.current = null;
612
+ }
613
+ const multiplier = 1 / (canvas.getZoom() || 1);
614
+ const dataUrl = canvas.toDataURL({
615
+ format: "png",
616
+ multiplier,
617
+ left: cropDraft.x,
618
+ top: cropDraft.y,
619
+ width: cropDraft.w,
620
+ height: cropDraft.h,
621
+ });
622
+ const { FabricImage } = await import("fabric");
623
+ const cropped = await FabricImage.fromURL(dataUrl);
624
+ canvas.clear();
625
+ canvas.backgroundColor = "#0b1220";
626
+ cropped.set({
627
+ left: 0,
628
+ top: 0,
629
+ originX: "left",
630
+ originY: "top",
631
+ selectable: false,
632
+ evented: false,
633
+ hasControls: false,
634
+ });
635
+ cropped.isBackground = true;
636
+ canvas.add(cropped);
637
+ fitCanvasToHost(cropped.width || cropDraft.w, cropped.height || cropDraft.h);
638
+ setCropDraft(null);
639
+ setTool("select");
640
+ pushHistory();
641
+ }
642
+ catch (_a) {
643
+ setError(labelsRef.current.cropFailed);
644
+ }
645
+ finally {
646
+ setBusy(false);
647
+ }
648
+ };
649
+ const applyRotate = async (direction) => {
650
+ const canvas = fabricRef.current;
651
+ if (!canvas || busy)
652
+ return;
653
+ if (textEditor)
654
+ commitTextEditor();
655
+ setBusy(true);
656
+ setError(null);
657
+ try {
658
+ if (draftObjectRef.current) {
659
+ canvas.remove(draftObjectRef.current);
660
+ draftObjectRef.current = null;
661
+ }
662
+ setCropDraft(null);
663
+ canvas.discardActiveObject();
664
+ canvas.requestRenderAll();
665
+ const multiplier = 1 / (canvas.getZoom() || 1);
666
+ const dataUrl = canvas.toDataURL({
667
+ format: "png",
668
+ multiplier,
669
+ });
670
+ const rotated = await rotateImageDataUrl(dataUrl, direction);
671
+ const { FabricImage } = await import("fabric");
672
+ const image = await FabricImage.fromURL(rotated.dataUrl);
673
+ canvas.clear();
674
+ canvas.backgroundColor = "#0b1220";
675
+ image.set({
676
+ left: 0,
677
+ top: 0,
678
+ originX: "left",
679
+ originY: "top",
680
+ selectable: false,
681
+ evented: false,
682
+ hasControls: false,
683
+ });
684
+ image.isBackground = true;
685
+ canvas.add(image);
686
+ fitCanvasToHost(image.width || rotated.width, image.height || rotated.height);
687
+ syncObjectInteractivity(canvas, toolRef.current);
688
+ pushHistory();
689
+ }
690
+ catch (_a) {
691
+ setError(labelsRef.current.rotateFailed);
692
+ }
693
+ finally {
694
+ setBusy(false);
695
+ }
696
+ };
697
+ const handleSave = async () => {
698
+ const canvas = fabricRef.current;
699
+ if (!canvas)
700
+ return;
701
+ if (textEditor)
702
+ commitTextEditor();
703
+ setBusy(true);
704
+ setError(null);
705
+ try {
706
+ if (draftObjectRef.current) {
707
+ canvas.remove(draftObjectRef.current);
708
+ draftObjectRef.current = null;
709
+ }
710
+ canvas.discardActiveObject();
711
+ canvas.requestRenderAll();
712
+ const multiplier = 1 / (canvas.getZoom() || 1);
713
+ const dataUrl = canvas.toDataURL({
714
+ format: "jpeg",
715
+ quality: 0.92,
716
+ multiplier,
717
+ });
718
+ const response = await fetch(dataUrl);
719
+ const blob = await response.blob();
720
+ const file = new File([blob], editedAttachmentFileName(fileName), {
721
+ type: "image/jpeg",
722
+ lastModified: Date.now(),
723
+ });
724
+ await onSave(file);
725
+ }
726
+ catch (_a) {
727
+ setError(labelsRef.current.saveFailed);
728
+ }
729
+ finally {
730
+ setBusy(false);
731
+ }
732
+ };
733
+ const updateTextValue = (value) => {
734
+ var _a;
735
+ setTextEditor((prev) => (prev ? Object.assign(Object.assign({}, prev), { value }) : prev));
736
+ if (!textEditor)
737
+ return;
738
+ const obj = findObjectByAnnotationId(textEditor.id);
739
+ if (obj) {
740
+ obj.set("text", value.trim() || labels.textPlaceholder);
741
+ (_a = fabricRef.current) === null || _a === void 0 ? void 0 : _a.requestRenderAll();
742
+ }
743
+ };
744
+ return {
745
+ hostRef,
746
+ canvasRef,
747
+ textInputRef,
748
+ tool,
749
+ color,
750
+ strokeWidth,
751
+ shapeStyle,
752
+ cropDraft,
753
+ ready,
754
+ busy,
755
+ error,
756
+ canUndo,
757
+ canRedo,
758
+ textEditor,
759
+ setColor,
760
+ setStrokeWidth,
761
+ setShapeStyle,
762
+ handleToolChange,
763
+ handleUndo,
764
+ handleRedo,
765
+ applyCrop,
766
+ applyRotate,
767
+ handleSave,
768
+ commitTextEditor,
769
+ updateTextValue,
770
+ dismissTextEditor: () => setTextEditor(null),
771
+ };
772
+ }
773
+ //# sourceMappingURL=use-image-editor.js.map