apexify.js 4.9.30 → 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 (126) hide show
  1. package/README.md +56 -1
  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 +1247 -418
  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/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
  43. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
  44. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
  45. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
  46. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
  47. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js +233 -0
  48. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
  49. package/dist/cjs/Canvas/utils/types.d.ts +121 -0
  50. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  51. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  52. package/dist/cjs/Canvas/utils/utils.d.ts +9 -2
  53. package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
  54. package/dist/cjs/Canvas/utils/utils.js +32 -1
  55. package/dist/cjs/Canvas/utils/utils.js.map +1 -1
  56. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  57. package/dist/esm/Canvas/ApexPainter.d.ts +96 -145
  58. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  59. package/dist/esm/Canvas/ApexPainter.js +1247 -418
  60. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  61. package/dist/esm/Canvas/utils/Charts/charts.d.ts +7 -2
  62. package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +1 -1
  63. package/dist/esm/Canvas/utils/Charts/charts.js +3 -1
  64. package/dist/esm/Canvas/utils/Charts/charts.js.map +1 -1
  65. package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts +75 -0
  66. package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
  67. package/dist/esm/Canvas/utils/Custom/advancedLines.js +263 -0
  68. package/dist/esm/Canvas/utils/Custom/advancedLines.js.map +1 -0
  69. package/dist/esm/Canvas/utils/Custom/customLines.d.ts +2 -1
  70. package/dist/esm/Canvas/utils/Custom/customLines.d.ts.map +1 -1
  71. package/dist/esm/Canvas/utils/Custom/customLines.js +73 -6
  72. package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
  73. package/dist/esm/Canvas/utils/General/batchOperations.d.ts +17 -0
  74. package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -0
  75. package/dist/esm/Canvas/utils/General/batchOperations.js +88 -0
  76. package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -0
  77. package/dist/esm/Canvas/utils/General/general functions.d.ts +25 -3
  78. package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
  79. package/dist/esm/Canvas/utils/General/general functions.js +37 -9
  80. package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
  81. package/dist/esm/Canvas/utils/General/imageCompression.d.ts +19 -0
  82. package/dist/esm/Canvas/utils/General/imageCompression.d.ts.map +1 -0
  83. package/dist/esm/Canvas/utils/General/imageCompression.js +262 -0
  84. package/dist/esm/Canvas/utils/General/imageCompression.js.map +1 -0
  85. package/dist/esm/Canvas/utils/General/imageStitching.d.ts +20 -0
  86. package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -0
  87. package/dist/esm/Canvas/utils/General/imageStitching.js +227 -0
  88. package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -0
  89. package/dist/esm/Canvas/utils/Image/imageEffects.d.ts +37 -0
  90. package/dist/esm/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
  91. package/dist/esm/Canvas/utils/Image/imageEffects.js +128 -0
  92. package/dist/esm/Canvas/utils/Image/imageEffects.js.map +1 -0
  93. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts +67 -0
  94. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
  95. package/dist/esm/Canvas/utils/Image/imageMasking.js +276 -0
  96. package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -0
  97. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
  98. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
  99. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
  100. package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
  101. package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
  102. package/dist/esm/Canvas/utils/Texts/textPathRenderer.js +233 -0
  103. package/dist/esm/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
  104. package/dist/esm/Canvas/utils/types.d.ts +121 -0
  105. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  106. package/dist/esm/Canvas/utils/types.js.map +1 -1
  107. package/dist/esm/Canvas/utils/utils.d.ts +9 -2
  108. package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
  109. package/dist/esm/Canvas/utils/utils.js +32 -1
  110. package/dist/esm/Canvas/utils/utils.js.map +1 -1
  111. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  112. package/lib/Canvas/ApexPainter.ts +1118 -266
  113. package/lib/Canvas/utils/Charts/charts.ts +16 -7
  114. package/lib/Canvas/utils/Custom/advancedLines.ts +335 -0
  115. package/lib/Canvas/utils/Custom/customLines.ts +84 -9
  116. package/lib/Canvas/utils/General/batchOperations.ts +103 -0
  117. package/lib/Canvas/utils/General/general functions.ts +85 -41
  118. package/lib/Canvas/utils/General/imageCompression.ts +316 -0
  119. package/lib/Canvas/utils/General/imageStitching.ts +252 -0
  120. package/lib/Canvas/utils/Image/imageEffects.ts +175 -0
  121. package/lib/Canvas/utils/Image/imageMasking.ts +335 -0
  122. package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +455 -444
  123. package/lib/Canvas/utils/Texts/textPathRenderer.ts +320 -0
  124. package/lib/Canvas/utils/types.ts +121 -0
  125. package/lib/Canvas/utils/utils.ts +49 -2
  126. package/package.json +69 -34
@@ -5,16 +5,26 @@ import { PassThrough} from "stream";
5
5
  import axios from 'axios';
6
6
  import fs, { PathLike } from "fs";
7
7
  import path from "path";
