apexify.js 4.5.30 → 4.5.40
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.
- package/dist/ai/ApexAI.d.ts.map +1 -1
- package/dist/ai/ApexAI.js +5 -12
- package/dist/ai/ApexAI.js.map +1 -1
- package/dist/ai/ApexModules.d.ts.map +1 -1
- package/dist/ai/ApexModules.js +26 -29
- package/dist/ai/ApexModules.js.map +1 -1
- package/dist/ai/functions/draw.d.ts.map +1 -1
- package/dist/ai/functions/draw.js +2 -10
- package/dist/ai/functions/draw.js.map +1 -1
- package/dist/ai/functions/validOptions.d.ts +6 -12
- package/dist/ai/functions/validOptions.d.ts.map +1 -1
- package/dist/ai/functions/validOptions.js +54 -32
- package/dist/ai/functions/validOptions.js.map +1 -1
- package/dist/ai/modals-chat/electronHub/chatmodels.d.ts +2 -2
- package/dist/ai/modals-chat/electronHub/chatmodels.d.ts.map +1 -1
- package/dist/ai/modals-chat/electronHub/chatmodels.js +26 -29
- package/dist/ai/modals-chat/electronHub/chatmodels.js.map +1 -1
- package/dist/ai/modals-chat/electronHub/imageModels.d.ts +1 -1
- package/dist/ai/modals-chat/electronHub/imageModels.d.ts.map +1 -1
- package/dist/ai/modals-chat/electronHub/imageModels.js +29 -24
- package/dist/ai/modals-chat/electronHub/imageModels.js.map +1 -1
- package/dist/ai/modals-chat/groq/chatgroq.d.ts.map +1 -1
- package/dist/ai/modals-chat/groq/chatgroq.js +10 -4
- package/dist/ai/modals-chat/groq/chatgroq.js.map +1 -1
- package/dist/ai/modals-chat/groq/imageAnalyzer.d.ts.map +1 -1
- package/dist/ai/modals-chat/groq/imageAnalyzer.js +0 -1
- package/dist/ai/modals-chat/groq/imageAnalyzer.js.map +1 -1
- package/dist/ai/modals-chat/groq/whisper.d.ts.map +1 -1
- package/dist/ai/modals-chat/groq/whisper.js +0 -3
- package/dist/ai/modals-chat/groq/whisper.js.map +1 -1
- package/dist/canvas/ApexPainter.d.ts +94 -7
- package/dist/canvas/ApexPainter.d.ts.map +1 -1
- package/dist/canvas/ApexPainter.js +443 -119
- package/dist/canvas/ApexPainter.js.map +1 -1
- package/dist/canvas/utils/bg.d.ts +3 -3
- package/dist/canvas/utils/bg.d.ts.map +1 -1
- package/dist/canvas/utils/bg.js +35 -15
- package/dist/canvas/utils/bg.js.map +1 -1
- package/dist/canvas/utils/customLines.d.ts +2 -1
- package/dist/canvas/utils/customLines.d.ts.map +1 -1
- package/dist/canvas/utils/customLines.js +67 -31
- package/dist/canvas/utils/customLines.js.map +1 -1
- package/dist/canvas/utils/general functions.d.ts +2 -2
- package/dist/canvas/utils/general functions.d.ts.map +1 -1
- package/dist/canvas/utils/general functions.js +52 -15
- package/dist/canvas/utils/general functions.js.map +1 -1
- package/dist/canvas/utils/types.d.ts +68 -0
- package/dist/canvas/utils/types.d.ts.map +1 -1
- package/dist/canvas/utils/types.js +2 -4
- package/dist/canvas/utils/types.js.map +1 -1
- package/dist/canvas/utils/utils.d.ts +2 -2
- package/dist/canvas/utils/utils.d.ts.map +1 -1
- package/dist/index.d.ts +4 -7
- package/dist/index.d.ts.map +1 -1
- package/examples/barchart.txt +71 -0
- package/examples/linechart.txt +93 -0
- package/examples/piechart.txt +67 -0
- package/lib/ai/ApexAI.ts +8 -11
- package/lib/ai/ApexModules.ts +42 -33
- package/lib/ai/functions/draw.ts +2 -8
- package/lib/ai/functions/validOptions.ts +83 -58
- package/lib/ai/modals-chat/electronHub/chatmodels.ts +40 -43
- package/lib/ai/modals-chat/electronHub/imageModels.ts +34 -32
- package/lib/ai/modals-chat/groq/chatgroq.ts +17 -7
- package/lib/ai/modals-chat/groq/imageAnalyzer.ts +0 -2
- package/lib/ai/modals-chat/groq/whisper.ts +0 -3
- package/lib/canvas/ApexPainter.ts +665 -262
- package/lib/canvas/utils/bg.ts +42 -16
- package/lib/canvas/utils/customLines.ts +88 -43
- package/lib/canvas/utils/general functions.ts +98 -41
- package/lib/canvas/utils/types.ts +77 -1
- package/lib/canvas/utils/utils.ts +6 -2
- package/package.json +24 -9
- package/lib/ai/modals-chat/freesedgpt/chat.ts +0 -31
- package/lib/ai/modals-chat/freesedgpt/fresedImagine.ts +0 -24
- package/lib/ai/modals-chat/rsn/rsnChat.ts +0 -74
|
@@ -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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
+
|
|
149
|
+
async createCustom(options: CustomOptions[], buffer: CanvasResults | Buffer, ): Promise<Buffer> {
|
|
130
150
|
try {
|
|
131
|
-
const existingImage = await loadImage(buffer.buffer);
|
|
132
151
|
|
|
133
|
-
|
|
152
|
+
if (!Array.isArray(options)) {
|
|
153
|
+
options = [options];
|
|
154
|
+
}
|
|
134
155
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
166
|
+
if (!existingImage) {
|
|
167
|
+
throw new Error('Unable to load image from buffer');
|
|
168
|
+
}
|
|
139
169
|
|
|
140
|
-
|
|
170
|
+
const canvas = createCanvas(existingImage.width, existingImage.height);
|
|
171
|
+
const ctx = canvas.getContext("2d");
|
|
141
172
|
|
|
142
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
192
|
+
function createOutputStream(outputFile: string): fs.WriteStream {
|
|
158
193
|
return fs.createWriteStream(outputFile);
|
|
159
194
|
}
|
|
160
195
|
|
|
161
|
-
|
|
196
|
+
function createBufferStream() {
|
|
162
197
|
const bufferStream = new PassThrough();
|
|
163
198
|
const chunks: Buffer[] = [];
|
|
164
|
-
|
|
199
|
+
|
|
165
200
|
bufferStream.on('data', (chunk: Buffer) => {
|
|
166
|
-
|
|
201
|
+
chunks.push(chunk);
|
|
167
202
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
239
|
+
try {
|
|
316
240
|
validateOptions(options);
|
|
317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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'.");
|