apexify.js 4.9.28 → 5.0.0

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