@overtone-art/canvas-editor-core 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilia Dzhiubanskii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,104 @@
1
+ // src/export.ts
2
+ function computePrintAreaClip(area, scaleX, scaleY, targetWidth, targetHeight) {
3
+ const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));
4
+ const top = Math.max(0, Math.min(targetHeight, area.top * scaleY));
5
+ const right = Math.max(left, Math.min(targetWidth, (area.left + area.width) * scaleX));
6
+ const bottom = Math.max(top, Math.min(targetHeight, (area.top + area.height) * scaleY));
7
+ return { left, top, width: right - left, height: bottom - top };
8
+ }
9
+ function computeCoverPlacement(sourceWidth, sourceHeight, targetWidth, targetHeight) {
10
+ if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {
11
+ throw new Error("Cover dimensions must be positive");
12
+ }
13
+ const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
14
+ const width = sourceWidth * scale;
15
+ const height = sourceHeight * scale;
16
+ return {
17
+ left: (targetWidth - width) / 2,
18
+ top: (targetHeight - height) / 2,
19
+ width,
20
+ height
21
+ };
22
+ }
23
+ function canvasElementToBlob(output, format, quality) {
24
+ const mime = format === "jpeg" ? "image/jpeg" : `image/${format}`;
25
+ return new Promise((resolve, reject) => {
26
+ output.toBlob(
27
+ (blob) => blob ? resolve(blob) : reject(new Error(`Failed to export ${format}`)),
28
+ mime,
29
+ quality
30
+ );
31
+ });
32
+ }
33
+ async function exportPNG(canvas, options = {}) {
34
+ const { multiplier = 1, format = "png", quality = 1 } = options;
35
+ const output = canvas.toCanvasElement(multiplier);
36
+ return canvasElementToBlob(output, format, quality);
37
+ }
38
+ async function exportMockup(canvas, mockup, options = {}) {
39
+ const { multiplier = 1, format = "png", quality = 1 } = options;
40
+ const design = canvas.toCanvasElement(multiplier);
41
+ const output = design.ownerDocument.createElement("canvas");
42
+ output.width = design.width;
43
+ output.height = design.height;
44
+ const context = output.getContext("2d");
45
+ if (!context) throw new Error("2D canvas context is unavailable");
46
+ const loadImage = (url) => new Promise((resolve, reject) => {
47
+ const element = new Image();
48
+ element.crossOrigin = "anonymous";
49
+ element.onload = () => resolve(element);
50
+ element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));
51
+ element.src = url;
52
+ });
53
+ const drawCover = (image) => {
54
+ const placement = computeCoverPlacement(
55
+ image.naturalWidth || image.width,
56
+ image.naturalHeight || image.height,
57
+ output.width,
58
+ output.height
59
+ );
60
+ context.drawImage(image, placement.left, placement.top, placement.width, placement.height);
61
+ };
62
+ drawCover(await loadImage(mockup.image));
63
+ context.save();
64
+ if (mockup.printArea && mockup.clipToPrintArea !== false) {
65
+ const clip = computePrintAreaClip(
66
+ mockup.printArea,
67
+ output.width / canvas.getWidth(),
68
+ output.height / canvas.getHeight(),
69
+ output.width,
70
+ output.height
71
+ );
72
+ context.beginPath();
73
+ context.rect(clip.left, clip.top, clip.width, clip.height);
74
+ context.clip();
75
+ }
76
+ context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));
77
+ context.globalCompositeOperation = !mockup.designBlendMode || mockup.designBlendMode === "normal" ? "source-over" : mockup.designBlendMode;
78
+ context.drawImage(design, 0, 0);
79
+ context.restore();
80
+ if (mockup.overlay) {
81
+ context.save();
82
+ context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));
83
+ context.globalCompositeOperation = mockup.overlay.blendMode === "normal" ? "source-over" : mockup.overlay.blendMode ?? "multiply";
84
+ drawCover(await loadImage(mockup.overlay.image));
85
+ context.restore();
86
+ }
87
+ return canvasElementToBlob(output, format, quality);
88
+ }
89
+ function exportSVG(canvas) {
90
+ return canvas.toSVG();
91
+ }
92
+ function exportDataURL(canvas, format = "png", multiplier = 1) {
93
+ return canvas.toDataURL({ format, multiplier });
94
+ }
95
+
96
+ export {
97
+ computePrintAreaClip,
98
+ computeCoverPlacement,
99
+ exportPNG,
100
+ exportMockup,
101
+ exportSVG,
102
+ exportDataURL
103
+ };
104
+ //# sourceMappingURL=chunk-NINRTPOJ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/export.ts"],"sourcesContent":["import type { Canvas, ImageFormat } from 'fabric';\nimport type { MockupConfig, MockupPrintArea } from './types';\n\nexport interface PngExportOptions {\n multiplier?: number;\n format?: ImageFormat;\n quality?: number;\n}\n\nexport interface CoverPlacement {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\nexport function computePrintAreaClip(\n area: MockupPrintArea,\n scaleX: number,\n scaleY: number,\n targetWidth: number,\n targetHeight: number,\n): MockupPrintArea {\n const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));\n const top = Math.max(0, Math.min(targetHeight, area.top * scaleY));\n const right = Math.max(left, Math.min(targetWidth, (area.left + area.width) * scaleX));\n const bottom = Math.max(top, Math.min(targetHeight, (area.top + area.height) * scaleY));\n return { left, top, width: right - left, height: bottom - top };\n}\n\n/** Object-fit: cover geometry, exported for deterministic preview/composite tests. */\nexport function computeCoverPlacement(\n sourceWidth: number,\n sourceHeight: number,\n targetWidth: number,\n targetHeight: number,\n): CoverPlacement {\n if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {\n throw new Error('Cover dimensions must be positive');\n }\n const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);\n const width = sourceWidth * scale;\n const height = sourceHeight * scale;\n return {\n left: (targetWidth - width) / 2,\n top: (targetHeight - height) / 2,\n width,\n height,\n };\n}\n\nfunction canvasElementToBlob(\n output: HTMLCanvasElement,\n format: ImageFormat,\n quality: number,\n): Promise<Blob> {\n const mime = format === 'jpeg' ? 'image/jpeg' : `image/${format}`;\n return new Promise<Blob>((resolve, reject) => {\n output.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(`Failed to export ${format}`))),\n mime,\n quality,\n );\n });\n}\n\nexport async function exportPNG(canvas: Canvas, options: PngExportOptions = {}): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const output = canvas.toCanvasElement(multiplier);\n return canvasElementToBlob(output, format, quality);\n}\n\n/** Rasterize the browser mockup preview together with the transparent design. */\nexport async function exportMockup(\n canvas: Canvas,\n mockup: MockupConfig,\n options: PngExportOptions = {},\n): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const design = canvas.toCanvasElement(multiplier);\n const output = design.ownerDocument.createElement('canvas');\n output.width = design.width;\n output.height = design.height;\n const context = output.getContext('2d');\n if (!context) throw new Error('2D canvas context is unavailable');\n\n const loadImage = (url: string) =>\n new Promise<HTMLImageElement>((resolve, reject) => {\n const element = new Image();\n element.crossOrigin = 'anonymous';\n element.onload = () => resolve(element);\n element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));\n element.src = url;\n });\n const drawCover = (image: HTMLImageElement) => {\n const placement = computeCoverPlacement(\n image.naturalWidth || image.width,\n image.naturalHeight || image.height,\n output.width,\n output.height,\n );\n context.drawImage(image, placement.left, placement.top, placement.width, placement.height);\n };\n\n drawCover(await loadImage(mockup.image));\n context.save();\n if (mockup.printArea && mockup.clipToPrintArea !== false) {\n const clip = computePrintAreaClip(\n mockup.printArea,\n output.width / canvas.getWidth(),\n output.height / canvas.getHeight(),\n output.width,\n output.height,\n );\n context.beginPath();\n context.rect(clip.left, clip.top, clip.width, clip.height);\n context.clip();\n }\n context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));\n context.globalCompositeOperation =\n !mockup.designBlendMode || mockup.designBlendMode === 'normal'\n ? 'source-over'\n : mockup.designBlendMode;\n context.drawImage(design, 0, 0);\n context.restore();\n\n if (mockup.overlay) {\n context.save();\n context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));\n context.globalCompositeOperation =\n mockup.overlay.blendMode === 'normal'\n ? 'source-over'\n : (mockup.overlay.blendMode ?? 'multiply');\n drawCover(await loadImage(mockup.overlay.image));\n context.restore();\n }\n return canvasElementToBlob(output, format, quality);\n}\n\nexport function exportSVG(canvas: Canvas): string {\n return canvas.toSVG();\n}\n\nexport function exportDataURL(canvas: Canvas, format: ImageFormat = 'png', multiplier = 1): string {\n return canvas.toDataURL({ format, multiplier });\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.mts CHANGED
@@ -1,221 +1,6 @@
1
- import { FabricObject, Canvas, ImageFormat } from 'fabric';
2
-
3
- type Unit = 'px' | 'mm' | 'in';
4
- type LayerType = 'image' | 'text' | 'shape' | 'template' | 'group';
5
- interface LayerData {
6
- id: string;
7
- type: LayerType;
8
- name: string;
9
- visible: boolean;
10
- locked: boolean;
11
- opacity: number;
12
- /** Non-fabric metadata persisted with the layer (pattern config, etc). */
13
- meta?: LayerMeta;
14
- }
15
- interface LayerMeta {
16
- pattern?: PatternState;
17
- [key: string]: unknown;
18
- }
19
- interface SerializedLayer extends LayerData {
20
- fabricObject: Record<string, unknown>;
21
- }
22
- interface EditorState {
23
- version: string;
24
- canvas: {
25
- width: number;
26
- height: number;
27
- };
28
- layers: SerializedLayer[];
29
- background?: string;
30
- mockup?: MockupConfig | null;
31
- }
32
- interface EditorConfig {
33
- width: number;
34
- height: number;
35
- backgroundColor?: string;
36
- fileAdapter?: FileAdapter;
37
- imageProvider?: ImageProvider;
38
- preserveObjectStacking?: boolean;
39
- unit?: Unit;
40
- dpi?: number;
41
- }
42
- type ExportFormat = 'png' | 'svg' | 'json';
43
- interface FileAdapter {
44
- save(data: Blob | string, filename: string, format: ExportFormat): Promise<string>;
45
- }
46
- interface ImageProviderResult {
47
- url: string;
48
- width: number;
49
- height: number;
50
- }
51
- interface ImageProvider {
52
- upload(file: File): Promise<ImageProviderResult>;
53
- browse?(): Promise<ImageProviderResult | null>;
54
- }
55
- interface ShapePlugin {
56
- name: string;
57
- icon: string;
58
- category?: string;
59
- create(options?: Record<string, unknown>): FabricObject;
60
- }
61
- interface TemplateParameter {
62
- key: string;
63
- label: string;
64
- type: 'text' | 'color' | 'number' | 'font';
65
- default: string | number;
66
- }
67
- interface TemplateDefinition {
68
- id: string;
69
- name: string;
70
- category: string;
71
- svg: string;
72
- parameters: TemplateParameter[];
73
- preview?: string;
74
- }
75
- interface PrintifyPositioning {
76
- x: number;
77
- y: number;
78
- scale: number;
79
- angle: number;
80
- }
81
- type TileMode = 'grid' | 'brick-horizontal' | 'brick-vertical';
82
- interface PatternConfig {
83
- mode: TileMode;
84
- /**
85
- * Tile size as a % of the source image's on-canvas size (100 = natural).
86
- * Smaller values pack more repeats across the print area; larger values make
87
- * fewer, bigger tiles. Independent of how the source layer was placed.
88
- */
89
- scale: number;
90
- /** Extra horizontal gap between tiles, as % of tile width. */
91
- horizontalSpacing: number;
92
- /** Extra vertical gap between tiles, as % of tile height. */
93
- verticalSpacing: number;
94
- /** Rotation of the whole pattern, in degrees. */
95
- angle: number;
96
- /** Brick row/column shift, as % of tile size. */
97
- horizontalOffset: number;
98
- /**
99
- * Phase shift of the whole tile grid inside the print area, as % of tile
100
- * width. The pattern object itself is pinned to the print area (it can't be
101
- * dragged — that would expose bare edges), so this is how the tiling is
102
- * nudged into alignment. Clamped to ±100% so coverage is never broken.
103
- */
104
- offsetX: number;
105
- /** Phase shift of the tile grid, as % of tile height. See {@link offsetX}. */
106
- offsetY: number;
107
- /** Added rotation per horizontal step, in degrees. */
108
- rotationStepH: number;
109
- /** Added rotation per vertical step, in degrees. */
110
- rotationStepV: number;
111
- }
112
- /** Fabric interaction flags frozen while a layer is rendered as a pattern. */
113
- interface PatternLocks {
114
- lockMovementX: boolean;
115
- lockMovementY: boolean;
116
- lockScalingX: boolean;
117
- lockScalingY: boolean;
118
- lockRotation: boolean;
119
- hasControls: boolean;
120
- }
121
- /** Persisted on a layer's meta while it is rendered as a repeating pattern. */
122
- interface PatternState {
123
- config: PatternConfig;
124
- /** Original (pre-pattern) image source, as a durable data URL when possible. */
125
- originalSrc: string;
126
- /**
127
- * Serialized `clipPath` the image carried before it became a pattern, or null
128
- * if it had none. A pattern must fill the whole print area, so any pre-pattern
129
- * clip (e.g. a template crop sized to the original box) is stripped while the
130
- * pattern is on and re-applied when it is turned off. Persisted so it survives
131
- * serialization / reload / undo.
132
- */
133
- originalClip?: Record<string, unknown> | null;
134
- /**
135
- * Interaction flags the object carried before it became a pattern. A pattern
136
- * fills the whole print area, so while it is on the object is locked in place
137
- * (a drag/resize would slide the tiled bitmap off the area and leave a bare
138
- * band); the captured flags are restored when the pattern is turned off.
139
- */
140
- originalLocks?: PatternLocks;
141
- /** Original fabric transform, restored when the pattern is turned off. */
142
- original: {
143
- left: number;
144
- top: number;
145
- scaleX: number;
146
- scaleY: number;
147
- width: number;
148
- height: number;
149
- angle: number;
150
- cropX: number;
151
- cropY: number;
152
- };
153
- }
154
- declare const DEFAULT_PATTERN_CONFIG: PatternConfig;
155
- interface MockupPrintArea {
156
- left: number;
157
- top: number;
158
- width: number;
159
- height: number;
160
- }
161
- interface MockupConfig {
162
- /** Garment/product image rendered behind the design (preview only). */
163
- image: string;
164
- /** Print-area rectangle, in canvas pixels. Drawn as a guide. */
165
- printArea?: MockupPrintArea;
166
- }
167
- interface EditorEvents {
168
- 'layer:added': {
169
- layer: LayerData;
170
- };
171
- 'layer:removed': {
172
- layerId: string;
173
- };
174
- 'layer:selected': {
175
- layerId: string | null;
176
- };
177
- 'layer:modified': {
178
- layerId: string;
179
- };
180
- 'layer:reordered': {
181
- layerIds: string[];
182
- };
183
- 'layers:changed': {
184
- layers: LayerData[];
185
- };
186
- 'selection:changed': {
187
- selected: string[];
188
- };
189
- 'history:changed': {
190
- canUndo: boolean;
191
- canRedo: boolean;
192
- };
193
- 'zoom:changed': {
194
- zoom: number;
195
- };
196
- 'snap:changed': {
197
- enabled: boolean;
198
- };
199
- 'crop:changed': {
200
- active: boolean;
201
- layerId: string | null;
202
- };
203
- 'mockup:changed': {
204
- mockup: MockupConfig | null;
205
- };
206
- 'canvas:modified': Record<string, never>;
207
- 'export:start': {
208
- format: string;
209
- };
210
- 'export:complete': {
211
- format: string;
212
- url?: string;
213
- };
214
- error: {
215
- message: string;
216
- error?: unknown;
217
- };
218
- }
1
+ import { Canvas, FabricObject, ImageFormat } from 'fabric';
2
+ import { b as EditorEvents, l as LayerType, k as LayerMeta, L as LayerData, S as SerializedLayer, t as PatternSourceResolver, P as PatternConfig, s as PatternLocks, U as Unit, r as MockupPrintArea, p as MockupConfig, e as FontDefinition, m as LicenseConfig, o as LicenseStatus, y as ProjectState, E as EditorConfig, z as ShapePlugin, T as TemplateDefinition, I as ImageAdjustments, c as EditorState, A as SvgExportOptions, w as PrintifyPositioning, N as NormalizedLayerPosition, v as PositioningAdapter, h as ImageProviderResult, i as ImageSearchOptions, j as ImageSearchResult, F as FileAdapter, g as ImageProvider, B as BackgroundImageOptions, R as ResizeOptions, a as DpiIssue, C as CanvasSizePreset } from './types-bOY7oYVq.mjs';
3
+ export { D as DEFAULT_PATTERN_CONFIG, d as ExportFormat, f as ImageAttribution, n as LicensePayload, M as MockupBlendMode, q as MockupOverlay, u as PatternState, x as ProjectPage, G as TemplateParameter, H as TileMode } from './types-bOY7oYVq.mjs';
219
4
 
