@coolms/image-editor-angular 2.0.0-alpha.2 → 2.0.0-alpha.4

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.
@@ -1,38 +1,36 @@
1
- import * as _angular_core from '@angular/core';
2
- import { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
3
-
1
+ import * as i0 from "@angular/core";
2
+ import { AfterViewInit, InjectionToken, OnDestroy, OnInit } from "@angular/core";
4
3
  /**
5
4
  * Pure geometry primitives shared across the engine and any UI shell
6
5
  * sitting on top. Deliberately framework-agnostic: no Angular, no
7
- * fabric.js, no Toast UI imports — these types are part of the public
6
+ * fabric.js, no Toast UI imports -- these types are part of the public
8
7
  * surface of `@coolms/image-editor` and need to compile without the
9
8
  * peer dependency installed.
10
9
  */
11
10
  interface Point {
12
- readonly x: number;
13
- readonly y: number;
11
+ readonly x: number;
12
+ readonly y: number;
14
13
  }
15
14
  interface Size {
16
- readonly width: number;
17
- readonly height: number;
15
+ readonly width: number;
16
+ readonly height: number;
18
17
  }
19
18
  /**
20
19
  * Axis-aligned rectangle. The engine's `crop(rect)` consumes this shape
21
20
  * directly; UI shells produce one from their cropzone overlay.
22
21
  */
23
22
  interface Rect {
24
- readonly x: number;
25
- readonly y: number;
26
- readonly width: number;
27
- readonly height: number;
23
+ readonly x: number;
24
+ readonly y: number;
25
+ readonly width: number;
26
+ readonly height: number;
28
27
  }
29
28
  interface Bounds {
30
- readonly minWidth: number;
31
- readonly minHeight: number;
32
- readonly maxWidth: number;
33
- readonly maxHeight: number;
29
+ readonly minWidth: number;
30
+ readonly minHeight: number;
31
+ readonly maxWidth: number;
32
+ readonly maxHeight: number;
34
33
  }
35
-
36
34
  /**
37
35
  * Filter contract for the engine.
38
36
  *
@@ -57,14 +55,13 @@ type FilterName = 'brightness' | 'contrast' | 'saturation' | 'sepia' | 'grayscal
57
55
  * `applyFilter(name)` and `removeFilter(name)`).
58
56
  */
59
57
  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;
58
+ readonly brightness?: number;
59
+ readonly contrast?: number;
60
+ readonly saturation?: number;
61
+ readonly blur?: number;
62
+ readonly noise?: number;
63
+ readonly pixelate?: number;
66
64
  }
67
-
68
65
  /**
69
66
  * Object / layer model for the image editor (Track C.1).
70
67
  *
@@ -74,11 +71,11 @@ interface FilterParams {
74
71
  * layer stacked on top. Fabric's canvas is already an ordered object
75
72
  * stack, so a "layer" is just a canvas object carrying a stable id.
76
73
  *
77
- * Resize / filters still operate on the base image only — they're
74
+ * Resize / filters still operate on the base image only -- they're
78
75
  * pixel-baking ops, meant to run before annotations. C.1c makes
79
76
  * **rotate / flip** target the selected non-image layer (falling back
80
77
  * to the base image) and adds **masking** (a clip-path on the target;
81
- * see {@link MaskSpec}); C.1e makes **crop** composition-aware — the
78
+ * see {@link MaskSpec}); C.1e makes **crop** composition-aware -- the
82
79
  * annotation layers shift with the cropped content and clip at the new
83
80
  * edges, so a crop after annotating stays aligned.
84
81
  */
@@ -87,51 +84,51 @@ type LayerKind = 'image' | 'shape' | 'text';
87
84
  /** The shape primitives the Shapes tool can insert. */
88
85
  type ShapeKind = 'rect' | 'ellipse' | 'line' | 'arrow' | 'triangle';
89
86
  /**
90
- * Canvas-space geometry of an annotation shape — the round-trippable
87
+ * Canvas-space geometry of an annotation shape -- the round-trippable
91
88
  * subset consumers that author geometry (e.g. the ImageMap region
92
89
  * page) read and write via `addShapeAt()` / `getShapeGeometry()`.
93
90
  * Coordinates are SCENE space (viewport zoom/pan factored out), the
94
91
  * same space `getImageBounds()` reports in.
95
92
  */
96
93
  type ShapeGeometry = {
97
- kind: 'rect';
98
- rect: {
99
- x: number;
100
- y: number;
101
- width: number;
102
- height: number;
103
- };
94
+ kind: 'rect';
95
+ rect: {
96
+ x: number;
97
+ y: number;
98
+ width: number;
99
+ height: number;
100
+ };
104
101
  } | {
105
- kind: 'ellipse';
106
- center: {
107
- x: number;
108
- y: number;
109
- };
110
- rx: number;
111
- ry: number;
102
+ kind: 'ellipse';
103
+ center: {
104
+ x: number;
105
+ y: number;
106
+ };
107
+ rx: number;
108
+ ry: number;
112
109
  } | {
113
- kind: 'polygon';
114
- points: {
115
- x: number;
116
- y: number;
117
- }[];
110
+ kind: 'polygon';
111
+ points: {
112
+ x: number;
113
+ y: number;
114
+ }[];
118
115
  };
119
116
  /**
120
117
  * Options for `addShapeAt()`. Style keys mirror {@link ShapeOptions};
121
118
  * the extras tune the shape for geometry-authoring use:
122
119
  * - `name`: layer display name (defaults to the shape kind).
123
- * - `lockRotation`: hide the rotate handle — authored geometry is
120
+ * - `lockRotation`: hide the rotate handle -- authored geometry is
124
121
  * axis-aligned (rect regions have no rotation representation).
125
122
  * - `lockNonUniformScaling`: hide the edge handles so corner drags
126
123
  * scale uniformly (keeps a circle circular).
127
124
  */
128
125
  interface AnnotationShapeOptions {
129
- fill?: string;
130
- stroke?: string;
131
- strokeWidth?: number;
132
- name?: string;
133
- lockRotation?: boolean;
134
- lockNonUniformScaling?: boolean;
126
+ fill?: string;
127
+ stroke?: string;
128
+ strokeWidth?: number;
129
+ name?: string;
130
+ lockRotation?: boolean;
131
+ lockNonUniformScaling?: boolean;
135
132
  }
136
133
  /**
137
134
  * How a shape / text layer is filled (C.1b).
@@ -149,10 +146,10 @@ type FillType = 'solid' | 'linear' | 'radial';
149
146
  * (0 = left->right, 90 = top->bottom) and only meaningful for `linear`.
150
147
  */
151
148
  interface FillSpec {
152
- type: FillType;
153
- color: string;
154
- color2: string;
155
- angle: number;
149
+ type: FillType;
150
+ color: string;
151
+ color2: string;
152
+ angle: number;
156
153
  }
157
154
  /**
158
155
  * A read-only view of one layer, surfaced to the Layers panel. The
@@ -160,57 +157,63 @@ interface FillSpec {
160
157
  * the UI binds to (never a live fabric handle).
161
158
  */
162
159
  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;
160
+ readonly id: string;
161
+ readonly kind: LayerKind;
162
+ readonly name: string;
163
+ readonly visible: boolean;
164
+ /** 0..1. */
165
+ readonly opacity: number;
166
+ /**
167
+ * True for the base image: it can't be deleted or reordered below
168
+ * itself (it's the canvas floor). Shapes / text are never locked.
169
+ */
170
+ readonly locked: boolean;
174
171
  }
175
172
  /** Style options for an inserted shape. */
176
173
  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;
174
+ fill: string;
175
+ stroke: string;
176
+ strokeWidth: number;
177
+ /**
178
+ * Richer fill description (solid colour or gradient). When present
179
+ * it takes precedence over the flat `fill` string. The Shapes tool
180
+ * always sends this; `fill` stays a plain-colour fallback for any
181
+ * caller that doesn't. (C.1b)
182
+ */
183
+ fillSpec?: FillSpec;
187
184
  }
188
185
  /** Style options for inserted text. */
189
186
  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;
