apexify.js 4.9.30 → 5.0.1

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 (129) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +119 -4
  3. package/apex-banner.png +0 -0
  4. package/dist/cjs/Canvas/ApexPainter.d.ts +158 -145
  5. package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
  6. package/dist/cjs/Canvas/ApexPainter.js +1443 -418
  7. package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
  8. package/dist/cjs/Canvas/utils/Charts/charts.d.ts +7 -2
  9. package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +1 -1
  10. package/dist/cjs/Canvas/utils/Charts/charts.js +3 -1
  11. package/dist/cjs/Canvas/utils/Charts/charts.js.map +1 -1
  12. package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts +75 -0
  13. package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
  14. package/dist/cjs/Canvas/utils/Custom/advancedLines.js +263 -0
  15. package/dist/cjs/Canvas/utils/Custom/advancedLines.js.map +1 -0
  16. package/dist/cjs/Canvas/utils/Custom/customLines.d.ts +2 -1
  17. package/dist/cjs/Canvas/utils/Custom/customLines.d.ts.map +1 -1
  18. package/dist/cjs/Canvas/utils/Custom/customLines.js +73 -6
  19. package/dist/cjs/Canvas/utils/Custom/customLines.js.map +1 -1
  20. package/dist/cjs/Canvas/utils/General/batchOperations.d.ts +17 -0
  21. package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -0
  22. package/dist/cjs/Canvas/utils/General/batchOperations.js +88 -0
  23. package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -0
  24. package/dist/cjs/Canvas/utils/General/general functions.d.ts +25 -3
  25. package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
  26. package/dist/cjs/Canvas/utils/General/general functions.js +37 -9
  27. package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
  28. package/dist/cjs/Canvas/utils/General/imageCompression.d.ts +19 -0
  29. package/dist/cjs/Canvas/utils/General/imageCompression.d.ts.map +1 -0
  30. package/dist/cjs/Canvas/utils/General/imageCompression.js +262 -0
  31. package/dist/cjs/Canvas/utils/General/imageCompression.js.map +1 -0
  32. package/dist/cjs/Canvas/utils/General/imageStitching.d.ts +20 -0
  33. package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -0
  34. package/dist/cjs/Canvas/utils/General/imageStitching.js +227 -0
  35. package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -0
  36. package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts +37 -0
  37. package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
  38. package/dist/cjs/Canvas/utils/Image/imageEffects.js +128 -0
  39. package/dist/cjs/Canvas/utils/Image/imageEffects.js.map +1 -0
  40. package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts +67 -0
  41. package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
  42. package/dist/cjs/Canvas/utils/Image/imageMasking.js +276 -0
  43. package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -0
  44. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
  45. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
  46. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
  47. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
  48. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
  49. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js +233 -0
  50. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
  51. package/dist/cjs/Canvas/utils/types.d.ts +153 -0
  52. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  53. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  54. package/dist/cjs/Canvas/utils/utils.d.ts +9 -2
  55. package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
  56. package/dist/cjs/Canvas/utils/utils.js +32 -1
  57. package/dist/cjs/Canvas/utils/utils.js.map +1 -1
  58. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  59. package/dist/esm/Canvas/ApexPainter.d.ts +158 -145
  60. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  61. package/dist/esm/Canvas/ApexPainter.js +1443 -418
  62. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  63. package/dist/esm/Canvas/utils/Charts/charts.d.ts +7 -2
  64. package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +1 -1
  65. package/dist/esm/Canvas/utils/Charts/charts.js +3 -1
  66. package/dist/esm/Canvas/utils/Charts/charts.js.map +1 -1
  67. package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts +75 -0
  68. package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
  69. package/dist/esm/Canvas/utils/Custom/advancedLines.js +263 -0
  70. package/dist/esm/Canvas/utils/Custom/advancedLines.js.map +1 -0
  71. package/dist/esm/Canvas/utils/Custom/customLines.d.ts +2 -1
  72. package/dist/esm/Canvas/utils/Custom/customLines.d.ts.map +1 -1
  73. package/dist/esm/Canvas/utils/Custom/customLines.js +73 -6
  74. package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
  75. package/dist/esm/Canvas/utils/General/batchOperations.d.ts +17 -0
  76. package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -0
  77. package/dist/esm/Canvas/utils/General/batchOperations.js +88 -0
  78. package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -0
  79. package/dist/esm/Canvas/utils/General/general functions.d.ts +25 -3
  80. package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
  81. package/dist/esm/Canvas/utils/General/general functions.js +37 -9
  82. package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
  83. package/dist/esm/Canvas/utils/General/imageCompression.d.ts +19 -0
  84. package/dist/esm/Canvas/utils/General/imageCompression.d.ts.map +1 -0
  85. package/dist/esm/Canvas/utils/General/imageCompression.js +262 -0
  86. package/dist/esm/Canvas/utils/General/imageCompression.js.map +1 -0
  87. package/dist/esm/Canvas/utils/General/imageStitching.d.ts +20 -0
  88. package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -0
  89. package/dist/esm/Canvas/utils/General/imageStitching.js +227 -0
  90. package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -0
  91. package/dist/esm/Canvas/utils/Image/imageEffects.d.ts +37 -0
  92. package/dist/esm/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
  93. package/dist/esm/Canvas/utils/Image/imageEffects.js +128 -0
  94. package/dist/esm/Canvas/utils/Image/imageEffects.js.map +1 -0
  95. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts +67 -0
  96. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
  97. package/dist/esm/Canvas/utils/Image/imageMasking.js +276 -0
  98. package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -0
  99. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
  100. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
  101. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
  102. package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
  103. package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
  104. package/dist/esm/Canvas/utils/Texts/textPathRenderer.js +233 -0
  105. package/dist/esm/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
  106. package/dist/esm/Canvas/utils/types.d.ts +153 -0
  107. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  108. package/dist/esm/Canvas/utils/types.js.map +1 -1
  109. package/dist/esm/Canvas/utils/utils.d.ts +9 -2
  110. package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
  111. package/dist/esm/Canvas/utils/utils.js +32 -1
  112. package/dist/esm/Canvas/utils/utils.js.map +1 -1
  113. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  114. package/lib/Canvas/ApexPainter.ts +1327 -266
  115. package/lib/Canvas/utils/Charts/charts.ts +16 -7
  116. package/lib/Canvas/utils/Custom/advancedLines.ts +335 -0
  117. package/lib/Canvas/utils/Custom/customLines.ts +84 -9
  118. package/lib/Canvas/utils/General/batchOperations.ts +103 -0
  119. package/lib/Canvas/utils/General/general functions.ts +85 -41
  120. package/lib/Canvas/utils/General/imageCompression.ts +316 -0
  121. package/lib/Canvas/utils/General/imageStitching.ts +252 -0
  122. package/lib/Canvas/utils/Image/imageEffects.ts +175 -0
  123. package/lib/Canvas/utils/Image/imageMasking.ts +335 -0
  124. package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +455 -444
  125. package/lib/Canvas/utils/Texts/textPathRenderer.ts +320 -0
  126. package/lib/Canvas/utils/types.ts +156 -0
  127. package/lib/Canvas/utils/utils.ts +52 -2
  128. package/package.json +75 -35
  129. package/types/imgur.d.ts +65 -0
@@ -16,6 +16,7 @@ const enhancedTextRenderer_1 = require("./utils/Texts/enhancedTextRenderer");
16
16
  const enhancedPatternRenderer_1 = require("./utils/Patterns/enhancedPatternRenderer");