220
5
  type EventHandler<T> = (data: T) => void;
221
6
  declare class EventEmitter<TEvents extends {} = Record<string, unknown>> {
@@ -236,7 +21,7 @@ declare class Layer {
236
21
  opacity: number;
237
22
  /** Non-fabric data (e.g. pattern config) that must persist with the layer. */
238
23
  meta: LayerMeta;
239
- readonly fabricObject: FabricObject;
24
+ fabricObject: FabricObject;
240
25
  constructor(type: LayerType, fabricObject: FabricObject, name?: string, id?: string);
241
26
  private hasMeta;
242
27
  toData(): LayerData;
@@ -246,9 +31,13 @@ declare class LayerManager {
246
31
  private canvas;
247
32
  private events;
248
33
  private layers;
34
+ private onPropertyChanged?;
249
35
  constructor(canvas: Canvas, events: EventEmitter<EditorEvents>);
36
+ setHistoryCallback(callback: () => void): void;
250
37
  add(type: LayerType, fabricObject: FabricObject, name?: string, id?: string): Layer;
251
38
  remove(id: string): boolean;
39
+ /** Replace a layer's render object while preserving its immutable ID and panel state. */
40
+ replaceObject(id: string, fabricObject: FabricObject): boolean;
252
41
  reorder(id: string, newIndex: number): boolean;
253
42
  select(id: string | null): void;
254
43
  get(id: string): Layer | undefined;
@@ -267,21 +56,28 @@ declare class HistoryManager {
267
56
  private undoStack;
268
57
  private redoStack;
269
58
  private maxSize;
59
+ private maxBytes;
270
60
  private paused;
271
61
  private debounceTimer;
272
62
  private debounceMs;
273
63
  private getState;
274
64
  private restoreState;
275
65
  private events;
66
+ private transactionDepth;
67
+ private transactionDirty;
276
68
  constructor(opts: {
277
69
  getState: () => string;
278
70
  restoreState: (state: string) => Promise<void>;
279
71
  events: EventEmitter<EditorEvents>;
280
72
  maxSize?: number;
73
+ maxBytes?: number;
281
74
  debounceMs?: number;
282
75
  });
283
76
  save(): void;
284
77
  saveImmediate(): void;
78
+ beginTransaction(): void;
79
+ endTransaction(): void;
80
+ transaction<T>(operation: () => T | Promise<T>): Promise<T>;
285
81
  undo(): Promise<void>;
286
82
  redo(): Promise<void>;
287
83
  /** True while a restore (undo/redo/deserialize) is in flight. Managers that
@@ -292,9 +88,12 @@ declare class HistoryManager {
292
88
  canUndo(): boolean;
293
89
  canRedo(): boolean;
294
90
  clear(): void;
91
+ getSnapshotBytes(): number;
295
92
  private cancelPending;
296
93
  dispose(): void;
297
94
  private emitChanged;
95
+ private snapshotBytes;
96
+ private trimToBudget;
298
97
  }
299
98
 
300
99
  /**
@@ -357,8 +156,10 @@ declare class PatternManager {
357
156
  private canvas;
358
157
  private layers;
359
158
  private history;
159
+ private events;
160
+ private sourceResolver?;
360
161
  private chains;
361
- constructor(canvas: Canvas, layers: LayerManager, history: HistoryManager);
162
+ constructor(canvas: Canvas, layers: LayerManager, history: HistoryManager, events: EventEmitter<EditorEvents>, sourceResolver?: PatternSourceResolver | undefined);
362
163
  isPattern(layerId: string): boolean;
363
164
  getConfig(layerId: string): PatternConfig | null;
364
165
  /** Turn a plain image layer into a pattern, or update an existing one. */
@@ -388,6 +189,7 @@ declare function applyPatternLocks(obj: FabricObject): void;
388
189
  * leaving it frozen forever.
389
190
  */
390
191
  declare function restoreLocks(obj: FabricObject, locks: PatternLocks | undefined): void;
192
+ declare function loadPatternImage(src: string, resolver?: PatternSourceResolver): Promise<HTMLImageElement>;
391
193
  /** Clear the decoded-image cache (e.g. on teardown). Exported for completeness. */
392
194
  declare function clearPatternImageCache(): void;
393
195
  /**
@@ -396,7 +198,7 @@ declare function clearPatternImageCache(): void;
396
198
  *
397
199
  * Exported for unit testing of the tile geometry.
398
200
  */
399
- declare function buildPatternDataURL(src: string, config: PatternConfig, targetW: number, targetH: number, baseW: number, baseH: number): Promise<string>;
201
+ declare function buildPatternDataURL(src: string, config: PatternConfig, targetW: number, targetH: number, baseW: number, baseH: number, sourceResolver?: PatternSourceResolver): Promise<string>;
400
202
  /** Placement of one tile, relative to the (already rotated) pattern centre. */
401
203
  interface TilePlacement {
402
204
  /** Tile-centre offset from the pattern centre, in px. */
@@ -442,10 +244,70 @@ interface PngExportOptions {
442
244
  format?: ImageFormat;
443
245
  quality?: number;
444
246
  }
247
+ interface CoverPlacement {
248
+ left: number;
249
+ top: number;
250
+ width: number;
251
+ height: number;
252
+ }
253
+ declare function computePrintAreaClip(area: MockupPrintArea, scaleX: number, scaleY: number, targetWidth: number, targetHeight: number): MockupPrintArea;
254
+ /** Object-fit: cover geometry, exported for deterministic preview/composite tests. */
255
+ declare function computeCoverPlacement(sourceWidth: number, sourceHeight: number, targetWidth: number, targetHeight: number): CoverPlacement;
445
256
  declare function exportPNG(canvas: Canvas, options?: PngExportOptions): Promise<Blob>;
257
+ /** Rasterize the browser mockup preview together with the transparent design. */
258
+ declare function exportMockup(canvas: Canvas, mockup: MockupConfig, options?: PngExportOptions): Promise<Blob>;
446
259
  declare function exportSVG(canvas: Canvas): string;
447
260
  declare function exportDataURL(canvas: Canvas, format?: ImageFormat, multiplier?: number): string;
448
261
 
262
+ /** Browser font loader and SVG embedding registry. */
263
+ declare class FontRegistry {
264
+ private definitions;
265
+ private loads;
266
+ register(definition: FontDefinition): void;
267
+ unregister(family: string): boolean;
268
+ getAll(): FontDefinition[];
269
+ load(family: string): Promise<FontFace>;
270
+ ready(): Promise<void>;
271
+ getEmbeddedCss(): Promise<string>;
272
+ }
273
+
274
+ /** Inert, honor-system licensing and usage seam; it never performs a network request. */
275
+ declare class LicenseManager {
276
+ private config;
277
+ private status;
278
+ private readyPromise;
279
+ constructor(config?: LicenseConfig);
280
+ getStatus(): LicenseStatus;
281
+ ready(): Promise<LicenseStatus>;
282
+ hasFeature(feature: string): boolean;
283
+ track(name: string): void;
284
+ private validate;
285
+ }
286
+
287
+ /** Multi-page document state bound to one live editor canvas. */
288
+ declare class ProjectManager {
289
+ private editor;
290
+ private pages;
291
+ private activePageId;
292
+ constructor(editor: CanvasEditor);
293
+ getAll(): Array<{
294
+ id: string;
295
+ name: string;
296
+ }>;
297
+ getActivePageId(): string;
298
+ add(name?: string, cloneCurrent?: boolean): string;
299
+ switchTo(id: string): Promise<boolean>;
300
+ duplicate(id: string): Promise<string | null>;
301
+ remove(id: string): Promise<boolean>;
302
+ rename(id: string, name: string): boolean;
303
+ reorder(id: string, newIndex: number): boolean;
304
+ toJSON(): ProjectState;
305
+ fromJSON(project: ProjectState): Promise<void>;
306
+ private saveCurrent;
307
+ private blankState;
308
+ private emitChanged;
309
+ }
310
+
449
311
  declare class CanvasEditor {
450
312
  readonly canvas: Canvas;
451
313
  readonly layers: LayerManager;
@@ -455,16 +317,22 @@ declare class CanvasEditor {
455
317
  readonly snapping: SnapManager;
456
318
  readonly crop: CropController;
457
319
  readonly patterns: PatternManager;
320
+ readonly fonts: FontRegistry;
321
+ readonly licensing: LicenseManager;
322
+ readonly pages: ProjectManager;
458
323
  private fileAdapter?;
459
324
  private imageProvider?;
460
325
  private zoomLevel;
461
326
  private mockup;
462
327
  private designBackground;
328
+ private designBackgroundImage;
463
329
  constructor(canvasElement: HTMLCanvasElement, config: EditorConfig);
464
330
  addImage(url: string, options?: Partial<Record<string, unknown>>): Promise<Layer>;
331
+ /** Replace an image source without changing its layer identity or visual transform. */
332
+ replaceImageSource(layerId: string, url: string): Promise<Layer>;
465
333
  addText(text: string, options?: Partial<Record<string, unknown>>): Layer;
466
334
  addShape(plugin: ShapePlugin, options?: Record<string, unknown>): Layer;
467
- addTemplate(template: TemplateDefinition, _params: Record<string, string | number>): Promise<Layer>;
335
+ addTemplate(template: TemplateDefinition, params: Record<string, string | number>): Promise<Layer>;
468
336
  removeLayer(id: string): void;
469
337
  selectLayer(id: string | null): void;
470
338
  getSelectedLayer(): Layer | null;
@@ -472,11 +340,22 @@ declare class CanvasEditor {
472
340
  flipLayer(id: string, axis: 'horizontal' | 'vertical'): void;
473
341
  /** Clone a layer (offset slightly) and select the copy. */
474
342
  duplicateLayer(id: string): Promise<Layer | null>;
343
+ applyImageAdjustments(layerId: string, adjustments: ImageAdjustments): boolean;
344
+ /** Combine two or more layers into a single editable group layer. */
345
+ groupLayers(ids: string[], name?: string): Promise<Layer | null>;
346
+ /** Restore a group created by groupLayers back to its original layer records. */
347
+ ungroupLayer(id: string): Promise<Layer[]>;
475
348
  toJSON(): EditorState;
476
349
  fromJSON(state: EditorState): Promise<void>;
477
350
  toPNG(options?: PngExportOptions): Promise<Blob>;
351
+ toJPEG(options?: Omit<PngExportOptions, 'format'>): Promise<Blob>;
352
+ toWebP(options?: Omit<PngExportOptions, 'format'>): Promise<Blob>;
353
+ private toRaster;
478
354
  toSVG(): string;
355
+ toSVGAsync(options?: SvgExportOptions): Promise<string>;
479
356
  toDataURL(format?: ImageFormat, multiplier?: number): string;
357
+ /** Export the current product-preview composite. Advanced warping is host-defined. */
358
+ toMockupImage(options?: PngExportOptions): Promise<Blob>;
480
359
  /**
481
360
  * Run an export with the configured design background applied, even when a
482
361
  * mockup preview has forced the live canvas transparent — so exports reflect
@@ -484,14 +363,34 @@ declare class CanvasEditor {
484
363
  */
485
364
  private withDesignBackground;
486
365
  toPrintifyPositioning(): Record<string, PrintifyPositioning>;
487
- save(filename: string, format: 'png' | 'svg' | 'json'): Promise<string | undefined>;
366
+ /** Provider-neutral, ID-stable placement data in 0..1 canvas coordinates. */
367
+ toNormalizedPositioning(): NormalizedLayerPosition[];
368
+ toProviderPositioning<T>(adapter: PositioningAdapter<T>): T;
369
+ save(filename: string, format: 'png' | 'jpeg' | 'webp' | 'svg' | 'json'): Promise<string | undefined>;
488
370
  uploadImage(file: File): Promise<ImageProviderResult | undefined>;
489
371
  browseImages(): Promise<ImageProviderResult | null>;
372
+ searchImages(query: string, options?: ImageSearchOptions): Promise<ImageSearchResult | null>;
373
+ /** Track provider usage, then insert its hotlinked image as a normal layer. */
374
+ addProviderImage(image: ImageProviderResult, options?: Partial<Record<string, unknown>>): Promise<Layer>;
490
375
  setFileAdapter(adapter: FileAdapter): void;
491
376
  setImageProvider(provider: ImageProvider): void;
492
377
  setBackground(color: string): void;
493
378
  getDesignBackground(): string;
494
- resize(width: number, height: number): void;
379
+ getDesignBackgroundImage(): FabricObject | null;
380
+ setBackgroundImage(url: string | null, options?: BackgroundImageOptions): Promise<void>;
381
+ /** Used by state restoration and advanced integrations with an existing Fabric object. */
382
+ setBackgroundImageObject(image: FabricObject | null, save?: boolean): void;
383
+ setTransparentBackground(): void;
384
+ resize(width: number, height: number, options?: ResizeOptions): void;
385
+ /** Effective source resolution for an image at its current physical size. */
386
+ getImageDpi(layerId: string): number | null;
387
+ validateImageDpi(minimumDpi?: number): DpiIssue[];
388
+ /** Render a small data-URL preview without changing document dimensions. */
389
+ toThumbnail(maxWidth?: number, maxHeight?: number, format?: ImageFormat): string;
390
+ bringToFront(id: string): boolean;
391
+ sendToBack(id: string): boolean;
392
+ bringForward(id: string): boolean;
393
+ sendBackward(id: string): boolean;
495
394
  setZoom(level: number): void;
496
395
  /** @deprecated use setZoom — kept for backward compatibility. */
497
396
  zoom(level: number): void;
@@ -501,6 +400,8 @@ declare class CanvasEditor {
501
400
  resetZoom(): void;
502
401
  /** Fit the canvas inside a viewport (in px), accounting for padding. */
503
402
  zoomToFit(viewportWidth: number, viewportHeight: number, padding?: number): void;
403
+ /** Zoom until the current selection fills the artboard viewport. */
404
+ zoomToSelection(padding?: number): void;
504
405
  applyPattern(layerId: string, config: PatternConfig): Promise<void>;
505
406
  clearPattern(layerId: string): Promise<void>;
506
407
  setMockup(mockup: MockupConfig | null): void;
@@ -515,7 +416,25 @@ declare const generateId: () => string;
515
416
  declare function clamp(v: number, min: number, max: number): number;
516
417
  declare function round2(v: number): number;
517
418
 
419
+ /** XML text/attribute escaping for values interpolated into an SVG document. */
420
+ declare function escapeXml(value: string | number): string;
421
+ /**
422
+ * Strip active and externally-loaded content from an SVG before it is parsed
423
+ * into canvas objects.
424
+ *
425
+ * This is the single implementation of the control — every path that turns
426
+ * untrusted SVG into layers must call it, so it is exported rather than
427
+ * re-declared per package.
428
+ */
429
+ declare function sanitizeSvg(svg: string): string;
430
+
431
+ /** True for values safe to use as a canvas/SVG colour. Empty means transparent. */
432
+ declare function isCssColor(value: unknown, allowEmpty?: boolean): value is string;
433
+
518
434
  declare function serializeEditor(editor: CanvasEditor): EditorState;
519
435
  declare function deserializeEditor(editor: CanvasEditor, state: EditorState): Promise<void>;
520
436
 
521
- export { CanvasEditor, CropController, DEFAULT_PATTERN_CONFIG, type EditorConfig, type EditorEvents, type EditorState, EventEmitter, type ExportFormat, type FileAdapter, HistoryManager, type ImageProvider, type ImageProviderResult, Layer, type LayerData, LayerManager, type LayerMeta, type LayerType, type MockupConfig, type MockupPrintArea, type PatternConfig, type PatternLocks, PatternManager, type PatternState, type PngExportOptions, type PrintifyPositioning, type SerializedLayer, type ShapePlugin, SnapManager, type TemplateDefinition, type TemplateParameter, type TileMode, type TilePlacement, type Unit, UnitConverter, applyPatternLocks, buildPatternDataURL, captureLocks, clamp, clearPatternImageCache, computeTilePositions, deserializeEditor, drawTiles, exportDataURL, exportPNG, exportSVG, generateId, restoreLocks, round2, serializeEditor };
437
+ /** Common print and digital canvas sizes. */
438
+ declare const CANVAS_SIZE_PRESETS: readonly CanvasSizePreset[];
439
+
440
+ export { BackgroundImageOptions, CANVAS_SIZE_PRESETS, CanvasEditor, CanvasSizePreset, type CoverPlacement, CropController, DpiIssue, EditorConfig, EditorEvents, EditorState, EventEmitter, FileAdapter, FontDefinition, FontRegistry, HistoryManager, ImageAdjustments, ImageProvider, ImageProviderResult, ImageSearchOptions, ImageSearchResult, Layer, LayerData, LayerManager, LayerMeta, LayerType, LicenseConfig, LicenseManager, LicenseStatus, MockupConfig, MockupPrintArea, NormalizedLayerPosition, PatternConfig, PatternLocks, PatternManager, PatternSourceResolver, type PngExportOptions, PositioningAdapter, PrintifyPositioning, ProjectManager, ProjectState, ResizeOptions, SerializedLayer, ShapePlugin, SnapManager, SvgExportOptions, TemplateDefinition, type TilePlacement, Unit, UnitConverter, applyPatternLocks, buildPatternDataURL, captureLocks, clamp, clearPatternImageCache, computeCoverPlacement, computePrintAreaClip, computeTilePositions, deserializeEditor, drawTiles, escapeXml, exportDataURL, exportMockup, exportPNG, exportSVG, generateId, isCssColor, loadPatternImage, restoreLocks, round2, sanitizeSvg, serializeEditor };