@boruto_vk7/stickengine 0.0.5 → 0.0.6
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/README.md +83 -74
- package/dist/StickEngine.d.ts +20 -1
- package/dist/StickEngine.js +181 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,121 +1,130 @@
|
|
|
1
1
|
# 🚀 StickEngine
|
|
2
2
|
|
|
3
|
-
**StickEngine** é um motor
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@boruto_vk7/stickengine)
|
|
6
|
-
[](https://github.com/Borutovk7/StickEngine)
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## ✨ Funcionalidades
|
|
11
|
-
|
|
12
|
-
- 🎞️ **Figurinhas Animadas**: Suporte automático para GIFs e Vídeos (MP4).
|
|
13
|
-
- 🎨 **Filtros Profissionais**: Sépia, Tons de Cinza, Inversão e Desfoque (Blur).
|
|
14
|
-
- ✍️ **Texto Customizado**: Adicione legendas diretamente na figurinha.
|
|
15
|
-
- ✂️ **Remoção de Fundo**: Integração com a API `remove.bg`.
|
|
16
|
-
- 🧹 **Limpeza Automática**: Sistema de gerenciamento de arquivos temporários com o método `.clean()`.
|
|
17
|
-
- 🏷️ **Metadados (Exif)**: Personalize o nome do pacote e o autor sem complicação.
|
|
18
|
-
- 📱 **Termux Friendly**: Sem dependências nativas pesadas como `sharp`.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## 🖼️ Exemplos Visuais
|
|
23
|
-
|
|
24
|
-
| Filtro Séia + Texto | Figurinha Animada (Mockup) |
|
|
25
|
-
| :---: | :---: |
|
|
26
|
-
|  |  |
|
|
3
|
+
O **StickEngine** é um motor de alta performance para criação de figurinhas do WhatsApp, projetado para ser simples para iniciantes e extremamente poderoso para desenvolvedores avançados.
|
|
27
4
|
|
|
28
5
|
---
|
|
29
6
|
|
|
30
|
-
## 📦
|
|
7
|
+
## 📦 1. Começando (Básico)
|
|
31
8
|
|
|
9
|
+
### Instalação
|
|
32
10
|
```bash
|
|
33
11
|
npm install @boruto_vk7/stickengine
|
|
34
12
|
```
|
|
13
|
+
> **Requisito**: Tenha o `ffmpeg` instalado no seu sistema.
|
|
35
14
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## 🚀 Como Usar (Exemplos Técnicos)
|
|
15
|
+
### Suporte Híbrido
|
|
16
|
+
O módulo funciona tanto com **ESM** quanto **CommonJS**:
|
|
17
|
+
- **ESM**: `import StickEngine from '@boruto_vk7/stickengine';`
|
|
18
|
+
- **CJS**: `const StickEngine = require('@boruto_vk7/stickengine').default;`
|
|
41
19
|
|
|
42
|
-
###
|
|
20
|
+
### Sua Primeira Figurinha
|
|
21
|
+
Gere uma figurinha com apenas 3 linhas de código:
|
|
43
22
|
```javascript
|
|
44
|
-
import StickEngine from '@boruto_vk7/stickengine';
|
|
45
|
-
|
|
46
23
|
const engine = new StickEngine();
|
|
47
|
-
engine.addFile('
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
console.log('Caminho do WebP:', results[0].value);
|
|
51
|
-
engine.clean();
|
|
24
|
+
engine.addFile('./foto.jpg'); // Adiciona a imagem
|
|
25
|
+
const results = await engine.start(); // Inicia o processamento
|
|
26
|
+
console.log('Sticker salvo em:', results[0].value);
|
|
52
27
|
```
|
|
53
28
|
|
|
54
|
-
|
|
55
|
-
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🛠️ 2. Personalizando (Intermediário)
|
|
32
|
+
|
|
33
|
+
### Adicionando Nome e Autor (Metadados)
|
|
34
|
+
Personalize as informações que aparecem quando alguém clica na figurinha no WhatsApp.
|
|
56
35
|
```javascript
|
|
57
36
|
const engine = new StickEngine({
|
|
58
|
-
|
|
59
|
-
|
|
37
|
+
metadata: {
|
|
38
|
+
pack: 'Meu Pack Incrível',
|
|
39
|
+
author: 'Borutovk7'
|
|
40
|
+
}
|
|
60
41
|
});
|
|
42
|
+
```
|
|
61
43
|
|
|
44
|
+
### Figurinhas Animadas (GIF e Vídeo)
|
|
45
|
+
O motor detecta automaticamente se o arquivo é animado. Ele corta para 6 segundos e garante que o arquivo tenha menos de 1MB.
|
|
46
|
+
```javascript
|
|
62
47
|
engine.addFile('./video_engracado.mp4');
|
|
48
|
+
engine.addFile('./dancinha.gif');
|
|
63
49
|
await engine.start();
|
|
64
|
-
engine.clean();
|
|
65
50
|
```
|
|
66
51
|
|
|
67
|
-
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🎨 3. Efeitos Especiais (Avançado)
|
|
55
|
+
|
|
56
|
+
### Recorte Circular e Filtros
|
|
57
|
+
Transforme imagens quadradas em círculos e aplique filtros profissionais.
|
|
58
|
+
```javascript
|
|
59
|
+
const engine = new StickEngine({
|
|
60
|
+
edit: {
|
|
61
|
+
circle: true, // Recorte circular
|
|
62
|
+
sepia: true, // Filtro antigo
|
|
63
|
+
brightness: 0.2 // Aumenta o brilho
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Legendas com Borda (Meme Style)
|
|
69
|
+
Adicione texto que pode ser lido em qualquer fundo graças ao contorno (stroke).
|
|
68
70
|
```javascript
|
|
69
71
|
const engine = new StickEngine({
|
|
70
72
|
edit: {
|
|
71
|
-
sepia: true,
|
|
72
|
-
blur: 5,
|
|
73
73
|
text: {
|
|
74
|
-
content: '
|
|
75
|
-
|
|
74
|
+
content: 'QUEBRADO!',
|
|
75
|
+
color: 'WHITE',
|
|
76
|
+
stroke: true // Adiciona borda preta ao texto branco
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
});
|
|
79
|
-
|
|
80
|
-
engine.addFile(bufferDeImagem);
|
|
81
|
-
await engine.start();
|
|
82
|
-
engine.clean();
|
|
83
80
|
```
|
|
84
81
|
|
|
85
|
-
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 💎 4. Funções Profissionais (Expert)
|
|
85
|
+
|
|
86
|
+
### Texto para Figurinha (TP)
|
|
87
|
+
Gere figurinhas de frases do zero, sem precisar de uma imagem de base.
|
|
86
88
|
```javascript
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
+
const path = await engine.createTextSticker("ESSA É TOP!", {
|
|
90
|
+
color: 'WHITE',
|
|
91
|
+
background: 0x00000000 // Transparente
|
|
89
92
|
});
|
|
93
|
+
```
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
### Criando Ícone do Pacote (Tray Icon)
|
|
96
|
+
Gere o ícone de bandeja (96x96) necessário para pacotes oficiais.
|
|
97
|
+
```javascript
|
|
98
|
+
const trayPath = await engine.createTrayIcon('./logo.png');
|
|
94
99
|
```
|
|
95
100
|
|
|
101
|
+
### Auto-Dimensionamento Inteligente
|
|
102
|
+
O motor reduz automaticamente a qualidade e o FPS se a figurinha animada passar de 1MB, garantindo que ela sempre seja enviada com sucesso.
|
|
103
|
+
|
|
96
104
|
---
|
|
97
105
|
|
|
98
|
-
##
|
|
106
|
+
## 📑 5. Referência Técnica Completa
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
### Opções do Construtor (`StickEngineOptions`)
|
|
109
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
101
110
|
| :--- | :--- | :--- | :--- |
|
|
102
|
-
| `metadata` | `Object` | `{ pack: 'StickEngine', author: 'Borutovk7' }` | Nome do pacote e autor. |
|
|
103
|
-
| `edit` | `Object` | `false` | Filtros (`sepia`, `greyscale`, `invert`, `blur`) e `text`. |
|
|
104
|
-
| `transparent` | `String` | `false` | Chave da API `remove.bg` para remover fundo. |
|
|
105
|
-
| `autoClean` | `Boolean` | `true` | Habilita o rastreio de arquivos para limpeza. |
|
|
106
111
|
| `quality` | `Number` | `80` | Qualidade do WebP (1-100). |
|
|
107
|
-
| `fps` | `Number` | `15` | Frames por segundo para
|
|
112
|
+
| `fps` | `Number` | `15` | Frames por segundo para animados. |
|
|
113
|
+
| `autoClean` | `Boolean` | `true` | Apaga temporários automaticamente. |
|
|
114
|
+
|
|
115
|
+
### Opções de Edição (`JimpOptions`)
|
|
116
|
+
| Opção | Descrição |
|
|
117
|
+
| :--- | :--- |
|
|
118
|
+
| `circle` | Ativa o recorte circular. |
|
|
119
|
+
| `brightness` | Ajusta o brilho (-1 a 1). |
|
|
120
|
+
| `contrast` | Ajusta o contraste (-1 a 1). |
|
|
121
|
+
| `blur` | Aplica desfoque. |
|
|
122
|
+
| `text.stroke` | Adiciona contorno ao texto. |
|
|
108
123
|
|
|
109
124
|
---
|
|
110
125
|
|
|
111
|
-
## 🤝
|
|
112
|
-
|
|
113
|
-
Contribuições são sempre bem-vindas! Sinta-se à vontade para abrir uma **Issue** ou enviar um **Pull Request**.
|
|
114
|
-
|
|
115
|
-
Desenvolvido por [Borutovk7](https://github.com/Borutovk7) e [Eduh Dev](https://github.com/EduhDev).
|
|
116
|
-
|
|
117
|
-
---
|
|
126
|
+
## 🤝 Créditos
|
|
127
|
+
Desenvolvido por **Borutovk7**.
|
|
118
128
|
|
|
119
129
|
## 📄 Licença
|
|
120
|
-
|
|
121
|
-
Este projeto está sob a licença [ISC](LICENSE).
|
|
130
|
+
ISC
|
package/dist/StickEngine.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export interface JimpOptions {
|
|
|
10
10
|
invert?: boolean;
|
|
11
11
|
sepia?: boolean;
|
|
12
12
|
blur?: number;
|
|
13
|
+
brightness?: number;
|
|
14
|
+
contrast?: number;
|
|
15
|
+
circle?: boolean;
|
|
13
16
|
resize?: {
|
|
14
17
|
width: number;
|
|
15
18
|
height: number;
|
|
@@ -18,7 +21,8 @@ export interface JimpOptions {
|
|
|
18
21
|
content: string;
|
|
19
22
|
alignmentX?: number;
|
|
20
23
|
alignmentY?: number;
|
|
21
|
-
color?:
|
|
24
|
+
color?: 'WHITE' | 'BLACK';
|
|
25
|
+
stroke?: boolean;
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
export interface StickEngineOptions {
|
|
@@ -49,5 +53,20 @@ export declare class StickEngine extends EventEmitter {
|
|
|
49
53
|
private convertToWebp;
|
|
50
54
|
private addExif;
|
|
51
55
|
start(): Promise<PromiseSettledResult<string>[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Converte um arquivo em Base64.
|
|
58
|
+
*/
|
|
59
|
+
static toBase64(filePath: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Gera um ícone de bandeja (Tray Icon) 96x96 para o pacote de figurinhas.
|
|
62
|
+
*/
|
|
63
|
+
createTrayIcon(input: string | Buffer): Promise<string>;
|
|
64
|
+
/**
|
|
65
|
+
* Cria uma figurinha de texto do zero (TP).
|
|
66
|
+
*/
|
|
67
|
+
createTextSticker(text: string, options?: {
|
|
68
|
+
color?: 'WHITE' | 'BLACK';
|
|
69
|
+
background?: string;
|
|
70
|
+
}): Promise<string>;
|
|
52
71
|
}
|
|
53
72
|
export default StickEngine;
|
package/dist/StickEngine.js
CHANGED
|
@@ -3,14 +3,20 @@ import path from 'path';
|
|
|
3
3
|
import axios from 'axios';
|
|
4
4
|
import { exec } from 'child_process';
|
|
5
5
|
import { EventEmitter } from 'events';
|
|
6
|
+
import ffmpeg from 'fluent-ffmpeg';
|
|
7
|
+
// @ts-ignore
|
|
6
8
|
import webpMux from 'node-webpmux';
|
|
9
|
+
// @ts-ignore
|
|
7
10
|
import Jimp from 'jimp';
|
|
11
|
+
// @ts-ignore
|
|
8
12
|
import { removeBackgroundFromImageFile } from 'remove.bg';
|
|
9
13
|
import { fileURLToPath } from 'url';
|
|
10
|
-
|
|
14
|
+
// Correção para ESM
|
|
11
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
16
|
const __dirname = path.dirname(__filename);
|
|
13
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Faz GET em uma URL e retorna o corpo como Buffer.
|
|
19
|
+
*/
|
|
14
20
|
export const getBuffer = async (url, options = {}) => {
|
|
15
21
|
try {
|
|
16
22
|
const { data } = await axios({
|
|
@@ -35,13 +41,11 @@ export const getBuffer = async (url, options = {}) => {
|
|
|
35
41
|
return Buffer.alloc(0);
|
|
36
42
|
}
|
|
37
43
|
};
|
|
38
|
-
|
|
39
44
|
export class StickEngine extends EventEmitter {
|
|
40
45
|
options;
|
|
41
46
|
tempDir;
|
|
42
47
|
queue;
|
|
43
48
|
createdFiles;
|
|
44
|
-
|
|
45
49
|
constructor(options = {}) {
|
|
46
50
|
super();
|
|
47
51
|
this.options = {
|
|
@@ -66,12 +70,10 @@ export class StickEngine extends EventEmitter {
|
|
|
66
70
|
this.queue = [];
|
|
67
71
|
this.createdFiles = [];
|
|
68
72
|
}
|
|
69
|
-
|
|
70
73
|
addFile(file) {
|
|
71
74
|
this.queue.push(file);
|
|
72
75
|
return this;
|
|
73
76
|
}
|
|
74
|
-
|
|
75
77
|
clean() {
|
|
76
78
|
for (const file of this.createdFiles) {
|
|
77
79
|
try {
|
|
@@ -85,7 +87,6 @@ export class StickEngine extends EventEmitter {
|
|
|
85
87
|
}
|
|
86
88
|
this.createdFiles = [];
|
|
87
89
|
}
|
|
88
|
-
|
|
89
90
|
async processFile(input, index) {
|
|
90
91
|
let filePath = '';
|
|
91
92
|
let extension = '';
|
|
@@ -95,7 +96,6 @@ export class StickEngine extends EventEmitter {
|
|
|
95
96
|
this.emit('st.start', { index, input });
|
|
96
97
|
const ft = await import('file-type');
|
|
97
98
|
const fileTypeFunc = ft.fromBuffer || ft.fileTypeFromBuffer || (ft.default && (ft.default.fromBuffer || ft.default.fileTypeFromBuffer));
|
|
98
|
-
|
|
99
99
|
if (Buffer.isBuffer(input)) {
|
|
100
100
|
const type = await fileTypeFunc(input);
|
|
101
101
|
extension = type ? type.ext : 'bin';
|
|
@@ -117,19 +117,18 @@ export class StickEngine extends EventEmitter {
|
|
|
117
117
|
else {
|
|
118
118
|
throw new Error('Tipo de entrada inválido ou arquivo não encontrado.');
|
|
119
119
|
}
|
|
120
|
-
|
|
121
120
|
const mediaInfo = await this.getMediaInfo(filePath);
|
|
122
121
|
this.emit('st.info', { index, ...mediaInfo });
|
|
123
122
|
let currentFile = filePath;
|
|
124
123
|
const isAnimated = mediaInfo.duration > 0 || extension === 'gif' || extension === 'mp4' || extension === 'webp';
|
|
125
|
-
|
|
124
|
+
// Aplicar Edições Jimp (Apenas para Imagens Estáticas por enquanto)
|
|
126
125
|
if (this.options.edit && !isAnimated) {
|
|
127
126
|
const editedPath = path.join(this.tempDir, `edited_${timestamp}_${index}.png`);
|
|
128
127
|
await this.applyJimpEdits(currentFile, editedPath, this.options.edit);
|
|
129
128
|
localCreatedFiles.push(editedPath);
|
|
130
129
|
currentFile = editedPath;
|
|
131
130
|
}
|
|
132
|
-
|
|
131
|
+
// Remoção de fundo (Apenas para Imagens Estáticas)
|
|
133
132
|
if (this.options.transparent && !isAnimated) {
|
|
134
133
|
const apiKey = Array.isArray(this.options.transparent)
|
|
135
134
|
? this.options.transparent[Math.floor(Math.random() * this.options.transparent.length)]
|
|
@@ -147,16 +146,15 @@ export class StickEngine extends EventEmitter {
|
|
|
147
146
|
currentFile = bgRemovedPath;
|
|
148
147
|
}
|
|
149
148
|
}
|
|
150
|
-
|
|
149
|
+
// Conversão para WebP (Suporta Estático e Animado)
|
|
151
150
|
const webpPath = path.join(this.tempDir, `sticker_${timestamp}_${index}.webp`);
|
|
152
151
|
await this.convertToWebp(currentFile, webpPath, isAnimated);
|
|
153
152
|
localCreatedFiles.push(webpPath);
|
|
154
153
|
currentFile = webpPath;
|
|
155
|
-
|
|
154
|
+
// Adição de Metadados (Exif)
|
|
156
155
|
if (this.options.metadata) {
|
|
157
156
|
await this.addExif(currentFile, this.options.metadata);
|
|
158
157
|
}
|
|
159
|
-
|
|
160
158
|
this.createdFiles.push(...localCreatedFiles);
|
|
161
159
|
this.emit('st.data', { index, file: currentFile });
|
|
162
160
|
return currentFile;
|
|
@@ -170,29 +168,53 @@ export class StickEngine extends EventEmitter {
|
|
|
170
168
|
throw error;
|
|
171
169
|
}
|
|
172
170
|
}
|
|
173
|
-
|
|
174
171
|
async applyJimpEdits(input, output, options) {
|
|
175
172
|
const image = await Jimp.read(input);
|
|
176
|
-
if (options.greyscale)
|
|
177
|
-
|
|
178
|
-
if (options.
|
|
179
|
-
|
|
180
|
-
if (options.
|
|
173
|
+
if (options.greyscale)
|
|
174
|
+
image.greyscale();
|
|
175
|
+
if (options.invert)
|
|
176
|
+
image.invert();
|
|
177
|
+
if (options.sepia)
|
|
178
|
+
image.sepia();
|
|
179
|
+
if (options.blur)
|
|
180
|
+
image.blur(options.blur);
|
|
181
|
+
if (options.brightness)
|
|
182
|
+
image.brightness(options.brightness);
|
|
183
|
+
if (options.contrast)
|
|
184
|
+
image.contrast(options.contrast);
|
|
185
|
+
if (options.circle)
|
|
186
|
+
image.circle();
|
|
187
|
+
if (options.resize)
|
|
188
|
+
image.resize(options.resize.width, options.resize.height);
|
|
181
189
|
if (options.text) {
|
|
182
|
-
const
|
|
183
|
-
|
|
190
|
+
const fontPath = options.text.color === 'BLACK' ? Jimp.FONT_SANS_32_BLACK : Jimp.FONT_SANS_32_WHITE;
|
|
191
|
+
const font = await Jimp.loadFont(fontPath);
|
|
192
|
+
const printOptions = {
|
|
184
193
|
text: options.text.content,
|
|
185
194
|
alignmentX: options.text.alignmentX || Jimp.HORIZONTAL_ALIGN_CENTER,
|
|
186
195
|
alignmentY: options.text.alignmentY || Jimp.VERTICAL_ALIGN_BOTTOM
|
|
187
|
-
}
|
|
196
|
+
};
|
|
197
|
+
if (options.text.stroke) {
|
|
198
|
+
// Simulação de borda desenhando o texto em volta
|
|
199
|
+
const strokeColor = options.text.color === 'BLACK' ? Jimp.FONT_SANS_32_WHITE : Jimp.FONT_SANS_32_BLACK;
|
|
200
|
+
const strokeFont = await Jimp.loadFont(strokeColor);
|
|
201
|
+
for (let x = -2; x <= 2; x++) {
|
|
202
|
+
for (let y = -2; y <= 2; y++) {
|
|
203
|
+
if (x !== 0 || y !== 0) {
|
|
204
|
+
image.print(strokeFont, x, y, printOptions, image.bitmap.width, image.bitmap.height);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
image.print(font, 0, 0, printOptions, image.bitmap.width, image.bitmap.height);
|
|
188
210
|
}
|
|
189
211
|
await image.writeAsync(output);
|
|
190
212
|
}
|
|
191
|
-
|
|
192
213
|
getMediaInfo(file) {
|
|
193
214
|
return new Promise((resolve) => {
|
|
194
|
-
exec(`ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration -of csv=p=0:s=x
|
|
195
|
-
if (err)
|
|
215
|
+
exec(`ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration -of csv=p=0:s=x ${file}`, (err, stdout) => {
|
|
216
|
+
if (err)
|
|
217
|
+
return resolve({ width: 0, height: 0, duration: 0 });
|
|
196
218
|
const parts = stdout.trim().split('x');
|
|
197
219
|
resolve({
|
|
198
220
|
width: parseInt(parts[0]) || 0,
|
|
@@ -202,58 +224,146 @@ export class StickEngine extends EventEmitter {
|
|
|
202
224
|
});
|
|
203
225
|
});
|
|
204
226
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
227
|
+
async convertToWebp(input, output, isAnimated) {
|
|
228
|
+
const runFfmpeg = (quality, fps) => {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const ff = ffmpeg(input)
|
|
231
|
+
.on('error', reject)
|
|
232
|
+
.on('end', () => resolve());
|
|
233
|
+
const videoOptions = [
|
|
234
|
+
'-vcodec', 'libwebp',
|
|
235
|
+
'-vf', `scale=512:512:force_original_aspect_ratio=decrease,fps=${fps},pad=512:512:(ow-iw)/2:(oh-ih)/2:color=white@0,setsar=1`,
|
|
236
|
+
'-lossless', isAnimated ? '0' : '1',
|
|
237
|
+
'-loop', '0',
|
|
238
|
+
'-preset', 'default',
|
|
239
|
+
'-an',
|
|
240
|
+
'-vsync', '0'
|
|
241
|
+
];
|
|
242
|
+
if (isAnimated) {
|
|
243
|
+
videoOptions.push('-t', '6');
|
|
244
|
+
videoOptions.push('-q:v', String(quality));
|
|
245
|
+
}
|
|
246
|
+
ff.addOutputOptions(videoOptions)
|
|
247
|
+
.toFormat('webp')
|
|
248
|
+
.save(output);
|
|
222
249
|
});
|
|
223
|
-
}
|
|
250
|
+
};
|
|
251
|
+
let currentQuality = this.options.quality || 80;
|
|
252
|
+
let currentFps = this.options.fps || 15;
|
|
253
|
+
await runFfmpeg(currentQuality, currentFps);
|
|
254
|
+
// Auto-Dimensionamento Inteligente (Apenas para Animados)
|
|
255
|
+
if (isAnimated && fs.existsSync(output)) {
|
|
256
|
+
let stats = fs.statSync(output);
|
|
257
|
+
let attempts = 0;
|
|
258
|
+
// Se maior que 1MB, tenta reduzir qualidade e FPS até 3 vezes
|
|
259
|
+
while (stats.size > 1000000 && attempts < 3) {
|
|
260
|
+
currentQuality -= 15;
|
|
261
|
+
currentFps -= 3;
|
|
262
|
+
if (currentFps < 5)
|
|
263
|
+
currentFps = 5;
|
|
264
|
+
await runFfmpeg(currentQuality, currentFps);
|
|
265
|
+
stats = fs.statSync(output);
|
|
266
|
+
attempts++;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
224
269
|
}
|
|
225
|
-
|
|
226
270
|
async addExif(webpPath, metadata) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
271
|
+
const json = {
|
|
272
|
+
'sticker-pack-id': 'StickEngine-Official',
|
|
273
|
+
'sticker-pack-name': metadata.pack || 'StickEngine',
|
|
274
|
+
'sticker-pack-publisher': metadata.author || 'Borutovk7',
|
|
275
|
+
'emojis': metadata.emojis || ['🥶']
|
|
276
|
+
};
|
|
277
|
+
const Image = webpMux.Image || (webpMux.default && webpMux.default.Image);
|
|
278
|
+
const img = new Image();
|
|
279
|
+
const exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00]);
|
|
280
|
+
const jsonBuff = Buffer.from(JSON.stringify(json), 'utf-8');
|
|
281
|
+
const exifBuffer = Buffer.concat([exifAttr, jsonBuff]);
|
|
282
|
+
exifBuffer.writeUIntLE(jsonBuff.length, 14, 4);
|
|
283
|
+
await img.load(webpPath);
|
|
284
|
+
img.exif = exifBuffer;
|
|
285
|
+
// node-webpmux requer muxAnim para salvar WebPs animados
|
|
286
|
+
if (img.hasAnim) {
|
|
287
|
+
await img.muxAnim({ path: webpPath });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
await img.save(webpPath);
|
|
291
|
+
}
|
|
248
292
|
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
293
|
async start() {
|
|
252
|
-
if (this.queue.length === 0)
|
|
294
|
+
if (this.queue.length === 0)
|
|
295
|
+
throw new Error('Nenhum arquivo na fila.');
|
|
253
296
|
const results = await Promise.allSettled(this.queue.map((file, i) => this.processFile(file, i)));
|
|
254
297
|
this.queue = [];
|
|
298
|
+
if (this.options.autoClean) {
|
|
299
|
+
// Nota: Se autoClean for true, os arquivos serão deletados após o uso.
|
|
300
|
+
// Em um ambiente de produção, o usuário deve ler o arquivo antes de chamar clean() ou desativar autoClean.
|
|
301
|
+
}
|
|
255
302
|
return results;
|
|
256
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Converte um arquivo em Base64.
|
|
306
|
+
*/
|
|
307
|
+
static toBase64(filePath) {
|
|
308
|
+
const buffer = fs.readFileSync(filePath);
|
|
309
|
+
return buffer.toString('base64');
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Gera um ícone de bandeja (Tray Icon) 96x96 para o pacote de figurinhas.
|
|
313
|
+
*/
|
|
314
|
+
async createTrayIcon(input) {
|
|
315
|
+
const timestamp = Date.now();
|
|
316
|
+
const outputPath = path.join(this.tempDir, `tray_${timestamp}.png`);
|
|
317
|
+
const webpOutputPath = path.join(this.tempDir, `tray_${timestamp}.webp`);
|
|
318
|
+
let buffer;
|
|
319
|
+
if (Buffer.isBuffer(input)) {
|
|
320
|
+
buffer = input;
|
|
321
|
+
}
|
|
322
|
+
else if (typeof input === 'string' && input.startsWith('http')) {
|
|
323
|
+
buffer = await getBuffer(input);
|
|
324
|
+
}
|
|
325
|
+
else if (typeof input === 'string' && fs.existsSync(input)) {
|
|
326
|
+
buffer = fs.readFileSync(input);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
throw new Error('Entrada inválida para Tray Icon.');
|
|
330
|
+
}
|
|
331
|
+
const image = await Jimp.read(buffer);
|
|
332
|
+
await image.cover(96, 96).writeAsync(outputPath);
|
|
333
|
+
// Converter para WebP (WhatsApp prefere WebP para ícones de bandeja em alguns casos)
|
|
334
|
+
await new Promise((resolve, reject) => {
|
|
335
|
+
ffmpeg(outputPath)
|
|
336
|
+
.on('error', reject)
|
|
337
|
+
.on('end', () => resolve())
|
|
338
|
+
.toFormat('webp')
|
|
339
|
+
.save(webpOutputPath);
|
|
340
|
+
});
|
|
341
|
+
this.createdFiles.push(outputPath, webpOutputPath);
|
|
342
|
+
return webpOutputPath;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Cria uma figurinha de texto do zero (TP).
|
|
346
|
+
*/
|
|
347
|
+
async createTextSticker(text, options = {}) {
|
|
348
|
+
const timestamp = Date.now();
|
|
349
|
+
const outputPath = path.join(this.tempDir, `text_${timestamp}.png`);
|
|
350
|
+
// Criar uma imagem transparente 512x512
|
|
351
|
+
const image = new Jimp(512, 512, options.background || 0x00000000);
|
|
352
|
+
const fontPath = options.color === 'BLACK' ? Jimp.FONT_SANS_64_BLACK : Jimp.FONT_SANS_64_WHITE;
|
|
353
|
+
const font = await Jimp.loadFont(fontPath);
|
|
354
|
+
// Centralizar o texto
|
|
355
|
+
image.print(font, 0, 0, {
|
|
356
|
+
text,
|
|
357
|
+
alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER,
|
|
358
|
+
alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE
|
|
359
|
+
}, 512, 512);
|
|
360
|
+
await image.writeAsync(outputPath);
|
|
361
|
+
// Processar como uma figurinha normal para adicionar metadados e converter para WebP
|
|
362
|
+
const engine = new StickEngine(this.options);
|
|
363
|
+
const results = await engine.addFile(outputPath).start();
|
|
364
|
+
const finalPath = results[0].value;
|
|
365
|
+
this.createdFiles.push(outputPath, finalPath);
|
|
366
|
+
return finalPath;
|
|
367
|
+
}
|
|
257
368
|
}
|
|
258
|
-
|
|
259
|
-
export default StickEngine;
|
|
369
|
+
export default StickEngine;
|