187
+ text: string;
188
+ fontFamily: string;
189
+ fontSize: number;
190
+ fill: string;
191
+ fontWeight: 'normal' | 'bold';
192
+ fontStyle: 'normal' | 'italic';
193
+ textAlign: 'left' | 'center' | 'right';
194
+ /** Gradient-capable fill; see {@link ShapeOptions.fillSpec}. (C.1b) */
195
+ fillSpec?: FillSpec;
199
196
  }
200
197
  /**
201
198
  * Generic property bag the style panels read/write against the active
202
199
  * object. Keys are fabric property names (`fill`, `stroke`,
203
- * `strokeWidth`, `opacity`, `fontFamily`, …); the engine reads them off
200
+ * `strokeWidth`, `opacity`, `fontFamily`, ...); the engine reads them off
204
201
  * / sets them on the active object without the UI importing fabric.
205
202
  */
206
203
  type ObjectProps = Record<string, string | number>;
204
+ declare const DEFAULT_SHAPE_OPTIONS: ShapeOptions;
205
+ /** Starting fill for the Shapes / Text tools (solid, blue accent). (C.1b) */
206
+ declare const DEFAULT_FILL_SPEC: FillSpec;
207
+ declare const DEFAULT_TEXT_OPTIONS: TextOptions;
208
+ /** Font families offered in the Text tool (web-safe set). */
209
+ declare const TEXT_FONT_FAMILIES: readonly ["Arial", "Helvetica", "Times New Roman", "Georgia", "Courier New", "Verdana", "Trebuchet MS", "Impact"];
207
210
  /**
208
211
  * Mask / clip shapes a layer can be cut to (C.1c / C.1d).
209
212
  *
210
213
  * `none` clears the mask. `rect` / `rounded` / `ellipse` / `triangle`
211
214
  * are geometric primitives; `star` / `heart` / `hexagon` are SVG-path
212
215
  * presets. `custom` (C.1d) clips to arbitrary path geometry the user
213
- * pasted or uploaded as an SVG — see {@link MaskSpec.svgPath}. The
216
+ * pasted or uploaded as an SVG -- see {@link MaskSpec.svgPath}. The
214
217
  * engine builds the matching fabric `clipPath` against the target's own
215
218
  * box, so the mask tracks the object through scale / move, and the
216
219
  * export flatten bakes it in (the clip is part of the object's normal
@@ -227,101 +230,98 @@ type MaskShape = 'none' | 'rect' | 'rounded' | 'ellipse' | 'triangle' | 'star' |
227
230
  * the clip's shorter half-dimension (50 = pill / circle).
228
231
  * - `svgPath` : SVG path `d` data for `shape: 'custom'` (C.1d). Sanitised
229
232
  * geometry extracted from a pasted path string or uploaded
230
- * SVG file — every drawable element is flattened to a single
233
+ * SVG file -- every drawable element is flattened to a single
231
234
  * combined path (paths only, no script / external refs). The
232
235
  * engine scales its bounding box to the clip box exactly
233
236
  * like the built-in path presets, so it tracks scale / move
234
237
  * and round-trips through the `coolmsMask` stamp.
235
238
  */
236
239
  interface MaskSpec {
237
- shape: MaskShape;
238
- inset: number;
239
- radius: number;
240
- svgPath?: string;
240
+ shape: MaskShape;
241
+ inset: number;
242
+ radius: number;
243
+ svgPath?: string;
241
244
  }
242
-
245
+ /** Starting mask for the Mask tool (no clip). (C.1c) */
246
+ declare const DEFAULT_MASK_SPEC: MaskSpec;
243
247
  /**
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.
248
+ * SVG path data for the path-preset masks, normalised to a 0..100 box.
249
+ * The engine scales each to the target's clip box. (C.1c)
249
250
  */
250
-
251
+ declare const MASK_PATHS: Readonly<Record<'star' | 'heart' | 'hexagon', string>>;
251
252
  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
- };
253
+ /** Fires after `loadImage()` resolves. Carries the loaded dimensions. */
254
+ readonly imageLoaded: {
255
+ readonly width: number;
256
+ readonly height: number;
257
+ };
258
+ /** Any operation that mutates the canvas (filter, crop, rotate, etc.). */
259
+ readonly imageChanged: void;
260
+ /** History stacks updated. Use `canUndo`/`canRedo` for current depth. */
261
+ readonly historyStateChanged: {
262
+ readonly canUndo: boolean;
263
+ readonly canRedo: boolean;
264
+ };
265
+ /**
266
+ * Fires whenever the image's transform state changes, from either
267
+ * direct mouse manipulation (corner / rotation handles) or a
268
+ * sidebar tool action (slider drag, flip button). The `source`
269
+ * field tells consumers which path produced the change so that
270
+ * tool components can decide whether to update bound display
271
+ * values without echoing back into the engine.
272
+ */
273
+ readonly imageTransformed: {
274
+ readonly angle: number;
275
+ readonly scaleX: number;
276
+ readonly scaleY: number;
277
+ readonly flipX: boolean;
278
+ readonly flipY: boolean;
279
+ readonly left: number;
280
+ readonly top: number;
281
+ readonly source: 'mouse' | 'sidebar';
282
+ };
283
+ /** A canvas object was activated (selected). */
284
+ readonly objectActivated: {
285
+ readonly objectType: string;
286
+ };
287
+ /** No object is currently selected. */
288
+ readonly objectDeactivated: void;
289
+ /**
290
+ * The layer stack changed -- a shape / text layer was added,
291
+ * removed, reordered, or had its visibility / opacity toggled.
292
+ * Carries no payload: consumers re-pull `getLayers()`. (C.1)
293
+ */
294
+ readonly layersChanged: void;
295
+ /**
296
+ * The active (selected) layer changed -- to another layer, or to
297
+ * none. `id` is the engine's stable layer id; `kind` lets the
298
+ * sidebar decide which style panel is relevant. Distinct from
299
+ * `objectActivated`, which predates the layer model and carries
300
+ * the raw fabric type only. (C.1)
301
+ */
302
+ readonly activeObjectChanged: {
303
+ readonly id: string | null;
304
+ readonly kind: LayerKind | null;
305
+ };
306
+ /**
307
+ * Raw pointer activity in SCENE coordinates (canvas space with the
308
+ * viewport zoom/pan factored out -- the same space `getImageBounds()`
309
+ * and `getShapeGeometry()` report in). The annotation seam for
310
+ * consumers that implement their own draw-by-drag interactions
311
+ * (e.g. the ImageMap region authoring page): `down`->`move`->`up`
312
+ * drives rubber-band shapes, `dblclick` closes polygon paths.
313
+ * `targetLayerId` is the stamped layer under the pointer, or null
314
+ * on empty canvas / when hit-testing is disabled.
315
+ */
316
+ readonly scenePointer: {
317
+ readonly phase: 'down' | 'move' | 'up' | 'dblclick';
318
+ readonly x: number;
319
+ readonly y: number;
320
+ readonly targetLayerId: string | null;
321
+ };
321
322
  }
322
323
  type EngineEventType = keyof EngineEvents;
323
324
  type EngineEventHandler<T extends EngineEventType> = (data: EngineEvents[T]) => void;
324
-
325
325
  /**
326
326
  * Engine contract for image manipulation.
327
327
  *
@@ -338,337 +338,336 @@ type EngineEventHandler<T extends EngineEventType> = (data: EngineEvents[T]) =>
338
338
  * without a tick.
339
339
  */
340
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;
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
668
  }
669
-
670
669
  interface FabricEngineCreateOptions {
671
- container: HTMLElement;
670
+ container: HTMLElement;
672
671
  }
