@boruto_vk7/stickengine 1.1.1 → 2.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/README.md +152 -219
- package/dist/cjs/StickEngine.js +508 -325
- package/dist/cjs/StickEngine.js.map +1 -1
- package/dist/cjs/compat.js +9 -0
- package/dist/cjs/compat.js.map +1 -0
- package/dist/cjs/index.js +28 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/logo.png +0 -0
- package/dist/cjs/package.json +1 -1
- package/dist/esm/StickEngine.js +470 -327
- package/dist/esm/StickEngine.js.map +1 -1
- package/dist/esm/compat.js +2 -0
- package/dist/esm/compat.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logo.png +0 -0
- package/dist/types/StickEngine.d.ts +31 -71
- package/dist/types/StickEngine.d.ts.map +1 -1
- package/dist/types/compat.d.ts +2 -0
- package/dist/types/compat.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/logo.png +0 -0
- package/package.json +20 -38
- package/LICENSE +0 -21
package/dist/esm/StickEngine.js
CHANGED
|
@@ -1,84 +1,137 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
4
5
|
import { execFile } from 'node:child_process';
|
|
5
6
|
import { EventEmitter } from 'node:events';
|
|
7
|
+
import Crypto from 'node:crypto';
|
|
6
8
|
import axios from 'axios';
|
|
7
9
|
import ffmpeg from 'fluent-ffmpeg';
|
|
8
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
9
|
-
// @ts-ignore
|
|
10
10
|
import webpMux from 'node-webpmux';
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
12
|
-
// @ts-ignore
|
|
13
11
|
import Jimp from 'jimp';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
let ffmpegPath = null;
|
|
16
|
+
let ffprobePath = null;
|
|
17
|
+
try {
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const installer = require('@ffmpeg-installer/ffmpeg');
|
|
20
|
+
if (installer?.path) {
|
|
21
|
+
ffmpegPath = installer.path;
|
|
22
|
+
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
try {
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const installer = require('@ffprobe-installer/ffprobe');
|
|
28
|
+
if (installer?.path) {
|
|
29
|
+
ffprobePath = installer.path;
|
|
30
|
+
ffmpeg.setFfprobePath(ffprobePath);
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
20
33
|
export async function getBuffer(url, options = {}) {
|
|
21
34
|
try {
|
|
22
35
|
const { data } = await axios({
|
|
23
36
|
method: 'get',
|
|
24
37
|
url,
|
|
25
38
|
headers: {
|
|
26
|
-
'user-agent': 'Mozilla/5.0 (Windows NT
|
|
27
|
-
DNT:
|
|
39
|
+
'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',
|
|
40
|
+
'DNT': 1,
|
|
41
|
+
'Upgrade-Insecure-Request': 1,
|
|
42
|
+
...(options.headers || {})
|
|
28
43
|
},
|
|
29
44
|
...options,
|
|
30
45
|
responseType: 'arraybuffer',
|
|
31
|
-
timeout: 45000
|
|
46
|
+
timeout: options.timeout || 45000
|
|
32
47
|
});
|
|
33
|
-
return Buffer.from(data);
|
|
48
|
+
return Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
34
49
|
}
|
|
35
50
|
catch (err) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
const candidates = [
|
|
52
|
+
path.join(moduleDir, 'emror.jpg'),
|
|
53
|
+
path.join(moduleDir, '../src/emror.jpg'),
|
|
54
|
+
path.join(moduleDir, 'logo.png'),
|
|
55
|
+
path.join(process.cwd(), 'src/emror.jpg'),
|
|
56
|
+
path.join(process.cwd(), 'dist/cjs/emror.jpg'),
|
|
57
|
+
path.join(process.cwd(), 'dist/esm/emror.jpg'),
|
|
58
|
+
path.join(os.tmpdir(), 'emror.jpg'),
|
|
59
|
+
path.resolve('./src/emror.jpg')
|
|
60
|
+
];
|
|
61
|
+
for (const p of candidates) {
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(p))
|
|
64
|
+
return fs.readFileSync(p);
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
}
|
|
68
|
+
return Buffer.alloc(0);
|
|
40
69
|
}
|
|
41
70
|
}
|
|
42
71
|
function resolveWebpImage() {
|
|
43
|
-
const Image = webpMux.Image || webpMux.default?.Image;
|
|
72
|
+
const Image = webpMux.Image || webpMux.default?.Image || webpMux.default;
|
|
44
73
|
if (!Image)
|
|
45
74
|
throw new Error('node-webpmux Image class not found');
|
|
46
75
|
return Image;
|
|
47
76
|
}
|
|
48
77
|
async function fileTypeFromBuf(buf) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
try {
|
|
79
|
+
const ft = await import('file-type');
|
|
80
|
+
const fn = ft.fileTypeFromBuffer || ft.fromBuffer || ft.default?.fileTypeFromBuffer || ft.default?.fromBuffer;
|
|
81
|
+
if (!fn)
|
|
82
|
+
return undefined;
|
|
83
|
+
return await fn(buf);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
try {
|
|
87
|
+
const ft = eval("require")('file-type');
|
|
88
|
+
const fn = ft.fileTypeFromBuffer || ft.fromBuffer || ft.default?.fileTypeFromBuffer || ft.default?.fromBuffer;
|
|
89
|
+
if (!fn)
|
|
90
|
+
return undefined;
|
|
91
|
+
return await fn(buf);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
58
97
|
}
|
|
59
98
|
function isHttp(s) {
|
|
60
99
|
return /^https?:\/\//i.test(s);
|
|
61
100
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
function isDataURI(s) {
|
|
102
|
+
return /^data:.*?\/.*?;base64,/i.test(s);
|
|
103
|
+
}
|
|
104
|
+
function parseDataURItoBuffer(dataURI) {
|
|
105
|
+
const base64 = dataURI.split(',')[1];
|
|
106
|
+
return Buffer.from(base64, 'base64');
|
|
107
|
+
}
|
|
108
|
+
function normalizeMetadata(options = {}) {
|
|
109
|
+
const meta = options.metadata || options;
|
|
110
|
+
const pack = meta.packname || meta.pack || options.packname || options.pack || 'Lagos Solutions';
|
|
111
|
+
const author = meta.author || options.author || 'StickEngine';
|
|
112
|
+
const emojis = meta.categories || meta.emojis || options.categories || options.emojis || ['✨'];
|
|
113
|
+
const id = meta.id || options.id;
|
|
114
|
+
return { pack, author, emojis, id };
|
|
115
|
+
}
|
|
116
|
+
function resolveTempDir(options) {
|
|
117
|
+
const dir = options.tempDir || options.dirTemp || options.Dirtemp || options.DirTemp || options.dirtemp || options.TempDir || path.join(os.tmpdir(), 'stickengine');
|
|
118
|
+
return path.resolve(dir);
|
|
119
|
+
}
|
|
120
|
+
function resolveWatermark(options) {
|
|
121
|
+
const wm = options.watermark ?? options.marcaDagua ?? options.marca_agua ?? options.MarcaDaAgua ?? options.marcaDaAgua ?? false;
|
|
122
|
+
return wm;
|
|
123
|
+
}
|
|
77
124
|
export class StickEngine extends EventEmitter {
|
|
125
|
+
options;
|
|
126
|
+
tempDir;
|
|
127
|
+
queue = [];
|
|
128
|
+
createdFiles = [];
|
|
78
129
|
constructor(options = {}) {
|
|
79
130
|
super();
|
|
80
|
-
|
|
81
|
-
this.
|
|
131
|
+
const normalizedMeta = normalizeMetadata(options);
|
|
132
|
+
this.tempDir = resolveTempDir(options);
|
|
133
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
134
|
+
const watermarkValue = resolveWatermark(options);
|
|
82
135
|
this.options = {
|
|
83
136
|
webp: true,
|
|
84
137
|
edit: false,
|
|
@@ -87,45 +140,45 @@ export class StickEngine extends EventEmitter {
|
|
|
87
140
|
fps: 15,
|
|
88
141
|
quality: 75,
|
|
89
142
|
maxDuration: 6,
|
|
90
|
-
fit: '
|
|
91
|
-
|
|
92
|
-
pack: 'StickEngine',
|
|
93
|
-
author: 'Borutovk7',
|
|
94
|
-
emojis: ['✨'],
|
|
95
|
-
},
|
|
143
|
+
fit: 'natural',
|
|
144
|
+
watermark: watermarkValue,
|
|
96
145
|
...options,
|
|
146
|
+
tempDir: this.tempDir,
|
|
147
|
+
dirTemp: this.tempDir,
|
|
148
|
+
metadata: {
|
|
149
|
+
pack: normalizedMeta.pack,
|
|
150
|
+
author: normalizedMeta.author,
|
|
151
|
+
emojis: normalizedMeta.emojis,
|
|
152
|
+
id: normalizedMeta.id,
|
|
153
|
+
...(options.metadata || {})
|
|
154
|
+
}
|
|
97
155
|
};
|
|
98
|
-
|
|
99
|
-
|
|
156
|
+
if (options.packname)
|
|
157
|
+
this.options.metadata.pack = options.packname;
|
|
158
|
+
if (options.author)
|
|
159
|
+
this.options.metadata.author = options.author;
|
|
160
|
+
if (options.categories)
|
|
161
|
+
this.options.metadata.emojis = options.categories;
|
|
162
|
+
const wm = resolveWatermark(options);
|
|
163
|
+
if (wm !== undefined)
|
|
164
|
+
this.options.watermark = wm;
|
|
100
165
|
}
|
|
101
|
-
// ── One-shot API (simplest) ──────────────────────────────────────────────
|
|
102
|
-
/**
|
|
103
|
-
* Create a sticker in one call. Returns path + buffer + base64.
|
|
104
|
-
*/
|
|
105
166
|
static async create(input, options = {}) {
|
|
106
167
|
const engine = new StickEngine({ ...options, autoClean: false });
|
|
107
|
-
|
|
108
|
-
return await engine.build(input);
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
// keep result file; drop other temps if any
|
|
112
|
-
}
|
|
168
|
+
return await engine.build(input);
|
|
113
169
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Create sticker and return only the Buffer (handy for bots).
|
|
116
|
-
*/
|
|
117
170
|
static async toBuffer(input, options = {}) {
|
|
118
171
|
const r = await StickEngine.create(input, options);
|
|
119
172
|
return r.buffer;
|
|
120
173
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Create sticker and return base64 string.
|
|
123
|
-
*/
|
|
124
174
|
static async toBase64String(input, options = {}) {
|
|
125
175
|
const r = await StickEngine.create(input, options);
|
|
126
176
|
return r.base64;
|
|
127
177
|
}
|
|
128
|
-
|
|
178
|
+
static async toFile(input, options = {}) {
|
|
179
|
+
const r = await StickEngine.create(input, options);
|
|
180
|
+
return r.path;
|
|
181
|
+
}
|
|
129
182
|
async build(input) {
|
|
130
183
|
const filePath = await this.processFile(input, 0);
|
|
131
184
|
const buffer = await fsp.readFile(filePath);
|
|
@@ -138,12 +191,11 @@ export class StickEngine extends EventEmitter {
|
|
|
138
191
|
animated,
|
|
139
192
|
width: info.width || 512,
|
|
140
193
|
height: info.height || 512,
|
|
141
|
-
size: buffer.length
|
|
194
|
+
size: buffer.length
|
|
142
195
|
};
|
|
143
196
|
this.emit('st.done', result);
|
|
144
197
|
return result;
|
|
145
198
|
}
|
|
146
|
-
// ── Queue API (batch) ────────────────────────────────────────────────────
|
|
147
199
|
addFile(file) {
|
|
148
200
|
this.queue.push(file);
|
|
149
201
|
return this;
|
|
@@ -152,81 +204,59 @@ export class StickEngine extends EventEmitter {
|
|
|
152
204
|
this.queue.push(...files);
|
|
153
205
|
return this;
|
|
154
206
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Process the queue. Returns settled results with `value` = absolute path.
|
|
157
|
-
* Prefer `build()` / `StickEngine.create()` for a simpler API.
|
|
158
|
-
*/
|
|
159
207
|
async start() {
|
|
160
208
|
if (!this.queue.length)
|
|
161
|
-
throw new Error('Queue is empty
|
|
162
|
-
const jobs = this.queue.map((
|
|
209
|
+
throw new Error('Queue is empty');
|
|
210
|
+
const jobs = this.queue.map((f, i) => this.processFile(f, i));
|
|
163
211
|
this.queue = [];
|
|
164
212
|
return Promise.allSettled(jobs);
|
|
165
213
|
}
|
|
166
|
-
/** Delete tracked temp files. */
|
|
167
214
|
clean() {
|
|
168
215
|
for (const file of this.createdFiles) {
|
|
169
216
|
try {
|
|
170
217
|
if (fs.existsSync(file))
|
|
171
218
|
fs.unlinkSync(file);
|
|
172
219
|
}
|
|
173
|
-
catch {
|
|
174
|
-
/* ignore */
|
|
175
|
-
}
|
|
220
|
+
catch { }
|
|
176
221
|
}
|
|
177
222
|
this.createdFiles = [];
|
|
178
223
|
}
|
|
179
|
-
// ── Core pipeline ────────────────────────────────────────────────────────
|
|
180
224
|
track(file) {
|
|
181
225
|
this.createdFiles.push(file);
|
|
182
226
|
return file;
|
|
183
227
|
}
|
|
184
228
|
async processFile(input, index) {
|
|
185
|
-
const ts = `${Date.now()}_${index}_${
|
|
229
|
+
const ts = `${Date.now()}_${index}_${Crypto.randomBytes(3).toString('hex')}`;
|
|
186
230
|
const local = [];
|
|
187
231
|
try {
|
|
188
|
-
this.emit('st.start', { index, input: typeof input === 'string' ? input : '<buffer>' });
|
|
232
|
+
this.emit('st.start', { index, input: typeof input === 'string' ? (input.length > 100 ? input.slice(0, 100) + '...' : input) : '<buffer>' });
|
|
189
233
|
const { filePath, extension } = await this.materializeInput(input, ts);
|
|
190
|
-
|
|
234
|
+
if (filePath.includes(this.tempDir))
|
|
235
|
+
local.push(filePath);
|
|
191
236
|
const mediaInfo = await this.getMediaInfo(filePath);
|
|
192
237
|
this.emit('st.info', { index, ...mediaInfo, extension });
|
|
193
238
|
let current = filePath;
|
|
194
|
-
let animated = mediaInfo.duration > 0.05 ||
|
|
195
|
-
['gif', 'mp4', 'webm', 'mov', 'mkv', 'avi'].includes(extension) ||
|
|
196
|
-
(extension === 'webp' && (await this.detectAnimatedWebp(filePath)));
|
|
197
|
-
// Static edits only
|
|
239
|
+
let animated = mediaInfo.duration > 0.05 || ['gif', 'mp4', 'webm', 'mov', 'mkv', 'avi'].includes(extension) || (extension === 'webp' && await this.detectAnimatedWebp(filePath));
|
|
198
240
|
if (this.options.edit && !animated) {
|
|
199
241
|
const edited = path.join(this.tempDir, `edited_${ts}.png`);
|
|
200
242
|
await this.applyJimpEdits(current, edited, this.options.edit);
|
|
201
243
|
local.push(edited);
|
|
202
244
|
current = edited;
|
|
203
245
|
}
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const out = path.join(this.tempDir, `nobg_${ts}.png`);
|
|
211
|
-
const result = await removeBackgroundFromImageFile({
|
|
212
|
-
path: current,
|
|
213
|
-
apiKey: key,
|
|
214
|
-
size: 'auto',
|
|
215
|
-
type: 'auto',
|
|
216
|
-
});
|
|
217
|
-
fs.writeFileSync(out, Buffer.from(result.base64img, 'base64'));
|
|
218
|
-
local.push(out);
|
|
219
|
-
current = out;
|
|
220
|
-
}
|
|
246
|
+
const watermark = this.options.watermark;
|
|
247
|
+
if (watermark && !animated) {
|
|
248
|
+
const watermarked = path.join(this.tempDir, `wm_${ts}.png`);
|
|
249
|
+
await this.applyWatermark(current, watermarked, watermark);
|
|
250
|
+
local.push(watermarked);
|
|
251
|
+
current = watermarked;
|
|
221
252
|
}
|
|
222
253
|
const webpPath = path.join(this.tempDir, `sticker_${ts}.webp`);
|
|
223
254
|
await this.convertToWebp(current, webpPath, animated);
|
|
224
255
|
local.push(webpPath);
|
|
225
256
|
current = webpPath;
|
|
226
|
-
// Re-check after ffmpeg (source of truth)
|
|
227
257
|
animated = await this.detectAnimatedWebp(current);
|
|
228
|
-
if (this.options.metadata
|
|
229
|
-
await this.addExif(current, this.options.metadata
|
|
258
|
+
if (this.options.metadata) {
|
|
259
|
+
await this.addExif(current, this.options.metadata);
|
|
230
260
|
}
|
|
231
261
|
for (const f of local)
|
|
232
262
|
this.track(f);
|
|
@@ -240,89 +270,93 @@ export class StickEngine extends EventEmitter {
|
|
|
240
270
|
if (fs.existsSync(f))
|
|
241
271
|
fs.unlinkSync(f);
|
|
242
272
|
}
|
|
243
|
-
catch {
|
|
244
|
-
/* ignore */
|
|
245
|
-
}
|
|
273
|
+
catch { }
|
|
246
274
|
}
|
|
247
275
|
throw error;
|
|
248
276
|
}
|
|
249
277
|
}
|
|
250
278
|
async materializeInput(input, ts) {
|
|
251
279
|
if (Buffer.isBuffer(input)) {
|
|
280
|
+
if (input.length === 0)
|
|
281
|
+
throw new Error('Buffer vazio');
|
|
252
282
|
const type = await fileTypeFromBuf(input);
|
|
253
|
-
const extension = type?.ext || '
|
|
283
|
+
const extension = type?.ext || 'png';
|
|
254
284
|
const filePath = path.join(this.tempDir, `input_${ts}.${extension}`);
|
|
255
285
|
fs.writeFileSync(filePath, input);
|
|
256
286
|
return { filePath, extension };
|
|
257
287
|
}
|
|
258
|
-
if (typeof input === 'string'
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
288
|
+
if (typeof input === 'string') {
|
|
289
|
+
if (isDataURI(input)) {
|
|
290
|
+
const buffer = parseDataURItoBuffer(input);
|
|
291
|
+
if (buffer.length === 0)
|
|
292
|
+
throw new Error('Base64 inválido');
|
|
293
|
+
const type = await fileTypeFromBuf(buffer);
|
|
294
|
+
const extension = type?.ext || 'png';
|
|
295
|
+
const filePath = path.join(this.tempDir, `input_${ts}.${extension}`);
|
|
296
|
+
fs.writeFileSync(filePath, buffer);
|
|
297
|
+
return { filePath, extension };
|
|
298
|
+
}
|
|
299
|
+
if (isHttp(input)) {
|
|
300
|
+
const buffer = await getBuffer(input);
|
|
301
|
+
if (buffer.length === 0)
|
|
302
|
+
throw new Error('Falha ao baixar');
|
|
303
|
+
const type = await fileTypeFromBuf(buffer);
|
|
304
|
+
const extension = type?.ext || path.extname(new URL(input).pathname).replace('.', '') || 'bin';
|
|
305
|
+
const filePath = path.join(this.tempDir, `input_${ts}.${extension}`);
|
|
306
|
+
fs.writeFileSync(filePath, buffer);
|
|
307
|
+
return { filePath, extension: extension.toLowerCase() };
|
|
308
|
+
}
|
|
309
|
+
if (fs.existsSync(input)) {
|
|
310
|
+
const filePath = path.resolve(input);
|
|
311
|
+
const extension = path.extname(filePath).replace('.', '').toLowerCase() || 'bin';
|
|
312
|
+
return { filePath, extension };
|
|
313
|
+
}
|
|
314
|
+
if (/^[A-Za-z0-9+/=]+$/.test(input.slice(0, 100)) && input.length > 100) {
|
|
315
|
+
try {
|
|
316
|
+
const buffer = Buffer.from(input, 'base64');
|
|
317
|
+
if (buffer.length > 50) {
|
|
318
|
+
const type = await fileTypeFromBuf(buffer);
|
|
319
|
+
if (type) {
|
|
320
|
+
const filePath = path.join(this.tempDir, `input_${ts}.${type.ext}`);
|
|
321
|
+
fs.writeFileSync(filePath, buffer);
|
|
322
|
+
return { filePath, extension: type.ext };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch { }
|
|
327
|
+
}
|
|
328
|
+
throw new Error('Invalid input: ' + input);
|
|
270
329
|
}
|
|
271
|
-
throw new Error('Invalid input
|
|
330
|
+
throw new Error('Invalid input type');
|
|
272
331
|
}
|
|
273
|
-
// ── EXIF (fixes animated save error) ─────────────────────────────────────
|
|
274
|
-
/**
|
|
275
|
-
* Write WhatsApp sticker pack EXIF.
|
|
276
|
-
* Animated WebP MUST use muxAnim — save() throws on animations.
|
|
277
|
-
*/
|
|
278
332
|
async addExif(webpPath, metadata) {
|
|
279
333
|
const Image = resolveWebpImage();
|
|
280
334
|
const img = new Image();
|
|
281
335
|
const json = {
|
|
282
336
|
'sticker-pack-id': metadata.id || `stickengine-${Date.now()}`,
|
|
283
|
-
'sticker-pack-name': metadata.pack || '
|
|
284
|
-
'sticker-pack-publisher': metadata.author || '
|
|
285
|
-
emojis: metadata.emojis?.length ? metadata.emojis : ['✨']
|
|
337
|
+
'sticker-pack-name': metadata.pack || 'Lagos Solutions',
|
|
338
|
+
'sticker-pack-publisher': metadata.author || 'StickEngine',
|
|
339
|
+
'emojis': metadata.emojis?.length ? metadata.emojis : metadata.categories?.length ? metadata.categories : ['✨']
|
|
286
340
|
};
|
|
287
|
-
const exifAttr = Buffer.from([
|
|
288
|
-
0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00,
|
|
289
|
-
0x00, 0x00, 0x16, 0x00, 0x00, 0x00,
|
|
290
|
-
]);
|
|
341
|
+
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]);
|
|
291
342
|
const jsonBuff = Buffer.from(JSON.stringify(json), 'utf-8');
|
|
292
343
|
const exif = Buffer.concat([exifAttr, jsonBuff]);
|
|
293
344
|
exif.writeUIntLE(jsonBuff.length, 14, 4);
|
|
294
345
|
await img.load(webpPath);
|
|
295
346
|
img.exif = exif;
|
|
296
|
-
const frameCount = typeof img.frameCount === 'number'
|
|
297
|
-
? img.frameCount
|
|
298
|
-
: Array.isArray(img.frames)
|
|
299
|
-
? img.frames.length
|
|
300
|
-
: 0;
|
|
347
|
+
const frameCount = typeof img.frameCount === 'number' ? img.frameCount : Array.isArray(img.frames) ? img.frames.length : 0;
|
|
301
348
|
const animated = !!img.hasAnim || frameCount > 1;
|
|
302
|
-
/**
|
|
303
|
-
* node-webpmux compatibility layer
|
|
304
|
-
*
|
|
305
|
-
* v3.x (current):
|
|
306
|
-
* await img.save(path) // works for static AND animated (passes frames)
|
|
307
|
-
*
|
|
308
|
-
* v1.x / early forks:
|
|
309
|
-
* static → img.save(path)
|
|
310
|
-
* anim → img.muxAnim({ path, exif: true }) OR Image.muxAnim({ frames, exif, path })
|
|
311
|
-
* calling save() on anim throws:
|
|
312
|
-
* "Using `save` for animations is not currently supported. Use `muxAnim` instead"
|
|
313
|
-
*/
|
|
314
349
|
const saveV3 = async () => {
|
|
315
|
-
// v3 instance save supports both; pass exif explicitly when supported
|
|
316
350
|
return img.save(webpPath, {
|
|
317
351
|
exif: true,
|
|
318
352
|
frames: animated ? img.frames : undefined,
|
|
319
353
|
width: img.width,
|
|
320
354
|
height: img.height,
|
|
321
355
|
loops: img.anim?.loops ?? img.anim?.loopCount ?? 0,
|
|
322
|
-
bgColor: img.anim?.bgColor ?? img.anim?.backgroundColor ?? [0, 0, 0, 0]
|
|
356
|
+
bgColor: img.anim?.bgColor ?? img.anim?.backgroundColor ?? [0, 0, 0, 0]
|
|
323
357
|
});
|
|
324
358
|
};
|
|
325
|
-
const
|
|
359
|
+
const saveLegacy = async () => {
|
|
326
360
|
const opts = {
|
|
327
361
|
path: webpPath,
|
|
328
362
|
frames: img.frames || [],
|
|
@@ -330,22 +364,12 @@ export class StickEngine extends EventEmitter {
|
|
|
330
364
|
height: img.height || 512,
|
|
331
365
|
loops: img.anim?.loopCount ?? img.anim?.loops ?? 0,
|
|
332
366
|
bgColor: img.anim?.backgroundColor ?? img.anim?.bgColor ?? [0, 0, 0, 0],
|
|
333
|
-
exif
|
|
367
|
+
exif
|
|
334
368
|
};
|
|
335
|
-
if (typeof img.muxAnim === 'function')
|
|
336
|
-
return img.muxAnim({
|
|
337
|
-
|
|
338
|
-
exif: true,
|
|
339
|
-
width: opts.width,
|
|
340
|
-
height: opts.height,
|
|
341
|
-
loops: opts.loops,
|
|
342
|
-
bgColor: opts.bgColor,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
if (typeof Image.muxAnim === 'function') {
|
|
369
|
+
if (typeof img.muxAnim === 'function')
|
|
370
|
+
return img.muxAnim({ path: webpPath, exif: true, width: opts.width, height: opts.height, loops: opts.loops, bgColor: opts.bgColor });
|
|
371
|
+
if (typeof Image.muxAnim === 'function')
|
|
346
372
|
return Image.muxAnim(opts);
|
|
347
|
-
}
|
|
348
|
-
// absolute last resort: try plain save (v3)
|
|
349
373
|
return img.save(webpPath);
|
|
350
374
|
};
|
|
351
375
|
try {
|
|
@@ -355,41 +379,31 @@ export class StickEngine extends EventEmitter {
|
|
|
355
379
|
}
|
|
356
380
|
catch (err) {
|
|
357
381
|
const msg = String(err?.message || err);
|
|
358
|
-
if (/muxAnim|
|
|
359
|
-
await
|
|
360
|
-
}
|
|
382
|
+
if (/muxAnim|not currently supported/i.test(msg))
|
|
383
|
+
await saveLegacy();
|
|
361
384
|
else if (animated) {
|
|
362
|
-
// some builds want save without second arg
|
|
363
385
|
try {
|
|
364
386
|
await img.save(webpPath);
|
|
365
387
|
}
|
|
366
|
-
catch (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
else {
|
|
372
|
-
throw err2;
|
|
373
|
-
}
|
|
388
|
+
catch (e2) {
|
|
389
|
+
if (/muxAnim|not currently supported/i.test(String(e2?.message)))
|
|
390
|
+
await saveLegacy();
|
|
391
|
+
else
|
|
392
|
+
throw e2;
|
|
374
393
|
}
|
|
375
394
|
}
|
|
376
|
-
else
|
|
395
|
+
else
|
|
377
396
|
throw err;
|
|
378
|
-
}
|
|
379
397
|
}
|
|
380
398
|
}
|
|
381
|
-
else if (typeof Image.save === 'function')
|
|
382
|
-
// very old static save(path, image)
|
|
399
|
+
else if (typeof Image.save === 'function')
|
|
383
400
|
await Image.save(webpPath, img);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
await saveLegacyMuxAnim();
|
|
387
|
-
}
|
|
401
|
+
else
|
|
402
|
+
await saveLegacy();
|
|
388
403
|
}
|
|
389
404
|
catch (err) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
await saveLegacyMuxAnim();
|
|
405
|
+
if (/muxAnim|not currently supported/i.test(String(err?.message))) {
|
|
406
|
+
await saveLegacy();
|
|
393
407
|
return;
|
|
394
408
|
}
|
|
395
409
|
throw err;
|
|
@@ -402,90 +416,51 @@ export class StickEngine extends EventEmitter {
|
|
|
402
416
|
const Image = resolveWebpImage();
|
|
403
417
|
const img = new Image();
|
|
404
418
|
await img.load(filePath);
|
|
405
|
-
return
|
|
406
|
-
(typeof img.frameCount === 'number' && img.frameCount > 1) ||
|
|
407
|
-
(Array.isArray(img.frames) && img.frames.length > 1));
|
|
419
|
+
return !!img.hasAnim || (typeof img.frameCount === 'number' && img.frameCount > 1) || (Array.isArray(img.frames) && img.frames.length > 1);
|
|
408
420
|
}
|
|
409
421
|
catch {
|
|
410
422
|
return false;
|
|
411
423
|
}
|
|
412
424
|
}
|
|
413
|
-
// ── FFmpeg / Jimp ────────────────────────────────────────────────────────
|
|
414
|
-
/**
|
|
415
|
-
* Build FFmpeg -vf chain for a clean 512 sticker canvas.
|
|
416
|
-
* Critical: `format=rgba` BEFORE pad, and pad color must be fully transparent
|
|
417
|
-
* (`black@0`). Using `color=0x00000000` after an opaque format yields black bars.
|
|
418
|
-
*/
|
|
419
425
|
buildScaleFilter(isAnimated, fps) {
|
|
420
|
-
const fit = this.options.fit || '
|
|
426
|
+
const fit = this.options.fit || 'natural';
|
|
421
427
|
const parts = ['format=rgba'];
|
|
422
|
-
if (isAnimated)
|
|
428
|
+
if (isAnimated)
|
|
423
429
|
parts.push(`fps=${Math.max(1, fps)}`);
|
|
424
|
-
}
|
|
425
430
|
switch (fit) {
|
|
431
|
+
case 'cover':
|
|
432
|
+
parts.push('scale=512:512:force_original_aspect_ratio=increase:flags=lanczos', 'crop=512:512:(iw-ow)/2:(ih-oh)/2');
|
|
433
|
+
break;
|
|
426
434
|
case 'contain':
|
|
427
|
-
|
|
428
|
-
parts.push('scale=512:512:force_original_aspect_ratio=decrease:flags=lanczos',
|
|
429
|
-
// black@0 = transparent; must come after format=rgba
|
|
430
|
-
'pad=512:512:(ow-iw)/2:(oh-ih)/2:color=black@0');
|
|
435
|
+
parts.push('scale=512:512:force_original_aspect_ratio=decrease:flags=lanczos', 'pad=512:512:(ow-iw)/2:(oh-ih)/2:color=black@0');
|
|
431
436
|
break;
|
|
432
437
|
case 'fill':
|
|
433
|
-
// Stretch (rarely wanted)
|
|
434
438
|
parts.push('scale=512:512:flags=lanczos');
|
|
435
439
|
break;
|
|
436
440
|
case 'none':
|
|
437
|
-
// Only shrink if larger than 512 on any side; keep original ratio; no pad
|
|
438
441
|
parts.push("scale='min(512,iw)':'min(512,ih)':force_original_aspect_ratio=decrease:flags=lanczos");
|
|
439
442
|
break;
|
|
440
|
-
case 'cover':
|
|
441
443
|
default:
|
|
442
|
-
|
|
443
|
-
// scale up to cover 512×512, then center-crop (no empty margins)
|
|
444
|
-
parts.push('scale=512:512:force_original_aspect_ratio=increase:flags=lanczos', 'crop=512:512:(iw-ow)/2:(ih-oh)/2');
|
|
444
|
+
parts.push("scale='if(gt(iw,ih),512,-2)':'if(gt(ih,iw),512,-2)':flags=lanczos");
|
|
445
445
|
break;
|
|
446
446
|
}
|
|
447
|
-
parts.push('setsar=1', 'format=rgba');
|
|
447
|
+
parts.push("scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos", 'setsar=1', 'format=rgba');
|
|
448
448
|
return parts.join(',');
|
|
449
449
|
}
|
|
450
450
|
async convertToWebp(input, output, isAnimated) {
|
|
451
451
|
const run = (quality, fps) => new Promise((resolve, reject) => {
|
|
452
|
-
const ff = ffmpeg(input).on('error', reject).on('end', () => resolve());
|
|
453
452
|
const vf = this.buildScaleFilter(isAnimated, fps);
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
'-
|
|
458
|
-
|
|
459
|
-
'-
|
|
460
|
-
|
|
461
|
-
'-lossless',
|
|
462
|
-
isAnimated ? '0' : '0',
|
|
463
|
-
'-compression_level',
|
|
464
|
-
'6',
|
|
465
|
-
'-q:v',
|
|
466
|
-
String(Math.max(1, Math.min(100, quality))),
|
|
467
|
-
'-loop',
|
|
468
|
-
'0',
|
|
469
|
-
'-preset',
|
|
470
|
-
'default',
|
|
471
|
-
'-an',
|
|
472
|
-
'-vsync',
|
|
473
|
-
'0',
|
|
474
|
-
];
|
|
475
|
-
// Prefer alpha-capable webp
|
|
476
|
-
if (!isAnimated) {
|
|
477
|
-
// slight quality bump for static; still lossy keeps alpha reliable across ffmpeg builds
|
|
478
|
-
opts.push('-quality', String(Math.max(1, Math.min(100, quality))));
|
|
479
|
-
}
|
|
480
|
-
if (isAnimated) {
|
|
481
|
-
opts.push('-t', String(this.options.maxDuration || 6));
|
|
482
|
-
}
|
|
483
|
-
ff.addOutputOptions(opts).toFormat('webp').save(output);
|
|
453
|
+
const q = Math.max(1, Math.min(100, quality));
|
|
454
|
+
const opts = ['-vcodec', 'libwebp', '-vf', vf, '-loop', '0', '-an', '-vsync', '0', '-preset', 'default'];
|
|
455
|
+
if (isAnimated)
|
|
456
|
+
opts.push('-lossless', '0', '-compression_level', '6', '-q:v', String(q), '-quality', String(q), '-t', String(this.options.maxDuration || 6));
|
|
457
|
+
else
|
|
458
|
+
opts.push('-lossless', q >= 90 ? '1' : '0', '-compression_level', '6', '-q:v', String(q), '-quality', String(q));
|
|
459
|
+
ffmpeg(input).on('error', reject).on('end', () => resolve()).addOutputOptions(opts).toFormat('webp').save(output);
|
|
484
460
|
});
|
|
485
|
-
let quality = this.options.quality ?? 80;
|
|
461
|
+
let quality = this.options.quality ?? (isAnimated ? 80 : 95);
|
|
486
462
|
let fps = this.options.fps ?? 15;
|
|
487
463
|
await run(quality, fps);
|
|
488
|
-
// WhatsApp animated stickers should stay under ~1MB
|
|
489
464
|
if (isAnimated && fs.existsSync(output)) {
|
|
490
465
|
let attempts = 0;
|
|
491
466
|
while (fs.statSync(output).size > 1000000 && attempts < 4) {
|
|
@@ -495,9 +470,8 @@ export class StickEngine extends EventEmitter {
|
|
|
495
470
|
attempts++;
|
|
496
471
|
}
|
|
497
472
|
}
|
|
498
|
-
if (!fs.existsSync(output) || fs.statSync(output).size < 20)
|
|
499
|
-
throw new Error('FFmpeg failed
|
|
500
|
-
}
|
|
473
|
+
if (!fs.existsSync(output) || fs.statSync(output).size < 20)
|
|
474
|
+
throw new Error('FFmpeg failed');
|
|
501
475
|
}
|
|
502
476
|
async applyJimpEdits(input, output, options) {
|
|
503
477
|
const image = await Jimp.read(input);
|
|
@@ -520,98 +494,267 @@ export class StickEngine extends EventEmitter {
|
|
|
520
494
|
if (options.text?.content) {
|
|
521
495
|
const fontPath = options.text.color === 'BLACK' ? Jimp.FONT_SANS_32_BLACK : Jimp.FONT_SANS_32_WHITE;
|
|
522
496
|
const font = await Jimp.loadFont(fontPath);
|
|
523
|
-
const printOptions = {
|
|
524
|
-
text: options.text.content,
|
|
525
|
-
alignmentX: options.text.alignmentX || Jimp.HORIZONTAL_ALIGN_CENTER,
|
|
526
|
-
alignmentY: options.text.alignmentY || Jimp.VERTICAL_ALIGN_BOTTOM,
|
|
527
|
-
};
|
|
497
|
+
const printOptions = { text: options.text.content, alignmentX: options.text.alignmentX || Jimp.HORIZONTAL_ALIGN_CENTER, alignmentY: options.text.alignmentY || Jimp.VERTICAL_ALIGN_BOTTOM };
|
|
528
498
|
if (options.text.stroke) {
|
|
529
499
|
const strokeFont = await Jimp.loadFont(options.text.color === 'BLACK' ? Jimp.FONT_SANS_32_WHITE : Jimp.FONT_SANS_32_BLACK);
|
|
530
|
-
for (let x = -2; x <= 2; x++)
|
|
531
|
-
for (let y = -2; y <= 2; y++)
|
|
532
|
-
if (x !== 0 || y !== 0)
|
|
500
|
+
for (let x = -2; x <= 2; x++)
|
|
501
|
+
for (let y = -2; y <= 2; y++)
|
|
502
|
+
if (x !== 0 || y !== 0)
|
|
533
503
|
image.print(strokeFont, x, y, printOptions, image.bitmap.width, image.bitmap.height);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
504
|
}
|
|
538
505
|
image.print(font, 0, 0, printOptions, image.bitmap.width, image.bitmap.height);
|
|
539
506
|
}
|
|
540
507
|
await image.writeAsync(output);
|
|
541
508
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
'
|
|
546
|
-
'
|
|
547
|
-
'
|
|
548
|
-
'
|
|
549
|
-
'
|
|
550
|
-
|
|
551
|
-
'
|
|
552
|
-
'
|
|
553
|
-
file,
|
|
509
|
+
async applyWatermark(input, output, watermark) {
|
|
510
|
+
const findLogo = () => {
|
|
511
|
+
const candidates = [
|
|
512
|
+
path.join(moduleDir, 'logo.png'),
|
|
513
|
+
path.join(moduleDir, '../src/logo.png'),
|
|
514
|
+
path.join(moduleDir, '../logo.png'),
|
|
515
|
+
path.join(process.cwd(), 'logo.png'),
|
|
516
|
+
path.join(process.cwd(), 'src/logo.png'),
|
|
517
|
+
path.join(process.cwd(), 'dist/cjs/logo.png'),
|
|
518
|
+
path.join(process.cwd(), 'dist/esm/logo.png'),
|
|
519
|
+
path.resolve('./logo.png')
|
|
554
520
|
];
|
|
555
|
-
|
|
521
|
+
for (const p of candidates) {
|
|
522
|
+
try {
|
|
523
|
+
if (fs.existsSync(p) && fs.statSync(p).size > 100)
|
|
524
|
+
return p;
|
|
525
|
+
}
|
|
526
|
+
catch { }
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
};
|
|
530
|
+
let isImageWatermark = false;
|
|
531
|
+
let logoPath = null;
|
|
532
|
+
let text = 'Lagos Solutions';
|
|
533
|
+
let position = 'bottom-right';
|
|
534
|
+
let margin = 10;
|
|
535
|
+
let sizePercent = 0.25;
|
|
536
|
+
if (watermark === true) {
|
|
537
|
+
isImageWatermark = true;
|
|
538
|
+
logoPath = findLogo();
|
|
539
|
+
}
|
|
540
|
+
else if (typeof watermark === 'string') {
|
|
541
|
+
const possiblePath = path.resolve(watermark);
|
|
542
|
+
if ((watermark.endsWith('.png') || watermark.endsWith('.jpg') || watermark.endsWith('.jpeg') || watermark.endsWith('.webp')) && fs.existsSync(possiblePath)) {
|
|
543
|
+
isImageWatermark = true;
|
|
544
|
+
logoPath = possiblePath;
|
|
545
|
+
}
|
|
546
|
+
else if (fs.existsSync(watermark)) {
|
|
547
|
+
isImageWatermark = true;
|
|
548
|
+
logoPath = watermark;
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
text = watermark;
|
|
552
|
+
isImageWatermark = false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else if (typeof watermark === 'object') {
|
|
556
|
+
text = watermark.text || text;
|
|
557
|
+
position = watermark.position || position;
|
|
558
|
+
margin = watermark.margin ?? margin;
|
|
559
|
+
sizePercent = watermark.size ?? watermark.sizePercent ?? sizePercent;
|
|
560
|
+
if (watermark.image || watermark.logo || watermark.path) {
|
|
561
|
+
isImageWatermark = true;
|
|
562
|
+
logoPath = watermark.image || watermark.logo || watermark.path;
|
|
563
|
+
if (logoPath && !fs.existsSync(logoPath)) {
|
|
564
|
+
const resolved = path.resolve(logoPath);
|
|
565
|
+
if (fs.existsSync(resolved))
|
|
566
|
+
logoPath = resolved;
|
|
567
|
+
else
|
|
568
|
+
logoPath = findLogo();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else if (watermark.text) {
|
|
572
|
+
isImageWatermark = false;
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
isImageWatermark = true;
|
|
576
|
+
logoPath = findLogo();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (isImageWatermark) {
|
|
580
|
+
if (!logoPath)
|
|
581
|
+
logoPath = findLogo();
|
|
582
|
+
if (!logoPath || !fs.existsSync(logoPath)) {
|
|
583
|
+
fs.copyFileSync(input, output);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const main = await Jimp.read(input);
|
|
588
|
+
const logo = await Jimp.read(logoPath);
|
|
589
|
+
const mainWidth = main.bitmap.width;
|
|
590
|
+
const mainHeight = main.bitmap.height;
|
|
591
|
+
const logoTargetWidth = Math.floor(mainWidth * sizePercent);
|
|
592
|
+
logo.resize(logoTargetWidth, Jimp.AUTO);
|
|
593
|
+
if (typeof watermark === 'object' && watermark.opacity !== undefined) {
|
|
594
|
+
logo.opacity(watermark.opacity);
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
logo.opacity(0.85);
|
|
598
|
+
}
|
|
599
|
+
let x = mainWidth - logo.bitmap.width - margin;
|
|
600
|
+
let y = mainHeight - logo.bitmap.height - margin;
|
|
601
|
+
if (position === 'bottom-left') {
|
|
602
|
+
x = margin;
|
|
603
|
+
y = mainHeight - logo.bitmap.height - margin;
|
|
604
|
+
}
|
|
605
|
+
if (position === 'top-right') {
|
|
606
|
+
x = mainWidth - logo.bitmap.width - margin;
|
|
607
|
+
y = margin;
|
|
608
|
+
}
|
|
609
|
+
if (position === 'top-left') {
|
|
610
|
+
x = margin;
|
|
611
|
+
y = margin;
|
|
612
|
+
}
|
|
613
|
+
if (position === 'center') {
|
|
614
|
+
x = (mainWidth - logo.bitmap.width) / 2;
|
|
615
|
+
y = (mainHeight - logo.bitmap.height) / 2;
|
|
616
|
+
}
|
|
617
|
+
main.composite(logo, x, y, { mode: Jimp.BLEND_SOURCE_OVER, opacitySource: 1, opacityDest: 1 });
|
|
618
|
+
await main.writeAsync(output);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
fs.copyFileSync(input, output);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (!text) {
|
|
627
|
+
fs.copyFileSync(input, output);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const image = await Jimp.read(input);
|
|
631
|
+
const font = await Jimp.loadFont(Jimp.FONT_SANS_16_WHITE);
|
|
632
|
+
const textWidth = Jimp.measureText(font, text);
|
|
633
|
+
const textHeight = Jimp.measureTextHeight(font, text, image.bitmap.width);
|
|
634
|
+
let x = image.bitmap.width - textWidth - margin;
|
|
635
|
+
let y = image.bitmap.height - textHeight - margin;
|
|
636
|
+
if (position === 'bottom-left') {
|
|
637
|
+
x = margin;
|
|
638
|
+
y = image.bitmap.height - textHeight - margin;
|
|
639
|
+
}
|
|
640
|
+
if (position === 'top-right') {
|
|
641
|
+
x = image.bitmap.width - textWidth - margin;
|
|
642
|
+
y = margin;
|
|
643
|
+
}
|
|
644
|
+
if (position === 'top-left') {
|
|
645
|
+
x = margin;
|
|
646
|
+
y = margin;
|
|
647
|
+
}
|
|
648
|
+
if (position === 'center') {
|
|
649
|
+
x = (image.bitmap.width - textWidth) / 2;
|
|
650
|
+
y = (image.bitmap.height - textHeight) / 2;
|
|
651
|
+
}
|
|
652
|
+
const bg = new Jimp(textWidth + 10, textHeight + 6, 0x00000088);
|
|
653
|
+
image.composite(bg, x - 5, y - 3);
|
|
654
|
+
image.print(font, x, y, text);
|
|
655
|
+
await image.writeAsync(output);
|
|
656
|
+
}
|
|
657
|
+
getMediaInfo(file) {
|
|
658
|
+
const probePath = ffprobePath || 'ffprobe';
|
|
659
|
+
return new Promise(resolve => {
|
|
660
|
+
execFile(probePath, ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height,duration', '-of', 'csv=p=0:s=x', file], { timeout: 20000 }, (err, stdout) => {
|
|
556
661
|
if (err)
|
|
557
662
|
return resolve({ width: 0, height: 0, duration: 0 });
|
|
558
663
|
const parts = String(stdout).trim().split('x');
|
|
559
|
-
resolve({
|
|
560
|
-
width: parseInt(parts[0], 10) || 0,
|
|
561
|
-
height: parseInt(parts[1], 10) || 0,
|
|
562
|
-
duration: parseFloat(parts[2]) || 0,
|
|
563
|
-
});
|
|
664
|
+
resolve({ width: parseInt(parts[0], 10) || 0, height: parseInt(parts[1], 10) || 0, duration: parseFloat(parts[2]) || 0 });
|
|
564
665
|
});
|
|
565
666
|
});
|
|
566
667
|
}
|
|
567
|
-
// ── Extras ───────────────────────────────────────────────────────────────
|
|
568
668
|
static fileToBase64(filePath) {
|
|
569
669
|
return fs.readFileSync(filePath).toString('base64');
|
|
570
670
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
671
|
+
}
|
|
672
|
+
export async function sendImageAsSticker(conn, jid, input, quoted, options = {}) {
|
|
673
|
+
const normalized = normalizeMetadata(options);
|
|
674
|
+
const engine = new StickEngine({
|
|
675
|
+
metadata: { pack: normalized.pack, author: normalized.author, emojis: normalized.emojis, id: normalized.id },
|
|
676
|
+
fit: options.fit || 'natural',
|
|
677
|
+
quality: options.quality ?? 95,
|
|
678
|
+
tempDir: resolveTempDir(options),
|
|
679
|
+
watermark: resolveWatermark(options),
|
|
680
|
+
...options
|
|
681
|
+
});
|
|
682
|
+
let finalInput = input;
|
|
683
|
+
if (typeof input === 'string') {
|
|
684
|
+
if (isDataURI(input))
|
|
685
|
+
finalInput = parseDataURItoBuffer(input);
|
|
686
|
+
else if (isHttp(input))
|
|
687
|
+
finalInput = await getBuffer(input);
|
|
688
|
+
}
|
|
689
|
+
const result = await engine.build(finalInput);
|
|
690
|
+
await conn.sendMessage(jid, { sticker: { url: result.buffer }, ...options }, { quoted });
|
|
691
|
+
return result.buffer;
|
|
692
|
+
}
|
|
693
|
+
export async function sendVideoAsSticker(conn, jid, input, quoted, options = {}) {
|
|
694
|
+
const normalized = normalizeMetadata(options);
|
|
695
|
+
const engine = new StickEngine({
|
|
696
|
+
metadata: { pack: normalized.pack, author: normalized.author, emojis: normalized.emojis, id: normalized.id },
|
|
697
|
+
fit: options.fit || 'natural',
|
|
698
|
+
fps: options.fps || 15,
|
|
699
|
+
maxDuration: options.maxDuration || 6,
|
|
700
|
+
quality: options.quality ?? 80,
|
|
701
|
+
tempDir: resolveTempDir(options),
|
|
702
|
+
watermark: resolveWatermark(options),
|
|
703
|
+
...options
|
|
704
|
+
});
|
|
705
|
+
let finalInput = input;
|
|
706
|
+
if (typeof input === 'string') {
|
|
707
|
+
if (isDataURI(input))
|
|
708
|
+
finalInput = parseDataURItoBuffer(input);
|
|
709
|
+
else if (isHttp(input))
|
|
710
|
+
finalInput = await getBuffer(input);
|
|
598
711
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
712
|
+
const result = await engine.build(finalInput);
|
|
713
|
+
await conn.sendMessage(jid, { sticker: { url: result.buffer }, ...options }, { quoted });
|
|
714
|
+
return result.buffer;
|
|
715
|
+
}
|
|
716
|
+
export const sendImageAsSticker2 = sendImageAsSticker;
|
|
717
|
+
export const sendVideoAsSticker2 = sendVideoAsSticker;
|
|
718
|
+
export async function imageToWebp(media) {
|
|
719
|
+
const engine = new StickEngine({ fit: 'natural', quality: 95 });
|
|
720
|
+
const ts = `${Date.now()}_${Crypto.randomBytes(3).toString('hex')}`;
|
|
721
|
+
const filePath = path.join(engine.tempDir, `img_${ts}.png`);
|
|
722
|
+
fs.writeFileSync(filePath, media);
|
|
723
|
+
const out = path.join(engine.tempDir, `out_${ts}.webp`);
|
|
724
|
+
await engine.convertToWebp(filePath, out, false);
|
|
725
|
+
const buf = fs.readFileSync(out);
|
|
726
|
+
try {
|
|
727
|
+
fs.unlinkSync(filePath);
|
|
728
|
+
fs.unlinkSync(out);
|
|
614
729
|
}
|
|
730
|
+
catch { }
|
|
731
|
+
return buf;
|
|
732
|
+
}
|
|
733
|
+
export async function videoToWebp(media) {
|
|
734
|
+
const engine = new StickEngine({ fit: 'natural', fps: 15, maxDuration: 5 });
|
|
735
|
+
const ts = `${Date.now()}_${Crypto.randomBytes(3).toString('hex')}`;
|
|
736
|
+
const filePath = path.join(engine.tempDir, `vid_${ts}.mp4`);
|
|
737
|
+
fs.writeFileSync(filePath, media);
|
|
738
|
+
const out = path.join(engine.tempDir, `out_${ts}.webp`);
|
|
739
|
+
await engine.convertToWebp(filePath, out, true);
|
|
740
|
+
const buf = fs.readFileSync(out);
|
|
741
|
+
try {
|
|
742
|
+
fs.unlinkSync(filePath);
|
|
743
|
+
fs.unlinkSync(out);
|
|
744
|
+
}
|
|
745
|
+
catch { }
|
|
746
|
+
return buf;
|
|
747
|
+
}
|
|
748
|
+
export async function writeExifImg(media, metadata) {
|
|
749
|
+
const result = await StickEngine.create(media, { metadata: { pack: metadata.packname || metadata.pack, author: metadata.author, emojis: metadata.categories || metadata.emojis } });
|
|
750
|
+
return result.path;
|
|
751
|
+
}
|
|
752
|
+
export async function writeExifVid(media, metadata) {
|
|
753
|
+
return writeExifImg(media, metadata);
|
|
615
754
|
}
|
|
755
|
+
export const imageToWebp2 = imageToWebp;
|
|
756
|
+
export const videoToWebp2 = videoToWebp;
|
|
757
|
+
export const writeExifImg2 = writeExifImg;
|
|
758
|
+
export const writeExifVid2 = writeExifVid;
|
|
616
759
|
export default StickEngine;
|
|
617
760
|
//# sourceMappingURL=StickEngine.js.map
|