@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.
- package/LICENSE +18 -12
- package/README.md +162 -83
- package/dist/cjs/StickEngine.js +616 -0
- package/dist/cjs/StickEngine.js.map +1 -0
- package/dist/cjs/emror.jpg +0 -0
- package/dist/cjs/index.js +9 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/esm/StickEngine.js +576 -0
- package/dist/esm/StickEngine.js.map +1 -0
- package/dist/esm/emror.jpg +0 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/StickEngine.d.ts +137 -0
- package/dist/types/StickEngine.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +55 -32
- package/assets/preview_animated.png +0 -0
- package/assets/preview_sepia.png +0 -0
- package/dist/StickEngine.d.ts +0 -72
- package/dist/StickEngine.js +0 -369
package/dist/StickEngine.js
DELETED
|
@@ -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;
|