apexify.js 4.9.28 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +727 -456
  2. package/dist/cjs/Canvas/ApexPainter.d.ts +96 -145
  3. package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
  4. package/dist/cjs/Canvas/ApexPainter.js +1416 -420
  5. package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
  6. package/dist/cjs/Canvas/utils/Charts/charts.d.ts +7 -2
  7. package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +1 -1
  8. package/dist/cjs/Canvas/utils/Charts/charts.js +3 -1
  9. package/dist/cjs/Canvas/utils/Charts/charts.js.map +1 -1
  10. package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts +75 -0
  11. package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
  12. package/dist/cjs/Canvas/utils/Custom/advancedLines.js +263 -0
  13. package/dist/cjs/Canvas/utils/Custom/advancedLines.js.map +1 -0
  14. package/dist/cjs/Canvas/utils/Custom/customLines.d.ts +2 -1
  15. package/dist/cjs/Canvas/utils/Custom/customLines.d.ts.map +1 -1
  16. package/dist/cjs/Canvas/utils/Custom/customLines.js +73 -6
  17. package/dist/cjs/Canvas/utils/Custom/customLines.js.map +1 -1
  18. package/dist/cjs/Canvas/utils/General/batchOperations.d.ts +17 -0
  19. package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -0
  20. package/dist/cjs/Canvas/utils/General/batchOperations.js +88 -0
  21. package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -0
  22. package/dist/cjs/Canvas/utils/General/general functions.d.ts +25 -3
  23. package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
  24. package/dist/cjs/Canvas/utils/General/general functions.js +37 -9
  25. package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
  26. package/dist/cjs/Canvas/utils/General/imageCompression.d.ts +19 -0
  27. package/dist/cjs/Canvas/utils/General/imageCompression.d.ts.map +1 -0
  28. package/dist/cjs/Canvas/utils/General/imageCompression.js +262 -0
  29. package/dist/cjs/Canvas/utils/General/imageCompression.js.map +1 -0
  30. package/dist/cjs/Canvas/utils/General/imageStitching.d.ts +20 -0
  31. package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -0
  32. package/dist/cjs/Canvas/utils/General/imageStitching.js +227 -0
  33. package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -0
  34. package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts +37 -0
  35. package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
  36. package/dist/cjs/Canvas/utils/Image/imageEffects.js +128 -0
  37. package/dist/cjs/Canvas/utils/Image/imageEffects.js.map +1 -0
  38. package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts +67 -0
  39. package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
  40. package/dist/cjs/Canvas/utils/Image/imageMasking.js +276 -0
  41. package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -0
  42. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  43. package/dist/cjs/Canvas/utils/Image/imageProperties.js +181 -2
  44. package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
  45. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
  46. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
  47. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
  48. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +33 -0
  49. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  50. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +237 -32
  51. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  52. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
  53. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
  54. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js +233 -0
  55. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
  56. package/dist/cjs/Canvas/utils/types.d.ts +171 -10
  57. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  58. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  59. package/dist/cjs/Canvas/utils/utils.d.ts +9 -2
  60. package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
  61. package/dist/cjs/Canvas/utils/utils.js +32 -1
  62. package/dist/cjs/Canvas/utils/utils.js.map +1 -1
  63. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  64. package/dist/esm/Canvas/ApexPainter.d.ts +96 -145
  65. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  66. package/dist/esm/Canvas/ApexPainter.js +1416 -420
  67. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  68. package/dist/esm/Canvas/utils/Charts/charts.d.ts +7 -2
  69. package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +1 -1
  70. package/dist/esm/Canvas/utils/Charts/charts.js +3 -1
  71. package/dist/esm/Canvas/utils/Charts/charts.js.map +1 -1
  72. package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts +75 -0
  73. package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
  74. package/dist/esm/Canvas/utils/Custom/advancedLines.js +263 -0
  75. package/dist/esm/Canvas/utils/Custom/advancedLines.js.map +1 -0
  76. package/dist/esm/Canvas/utils/Custom/customLines.d.ts +2 -1
  77. package/dist/esm/Canvas/utils/Custom/customLines.d.ts.map +1 -1
  78. package/dist/esm/Canvas/utils/Custom/customLines.js +73 -6
  79. package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
  80. package/dist/esm/Canvas/utils/General/batchOperations.d.ts +17 -0
  81. package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -0
  82. package/dist/esm/Canvas/utils/General/batchOperations.js +88 -0
  83. package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -0
  84. package/dist/esm/Canvas/utils/General/general functions.d.ts +25 -3
  85. package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
  86. package/dist/esm/Canvas/utils/General/general functions.js +37 -9
  87. package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
  88. package/dist/esm/Canvas/utils/General/imageCompression.d.ts +19 -0
  89. package/dist/esm/Canvas/utils/General/imageCompression.d.ts.map +1 -0
  90. package/dist/esm/Canvas/utils/General/imageCompression.js +262 -0
  91. package/dist/esm/Canvas/utils/General/imageCompression.js.map +1 -0
  92. package/dist/esm/Canvas/utils/General/imageStitching.d.ts +20 -0
  93. package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -0
  94. package/dist/esm/Canvas/utils/General/imageStitching.js +227 -0
  95. package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -0
  96. package/dist/esm/Canvas/utils/Image/imageEffects.d.ts +37 -0
  97. package/dist/esm/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
  98. package/dist/esm/Canvas/utils/Image/imageEffects.js +128 -0
  99. package/dist/esm/Canvas/utils/Image/imageEffects.js.map +1 -0
  100. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts +67 -0
  101. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
  102. package/dist/esm/Canvas/utils/Image/imageMasking.js +276 -0
  103. package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -0
  104. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  105. package/dist/esm/Canvas/utils/Image/imageProperties.js +181 -2
  106. package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
  107. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
  108. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
  109. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
  110. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +33 -0
  111. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  112. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +237 -32
  113. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  114. package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
  115. package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
  116. package/dist/esm/Canvas/utils/Texts/textPathRenderer.js +233 -0
  117. package/dist/esm/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
  118. package/dist/esm/Canvas/utils/types.d.ts +171 -10
  119. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  120. package/dist/esm/Canvas/utils/types.js.map +1 -1
  121. package/dist/esm/Canvas/utils/utils.d.ts +9 -2
  122. package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
  123. package/dist/esm/Canvas/utils/utils.js +32 -1
  124. package/dist/esm/Canvas/utils/utils.js.map +1 -1
  125. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  126. package/lib/Canvas/ApexPainter.ts +1309 -267
  127. package/lib/Canvas/utils/Charts/charts.ts +16 -7
  128. package/lib/Canvas/utils/Custom/advancedLines.ts +335 -0
  129. package/lib/Canvas/utils/Custom/customLines.ts +84 -9
  130. package/lib/Canvas/utils/General/batchOperations.ts +103 -0
  131. package/lib/Canvas/utils/General/general functions.ts +85 -41
  132. package/lib/Canvas/utils/General/imageCompression.ts +316 -0
  133. package/lib/Canvas/utils/General/imageStitching.ts +252 -0
  134. package/lib/Canvas/utils/Image/imageEffects.ts +175 -0
  135. package/lib/Canvas/utils/Image/imageMasking.ts +335 -0
  136. package/lib/Canvas/utils/Image/imageProperties.ts +207 -2
  137. package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +455 -444
  138. package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +274 -36
  139. package/lib/Canvas/utils/Texts/textPathRenderer.ts +320 -0
  140. package/lib/Canvas/utils/types.ts +173 -10
  141. package/lib/Canvas/utils/utils.ts +49 -2
  142. package/package.json +69 -34