8
- import { OutputFormat, CanvasConfig, TextObject, TextProperties, ImageProperties, GIFOptions, GIFResults, CustomOptions, cropOptions,
9
- drawBackgroundGradient, drawBackgroundColor, customBackground, customLines,
10
- drawText, converter, resizingImg, applyColorFilters, imgEffects,verticalBarChart, pieChart,
11
- lineChart, cropInner, cropOuter, bgRemoval, detectColors, removeColor, dataURL, base64, arrayBuffer, blob, url, GradientConfig, Frame,
12
- PatternOptions, ExtractFramesOptions, buildPath, ResizeOptions, MaskOptions, BlendOptions,
8
+ import { OutputFormat, CanvasConfig, TextProperties, ImageProperties, GIFOptions, GIFResults, CustomOptions, cropOptions,
9
+ drawBackgroundGradient, drawBackgroundColor, customBackground, customLines,
10
+ converter, resizingImg, applyColorFilters, imgEffects,verticalBarChart, pieChart,
11
+ lineChart, cropInner, cropOuter, bgRemoval, detectColors, removeColor, dataURL,
12
+ base64, arrayBuffer, blob, url, GradientConfig, Frame,
13
+ ExtractFramesOptions, buildPath, ResizeOptions, MaskOptions, BlendOptions,
13
14
  applyCanvasZoom, applyNoise,
14
15
  applyStroke, applyRotation, applyShadow, drawBoxBackground, fitInto, loadImageCached,
15
- drawShape, isShapeSource, ShapeType, createShapePath, createGradientFill, applyImageFilters, applySimpleProfessionalFilters
16
+ drawShape, isShapeSource, ShapeType, createShapePath, createGradientFill, applySimpleProfessionalFilters,
17
+ ImageFilter, barChart_1, PieChartData, LineChartConfig,
18
+ // New features
19
+ applyImageMask, applyClipPath, applyPerspectiveDistortion, applyBulgeDistortion, applyMeshWarp,
20
+ applyVignette, applyLensFlare, applyChromaticAberration, applyFilmGrain,
21
+ renderTextOnPath,
22
+ drawArrow, drawMarker, createSmoothPath, createCatmullRomPath, applyLinePattern, applyLineTexture, getPointOnLinePath,
23
+ batchOperations, chainOperations,
24
+ stitchImages as stitchImagesUtil, createCollage,
25
+ compressImage, extractPalette as extractPaletteUtil,
26
+ BatchOperation, ChainOperation, StitchOptions, CollageLayout, CompressionOptions, PaletteOptions
16
27
  } from "./utils/utils";
17
- import { } from "./utils/Image/imageProperties";
18
28
  import { EnhancedTextRenderer } from "./utils/Texts/enhancedTextRenderer";
19
29
  import { EnhancedPatternRenderer } from "./utils/Patterns/enhancedPatternRenderer";
20
30
 
@@ -60,7 +70,12 @@ export class ApexPainter {
60
70
  * @param textProps - Text properties
61
71
  */
62
72
  async #renderEnhancedText(ctx: SKRSContext2D, textProps: TextProperties): Promise<void> {
63
- await EnhancedTextRenderer.renderText(ctx, textProps);
73
+ // Check if text should be rendered on a path
74
+ if (textProps.path && textProps.textOnPath) {
75
+ renderTextOnPath(ctx, textProps.text, textProps.path, textProps.path.offset ?? 0);
76
+ } else {
77
+ await EnhancedTextRenderer.renderText(ctx, textProps);
78
+ }
64
79
  }
65
80
 
66
81
  /**
@@ -94,20 +109,66 @@ export class ApexPainter {
94
109
  * const buffer = result.buffer;
95
110
  * ```
96
111
  */
97
- async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
98
- // Handle inherit sizing
99
- if (canvas.customBg?.inherit) {
100
- let p = canvas.customBg.source;
101
- if (!/^https?:\/\//i.test(p)) p = path.join(process.cwd(), p);
102
- try {
103
- const img = await loadImage(p);
104
- canvas.width = img.width;
105
- canvas.height = img.height;
106
- } catch (e:any) {
107
- console.error('inherit load failed:', e?.message ?? e);
112
+ /**
113
+ * Validates canvas configuration.
114
+ * @private
115
+ * @param canvas - Canvas configuration to validate
116
+ */
117
+ #validateCanvasConfig(canvas: CanvasConfig): void {
118
+ if (!canvas) {
119
+ throw new Error("createCanvas: canvas configuration is required.");
120
+ }
121
+
122
+ if (canvas.width !== undefined && (typeof canvas.width !== 'number' || canvas.width <= 0)) {
123
+ throw new Error("createCanvas: width must be a positive number.");
124
+ }
125
+
126
+ if (canvas.height !== undefined && (typeof canvas.height !== 'number' || canvas.height <= 0)) {
127
+ throw new Error("createCanvas: height must be a positive number.");
128
+ }
129
+
130
+ if (canvas.opacity !== undefined && (typeof canvas.opacity !== 'number' || canvas.opacity < 0 || canvas.opacity > 1)) {
131
+ throw new Error("createCanvas: opacity must be a number between 0 and 1.");
132
+ }
133
+
134
+ if (canvas.zoom?.scale !== undefined && (typeof canvas.zoom.scale !== 'number' || canvas.zoom.scale <= 0)) {
135
+ throw new Error("createCanvas: zoom.scale must be a positive number.");
108
136
  }
109
137
  }
110
138
 
139
+ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
140
+ try {
141
+ // Validate canvas configuration
142
+ this.#validateCanvasConfig(canvas);
143
+
144
+ // Handle inherit sizing
145
+ if (canvas.customBg?.inherit) {
146
+ let p = canvas.customBg.source;
147
+ if (!/^https?:\/\//i.test(p)) p = path.join(process.cwd(), p);
148
+ try {
149
+ const img = await loadImage(p);
150
+ canvas.width = img.width;
151
+ canvas.height = img.height;
152
+ } catch (e: unknown) {
153
+ const errorMessage = e instanceof Error ? e.message : String(e);
154
+ throw new Error(`createCanvas: Failed to load image for inherit sizing: ${errorMessage}`);
155
+ }
156
+ }
157
+
158
+ // Handle video background inherit sizing
159
+ if (canvas.videoBg) {
160
+ try {
161
+ const frameBuffer = await this.#extractVideoFrame(canvas.videoBg.source, canvas.videoBg.frame ?? 0);
162
+ if (frameBuffer) {
163
+ const img = await loadImage(frameBuffer);
164
+ if (!canvas.width) canvas.width = img.width;
165
+ if (!canvas.height) canvas.height = img.height;
166
+ }
167
+ } catch (e: unknown) {
168
+ console.warn('createCanvas: Failed to extract video frame for sizing, using defaults');
169
+ }
170
+ }
171
+
111
172
  // 2) Use final width/height after inherit
112
173
  const width = canvas.width ?? 500;
113
174
  const height = canvas.height ?? 500;
@@ -118,7 +179,7 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
118
179
  borderRadius = 0,
119
180
  borderPosition = 'all',
120
181
  opacity = 1,
121
- colorBg, customBg, gradientBg,
182
+ colorBg, customBg, gradientBg, videoBg,
122
183
  patternBg, noiseBg, blendMode,
123
184
  zoom, stroke, shadow,
124
185
  blur
@@ -135,54 +196,97 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
135
196
  throw new Error(`createCanvas: only one of colorBg, gradientBg, or customBg can be used. You provided: ${bgSources.join(', ')}`);
136
197
  }
137
198
 
138
- const cv = createCanvas(width, height);
139
- const ctx = cv.getContext('2d') as SKRSContext2D;
140
- if (!ctx) throw new Error('Unable to get 2D context');
199
+ const cv = createCanvas(width, height);
200
+ const ctx = cv.getContext('2d') as SKRSContext2D;
201
+ if (!ctx) throw new Error('Unable to get 2D context');
141
202
 
142
203
 
143
- ctx.globalAlpha = opacity;
204
+ ctx.globalAlpha = opacity;
144
205
 
145
- // ---- BACKGROUND (clipped) ----
146
- ctx.save();
147
- applyRotation(ctx, rotation, x, y, width, height);
206
+ // ---- BACKGROUND (clipped) ----
207
+ ctx.save();
208
+ applyRotation(ctx, rotation, x, y, width, height);
148
209
 
149
- buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
150
- ctx.clip();
210
+ buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
211
+ ctx.clip();
151
212
 
152
- applyCanvasZoom(ctx, width, height, zoom);
213
+ applyCanvasZoom(ctx, width, height, zoom);
153
214
 
154
- ctx.translate(x, y);
155
- if (typeof blendMode === 'string') {
156
- ctx.globalCompositeOperation = blendMode as any;
157
- }
215
+ ctx.translate(x, y);
216
+ if (typeof blendMode === 'string') {
217
+ ctx.globalCompositeOperation = blendMode as GlobalCompositeOperation;
218
+ }
158
219
 
159
- if (customBg) await customBackground(ctx, { ...canvas, blur });
160
- else if (gradientBg) await drawBackgroundGradient(ctx, { ...canvas, blur });
161
- else await drawBackgroundColor(ctx, { ...canvas, blur, colorBg: colorBg ?? '#000' });
220
+ // Draw video background if specified
221
+ if (videoBg) {
222
+ try {
223
+ const frameBuffer = await this.#extractVideoFrame(videoBg.source, videoBg.frame ?? 0);
224
+ if (frameBuffer) {
225
+ const videoImg = await loadImage(frameBuffer);
226
+ ctx.globalAlpha = videoBg.opacity ?? 1;
227
+ ctx.drawImage(videoImg, 0, 0, width, height);
228
+ ctx.globalAlpha = opacity;
229
+ }
230
+ } catch (e: unknown) {
231
+ console.warn('createCanvas: Failed to load video background frame');
232
+ }
233
+ }
162
234
 
163
- if (patternBg) await EnhancedPatternRenderer.renderPattern(ctx, cv, patternBg);
164
- if (noiseBg) applyNoise(ctx, width, height, noiseBg.intensity ?? 0.05);
235
+ // Draw custom background with filters and opacity support
236
+ if (customBg) {
237
+ await customBackground(ctx, { ...canvas, blur });
238
+ // Apply filters to background if specified
239
+ if (customBg.filters && customBg.filters.length > 0) {
240
+ const tempCanvas = createCanvas(width, height);
241
+ const tempCtx = tempCanvas.getContext('2d') as SKRSContext2D;
242
+ if (tempCtx) {
243
+ tempCtx.drawImage(cv, 0, 0);
244
+ await applySimpleProfessionalFilters(tempCtx, customBg.filters, width, height);
245
+ ctx.clearRect(0, 0, width, height);
246
+ ctx.globalAlpha = customBg.opacity ?? 1;
247
+ ctx.drawImage(tempCanvas, 0, 0);
248
+ ctx.globalAlpha = opacity;
249
+ }
250
+ } else if (customBg.opacity !== undefined && customBg.opacity !== 1) {
251
+ ctx.globalAlpha = customBg.opacity;
252
+ await customBackground(ctx, { ...canvas, blur });
253
+ ctx.globalAlpha = opacity;
254
+ } else {
255
+ await customBackground(ctx, { ...canvas, blur });
256
+ }
257
+ } else if (gradientBg) {
258
+ await drawBackgroundGradient(ctx, { ...canvas, blur });
259
+ } else {
260
+ await drawBackgroundColor(ctx, { ...canvas, blur, colorBg: colorBg ?? '#000' });
261
+ }
165
262
 
166
- ctx.restore();
263
+ if (patternBg) await EnhancedPatternRenderer.renderPattern(ctx, cv, patternBg);
264
+ if (noiseBg) applyNoise(ctx, width, height, noiseBg.intensity ?? 0.05);
167
265
 
168
- // Apply shadow effect
169
- if (shadow) {
170
- ctx.save();
171
- buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
172
- applyShadow(ctx, shadow, x, y, width, height);
173
- ctx.restore();
174
- }
266
+ ctx.restore();
175
267
 
176
- // Apply stroke effect
177
- if (stroke) {
178
- ctx.save();
179
- buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
180
- applyStroke(ctx, stroke, x, y, width, height);
181
- ctx.restore();
182
- }
268
+ // Apply shadow effect
269
+ if (shadow) {
270
+ ctx.save();
271
+ buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
272
+ applyShadow(ctx, shadow, x, y, width, height);
273
+ ctx.restore();
274
+ }
275
+
276
+ // Apply stroke effect
277
+ if (stroke) {
278
+ ctx.save();
279
+ buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
280
+ applyStroke(ctx, stroke, x, y, width, height);
281
+ ctx.restore();
282
+ }
183
283
 
184
- return { buffer: cv.toBuffer('image/png'), canvas };
185
- }
284
+ return { buffer: cv.toBuffer('image/png'), canvas };
285
+ } catch (error) {
286
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
287
+ throw new Error(`createCanvas failed: ${errorMessage}`);
288
+ }
289
+ }
186
290
 
187
291
 
188
292
 
@@ -228,31 +332,57 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
228
332
  * ], canvasBuffer);
