apexify.js 4.5.30 → 4.5.31

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.
@@ -1,27 +1,25 @@
1
- import { createCanvas, loadImage, GlobalFonts, Image } from "@napi-rs/canvas";
1
+ import { createCanvas, loadImage, GlobalFonts, Image, Canvas } from "@napi-rs/canvas";
2
2
  import GIFEncoder from "gifencoder";
3
- import { PassThrough, Writable} from "stream";
3
+ import ffmpeg from 'fluent-ffmpeg';
4
+ import { PassThrough} from "stream";
5
+ import axios from 'axios';
4
6
  import fs from "fs";
5
7
  import path from "path";
6
8
  import { OutputFormat, CanvasConfig, TextObject, ImageProperties, ImageObject, GIFOptions, GIFResults, CustomOptions, cropOptions,
7
- drawBackgroundGradient, drawBackgroundColor, customBackground, circularBorder, radiusBorder, customLines,
8
- applyRotation, applyStroke, applyShadow, imageRadius, drawShape, drawText,
9
- converter, resizingImg, applyColorFilters, imgEffects, verticalBarChart, pieChart, lineChart, cropInner, cropOuter, bgRemoval, detectColors, removeColor,
10
- dataURL,
11
- base64,
12
- arrayBuffer,
13
- blob,
14
- url
9
+ drawBackgroundGradient, drawBackgroundColor, customBackground, circularBorder, radiusBorder, 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
15
13
  } from "./utils/utils";
16
14
 
17
-
18
15
  interface CanvasResults {
19
16
  buffer: Buffer;
20
17
  canvas: CanvasConfig;
21
18
  }