@@ -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
+ }
183
275
 
184
- return { buffer: cv.toBuffer('image/png'), canvas };
185
- }
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
+ }
283
+
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);
@@ -556,7 +797,8 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
556
797
  width: strokeWidth = 2,
557
798
  position = 0,
558
799
  blur = 0,
559
- opacity = 1
800
+ opacity = 1,
801
+ style = 'solid'
560
802
  } = stroke;
561
803
 
562
804
  ctx.save();
@@ -573,9 +815,18 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
573
815
 
574
816
  ctx.lineWidth = strokeWidth;
575
817
 
818
+ // Apply stroke style
819
+ this.#applyShapeStrokeStyle(ctx, style, strokeWidth);
820
+
576
821
  // Create stroke path
577
822
  createShapePath(ctx, shapeType, x, y, width, height, shapeProps);
578
- ctx.stroke();
823
+
824
+ // Handle complex stroke styles
825
+ if (style === 'groove' || style === 'ridge' || style === 'double') {
826
+ this.#applyComplexShapeStroke(ctx, style, strokeWidth, color, gradient);
827
+ } else {
828
+ ctx.stroke();
829
+ }
579
830
 
580
831
  ctx.filter = "none";
581
832
  ctx.globalAlpha = 1;
@@ -637,18 +888,34 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
637
888
  * ], canvasBuffer);