229
333
  * ```
230
334
  */
335
+ /**
336
+ * Validates image/shape properties array.
337
+ * @private
338
+ * @param images - Image properties to validate
339
+ */
340
+ #validateImageArray(images: ImageProperties | ImageProperties[]): void {
341
+ const list = Array.isArray(images) ? images : [images];
342
+ if (list.length === 0) {
343
+ throw new Error("createImage: At least one image/shape is required.");
344
+ }
345
+ for (const ip of list) {
346
+ this.#validateImageProperties(ip);
347
+ }
348
+ }
349
+
231
350
  async createImage(
232
351
  images: ImageProperties | ImageProperties[],
233
352
  canvasBuffer: CanvasResults | Buffer
234
353
  ): Promise<Buffer> {
235
- const list = Array.isArray(images) ? images : [images];
354
+ try {
355
+ // Validate inputs
356
+ if (!canvasBuffer) {
357
+ throw new Error("createImage: canvasBuffer is required.");
358
+ }
359
+ this.#validateImageArray(images);
360
+
361
+ const list = Array.isArray(images) ? images : [images];
236
362
 
237
- // Load base canvas buffer
238
- const base: Image = Buffer.isBuffer(canvasBuffer)
239
- ? await loadImage(canvasBuffer)
240
- : await loadImage((canvasBuffer as CanvasResults).buffer);
363
+ // Load base canvas buffer
364
+ const base: Image = Buffer.isBuffer(canvasBuffer)
365
+ ? await loadImage(canvasBuffer)
366
+ : await loadImage((canvasBuffer as CanvasResults).buffer);
241
367
 
242
- const cv = createCanvas(base.width, base.height);
243
- const ctx = cv.getContext("2d") as SKRSContext2D;
244
- if (!ctx) throw new Error("Unable to get 2D rendering context");
368
+ const cv = createCanvas(base.width, base.height);
369
+ const ctx = cv.getContext("2d") as SKRSContext2D;
370
+ if (!ctx) throw new Error("Unable to get 2D rendering context");
245
371
 
246
- // Paint bg
247
- ctx.drawImage(base, 0, 0);
372
+ // Paint bg
373
+ ctx.drawImage(base, 0, 0);
248
374
 
249
- // Draw each image/shape on canvas
250
- for (const ip of list) {
251
- await this.#drawImageBitmap(ctx, ip);
252
- }
375
+ // Draw each image/shape on canvas
376
+ for (const ip of list) {
377
+ await this.#drawImageBitmap(ctx, ip);
378
+ }
253
379
 
254
- // Return updated buffer
255
- return cv.toBuffer("image/png");
380
+ // Return updated buffer
381
+ return cv.toBuffer("image/png");
382
+ } catch (error) {
383
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
384
+ throw new Error(`createImage failed: ${errorMessage}`);
385
+ }
256
386
  }
257
387
 
258
388
  /**
@@ -277,7 +407,14 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
277
407
  stroke,
278
408
  boxBackground,
279
409
  shape,
280
- filters
410
+ filters,
411
+ filterIntensity = 1,
412
+ filterOrder = 'post',
413
+ mask,
414
+ clipPath,
415
+ distortion,
416
+ meshWarp,
417
+ effects
281
418
  } = ip;
282
419
 
283
420
  this.#validateImageProperties(ip);
@@ -318,10 +455,14 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
318
455
  // 2) Optional box background (under bitmap, inside clip) — color or gradient
319
456
  drawBoxBackground(ctx, box, boxBackground, borderRadius, borderPosition);
320
457
 
321
- // 3) Clip to image border radius, then draw the bitmap with blur/opacity and fit/align
458
+ // 3) Clip to image border radius or custom clip path, then draw the bitmap with blur/opacity and fit/align
322
459
  ctx.save();
323
- buildPath(ctx, box.x, box.y, box.w, box.h, borderRadius, borderPosition);
324
- ctx.clip();
460
+ if (clipPath && clipPath.length >= 3) {
461
+ applyClipPath(ctx, clipPath);
462
+ } else {
463
+ buildPath(ctx, box.x, box.y, box.w, box.h, borderRadius, borderPosition);
464
+ ctx.clip();
465
+ }
325
466
 
326
467
  const { dx, dy, dw, dh, sx, sy, sw, sh } =
327
468
  fitInto(box.x, box.y, box.w, box.h, img.width, img.height, fit, align);
@@ -330,17 +471,117 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
330
471
  ctx.globalAlpha = opacity ?? 1;
331
472
  if ((blur ?? 0) > 0) ctx.filter = `blur(${blur}px)`;
332
473
 
333
- // Apply professional image filters BEFORE drawing
334
- if (filters && filters.length > 0) {
335
- await applySimpleProfessionalFilters(ctx, filters, dw, dh);
474
+ // Apply professional image filters BEFORE drawing if filterOrder is 'pre'
475
+ if (filters && filters.length > 0 && filterOrder === 'pre') {
476
+ const adjustedFilters = filters.map(f => ({
477
+ ...f,
478
+ intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
479
+ value: f.value !== undefined ? f.value * filterIntensity : f.value,
480
+ radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
481
+ }));
482
+ await applySimpleProfessionalFilters(ctx, adjustedFilters, dw, dh);
483
+ }
484
+
485
+ // Apply distortion if specified (before drawing)
486
+ if (distortion) {
487
+ if (distortion.type === 'perspective' && distortion.points && distortion.points.length === 4) {
488
+ applyPerspectiveDistortion(ctx, img, distortion.points, dx, dy, dw, dh);
489
+ ctx.filter = "none";
490
+ ctx.globalAlpha = prevAlpha;
491
+ ctx.restore();
492
+ ctx.restore();
493
+ return;
494
+ } else if (distortion.type === 'bulge' || distortion.type === 'pinch') {
495
+ const centerX = dx + dw / 2;
496
+ const centerY = dy + dh / 2;
497
+ const radius = Math.min(dw, dh) / 2;
498
+ const intensity = (distortion.intensity ?? 0.5) * (distortion.type === 'pinch' ? -1 : 1);
499
+ applyBulgeDistortion(ctx, img, centerX, centerY, radius, intensity, dx, dy, dw, dh);
500
+ ctx.filter = "none";
501
+ ctx.globalAlpha = prevAlpha;
502
+ ctx.restore();
503
+ ctx.restore();
504
+ return;
505
+ }
506
+ }
507
+
508
+ // Apply mesh warp if specified
509
+ if (meshWarp && meshWarp.controlPoints) {
510
+ applyMeshWarp(ctx, img, meshWarp.gridX ?? 10, meshWarp.gridY ?? 10, meshWarp.controlPoints, dx, dy, dw, dh);
511
+ ctx.filter = "none";
512
+ ctx.globalAlpha = prevAlpha;
513
+ ctx.restore();
514
+ ctx.restore();
515
+ return;
336
516
  }
337
517
 
338
- ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
518
+ // Draw image with or without masking
519
+ if (mask) {
520
+ await applyImageMask(ctx, img, mask.source, mask.mode ?? 'alpha', dx, dy, dw, dh);
521
+ } else {
522
+ ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
523
+ }
339
524
 
340
525
  ctx.filter = "none";
341
526
  ctx.globalAlpha = prevAlpha;
342
527
  ctx.restore();
343
528
 
529
+ // Apply professional image filters AFTER drawing if filterOrder is 'post'
530
+ if (filters && filters.length > 0 && filterOrder === 'post') {
531
+ ctx.save();
532
+ const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
533
+ const tempCanvas = createCanvas(box.w, box.h);
534
+ const tempCtx = tempCanvas.getContext('2d') as SKRSContext2D;
535
+ if (tempCtx) {
536
+ tempCtx.putImageData(imageData, 0, 0);
537
+ const adjustedFilters = filters.map(f => ({
538
+ ...f,
539
+ intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
540
+ value: f.value !== undefined ? f.value * filterIntensity : f.value,
541
+ radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
542
+ }));
543
+ await applySimpleProfessionalFilters(tempCtx, adjustedFilters, box.w, box.h);
544
+ ctx.clearRect(box.x, box.y, box.w, box.h);
545
+ ctx.drawImage(tempCanvas, box.x, box.y);
546
+ }
547
+ ctx.restore();
548
+ }
549
+
550
+ // Apply effects stack
551
+ if (effects) {
552
+ ctx.save();
553
+ const effectsCtx = ctx;
554
+ if (effects.vignette) {
555
+ applyVignette(effectsCtx, effects.vignette.intensity, effects.vignette.size, box.w, box.h);
556
+ }
557
+ if (effects.lensFlare) {
558
+ applyLensFlare(effectsCtx, box.x + effects.lensFlare.x, box.y + effects.lensFlare.y, effects.lensFlare.intensity, box.w, box.h);
559
+ }
560
+ if (effects.chromaticAberration) {
561
+ const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
562
+ const tempCanvas = createCanvas(box.w, box.h);
563
+ const tempCtx = tempCanvas.getContext('2d') as SKRSContext2D;
564
+ if (tempCtx) {
565
+ tempCtx.putImageData(imageData, 0, 0);
566
+ applyChromaticAberration(tempCtx, effects.chromaticAberration.intensity, box.w, box.h);
567
+ ctx.clearRect(box.x, box.y, box.w, box.h);
568
+ ctx.drawImage(tempCanvas, box.x, box.y);
569
+ }
570
+ }
571
+ if (effects.filmGrain) {
572
+ const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
573
+ const tempCanvas = createCanvas(box.w, box.h);
574
+ const tempCtx = tempCanvas.getContext('2d') as SKRSContext2D;
575
+ if (tempCtx) {
576
+ tempCtx.putImageData(imageData, 0, 0);
577
+ applyFilmGrain(tempCtx, effects.filmGrain.intensity, box.w, box.h);
578
+ ctx.clearRect(box.x, box.y, box.w, box.h);
579
+ ctx.drawImage(tempCanvas, box.x, box.y);
580
+ }
581
+ }
582
+ ctx.restore();
583
+ }
584
+
344
585
 
345
586
  // 4) Stroke (independent) — supports gradient or color
346
587
  applyStroke(ctx, box, stroke);
@@ -647,18 +888,34 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
647
888
  * ], canvasBuffer);
648
889
  * ```
649
890
  */