19
+
22
20
  export class ApexPainter {
23
21
  private format?: OutputFormat;
24
-
22
+
25
23
  constructor({ type }: OutputFormat = { type: 'buffer' }) {
26
24
  this.format = { type: type || 'buffer' };
27
25
  }
@@ -61,20 +59,26 @@ export class ApexPainter {
61
59
  return { buffer: canvasInstance.toBuffer('image/png'), canvas }
62
60
  }
63
61
 
64
- async createImage(images: ImageProperties[], canvasBuffer: CanvasResults): Promise<Buffer> {
62
+ async createImage(images: ImageProperties[], canvasBuffer: CanvasResults | Buffer): Promise<Buffer> {
65
63
 
66
64
  if (!Array.isArray(images)) {
67
65
  images = [images];
68
66
  }
69
67
 
70
- if (!canvasBuffer.buffer) {
71
- throw new Error('You need to provide your canvasConfig. Check documentation if you don\'t know.');
72
- }
73
- const existingCanvas: any = await loadImage(canvasBuffer.buffer);
74
-
75
- if (!existingCanvas) {
76
- throw new Error('Either buffer or canvasConfig is an empty background with no properties');
77
- }
68
+ let existingCanvas: any;
69
+
70
+ if (Buffer.isBuffer(canvasBuffer)) {
71
+ existingCanvas = await loadImage(canvasBuffer);
72
+ } else if (canvasBuffer && canvasBuffer.buffer) {
73
+ existingCanvas = await loadImage(canvasBuffer.buffer);
74
+ } else {
75
+ throw new Error('Invalid canvasBuffer provided, should be Buffer or CanvasResults object with buffer');
76
+ }
77
+
78
+
79
+ if (!existingCanvas) {
80
+ throw new Error('Unable to load image from buffer');
81
+ }
78
82
 
79
83
  const canvas = createCanvas(existingCanvas.width, existingCanvas.height);
80
84
  const ctx = canvas.getContext('2d');
@@ -92,11 +96,26 @@ export class ApexPainter {
92
96
  return canvas.toBuffer('image/png');
93
97
  }
94
98
 
95
- async createText(textOptionsArray: TextObject[], buffer: CanvasResults): Promise<Buffer> {
99
+ async createText(textOptionsArray: TextObject[], buffer: CanvasResults | Buffer ): Promise<Buffer> {
96
100
  try {
97
- const existingImage = await loadImage(buffer.buffer);
98
101
 
99
- if (!existingImage) throw new Error("Invalid image buffer. Make sure to pass a valid canvas buffer.");
102
+ if (!Array.isArray(textOptionsArray)) {
103
+ textOptionsArray = [textOptionsArray];
104
+ }
105
+
106
+ let existingImage: any;
107
+
108
+ if (Buffer.isBuffer(buffer)) {
109
+ existingImage = await loadImage(buffer);
110
+ } else if (buffer && buffer.buffer) {
111
+ existingImage = await loadImage(buffer.buffer);
112
+ } else {
113
+ throw new Error('Invalid canvasBuffer provided. It should be a Buffer or CanvasResults object with a buffer');
114
+ }
115
+
116
+ if (!existingImage) {
117
+ throw new Error('Unable to load image from buffer');
118
+ }
100
119
 
101
120
  const canvas = createCanvas(existingImage.width, existingImage.height);
102
121
  const ctx = canvas.getContext("2d");
@@ -126,298 +145,161 @@ export class ApexPainter {
126
145
  }
127
146
 
128
147
 
129
- async createCustom(buffer: CanvasResults, options: CustomOptions[]): Promise<Buffer> {
148
+
149
+ async createCustom(options: CustomOptions[], buffer: CanvasResults | Buffer, ): Promise<Buffer> {
130
150
  try {
131
- const existingImage = await loadImage(buffer.buffer);
132
151
 
133
- if (!existingImage) throw new Error("Invalid image buffer. Make sure to pass a valid canvas buffer.");
152
+ if (!Array.isArray(options)) {
153
+ options = [options];
154
+ }
134
155
 
135
- const canvas = createCanvas(existingImage.width, existingImage.height);
136
- const ctx = canvas.getContext("2d");
156
+ let existingImage: any;
157
+
158
+ if (Buffer.isBuffer(buffer)) {
159
+ existingImage = await loadImage(buffer);
160
+ } else if (buffer && buffer.buffer) {
161
+ existingImage = await loadImage(buffer.buffer);
162
+ } else {
163
+ throw new Error('Invalid canvasBuffer provided. It should be a Buffer or CanvasResults object with a buffer');
164
+ }
137
165
 
138
- ctx.drawImage(existingImage, 0, 0);
166
+ if (!existingImage) {
167
+ throw new Error('Unable to load image from buffer');
168
+ }
139
169
 
140
- customLines(ctx, options)
170
+ const canvas = createCanvas(existingImage.width, existingImage.height);
171
+ const ctx = canvas.getContext("2d");
141
172
 
142
- return canvas.toBuffer("image/png");
173
+ ctx.drawImage(existingImage, 0, 0);
174
+
175
+ customLines(ctx, options);
176
+
177
+ return canvas.toBuffer("image/png");
143
178
  } catch (error) {
144
179
  console.error("Error creating custom image:", error);
145
180
  throw new Error("Failed to create custom image");
146
181
  }
147
182
  }
148
183
 
149
- async createGIF(images: ImageObject[], options: GIFOptions): Promise<GIFResults | any> {
150
- async function resizeImage(image: any, targetWidth: number, targetHeight: number) {
184
+ async createGIF(gifFrames: { background: string; duration: number }[], options: GIFOptions): Promise<GIFResults | any> {
185
+ async function resizeImage(image: any, targetWidth: number, targetHeight: number) {
151
186
  const canvas = createCanvas(targetWidth, targetHeight);
152
187
  const ctx = canvas.getContext("2d");
153
188
  ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
154
189
  return canvas;
155
- }
190
+ }
156
191
 
157
- function createOutputStream(outputFile: any): fs.WriteStream {
192
+ function createOutputStream(outputFile: string): fs.WriteStream {
158
193
  return fs.createWriteStream(outputFile);
159
194
  }
160
195
 
161
- function createBufferStream(): Writable & { getBuffer: () => Buffer } {
196
+ function createBufferStream() {
162
197
  const bufferStream = new PassThrough();
163
198
  const chunks: Buffer[] = [];
164
-
199
+
165
200
  bufferStream.on('data', (chunk: Buffer) => {
166
- chunks.push(chunk);
201
+ chunks.push(chunk);
167
202
  });
168
-
169
- const customBufferStream = bufferStream as unknown as Writable & { getBuffer: () => Buffer };
170
- customBufferStream.getBuffer = function(): Buffer {
171
- return Buffer.concat(chunks);
172
- };
173
-
174
- return customBufferStream;
175
- }
176
-
177
- function validateOptions(options: any) {
178
- if (options.outputFormat === "file" && !options.outputFile) {
179
- throw new Error(
180
- "Output file path is required when using file output format.",
181
- );
182
- }
183
-
184
- if (
185
- options.repeat !== undefined &&
186
- (typeof options.repeat !== "number" || options.repeat < 0)
187
- ) {
188
- throw new Error("Repeat must be a non-negative number or undefined.");
189
- }
203
+
204
+ return {
205
+ ...bufferStream,
206
+ getBuffer: function (): Buffer {
207
+ return Buffer.concat(chunks);
208
+ }
209
+ } as any;
210
+ }
190
211
 
191
- if (
192
- options.quality !== undefined &&
193
- (typeof options.quality !== "number" ||
194
- options.quality < 1 ||
195
- options.quality > 20)
196
- ) {
197
- throw new Error(
198
- "Quality must be a number between 1 and 20 or undefined.",
199
- );
212
+ function validateOptions(options: GIFOptions) {
213
+ if (options.outputFormat === "file" && !options.outputFile) {
214
+ throw new Error("Output file path is required when using file output format.");
200
215
  }
201
-
202
- if (options.canvasSize) {
203
- if (
204
- options.canvasSize.width !== undefined &&
205
- (!Number.isInteger(options.canvasSize.width) ||
206
- options.canvasSize.width <= 0)
207
- ) {
208
- throw new Error(
209
- "Canvas width must be a positive integer or undefined.",
210
- );
211
- }
212
-
213
- if (
214
- options.canvasSize.height !== undefined &&
215
- (!Number.isInteger(options.canvasSize.height) ||
216
- options.canvasSize.height <= 0)
217
- ) {
218
- throw new Error(
219
- "Canvas height must be a positive integer or undefined.",
220
- );
221
- }
216
+ if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
217
+ throw new Error("Repeat must be a non-negative number or undefined.");
222
218
  }
223
-
224
- if (
225
- options.delay !== undefined &&
226
- (!Number.isInteger(options.delay) || options.delay <= 0)
227
- ) {
228
- throw new Error("Delay must be a positive integer or undefined.");
219
+ if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
220
+ throw new Error("Quality must be a number between 1 and 20 or undefined.");
229
221
  }
230
-
231
- if (
232
- options.watermark !== undefined &&
233
- typeof options.watermark !== "boolean"
234
- ) {
235
- throw new Error("Watermark must be a boolean or undefined.");
222
+ if (options.watermark && typeof options.watermark.enable !== "boolean") {
223
+ throw new Error("Watermark must be a boolean or undefined.");
236
224
  }
237
-
238
- if (options.textOverlay !== undefined) {
239
- if (
240
- !options.textOverlay.text ||
241
- typeof options.textOverlay.text !== "string"
242
- ) {
243
- throw new Error(
244
- "Text overlay text is required and must be a string.",
245
- );
246
- }
247
-
248
- if (
249
- options.textOverlay.fontName !== undefined &&
250
- typeof options.textOverlay.fontName !== "string"
251
- ) {
252
- throw new Error(
253
- "Text overlay fontName must be a string or undefined.",
254
- );
255
- }
256
-
257
- if (
258
- options.textOverlay.fontPath !== undefined &&
259
- typeof options.textOverlay.fontPath !== "string"
260
- ) {
261
- throw new Error(
262
- "Text overlay fontPath must be a string or undefined.",
263
- );
264
- }
265
-
266
- if (
267
- options.textOverlay.fontSize !== undefined &&
268
- (!Number.isInteger(options.textOverlay.fontSize) ||
269
- options.textOverlay.fontSize <= 0)
270
- ) {
271
- throw new Error(
272
- "Text overlay fontSize must be a positive integer or undefined.",
273
- );
274
- }
275
-
276
- if (
277
- options.textOverlay.fontColor !== undefined &&
278
- typeof options.textOverlay.fontColor !== "string"
279
- ) {
280
- throw new Error(
281
- "Text overlay fontColor must be a string or undefined.",
282
- );
283
- }
284
- }
285
- }
286
- function validateImageObject(imageObject: any) {
287
- return (
288
- imageObject &&
289
- typeof imageObject === "object" &&
290
- "source" in imageObject &&
291
- "isRemote" in imageObject
292
- );
293
- }
294
-
295
- function validateImages(images: any) {
296
- if (!Array.isArray(images)) {
297
- throw new Error('The "images" parameter must be an array.');
298
- }
299
-
300
- if (images.length === 0) {
301
- throw new Error(
302
- 'The "images" array must contain at least one image object.',
303
- );
304
- }
305
-
306
- for (const imageObject of images) {
307
- if (!validateImageObject(imageObject)) {
308
- throw new Error(
309
- 'Each image object must have "source" and "isRemote" properties.',
310
- );
311
- }
225
+ if (options.textOverlay) {
226
+ const textOptions = options.textOverlay;
227
+ if (!textOptions.text || typeof textOptions.text !== "string") {
228
+ throw new Error("Text overlay text is required and must be a string.");
229
+ }
230
+ if (textOptions.fontSize !== undefined && (!Number.isInteger(textOptions.fontSize) || textOptions.fontSize <= 0)) {
231
+ throw new Error("Text overlay fontSize must be a positive integer or undefined.");
232
+ }
233
+ if (textOptions.fontColor !== undefined && typeof textOptions.fontColor !== "string") {
234
+ throw new Error("Text overlay fontColor must be a string or undefined.");
235
+ }
312
236
  }
313
- }
237
+ }
314
238
 
315
- try {
239
+ try {
316
240
  validateOptions(options);
317
- validateImages(images);
318
-
241
+
319
242
  const canvasWidth = options.width || 1200;
320
243
  const canvasHeight = options.height || 1200;
321
244
 
322
245
  const encoder = new GIFEncoder(canvasWidth, canvasHeight);
323
- const outputStream = options.outputFile
324
- ? createOutputStream(options.outputFile)
325
- : createBufferStream();
246
+ const outputStream = options.outputFile ? createOutputStream(options.outputFile) : createBufferStream();
326
247
 
327
- // @ts-ignore: Ignore type checking for this line
328
248
  encoder.createReadStream().pipe(outputStream);
329
249
 
330
250
  encoder.start();
331
251
  encoder.setRepeat(options.repeat || 0);
332
252
  encoder.setQuality(options.quality || 10);
333
- encoder.setDelay(options.delay || 3000);
334
-
253
+
335
254
  const canvas = createCanvas(canvasWidth, canvasHeight);
336
- const ctx = canvas.getContext("2d");
337
-
338
- for (const imageInfo of images) {
339
- const image = imageInfo.isRemote
340
- ? await loadImage(imageInfo.source)
341
- : await loadImage(imageInfo.source);
342
-
343
- const resizedImage = await resizeImage(
344
- image,
345
- canvasWidth,
346
- canvasHeight,
347
- );
348
-
349
- ctx.clearRect(0, 0, canvas.width, canvas.height);
350
- ctx.drawImage(resizedImage, 0, 0);
351
-
352
- if (options.watermark?.enable) {
353
- const watermark = await loadImage(options.watermark?.url);
354
- if (!watermark) throw new Error("Invalid watermark url");
355
- ctx.drawImage(watermark, 10, canvasHeight - watermark.height - 10);
356
- }
357
-
358
- if (options.textOverlay) {
359
- const textOptions = options.textOverlay;
360
- const fontPath = textOptions.fontPath;
361
- const fontName = textOptions.fontName || "Arial";
362
- const fontSize = textOptions.fontSize || 20;
363
- const fontColor = textOptions.fontColor || "white";
364
- const x = textOptions.x || 10;
365
- const y = textOptions.y || 30;
255
+ const ctx:any = canvas.getContext("2d");
366
256
 
367
- if (fontPath) {
368
- GlobalFonts.registerFromPath(
369
- path.join(options.basDir, fontPath),
370
- fontName,
371
- );
257
+ for (const frame of gifFrames) {
258
+ const image = await loadImage(frame.background);
259
+ const resizedImage = await resizeImage(image, canvasWidth, canvasHeight);
260
+
261
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
262
+ ctx.drawImage(resizedImage, 0, 0);
263
+
264
+ if (options.watermark?.enable) {
265
+ const watermark = await loadImage(options.watermark.url);
266
+ ctx.drawImage(watermark, 10, canvasHeight - watermark.height - 10);
372
267
  }
373
-
374
- ctx.font = `${fontSize}px ${fontName}`;
375
- ctx.fillStyle = fontColor;
376
- ctx.fillText(textOptions.text, x, y);
377
- }
378
-
379
- encoder.addFrame(ctx as any);
268
+
269
+ if (options.textOverlay) {
270
+ ctx.font = `${options.textOverlay.fontSize || 20}px Arial`;
271
+ ctx.fillStyle = options.textOverlay.fontColor || "white";
272
+ ctx.fillText(options.textOverlay.text, options.textOverlay.x || 10, options.textOverlay.y || 30);
273
+ }
274
+
275
+ encoder.setDelay(frame.duration);
276
+ encoder.addFrame(ctx);
380
277
  }
381
278
 
382
279
  encoder.finish();
383
280
  outputStream.end();
281
+
384
282
  if (options.outputFormat === "file") {
385
- if (!options.outputFile) {
386
- throw new Error("Please provide a valid file path");
387
- }
388
- await new Promise((resolve) => outputStream.on("finish", resolve));
283
+ await new Promise((resolve) => outputStream.on("finish", resolve));
389
284
  } else if (options.outputFormat === "base64") {
390
- outputStream.on("finish", () => {
391
- });
392
-
393
- if ('getBuffer' in outputStream) {
394
- return outputStream.getBuffer().toString("base64");
395
- } else {
396
- throw new Error("outputStream does not have getBuffer method");
397
- }
285
+ if ('getBuffer' in outputStream) {
286
+ return outputStream.getBuffer().toString("base64");
287
+ }
398
288
  } else if (options.outputFormat === "attachment") {
399
- outputStream.on("finish", () => {
400
- });
401
-
402
- const gifStream = encoder.createReadStream();
403
- return [{ attachment: gifStream, name: "gif.js" }];
289
+ const gifStream = encoder.createReadStream();
290
+ return [{ attachment: gifStream, name: "gif.js" }];
404
291
  } else if (options.outputFormat === "buffer") {
405
- outputStream.on("finish", () => {
406
- });
407
- if ('getBuffer' in outputStream) {
408
- return outputStream.getBuffer();
409
- } else {
410
- throw new Error("outputStream does not have getBuffer method");
411
- }
292
+ if ('getBuffer' in outputStream) {
293
+ return outputStream.getBuffer();
294
+ }
412
295
  } else {
413
- throw new Error(
414
- "Error: Please provide a valid format: 'buffer', 'base64', 'attachment', or 'output/file/path'.",
415
- );
296
+ throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
416
297
  }
417
- } catch (e: any) {
298
+ } catch (e: any) {
418
299
  console.error(e.message);
419
- }
300
+ throw e; // Re-throw the error after logging
420
301
  }
302
+ }
421
303
 
422
304
  async resize(resizeOptions: { imagePath: string | Buffer; size: { width: number; height: number } }) {
423
305
  return resizingImg(resizeOptions)
@@ -431,8 +313,8 @@ export class ApexPainter {
431
313
  return imgEffects(imagePath, filters)
432
314
  }
433
315
 
434
- async colorsFilter(imagePath: string, filterColor: string) {
435
- return applyColorFilters(imagePath, filterColor)
316
+ async colorsFilter(imagePath: string, filterColor: any, opacity?: number) {
317
+ return applyColorFilters(imagePath, filterColor, opacity)
436
318
  }
437
319
 
438
320
  async colorAnalysis(imagePath: string) {
@@ -447,6 +329,49 @@ export class ApexPainter {
447
329
  return bgRemoval(imageURL, apiKey)
448
330
  }
449
331
 
332
+ async blend(
333
+ layers: {
334
+ image: string | Buffer;
335
+ blendMode: 'source-over' | 'source-in' | 'source-out' | 'source-atop' |
336
+ 'destination-over' | 'destination-in' | 'destination-out' |
337
+ 'destination-atop' | 'lighter' | 'copy' | 'xor' |
338
+ 'multiply' | 'screen' | 'overlay' | 'darken' |
339
+ 'lighten' | 'color-dodge' | 'color-burn' |
340
+ 'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
341
+ 'hue' | 'saturation' | 'color' | 'luminosity';
342
+ position?: { x: number; y: number };
343
+ opacity?: number;
344
+ }[],
345
+ baseImageBuffer: Buffer
346
+ ): Promise<Buffer> {
347
+ try {
348
+ // Load the base image
349
+ const baseImage = await loadImage(baseImageBuffer);
350
+ const canvas = createCanvas(baseImage.width, baseImage.height);
351
+ const ctx = canvas.getContext('2d');
352
+
353
+ // Draw the base image
354
+ ctx.drawImage(baseImage, 0, 0);
355
+
356
+ // Process each layer
357
+ for (const layer of layers) {
358
+ const layerImage = await loadImage(layer.image);
359
+ ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1.0;
360
+
361
+ ctx.globalCompositeOperation = layer.blendMode;
362
+ ctx.drawImage(layerImage, layer.position?.x || 0, layer.position?.y || 0);
363
+ }
364
+
365
+ ctx.globalAlpha = 1.0;
366
+ ctx.globalCompositeOperation = 'source-over';
367
+
368
+ return canvas.toBuffer('image/png');
369
+ } catch (error) {
370
+ console.error('Error creating layered composition:', error);
371
+ throw new Error('Failed to create layered composition');
372
+ }
373
+ }
374
+
450
375
  async createChart(data: any, type: { chartType: string, chartNumber: number}) {
451
376
 
452
377
  if (!data || Object.keys(data).length === 0) {
@@ -558,8 +483,486 @@ export class ApexPainter {
558
483
  ctx.globalAlpha = 1.0;
559
484
  }
560
485
  }
486
+ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions): Promise<ImageObject[]> {
487
+ const frames: ImageObject[] = [];
488
+ const frameDir = path.join(__dirname, 'frames');
489
+
490
+ if (!fs.existsSync(frameDir)) {
491
+ fs.mkdirSync(frameDir);
492
+ }
493
+
494
+ const videoPath = typeof videoSource === 'string' ? videoSource : path.join(frameDir, 'temp-video.mp4');
495
+
496
+ if (Buffer.isBuffer(videoSource)) {
497
+ fs.writeFileSync(videoPath, videoSource);
498
+ } else if (videoSource.startsWith('http')) {
499
+ await axios({
500
+ method: 'get',
501
+ url: videoSource,
502
+ responseType: 'arraybuffer'
503
+ })
504
+ .then((response) => {
505
+ fs.writeFileSync(videoPath, response.data);
506
+ })
507
+ .catch(err => {
508
+ throw new Error(`Error downloading video: ${err.message}`);
509
+ });
510
+ } else if (!fs.existsSync(videoPath)) {
511
+ throw new Error("Video file not found at specified path.");
512
+ }
513
+
514
+ function processVideoExtraction(videoPath: string, frames: ImageObject[], options: ExtractFramesOptions, resolve: any, reject: any) {
515
+ const outputFormat = options.outputFormat || 'jpg';
516
+ const outputFileTemplate = `frame-%03d.${outputFormat}`;
517
+
518
+ ffmpeg(videoPath)
519
+ .on('end', () => {
520
+ console.log('Frames extracted successfully.');
521
+ resolve(frames);
522
+ })
523
+ .on('error', (err) => {
524
+ console.error('Error extracting frames:', err.message);
525
+ reject(err);
526
+ })
527
+ .outputOptions([`-vf fps=1/${options.interval / 1000}`, `-q:v 2`]) // Set frame rate
528
+ .saveToFile(path.join(frameDir, outputFileTemplate)); // Save frames with a sequence number
529
+
530
+ ffmpeg.ffprobe(videoPath, (err, metadata) => {
531
+ if (err) {
532
+ return reject(err);
533
+ }
534
+ const duration = metadata?.format?.duration;
535
+ if (duration === undefined) {
536
+ return reject(new Error("Video duration not found in metadata."));
537
+ }
538
+ const totalFrames = Math.floor(duration * 1000 / options.interval);
539
+ for (let i = 0; i < totalFrames; i++) {
540
+ if (options.frameSelection && (i < (options.frameSelection.start || 0) || i > (options.frameSelection.end || totalFrames - 1))) {
541
+ continue;
542
+ }
543
+ frames.push({
544
+ source: path.join(frameDir, `frame-${String(i).padStart(3, '0')}.${outputFormat}`),
545
+ isRemote: false
546
+ });
547
+ }
548
+ });
549
+ }
550
+
551
+ return new Promise((resolve, reject) => {
552
+ processVideoExtraction(videoPath, frames, options, resolve, reject);
553
+ });
554
+ }
555
+
556
+
557
+
558
+ /**
559
+ * Sets a pattern on a specific area of a buffered image.
560
+ * @param {Buffer} buffer - The source image buffer.
561
+ * @param {Object} options - Options to customize the pattern.
562
+ * @returns {Promise<Buffer>} - The adjusted image buffer.
563
+ */
564
+ async setPatternBackground(buffer:Buffer, options: PatternOptions) {
565
+ // Load the buffered image onto a canvas
566
+ const img = await loadImage(buffer);
567
+ const canvas = createCanvas(img.width, img.height);
568
+ const ctx: any = canvas.getContext('2d');
569
+ ctx.drawImage(img, 0, 0);
570
+
571
+ // Set up the area to cover (default to full image if not specified)
572
+ const x = options.x || 0;
573
+ const y = options.y || 0;
574
+ const width = options.width || img.width;
575
+ const height = options.height || img.height;
576
+
577
+ ctx.save();
578
+ ctx.beginPath();
579
+ ctx.rect(x, y, width, height);
580
+ ctx.clip();
581
+
582
+ if (options.gradient) {
583
+ await this.fillWithGradient(ctx, width, height, options.gradient, x, y);
584
+ } else if (options.backgroundColor) {
585
+ ctx.fillStyle = options.backgroundColor;
586
+ ctx.fillRect(x, y, width, height);
587
+ }
588
+
589
+ switch (options.type) {
590
+ case 'dots':
591
+ await this.drawDotsPattern(ctx, width, height, options, x, y);
592
+ break;
593
+ case 'stripes':
594
+ await this.drawStripesPattern(ctx, width, height, options, x, y);
595
+ break;
596
+ case 'grid':
597
+ await this.drawGridPattern(ctx, width, height, options, x, y);
598
+ break;
599
+ case 'checkerboard':
600
+ await this.drawCheckerboardPattern(ctx, width, height, options, x, y);
601
+ break;
602
+ case 'custom':
603
+ await this.drawCustomPattern(ctx, width, height, options, x, y);
604
+ break;
605
+ default:
606
+ throw new Error('Invalid pattern type specified.');
607
+ }
608
+
609
+ ctx.restore();
610
+
611
+ return canvas.toBuffer('image/png');
612
+ }
613
+
614
+
615
+ /**
616
+ * Fills the specified area with a gradient.
617
+ * @param ctx The rendering context of the canvas.
618
+ * @param width The width of the area to fill.
619
+ * @param height The height of the area to fill.
620
+ * @param gradient The gradient options.
621
+ * @param x The x offset of the area.
622
+ * @param y The y offset of the area.
623
+ */
624
+ async fillWithGradient(
625
+ ctx: any,
626
+ width: number,
627
+ height: number,
628
+ gradient: GradientConfig,
629
+ x: number = 0,
630
+ y: number = 0
631
+ ): Promise<void> {
632
+ let grad: CanvasGradient;
633
+
634
+ if (gradient.type === 'linear') {
635
+ grad = ctx.createLinearGradient(
636
+ (gradient.startX || 0) + x,
637
+ (gradient.startY || 0) + y,
638
+ (gradient.endX || width) + x,
639
+ (gradient.endY || height) + y
640
+ );
641
+ } else {
642
+ grad = ctx.createRadialGradient(
643
+ (gradient.startX || width / 2) + x,
644
+ (gradient.startY || height / 2) + y,
645
+ gradient.startRadius || 0,
646
+ (gradient.endX || width / 2) + x,
647
+ (gradient.endY || height / 2) + y,
648
+ gradient.endRadius || Math.max(width, height)
649
+ );
650
+ }
651
+
652
+ gradient.colors.forEach(stop => {
653
+ grad.addColorStop(stop.stop, stop.color);
654
+ });
655
+
656
+ ctx.fillStyle = grad;
657
+ ctx.fillRect(x, y, width, height);
658
+ }
659
+
660
+ /**
661
+ * Draws a dots pattern in the specified area.
662
+ * @param ctx The rendering context of the canvas.
663
+ * @param width The width of the area.
664
+ * @param height The height of the area.
665
+ * @param options Options to customize the dot pattern.
666
+ * @param x The x offset of the area.
667
+ * @param y The y offset of the area.
668
+ */
669
+ async drawDotsPattern(
670
+ ctx: any,
671
+ width: number,
672
+ height: number,
673
+ options: PatternOptions,
674
+ x: number = 0,
675
+ y: number = 0
676
+ ): Promise<void> {
677
+ const color = options.color || 'black';
678
+ const radius = options.size || 5;
679
+ const spacing = options.spacing || 10;
680
+
681
+ ctx.fillStyle = color;
682
+
683
+ for (let posY = y; posY < y + height; posY += spacing) {
684
+ for (let posX = x; posX < x + width; posX += spacing) {
685
+ ctx.beginPath();
686
+ ctx.arc(posX, posY, radius, 0, Math.PI * 2);
687
+ ctx.fill();
688
+ }
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Draws a stripes pattern in the specified area.
694
+ * @param ctx The rendering context of the canvas.
695
+ * @param width The width of the area.
696
+ * @param height The height of the area.
697
+ * @param options Options to customize the stripes pattern.
698
+ * @param x The x offset of the area.
699
+ * @param y The y offset of the area.
700
+ */
701
+ async drawStripesPattern(
702
+ ctx: any,
703
+ width: number,
704
+ height: number,
705
+ options: PatternOptions,
706
+ x: number = 0,
707
+ y: number = 0
708
+ ): Promise<void> {
709
+ const color = options.color || 'black';
710
+ const stripeWidth = options.size || 10;
711
+ const spacing = options.spacing || 20;
712
+ const angle = options.angle || 0;
713
+
714
+ ctx.fillStyle = color;
715
+
716
+ // Save the context to apply transformations and restore later
717
+ ctx.save();
718
+
719
+ // Translate and rotate the canvas
720
+ ctx.translate(x + width / 2, y + height / 2); // Move to the center of the area
721
+ ctx.rotate((angle * Math.PI) / 180); // Rotate by the specified angle
722
+ ctx.translate(-width / 2, -height / 2); // Translate back to top-left
723
+
724
+ // Draw the stripes pattern with the current transformations
725
+ for (let posX = 0; posX < width; posX += stripeWidth + spacing) {
726
+ ctx.fillRect(posX, 0, stripeWidth, height);
727
+ }
728
+
729
+ // Restore the original context to prevent transformations affecting other drawings
730
+ ctx.restore();
731
+ }
732
+ /**
733
+ * Draws a grid pattern in the specified area.
734
+ * @param ctx The rendering context of the canvas.
735
+ * @param width The width of the area.
736
+ * @param height The height of the area.
737
+ * @param options Options to customize the grid pattern.
738
+ * @param x The x offset of the area.
739
+ * @param y The y offset of the area.
740
+ */
741
+ async drawGridPattern(
742
+ ctx: any,
743
+ width: number,
744
+ height: number,
745
+ options: PatternOptions,
746
+ x: number = 0,
747
+ y: number = 0
748
+ ): Promise<void> {
749
+ const color = options.color || 'black';
750
+ const size = options.size || 20;
751
+
752
+ ctx.strokeStyle = color;
753
+ ctx.lineWidth = 1;
754
+
755
+ for (let posX = x; posX <= x + width; posX += size) {
756
+ ctx.beginPath();
757
+ ctx.moveTo(posX, y);
758
+ ctx.lineTo(posX, y + height);
759
+ ctx.stroke();
760
+ }
561
761
 
562
- public validHex(hexColor: string): any {
762
+ for (let posY = y; posY <= y + height; posY += size) {
763
+ ctx.beginPath();
764
+ ctx.moveTo(x, posY);
765
+ ctx.lineTo(x + width, posY);
766
+ ctx.stroke();
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Draws a checkerboard pattern in the specified area.
772
+ * @param ctx The rendering context of the canvas.
773
+ * @param width The width of the area.
774
+ * @param height The height of the area.
775
+ * @param options Options to customize the checkerboard pattern.
776
+ * @param x The x offset of the area.
777
+ * @param y The y offset of the area.
778
+ */
779
+ async drawCheckerboardPattern(
780
+ ctx: any,
781
+ width: number,
782
+ height: number,
783
+ options: PatternOptions,
784
+ x: number = 0,
785
+ y: number = 0
786
+ ): Promise<void> {
787
+ const color1 = options.color || 'black';
788
+ const color2 = options.secondaryColor || 'white';
789
+ const size = options.size || 20;
790
+
791
+ for (let posY = y; posY < y + height; posY += size) {
792
+ for (let posX = x; posX < x + width; posX += size) {
793
+ ctx.fillStyle = (Math.floor(posX / size) + Math.floor(posY / size)) % 2 === 0 ? color1 : color2;
794
+ ctx.fillRect(posX, posY, size, size);
795
+ }
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Draws a custom image pattern in the specified area.
801
+ * @param ctx The rendering context of the canvas.
802
+ * @param width The width of the area.
803
+ * @param height The height of the area.
804
+ * @param options Options to customize the custom pattern.
805
+ * @param x The x offset of the area.
806
+ * @param y The y offset of the area.
807
+ */
808
+ async drawCustomPattern(
809
+ ctx: any,
810
+ width: number,
811
+ height: number,
812
+ options: PatternOptions,
813
+ x: number = 0,
814
+ y: number = 0
815
+ ): Promise<void> {
816
+ if (!options.customPatternImage) {
817
+ throw new Error('Custom pattern image path is required for custom patterns.');
818
+ }
819
+
820
+ const patternImage = await loadImage(options.customPatternImage);
821
+ const pattern = ctx.createPattern(patternImage, 'repeat');
822
+ ctx.fillStyle = pattern;
823
+ ctx.translate(x, y);
824
+ ctx.fillRect(0, 0, width, height);
825
+ ctx.resetTransform();
826
+ }
827
+
828
+
829
+ async animate(
830
+ frames: Frame[],
831
+ defaultDuration: number,
832
+ defaultWidth: number = 800,
833
+ defaultHeight: number = 600,
834
+ options?: {
835
+ gif?: boolean;
836
+ gifPath?: string;
837
+ onStart?: () => void;
838
+ onFrame?: (index: number) => void;
839
+ onEnd?: () => void;
840
+ }
841
+ ): Promise<Buffer[] | undefined> {
842
+ const buffers: Buffer[] = [];
843
+ const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
844
+
845
+ if (options?.onStart) options.onStart();
846
+
847
+ let encoder: GIFEncoder | null = null;
848
+ let gifStream: fs.WriteStream | null = null;
849
+
850
+ if (options?.gif) {
851
+ if (!options.gifPath) {
852
+ throw new Error("GIF generation enabled but no gifPath provided.");
853
+ }
854
+ encoder = new GIFEncoder(defaultWidth, defaultHeight);
855
+ gifStream = fs.createWriteStream(options.gifPath);
856
+ encoder.createReadStream().pipe(gifStream);
857
+ encoder.start();
858
+ encoder.setRepeat(0);
859
+ encoder.setQuality(10);
860
+ }
861
+
862
+ for (let i = 0; i < frames.length; i++) {
863
+ const frame = frames[i];
864
+
865
+ const width = frame.width || defaultWidth;
866
+ const height = frame.height || defaultHeight;
867
+ const canvas = createCanvas(width, height);
868
+ const ctx: any = canvas.getContext('2d');
869
+
870
+ if (!isNode) {
871
+ canvas.width = width;
872
+ canvas.height = height;
873
+ document.body.appendChild(canvas as unknown as Node);
874
+ }
875
+
876
+ ctx.clearRect(0, 0, width, height);
877
+
878
+ if (frame.transformations) {
879
+ const { scaleX = 1, scaleY = 1, rotate = 0, translateX = 0, translateY = 0 } = frame.transformations;
880
+ ctx.save();
881
+ ctx.translate(translateX, translateY);
882
+ ctx.rotate((rotate * Math.PI) / 180);
883
+ ctx.scale(scaleX, scaleY);
884
+ }
885
+
886
+ let fillStyle: string | CanvasGradient | CanvasPattern | null = null;
887
+
888
+ if (frame.gradient) {
889
+ const { type, startX, startY, endX, endY, startRadius, endRadius, colors } = frame.gradient;
890
+ let gradient: CanvasGradient | null = null;
891
+
892
+ if (type === 'linear') {
893
+ gradient = ctx.createLinearGradient(startX || 0, startY || 0, endX || width, endY || height);
894
+ } else if (type === 'radial') {
895
+ gradient = ctx.createRadialGradient(
896
+ startX || width / 2,
897
+ startY || height / 2,
898
+ startRadius || 0,
899
+ endX || width / 2,
900
+ endY || height / 2,
901
+ endRadius || Math.max(width, height)
902
+ );
903
+ }
904
+
905
+ colors.forEach(colorStop => {
906
+ if (gradient) gradient.addColorStop(colorStop.stop, colorStop.color);
907
+ });
908
+
909
+ fillStyle = gradient;
910
+ }
911
+
912
+ if (frame.pattern) {
913
+ const patternImage = await loadImage(frame.pattern.imagePath);
914
+ const pattern = ctx.createPattern(patternImage, frame.pattern.repeat || 'repeat');
915
+ fillStyle = pattern;
916
+ }
917
+
918
+ if (!fillStyle && frame.backgroundColor) {
919
+ fillStyle = frame.backgroundColor;
920
+ }
921
+
922
+ if (fillStyle) {
923
+ ctx.fillStyle = fillStyle;
924
+ ctx.fillRect(0, 0, width, height);
925
+ }
926
+
927
+ if (frame.imagePath) {
928
+ const image = await loadImage(frame.imagePath);
929
+ ctx.globalCompositeOperation = frame.blendMode || 'source-over';
930
+ ctx.drawImage(image, 0, 0, width, height);
931
+ }
932
+
933
+ if (frame.onDrawCustom) {
934
+ frame.onDrawCustom(ctx, canvas);
935
+ }
936
+
937
+ if (frame.transformations) {
938
+ ctx.restore();
939
+ }
940
+
941
+ const buffer = canvas.toBuffer('image/png');
942
+ buffers.push(buffer);
943
+
944
+ if (encoder) {
945
+
946
+ const frameDuration = frame.duration || defaultDuration;
947
+ encoder.setDelay(frameDuration);
948
+ encoder.addFrame(ctx);
949
+ }
950
+
951
+ if (options?.onFrame) options.onFrame(i);
952
+
953
+ await new Promise(resolve => setTimeout(resolve, frame.duration || defaultDuration));
954
+ }
955
+
956
+ if (encoder) {
957
+ encoder.finish();
958
+ }
959
+
960
+ if (options?.onEnd) options.onEnd();
961
+
962
+ return options?.gif ? undefined : buffers;
963
+ }
964
+
965
+ public validHex(hexColor: string): any {
563
966
  const hexPattern = /^#[0-9a-fA-F]{6}$/;
564
967
  if (!hexPattern.test(hexColor)) {
565
968
  throw new Error("Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");