638
889
  * ```
639
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
+
640
906
  async createText(textArray: TextProperties | TextProperties[], canvasBuffer: CanvasResults | Buffer): Promise<Buffer> {
641
907
  try {
908
+ // Validate inputs
909
+ if (!canvasBuffer) {
910
+ throw new Error("createText: canvasBuffer is required.");
911
+ }
912
+ this.#validateTextArray(textArray);
913
+
642
914
  // Ensure textArray is an array
643
915
  const textList = Array.isArray(textArray) ? textArray : [textArray];
644
916
 
645
- // Validate each text object
646
- for (const textProps of textList) {
647
- this.#validateTextProperties(textProps);
648
- }
649
-
650
917
  // Load existing canvas buffer
651
- let existingImage: any;
918
+ let existingImage: Image;
652
919
 
653
920
  if (Buffer.isBuffer(canvasBuffer)) {
654
921
  existingImage = await loadImage(canvasBuffer);
@@ -680,21 +947,44 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
680
947
 
681
948
  return canvas.toBuffer("image/png");
682
949
  } catch (error) {
683
- console.error("Error creating text:", error);
684
- 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}`);
685
952
  }
686
953
  }
687
954
 
688
955
 
689
956
 
690
- async createCustom(options: CustomOptions[], buffer: CanvasResults | Buffer, ): Promise<Buffer> {
691
- 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
+ }
692
976
 
693
- if (!Array.isArray(options)) {
694
- 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.");
695
982
  }
983
+ this.#validateCustomOptions(options);
984
+
985
+ const opts = Array.isArray(options) ? options : [options];
696
986
 
697
- let existingImage: any;
987
+ let existingImage: Image;
698
988
 
