@boruto_vk7/stickengine 0.0.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,369 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import axios from 'axios';
4
- import { exec } from 'child_process';
5
- import { EventEmitter } from 'events';
6
- import ffmpeg from 'fluent-ffmpeg';
7
- // @ts-ignore
8
- import webpMux from 'node-webpmux';
9
- // @ts-ignore
10
- import Jimp from 'jimp';
11
- // @ts-ignore
12
- import { removeBackgroundFromImageFile } from 'remove.bg';
13
- import { fileURLToPath } from 'url';
14
- // Correção para ESM
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = path.dirname(__filename);
17
- /**
18
- * Faz GET em uma URL e retorna o corpo como Buffer.
19
- */
20
- export const getBuffer = async (url, options = {}) => {
21
- try {
22
- const { data } = await axios({
23
- method: 'get',
24
- url,
25
- headers: {
26
- 'user-agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36',
27
- 'DNT': '1',
28
- 'Upgrade-Insecure-Request': '1',
29
- },
30
- ...options,
31
- responseType: 'arraybuffer',
32
- });
33
- return Buffer.from(data);
34
- }
35
- catch (err) {
36
- console.error(`Erro em getBuffer: ${err}`);
37
- const errorImagePath = path.join(__dirname, 'emror.jpg');
38
- if (fs.existsSync(errorImagePath)) {
39
- return fs.readFileSync(errorImagePath);
40
- }
41
- return Buffer.alloc(0);
42
- }
43
- };
44
- export class StickEngine extends EventEmitter {
45
- options;
46
- tempDir;
47
- queue;
48
- createdFiles;
49
- constructor(options = {}) {
50
- super();
51
- this.options = {
52
- webp: true,
53
- edit: false,
54
- convert: false,
55
- transparent: false,
56
- autoClean: true,
57
- fps: 15,
58
- quality: 80,
59
- metadata: {
60
- pack: 'StickEngine',
61
- author: 'Borutovk7 & Eduh Dev',
62
- emojis: ['🥶']
63
- },
64
- ...options
65
- };
66
- this.tempDir = path.join(process.cwd(), 'tmp_stickengine');
67
- if (!fs.existsSync(this.tempDir)) {
68
- fs.mkdirSync(this.tempDir, { recursive: true });
69
- }
70
- this.queue = [];
71
- this.createdFiles = [];
72
- }
73
- addFile(file) {
74
- this.queue.push(file);
75
- return this;
76
- }
77
- clean() {
78
- for (const file of this.createdFiles) {
79
- try {
80
- if (fs.existsSync(file)) {
81
- fs.unlinkSync(file);
82
- }
83
- }
84
- catch (err) {
85
- console.error(`Erro ao deletar arquivo temporário ${file}:`, err);
86
- }
87
- }
88
- this.createdFiles = [];
89
- }
90
- async processFile(input, index) {
91
- let filePath = '';
92
- let extension = '';
93
- const timestamp = Date.now();
94
- const localCreatedFiles = [];
95
- try {
96
- this.emit('st.start', { index, input });
97
- const ft = await import('file-type');
98
- const fileTypeFunc = ft.fromBuffer || ft.fileTypeFromBuffer || (ft.default && (ft.default.fromBuffer || ft.default.fileTypeFromBuffer));
99
- if (Buffer.isBuffer(input)) {
100
- const type = await fileTypeFunc(input);
101
- extension = type ? type.ext : 'bin';
102
- filePath = path.join(this.tempDir, `input_${timestamp}_${index}.${extension}`);
103
- fs.writeFileSync(filePath, input);
104
- localCreatedFiles.push(filePath);
105
- }
106
- else if (typeof input === 'string' && input.startsWith('http')) {
107
- const buffer = await getBuffer(input);
108
- const type = await fileTypeFunc(buffer);
109
- extension = type ? type.ext : 'bin';
110
- filePath = path.join(this.tempDir, `input_${timestamp}_${index}.${extension}`);
111
- fs.writeFileSync(filePath, buffer);
112
- localCreatedFiles.push(filePath);
113
- }
114
- else if (typeof input === 'string' && fs.existsSync(input)) {
115
- filePath = input;
116
- }
117
- else {
118
- throw new Error('Tipo de entrada inválido ou arquivo não encontrado.');
119
- }
120
- const mediaInfo = await this.getMediaInfo(filePath);
121
- this.emit('st.info', { index, ...mediaInfo });
122
- let currentFile = filePath;
123
- const isAnimated = mediaInfo.duration > 0 || extension === 'gif' || extension === 'mp4' || extension === 'webp';
124
- // Aplicar Edições Jimp (Apenas para Imagens Estáticas por enquanto)
125
- if (this.options.edit && !isAnimated) {
126
- const editedPath = path.join(this.tempDir, `edited_${timestamp}_${index}.png`);
127
- await this.applyJimpEdits(currentFile, editedPath, this.options.edit);
128
- localCreatedFiles.push(editedPath);
129
- currentFile = editedPath;
130
- }
131
- // Remoção de fundo (Apenas para Imagens Estáticas)
132
- if (this.options.transparent && !isAnimated) {
133
- const apiKey = Array.isArray(this.options.transparent)
134
- ? this.options.transparent[Math.floor(Math.random() * this.options.transparent.length)]
135
- : this.options.transparent;
136
- if (typeof apiKey === 'string') {
137
- const bgRemovedPath = path.join(this.tempDir, `nobg_${timestamp}_${index}.png`);
138
- const result = await removeBackgroundFromImageFile({
139
- path: currentFile,
140
- apiKey,
141
- size: 'auto',
142
- type: 'auto'
143
- });
144
- fs.writeFileSync(bgRemovedPath, Buffer.from(result.base64img, 'base64'));
145
- localCreatedFiles.push(bgRemovedPath);
146
- currentFile = bgRemovedPath;
147
- }
148
- }
149
- // Conversão para WebP (Suporta Estático e Animado)
150
- const webpPath = path.join(this.tempDir, `sticker_${timestamp}_${index}.webp`);
151
- await this.convertToWebp(currentFile, webpPath, isAnimated);
152
- localCreatedFiles.push(webpPath);
153
- currentFile = webpPath;
154
- // Adição de Metadados (Exif)
155
- if (this.options.metadata) {
156
- await this.addExif(currentFile, this.options.metadata);
157
- }
158
- this.createdFiles.push(...localCreatedFiles);
159
- this.emit('st.data', { index, file: currentFile });
160
- return currentFile;
161
- }
162
- catch (error) {
163
- this.emit('st.error', { index, error: error.message });
164
- for (const f of localCreatedFiles) {
165
- if (fs.existsSync(f))
166
- fs.unlinkSync(f);
167
- }
168
- throw error;
169
- }
170
- }
171
- async applyJimpEdits(input, output, options) {
172
- const image = await Jimp.read(input);
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);
189
- if (options.text) {
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 = {
193
- text: options.text.content,
194
- alignmentX: options.text.alignmentX || Jimp.HORIZONTAL_ALIGN_CENTER,
195
- alignmentY: options.text.alignmentY || Jimp.VERTICAL_ALIGN_BOTTOM
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);
210
- }
211
- await image.writeAsync(output);
212
- }
213
- getMediaInfo(file) {
214
- return new Promise((resolve) => {
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 });
218
- const parts = stdout.trim().split('x');
219
- resolve({
220
- width: parseInt(parts[0]) || 0,
221
- height: parseInt(parts[1]) || 0,
222
- duration: parseFloat(parts[2]) || 0
223
- });
224
- });
225
- });
226
- }
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);
249
- });
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
- }
269
- }
270
- async addExif(webpPath, metadata) {
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
- }
292
- }
293
- async start() {
294
- if (this.queue.length === 0)
295
- throw new Error('Nenhum arquivo na fila.');
296
- const results = await Promise.allSettled(this.queue.map((file, i) => this.processFile(file, i)));
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
- }
302
- return results;
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
- }
368
- }
369
- export default StickEngine;