@bensitu/image-editor 1.5.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +367 -518
  2. package/dist/cjs/index.cjs +5422 -0
  3. package/dist/cjs/index.cjs.map +1 -0
  4. package/dist/esm/animation/animation-queue.js +67 -0
  5. package/dist/esm/animation/animation-queue.js.map +1 -0
  6. package/dist/esm/core/callback-reporter.js +23 -0
  7. package/dist/esm/core/callback-reporter.js.map +1 -0
  8. package/dist/esm/core/default-options.js +322 -0
  9. package/dist/esm/core/default-options.js.map +1 -0
  10. package/dist/esm/core/errors.js +156 -0
  11. package/dist/esm/core/errors.js.map +1 -0
  12. package/dist/esm/core/operation-guard.js +129 -0
  13. package/dist/esm/core/operation-guard.js.map +1 -0
  14. package/dist/esm/core/public-types.js +4 -0
  15. package/dist/esm/core/public-types.js.map +1 -0
  16. package/dist/esm/core/state-serializer.js +251 -0
  17. package/dist/esm/core/state-serializer.js.map +1 -0
  18. package/dist/esm/crop/crop-controller.js +403 -0
  19. package/dist/esm/crop/crop-controller.js.map +1 -0
  20. package/dist/esm/export/export-format.js +53 -0
  21. package/dist/esm/export/export-format.js.map +1 -0
  22. package/dist/esm/export/export-service.js +596 -0
  23. package/dist/esm/export/export-service.js.map +1 -0
  24. package/dist/esm/fabric/fabric-adapter.js +37 -0
  25. package/dist/esm/fabric/fabric-adapter.js.map +1 -0
  26. package/dist/esm/fabric/fabric-animation.js +37 -0
  27. package/dist/esm/fabric/fabric-animation.js.map +1 -0
  28. package/dist/esm/history/command.js +2 -0
  29. package/dist/esm/history/command.js.map +1 -0
  30. package/dist/esm/history/history-manager.js +103 -0
  31. package/dist/esm/history/history-manager.js.map +1 -0
  32. package/dist/esm/image/image-loader.js +245 -0
  33. package/dist/esm/image/image-loader.js.map +1 -0
  34. package/dist/esm/image/image-resampler.js +55 -0
  35. package/dist/esm/image/image-resampler.js.map +1 -0
  36. package/dist/esm/image/layout-manager.js +224 -0
  37. package/dist/esm/image/layout-manager.js.map +1 -0
  38. package/dist/esm/image/transform-controller.js +132 -0
  39. package/dist/esm/image/transform-controller.js.map +1 -0
  40. package/dist/esm/image-editor.js +1740 -0
  41. package/dist/esm/image-editor.js.map +1 -0
  42. package/dist/esm/index.js +5 -0
  43. package/dist/esm/index.js.map +1 -0
  44. package/dist/esm/mask/mask-factory.js +332 -0
  45. package/dist/esm/mask/mask-factory.js.map +1 -0
  46. package/dist/esm/mask/mask-label-manager.js +120 -0
  47. package/dist/esm/mask/mask-label-manager.js.map +1 -0
  48. package/dist/esm/mask/mask-list.js +47 -0
  49. package/dist/esm/mask/mask-list.js.map +1 -0
  50. package/dist/esm/mask/mask-style.js +182 -0
  51. package/dist/esm/mask/mask-style.js.map +1 -0
  52. package/dist/esm/ui/dom-bindings.js +60 -0
  53. package/dist/esm/ui/dom-bindings.js.map +1 -0
  54. package/dist/esm/ui/ui-state.js +25 -0
  55. package/dist/esm/ui/ui-state.js.map +1 -0
  56. package/dist/esm/ui/visibility-state.js +11 -0
  57. package/dist/esm/ui/visibility-state.js.map +1 -0
  58. package/dist/esm/utils/canvas-region.js +100 -0
  59. package/dist/esm/utils/canvas-region.js.map +1 -0
  60. package/dist/esm/utils/dom.js +6 -0
  61. package/dist/esm/utils/dom.js.map +1 -0
  62. package/dist/esm/utils/file.js +53 -0
  63. package/dist/esm/utils/file.js.map +1 -0
  64. package/dist/esm/utils/number.js +24 -0
  65. package/dist/esm/utils/number.js.map +1 -0
  66. package/dist/esm/utils/timeout.js +17 -0
  67. package/dist/esm/utils/timeout.js.map +1 -0
  68. package/dist/types/animation/animation-queue.d.ts +111 -0
  69. package/dist/types/animation/animation-queue.d.ts.map +1 -0
  70. package/dist/types/core/callback-reporter.d.ts +125 -0
  71. package/dist/types/core/callback-reporter.d.ts.map +1 -0
  72. package/dist/types/core/default-options.d.ts +56 -0
  73. package/dist/types/core/default-options.d.ts.map +1 -0
  74. package/dist/types/core/errors.d.ts +142 -0
  75. package/dist/types/core/errors.d.ts.map +1 -0
  76. package/dist/types/core/operation-guard.d.ts +192 -0
  77. package/dist/types/core/operation-guard.d.ts.map +1 -0
  78. package/dist/types/core/public-types.d.ts +678 -0
  79. package/dist/types/core/public-types.d.ts.map +1 -0
  80. package/dist/types/core/state-serializer.d.ts +301 -0
  81. package/dist/types/core/state-serializer.d.ts.map +1 -0
  82. package/dist/types/crop/crop-controller.d.ts +407 -0
  83. package/dist/types/crop/crop-controller.d.ts.map +1 -0
  84. package/dist/types/export/export-format.d.ts +136 -0
  85. package/dist/types/export/export-format.d.ts.map +1 -0
  86. package/dist/types/export/export-service.d.ts +333 -0
  87. package/dist/types/export/export-service.d.ts.map +1 -0
  88. package/dist/types/fabric/fabric-adapter.d.ts +74 -0
  89. package/dist/types/fabric/fabric-adapter.d.ts.map +1 -0
  90. package/dist/types/fabric/fabric-animation.d.ts +141 -0
  91. package/dist/types/fabric/fabric-animation.d.ts.map +1 -0
  92. package/dist/types/history/command.d.ts +16 -0
  93. package/dist/types/history/command.d.ts.map +1 -0
  94. package/dist/types/history/history-manager.d.ts +129 -0
  95. package/dist/types/history/history-manager.d.ts.map +1 -0
  96. package/dist/types/image/image-loader.d.ts +265 -0
  97. package/dist/types/image/image-loader.d.ts.map +1 -0
  98. package/dist/types/image/image-resampler.d.ts +139 -0
  99. package/dist/types/image/image-resampler.d.ts.map +1 -0
  100. package/dist/types/image/layout-manager.d.ts +255 -0
  101. package/dist/types/image/layout-manager.d.ts.map +1 -0
  102. package/dist/types/image/transform-controller.d.ts +287 -0
  103. package/dist/types/image/transform-controller.d.ts.map +1 -0
  104. package/dist/types/image-editor.d.ts +650 -0
  105. package/dist/types/image-editor.d.ts.map +1 -0
  106. package/dist/types/index.d.cts +31 -0
  107. package/dist/types/index.d.cts.map +1 -0
  108. package/dist/types/index.d.ts +31 -0
  109. package/dist/types/index.d.ts.map +1 -0
  110. package/dist/types/mask/mask-factory.d.ts +209 -0
  111. package/dist/types/mask/mask-factory.d.ts.map +1 -0
  112. package/dist/types/mask/mask-label-manager.d.ts +171 -0
  113. package/dist/types/mask/mask-label-manager.d.ts.map +1 -0
  114. package/dist/types/mask/mask-list.d.ts +144 -0
  115. package/dist/types/mask/mask-list.d.ts.map +1 -0
  116. package/dist/types/mask/mask-style.d.ts +338 -0
  117. package/dist/types/mask/mask-style.d.ts.map +1 -0
  118. package/dist/types/ui/dom-bindings.d.ts +103 -0
  119. package/dist/types/ui/dom-bindings.d.ts.map +1 -0
  120. package/dist/types/ui/ui-state.d.ts +112 -0
  121. package/dist/types/ui/ui-state.d.ts.map +1 -0
  122. package/dist/types/ui/visibility-state.d.ts +77 -0
  123. package/dist/types/ui/visibility-state.d.ts.map +1 -0
  124. package/dist/types/utils/canvas-region.d.ts +177 -0
  125. package/dist/types/utils/canvas-region.d.ts.map +1 -0
  126. package/dist/types/utils/dom.d.ts +26 -0
  127. package/dist/types/utils/dom.d.ts.map +1 -0
  128. package/dist/types/utils/file.d.ts +80 -0
  129. package/dist/types/utils/file.d.ts.map +1 -0
  130. package/dist/types/utils/number.d.ts +132 -0
  131. package/dist/types/utils/number.d.ts.map +1 -0
  132. package/dist/types/utils/timeout.d.ts +84 -0
  133. package/dist/types/utils/timeout.d.ts.map +1 -0
  134. package/dist/umd/image-editor.umd.js +2 -0
  135. package/dist/umd/image-editor.umd.js.map +1 -0
  136. package/package.json +72 -66
  137. package/dist/image-editor.cjs +0 -4407
  138. package/dist/image-editor.cjs.map +0 -7
  139. package/dist/image-editor.esm.js +0 -4376
  140. package/dist/image-editor.esm.js.map +0 -7
  141. package/dist/image-editor.esm.min.js +0 -9
  142. package/dist/image-editor.esm.min.js.map +0 -7
  143. package/dist/image-editor.esm.min.mjs +0 -9
  144. package/dist/image-editor.esm.min.mjs.map +0 -7
  145. package/dist/image-editor.esm.mjs +0 -4376
  146. package/dist/image-editor.esm.mjs.map +0 -7
  147. package/dist/image-editor.js +0 -4373
  148. package/dist/image-editor.js.map +0 -7
  149. package/dist/image-editor.min.js +0 -9
  150. package/dist/image-editor.min.js.map +0 -7
  151. package/image-editor.d.ts +0 -271
  152. package/src/browser.js +0 -11
  153. package/src/esm.js +0 -9
  154. package/src/image-editor.js +0 -5013
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Crop session lifecycle owner. Implements the
3
+ * `enterCropMode → applyCrop` and
4
+ * `enterCropMode → cancelCrop` transitions atop the
5
+ * legacy crop pipeline, plus the dedicated crop rectangle
6
+ * shape, its drag/scale clamps, and the per-object
7
+ * `evented`/`selectable` freeze that keeps only the crop
8
+ * rectangle interactive while a session is open.
9
+ *
10
+ * ## Owned contracts
11
+ *
12
+ * - `enterCropMode`, `applyCrop`, and `cancelCrop`
13
+ * each discard any active Fabric `ActiveSelection` BEFORE mutating crop
14
+ * state. The state serializer's `saveState` also discards on its own
15
+ * path, so the explicit call here is a defense-in-depth that keeps the
16
+ * contract independent of the serializer's internal behaviour.
17
+ * - `enterCropMode` creates a {@link CropSession}
18
+ * that captures the pre-crop canvas snapshot (without the crop rectangle),
19
+ * the prior `canvas.selection` setting, and per-object `evented` /
20
+ * `selectable` values. The pre-crop snapshot is taken *before* the crop
21
+ * rectangle is added so the JSON is guaranteed crop-rect-free without
22
+ * relying on Fabric's serialization of the `isCropRect` custom key.
23
+ * - `applyCrop` pushes exactly one
24
+ * {@link Command} whose `undo` restores the pre-crop snapshot and whose
25
+ * `execute` re-applies the post-crop snapshot. The command is pushed via
26
+ * {@link HistoryManager.push} (NOT `execute`) because the cropped state
27
+ * is already on the canvas — the first `redo` after an `undo` should
28
+ * re-run the post-crop restore, but the initial commit must not
29
+ * double-render.
30
+ * - On any failure between the crop region read
31
+ * and the history push, the pre-crop snapshot is restored via
32
+ * `context.loadFromState`, the {@link CropSession} is dropped, all
33
+ * crop-specific Fabric event handlers are detached, and the returned
34
+ * promise rejects with {@link CropApplyError} wrapping the original
35
+ * cause. A failure inside the rollback itself is logged but does not
36
+ * mask the original error.
37
+ * - `cancelCrop` restores the pre-crop snapshot
38
+ * (via the captured `prevEvented` / `prevSelection` values, plus the
39
+ * per-mask style backups captured on entry), removes the crop
40
+ * rectangle, and drops the session WITHOUT producing a history entry.
41
+ * - All three transitions detach every Fabric
42
+ * event handler bound on the crop rectangle when the session ends, so
43
+ * no stale handlers remain attached to the disposed shape.
44
+ * - `enterCropMode` honours
45
+ * `options.crop.hideMasksDuringCrop`: when `true`, every mask's prior
46
+ * live style is captured into `session.maskBackups` via
47
+ * {@link captureMaskStyleBackup} and the mask is then forced to
48
+ * `opacity: 0`, `evented: false`, `selectable: false` via
49
+ * {@link applyCropHideMaskStyle}. Backups are captured BEFORE the
50
+ * freeze loop so the recorded `selectable` value is the true pre-crop
51
+ * value and not the freeze-loop's enforced `false`.
52
+ * - `cancelCrop` restores every entry in
53
+ * `session.maskBackups` via {@link restoreMaskStyleBackup} after the
54
+ * per-object `evented` / `selectable` restore so the mask backup's
55
+ * fields (`opacity`, `fill`, `strokeWidth`, `stroke`, `selectable`,
56
+ * `lockRotation`) are the final word.
57
+ * - `applyCrop` honours
58
+ * `options.crop.preserveMasksAfterCrop` defaulting to `false` in current:
59
+ * the inner `context.loadImage(croppedBase64)` replaces every canvas
60
+ * object with the cropped base image, so masks disappear naturally
61
+ * when the option is `false`.
62
+ * - When `options.crop.preserveMasksAfterCrop`
63
+ * is `true`, `applyCrop` captures each mask's `left`, `top`,
64
+ * `angle`, `scaleX`, and `scaleY` BEFORE the canvas is exported, and
65
+ * re-adds the masks AFTER the loader commits the cropped image with
66
+ * `left` and `top` shifted by `-cropRegion.left, -cropRegion.top`.
67
+ * Per-mask `angle`, `scaleX`, and `scaleY` are restored verbatim so
68
+ * the visible mask shape does not change size or orientation. The
69
+ * cropRegion-relative shift matches legacy's
70
+ * `_translateObjectByCanvasOffset(mask, -cropRegion.sourceX,
71
+ * -cropRegion.sourceY)` and is the documented legacy behavior to
72
+ * preserve. Because shifting `left` / `top` by a constant translates
73
+ * the entire object (including its rotated visual) by the same
74
+ * constant in canvas pixels, the post-crop position relative to the
75
+ * new image bounding box matches the pre-crop position relative to
76
+ * the old image bounding box without any explicit trig in this
77
+ * module — the rotation angle is encoded in the rotated image's
78
+ * bounding rect, which moves with the same translation as the masks.
79
+ *
80
+ * ## Post-crop mask preservation
81
+ *
82
+ * Mask visibility during crop mode is owned by the
83
+ * `hideMasksDuringCrop` path above: masks can be hidden on entry and
84
+ * restored on cancel. The apply path separately owns
85
+ * `preserveMasksAfterCrop`: when the option is `true`, the controller
86
+ * captures each mask's `left`, `top`, `angle`, `scaleX`, and `scaleY`
87
+ * before export and re-adds the masks shifted by
88
+ * `-cropRegion.left, -cropRegion.top` after
89
+ * `context.loadImage(croppedBase64)` commits. The intersection filter
90
+ * drops masks that do not overlap the crop region, matching legacy
91
+ * observable behavior: masks fully outside the cropped region are
92
+ * removed, while intersecting masks are preserved.
93
+ *
94
+ * ## Implementation notes
95
+ *
96
+ * - The controller is a set of stateless functions taking a
97
+ * {@link CropControllerContext}. The `ImageEditor` facade keeps
98
+ * ownership of the canonical session pointer (`getCropSession` /
99
+ * `setCropSession`) so multiple editors on the same page do not share
100
+ * crop state and a sub-agent unit test can exercise the controller
101
+ * without instantiating the full facade.
102
+ * - The crop rectangle's drag/scale handlers clamp `scaleX` / `scaleY`
103
+ * so the rect cannot grow past the available image bounding box and
104
+ * cannot shrink below the configured `crop.minWidth` / `crop.minHeight`.
105
+ * This matches legacy's `handleCropRectModified`.
106
+ * - In Fabric v7 the rotation handle (`mtr`) is hidden via
107
+ * `setControlVisible('mtr', false)` because `hasRotatingPoint` is
108
+ * silently ignored. `lockRotation: true` is also set as runtime
109
+ * defence so the rotation transform itself cannot fire even if the
110
+ * handle were somehow shown.
111
+ * - The pre-crop snapshot is captured once, in `enterCropMode`, and
112
+ * reused by `applyCrop`'s history command and rollback path. This
113
+ * avoids a re-serialization right before the crop, and — more
114
+ * importantly — avoids the legacy fragility of filtering `isCropRect`
115
+ * objects out of a post-rect snapshot when Fabric's custom-key
116
+ * serializer occasionally drops the marker.
117
+ *
118
+ * Owner module references (per the documented "Mapping Contracts to
119
+ * modules" table): this module is imported only by `image-editor.ts` and
120
+ * is intentionally NOT re-exported from `src/index.ts`.
121
+ *
122
+ * @module
123
+ */
124
+ import type * as FabricNS from 'fabric';
125
+ import type { CropHandler, CropPrevEvented, FabricModule, ImageMimeType, LoadImageOptions, MaskBackup, ResolvedOptions } from '../core/public-types.js';
126
+ import { type HistoryManager } from '../history/history-manager.js';
127
+ /**
128
+ * Internal state of an open crop session. Built by {@link enterCropMode},
129
+ * consumed and discarded by {@link applyCrop} / {@link cancelCrop}.
130
+ *
131
+ * The `ImageEditor` facade owns the live pointer to this object; the
132
+ * controller reads and writes it through the
133
+ * {@link CropControllerContext.getCropSession} /
134
+ * {@link CropControllerContext.setCropSession} callbacks so multiple
135
+ * editor instances do not share crop state.
136
+ *
137
+ * Fields:
138
+ *
139
+ * - `beforeJson` — JSON snapshot of the canvas captured BEFORE the crop
140
+ * rectangle was added. Used as both the rollback target on
141
+ * `applyCrop` failure and the `undo` payload of
142
+ * the history entry pushed on success.
143
+ * - `prevSelection` — value of `canvas.selection` immediately before the
144
+ * controller forced it to `false` to keep only the crop rectangle
145
+ * interactive. Restored on apply or cancel.
146
+ * - `prevEvented` — list of `{ object, evented, selectable}` records for
147
+ * every canvas object that was frozen on entry. Restored on apply or
148
+ * cancel.
149
+ * - `maskBackups` — per-mask style backup list. Populated by
150
+ * `enterCropMode` when `options.crop.hideMasksDuringCrop` is `true`,
151
+ * consumed by `cancelCrop` to restore each mask's pre-crop visual
152
+ * state. Empty when the option is `false`.
153
+ * - `cropRect` — the active crop rectangle, or `null` after the rect has
154
+ * been removed (so subsequent calls to {@link removeCropRect} are
155
+ * idempotent on the success and rollback paths).
156
+ * - `handlers` — bound `modified` / `moving` / `scaling` handler records
157
+ * on the crop rectangle. Detached when the session ends.
158
+ *
159
+ */
160
+ export interface CropSession {
161
+ beforeJson: string;
162
+ prevSelection: boolean;
163
+ prevEvented: CropPrevEvented[];
164
+ /** Per-mask style backups captured when masks are hidden during crop mode. */
165
+ maskBackups: MaskBackup[];
166
+ cropRect: FabricNS.Rect | null;
167
+ handlers: CropHandler[];
168
+ }
169
+ /**
170
+ * Dependency bundle passed by the `ImageEditor` facade into every crop
171
+ * entry point. The controller has no class state of its own — every
172
+ * editor field it reads is exposed here as a value or callback so the
173
+ * facade keeps ownership of the canonical state.
174
+ *
175
+ * Mirrors the shape of
176
+ * {@link import('../export/export-service.js').MergeMasksContext} for
177
+ * consistency across pipeline modules.
178
+ *
179
+ * The `ImageEditor` facade constructs this bundle from its own state.
180
+ */
181
+ export interface CropControllerContext {
182
+ /** Fabric module providing `Rect` for the crop rectangle. */
183
+ readonly fabric: FabricModule;
184
+ /** Live Fabric canvas. */
185
+ readonly canvas: FabricNS.Canvas;
186
+ /** Resolved editor options — supplies `crop.padding`, `crop.minWidth`,
187
+ * `crop.minHeight`, `crop.allowRotationOfCropRect`, and
188
+ * `downsampleQuality` (used as the cropped JPEG export quality). */
189
+ readonly options: ResolvedOptions;
190
+ /** History manager that records the single crop command on success. */
191
+ readonly historyManager: HistoryManager;
192
+ /**
193
+ * Predicate matching `ImageEditor.isImageLoaded`. Returns `true`
194
+ * only when an `originalImage` has been committed and has positive
195
+ * dimensions. `enterCropMode` and `applyCrop` no-op when this is
196
+ * `false` so a caller cannot open a crop session against an empty
197
+ * canvas (matches legacy's `isImageLoaded` gate).
198
+ */
199
+ isImageLoaded(): boolean;
200
+ /**
201
+ * The currently committed `originalImage`, or `null`. Read by
202
+ * {@link enterCropMode} to derive the initial crop rectangle bounds
203
+ * from the image bounding box.
204
+ */
205
+ getOriginalImage(): FabricNS.FabricImage | null;
206
+ /**
207
+ * MIME type of the currently committed image, used by source-preserving
208
+ * crop export.
209
+ */
210
+ getCurrentImageMimeType?(): ImageMimeType | null;
211
+ /** Reads the live crop session, or `null`. */
212
+ getCropSession(): CropSession | null;
213
+ /** Writes the live crop session pointer (or clears it with `null`). */
214
+ setCropSession(session: CropSession | null): void;
215
+ /**
216
+ * Capture a snapshot suitable for {@link loadFromState}. Used in
217
+ * `enterCropMode` to record the pre-crop state and again in
218
+ * `applyCrop` after the cropped image is on the canvas to record
219
+ * the post-crop state for the redo command.
220
+ */
221
+ saveState(): string;
222
+ /**
223
+ * Restore a snapshot produced by {@link saveState}. Used as the
224
+ * `undo` callback of the crop command and as the rollback step on
225
+ * any `applyCrop` failure.
226
+ */
227
+ loadFromState(snapshot: string): Promise<void>;
228
+ /**
229
+ * Transactional image loader (`image/image-loader.ts`). `applyCrop`
230
+ * routes the cropped data URL through this so a failed reload
231
+ * propagates back here and the rollback path catches it.
232
+ */
233
+ loadImage(imageBase64: string, options?: LoadImageOptions): Promise<void>;
234
+ /**
235
+ * Reads the orchestrator's mask counter. Used by the
236
+ * `preserveMasksAfterCrop` path so re-added masks restore the
237
+ * counter to `max(maskId)` after `context.loadImage` reset it to `0`
238
+ * on commit (invariant: subsequent `createMask`
239
+ * calls must not collide with a preserved mask's `maskId`).
240
+ *
241
+ * Optional — only consulted by the preserve path. The orchestrator
242
+ * may omit this in unit-test contexts that never enable
243
+ * `preserveMasksAfterCrop`.
244
+ */
245
+ getMaskCounter?(): number;
246
+ /**
247
+ * Writes the orchestrator's mask counter. See {@link getMaskCounter}
248
+ * for the contract.
249
+ */
250
+ setMaskCounter?(n: number): void;
251
+ /**
252
+ * Re-render the mask list DOM after preserved masks are re-added to
253
+ * the post-crop canvas. Optional — the orchestrator may omit this
254
+ * when no DOM list is wired (e.g., headless unit tests). Mirrors
255
+ * legacy's `updateMaskList` call after preserved masks land.
256
+ */
257
+ updateMaskList?(): void;
258
+ }
259
+ /**
260
+ * Open a crop session. Builds a {@link CropSession} that captures:
261
+ *
262
+ * - the pre-crop canvas JSON snapshot (without the crop rectangle),
263
+ * - the prior `canvas.selection` setting,
264
+ * - per-object `evented` / `selectable` values for every existing canvas
265
+ * object,
266
+ * - per-mask style backups when `options.crop.hideMasksDuringCrop` is
267
+ * `true` so {@link cancelCrop} can revert the hide.
268
+ *
269
+ * After capturing the session, the function:
270
+ *
271
+ * 1. Adds an interactive crop rectangle inside the image bounding box
272
+ * (with the configured padding inset) and binds drag/scale clamp
273
+ * handlers so it cannot grow past the image bounds nor shrink below
274
+ * `crop.minWidth` / `crop.minHeight`.
275
+ * 2. Forces `canvas.selection = false` and freezes every other canvas
276
+ * object (`evented = false`, `selectable = false`) so only the crop
277
+ * rectangle responds to pointer events.
278
+ * 3. Marks the rectangle with the `isCropRect` custom property so the
279
+ * state serializer's session-only filter excludes it from any future
280
+ * snapshot taken while the session is open.
281
+ * 4. When `options.crop.hideMasksDuringCrop` is `true`, captures a
282
+ * {@link MaskBackup} for every mask BEFORE the freeze loop runs and
283
+ * then applies the crop-mode hide style (`opacity: 0`,
284
+ * `evented: false`, `selectable: false`) via
285
+ * {@link applyCropHideMaskStyle}. Capturing first ensures the
286
+ * backup's `selectable` field reflects the true pre-crop value
287
+ * rather than the freeze-loop's enforced `false`.
288
+ *
289
+ * No-ops when:
290
+ *
291
+ * - a session is already open (idempotent re-entry),
292
+ * - no `originalImage` is committed,
293
+ * - `isImageLoaded` returns `false`.
294
+ *
295
+ * `discardActiveObject` runs at the very top so the
296
+ * pre-crop snapshot does not capture an `ActiveSelection` wrapper. The
297
+ * state serializer's own discard provides a second line of defence.
298
+ *
299
+ * @param context - Editor dependency bundle — see {@link CropControllerContext}.
300
+ *
301
+ */
302
+ export declare function enterCropMode(context: CropControllerContext): void;
303
+ /**
304
+ * Close an open crop session WITHOUT applying the crop. Restores the
305
+ * pre-crop `canvas.selection`, the per-object `evented` / `selectable`
306
+ * values, removes the crop rectangle, detaches every crop-bound Fabric
307
+ * handler, and drops the session.
308
+ *
309
+ * Produces NO history entry — the user explicitly chose to abandon the
310
+ * crop, and the canvas state at the end of `cancelCrop` is the same one
311
+ * the previous history entry already covers.
312
+ *
313
+ * No-op when no session is open.
314
+ *
315
+ * `discardActiveObject` runs at the very top so any
316
+ * currently-active selection (typically the crop rectangle itself) is
317
+ * cleared before the rect is removed.
318
+ *
319
+ * @param context - Editor dependency bundle — see {@link CropControllerContext}.
320
+ *
321
+ */
322
+ export declare function cancelCrop(context: CropControllerContext): void;
323
+ /**
324
+ * Apply the active crop session: export the crop region as a JPEG data
325
+ * URL, reload it as the new base image through the transactional
326
+ * loader, and push exactly one history entry whose `undo` restores the
327
+ * pre-crop snapshot and whose `execute` re-applies the post-crop
328
+ * snapshot.
329
+ *
330
+ * Atomic: either the cropped image is committed and one history entry
331
+ * is pushed, or the editor is rewound to its pre-crop state and the
332
+ * returned promise rejects with {@link CropApplyError}.
333
+ *
334
+ * Steps, in order:
335
+ *
336
+ * 1. **No-op gates** — return without mutating anything when no session
337
+ * is open or the session has no crop rectangle.
338
+ * 2. **Discard ActiveSelection** — drop any active
339
+ * selection wrapper before reading the crop rect's bounding box so
340
+ * the export region is computed against the rect itself.
341
+ * 3. **Read crop region** — refresh the rect's coordinate cache, read
342
+ * its bounding rect, convert it to an integer region with trailing
343
+ * partial pixels excluded, and clamp it to the source canvas.
344
+ * 3a. **Capture preserved masks** — when
345
+ * `options.crop.preserveMasksAfterCrop === true`, capture each mask's
346
+ * pre-crop `left`, `top`, `angle`, `scaleX`, and `scaleY`, then
347
+ * remove the masks from the canvas so the cropped JPEG export does
348
+ * not bake them in (and so the inner `context.loadImage`'s
349
+ * `canvas.clear` does not dispose the captured reference). Masks
350
+ * fully outside the crop region are removed without a record so
351
+ * they do not reappear after the load (matches legacy's `intersectsCrop`
352
+ * filter).
353
+ * 4. **Tear down session in place** — restore per-object evented /
354
+ * selectable values (so the export sees masks in their pre-crop
355
+ * state) and remove the crop rectangle along with its handlers. The
356
+ * session pointer is NOT cleared yet
357
+ * because the catch path may still need `session.beforeJson`.
358
+ * 5. **Restore `canvas.selection`** — back to the pre-crop value before
359
+ * the cropped image is exported.
360
+ * 6. **Export the crop region** via `canvas.toDataURL` with the
361
+ * integer region as `left` / `top` / `width` / `height` (matches
362
+ * legacy's `_exportCanvasRegionToDataURL`). The cropped image is JPEG
363
+ * at the configured downsample quality.
364
+ * 7. **Reload the cropped image** through `context.loadImage`. The
365
+ * transactional loader either commits the new image or rolls back —
366
+ * a failure propagates here so the rollback path below catches it.
367
+ * 7a. **Reapply preserved masks** — when
368
+ * records were captured in step 3a, re-add each mask to the
369
+ * post-crop canvas with `left` and `top` shifted by
370
+ * `-cropRegion.left, -cropRegion.top` and `angle`, `scaleX`,
371
+ * `scaleY` restored verbatim. Restores the orchestrator's mask
372
+ * counter to `max(maskId)` so subsequent `createMask` calls do not
373
+ * collide with preserved IDs.
374
+ * 8. **Capture post-crop snapshot** for the redo command.
375
+ * 9. **Drop the session pointer** before pushing history.
376
+ * 10. **Push exactly one history command** whose
377
+ * `undo` restores the pre-crop snapshot and whose `execute`
378
+ * re-applies the post-crop snapshot.
379
+ *
380
+ * On any failure between step 3 and step 10, the helper:
381
+ *
382
+ * - tears down the session (handlers detached, rect removed,
383
+ * per-object state restored, `canvas.selection` reverted) so no
384
+ * stale crop state remains,
385
+ * - clears the session pointer,
386
+ * - restores the pre-crop snapshot via `context.loadFromState`,
387
+ * - rejects with {@link CropApplyError} wrapping the original cause.
388
+ *
389
+ * Mask handling note: when `options.crop.preserveMasksAfterCrop` is
390
+ * `false` (the current default), the inner
391
+ * `context.loadImage(croppedBase64)` call replaces every canvas object
392
+ * with the cropped base image, so any masks are removed naturally
393
+ * with no extra work in this function. When `preserveMasksAfterCrop`
394
+ * is `true`, masks intersecting the crop region are captured before
395
+ * the export and re-added after the load via
396
+ * {@link capturePreservedMasks} / {@link reapplyPreservedMasks}.
397
+ * Crop-mode mask hiding on entry / restoration on cancel is handled
398
+ * in {@link enterCropMode} and the {@link teardownSession} chain.
399
+ *
400
+ * @param context - Editor dependency bundle — see {@link CropControllerContext}.
401
+ * @returns Resolves on success; rejects with {@link CropApplyError}
402
+ * on any pipeline failure (after the pre-crop snapshot has
403
+ * been restored).
404
+ *
405
+ */
406
+ export declare function applyCrop(context: CropControllerContext): Promise<void>;
407
+ //# sourceMappingURL=crop-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crop-controller.d.ts","sourceRoot":"","sources":["../../../src/crop/crop-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0HG;AAEH,OAAO,KAAK,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAGxC,OAAO,KAAK,EACR,WAAW,EACX,eAAe,EACf,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,UAAU,EAGV,eAAe,EAClB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAoB7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,WAAW,WAAW;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,8EAA8E;IAC9E,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,QAAQ,EAAE,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;CAC3B;AAID;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,qBAAqB;IAClC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,0BAA0B;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC;IACjC;;yEAEqE;IACrE,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,uEAAuE;IACvE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IAExC;;;;;;OAMG;IACH,aAAa,IAAI,OAAO,CAAC;IAEzB;;;;OAIG;IACH,gBAAgB,IAAI,QAAQ,CAAC,WAAW,GAAG,IAAI,CAAC;IAEhD;;;OAGG;IACH,uBAAuB,CAAC,IAAI,aAAa,GAAG,IAAI,CAAC;IAEjD,8CAA8C;IAC9C,cAAc,IAAI,WAAW,GAAG,IAAI,CAAC;IACrC,uEAAuE;IACvE,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;IAElD;;;;;OAKG;IACH,SAAS,IAAI,MAAM,CAAC;IAEpB;;;;OAIG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;;;OAIG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1E;;;;;;;;;;OAUG;IACH,cAAc,CAAC,IAAI,MAAM,CAAC;IAE1B;;;OAGG;IACH,cAAc,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAEjC;;;;;OAKG;IACH,cAAc,CAAC,IAAI,IAAI,CAAC;CAC3B;AAybD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAyLlE;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAiB/D;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkFG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoJ7E"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Pure helpers that normalize the user-facing
3
+ * `Base64ExportOptions` / `ImageFileExportOptions` surface into the
4
+ * canvas-/Fabric-shaped values consumed by `export/export-service.ts`.
5
+ *
6
+ * The module owns three small, individually testable building blocks plus
7
+ * one orchestrating function, mirroring the shape of
8
+ * `image/image-resampler.ts`:
9
+ *
10
+ * - {@link normalizeImageFormat} — collapses `'jpg'` to `'jpeg'`, strips
11
+ * the `image/` MIME prefix, and falls back to `'jpeg'` for unknown or
12
+ * omitted input.
13
+ * - {@link mimeTypeFor} — derives the matching `image/...` MIME
14
+ * for a normalized format token.
15
+ * - {@link clampQuality} — coerces input to a finite number and
16
+ * clamps it into `[0, 1]`, falling back to a caller-supplied default
17
+ * when the input is not finite.
18
+ * - {@link resolveExportFormat} — composes the above with the documented
19
+ * `fileType`-wins-over-`format` precedence and drops `quality` for PNG
20
+ * output.
21
+ *
22
+ * legacy parity:
23
+ * - The format mapping table mirrors `_normalizeImageFormat` from
24
+ * `src/image-editor.js@legacy.4.0`, including the lowercase lookup and the
25
+ * `'jpeg'` default for unknown input.
26
+ * - The quality clamp mirrors `_normalizeQuality` from the same legacy file:
27
+ * non-finite input falls back to `options.downsampleQuality`, finite
28
+ * input is clamped to `[0, 1]`.
29
+ *
30
+ * This module is internal — it is NOT re-exported from `src/index.ts`.
31
+ *
32
+ * Fabric.
33
+ * lossless.
34
+ * `quality` defaulting to `options.downsampleQuality`.
35
+ *
36
+ * @module
37
+ */
38
+ import type { Base64ExportOptions, ImageFileExportOptions, ImageMimeType, NormalizedImageFormat } from '../core/public-types.js';
39
+ /**
40
+ * Result of {@link resolveExportFormat}.
41
+ *
42
+ * `quality` is `undefined` when the resolved format is `'png'` so callers can
43
+ * omit it from `toDataURL` / Fabric's region-export options without an extra
44
+ * branch.
45
+ */
46
+ export interface ResolvedExportFormat {
47
+ /** Canvas-/Fabric-compatible format token. */
48
+ format: NormalizedImageFormat;
49
+ /** Matching MIME type for `toDataURL` / `Blob` construction. */
50
+ mimeType: ImageMimeType;
51
+ /** Lossy quality in `[0, 1]`, or `undefined` for PNG. */
52
+ quality: number | undefined;
53
+ }
54
+ /**
55
+ * Collapse a public `ImageFileType` (or any string-ish input) to the
56
+ * canonical `NormalizedImageFormat` token consumed by Fabric.
57
+ *
58
+ * Behavior:
59
+ * - `'jpg'` (and `'JPG'`, `'image/jpeg'`, `'IMAGE/JPEG'`, …) → `'jpeg'`.
60
+ * - `'png'` / `'image/png'` → `'png'` (case-insensitive).
61
+ * - `'webp'` / `'image/webp'` → `'webp'` (case-insensitive).
62
+ * - `null` / `undefined` / empty / unknown → `'jpeg'`.
63
+ *
64
+ * Pure function — no DOM access, safe to call from property tests.
65
+ *
66
+ * @param input - Requested file type or MIME alias (case-insensitive).
67
+ * @returns The normalized format token to pass to Fabric's `format` arg.
68
+ *
69
+ */
70
+ export declare function normalizeImageFormat(input?: string | null): NormalizedImageFormat;
71
+ /**
72
+ * Collapse a public file type or MIME alias to a normalized format token.
73
+ * Returns `null` for omitted or unknown input so call sites with a different
74
+ * fallback policy can decide explicitly.
75
+ *
76
+ * Pure function — no DOM access, safe to call from property tests.
77
+ */
78
+ export declare function tryNormalizeImageFormat(input?: string | null): NormalizedImageFormat | null;
79
+ /**
80
+ * Derive the `image/...` MIME type for a normalized format token.
81
+ *
82
+ * Pure function — no DOM access, safe to call from property tests.
83
+ *
84
+ * @param format - Normalized format token.
85
+ * @returns The matching `ImageMimeType`.
86
+ *
87
+ */
88
+ export declare function mimeTypeFor(format: NormalizedImageFormat): ImageMimeType;
89
+ /**
90
+ * Coerce `quality` to a finite number and clamp it into `[0, 1]`.
91
+ *
92
+ * Behavior:
93
+ * - `Number(quality)` is finite → `Math.max(0, Math.min(1, n))`.
94
+ * - Otherwise (`NaN`, `Infinity`, non-numeric strings, `null`, `undefined`,
95
+ * objects, …) → `fallback` (NOT re-clamped; the editor pre-validates
96
+ * `downsampleQuality` at construction time).
97
+ *
98
+ * Pure function — no DOM access, safe to call from property tests.
99
+ *
100
+ * @param quality - Caller-supplied quality (may be any type).
101
+ * @param fallback - Quality to use when `quality` is not a finite number.
102
+ * Typically `options.downsampleQuality`.
103
+ * @returns A finite quality value in `[0, 1]`, or the `fallback` when input
104
+ * was not finite.
105
+ *
106
+ */
107
+ export declare function clampQuality(quality: unknown, fallback: number): number;
108
+ /**
109
+ * Resolve the user-facing export options into the canvas-/Fabric-shaped
110
+ * values consumed by `export/export-service.ts`.
111
+ *
112
+ * Precedence (matches legacy `exportImageBase64` / `exportImageFile`):
113
+ * 1. `options.fileType` wins over `options.format` when both are supplied
114
+ * and `options.fileType` is truthy. Falsy `fileType` falls through to
115
+ * `options.format`. Both omitted → `'jpeg'`.
116
+ * 2. `options.quality` is normalized through {@link clampQuality} with
117
+ * `downsampleQuality` as the fallback. When `options.quality` is
118
+ * `undefined` or `null`, the fallback is used directly.
119
+ * 3. When the resolved format is `'png'`, `quality` is dropped from the
120
+ * result so call sites pass `undefined` to `toDataURL`.
121
+ *
122
+ * Pure function — no DOM access, safe to call from property tests.
123
+ *
124
+ * @param options - Subset of `Base64ExportOptions` /
125
+ * `ImageFileExportOptions` carrying `fileType`,
126
+ * `format`, and `quality`. Other fields are
127
+ * ignored. `format` is only read from options
128
+ * that declare it (i.e. `Base64ExportOptions`).
129
+ * @param downsampleQuality - Default quality used when `options.quality` is
130
+ * omitted or non-finite. Sourced from
131
+ * `ResolvedOptions.downsampleQuality`.
132
+ * @returns The resolved `{ format, mimeType, quality}` triple.
133
+ *
134
+ */
135
+ export declare function resolveExportFormat(options: Pick<Base64ExportOptions, 'fileType' | 'format' | 'quality'> | Pick<ImageFileExportOptions, 'fileType' | 'quality'> | undefined | null, downsampleQuality: number): ResolvedExportFormat;
136
+ //# sourceMappingURL=export-format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-format.d.ts","sourceRoot":"","sources":["../../../src/export/export-format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EACR,mBAAmB,EACnB,sBAAsB,EACtB,aAAa,EACb,qBAAqB,EACxB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACjC,8CAA8C;IAC9C,MAAM,EAAE,qBAAqB,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AA+BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,qBAAqB,CAEjF;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,qBAAqB,GAAG,IAAI,CAS3F;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,qBAAqB,GAAG,aAAa,CAExE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CAC/B,OAAO,EACD,IAAI,CAAC,mBAAmB,EAAE,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC,GAC5D,IAAI,CAAC,sBAAsB,EAAE,UAAU,GAAG,SAAS,CAAC,GACpD,SAAS,GACT,IAAI,EACV,iBAAiB,EAAE,MAAM,GAC1B,oBAAoB,CA8BtB"}