699
989
  if (Buffer.isBuffer(buffer)) {
700
990
  existingImage = await loadImage(buffer);
@@ -713,17 +1003,49 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
713
1003
 
714
1004
  ctx.drawImage(existingImage, 0, 0);
715
1005
 
716
- customLines(ctx, options);
1006
+ await customLines(ctx, opts);
717
1007
 
718
1008
  return canvas.toBuffer("image/png");
719
1009
  } catch (error) {
720
- console.error("Error creating custom image:", error);
721
- 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}`);
722
1012
  }
723
- }
1013
+ }
724
1014
 
725
- async createGIF(gifFrames: { background: string; duration: number }[], options: GIFOptions): Promise<GIFResults | any> {
726
- 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) {
727
1049
  const canvas = createCanvas(targetWidth, targetHeight);
728
1050
  const ctx = canvas.getContext("2d");
729
1051
  ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
@@ -747,38 +1069,10 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
747
1069
  getBuffer: function (): Buffer {
748
1070
  return Buffer.concat(chunks);
749
1071
  }
750
- } as any;
1072
+ } as PassThrough & { getBuffer: () => Buffer };
751
1073
  }
752
1074
 
753
- function validateOptions(options: GIFOptions) {
754
- if (options.outputFormat === "file" && !options.outputFile) {
755
- throw new Error("Output file path is required when using file output format.");
756
- }
757
- if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
758
- throw new Error("Repeat must be a non-negative number or undefined.");
759
- }
760
- if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
761
- throw new Error("Quality must be a number between 1 and 20 or undefined.");
762
- }
763
- if (options.watermark && typeof options.watermark.enable !== "boolean") {
764
- throw new Error("Watermark must be a boolean or undefined.");
765
- }
766
- if (options.textOverlay) {
767
- const textOptions = options.textOverlay;
768
- if (!textOptions.text || typeof textOptions.text !== "string") {
769
- throw new Error("Text overlay text is required and must be a string.");
770
- }
771
- if (textOptions.fontSize !== undefined && (!Number.isInteger(textOptions.fontSize) || textOptions.fontSize <= 0)) {
772
- throw new Error("Text overlay fontSize must be a positive integer or undefined.");
773
- }
774
- if (textOptions.fontColor !== undefined && typeof textOptions.fontColor !== "string") {
775
- throw new Error("Text overlay fontColor must be a string or undefined.");
776
- }
777
- }
778
- }
779
-
780
- try {
781
- validateOptions(options);
1075
+ // Validation is done in #validateGIFOptions
782
1076
 
783
1077
  const canvasWidth = options.width || 1200;
784
1078
  const canvasHeight = options.height || 1200;
@@ -793,7 +1087,8 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
793
1087
  encoder.setQuality(options.quality || 10);
794
1088
 
795
1089
  const canvas = createCanvas(canvasWidth, canvasHeight);
796
- 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");
797
1092
 
798
1093
  for (const frame of gifFrames) {
799
1094
  const image = await loadImage(frame.background);
@@ -814,18 +1109,19 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
814
1109
  }
815
1110
 
816
1111
  encoder.setDelay(frame.duration);
817
- encoder.addFrame(ctx);
1112
+ encoder.addFrame(ctx as unknown as CanvasRenderingContext2D);
818
1113
  }
819
1114
 
820
1115
  encoder.finish();
821
1116
  outputStream.end();
822
1117
 
823
1118
  if (options.outputFormat === "file") {
824
- await new Promise((resolve) => outputStream.on("finish", resolve));
1119
+ await new Promise<void>((resolve) => outputStream.on("finish", () => resolve()));
825
1120
  } else if (options.outputFormat === "base64") {
826
1121
  if ('getBuffer' in outputStream) {
827
1122
  return outputStream.getBuffer().toString("base64");
828
1123
  }
1124
+ throw new Error("createGIF: Unable to get buffer for base64 output.");
829
1125
  } else if (options.outputFormat === "attachment") {
830
1126
  const gifStream = encoder.createReadStream();
831
1127
  return [{ attachment: gifStream, name: "gif.js" }];
@@ -833,69 +1129,225 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
833
1129
  if ('getBuffer' in outputStream) {
834
1130
  return outputStream.getBuffer();
835
1131
  }
1132
+ throw new Error("createGIF: Unable to get buffer for buffer output.");
836
1133
  } else {
837
1134
  throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
838
1135
  }
839
- } catch (e: any) {
840
- console.error(e.message);
841
- 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
+ }
1172
+ }
1173
+
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
+ }
1191
+ }
1192
+
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
+ }
1201
+ }
1202
+
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
+ }
842
1216
  }
843
- }
844
1217
 
845
- async resize(resizeOptions: ResizeOptions) {
846
- return resizingImg(resizeOptions)
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
+ }
847
1226
  }
848
1227
 
849
- async imgConverter(source: string, newExtension: string) {
850
- return converter(source, newExtension)
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
+ }
851
1241
  }
852
1242
 
853
- async effects(source: string, filters: any[]) {
854
- return imgEffects(source, filters)
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
+ }
855
1251
  }
856
1252
 
857
- async colorsFilter(source: string, filterColor: any, opacity?: number) {
858
- return applyColorFilters(source, filterColor, opacity)
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
+ }
859
1263
  }
860
1264
 
861
- async colorAnalysis(source: string) {
862
- return detectColors(source)
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
+ }
863
1283
  }
864
1284
 
865
- async colorsRemover(source: string, colorToRemove: { red: number, green: number, blue: number }) {
866
- return removeColor(source, colorToRemove)
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
+ }
867
1298
  }
868
1299
 
869
- async removeBackground(imageURL: string, apiKey: string) {
870
- return bgRemoval(imageURL, apiKey)
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
+ }
871
1332
  }
872
1333
 
873
1334
  async blend(
874
- layers: {
1335
+ layers: Array<{
875
1336
  image: string | Buffer;
876
- blendMode: 'source-over' | 'source-in' | 'source-out' | 'source-atop' |
877
- 'destination-over' | 'destination-in' | 'destination-out' |
878
- 'destination-atop' | 'lighter' | 'copy' | 'xor' |
879
- 'multiply' | 'screen' | 'overlay' | 'darken' |
880
- 'lighten' | 'color-dodge' | 'color-burn' |
881
- 'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
882
- 'hue' | 'saturation' | 'color' | 'luminosity';
1337
+ blendMode: GlobalCompositeOperation;
883
1338
  position?: { x: number; y: number };
884
1339
  opacity?: number;
885
- }[],
1340
+ }>,
886
1341
  baseImageBuffer: Buffer,
887
- defaultBlendMode: 'source-over' | 'source-in' | 'source-out' | 'source-atop' |
888
- 'destination-over' | 'destination-in' | 'destination-out' |
889
- 'destination-atop' | 'lighter' | 'copy' | 'xor' |
890
- 'multiply' | 'screen' | 'overlay' | 'darken' |
891
- 'lighten' | 'color-dodge' | 'color-burn' |
892
- 'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
893
- 'hue' | 'saturation' | 'color' | 'luminosity' = 'source-over'
1342
+ defaultBlendMode: GlobalCompositeOperation = 'source-over'
894
1343
  ): Promise<Buffer> {
895
1344
  try {
1345
+ this.#validateBlendInputs(layers, baseImageBuffer);
1346
+
896
1347
  const baseImage = await loadImage(baseImageBuffer);
897
1348
  const canvas = createCanvas(baseImage.width, baseImage.height);
898
- const ctx = canvas.getContext('2d');
1349
+ const ctx = canvas.getContext('2d') as SKRSContext2D;
1350
+ if (!ctx) throw new Error("Unable to get 2D context");
899
1351
 
900
1352
  ctx.globalCompositeOperation = defaultBlendMode;
901
1353
  ctx.drawImage(baseImage, 0, 0);
@@ -913,29 +1365,52 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
913
1365
 
914
1366
  return canvas.toBuffer('image/png');
915
1367
  } catch (error) {
916
- console.error('Error creating layered composition:', error);
917
- 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}`);
918
1370
  }