673
672
  /**
674
673
  * fabric.js v7 adapter for the CoolMS image editor.
@@ -694,323 +693,321 @@ interface FabricEngineCreateOptions {
694
693
  * factory wires up the canvas element and seeds initial dimensions
695
694
  * from the container's measured size.
696
695
  */
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;
696
+ export declare class FabricEngineAdapter implements ImageEditorEngine {
697
+ private static readonly TOGGLE_FILTERS;
698
+ private static readonly SLIDER_FILTERS;
699
+ private readonly canvas;
700
+ private readonly canvasEl;
701
+ private readonly container;
702
+ /** The default annotation-shape fill; `setLayerFill(id, null)` restores it. */
703
+ private static readonly DEFAULT_SHAPE_FILL;
704
+ private readonly listeners;
705
+ private readonly commandStack;
706
+ private image;
707
+ private vertexEdit;
708
+ private originalSource;
709
+ private cropOverlay;
710
+ private frameOverlay;
711
+ private mouseTransformSnapshot;
712
+ /** Monotonic id source for shape / text layers. */
713
+ private nextLayerId;
714
+ /**
715
+ * Transform snapshot for a non-image object captured on
716
+ * `mouse:down`, so `object:modified` can build the inverse for
717
+ * undo (mirrors `mouseTransformSnapshot` but for shapes / text).
718
+ */
719
+ private objTransformSnapshot;
720
+ private constructor();
721
+ /**
722
+ * Build an adapter against the given container element. Creates a
723
+ * `<canvas>` child sized to the container and constructs a fabric
724
+ * Canvas around it.
725
+ */
726
+ static create(options: FabricEngineCreateOptions): Promise<FabricEngineAdapter>;
727
+ loadImage(source: string | Blob, name?: string): Promise<Size>;
728
+ reset(): Promise<void>;
729
+ crop(canvasSpaceRect: Rect): Promise<void>;
730
+ enterCropMode(): Promise<void>;
731
+ exitCropMode(): Promise<void>;
732
+ /** Toggle selectability of the annotation layers (used by crop mode). */
733
+ private setLayersInteractive;
734
+ setCropAspectRatio(ratio: number | null): void;
735
+ getCropRect(): Rect | null;
736
+ rotate(degrees: number): Promise<void>;
737
+ flip(axis: 'horizontal' | 'vertical'): Promise<void>;
738
+ resize(size: Size): Promise<void>;
739
+ applyFilter(name: FilterName, params?: FilterParams): Promise<void>;
740
+ removeFilter(name: FilterName): Promise<void>;
741
+ hasFilter(name: FilterName): boolean;
742
+ addShape(kind: ShapeKind, opts?: Partial<ShapeOptions>): void;
743
+ addText(opts?: Partial<TextOptions>): void;
744
+ getImageBounds(): Rect | null;
745
+ addShapeAt(geometry: ShapeGeometry, opts?: AnnotationShapeOptions): string;
746
+ getShapeGeometry(id: string): ShapeGeometry | null;
747
+ setVertexEditing(id: string, enabled: boolean): boolean;
748
+ /**
749
+ * Swap the polygon's default handles for one draggable Control per
750
+ * vertex -- the official fabric "custom polygon controls" pattern:
751
+ * `positionHandler` projects `points[i]` through viewport x object
752
+ * transform; `actionHandler` writes the pointer back into
753
+ * `points[i]` (object-local, stroke-compensated) and re-anchors the
754
+ * polygon on a neighbouring vertex after `setDimensions()` so the
755
+ * shape doesn't jump while its bounding box is recomputed.
756
+ */
757
+ private enterVertexEdit;
758
+ private exitVertexEdit;
759
+ private vertexControlPosition;
760
+ private moveVertex;
761
+ /**
762
+ * (Re)build one draggable Control per vertex -- used on entering vertex
763
+ * edit AND after a topology change (add/remove) so the `vertexN` controls
764
+ * stay index-aligned with the current `points` array.
765
+ */
766
+ private rebuildVertexControls;
767
+ addVertexAt(id: string, sceneX: number, sceneY: number): boolean;
768
+ removeVertexAt(id: string, sceneX: number, sceneY: number): boolean;
769
+ /** Closest point on segment `a->b` to `p` (all object-local coords). */
770
+ private projectOnSegment;
771
+ /**
772
+ * Replace the polygon's `points` and re-index its vertex controls,
773
+ * keeping one SURVIVING vertex visually fixed across the
774
+ * `setDimensions()` bbox recompute so the shape never jumps.
775
+ *
776
+ * `fromIndex` is the anchor vertex's index in the CURRENT points,
777
+ * `toIndex` its index in the NEW `pts` -- the two must be the SAME
778
+ * vertex. For an insert (add) or the identity case both are 0 (the
779
+ * defaults). A removal that drops vertex 0 shifts every later vertex
780
+ * down one, so its surviving anchor lives at different indices before
781
+ * vs after -- hence the explicit pair.
782
+ */
783
+ private applyVertexTopology;
784
+ private polygonSizeWithStroke;
785
+ private vertexDragStart;
786
+ private vertexDragEnd;
787
+ getLayers(): LayerInfo[];
788
+ getActiveLayerId(): string | null;
789
+ selectLayer(id: string | null): void;
790
+ setLayerFill(id: string, fill: string | null): void;
791
+ removeLayer(id: string): void;
792
+ setLayerVisibility(id: string, visible: boolean): void;
793
+ setLayerOpacity(id: string, opacity: number): void;
794
+ reorderLayer(id: string, direction: 'up' | 'down' | 'front' | 'back'): void;
795
+ getActiveObjectProps(): ObjectProps | null;
796
+ getActiveObjectFill(): FillSpec | null;
797
+ setActiveObjectFill(spec: FillSpec): void;
798
+ getActiveLayerTransform(): {
799
+ angle: number;
800
+ flipX: boolean;
801
+ flipY: boolean;
802
+ } | null;
803
+ setMask(spec: MaskSpec): void;
804
+ getMask(): MaskSpec | null;
805
+ updateActiveObject(props: ObjectProps): void;
806
+ undo(): Promise<void>;
807
+ redo(): Promise<void>;
808
+ canUndo(): boolean;
809
+ canRedo(): boolean;
810
+ clearRedoStack(): void;
811
+ export(format?: 'png' | 'jpeg' | 'webp', quality?: number): Promise<Blob>;
812
+ exportDataUrl(format?: 'png' | 'jpeg' | 'webp', quality?: number): string;
813
+ hasAlpha(): boolean;
814
+ /** True when `obj` carries a fabric `clipPath` (a mask). */
815
+ private objectHasClip;
816
+ /**
817
+ * Render the base image plus every visible annotation layer into a
818
+ * single bitmap (the export-time flatten). With no annotations the
819
+ * fast path returns the image baked at its own resolution; with
820
+ * annotations we composite each object onto a canvas sized to the
821
+ * image's bounding box, offsetting every object by its position
822
+ * relative to the image (both in canvas-space px, multiplier 1).
823
+ *
824
+ * C.1 keeps transforms whole-image, so the common authoring flow
825
+ * (edit pixels -> annotate -> save) lands annotations at full
826
+ * resolution; a base image the user manually scaled with handles
827
+ * exports at its displayed size (a documented limitation).
828
+ */
829
+ private renderFlattened;
830
+ getCanvasSize(): Size;
831
+ setZoom(scale: number): void;
832
+ getZoom(): number;
833
+ resetZoom(): void;
834
+ pan(dx: number, dy: number): void;
835
+ getPanOffset(): Point;
836
+ setPanOffset(offset: Point): void;
837
+ resetPan(): void;
838
+ getFitZoom(viewportSize: Size): number;
839
+ setCursor(cursor: 'default' | 'grab' | 'grabbing' | 'crosshair'): void;
840
+ setSelectionEnabled(enabled: boolean): void;
841
+ setCanvasDimensions(viewport: Size): void;
842
+ on<T extends EngineEventType>(event: T, handler: EngineEventHandler<T>): void;
843
+ off<T extends EngineEventType>(event: T, handler?: EngineEventHandler<T>): void;
844
+ destroy(): void;
845
+ get canvasElement(): HTMLCanvasElement | null;
846
+ private requireImage;
847
+ /**
848
+ * Add a thin orange rectangle that mirrors the image's transform
849
+ * so the user can see exactly where the image edge is against the
850
+ * dark canvas background. Sent to the back so the image renders
851
+ * on top of the inner half of the stroke; the outer half stays
852
+ * visible just outside the image bounds. `strokeWidth = 2 / zoom`
853
+ * keeps that outer band a constant 1 visual pixel regardless of
854
+ * viewport zoom. Excluded from export by both convention and
855
+ * construction: our `export` / `exportDataUrl` paths render only
856
+ * the FabricImage object, not the canvas.
857
+ */
858
+ private addFrameOverlay;
859
+ /**
860
+ * Refresh the frame to mirror the image's current transform and
861
+ * keep the stroke at a constant 1 visual pixel. Called whenever
862
+ * the image moves, scales, rotates, or flips, and after viewport
863
+ * zoom changes (since the latter only matters for the stroke
864
+ * width). Hidden frame stays hidden but its geometry still
865
+ * updates so the next show is in sync.
866
+ */
867
+ private syncFrameOverlay;
868
+ private removeFrameOverlay;
869
+ /**
870
+ * Install a new FabricImage as the active image after a crop or
871
+ * resize replacement. Adds it to the canvas, centers it in canvas
872
+ * coordinates, drops any leftover viewport zoom/pan from the
873
+ * previous image (otherwise the new, smaller image lands
874
+ * off-screen because the user was zoomed into the corner of the
875
+ * old one), and re-emits `imageLoaded` so consumers refit the
876
+ * view to the new dimensions.
877
+ */
878
+ private replaceImage;
879
+ /**
880
+ * Make sure fabric's WebGL filter backend has a GL canvas large
881
+ * enough to hold the image's full pixel area. Fabric defaults
882
+ * `config.textureSize` to 4096 and constructs a 4096x4096 GL
883
+ * canvas; rendering a 4608x3456 image into that backend leaves
884
+ * a ~512 px right-edge band un-rendered (the WebGL viewport is
885
+ * set to image dims but the GL canvas can't hold them), which
886
+ * `copyGLTo2D` then reads as transparent. That surfaced as the
887
+ * right-side crop the user reported on Sharpen.
888
+ *
889
+ * Fix: bump `config.textureSize` to the next power-of-two that
890
+ * fits the image, capped at 16384, then drop the cached backend
891
+ * so `getFilterBackend()` re-initialises with the new tile size.
892
+ * The probe inside `initFilterBackend` falls back to the Canvas2D
893
+ * backend automatically if the GPU can't support the new size,
894
+ * so this is safe on low-end hardware.
895
+ */
896
+ private ensureFilterBackendFitsImage;
897
+ private pushCommand;
898
+ private resolveFilterCtor;
899
+ private instantiateFilter;
900
+ private attachVendorListeners;
901
+ private captureTransform;
902
+ private transformsEqual;
903
+ private emitImageTransformed;
904
+ /** A filled right-pointing arrow, drawn as a single polygon. */
905
+ private static readonly ARROW_POINTS;
906
+ private createShape;
907
+ /** A fabric fill value: a flat colour string or a `Gradient`. */
908
+ private resolveFill;
909
+ /**
910
+ * Build a fabric solid colour or `Gradient` from a {@link FillSpec}.
911
+ * Gradients use `gradientUnits: 'percentage'`, so coords are 0..1 of
912
+ * the object's bounding box (origin top-left) and the gradient
913
+ * tracks the object through resize / text reflow -- fabric applies
914
+ * `ctx.transform(width, 0, 0, height, ...)` at paint time, including
915
+ * the `toCanvasElement` flatten the export composites.
916
+ */
917
+ private buildFill;
918
+ /** Resolve a fabric fill (string or `Gradient`) back to a {@link FillSpec}. */
919
+ private readFill;
920
+ /** Best-effort solid-colour hex for the generic prop bag (gradients collapse to their first stop). */
921
+ private fillToHex;
922
+ private isGradient;
923
+ /** Coerce a colour-stop value to an `<input type=color>`-safe 6-digit hex. */
924
+ private colorStopHex;
925
+ private shapeLabel;
926
+ /**
927
+ * Centre `obj` on the image, stamp it as a layer, add + select it,
928
+ * and push an add/remove undo command. Shared by addShape/addText.
929
+ */
930
+ private placeAndAdd;
931
+ private stampLayer;
932
+ /** Every stamped layer object on the canvas (base image + extras). */
933
+ private layerObjects;
934
+ /** Drop every non-image layer (used by load / reset). */
935
+ private removeLayerObjects;
936
+ private findLayer;
937
+ private layerId;
938
+ private layerKind;
939
+ private layerName;
940
+ /** Keep the base image pinned to the canvas floor (z-index 0). */
941
+ private keepImageAtBack;
942
+ /** Notify consumers a layer changed + re-broadcast the active layer. */
943
+ private afterLayerMutation;
944
+ private emitActiveFromCanvas;
945
+ private captureObjectState;
946
+ /**
947
+ * The active object when it's a selectable shape / text layer (not
948
+ * the base image, not nothing). The rotate / flip / mask targeting
949
+ * keys off this: a non-null result means "operate on this layer".
950
+ */
951
+ private activeNonImageLayer;
952
+ /**
953
+ * The mask target: the active non-image layer if one is selected,
954
+ * else the base image (which may be `null` before any load).
955
+ */
956
+ private maskTarget;
957
+ private rotateLayer;
958
+ private flipLayer;
959
+ /**
960
+ * Build a fabric `clipPath` for `spec` against `target`'s own box.
961
+ * The clip is centred on the object (originX/Y = 'center', left/top
962
+ * = 0): fabric renders clipPaths in the object's centred local space
963
+ * before applying the object's own scale, so the mask tracks the
964
+ * object through scale / move and bakes into `toCanvasElement`.
965
+ * Returns `null` for `shape: 'none'` (clears the mask).
966
+ */
967
+ private buildClipPath;
968
+ /**
969
+ * Build a `Path` clip from raw SVG path `d` data, scaling its
970
+ * bounding box to the `cw x ch` clip box (centred via `common`).
971
+ * Shared by the built-in path presets and arbitrary `custom` masks
972
+ * (C.1d) -- fabric computes the path's intrinsic bbox, and the
973
+ * `scaleX/Y` maps it onto the target's clip box, so any path tracks
974
+ * scale / move and bakes into `toCanvasElement` like the presets.
975
+ * Returns `null` for empty / unparseable data (clears the mask).
976
+ */
977
+ private buildPathClip;
978
+ private emit;
980
979
  }