891
+ /**
892
+ * Validates text properties array.
893
+ * @private
894
+ * @param textArray - Text properties to validate
895
+ */
896
+ #validateTextArray(textArray: TextProperties | TextProperties[]): void {
897
+ const textList = Array.isArray(textArray) ? textArray : [textArray];
898
+ if (textList.length === 0) {
899
+ throw new Error("createText: At least one text object is required.");
900
+ }
901
+ for (const textProps of textList) {
902
+ this.#validateTextProperties(textProps);
903
+ }
904
+ }
905
+
650
906
  async createText(textArray: TextProperties | TextProperties[], canvasBuffer: CanvasResults | Buffer): Promise<Buffer> {
651
907
  try {
908
+ // Validate inputs
909
+ if (!canvasBuffer) {
910
+ throw new Error("createText: canvasBuffer is required.");
911
+ }
912
+ this.#validateTextArray(textArray);
913
+
652
914
  // Ensure textArray is an array
653
915
  const textList = Array.isArray(textArray) ? textArray : [textArray];
654
916
 
655
- // Validate each text object
656
- for (const textProps of textList) {
657
- this.#validateTextProperties(textProps);
658
- }
659
-
660
917
  // Load existing canvas buffer
661
- let existingImage: any;
918
+ let existingImage: Image;
662
919
 
663
920
  if (Buffer.isBuffer(canvasBuffer)) {
664
921
  existingImage = await loadImage(canvasBuffer);
@@ -690,21 +947,44 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
690
947
 
691
948
  return canvas.toBuffer("image/png");
692
949
  } catch (error) {
693
- console.error("Error creating text:", error);
694
- throw new Error("Failed to create text on canvas");
950
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
951
+ throw new Error(`createText failed: ${errorMessage}`);
695
952
  }
696
953
  }
697
954
 
698
955
 
699
956
 
700
- async createCustom(options: CustomOptions[], buffer: CanvasResults | Buffer, ): Promise<Buffer> {
701
- try {
957
+ /**
958
+ * Validates custom line options.
959
+ * @private
960
+ * @param options - Custom options to validate
961
+ */
962
+ #validateCustomOptions(options: CustomOptions | CustomOptions[]): void {
963
+ const opts = Array.isArray(options) ? options : [options];
964
+ if (opts.length === 0) {
965
+ throw new Error("createCustom: At least one custom option is required.");
966
+ }
967
+ for (const opt of opts) {
968
+ if (!opt.startCoordinates || typeof opt.startCoordinates.x !== 'number' || typeof opt.startCoordinates.y !== 'number') {
969
+ throw new Error("createCustom: startCoordinates with valid x and y are required.");
970
+ }
971
+ if (!opt.endCoordinates || typeof opt.endCoordinates.x !== 'number' || typeof opt.endCoordinates.y !== 'number') {
972
+ throw new Error("createCustom: endCoordinates with valid x and y are required.");
973
+ }
974
+ }
975
+ }
702
976
 
703
- if (!Array.isArray(options)) {
704
- options = [options];
977
+ async createCustom(options: CustomOptions | CustomOptions[], buffer: CanvasResults | Buffer): Promise<Buffer> {
978
+ try {
979
+ // Validate inputs
980
+ if (!buffer) {
981
+ throw new Error("createCustom: buffer is required.");
705
982
  }
983
+ this.#validateCustomOptions(options);
706
984
 
707
- let existingImage: any;
985
+ const opts = Array.isArray(options) ? options : [options];
986
+
987
+ let existingImage: Image;
708
988
 
709
989
  if (Buffer.isBuffer(buffer)) {
710
990
  existingImage = await loadImage(buffer);
@@ -723,17 +1003,49 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
723
1003
 
724
1004
  ctx.drawImage(existingImage, 0, 0);
725
1005
 
726
- customLines(ctx, options);
1006
+ await customLines(ctx, opts);
727
1007
 
728
1008
  return canvas.toBuffer("image/png");
729
1009
  } catch (error) {
730
- console.error("Error creating custom image:", error);
731
- throw new Error("Failed to create custom image");
1010
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1011
+ throw new Error(`createCustom failed: ${errorMessage}`);
732
1012
  }
733
- }
1013
+ }
734
1014
 
735
- async createGIF(gifFrames: { background: string; duration: number }[], options: GIFOptions): Promise<GIFResults | any> {
736
- async function resizeImage(image: any, targetWidth: number, targetHeight: number) {
1015
+ /**
1016
+ * Validates GIF options and frames.
1017
+ * @private
1018
+ * @param gifFrames - GIF frames to validate
1019
+ * @param options - GIF options to validate
1020
+ */
1021
+ #validateGIFOptions(gifFrames: { background: string; duration: number }[], options: GIFOptions): void {
1022
+ if (!gifFrames || gifFrames.length === 0) {
1023
+ throw new Error("createGIF: At least one frame is required.");
1024
+ }
1025
+ for (const frame of gifFrames) {
1026
+ if (!frame.background) {
1027
+ throw new Error("createGIF: Each frame must have a background property.");
1028
+ }
1029
+ if (typeof frame.duration !== 'number' || frame.duration < 0) {
1030
+ throw new Error("createGIF: Each frame duration must be a non-negative number.");
1031
+ }
1032
+ }
1033
+ if (options.outputFormat === "file" && !options.outputFile) {
1034
+ throw new Error("createGIF: outputFile is required when outputFormat is 'file'.");
1035
+ }
1036
+ if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
1037
+ throw new Error("createGIF: repeat must be a non-negative number or undefined.");
1038
+ }
1039
+ if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
1040
+ throw new Error("createGIF: quality must be a number between 1 and 20 or undefined.");
1041
+ }
1042
+ }
1043
+
1044
+ async createGIF(gifFrames: { background: string; duration: number }[], options: GIFOptions): Promise<GIFResults | Buffer | string | Array<{ attachment: NodeJS.ReadableStream | any; name: string }> | undefined> {
1045
+ try {
1046
+ this.#validateGIFOptions(gifFrames, options);
1047
+
1048
+ async function resizeImage(image: Image, targetWidth: number, targetHeight: number) {
737
1049
  const canvas = createCanvas(targetWidth, targetHeight);
738
1050
  const ctx = canvas.getContext("2d");
739
1051
  ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
@@ -757,38 +1069,10 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
757
1069
  getBuffer: function (): Buffer {
758
1070
  return Buffer.concat(chunks);
759
1071
  }
760
- } as any;
761
- }
762
-
763
- function validateOptions(options: GIFOptions) {
764
- if (options.outputFormat === "file" && !options.outputFile) {
765
- throw new Error("Output file path is required when using file output format.");
766
- }
767
- if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
768
- throw new Error("Repeat must be a non-negative number or undefined.");
769
- }
770
- if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
771
- throw new Error("Quality must be a number between 1 and 20 or undefined.");
772
- }
773
- if (options.watermark && typeof options.watermark.enable !== "boolean") {
774
- throw new Error("Watermark must be a boolean or undefined.");
775
- }
776
- if (options.textOverlay) {
777
- const textOptions = options.textOverlay;
778
- if (!textOptions.text || typeof textOptions.text !== "string") {
779
- throw new Error("Text overlay text is required and must be a string.");
780
- }
781
- if (textOptions.fontSize !== undefined && (!Number.isInteger(textOptions.fontSize) || textOptions.fontSize <= 0)) {
782
- throw new Error("Text overlay fontSize must be a positive integer or undefined.");
783
- }
784
- if (textOptions.fontColor !== undefined && typeof textOptions.fontColor !== "string") {
785
- throw new Error("Text overlay fontColor must be a string or undefined.");
786
- }
787
- }
1072
+ } as PassThrough & { getBuffer: () => Buffer };
788
1073
  }
789
1074
 
790
- try {
791
- validateOptions(options);
1075
+ // Validation is done in #validateGIFOptions
792
1076
 
793
1077
  const canvasWidth = options.width || 1200;
794
1078
  const canvasHeight = options.height || 1200;
@@ -803,7 +1087,8 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
803
1087
  encoder.setQuality(options.quality || 10);
804
1088
 
805
1089
  const canvas = createCanvas(canvasWidth, canvasHeight);
806
- const ctx:any = canvas.getContext("2d");
1090
+ const ctx = canvas.getContext("2d") as SKRSContext2D;
1091
+ if (!ctx) throw new Error("Unable to get 2D context");
807
1092
 
808
1093
  for (const frame of gifFrames) {
809
1094
  const image = await loadImage(frame.background);
@@ -824,18 +1109,19 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
824
1109
  }
825
1110
 
826
1111
  encoder.setDelay(frame.duration);
827
- encoder.addFrame(ctx);
1112
+ encoder.addFrame(ctx as unknown as CanvasRenderingContext2D);
828
1113
  }
829
1114
 
830
1115
  encoder.finish();
831
1116
  outputStream.end();
832
1117
 
833
1118
  if (options.outputFormat === "file") {
834
- await new Promise((resolve) => outputStream.on("finish", resolve));
1119
+ await new Promise<void>((resolve) => outputStream.on("finish", () => resolve()));
835
1120
  } else if (options.outputFormat === "base64") {
836
1121
  if ('getBuffer' in outputStream) {
837
1122
  return outputStream.getBuffer().toString("base64");
838
1123
  }
1124
+ throw new Error("createGIF: Unable to get buffer for base64 output.");
839
1125
  } else if (options.outputFormat === "attachment") {
840
1126
  const gifStream = encoder.createReadStream();
841
1127
  return [{ attachment: gifStream, name: "gif.js" }];
@@ -843,69 +1129,225 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
843
1129
  if ('getBuffer' in outputStream) {
844
1130
  return outputStream.getBuffer();
845
1131
  }
1132
+ throw new Error("createGIF: Unable to get buffer for buffer output.");
846
1133
  } else {
847
1134
  throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
848
1135
  }
849
- } catch (e: any) {
850
- console.error(e.message);
851
- throw e;
1136
+ } catch (error) {
1137
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1138
+ throw new Error(`createGIF failed: ${errorMessage}`);
1139
+ }
1140
+ }
1141
+
1142
+ /**
1143
+ * Validates resize options.
1144
+ * @private
1145
+ * @param options - Resize options to validate
1146
+ */
1147
+ #validateResizeOptions(options: ResizeOptions): void {
1148
+ if (!options || !options.imagePath) {
1149
+ throw new Error("resize: imagePath is required.");
1150
+ }
1151
+ if (options.size) {
1152
+ if (options.size.width !== undefined && (typeof options.size.width !== 'number' || options.size.width <= 0)) {
1153
+ throw new Error("resize: size.width must be a positive number.");
1154
+ }
1155
+ if (options.size.height !== undefined && (typeof options.size.height !== 'number' || options.size.height <= 0)) {
1156
+ throw new Error("resize: size.height must be a positive number.");
1157
+ }
1158
+ }
1159
+ if (options.quality !== undefined && (typeof options.quality !== 'number' || options.quality < 0 || options.quality > 100)) {
1160
+ throw new Error("resize: quality must be a number between 0 and 100.");
1161
+ }
1162
+ }
1163
+
1164
+ async resize(resizeOptions: ResizeOptions): Promise<Buffer> {
1165
+ try {
1166
+ this.#validateResizeOptions(resizeOptions);
1167
+ return await resizingImg(resizeOptions);
1168
+ } catch (error) {
1169
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1170
+ throw new Error(`resize failed: ${errorMessage}`);
1171
+ }
852
1172
  }
853
- }
854
1173
 