919
1371
  }
920
1372
 
921
1373
 
922
- async createChart(data: any, type: { chartType: string, chartNumber: number}) {
923
-
924
- if (!data || Object.keys(data).length === 0) {
925
- 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.");
926
1383
  }
927
-
928
- if (!type || !type.chartNumber || !type.chartType) {
929
- 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.");
1389
+ }
1390
+ if (typeof type.chartNumber !== 'number' || type.chartNumber < 1) {
1391
+ throw new Error("createChart: type.chartNumber must be a positive number.");
930
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
+ }
1398
+
1399
+ async createChart(data: unknown, type: { chartType: string; chartNumber: number }): Promise<Buffer> {
1400
+ try {
1401
+ this.#validateChartInputs(data, type);
931
1402
 
932
- const { chartType, chartNumber } = type;
1403
+ const { chartType, chartNumber } = type;
933
1404
 
934
1405
  switch (chartType.toLowerCase()) {
935
1406
  case 'bar':
936
1407
  switch (chartNumber) {
937
1408
  case 1:
938
- 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;
939
1414
  case 2:
940
1415
  throw new Error('Type 2 is still under development.');
941
1416
  default:
@@ -944,7 +1419,9 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
944
1419
  case 'line':
945
1420
  switch (chartNumber) {
946
1421
  case 1:
947
- 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 });
948
1425
  case 2:
949
1426
  throw new Error('Type 2 is still under development.');
950
1427
  default:
@@ -953,7 +1430,7 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
953
1430
  case 'pie':
954
1431
  switch (chartNumber) {
955
1432
  case 1:
956
- return await pieChart(data);
1433
+ return await pieChart(data as PieChartData);
957
1434
  case 2:
958
1435
  throw new Error('Type 2 is still under development.');
959
1436
  default:
@@ -961,37 +1438,162 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
961
1438
  }
962
1439
  default:
963
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}`);
964
1445
  }
965
1446
  }
966
1447
 
967
1448
 
968
- 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> {
969
1470
  try {
970
- if (!options.imageSource) throw new Error('The "imageSource" option is needed. Please provide the path to the image to crop.');
971
- 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);
972
1472
 
973
1473
  if (options.crop === 'outer') {
974
- return await cropOuter(options);
975
- } else if (options.crop === 'inner') {
976
- return await cropInner(options);
1474
+ return await cropOuter(options);
977
1475
  } else {
978
- throw new Error('Invalid crop option. Please specify "inner" or "outer".');
1476
+ return await cropInner(options);
979
1477
  }
980
1478
  } catch (error) {
981
- console.error('An error occurred:', error);
982
- throw error;
1479
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
1480
+ throw new Error(`cropImage failed: ${errorMessage}`);
983
1481
  }
984
1482
  }
985
1483
 
986
- 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`);
987
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
+ }
988
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
+ }
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
+ }
989
1589
  }