981
-
982
980
  /**
983
981
  * Construction options for any `ImageEditorEngine`. The container is
984
982
  * required; everything else is optional with sensible defaults.
985
983
  *
986
- * No engine-specific knobs leak into this shape — Toast-UI-only
984
+ * No engine-specific knobs leak into this shape -- Toast-UI-only
987
985
  * options (selectionStyle borderColor etc.) belong on a Toast-UI-only
988
986
  * sub-options bag if a future shell needs to thread them through. For
989
987
  * MVP, the defaults are enough.
990
988
  */
991
989
  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;
990
+ /** DOM element the engine mounts its canvas into. */
991
+ readonly container: HTMLElement;
992
+ /** Initial canvas size. Defaults to the container's measured size. */
993
+ readonly initialSize?: Size;
994
+ /** Display-side cap for canvas width in CSS pixels. */
995
+ readonly cssMaxWidth?: number;
996
+ /** Display-side cap for canvas height in CSS pixels. */
997
+ readonly cssMaxHeight?: number;
998
+ /**
999
+ * Toast UI's anonymised usage telemetry. Off by default; flip true
1000
+ * only with explicit user consent.
1001
+ */
1002
+ readonly usageStatistics?: boolean;
1005
1003
  }
1006
-
1007
1004
  /**
1008
1005
  * Lifecycle states the shell renders against.
1009
1006
  *
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`
1007
+ * `uninitialized` -- fresh service instance, no engine yet.
1008
+ * `loading` -- engine creation or initial image load is in flight.
1009
+ * `ready` -- engine is mounted and the image is on the canvas.
1010
+ * `error` -- engine creation or initial load failed; `errorMessage`
1014
1011
  * holds the user-facing reason.
1015
1012
  */
1016
1013
  type EditorState = 'uninitialized' | 'loading' | 'ready' | 'error';
@@ -1025,170 +1022,172 @@ type EditorState = 'uninitialized' | 'loading' | 'ready' | 'error';
1025
1022
  * implementations in beta.4 reach the engine through `engine()` rather
1026
1023
  * than threading domain logic through this service.