855
- async resize(resizeOptions: ResizeOptions) {
856
- return resizingImg(resizeOptions)
1174
+ /**
1175
+ * Validates image converter inputs.
1176
+ * @private
1177
+ * @param source - Image source to validate
1178
+ * @param newExtension - Extension to validate
1179
+ */
1180
+ #validateConverterInputs(source: string, newExtension: string): void {
1181
+ if (!source) {
1182
+ throw new Error("imgConverter: source is required.");
1183
+ }
1184
+ if (!newExtension) {
1185
+ throw new Error("imgConverter: newExtension is required.");
1186
+ }
1187
+ const validExtensions = ['jpeg', 'png', 'webp', 'tiff', 'gif', 'avif', 'heif', 'raw', 'pdf', 'svg'];
1188
+ if (!validExtensions.includes(newExtension.toLowerCase())) {
1189
+ throw new Error(`imgConverter: Invalid extension. Supported: ${validExtensions.join(', ')}`);
1190
+ }
857
1191
  }
858
1192
 
859
- async imgConverter(source: string, newExtension: string) {
860
- return converter(source, newExtension)
1193
+ async imgConverter(source: string, newExtension: string): Promise<Buffer> {
1194
+ try {
1195
+ this.#validateConverterInputs(source, newExtension);
1196
+ return await converter(source, newExtension);
1197
+ } catch (error) {
1198
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1199
+ throw new Error(`imgConverter failed: ${errorMessage}`);
1200
+ }
861
1201
  }
862
1202
 
863
- async effects(source: string, filters: any[]) {
864
- return imgEffects(source, filters)
1203
+ /**
1204
+ * Validates effects inputs.
1205
+ * @private
1206
+ * @param source - Image source to validate
1207
+ * @param filters - Filters array to validate
1208
+ */
1209
+ #validateEffectsInputs(source: string, filters: ImageFilter[]): void {
1210
+ if (!source) {
1211
+ throw new Error("effects: source is required.");
1212
+ }
1213
+ if (!filters || !Array.isArray(filters) || filters.length === 0) {
1214
+ throw new Error("effects: filters array with at least one filter is required.");
1215
+ }
1216
+ }
1217
+
1218
+ async effects(source: string, filters: ImageFilter[]): Promise<Buffer> {
1219
+ try {
1220
+ this.#validateEffectsInputs(source, filters);
1221
+ return await imgEffects(source, filters);
1222
+ } catch (error) {
1223
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1224
+ throw new Error(`effects failed: ${errorMessage}`);
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * Validates color filter inputs.
1230
+ * @private
1231
+ * @param source - Image source to validate
1232
+ * @param opacity - Opacity to validate
1233
+ */
1234
+ #validateColorFilterInputs(source: string, opacity?: number): void {
1235
+ if (!source) {
1236
+ throw new Error("colorsFilter: source is required.");
1237
+ }
1238
+ if (opacity !== undefined && (typeof opacity !== 'number' || opacity < 0 || opacity > 1)) {
1239
+ throw new Error("colorsFilter: opacity must be a number between 0 and 1.");
1240
+ }
865
1241
  }
866
1242
 
867
- async colorsFilter(source: string, filterColor: any, opacity?: number) {
868
- return applyColorFilters(source, filterColor, opacity)
1243
+ async colorsFilter(source: string, filterColor: string | GradientConfig, opacity?: number): Promise<Buffer> {
1244
+ try {
1245
+ this.#validateColorFilterInputs(source, opacity);
1246
+ return await applyColorFilters(source, filterColor, opacity);
1247
+ } catch (error) {
1248
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1249
+ throw new Error(`colorsFilter failed: ${errorMessage}`);
1250
+ }
869
1251
  }
870
1252
 
871
- async colorAnalysis(source: string) {
872
- return detectColors(source)
1253
+ async colorAnalysis(source: string): Promise<{ color: string; frequency: string }[]> {
1254
+ try {
1255
+ if (!source) {
1256
+ throw new Error("colorAnalysis: source is required.");
1257
+ }
1258
+ return await detectColors(source);
1259
+ } catch (error) {
1260
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1261
+ throw new Error(`colorAnalysis failed: ${errorMessage}`);
1262
+ }
873
1263
  }
874
1264
 
875
- async colorsRemover(source: string, colorToRemove: { red: number, green: number, blue: number }) {
876
- return removeColor(source, colorToRemove)
1265
+ async colorsRemover(source: string, colorToRemove: { red: number, green: number, blue: number }): Promise<Buffer | undefined> {
1266
+ try {
1267
+ if (!source) {
1268
+ throw new Error("colorsRemover: source is required.");
1269
+ }
1270
+ if (!colorToRemove || typeof colorToRemove.red !== 'number' || typeof colorToRemove.green !== 'number' || typeof colorToRemove.blue !== 'number') {
1271
+ throw new Error("colorsRemover: colorToRemove must be an object with red, green, and blue properties (0-255).");
1272
+ }
1273
+ if (colorToRemove.red < 0 || colorToRemove.red > 255 ||
1274
+ colorToRemove.green < 0 || colorToRemove.green > 255 ||
1275
+ colorToRemove.blue < 0 || colorToRemove.blue > 255) {
1276
+ throw new Error("colorsRemover: colorToRemove RGB values must be between 0 and 255.");
1277
+ }
1278
+ return await removeColor(source, colorToRemove);
1279
+ } catch (error) {
1280
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1281
+ throw new Error(`colorsRemover failed: ${errorMessage}`);
1282
+ }
877
1283
  }
878
1284
 
879
- async removeBackground(imageURL: string, apiKey: string) {
880
- return bgRemoval(imageURL, apiKey)
1285
+ async removeBackground(imageURL: string, apiKey: string): Promise<Buffer | undefined> {
1286
+ try {
1287
+ if (!imageURL) {
1288
+ throw new Error("removeBackground: imageURL is required.");
1289
+ }
1290
+ if (!apiKey) {
1291
+ throw new Error("removeBackground: apiKey is required.");
1292
+ }
1293
+ return await bgRemoval(imageURL, apiKey);
1294
+ } catch (error) {
1295
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1296
+ throw new Error(`removeBackground failed: ${errorMessage}`);
1297
+ }
1298
+ }
1299
+
1300
+ /**
1301
+ * Validates blend inputs.
1302
+ * @private
1303
+ * @param layers - Layers to validate
1304
+ * @param baseImageBuffer - Base image buffer to validate
1305
+ */
1306
+ #validateBlendInputs(
1307
+ layers: Array<{
1308
+ image: string | Buffer;
1309
+ blendMode: GlobalCompositeOperation;
1310
+ position?: { x: number; y: number };
1311
+ opacity?: number;
1312
+ }>,
1313
+ baseImageBuffer: Buffer
1314
+ ): void {
1315
+ if (!baseImageBuffer || !Buffer.isBuffer(baseImageBuffer)) {
1316
+ throw new Error("blend: baseImageBuffer must be a valid Buffer.");
1317
+ }
1318
+ if (!layers || !Array.isArray(layers) || layers.length === 0) {
1319
+ throw new Error("blend: layers array with at least one layer is required.");
1320
+ }
1321
+ for (const layer of layers) {
1322
+ if (!layer.image) {
1323
+ throw new Error("blend: Each layer must have an image property.");
1324
+ }
1325
+ if (!layer.blendMode) {
1326
+ throw new Error("blend: Each layer must have a blendMode property.");
1327
+ }
1328
+ if (layer.opacity !== undefined && (typeof layer.opacity !== 'number' || layer.opacity < 0 || layer.opacity > 1)) {
1329
+ throw new Error("blend: Layer opacity must be a number between 0 and 1.");
1330
+ }
1331
+ }
881
1332
  }
882
1333
 
883
1334
  async blend(
884
- layers: {
1335
+ layers: Array<{
885
1336
  image: string | Buffer;
886
- blendMode: 'source-over' | 'source-in' | 'source-out' | 'source-atop' |
887
- 'destination-over' | 'destination-in' | 'destination-out' |
888
- 'destination-atop' | 'lighter' | 'copy' | 'xor' |
889
- 'multiply' | 'screen' | 'overlay' | 'darken' |
890
- 'lighten' | 'color-dodge' | 'color-burn' |
891
- 'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
892
- 'hue' | 'saturation' | 'color' | 'luminosity';
1337
+ blendMode: GlobalCompositeOperation;
893
1338
  position?: { x: number; y: number };
894
1339
  opacity?: number;
895
- }[],
1340
+ }>,
896
1341
  baseImageBuffer: Buffer,
897
- defaultBlendMode: 'source-over' | 'source-in' | 'source-out' | 'source-atop' |
898
- 'destination-over' | 'destination-in' | 'destination-out' |
899
- 'destination-atop' | 'lighter' | 'copy' | 'xor' |
900
- 'multiply' | 'screen' | 'overlay' | 'darken' |
901
- 'lighten' | 'color-dodge' | 'color-burn' |
902
- 'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
903
- 'hue' | 'saturation' | 'color' | 'luminosity' = 'source-over'
1342
+ defaultBlendMode: GlobalCompositeOperation = 'source-over'
904
1343
  ): Promise<Buffer> {
905
1344
  try {
1345
+ this.#validateBlendInputs(layers, baseImageBuffer);
1346
+
906
1347
  const baseImage = await loadImage(baseImageBuffer);
907
1348
  const canvas = createCanvas(baseImage.width, baseImage.height);
908
- const ctx = canvas.getContext('2d');
1349
+ const ctx = canvas.getContext('2d') as SKRSContext2D;
1350
+ if (!ctx) throw new Error("Unable to get 2D context");
909
1351
 
910
1352
  ctx.globalCompositeOperation = defaultBlendMode;
911
1353
  ctx.drawImage(baseImage, 0, 0);
@@ -923,29 +1365,52 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
923
1365
 
924
1366
  return canvas.toBuffer('image/png');
925
1367
  } catch (error) {
926
- console.error('Error creating layered composition:', error);
927
- throw new Error('Failed to create layered composition');
1368
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1369
+ throw new Error(`blend failed: ${errorMessage}`);
928
1370
  }
929
1371
  }
930
1372
 
931
1373
 
932
- async createChart(data: any, type: { chartType: string, chartNumber: number}) {
933
-
934
- if (!data || Object.keys(data).length === 0) {
935
- throw new Error('You need to provide datasets to create charts.');
1374
+ /**
1375
+ * Validates chart inputs.
1376
+ * @private
1377
+ * @param data - Chart data to validate
1378
+ * @param type - Chart type configuration to validate
1379
+ */
1380
+ #validateChartInputs(data: unknown, type: { chartType: string; chartNumber: number }): void {
1381
+ if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
1382
+ throw new Error("createChart: data object with datasets is required.");
936
1383
  }
937
-
938
- if (!type || !type.chartNumber || !type.chartType) {
939
- throw new Error('Type arguments are missing.');
1384
+ if (!type || typeof type !== 'object') {
1385
+ throw new Error("createChart: type configuration object is required.");
1386
+ }
1387
+ if (!type.chartType || typeof type.chartType !== 'string') {
1388
+ throw new Error("createChart: type.chartType must be a string.");
940
1389
  }
1390
+ if (typeof type.chartNumber !== 'number' || type.chartNumber < 1) {
1391
+ throw new Error("createChart: type.chartNumber must be a positive number.");
1392
+ }
1393
+ const validChartTypes = ['bar', 'line', 'pie'];
1394
+ if (!validChartTypes.includes(type.chartType.toLowerCase())) {
1395
+ throw new Error(`createChart: Invalid chartType. Supported: ${validChartTypes.join(', ')}`);
1396
+ }
1397
+ }
941
1398
 
942
- const { chartType, chartNumber } = type;
1399
+ async createChart(data: unknown, type: { chartType: string; chartNumber: number }): Promise<Buffer> {
1400
+ try {
1401
+ this.#validateChartInputs(data, type);
1402
+
1403
+ const { chartType, chartNumber } = type;
943
1404
 
944
1405
  switch (chartType.toLowerCase()) {
945
1406
  case 'bar':
946
1407
  switch (chartNumber) {
947
1408
  case 1:
948
- return await verticalBarChart(data);
1409
+ const barResult = await verticalBarChart(data as barChart_1);
1410
+ if (!barResult) {
1411
+ throw new Error("createChart: Failed to generate bar chart.");
1412
+ }
1413
+ return barResult;
949
1414
  case 2:
950
1415
  throw new Error('Type 2 is still under development.');
951
1416
  default:
@@ -954,7 +1419,9 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
954
1419
  case 'line':
955
1420
  switch (chartNumber) {
956
1421
  case 1:
957
- return await lineChart(data);
1422
+ // LineChart expects DataPoint[][] where DataPoint has { label: string; y: number }
1423
+ // Type assertion needed because there are two different DataPoint interfaces
1424
+ return await lineChart(data as unknown as { data: Array<Array<{ label: string; y: number }>>; lineConfig: LineChartConfig });
958
1425
  case 2:
959
1426
  throw new Error('Type 2 is still under development.');
960
1427
  default:
@@ -963,7 +1430,7 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
963
1430
  case 'pie':
964
1431
  switch (chartNumber) {
965
1432
  case 1:
966
- return await pieChart(data);
1433
+ return await pieChart(data as PieChartData);
967
1434
  case 2:
968
1435
  throw new Error('Type 2 is still under development.');
969
1436
  default:
@@ -971,37 +1438,162 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
971
1438
  }
972
1439
  default:
973
1440
  throw new Error(`Unsupported chart type "${chartType}".`);
1441
+ }
1442
+ } catch (error) {
1443
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1444
+ throw new Error(`createChart failed: ${errorMessage}`);
974
1445
  }
975
1446
  }
976
1447
 
977
1448
 
978
- async cropImage(options: cropOptions): Promise<Buffer> {
1449
+ /**
1450
+ * Validates crop options.
1451
+ * @private
1452
+ * @param options - Crop options to validate
1453
+ */
1454
+ #validateCropOptions(options: cropOptions): void {
1455
+ if (!options) {
1456
+ throw new Error("cropImage: options object is required.");
1457
+ }
1458
+ if (!options.imageSource) {
1459
+ throw new Error("cropImage: imageSource is required.");
1460
+ }
1461
+ if (!options.coordinates || !Array.isArray(options.coordinates) || options.coordinates.length < 3) {
1462
+ throw new Error("cropImage: coordinates array with at least 3 points is required.");
1463
+ }
1464
+ if (options.crop !== 'inner' && options.crop !== 'outer') {
1465
+ throw new Error("cropImage: crop must be either 'inner' or 'outer'.");
1466
+ }
1467
+ }
1468
+
1469
+ async cropImage(options: cropOptions): Promise<Buffer> {
979
1470
  try {
980
- if (!options.imageSource) throw new Error('The "imageSource" option is needed. Please provide the path to the image to crop.');
981
- if (!options.coordinates || options.coordinates.length < 3) throw new Error('The "coordinates" option is needed. Please provide coordinates to crop the image.');
1471
+ this.#validateCropOptions(options);
982
1472
 
983
1473
  if (options.crop === 'outer') {
984
- return await cropOuter(options);
985
- } else if (options.crop === 'inner') {
986
- return await cropInner(options);
1474
+ return await cropOuter(options);
987
1475
  } else {
988
- throw new Error('Invalid crop option. Please specify "inner" or "outer".');
1476
+ return await cropInner(options);
989
1477
  }
990
1478
  } catch (error) {
991
- console.error('An error occurred:', error);
992
- throw error;
1479
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1480
+ throw new Error(`cropImage failed: ${errorMessage}`);
993
1481
  }
994
1482
  }
995
1483
 
996
- private async drawImage(ctx: SKRSContext2D, image: ImageProperties): Promise<void> {
1484
+ /**
1485
+ * Extracts a single frame from a video
1486
+ * @private
1487
+ * @param videoSource - Video source (path, URL, or Buffer)
1488
+ * @param frameNumber - Frame number to extract (default: 0)
1489
+ * @returns Buffer containing the frame image
1490
+ */
1491
+ async #extractVideoFrame(videoSource: string | Buffer, frameNumber: number = 0): Promise<Buffer | null> {
1492
+ try {
1493
+ const frameDir = path.join(process.cwd(), '.temp-frames');
1494
+ if (!fs.existsSync(frameDir)) {
1495
+ fs.mkdirSync(frameDir, { recursive: true });
1496
+ }
1497
+
1498
+ const tempVideoPath = path.join(frameDir, `temp-video-${Date.now()}.mp4`);
1499
+ const frameOutputPath = path.join(frameDir, `frame-${Date.now()}.jpg`);
997
1500
 
1501
+ // Handle video source
1502
+ if (Buffer.isBuffer(videoSource)) {
1503
+ fs.writeFileSync(tempVideoPath, videoSource);
1504
+ } else if (typeof videoSource === 'string' && videoSource.startsWith('http')) {
1505
+ const response = await axios({
1506
+ method: 'get',
1507
+ url: videoSource,
1508
+ responseType: 'arraybuffer'
1509
+ });
1510
+ fs.writeFileSync(tempVideoPath, Buffer.from(response.data));
1511
+ } else {
1512
+ // Local file path
1513
+ if (!fs.existsSync(videoSource)) {
1514
+ throw new Error(`Video file not found: ${videoSource}`);
1515
+ }
1516
+ // Use the existing path
1517
+ return await new Promise<Buffer | null>((resolve, reject) => {
1518
+ ffmpeg(videoSource)
1519
+ .seekInput(frameNumber / 1000) // Convert frame to seconds (approximate)
1520
+ .frames(1)
1521
+ .output(frameOutputPath)
1522
+ .on('end', () => {
1523
+ try {
1524
+ const buffer = fs.readFileSync(frameOutputPath);
1525
+ // Cleanup
1526
+ if (fs.existsSync(frameOutputPath)) fs.unlinkSync(frameOutputPath);
1527
+ resolve(buffer);
1528
+ } catch (e) {
1529
+ resolve(null);
1530
+ }
1531
+ })
1532
+ .on('error', (err: any) => {
1533
+ reject(err);
1534
+ })
1535
+ .run();
1536
+ });
1537
+ }
1538
+
1539
+ // Extract frame from temp video
1540
+ return await new Promise<Buffer | null>((resolve, reject) => {
1541
+ ffmpeg(tempVideoPath)
1542
+ .seekInput(frameNumber / 1000)
1543
+ .frames(1)
1544
+ .output(frameOutputPath)
1545
+ .on('end', () => {
1546
+ try {
1547
+ const buffer = fs.readFileSync(frameOutputPath);
1548
+ // Cleanup
1549
+ if (fs.existsSync(tempVideoPath)) fs.unlinkSync(tempVideoPath);
1550
+ if (fs.existsSync(frameOutputPath)) fs.unlinkSync(frameOutputPath);
1551
+ resolve(buffer);
1552
+ } catch (e) {
1553
+ resolve(null);
1554
+ }
1555
+ })
1556
+ .on('error', (err: any) => {
1557
+ // Cleanup on error
1558
+ if (fs.existsSync(tempVideoPath)) fs.unlinkSync(tempVideoPath);
1559
+ if (fs.existsSync(frameOutputPath)) fs.unlinkSync(frameOutputPath);
1560
+ reject(err);
1561
+ })
1562
+ .run();
1563
+ });
1564
+ } catch (error) {
1565
+ console.error('Error extracting video frame:', error);
1566
+ return null;
1567
+ }
1568
+ }
998
1569
 
1570
+ /**
1571
+ * Validates extract frames inputs.
1572
+ * @private
1573
+ * @param videoSource - Video source to validate
1574
+ * @param options - Extract frames options to validate
1575
+ */
1576
+ #validateExtractFramesInputs(videoSource: string | Buffer, options: ExtractFramesOptions): void {
1577
+ if (!videoSource) {
1578
+ throw new Error("extractFrames: videoSource is required.");
1579
+ }
1580
+ if (!options || typeof options !== 'object') {
1581
+ throw new Error("extractFrames: options object is required.");
1582
+ }
1583
+ if (typeof options.interval !== 'number' || options.interval <= 0) {
1584
+ throw new Error("extractFrames: options.interval must be a positive number (milliseconds).");
1585
+ }
1586
+ if (options.outputFormat && !['jpg', 'png'].includes(options.outputFormat)) {
1587
+ throw new Error("extractFrames: outputFormat must be 'jpg' or 'png'.");
1588
+ }
999
1589
  }
1000
-
1001
1590
 
1002
- async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions): Promise<any[]> {
1003
- const frames: any[]= [];
1004
- const frameDir = path.join(__dirname, 'frames');
1591
+ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions): Promise<Array<{ source: string; isRemote: boolean }>> {
1592
+ try {
1593
+ this.#validateExtractFramesInputs(videoSource, options);
1594
+
1595
+ const frames: Array<{ source: string; isRemote: boolean }> = [];
1596
+ const frameDir = path.join(__dirname, 'frames');
1005
1597
 
1006
1598
  if (!fs.existsSync(frameDir)) {
1007
1599
  fs.mkdirSync(frameDir);
@@ -1064,10 +1656,14 @@ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions)
1064
1656
  });
1065
1657
  }
1066
1658
 
1067
- return new Promise((resolve, reject) => {
1068
- processVideoExtraction(videoPath, frames, options, resolve, reject);
1069
- });
1070
- }
1659
+ return new Promise((resolve, reject) => {
1660
+ processVideoExtraction(videoPath, frames, options, resolve, reject);
1661
+ });
1662
+ } catch (error) {
1663
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1664
+ throw new Error(`extractFrames failed: ${errorMessage}`);
1665
+ }
1666
+ }
1071
1667
 
1072
1668
 
1073
1669
 
@@ -1077,13 +1673,45 @@ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions)
1077
1673
 
1078
1674
 
1079
1675
 
1080
- async masking(
1081
- source: string | Buffer | PathLike | Uint8Array,
1082
- maskSource: string | Buffer | PathLike | Uint8Array,
1083
- options: MaskOptions = { type: "alpha" }
1084
- ): Promise<Buffer> {
1085
- const img = await loadImage(source);
1086
- const mask = await loadImage(maskSource);
1676
+ /**
1677
+ * Validates masking inputs.
1678
+ * @private
1679
+ * @param source - Source image to validate
1680
+ * @param maskSource - Mask image to validate
1681
+ * @param options - Mask options to validate
1682
+ */
1683
+ #validateMaskingInputs(
1684
+ source: string | Buffer | PathLike | Uint8Array,
1685
+ maskSource: string | Buffer | PathLike | Uint8Array,
1686
+ options: MaskOptions
1687
+ ): void {
1688
+ if (!source) {
1689
+ throw new Error("masking: source is required.");
1690
+ }
1691
+ if (!maskSource) {
1692
+ throw new Error("masking: maskSource is required.");
1693
+ }
1694
+ if (options.type && !['alpha', 'grayscale', 'color'].includes(options.type)) {
1695
+ throw new Error("masking: type must be 'alpha', 'grayscale', or 'color'.");
1696
+ }
1697
+ if (options.type === 'color' && !options.colorKey) {
1698
+ throw new Error("masking: colorKey is required when type is 'color'.");
1699
+ }
1700
+ if (options.threshold !== undefined && (typeof options.threshold !== 'number' || options.threshold < 0 || options.threshold > 255)) {
1701
+ throw new Error("masking: threshold must be a number between 0 and 255.");
1702
+ }
1703
+ }
1704
+
1705
+ async masking(
1706
+ source: string | Buffer | PathLike | Uint8Array,
1707
+ maskSource: string | Buffer | PathLike | Uint8Array,
1708
+ options: MaskOptions = { type: "alpha" }
1709
+ ): Promise<Buffer> {
1710
+ try {
1711
+ this.#validateMaskingInputs(source, maskSource, options);
1712
+
1713
+ const img = await loadImage(source);
1714
+ const mask = await loadImage(maskSource);
1087
1715
 
1088
1716
  const canvas = createCanvas(img.width, img.height);
1089
1717
  const ctx = canvas.getContext("2d") as SKRSContext2D;
@@ -1115,21 +1743,58 @@ async masking(
1115
1743
 
1116
1744
  if (options.invert) alphaValue = 255 - alphaValue;
1117
1745
 
1118
- imgData.data[i + 3] = alphaValue;
1119
- }
1746
+ imgData.data[i + 3] = alphaValue;
1747
+ }
1120
1748
 
1121
- ctx.putImageData(imgData, 0, 0);
1749
+ ctx.putImageData(imgData, 0, 0);
1122
1750
 
1123
- return canvas.toBuffer("image/png");
1124
- }
1751
+ return canvas.toBuffer("image/png");
1752
+ } catch (error) {
1753
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1754
+ throw new Error(`masking failed: ${errorMessage}`);
1755
+ }
1756
+ }
1125
1757
 
