@cognima/banners 0.0.1-beta

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 (48) hide show
  1. package/assets/fonts/Manrope/Manrope-Bold.ttf +0 -0
  2. package/assets/fonts/Manrope/Manrope-Regular.ttf +0 -0
  3. package/assets/fonts/Others/AbyssinicaSIL-Regular.ttf +0 -0
  4. package/assets/fonts/Others/ChirpRegular.ttf +0 -0
  5. package/assets/fonts/Poppins/Poppins-Bold.ttf +0 -0
  6. package/assets/fonts/Poppins/Poppins-Medium.ttf +0 -0
  7. package/assets/fonts/Poppins/Poppins-Regular.ttf +0 -0
  8. package/assets/placeholders/album_art.png +0 -0
  9. package/assets/placeholders/avatar.png +0 -0
  10. package/assets/placeholders/badge.jpg +0 -0
  11. package/assets/placeholders/badge.png +0 -0
  12. package/assets/placeholders/badge_2.jpg +0 -0
  13. package/assets/placeholders/badge_3.jpg +0 -0
  14. package/assets/placeholders/badge_4.jpg +0 -0
  15. package/assets/placeholders/badge_5.jpg +0 -0
  16. package/assets/placeholders/banner.jpeg +0 -0
  17. package/assets/placeholders/images.jpeg +0 -0
  18. package/index.js +153 -0
  19. package/package.json +34 -0
  20. package/src/animation-effects.js +631 -0
  21. package/src/cache-manager.js +258 -0
  22. package/src/community-banner.js +1536 -0
  23. package/src/constants.js +208 -0
  24. package/src/discord-profile.js +584 -0
  25. package/src/e-commerce-banner.js +1214 -0
  26. package/src/effects.js +355 -0
  27. package/src/error-handler.js +305 -0
  28. package/src/event-banner.js +1319 -0
  29. package/src/facebook-post.js +679 -0
  30. package/src/gradient-welcome.js +430 -0
  31. package/src/image-filters.js +1034 -0
  32. package/src/image-processor.js +1014 -0
  33. package/src/instagram-post.js +504 -0
  34. package/src/interactive-elements.js +1208 -0
  35. package/src/linkedin-post.js +658 -0
  36. package/src/marketing-banner.js +1089 -0
  37. package/src/minimalist-banner.js +892 -0
  38. package/src/modern-profile.js +755 -0
  39. package/src/performance-optimizer.js +216 -0
  40. package/src/telegram-header.js +544 -0
  41. package/src/test-runner.js +645 -0
  42. package/src/tiktok-post.js +713 -0
  43. package/src/twitter-header.js +604 -0
  44. package/src/validator.js +442 -0
  45. package/src/welcome-leave.js +445 -0
  46. package/src/whatsapp-status.js +386 -0
  47. package/src/youtube-thumbnail.js +681 -0
  48. package/utils.js +710 -0