1027
1024
  */
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>;
1025
+ export declare class ImageEditorStateService {
1026
+ private readonly engineInstance;
1027
+ readonly state: import("@angular/core").WritableSignal<EditorState>;
1028
+ readonly canvasSize: import("@angular/core").WritableSignal<Size | null>;
1029
+ readonly canUndo: import("@angular/core").WritableSignal<boolean>;
1030
+ readonly canRedo: import("@angular/core").WritableSignal<boolean>;
1031
+ /** The active tool's id, set from the tool rail or a tool's shortcut; null when none is. */
1032
+ readonly activeTool: import("@angular/core").WritableSignal<string | null>;
1033
+ readonly errorMessage: import("@angular/core").WritableSignal<string | null>;
1034
+ /**
1035
+ * Viewport zoom level, where 1.0 = 100%. Mirrors the engine's
1036
+ * canvas zoom; mutated through `zoomIn` / `zoomOut` / `resetZoom` /
1037
+ * `setZoom`, never directly by consumers (the value here must
1038
+ * stay in lockstep with what the engine actually rendered).
1039
+ */
1040
+ readonly currentZoom: import("@angular/core").WritableSignal<number>;
1041
+ /**
1042
+ * Fit-to-viewport zoom -- the scale at which the loaded image
1043
+ * fills the canvas-mount (with a small margin). Calculated by
1044
+ * the canvas-mount component once it has measured its viewport,
1045
+ * and re-calculated on container resize. Used as the target for
1046
+ * `resetZoom()` and the full `reset()` so "back to 100%" lands
1047
+ * the user at the same view they saw on first mount.
1048
+ */
1049
+ readonly fitZoom: import("@angular/core").WritableSignal<number>;
1050
+ /**
1051
+ * Hand-tool toggle. When true, mouse-down + drag on the canvas
1052
+ * pans the viewport instead of doing nothing. The cursor swaps
1053
+ * to grab / grabbing via canvas-mount's class bindings.
1054
+ */
1055
+ readonly handToolActive: import("@angular/core").WritableSignal<boolean>;
1056
+ readonly imageRotation: import("@angular/core").WritableSignal<number>;
1057
+ readonly imageScaleX: import("@angular/core").WritableSignal<number>;
1058
+ readonly imageScaleY: import("@angular/core").WritableSignal<number>;
1059
+ readonly imageFlipX: import("@angular/core").WritableSignal<boolean>;
1060
+ readonly imageFlipY: import("@angular/core").WritableSignal<boolean>;
1061
+ readonly imagePosition: import("@angular/core").WritableSignal<{
1062
+ x: number;
1063
+ y: number;
1064
+ }>;
1065
+ /**
1066
+ * The layer stack (top-most first), mirrored from the engine's
1067
+ * `layersChanged` event. The Layers panel binds to this. (C.1)
1068
+ */
1069
+ readonly layers: import("@angular/core").WritableSignal<LayerInfo[]>;
1070
+ /** Active layer id, mirrored from `activeObjectChanged`. (C.1) */
1071
+ readonly activeLayerId: import("@angular/core").WritableSignal<string | null>;
1072
+ /** Active layer kind, so style panels know which controls apply. (C.1) */
1073
+ readonly activeLayerKind: import("@angular/core").WritableSignal<LayerKind | null>;
1074
+ /**
1075
+ * The active non-image layer's transform, mirrored from the engine
1076
+ * (C.1c). The Rotate / Flip tools read these via the
1077
+ * `transformTarget*` computeds so a selected shape / text layer is
1078
+ * the transform target instead of the base image.
1079
+ */
1080
+ readonly activeLayerAngle: import("@angular/core").WritableSignal<number>;
1081
+ readonly activeLayerFlipX: import("@angular/core").WritableSignal<boolean>;
1082
+ readonly activeLayerFlipY: import("@angular/core").WritableSignal<boolean>;
1083
+ /** True when a non-image layer (not the Background) is the active object. */
1084
+ private readonly targetingLayer;
1085
+ /**
1086
+ * The rotation / flip the Rotate + Flip tools bind to: the selected
1087
+ * non-image layer's when one is active, else the base image's. (C.1c)
1088
+ */
1089
+ readonly transformTargetRotation: import("@angular/core").Signal<number>;
1090
+ readonly transformTargetFlipX: import("@angular/core").Signal<boolean>;
1091
+ readonly transformTargetFlipY: import("@angular/core").Signal<boolean>;
1092
+ /**
1093
+ * Image scale as a percentage rounded to a whole number. Uses the
1094
+ * absolute scale value so a horizontally-flipped image at scale
1095
+ * -1 reads as 100%, not -100%. Flip is reported separately.
1096
+ */
1097
+ readonly imageScalePercent: import("@angular/core").Signal<number>;
1098
+ /** Read-only handle the toolbar and tools use to drive the engine. */
1099
+ readonly engine: import("@angular/core").Signal<ImageEditorEngine | null>;
1100
+ readonly isReady: import("@angular/core").Signal<boolean>;
1101
+ /**
1102
+ * Create the engine, load the image, and attach the listeners that
1103
+ * keep the history-state signals in sync. Throws (and flips state to
1104
+ * 'error') if either step fails -- the shell surfaces the message
1105
+ * through the canvas-mount overlay.
1106
+ *
1107
+ * The 'uninitialized' guard is what makes the service safe to call
1108
+ * from `ngAfterViewInit`: a re-entrant call (e.g. after a hot
1109
+ * module replace cycle in dev) is a no-op rather than a duplicate
1110
+ * mount.
1111
+ */
1112
+ initializeEngine(container: HTMLElement, sourceUrl: string, filename: string): Promise<void>;
1113
+ /**
1114
+ * Tear down the engine and reset every signal to its starting
1115
+ * state. Idempotent so the host can call it from `ngOnDestroy`
1116
+ * without checking `state()` first.
1117
+ */
1118
+ destroyEngine(): void;
1119
+ setActiveTool(toolId: string | null): void;
1120
+ /**
1121
+ * Refresh the cached canvas dimensions after an in-place
1122
+ * operation that the engine doesn't proactively re-measure for
1123
+ * us (Resize tool's primary use). Engine load + initial mount
1124
+ * still write through `initializeEngine`.
1125
+ */
1126
+ updateCanvasSize(size: Size): void;
1127
+ /**
1128
+ * Viewport zoom controls. Clamped 0.05..10 (5%..1000%) -- wide
1129
+ * range to accommodate fit-zoom for very large images (a 8000px
1130
+ * image in a 600px viewport fits at ~7%) and the occasional
1131
+ * pixel-peeping zoom-in on small images. Step factor of 1.25
1132
+ * matches the convention used by Photoshop, Affinity, and most
1133
+ * desktop image editors.
1134
+ */
1135
+ private static readonly ZOOM_MIN;
1136
+ private static readonly ZOOM_MAX;
1137
+ private static readonly ZOOM_STEP;
1138
+ setZoom(scale: number): void;
1139
+ zoomIn(): void;
1140
+ zoomOut(): void;
1141
+ resetZoom(): void;
1142
+ /**
1143
+ * Compute and apply the fit-to-viewport zoom for the currently
1144
+ * loaded image. Called by canvas-mount after the engine emits
1145
+ * `imageLoaded` and on container resize. The fabric adapter
1146
+ * owns canvas dimensions, so the returned fit factor drives the
1147
+ * viewport zoom directly (small images upscale beyond 1.0 to
1148
+ * fill the viewport, large images shrink below 1.0 to fit).
1149
+ */
1150
+ applyFitZoom(viewportSize: Size): void;
1151
+ /**
1152
+ * Update the cached fit zoom without touching the live view.
1153
+ * Used by the resize observer so a window resize while the
1154
+ * user has manually zoomed in doesn't yank them back to fit
1155
+ * mid-edit; the next Reset / "%" click picks up the fresh
1156
+ * value instead.
1157
+ */
1158
+ recalculateFitZoom(viewportSize: Size): void;
1159
+ toggleHandTool(): void;
1160
+ /**
1161
+ * Top-toolbar Reset button entry point. Restores the canvas to
1162
+ * the pristine loaded state in one call:
1163
+ * - any active tool is closed (its `ngOnDestroy` runs the
1164
+ * tool's deactivation, e.g. `exitCropMode` for the cropper);
1165
+ * - the Hand tool is deactivated;
1166
+ * - the engine reloads the original image and clears both
1167
+ * undo and redo stacks;
1168
+ * - the viewport pan offset is zeroed and the zoom is set to
1169
+ * the fit-to-viewport value (matches what the user saw on
1170
+ * first mount, not the raw 1.0x that would shrink large
1171
+ * images).
1172
+ *
1173
+ * The order matters: tool deactivation first so a mid-edit
1174
+ * cropper doesn't try to re-render against a stale canvas while
1175
+ * the engine is reloading the original image.
1176
+ */
1177
+ reset(): Promise<void>;
1178
+ private attachEngineListeners;
1179
+ static ɵfac: i0.ɵɵFactoryDeclaration<ImageEditorStateService, never>;
1180
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
1184
1181
  }
