apexify.js 4.9.26 → 4.9.28

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 (108) hide show
  1. package/README.md +437 -47
  2. package/dist/cjs/Canvas/ApexPainter.d.ts +122 -78
  3. package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
  4. package/dist/cjs/Canvas/ApexPainter.js +461 -352
  5. package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
  6. package/dist/cjs/Canvas/utils/Background/bg.d.ts +23 -11
  7. package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
  8. package/dist/cjs/Canvas/utils/Background/bg.js +174 -107
  9. package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
  10. package/dist/cjs/Canvas/utils/Custom/customLines.js +2 -2
  11. package/dist/cjs/Canvas/utils/Custom/customLines.js.map +1 -1
  12. package/dist/cjs/Canvas/utils/Image/imageFilters.d.ts +11 -0
  13. package/dist/cjs/Canvas/utils/Image/imageFilters.d.ts.map +1 -0
  14. package/dist/cjs/Canvas/utils/Image/imageFilters.js +307 -0
  15. package/dist/cjs/Canvas/utils/Image/imageFilters.js.map +1 -0
  16. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts +47 -112
  17. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  18. package/dist/cjs/Canvas/utils/Image/imageProperties.js +229 -560
  19. package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
  20. package/dist/cjs/Canvas/utils/Image/professionalImageFilters.d.ts +11 -0
  21. package/dist/cjs/Canvas/utils/Image/professionalImageFilters.d.ts.map +1 -0
  22. package/dist/cjs/Canvas/utils/Image/professionalImageFilters.js +351 -0
  23. package/dist/cjs/Canvas/utils/Image/professionalImageFilters.js.map +1 -0
  24. package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.d.ts +11 -0
  25. package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.d.ts.map +1 -0
  26. package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.js +215 -0
  27. package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.js.map +1 -0
  28. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts +71 -0
  29. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -0
  30. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +392 -0
  31. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -0
  32. package/dist/cjs/Canvas/utils/Shapes/shapes.d.ts +29 -0
  33. package/dist/cjs/Canvas/utils/Shapes/shapes.d.ts.map +1 -0
  34. package/dist/cjs/Canvas/utils/Shapes/shapes.js +334 -0
  35. package/dist/cjs/Canvas/utils/Shapes/shapes.js.map +1 -0
  36. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +127 -0
  37. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -0
  38. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +365 -0
  39. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -0
  40. package/dist/cjs/Canvas/utils/types.d.ts +227 -131
  41. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  42. package/dist/cjs/Canvas/utils/types.js +0 -1
  43. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  44. package/dist/cjs/Canvas/utils/utils.d.ts +7 -4
  45. package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
  46. package/dist/cjs/Canvas/utils/utils.js +17 -7
  47. package/dist/cjs/Canvas/utils/utils.js.map +1 -1
  48. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  49. package/dist/esm/Canvas/ApexPainter.d.ts +122 -78
  50. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  51. package/dist/esm/Canvas/ApexPainter.js +461 -352
  52. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  53. package/dist/esm/Canvas/utils/Background/bg.d.ts +23 -11
  54. package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
  55. package/dist/esm/Canvas/utils/Background/bg.js +174 -107
  56. package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
  57. package/dist/esm/Canvas/utils/Custom/customLines.js +2 -2
  58. package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
  59. package/dist/esm/Canvas/utils/Image/imageFilters.d.ts +11 -0
  60. package/dist/esm/Canvas/utils/Image/imageFilters.d.ts.map +1 -0
  61. package/dist/esm/Canvas/utils/Image/imageFilters.js +307 -0
  62. package/dist/esm/Canvas/utils/Image/imageFilters.js.map +1 -0
  63. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts +47 -112
  64. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  65. package/dist/esm/Canvas/utils/Image/imageProperties.js +229 -560
  66. package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
  67. package/dist/esm/Canvas/utils/Image/professionalImageFilters.d.ts +11 -0
  68. package/dist/esm/Canvas/utils/Image/professionalImageFilters.d.ts.map +1 -0
  69. package/dist/esm/Canvas/utils/Image/professionalImageFilters.js +351 -0
  70. package/dist/esm/Canvas/utils/Image/professionalImageFilters.js.map +1 -0
  71. package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.d.ts +11 -0
  72. package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.d.ts.map +1 -0
  73. package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.js +215 -0
  74. package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.js.map +1 -0
  75. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts +71 -0
  76. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -0
  77. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +392 -0
  78. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -0
  79. package/dist/esm/Canvas/utils/Shapes/shapes.d.ts +29 -0
  80. package/dist/esm/Canvas/utils/Shapes/shapes.d.ts.map +1 -0
  81. package/dist/esm/Canvas/utils/Shapes/shapes.js +334 -0
  82. package/dist/esm/Canvas/utils/Shapes/shapes.js.map +1 -0
  83. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +127 -0
  84. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -0
  85. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +365 -0
  86. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -0
  87. package/dist/esm/Canvas/utils/types.d.ts +227 -131
  88. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  89. package/dist/esm/Canvas/utils/types.js +0 -1
  90. package/dist/esm/Canvas/utils/types.js.map +1 -1
  91. package/dist/esm/Canvas/utils/utils.d.ts +7 -4
  92. package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
  93. package/dist/esm/Canvas/utils/utils.js +17 -7
  94. package/dist/esm/Canvas/utils/utils.js.map +1 -1
  95. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  96. package/lib/Canvas/ApexPainter.ts +1325 -1218
  97. package/lib/Canvas/utils/Background/bg.ts +247 -173
  98. package/lib/Canvas/utils/Custom/customLines.ts +3 -3
  99. package/lib/Canvas/utils/Image/imageFilters.ts +356 -0
  100. package/lib/Canvas/utils/Image/imageProperties.ts +322 -775
  101. package/lib/Canvas/utils/Image/professionalImageFilters.ts +391 -0
  102. package/lib/Canvas/utils/Image/simpleProfessionalFilters.ts +229 -0
  103. package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +444 -0
  104. package/lib/Canvas/utils/Shapes/shapes.ts +528 -0
  105. package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +478 -0
  106. package/lib/Canvas/utils/types.ts +301 -117
  107. package/lib/Canvas/utils/utils.ts +85 -72
  108. package/package.json +106 -188