17
17
  class ApexPainter {
18
18
  format;
19
+ saveCounter = 1;
19
20
  constructor({ type } = { type: 'buffer' }) {
20
21
  this.format = { type: type || 'buffer' };
21
22
  }
@@ -46,7 +47,13 @@ class ApexPainter {
46
47
  * @param textProps - Text properties
47
48
  */
48
49
  async #renderEnhancedText(ctx, textProps) {
49
- await enhancedTextRenderer_1.EnhancedTextRenderer.renderText(ctx, textProps);
50
+ // Check if text should be rendered on a path
51
+ if (textProps.path && textProps.textOnPath) {
52
+ (0, utils_1.renderTextOnPath)(ctx, textProps.text, textProps.path, textProps.path.offset ?? 0);
53
+ }
54
+ else {
55
+ await enhancedTextRenderer_1.EnhancedTextRenderer.renderText(ctx, textProps);
56
+ }
50
57
  }
51
58
  /**
52
59
  * Creates a canvas with the given configuration.
@@ -79,75 +86,162 @@ class ApexPainter {
79
86
  * const buffer = result.buffer;
80
87
  * ```
81
88
  */
89
+ /**
90
+ * Validates canvas configuration.
91
+ * @private
92
+ * @param canvas - Canvas configuration to validate
93
+ */
94
+ #validateCanvasConfig(canvas) {
95
+ if (!canvas) {
96
+ throw new Error("createCanvas: canvas configuration is required.");
97
+ }
98
+ if (canvas.width !== undefined && (typeof canvas.width !== 'number' || canvas.width <= 0)) {
99
+ throw new Error("createCanvas: width must be a positive number.");
100
+ }
101
+ if (canvas.height !== undefined && (typeof canvas.height !== 'number' || canvas.height <= 0)) {
102
+ throw new Error("createCanvas: height must be a positive number.");
103
+ }
104
+ if (canvas.opacity !== undefined && (typeof canvas.opacity !== 'number' || canvas.opacity < 0 || canvas.opacity > 1)) {
105
+ throw new Error("createCanvas: opacity must be a number between 0 and 1.");
106
+ }
107
+ if (canvas.zoom?.scale !== undefined && (typeof canvas.zoom.scale !== 'number' || canvas.zoom.scale <= 0)) {
108
+ throw new Error("createCanvas: zoom.scale must be a positive number.");
109
+ }
110
+ }
82
111
  async createCanvas(canvas) {
83
- // Handle inherit sizing
84
- if (canvas.customBg?.inherit) {
85
- let p = canvas.customBg.source;
86
- if (!/^https?:\/\//i.test(p))
87
- p = path_1.default.join(process.cwd(), p);
88
- try {
89
- const img = await (0, canvas_1.loadImage)(p);
90
- canvas.width = img.width;
91
- canvas.height = img.height;
92
- }
93
- catch (e) {
94
- console.error('inherit load failed:', e?.message ?? e);
95
- }
96
- }
97
- // 2) Use final width/height after inherit
98
- const width = canvas.width ?? 500;
99
- const height = canvas.height ?? 500;
100
- const { x = 0, y = 0, rotation = 0, borderRadius = 0, borderPosition = 'all', opacity = 1, colorBg, customBg, gradientBg, patternBg, noiseBg, blendMode, zoom, stroke, shadow, blur } = canvas;
101
- // Validate background configuration
102
- const bgSources = [
103
- canvas.colorBg ? 'colorBg' : null,
104
- canvas.gradientBg ? 'gradientBg' : null,
105
- canvas.customBg ? 'customBg' : null
106
- ].filter(Boolean);
107
- if (bgSources.length > 1) {
108
- throw new Error(`createCanvas: only one of colorBg, gradientBg, or customBg can be used. You provided: ${bgSources.join(', ')}`);
109
- }
110
- const cv = (0, canvas_1.createCanvas)(width, height);
111
- const ctx = cv.getContext('2d');
112
- if (!ctx)
113
- throw new Error('Unable to get 2D context');
114
- ctx.globalAlpha = opacity;
115
- // ---- BACKGROUND (clipped) ----
116
- ctx.save();
117
- (0, utils_1.applyRotation)(ctx, rotation, x, y, width, height);
118
- (0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
119
- ctx.clip();
120
- (0, utils_1.applyCanvasZoom)(ctx, width, height, zoom);
121
- ctx.translate(x, y);
122
- if (typeof blendMode === 'string') {
123
- ctx.globalCompositeOperation = blendMode;
124
- }
125
- if (customBg)
126
- await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
127
- else if (gradientBg)
128
- await (0, utils_1.drawBackgroundGradient)(ctx, { ...canvas, blur });
129
- else
130
- await (0, utils_1.drawBackgroundColor)(ctx, { ...canvas, blur, colorBg: colorBg ?? '#000' });
131
- if (patternBg)
132
- await enhancedPatternRenderer_1.EnhancedPatternRenderer.renderPattern(ctx, cv, patternBg);
133
- if (noiseBg)
134
- (0, utils_1.applyNoise)(ctx, width, height, noiseBg.intensity ?? 0.05);
135
- ctx.restore();
136
- // Apply shadow effect
137
- if (shadow) {
112
+ try {
113
+ // Validate canvas configuration
114
+ this.#validateCanvasConfig(canvas);
115
+ // Handle inherit sizing
116
+ if (canvas.customBg?.inherit) {
117
+ let p = canvas.customBg.source;
118
+ if (!/^https?:\/\//i.test(p))
119
+ p = path_1.default.join(process.cwd(), p);
120
+ try {
121
+ const img = await (0, canvas_1.loadImage)(p);
122
+ canvas.width = img.width;
123
+ canvas.height = img.height;
124
+ }
125
+ catch (e) {
126
+ const errorMessage = e instanceof Error ? e.message : String(e);
127
+ throw new Error(`createCanvas: Failed to load image for inherit sizing: ${errorMessage}`);
128
+ }
129
+ }
130
+ // Handle video background inherit sizing
131
+ if (canvas.videoBg) {
132
+ try {
133
+ const frameBuffer = await this.#extractVideoFrame(canvas.videoBg.source, canvas.videoBg.frame ?? 0);
134
+ if (frameBuffer) {
135
+ const img = await (0, canvas_1.loadImage)(frameBuffer);
136
+ if (!canvas.width)
137
+ canvas.width = img.width;
138
+ if (!canvas.height)
139
+ canvas.height = img.height;
140
+ }
141
+ }
142
+ catch (e) {
143
+ console.warn('createCanvas: Failed to extract video frame for sizing, using defaults');
144
+ }
145
+ }
146
+ // 2) Use final width/height after inherit
147
+ const width = canvas.width ?? 500;
148
+ const height = canvas.height ?? 500;
149
+ const { x = 0, y = 0, rotation = 0, borderRadius = 0, borderPosition = 'all', opacity = 1, colorBg, customBg, gradientBg, videoBg, patternBg, noiseBg, blendMode, zoom, stroke, shadow, blur } = canvas;
150
+ // Validate background configuration
151
+ const bgSources = [
152
+ canvas.colorBg ? 'colorBg' : null,
153
+ canvas.gradientBg ? 'gradientBg' : null,
154
+ canvas.customBg ? 'customBg' : null
155
+ ].filter(Boolean);
156
+ if (bgSources.length > 1) {
157
+ throw new Error(`createCanvas: only one of colorBg, gradientBg, or customBg can be used. You provided: ${bgSources.join(', ')}`);
158
+ }
159
+ const cv = (0, canvas_1.createCanvas)(width, height);
160
+ const ctx = cv.getContext('2d');
161
+ if (!ctx)
162
+ throw new Error('Unable to get 2D context');
163
+ ctx.globalAlpha = opacity;
164
+ // ---- BACKGROUND (clipped) ----
138
165
  ctx.save();
166
+ (0, utils_1.applyRotation)(ctx, rotation, x, y, width, height);
139
167
  (0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
140
- (0, utils_1.applyShadow)(ctx, shadow, x, y, width, height);
168
+ ctx.clip();
169
+ (0, utils_1.applyCanvasZoom)(ctx, width, height, zoom);
170
+ ctx.translate(x, y);
171
+ if (typeof blendMode === 'string') {
172
+ ctx.globalCompositeOperation = blendMode;
173
+ }
174
+ // Draw video background if specified
175
+ if (videoBg) {
176
+ try {
177
+ const frameBuffer = await this.#extractVideoFrame(videoBg.source, videoBg.frame ?? 0);
178
+ if (frameBuffer) {
179
+ const videoImg = await (0, canvas_1.loadImage)(frameBuffer);
180
+ ctx.globalAlpha = videoBg.opacity ?? 1;
181
+ ctx.drawImage(videoImg, 0, 0, width, height);
182
+ ctx.globalAlpha = opacity;
183
+ }
184
+ }
185
+ catch (e) {
186
+ console.warn('createCanvas: Failed to load video background frame');
187
+ }
188
+ }
189
+ // Draw custom background with filters and opacity support
190
+ if (customBg) {
191
+ await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
192
+ // Apply filters to background if specified
193
+ if (customBg.filters && customBg.filters.length > 0) {
194
+ const tempCanvas = (0, canvas_1.createCanvas)(width, height);
195
+ const tempCtx = tempCanvas.getContext('2d');
196
+ if (tempCtx) {
197
+ tempCtx.drawImage(cv, 0, 0);
198
+ await (0, utils_1.applySimpleProfessionalFilters)(tempCtx, customBg.filters, width, height);
199
+ ctx.clearRect(0, 0, width, height);
200
+ ctx.globalAlpha = customBg.opacity ?? 1;
201
+ ctx.drawImage(tempCanvas, 0, 0);
202
+ ctx.globalAlpha = opacity;
203
+ }
204
+ }
205
+ else if (customBg.opacity !== undefined && customBg.opacity !== 1) {
206
+ ctx.globalAlpha = customBg.opacity;
207
+ await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
208
+ ctx.globalAlpha = opacity;
209
+ }
210
+ else {
211
+ await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
212
+ }
213
+ }
214
+ else if (gradientBg) {
215
+ await (0, utils_1.drawBackgroundGradient)(ctx, { ...canvas, blur });
216
+ }
217
+ else {
218
+ await (0, utils_1.drawBackgroundColor)(ctx, { ...canvas, blur, colorBg: colorBg ?? '#000' });
219
+ }
220
+ if (patternBg)
221
+ await enhancedPatternRenderer_1.EnhancedPatternRenderer.renderPattern(ctx, cv, patternBg);
222
+ if (noiseBg)
223
+ (0, utils_1.applyNoise)(ctx, width, height, noiseBg.intensity ?? 0.05);
141
224
  ctx.restore();
225
+ // Apply shadow effect
226
+ if (shadow) {
227
+ ctx.save();
228
+ (0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
229
+ (0, utils_1.applyShadow)(ctx, shadow, x, y, width, height);
230
+ ctx.restore();
231
+ }
232
+ // Apply stroke effect
233
+ if (stroke) {
234
+ ctx.save();
235
+ (0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
236
+ (0, utils_1.applyStroke)(ctx, stroke, x, y, width, height);
237
+ ctx.restore();
238
+ }
239
+ return { buffer: cv.toBuffer('image/png'), canvas };
142
240
  }
143
- // Apply stroke effect
144
- if (stroke) {
145
- ctx.save();
146
- (0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
147
- (0, utils_1.applyStroke)(ctx, stroke, x, y, width, height);
148
- ctx.restore();
241
+ catch (error) {
242
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
243
+ throw new Error(`createCanvas failed: ${errorMessage}`);
149
244
  }
150
- return { buffer: cv.toBuffer('image/png'), canvas };
151
245
  }
152
246
  /**
153
247
  * Draws one or more images (or shapes) on an existing canvas buffer.
@@ -191,24 +285,49 @@ class ApexPainter {
191
285
  * ], canvasBuffer);
192
286
  * ```
193
287
  */
194
- async createImage(images, canvasBuffer) {
288
+ /**
289
+ * Validates image/shape properties array.
290
+ * @private
291
+ * @param images - Image properties to validate
292
+ */
293
+ #validateImageArray(images) {
195
294
  const list = Array.isArray(images) ? images : [images];
196
- // Load base canvas buffer
197
- const base = Buffer.isBuffer(canvasBuffer)
198
- ? await (0, canvas_1.loadImage)(canvasBuffer)
199
- : await (0, canvas_1.loadImage)(canvasBuffer.buffer);
200
- const cv = (0, canvas_1.createCanvas)(base.width, base.height);
201
- const ctx = cv.getContext("2d");
202
- if (!ctx)
203
- throw new Error("Unable to get 2D rendering context");
204
- // Paint bg
205
- ctx.drawImage(base, 0, 0);
206
- // Draw each image/shape on canvas
295
+ if (list.length === 0) {
296
+ throw new Error("createImage: At least one image/shape is required.");
297
+ }
207
298
  for (const ip of list) {
208
- await this.#drawImageBitmap(ctx, ip);
299
+ this.#validateImageProperties(ip);
300
+ }
301
+ }
302
+ async createImage(images, canvasBuffer) {
303
+ try {
304
+ // Validate inputs
305
+ if (!canvasBuffer) {
306
+ throw new Error("createImage: canvasBuffer is required.");
307
+ }
308
+ this.#validateImageArray(images);
309
+ const list = Array.isArray(images) ? images : [images];
310
+ // Load base canvas buffer
311
+ const base = Buffer.isBuffer(canvasBuffer)
312
+ ? await (0, canvas_1.loadImage)(canvasBuffer)
313
+ : await (0, canvas_1.loadImage)(canvasBuffer.buffer);
314
+ const cv = (0, canvas_1.createCanvas)(base.width, base.height);
315
+ const ctx = cv.getContext("2d");
316
+ if (!ctx)
317
+ throw new Error("Unable to get 2D rendering context");
318
+ // Paint bg
319
+ ctx.drawImage(base, 0, 0);
320
+ // Draw each image/shape on canvas
321
+ for (const ip of list) {
322
+ await this.#drawImageBitmap(ctx, ip);
323
+ }
324
+ // Return updated buffer
325
+ return cv.toBuffer("image/png");
326
+ }
327
+ catch (error) {
328
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
329
+ throw new Error(`createImage failed: ${errorMessage}`);
209
330
  }
210
- // Return updated buffer
211
- return cv.toBuffer("image/png");
212
331
  }
213
332
  /**
214
333
  * Draws a single bitmap or shape with independent shadow & stroke.
@@ -217,7 +336,7 @@ class ApexPainter {
217
336
  * @param ip - Image properties
218
337
  */
219
338
  async #drawImageBitmap(ctx, ip) {
220
- const { source, x, y, width, height, inherit, fit = "fill", align = "center", rotation = 0, opacity = 1, blur = 0, borderRadius = 0, borderPosition = "all", shadow, stroke, boxBackground, shape, filters } = ip;
339
+ const { source, x, y, width, height, inherit, fit = "fill", align = "center", rotation = 0, opacity = 1, blur = 0, borderRadius = 0, borderPosition = "all", shadow, stroke, boxBackground, shape, filters, filterIntensity = 1, filterOrder = 'post', mask, clipPath, distortion, meshWarp, effects } = ip;
221
340
  this.#validateImageProperties(ip);
222
341
  // Check if source is a shape
223
342
  if ((0, utils_1.isShapeSource)(source)) {
@@ -248,23 +367,126 @@ class ApexPainter {
248
367
  (0, utils_1.applyShadow)(ctx, box, shadow);
249
368
  // 2) Optional box background (under bitmap, inside clip) — color or gradient
250
369
  (0, utils_1.drawBoxBackground)(ctx, box, boxBackground, borderRadius, borderPosition);
251
- // 3) Clip to image border radius, then draw the bitmap with blur/opacity and fit/align
370
+ // 3) Clip to image border radius or custom clip path, then draw the bitmap with blur/opacity and fit/align
252
371
  ctx.save();
253
- (0, utils_1.buildPath)(ctx, box.x, box.y, box.w, box.h, borderRadius, borderPosition);
254
- ctx.clip();
372
+ if (clipPath && clipPath.length >= 3) {
373
+ (0, utils_1.applyClipPath)(ctx, clipPath);
374
+ }
375
+ else {
376
+ (0, utils_1.buildPath)(ctx, box.x, box.y, box.w, box.h, borderRadius, borderPosition);
377
+ ctx.clip();
378
+ }
255
379
  const { dx, dy, dw, dh, sx, sy, sw, sh } = (0, utils_1.fitInto)(box.x, box.y, box.w, box.h, img.width, img.height, fit, align);
256
380
  const prevAlpha = ctx.globalAlpha;
257
381
  ctx.globalAlpha = opacity ?? 1;
258
382
  if ((blur ?? 0) > 0)
259
383
  ctx.filter = `blur(${blur}px)`;
260
- // Apply professional image filters BEFORE drawing
261
- if (filters && filters.length > 0) {
262
- await (0, utils_1.applySimpleProfessionalFilters)(ctx, filters, dw, dh);
384
+ // Apply professional image filters BEFORE drawing if filterOrder is 'pre'
385
+ if (filters && filters.length > 0 && filterOrder === 'pre') {
386
+ const adjustedFilters = filters.map(f => ({
387
+ ...f,
388
+ intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
389
+ value: f.value !== undefined ? f.value * filterIntensity : f.value,
390
+ radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
391
+ }));
392
+ await (0, utils_1.applySimpleProfessionalFilters)(ctx, adjustedFilters, dw, dh);
393
+ }
394
+ // Apply distortion if specified (before drawing)
395
+ if (distortion) {
396
+ if (distortion.type === 'perspective' && distortion.points && distortion.points.length === 4) {
397
+ (0, utils_1.applyPerspectiveDistortion)(ctx, img, distortion.points, dx, dy, dw, dh);
398
+ ctx.filter = "none";
399
+ ctx.globalAlpha = prevAlpha;
400
+ ctx.restore();
401
+ ctx.restore();
402
+ return;
403
+ }
404
+ else if (distortion.type === 'bulge' || distortion.type === 'pinch') {
405
+ const centerX = dx + dw / 2;
406
+ const centerY = dy + dh / 2;
407
+ const radius = Math.min(dw, dh) / 2;
408
+ const intensity = (distortion.intensity ?? 0.5) * (distortion.type === 'pinch' ? -1 : 1);
409
+ (0, utils_1.applyBulgeDistortion)(ctx, img, centerX, centerY, radius, intensity, dx, dy, dw, dh);
410
+ ctx.filter = "none";
411
+ ctx.globalAlpha = prevAlpha;
412
+ ctx.restore();
413
+ ctx.restore();
414
+ return;
415
+ }
416
+ }
417
+ // Apply mesh warp if specified
418
+ if (meshWarp && meshWarp.controlPoints) {
419
+ (0, utils_1.applyMeshWarp)(ctx, img, meshWarp.gridX ?? 10, meshWarp.gridY ?? 10, meshWarp.controlPoints, dx, dy, dw, dh);
420
+ ctx.filter = "none";
421
+ ctx.globalAlpha = prevAlpha;
422
+ ctx.restore();
423
+ ctx.restore();
424
+ return;
425
+ }
426
+ // Draw image with or without masking
427
+ if (mask) {
428
+ await (0, utils_1.applyImageMask)(ctx, img, mask.source, mask.mode ?? 'alpha', dx, dy, dw, dh);
429
+ }
430
+ else {
431
+ ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
263
432
  }
264
- ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
265
433
  ctx.filter = "none";
266
434
  ctx.globalAlpha = prevAlpha;
267
435
  ctx.restore();
436
+ // Apply professional image filters AFTER drawing if filterOrder is 'post'
437
+ if (filters && filters.length > 0 && filterOrder === 'post') {
438
+ ctx.save();
439
+ const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
440
+ const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
441
+ const tempCtx = tempCanvas.getContext('2d');
442
+ if (tempCtx) {
443
+ tempCtx.putImageData(imageData, 0, 0);
444
+ const adjustedFilters = filters.map(f => ({
445
+ ...f,
446
+ intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
447
+ value: f.value !== undefined ? f.value * filterIntensity : f.value,
448
+ radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
449
+ }));
450
+ await (0, utils_1.applySimpleProfessionalFilters)(tempCtx, adjustedFilters, box.w, box.h);
451
+ ctx.clearRect(box.x, box.y, box.w, box.h);
452
+ ctx.drawImage(tempCanvas, box.x, box.y);
453
+ }
454
+ ctx.restore();
455
+ }
456
+ // Apply effects stack
457
+ if (effects) {
458
+ ctx.save();
459
+ const effectsCtx = ctx;
460
+ if (effects.vignette) {
461
+ (0, utils_1.applyVignette)(effectsCtx, effects.vignette.intensity, effects.vignette.size, box.w, box.h);
462
+ }
463
+ if (effects.lensFlare) {
464
+ (0, utils_1.applyLensFlare)(effectsCtx, box.x + effects.lensFlare.x, box.y + effects.lensFlare.y, effects.lensFlare.intensity, box.w, box.h);
465
+ }
466
+ if (effects.chromaticAberration) {
467
+ const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
468
+ const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
469
+ const tempCtx = tempCanvas.getContext('2d');
470
+ if (tempCtx) {
471
+ tempCtx.putImageData(imageData, 0, 0);
472
+ (0, utils_1.applyChromaticAberration)(tempCtx, effects.chromaticAberration.intensity, box.w, box.h);
473
+ ctx.clearRect(box.x, box.y, box.w, box.h);
474
+ ctx.drawImage(tempCanvas, box.x, box.y);
475
+ }
476
+ }
477
+ if (effects.filmGrain) {
478
+ const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
479
+ const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
480
+ const tempCtx = tempCanvas.getContext('2d');
481
+ if (tempCtx) {
482
+ tempCtx.putImageData(imageData, 0, 0);
483
+ (0, utils_1.applyFilmGrain)(tempCtx, effects.filmGrain.intensity, box.w, box.h);
484
+ ctx.clearRect(box.x, box.y, box.w, box.h);
485
+ ctx.drawImage(tempCanvas, box.x, box.y);
486
+ }
487
+ }
488
+ ctx.restore();
489
+ }
268
490
  // 4) Stroke (independent) — supports gradient or color
269
491
  (0, utils_1.applyStroke)(ctx, box, stroke);
270
492
  ctx.restore();
@@ -488,14 +710,29 @@ class ApexPainter {
488
710
  * ], canvasBuffer);
489
711
  * ```
490
712
  */
713
+ /**
714
+ * Validates text properties array.
715
+ * @private
716
+ * @param textArray - Text properties to validate
717
+ */
718
+ #validateTextArray(textArray) {
719
+ const textList = Array.isArray(textArray) ? textArray : [textArray];
720
+ if (textList.length === 0) {
721
+ throw new Error("createText: At least one text object is required.");
722
+ }
723
+ for (const textProps of textList) {
724
+ this.#validateTextProperties(textProps);
725
+ }
726
+ }
491
727
  async createText(textArray, canvasBuffer) {
492
728
  try {
729
+ // Validate inputs
730
+ if (!canvasBuffer) {
731
+ throw new Error("createText: canvasBuffer is required.");
732
+ }
733
+ this.#validateTextArray(textArray);
493
734
  // Ensure textArray is an array
494
735
  const textList = Array.isArray(textArray) ? textArray : [textArray];
495
- // Validate each text object
496
- for (const textProps of textList) {
497
- this.#validateTextProperties(textProps);
498
- }
499
736
  // Load existing canvas buffer
500
737
  let existingImage;
501
738
  if (Buffer.isBuffer(canvasBuffer)) {
@@ -525,15 +762,37 @@ class ApexPainter {
525
762
  return canvas.toBuffer("image/png");
526
763
  }
527
764
  catch (error) {
528
- console.error("Error creating text:", error);
529
- throw new Error("Failed to create text on canvas");
765
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
766
+ throw new Error(`createText failed: ${errorMessage}`);
767
+ }
768
+ }
769
+ /**
770
+ * Validates custom line options.
771
+ * @private
772
+ * @param options - Custom options to validate
773
+ */
774
+ #validateCustomOptions(options) {
775
+ const opts = Array.isArray(options) ? options : [options];
776
+ if (opts.length === 0) {
777
+ throw new Error("createCustom: At least one custom option is required.");
778
+ }
779
+ for (const opt of opts) {
780
+ if (!opt.startCoordinates || typeof opt.startCoordinates.x !== 'number' || typeof opt.startCoordinates.y !== 'number') {
781
+ throw new Error("createCustom: startCoordinates with valid x and y are required.");
782
+ }
783
+ if (!opt.endCoordinates || typeof opt.endCoordinates.x !== 'number' || typeof opt.endCoordinates.y !== 'number') {
784
+ throw new Error("createCustom: endCoordinates with valid x and y are required.");
785
+ }
530
786
  }
531
787
  }
532
788
  async createCustom(options, buffer) {
533
789
  try {
534
- if (!Array.isArray(options)) {
535
- options = [options];
790
+ // Validate inputs
791
+ if (!buffer) {
792
+ throw new Error("createCustom: buffer is required.");
536
793
  }
794
+ this.#validateCustomOptions(options);
795
+ const opts = Array.isArray(options) ? options : [options];
537
796
  let existingImage;
538
797
  if (Buffer.isBuffer(buffer)) {
539
798
  existingImage = await (0, canvas_1.loadImage)(buffer);
@@ -550,65 +809,68 @@ class ApexPainter {
550
809
  const canvas = (0, canvas_1.createCanvas)(existingImage.width, existingImage.height);
551
810
  const ctx = canvas.getContext("2d");
552
811
  ctx.drawImage(existingImage, 0, 0);
553
- (0, utils_1.customLines)(ctx, options);
812
+ await (0, utils_1.customLines)(ctx, opts);
554
813
  return canvas.toBuffer("image/png");
555
814
  }
556
815
  catch (error) {
557
- console.error("Error creating custom image:", error);
558
- throw new Error("Failed to create custom image");
816
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
817
+ throw new Error(`createCustom failed: ${errorMessage}`);
559
818
  }
560
819
  }
561
- async createGIF(gifFrames, options) {
562
- async function resizeImage(image, targetWidth, targetHeight) {
563
- const canvas = (0, canvas_1.createCanvas)(targetWidth, targetHeight);
564
- const ctx = canvas.getContext("2d");
565
- ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
566
- return canvas;
567
- }
568
- function createOutputStream(outputFile) {
569
- return fs_1.default.createWriteStream(outputFile);
570
- }
571
- function createBufferStream() {
572
- const bufferStream = new stream_1.PassThrough();
573
- const chunks = [];
574
- bufferStream.on('data', (chunk) => {
575
- chunks.push(chunk);
576
- });
577
- return {
578
- ...bufferStream,
579
- getBuffer: function () {
580
- return Buffer.concat(chunks);
581
- }
582
- };
820
+ /**
821
+ * Validates GIF options and frames.
822
+ * @private
823
+ * @param gifFrames - GIF frames to validate
824
+ * @param options - GIF options to validate
825
+ */
826
+ #validateGIFOptions(gifFrames, options) {
827
+ if (!gifFrames || gifFrames.length === 0) {
828
+ throw new Error("createGIF: At least one frame is required.");
583
829
  }
584
- function validateOptions(options) {
585
- if (options.outputFormat === "file" && !options.outputFile) {
586
- throw new Error("Output file path is required when using file output format.");
830
+ for (const frame of gifFrames) {
831
+ if (!frame.background) {
832
+ throw new Error("createGIF: Each frame must have a background property.");
587
833
  }
588
- if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
589
- throw new Error("Repeat must be a non-negative number or undefined.");
834
+ if (typeof frame.duration !== 'number' || frame.duration < 0) {
835
+ throw new Error("createGIF: Each frame duration must be a non-negative number.");
590
836
  }
591
- if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
592
- throw new Error("Quality must be a number between 1 and 20 or undefined.");
837
+ }
838
+ if (options.outputFormat === "file" && !options.outputFile) {
839
+ throw new Error("createGIF: outputFile is required when outputFormat is 'file'.");
840
+ }
841
+ if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
842
+ throw new Error("createGIF: repeat must be a non-negative number or undefined.");
843
+ }
844
+ if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
845
+ throw new Error("createGIF: quality must be a number between 1 and 20 or undefined.");
846
+ }
847
+ }
848
+ async createGIF(gifFrames, options) {
849
+ try {
850
+ this.#validateGIFOptions(gifFrames, options);
851
+ async function resizeImage(image, targetWidth, targetHeight) {
852
+ const canvas = (0, canvas_1.createCanvas)(targetWidth, targetHeight);
853
+ const ctx = canvas.getContext("2d");
854
+ ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
855
+ return canvas;
593
856
  }
594
- if (options.watermark && typeof options.watermark.enable !== "boolean") {
595
- throw new Error("Watermark must be a boolean or undefined.");
857
+ function createOutputStream(outputFile) {
858
+ return fs_1.default.createWriteStream(outputFile);
596
859
  }
597
- if (options.textOverlay) {
598
- const textOptions = options.textOverlay;
599
- if (!textOptions.text || typeof textOptions.text !== "string") {
600
- throw new Error("Text overlay text is required and must be a string.");
601
- }
602
- if (textOptions.fontSize !== undefined && (!Number.isInteger(textOptions.fontSize) || textOptions.fontSize <= 0)) {
603
- throw new Error("Text overlay fontSize must be a positive integer or undefined.");
604
- }
605
- if (textOptions.fontColor !== undefined && typeof textOptions.fontColor !== "string") {
606
- throw new Error("Text overlay fontColor must be a string or undefined.");
607
- }
860
+ function createBufferStream() {
861
+ const bufferStream = new stream_1.PassThrough();
862
+ const chunks = [];
863
+ bufferStream.on('data', (chunk) => {
864
+ chunks.push(chunk);
865
+ });
866
+ return {
867
+ ...bufferStream,
868
+ getBuffer: function () {
869
+ return Buffer.concat(chunks);
870
+ }
871
+ };
608
872
  }
609
- }
610
- try {
611
- validateOptions(options);
873
+ // Validation is done in #validateGIFOptions
612
874
  const canvasWidth = options.width || 1200;
613
875
  const canvasHeight = options.height || 1200;
614
876
  const encoder = new gifencoder_1.default(canvasWidth, canvasHeight);
@@ -619,6 +881,8 @@ class ApexPainter {
619
881
  encoder.setQuality(options.quality || 10);
620
882
  const canvas = (0, canvas_1.createCanvas)(canvasWidth, canvasHeight);
621
883
  const ctx = canvas.getContext("2d");
884
+ if (!ctx)
885
+ throw new Error("Unable to get 2D context");
622
886
  for (const frame of gifFrames) {
623
887
  const image = await (0, canvas_1.loadImage)(frame.background);
624
888
  const resizedImage = await resizeImage(image, canvasWidth, canvasHeight);
@@ -639,12 +903,13 @@ class ApexPainter {
639
903
  encoder.finish();
640
904
  outputStream.end();
641
905
  if (options.outputFormat === "file") {
642
- await new Promise((resolve) => outputStream.on("finish", resolve));
906
+ await new Promise((resolve) => outputStream.on("finish", () => resolve()));
643
907
  }
644
908
  else if (options.outputFormat === "base64") {
645
909
  if ('getBuffer' in outputStream) {
646
910
  return outputStream.getBuffer().toString("base64");
647
911
  }
912
+ throw new Error("createGIF: Unable to get buffer for base64 output.");
648
913
  }
649
914
  else if (options.outputFormat === "attachment") {
650
915
  const gifStream = encoder.createReadStream();
@@ -654,42 +919,204 @@ class ApexPainter {
654
919
  if ('getBuffer' in outputStream) {
655
920
  return outputStream.getBuffer();
656
921
  }
922
+ throw new Error("createGIF: Unable to get buffer for buffer output.");
657
923
  }
658
924
  else {
659
925
  throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
660
926
  }
661
927
  }
662
- catch (e) {
663
- console.error(e.message);
664
- throw e;
928
+ catch (error) {
929
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
930
+ throw new Error(`createGIF failed: ${errorMessage}`);
931
+ }
932
+ }
933
+ /**
934
+ * Validates resize options.
935
+ * @private
936
+ * @param options - Resize options to validate
937
+ */
938
+ #validateResizeOptions(options) {
939
+ if (!options || !options.imagePath) {
940
+ throw new Error("resize: imagePath is required.");
941
+ }
942
+ if (options.size) {
943
+ if (options.size.width !== undefined && (typeof options.size.width !== 'number' || options.size.width <= 0)) {
944
+ throw new Error("resize: size.width must be a positive number.");
945
+ }
946
+ if (options.size.height !== undefined && (typeof options.size.height !== 'number' || options.size.height <= 0)) {
947
+ throw new Error("resize: size.height must be a positive number.");
948
+ }
949
+ }
950
+ if (options.quality !== undefined && (typeof options.quality !== 'number' || options.quality < 0 || options.quality > 100)) {
951
+ throw new Error("resize: quality must be a number between 0 and 100.");
665
952
  }
666
953
  }
667
954
  async resize(resizeOptions) {
668
- return (0, utils_1.resizingImg)(resizeOptions);
955
+ try {
956
+ this.#validateResizeOptions(resizeOptions);
957
+ return await (0, utils_1.resizingImg)(resizeOptions);
958
+ }
959
+ catch (error) {
960
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
961
+ throw new Error(`resize failed: ${errorMessage}`);
962
+ }
963
+ }
964
+ /**
965
+ * Validates image converter inputs.
966
+ * @private
967
+ * @param source - Image source to validate
968
+ * @param newExtension - Extension to validate
969
+ */
970
+ #validateConverterInputs(source, newExtension) {
971
+ if (!source) {
972
+ throw new Error("imgConverter: source is required.");
973
+ }
974
+ if (!newExtension) {
975
+ throw new Error("imgConverter: newExtension is required.");
976
+ }
977
+ const validExtensions = ['jpeg', 'png', 'webp', 'tiff', 'gif', 'avif', 'heif', 'raw', 'pdf', 'svg'];
978
+ if (!validExtensions.includes(newExtension.toLowerCase())) {
979
+ throw new Error(`imgConverter: Invalid extension. Supported: ${validExtensions.join(', ')}`);
980
+ }
669
981
  }
670
982
  async imgConverter(source, newExtension) {
671
- return (0, utils_1.converter)(source, newExtension);
983
+ try {
984
+ this.#validateConverterInputs(source, newExtension);
985
+ return await (0, utils_1.converter)(source, newExtension);
986
+ }
987
+ catch (error) {
988
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
989
+ throw new Error(`imgConverter failed: ${errorMessage}`);
990
+ }
991
+ }
992
+ /**
993
+ * Validates effects inputs.
994
+ * @private
995
+ * @param source - Image source to validate
996
+ * @param filters - Filters array to validate
997
+ */
998
+ #validateEffectsInputs(source, filters) {
999
+ if (!source) {
1000
+ throw new Error("effects: source is required.");
1001
+ }
1002
+ if (!filters || !Array.isArray(filters) || filters.length === 0) {
1003
+ throw new Error("effects: filters array with at least one filter is required.");
1004
+ }
672
1005
  }
673
1006
  async effects(source, filters) {
674
- return (0, utils_1.imgEffects)(source, filters);
1007
+ try {
1008
+ this.#validateEffectsInputs(source, filters);
1009
+ return await (0, utils_1.imgEffects)(source, filters);
1010
+ }
1011
+ catch (error) {
1012
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1013
+ throw new Error(`effects failed: ${errorMessage}`);
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Validates color filter inputs.
1018
+ * @private
1019
+ * @param source - Image source to validate
1020
+ * @param opacity - Opacity to validate
1021
+ */
1022
+ #validateColorFilterInputs(source, opacity) {
1023
+ if (!source) {
1024
+ throw new Error("colorsFilter: source is required.");
1025
+ }
1026
+ if (opacity !== undefined && (typeof opacity !== 'number' || opacity < 0 || opacity > 1)) {
1027
+ throw new Error("colorsFilter: opacity must be a number between 0 and 1.");
1028
+ }
675
1029
  }
676
1030
  async colorsFilter(source, filterColor, opacity) {
677
- return (0, utils_1.applyColorFilters)(source, filterColor, opacity);
1031
+ try {
1032
+ this.#validateColorFilterInputs(source, opacity);
1033
+ return await (0, utils_1.applyColorFilters)(source, filterColor, opacity);
1034
+ }
1035
+ catch (error) {
1036
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1037
+ throw new Error(`colorsFilter failed: ${errorMessage}`);
1038
+ }
678
1039
  }
679
1040
  async colorAnalysis(source) {
680
- return (0, utils_1.detectColors)(source);
1041
+ try {
1042
+ if (!source) {
1043
+ throw new Error("colorAnalysis: source is required.");
1044
+ }
1045
+ return await (0, utils_1.detectColors)(source);
1046
+ }
1047
+ catch (error) {
1048
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1049
+ throw new Error(`colorAnalysis failed: ${errorMessage}`);
1050
+ }
681
1051
  }
682
1052
  async colorsRemover(source, colorToRemove) {
683
- return (0, utils_1.removeColor)(source, colorToRemove);
1053
+ try {
1054
+ if (!source) {
1055
+ throw new Error("colorsRemover: source is required.");
1056
+ }
1057
+ if (!colorToRemove || typeof colorToRemove.red !== 'number' || typeof colorToRemove.green !== 'number' || typeof colorToRemove.blue !== 'number') {
1058
+ throw new Error("colorsRemover: colorToRemove must be an object with red, green, and blue properties (0-255).");
1059
+ }
1060
+ if (colorToRemove.red < 0 || colorToRemove.red > 255 ||
1061
+ colorToRemove.green < 0 || colorToRemove.green > 255 ||
1062
+ colorToRemove.blue < 0 || colorToRemove.blue > 255) {
1063
+ throw new Error("colorsRemover: colorToRemove RGB values must be between 0 and 255.");
1064
+ }
1065
+ return await (0, utils_1.removeColor)(source, colorToRemove);
1066
+ }
1067
+ catch (error) {
1068
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1069
+ throw new Error(`colorsRemover failed: ${errorMessage}`);
1070
+ }
684
1071
  }
685
1072
  async removeBackground(imageURL, apiKey) {
686
- return (0, utils_1.bgRemoval)(imageURL, apiKey);
1073
+ try {
1074
+ if (!imageURL) {
1075
+ throw new Error("removeBackground: imageURL is required.");
1076
+ }
1077
+ if (!apiKey) {
1078
+ throw new Error("removeBackground: apiKey is required.");
1079
+ }
1080
+ return await (0, utils_1.bgRemoval)(imageURL, apiKey);
1081
+ }
1082
+ catch (error) {
1083
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1084
+ throw new Error(`removeBackground failed: ${errorMessage}`);
1085
+ }
1086
+ }
1087
+ /**
1088
+ * Validates blend inputs.
1089
+ * @private
1090
+ * @param layers - Layers to validate
1091
+ * @param baseImageBuffer - Base image buffer to validate
1092
+ */
1093
+ #validateBlendInputs(layers, baseImageBuffer) {
1094
+ if (!baseImageBuffer || !Buffer.isBuffer(baseImageBuffer)) {
1095
+ throw new Error("blend: baseImageBuffer must be a valid Buffer.");
1096
+ }
1097
+ if (!layers || !Array.isArray(layers) || layers.length === 0) {
1098
+ throw new Error("blend: layers array with at least one layer is required.");
1099
+ }
1100
+ for (const layer of layers) {
1101
+ if (!layer.image) {
1102
+ throw new Error("blend: Each layer must have an image property.");
1103
+ }
1104
+ if (!layer.blendMode) {
1105
+ throw new Error("blend: Each layer must have a blendMode property.");
1106
+ }
1107
+ if (layer.opacity !== undefined && (typeof layer.opacity !== 'number' || layer.opacity < 0 || layer.opacity > 1)) {
1108
+ throw new Error("blend: Layer opacity must be a number between 0 and 1.");
1109
+ }
1110
+ }
687
1111
  }
688
1112
  async blend(layers, baseImageBuffer, defaultBlendMode = 'source-over') {
689
1113
  try {
1114
+ this.#validateBlendInputs(layers, baseImageBuffer);
690
1115
  const baseImage = await (0, canvas_1.loadImage)(baseImageBuffer);
691
1116
  const canvas = (0, canvas_1.createCanvas)(baseImage.width, baseImage.height);
692
1117
  const ctx = canvas.getContext('2d');
1118
+ if (!ctx)
1119
+ throw new Error("Unable to get 2D context");
693
1120
  ctx.globalCompositeOperation = defaultBlendMode;
694
1121
  ctx.drawImage(baseImage, 0, 0);
695
1122
  for (const layer of layers) {
@@ -703,323 +1130,921 @@ class ApexPainter {
703
1130
  return canvas.toBuffer('image/png');
704
1131
  }
705
1132
  catch (error) {
706
- console.error('Error creating layered composition:', error);
707
- throw new Error('Failed to create layered composition');
1133
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1134
+ throw new Error(`blend failed: ${errorMessage}`);
1135
+ }
1136
+ }
1137
+ /**
1138
+ * Validates chart inputs.
1139
+ * @private
1140
+ * @param data - Chart data to validate
1141
+ * @param type - Chart type configuration to validate
1142
+ */
1143
+ #validateChartInputs(data, type) {
1144
+ if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
1145
+ throw new Error("createChart: data object with datasets is required.");
1146
+ }
1147
+ if (!type || typeof type !== 'object') {
1148
+ throw new Error("createChart: type configuration object is required.");
1149
+ }
1150
+ if (!type.chartType || typeof type.chartType !== 'string') {
1151
+ throw new Error("createChart: type.chartType must be a string.");
1152
+ }
1153
+ if (typeof type.chartNumber !== 'number' || type.chartNumber < 1) {
1154
+ throw new Error("createChart: type.chartNumber must be a positive number.");
1155
+ }
1156
+ const validChartTypes = ['bar', 'line', 'pie'];
1157
+ if (!validChartTypes.includes(type.chartType.toLowerCase())) {
1158
+ throw new Error(`createChart: Invalid chartType. Supported: ${validChartTypes.join(', ')}`);
708
1159
  }
709
1160
  }
710
1161
  async createChart(data, type) {
711
- if (!data || Object.keys(data).length === 0) {
712
- throw new Error('You need to provide datasets to create charts.');
713
- }
714
- if (!type || !type.chartNumber || !type.chartType) {
715
- throw new Error('Type arguments are missing.');
716
- }
717
- const { chartType, chartNumber } = type;
718
- switch (chartType.toLowerCase()) {
719
- case 'bar':
720
- switch (chartNumber) {
721
- case 1:
722
- return await (0, utils_1.verticalBarChart)(data);
723
- case 2:
724
- throw new Error('Type 2 is still under development.');
725
- default:
726
- throw new Error('Invalid chart number for chart type "bar".');
727
- }
728
- case 'line':
729
- switch (chartNumber) {
730
- case 1:
731
- return await (0, utils_1.lineChart)(data);
732
- case 2:
733
- throw new Error('Type 2 is still under development.');
734
- default:
735
- throw new Error('Invalid chart number for chart type "line".');
736
- }
737
- case 'pie':
738
- switch (chartNumber) {
739
- case 1:
740
- return await (0, utils_1.pieChart)(data);
741
- case 2:
742
- throw new Error('Type 2 is still under development.');
743
- default:
744
- throw new Error('Invalid chart number for chart type "pie".');
745
- }
746
- default:
747
- throw new Error(`Unsupported chart type "${chartType}".`);
1162
+ try {
1163
+ this.#validateChartInputs(data, type);
1164
+ const { chartType, chartNumber } = type;
1165
+ switch (chartType.toLowerCase()) {
1166
+ case 'bar':
1167
+ switch (chartNumber) {
1168
+ case 1:
1169
+ const barResult = await (0, utils_1.verticalBarChart)(data);
1170
+ if (!barResult) {
1171
+ throw new Error("createChart: Failed to generate bar chart.");
1172
+ }
1173
+ return barResult;
1174
+ case 2:
1175
+ throw new Error('Type 2 is still under development.');
1176
+ default:
1177
+ throw new Error('Invalid chart number for chart type "bar".');
1178
+ }
1179
+ case 'line':
1180
+ switch (chartNumber) {
1181
+ case 1:
1182
+ // LineChart expects DataPoint[][] where DataPoint has { label: string; y: number }
1183
+ // Type assertion needed because there are two different DataPoint interfaces
1184
+ return await (0, utils_1.lineChart)(data);
1185
+ case 2:
1186
+ throw new Error('Type 2 is still under development.');
1187
+ default:
1188
+ throw new Error('Invalid chart number for chart type "line".');
1189
+ }
1190
+ case 'pie':
1191
+ switch (chartNumber) {
1192
+ case 1:
1193
+ return await (0, utils_1.pieChart)(data);
1194
+ case 2:
1195
+ throw new Error('Type 2 is still under development.');
1196
+ default:
1197
+ throw new Error('Invalid chart number for chart type "pie".');
1198
+ }
1199
+ default:
1200
+ throw new Error(`Unsupported chart type "${chartType}".`);
1201
+ }
1202
+ }
1203
+ catch (error) {
1204
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1205
+ throw new Error(`createChart failed: ${errorMessage}`);
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Validates crop options.
1210
+ * @private
1211
+ * @param options - Crop options to validate
1212
+ */
1213
+ #validateCropOptions(options) {
1214
+ if (!options) {
1215
+ throw new Error("cropImage: options object is required.");
1216
+ }
1217
+ if (!options.imageSource) {
1218
+ throw new Error("cropImage: imageSource is required.");
1219
+ }
1220
+ if (!options.coordinates || !Array.isArray(options.coordinates) || options.coordinates.length < 3) {
1221
+ throw new Error("cropImage: coordinates array with at least 3 points is required.");
1222
+ }
1223
+ if (options.crop !== 'inner' && options.crop !== 'outer') {
1224
+ throw new Error("cropImage: crop must be either 'inner' or 'outer'.");
748
1225
  }
749
1226
  }
750
1227
  async cropImage(options) {
751
1228
  try {
752
- if (!options.imageSource)
753
- throw new Error('The "imageSource" option is needed. Please provide the path to the image to crop.');
754
- if (!options.coordinates || options.coordinates.length < 3)
755
- throw new Error('The "coordinates" option is needed. Please provide coordinates to crop the image.');
1229
+ this.#validateCropOptions(options);
756
1230
  if (options.crop === 'outer') {
757
1231
  return await (0, utils_1.cropOuter)(options);
758
1232
  }
759
- else if (options.crop === 'inner') {
1233
+ else {
760
1234
  return await (0, utils_1.cropInner)(options);
761
1235
  }
1236
+ }
1237
+ catch (error) {
1238
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1239
+ throw new Error(`cropImage failed: ${errorMessage}`);
1240
+ }
1241
+ }
1242
+ /**
1243
+ * Extracts a single frame from a video
1244
+ * @private
1245
+ * @param videoSource - Video source (path, URL, or Buffer)
1246
+ * @param frameNumber - Frame number to extract (default: 0)
1247
+ * @returns Buffer containing the frame image
1248
+ */
1249
+ async #extractVideoFrame(videoSource, frameNumber = 0) {
1250
+ try {
1251
+ const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
1252
+ if (!fs_1.default.existsSync(frameDir)) {
1253
+ fs_1.default.mkdirSync(frameDir, { recursive: true });
1254
+ }
1255
+ const tempVideoPath = path_1.default.join(frameDir, `temp-video-${Date.now()}.mp4`);
1256
+ const frameOutputPath = path_1.default.join(frameDir, `frame-${Date.now()}.jpg`);
1257
+ // Handle video source
1258
+ if (Buffer.isBuffer(videoSource)) {
1259
+ fs_1.default.writeFileSync(tempVideoPath, videoSource);
1260
+ }
1261
+ else if (typeof videoSource === 'string' && videoSource.startsWith('http')) {
1262
+ const response = await (0, axios_1.default)({
1263
+ method: 'get',
1264
+ url: videoSource,
1265
+ responseType: 'arraybuffer'
1266
+ });
1267
+ fs_1.default.writeFileSync(tempVideoPath, Buffer.from(response.data));
1268
+ }
762
1269
  else {
763
- throw new Error('Invalid crop option. Please specify "inner" or "outer".');
1270
+ // Local file path
1271
+ if (!fs_1.default.existsSync(videoSource)) {
1272
+ throw new Error(`Video file not found: ${videoSource}`);
1273
+ }
1274
+ // Use the existing path
1275
+ return await new Promise((resolve, reject) => {
1276
+ (0, fluent_ffmpeg_1.default)(videoSource)
1277
+ .seekInput(frameNumber / 1000) // Convert frame to seconds (approximate)
1278
+ .frames(1)
1279
+ .output(frameOutputPath)
1280
+ .on('end', () => {
1281
+ try {
1282
+ const buffer = fs_1.default.readFileSync(frameOutputPath);
1283
+ // Cleanup
1284
+ if (fs_1.default.existsSync(frameOutputPath))
1285
+ fs_1.default.unlinkSync(frameOutputPath);
1286
+ resolve(buffer);
1287
+ }
1288
+ catch (e) {
1289
+ resolve(null);
1290
+ }
1291
+ })
1292
+ .on('error', (err) => {
1293
+ reject(err);
1294
+ })
1295
+ .run();
1296
+ });
764
1297
  }
1298
+ // Extract frame from temp video
1299
+ return await new Promise((resolve, reject) => {
1300
+ (0, fluent_ffmpeg_1.default)(tempVideoPath)
1301
+ .seekInput(frameNumber / 1000)
1302
+ .frames(1)
1303
+ .output(frameOutputPath)
1304
+ .on('end', () => {
1305
+ try {
1306
+ const buffer = fs_1.default.readFileSync(frameOutputPath);
1307
+ // Cleanup
1308
+ if (fs_1.default.existsSync(tempVideoPath))
1309
+ fs_1.default.unlinkSync(tempVideoPath);
1310
+ if (fs_1.default.existsSync(frameOutputPath))
1311
+ fs_1.default.unlinkSync(frameOutputPath);
1312
+ resolve(buffer);
1313
+ }
1314
+ catch (e) {
1315
+ resolve(null);
1316
+ }
1317
+ })
1318
+ .on('error', (err) => {
1319
+ // Cleanup on error
1320
+ if (fs_1.default.existsSync(tempVideoPath))
1321
+ fs_1.default.unlinkSync(tempVideoPath);
1322
+ if (fs_1.default.existsSync(frameOutputPath))
1323
+ fs_1.default.unlinkSync(frameOutputPath);
1324
+ reject(err);
1325
+ })
1326
+ .run();
1327
+ });
765
1328
  }
766
1329
  catch (error) {
767
- console.error('An error occurred:', error);
768
- throw error;
1330
+ console.error('Error extracting video frame:', error);
1331
+ return null;
769
1332
  }
770
1333
  }
771
- async drawImage(ctx, image) {
1334
+ /**
1335
+ * Validates extract frames inputs.
1336
+ * @private
1337
+ * @param videoSource - Video source to validate
1338
+ * @param options - Extract frames options to validate
1339
+ */
1340
+ #validateExtractFramesInputs(videoSource, options) {
1341
+ if (!videoSource) {
1342
+ throw new Error("extractFrames: videoSource is required.");
1343
+ }
1344
+ if (!options || typeof options !== 'object') {
1345
+ throw new Error("extractFrames: options object is required.");
1346
+ }
1347
+ if (typeof options.interval !== 'number' || options.interval <= 0) {
1348
+ throw new Error("extractFrames: options.interval must be a positive number (milliseconds).");
1349
+ }
1350
+ if (options.outputFormat && !['jpg', 'png'].includes(options.outputFormat)) {
1351
+ throw new Error("extractFrames: outputFormat must be 'jpg' or 'png'.");
1352
+ }
772
1353
  }
773
1354
  async extractFrames(videoSource, options) {
774
- const frames = [];
775
- const frameDir = path_1.default.join(__dirname, 'frames');
776
- if (!fs_1.default.existsSync(frameDir)) {
777
- fs_1.default.mkdirSync(frameDir);
778
- }
779
- const videoPath = typeof videoSource === 'string' ? videoSource : path_1.default.join(frameDir, 'temp-video.mp4');
780
- if (Buffer.isBuffer(videoSource)) {
781
- fs_1.default.writeFileSync(videoPath, videoSource);
782
- }
783
- else if (videoSource.startsWith('http')) {
784
- await (0, axios_1.default)({
785
- method: 'get',
786
- url: videoSource,
787
- responseType: 'arraybuffer'
788
- })
789
- .then((response) => {
790
- fs_1.default.writeFileSync(videoPath, response.data);
791
- })
792
- .catch(err => {
793
- throw new Error(`Error downloading video: ${err.message}`);
1355
+ try {
1356
+ this.#validateExtractFramesInputs(videoSource, options);
1357
+ const frames = [];
1358
+ const frameDir = path_1.default.join(__dirname, 'frames');
1359
+ if (!fs_1.default.existsSync(frameDir)) {
1360
+ fs_1.default.mkdirSync(frameDir);
1361
+ }
1362
+ const videoPath = typeof videoSource === 'string' ? videoSource : path_1.default.join(frameDir, 'temp-video.mp4');
1363
+ if (Buffer.isBuffer(videoSource)) {
1364
+ fs_1.default.writeFileSync(videoPath, videoSource);
1365
+ }
1366
+ else if (videoSource.startsWith('http')) {
1367
+ await (0, axios_1.default)({
1368
+ method: 'get',
1369
+ url: videoSource,
1370
+ responseType: 'arraybuffer'
1371
+ })
1372
+ .then((response) => {
1373
+ fs_1.default.writeFileSync(videoPath, response.data);
1374
+ })
1375
+ .catch(err => {
1376
+ throw new Error(`Error downloading video: ${err.message}`);
1377
+ });
1378
+ }
1379
+ else if (!fs_1.default.existsSync(videoPath)) {
1380
+ throw new Error("Video file not found at specified path.");
1381
+ }
1382
+ function processVideoExtraction(videoPath, frames, options, resolve, reject) {
1383
+ const outputFormat = options.outputFormat || 'jpg';
1384
+ const outputFileTemplate = `frame-%03d.${outputFormat}`;
1385
+ (0, fluent_ffmpeg_1.default)(videoPath)
1386
+ .on('end', () => {
1387
+ console.log('Frames extracted successfully.');
1388
+ resolve(frames);
1389
+ })
1390
+ .on('error', (err) => {
1391
+ console.error('Error extracting frames:', err.message);
1392
+ reject(err);
1393
+ })
1394
+ .outputOptions([`-vf fps=1/${options.interval / 1000}`, `-q:v 2`])
1395
+ .saveToFile(path_1.default.join(frameDir, outputFileTemplate));
1396
+ fluent_ffmpeg_1.default.ffprobe(videoPath, (err, metadata) => {
1397
+ if (err) {
1398
+ return reject(err);
1399
+ }
1400
+ const duration = metadata?.format?.duration;
1401
+ if (typeof duration !== "number") {
1402
+ return reject(new Error("Video duration not found in metadata."));
1403
+ }
1404
+ const totalFrames = Math.floor(duration * 1000 / options.interval);
1405
+ for (let i = 0; i < totalFrames; i++) {
1406
+ if (options.frameSelection && (i < (options.frameSelection.start || 0) || i > (options.frameSelection.end || totalFrames - 1))) {
1407
+ continue;
1408
+ }
1409
+ frames.push({
1410
+ source: path_1.default.join(frameDir, `frame-${String(i).padStart(3, '0')}.${outputFormat}`),
1411
+ isRemote: false
1412
+ });
1413
+ }
1414
+ });
1415
+ }
1416
+ return new Promise((resolve, reject) => {
1417
+ processVideoExtraction(videoPath, frames, options, resolve, reject);
794
1418
  });
795
1419
  }
796
- else if (!fs_1.default.existsSync(videoPath)) {
797
- throw new Error("Video file not found at specified path.");
798
- }
799
- function processVideoExtraction(videoPath, frames, options, resolve, reject) {
800
- const outputFormat = options.outputFormat || 'jpg';
801
- const outputFileTemplate = `frame-%03d.${outputFormat}`;
802
- (0, fluent_ffmpeg_1.default)(videoPath)
803
- .on('end', () => {
804
- console.log('Frames extracted successfully.');
805
- resolve(frames);
806
- })
807
- .on('error', (err) => {
808
- console.error('Error extracting frames:', err.message);
809
- reject(err);
810
- })
811
- .outputOptions([`-vf fps=1/${options.interval / 1000}`, `-q:v 2`])
812
- .saveToFile(path_1.default.join(frameDir, outputFileTemplate));
813
- fluent_ffmpeg_1.default.ffprobe(videoPath, (err, metadata) => {
814
- if (err) {
815
- return reject(err);
1420
+ catch (error) {
1421
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1422
+ throw new Error(`extractFrames failed: ${errorMessage}`);
1423
+ }
1424
+ }
1425
+ /**
1426
+ * Validates masking inputs.
1427
+ * @private
1428
+ * @param source - Source image to validate
1429
+ * @param maskSource - Mask image to validate
1430
+ * @param options - Mask options to validate
1431
+ */
1432
+ #validateMaskingInputs(source, maskSource, options) {
1433
+ if (!source) {
1434
+ throw new Error("masking: source is required.");
1435
+ }
1436
+ if (!maskSource) {
1437
+ throw new Error("masking: maskSource is required.");
1438
+ }
1439
+ if (options.type && !['alpha', 'grayscale', 'color'].includes(options.type)) {
1440
+ throw new Error("masking: type must be 'alpha', 'grayscale', or 'color'.");
1441
+ }
1442
+ if (options.type === 'color' && !options.colorKey) {
1443
+ throw new Error("masking: colorKey is required when type is 'color'.");
1444
+ }
1445
+ if (options.threshold !== undefined && (typeof options.threshold !== 'number' || options.threshold < 0 || options.threshold > 255)) {
1446
+ throw new Error("masking: threshold must be a number between 0 and 255.");
1447
+ }
1448
+ }
1449
+ async masking(source, maskSource, options = { type: "alpha" }) {
1450
+ try {
1451
+ this.#validateMaskingInputs(source, maskSource, options);
1452
+ const img = await (0, canvas_1.loadImage)(source);
1453
+ const mask = await (0, canvas_1.loadImage)(maskSource);
1454
+ const canvas = (0, canvas_1.createCanvas)(img.width, img.height);
1455
+ const ctx = canvas.getContext("2d");
1456
+ ctx.drawImage(img, 0, 0, img.width, img.height);
1457
+ const maskCanvas = (0, canvas_1.createCanvas)(img.width, img.height);
1458
+ const maskCtx = maskCanvas.getContext("2d");
1459
+ maskCtx.drawImage(mask, 0, 0, img.width, img.height);
1460
+ const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
1461
+ const imgData = ctx.getImageData(0, 0, img.width, img.height);
1462
+ for (let i = 0; i < maskData.data.length; i += 4) {
1463
+ let alphaValue = 255;
1464
+ if (options.type === "grayscale") {
1465
+ const grayscale = maskData.data[i] * 0.3 + maskData.data[i + 1] * 0.59 + maskData.data[i + 2] * 0.11;
1466
+ alphaValue = grayscale >= (options.threshold ?? 128) ? 255 : 0;
816
1467
  }
817
- const duration = metadata?.format?.duration;
818
- if (typeof duration !== "number") {
819
- return reject(new Error("Video duration not found in metadata."));
1468
+ else if (options.type === "alpha") {
1469
+ alphaValue = maskData.data[i + 3];
820
1470
  }
821
- const totalFrames = Math.floor(duration * 1000 / options.interval);
822
- for (let i = 0; i < totalFrames; i++) {
823
- if (options.frameSelection && (i < (options.frameSelection.start || 0) || i > (options.frameSelection.end || totalFrames - 1))) {
824
- continue;
825
- }
826
- frames.push({
827
- source: path_1.default.join(frameDir, `frame-${String(i).padStart(3, '0')}.${outputFormat}`),
828
- isRemote: false
829
- });
1471
+ else if (options.type === "color" && options.colorKey) {
1472
+ const colorMatch = maskData.data[i] === parseInt(options.colorKey.slice(1, 3), 16) &&
1473
+ maskData.data[i + 1] === parseInt(options.colorKey.slice(3, 5), 16) &&
1474
+ maskData.data[i + 2] === parseInt(options.colorKey.slice(5, 7), 16);
1475
+ alphaValue = colorMatch ? 0 : 255;
830
1476
  }
831
- });
1477
+ if (options.invert)
1478
+ alphaValue = 255 - alphaValue;
1479
+ imgData.data[i + 3] = alphaValue;
1480
+ }
1481
+ ctx.putImageData(imgData, 0, 0);
1482
+ return canvas.toBuffer("image/png");
1483
+ }
1484
+ catch (error) {
1485
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1486
+ throw new Error(`masking failed: ${errorMessage}`);
832
1487
  }
833
- return new Promise((resolve, reject) => {
834
- processVideoExtraction(videoPath, frames, options, resolve, reject);
835
- });
836
1488
  }
837
- async masking(source, maskSource, options = { type: "alpha" }) {
838
- const img = await (0, canvas_1.loadImage)(source);
839
- const mask = await (0, canvas_1.loadImage)(maskSource);
840
- const canvas = (0, canvas_1.createCanvas)(img.width, img.height);
841
- const ctx = canvas.getContext("2d");
842
- ctx.drawImage(img, 0, 0, img.width, img.height);
843
- const maskCanvas = (0, canvas_1.createCanvas)(img.width, img.height);
844
- const maskCtx = maskCanvas.getContext("2d");
845
- maskCtx.drawImage(mask, 0, 0, img.width, img.height);
846
- const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
847
- const imgData = ctx.getImageData(0, 0, img.width, img.height);
848
- for (let i = 0; i < maskData.data.length; i += 4) {
849
- let alphaValue = 255;
850
- if (options.type === "grayscale") {
851
- const grayscale = maskData.data[i] * 0.3 + maskData.data[i + 1] * 0.59 + maskData.data[i + 2] * 0.11;
852
- alphaValue = grayscale >= (options.threshold ?? 128) ? 255 : 0;
853
- }
854
- else if (options.type === "alpha") {
855
- alphaValue = maskData.data[i + 3];
856
- }
857
- else if (options.type === "color" && options.colorKey) {
858
- const colorMatch = maskData.data[i] === parseInt(options.colorKey.slice(1, 3), 16) &&
859
- maskData.data[i + 1] === parseInt(options.colorKey.slice(3, 5), 16) &&
860
- maskData.data[i + 2] === parseInt(options.colorKey.slice(5, 7), 16);
861
- alphaValue = colorMatch ? 0 : 255;
862
- }
863
- if (options.invert)
864
- alphaValue = 255 - alphaValue;
865
- imgData.data[i + 3] = alphaValue;
866
- }
867
- ctx.putImageData(imgData, 0, 0);
868
- return canvas.toBuffer("image/png");
1489
+ /**
1490
+ * Validates gradient blend inputs.
1491
+ * @private
1492
+ * @param source - Source image to validate
1493
+ * @param options - Blend options to validate
1494
+ */
1495
+ #validateGradientBlendInputs(source, options) {
1496
+ if (!source) {
1497
+ throw new Error("gradientBlend: source is required.");
1498
+ }
1499
+ if (!options || typeof options !== 'object') {
1500
+ throw new Error("gradientBlend: options object is required.");
1501
+ }
1502
+ if (!options.colors || !Array.isArray(options.colors) || options.colors.length === 0) {
1503
+ throw new Error("gradientBlend: options.colors array with at least one color stop is required.");
1504
+ }
1505
+ if (options.type && !['linear', 'radial', 'conic'].includes(options.type)) {
1506
+ throw new Error("gradientBlend: type must be 'linear', 'radial', or 'conic'.");
1507
+ }
1508
+ for (const colorStop of options.colors) {
1509
+ if (typeof colorStop.stop !== 'number' || colorStop.stop < 0 || colorStop.stop > 1) {
1510
+ throw new Error("gradientBlend: Each color stop must have a stop value between 0 and 1.");
1511
+ }
1512
+ if (!colorStop.color || typeof colorStop.color !== 'string') {
1513
+ throw new Error("gradientBlend: Each color stop must have a valid color string.");
1514
+ }
1515
+ }
869
1516
  }
870
1517
  async gradientBlend(source, options) {
871
- const img = await (0, canvas_1.loadImage)(source);
872
- const canvas = (0, canvas_1.createCanvas)(img.width, img.height);
873
- const ctx = canvas.getContext("2d");
874
- ctx.drawImage(img, 0, 0, img.width, img.height);
875
- let gradient;
876
- if (options.type === "linear") {
877
- const angle = options.angle ?? 0;
878
- const radians = (angle * Math.PI) / 180;
879
- const x1 = img.width / 2 - (Math.cos(radians) * img.width) / 2;
880
- const y1 = img.height / 2 - (Math.sin(radians) * img.height) / 2;
881
- const x2 = img.width / 2 + (Math.cos(radians) * img.width) / 2;
882
- const y2 = img.height / 2 + (Math.sin(radians) * img.height) / 2;
883
- gradient = ctx.createLinearGradient(x1, y1, x2, y2);
884
- }
885
- else if (options.type === "radial") {
886
- gradient = ctx.createRadialGradient(img.width / 2, img.height / 2, 0, img.width / 2, img.height / 2, Math.max(img.width, img.height));
1518
+ try {
1519
+ this.#validateGradientBlendInputs(source, options);
1520
+ const img = await (0, canvas_1.loadImage)(source);
1521
+ const canvas = (0, canvas_1.createCanvas)(img.width, img.height);
1522
+ const ctx = canvas.getContext("2d");
1523
+ if (!ctx)
1524
+ throw new Error("Unable to get 2D context");
1525
+ ctx.drawImage(img, 0, 0, img.width, img.height);
1526
+ let gradient;
1527
+ if (options.type === "linear") {
1528
+ const angle = options.angle ?? 0;
1529
+ const radians = (angle * Math.PI) / 180;
1530
+ const x1 = img.width / 2 - (Math.cos(radians) * img.width) / 2;
1531
+ const y1 = img.height / 2 - (Math.sin(radians) * img.height) / 2;
1532
+ const x2 = img.width / 2 + (Math.cos(radians) * img.width) / 2;
1533
+ const y2 = img.height / 2 + (Math.sin(radians) * img.height) / 2;
1534
+ gradient = ctx.createLinearGradient(x1, y1, x2, y2);
1535
+ }
1536
+ else if (options.type === "radial") {
1537
+ gradient = ctx.createRadialGradient(img.width / 2, img.height / 2, 0, img.width / 2, img.height / 2, Math.max(img.width, img.height));
1538
+ }
1539
+ else {
1540
+ gradient = ctx.createConicGradient(Math.PI, img.width / 2, img.height / 2);
1541
+ }
1542
+ options.colors.forEach(({ stop, color }) => gradient.addColorStop(stop, color));
1543
+ ctx.fillStyle = gradient;
1544
+ ctx.globalCompositeOperation = options.blendMode ?? "multiply";
1545
+ ctx.fillRect(0, 0, img.width, img.height);
1546
+ if (options.maskSource) {
1547
+ const mask = await (0, canvas_1.loadImage)(options.maskSource);
1548
+ ctx.globalCompositeOperation = "destination-in";
1549
+ ctx.drawImage(mask, 0, 0, img.width, img.height);
1550
+ }
1551
+ ctx.globalCompositeOperation = "source-over";
1552
+ return canvas.toBuffer("image/png");
1553
+ }
1554
+ catch (error) {
1555
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1556
+ throw new Error(`gradientBlend failed: ${errorMessage}`);
1557
+ }
1558
+ }
1559
+ /**
1560
+ * Validates animate inputs.
1561
+ * @private
1562
+ * @param frames - Animation frames to validate
1563
+ * @param defaultDuration - Default duration to validate
1564
+ * @param defaultWidth - Default width to validate
1565
+ * @param defaultHeight - Default height to validate
1566
+ * @param options - Animation options to validate
1567
+ */
1568
+ #validateAnimateInputs(frames, defaultDuration, defaultWidth, defaultHeight, options) {
1569
+ if (!frames || !Array.isArray(frames) || frames.length === 0) {
1570
+ throw new Error("animate: frames array with at least one frame is required.");
1571
+ }
1572
+ if (typeof defaultDuration !== 'number' || defaultDuration < 0) {
1573
+ throw new Error("animate: defaultDuration must be a non-negative number.");
1574
+ }
1575
+ if (typeof defaultWidth !== 'number' || defaultWidth <= 0) {
1576
+ throw new Error("animate: defaultWidth must be a positive number.");
1577
+ }
1578
+ if (typeof defaultHeight !== 'number' || defaultHeight <= 0) {
1579
+ throw new Error("animate: defaultHeight must be a positive number.");
1580
+ }
1581
+ if (options?.gif && !options.gifPath) {
1582
+ throw new Error("animate: gifPath is required when gif is enabled.");
887
1583
  }
888
- else {
889
- gradient = ctx.createConicGradient(Math.PI, img.width / 2, img.height / 2);
890
- }
891
- options.colors.forEach(({ stop, color }) => gradient.addColorStop(stop, color));
892
- ctx.fillStyle = gradient;
893
- ctx.globalCompositeOperation = options.blendMode ?? "multiply";
894
- ctx.fillRect(0, 0, img.width, img.height);
895
- if (options.maskSource) {
896
- const mask = await (0, canvas_1.loadImage)(options.maskSource);
897
- ctx.globalCompositeOperation = "destination-in";
898
- ctx.drawImage(mask, 0, 0, img.width, img.height);
899
- }
900
- ctx.globalCompositeOperation = "source-over";
901
- return canvas.toBuffer("image/png");
902
1584
  }
903
1585
  async animate(frames, defaultDuration, defaultWidth = 800, defaultHeight = 600, options) {
904
- const buffers = [];
905
- const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
906
- if (options?.onStart)
907
- options.onStart();
908
- let encoder = null;
909
- let gifStream = null;
910
- if (options?.gif) {
911
- if (!options.gifPath) {
912
- throw new Error("GIF generation enabled but no gifPath provided.");
913
- }
914
- encoder = new gifencoder_1.default(defaultWidth, defaultHeight);
915
- gifStream = fs_1.default.createWriteStream(options.gifPath);
916
- encoder.createReadStream().pipe(gifStream);
917
- encoder.start();
918
- encoder.setRepeat(0);
919
- encoder.setQuality(10);
920
- }
921
- for (let i = 0; i < frames.length; i++) {
922
- const frame = frames[i];
923
- const width = frame.width || defaultWidth;
924
- const height = frame.height || defaultHeight;
925
- const canvas = (0, canvas_1.createCanvas)(width, height);
926
- const ctx = canvas.getContext('2d');
927
- if (!isNode) {
928
- canvas.width = width;
929
- canvas.height = height;
930
- document.body.appendChild(canvas);
931
- }
932
- ctx.clearRect(0, 0, width, height);
933
- if (frame.transformations) {
934
- const { scaleX = 1, scaleY = 1, rotate = 0, translateX = 0, translateY = 0 } = frame.transformations;
935
- ctx.save();
936
- ctx.translate(translateX, translateY);
937
- ctx.rotate((rotate * Math.PI) / 180);
938
- ctx.scale(scaleX, scaleY);
939
- }
940
- let fillStyle = null;
941
- if (frame.gradient) {
942
- const { type, startX, startY, endX, endY, startRadius, endRadius, colors } = frame.gradient;
943
- let gradient = null;
944
- if (type === 'linear') {
945
- gradient = ctx.createLinearGradient(startX || 0, startY || 0, endX || width, endY || height);
946
- }
947
- else if (type === 'radial') {
948
- gradient = ctx.createRadialGradient(startX || width / 2, startY || height / 2, startRadius || 0, endX || width / 2, endY || height / 2, endRadius || Math.max(width, height));
1586
+ try {
1587
+ this.#validateAnimateInputs(frames, defaultDuration, defaultWidth, defaultHeight, options);
1588
+ const buffers = [];
1589
+ const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
1590
+ if (options?.onStart)
1591
+ options.onStart();
1592
+ let encoder = null;
1593
+ let gifStream = null;
1594
+ if (options?.gif) {
1595
+ if (!options.gifPath) {
1596
+ throw new Error("animate: gifPath is required when gif is enabled.");
949
1597
  }
950
- colors.forEach((colorStop) => {
951
- if (gradient)
952
- gradient.addColorStop(colorStop.stop, colorStop.color);
953
- });
954
- fillStyle = gradient;
1598
+ encoder = new gifencoder_1.default(defaultWidth, defaultHeight);
1599
+ gifStream = fs_1.default.createWriteStream(options.gifPath);
1600
+ encoder.createReadStream().pipe(gifStream);
1601
+ encoder.start();
1602
+ encoder.setRepeat(0);
1603
+ encoder.setQuality(10);
955
1604
  }
956
- if (frame.pattern) {
957
- const patternImage = await (0, canvas_1.loadImage)(frame.pattern.source);
958
- const pattern = ctx.createPattern(patternImage, frame.pattern.repeat || 'repeat');
959
- fillStyle = pattern;
1605
+ for (let i = 0; i < frames.length; i++) {
1606
+ const frame = frames[i];
1607
+ const width = frame.width || defaultWidth;
1608
+ const height = frame.height || defaultHeight;
1609
+ const canvas = (0, canvas_1.createCanvas)(width, height);
1610
+ const ctx = canvas.getContext('2d');
1611
+ if (!isNode) {
1612
+ canvas.width = width;
1613
+ canvas.height = height;
1614
+ document.body.appendChild(canvas);
1615
+ }
1616
+ ctx.clearRect(0, 0, width, height);
1617
+ if (frame.transformations) {
1618
+ const { scaleX = 1, scaleY = 1, rotate = 0, translateX = 0, translateY = 0 } = frame.transformations;
1619
+ ctx.save();
1620
+ ctx.translate(translateX, translateY);
1621
+ ctx.rotate((rotate * Math.PI) / 180);
1622
+ ctx.scale(scaleX, scaleY);
1623
+ }
1624
+ let fillStyle = null;
1625
+ if (frame.gradient) {
1626
+ const { type, startX, startY, endX, endY, startRadius, endRadius, colors } = frame.gradient;
1627
+ let gradient = null;
1628
+ if (type === 'linear') {
1629
+ gradient = ctx.createLinearGradient(startX || 0, startY || 0, endX || width, endY || height);
1630
+ }
1631
+ else if (type === 'radial') {
1632
+ gradient = ctx.createRadialGradient(startX || width / 2, startY || height / 2, startRadius || 0, endX || width / 2, endY || height / 2, endRadius || Math.max(width, height));
1633
+ }
1634
+ colors.forEach((colorStop) => {
1635
+ if (gradient)
1636
+ gradient.addColorStop(colorStop.stop, colorStop.color);
1637
+ });
1638
+ fillStyle = gradient;
1639
+ }
1640
+ if (frame.pattern) {
1641
+ const patternImage = await (0, canvas_1.loadImage)(frame.pattern.source);
1642
+ const pattern = ctx.createPattern(patternImage, frame.pattern.repeat || 'repeat');
1643
+ fillStyle = pattern;
1644
+ }
1645
+ if (!fillStyle && frame.backgroundColor) {
1646
+ fillStyle = frame.backgroundColor;
1647
+ }
1648
+ if (fillStyle) {
1649
+ ctx.fillStyle = fillStyle;
1650
+ ctx.fillRect(0, 0, width, height);
1651
+ }
1652
+ if (frame.source) {
1653
+ const image = await (0, canvas_1.loadImage)(frame.source);
1654
+ ctx.globalCompositeOperation = frame.blendMode || 'source-over';
1655
+ ctx.drawImage(image, 0, 0, width, height);
1656
+ }
1657
+ if (frame.onDrawCustom) {
1658
+ frame.onDrawCustom(ctx, canvas);
1659
+ }
1660
+ if (frame.transformations) {
1661
+ ctx.restore();
1662
+ }
1663
+ const buffer = canvas.toBuffer('image/png');
1664
+ buffers.push(buffer);
1665
+ if (encoder) {
1666
+ const frameDuration = frame.duration || defaultDuration;
1667
+ encoder.setDelay(frameDuration);
1668
+ encoder.addFrame(ctx);
1669
+ }
1670
+ if (options?.onFrame)
1671
+ options.onFrame(i);
1672
+ await new Promise(resolve => setTimeout(resolve, frame.duration || defaultDuration));
960
1673
  }
961
- if (!fillStyle && frame.backgroundColor) {
962
- fillStyle = frame.backgroundColor;
1674
+ if (encoder) {
1675
+ encoder.finish();
963
1676
  }
964
- if (fillStyle) {
965
- ctx.fillStyle = fillStyle;
966
- ctx.fillRect(0, 0, width, height);
1677
+ if (options?.onEnd)
1678
+ options.onEnd();
1679
+ return options?.gif ? undefined : buffers;
1680
+ }
1681
+ catch (error) {
1682
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1683
+ throw new Error(`animate failed: ${errorMessage}`);
1684
+ }
1685
+ }
1686
+ /**
1687
+ * Processes multiple operations in parallel
1688
+ * @param operations - Array of operations to process
1689
+ * @returns Array of result buffers
1690
+ */
1691
+ async batch(operations) {
1692
+ try {
1693
+ return await (0, utils_1.batchOperations)(this, operations);
1694
+ }
1695
+ catch (error) {
1696
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1697
+ throw new Error(`batch failed: ${errorMessage}`);
1698
+ }
1699
+ }
1700
+ /**
1701
+ * Chains multiple operations sequentially
1702
+ * @param operations - Array of operations to chain
1703
+ * @returns Final result buffer
1704
+ */
1705
+ async chain(operations) {
1706
+ try {
1707
+ return await (0, utils_1.chainOperations)(this, operations);
1708
+ }
1709
+ catch (error) {
1710
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1711
+ throw new Error(`chain failed: ${errorMessage}`);
1712
+ }
1713
+ }
1714
+ /**
1715
+ * Stitches multiple images together
1716
+ * @param images - Array of image sources
1717
+ * @param options - Stitching options
1718
+ * @returns Stitched image buffer
1719
+ */
1720
+ async stitchImages(images, options) {
1721
+ try {
1722
+ if (!images || images.length === 0) {
1723
+ throw new Error("stitchImages: images array is required");
967
1724
  }
968
- if (frame.source) {
969
- const image = await (0, canvas_1.loadImage)(frame.source);
970
- ctx.globalCompositeOperation = frame.blendMode || 'source-over';
971
- ctx.drawImage(image, 0, 0, width, height);
1725
+ return await (0, utils_1.stitchImages)(images, options);
1726
+ }
1727
+ catch (error) {
1728
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1729
+ throw new Error(`stitchImages failed: ${errorMessage}`);
1730
+ }
1731
+ }
1732
+ /**
1733
+ * Creates an image collage
1734
+ * @param images - Array of image sources with optional dimensions
1735
+ * @param layout - Collage layout configuration
1736
+ * @returns Collage image buffer
1737
+ */
1738
+ async createCollage(images, layout) {
1739
+ try {
1740
+ if (!images || images.length === 0) {
1741
+ throw new Error("createCollage: images array is required");
972
1742
  }
973
- if (frame.onDrawCustom) {
974
- frame.onDrawCustom(ctx, canvas);
1743
+ if (!layout) {
1744
+ throw new Error("createCollage: layout configuration is required");
975
1745
  }
976
- if (frame.transformations) {
977
- ctx.restore();
1746
+ return await (0, utils_1.createCollage)(images, layout);
1747
+ }
1748
+ catch (error) {
1749
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1750
+ throw new Error(`createCollage failed: ${errorMessage}`);
1751
+ }
1752
+ }
1753
+ /**
1754
+ * Compresses an image with quality control
1755
+ * @param image - Image source (path, URL, or Buffer)
1756
+ * @param options - Compression options
1757
+ * @returns Compressed image buffer
1758
+ */
1759
+ async compress(image, options) {
1760
+ try {
1761
+ if (!image) {
1762
+ throw new Error("compress: image is required");
978
1763
  }
979
- const buffer = canvas.toBuffer('image/png');
980
- buffers.push(buffer);
981
- if (encoder) {
982
- const frameDuration = frame.duration || defaultDuration;
983
- encoder.setDelay(frameDuration);
984
- encoder.addFrame(ctx);
1764
+ return await (0, utils_1.compressImage)(image, options);
1765
+ }
1766
+ catch (error) {
1767
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1768
+ throw new Error(`compress failed: ${errorMessage}`);
1769
+ }
1770
+ }
1771
+ /**
1772
+ * Extracts color palette from an image
1773
+ * @param image - Image source (path, URL, or Buffer)
1774
+ * @param options - Palette extraction options
1775
+ * @returns Array of colors with percentages
1776
+ */
1777
+ async extractPalette(image, options) {
1778
+ try {
1779
+ if (!image) {
1780
+ throw new Error("extractPalette: image is required");
985
1781
  }
986
- if (options?.onFrame)
987
- options.onFrame(i);
988
- await new Promise(resolve => setTimeout(resolve, frame.duration || defaultDuration));
1782
+ return await (0, utils_1.extractPalette)(image, options);
989
1783
  }
990
- if (encoder) {
991
- encoder.finish();
1784
+ catch (error) {
1785
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1786
+ throw new Error(`extractPalette failed: ${errorMessage}`);
992
1787
  }
993
- if (options?.onEnd)
994
- options.onEnd();
995
- return options?.gif ? undefined : buffers;
996
1788
  }
1789
+ /**
1790
+ * Validates a hexadecimal color string.
1791
+ * @param hexColor - Hexadecimal color string to validate (format: #RRGGBB)
1792
+ * @returns True if the color is valid
1793
+ * @throws Error if the color format is invalid
1794
+ *
1795
+ * @example
1796
+ * ```typescript
1797
+ * painter.validHex('#ff0000'); // true
1798
+ * painter.validHex('#FF00FF'); // true
1799
+ * painter.validHex('invalid'); // throws Error
1800
+ * ```
1801
+ */
997
1802
  validHex(hexColor) {
1803
+ if (typeof hexColor !== 'string') {
1804
+ throw new Error("validHex: hexColor must be a string.");
1805
+ }
998
1806
  const hexPattern = /^#[0-9a-fA-F]{6}$/;
999
1807
  if (!hexPattern.test(hexColor)) {
1000
- throw new Error("Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
1808
+ throw new Error("validHex: Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
1001
1809
  }
1002
1810
  return true;
1003
1811
  }
1812
+ /**
1813
+ * Converts results to the configured output format.
1814
+ * @param results - Buffer or result to convert
1815
+ * @returns Converted result in the configured format
1816
+ * @throws Error if format is unsupported or conversion fails
1817
+ *
1818
+ * @example
1819
+ * ```typescript
1820
+ * const painter = new ApexPainter({ type: 'base64' });
1821
+ * const result = await painter.createCanvas({ width: 100, height: 100 });
1822
+ * const base64String = await painter.outPut(result.buffer); // Returns base64 string
1823
+ * ```
1824
+ */
1004
1825
  async outPut(results) {
1005
- const formatType = this.format?.type || 'buffer';
1006
- switch (formatType) {
1007
- case 'buffer':
1008
- return results;
1009
- case 'url':
1010
- return await (0, utils_1.url)(results);
1011
- case 'dataURL':
1012
- return (0, utils_1.dataURL)(results);
1013
- case 'blob':
1014
- return (0, utils_1.blob)(results);
1015
- case 'base64':
1016
- return (0, utils_1.base64)(results);
1017
- case 'arraybuffer':
1018
- return (0, utils_1.arrayBuffer)(results);
1019
- default:
1020
- throw new Error('Unsupported format');
1826
+ try {
1827
+ if (!Buffer.isBuffer(results)) {
1828
+ throw new Error("outPut: results must be a Buffer.");
1829
+ }
1830
+ const formatType = this.format?.type || 'buffer';
1831
+ switch (formatType) {
1832
+ case 'buffer':
1833
+ return results;
1834
+ case 'url':
1835
+ return await (0, utils_1.url)(results);
1836
+ case 'dataURL':
1837
+ return (0, utils_1.dataURL)(results);
1838
+ case 'blob':
1839
+ return (0, utils_1.blob)(results);
1840
+ case 'base64':
1841
+ return (0, utils_1.base64)(results);
1842
+ case 'arraybuffer':
1843
+ return (0, utils_1.arrayBuffer)(results);
1844
+ default:
1845
+ throw new Error(`outPut: Unsupported format '${formatType}'. Supported: buffer, url, dataURL, blob, base64, arraybuffer`);
1846
+ }
1847
+ }
1848
+ catch (error) {
1849
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1850
+ throw new Error(`outPut failed: ${errorMessage}`);
1851
+ }
1852
+ }
1853
+ /**
1854
+ * Advanced save method to save buffers to local files with extensive customization options.
1855
+ *
1856
+ * @param buffer - Buffer to save (from createCanvas, createImage, createText, etc.)
1857
+ * @param options - Save options for file path, format, naming, etc.
1858
+ * @returns SaveResult with file path, name, size, and format
1859
+ *
1860
+ * @example
1861
+ * ```typescript
1862
+ * // Simple save with auto-generated name
1863
+ * const canvas = await painter.createCanvas({ width: 800, height: 600 });
1864
+ * const result = await painter.save(canvas.buffer);
1865
+ * // Saves to: ./ApexPainter_output/20241220_143025_123.png
1866
+ *
1867
+ * // Custom filename and directory
1868
+ * await painter.save(canvas.buffer, {
1869
+ * directory: './my-images',
1870
+ * filename: 'my-canvas',
1871
+ * format: 'jpg',
1872
+ * quality: 95
1873
+ * });
1874
+ *
1875
+ * // Save with counter naming
1876
+ * await painter.save(canvas.buffer, {
1877
+ * naming: 'counter',
1878
+ * prefix: 'image-',
1879
+ * counterStart: 1
1880
+ * });
1881
+ * // Saves to: ./ApexPainter_output/image-1.png, image-2.png, etc.
1882
+ *
1883
+ * // Save multiple buffers
1884
+ * const buffers = [canvas1.buffer, canvas2.buffer, canvas3.buffer];
1885
+ * const results = await painter.saveMultiple(buffers, {
1886
+ * prefix: 'batch-',
1887
+ * naming: 'counter'
1888
+ * });
1889
+ * ```
1890
+ */
1891
+ async save(buffer, options) {
1892
+ try {
1893
+ if (!Buffer.isBuffer(buffer)) {
1894
+ throw new Error("save: buffer must be a Buffer.");
1895
+ }
1896
+ const opts = {
1897
+ directory: options?.directory ?? './ApexPainter_output',
1898
+ filename: options?.filename,
1899
+ format: options?.format ?? 'png',
1900
+ quality: options?.quality ?? 90,
1901
+ createDirectory: options?.createDirectory ?? true,
1902
+ naming: options?.naming ?? 'timestamp',
1903
+ counterStart: options?.counterStart ?? 1,
1904
+ prefix: options?.prefix ?? '',
1905
+ suffix: options?.suffix ?? '',
1906
+ overwrite: options?.overwrite ?? false
1907
+ };
1908
+ // Create directory if needed
1909
+ if (opts.createDirectory && !fs_1.default.existsSync(opts.directory)) {
1910
+ fs_1.default.mkdirSync(opts.directory, { recursive: true });
1911
+ }
1912
+ // Generate filename
1913
+ let filename;
1914
+ if (opts.filename) {
1915
+ filename = opts.filename;
1916
+ // Add extension if not present
1917
+ if (!filename.includes('.')) {
1918
+ filename += `.${opts.format}`;
1919
+ }
1920
+ }
1921
+ else {
1922
+ // Auto-generate filename based on naming pattern
1923
+ switch (opts.naming) {
1924
+ case 'timestamp':
1925
+ const now = new Date();
1926
+ const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}_${String(now.getMilliseconds()).padStart(3, '0')}`;
1927
+ filename = `${opts.prefix}${timestamp}${opts.suffix}.${opts.format}`;
1928
+ break;
1929
+ case 'counter':
1930
+ filename = `${opts.prefix}${this.saveCounter}${opts.suffix}.${opts.format}`;
1931
+ this.saveCounter++;
1932
+ break;
1933
+ case 'custom':
1934
+ filename = `${opts.prefix}${opts.suffix}.${opts.format}`;
1935
+ break;
1936
+ default:
1937
+ filename = `${opts.prefix}${Date.now()}${opts.suffix}.${opts.format}`;
1938
+ }
1939
+ }
1940
+ // Handle file overwrite
1941
+ const filePath = path_1.default.join(opts.directory, filename);
1942
+ if (!opts.overwrite && fs_1.default.existsSync(filePath)) {
1943
+ // Add number suffix if file exists
1944
+ let counter = 1;
1945
+ let newPath = filePath;
1946
+ const ext = path_1.default.extname(filePath);
1947
+ const baseName = path_1.default.basename(filePath, ext);
1948
+ const dir = path_1.default.dirname(filePath);
1949
+ while (fs_1.default.existsSync(newPath)) {
1950
+ newPath = path_1.default.join(dir, `${baseName}_${counter}${ext}`);
1951
+ counter++;
1952
+ }
1953
+ filename = path_1.default.basename(newPath);
1954
+ }
1955
+ // Convert buffer format if needed
1956
+ let finalBuffer = buffer;
1957
+ if (opts.format !== 'png') {
1958
+ // Use Sharp for format conversion
1959
+ const sharp = require('sharp');
1960
+ let sharpImage = sharp(buffer);
1961
+ switch (opts.format) {
1962
+ case 'jpg':
1963
+ case 'jpeg':
1964
+ finalBuffer = await sharpImage
1965
+ .jpeg({ quality: opts.quality, progressive: false })
1966
+ .toBuffer();
1967
+ break;
1968
+ case 'webp':
1969
+ finalBuffer = await sharpImage
1970
+ .webp({ quality: opts.quality })
1971
+ .toBuffer();
1972
+ break;
1973
+ case 'avif':
1974
+ finalBuffer = await sharpImage
1975
+ .avif({ quality: opts.quality })
1976
+ .toBuffer();
1977
+ break;
1978
+ case 'gif':
1979
+ // GIF requires special handling - keep as PNG if not already GIF
1980
+ if (!buffer.toString('ascii', 0, 3).includes('GIF')) {
1981
+ console.warn('save: Converting to GIF may not preserve quality. Consider using PNG.');
1982
+ finalBuffer = buffer; // Keep original for now
1983
+ }
1984
+ break;
1985
+ }
1986
+ }
1987
+ // Write file
1988
+ const finalPath = path_1.default.join(opts.directory, filename);
1989
+ fs_1.default.writeFileSync(finalPath, finalBuffer);
1990
+ return {
1991
+ path: finalPath,
1992
+ filename: filename,
1993
+ size: finalBuffer.length,
1994
+ format: opts.format
1995
+ };
1996
+ }
1997
+ catch (error) {
1998
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1999
+ throw new Error(`save failed: ${errorMessage}`);
1021
2000
  }
1022
2001
  }
2002
+ /**
2003
+ * Save multiple buffers at once with batch options.
2004
+ *
2005
+ * @param buffers - Array of buffers to save
2006
+ * @param options - Save options (applied to all files)
2007
+ * @returns Array of SaveResult objects
2008
+ *
2009
+ * @example
2010
+ * ```typescript
2011
+ * const canvas1 = await painter.createCanvas({ width: 800, height: 600 });
2012
+ * const canvas2 = await painter.createCanvas({ width: 800, height: 600 });
2013
+ * const results = await painter.saveMultiple([canvas1.buffer, canvas2.buffer], {
2014
+ * prefix: 'batch-',
2015
+ * naming: 'counter'
2016
+ * });
2017
+ * ```
2018
+ */
2019
+ async saveMultiple(buffers, options) {
2020
+ try {
2021
+ if (!Array.isArray(buffers) || buffers.length === 0) {
2022
+ throw new Error("saveMultiple: buffers must be a non-empty array.");
2023
+ }
2024
+ const results = [];
2025
+ const baseCounter = options?.counterStart ?? this.saveCounter;
2026
+ for (let i = 0; i < buffers.length; i++) {
2027
+ const bufferOptions = {
2028
+ ...options,
2029
+ counterStart: baseCounter + i,
2030
+ naming: options?.naming === 'counter' ? 'counter' : options?.naming
2031
+ };
2032
+ const result = await this.save(buffers[i], bufferOptions);
2033
+ results.push(result);
2034
+ }
2035
+ return results;
2036
+ }
2037
+ catch (error) {
2038
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2039
+ throw new Error(`saveMultiple failed: ${errorMessage}`);
2040
+ }
2041
+ }
2042
+ /**
2043
+ * Reset the save counter (useful when using 'counter' naming).
2044
+ */
2045
+ resetSaveCounter() {
2046
+ this.saveCounter = 1;
2047
+ }
1023
2048
  /**
1024
2049
  * Applies stroke style to shape context
1025
2050
  * @private