apexify.js 4.9.30 → 5.0.1

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