@@ -0,0 +1,1014 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Módulo de Processamento de Imagem
5
+ *
6
+ * Este módulo fornece funções para processamento avançado de imagens,
7
+ * incluindo redimensionamento, recorte, composição e outros ajustes.
8
+ *
9
+ * @author Cognima Team (melhorado)
10
+ * @version 2.0.0
11
+ */
12
+
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+
15
+ const pureimage = require("pureimage");
16
+ const path = require("path");
17
+ const fs = require("fs");
18
+ const { loadImageWithAxios, encodeToBuffer } = require("../utils");
19
+ const filters = require("./image-filters");
20
+
21
+ /**
22
+ * Classe para processamento avançado de imagens
23
+ */
24
+ class ImageProcessor {
25
+ /**
26
+ * Cria uma nova instância do processador de imagens
27
+ * @param {Object} options - Opções de configuração
28
+ */
29
+ constructor(options = {}) {
30
+ this.canvas = null;
31
+ this.ctx = null;
32
+ this.width = options.width || 800;
33
+ this.height = options.height || 600;
34
+ this.backgroundColor = options.backgroundColor || "#FFFFFF";
35
+ this.quality = options.quality || 0.9;
36
+
37
+ // Inicializa o canvas
38
+ this._initCanvas();
39
+ }
40
+
41
+ /**
42
+ * Inicializa o canvas com as dimensões especificadas
43
+ * @private
44
+ */
45
+ _initCanvas() {
46
+ this.canvas = pureimage.make(this.width, this.height);
47
+ this.ctx = this.canvas.getContext("2d");
48
+
49
+ // Preenche o fundo com a cor especificada
50
+ this.ctx.fillStyle = this.backgroundColor;
51
+ this.ctx.fillRect(0, 0, this.width, this.height);
52
+ }
53
+
54
+ /**
55
+ * Redimensiona o canvas
56
+ * @param {number} width - Nova largura
57
+ * @param {number} height - Nova altura
58
+ * @param {boolean} preserveContent - Se deve preservar o conteúdo atual
59
+ * @returns {ImageProcessor} - Instância atual para encadeamento
60
+ */
61
+ resize(width, height, preserveContent = true) {
62
+ if (width === this.width && height === this.height) {
63
+ return this; // Nenhuma alteração necessária
64
+ }
65
+
66
+ if (preserveContent) {
67
+ // Cria um novo canvas com as novas dimensões
68
+ const newCanvas = pureimage.make(width, height);
69
+ const newCtx = newCanvas.getContext("2d");
70
+
71
+ // Preenche o fundo com a cor especificada
72
+ newCtx.fillStyle = this.backgroundColor;
73
+ newCtx.fillRect(0, 0, width, height);
74
+
75
+ // Copia o conteúdo do canvas atual para o novo canvas
76
+ newCtx.drawImage(this.canvas, 0, 0, this.width, this.height, 0, 0, width, height);
77
+
78
+ // Atualiza as referências
79
+ this.canvas = newCanvas;
80
+ this.ctx = newCtx;
81
+ } else {
82
+ // Simplesmente cria um novo canvas vazio
83
+ this.width = width;
84
+ this.height = height;
85
+ this._initCanvas();
86
+ }
87
+
88
+ this.width = width;
89
+ this.height = height;
90
+
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Carrega uma imagem no canvas
96
+ * @param {string|Buffer|Object} source - URL, Buffer ou caminho da imagem
97
+ * @param {Object} options - Opções de carregamento
98
+ * @returns {Promise<ImageProcessor>} - Instância atual para encadeamento
99
+ */
100
+ async loadImage(source, options = {}) {
101
+ const {
102
+ x = 0,
103
+ y = 0,
104
+ width = null,
105
+ height = null,
106
+ fit = "contain", // contain, cover, fill, none
107
+ alignX = "center", // left, center, right
108
+ alignY = "center", // top, center, bottom
109
+ offsetX = 0,
110
+ offsetY = 0
111
+ } = options;
112
+
113
+ try {
114
+ const img = await loadImageWithAxios(source);
115
+
116
+ // Calcula as dimensões de desenho
117
+ const sourceWidth = img.width;
118
+ const sourceHeight = img.height;
119
+ const sourceAspect = sourceWidth / sourceHeight;
120
+
121
+ let drawWidth = width || this.width;
122
+ let drawHeight = height || this.height;
123
+ const targetAspect = drawWidth / drawHeight;
124
+
125
+ // Ajusta as dimensões com base no modo de ajuste
126
+ if (fit === "contain") {
127
+ if (sourceAspect > targetAspect) {
128
+ drawHeight = drawWidth / sourceAspect;
129
+ } else {
130
+ drawWidth = drawHeight * sourceAspect;
131
+ }
132
+ } else if (fit === "cover") {
133
+ if (sourceAspect > targetAspect) {
134
+ drawWidth = drawHeight * sourceAspect;
135
+ } else {
136
+ drawHeight = drawWidth / sourceAspect;
137
+ }
138
+ } else if (fit === "none") {
139
+ drawWidth = sourceWidth;
140
+ drawHeight = sourceHeight;
141
+ }
142
+ // Para "fill", mantém as dimensões especificadas
143
+
144
+ // Calcula a posição com base no alinhamento
145
+ let drawX = x;
146
+ let drawY = y;
147
+
148
+ if (width !== null && fit !== "fill") {
149
+ if (alignX === "center") {
150
+ drawX += (width - drawWidth) / 2;
151
+ } else if (alignX === "right") {
152
+ drawX += width - drawWidth;
153
+ }
154
+ }
155
+
156
+ if (height !== null && fit !== "fill") {
157
+ if (alignY === "center") {
158
+ drawY += (height - drawHeight) / 2;
159
+ } else if (alignY === "bottom") {
160
+ drawY += height - drawHeight;
161
+ }
162
+ }
163
+
164
+ // Aplica os deslocamentos
165
+ drawX += offsetX;
166
+ drawY += offsetY;
167
+
168
+ // Desenha a imagem no canvas
169
+ this.ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
170
+
171
+ return this;
172
+ } catch (err) {
173
+ console.error("Falha ao carregar imagem:", err.message);
174
+ throw new Error("Não foi possível carregar a imagem.");
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Recorta o canvas para as dimensões especificadas
180
+ * @param {number} x - Posição X do recorte
181
+ * @param {number} y - Posição Y do recorte
182
+ * @param {number} width - Largura do recorte
183
+ * @param {number} height - Altura do recorte
184
+ * @returns {ImageProcessor} - Instância atual para encadeamento
185
+ */
186
+ crop(x, y, width, height) {
187
+ // Garante que as coordenadas estão dentro dos limites do canvas
188
+ x = Math.max(0, Math.min(x, this.width));
189
+ y = Math.max(0, Math.min(y, this.height));
190
+ width = Math.min(width, this.width - x);
191
+ height = Math.min(height, this.height - y);
192
+
193
+ // Obtém os dados da imagem na área de recorte
194
+ const imageData = this.ctx.getImageData(x, y, width, height);
195
+
196
+ // Cria um novo canvas com as dimensões do recorte
197
+ const newCanvas = pureimage.make(width, height);
198
+ const newCtx = newCanvas.getContext("2d");
199
+
200
+ // Copia os dados da imagem para o novo canvas
201
+ newCtx.putImageData(imageData, 0, 0);
202
+
203
+ // Atualiza as referências
204
+ this.canvas = newCanvas;
205
+ this.ctx = newCtx;
206
+ this.width = width;
207
+ this.height = height;
208
+
209
+ return this;
210
+ }
211
+
212
+ /**
213
+ * Rotaciona o canvas
214
+ * @param {number} angle - Ângulo de rotação em graus
215
+ * @param {boolean} resize - Se deve redimensionar o canvas para acomodar a imagem rotacionada
216
+ * @returns {ImageProcessor} - Instância atual para encadeamento
217
+ */
218
+ rotate(angle, resize = true) {
219
+ // Converte o ângulo para radianos
220
+ const radians = (angle * Math.PI) / 180;
221
+
222
+ // Calcula as novas dimensões se resize for true
223
+ let newWidth = this.width;
224
+ let newHeight = this.height;
225
+
226
+ if (resize) {
227
+ // Calcula as novas dimensões para acomodar a imagem rotacionada
228
+ const cos = Math.abs(Math.cos(radians));
229
+ const sin = Math.abs(Math.sin(radians));
230
+ newWidth = Math.ceil(this.width * cos + this.height * sin);
231
+ newHeight = Math.ceil(this.width * sin + this.height * cos);
232
+ }
233
+
234
+ // Cria um novo canvas com as novas dimensões
235
+ const newCanvas = pureimage.make(newWidth, newHeight);
236
+ const newCtx = newCanvas.getContext("2d");
237
+
238
+ // Preenche o fundo com a cor especificada
239
+ newCtx.fillStyle = this.backgroundColor;
240
+ newCtx.fillRect(0, 0, newWidth, newHeight);
241
+
242
+ // Translada para o centro do novo canvas
243
+ newCtx.translate(newWidth / 2, newHeight / 2);
244
+
245
+ // Rotaciona o contexto
246
+ newCtx.rotate(radians);
247
+
248
+ // Desenha a imagem original centralizada
249
+ newCtx.drawImage(
250
+ this.canvas,
251
+ -this.width / 2,
252
+ -this.height / 2,
253
+ this.width,
254
+ this.height
255
+ );
256
+
257
+ // Restaura a transformação
258
+ newCtx.setTransform(1, 0, 0, 1, 0, 0);
259
+
260
+ // Atualiza as referências
261
+ this.canvas = newCanvas;
262
+ this.ctx = newCtx;
263
+ this.width = newWidth;
264
+ this.height = newHeight;
265
+
266
+ return this;
267
+ }
268
+
269
+ /**
270
+ * Espelha o canvas horizontalmente
271
+ * @returns {ImageProcessor} - Instância atual para encadeamento
272
+ */
273
+ flipHorizontal() {
274
+ // Cria um novo canvas com as mesmas dimensões
275
+ const newCanvas = pureimage.make(this.width, this.height);
276
+ const newCtx = newCanvas.getContext("2d");
277
+
278
+ // Espelha horizontalmente
279
+ newCtx.scale(-1, 1);
280
+ newCtx.drawImage(this.canvas, -this.width, 0, this.width, this.height);
281
+
282
+ // Restaura a transformação
283
+ newCtx.setTransform(1, 0, 0, 1, 0, 0);
284
+
285
+ // Atualiza as referências
286
+ this.canvas = newCanvas;
287
+ this.ctx = newCtx;
288
+
289
+ return this;
290
+ }
291
+
292
+ /**
293
+ * Espelha o canvas verticalmente
294
+ * @returns {ImageProcessor} - Instância atual para encadeamento
295
+ */
296
+ flipVertical() {
297
+ // Cria um novo canvas com as mesmas dimensões
298
+ const newCanvas = pureimage.make(this.width, this.height);
299
+ const newCtx = newCanvas.getContext("2d");
300
+
301
+ // Espelha verticalmente
302
+ newCtx.scale(1, -1);
303
+ newCtx.drawImage(this.canvas, 0, -this.height, this.width, this.height);
304
+
305
+ // Restaura a transformação
306
+ newCtx.setTransform(1, 0, 0, 1, 0, 0);
307
+
308
+ // Atualiza as referências
309
+ this.canvas = newCanvas;
310
+ this.ctx = newCtx;
311
+
312
+ return this;
313
+ }
314
+
315
+ /**
316
+ * Adiciona uma borda ao canvas
317
+ * @param {number} thickness - Espessura da borda em pixels
318
+ * @param {string} color - Cor da borda (hexadecimal)
319
+ * @param {string} style - Estilo da borda ('solid', 'dashed', 'dotted')
320
+ * @returns {ImageProcessor} - Instância atual para encadeamento
321
+ */
322
+ addBorder(thickness = 5, color = "#000000", style = "solid") {
323
+ this.ctx.strokeStyle = color;
324
+ this.ctx.lineWidth = thickness;
325
+
326
+ if (style === "dashed") {
327
+ this.ctx.setLineDash([thickness * 2, thickness]);
328
+ } else if (style === "dotted") {
329
+ this.ctx.setLineDash([thickness, thickness]);
330
+ }
331
+
332
+ this.ctx.strokeRect(
333
+ thickness / 2,
334
+ thickness / 2,
335
+ this.width - thickness,
336
+ this.height - thickness
337
+ );
338
+
339
+ // Restaura o estilo de linha
340
+ this.ctx.setLineDash([]);
341
+
342
+ return this;
343
+ }
344
+
345
+ /**
346
+ * Adiciona uma marca d'água de texto ao canvas
347
+ * @param {string} text - Texto da marca d'água
348
+ * @param {Object} options - Opções da marca d'água
349
+ * @returns {ImageProcessor} - Instância atual para encadeamento
350
+ */
351
+ addWatermark(text, options = {}) {
352
+ const {
353
+ font = "Arial",
354
+ fontSize = 30,
355
+ color = "#000000",
356
+ opacity = 0.3,
357
+ angle = -30,
358
+ x = this.width / 2,
359
+ y = this.height / 2
360
+ } = options;
361
+
362
+ filters.applyWatermark(
363
+ this.ctx,
364
+ 0,
365
+ 0,
366
+ this.width,
367
+ this.height,
368
+ text,
369
+ font,
370
+ fontSize,
371
+ color,
372
+ opacity,
373
+ angle
374
+ );
375
+
376
+ return this;
377
+ }
378
+
379
+ /**
380
+ * Adiciona uma marca d'água de imagem ao canvas
381
+ * @param {string|Buffer|Object} source - URL, Buffer ou caminho da imagem
382
+ * @param {Object} options - Opções da marca d'água
383
+ * @returns {Promise<ImageProcessor>} - Instância atual para encadeamento
384
+ */
385
+ async addImageWatermark(source, options = {}) {
386
+ const {
387
+ x = this.width / 2,
388
+ y = this.height / 2,
389
+ width = this.width * 0.3,
390
+ height = null,
391
+ opacity = 0.5,
392
+ align = "center", // center, topLeft, topRight, bottomLeft, bottomRight
393
+ offsetX = 0,
394
+ offsetY = 0
395
+ } = options;
396
+
397
+ try {
398
+ const img = await loadImageWithAxios(source);
399
+
400
+ // Calcula as dimensões proporcionais
401
+ const imgAspect = img.width / img.height;
402
+ let drawWidth = width;
403
+ let drawHeight = height || width / imgAspect;
404
+
405
+ if (!height) {
406
+ drawHeight = drawWidth / imgAspect;
407
+ } else {
408
+ drawWidth = drawHeight * imgAspect;
409
+ }
410
+
411
+ // Calcula a posição com base no alinhamento
412
+ let drawX = x;
413
+ let drawY = y;
414
+ const padding = 10; // Padding padrão para posicionamento
415
+
416
+ switch (align) {
417
+ case "topLeft":
418
+ drawX = padding + offsetX;
419
+ drawY = padding + offsetY;
420
+ break;
421
+ case "topRight":
422
+ drawX = this.width - drawWidth - padding + offsetX;
423
+ drawY = padding + offsetY;
424
+ break;
425
+ case "bottomLeft":
426
+ drawX = padding + offsetX;
427
+ drawY = this.height - drawHeight - padding + offsetY;
428
+ break;
429
+ case "bottomRight":
430
+ drawX = this.width - drawWidth - padding + offsetX;
431
+ drawY = this.height - drawHeight - padding + offsetY;
432
+ break;
433
+ case "center":
434
+ default:
435
+ drawX = (this.width - drawWidth) / 2 + offsetX;
436
+ drawY = (this.height - drawHeight) / 2 + offsetY;
437
+ break;
438
+ }
439
+
440
+ // Salva o estado atual do contexto
441
+ this.ctx.save();
442
+
443
+ // Define a opacidade
444
+ this.ctx.globalAlpha = opacity;
445
+
446
+ // Desenha a imagem
447
+ this.ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
448
+
449
+ // Restaura o estado do contexto
450
+ this.ctx.restore();
451
+
452
+ return this;
453
+ } catch (err) {
454
+ console.error("Falha ao adicionar marca d'água de imagem:", err.message);
455
+ throw new Error("Não foi possível adicionar a marca d'água de imagem.");
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Aplica um filtro ao canvas
461
+ * @param {string} filterName - Nome do filtro a ser aplicado
462
+ * @param {Object} options - Opções do filtro
463
+ * @returns {ImageProcessor} - Instância atual para encadeamento
464
+ */
465
+ applyFilter(filterName, options = {}) {
466
+ // Verifica se o filtro existe
467
+ if (!filters[filterName]) {
468
+ throw new Error(`Filtro "${filterName}" não encontrado.`);
469
+ }
470
+
471
+ // Aplica o filtro com os parâmetros fornecidos
472
+ filters[filterName](this.ctx, 0, 0, this.width, this.height, ...Object.values(options));
473
+
474
+ return this;
475
+ }
476
+
477
+ /**
478
+ * Aplica múltiplos filtros ao canvas
479
+ * @param {Array} filtersList - Lista de filtros a serem aplicados
480
+ * @returns {ImageProcessor} - Instância atual para encadeamento
481
+ */
482
+ applyFilters(filtersList) {
483
+ for (const filter of filtersList) {
484
+ this.applyFilter(filter.name, filter.options || {});
485
+ }
486
+
487
+ return this;
488
+ }
489
+
490
+ /**
491
+ * Adiciona texto ao canvas
492
+ * @param {string} text - Texto a ser adicionado
493
+ * @param {Object} options - Opções do texto
494
+ * @returns {ImageProcessor} - Instância atual para encadeamento
495
+ */
496
+ addText(text, options = {}) {
497
+ const {
498
+ x = this.width / 2,
499
+ y = this.height / 2,
500
+ font = "Arial",
501
+ fontSize = 30,
502
+ color = "#000000",
503
+ align = "center", // left, center, right
504
+ baseline = "middle", // top, middle, bottom
505
+ maxWidth = null,
506
+ lineHeight = 1.2,
507
+ shadow = false,
508
+ shadowColor = "rgba(0, 0, 0, 0.5)",
509
+ shadowBlur = 3,
510
+ shadowOffsetX = 1,
511
+ shadowOffsetY = 1,
512
+ stroke = false,
513
+ strokeColor = "#000000",
514
+ strokeWidth = 1,
515
+ background = false,
516
+ backgroundColor = "rgba(255, 255, 255, 0.5)",
517
+ padding = 5,
518
+ rotate = 0
519
+ } = options;
520
+
521
+ // Salva o estado atual do contexto
522
+ this.ctx.save();
523
+
524
+ // Configura o texto
525
+ this.ctx.font = `${fontSize}px ${font}`;
526
+ this.ctx.fillStyle = color;
527
+ this.ctx.textAlign = align;
528
+ this.ctx.textBaseline = baseline;
529
+
530
+ // Aplica rotação se necessário
531
+ if (rotate !== 0) {
532
+ this.ctx.translate(x, y);
533
+ this.ctx.rotate((rotate * Math.PI) / 180);
534
+ this.ctx.translate(-x, -y);
535
+ }
536
+
537
+ // Aplica sombra se necessário
538
+ if (shadow) {
539
+ this.ctx.shadowColor = shadowColor;
540
+ this.ctx.shadowBlur = shadowBlur;
541
+ this.ctx.shadowOffsetX = shadowOffsetX;
542
+ this.ctx.shadowOffsetY = shadowOffsetY;
543
+ }
544
+
545
+ // Se maxWidth for fornecido, quebra o texto em linhas
546
+ if (maxWidth) {
547
+ const words = text.split(" ");
548
+ const lines = [];
549
+ let currentLine = words[0];
550
+
551
+ for (let i = 1; i < words.length; i++) {
552
+ const word = words[i];
553
+ const width = this.ctx.measureText(currentLine + " " + word).width;
554
+
555
+ if (width < maxWidth) {
556
+ currentLine += " " + word;
557
+ } else {
558
+ lines.push(currentLine);
559
+ currentLine = word;
560
+ }
561
+ }
562
+
563
+ lines.push(currentLine);
564
+
565
+ // Desenha o fundo se necessário
566
+ if (background) {
567
+ let textWidth = 0;
568
+ for (const line of lines) {
569
+ const lineWidth = this.ctx.measureText(line).width;
570
+ textWidth = Math.max(textWidth, lineWidth);
571
+ }
572
+
573
+ const textHeight = lines.length * fontSize * lineHeight;
574
+ let bgX, bgY;
575
+
576
+ switch (align) {
577
+ case "left":
578
+ bgX = x;
579
+ break;
580
+ case "right":
581
+ bgX = x - textWidth;
582
+ break;
583
+ case "center":
584
+ default:
585
+ bgX = x - textWidth / 2;
586
+ break;
587
+ }
588
+
589
+ switch (baseline) {
590
+ case "top":
591
+ bgY = y;
592
+ break;
593
+ case "bottom":
594
+ bgY = y - textHeight;
595
+ break;
596
+ case "middle":
597
+ default:
598
+ bgY = y - textHeight / 2;
599
+ break;
600
+ }
601
+
602
+ this.ctx.fillStyle = backgroundColor;
603
+ this.ctx.fillRect(
604
+ bgX - padding,
605
+ bgY - padding,
606
+ textWidth + padding * 2,
607
+ textHeight + padding * 2
608
+ );
609
+
610
+ this.ctx.fillStyle = color;
611
+ }
612
+
613
+ // Desenha cada linha
614
+ for (let i = 0; i < lines.length; i++) {
615
+ const line = lines[i];
616
+ const lineY = y + (i - (lines.length - 1) / 2) * fontSize * lineHeight;
617
+
618
+ if (stroke) {
619
+ this.ctx.strokeStyle = strokeColor;
620
+ this.ctx.lineWidth = strokeWidth;
621
+ this.ctx.strokeText(line, x, lineY);
622
+ }
623
+
624
+ this.ctx.fillText(line, x, lineY);
625
+ }
626
+ } else {
627
+ // Desenha o fundo se necessário
628
+ if (background) {
629
+ const textWidth = this.ctx.measureText(text).width;
630
+ const textHeight = fontSize;
631
+ let bgX, bgY;
632
+
633
+ switch (align) {
634
+ case "left":
635
+ bgX = x;
636
+ break;
637
+ case "right":
638
+ bgX = x - textWidth;
639
+ break;
640
+ case "center":
641
+ default:
642
+ bgX = x - textWidth / 2;
643
+ break;
644
+ }
645
+
646
+ switch (baseline) {
647
+ case "top":
648
+ bgY = y;
649
+ break;
650
+ case "bottom":
651
+ bgY = y - textHeight;
652
+ break;
653
+ case "middle":
654
+ default:
655
+ bgY = y - textHeight / 2;
656
+ break;
657
+ }
658
+
659
+ this.ctx.fillStyle = backgroundColor;
660
+ this.ctx.fillRect(
661
+ bgX - padding,
662
+ bgY - padding,
663
+ textWidth + padding * 2,
664
+ textHeight + padding * 2
665
+ );
666
+
667
+ this.ctx.fillStyle = color;
668
+ }
669
+
670
+ // Desenha o texto
671
+ if (stroke) {
672
+ this.ctx.strokeStyle = strokeColor;
673
+ this.ctx.lineWidth = strokeWidth;
674
+ this.ctx.strokeText(text, x, y);
675
+ }
676
+
677
+ this.ctx.fillText(text, x, y);
678
+ }
679
+
680
+ // Restaura o estado do contexto
681
+ this.ctx.restore();
682
+
683
+ return this;
684
+ }
685
+
686
+ /**
687
+ * Adiciona uma forma ao canvas
688
+ * @param {string} shape - Tipo de forma ('rectangle', 'circle', 'ellipse', 'polygon')
689
+ * @param {Object} options - Opções da forma
690
+ * @returns {ImageProcessor} - Instância atual para encadeamento
691
+ */
692
+ addShape(shape, options = {}) {
693
+ const {
694
+ x = this.width / 2,
695
+ y = this.height / 2,
696
+ width = 100,
697
+ height = 100,
698
+ radius = 50,
699
+ radiusX = 50,
700
+ radiusY = 30,
701
+ sides = 6,
702
+ rotation = 0,
703
+ fill = true,
704
+ fillColor = "#000000",
705
+ stroke = false,
706
+ strokeColor = "#000000",
707
+ strokeWidth = 1,
708
+ opacity = 1
709
+ } = options;
710
+
711
+ // Salva o estado atual do contexto
712
+ this.ctx.save();
713
+
714
+ // Define a opacidade
715
+ this.ctx.globalAlpha = opacity;
716
+
717
+ // Configura o estilo de preenchimento e contorno
718
+ this.ctx.fillStyle = fillColor;
719
+ this.ctx.strokeStyle = strokeColor;
720
+ this.ctx.lineWidth = strokeWidth;
721
+
722
+ // Desenha a forma com base no tipo
723
+ switch (shape) {
724
+ case "rectangle":
725
+ if (fill) {
726
+ this.ctx.fillRect(x - width / 2, y - height / 2, width, height);
727
+ }
728
+ if (stroke) {
729
+ this.ctx.strokeRect(x - width / 2, y - height / 2, width, height);
730
+ }
731
+ break;
732
+
733
+ case "circle":
734
+ this.ctx.beginPath();
735
+ this.ctx.arc(x, y, radius, 0, Math.PI * 2);
736
+ if (fill) {
737
+ this.ctx.fill();
738
+ }
739
+ if (stroke) {
740
+ this.ctx.stroke();
741
+ }
742
+ break;
743
+
744
+ case "ellipse":
745
+ this.ctx.beginPath();
746
+ this.ctx.ellipse(x, y, radiusX, radiusY, 0, 0, Math.PI * 2);
747
+ if (fill) {
748
+ this.ctx.fill();
749
+ }
750
+ if (stroke) {
751
+ this.ctx.stroke();
752
+ }
753
+ break;
754
+
755
+ case "polygon":
756
+ this.ctx.beginPath();
757
+
758
+ const rotationInRadians = (rotation * Math.PI) / 180;
759
+
760
+ for (let i = 0; i < sides; i++) {
761
+ const angle = rotationInRadians + (i * 2 * Math.PI) / sides;
762
+ const pointX = x + radius * Math.cos(angle);
763
+ const pointY = y + radius * Math.sin(angle);
764
+
765
+ if (i === 0) {
766
+ this.ctx.moveTo(pointX, pointY);
767
+ } else {
768
+ this.ctx.lineTo(pointX, pointY);
769
+ }
770
+ }
771
+
772
+ this.ctx.closePath();
773
+
774
+ if (fill) {
775
+ this.ctx.fill();
776
+ }
777
+ if (stroke) {
778
+ this.ctx.stroke();
779
+ }
780
+ break;
781
+
782
+ default:
783
+ throw new Error(`Forma "${shape}" não suportada.`);
784
+ }
785
+
786
+ // Restaura o estado do contexto
787
+ this.ctx.restore();
788
+
789
+ return this;
790
+ }
791
+
792
+ /**
793
+ * Adiciona um gradiente ao canvas
794
+ * @param {string} type - Tipo de gradiente ('linear', 'radial')
795
+ * @param {Object} options - Opções do gradiente
796
+ * @returns {ImageProcessor} - Instância atual para encadeamento
797
+ */
798
+ addGradient(type, options = {}) {
799
+ const {
800
+ x1 = 0,
801
+ y1 = 0,
802
+ x2 = this.width,
803
+ y2 = this.height,
804
+ centerX = this.width / 2,
805
+ centerY = this.height / 2,
806
+ radius1 = 0,
807
+ radius2 = Math.max(this.width, this.height) / 2,
808
+ colors = ["#000000", "#FFFFFF"],
809
+ stops = null,
810
+ opacity = 1
811
+ } = options;
812
+
813
+ // Salva o estado atual do contexto
814
+ this.ctx.save();
815
+
816
+ // Define a opacidade
817
+ this.ctx.globalAlpha = opacity;
818
+
819
+ // Cria o gradiente com base no tipo
820
+ let gradient;
821
+
822
+ if (type === "linear") {
823
+ gradient = this.ctx.createLinearGradient(x1, y1, x2, y2);
824
+ } else if (type === "radial") {
825
+ gradient = this.ctx.createRadialGradient(
826
+ centerX,
827
+ centerY,
828
+ radius1,
829
+ centerX,
830
+ centerY,
831
+ radius2
832
+ );
833
+ } else {
834
+ throw new Error(`Tipo de gradiente "${type}" não suportado.`);
835
+ }
836
+
837
+ // Adiciona as cores ao gradiente
838
+ if (stops) {
839
+ // Se os stops forem fornecidos, usa-os
840
+ for (let i = 0; i < colors.length; i++) {
841
+ gradient.addColorStop(stops[i], colors[i]);
842
+ }
843
+ } else {
844
+ // Caso contrário, distribui as cores uniformemente
845
+ for (let i = 0; i < colors.length; i++) {
846
+ gradient.addColorStop(i / (colors.length - 1), colors[i]);
847
+ }
848
+ }
849
+
850
+ // Preenche o canvas com o gradiente
851
+ this.ctx.fillStyle = gradient;
852
+ this.ctx.fillRect(0, 0, this.width, this.height);
853
+
854
+ // Restaura o estado do contexto
855
+ this.ctx.restore();
856
+
857
+ return this;
858
+ }
859
+
860
+ /**
861
+ * Adiciona um padrão ao canvas
862
+ * @param {string} type - Tipo de padrão ('dots', 'lines', 'grid', 'checkerboard')
863
+ * @param {Object} options - Opções do padrão
864
+ * @returns {ImageProcessor} - Instância atual para encadeamento
865
+ */
866
+ addPattern(type, options = {}) {
867
+ const {
868
+ color = "#000000",
869
+ backgroundColor = null,
870
+ size = 10,
871
+ spacing = 20,
872
+ lineWidth = 1,
873
+ angle = 0,
874
+ opacity = 0.2
875
+ } = options;
876
+
877
+ // Salva o estado atual do contexto
878
+ this.ctx.save();
879
+
880
+ // Define a opacidade
881
+ this.ctx.globalAlpha = opacity;
882
+
883
+ // Preenche o fundo se fornecido
884
+ if (backgroundColor) {
885
+ this.ctx.fillStyle = backgroundColor;
886
+ this.ctx.fillRect(0, 0, this.width, this.height);
887
+ }
888
+
889
+ // Configura o estilo de desenho
890
+ this.ctx.fillStyle = color;
891
+ this.ctx.strokeStyle = color;
892
+ this.ctx.lineWidth = lineWidth;
893
+
894
+ // Aplica rotação se necessário
895
+ if (angle !== 0) {
896
+ this.ctx.translate(this.width / 2, this.height / 2);
897
+ this.ctx.rotate((angle * Math.PI) / 180);
898
+ this.ctx.translate(-this.width / 2, -this.height / 2);
899
+ }
900
+
901
+ // Desenha o padrão com base no tipo
902
+ switch (type) {
903
+ case "dots":
904
+ for (let y = spacing / 2; y < this.height + spacing; y += spacing) {
905
+ for (let x = spacing / 2; x < this.width + spacing; x += spacing) {
906
+ this.ctx.beginPath();
907
+ this.ctx.arc(x, y, size / 2, 0, Math.PI * 2);
908
+ this.ctx.fill();
909
+ }
910
+ }
911
+ break;
912
+
913
+ case "lines":
914
+ for (let y = 0; y < this.height + spacing; y += spacing) {
915
+ this.ctx.beginPath();
916
+ this.ctx.moveTo(0, y);
917
+ this.ctx.lineTo(this.width, y);
918
+ this.ctx.stroke();
919
+ }
920
+ break;
921
+
922
+ case "grid":
923
+ // Linhas horizontais
924
+ for (let y = 0; y < this.height + spacing; y += spacing) {
925
+ this.ctx.beginPath();
926
+ this.ctx.moveTo(0, y);
927
+ this.ctx.lineTo(this.width, y);
928
+ this.ctx.stroke();
929
+ }
930
+
931
+ // Linhas verticais
932
+ for (let x = 0; x < this.width + spacing; x += spacing) {
933
+ this.ctx.beginPath();
934
+ this.ctx.moveTo(x, 0);
935
+ this.ctx.lineTo(x, this.height);
936
+ this.ctx.stroke();
937
+ }
938
+ break;
939
+
940
+ case "checkerboard":
941
+ const cellSize = spacing;
942
+
943
+ for (let y = 0; y < this.height + cellSize; y += cellSize) {
944
+ for (let x = 0; x < this.width + cellSize; x += cellSize) {
945
+ if ((Math.floor(x / cellSize) + Math.floor(y / cellSize)) % 2 === 0) {
946
+ this.ctx.fillRect(x, y, cellSize, cellSize);
947
+ }
948
+ }
949
+ }
950
+ break;
951
+
952
+ default:
953
+ throw new Error(`Tipo de padrão "${type}" não suportado.`);
954
+ }
955
+
956
+ // Restaura o estado do contexto
957
+ this.ctx.restore();
958
+
959
+ return this;
960
+ }
961
+
962
+ /**
963
+ * Converte o canvas para um buffer de imagem
964
+ * @param {string} format - Formato da imagem ('png', 'jpeg')
965
+ * @param {Object} options - Opções de codificação
966
+ * @returns {Promise<Buffer>} - Buffer contendo a imagem
967
+ */
968
+ async toBuffer(format = "png", options = {}) {
969
+ const { quality = this.quality } = options;
970
+
971
+ try {
972
+ return await encodeToBuffer(this.canvas, format, quality);
973
+ } catch (err) {
974
+ console.error("Falha ao codificar a imagem:", err.message);
975
+ throw new Error("Não foi possível gerar o buffer de imagem.");
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Salva o canvas em um arquivo
981
+ * @param {string} filePath - Caminho do arquivo
982
+ * @param {string} format - Formato da imagem ('png', 'jpeg')
983
+ * @param {Object} options - Opções de codificação
984
+ * @returns {Promise<string>} - Caminho do arquivo salvo
985
+ */
986
+ async saveToFile(filePath, format = null, options = {}) {
987
+ // Determina o formato com base na extensão do arquivo se não for fornecido
988
+ if (!format) {
989
+ const ext = path.extname(filePath).toLowerCase();
990
+ format = ext === ".jpg" || ext === ".jpeg" ? "jpeg" : "png";
991
+ }
992
+
993
+ try {
994
+ const buffer = await this.toBuffer(format, options);
995
+
996
+ // Cria o diretório se não existir
997
+ const dir = path.dirname(filePath);
998
+ if (!fs.existsSync(dir)) {
999
+ fs.mkdirSync(dir, { recursive: true });
1000
+ }
1001
+
1002
+ // Salva o buffer no arquivo
1003
+ fs.writeFileSync(filePath, buffer);
1004
+
1005
+ return filePath;
1006
+ } catch (err) {
1007
+ console.error("Falha ao salvar a imagem:", err.message);
1008
+ throw new Error("Não foi possível salvar a imagem no arquivo.");
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ module.exports = ImageProcessor;
1014
+