990
-
991
1590
 
992
- async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions): Promise<any[]> {
993
- const frames: any[]= [];
994
- 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');
995
1597
 
996
1598
  if (!fs.existsSync(frameDir)) {
997
1599
  fs.mkdirSync(frameDir);
@@ -1054,10 +1656,14 @@ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions)
1054
1656
  });
1055
1657
  }
1056
1658
 
1057
- return new Promise((resolve, reject) => {
1058
- processVideoExtraction(videoPath, frames, options, resolve, reject);
1059
- });
1060
- }
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
+ }
1061
1667
 
1062
1668
 
1063
1669
 
@@ -1067,13 +1673,45 @@ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions)
1067
1673
 
1068
1674
 
1069
1675
 
1070
- async masking(
1071
- source: string | Buffer | PathLike | Uint8Array,
1072
- maskSource: string | Buffer | PathLike | Uint8Array,
1073
- options: MaskOptions = { type: "alpha" }
1074
- ): Promise<Buffer> {
1075
- const img = await loadImage(source);
1076
- 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);
1077
1715
 
1078
1716
  const canvas = createCanvas(img.width, img.height);
1079
1717
  const ctx = canvas.getContext("2d") as SKRSContext2D;
@@ -1105,21 +1743,58 @@ async masking(
1105
1743
 
1106
1744
  if (options.invert) alphaValue = 255 - alphaValue;
1107
1745
 
1108
- imgData.data[i + 3] = alphaValue;
1109
- }
1746
+ imgData.data[i + 3] = alphaValue;
1747
+ }
1110
1748
 
1111
- ctx.putImageData(imgData, 0, 0);
1749
+ ctx.putImageData(imgData, 0, 0);
1112
1750
 
1113
- return canvas.toBuffer("image/png");
1114
- }
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
+ }
1115
1757
 
1116
- async gradientBlend(
1117
- source: string | Buffer | PathLike | Uint8Array,
1118
- options: BlendOptions
1119
- ): Promise<Buffer> {
1120
- const img = await loadImage(source);
1121
- const canvas = createCanvas(img.width, img.height);
1122
- 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");
1123
1798
 
1124
1799
  ctx.drawImage(img, 0, 0, img.width, img.height);
1125
1800
 
@@ -1152,38 +1827,78 @@ async gradientBlend(
1152
1827
  ctx.drawImage(mask, 0, 0, img.width, img.height);
1153
1828
  }
1154
1829
 
1155
- ctx.globalCompositeOperation = "source-over";
1830
+ ctx.globalCompositeOperation = "source-over";
1831
+
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
+ }
1156
1838
 
1157
- return canvas.toBuffer("image/png");
1158
- }
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
+ }
1159
1871
 
1160
- async animate(
1161
- frames: Frame[],
1162
- defaultDuration: number,
1163
- defaultWidth: number = 800,
1164
- defaultHeight: number = 600,
1165
- options?: {
1872
+ async animate(
1873
+ frames: Frame[],
1874
+ defaultDuration: number,
1875
+ defaultWidth: number = 800,
1876
+ defaultHeight: number = 600,
1877
+ options?: {
1166
1878
  gif?: boolean;
1167
1879
  gifPath?: string;
1168
1880
  onStart?: () => void;
1169
1881
  onFrame?: (index: number) => void;
1170
1882
  onEnd?: () => void;
1171
- }
1172
- ): Promise<Buffer[] | undefined> {
1173
- const buffers: Buffer[] = [];
1174
- 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;
1175
1890
 
1176
- if (options?.onStart) options.onStart();
1891
+ if (options?.onStart) options.onStart();
1177
1892
 
1178
- let encoder: GIFEncoder | null = null;
1179
- let gifStream: fs.WriteStream | null = null;
1893
+ let encoder: GIFEncoder | null = null;
1894
+ let gifStream: fs.WriteStream | null = null;
1180
1895
 
1181
- if (options?.gif) {
1182
- if (!options.gifPath) {
1183
- throw new Error("GIF generation enabled but no gifPath provided.");
1184
- }
1185
- encoder = new GIFEncoder(defaultWidth, defaultHeight);
1186
- 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);
1187
1902
  encoder.createReadStream().pipe(gifStream);
1188
1903
  encoder.start();
1189
1904
  encoder.setRepeat(0);
@@ -1288,39 +2003,366 @@ async animate(
1288
2003
  encoder.finish();
1289
2004
  }
1290
2005
 
1291
- if (options?.onEnd) options.onEnd();
2006
+ if (options?.onEnd) options.onEnd();
1292
2007
 
1293
- return options?.gif ? undefined : buffers;
1294
- }
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
+ }
1295
2014
 
1296
2015
 
1297
2016
 
1298
- public validHex(hexColor: string): any {
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
+ }
2062
+
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
+ }
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
+ }
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
+ }
2122
+
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
+ }
1299
2140
  const hexPattern = /^#[0-9a-fA-F]{6}$/;