1185
-
1186
1182
  /**
1187
- * Three-pane shell for the CoolMS image editor.
1183
+ * Shell for the CoolMS image editor.
1188
1184
  *
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)
1185
+ * Composition: top toolbar (action commands + size badge); below it the
1186
+ * tool rail on the left (the tools in their groups, with names or as
1187
+ * icons), the canvas mount (fills the remaining space), and the
1188
+ * properties panel on the right (the active tool's, or the image's own
1189
+ * when no tool is active). A tool's shortcut selects it while the editor
1190
+ * has the keyboard. The footer (Save / Save as / Cancel)
1192
1191
  * lives in the host component so the shell can later be embedded in
1193
1192
  * non-dialog contexts (inline editing, side panels) where the save
1194
1193
  * flow looks different.
@@ -1202,60 +1201,156 @@ declare class ImageEditorStateService {
1202
1201
  * `static: true` for its container ViewChild, so by the time this
1203
1202
  * component's `ngAfterViewInit` runs the inner `<div #container>`
1204
1203
  * exists. The engine adapter is created against that element and the
1205
- * image is loaded before `engineReady` emits — consumers never see
1204
+ * image is loaded before `engineReady` emits -- consumers never see
1206
1205
  * an unloaded engine.
1207
1206
  */
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>;
1207
+ export declare class CoolmsImageEditorComponent implements AfterViewInit, OnDestroy {
1208
+ sourceUrl: import("@angular/core").InputSignal<string>;
1209
+ sourceFilename: import("@angular/core").InputSignal<string>;
1210
+ /** Fires after the engine is created and the initial image has loaded. */
1211
+ readonly engineReady: import("@angular/core").OutputEmitterRef<ImageEditorEngine>;
1212
+ /**
1213
+ * Fires if engine creation or initial load throws.
1214
+ *
1215
+ * `failed`, not `error`: an output named after a native DOM event is
1216
+ * ambiguous in a template -- `(error)` could bind either, and which one
1217
+ * wins is not visible to whoever reads the template.
1218
+ */
1219
+ readonly failed: import("@angular/core").OutputEmitterRef<Error>;
1220
+ protected readonly state: ImageEditorStateService;
1221
+ private readonly registry;
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
+ /**
1255
+ * Delete on the selected shape or text layer removes it (one undoable step, as the Layers panel's delete). With
1256
+ * nothing selected, or the base image selected, it does nothing. True when it removed a layer.
1257
+ */
1258
+ onKeydown(event: KeyboardEvent): boolean;
1259
+ ngOnDestroy(): void;
1260
+ static ɵfac: i0.ɵɵFactoryDeclaration<CoolmsImageEditorComponent, never>;
1261
+ static ɵcmp: i0.ɵɵComponentDeclaration<CoolmsImageEditorComponent, "coolms-image-editor", never, {
1262
+ "sourceUrl": {
1263
+ "alias": "sourceUrl";
1264
+ "required": true;
1265
+ "isSignal": true;
1266
+ };
1267
+ "sourceFilename": {
1268
+ "alias": "sourceFilename";
1269
+ "required": true;
1270
+ "isSignal": true;
1271
+ };
1272
+ }, {
1273
+ "engineReady": "engineReady";
1274
+ "failed": "failed";
1275
+ }, never, never, true, never>;
1276
+ }
1277
+ /**
1278
+ * The image editor's own text: keys, and the base catalogue that holds their English. A host translates by handing the
1279
+ * editor a `Translator`; one that does not hear a key answers what the base catalogue holds, never the key.
1280
+ *
1281
+ * Imports nothing outside `src/core/` (the lint rule for this directory refuses it).
1282
+ */
1283
+ /** Answers a key's text, `%name%` placeholders filled from `params`. */
1284
+ type Translator = (key: string, params?: Readonly<Record<string, string | number>>) => string;
1285
+ export declare const IMAGE_EDITOR_MESSAGES: Readonly<Record<string, string>>;
1286
+ /** Fills `%name%` placeholders; a placeholder with no value is left as it is. */
1287
+ declare function fill(text: string, params?: Readonly<Record<string, string | number>>): string;
1288
+ /** The base catalogue's translator: a key it does not hold answers the key itself, so a gap shows in development. */
1289
+ export declare const baseTranslator: Translator;
1290
+ /**
1291
+ * A host's catalogue in front of the base one: a key the host does not hold, or holds blank, answers the base
1292
+ * catalogue's English. A partial translation degrades one message at a time, never to a key or an empty label.
1293
+ */
1294
+ export declare function catalogueTranslator(messages: Readonly<Record<string, string>>): Translator;
1295
+ /**
1296
+ * The image editor's tools as data, with no framework: what a tool is called, where it sits on the rail, and which key
1297
+ * selects it. An Angular or React host maps a tool's id to its own properties component; nothing here names one.
1298
+ *
1299
+ * Imports nothing outside `src/core/` (the lint rule for this directory refuses it).
1300
+ */
1301
+ /** The rail's groups, in the order they are drawn. */
1302
+ type ToolGroup = 'transform' | 'insert' | 'filter' | 'layers';
1303
+ declare const TOOL_GROUPS: readonly {
1304
+ readonly id: ToolGroup;
1305
+ readonly labelKey: string;
1306
+ }[];
1307
+ interface ToolDefinition {
1308
+ readonly id: string;
1309
+ /** The tool's name, as a message key (`imageEditor.tool.<id>`). */
1310
+ readonly labelKey: string;
1311
+ /** A Bootstrap Icons class. */
1312
+ readonly icon: string;
1313
+ readonly group: ToolGroup;
1314
+ /** Its place within the group, smallest first. */
1315
+ readonly order: number;
1316
+ /** One printable key that selects the tool while the editor has the keyboard, e.g. `c`. */
1317
+ readonly shortcut?: string;
1257
1318
  }