1126
- async gradientBlend(
1127
- source: string | Buffer | PathLike | Uint8Array,
1128
- options: BlendOptions
1129
- ): Promise<Buffer> {
1130
- const img = await loadImage(source);
1131
- const canvas = createCanvas(img.width, img.height);
1132
- const ctx = canvas.getContext("2d") as SKRSContext2D;
1758
+ /**
1759
+ * Validates gradient blend inputs.
1760
+ * @private
1761
+ * @param source - Source image to validate
1762
+ * @param options - Blend options to validate
1763
+ */
1764
+ #validateGradientBlendInputs(source: string | Buffer | PathLike | Uint8Array, options: BlendOptions): void {
1765
+ if (!source) {
1766
+ throw new Error("gradientBlend: source is required.");
1767
+ }
1768
+ if (!options || typeof options !== 'object') {
1769
+ throw new Error("gradientBlend: options object is required.");
1770
+ }
1771
+ if (!options.colors || !Array.isArray(options.colors) || options.colors.length === 0) {
1772
+ throw new Error("gradientBlend: options.colors array with at least one color stop is required.");
1773
+ }
1774
+ if (options.type && !['linear', 'radial', 'conic'].includes(options.type)) {
1775
+ throw new Error("gradientBlend: type must be 'linear', 'radial', or 'conic'.");
1776
+ }
1777
+ for (const colorStop of options.colors) {
1778
+ if (typeof colorStop.stop !== 'number' || colorStop.stop < 0 || colorStop.stop > 1) {
1779
+ throw new Error("gradientBlend: Each color stop must have a stop value between 0 and 1.");
1780
+ }
1781
+ if (!colorStop.color || typeof colorStop.color !== 'string') {
1782
+ throw new Error("gradientBlend: Each color stop must have a valid color string.");
1783
+ }
1784
+ }
1785
+ }
1786
+
1787
+ async gradientBlend(
1788
+ source: string | Buffer | PathLike | Uint8Array,
1789
+ options: BlendOptions
1790
+ ): Promise<Buffer> {
1791
+ try {
1792
+ this.#validateGradientBlendInputs(source, options);
1793
+
1794
+ const img = await loadImage(source);
1795
+ const canvas = createCanvas(img.width, img.height);
1796
+ const ctx = canvas.getContext("2d") as SKRSContext2D;
1797
+ if (!ctx) throw new Error("Unable to get 2D context");
1133
1798
 
1134
1799
  ctx.drawImage(img, 0, 0, img.width, img.height);
1135
1800
 
@@ -1162,38 +1827,78 @@ async gradientBlend(
1162
1827
  ctx.drawImage(mask, 0, 0, img.width, img.height);
1163
1828
  }
1164
1829
 
1165
- ctx.globalCompositeOperation = "source-over";
1830
+ ctx.globalCompositeOperation = "source-over";
1166
1831
 
1167
- return canvas.toBuffer("image/png");
1168
- }
1832
+ return canvas.toBuffer("image/png");
1833
+ } catch (error) {
1834
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1835
+ throw new Error(`gradientBlend failed: ${errorMessage}`);
1836
+ }
1837
+ }
1169
1838
 