1300
2141
  if (!hexPattern.test(hexColor)) {
1301
- throw new Error("Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
1302
- }
1303
- return true
1304
- }
1305
-
1306
- public async outPut(results: any): Promise< void | Buffer | string | Blob | Object | HTMLCanvasElement> {
1307
-
1308
- const formatType: string = this.format?.type || 'buffer';
1309
- switch (formatType) {
1310
- case 'buffer':
1311
- return results;
1312
- case 'url':
1313
- return await url(results);
1314
- case 'dataURL':
1315
- return dataURL(results);
1316
- case 'blob':
1317
- return blob(results);
1318
- case 'base64':
1319
- return base64(results);
1320
- case 'arraybuffer':
1321
- return arrayBuffer(results);
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}`);
2186
+ }
2187
+ }
2188
+
2189
+ /**
2190
+ * Applies stroke style to shape context
2191
+ * @private
2192
+ * @param ctx - Canvas 2D context
2193
+ * @param style - Stroke style type
2194
+ * @param width - Stroke width for calculating dash patterns
2195
+ */
2196
+ #applyShapeStrokeStyle(
2197
+ ctx: SKRSContext2D,
2198
+ style: 'solid' | 'dashed' | 'dotted' | 'groove' | 'ridge' | 'double',
2199
+ width: number
2200
+ ): void {
2201
+ switch (style) {
2202
+ case 'solid':
2203
+ ctx.setLineDash([]);
2204
+ ctx.lineCap = 'butt';
2205
+ ctx.lineJoin = 'miter';
2206
+ break;
2207
+
2208
+ case 'dashed':
2209
+ ctx.setLineDash([width * 3, width * 2]);
2210
+ ctx.lineCap = 'butt';
2211
+ ctx.lineJoin = 'miter';
2212
+ break;
2213
+
2214
+ case 'dotted':
2215
+ ctx.setLineDash([width, width]);
2216
+ ctx.lineCap = 'round';
2217
+ ctx.lineJoin = 'round';
2218
+ break;
2219
+
2220
+ case 'groove':
2221
+ case 'ridge':
2222
+ case 'double':
2223
+ ctx.setLineDash([]);
2224
+ ctx.lineCap = 'butt';
2225
+ ctx.lineJoin = 'miter';
2226
+ break;
2227
+
1322
2228
  default:
1323
- throw new Error('Unsupported format');
2229
+ ctx.setLineDash([]);
2230
+ ctx.lineCap = 'butt';
2231
+ ctx.lineJoin = 'miter';
2232
+ break;
2233
+ }
2234
+ }
2235
+
2236
+ /**
2237
+ * Applies complex shape stroke styles that require multiple passes
2238
+ * @private
2239
+ * @param ctx - Canvas 2D context
2240
+ * @param style - Complex stroke style type
2241
+ * @param width - Stroke width
2242
+ * @param color - Base stroke color
2243
+ * @param gradient - Optional gradient
2244
+ */
2245
+ #applyComplexShapeStroke(
2246
+ ctx: SKRSContext2D,
2247
+ style: 'groove' | 'ridge' | 'double',
2248
+ width: number,
2249
+ color: string,
2250
+ gradient: any
2251
+ ): void {
2252
+ const halfWidth = width / 2;
2253
+
2254
+ switch (style) {
2255
+ case 'groove':
2256
+ // Groove: dark outer, light inner
2257
+ ctx.lineWidth = halfWidth;
2258
+
2259
+ // Outer dark stroke
2260
+ if (gradient) {
2261
+ const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
2262
+ ctx.strokeStyle = gstroke as any;
2263
+ } else {
2264
+ ctx.strokeStyle = this.#darkenColor(color, 0.3);
2265
+ }
2266
+ ctx.stroke();
2267
+
2268
+ // Inner light stroke
2269
+ ctx.lineWidth = halfWidth;
2270
+ if (gradient) {
2271
+ const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
2272
+ ctx.strokeStyle = gstroke as any;
2273
+ } else {
2274
+ ctx.strokeStyle = this.#lightenColor(color, 0.3);
2275
+ }
2276
+ ctx.stroke();
2277
+ break;
2278
+
2279
+ case 'ridge':
2280
+ // Ridge: light outer, dark inner
2281
+ ctx.lineWidth = halfWidth;
2282
+
2283
+ // Outer light stroke
2284
+ if (gradient) {
2285
+ const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
2286
+ ctx.strokeStyle = gstroke as any;
2287
+ } else {
2288
+ ctx.strokeStyle = this.#lightenColor(color, 0.3);
2289
+ }
2290
+ ctx.stroke();
2291
+
2292
+ // Inner dark stroke
2293
+ ctx.lineWidth = halfWidth;
2294
+ if (gradient) {
2295
+ const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
2296
+ ctx.strokeStyle = gstroke as any;
2297
+ } else {
2298
+ ctx.strokeStyle = this.#darkenColor(color, 0.3);
2299
+ }
2300
+ ctx.stroke();
2301
+ break;
2302
+
2303
+ case 'double':
2304
+ // Double: two parallel strokes
2305
+ ctx.lineWidth = halfWidth;
2306
+
2307
+ // First stroke (outer)
2308
+ if (gradient) {
2309
+ const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
2310
+ ctx.strokeStyle = gstroke as any;
2311
+ } else {
2312
+ ctx.strokeStyle = color;
2313
+ }
2314
+ ctx.stroke();
2315
+
2316
+ // Second stroke (inner)
2317
+ ctx.lineWidth = halfWidth;
2318
+ if (gradient) {
2319
+ const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
2320
+ ctx.strokeStyle = gstroke as any;
2321
+ } else {
2322
+ ctx.strokeStyle = color;
2323
+ }
2324
+ ctx.stroke();
2325
+ break;
2326
+ }
2327
+ }
2328
+
2329
+ /**
2330
+ * Darkens a color by a factor
2331
+ * @private
2332
+ * @param color - Color string
2333
+ * @param factor - Darkening factor (0-1)
2334
+ * @returns Darkened color string
2335
+ */
2336
+ #darkenColor(color: string, factor: number): string {
2337
+ // Simple darkening for hex colors
2338
+ if (color.startsWith('#')) {
2339
+ const hex = color.slice(1);
2340
+ const num = parseInt(hex, 16);
2341
+ const r = Math.max(0, Math.floor((num >> 16) * (1 - factor)));
2342
+ const g = Math.max(0, Math.floor(((num >> 8) & 0x00FF) * (1 - factor)));
2343
+ const b = Math.max(0, Math.floor((num & 0x0000FF) * (1 - factor)));
2344
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
2345
+ }
2346
+ return color; // Return original for non-hex colors
2347
+ }
2348
+
2349
+ /**
2350
+ * Lightens a color by a factor
2351
+ * @private
2352
+ * @param color - Color string
2353
+ * @param factor - Lightening factor (0-1)
2354
+ * @returns Lightened color string
2355
+ */
2356
+ #lightenColor(color: string, factor: number): string {
2357
+ // Simple lightening for hex colors
2358
+ if (color.startsWith('#')) {
2359
+ const hex = color.slice(1);
2360
+ const num = parseInt(hex, 16);
2361
+ const r = Math.min(255, Math.floor((num >> 16) + (255 - (num >> 16)) * factor));
2362
+ const g = Math.min(255, Math.floor(((num >> 8) & 0x00FF) + (255 - ((num >> 8) & 0x00FF)) * factor));
2363
+ const b = Math.min(255, Math.floor((num & 0x0000FF) + (255 - (num & 0x0000FF)) * factor));
2364
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
1324
2365
  }
2366
+ return color; // Return original for non-hex colors
1325
2367
  }
1326
2368
  }