1258
-
1319
+ /** How the rail draws its tools: with their names, or as icons whose names show as tooltips. */
1320
+ type RailMode = 'labels' | 'icons';
1321
+ export declare const DEFAULT_RAIL_MODE: RailMode;
1322
+ /** A stored preference read back: anything but a known mode is the default. */
1323
+ export declare function railModeFrom(stored: unknown): RailMode;
1324
+ /** The tools of each group, groups in rail order and tools by `order` then id; empty groups left out. */
1325
+ declare function groupTools<T extends ToolDefinition>(tools: readonly T[]): {
1326
+ group: ToolGroup;
1327
+ labelKey: string;
1328
+ tools: T[];
1329
+ }[];
1330
+ /**
1331
+ * The tool a key selects: an unmodified printable key equal to a tool's shortcut (case ignored), or null. A key with
1332
+ * Ctrl, Alt or Meta is never a tool's: those belong to the editor's commands and the browser.
1333
+ */
1334
+ declare function toolForKey<T extends ToolDefinition>(tools: readonly T[], event: {
1335
+ readonly key: string;
1336
+ readonly ctrlKey: boolean;
1337
+ readonly altKey: boolean;
1338
+ readonly metaKey: boolean;
1339
+ }): T | null;
1340
+ /**
1341
+ * The editor's text. A host provides its own translator (the platform's catalogues, say); without one the editor speaks
1342
+ * the base catalogue's English (`core/messages.ts`).
1343
+ */
1344
+ export declare const IMAGE_EDITOR_TRANSLATOR: InjectionToken<Translator>;
1345
+ /** Where the rail's mode is remembered. The admin keeps it in the person's preferences, so it follows them. */
1346
+ interface RailModeStore {
1347
+ /** The mode last chosen, or null when none was. */
1348
+ read(): RailMode | null;
1349
+ write(mode: RailMode): void;
1350
+ }
1351
+ /** Remembered for this page load only: what a host without preferences gets. */
1352
+ export declare function sessionRailModeStore(): RailModeStore;
1353
+ export declare const IMAGE_EDITOR_RAIL_MODE: InjectionToken<RailModeStore>;
1259
1354
  /**
1260
1355
  * Wire-shape types for the CDK Dialog host.
1261
1356
  *
@@ -1264,13 +1359,13 @@ declare class CoolmsImageEditorComponent implements AfterViewInit, OnDestroy {
1264
1359
  * caller having to know which one. Each branch carries exactly the
1265
1360
  * fields that branch needs:
1266
1361
  *
1267
- * - **Media context** — the asset is a registered `MediaAsset`,
1362
+ * - **Media context** -- the asset is a registered `MediaAsset`,
1268
1363
  * so save flows through Media's APIs (`POST /media/{id}/replace`
1269
1364
  * for overwrite, `POST /media/upload` for save-as-new). The
1270
1365
  * asset's `path` carries the VFS location; we derive the parent
1271
1366
  * directory from it for the save-as-new target.
1272
1367
  *
1273
- * - **VFS context** — the file is a plain VFS file with no
1368
+ * - **VFS context** -- the file is a plain VFS file with no
1274
1369
  * MediaAsset record (e.g., a theme asset, a template image).
1275
1370
  * Save flows through VFS's binary endpoint
1276
1371
  * (`POST /vfs/files/binary`). `canWrite` gates the Save button:
@@ -1279,67 +1374,88 @@ declare class CoolmsImageEditorComponent implements AfterViewInit, OnDestroy {
1279
1374
  */
1280
1375
  type CoolmsImageEditorHostData = CoolmsImageEditorMediaContext | CoolmsImageEditorVfsContext;
1281
1376
  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;
1377
+ readonly context: 'media';
1378
+ readonly asset: {
1379
+ /** Maps to `MediaAsset.id`; backend resolves the VFS node from it. */
1380
+ readonly uuid: string;
1381
+ /** Source URL the engine loads via `loadImage(url)` when no `sourceUrl` is given. */
1382
+ readonly originalUrl: string;
1383
+ /**
1384
+ * Where the asset's own bytes are read with their version (the response's ETag), as for a file: the version a
1385
+ * replace names as If-Match is the version of the pixels opened, from that one read (2026-10-15). Without it
1386
+ * the image is shown from `originalUrl`, which names no version, and Save is off: the edits go to a new image.
1387
+ */
1388
+ readonly sourceUrl?: string;
1389
+ /** Display label in the dialog header and default save-as filename. */
1390
+ readonly filename: string;
1391
+ /** Picks the export format (source MIME wins per the existing contract). */
1392
+ readonly mimeType: string;
1393
+ /**
1394
+ * Materialised VFS path of the asset (e.g., `/media/uploads/photo.jpg`).
1395
+ * The save-as-new flow derives the target directory from this -- the
1396
+ * new asset lands in the same collection as the source. Optional
1397
+ * because `MediaAssetDto.path` can be null for fresh upload responses;
1398
+ * callers set this when they have it (the editor falls back to
1399
+ * `/media/uploads` when absent).
1400
+ */
1401
+ readonly path?: string | null;
1402
+ /** Pre-load dimensions surfaced in the header before the engine reports its own. */
1403
+ readonly dimensions: {
1404
+ readonly width: number;
1405
+ readonly height: number;
1406
+ } | null;
1407
+ };
1408
+ /** BCP-47 locale; held for future locale work, beta.X doesn't read it. */
1409
+ readonly locale?: string;
1309
1410
  }
1310
1411
  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
- };
1412
+ readonly context: 'vfs';
1413
+ readonly node: {
1414
+ /** Absolute VFS path of the file. */
1415
+ readonly path: string;
1416
+ /** The file's node id: the host's file watch follows it to say when it changes or goes elsewhere. */
1417
+ readonly id?: string;
1418
+ /** Caller's effective write permission on the file (mirrors `VfsNodeDto.permissions.write`). */
1419
+ readonly canWrite: boolean;
1420
+ /**
1421
+ * Offered when `canWrite` is false: what Save does INSTEAD of being
1422
+ * dead (the elevation client requirement, point 2). The caller asks
1423
+ * the server for elevation and resolves true when the session has
1424
+ * it; the host then treats the file as writable and saves. Absent,
1425
+ * Save is disabled as before. The host decides nothing itself.
1426
+ */
1427
+ readonly requestWrite?: () => Promise<boolean>;
1428
+ /**
1429
+ * Where the file's own bytes are read, and with them their version (the response's ETag): the version an
1430
+ * overwrite names as If-Match is the version of the pixels the editor opened, from that one read -- a file
1431
+ * changed after it was listed is edited as it is now, and a copy served from a cache carries its own older
1432
+ * version, so its save is refused (412), never written over the newer file (2026-10-15). A read that names no
1433
+ * version leaves Save off: the edits can go to a new image only.
1434
+ */
1435
+ readonly sourceUrl: string;
1436
+ /** Display label in the dialog header and default save-as filename. */
1437
+ readonly filename: string;
1438
+ /** Picks the export format. */
1439
+ readonly mimeType: string;
1440
+ /** Parent directory; the save-as-new flow joins `${parentPath}/${newName}`. */
1441
+ readonly parentPath: string;
1442
+ /** Optional dimensions; null when not available pre-load. */
1443
+ readonly dimensions?: {
1444
+ readonly width: number;
1445
+ readonly height: number;
1446
+ } | null;
1447
+ };
1331
1448
  }
1332
1449
  type CoolmsImageEditorHostResult = {
1333
- readonly kind: 'cancelled';
1450
+ readonly kind: 'cancelled';
1334
1451
  } | {
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;
1452
+ readonly kind: 'saved';
1453
+ readonly mode: 'overwrite' | 'save_as_new';
1454
+ /** Set on the Media context. */
1455
+ readonly assetUuid?: string;
1456
+ /** Set on the VFS context. */
1457
+ readonly vfsPath?: string;
1341
1458
  };
1342
-
1343
1459
  type HostState = 'preparing' | 'editing' | 'saving' | 'error';