1170
- async animate(
1171
- frames: Frame[],
1172
- defaultDuration: number,
1173
- defaultWidth: number = 800,
1174
- defaultHeight: number = 600,
1175
- options?: {
1839
+ /**
1840
+ * Validates animate inputs.
1841
+ * @private
1842
+ * @param frames - Animation frames to validate
1843
+ * @param defaultDuration - Default duration to validate
1844
+ * @param defaultWidth - Default width to validate
1845
+ * @param defaultHeight - Default height to validate
1846
+ * @param options - Animation options to validate
1847
+ */
1848
+ #validateAnimateInputs(
1849
+ frames: Frame[],
1850
+ defaultDuration: number,
1851
+ defaultWidth: number,
1852
+ defaultHeight: number,
1853
+ options?: { gif?: boolean; gifPath?: string; onStart?: () => void; onFrame?: (index: number) => void; onEnd?: () => void }
1854
+ ): void {
1855
+ if (!frames || !Array.isArray(frames) || frames.length === 0) {
1856
+ throw new Error("animate: frames array with at least one frame is required.");
1857
+ }
1858
+ if (typeof defaultDuration !== 'number' || defaultDuration < 0) {
1859
+ throw new Error("animate: defaultDuration must be a non-negative number.");
1860
+ }
1861
+ if (typeof defaultWidth !== 'number' || defaultWidth <= 0) {
1862
+ throw new Error("animate: defaultWidth must be a positive number.");
1863
+ }
1864
+ if (typeof defaultHeight !== 'number' || defaultHeight <= 0) {
1865
+ throw new Error("animate: defaultHeight must be a positive number.");
1866
+ }
1867
+ if (options?.gif && !options.gifPath) {
1868
+ throw new Error("animate: gifPath is required when gif is enabled.");
1869
+ }
1870
+ }
1871
+
1872
+ async animate(
1873
+ frames: Frame[],
1874
+ defaultDuration: number,
1875
+ defaultWidth: number = 800,
1876
+ defaultHeight: number = 600,
1877
+ options?: {
1176
1878
  gif?: boolean;
1177
1879
  gifPath?: string;
1178
1880
  onStart?: () => void;
1179
1881
  onFrame?: (index: number) => void;
1180
1882
  onEnd?: () => void;
1181
- }
1182
- ): Promise<Buffer[] | undefined> {
1183
- const buffers: Buffer[] = [];
1184
- const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
1883
+ }
1884
+ ): Promise<Buffer[] | undefined> {
1885
+ try {
1886
+ this.#validateAnimateInputs(frames, defaultDuration, defaultWidth, defaultHeight, options);
1887
+
1888
+ const buffers: Buffer[] = [];
1889
+ const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
1185
1890
 
1186
- if (options?.onStart) options.onStart();
1891
+ if (options?.onStart) options.onStart();
1187
1892
 
1188
- let encoder: GIFEncoder | null = null;
1189
- let gifStream: fs.WriteStream | null = null;
1893
+ let encoder: GIFEncoder | null = null;
1894
+ let gifStream: fs.WriteStream | null = null;
1190
1895
 
1191
- if (options?.gif) {
1192
- if (!options.gifPath) {
1193
- throw new Error("GIF generation enabled but no gifPath provided.");
1194
- }
1195
- encoder = new GIFEncoder(defaultWidth, defaultHeight);
1196
- gifStream = fs.createWriteStream(options.gifPath);
1896
+ if (options?.gif) {
1897
+ if (!options.gifPath) {
1898
+ throw new Error("animate: gifPath is required when gif is enabled.");
1899
+ }
1900
+ encoder = new GIFEncoder(defaultWidth, defaultHeight);
1901
+ gifStream = fs.createWriteStream(options.gifPath);
1197
1902
  encoder.createReadStream().pipe(gifStream);
1198
1903
  encoder.start();
1199
1904
  encoder.setRepeat(0);
@@ -1298,39 +2003,186 @@ async animate(
1298
2003
  encoder.finish();
1299
2004
  }
1300
2005
 
1301
- if (options?.onEnd) options.onEnd();
2006
+ if (options?.onEnd) options.onEnd();
2007
+
2008
+ return options?.gif ? undefined : buffers;
2009
+ } catch (error) {
2010
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2011
+ throw new Error(`animate failed: ${errorMessage}`);
2012
+ }
2013
+ }
2014
+
2015
+
2016
+
2017
+ /**
2018
+ * Processes multiple operations in parallel
2019
+ * @param operations - Array of operations to process
2020
+ * @returns Array of result buffers
2021
+ */
2022
+ async batch(operations: BatchOperation[]): Promise<Buffer[]> {
2023
+ try {
2024
+ return await batchOperations(this, operations);
2025
+ } catch (error) {
2026
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2027
+ throw new Error(`batch failed: ${errorMessage}`);
2028
+ }
2029
+ }
2030
+
2031
+ /**
2032
+ * Chains multiple operations sequentially
2033
+ * @param operations - Array of operations to chain
2034
+ * @returns Final result buffer
2035
+ */
2036
+ async chain(operations: ChainOperation[]): Promise<Buffer> {
2037
+ try {
2038
+ return await chainOperations(this, operations);
2039
+ } catch (error) {
2040
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2041
+ throw new Error(`chain failed: ${errorMessage}`);
2042
+ }
2043
+ }
2044
+
2045
+ /**
2046
+ * Stitches multiple images together
2047
+ * @param images - Array of image sources
2048
+ * @param options - Stitching options
2049
+ * @returns Stitched image buffer
2050
+ */
2051
+ async stitchImages(images: Array<string | Buffer>, options?: StitchOptions): Promise<Buffer> {
2052
+ try {
2053
+ if (!images || images.length === 0) {
2054
+ throw new Error("stitchImages: images array is required");
2055
+ }
2056
+ return await stitchImagesUtil(images, options);
2057
+ } catch (error) {
2058
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2059
+ throw new Error(`stitchImages failed: ${errorMessage}`);
2060
+ }
2061
+ }
1302
2062
 
1303
- return options?.gif ? undefined : buffers;
1304
- }
2063
+ /**
2064
+ * Creates an image collage
2065
+ * @param images - Array of image sources with optional dimensions
2066
+ * @param layout - Collage layout configuration
2067
+ * @returns Collage image buffer
2068
+ */
2069
+ async createCollage(
2070
+ images: Array<{ source: string | Buffer; width?: number; height?: number }>,
2071
+ layout: CollageLayout
2072
+ ): Promise<Buffer> {
2073
+ try {
2074
+ if (!images || images.length === 0) {
2075
+ throw new Error("createCollage: images array is required");
2076
+ }
2077
+ if (!layout) {
2078
+ throw new Error("createCollage: layout configuration is required");
2079
+ }
2080
+ return await createCollage(images, layout);
2081
+ } catch (error) {
2082
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2083
+ throw new Error(`createCollage failed: ${errorMessage}`);
2084
+ }
2085
+ }
1305
2086
 
2087
+ /**
2088
+ * Compresses an image with quality control
2089
+ * @param image - Image source (path, URL, or Buffer)
2090
+ * @param options - Compression options
2091
+ * @returns Compressed image buffer
2092
+ */
2093
+ async compress(image: string | Buffer, options?: CompressionOptions): Promise<Buffer> {
2094
+ try {
2095
+ if (!image) {
2096
+ throw new Error("compress: image is required");
2097
+ }
2098
+ return await compressImage(image, options);
2099
+ } catch (error) {
2100
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2101
+ throw new Error(`compress failed: ${errorMessage}`);
2102
+ }
2103
+ }
1306
2104
 
2105
+ /**
2106
+ * Extracts color palette from an image
2107
+ * @param image - Image source (path, URL, or Buffer)
2108
+ * @param options - Palette extraction options
2109
+ * @returns Array of colors with percentages
2110
+ */
2111
+ async extractPalette(image: string | Buffer, options?: PaletteOptions): Promise<Array<{ color: string; percentage: number }>> {
2112
+ try {
2113
+ if (!image) {
2114
+ throw new Error("extractPalette: image is required");
2115
+ }
2116
+ return await extractPaletteUtil(image, options);
2117
+ } catch (error) {
2118
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2119
+ throw new Error(`extractPalette failed: ${errorMessage}`);
2120
+ }
2121
+ }
1307
2122
 
1308
- public validHex(hexColor: string): any {
2123
+ /**
2124
+ * Validates a hexadecimal color string.
2125
+ * @param hexColor - Hexadecimal color string to validate (format: #RRGGBB)
2126
+ * @returns True if the color is valid
2127
+ * @throws Error if the color format is invalid
2128
+ *
2129
+ * @example
2130
+ * ```typescript
2131
+ * painter.validHex('#ff0000'); // true
2132
+ * painter.validHex('#FF00FF'); // true
2133
+ * painter.validHex('invalid'); // throws Error
2134
+ * ```
2135
+ */
2136
+ public validHex(hexColor: string): boolean {
2137
+ if (typeof hexColor !== 'string') {
2138
+ throw new Error("validHex: hexColor must be a string.");
2139
+ }
1309
2140
  const hexPattern = /^#[0-9a-fA-F]{6}$/;
1310
2141
  if (!hexPattern.test(hexColor)) {
1311
- throw new Error("Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
1312
- }
1313
- return true
1314
- }
1315
-
1316
- public async outPut(results: any): Promise< void | Buffer | string | Blob | Object | HTMLCanvasElement> {
1317
-
1318
- const formatType: string = this.format?.type || 'buffer';
1319
- switch (formatType) {
1320
- case 'buffer':
1321
- return results;
1322
- case 'url':
1323
- return await url(results);
1324
- case 'dataURL':
1325
- return dataURL(results);
1326
- case 'blob':
1327
- return blob(results);
1328
- case 'base64':
1329
- return base64(results);
1330
- case 'arraybuffer':
1331
- return arrayBuffer(results);
1332
- default:
1333
- throw new Error('Unsupported format');
2142
+ throw new Error("validHex: Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
2143
+ }
2144
+ return true;
2145
+ }
2146
+
2147
+ /**
2148
+ * Converts results to the configured output format.
2149
+ * @param results - Buffer or result to convert
2150
+ * @returns Converted result in the configured format
2151
+ * @throws Error if format is unsupported or conversion fails
2152
+ *
2153
+ * @example
2154
+ * ```typescript
2155
+ * const painter = new ApexPainter({ type: 'base64' });
2156
+ * const result = await painter.createCanvas({ width: 100, height: 100 });
2157
+ * const base64String = await painter.outPut(result.buffer); // Returns base64 string
2158
+ * ```
2159
+ */
2160
+ public async outPut(results: Buffer): Promise<Buffer | string | Blob | ArrayBuffer> {
2161
+ try {
2162
+ if (!Buffer.isBuffer(results)) {
2163
+ throw new Error("outPut: results must be a Buffer.");
2164
+ }
2165
+
2166
+ const formatType: string = this.format?.type || 'buffer';
2167
+ switch (formatType) {
2168
+ case 'buffer':
2169
+ return results;
2170
+ case 'url':
2171
+ return await url(results);
2172
+ case 'dataURL':
2173
+ return dataURL(results);
2174
+ case 'blob':
2175
+ return blob(results);
2176
+ case 'base64':
2177
+ return base64(results);
2178
+ case 'arraybuffer':
2179
+ return arrayBuffer(results);
2180
+ default:
2181
+ throw new Error(`outPut: Unsupported format '${formatType}'. Supported: buffer, url, dataURL, blob, base64, arraybuffer`);
2182
+ }
2183
+ } catch (error) {
2184
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
2185
+ throw new Error(`outPut failed: ${errorMessage}`);
1334
2186
  }
1335
2187
  }
1336
2188