@coolms/image-editor-angular 2.0.0-alpha.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.
@@ -0,0 +1,1488 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
3
+
4
+ /**
5
+ * Pure geometry primitives shared across the engine and any UI shell
6
+ * sitting on top. Deliberately framework-agnostic: no Angular, no
7
+ * fabric.js, no Toast UI imports — these types are part of the public
8
+ * surface of `@coolms/image-editor` and need to compile without the
9
+ * peer dependency installed.
10
+ */
11
+ interface Point {
12
+ readonly x: number;
13
+ readonly y: number;
14
+ }
15
+ interface Size {
16
+ readonly width: number;
17
+ readonly height: number;
18
+ }
19
+ /**
20
+ * Axis-aligned rectangle. The engine's `crop(rect)` consumes this shape
21
+ * directly; UI shells produce one from their cropzone overlay.
22
+ */
23
+ interface Rect {
24
+ readonly x: number;
25
+ readonly y: number;
26
+ readonly width: number;
27
+ readonly height: number;
28
+ }
29
+ interface Bounds {
30
+ readonly minWidth: number;
31
+ readonly minHeight: number;
32
+ readonly maxWidth: number;
33
+ readonly maxHeight: number;
34
+ }
35
+
36
+ /**
37
+ * Filter contract for the engine.
38
+ *
39
+ * The public API uses lowercase names and normalised parameter ranges
40
+ * so the UI shell never has to know about fabric.js conventions.
41
+ * The `FabricEngineAdapter` is the single translation point: lowercase
42
+ * filter names map to fabric's BaseFilter classes, and `sharpen`
43
+ * resolves to a `Convolute` filter constructed with the canonical
44
+ * 3x3 sharpen kernel.
45
+ */
46
+ type FilterName = 'brightness' | 'contrast' | 'saturation' | 'sepia' | 'grayscale' | 'invert' | 'blur' | 'sharpen' | 'noise' | 'pixelate';
47
+ /**
48
+ * Per-filter parameter shape. Range conventions:
49
+ * brightness -1..1 (0 = no change)
50
+ * contrast -1..1 (0 = no change)
51
+ * saturation -1..1 (0 = no change)
52
+ * blur 0..1 (0 = no blur, 1 = max)
53
+ * noise 0..1
54
+ * pixelate blockSize in pixels (>= 2)
55
+ *
56
+ * sepia / grayscale / invert are parameterless (boolean toggles via
57
+ * `applyFilter(name)` and `removeFilter(name)`).
58
+ */
59
+ interface FilterParams {
60
+ readonly brightness?: number;
61
+ readonly contrast?: number;
62
+ readonly saturation?: number;
63
+ readonly blur?: number;
64
+ readonly noise?: number;
65
+ readonly pixelate?: number;
66
+ }
67
+
68
+ /**
69
+ * Object / layer model for the image editor (Track C.1).
70
+ *
71
+ * The editor was historically single-image (one `FabricImage`, flat
72
+ * bitmap). C.1 introduces an **objects-as-layers** model: the base
73
+ * image is the bottom layer; every shape / text the author adds is a
74
+ * layer stacked on top. Fabric's canvas is already an ordered object
75
+ * stack, so a "layer" is just a canvas object carrying a stable id.
76
+ *
77
+ * Resize / filters still operate on the base image only — they're
78
+ * pixel-baking ops, meant to run before annotations. C.1c makes
79
+ * **rotate / flip** target the selected non-image layer (falling back
80
+ * to the base image) and adds **masking** (a clip-path on the target;
81
+ * see {@link MaskSpec}); C.1e makes **crop** composition-aware — the
82
+ * annotation layers shift with the cropped content and clip at the new
83
+ * edges, so a crop after annotating stays aligned.
84
+ */
85
+ /** What a layer is. `image` is the immovable-from-the-stack base. */
86
+ type LayerKind = 'image' | 'shape' | 'text';
87
+ /** The shape primitives the Shapes tool can insert. */
88
+ type ShapeKind = 'rect' | 'ellipse' | 'line' | 'arrow' | 'triangle';
89
+ /**
90
+ * Canvas-space geometry of an annotation shape — the round-trippable
91
+ * subset consumers that author geometry (e.g. the ImageMap region
92
+ * page) read and write via `addShapeAt()` / `getShapeGeometry()`.
93
+ * Coordinates are SCENE space (viewport zoom/pan factored out), the
94
+ * same space `getImageBounds()` reports in.
95
+ */
96
+ type ShapeGeometry = {
97
+ kind: 'rect';
98
+ rect: {
99
+ x: number;
100
+ y: number;
101
+ width: number;
102
+ height: number;
103
+ };
104
+ } | {
105
+ kind: 'ellipse';
106
+ center: {
107
+ x: number;
108
+ y: number;
109
+ };
110
+ rx: number;
111
+ ry: number;
112
+ } | {
113
+ kind: 'polygon';
114
+ points: {
115
+ x: number;
116
+ y: number;
117
+ }[];
118
+ };
119
+ /**
120
+ * Options for `addShapeAt()`. Style keys mirror {@link ShapeOptions};
121
+ * the extras tune the shape for geometry-authoring use:
122
+ * - `name`: layer display name (defaults to the shape kind).
123
+ * - `lockRotation`: hide the rotate handle — authored geometry is
124
+ * axis-aligned (rect regions have no rotation representation).
125
+ * - `lockNonUniformScaling`: hide the edge handles so corner drags
126
+ * scale uniformly (keeps a circle circular).
127
+ */
128
+ interface AnnotationShapeOptions {
129
+ fill?: string;
130
+ stroke?: string;
131
+ strokeWidth?: number;
132
+ name?: string;
133
+ lockRotation?: boolean;
134
+ lockNonUniformScaling?: boolean;
135
+ }
136
+ /**
137
+ * How a shape / text layer is filled (C.1b).
138
+ *
139
+ * - `solid` : a single flat colour.
140
+ * - `linear` : a linear gradient from `color` -> `color2` along `angle`.
141
+ * - `radial` : a radial gradient from `color` (centre) -> `color2` (edge).
142
+ */
143
+ type FillType = 'solid' | 'linear' | 'radial';
144
+ /**
145
+ * UI-friendly, serialisable description of a fill. The engine
146
+ * translates this to / from a fabric solid colour or `Gradient`, so the
147
+ * tool components never import fabric. Colours are `#rrggbb` hex (what
148
+ * `<input type="color">` reads / writes); `angle` is in degrees
149
+ * (0 = left->right, 90 = top->bottom) and only meaningful for `linear`.
150
+ */
151
+ interface FillSpec {
152
+ type: FillType;
153
+ color: string;
154
+ color2: string;
155
+ angle: number;
156
+ }
157
+ /**
158
+ * A read-only view of one layer, surfaced to the Layers panel. The
159
+ * engine owns the fabric objects; this is the serialisable projection
160
+ * the UI binds to (never a live fabric handle).
161
+ */
162
+ interface LayerInfo {
163
+ readonly id: string;
164
+ readonly kind: LayerKind;
165
+ readonly name: string;
166
+ readonly visible: boolean;
167
+ /** 0..1. */
168
+ readonly opacity: number;
169
+ /**
170
+ * True for the base image: it can't be deleted or reordered below
171
+ * itself (it's the canvas floor). Shapes / text are never locked.
172
+ */
173
+ readonly locked: boolean;
174
+ }
175
+ /** Style options for an inserted shape. */
176
+ interface ShapeOptions {
177
+ fill: string;
178
+ stroke: string;
179
+ strokeWidth: number;
180
+ /**
181
+ * Richer fill description (solid colour or gradient). When present
182
+ * it takes precedence over the flat `fill` string. The Shapes tool
183
+ * always sends this; `fill` stays a plain-colour fallback for any
184
+ * caller that doesn't. (C.1b)
185
+ */
186
+ fillSpec?: FillSpec;
187
+ }
188
+ /** Style options for inserted text. */
189
+ interface TextOptions {
190
+ text: string;
191
+ fontFamily: string;
192
+ fontSize: number;
193
+ fill: string;
194
+ fontWeight: 'normal' | 'bold';
195
+ fontStyle: 'normal' | 'italic';
196
+ textAlign: 'left' | 'center' | 'right';
197
+ /** Gradient-capable fill; see {@link ShapeOptions.fillSpec}. (C.1b) */
198
+ fillSpec?: FillSpec;
199
+ }
200
+ /**
201
+ * Generic property bag the style panels read/write against the active
202
+ * object. Keys are fabric property names (`fill`, `stroke`,
203
+ * `strokeWidth`, `opacity`, `fontFamily`, …); the engine reads them off
204
+ * / sets them on the active object without the UI importing fabric.
205
+ */
206
+ type ObjectProps = Record<string, string | number>;
207
+ /**
208
+ * Mask / clip shapes a layer can be cut to (C.1c / C.1d).
209
+ *
210
+ * `none` clears the mask. `rect` / `rounded` / `ellipse` / `triangle`
211
+ * are geometric primitives; `star` / `heart` / `hexagon` are SVG-path
212
+ * presets. `custom` (C.1d) clips to arbitrary path geometry the user
213
+ * pasted or uploaded as an SVG — see {@link MaskSpec.svgPath}. The
214
+ * engine builds the matching fabric `clipPath` against the target's own
215
+ * box, so the mask tracks the object through scale / move, and the
216
+ * export flatten bakes it in (the clip is part of the object's normal
217
+ * render path).
218
+ */
219
+ type MaskShape = 'none' | 'rect' | 'rounded' | 'ellipse' | 'triangle' | 'star' | 'heart' | 'hexagon' | 'custom';
220
+ /**
221
+ * UI-friendly description of a layer mask. The engine translates it to /
222
+ * from a fabric `clipPath`, so the tool component never imports fabric.
223
+ *
224
+ * - `inset` : 0..40, percent each edge is pulled in from the box (a
225
+ * symmetric margin around the clip).
226
+ * - `radius` : 0..50, corner radius for `rounded` only, as a percent of
227
+ * the clip's shorter half-dimension (50 = pill / circle).
228
+ * - `svgPath` : SVG path `d` data for `shape: 'custom'` (C.1d). Sanitised
229
+ * geometry extracted from a pasted path string or uploaded
230
+ * SVG file — every drawable element is flattened to a single
231
+ * combined path (paths only, no script / external refs). The
232
+ * engine scales its bounding box to the clip box exactly
233
+ * like the built-in path presets, so it tracks scale / move
234
+ * and round-trips through the `coolmsMask` stamp.
235
+ */
236
+ interface MaskSpec {
237
+ shape: MaskShape;
238
+ inset: number;
239
+ radius: number;
240
+ svgPath?: string;
241
+ }
242
+
243
+ /**
244
+ * Normalised event surface for `ImageEditorEngine`.
245
+ *
246
+ * The adapter translates engine-internal events (Toast UI's
247
+ * `objectActivated`, `undoStackChanged`, etc.) into this stable map
248
+ * so the UI shell never imports vendor types.
249
+ */
250
+
251
+ interface EngineEvents {
252
+ /** Fires after `loadImage()` resolves. Carries the loaded dimensions. */
253
+ readonly imageLoaded: {
254
+ readonly width: number;
255
+ readonly height: number;
256
+ };
257
+ /** Any operation that mutates the canvas (filter, crop, rotate, etc.). */
258
+ readonly imageChanged: void;
259
+ /** History stacks updated. Use `canUndo`/`canRedo` for current depth. */
260
+ readonly historyStateChanged: {
261
+ readonly canUndo: boolean;
262
+ readonly canRedo: boolean;
263
+ };
264
+ /**
265
+ * Fires whenever the image's transform state changes, from either
266
+ * direct mouse manipulation (corner / rotation handles) or a
267
+ * sidebar tool action (slider drag, flip button). The `source`
268
+ * field tells consumers which path produced the change so that
269
+ * tool components can decide whether to update bound display
270
+ * values without echoing back into the engine.
271
+ */
272
+ readonly imageTransformed: {
273
+ readonly angle: number;
274
+ readonly scaleX: number;
275
+ readonly scaleY: number;
276
+ readonly flipX: boolean;
277
+ readonly flipY: boolean;
278
+ readonly left: number;
279
+ readonly top: number;
280
+ readonly source: 'mouse' | 'sidebar';
281
+ };
282
+ /** A canvas object was activated (selected). */
283
+ readonly objectActivated: {
284
+ readonly objectType: string;
285
+ };
286
+ /** No object is currently selected. */
287
+ readonly objectDeactivated: void;
288
+ /**
289
+ * The layer stack changed — a shape / text layer was added,
290
+ * removed, reordered, or had its visibility / opacity toggled.
291
+ * Carries no payload: consumers re-pull `getLayers()`. (C.1)
292
+ */
293
+ readonly layersChanged: void;
294
+ /**
295
+ * The active (selected) layer changed — to another layer, or to
296
+ * none. `id` is the engine's stable layer id; `kind` lets the
297
+ * sidebar decide which style panel is relevant. Distinct from
298
+ * `objectActivated`, which predates the layer model and carries
299
+ * the raw fabric type only. (C.1)
300
+ */
301
+ readonly activeObjectChanged: {
302
+ readonly id: string | null;
303
+ readonly kind: LayerKind | null;
304
+ };
305
+ /**
306
+ * Raw pointer activity in SCENE coordinates (canvas space with the
307
+ * viewport zoom/pan factored out — the same space `getImageBounds()`
308
+ * and `getShapeGeometry()` report in). The annotation seam for
309
+ * consumers that implement their own draw-by-drag interactions
310
+ * (e.g. the ImageMap region authoring page): `down`->`move`->`up`
311
+ * drives rubber-band shapes, `dblclick` closes polygon paths.
312
+ * `targetLayerId` is the stamped layer under the pointer, or null
313
+ * on empty canvas / when hit-testing is disabled.
314
+ */
315
+ readonly scenePointer: {
316
+ readonly phase: 'down' | 'move' | 'up' | 'dblclick';
317
+ readonly x: number;
318
+ readonly y: number;
319
+ readonly targetLayerId: string | null;
320
+ };
321
+ }
322
+ type EngineEventType = keyof EngineEvents;
323
+ type EngineEventHandler<T extends EngineEventType> = (data: EngineEvents[T]) => void;
324
+
325
+ /**
326
+ * Engine contract for image manipulation.
327
+ *
328
+ * `FabricEngineAdapter` is the current implementation, built on
329
+ * fabric.js v6. The interface is the firewall between the UI shell
330
+ * and the rendering engine: a future implementation could target a
331
+ * different canvas library without touching consumers.
332
+ *
333
+ * Async semantics:
334
+ * - All mutations return `Promise` so consumers can chain reliably
335
+ * even when the underlying engine is partly synchronous.
336
+ * - Read-only accessors (`canUndo`, `getCanvasSize`, etc.) are
337
+ * synchronous because the engine's internal state is queryable
338
+ * without a tick.
339
+ */
340
+ interface ImageEditorEngine {
341
+ /**
342
+ * Load an image into the canvas. Replaces any existing content.
343
+ * Returns the loaded image's intrinsic dimensions.
344
+ */
345
+ loadImage(source: string | Blob, name?: string): Promise<Size>;
346
+ /**
347
+ * Roll back every change since the last `loadImage()`. Implemented
348
+ * via repeated `undo()` until the stack is empty rather than a
349
+ * separate "reset" semantic, so behaviour is consistent with the
350
+ * undo button in the UI.
351
+ */
352
+ reset(): Promise<void>;
353
+ /** Crop to the given rectangle (in canvas coordinates). */
354
+ crop(rect: Rect): Promise<void>;
355
+ /**
356
+ * Enter the engine's interactive crop UI: a draggable cropzone
357
+ * appears on the canvas and the user resizes / repositions it
358
+ * with the mouse. The actual crop operation only happens when
359
+ * `crop(rect)` is called against the rect returned by
360
+ * `getCropRect()`. Idempotent: a second call while already in
361
+ * crop mode is a no-op.
362
+ */
363
+ enterCropMode(): Promise<void>;
364
+ /**
365
+ * Exit the interactive crop UI without applying. The cropzone
366
+ * overlay is removed; the canvas state is unchanged.
367
+ */
368
+ exitCropMode(): Promise<void>;
369
+ /**
370
+ * Lock the cropzone to a fixed `width / height` ratio. `null`
371
+ * frees the cropzone (user can drag any rectangle). Only valid
372
+ * while in crop mode; outside crop mode this is a no-op.
373
+ */
374
+ setCropAspectRatio(ratio: number | null): void;
375
+ /**
376
+ * Read the current cropzone rectangle in canvas-space coordinates
377
+ * (the overlay's `getBoundingRect()` AABB, no viewport zoom or
378
+ * pan factored in). Returns `null` when not in crop mode or when
379
+ * the overlay has zero width or height.
380
+ */
381
+ getCropRect(): Rect | null;
382
+ /**
383
+ * Rotate by `degrees` (positive = clockwise). Targets the active
384
+ * non-image layer (shape / text) when one is selected, otherwise
385
+ * the base image (C.1c per-layer transforms).
386
+ */
387
+ rotate(degrees: number): Promise<void>;
388
+ /**
389
+ * Flip along the given axis. Targets the active non-image layer
390
+ * when one is selected, otherwise the base image (C.1c).
391
+ */
392
+ flip(axis: 'horizontal' | 'vertical'): Promise<void>;
393
+ /** Resize the canvas (and the loaded image) to `size`. */
394
+ resize(size: Size): Promise<void>;
395
+ /**
396
+ * Apply a filter. Idempotent: re-calling with new params updates
397
+ * the existing instance of that filter rather than stacking a new
398
+ * one. Parameter ranges are normalised — see FilterParams docblock.
399
+ */
400
+ applyFilter(name: FilterName, params?: FilterParams): Promise<void>;
401
+ /** Remove a previously-applied filter (no-op if not active). */
402
+ removeFilter(name: FilterName): Promise<void>;
403
+ /** True when the named filter is currently active. */
404
+ hasFilter(name: FilterName): boolean;
405
+ /**
406
+ * Add a shape, centred on the image, styled per `opts` (defaults
407
+ * fill in). The new shape becomes the active object.
408
+ */
409
+ addShape(kind: ShapeKind, opts?: Partial<ShapeOptions>): void;
410
+ /**
411
+ * Add an editable text box, centred on the image. The new text
412
+ * becomes the active object.
413
+ */
414
+ addText(opts?: Partial<TextOptions>): void;
415
+ /**
416
+ * The base image's scene-space bounding rect (the frame authored
417
+ * geometry is normalized against), or `null` before `loadImage()`.
418
+ */
419
+ getImageBounds(): {
420
+ x: number;
421
+ y: number;
422
+ width: number;
423
+ height: number;
424
+ } | null;
425
+ /**
426
+ * Add a shape at explicit scene-space geometry (no centering).
427
+ * Becomes the active object; undoable like `addShape`. Returns the
428
+ * new layer's id so the caller can key its own metadata to it.
429
+ */
430
+ addShapeAt(geometry: ShapeGeometry, opts?: AnnotationShapeOptions): string;
431
+ /**
432
+ * Read a shape layer's current scene-space geometry, reflecting
433
+ * any interactive move / scale the user applied. Rects report the
434
+ * axis-aligned box (rotation is not representable — pair with
435
+ * `lockRotation`); ellipses report center + scaled radii; polygons
436
+ * report fully-transformed vertices. `null` for unknown ids, the
437
+ * base image, or non-shape layers.
438
+ */
439
+ getShapeGeometry(id: string): ShapeGeometry | null;
440
+ /**
441
+ * Toggle per-vertex editing on a POLYGON shape layer. While enabled
442
+ * the polygon's default scale/rotate handles are replaced by one
443
+ * draggable control per vertex (whole-shape dragging is locked so
444
+ * the vertices are the only interaction); a completed vertex drag
445
+ * is undoable like any other mutation. Disabling restores the
446
+ * default handles. Returns `false` (no-op) for unknown ids or
447
+ * non-polygon layers; enabling twice is idempotent.
448
+ */
449
+ setVertexEditing(id: string, enabled: boolean): boolean;
450
+ /**
451
+ * Set a layer's fill for a TRANSIENT preview tint (e.g. a live
452
+ * busy/free status overlay while authoring). Not undoable and not part
453
+ * of the shape's geometry, so it never affects what a save persists.
454
+ * Passing `null` restores the default annotation fill. No-op for the
455
+ * base image or an unknown id.
456
+ */
457
+ setLayerFill(id: string, fill: string | null): void;
458
+ /**
459
+ * Insert a new draggable vertex into the polygon `id` currently in
460
+ * vertex-edit mode, at the point on its outline NEAREST to the scene
461
+ * coordinate `(sceneX, sceneY)` — a collinear insert, so the polygon's
462
+ * shape is unchanged, it just gains a handle the author can then drag.
463
+ * The controls are rebuilt (re-indexed) and the insert is undoable.
464
+ * Returns `false` (no-op) if `id` is not the polygon currently in
465
+ * vertex-edit mode. Intended to be wired to a double-click on the
466
+ * outline while editing vertices.
467
+ */
468
+ addVertexAt(id: string, sceneX: number, sceneY: number): boolean;
469
+ /**
470
+ * Remove the vertex of the polygon `id` (currently in vertex-edit mode)
471
+ * NEAREST to the scene coordinate `(sceneX, sceneY)`, but only when the
472
+ * coordinate lands ON a vertex handle (within a fraction of the polygon's
473
+ * own size). The controls are rebuilt (re-indexed) and the removal is
474
+ * undoable. Returns `false` (no-op) if `id` is not the polygon in
475
+ * vertex-edit mode, if removing would drop below the 3-vertex minimum a
476
+ * polygon requires, OR if the coordinate is too far from any vertex — so
477
+ * a caller can route a double-click to this FIRST and fall through to
478
+ * {@link addVertexAt} (which projects onto the outline) when it returns
479
+ * `false`. Intended to be wired to a double-click on a vertex handle.
480
+ */
481
+ removeVertexAt(id: string, sceneX: number, sceneY: number): boolean;
482
+ /**
483
+ * The current layer stack, **top-most first** (so a Layers panel
484
+ * lists it the way every editor does). Always includes the base
485
+ * image as the last (bottom) entry.
486
+ */
487
+ getLayers(): LayerInfo[];
488
+ /** The active layer's id, or `null` when nothing is selected. */
489
+ getActiveLayerId(): string | null;
490
+ /** Select a layer by id (`null` clears selection). */
491
+ selectLayer(id: string | null): void;
492
+ /** Delete a shape / text layer (no-op for the locked base image). */
493
+ removeLayer(id: string): void;
494
+ /** Show / hide a layer. */
495
+ setLayerVisibility(id: string, visible: boolean): void;
496
+ /** Set a layer's opacity (0..1). */
497
+ setLayerOpacity(id: string, opacity: number): void;
498
+ /**
499
+ * Restack a layer. `up`/`down` move one step; `front`/`back` jump
500
+ * to the extremes. The base image is pinned to the bottom — other
501
+ * layers can't sink below it.
502
+ */
503
+ reorderLayer(id: string, direction: 'up' | 'down' | 'front' | 'back'): void;
504
+ /**
505
+ * Read the active object's editable style props (`fill`, `stroke`,
506
+ * `strokeWidth`, `opacity`, plus font props for text). Returns
507
+ * `null` when no object — or the base image — is selected.
508
+ */
509
+ getActiveObjectProps(): ObjectProps | null;
510
+ /**
511
+ * Apply style props to the active object (single undo command).
512
+ * No-op when nothing editable is selected.
513
+ */
514
+ updateActiveObject(props: ObjectProps): void;
515
+ /**
516
+ * Read the active object's fill as a UI-friendly {@link FillSpec},
517
+ * resolving a fabric `Gradient` back to mode + colour stops + angle
518
+ * so the style panels can round-trip a gradient on re-select.
519
+ * Returns `null` when no shape / text layer is selected. (C.1b)
520
+ */
521
+ getActiveObjectFill(): FillSpec | null;
522
+ /**
523
+ * Set the active object's fill from a {@link FillSpec}, building a
524
+ * fabric `Gradient` for the linear / radial modes (single undo
525
+ * command). No-op when nothing editable is selected. (C.1b)
526
+ */
527
+ setActiveObjectFill(spec: FillSpec): void;
528
+ /**
529
+ * The active non-image layer's rotation + flip state, so the
530
+ * Rotate / Flip tools can bind to the selected layer rather than
531
+ * the base image. Returns `null` when the base image — or nothing
532
+ * — is the active object (the tools fall back to image state).
533
+ */
534
+ getActiveLayerTransform(): {
535
+ angle: number;
536
+ flipX: boolean;
537
+ flipY: boolean;
538
+ } | null;
539
+ /**
540
+ * Clip the mask target to a shape (single undo command). The target
541
+ * is the active non-image layer when one is selected, otherwise the
542
+ * base image — so selecting a layer masks it, selecting nothing (or
543
+ * the Background) masks the photo. `shape: 'none'` clears the mask.
544
+ */
545
+ setMask(spec: MaskSpec): void;
546
+ /**
547
+ * Read the mask target's current {@link MaskSpec} so the Mask tool
548
+ * can seed its controls on (re)selection. Returns the default
549
+ * (`shape: 'none'`) when the target has no mask, or `null` when
550
+ * there is no target at all (no image loaded).
551
+ */
552
+ getMask(): MaskSpec | null;
553
+ undo(): Promise<void>;
554
+ redo(): Promise<void>;
555
+ canUndo(): boolean;
556
+ canRedo(): boolean;
557
+ /**
558
+ * Forget every entry currently in the redo stack.
559
+ *
560
+ * Used by the live-preview filter tools: each slider drag does an
561
+ * `undo()` followed by an `applyFilter()`, so the redo stack
562
+ * accumulates one stale entry per drag. Calling this on commit
563
+ * leaves the stack in the same shape it would have had if the
564
+ * user had jumped straight to the final value, with no preview
565
+ * cycle at all.
566
+ */
567
+ clearRedoStack(): void;
568
+ /**
569
+ * Whether the flattened render contains any transparency, so the
570
+ * save flow can force a PNG export instead of flattening a masked /
571
+ * transparent image onto an opaque (JPEG) background — which loses
572
+ * the cut-away region to opaque black (C.1d). True when the base
573
+ * image or any visible layer carries a mask (`clipPath`), or when
574
+ * the flattened pixels contain a non-opaque alpha value. Cheap for
575
+ * the masking case; scans pixels only as a fallback.
576
+ */
577
+ hasAlpha(): boolean;
578
+ /**
579
+ * Render the current canvas as a Blob. `format` defaults to 'png';
580
+ * `quality` (0..1) applies to lossy formats only.
581
+ */
582
+ export(format?: 'png' | 'jpeg' | 'webp', quality?: number): Promise<Blob>;
583
+ /**
584
+ * Synchronous data-URL export. Useful when the consumer needs the
585
+ * dataURL form directly (e.g., immediate <img src> assignment).
586
+ */
587
+ exportDataUrl(format?: 'png' | 'jpeg' | 'webp', quality?: number): string;
588
+ /** Current canvas dimensions. */
589
+ getCanvasSize(): Size;
590
+ /**
591
+ * Set the viewport zoom centred on the canvas. Pure rendering
592
+ * concern — does not modify image data, does not push to the
593
+ * undo stack. The exported bytes are independent of zoom level.
594
+ *
595
+ * `scale = 1` is 100% (one canvas pixel per CSS pixel). Values
596
+ * are clamped at the consumer level (the state service uses
597
+ * 0.1..5).
598
+ */
599
+ setZoom(scale: number): void;
600
+ /** Read the current viewport zoom. Returns 1 when at 100%. */
601
+ getZoom(): number;
602
+ /** Reset the viewport transform to the identity (zoom = 1, no pan). */
603
+ resetZoom(): void;
604
+ /**
605
+ * Pan the viewport by the given pixel deltas. Pure rendering;
606
+ * does not modify image data or push to undo. Used by the Hand
607
+ * tool's drag handler.
608
+ */
609
+ pan(dx: number, dy: number): void;
610
+ /** Current pan offset in viewport coordinates. */
611
+ getPanOffset(): Point;
612
+ /**
613
+ * Set the pan offset to the given absolute viewport translation,
614
+ * leaving the current zoom in place. Used by the state service
615
+ * to centre the scaled image inside the visible viewport after
616
+ * applying a new zoom factor.
617
+ */
618
+ setPanOffset(offset: Point): void;
619
+ /**
620
+ * Reset the pan offset to zero, leaving the current zoom in
621
+ * place. Distinct from `resetZoom()`, which clears both.
622
+ */
623
+ resetPan(): void;
624
+ /**
625
+ * Compute the zoom factor that fits the image into the given
626
+ * viewport rectangle, leaving a small margin so the image edges
627
+ * stay visible. Not clamped: small images upscale to fit the
628
+ * viewport, large images shrink to fit. The adapter computes
629
+ * from image dimensions vs viewport dimensions directly.
630
+ */
631
+ getFitZoom(viewportSize: Size): number;
632
+ /**
633
+ * Set the canvas cursor. fabric.js owns its own cursor state
634
+ * (defaultCursor / hoverCursor / moveCursor) independent of DOM
635
+ * CSS, so a `cursor: grab` rule on the host element is silently
636
+ * overridden by fabric's defaults. Used by the Hand tool to
637
+ * surface grab / grabbing.
638
+ */
639
+ setCursor(cursor: 'default' | 'grab' | 'grabbing' | 'crosshair'): void;
640
+ /**
641
+ * Toggle fabric's group-selection rectangle and object hit
642
+ * detection. Disabled when the Hand tool is active so a drag on
643
+ * empty canvas pans instead of drawing a marquee, and so pointer
644
+ * events don't get sniped by hit-tests against canvas objects.
645
+ */
646
+ setSelectionEnabled(enabled: boolean): void;
647
+ /**
648
+ * Set the canvas backstore + display dimensions to the given
649
+ * viewport. Owning canvas size in the adapter (rather than
650
+ * inheriting from a vendor's CSS auto-fit layer) is what makes
651
+ * viewport zoom the single, predictable scale knob: small
652
+ * images can upscale to fill the viewport without being clipped
653
+ * by the canvas backstore, and large images shrink through the
654
+ * same setZoom path. Recenters the loaded image on the resized
655
+ * canvas.
656
+ */
657
+ setCanvasDimensions(viewport: Size): void;
658
+ on<T extends EngineEventType>(event: T, handler: EngineEventHandler<T>): void;
659
+ off<T extends EngineEventType>(event: T, handler?: EngineEventHandler<T>): void;
660
+ /** Tear down the engine and free its DOM/canvas resources. */
661
+ destroy(): void;
662
+ /**
663
+ * Direct access to the underlying canvas element. Escape hatch for
664
+ * shells that need to hit the DOM API directly (pixel sampling,
665
+ * pointer-event hookup, etc.). Most shells should not need this.
666
+ */
667
+ readonly canvasElement: HTMLCanvasElement | null;
668
+ }
669
+
670
+ interface FabricEngineCreateOptions {
671
+ container: HTMLElement;
672
+ }
673
+ /**
674
+ * fabric.js v7 adapter for the CoolMS image editor.
675
+ *
676
+ * Mounts a fresh `<canvas>` element inside the consumer-provided
677
+ * container and takes ownership of canvas backstore + CSS dimensions
678
+ * so viewport zoom is the single source of truth for visual scale.
679
+ * No vendor CSS auto-fit layer in front of fabric, so small images
680
+ * (sub-viewport) zoom predictably and large images shrink-to-fit
681
+ * through the same `setZoom` path.
682
+ *
683
+ * fabric 7 flipped the FabricObject default origin from 'left'/'top'
684
+ * (v6) to 'center'/'center'. The crop overlay's `bounds.left/top`
685
+ * positioning math assumes 'left'/'top' semantics, and we keep
686
+ * `image.left/image.top` referring to the visual top-left corner so
687
+ * the snapshot/restore captureTransform path stays a no-op. Image
688
+ * and crop-overlay constructors below pass explicit `originX: 'left',
689
+ * originY: 'top'` to preserve v6 behavior bit-for-bit. The frame
690
+ * overlay mirrors `image.originX/originY` so it follows whatever the
691
+ * image uses.
692
+ *
693
+ * Use `FabricEngineAdapter.create(options)` rather than `new`. The
694
+ * factory wires up the canvas element and seeds initial dimensions
695
+ * from the container's measured size.
696
+ */
697
+ declare class FabricEngineAdapter implements ImageEditorEngine {
698
+ private static readonly TOGGLE_FILTERS;
699
+ private static readonly SLIDER_FILTERS;
700
+ private readonly canvas;
701
+ private readonly canvasEl;
702
+ private readonly container;
703
+ /** The default annotation-shape fill; `setLayerFill(id, null)` restores it. */
704
+ private static readonly DEFAULT_SHAPE_FILL;
705
+ private readonly listeners;
706
+ private readonly commandStack;
707
+ private image;
708
+ private vertexEdit;
709
+ private originalSource;
710
+ private cropOverlay;
711
+ private frameOverlay;
712
+ private mouseTransformSnapshot;
713
+ /** Monotonic id source for shape / text layers. */
714
+ private nextLayerId;
715
+ /**
716
+ * Transform snapshot for a non-image object captured on
717
+ * `mouse:down`, so `object:modified` can build the inverse for
718
+ * undo (mirrors `mouseTransformSnapshot` but for shapes / text).
719
+ */
720
+ private objTransformSnapshot;
721
+ private constructor();
722
+ /**
723
+ * Build an adapter against the given container element. Creates a
724
+ * `<canvas>` child sized to the container and constructs a fabric
725
+ * Canvas around it.
726
+ */
727
+ static create(options: FabricEngineCreateOptions): Promise<FabricEngineAdapter>;
728
+ loadImage(source: string | Blob, name?: string): Promise<Size>;
729
+ reset(): Promise<void>;
730
+ crop(canvasSpaceRect: Rect): Promise<void>;
731
+ enterCropMode(): Promise<void>;
732
+ exitCropMode(): Promise<void>;
733
+ /** Toggle selectability of the annotation layers (used by crop mode). */
734
+ private setLayersInteractive;
735
+ setCropAspectRatio(ratio: number | null): void;
736
+ getCropRect(): Rect | null;
737
+ rotate(degrees: number): Promise<void>;
738
+ flip(axis: 'horizontal' | 'vertical'): Promise<void>;
739
+ resize(size: Size): Promise<void>;
740
+ applyFilter(name: FilterName, params?: FilterParams): Promise<void>;
741
+ removeFilter(name: FilterName): Promise<void>;
742
+ hasFilter(name: FilterName): boolean;
743
+ addShape(kind: ShapeKind, opts?: Partial<ShapeOptions>): void;
744
+ addText(opts?: Partial<TextOptions>): void;
745
+ getImageBounds(): Rect | null;
746
+ addShapeAt(geometry: ShapeGeometry, opts?: AnnotationShapeOptions): string;
747
+ getShapeGeometry(id: string): ShapeGeometry | null;
748
+ setVertexEditing(id: string, enabled: boolean): boolean;
749
+ /**
750
+ * Swap the polygon's default handles for one draggable Control per
751
+ * vertex — the official fabric "custom polygon controls" pattern:
752
+ * `positionHandler` projects `points[i]` through viewport × object
753
+ * transform; `actionHandler` writes the pointer back into
754
+ * `points[i]` (object-local, stroke-compensated) and re-anchors the
755
+ * polygon on a neighbouring vertex after `setDimensions()` so the
756
+ * shape doesn't jump while its bounding box is recomputed.
757
+ */
758
+ private enterVertexEdit;
759
+ private exitVertexEdit;
760
+ private vertexControlPosition;
761
+ private moveVertex;
762
+ /**
763
+ * (Re)build one draggable Control per vertex — used on entering vertex
764
+ * edit AND after a topology change (add/remove) so the `vertexN` controls
765
+ * stay index-aligned with the current `points` array.
766
+ */
767
+ private rebuildVertexControls;
768
+ addVertexAt(id: string, sceneX: number, sceneY: number): boolean;
769
+ removeVertexAt(id: string, sceneX: number, sceneY: number): boolean;
770
+ /** Closest point on segment `a->b` to `p` (all object-local coords). */
771
+ private projectOnSegment;
772
+ /**
773
+ * Replace the polygon's `points` and re-index its vertex controls,
774
+ * keeping one SURVIVING vertex visually fixed across the
775
+ * `setDimensions()` bbox recompute so the shape never jumps.
776
+ *
777
+ * `fromIndex` is the anchor vertex's index in the CURRENT points,
778
+ * `toIndex` its index in the NEW `pts` — the two must be the SAME
779
+ * vertex. For an insert (add) or the identity case both are 0 (the
780
+ * defaults). A removal that drops vertex 0 shifts every later vertex
781
+ * down one, so its surviving anchor lives at different indices before
782
+ * vs after — hence the explicit pair.
783
+ */
784
+ private applyVertexTopology;
785
+ private polygonSizeWithStroke;
786
+ private vertexDragStart;
787
+ private vertexDragEnd;
788
+ getLayers(): LayerInfo[];
789
+ getActiveLayerId(): string | null;
790
+ selectLayer(id: string | null): void;
791
+ setLayerFill(id: string, fill: string | null): void;
792
+ removeLayer(id: string): void;
793
+ setLayerVisibility(id: string, visible: boolean): void;
794
+ setLayerOpacity(id: string, opacity: number): void;
795
+ reorderLayer(id: string, direction: 'up' | 'down' | 'front' | 'back'): void;
796
+ getActiveObjectProps(): ObjectProps | null;
797
+ getActiveObjectFill(): FillSpec | null;
798
+ setActiveObjectFill(spec: FillSpec): void;
799
+ getActiveLayerTransform(): {
800
+ angle: number;
801
+ flipX: boolean;
802
+ flipY: boolean;
803
+ } | null;
804
+ setMask(spec: MaskSpec): void;
805
+ getMask(): MaskSpec | null;
806
+ updateActiveObject(props: ObjectProps): void;
807
+ undo(): Promise<void>;
808
+ redo(): Promise<void>;
809
+ canUndo(): boolean;
810
+ canRedo(): boolean;
811
+ clearRedoStack(): void;
812
+ export(format?: 'png' | 'jpeg' | 'webp', quality?: number): Promise<Blob>;
813
+ exportDataUrl(format?: 'png' | 'jpeg' | 'webp', quality?: number): string;
814
+ hasAlpha(): boolean;
815
+ /** True when `obj` carries a fabric `clipPath` (a mask). */
816
+ private objectHasClip;
817
+ /**
818
+ * Render the base image plus every visible annotation layer into a
819
+ * single bitmap (the export-time flatten). With no annotations the
820
+ * fast path returns the image baked at its own resolution; with
821
+ * annotations we composite each object onto a canvas sized to the
822
+ * image's bounding box, offsetting every object by its position
823
+ * relative to the image (both in canvas-space px, multiplier 1).
824
+ *
825
+ * C.1 keeps transforms whole-image, so the common authoring flow
826
+ * (edit pixels -> annotate -> save) lands annotations at full
827
+ * resolution; a base image the user manually scaled with handles
828
+ * exports at its displayed size (a documented limitation).
829
+ */
830
+ private renderFlattened;
831
+ getCanvasSize(): Size;
832
+ setZoom(scale: number): void;
833
+ getZoom(): number;
834
+ resetZoom(): void;
835
+ pan(dx: number, dy: number): void;
836
+ getPanOffset(): Point;
837
+ setPanOffset(offset: Point): void;
838
+ resetPan(): void;
839
+ getFitZoom(viewportSize: Size): number;
840
+ setCursor(cursor: 'default' | 'grab' | 'grabbing' | 'crosshair'): void;
841
+ setSelectionEnabled(enabled: boolean): void;
842
+ setCanvasDimensions(viewport: Size): void;
843
+ on<T extends EngineEventType>(event: T, handler: EngineEventHandler<T>): void;
844
+ off<T extends EngineEventType>(event: T, handler?: EngineEventHandler<T>): void;
845
+ destroy(): void;
846
+ get canvasElement(): HTMLCanvasElement | null;
847
+ private requireImage;
848
+ /**
849
+ * Add a thin orange rectangle that mirrors the image's transform
850
+ * so the user can see exactly where the image edge is against the
851
+ * dark canvas background. Sent to the back so the image renders
852
+ * on top of the inner half of the stroke; the outer half stays
853
+ * visible just outside the image bounds. `strokeWidth = 2 / zoom`
854
+ * keeps that outer band a constant 1 visual pixel regardless of
855
+ * viewport zoom. Excluded from export by both convention and
856
+ * construction: our `export` / `exportDataUrl` paths render only
857
+ * the FabricImage object, not the canvas.
858
+ */
859
+ private addFrameOverlay;
860
+ /**
861
+ * Refresh the frame to mirror the image's current transform and
862
+ * keep the stroke at a constant 1 visual pixel. Called whenever
863
+ * the image moves, scales, rotates, or flips, and after viewport
864
+ * zoom changes (since the latter only matters for the stroke
865
+ * width). Hidden frame stays hidden but its geometry still
866
+ * updates so the next show is in sync.
867
+ */
868
+ private syncFrameOverlay;
869
+ private removeFrameOverlay;
870
+ /**
871
+ * Install a new FabricImage as the active image after a crop or
872
+ * resize replacement. Adds it to the canvas, centers it in canvas
873
+ * coordinates, drops any leftover viewport zoom/pan from the
874
+ * previous image (otherwise the new, smaller image lands
875
+ * off-screen because the user was zoomed into the corner of the
876
+ * old one), and re-emits `imageLoaded` so consumers refit the
877
+ * view to the new dimensions.
878
+ */
879
+ private replaceImage;
880
+ /**
881
+ * Make sure fabric's WebGL filter backend has a GL canvas large
882
+ * enough to hold the image's full pixel area. Fabric defaults
883
+ * `config.textureSize` to 4096 and constructs a 4096×4096 GL
884
+ * canvas; rendering a 4608×3456 image into that backend leaves
885
+ * a ~512 px right-edge band un-rendered (the WebGL viewport is
886
+ * set to image dims but the GL canvas can't hold them), which
887
+ * `copyGLTo2D` then reads as transparent. That surfaced as the
888
+ * right-side crop the user reported on Sharpen.
889
+ *
890
+ * Fix: bump `config.textureSize` to the next power-of-two that
891
+ * fits the image, capped at 16384, then drop the cached backend
892
+ * so `getFilterBackend()` re-initialises with the new tile size.
893
+ * The probe inside `initFilterBackend` falls back to the Canvas2D
894
+ * backend automatically if the GPU can't support the new size,
895
+ * so this is safe on low-end hardware.
896
+ */
897
+ private ensureFilterBackendFitsImage;
898
+ private pushCommand;
899
+ private resolveFilterCtor;
900
+ private instantiateFilter;
901
+ private attachVendorListeners;
902
+ private captureTransform;
903
+ private transformsEqual;
904
+ private emitImageTransformed;
905
+ /** A filled right-pointing arrow, drawn as a single polygon. */
906
+ private static readonly ARROW_POINTS;
907
+ private createShape;
908
+ /** A fabric fill value: a flat colour string or a `Gradient`. */
909
+ private resolveFill;
910
+ /**
911
+ * Build a fabric solid colour or `Gradient` from a {@link FillSpec}.
912
+ * Gradients use `gradientUnits: 'percentage'`, so coords are 0..1 of
913
+ * the object's bounding box (origin top-left) and the gradient
914
+ * tracks the object through resize / text reflow — fabric applies
915
+ * `ctx.transform(width, 0, 0, height, …)` at paint time, including
916
+ * the `toCanvasElement` flatten the export composites.
917
+ */
918
+ private buildFill;
919
+ /** Resolve a fabric fill (string or `Gradient`) back to a {@link FillSpec}. */
920
+ private readFill;
921
+ /** Best-effort solid-colour hex for the generic prop bag (gradients collapse to their first stop). */
922
+ private fillToHex;
923
+ private isGradient;
924
+ /** Coerce a colour-stop value to an `<input type=color>`-safe 6-digit hex. */
925
+ private colorStopHex;
926
+ private shapeLabel;
927
+ /**
928
+ * Centre `obj` on the image, stamp it as a layer, add + select it,
929
+ * and push an add/remove undo command. Shared by addShape/addText.
930
+ */
931
+ private placeAndAdd;
932
+ private stampLayer;
933
+ /** Every stamped layer object on the canvas (base image + extras). */
934
+ private layerObjects;
935
+ /** Drop every non-image layer (used by load / reset). */
936
+ private removeLayerObjects;
937
+ private findLayer;
938
+ private layerId;
939
+ private layerKind;
940
+ private layerName;
941
+ /** Keep the base image pinned to the canvas floor (z-index 0). */
942
+ private keepImageAtBack;
943
+ /** Notify consumers a layer changed + re-broadcast the active layer. */
944
+ private afterLayerMutation;
945
+ private emitActiveFromCanvas;
946
+ private captureObjectState;
947
+ /**
948
+ * The active object when it's a selectable shape / text layer (not
949
+ * the base image, not nothing). The rotate / flip / mask targeting
950
+ * keys off this: a non-null result means "operate on this layer".
951
+ */
952
+ private activeNonImageLayer;
953
+ /**
954
+ * The mask target: the active non-image layer if one is selected,
955
+ * else the base image (which may be `null` before any load).
956
+ */
957
+ private maskTarget;
958
+ private rotateLayer;
959
+ private flipLayer;
960
+ /**
961
+ * Build a fabric `clipPath` for `spec` against `target`'s own box.
962
+ * The clip is centred on the object (originX/Y = 'center', left/top
963
+ * = 0): fabric renders clipPaths in the object's centred local space
964
+ * before applying the object's own scale, so the mask tracks the
965
+ * object through scale / move and bakes into `toCanvasElement`.
966
+ * Returns `null` for `shape: 'none'` (clears the mask).
967
+ */
968
+ private buildClipPath;
969
+ /**
970
+ * Build a `Path` clip from raw SVG path `d` data, scaling its
971
+ * bounding box to the `cw × ch` clip box (centred via `common`).
972
+ * Shared by the built-in path presets and arbitrary `custom` masks
973
+ * (C.1d) — fabric computes the path's intrinsic bbox, and the
974
+ * `scaleX/Y` maps it onto the target's clip box, so any path tracks
975
+ * scale / move and bakes into `toCanvasElement` like the presets.
976
+ * Returns `null` for empty / unparseable data (clears the mask).
977
+ */
978
+ private buildPathClip;
979
+ private emit;
980
+ }
981
+
982
+ /**
983
+ * Construction options for any `ImageEditorEngine`. The container is
984
+ * required; everything else is optional with sensible defaults.
985
+ *
986
+ * No engine-specific knobs leak into this shape — Toast-UI-only
987
+ * options (selectionStyle borderColor etc.) belong on a Toast-UI-only
988
+ * sub-options bag if a future shell needs to thread them through. For
989
+ * MVP, the defaults are enough.
990
+ */
991
+ interface EngineOptions {
992
+ /** DOM element the engine mounts its canvas into. */
993
+ readonly container: HTMLElement;
994
+ /** Initial canvas size. Defaults to the container's measured size. */
995
+ readonly initialSize?: Size;
996
+ /** Display-side cap for canvas width in CSS pixels. */
997
+ readonly cssMaxWidth?: number;
998
+ /** Display-side cap for canvas height in CSS pixels. */
999
+ readonly cssMaxHeight?: number;
1000
+ /**
1001
+ * Toast UI's anonymised usage telemetry. Off by default; flip true
1002
+ * only with explicit user consent.
1003
+ */
1004
+ readonly usageStatistics?: boolean;
1005
+ }
1006
+
1007
+ /**
1008
+ * Lifecycle states the shell renders against.
1009
+ *
1010
+ * `uninitialized` — fresh service instance, no engine yet.
1011
+ * `loading` — engine creation or initial image load is in flight.
1012
+ * `ready` — engine is mounted and the image is on the canvas.
1013
+ * `error` — engine creation or initial load failed; `errorMessage`
1014
+ * holds the user-facing reason.
1015
+ */
1016
+ type EditorState = 'uninitialized' | 'loading' | 'ready' | 'error';
1017
+ /**
1018
+ * Shared editor state for the in-tree shell.
1019
+ *
1020
+ * Scoped to the `CoolmsImageEditorComponent` instance via that
1021
+ * component's `providers` array, so opening two editors at once would
1022
+ * give each one its own engine, history flags, and active-tool slot.
1023
+ *
1024
+ * Kept deliberately thin: signals + the methods that mutate them. Tool
1025
+ * implementations in beta.4 reach the engine through `engine()` rather
1026
+ * than threading domain logic through this service.
1027
+ */
1028
+ declare class ImageEditorStateService {
1029
+ private readonly engineInstance;
1030
+ readonly state: _angular_core.WritableSignal<EditorState>;
1031
+ readonly canvasSize: _angular_core.WritableSignal<Size | null>;
1032
+ readonly canUndo: _angular_core.WritableSignal<boolean>;
1033
+ readonly canRedo: _angular_core.WritableSignal<boolean>;
1034
+ /** Tool ID set by the right-sidebar picker. beta.3 leaves this null. */
1035
+ readonly activeTool: _angular_core.WritableSignal<string | null>;
1036
+ readonly errorMessage: _angular_core.WritableSignal<string | null>;
1037
+ /**
1038
+ * Viewport zoom level, where 1.0 = 100%. Mirrors the engine's
1039
+ * canvas zoom; mutated through `zoomIn` / `zoomOut` / `resetZoom` /
1040
+ * `setZoom`, never directly by consumers (the value here must
1041
+ * stay in lockstep with what the engine actually rendered).
1042
+ */
1043
+ readonly currentZoom: _angular_core.WritableSignal<number>;
1044
+ /**
1045
+ * Fit-to-viewport zoom — the scale at which the loaded image
1046
+ * fills the canvas-mount (with a small margin). Calculated by
1047
+ * the canvas-mount component once it has measured its viewport,
1048
+ * and re-calculated on container resize. Used as the target for
1049
+ * `resetZoom()` and the full `reset()` so "back to 100%" lands
1050
+ * the user at the same view they saw on first mount.
1051
+ */
1052
+ readonly fitZoom: _angular_core.WritableSignal<number>;
1053
+ /**
1054
+ * Hand-tool toggle. When true, mouse-down + drag on the canvas
1055
+ * pans the viewport instead of doing nothing. The cursor swaps
1056
+ * to grab / grabbing via canvas-mount's class bindings.
1057
+ */
1058
+ readonly handToolActive: _angular_core.WritableSignal<boolean>;
1059
+ readonly imageRotation: _angular_core.WritableSignal<number>;
1060
+ readonly imageScaleX: _angular_core.WritableSignal<number>;
1061
+ readonly imageScaleY: _angular_core.WritableSignal<number>;
1062
+ readonly imageFlipX: _angular_core.WritableSignal<boolean>;
1063
+ readonly imageFlipY: _angular_core.WritableSignal<boolean>;
1064
+ readonly imagePosition: _angular_core.WritableSignal<{
1065
+ x: number;
1066
+ y: number;
1067
+ }>;
1068
+ /**
1069
+ * The layer stack (top-most first), mirrored from the engine's
1070
+ * `layersChanged` event. The Layers panel binds to this. (C.1)
1071
+ */
1072
+ readonly layers: _angular_core.WritableSignal<LayerInfo[]>;
1073
+ /** Active layer id, mirrored from `activeObjectChanged`. (C.1) */
1074
+ readonly activeLayerId: _angular_core.WritableSignal<string | null>;
1075
+ /** Active layer kind, so style panels know which controls apply. (C.1) */
1076
+ readonly activeLayerKind: _angular_core.WritableSignal<LayerKind | null>;
1077
+ /**
1078
+ * The active non-image layer's transform, mirrored from the engine
1079
+ * (C.1c). The Rotate / Flip tools read these via the
1080
+ * `transformTarget*` computeds so a selected shape / text layer is
1081
+ * the transform target instead of the base image.
1082
+ */
1083
+ readonly activeLayerAngle: _angular_core.WritableSignal<number>;
1084
+ readonly activeLayerFlipX: _angular_core.WritableSignal<boolean>;
1085
+ readonly activeLayerFlipY: _angular_core.WritableSignal<boolean>;
1086
+ /** True when a non-image layer (not the Background) is the active object. */
1087
+ private readonly targetingLayer;
1088
+ /**
1089
+ * The rotation / flip the Rotate + Flip tools bind to: the selected
1090
+ * non-image layer's when one is active, else the base image's. (C.1c)
1091
+ */
1092
+ readonly transformTargetRotation: _angular_core.Signal<number>;
1093
+ readonly transformTargetFlipX: _angular_core.Signal<boolean>;
1094
+ readonly transformTargetFlipY: _angular_core.Signal<boolean>;
1095
+ /**
1096
+ * Image scale as a percentage rounded to a whole number. Uses the
1097
+ * absolute scale value so a horizontally-flipped image at scale
1098
+ * -1 reads as 100%, not -100%. Flip is reported separately.
1099
+ */
1100
+ readonly imageScalePercent: _angular_core.Signal<number>;
1101
+ /** Read-only handle the toolbar and tools use to drive the engine. */
1102
+ readonly engine: _angular_core.Signal<ImageEditorEngine | null>;
1103
+ readonly isReady: _angular_core.Signal<boolean>;
1104
+ /**
1105
+ * Create the engine, load the image, and attach the listeners that
1106
+ * keep the history-state signals in sync. Throws (and flips state to
1107
+ * 'error') if either step fails — the shell surfaces the message
1108
+ * through the canvas-mount overlay.
1109
+ *
1110
+ * The 'uninitialized' guard is what makes the service safe to call
1111
+ * from `ngAfterViewInit`: a re-entrant call (e.g. after a hot
1112
+ * module replace cycle in dev) is a no-op rather than a duplicate
1113
+ * mount.
1114
+ */
1115
+ initializeEngine(container: HTMLElement, sourceUrl: string, filename: string): Promise<void>;
1116
+ /**
1117
+ * Tear down the engine and reset every signal to its starting
1118
+ * state. Idempotent so the host can call it from `ngOnDestroy`
1119
+ * without checking `state()` first.
1120
+ */
1121
+ destroyEngine(): void;
1122
+ setActiveTool(toolId: string | null): void;
1123
+ /**
1124
+ * Refresh the cached canvas dimensions after an in-place
1125
+ * operation that the engine doesn't proactively re-measure for
1126
+ * us (Resize tool's primary use). Engine load + initial mount
1127
+ * still write through `initializeEngine`.
1128
+ */
1129
+ updateCanvasSize(size: Size): void;
1130
+ /**
1131
+ * Viewport zoom controls. Clamped 0.05..10 (5%..1000%) — wide
1132
+ * range to accommodate fit-zoom for very large images (a 8000px
1133
+ * image in a 600px viewport fits at ~7%) and the occasional
1134
+ * pixel-peeping zoom-in on small images. Step factor of 1.25
1135
+ * matches the convention used by Photoshop, Affinity, and most
1136
+ * desktop image editors.
1137
+ */
1138
+ private static readonly ZOOM_MIN;
1139
+ private static readonly ZOOM_MAX;
1140
+ private static readonly ZOOM_STEP;
1141
+ setZoom(scale: number): void;
1142
+ zoomIn(): void;
1143
+ zoomOut(): void;
1144
+ resetZoom(): void;
1145
+ /**
1146
+ * Compute and apply the fit-to-viewport zoom for the currently
1147
+ * loaded image. Called by canvas-mount after the engine emits
1148
+ * `imageLoaded` and on container resize. The fabric adapter
1149
+ * owns canvas dimensions, so the returned fit factor drives the
1150
+ * viewport zoom directly (small images upscale beyond 1.0 to
1151
+ * fill the viewport, large images shrink below 1.0 to fit).
1152
+ */
1153
+ applyFitZoom(viewportSize: Size): void;
1154
+ /**
1155
+ * Update the cached fit zoom without touching the live view.
1156
+ * Used by the resize observer so a window resize while the
1157
+ * user has manually zoomed in doesn't yank them back to fit
1158
+ * mid-edit; the next Reset / "%" click picks up the fresh
1159
+ * value instead.
1160
+ */
1161
+ recalculateFitZoom(viewportSize: Size): void;
1162
+ toggleHandTool(): void;
1163
+ /**
1164
+ * Top-toolbar Reset button entry point. Restores the canvas to
1165
+ * the pristine loaded state in one call:
1166
+ * - any active tool is closed (its `ngOnDestroy` runs the
1167
+ * tool's deactivation, e.g. `exitCropMode` for the cropper);
1168
+ * - the Hand tool is deactivated;
1169
+ * - the engine reloads the original image and clears both
1170
+ * undo and redo stacks;
1171
+ * - the viewport pan offset is zeroed and the zoom is set to
1172
+ * the fit-to-viewport value (matches what the user saw on
1173
+ * first mount, not the raw 1.0× that would shrink large
1174
+ * images).
1175
+ *
1176
+ * The order matters: tool deactivation first so a mid-edit
1177
+ * cropper doesn't try to re-render against a stale canvas while
1178
+ * the engine is reloading the original image.
1179
+ */
1180
+ reset(): Promise<void>;
1181
+ private attachEngineListeners;
1182
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ImageEditorStateService, never>;
1183
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<ImageEditorStateService>;
1184
+ }
1185
+
1186
+ /**
1187
+ * Three-pane shell for the CoolMS image editor.
1188
+ *
1189
+ * Composition: top toolbar (action commands + size badge), canvas
1190
+ * mount (centre / left, fills remaining space), right sidebar (tool
1191
+ * picker placeholder, 320 px). The footer (Save / Save as / Cancel)
1192
+ * lives in the host component so the shell can later be embedded in
1193
+ * non-dialog contexts (inline editing, side panels) where the save
1194
+ * flow looks different.
1195
+ *
1196
+ * The shell owns engine lifecycle through `ImageEditorStateService`,
1197
+ * which is provided locally so each shell instance has its own state.
1198
+ * `engineReady` fires once the image is loaded so the host can wire up
1199
+ * its save action against a known-good engine handle.
1200
+ *
1201
+ * Ordering note on `ngAfterViewInit`: the canvas-mount child uses
1202
+ * `static: true` for its container ViewChild, so by the time this
1203
+ * component's `ngAfterViewInit` runs the inner `<div #container>`
1204
+ * exists. The engine adapter is created against that element and the
1205
+ * image is loaded before `engineReady` emits — consumers never see
1206
+ * an unloaded engine.
1207
+ */
1208
+ declare class CoolmsImageEditorComponent implements AfterViewInit, OnDestroy {
1209
+ sourceUrl: _angular_core.InputSignal<string>;
1210
+ sourceFilename: _angular_core.InputSignal<string>;
1211
+ /** Fires after the engine is created and the initial image has loaded. */
1212
+ readonly engineReady: _angular_core.OutputEmitterRef<ImageEditorEngine>;
1213
+ /**
1214
+ * Fires if engine creation or initial load throws.
1215
+ *
1216
+ * `failed`, not `error`: an output named after a native DOM event is
1217
+ * ambiguous in a template -- `(error)` could bind either, and which one
1218
+ * wins is not visible to whoever reads the template.
1219
+ */
1220
+ readonly failed: _angular_core.OutputEmitterRef<Error>;
1221
+ protected readonly state: ImageEditorStateService;
1222
+ private readonly canvasMount;
1223
+ /**
1224
+ * Tracks teardown so an engine that finishes loading after the host
1225
+ * dialog has already been dismissed gets cleaned up promptly. Without
1226
+ * this guard a slow `loadImage` could leave an orphan canvas in the
1227
+ * DOM after `ngOnDestroy`.
1228
+ */
1229
+ private destroyed;
1230
+ constructor();
1231
+ /**
1232
+ * Public escape hatch the host calls before `engine.export()` to
1233
+ * make sure no tool is mid-edit (e.g., user forgot to Apply a
1234
+ * crop). Setting `activeTool` to `null` destroys the properties
1235
+ * component, whose `ngOnDestroy` runs the tool's deactivation
1236
+ * (e.g., `exitCropMode()`) — so the export captures clean canvas
1237
+ * pixels with no overlay.
1238
+ *
1239
+ * The adapter's `exportDataUrl()` also defensively calls
1240
+ * `stopDrawingMode()`, but doing it here too makes the
1241
+ * higher-level intent obvious in the host's save flow.
1242
+ */
1243
+ deactivateActiveTool(): void;
1244
+ /**
1245
+ * Public hook the host calls after an intentional dialog size
1246
+ * change (fullscreen toggle). Delegates to the canvas mount which
1247
+ * re-syncs the fabric canvas dimensions, recalculates fit zoom,
1248
+ * and re-applies it so the image stays centred. The
1249
+ * ResizeObserver inside canvas-mount stays conservative for
1250
+ * casual window resizes (preserves manual zoom).
1251
+ */
1252
+ refit(): void;
1253
+ ngAfterViewInit(): Promise<void>;
1254
+ ngOnDestroy(): void;
1255
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<CoolmsImageEditorComponent, never>;
1256
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<CoolmsImageEditorComponent, "coolms-image-editor", never, { "sourceUrl": { "alias": "sourceUrl"; "required": true; "isSignal": true; }; "sourceFilename": { "alias": "sourceFilename"; "required": true; "isSignal": true; }; }, { "engineReady": "engineReady"; "failed": "failed"; }, never, never, true, never>;
1257
+ }
1258
+
1259
+ /**
1260
+ * Wire-shape types for the CDK Dialog host.
1261
+ *
1262
+ * Discriminated union (`context: 'media' | 'vfs'`) so the host's
1263
+ * save flow can route to the correct backend endpoint without the
1264
+ * caller having to know which one. Each branch carries exactly the
1265
+ * fields that branch needs:
1266
+ *
1267
+ * - **Media context** — the asset is a registered `MediaAsset`,
1268
+ * so save flows through Media's APIs (`POST /media/{id}/replace`
1269
+ * for overwrite, `POST /media/upload` for save-as-new). The
1270
+ * asset's `path` carries the VFS location; we derive the parent
1271
+ * directory from it for the save-as-new target.
1272
+ *
1273
+ * - **VFS context** — the file is a plain VFS file with no
1274
+ * MediaAsset record (e.g., a theme asset, a template image).
1275
+ * Save flows through VFS's binary endpoint
1276
+ * (`POST /vfs/files/binary`). `canWrite` gates the Save button:
1277
+ * read-only files allow Save as (creates a new file in the
1278
+ * parent directory) but not Save (would overwrite the source).
1279
+ */
1280
+ type CoolmsImageEditorHostData = CoolmsImageEditorMediaContext | CoolmsImageEditorVfsContext;
1281
+ interface CoolmsImageEditorMediaContext {
1282
+ readonly context: 'media';
1283
+ readonly asset: {
1284
+ /** Maps to `MediaAsset.id`; backend resolves the VFS node from it. */
1285
+ readonly uuid: string;
1286
+ /** Source URL the engine loads via `loadImage(url)`. */
1287
+ readonly originalUrl: string;
1288
+ /** Display label in the dialog header and default save-as filename. */
1289
+ readonly filename: string;
1290
+ /** Picks the export format (source MIME wins per the existing contract). */
1291
+ readonly mimeType: string;
1292
+ /**
1293
+ * Materialised VFS path of the asset (e.g., `/media/uploads/photo.jpg`).
1294
+ * The save-as-new flow derives the target directory from this — the
1295
+ * new asset lands in the same collection as the source. Optional
1296
+ * because `MediaAssetDto.path` can be null for fresh upload responses;
1297
+ * callers set this when they have it (the editor falls back to
1298
+ * `/media/uploads` when absent).
1299
+ */
1300
+ readonly path?: string | null;
1301
+ /** Pre-load dimensions surfaced in the header before the engine reports its own. */
1302
+ readonly dimensions: {
1303
+ readonly width: number;
1304
+ readonly height: number;
1305
+ } | null;
1306
+ };
1307
+ /** BCP-47 locale; held for future locale work, beta.X doesn't read it. */
1308
+ readonly locale?: string;
1309
+ }
1310
+ interface CoolmsImageEditorVfsContext {
1311
+ readonly context: 'vfs';
1312
+ readonly node: {
1313
+ /** Absolute VFS path of the file. */
1314
+ readonly path: string;
1315
+ /** Caller's effective write permission on the file (mirrors `VfsNodeDto.permissions.write`). */
1316
+ readonly canWrite: boolean;
1317
+ /** URL the engine loads via `loadImage(url)`. */
1318
+ readonly sourceUrl: string;
1319
+ /** Display label in the dialog header and default save-as filename. */
1320
+ readonly filename: string;
1321
+ /** Picks the export format. */
1322
+ readonly mimeType: string;
1323
+ /** Parent directory; the save-as-new flow joins `${parentPath}/${newName}`. */
1324
+ readonly parentPath: string;
1325
+ /** Optional dimensions; null when not available pre-load. */
1326
+ readonly dimensions?: {
1327
+ readonly width: number;
1328
+ readonly height: number;
1329
+ } | null;
1330
+ };
1331
+ }
1332
+ type CoolmsImageEditorHostResult = {
1333
+ readonly kind: 'cancelled';
1334
+ } | {
1335
+ readonly kind: 'saved';
1336
+ readonly mode: 'overwrite' | 'save_as_new';
1337
+ /** Set on the Media context. */
1338
+ readonly assetUuid?: string;
1339
+ /** Set on the VFS context. */
1340
+ readonly vfsPath?: string;
1341
+ };
1342
+
1343
+ type HostState = 'preparing' | 'editing' | 'saving' | 'error';
1344
+ /**
1345
+ * CDK Dialog host wrapping `CoolmsImageEditorComponent`.
1346
+ *
1347
+ * Save flow (discriminated context):
1348
+ *
1349
+ * Media context:
1350
+ * Save -> OverwriteConfirm -> POST /api/v1/media/{id}/replace
1351
+ * SaveAs -> SaveAs prompt -> POST /api/v1/vfs/files/binary (new file under
1352
+ * the source collection; auto-promoted to an asset)
1353
+ *
1354
+ * VFS context:
1355
+ * Save -> OverwriteConfirm -> POST /api/v1/vfs/files/binary (overwrite=true)
1356
+ * SaveAs -> SaveAs prompt -> POST /api/v1/vfs/files/binary (overwrite=false)
1357
+ *
1358
+ * Cancel -> close with `{ kind: 'cancelled' }` in either context.
1359
+ *
1360
+ * Save button gating: in the VFS context, `canWrite=false` disables
1361
+ * Save (overwriting a read-only file would 403 anyway) but Save as
1362
+ * stays enabled — the user can always save a copy if they have
1363
+ * write permission on the parent directory.
1364
+ *
1365
+ * Failures stay non-fatal: the dialog stays open with state set back
1366
+ * to `editing` so the author can retry, and a toast carries a
1367
+ * descriptive message (status code -> friendly label, or `.message`
1368
+ * fallback). The engine remains live throughout.
1369
+ */
1370
+ declare class CoolmsImageEditorHostComponent implements OnInit, OnDestroy {
1371
+ private readonly cdkDialog;
1372
+ private readonly dialogRef;
1373
+ private readonly editService;
1374
+ private readonly toast;
1375
+ private readonly http;
1376
+ private readonly esc;
1377
+ private readonly confirm;
1378
+ private readonly unsaved;
1379
+ private readonly destroyRef;
1380
+ protected readonly data: CoolmsImageEditorHostData;
1381
+ readonly state: _angular_core.WritableSignal<HostState>;
1382
+ readonly errorMessage: _angular_core.WritableSignal<string | null>;
1383
+ /**
1384
+ * Object URL fed into the embedded shell. Resolved asynchronously in
1385
+ * `ngOnInit`: VFS sources go through HttpClient (so the auth interceptor
1386
+ * attaches the Bearer token) and become a `blob:` URL the engine can
1387
+ * load via a regular `<img>` element. Media sources are public, so the
1388
+ * original URL passes through unchanged.
1389
+ */
1390
+ readonly resolvedSourceUrl: _angular_core.WritableSignal<string | null>;
1391
+ /** Tracks the blob: URL we own so `ngOnDestroy` can revoke it. */
1392
+ private ownedObjectUrl;
1393
+ /**
1394
+ * Captured the moment the shell finishes loading the image. Save
1395
+ * actions check this through the `engineReadyOk` signal so the
1396
+ * Save buttons only enable once an engine handle is available.
1397
+ */
1398
+ private engine;
1399
+ private readonly shell?;
1400
+ readonly engineReadyOk: _angular_core.WritableSignal<boolean>;
1401
+ readonly isSaving: _angular_core.Signal<boolean>;
1402
+ /** Save (overwrite) is gated on engine readiness AND, for VFS context, write permission. */
1403
+ readonly canSave: _angular_core.Signal<boolean>;
1404
+ /** Save as is gated only on engine readiness — even read-only sources can be saved as new. */
1405
+ readonly canSaveAs: _angular_core.Signal<boolean>;
1406
+ readonly canClose: _angular_core.Signal<boolean>;
1407
+ /** Filename header label and default save-as name. */
1408
+ readonly filename: _angular_core.Signal<string>;
1409
+ /** Source MIME, drives export format. */
1410
+ readonly mimeType: _angular_core.Signal<string>;
1411
+ readonly dimensionsLabel: _angular_core.Signal<string>;
1412
+ /**
1413
+ * CSS-only fullscreen mode, mirroring viewer-modal's pattern.
1414
+ * Toggle from the header button or by dblclicking a non-interactive
1415
+ * area of the host. ESC exits fullscreen via the EscCoordinator
1416
+ * stack so it composes cleanly with any future dialog-level ESC
1417
+ * handlers; the host opens with CDK `disableClose: true`, so the
1418
+ * dialog itself never closes on ESC — only the X / Cancel buttons
1419
+ * exit the editor.
1420
+ */
1421
+ readonly isFullscreen: _angular_core.WritableSignal<boolean>;
1422
+ get fullscreenClass(): boolean;
1423
+ toggleFullscreen(): void;
1424
+ /**
1425
+ * Double-click anywhere on the dialog toggles fullscreen, EXCEPT
1426
+ * on interactive controls and the editor canvas. The canvas is
1427
+ * the primary work surface and fabric.js may bind its own
1428
+ * dblclick semantics (object enter, text edit, etc.); excluding
1429
+ * it keeps that path clear.
1430
+ */
1431
+ protected onDblClick(event: MouseEvent): void;
1432
+ constructor();
1433
+ ngOnInit(): Promise<void>;
1434
+ ngOnDestroy(): void;
1435
+ onEngineReady(engine: ImageEditorEngine): void;
1436
+ onShellError(err: Error): void;
1437
+ onSave(): Promise<void>;
1438
+ onSaveAs(): Promise<void>;
1439
+ /**
1440
+ * `canClose()` only ever meant "not mid-save" -- there was no dirty
1441
+ * concept here at all, so a crop, a rotate and three filters were
1442
+ * thrown away by Cancel without a word.
1443
+ *
1444
+ * `engine.canUndo()` is the dirty flag we would otherwise have had to
1445
+ * invent: it is true exactly when the user has done something undoable,
1446
+ * and it goes back to false if they undo their way to the start.
1447
+ */
1448
+ onCancel(): void;
1449
+ /** The engine's undo depth IS "has anything been edited". */
1450
+ private hasEdits;
1451
+ private executeSave;
1452
+ private saveMedia;
1453
+ private saveVfs;
1454
+ /**
1455
+ * Pull a user-readable message out of whatever the HTTP layer
1456
+ * threw at us. Angular's `HttpErrorResponse` implements the
1457
+ * `Error` interface but doesn't extend the class, so a plain
1458
+ * `instanceof Error` check misses it and we'd fall through to
1459
+ * "unknown error".
1460
+ */
1461
+ private describeError;
1462
+ /**
1463
+ * Export format. Content wins over source: when the flattened image
1464
+ * has any transparency (a mask / clipPath cut-out, or a transparent
1465
+ * source) we force PNG, because flattening it onto an opaque JPEG
1466
+ * loses the cut-away region to opaque black (the C.1d fix — masked
1467
+ * Save-as previously baked black corners). Otherwise it follows the
1468
+ * source MIME (PNG stays PNG, everything else -> JPEG, the two formats
1469
+ * the underlying canvas reliably encodes).
1470
+ */
1471
+ private deriveFormat;
1472
+ /** Whether the current edit has transparency the export must preserve. */
1473
+ private contentWantsPng;
1474
+ /**
1475
+ * Normalise a filename's extension to match the export format, so a
1476
+ * forced-PNG save (transparency preserved) doesn't keep a `.jpg`
1477
+ * name. `jpg`/`jpeg` both satisfy the JPEG format.
1478
+ */
1479
+ private ensureExtension;
1480
+ private deriveSaveAsName;
1481
+ private parentDir;
1482
+ private joinPath;
1483
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<CoolmsImageEditorHostComponent, never>;
1484
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<CoolmsImageEditorHostComponent, "coolms-image-editor-host", never, {}, {}, never, never, true, never>;
1485
+ }
1486
+
1487
+ export { CoolmsImageEditorComponent, CoolmsImageEditorHostComponent, FabricEngineAdapter, ImageEditorStateService };
1488
+ export type { AnnotationShapeOptions, Bounds, CoolmsImageEditorHostData, CoolmsImageEditorHostResult, EditorState, EngineEventHandler, EngineEventType, EngineEvents, EngineOptions, FilterName, FilterParams, ImageEditorEngine, Point, Rect, ShapeGeometry, Size };