1344
1460
  /**
1345
1461
  * CDK Dialog host wrapping `CoolmsImageEditorComponent`.
@@ -1359,7 +1475,7 @@ type HostState = 'preparing' | 'editing' | 'saving' | 'error';
1359
1475
  *
1360
1476
  * Save button gating: in the VFS context, `canWrite=false` disables
1361
1477
  * 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
1478
+ * stays enabled -- the user can always save a copy if they have
1363
1479
  * write permission on the parent directory.
1364
1480
  *
1365
1481
  * Failures stay non-fatal: the dialog stays open with state set back
@@ -1367,122 +1483,153 @@ type HostState = 'preparing' | 'editing' | 'saving' | 'error';
1367
1483
  * descriptive message (status code -> friendly label, or `.message`
1368
1484
  * fallback). The engine remains live throughout.
1369
1485
  */
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>;
1486
+ /**
1487
+ * The format Save writes a source back as, or null when Save may not overwrite it: only PNG and
1488
+ * JPEG, which the editor encodes as themselves. GIF (animated or not), SVG, TIFF, HEIC, BMP and WebP -- until its export
1489
+ * is verified in every supported browser -- have Save as only: overwriting them would put PNG or JPEG bytes under their
1490
+ * name, and an animation or vectors would be lost.
1491
+ */
1492
+ declare function writtenBackAs(mimeType: string): 'png' | 'jpeg' | null;
1493
+ export declare class CoolmsImageEditorHostComponent implements OnInit, OnDestroy {
1494
+ private readonly cdkDialog;
1495
+ private readonly dialogRef;
1496
+ private readonly editService;
1497
+ private readonly toast;
1498
+ private readonly http;
1499
+ private readonly esc;
1500
+ private readonly confirm;
1501
+ private readonly unsaved;
1502
+ private readonly destroyRef;
1503
+ protected readonly data: CoolmsImageEditorHostData;
1504
+ /**
1505
+ * The file was changed or deleted elsewhere while the editor is open (2026-10-14: "the editor says so, from the VFS
1506
+ * change event"). Said above the canvas; Save then answers 412 / 404 and goes to "Save as".
1507
+ */
1508
+ protected readonly changedElsewhere: import("@angular/core").WritableSignal<"changed" | "deleted" | null>;
1509
+ readonly state: import("@angular/core").WritableSignal<HostState>;
1510
+ readonly errorMessage: import("@angular/core").WritableSignal<string | null>;
1511
+ /**
1512
+ * Object URL fed into the embedded shell. Resolved asynchronously in
1513
+ * `ngOnInit`: VFS sources go through HttpClient (so the auth interceptor
1514
+ * attaches the Bearer token) and become a `blob:` URL the engine can
1515
+ * load via a regular `<img>` element. Media sources are public, so the
1516
+ * original URL passes through unchanged.
1517
+ */
1518
+ readonly resolvedSourceUrl: import("@angular/core").WritableSignal<string | null>;
1519
+ /** Tracks the blob: URL we own so `ngOnDestroy` can revoke it. */
1520
+ private ownedObjectUrl;
1521
+ /**
1522
+ * Captured the moment the shell finishes loading the image. Save
1523
+ * actions check this through the `engineReadyOk` signal so the
1524
+ * Save buttons only enable once an engine handle is available.
1525
+ */
1526
+ private engine;
1527
+ private readonly shell?;
1528
+ readonly engineReadyOk: import("@angular/core").WritableSignal<boolean>;
1529
+ readonly isSaving: import("@angular/core").Signal<boolean>;
1530
+ /**
1531
+ * Set once the caller's `requestWrite` resolved true: the server elevated
1532
+ * the session, so the flag it computed before no longer describes it.
1533
+ */
1534
+ private readonly writeGranted;
1535
+ /** Whether the VFS file is writable: the flag, or an elevation granted since. */
1536
+ private readonly canWrite;
1537
+ /**
1538
+ * Save (overwrite) is gated on engine readiness AND, for VFS context,
1539
+ * write permission -- unless the caller offered `requestWrite`, in which
1540
+ * case the button stays live and asks for elevation instead of sitting
1541
+ * dead (the control stays live and the server decides).
1542
+ */
1543
+ readonly canSave: import("@angular/core").Signal<boolean>;
1544
+ /** The version of the bytes the editor opened: the ETag of the read that delivered them. */
1545
+ private readonly readVersion;
1546
+ private readonly canRequestWrite;
1547
+ /** Save as is gated only on engine readiness -- even read-only sources can be saved as new. */
1548
+ readonly canSaveAs: import("@angular/core").Signal<boolean>;
1549
+ readonly canClose: import("@angular/core").Signal<boolean>;
1550
+ /** Filename header label and default save-as name. */
1551
+ readonly filename: import("@angular/core").Signal<string>;
1552
+ /** Source MIME, drives export format. */
1553
+ readonly mimeType: import("@angular/core").Signal<string>;
1554
+ /** Whether Save may overwrite the source: the editor writes it back in its own format. */
1555
+ readonly savesInItsOwnFormat: import("@angular/core").Signal<boolean>;
1556
+ /** Why Save is off for this format, said on the button; null when it is not the format. */
1557
+ readonly saveFormatNote: import("@angular/core").Signal<"Save writes an image back only as PNG or JPEG, in its own format. Use Save as for this one." | null>;
1558
+ readonly dimensionsLabel: import("@angular/core").Signal<string>;
1559
+ /**
1560
+ * CSS-only fullscreen mode, mirroring viewer-modal's pattern.
1561
+ * Toggle from the header button or by dblclicking a non-interactive
1562
+ * area of the host. ESC exits fullscreen via the EscCoordinator
1563
+ * stack so it composes cleanly with any future dialog-level ESC
1564
+ * handlers; the host opens with CDK `disableClose: true`, so the
1565
+ * dialog itself never closes on ESC -- only the X / Cancel buttons
1566
+ * exit the editor.
1567
+ */
1568
+ readonly isFullscreen: import("@angular/core").WritableSignal<boolean>;
1569
+ get fullscreenClass(): boolean;
1570
+ toggleFullscreen(): void;
1571
+ /**
1572
+ * Double-click anywhere on the dialog toggles fullscreen, EXCEPT
1573
+ * on interactive controls and the editor canvas. The canvas is
1574
+ * the primary work surface and fabric.js may bind its own
1575
+ * dblclick semantics (object enter, text edit, etc.); excluding
1576
+ * it keeps that path clear.
1577
+ */
1578
+ protected onDblClick(event: MouseEvent): void;
1579
+ constructor();
1580
+ ngOnInit(): Promise<void>;
1581
+ ngOnDestroy(): void;
1582
+ onEngineReady(engine: ImageEditorEngine): void;
1583
+ onShellError(err: Error): void;
1584
+ onSave(): Promise<void>;
1585
+ onSaveAs(): Promise<void>;
1586
+ /**
1587
+ * `canClose()` only ever meant "not mid-save" -- there was no dirty
1588
+ * concept here at all, so a crop, a rotate and three filters were
1589
+ * thrown away by Cancel without a word.
1590
+ *
1591
+ * `engine.canUndo()` is the dirty flag we would otherwise have had to
1592
+ * invent: it is true exactly when the user has done something undoable,
1593
+ * and it goes back to false if they undo their way to the start.
1594
+ */
1595
+ onCancel(): void;
1596
+ /** The engine's undo depth IS "has anything been edited". */
1597
+ private hasEdits;
1598
+ private executeSave;
1599
+ private saveMedia;
1600
+ private saveVfs;
1601
+ /**
1602
+ * Pull a user-readable message out of whatever the HTTP layer
1603
+ * threw at us. Angular's `HttpErrorResponse` implements the
1604
+ * `Error` interface but doesn't extend the class, so a plain
1605
+ * `instanceof Error` check misses it and we'd fall through to
1606
+ * "unknown error".
1607
+ */
1608
+ private describeError;
1609
+ /**
1610
+ * Export format. Content wins over source: when the flattened image
1611
+ * has any transparency (a mask / clipPath cut-out, or a transparent
1612
+ * source) we force PNG, because flattening it onto an opaque JPEG
1613
+ * loses the cut-away region to opaque black (the C.1d fix -- masked
1614
+ * Save-as previously baked black corners). Otherwise it follows the
1615
+ * source MIME (PNG stays PNG, everything else -> JPEG, the two formats
1616
+ * the underlying canvas reliably encodes).
1617
+ */
1618
+ private deriveFormat;
1619
+ /** Whether the current edit has transparency the export must preserve. */
1620
+ private contentWantsPng;
1621
+ /**
1622
+ * Normalise a filename's extension to match the export format, so a
1623
+ * forced-PNG save (transparency preserved) doesn't keep a `.jpg`
1624
+ * name. `jpg`/`jpeg` both satisfy the JPEG format.
1625
+ */
1626
+ private ensureExtension;
1627
+ private deriveSaveAsName;
1628
+ private parentDir;
1629
+ private joinPath;
1630
+ static ɵfac: i0.ɵɵFactoryDeclaration<CoolmsImageEditorHostComponent, never>;
1631
+ static ɵcmp: i0.ɵɵComponentDeclaration<CoolmsImageEditorHostComponent, "coolms-image-editor-host", never, {}, {}, never, never, true, never>;
1485
1632
  }
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 };
1633
+ /** The version an ETag names: `"abc"` and `W/"abc"` are both `abc`; no ETag, no version. */
1634
+ declare function versionOf(etag: string | null): string | null;
1635
+ export type { AnnotationShapeOptions, Bounds, CoolmsImageEditorHostData, CoolmsImageEditorHostResult, EditorState, EngineEventHandler, EngineEventType, EngineEvents, EngineOptions, FilterName, FilterParams, ImageEditorEngine, Point, RailMode, RailModeStore, Rect, ShapeGeometry, Size, ToolDefinition, ToolGroup, Translator };