@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/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,
|
|
@@ -127,44 +220,44 @@ class StickEngine extends node_events_1.EventEmitter {
|
|
|
127
220
|
quality: 75,
|
|
128
221
|
maxDuration: 6,
|
|
129
222
|
fit: 'natural',
|
|
130
|
-
|
|
131
|
-
pack: 'StickEngine',
|
|
132
|
-
author: 'Borutovk7',
|
|
133
|
-
emojis: ['✨'],
|
|
134
|
-
},
|
|
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,91 +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.
|
|
455
|
-
*
|
|
456
|
-
* Normal WhatsApp stickers are NOT forced square:
|
|
457
|
-
* the longest side becomes 512 and the other side stays proportional
|
|
458
|
-
* (e.g. 512×320 or 280×512) with real alpha — same look as hand-made stickers.
|
|
459
|
-
*/
|
|
460
504
|
buildScaleFilter(isAnimated, fps) {
|
|
461
505
|
const fit = this.options.fit || 'natural';
|
|
462
506
|
const parts = ['format=rgba'];
|
|
463
|
-
if (isAnimated)
|
|
507
|
+
if (isAnimated)
|
|
464
508
|
parts.push(`fps=${Math.max(1, fps)}`);
|
|
465
|
-
}
|
|
466
509
|
switch (fit) {
|
|
467
510
|
case 'cover':
|
|
468
|
-
// Force square sticker: fill 512×512, center-crop overflow
|
|
469
511
|
parts.push('scale=512:512:force_original_aspect_ratio=increase:flags=lanczos', 'crop=512:512:(iw-ow)/2:(ih-oh)/2');
|
|
470
512
|
break;
|
|
471
513
|
case 'contain':
|
|
472
|
-
// Full image inside a square 512×512 with transparent pad (no black bars)
|
|
473
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');
|
|
474
515
|
break;
|
|
475
516
|
case 'fill':
|
|
476
517
|
parts.push('scale=512:512:flags=lanczos');
|
|
477
518
|
break;
|
|
478
519
|
case 'none':
|
|
479
|
-
// Shrink only if larger than 512; never upscale; keep ratio
|
|
480
520
|
parts.push("scale='min(512,iw)':'min(512,ih)':force_original_aspect_ratio=decrease:flags=lanczos");
|
|
481
521
|
break;
|
|
482
|
-
case 'natural':
|
|
483
522
|
default:
|
|
484
|
-
|
|
485
|
-
// longest side = 512, other side proportional, NO pad, NO crop
|
|
486
|
-
// Example: 1080×1920 → 288×512 | 1920×1080 → 512×288
|
|
487
|
-
parts.push(
|
|
488
|
-
// -2 keeps aspect and forces even dims (friendlier for some encoders)
|
|
489
|
-
"scale='if(gt(iw,ih),512,-2)':'if(gt(ih,iw),512,-2)':flags=lanczos");
|
|
523
|
+
parts.push("scale='if(gt(iw,ih),512,-2)':'if(gt(ih,iw),512,-2)':flags=lanczos");
|
|
490
524
|
break;
|
|
491
525
|
}
|
|
492
|
-
// Ensure even width/height (webp/yuv safety) without changing aspect much
|
|
493
526
|
parts.push("scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos", 'setsar=1', 'format=rgba');
|
|
494
527
|
return parts.join(',');
|
|
495
528
|
}
|
|
496
529
|
async convertToWebp(input, output, isAnimated) {
|
|
497
530
|
const run = (quality, fps) => new Promise((resolve, reject) => {
|
|
498
|
-
const ff = (0, fluent_ffmpeg_1.default)(input).on('error', reject).on('end', () => resolve());
|
|
499
531
|
const vf = this.buildScaleFilter(isAnimated, fps);
|
|
500
532
|
const q = Math.max(1, Math.min(100, quality));
|
|
501
|
-
const opts = [
|
|
502
|
-
|
|
503
|
-
'libwebp',
|
|
504
|
-
'-vf',
|
|
505
|
-
vf,
|
|
506
|
-
'-loop',
|
|
507
|
-
'0',
|
|
508
|
-
'-an',
|
|
509
|
-
'-vsync',
|
|
510
|
-
'0',
|
|
511
|
-
'-preset',
|
|
512
|
-
'default',
|
|
513
|
-
];
|
|
514
|
-
if (isAnimated) {
|
|
515
|
-
// Animated: lossy + size budget
|
|
533
|
+
const opts = ['-vcodec', 'libwebp', '-vf', vf, '-loop', '0', '-an', '-vsync', '0', '-preset', 'default'];
|
|
534
|
+
if (isAnimated)
|
|
516
535
|
opts.push('-lossless', '0', '-compression_level', '6', '-q:v', String(q), '-quality', String(q), '-t', String(this.options.maxDuration || 6));
|
|
517
|
-
|
|
518
|
-
else {
|
|
519
|
-
// Static anime/art stickers look much better near-lossless
|
|
536
|
+
else
|
|
520
537
|
opts.push('-lossless', q >= 90 ? '1' : '0', '-compression_level', '6', '-q:v', String(q), '-quality', String(q));
|
|
521
|
-
|
|
522
|
-
ff.addOutputOptions(opts).toFormat('webp').save(output);
|
|
538
|
+
(0, fluent_ffmpeg_1.default)(input).on('error', reject).on('end', () => resolve()).addOutputOptions(opts).toFormat('webp').save(output);
|
|
523
539
|
});
|
|
524
|
-
// Static defaults higher quality; animated stays flexible for 1MB cap
|
|
525
540
|
let quality = this.options.quality ?? (isAnimated ? 80 : 95);
|
|
526
541
|
let fps = this.options.fps ?? 15;
|
|
527
542
|
await run(quality, fps);
|
|
528
|
-
// WhatsApp animated stickers should stay under ~1MB
|
|
529
543
|
if (isAnimated && node_fs_1.default.existsSync(output)) {
|
|
530
544
|
let attempts = 0;
|
|
531
545
|
while (node_fs_1.default.statSync(output).size > 1000000 && attempts < 4) {
|
|
@@ -535,9 +549,8 @@ class StickEngine extends node_events_1.EventEmitter {
|
|
|
535
549
|
attempts++;
|
|
536
550
|
}
|
|
537
551
|
}
|
|
538
|
-
if (!node_fs_1.default.existsSync(output) || node_fs_1.default.statSync(output).size < 20)
|
|
539
|
-
throw new Error('FFmpeg failed
|
|
540
|
-
}
|
|
552
|
+
if (!node_fs_1.default.existsSync(output) || node_fs_1.default.statSync(output).size < 20)
|
|
553
|
+
throw new Error('FFmpeg failed');
|
|
541
554
|
}
|
|
542
555
|
async applyJimpEdits(input, output, options) {
|
|
543
556
|
const image = await jimp_1.default.read(input);
|
|
@@ -560,99 +573,268 @@ class StickEngine extends node_events_1.EventEmitter {
|
|
|
560
573
|
if (options.text?.content) {
|
|
561
574
|
const fontPath = options.text.color === 'BLACK' ? jimp_1.default.FONT_SANS_32_BLACK : jimp_1.default.FONT_SANS_32_WHITE;
|
|
562
575
|
const font = await jimp_1.default.loadFont(fontPath);
|
|
563
|
-
const printOptions = {
|
|
564
|
-
text: options.text.content,
|
|
565
|
-
alignmentX: options.text.alignmentX || jimp_1.default.HORIZONTAL_ALIGN_CENTER,
|
|
566
|
-
alignmentY: options.text.alignmentY || jimp_1.default.VERTICAL_ALIGN_BOTTOM,
|
|
567
|
-
};
|
|
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 };
|
|
568
577
|
if (options.text.stroke) {
|
|
569
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);
|
|
570
|
-
for (let x = -2; x <= 2; x++)
|
|
571
|
-
for (let y = -2; y <= 2; y++)
|
|
572
|
-
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)
|
|
573
582
|
image.print(strokeFont, x, y, printOptions, image.bitmap.width, image.bitmap.height);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
583
|
}
|
|
578
584
|
image.print(font, 0, 0, printOptions, image.bitmap.width, image.bitmap.height);
|
|
579
585
|
}
|
|
580
586
|
await image.writeAsync(output);
|
|
581
587
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
'
|
|
586
|
-
'
|
|
587
|
-
'
|
|
588
|
-
'
|
|
589
|
-
'
|
|
590
|
-
|
|
591
|
-
'
|
|
592
|
-
'
|
|
593
|
-
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')
|
|
594
599
|
];
|
|
595
|
-
|
|
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) => {
|
|
596
740
|
if (err)
|
|
597
741
|
return resolve({ width: 0, height: 0, duration: 0 });
|
|
598
742
|
const parts = String(stdout).trim().split('x');
|
|
599
|
-
resolve({
|
|
600
|
-
width: parseInt(parts[0], 10) || 0,
|
|
601
|
-
height: parseInt(parts[1], 10) || 0,
|
|
602
|
-
duration: parseFloat(parts[2]) || 0,
|
|
603
|
-
});
|
|
743
|
+
resolve({ width: parseInt(parts[0], 10) || 0, height: parseInt(parts[1], 10) || 0, duration: parseFloat(parts[2]) || 0 });
|
|
604
744
|
});
|
|
605
745
|
});
|
|
606
746
|
}
|
|
607
|
-
// ── Extras ───────────────────────────────────────────────────────────────
|
|
608
747
|
static fileToBase64(filePath) {
|
|
609
748
|
return node_fs_1.default.readFileSync(filePath).toString('base64');
|
|
610
749
|
}
|
|
611
|
-
/** 96×96 tray icon WebP for sticker packs. */
|
|
612
|
-
async createTrayIcon(input) {
|
|
613
|
-
const ts = Date.now();
|
|
614
|
-
const pngPath = node_path_1.default.join(this.tempDir, `tray_${ts}.png`);
|
|
615
|
-
const webpPath = node_path_1.default.join(this.tempDir, `tray_${ts}.webp`);
|
|
616
|
-
let buffer;
|
|
617
|
-
if (Buffer.isBuffer(input))
|
|
618
|
-
buffer = input;
|
|
619
|
-
else if (typeof input === 'string' && isHttp(input))
|
|
620
|
-
buffer = await getBuffer(input);
|
|
621
|
-
else if (typeof input === 'string' && node_fs_1.default.existsSync(input))
|
|
622
|
-
buffer = node_fs_1.default.readFileSync(input);
|
|
623
|
-
else
|
|
624
|
-
throw new Error('Invalid tray icon input.');
|
|
625
|
-
const image = await jimp_1.default.read(buffer);
|
|
626
|
-
await image.cover(96, 96).writeAsync(pngPath);
|
|
627
|
-
await new Promise((resolve, reject) => {
|
|
628
|
-
(0, fluent_ffmpeg_1.default)(pngPath)
|
|
629
|
-
.on('error', reject)
|
|
630
|
-
.on('end', () => resolve())
|
|
631
|
-
.addOutputOptions(['-vcodec', 'libwebp', '-lossless', '1', '-q:v', '80'])
|
|
632
|
-
.toFormat('webp')
|
|
633
|
-
.save(webpPath);
|
|
634
|
-
});
|
|
635
|
-
this.track(pngPath);
|
|
636
|
-
this.track(webpPath);
|
|
637
|
-
return webpPath;
|
|
638
|
-
}
|
|
639
|
-
/** Text-only sticker (transparent 512×512). */
|
|
640
|
-
async createTextSticker(text, options = {}) {
|
|
641
|
-
const ts = Date.now();
|
|
642
|
-
const pngPath = node_path_1.default.join(this.tempDir, `text_${ts}.png`);
|
|
643
|
-
const image = new jimp_1.default(512, 512, options.background ?? 0x00000000);
|
|
644
|
-
const fontPath = options.color === 'BLACK' ? jimp_1.default.FONT_SANS_64_BLACK : jimp_1.default.FONT_SANS_64_WHITE;
|
|
645
|
-
const font = await jimp_1.default.loadFont(fontPath);
|
|
646
|
-
image.print(font, 0, 0, {
|
|
647
|
-
text,
|
|
648
|
-
alignmentX: jimp_1.default.HORIZONTAL_ALIGN_CENTER,
|
|
649
|
-
alignmentY: jimp_1.default.VERTICAL_ALIGN_MIDDLE,
|
|
650
|
-
}, 512, 512);
|
|
651
|
-
await image.writeAsync(pngPath);
|
|
652
|
-
this.track(pngPath);
|
|
653
|
-
return this.build(pngPath);
|
|
654
|
-
}
|
|
655
750
|
}
|
|
656
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;
|
|
657
839
|
exports.default = StickEngine;
|
|
658
840
|
//# sourceMappingURL=StickEngine.js.map
|