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