@@ -1,1219 +1,1326 @@
1
- import { createCanvas, loadImage, GlobalFonts, Image, SKRSContext2D } from "@napi-rs/canvas";
2
- import GIFEncoder from "gifencoder";
3
- import ffmpeg from 'fluent-ffmpeg';
4
- import { PassThrough} from "stream";
5
- import axios from 'axios';
6
- import fs, { PathLike } from "fs";
7
- import path from "path";
8
- import { OutputFormat, CanvasConfig, TextObject, ImageProperties, GIFOptions, GIFResults, CustomOptions, cropOptions,
9
- drawBackgroundGradient, drawBackgroundColor, customBackground, customLines, applyRotation, applyStroke,
10
- applyShadow, imageRadius, drawShape, drawText, converter, resizingImg, applyColorFilters, imgEffects,verticalBarChart, pieChart,
11
- lineChart, cropInner, cropOuter, bgRemoval, detectColors, removeColor, dataURL, base64, arrayBuffer, blob, url, GradientConfig, Frame,
12
- PatternOptions, ExtractFramesOptions, backgroundRadius, applyZoom, ResizeOptions, applyPerspective, MaskOptions, BlendOptions
13
- } from "./utils/utils";
14
-
15
- interface CanvasResults {
16
- buffer: Buffer;
17
- canvas: CanvasConfig;
18
- }
19
-
20
- export class ApexPainter {
21
- private format?: OutputFormat;
22
-
23
- constructor({ type }: OutputFormat = { type: 'buffer' }) {
24
- this.format = { type: type || 'buffer' };
25
- }
26
-
27
- /**
28
- * Creates a canvas with the given configuration.
29
- * Applies rotation, shadow, border effects, background, and stroke.
30
- *
31
- * @param canvas - The configuration options for the canvas.
32
- * @returns A promise that resolves to an object containing the image buffer and the canvas config.
33
- * @throws If the 2D context cannot be retrieved.
34
- */
35
- async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
36
- const {
37
- width = 500,
38
- height = 500,
39
- x = 0,
40
- y = 0,
41
- rotation = 0,
42
- borderRadius = 0,
43
- customBg,
44
- gradientBg,
45
- stroke,
46
- shadow,
47
- borderPosition = 'all',
48
- opacity = 1,
49
- zoom,
50
- blur
51
- } = canvas;
52
-
53
- const canvasInstance = createCanvas(width, height);
54
- const ctx = canvasInstance.getContext('2d') as SKRSContext2D | null;
55
- if (!ctx) {
56
- throw new Error('Unable to get 2D rendering context from canvas');
57
- }
58
-
59
- ctx.globalAlpha = opacity;
60
-
61
- applyRotation(ctx, rotation, x, y, width, height);
62
- applyShadow(ctx, shadow, x, y, width, height);
63
-
64
- ctx.save();
65
- if (borderRadius) {
66
- backgroundRadius(ctx, x, y, width, height, borderRadius, borderPosition);
67
- }
68
-
69
- ctx.translate(x, y);
70
-
71
- if (customBg) {
72
- await customBackground(ctx, canvas, zoom, blur);
73
- } else if (gradientBg) {
74
- await drawBackgroundGradient(ctx, canvas, zoom, blur);
75
- } else {
76
- await drawBackgroundColor(ctx, canvas, zoom, blur);
77
- }
78
-
79
- ctx.restore();
80
-
81
- applyStroke(ctx, stroke, x, y, width, height);
82
-
83
- return { buffer: canvasInstance.toBuffer('image/png'), canvas };
84
- }
85
-
86
- /**
87
- * Draws one or more images (or shapes) on an existing canvas buffer.
88
- * @param images - A single image properties object or an array of them.
89
- * @param canvasBuffer - A Buffer or a CanvasResults object containing a buffer.
90
- * @returns A promise that resolves to a Buffer representing the updated canvas.
91
- */
92
- async createImage(
93
- images: ImageProperties | ImageProperties[],
94
- canvasBuffer: CanvasResults | Buffer
95
- ): Promise<Buffer> {
96
-
97
- if (!Array.isArray(images)) {
98
- images = [images];
99
- }
100
-
101
- let baseImage: Image;
102
-
103
- if (Buffer.isBuffer(canvasBuffer)) {
104
- baseImage = await loadImage(canvasBuffer);
105
- } else if ((canvasBuffer as CanvasResults).buffer) {
106
- baseImage = await loadImage((canvasBuffer as CanvasResults).buffer);
107
- } else {
108
- throw new Error('Invalid canvasBuffer provided; expected Buffer or CanvasResults with a buffer');
109
- }
110
-
111
- if (!baseImage) {
112
- throw new Error('Unable to load base image from the provided buffer');
113
- }
114
-
115
- const canvas = createCanvas(baseImage.width, baseImage.height);
116
- const ctx = canvas.getContext('2d') as SKRSContext2D | null;
117
-
118
- if (!ctx) {
119
- throw new Error('Unable to get 2D rendering context from canvas');
120
- }
121
-
122
- ctx.drawImage(baseImage, 0, 0);
123
-
124
- for (const imageProps of images) {
125
- await this.drawImage(ctx, imageProps);
126
- }
127
-
128
- return canvas.toBuffer('image/png');
129
- }
130
-
131
- async createText(textOptionsArray: TextObject[], buffer: CanvasResults | Buffer ): Promise<Buffer> {
132
- try {
133
-
134
- if (!Array.isArray(textOptionsArray)) {
135
- textOptionsArray = [textOptionsArray];
136
- }
137
-
138
- let existingImage: any;
139
-
140
- if (Buffer.isBuffer(buffer)) {
141
- existingImage = await loadImage(buffer);
142
- } else if (buffer && buffer.buffer) {
143
- existingImage = await loadImage(buffer.buffer);
144
- } else {
145
- throw new Error('Invalid canvasBuffer provided. It should be a Buffer or CanvasResults object with a buffer');
146
- }
147
-
148
- if (!existingImage) {
149
- throw new Error('Unable to load image from buffer');
150
- }
151
-
152
- const canvas = createCanvas(existingImage.width, existingImage.height);
153
- const ctx = canvas.getContext("2d");
154
-
155
- ctx.drawImage(existingImage, 0, 0);
156
-
157
- for (const textOptions of textOptionsArray) {
158
- const mergedTextOptions = textOptions;
159
-
160
- if (mergedTextOptions.fontPath) {
161
-
162
- GlobalFonts.registerFromPath(
163
- path.join(process.cwd(), mergedTextOptions.fontPath),
164
- (mergedTextOptions.fontName || 'customFont'),
165
- );
166
- }
167
-
168
- drawText(ctx, mergedTextOptions);
169
- }
170
-
171
- return canvas.toBuffer("image/png");
172
- } catch (error) {
173
- console.error("Error loading existing image:", error);
174
- throw new Error("Invalid image buffer");
175
- }
176
- }
177
-
178
-
179
-
180
- async createCustom(options: CustomOptions[], buffer: CanvasResults | Buffer, ): Promise<Buffer> {
181
- try {
182
-
183
- if (!Array.isArray(options)) {
184
- options = [options];
185
- }
186
-
187
- let existingImage: any;
188
-
189
- if (Buffer.isBuffer(buffer)) {
190
- existingImage = await loadImage(buffer);
191
- } else if (buffer && buffer.buffer) {
192
- existingImage = await loadImage(buffer.buffer);
193
- } else {
194
- throw new Error('Invalid canvasBuffer provided. It should be a Buffer or CanvasResults object with a buffer');
195
- }
196
-
197
- if (!existingImage) {
198
- throw new Error('Unable to load image from buffer');
199
- }
200
-
201
- const canvas = createCanvas(existingImage.width, existingImage.height);
202
- const ctx = canvas.getContext("2d");
203
-
204
- ctx.drawImage(existingImage, 0, 0);
205
-
206
- customLines(ctx, options);
207
-
208
- return canvas.toBuffer("image/png");
209
- } catch (error) {
210
- console.error("Error creating custom image:", error);
211
- throw new Error("Failed to create custom image");
212
- }
213
- }
214
-
215
- async createGIF(gifFrames: { background: string; duration: number }[], options: GIFOptions): Promise<GIFResults | any> {
216
- async function resizeImage(image: any, targetWidth: number, targetHeight: number) {
217
- const canvas = createCanvas(targetWidth, targetHeight);
218
- const ctx = canvas.getContext("2d");
219
- ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
220
- return canvas;
221
- }
222
-
223
- function createOutputStream(outputFile: string): fs.WriteStream {
224
- return fs.createWriteStream(outputFile);
225
- }
226
-
227
- function createBufferStream() {
228
- const bufferStream = new PassThrough();
229
- const chunks: Buffer[] = [];
230
-
231
- bufferStream.on('data', (chunk: Buffer) => {
232
- chunks.push(chunk);
233
- });
234
-
235
- return {
236
- ...bufferStream,
237
- getBuffer: function (): Buffer {
238
- return Buffer.concat(chunks);
239
- }
240
- } as any;
241
- }
242
-
243
- function validateOptions(options: GIFOptions) {
244
- if (options.outputFormat === "file" && !options.outputFile) {
245
- throw new Error("Output file path is required when using file output format.");
246
- }
247
- if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
248
- throw new Error("Repeat must be a non-negative number or undefined.");
249
- }
250
- if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
251
- throw new Error("Quality must be a number between 1 and 20 or undefined.");
252
- }
253
- if (options.watermark && typeof options.watermark.enable !== "boolean") {
254
- throw new Error("Watermark must be a boolean or undefined.");
255
- }
256
- if (options.textOverlay) {
257
- const textOptions = options.textOverlay;
258
- if (!textOptions.text || typeof textOptions.text !== "string") {
259
- throw new Error("Text overlay text is required and must be a string.");
260
- }
261
- if (textOptions.fontSize !== undefined && (!Number.isInteger(textOptions.fontSize) || textOptions.fontSize <= 0)) {
262
- throw new Error("Text overlay fontSize must be a positive integer or undefined.");
263
- }
264
- if (textOptions.fontColor !== undefined && typeof textOptions.fontColor !== "string") {
265
- throw new Error("Text overlay fontColor must be a string or undefined.");
266
- }
267
- }
268
- }
269
-
270
- try {
271
- validateOptions(options);
272
-
273
- const canvasWidth = options.width || 1200;
274
- const canvasHeight = options.height || 1200;
275
-
276
- const encoder = new GIFEncoder(canvasWidth, canvasHeight);
277
- const outputStream = options.outputFile ? createOutputStream(options.outputFile) : createBufferStream();
278
-
279
- encoder.createReadStream().pipe(outputStream);
280
-
281
- encoder.start();
282
- encoder.setRepeat(options.repeat || 0);
283
- encoder.setQuality(options.quality || 10);
284
-
285
- const canvas = createCanvas(canvasWidth, canvasHeight);
286
- const ctx:any = canvas.getContext("2d");
287
-
288
- for (const frame of gifFrames) {
289
- const image = await loadImage(frame.background);
290
- const resizedImage = await resizeImage(image, canvasWidth, canvasHeight);
291
-
292
- ctx.clearRect(0, 0, canvas.width, canvas.height);
293
- ctx.drawImage(resizedImage, 0, 0);
294
-
295
- if (options.watermark?.enable) {
296
- const watermark = await loadImage(options.watermark.url);
297
- ctx.drawImage(watermark, 10, canvasHeight - watermark.height - 10);
298
- }
299
-
300
- if (options.textOverlay) {
301
- ctx.font = `${options.textOverlay.fontSize || 20}px Arial`;
302
- ctx.fillStyle = options.textOverlay.fontColor || "white";
303
- ctx.fillText(options.textOverlay.text, options.textOverlay.x || 10, options.textOverlay.y || 30);
304
- }
305
-
306
- encoder.setDelay(frame.duration);
307
- encoder.addFrame(ctx);
308
- }
309
-
310
- encoder.finish();
311
- outputStream.end();
312
-
313
- if (options.outputFormat === "file") {
314
- await new Promise((resolve) => outputStream.on("finish", resolve));
315
- } else if (options.outputFormat === "base64") {
316
- if ('getBuffer' in outputStream) {
317
- return outputStream.getBuffer().toString("base64");
318
- }
319
- } else if (options.outputFormat === "attachment") {
320
- const gifStream = encoder.createReadStream();
321
- return [{ attachment: gifStream, name: "gif.js" }];
322
- } else if (options.outputFormat === "buffer") {
323
- if ('getBuffer' in outputStream) {
324
- return outputStream.getBuffer();
325
- }
326
- } else {
327
- throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
328
- }
329
- } catch (e: any) {
330
- console.error(e.message);
331
- throw e;
332
- }
333
- }
334
-
335
- async resize(resizeOptions: ResizeOptions) {
336
- return resizingImg(resizeOptions)
337
- }
338
-
339
- async imgConverter(source: string, newExtension: string) {
340
- return converter(source, newExtension)
341
- }
342
-
343
- async effects(source: string, filters: any[]) {
344
- return imgEffects(source, filters)
345
- }
346
-
347
- async colorsFilter(source: string, filterColor: any, opacity?: number) {
348
- return applyColorFilters(source, filterColor, opacity)
349
- }
350
-
351
- async colorAnalysis(source: string) {
352
- return detectColors(source)
353
- }
354
-
355
- async colorsRemover(source: string, colorToRemove: { red: number, green: number, blue: number }) {
356
- return removeColor(source, colorToRemove)
357
- }
358
-
359
- async removeBackground(imageURL: string, apiKey: string) {
360
- return bgRemoval(imageURL, apiKey)
361
- }
362
-
363
- async blend(
364
- layers: {
365
- image: string | Buffer;
366
- blendMode: 'source-over' | 'source-in' | 'source-out' | 'source-atop' |
367
- 'destination-over' | 'destination-in' | 'destination-out' |
368
- 'destination-atop' | 'lighter' | 'copy' | 'xor' |
369
- 'multiply' | 'screen' | 'overlay' | 'darken' |
370
- 'lighten' | 'color-dodge' | 'color-burn' |
371
- 'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
372
- 'hue' | 'saturation' | 'color' | 'luminosity';
373
- position?: { x: number; y: number };
374
- opacity?: number;
375
- }[],
376
- baseImageBuffer: Buffer,
377
- defaultBlendMode: 'source-over' | 'source-in' | 'source-out' | 'source-atop' |
378
- 'destination-over' | 'destination-in' | 'destination-out' |
379
- 'destination-atop' | 'lighter' | 'copy' | 'xor' |
380
- 'multiply' | 'screen' | 'overlay' | 'darken' |
381
- 'lighten' | 'color-dodge' | 'color-burn' |
382
- 'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
383
- 'hue' | 'saturation' | 'color' | 'luminosity' = 'source-over'
384
- ): Promise<Buffer> {
385
- try {
386
- const baseImage = await loadImage(baseImageBuffer);
387
- const canvas = createCanvas(baseImage.width, baseImage.height);
388
- const ctx = canvas.getContext('2d');
389
-
390
- ctx.globalCompositeOperation = defaultBlendMode;
391
- ctx.drawImage(baseImage, 0, 0);
392
-
393
- for (const layer of layers) {
394
- const layerImage = await loadImage(layer.image);
395
- ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1.0;
396
-
397
- ctx.globalCompositeOperation = layer.blendMode;
398
- ctx.drawImage(layerImage, layer.position?.x || 0, layer.position?.y || 0);
399
- }
400
-
401
- ctx.globalAlpha = 1.0;
402
- ctx.globalCompositeOperation = defaultBlendMode; // Reset to user-defined default
403
-
404
- return canvas.toBuffer('image/png');
405
- } catch (error) {
406
- console.error('Error creating layered composition:', error);
407
- throw new Error('Failed to create layered composition');
408
- }
409
- }
410
-
411
-
412
- async createChart(data: any, type: { chartType: string, chartNumber: number}) {
413
-
414
- if (!data || Object.keys(data).length === 0) {
415
- throw new Error('You need to provide datasets to create charts.');
416
- }
417
-
418
- if (!type || !type.chartNumber || !type.chartType) {
419
- throw new Error('Type arguments are missing.');
420
- }
421
-
422
- const { chartType, chartNumber } = type;
423
-
424
- switch (chartType.toLowerCase()) {
425
- case 'bar':
426
- switch (chartNumber) {
427
- case 1:
428
- return await verticalBarChart(data);
429
- case 2:
430
- throw new Error('Type 2 is still under development.');
431
- default:
432
- throw new Error('Invalid chart number for chart type "bar".');
433
- }
434
- case 'line':
435
- switch (chartNumber) {
436
- case 1:
437
- return await lineChart(data);
438
- case 2:
439
- throw new Error('Type 2 is still under development.');
440
- default:
441
- throw new Error('Invalid chart number for chart type "line".');
442
- }
443
- case 'pie':
444
- switch (chartNumber) {
445
- case 1:
446
- return await pieChart(data);
447
- case 2:
448
- throw new Error('Type 2 is still under development.');
449
- default:
450
- throw new Error('Invalid chart number for chart type "pie".');
451
- }
452
- default:
453
- throw new Error(`Unsupported chart type "${chartType}".`);
454
- }
455
- }
456
-
457
-
458
- async cropImage(options: cropOptions): Promise<Buffer> {
459
- try {
460
- if (!options.imageSource) throw new Error('The "imageSource" option is needed. Please provide the path to the image to crop.');
461
- if (!options.coordinates || options.coordinates.length < 3) throw new Error('The "coordinates" option is needed. Please provide coordinates to crop the image.');
462
-
463
- if (options.crop === 'outer') {
464
- return await cropOuter(options);
465
- } else if (options.crop === 'inner') {
466
- return await cropInner(options);
467
- } else {
468
- throw new Error('Invalid crop option. Please specify "inner" or "outer".');
469
- }
470
- } catch (error) {
471
- console.error('An error occurred:', error);
472
- throw error;
473
- }
474
- }
475
-
476
- private async drawImage(ctx: SKRSContext2D, image: ImageProperties): Promise<void> {
477
- const {
478
- source,
479
- x, y, width, height,
480
- rotation = 0,
481
- borderRadius = 0,
482
- stroke, shadow,
483
- isFilled, color, gradient,
484
- borderPosition = 'all',
485
- opacity,
486
- perspective,
487
- blur = 0,
488
- zoom,
489
- filling
490
- } = image;
491
-
492
- if (perspective) throw new Error("`perspective` Feature is under construction. Please refrain from using it.");
493
-
494
- if (!source || typeof x !== 'number' || typeof y !== 'number' || typeof width !== 'number' || typeof height !== 'number') {
495
- throw new Error('Invalid image properties: source, x, y, width, and height are required.');
496
- }
497
-
498
- const shapeNames = ['circle', 'square', 'triangle', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'star', 'kite', 'arrow', 'star', 'oval', 'heart', 'diamond', 'trapezoid', 'cloud'];
499
- const isShape = shapeNames.includes(source.toLowerCase());
500
-
501
-
502
- if (isShape) {
503
- drawShape(ctx, { source, x, y, width, height, opacity, rotation, borderRadius, stroke, shadow, isFilled, color, gradient, borderPosition, filling });
504
- return;
505
- }
506
-
507
- let loadedImage: Image | undefined;
508
- try {
509
- if (source.startsWith('http')) {
510
- loadedImage = await loadImage(source);
511
- } else {
512
- const imagePath = path.resolve(process.cwd(), source);
513
- loadedImage = await loadImage(imagePath);
514
- }
515
- } catch (e: any) {
516
- throw new Error(`Error loading image: ${e.message}`);
517
- }
518
-
519
- if (!loadedImage) {
520
- throw new Error('Invalid image source or failed to load image');
521
- }
522
-
523
- ctx.save();
524
-
525
- const scale = zoom?.scale ?? 1;
526
- const focusX = zoom?.x ?? 0.5;
527
- const focusY = zoom?.y ?? 0.5;
528
-
529
- if (scale !== 1) {
530
- ctx.translate(focusX, focusY);
531
- ctx.scale(scale, scale);
532
- }
533
-
534
- if (opacity !== undefined) {
535
- ctx.globalAlpha = opacity;
536
- }
537
-
538
- if (blur > 0) {
539
- ctx.filter = `blur(${blur}px)`;
540
- }
541
-
542
- applyRotation(ctx, rotation, x, y, width, height);
543
- applyShadow(ctx, shadow, x, y, width, height);
544
-
545
- if (perspective) {
546
- throw new Error('Under construction');
547
- }
548
-
549
- imageRadius(ctx, loadedImage, x, y, width, height, borderRadius, borderPosition);
550
- ctx.globalAlpha = 1.0;
551
-
552
- if (stroke?.opacity) ctx.globalAlpha = stroke.opacity as number;
553
-
554
- applyStroke(ctx, stroke, x, y, width, height, undefined);
555
- ctx.globalAlpha = 1.0;
556
-
557
- ctx.restore();
558
- }
559
-
560
-
561
- async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions): Promise<any[]> {
562
- const frames: any[]= [];
563
- const frameDir = path.join(__dirname, 'frames');
564
-
565
- if (!fs.existsSync(frameDir)) {
566
- fs.mkdirSync(frameDir);
567
- }
568
-
569
- const videoPath = typeof videoSource === 'string' ? videoSource : path.join(frameDir, 'temp-video.mp4');
570
-
571
- if (Buffer.isBuffer(videoSource)) {
572
- fs.writeFileSync(videoPath, videoSource);
573
- } else if (videoSource.startsWith('http')) {
574
- await axios({
575
- method: 'get',
576
- url: videoSource,
577
- responseType: 'arraybuffer'
578
- })
579
- .then((response) => {
580
- fs.writeFileSync(videoPath, response.data);
581
- })
582
- .catch(err => {
583
- throw new Error(`Error downloading video: ${err.message}`);
584
- });
585
- } else if (!fs.existsSync(videoPath)) {
586
- throw new Error("Video file not found at specified path.");
587
- }
588
-
589
- function processVideoExtraction(videoPath: string, frames: any[], options: ExtractFramesOptions, resolve: any, reject: any) {
590
- const outputFormat = options.outputFormat || 'jpg';
591
- const outputFileTemplate = `frame-%03d.${outputFormat}`;
592
-
593
- ffmpeg(videoPath)
594
- .on('end', () => {
595
- console.log('Frames extracted successfully.');
596
- resolve(frames);
597
- })
598
- .on('error', (err: { message: any; }) => {
599
- console.error('Error extracting frames:', err.message);
600
- reject(err);
601
- })
602
- .outputOptions([`-vf fps=1/${options.interval / 1000}`, `-q:v 2`])
603
- .saveToFile(path.join(frameDir, outputFileTemplate));
604
-
605
- ffmpeg.ffprobe(videoPath, (err: any, metadata: any) => {
606
- if (err) {
607
- return reject(err);
608
- }
609
- const duration = metadata?.format?.duration;
610
- if (typeof duration !== "number") {
611
- return reject(new Error("Video duration not found in metadata."));
612
- }
613
- const totalFrames = Math.floor(duration * 1000 / options.interval);
614
- for (let i = 0; i < totalFrames; i++) {
615
- if (options.frameSelection && (i < (options.frameSelection.start || 0) || i > (options.frameSelection.end || totalFrames - 1))) {
616
- continue;
617
- }
618
- frames.push({
619
- source: path.join(frameDir, `frame-${String(i).padStart(3, '0')}.${outputFormat}`),
620
- isRemote: false
621
- });
622
- }
623
- });
624
- }
625
-
626
- return new Promise((resolve, reject) => {
627
- processVideoExtraction(videoPath, frames, options, resolve, reject);
628
- });
629
- }
630
-
631
-
632
-
633
- /**
634
- * Sets a pattern on a specific area of a buffered image.
635
- * @param {Buffer} buffer - The source image buffer.
636
- * @param {Object} options - Options to customize the pattern.
637
- * @returns {Promise<Buffer>} - The adjusted image buffer.
638
- */
639
- async patterns(buffer:Buffer, options: PatternOptions) {
640
-
641
- const img = await loadImage(buffer);
642
- const canvas = createCanvas(img.width, img.height);
643
- const ctx: SKRSContext2D = canvas.getContext('2d');
644
- ctx.drawImage(img, 0, 0);
645
-
646
- const x = options.x || 0;
647
- const y = options.y || 0;
648
- const width = options.width || img.width;
649
- const height = options.height || img.height;
650
-
651
- ctx.save();
652
- ctx.beginPath();
653
- ctx.rect(x, y, width, height);
654
- ctx.clip();
655
-
656
- if (options.gradient) {
657
- await this.fillWithGradient(ctx, width, height, options.gradient, x, y);
658
- } else if (options.backgroundColor) {
659
- ctx.fillStyle = options.backgroundColor;
660
- ctx.fillRect(x, y, width, height);
661
- }
662
-
663
- switch (options.type) {
664
- case 'dots':
665
- await this.drawDotsPattern(ctx, width, height, options, x, y);
666
- break;
667
- case 'stripes':
668
- await this.drawStripesPattern(ctx, width, height, options, x, y);
669
- break;
670
- case 'grid':
671
- await this.drawGridPattern(ctx, width, height, options, x, y);
672
- break;
673
- case 'checkerboard':
674
- await this.drawCheckerboardPattern(ctx, width, height, options, x, y);
675
- break;
676
- case 'custom':
677
- await this.drawCustomPattern(ctx, width, height, options, x, y);
678
- break;
679
- case 'noise':
680
- await this.drawNoisePattern(ctx, width, height, x, y);
681
- break;
682
- case 'waves':
683
- await this.drawWavePattern(ctx, width, height, options, x, y);
684
- break;
685
- case 'diagonal-checkerboard':
686
- await this.drawDiagonalCheckerboardPattern(ctx, width, height, options, x, y);
687
- break;
688
- default:
689
- throw new Error('Invalid pattern type specified.');
690
- }
691
-
692
- ctx.restore();
693
-
694
- return canvas.toBuffer('image/png');
695
- }
696
-
697
-
698
- /**
699
- * Fills the specified area with a gradient.
700
- * @param ctx The rendering context of the canvas.
701
- * @param width The width of the area to fill.
702
- * @param height The height of the area to fill.
703
- * @param gradient The gradient options.
704
- * @param x The x offset of the area.
705
- * @param y The y offset of the area.
706
- */
707
- private async fillWithGradient(
708
- ctx: SKRSContext2D,
709
- width: number,
710
- height: number,
711
- gradient: GradientConfig,
712
- x: number = 0,
713
- y: number = 0
714
- ): Promise<void> {
715
- let grad: CanvasGradient;
716
-
717
- if (gradient.type === 'linear') {
718
- grad = ctx.createLinearGradient(
719
- (gradient.startX || 0) + x,
720
- (gradient.startY || 0) + y,
721
- (gradient.endX || width) + x,
722
- (gradient.endY || height) + y
723
- );
724
- } else {
725
- grad = ctx.createRadialGradient(
726
- (gradient.startX || width / 2) + x,
727
- (gradient.startY || height / 2) + y,
728
- gradient.startRadius || 0,
729
- (gradient.endX || width / 2) + x,
730
- (gradient.endY || height / 2) + y,
731
- gradient.endRadius || Math.max(width, height)
732
- );
733
- }
734
-
735
- gradient.colors.forEach((stop: any) => {
736
- grad.addColorStop(stop.stop, stop.color);
737
- });
738
-
739
- ctx.fillStyle = grad;
740
- ctx.fillRect(x, y, width, height);
741
- }
742
-
743
- /**
744
- * Draws a dots pattern in the specified area.
745
- * @param ctx The rendering context of the canvas.
746
- * @param width The width of the area.
747
- * @param height The height of the area.
748
- * @param options Options to customize the dot pattern.
749
- * @param x The x offset of the area.
750
- * @param y The y offset of the area.
751
- */
752
- private async drawDotsPattern(
753
- ctx: SKRSContext2D,
754
- width: number,
755
- height: number,
756
- options: PatternOptions,
757
- x: number = 0,
758
- y: number = 0
759
- ): Promise<void> {
760
- const color = options.color || 'black';
761
- const radius = options.size || 5;
762
- const spacing = options.spacing || 10;
763
-
764
- ctx.fillStyle = color;
765
-
766
- for (let posY = y; posY < y + height; posY += spacing) {
767
- for (let posX = x; posX < x + width; posX += spacing) {
768
- ctx.beginPath();
769
- ctx.arc(posX, posY, radius, 0, Math.PI * 2);
770
- ctx.fill();
771
- }
772
- }
773
- }
774
-
775
- /**
776
- * Draws a stripes pattern in the specified area.
777
- * @param ctx The rendering context of the canvas.
778
- * @param width The width of the area.
779
- * @param height The height of the area.
780
- * @param options Options to customize the stripes pattern.
781
- * @param x The x offset of the area.
782
- * @param y The y offset of the area.
783
- */
784
- private async drawStripesPattern(
785
- ctx: SKRSContext2D,
786
- width: number,
787
- height: number,
788
- options: PatternOptions,
789
- x: number = 0,
790
- y: number = 0
791
- ): Promise<void> {
792
- const color = options.color || 'black';
793
- const stripeWidth = options.size || 10;
794
- const spacing = options.spacing || 20;
795
- const angle = options.angle || 0;
796
-
797
- ctx.fillStyle = color;
798
-
799
- ctx.save();
800
-
801
- ctx.translate(x + width / 2, y + height / 2);
802
- ctx.rotate((angle * Math.PI) / 180);
803
- ctx.translate(-width / 2, -height / 2);
804
-
805
- for (let posX = 0; posX < width; posX += stripeWidth + spacing) {
806
- ctx.fillRect(posX, 0, stripeWidth, height);
807
- }
808
-
809
- ctx.restore();
810
- }
811
- /**
812
- * Draws a grid pattern in the specified area.
813
- * @param ctx The rendering context of the canvas.
814
- * @param width The width of the area.
815
- * @param height The height of the area.
816
- * @param options Options to customize the grid pattern.
817
- * @param x The x offset of the area.
818
- * @param y The y offset of the area.
819
- */
820
- private async drawGridPattern(
821
- ctx: SKRSContext2D,
822
- width: number,
823
- height: number,
824
- options: PatternOptions,
825
- x: number = 0,
826
- y: number = 0
827
- ): Promise<void> {
828
- const color = options.color || 'black';
829
- const size = options.size || 20;
830
-
831
- ctx.strokeStyle = color;
832
- ctx.lineWidth = 1;
833
-
834
- for (let posX = x; posX <= x + width; posX += size) {
835
- ctx.beginPath();
836
- ctx.moveTo(posX, y);
837
- ctx.lineTo(posX, y + height);
838
- ctx.stroke();
839
- }
840
-
841
- for (let posY = y; posY <= y + height; posY += size) {
842
- ctx.beginPath();
843
- ctx.moveTo(x, posY);
844
- ctx.lineTo(x + width, posY);
845
- ctx.stroke();
846
- }
847
- }
848
-
849
- /**
850
- * Draws a checkerboard pattern in the specified area.
851
- * @param ctx The rendering context of the canvas.
852
- * @param width The width of the area.
853
- * @param height The height of the area.
854
- * @param options Options to customize the checkerboard pattern.
855
- * @param x The x offset of the area.
856
- * @param y The y offset of the area.
857
- */
858
- private async drawCheckerboardPattern(
859
- ctx: SKRSContext2D,
860
- width: number,
861
- height: number,
862
- options: PatternOptions,
863
- x: number = 0,
864
- y: number = 0
865
- ): Promise<void> {
866
- const color1 = options.color || 'black';
867
- const color2 = options.secondaryColor || 'white';
868
- const size = options.size || 20;
869
-
870
- for (let posY = y; posY < y + height; posY += size) {
871
- for (let posX = x; posX < x + width; posX += size) {
872
- ctx.fillStyle = (Math.floor(posX / size) + Math.floor(posY / size)) % 2 === 0 ? color1 : color2;
873
- ctx.fillRect(posX, posY, size, size);
874
- }
875
- }
876
- }
877
-
878
- /**
879
- * Draws a custom image pattern in the specified area.
880
- * @param ctx The rendering context of the canvas.
881
- * @param width The width of the area.
882
- * @param height The height of the area.
883
- * @param options Options to customize the custom pattern.
884
- * @param x The x offset of the area.
885
- * @param y The y offset of the area.
886
- */
887
- private async drawCustomPattern(
888
- ctx: SKRSContext2D,
889
- width: number,
890
- height: number,
891
- options: PatternOptions,
892
- x: number = 0,
893
- y: number = 0
894
- ): Promise<void> {
895
- if (!options.customPatternImage) {
896
- throw new Error('Custom pattern image path is required for custom patterns.');
897
- }
898
-
899
- const patternImage = await loadImage(options.customPatternImage);
900
- const pattern = ctx.createPattern(patternImage, 'repeat');
901
- ctx.fillStyle = pattern;
902
- ctx.translate(x, y);
903
- ctx.fillRect(0, 0, width, height);
904
- ctx.resetTransform();
905
- }
906
-
907
- private async drawWavePattern(ctx: SKRSContext2D, width: number, height: number, options: PatternOptions, x = 0, y = 0) {
908
- const color = options.color || 'black';
909
- const waveHeight = options.size || 10;
910
- const frequency = options.spacing || 20;
911
-
912
- ctx.strokeStyle = color;
913
- ctx.lineWidth = 2;
914
-
915
- for (let posY = y; posY < y + height; posY += frequency) {
916
- ctx.beginPath();
917
- for (let posX = x; posX < x + width; posX += 5) {
918
- const waveY = posY + Math.sin(posX / frequency) * waveHeight;
919
- ctx.lineTo(posX, waveY);
920
- }
921
- ctx.stroke();
922
- }
923
- }
924
-
925
-
926
- private async drawNoisePattern(ctx: SKRSContext2D, width: number, height: number, x = 0, y = 0) {
927
- const imageData = ctx.createImageData(width, height);
928
- const data = imageData.data;
929
-
930
- for (let i = 0; i < data.length; i += 4) {
931
- const gray = Math.random() * 255;
932
- data[i] = gray;
933
- data[i + 1] = gray;
934
- data[i + 2] = gray;
935
- data[i + 3] = Math.random() * 255;
936
- }
937
-
938
- ctx.putImageData(imageData, x, y);
939
- }
940
-
941
-
942
- private async drawDiagonalCheckerboardPattern(ctx: SKRSContext2D, width: number, height: number, options: PatternOptions, x = 0, y = 0) {
943
- const color1 = options.color || 'black';
944
- const color2 = options.secondaryColor || 'white';
945
- const size = options.size || 20;
946
-
947
- ctx.save();
948
- ctx.translate(x + width / 2, y + height / 2);
949
- ctx.rotate(Math.PI / 4);
950
- ctx.translate(-width / 2, -height / 2);
951
-
952
- for (let posY = 0; posY < height; posY += size) {
953
- for (let posX = 0; posX < width; posX += size) {
954
- ctx.fillStyle = (Math.floor(posX / size) + Math.floor(posY / size)) % 2 === 0 ? color1 : color2;
955
- ctx.fillRect(posX, posY, size, size);
956
- }
957
- }
958
-
959
- ctx.restore();
960
- }
961
-
962
-
963
- async masking(
964
- source: string | Buffer | PathLike | Uint8Array,
965
- maskSource: string | Buffer | PathLike | Uint8Array,
966
- options: MaskOptions = { type: "alpha" }
967
- ): Promise<Buffer> {
968
- const img = await loadImage(source);
969
- const mask = await loadImage(maskSource);
970
-
971
- const canvas = createCanvas(img.width, img.height);
972
- const ctx = canvas.getContext("2d") as SKRSContext2D;
973
-
974
- ctx.drawImage(img, 0, 0, img.width, img.height);
975
-
976
- const maskCanvas = createCanvas(img.width, img.height);
977
- const maskCtx = maskCanvas.getContext("2d") as SKRSContext2D;
978
- maskCtx.drawImage(mask, 0, 0, img.width, img.height);
979
-
980
- const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
981
- const imgData = ctx.getImageData(0, 0, img.width, img.height);
982
-
983
- for (let i = 0; i < maskData.data.length; i += 4) {
984
- let alphaValue = 255;
985
-
986
- if (options.type === "grayscale") {
987
- const grayscale = maskData.data[i] * 0.3 + maskData.data[i + 1] * 0.59 + maskData.data[i + 2] * 0.11;
988
- alphaValue = grayscale >= (options.threshold ?? 128) ? 255 : 0;
989
- } else if (options.type === "alpha") {
990
- alphaValue = maskData.data[i + 3];
991
- } else if (options.type === "color" && options.colorKey) {
992
- const colorMatch =
993
- maskData.data[i] === parseInt(options.colorKey.slice(1, 3), 16) &&
994
- maskData.data[i + 1] === parseInt(options.colorKey.slice(3, 5), 16) &&
995
- maskData.data[i + 2] === parseInt(options.colorKey.slice(5, 7), 16);
996
- alphaValue = colorMatch ? 0 : 255;
997
- }
998
-
999
- if (options.invert) alphaValue = 255 - alphaValue;
1000
-
1001
- imgData.data[i + 3] = alphaValue;
1002
- }
1003
-
1004
- ctx.putImageData(imgData, 0, 0);
1005
-
1006
- return canvas.toBuffer("image/png");
1007
- }
1008
-
1009
- async gradientBlend(
1010
- source: string | Buffer | PathLike | Uint8Array,
1011
- options: BlendOptions
1012
- ): Promise<Buffer> {
1013
- const img = await loadImage(source);
1014
- const canvas = createCanvas(img.width, img.height);
1015
- const ctx = canvas.getContext("2d") as SKRSContext2D;
1016
-
1017
- ctx.drawImage(img, 0, 0, img.width, img.height);
1018
-
1019
- let gradient: CanvasGradient;
1020
- if (options.type === "linear") {
1021
- const angle = options.angle ?? 0;
1022
- const radians = (angle * Math.PI) / 180;
1023
- const x1 = img.width / 2 - (Math.cos(radians) * img.width) / 2;
1024
- const y1 = img.height / 2 - (Math.sin(radians) * img.height) / 2;
1025
- const x2 = img.width / 2 + (Math.cos(radians) * img.width) / 2;
1026
- const y2 = img.height / 2 + (Math.sin(radians) * img.height) / 2;
1027
- gradient = ctx.createLinearGradient(x1, y1, x2, y2);
1028
- } else if (options.type === "radial") {
1029
- gradient = ctx.createRadialGradient(
1030
- img.width / 2, img.height / 2, 0, img.width / 2, img.height / 2, Math.max(img.width, img.height)
1031
- );
1032
- } else {
1033
- gradient = ctx.createConicGradient(Math.PI, img.width / 2, img.height / 2);
1034
- }
1035
-
1036
- options.colors.forEach(({ stop, color }: any) => gradient.addColorStop(stop, color));
1037
- ctx.fillStyle = gradient;
1038
-
1039
- ctx.globalCompositeOperation = options.blendMode ?? "multiply";
1040
- ctx.fillRect(0, 0, img.width, img.height);
1041
-
1042
- if (options.maskSource) {
1043
- const mask = await loadImage(options.maskSource);
1044
- ctx.globalCompositeOperation = "destination-in";
1045
- ctx.drawImage(mask, 0, 0, img.width, img.height);
1046
- }
1047
-
1048
- ctx.globalCompositeOperation = "source-over";
1049
-
1050
- return canvas.toBuffer("image/png");
1051
- }
1052
-
1053
- async animate(
1054
- frames: Frame[],
1055
- defaultDuration: number,
1056
- defaultWidth: number = 800,
1057
- defaultHeight: number = 600,
1058
- options?: {
1059
- gif?: boolean;
1060
- gifPath?: string;
1061
- onStart?: () => void;
1062
- onFrame?: (index: number) => void;
1063
- onEnd?: () => void;
1064
- }
1065
- ): Promise<Buffer[] | undefined> {
1066
- const buffers: Buffer[] = [];
1067
- const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
1068
-
1069
- if (options?.onStart) options.onStart();
1070
-
1071
- let encoder: GIFEncoder | null = null;
1072
- let gifStream: fs.WriteStream | null = null;
1073
-
1074
- if (options?.gif) {
1075
- if (!options.gifPath) {
1076
- throw new Error("GIF generation enabled but no gifPath provided.");
1077
- }
1078
- encoder = new GIFEncoder(defaultWidth, defaultHeight);
1079
- gifStream = fs.createWriteStream(options.gifPath);
1080
- encoder.createReadStream().pipe(gifStream);
1081
- encoder.start();
1082
- encoder.setRepeat(0);
1083
- encoder.setQuality(10);
1084
- }
1085
-
1086
- for (let i = 0; i < frames.length; i++) {
1087
- const frame = frames[i];
1088
-
1089
- const width = frame.width || defaultWidth;
1090
- const height = frame.height || defaultHeight;
1091
- const canvas = createCanvas(width, height);
1092
- const ctx: SKRSContext2D = canvas.getContext('2d');
1093
-
1094
- if (!isNode) {
1095
- canvas.width = width;
1096
- canvas.height = height;
1097
- document.body.appendChild(canvas as unknown as Node);
1098
- }
1099
-
1100
- ctx.clearRect(0, 0, width, height);
1101
-
1102
- if (frame.transformations) {
1103
- const { scaleX = 1, scaleY = 1, rotate = 0, translateX = 0, translateY = 0 } = frame.transformations;
1104
- ctx.save();
1105
- ctx.translate(translateX, translateY);
1106
- ctx.rotate((rotate * Math.PI) / 180);
1107
- ctx.scale(scaleX, scaleY);
1108
- }
1109
-
1110
- let fillStyle: string | CanvasGradient | CanvasPattern | null = null;
1111
-
1112
- if (frame.gradient) {
1113
- const { type, startX, startY, endX, endY, startRadius, endRadius, colors } = frame.gradient;
1114
- let gradient: CanvasGradient | null = null;
1115
-
1116
- if (type === 'linear') {
1117
- gradient = ctx.createLinearGradient(startX || 0, startY || 0, endX || width, endY || height);
1118
- } else if (type === 'radial') {
1119
- gradient = ctx.createRadialGradient(
1120
- startX || width / 2,
1121
- startY || height / 2,
1122
- startRadius || 0,
1123
- endX || width / 2,
1124
- endY || height / 2,
1125
- endRadius || Math.max(width, height)
1126
- );
1127
- }
1128
-
1129
- colors.forEach((colorStop: any) => {
1130
- if (gradient) gradient.addColorStop(colorStop.stop, colorStop.color);
1131
- });
1132
-
1133
- fillStyle = gradient;
1134
- }
1135
-
1136
- if (frame.pattern) {
1137
- const patternImage = await loadImage(frame.pattern.source);
1138
- const pattern = ctx.createPattern(patternImage, frame.pattern.repeat || 'repeat');
1139
- fillStyle = pattern;
1140
- }
1141
-
1142
- if (!fillStyle && frame.backgroundColor) {
1143
- fillStyle = frame.backgroundColor;
1144
- }
1145
-
1146
- if (fillStyle) {
1147
- ctx.fillStyle = fillStyle;
1148
- ctx.fillRect(0, 0, width, height);
1149
- }
1150
-
1151
- if (frame.source) {
1152
- const image = await loadImage(frame.source);
1153
- ctx.globalCompositeOperation = frame.blendMode || 'source-over';
1154
- ctx.drawImage(image, 0, 0, width, height);
1155
- }
1156
-
1157
- if (frame.onDrawCustom) {
1158
- frame.onDrawCustom(ctx as unknown as SKRSContext2D, canvas);
1159
- }
1160
-
1161
- if (frame.transformations) {
1162
- ctx.restore();
1163
- }
1164
-
1165
- const buffer = canvas.toBuffer('image/png');
1166
- buffers.push(buffer);
1167
-
1168
- if (encoder) {
1169
-
1170
- const frameDuration = frame.duration || defaultDuration;
1171
- encoder.setDelay(frameDuration);
1172
- encoder.addFrame(ctx as unknown as CanvasRenderingContext2D);
1173
- }
1174
-
1175
- if (options?.onFrame) options.onFrame(i);
1176
-
1177
- await new Promise(resolve => setTimeout(resolve, frame.duration || defaultDuration));
1178
- }
1179
-
1180
- if (encoder) {
1181
- encoder.finish();
1182
- }
1183
-
1184
- if (options?.onEnd) options.onEnd();
1185
-
1186
- return options?.gif ? undefined : buffers;
1187
- }
1188
-
1189
-
1190
-
1191
- public validHex(hexColor: string): any {
1192
- const hexPattern = /^#[0-9a-fA-F]{6}$/;
1193
- if (!hexPattern.test(hexColor)) {
1194
- throw new Error("Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
1195
- }
1196
- return true
1197
- }
1198
-
1199
- public async outPut(results: any): Promise< void | Buffer | string | Blob | Object | HTMLCanvasElement> {
1200
-
1201
- const formatType: string = this.format?.type || 'buffer';
1202
- switch (formatType) {
1203
- case 'buffer':
1204
- return results;
1205
- case 'url':
1206
- return await url(results);
1207
- case 'dataURL':
1208
- return dataURL(results);
1209
- case 'blob':
1210
- return blob(results);
1211
- case 'base64':
1212
- return base64(results);
1213
- case 'arraybuffer':
1214
- return arrayBuffer(results);
1215
- default:
1216
- throw new Error('Unsupported format');
1217
- }
1218
- }
1
+ import { createCanvas, loadImage, GlobalFonts, Image, SKRSContext2D } from "@napi-rs/canvas";
2
+ import GIFEncoder from "gifencoder";
3
+ import ffmpeg from 'fluent-ffmpeg';
4
+ import { PassThrough} from "stream";
5
+ import axios from 'axios';
6
+ import fs, { PathLike } from "fs";
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,
13
+ applyCanvasZoom, applyNoise,
14
+ applyStroke, applyRotation, applyShadow, drawBoxBackground, fitInto, loadImageCached,
15
+ drawShape, isShapeSource, ShapeType, createShapePath, createGradientFill, applyImageFilters, applySimpleProfessionalFilters
16
+ } from "./utils/utils";
17
+ import { } from "./utils/Image/imageProperties";
18
+ import { EnhancedTextRenderer } from "./utils/Texts/enhancedTextRenderer";
19
+ import { EnhancedPatternRenderer } from "./utils/Patterns/enhancedPatternRenderer";
20
+
21
+ interface CanvasResults {
22
+ buffer: Buffer;
23
+ canvas: CanvasConfig;
24
+ }
25
+
26
+
27
+ export class ApexPainter {
28
+ private format?: OutputFormat;
29
+
30
+ constructor({ type }: OutputFormat = { type: 'buffer' }) {
31
+ this.format = { type: type || 'buffer' };
32
+ }
33
+
34
+ /**
35
+ * Validates image properties for required fields.
36
+ * @private
37
+ * @param ip - Image properties to validate
38
+ */
39
+ #validateImageProperties(ip: ImageProperties): void {
40
+ if (!ip.source || ip.x == null || ip.y == null) {
41
+ throw new Error("createImage: source, x, and y are required.");
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Validates text properties for required fields.
47
+ * @private
48
+ * @param textProps - Text properties to validate
49
+ */
50
+ #validateTextProperties(textProps: TextProperties): void {
51
+ if (!textProps.text || textProps.x == null || textProps.y == null) {
52
+ throw new Error("createText: text, x, and y are required.");
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Renders enhanced text using the new text renderer.
58
+ * @private
59
+ * @param ctx - Canvas 2D context
60
+ * @param textProps - Text properties
61
+ */
62
+ async #renderEnhancedText(ctx: SKRSContext2D, textProps: TextProperties): Promise<void> {
63
+ await EnhancedTextRenderer.renderText(ctx, textProps);
64
+ }
65
+
66
+ /**
67
+ * Creates a canvas with the given configuration.
68
+ * Applies rotation, shadow, border effects, background, and stroke.
69
+ *
70
+ * @param canvas - Canvas configuration object containing:
71
+ * - width: Canvas width in pixels
72
+ * - height: Canvas height in pixels
73
+ * - x: X position offset (default: 0)
74
+ * - y: Y position offset (default: 0)
75
+ * - colorBg: Solid color background (hex, rgb, rgba, hsl, etc.)
76
+ * - gradientBg: Gradient background configuration
77
+ * - customBg: Custom background image buffer
78
+ * - zoom: Canvas zoom level (default: 1)
79
+ * - pattern: Pattern background configuration
80
+ * - noise: Noise effect configuration
81
+ *
82
+ * @returns Promise<CanvasResults> - Object containing canvas buffer and configuration
83
+ *
84
+ * @throws Error if canvas configuration is invalid or conflicting
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const result = await painter.createCanvas({
89
+ * width: 800,
90
+ * height: 600,
91
+ * colorBg: '#ffffff',
92
+ * zoom: 1.5
93
+ * });
94
+ * const buffer = result.buffer;
95
+ * ```
96
+ */
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);
108
+ }
109
+ }
110
+
111
+ // 2) Use final width/height after inherit
112
+ const width = canvas.width ?? 500;
113
+ const height = canvas.height ?? 500;
114
+
115
+ const {
116
+ x = 0, y = 0,
117
+ rotation = 0,
118
+ borderRadius = 0,
119
+ borderPosition = 'all',
120
+ opacity = 1,
121
+ colorBg, customBg, gradientBg,
122
+ patternBg, noiseBg, blendMode,
123
+ zoom, stroke, shadow,
124
+ blur
125
+ } = canvas;
126
+
127
+ // Validate background configuration
128
+ const bgSources = [
129
+ canvas.colorBg ? 'colorBg' : null,
130
+ canvas.gradientBg ? 'gradientBg' : null,
131
+ canvas.customBg ? 'customBg' : null
132
+ ].filter(Boolean);
133
+
134
+ if (bgSources.length > 1) {
135
+ throw new Error(`createCanvas: only one of colorBg, gradientBg, or customBg can be used. You provided: ${bgSources.join(', ')}`);
136
+ }
137
+
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');
141
+
142
+
143
+ ctx.globalAlpha = opacity;
144
+
145
+ // ---- BACKGROUND (clipped) ----
146
+ ctx.save();
147
+ applyRotation(ctx, rotation, x, y, width, height);
148
+
149
+ buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
150
+ ctx.clip();
151
+
152
+ applyCanvasZoom(ctx, width, height, zoom);
153
+
154
+ ctx.translate(x, y);
155
+ if (typeof blendMode === 'string') {
156
+ ctx.globalCompositeOperation = blendMode as any;
157
+ }
158
+
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' });
162
+
163
+ if (patternBg) await EnhancedPatternRenderer.renderPattern(ctx, cv, patternBg);
164
+ if (noiseBg) applyNoise(ctx, width, height, noiseBg.intensity ?? 0.05);
165
+
166
+ ctx.restore();
167
+
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
+ }
175
+
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
+ }
183
+
184
+ return { buffer: cv.toBuffer('image/png'), canvas };
185
+ }
186
+
187
+
188
+
189
+ /**
190
+ * Draws one or more images (or shapes) on an existing canvas buffer.
191
+ *
192
+ * @param images - Single ImageProperties object or array of ImageProperties containing:
193
+ * - source: Image path/URL/Buffer or ShapeType ('rectangle', 'circle', etc.)
194
+ * - x: X position on canvas
195
+ * - y: Y position on canvas
196
+ * - width: Image/shape width (optional, defaults to original size)
197
+ * - height: Image/shape height (optional, defaults to original size)
198
+ * - inherit: Use original image dimensions (boolean)
199
+ * - fit: Image fitting mode ('fill', 'contain', 'cover', 'scale-down', 'none')
200
+ * - align: Image alignment ('center', 'start', 'end')
201
+ * - rotation: Rotation angle in degrees (default: 0)
202
+ * - opacity: Opacity level 0-1 (default: 1)
203
+ * - blur: Blur radius in pixels (default: 0)
204
+ * - borderRadius: Border radius or 'circular' (default: 0)
205
+ * - borderPosition: Border position ('all', 'top', 'bottom', 'left', 'right')
206
+ * - filters: Array of image filters to apply
207
+ * - shape: Shape properties (when source is a shape)
208
+ * - shadow: Shadow configuration
209
+ * - stroke: Stroke configuration
210
+ * - boxBackground: Background behind image/shape
211
+ *
212
+ * @param canvasBuffer - Existing canvas buffer (Buffer) or CanvasResults object
213
+ *
214
+ * @returns Promise<Buffer> - Updated canvas buffer in PNG format
215
+ *
216
+ * @throws Error if source, x, or y are missing
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * const result = await painter.createImage([
221
+ * {
222
+ * source: 'rectangle',
223
+ * x: 100, y: 100,
224
+ * width: 200, height: 150,
225
+ * shape: { fill: true, color: '#ff6b6b' },
226
+ * shadow: { color: 'rgba(0,0,0,0.3)', offsetX: 5, offsetY: 5, blur: 10 }
227
+ * }
228
+ * ], canvasBuffer);
229
+ * ```
230
+ */
231
+ async createImage(
232
+ images: ImageProperties | ImageProperties[],
233
+ canvasBuffer: CanvasResults | Buffer
234
+ ): Promise<Buffer> {
235
+ const list = Array.isArray(images) ? images : [images];
236
+
237
+ // Load base canvas buffer
238
+ const base: Image = Buffer.isBuffer(canvasBuffer)
239
+ ? await loadImage(canvasBuffer)
240
+ : await loadImage((canvasBuffer as CanvasResults).buffer);
241
+
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");
245
+
246
+ // Paint bg
247
+ ctx.drawImage(base, 0, 0);
248
+
249
+ // Draw each image/shape on canvas
250
+ for (const ip of list) {
251
+ await this.#drawImageBitmap(ctx, ip);
252
+ }
253
+
254
+ // Return updated buffer
255
+ return cv.toBuffer("image/png");
256
+ }
257
+
258
+ /**
259
+ * Draws a single bitmap or shape with independent shadow & stroke.
260
+ * @private
261
+ * @param ctx - Canvas 2D context
262
+ * @param ip - Image properties
263
+ */
264
+ async #drawImageBitmap(ctx: SKRSContext2D, ip: ImageProperties): Promise<void> {
265
+ const {
266
+ source, x, y,
267
+ width, height,
268
+ inherit,
269
+ fit = "fill",
270
+ align = "center",
271
+ rotation = 0,
272
+ opacity = 1,
273
+ blur = 0,
274
+ borderRadius = 0,
275
+ borderPosition = "all",
276
+ shadow,
277
+ stroke,
278
+ boxBackground,
279
+ shape,
280
+ filters
281
+ } = ip;
282
+
283
+ this.#validateImageProperties(ip);
284
+
285
+ // Check if source is a shape
286
+ if (isShapeSource(source)) {
287
+ await this.#drawShape(ctx, source, x, y, width ?? 100, height ?? 100, {
288
+ ...shape,
289
+ rotation,
290
+ opacity,
291
+ blur,
292
+ borderRadius,
293
+ borderPosition,
294
+ shadow,
295
+ stroke,
296
+ boxBackground,
297
+ filters
298
+ });
299
+ return;
300
+ }
301
+
302
+ // Handle image sources
303
+ const img = await loadImageCached(source);
304
+
305
+ // Resolve this image's destination box
306
+ const boxW = (inherit && !width) ? img.width : (width ?? img.width);
307
+ const boxH = (inherit && !height) ? img.height : (height ?? img.height);
308
+ const box = { x, y, w: boxW, h: boxH };
309
+
310
+ ctx.save();
311
+
312
+ // Rotate around the box center; affects shadow, background, bitmap, stroke uniformly
313
+ applyRotation(ctx, rotation, box.x, box.y, box.w, box.h);
314
+
315
+ // 1) Shadow (independent) supports gradient or color
316
+ applyShadow(ctx, box, shadow);
317
+
318
+ // 2) Optional box background (under bitmap, inside clip) — color or gradient
319
+ drawBoxBackground(ctx, box, boxBackground, borderRadius, borderPosition);
320
+
321
+ // 3) Clip to image border radius, then draw the bitmap with blur/opacity and fit/align
322
+ ctx.save();
323
+ buildPath(ctx, box.x, box.y, box.w, box.h, borderRadius, borderPosition);
324
+ ctx.clip();
325
+
326
+ const { dx, dy, dw, dh, sx, sy, sw, sh } =
327
+ fitInto(box.x, box.y, box.w, box.h, img.width, img.height, fit, align);
328
+
329
+ const prevAlpha = ctx.globalAlpha;
330
+ ctx.globalAlpha = opacity ?? 1;
331
+ if ((blur ?? 0) > 0) ctx.filter = `blur(${blur}px)`;
332
+
333
+ // Apply professional image filters BEFORE drawing
334
+ if (filters && filters.length > 0) {
335
+ await applySimpleProfessionalFilters(ctx, filters, dw, dh);
336
+ }
337
+
338
+ ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
339
+
340
+ ctx.filter = "none";
341
+ ctx.globalAlpha = prevAlpha;
342
+ ctx.restore();
343
+
344
+
345
+ // 4) Stroke (independent) — supports gradient or color
346
+ applyStroke(ctx, box, stroke);
347
+
348
+ ctx.restore();
349
+ }
350
+
351
+ /**
352
+ * Draws a shape with all effects (shadow, stroke, filters, etc.).
353
+ * @private
354
+ * @param ctx - Canvas 2D context
355
+ * @param shapeType - Type of shape to draw
356
+ * @param x - X position
357
+ * @param y - Y position
358
+ * @param width - Shape width
359
+ * @param height - Shape height
360
+ * @param options - Shape drawing options
361
+ */
362
+ async #drawShape(
363
+ ctx: SKRSContext2D,
364
+ shapeType: ShapeType,
365
+ x: number,
366
+ y: number,
367
+ width: number,
368
+ height: number,
369
+ options: {
370
+ rotation?: number;
371
+ opacity?: number;
372
+ blur?: number;
373
+ borderRadius?: number | 'circular';
374
+ borderPosition?: string;
375
+ shadow?: any;
376
+ stroke?: any;
377
+ boxBackground?: any;
378
+ fill?: boolean;
379
+ color?: string;
380
+ gradient?: any;
381
+ radius?: number;
382
+ sides?: number;
383
+ innerRadius?: number;
384
+ outerRadius?: number;
385
+ filters?: any[];
386
+ }
387
+ ): Promise<void> {
388
+ const box = { x, y, w: width, h: height };
389
+
390
+ ctx.save();
391
+
392
+ // Apply rotation
393
+ if (options.rotation) {
394
+ applyRotation(ctx, options.rotation, box.x, box.y, box.w, box.h);
395
+ }
396
+
397
+ // Apply opacity
398
+ if (options.opacity !== undefined) {
399
+ ctx.globalAlpha = options.opacity;
400
+ }
401
+
402
+ // Apply blur
403
+ if (options.blur && options.blur > 0) {
404
+ ctx.filter = `blur(${options.blur}px)`;
405
+ }
406
+
407
+ // 1) Custom Shadow for complex shapes (heart, star)
408
+ if (options.shadow && this.#isComplexShape(shapeType)) {
409
+ this.#applyShapeShadow(ctx, shapeType, x, y, width, height, options.shadow, {
410
+ radius: options.radius,
411
+ sides: options.sides,
412
+ innerRadius: options.innerRadius,
413
+ outerRadius: options.outerRadius
414
+ });
415
+ } else if (options.shadow) {
416
+ // Use standard shadow for simple shapes
417
+ applyShadow(ctx, box, options.shadow);
418
+ }
419
+
420
+ // 2) Optional box background
421
+ if (options.boxBackground) {
422
+ drawBoxBackground(ctx, box, options.boxBackground, options.borderRadius, options.borderPosition);
423
+ }
424
+
425
+ // 3) Draw the shape
426
+ ctx.save();
427
+ if (options.borderRadius) {
428
+ buildPath(ctx, box.x, box.y, box.w, box.h, options.borderRadius, options.borderPosition);
429
+ ctx.clip();
430
+ }
431
+
432
+ // Apply professional filters BEFORE drawing the shape
433
+ if (options.filters && options.filters.length > 0) {
434
+ await applySimpleProfessionalFilters(ctx, options.filters, width, height);
435
+ }
436
+
437
+ drawShape(ctx, shapeType, x, y, width, height, {
438
+ fill: options.fill,
439
+ color: options.color,
440
+ gradient: options.gradient,
441
+ radius: options.radius,
442
+ sides: options.sides,
443
+ innerRadius: options.innerRadius,
444
+ outerRadius: options.outerRadius
445
+ });
446
+
447
+ ctx.restore();
448
+
449
+
450
+ // 4) Custom Stroke for complex shapes (heart, star)
451
+ if (options.stroke && this.#isComplexShape(shapeType)) {
452
+ this.#applyShapeStroke(ctx, shapeType, x, y, width, height, options.stroke, {
453
+ radius: options.radius,
454
+ sides: options.sides,
455
+ innerRadius: options.innerRadius,
456
+ outerRadius: options.outerRadius
457
+ });
458
+ } else if (options.stroke) {
459
+ // Use standard stroke for simple shapes
460
+ applyStroke(ctx, box, options.stroke);
461
+ }
462
+
463
+ // Reset filters and alpha
464
+ ctx.filter = "none";
465
+ ctx.globalAlpha = 1;
466
+ ctx.restore();
467
+ }
468
+
469
+ /**
470
+ * Checks if shape needs custom shadow/stroke (heart, star).
471
+ * @private
472
+ * @param shapeType - Type of shape
473
+ * @returns True if shape is complex and needs custom effects
474
+ */
475
+ #isComplexShape(shapeType: ShapeType): boolean {
476
+ return shapeType === 'heart' || shapeType === 'star';
477
+ }
478
+
479
+ /**
480
+ * Applies custom shadow for complex shapes (heart, star).
481
+ * @private
482
+ * @param ctx - Canvas 2D context
483
+ * @param shapeType - Type of shape
484
+ * @param x - X position
485
+ * @param y - Y position
486
+ * @param width - Shape width
487
+ * @param height - Shape height
488
+ * @param shadow - Shadow options
489
+ * @param shapeOptions - Shape-specific options
490
+ */
491
+ #applyShapeShadow(
492
+ ctx: SKRSContext2D,
493
+ shapeType: ShapeType,
494
+ x: number,
495
+ y: number,
496
+ width: number,
497
+ height: number,
498
+ shadow: any,
499
+ shapeProps: any
500
+ ): void {
501
+ const {
502
+ color = "rgba(0,0,0,1)",
503
+ gradient,
504
+ opacity = 0.4,
505
+ offsetX = 0,
506
+ offsetY = 0,
507
+ blur = 20
508
+ } = shadow;
509
+
510
+ ctx.save();
511
+ ctx.globalAlpha = opacity;
512
+ if (blur > 0) ctx.filter = `blur(${blur}px)`;
513
+
514
+ // Set shadow color or gradient
515
+ if (gradient) {
516
+ const gfill = createGradientFill(ctx, gradient, { x: x + offsetX, y: y + offsetY, w: width, h: height });
517
+ ctx.fillStyle = gfill as any;
518
+ } else {
519
+ ctx.fillStyle = color;
520
+ }
521
+
522
+ // Create shadow path
523
+ createShapePath(ctx, shapeType, x + offsetX, y + offsetY, width, height, shapeProps);
524
+ ctx.fill();
525
+
526
+ ctx.filter = "none";
527
+ ctx.globalAlpha = 1;
528
+ ctx.restore();
529
+ }
530
+
531
+ /**
532
+ * Applies custom stroke for complex shapes (heart, star).
533
+ * @private
534
+ * @param ctx - Canvas 2D context
535
+ * @param shapeType - Type of shape
536
+ * @param x - X position
537
+ * @param y - Y position
538
+ * @param width - Shape width
539
+ * @param height - Shape height
540
+ * @param stroke - Stroke options
541
+ * @param shapeOptions - Shape-specific options
542
+ */
543
+ #applyShapeStroke(
544
+ ctx: SKRSContext2D,
545
+ shapeType: ShapeType,
546
+ x: number,
547
+ y: number,
548
+ width: number,
549
+ height: number,
550
+ stroke: any,
551
+ shapeProps: any
552
+ ): void {
553
+ const {
554
+ color = "#000",
555
+ gradient,
556
+ width: strokeWidth = 2,
557
+ position = 0,
558
+ blur = 0,
559
+ opacity = 1
560
+ } = stroke;
561
+
562
+ ctx.save();
563
+ if (blur > 0) ctx.filter = `blur(${blur}px)`;
564
+ ctx.globalAlpha = opacity;
565
+
566
+ // Set stroke color or gradient
567
+ if (gradient) {
568
+ const gstroke = createGradientFill(ctx, gradient, { x, y, w: width, h: height });
569
+ ctx.strokeStyle = gstroke as any;
570
+ } else {
571
+ ctx.strokeStyle = color;
572
+ }
573
+
574
+ ctx.lineWidth = strokeWidth;
575
+
576
+ // Create stroke path
577
+ createShapePath(ctx, shapeType, x, y, width, height, shapeProps);
578
+ ctx.stroke();
579
+
580
+ ctx.filter = "none";
581
+ ctx.globalAlpha = 1;
582
+ ctx.restore();
583
+ }
584
+
585
+
586
+ /**
587
+ * Creates text on an existing canvas buffer with enhanced styling options.
588
+ *
589
+ * @param textArray - Single TextProperties object or array of TextProperties containing:
590
+ * - text: Text content to render (required)
591
+ * - x: X position on canvas (required)
592
+ * - y: Y position on canvas (required)
593
+ * - fontPath: Path to custom font file (.ttf, .otf, .woff, etc.)
594
+ * - fontName: Custom font name (used with fontPath)
595
+ * - fontSize: Font size in pixels (default: 16)
596
+ * - fontFamily: Font family name (e.g., 'Arial', 'Helvetica')
597
+ * - bold: Make text bold (boolean)
598
+ * - italic: Make text italic (boolean)
599
+ * - underline: Add underline decoration (boolean)
600
+ * - overline: Add overline decoration (boolean)
601
+ * - strikethrough: Add strikethrough decoration (boolean)
602
+ * - highlight: Highlight text with background color and opacity
603
+ * - lineHeight: Line height multiplier (default: 1.4)
604
+ * - letterSpacing: Space between letters in pixels
605
+ * - wordSpacing: Space between words in pixels
606
+ * - maxWidth: Maximum width for text wrapping
607
+ * - maxHeight: Maximum height for text (truncates with ellipsis)
608
+ * - textAlign: Horizontal text alignment ('left', 'center', 'right', 'start', 'end')
609
+ * - textBaseline: Vertical text baseline ('alphabetic', 'bottom', 'hanging', 'ideographic', 'middle', 'top')
610
+ * - color: Text color (hex, rgb, rgba, hsl, etc.)
611
+ * - gradient: Gradient fill for text
612
+ * - opacity: Text opacity (0-1, default: 1)
613
+ * - glow: Text glow effect with color, intensity, and opacity
614
+ * - shadow: Text shadow effect with color, offset, blur, and opacity
615
+ * - stroke: Text stroke/outline with color, width, gradient, and opacity
616
+ * - rotation: Text rotation in degrees
617
+ *
618
+ * @param canvasBuffer - Existing canvas buffer (Buffer) or CanvasResults object
619
+ *
620
+ * @returns Promise<Buffer> - Updated canvas buffer in PNG format
621
+ *
622
+ * @throws Error if text, x, or y are missing, or if canvas buffer is invalid
623
+ *
624
+ * @example
625
+ * ```typescript
626
+ * const result = await painter.createText([
627
+ * {
628
+ * text: "Hello World!",
629
+ * x: 100, y: 100,
630
+ * fontSize: 24,
631
+ * bold: true,
632
+ * color: '#ff6b6b',
633
+ * shadow: { color: 'rgba(0,0,0,0.3)', offsetX: 2, offsetY: 2, blur: 4 },
634
+ * underline: true,
635
+ * highlight: { color: '#ffff00', opacity: 0.3 }
636
+ * }
637
+ * ], canvasBuffer);
638
+ * ```
639
+ */
640
+ async createText(textArray: TextProperties | TextProperties[], canvasBuffer: CanvasResults | Buffer): Promise<Buffer> {
641
+ try {
642
+ // Ensure textArray is an array
643
+ const textList = Array.isArray(textArray) ? textArray : [textArray];
644
+
645
+ // Validate each text object
646
+ for (const textProps of textList) {
647
+ this.#validateTextProperties(textProps);
648
+ }
649
+
650
+ // Load existing canvas buffer
651
+ let existingImage: any;
652
+
653
+ if (Buffer.isBuffer(canvasBuffer)) {
654
+ existingImage = await loadImage(canvasBuffer);
655
+ } else if (canvasBuffer && canvasBuffer.buffer) {
656
+ existingImage = await loadImage(canvasBuffer.buffer);
657
+ } else {
658
+ throw new Error('Invalid canvasBuffer provided. It should be a Buffer or CanvasResults object with a buffer');
659
+ }
660
+
661
+ if (!existingImage) {
662
+ throw new Error('Unable to load image from buffer');
663
+ }
664
+
665
+ // Create new canvas with same dimensions
666
+ const canvas = createCanvas(existingImage.width, existingImage.height);
667
+ const ctx = canvas.getContext("2d");
668
+
669
+ if (!ctx) {
670
+ throw new Error("Unable to get 2D rendering context");
671
+ }
672
+
673
+ // Draw existing image as background
674
+ ctx.drawImage(existingImage, 0, 0);
675
+
676
+ // Render each text object with enhanced features
677
+ for (const textProps of textList) {
678
+ await this.#renderEnhancedText(ctx, textProps);
679
+ }
680
+
681
+ return canvas.toBuffer("image/png");
682
+ } catch (error) {
683
+ console.error("Error creating text:", error);
684
+ throw new Error("Failed to create text on canvas");
685
+ }
686
+ }
687
+
688
+
689
+
690
+ async createCustom(options: CustomOptions[], buffer: CanvasResults | Buffer, ): Promise<Buffer> {
691
+ try {
692
+
693
+ if (!Array.isArray(options)) {
694
+ options = [options];
695
+ }
696
+
697
+ let existingImage: any;
698
+
699
+ if (Buffer.isBuffer(buffer)) {
700
+ existingImage = await loadImage(buffer);
701
+ } else if (buffer && buffer.buffer) {
702
+ existingImage = await loadImage(buffer.buffer);
703
+ } else {
704
+ throw new Error('Invalid canvasBuffer provided. It should be a Buffer or CanvasResults object with a buffer');
705
+ }
706
+
707
+ if (!existingImage) {
708
+ throw new Error('Unable to load image from buffer');
709
+ }
710
+
711
+ const canvas = createCanvas(existingImage.width, existingImage.height);
712
+ const ctx = canvas.getContext("2d");
713
+
714
+ ctx.drawImage(existingImage, 0, 0);
715
+
716
+ customLines(ctx, options);
717
+
718
+ return canvas.toBuffer("image/png");
719
+ } catch (error) {
720
+ console.error("Error creating custom image:", error);
721
+ throw new Error("Failed to create custom image");
722
+ }
723
+ }
724
+
725
+ async createGIF(gifFrames: { background: string; duration: number }[], options: GIFOptions): Promise<GIFResults | any> {
726
+ async function resizeImage(image: any, targetWidth: number, targetHeight: number) {
727
+ const canvas = createCanvas(targetWidth, targetHeight);
728
+ const ctx = canvas.getContext("2d");
729
+ ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
730
+ return canvas;
731
+ }
732
+
733
+ function createOutputStream(outputFile: string): fs.WriteStream {
734
+ return fs.createWriteStream(outputFile);
735
+ }
736
+
737
+ function createBufferStream() {
738
+ const bufferStream = new PassThrough();
739
+ const chunks: Buffer[] = [];
740
+
741
+ bufferStream.on('data', (chunk: Buffer) => {
742
+ chunks.push(chunk);
743
+ });
744
+
745
+ return {
746
+ ...bufferStream,
747
+ getBuffer: function (): Buffer {
748
+ return Buffer.concat(chunks);
749
+ }
750
+ } as any;
751
+ }
752
+
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);
782
+
783
+ const canvasWidth = options.width || 1200;
784
+ const canvasHeight = options.height || 1200;
785
+
786
+ const encoder = new GIFEncoder(canvasWidth, canvasHeight);
787
+ const outputStream = options.outputFile ? createOutputStream(options.outputFile) : createBufferStream();
788
+
789
+ encoder.createReadStream().pipe(outputStream);
790
+
791
+ encoder.start();
792
+ encoder.setRepeat(options.repeat || 0);
793
+ encoder.setQuality(options.quality || 10);
794
+
795
+ const canvas = createCanvas(canvasWidth, canvasHeight);
796
+ const ctx:any = canvas.getContext("2d");
797
+
798
+ for (const frame of gifFrames) {
799
+ const image = await loadImage(frame.background);
800
+ const resizedImage = await resizeImage(image, canvasWidth, canvasHeight);
801
+
802
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
803
+ ctx.drawImage(resizedImage, 0, 0);
804
+
805
+ if (options.watermark?.enable) {
806
+ const watermark = await loadImage(options.watermark.url);
807
+ ctx.drawImage(watermark, 10, canvasHeight - watermark.height - 10);
808
+ }
809
+
810
+ if (options.textOverlay) {
811
+ ctx.font = `${options.textOverlay.fontSize || 20}px Arial`;
812
+ ctx.fillStyle = options.textOverlay.fontColor || "white";
813
+ ctx.fillText(options.textOverlay.text, options.textOverlay.x || 10, options.textOverlay.y || 30);
814
+ }
815
+
816
+ encoder.setDelay(frame.duration);
817
+ encoder.addFrame(ctx);
818
+ }
819
+
820
+ encoder.finish();
821
+ outputStream.end();
822
+
823
+ if (options.outputFormat === "file") {
824
+ await new Promise((resolve) => outputStream.on("finish", resolve));
825
+ } else if (options.outputFormat === "base64") {
826
+ if ('getBuffer' in outputStream) {
827
+ return outputStream.getBuffer().toString("base64");
828
+ }
829
+ } else if (options.outputFormat === "attachment") {
830
+ const gifStream = encoder.createReadStream();
831
+ return [{ attachment: gifStream, name: "gif.js" }];
832
+ } else if (options.outputFormat === "buffer") {
833
+ if ('getBuffer' in outputStream) {
834
+ return outputStream.getBuffer();
835
+ }
836
+ } else {
837
+ throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
838
+ }
839
+ } catch (e: any) {
840
+ console.error(e.message);
841
+ throw e;
842
+ }
843
+ }
844
+
845
+ async resize(resizeOptions: ResizeOptions) {
846
+ return resizingImg(resizeOptions)
847
+ }
848
+
849
+ async imgConverter(source: string, newExtension: string) {
850
+ return converter(source, newExtension)
851
+ }
852
+
853
+ async effects(source: string, filters: any[]) {
854
+ return imgEffects(source, filters)
855
+ }
856
+
857
+ async colorsFilter(source: string, filterColor: any, opacity?: number) {
858
+ return applyColorFilters(source, filterColor, opacity)
859
+ }
860
+
861
+ async colorAnalysis(source: string) {
862
+ return detectColors(source)
863
+ }
864
+
865
+ async colorsRemover(source: string, colorToRemove: { red: number, green: number, blue: number }) {
866
+ return removeColor(source, colorToRemove)
867
+ }
868
+
869
+ async removeBackground(imageURL: string, apiKey: string) {
870
+ return bgRemoval(imageURL, apiKey)
871
+ }
872
+
873
+ async blend(
874
+ layers: {
875
+ 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';
883
+ position?: { x: number; y: number };
884
+ opacity?: number;
885
+ }[],
886
+ 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'
894
+ ): Promise<Buffer> {
895
+ try {
896
+ const baseImage = await loadImage(baseImageBuffer);
897
+ const canvas = createCanvas(baseImage.width, baseImage.height);
898
+ const ctx = canvas.getContext('2d');
899
+
900
+ ctx.globalCompositeOperation = defaultBlendMode;
901
+ ctx.drawImage(baseImage, 0, 0);
902
+
903
+ for (const layer of layers) {
904
+ const layerImage = await loadImage(layer.image);
905
+ ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1.0;
906
+
907
+ ctx.globalCompositeOperation = layer.blendMode;
908
+ ctx.drawImage(layerImage, layer.position?.x || 0, layer.position?.y || 0);
909
+ }
910
+
911
+ ctx.globalAlpha = 1.0;
912
+ ctx.globalCompositeOperation = defaultBlendMode; // Reset to user-defined default
913
+
914
+ return canvas.toBuffer('image/png');
915
+ } catch (error) {
916
+ console.error('Error creating layered composition:', error);
917
+ throw new Error('Failed to create layered composition');
918
+ }
919
+ }
920
+
921
+
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.');
926
+ }
927
+
928
+ if (!type || !type.chartNumber || !type.chartType) {
929
+ throw new Error('Type arguments are missing.');
930
+ }
931
+
932
+ const { chartType, chartNumber } = type;
933
+
934
+ switch (chartType.toLowerCase()) {
935
+ case 'bar':
936
+ switch (chartNumber) {
937
+ case 1:
938
+ return await verticalBarChart(data);
939
+ case 2:
940
+ throw new Error('Type 2 is still under development.');
941
+ default:
942
+ throw new Error('Invalid chart number for chart type "bar".');
943
+ }
944
+ case 'line':
945
+ switch (chartNumber) {
946
+ case 1:
947
+ return await lineChart(data);
948
+ case 2:
949
+ throw new Error('Type 2 is still under development.');
950
+ default:
951
+ throw new Error('Invalid chart number for chart type "line".');
952
+ }
953
+ case 'pie':
954
+ switch (chartNumber) {
955
+ case 1:
956
+ return await pieChart(data);
957
+ case 2:
958
+ throw new Error('Type 2 is still under development.');
959
+ default:
960
+ throw new Error('Invalid chart number for chart type "pie".');
961
+ }
962
+ default:
963
+ throw new Error(`Unsupported chart type "${chartType}".`);
964
+ }
965
+ }
966
+
967
+
968
+ async cropImage(options: cropOptions): Promise<Buffer> {
969
+ 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.');
972
+
973
+ if (options.crop === 'outer') {
974
+ return await cropOuter(options);
975
+ } else if (options.crop === 'inner') {
976
+ return await cropInner(options);
977
+ } else {
978
+ throw new Error('Invalid crop option. Please specify "inner" or "outer".');
979
+ }
980
+ } catch (error) {
981
+ console.error('An error occurred:', error);
982
+ throw error;
983
+ }
984
+ }
985
+
986
+ private async drawImage(ctx: SKRSContext2D, image: ImageProperties): Promise<void> {
987
+
988
+
989
+ }
990
+
991
+
992
+ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions): Promise<any[]> {
993
+ const frames: any[]= [];
994
+ const frameDir = path.join(__dirname, 'frames');
995
+
996
+ if (!fs.existsSync(frameDir)) {
997
+ fs.mkdirSync(frameDir);
998
+ }
999
+
1000
+ const videoPath = typeof videoSource === 'string' ? videoSource : path.join(frameDir, 'temp-video.mp4');
1001
+
1002
+ if (Buffer.isBuffer(videoSource)) {
1003
+ fs.writeFileSync(videoPath, videoSource);
1004
+ } else if (videoSource.startsWith('http')) {
1005
+ await axios({
1006
+ method: 'get',
1007
+ url: videoSource,
1008
+ responseType: 'arraybuffer'
1009
+ })
1010
+ .then((response) => {
1011
+ fs.writeFileSync(videoPath, response.data);
1012
+ })
1013
+ .catch(err => {
1014
+ throw new Error(`Error downloading video: ${err.message}`);
1015
+ });
1016
+ } else if (!fs.existsSync(videoPath)) {
1017
+ throw new Error("Video file not found at specified path.");
1018
+ }
1019
+
1020
+ function processVideoExtraction(videoPath: string, frames: any[], options: ExtractFramesOptions, resolve: any, reject: any) {
1021
+ const outputFormat = options.outputFormat || 'jpg';
1022
+ const outputFileTemplate = `frame-%03d.${outputFormat}`;
1023
+
1024
+ ffmpeg(videoPath)
1025
+ .on('end', () => {
1026
+ console.log('Frames extracted successfully.');
1027
+ resolve(frames);
1028
+ })
1029
+ .on('error', (err: { message: any; }) => {
1030
+ console.error('Error extracting frames:', err.message);
1031
+ reject(err);
1032
+ })
1033
+ .outputOptions([`-vf fps=1/${options.interval / 1000}`, `-q:v 2`])
1034
+ .saveToFile(path.join(frameDir, outputFileTemplate));
1035
+
1036
+ ffmpeg.ffprobe(videoPath, (err: any, metadata: any) => {
1037
+ if (err) {
1038
+ return reject(err);
1039
+ }
1040
+ const duration = metadata?.format?.duration;
1041
+ if (typeof duration !== "number") {
1042
+ return reject(new Error("Video duration not found in metadata."));
1043
+ }
1044
+ const totalFrames = Math.floor(duration * 1000 / options.interval);
1045
+ for (let i = 0; i < totalFrames; i++) {
1046
+ if (options.frameSelection && (i < (options.frameSelection.start || 0) || i > (options.frameSelection.end || totalFrames - 1))) {
1047
+ continue;
1048
+ }
1049
+ frames.push({
1050
+ source: path.join(frameDir, `frame-${String(i).padStart(3, '0')}.${outputFormat}`),
1051
+ isRemote: false
1052
+ });
1053
+ }
1054
+ });
1055
+ }
1056
+
1057
+ return new Promise((resolve, reject) => {
1058
+ processVideoExtraction(videoPath, frames, options, resolve, reject);
1059
+ });
1060
+ }
1061
+
1062
+
1063
+
1064
+
1065
+
1066
+
1067
+
1068
+
1069
+
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);
1077
+
1078
+ const canvas = createCanvas(img.width, img.height);
1079
+ const ctx = canvas.getContext("2d") as SKRSContext2D;
1080
+
1081
+ ctx.drawImage(img, 0, 0, img.width, img.height);
1082
+
1083
+ const maskCanvas = createCanvas(img.width, img.height);
1084
+ const maskCtx = maskCanvas.getContext("2d") as SKRSContext2D;
1085
+ maskCtx.drawImage(mask, 0, 0, img.width, img.height);
1086
+
1087
+ const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
1088
+ const imgData = ctx.getImageData(0, 0, img.width, img.height);
1089
+
1090
+ for (let i = 0; i < maskData.data.length; i += 4) {
1091
+ let alphaValue = 255;
1092
+
1093
+ if (options.type === "grayscale") {
1094
+ const grayscale = maskData.data[i] * 0.3 + maskData.data[i + 1] * 0.59 + maskData.data[i + 2] * 0.11;
1095
+ alphaValue = grayscale >= (options.threshold ?? 128) ? 255 : 0;
1096
+ } else if (options.type === "alpha") {
1097
+ alphaValue = maskData.data[i + 3];
1098
+ } else if (options.type === "color" && options.colorKey) {
1099
+ const colorMatch =
1100
+ maskData.data[i] === parseInt(options.colorKey.slice(1, 3), 16) &&
1101
+ maskData.data[i + 1] === parseInt(options.colorKey.slice(3, 5), 16) &&
1102
+ maskData.data[i + 2] === parseInt(options.colorKey.slice(5, 7), 16);
1103
+ alphaValue = colorMatch ? 0 : 255;
1104
+ }
1105
+
1106
+ if (options.invert) alphaValue = 255 - alphaValue;
1107
+
1108
+ imgData.data[i + 3] = alphaValue;
1109
+ }
1110
+
1111
+ ctx.putImageData(imgData, 0, 0);
1112
+
1113
+ return canvas.toBuffer("image/png");
1114
+ }
1115
+
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;
1123
+
1124
+ ctx.drawImage(img, 0, 0, img.width, img.height);
1125
+
1126
+ let gradient: CanvasGradient;
1127
+ if (options.type === "linear") {
1128
+ const angle = options.angle ?? 0;
1129
+ const radians = (angle * Math.PI) / 180;
1130
+ const x1 = img.width / 2 - (Math.cos(radians) * img.width) / 2;
1131
+ const y1 = img.height / 2 - (Math.sin(radians) * img.height) / 2;
1132
+ const x2 = img.width / 2 + (Math.cos(radians) * img.width) / 2;
1133
+ const y2 = img.height / 2 + (Math.sin(radians) * img.height) / 2;
1134
+ gradient = ctx.createLinearGradient(x1, y1, x2, y2);
1135
+ } else if (options.type === "radial") {
1136
+ gradient = ctx.createRadialGradient(
1137
+ img.width / 2, img.height / 2, 0, img.width / 2, img.height / 2, Math.max(img.width, img.height)
1138
+ );
1139
+ } else {
1140
+ gradient = ctx.createConicGradient(Math.PI, img.width / 2, img.height / 2);
1141
+ }
1142
+
1143
+ options.colors.forEach(({ stop, color }: any) => gradient.addColorStop(stop, color));
1144
+ ctx.fillStyle = gradient;
1145
+
1146
+ ctx.globalCompositeOperation = options.blendMode ?? "multiply";
1147
+ ctx.fillRect(0, 0, img.width, img.height);
1148
+
1149
+ if (options.maskSource) {
1150
+ const mask = await loadImage(options.maskSource);
1151
+ ctx.globalCompositeOperation = "destination-in";
1152
+ ctx.drawImage(mask, 0, 0, img.width, img.height);
1153
+ }
1154
+
1155
+ ctx.globalCompositeOperation = "source-over";
1156
+
1157
+ return canvas.toBuffer("image/png");
1158
+ }
1159
+
1160
+ async animate(
1161
+ frames: Frame[],
1162
+ defaultDuration: number,
1163
+ defaultWidth: number = 800,
1164
+ defaultHeight: number = 600,
1165
+ options?: {
1166
+ gif?: boolean;
1167
+ gifPath?: string;
1168
+ onStart?: () => void;
1169
+ onFrame?: (index: number) => void;
1170
+ onEnd?: () => void;
1171
+ }
1172
+ ): Promise<Buffer[] | undefined> {
1173
+ const buffers: Buffer[] = [];
1174
+ const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
1175
+
1176
+ if (options?.onStart) options.onStart();
1177
+
1178
+ let encoder: GIFEncoder | null = null;
1179
+ let gifStream: fs.WriteStream | null = null;
1180
+
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);
1187
+ encoder.createReadStream().pipe(gifStream);
1188
+ encoder.start();
1189
+ encoder.setRepeat(0);
1190
+ encoder.setQuality(10);
1191
+ }
1192
+
1193
+ for (let i = 0; i < frames.length; i++) {
1194
+ const frame = frames[i];
1195
+
1196
+ const width = frame.width || defaultWidth;
1197
+ const height = frame.height || defaultHeight;
1198
+ const canvas = createCanvas(width, height);
1199
+ const ctx: SKRSContext2D = canvas.getContext('2d');
1200
+
1201
+ if (!isNode) {
1202
+ canvas.width = width;
1203
+ canvas.height = height;
1204
+ document.body.appendChild(canvas as unknown as Node);
1205
+ }
1206
+
1207
+ ctx.clearRect(0, 0, width, height);
1208
+
1209
+ if (frame.transformations) {
1210
+ const { scaleX = 1, scaleY = 1, rotate = 0, translateX = 0, translateY = 0 } = frame.transformations;
1211
+ ctx.save();
1212
+ ctx.translate(translateX, translateY);
1213
+ ctx.rotate((rotate * Math.PI) / 180);
1214
+ ctx.scale(scaleX, scaleY);
1215
+ }
1216
+
1217
+ let fillStyle: string | CanvasGradient | CanvasPattern | null = null;
1218
+
1219
+ if (frame.gradient) {
1220
+ const { type, startX, startY, endX, endY, startRadius, endRadius, colors } = frame.gradient;
1221
+ let gradient: CanvasGradient | null = null;
1222
+
1223
+ if (type === 'linear') {
1224
+ gradient = ctx.createLinearGradient(startX || 0, startY || 0, endX || width, endY || height);
1225
+ } else if (type === 'radial') {
1226
+ gradient = ctx.createRadialGradient(
1227
+ startX || width / 2,
1228
+ startY || height / 2,
1229
+ startRadius || 0,
1230
+ endX || width / 2,
1231
+ endY || height / 2,
1232
+ endRadius || Math.max(width, height)
1233
+ );
1234
+ }
1235
+
1236
+ colors.forEach((colorStop: any) => {
1237
+ if (gradient) gradient.addColorStop(colorStop.stop, colorStop.color);
1238
+ });
1239
+
1240
+ fillStyle = gradient;
1241
+ }
1242
+
1243
+ if (frame.pattern) {
1244
+ const patternImage = await loadImage(frame.pattern.source);
1245
+ const pattern = ctx.createPattern(patternImage, frame.pattern.repeat || 'repeat');
1246
+ fillStyle = pattern;
1247
+ }
1248
+
1249
+ if (!fillStyle && frame.backgroundColor) {
1250
+ fillStyle = frame.backgroundColor;
1251
+ }
1252
+
1253
+ if (fillStyle) {
1254
+ ctx.fillStyle = fillStyle;
1255
+ ctx.fillRect(0, 0, width, height);
1256
+ }
1257
+
1258
+ if (frame.source) {
1259
+ const image = await loadImage(frame.source);
1260
+ ctx.globalCompositeOperation = frame.blendMode || 'source-over';
1261
+ ctx.drawImage(image, 0, 0, width, height);
1262
+ }
1263
+
1264
+ if (frame.onDrawCustom) {
1265
+ frame.onDrawCustom(ctx as unknown as SKRSContext2D, canvas);
1266
+ }
1267
+
1268
+ if (frame.transformations) {
1269
+ ctx.restore();
1270
+ }
1271
+
1272
+ const buffer = canvas.toBuffer('image/png');
1273
+ buffers.push(buffer);
1274
+
1275
+ if (encoder) {
1276
+
1277
+ const frameDuration = frame.duration || defaultDuration;
1278
+ encoder.setDelay(frameDuration);
1279
+ encoder.addFrame(ctx as unknown as CanvasRenderingContext2D);
1280
+ }
1281
+
1282
+ if (options?.onFrame) options.onFrame(i);
1283
+
1284
+ await new Promise(resolve => setTimeout(resolve, frame.duration || defaultDuration));
1285
+ }
1286
+
1287
+ if (encoder) {
1288
+ encoder.finish();
1289
+ }
1290
+
1291
+ if (options?.onEnd) options.onEnd();
1292
+
1293
+ return options?.gif ? undefined : buffers;
1294
+ }
1295
+
1296
+
1297
+
1298
+ public validHex(hexColor: string): any {
1299
+ const hexPattern = /^#[0-9a-fA-F]{6}$/;
1300
+ 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);
1322
+ default:
1323
+ throw new Error('Unsupported format');
1324
+ }
1325
+ }
1219
1326
  }