@boruto_vk7/stickengine 1.2.0 → 2.0.1
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 +102 -235
- package/dist/cjs/StickEngine.js +497 -315
- 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/logo_small.png +0 -0
- package/dist/cjs/package.json +1 -1
- package/dist/esm/StickEngine.js +459 -317
- 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/esm/logo_small.png +0 -0
- package/dist/types/StickEngine.d.ts +30 -73
- 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/logo_small.png +0 -0
- package/package.json +21 -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,
|
|
@@ -88,44 +141,44 @@ export class StickEngine extends EventEmitter {
|
|
|
88
141
|
quality: 75,
|
|
89
142
|
maxDuration: 6,
|
|
90
143
|
fit: 'natural',
|
|
91
|
-
|
|
92
|
-
pack: 'StickEngine',
|
|
93
|
-
author: 'Borutovk7',
|
|
94
|
-
emojis: ['✨'],
|
|
95
|
-
},
|
|
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,91 +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.
|
|
416
|
-
*
|
|
417
|
-
* Normal WhatsApp stickers are NOT forced square:
|
|
418
|
-
* the longest side becomes 512 and the other side stays proportional
|
|
419
|
-
* (e.g. 512×320 or 280×512) with real alpha — same look as hand-made stickers.
|
|
420
|
-
*/
|
|
421
425
|
buildScaleFilter(isAnimated, fps) {
|
|
422
426
|
const fit = this.options.fit || 'natural';
|
|
423
427
|
const parts = ['format=rgba'];
|
|
424
|
-
if (isAnimated)
|
|
428
|
+
if (isAnimated)
|
|
425
429
|
parts.push(`fps=${Math.max(1, fps)}`);
|
|
426
|
-
}
|
|
427
430
|
switch (fit) {
|
|
428
431
|
case 'cover':
|
|
429
|
-
// Force square sticker: fill 512×512, center-crop overflow
|
|
430
432
|
parts.push('scale=512:512:force_original_aspect_ratio=increase:flags=lanczos', 'crop=512:512:(iw-ow)/2:(ih-oh)/2');
|
|
431
433
|
break;
|
|
432
434
|
case 'contain':
|
|
433
|
-
// Full image inside a square 512×512 with transparent pad (no black bars)
|
|
434
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');
|
|
435
436
|
break;
|
|
436
437
|
case 'fill':
|
|
437
438
|
parts.push('scale=512:512:flags=lanczos');
|
|
438
439
|
break;
|
|
439
440
|
case 'none':
|
|
440
|
-
// Shrink only if larger than 512; never upscale; keep ratio
|
|
441
441
|
parts.push("scale='min(512,iw)':'min(512,ih)':force_original_aspect_ratio=decrease:flags=lanczos");
|
|
442
442
|
break;
|
|
443
|
-
case 'natural':
|
|
444
443
|
default:
|
|
445
|
-
|
|
446
|
-
// longest side = 512, other side proportional, NO pad, NO crop
|
|
447
|
-
// Example: 1080×1920 → 288×512 | 1920×1080 → 512×288
|
|
448
|
-
parts.push(
|
|
449
|
-
// -2 keeps aspect and forces even dims (friendlier for some encoders)
|
|
450
|
-
"scale='if(gt(iw,ih),512,-2)':'if(gt(ih,iw),512,-2)':flags=lanczos");
|
|
444
|
+
parts.push("scale='if(gt(iw,ih),512,-2)':'if(gt(ih,iw),512,-2)':flags=lanczos");
|
|
451
445
|
break;
|
|
452
446
|
}
|
|
453
|
-
// Ensure even width/height (webp/yuv safety) without changing aspect much
|
|
454
447
|
parts.push("scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos", 'setsar=1', 'format=rgba');
|
|
455
448
|
return parts.join(',');
|
|
456
449
|
}
|
|
457
450
|
async convertToWebp(input, output, isAnimated) {
|
|
458
451
|
const run = (quality, fps) => new Promise((resolve, reject) => {
|
|
459
|
-
const ff = ffmpeg(input).on('error', reject).on('end', () => resolve());
|
|
460
452
|
const vf = this.buildScaleFilter(isAnimated, fps);
|
|
461
453
|
const q = Math.max(1, Math.min(100, quality));
|
|
462
|
-
const opts = [
|
|
463
|
-
|
|
464
|
-
'libwebp',
|
|
465
|
-
'-vf',
|
|
466
|
-
vf,
|
|
467
|
-
'-loop',
|
|
468
|
-
'0',
|
|
469
|
-
'-an',
|
|
470
|
-
'-vsync',
|
|
471
|
-
'0',
|
|
472
|
-
'-preset',
|
|
473
|
-
'default',
|
|
474
|
-
];
|
|
475
|
-
if (isAnimated) {
|
|
476
|
-
// Animated: lossy + size budget
|
|
454
|
+
const opts = ['-vcodec', 'libwebp', '-vf', vf, '-loop', '0', '-an', '-vsync', '0', '-preset', 'default'];
|
|
455
|
+
if (isAnimated)
|
|
477
456
|
opts.push('-lossless', '0', '-compression_level', '6', '-q:v', String(q), '-quality', String(q), '-t', String(this.options.maxDuration || 6));
|
|
478
|
-
|
|
479
|
-
else {
|
|
480
|
-
// Static anime/art stickers look much better near-lossless
|
|
457
|
+
else
|
|
481
458
|
opts.push('-lossless', q >= 90 ? '1' : '0', '-compression_level', '6', '-q:v', String(q), '-quality', String(q));
|
|
482
|
-
|
|
483
|
-
ff.addOutputOptions(opts).toFormat('webp').save(output);
|
|
459
|
+
ffmpeg(input).on('error', reject).on('end', () => resolve()).addOutputOptions(opts).toFormat('webp').save(output);
|
|
484
460
|
});
|
|
485
|
-
// Static defaults higher quality; animated stays flexible for 1MB cap
|
|
486
461
|
let quality = this.options.quality ?? (isAnimated ? 80 : 95);
|
|
487
462
|
let fps = this.options.fps ?? 15;
|
|
488
463
|
await run(quality, fps);
|
|
489
|
-
// WhatsApp animated stickers should stay under ~1MB
|
|
490
464
|
if (isAnimated && fs.existsSync(output)) {
|
|
491
465
|
let attempts = 0;
|
|
492
466
|
while (fs.statSync(output).size > 1000000 && attempts < 4) {
|
|
@@ -496,9 +470,8 @@ export class StickEngine extends EventEmitter {
|
|
|
496
470
|
attempts++;
|
|
497
471
|
}
|
|
498
472
|
}
|
|
499
|
-
if (!fs.existsSync(output) || fs.statSync(output).size < 20)
|
|
500
|
-
throw new Error('FFmpeg failed
|
|
501
|
-
}
|
|
473
|
+
if (!fs.existsSync(output) || fs.statSync(output).size < 20)
|
|
474
|
+
throw new Error('FFmpeg failed');
|
|
502
475
|
}
|
|
503
476
|
async applyJimpEdits(input, output, options) {
|
|
504
477
|
const image = await Jimp.read(input);
|
|
@@ -521,98 +494,267 @@ export class StickEngine extends EventEmitter {
|
|
|
521
494
|
if (options.text?.content) {
|
|
522
495
|
const fontPath = options.text.color === 'BLACK' ? Jimp.FONT_SANS_32_BLACK : Jimp.FONT_SANS_32_WHITE;
|
|
523
496
|
const font = await Jimp.loadFont(fontPath);
|
|
524
|
-
const printOptions = {
|
|
525
|
-
text: options.text.content,
|
|
526
|
-
alignmentX: options.text.alignmentX || Jimp.HORIZONTAL_ALIGN_CENTER,
|
|
527
|
-
alignmentY: options.text.alignmentY || Jimp.VERTICAL_ALIGN_BOTTOM,
|
|
528
|
-
};
|
|
497
|
+
const printOptions = { text: options.text.content, alignmentX: options.text.alignmentX || Jimp.HORIZONTAL_ALIGN_CENTER, alignmentY: options.text.alignmentY || Jimp.VERTICAL_ALIGN_BOTTOM };
|
|
529
498
|
if (options.text.stroke) {
|
|
530
499
|
const strokeFont = await Jimp.loadFont(options.text.color === 'BLACK' ? Jimp.FONT_SANS_32_WHITE : Jimp.FONT_SANS_32_BLACK);
|
|
531
|
-
for (let x = -2; x <= 2; x++)
|
|
532
|
-
for (let y = -2; y <= 2; y++)
|
|
533
|
-
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)
|
|
534
503
|
image.print(strokeFont, x, y, printOptions, image.bitmap.width, image.bitmap.height);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
504
|
}
|
|
539
505
|
image.print(font, 0, 0, printOptions, image.bitmap.width, image.bitmap.height);
|
|
540
506
|
}
|
|
541
507
|
await image.writeAsync(output);
|
|
542
508
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
'
|
|
547
|
-
'
|
|
548
|
-
'
|
|
549
|
-
'
|
|
550
|
-
'
|
|
551
|
-
|
|
552
|
-
'
|
|
553
|
-
'
|
|
554
|
-
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')
|
|
555
520
|
];
|
|
556
|
-
|
|
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) => {
|
|
557
661
|
if (err)
|
|
558
662
|
return resolve({ width: 0, height: 0, duration: 0 });
|
|
559
663
|
const parts = String(stdout).trim().split('x');
|
|
560
|
-
resolve({
|
|
561
|
-
width: parseInt(parts[0], 10) || 0,
|
|
562
|
-
height: parseInt(parts[1], 10) || 0,
|
|
563
|
-
duration: parseFloat(parts[2]) || 0,
|
|
564
|
-
});
|
|
664
|
+
resolve({ width: parseInt(parts[0], 10) || 0, height: parseInt(parts[1], 10) || 0, duration: parseFloat(parts[2]) || 0 });
|
|
565
665
|
});
|
|
566
666
|
});
|
|
567
667
|
}
|
|
568
|
-
// ── Extras ───────────────────────────────────────────────────────────────
|
|
569
668
|
static fileToBase64(filePath) {
|
|
570
669
|
return fs.readFileSync(filePath).toString('base64');
|
|
571
670
|
}
|
|
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
|
-
|
|
598
|
-
|
|
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);
|
|
599
711
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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);
|
|
615
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);
|
|
616
754
|
}
|
|
755
|
+
export const imageToWebp2 = imageToWebp;
|
|
756
|
+
export const videoToWebp2 = videoToWebp;
|
|
757
|
+
export const writeExifImg2 = writeExifImg;
|
|
758
|
+
export const writeExifVid2 = writeExifVid;
|
|
617
759
|
export default StickEngine;
|
|
618
760
|
//# sourceMappingURL=StickEngine.js.map
|