medias-fakerator 1.0.0-dra

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.
Files changed (37) hide show
  1. package/assets/fonts/Chirp-Regular.ttf +0 -0
  2. package/assets/fonts/Noto-Bold.ttf +0 -0
  3. package/assets/fonts/Noto-Emoji.ttf +0 -0
  4. package/assets/fonts/Noto-Regular.ttf +0 -0
  5. package/assets/fonts/SfProDisplay.ttf +0 -0
  6. package/assets/images/android/background-call.jpg +0 -0
  7. package/assets/images/google/google-lyrics.jpg +0 -0
  8. package/assets/images/google/google-search.jpg +0 -0
  9. package/assets/images/instagram/fakestory.png +0 -0
  10. package/assets/images/instagram/instagram-verified.png +0 -0
  11. package/assets/images/iphone/background-call.jpg +0 -0
  12. package/assets/images/iphone/background.jpg +0 -0
  13. package/assets/images/iphone/battery.png +0 -0
  14. package/assets/images/iphone/copy.png +0 -0
  15. package/assets/images/iphone/delete.png +0 -0
  16. package/assets/images/iphone/forward.png +0 -0
  17. package/assets/images/iphone/pin.png +0 -0
  18. package/assets/images/iphone/plus.png +0 -0
  19. package/assets/images/iphone/reply.png +0 -0
  20. package/assets/images/iphone/report.png +0 -0
  21. package/assets/images/iphone/signal.png +0 -0
  22. package/assets/images/iphone/star.png +0 -0
  23. package/assets/images/iphone/wifi.png +0 -0
  24. package/assets/images/pattern_02.png +0 -0
  25. package/assets/images/tiktok/tiktok-verified.png +0 -0
  26. package/assets/images/tweet/like.png +0 -0
  27. package/assets/images/tweet/other.png +0 -0
  28. package/assets/images/tweet/reply.png +0 -0
  29. package/assets/images/tweet/retweet.png +0 -0
  30. package/assets/images/tweet/share.png +0 -0
  31. package/assets/images/tweet/twitter-verified.png +0 -0
  32. package/assets/images/youtube/youtube-verified.png +0 -0
  33. package/index.js +39 -0
  34. package/lib/generators.js +3220 -0
  35. package/lib/quote-generator.js +1621 -0
  36. package/lib/utils.js +166 -0
  37. package/package.json +24 -0
@@ -0,0 +1,1621 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const runes = require("runes");
4
+ const sharp = require("sharp");
5
+ const EmojiDbLib = require("emoji-db");
6
+ const { LRUCache } = require("lru-cache");
7
+ const emojiDb = new EmojiDbLib({ useDefaultDb: true });
8
+ const emojiImageByBrandPromise = require("emoji-cache");
9
+ const ALLOWED_MEDIA_DIRECTORY = path.resolve(__dirname, "../");
10
+ const { createCanvas, loadImage, registerFont } = require("canvas");
11
+
12
+ async function loadFont() {
13
+ const fontsDir = path.join(__dirname, "../assets/fonts");
14
+ if (!fs.existsSync(fontsDir)) {
15
+ console.error(
16
+ `PENTING: Direktori font tidak ditemukan di '${path.resolve(fontsDir)}'.`
17
+ );
18
+ return;
19
+ }
20
+ try {
21
+ const files = await fs.promises.readdir(fontsDir);
22
+ if (!files || files.length === 0) {
23
+ console.error(
24
+ `Tidak ada font yang ditemukan di direktori '${path.resolve(
25
+ fontsDir
26
+ )}'.`
27
+ );
28
+ return;
29
+ }
30
+ for (const file of files) {
31
+ try {
32
+ registerFont(path.join(fontsDir, file), {
33
+ family: file.replace(/\.[^/.]+$/, ""),
34
+ });
35
+ } catch (error) {
36
+ console.error(`Gagal memuat font: ${path.join(fontsDir, file)}.`);
37
+ }
38
+ }
39
+ } catch (err) {
40
+ console.error("Gagal membaca direktori font:", err);
41
+ }
42
+ }
43
+
44
+ const fontsLoadedPromise = loadFont();
45
+ const avatarCache = new LRUCache({
46
+ max: 20,
47
+ ttl: 1000 * 60 * 5,
48
+ });
49
+
50
+ function _normalizeColor(color) {
51
+ const canvas = createCanvas(0, 0);
52
+ const canvasCtx = canvas.getContext("2d");
53
+ canvasCtx.fillStyle = color;
54
+ return canvasCtx.fillStyle;
55
+ }
56
+ function _colorLuminance(hex, lum) {
57
+ hex = String(hex).replace(/[^0-9a-f]/gi, "");
58
+ if (hex.length < 6) {
59
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
60
+ }
61
+ lum = lum || 0;
62
+ let rgb = "#",
63
+ c,
64
+ i;
65
+ for (i = 0; i < 3; i++) {
66
+ c = parseInt(hex.substr(i * 2, 2), 16);
67
+ c = Math.round(Math.min(Math.max(0, c + c * lum), 255)).toString(16);
68
+ rgb += ("00" + c).substr(c.length);
69
+ }
70
+ return rgb;
71
+ }
72
+ function _hexToRgb(hex) {
73
+ return hex
74
+ .replace(
75
+ /^#?([a-f\d])([a-f\d])([a-f\d])$/i,
76
+ (m, r, g, b) => "#" + r + r + g + g + b + b
77
+ )
78
+ .substring(1)
79
+ .match(/.{2}/g)
80
+ .map((x) => parseInt(x, 16));
81
+ }
82
+
83
+ class ColorContrast {
84
+ constructor() {
85
+ this.brightnessThreshold = 175;
86
+ }
87
+ hexToRgb(hex) {
88
+ return _hexToRgb(hex);
89
+ }
90
+ rgbToHex([r, g, b]) {
91
+ return `#${r.toString(16).padStart(2, "0")}${g
92
+ .toString(16)
93
+ .padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
94
+ }
95
+ getBrightness(color) {
96
+ const [r, g, b] = this.hexToRgb(color);
97
+ return (r * 299 + g * 587 + b * 114) / 1000;
98
+ }
99
+ adjustBrightness(color, amount) {
100
+ const [r, g, b] = this.hexToRgb(color);
101
+ const newR = Math.max(0, Math.min(255, r + amount));
102
+ const newG = Math.max(0, Math.min(255, g + amount));
103
+ const newB = Math.max(0, Math.min(255, b + amount));
104
+ return this.rgbToHex([newR, newG, newB]);
105
+ }
106
+ getContrastRatio(background, foreground) {
107
+ const brightness1 = this.getBrightness(background);
108
+ const brightness2 = this.getBrightness(foreground);
109
+ const lightest = Math.max(brightness1, brightness2);
110
+ const darkest = Math.min(brightness1, brightness2);
111
+ return (lightest + 0.05) / (darkest + 0.05);
112
+ }
113
+ adjustContrast(background, foreground) {
114
+ const contrastRatio = this.getContrastRatio(background, foreground);
115
+ const brightnessDiff =
116
+ this.getBrightness(background) - this.getBrightness(foreground);
117
+ if (contrastRatio >= 4.5) {
118
+ return foreground;
119
+ } else if (brightnessDiff >= 0) {
120
+ const amount = Math.ceil(
121
+ (this.brightnessThreshold - this.getBrightness(foreground)) / 2
122
+ );
123
+ return this.adjustBrightness(foreground, amount);
124
+ } else {
125
+ const amount = Math.ceil(
126
+ (this.getBrightness(foreground) - this.brightnessThreshold) / 2
127
+ );
128
+ return this.adjustBrightness(foreground, -amount);
129
+ }
130
+ }
131
+ }
132
+ class QuoteGenerate {
133
+ constructor() {}
134
+ async avatarImageletters(letters, color) {
135
+ const size = 500;
136
+ const canvas = createCanvas(size, size);
137
+ const context = canvas.getContext("2d");
138
+ const gradient = context.createLinearGradient(
139
+ 0,
140
+ 0,
141
+ canvas.width,
142
+ canvas.height
143
+ );
144
+ gradient.addColorStop(0, color[0]);
145
+ gradient.addColorStop(1, color[1]);
146
+ context.fillStyle = gradient;
147
+ context.fillRect(0, 0, canvas.width, canvas.height);
148
+ const drawLetters = await this.drawMultilineText(
149
+ letters,
150
+ null,
151
+ size / 2,
152
+ "#FFF",
153
+ 0,
154
+ size,
155
+ size * 5,
156
+ size * 5
157
+ );
158
+ context.drawImage(
159
+ drawLetters,
160
+ (canvas.width - drawLetters.width) / 2,
161
+ (canvas.height - drawLetters.height) / 1.5
162
+ );
163
+ return canvas.toBuffer();
164
+ }
165
+ async downloadAvatarImage(user) {
166
+ const cacheKey = user.id;
167
+ const avatarImageCache = avatarCache.get(cacheKey);
168
+ if (avatarImageCache) {
169
+ return avatarImageCache;
170
+ }
171
+ let avatarImage;
172
+ try {
173
+ if (!user.photo || (!user.photo.path && !user.photo.buffer)) {
174
+ throw new Error(
175
+ "Tidak ada sumber foto (path/buffer), gunakan fallback inisial."
176
+ );
177
+ }
178
+ let imageSource;
179
+ if (user.photo.buffer) {
180
+ imageSource = user.photo.buffer;
181
+ } else {
182
+ const requestedPath = path.resolve(user.photo.path);
183
+ if (!requestedPath.startsWith(ALLOWED_MEDIA_DIRECTORY)) {
184
+ console.error(`Akses path ditolak untuk avatar: ${user.photo.path}`);
185
+ throw new Error("Invalid avatar path specified.");
186
+ }
187
+ imageSource = requestedPath;
188
+ }
189
+ avatarImage = await loadImage(imageSource);
190
+ } catch (error) {
191
+ let nameletters;
192
+ if (user.first_name && user.last_name) {
193
+ nameletters =
194
+ runes(user.first_name)[0] + (runes(user.last_name || "")[0] || "");
195
+ } else {
196
+ let name = user.first_name || user.name || user.title || "FN";
197
+ name = name.toUpperCase();
198
+ const nameWords = name.split(" ").filter(Boolean);
199
+ if (nameWords.length > 1) {
200
+ nameletters =
201
+ runes(nameWords[0])[0] + runes(nameWords[nameWords.length - 1])[0];
202
+ } else if (nameWords.length === 1) {
203
+ nameletters = runes(nameWords[0])[0];
204
+ } else {
205
+ nameletters = "FN";
206
+ }
207
+ }
208
+ const avatarColorArray = [
209
+ ["#FF885E", "#FF516A"],
210
+ ["#FFCD6A", "#FFA85C"],
211
+ ["#E0A2F3", "#D669ED"],
212
+ ["#A0DE7E", "#54CB68"],
213
+ ["#53EDD6", "#28C9B7"],
214
+ ["#72D5FD", "#2A9EF1"],
215
+ ["#FFA8A8", "#FF719A"],
216
+ ];
217
+ const nameIndex = user.id
218
+ ? Math.abs(user.id) % 7
219
+ : Math.abs(user.name?.charCodeAt(0) || 1) % 7;
220
+ const avatarColor = avatarColorArray[nameIndex];
221
+ const avatarBuffer = await this.avatarImageletters(
222
+ nameletters,
223
+ avatarColor
224
+ );
225
+ avatarImage = await loadImage(avatarBuffer);
226
+ }
227
+ if (avatarImage) {
228
+ avatarCache.set(cacheKey, avatarImage);
229
+ }
230
+ return avatarImage;
231
+ }
232
+ async downloadMediaImage(media) {
233
+ if (!media || (!media.path && !media.buffer)) {
234
+ console.log(
235
+ "Media tidak memiliki sumber (path/buffer), tidak dapat diunduh."
236
+ );
237
+ return null;
238
+ }
239
+ try {
240
+ let imageBuffer;
241
+ if (media.buffer) {
242
+ imageBuffer = media.buffer;
243
+ } else {
244
+ const requestedPath = path.resolve(media.path);
245
+ if (!requestedPath.startsWith(ALLOWED_MEDIA_DIRECTORY)) {
246
+ console.error(
247
+ `Akses path ditolak (Path Traversal attempt): ${media.path}`
248
+ );
249
+ throw new Error("Invalid path specified.");
250
+ }
251
+ imageBuffer = await fs.promises.readFile(requestedPath);
252
+ }
253
+ return loadImage(imageBuffer);
254
+ } catch (e) {
255
+ console.error(`Gagal memuat media dari sumber lokal.`, e);
256
+ return null;
257
+ }
258
+ }
259
+ hexToRgb(hex) {
260
+ return _hexToRgb(hex);
261
+ }
262
+ colorLuminance(hex, lum) {
263
+ return _colorLuminance(hex, lum);
264
+ }
265
+ normalizeColor(color) {
266
+ return _normalizeColor(color);
267
+ }
268
+ lightOrDark(color) {
269
+ let r, g, b;
270
+ if (color.match(/^rgb/)) {
271
+ color = color.match(
272
+ /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/
273
+ );
274
+ r = color[1];
275
+ g = color[2];
276
+ b = color[3];
277
+ } else {
278
+ color = +(
279
+ "0x" + color.slice(1).replace(color.length < 5 && /./g, "$&$&")
280
+ );
281
+ r = color >> 16;
282
+ g = (color >> 8) & 255;
283
+ b = color & 255;
284
+ }
285
+ const hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b));
286
+ return hsp > 127.5 ? "light" : "dark";
287
+ }
288
+ async drawMultilineText(
289
+ text,
290
+ entities,
291
+ fontSize,
292
+ fontColor,
293
+ textX,
294
+ textY,
295
+ maxWidth,
296
+ maxHeight,
297
+ emojiBrand = "apple"
298
+ ) {
299
+ if (!text || typeof text !== "string") return createCanvas(1, 1);
300
+ if (maxWidth > 10000) maxWidth = 10000;
301
+ if (maxHeight > 10000) maxHeight = 10000;
302
+ const allEmojiImages = await emojiImageByBrandPromise;
303
+ const emojiImageJson = allEmojiImages[emojiBrand] || {};
304
+ const fallbackEmojiImageJson = allEmojiImages["apple"] || {};
305
+ const canvas = createCanvas(maxWidth + fontSize, maxHeight + fontSize);
306
+ const ctx = canvas.getContext("2d");
307
+ text = text.replace(/і/g, "i");
308
+ const lineHeight = fontSize * 1.2;
309
+ const charStyles = new Array(runes(text).length).fill(null).map(() => []);
310
+ if (entities && typeof entities === "object" && Array.isArray(entities)) {
311
+ for (const entity of entities) {
312
+ const style = [];
313
+ if (["pre", "code", "pre_code", "monospace"].includes(entity.type))
314
+ style.push("monospace");
315
+ else if (
316
+ [
317
+ "mention",
318
+ "text_mention",
319
+ "hashtag",
320
+ "email",
321
+ "phone_number",
322
+ "bot_command",
323
+ "url",
324
+ "text_link",
325
+ ].includes(entity.type)
326
+ )
327
+ style.push("mention");
328
+ else style.push(entity.type);
329
+ for (
330
+ let i = entity.offset;
331
+ i < Math.min(entity.offset + entity.length, charStyles.length);
332
+ i++
333
+ ) {
334
+ if (charStyles[i]) charStyles[i].push(...style);
335
+ }
336
+ }
337
+ } else if (typeof entities === "string") {
338
+ for (let i = 0; i < charStyles.length; i++) charStyles[i].push(entities);
339
+ }
340
+ const styledWords = [];
341
+ const emojiData = emojiDb.searchFromText({
342
+ input: text,
343
+ fixCodePoints: true,
344
+ });
345
+ let currentIndex = 0;
346
+ const processPlainText = (plainText, startOffset) => {
347
+ if (!plainText) return;
348
+ const chars = runes(plainText);
349
+ let currentWord = "";
350
+ let currentStyle = JSON.stringify(charStyles[startOffset] || []);
351
+ const pushWord = () => {
352
+ if (currentWord) {
353
+ styledWords.push({
354
+ word: currentWord,
355
+ style: JSON.parse(currentStyle),
356
+ });
357
+ currentWord = "";
358
+ }
359
+ };
360
+ for (let i = 0; i < chars.length; i++) {
361
+ const char = chars[i];
362
+ const charIndexInOriginal = startOffset + i;
363
+ const newStyle = JSON.stringify(charStyles[charIndexInOriginal] || []);
364
+ if (newStyle !== currentStyle || /<br>|\n|\r|\s/.test(char)) {
365
+ pushWord();
366
+ currentStyle = newStyle;
367
+ }
368
+ if (/<br>|\n|\r/.test(char)) {
369
+ styledWords.push({ word: "\n", style: [] });
370
+ } else if (/\s/.test(char)) {
371
+ styledWords.push({ word: " ", style: [] });
372
+ } else {
373
+ currentWord += char;
374
+ }
375
+ }
376
+ pushWord();
377
+ };
378
+ emojiData.forEach((emojiInfo) => {
379
+ if (emojiInfo.offset > currentIndex) {
380
+ processPlainText(
381
+ text.substring(currentIndex, emojiInfo.offset),
382
+ currentIndex
383
+ );
384
+ }
385
+ styledWords.push({
386
+ word: emojiInfo.emoji,
387
+ style: charStyles[emojiInfo.offset] || [],
388
+ emoji: { code: emojiInfo.found },
389
+ });
390
+ currentIndex = emojiInfo.offset + emojiInfo.length;
391
+ });
392
+ if (currentIndex < text.length) {
393
+ processPlainText(text.substring(currentIndex), currentIndex);
394
+ }
395
+ let lineX = textX;
396
+ let lineY = textY;
397
+ let textWidth = 0;
398
+ for (let index = 0; index < styledWords.length; index++) {
399
+ const styledWord = styledWords[index];
400
+ let emojiImage;
401
+ if (styledWord.emoji) {
402
+ const emojiImageBase = emojiImageJson[styledWord.emoji.code];
403
+ if (emojiImageBase) {
404
+ emojiImage = await loadImage(Buffer.from(emojiImageBase, "base64"));
405
+ } else if (fallbackEmojiImageJson[styledWord.emoji.code]) {
406
+ emojiImage = await loadImage(
407
+ Buffer.from(fallbackEmojiImageJson[styledWord.emoji.code], "base64")
408
+ );
409
+ }
410
+ }
411
+ let fontType = "";
412
+ let fontName = "Noto Sans";
413
+ let fillStyle = fontColor;
414
+ if (styledWord.style.includes("bold")) fontType += "bold ";
415
+ if (styledWord.style.includes("italic")) fontType += "italic ";
416
+ if (styledWord.style.includes("monospace")) fontName = "NotoSansMono";
417
+ if (
418
+ styledWord.style.includes("mention") &&
419
+ styledWord.style.includes("monospace")
420
+ ) {
421
+ fillStyle = "#005740";
422
+ } else if (styledWord.style.includes("mention")) {
423
+ fillStyle = "#007AFF";
424
+ } else if (styledWord.style.includes("monospace")) {
425
+ fillStyle = "#008069";
426
+ } else {
427
+ fillStyle = fontColor;
428
+ }
429
+ ctx.font = `${fontType}${fontSize}px "${fontName}"`;
430
+ ctx.fillStyle = fillStyle;
431
+ const isNewline = styledWord.word.match(/\n|\r/);
432
+ const wordWidth = styledWord.emoji
433
+ ? fontSize
434
+ : ctx.measureText(styledWord.word).width;
435
+ if (isNewline) {
436
+ if (textWidth < lineX) textWidth = lineX;
437
+ lineX = textX;
438
+ lineY += lineHeight;
439
+ continue;
440
+ } else if (!styledWord.emoji && wordWidth > maxWidth) {
441
+ for (let ci = 0; ci < styledWord.word.length; ci++) {
442
+ const c = styledWord.word[ci];
443
+ const charWidth = ctx.measureText(c).width;
444
+ if (lineX + charWidth > maxWidth) {
445
+ if (textWidth < lineX) textWidth = lineX;
446
+ lineX = textX;
447
+ lineY += lineHeight;
448
+ }
449
+ ctx.fillText(c, lineX, lineY);
450
+ lineX += charWidth;
451
+ }
452
+ continue;
453
+ }
454
+ if (lineX + wordWidth > maxWidth && styledWord.word !== " ") {
455
+ if (textWidth < lineX) textWidth = lineX;
456
+ lineX = textX;
457
+ lineY += lineHeight;
458
+ }
459
+ if (lineY > maxHeight) break;
460
+ if (emojiImage) {
461
+ const emojiYOffset = fontSize * 0.85;
462
+ ctx.drawImage(
463
+ emojiImage,
464
+ lineX,
465
+ lineY - emojiYOffset,
466
+ fontSize,
467
+ fontSize
468
+ );
469
+ } else if (styledWord.word !== " ") {
470
+ ctx.fillText(styledWord.word, lineX, lineY);
471
+ }
472
+ lineX += styledWord.emoji
473
+ ? fontSize
474
+ : ctx.measureText(styledWord.word).width;
475
+ if (textWidth < lineX) {
476
+ textWidth = lineX;
477
+ }
478
+ }
479
+ const finalHeight = lineY + lineHeight;
480
+ const canvasResize = createCanvas(
481
+ Math.ceil(textWidth),
482
+ Math.ceil(finalHeight)
483
+ );
484
+ const canvasResizeCtx = canvasResize.getContext("2d");
485
+ canvasResizeCtx.drawImage(canvas, 0, 0);
486
+ return canvasResize;
487
+ }
488
+ async drawTruncatedText(
489
+ text,
490
+ entities,
491
+ fontSize,
492
+ fontColor,
493
+ maxWidth,
494
+ emojiBrand = "apple",
495
+ truncationRatio = 0.95,
496
+ truncateMaxWidth = null
497
+ ) {
498
+ if (!text || typeof text !== "string") return createCanvas(1, 1);
499
+ const allEmojiImages = await emojiImageByBrandPromise;
500
+ const emojiImageJson = allEmojiImages[emojiBrand] || {};
501
+ const fallbackEmojiImageJson = allEmojiImages["apple"] || {};
502
+ const canvas = createCanvas(maxWidth, fontSize * 1.7);
503
+ const ctx = canvas.getContext("2d");
504
+ const isLongUnbreakableUrl = () => {
505
+ const isUrl = entities?.some((e) =>
506
+ ["url", "text_link"].includes(e.type)
507
+ );
508
+ const noSpaces = !text.includes(" ");
509
+ return isUrl && noSpaces && runes(text).length > 30;
510
+ };
511
+ const charStyles = new Array(runes(text).length).fill(null).map(() => []);
512
+ if (entities && Array.isArray(entities)) {
513
+ for (const entity of entities) {
514
+ const style = [];
515
+ if (["pre", "code", "pre_code", "monospace"].includes(entity.type)) {
516
+ style.push("monospace");
517
+ } else if (
518
+ [
519
+ "mention",
520
+ "text_mention",
521
+ "hashtag",
522
+ "email",
523
+ "phone_number",
524
+ "bot_command",
525
+ "url",
526
+ "text_link",
527
+ ].includes(entity.type)
528
+ ) {
529
+ style.push("mention");
530
+ } else {
531
+ style.push(entity.type);
532
+ }
533
+ for (
534
+ let i = entity.offset;
535
+ i < Math.min(entity.offset + entity.length, charStyles.length);
536
+ i++
537
+ ) {
538
+ if (charStyles[i]) charStyles[i].push(...style);
539
+ }
540
+ }
541
+ }
542
+ const styledWords = [];
543
+ const emojiData = emojiDb.searchFromText({
544
+ input: text,
545
+ fixCodePoints: true,
546
+ });
547
+ let currentIndex = 0;
548
+ const processPlainText = (plainText, startOffset) => {
549
+ if (!plainText) return;
550
+ const chars = runes(plainText);
551
+ let currentWord = "";
552
+ let currentStyle = JSON.stringify(charStyles[startOffset] || []);
553
+ const pushWord = () => {
554
+ if (currentWord) {
555
+ styledWords.push({
556
+ word: currentWord,
557
+ style: JSON.parse(currentStyle),
558
+ });
559
+ currentWord = "";
560
+ }
561
+ };
562
+ for (let i = 0; i < chars.length; i++) {
563
+ const char = chars[i];
564
+ const charIndexInOriginal = startOffset + i;
565
+ const newStyle = JSON.stringify(charStyles[charIndexInOriginal] || []);
566
+ if (newStyle !== currentStyle || /\s/.test(char)) {
567
+ pushWord();
568
+ currentStyle = newStyle;
569
+ }
570
+ if (/\s/.test(char)) {
571
+ styledWords.push({ word: " ", style: [] });
572
+ } else {
573
+ currentWord += char;
574
+ }
575
+ }
576
+ pushWord();
577
+ };
578
+ emojiData.forEach((emojiInfo) => {
579
+ if (emojiInfo.offset > currentIndex) {
580
+ processPlainText(
581
+ text.substring(currentIndex, emojiInfo.offset),
582
+ currentIndex
583
+ );
584
+ }
585
+ styledWords.push({
586
+ word: emojiInfo.emoji,
587
+ style: charStyles[emojiInfo.offset] || [],
588
+ emoji: { code: emojiInfo.found },
589
+ });
590
+ currentIndex = emojiInfo.offset + emojiInfo.length;
591
+ });
592
+ if (currentIndex < text.length) {
593
+ processPlainText(text.substring(currentIndex), currentIndex);
594
+ }
595
+ ctx.font = `${fontSize}px "Noto Sans"`;
596
+ const ellipsisWidth = ctx.measureText("…").width;
597
+ const areaTruncate = truncateMaxWidth
598
+ ? truncateMaxWidth * truncationRatio
599
+ : maxWidth * truncationRatio;
600
+ let drawX = 0;
601
+ let truncated = false;
602
+ const visibleWords = [];
603
+ for (const styledWord of styledWords) {
604
+ let wordWidth;
605
+ let fontType = "";
606
+ let fontName = "Noto Sans";
607
+ let fillStyle = fontColor;
608
+ if (styledWord.style.includes("bold")) fontType += "bold ";
609
+ if (styledWord.style.includes("italic")) fontType += "italic ";
610
+ if (styledWord.style.includes("monospace")) fontName = "NotoSansMono";
611
+ if (styledWord.style.includes("mention")) fillStyle = "#007AFF";
612
+ ctx.font = `${fontType}${fontSize}px "${fontName}"`;
613
+ ctx.fillStyle = fillStyle;
614
+ if (styledWord.emoji) {
615
+ wordWidth = fontSize;
616
+ } else {
617
+ wordWidth = ctx.measureText(styledWord.word).width;
618
+ if (
619
+ wordWidth > areaTruncate &&
620
+ styledWord.style.includes("monospace")
621
+ ) {
622
+ const chars = runes(styledWord.word);
623
+ let charCount = 0;
624
+ for (const char of chars) {
625
+ const charWidth = ctx.measureText(char).width;
626
+ if (drawX + charWidth + ellipsisWidth > areaTruncate) {
627
+ truncated = true;
628
+ break;
629
+ }
630
+ visibleWords.push({
631
+ word: char,
632
+ style: styledWord.style,
633
+ emoji: styledWord.emoji,
634
+ });
635
+ drawX += charWidth;
636
+ charCount++;
637
+ if (charCount > 100) break;
638
+ }
639
+ continue;
640
+ }
641
+ }
642
+ if (
643
+ drawX + wordWidth + ellipsisWidth > areaTruncate &&
644
+ styledWord.word !== " "
645
+ ) {
646
+ if (visibleWords.length > 0) {
647
+ truncated = true;
648
+ }
649
+ break;
650
+ }
651
+ visibleWords.push(styledWord);
652
+ drawX += wordWidth;
653
+ }
654
+ if (visibleWords.length === 0 && isLongUnbreakableUrl()) {
655
+ const firstFewChars = runes(text).slice(0, 10).join("");
656
+ visibleWords.push({
657
+ word: firstFewChars,
658
+ style: ["mention"],
659
+ });
660
+ truncated = true;
661
+ }
662
+ drawX = 0;
663
+ for (const styledWord of visibleWords) {
664
+ let fontType = "";
665
+ let fontName = "Noto Sans";
666
+ let fillStyle = fontColor;
667
+ if (styledWord.style.includes("bold")) fontType += "bold ";
668
+ if (styledWord.style.includes("italic")) fontType += "italic ";
669
+ if (styledWord.style.includes("monospace")) {
670
+ fontName = "NotoSansMono";
671
+ fillStyle = "#008069";
672
+ }
673
+ if (styledWord.style.includes("mention")) {
674
+ fillStyle = "#007AFF";
675
+ }
676
+ ctx.font = `${fontType}${fontSize}px "${fontName}"`;
677
+ ctx.fillStyle = fillStyle;
678
+ if (styledWord.emoji) {
679
+ const emojiImageBase =
680
+ emojiImageJson[styledWord.emoji.code] ||
681
+ fallbackEmojiImageJson[styledWord.emoji.code];
682
+ if (emojiImageBase) {
683
+ const emojiImage = await loadImage(
684
+ Buffer.from(emojiImageBase, "base64")
685
+ );
686
+ const emojiYOffset = fontSize * 0.85;
687
+ ctx.drawImage(
688
+ emojiImage,
689
+ drawX,
690
+ fontSize - emojiYOffset,
691
+ fontSize,
692
+ fontSize
693
+ );
694
+ }
695
+ drawX += fontSize;
696
+ } else {
697
+ ctx.fillText(styledWord.word, drawX, fontSize);
698
+ drawX += ctx.measureText(styledWord.word).width;
699
+ }
700
+ }
701
+ if (truncated) {
702
+ ctx.fillStyle = fontColor;
703
+ ctx.font = `${fontSize}px "Noto Sans"`;
704
+ ctx.fillText("…", drawX, fontSize);
705
+ }
706
+ return canvas;
707
+ }
708
+ drawRoundRect(color, w, h, r) {
709
+ const x = 0;
710
+ const y = 0;
711
+ const canvas = createCanvas(w, h);
712
+ const canvasCtx = canvas.getContext("2d");
713
+ canvasCtx.fillStyle = color;
714
+ if (w < 2 * r) r = w / 2;
715
+ if (h < 2 * r) r = h / 2;
716
+ canvasCtx.beginPath();
717
+ canvasCtx.moveTo(x + r, y);
718
+ canvasCtx.arcTo(x + w, y, x + w, y + h, r);
719
+ canvasCtx.arcTo(x + w, y + h, x, y + h, r);
720
+ canvasCtx.arcTo(x, y + h, x, y, r);
721
+ canvasCtx.arcTo(x, y, x + w, y, r);
722
+ canvasCtx.closePath();
723
+ canvasCtx.fill();
724
+ return canvas;
725
+ }
726
+ drawGradientRoundRect(colorOne, colorTwo, w, h, r) {
727
+ const x = 0;
728
+ const y = 0;
729
+ const canvas = createCanvas(w, h);
730
+ const canvasCtx = canvas.getContext("2d");
731
+ const gradient = canvasCtx.createLinearGradient(0, 0, w, h);
732
+ gradient.addColorStop(0, colorOne);
733
+ gradient.addColorStop(1, colorTwo);
734
+ canvasCtx.fillStyle = gradient;
735
+ if (w < 2 * r) r = w / 2;
736
+ if (h < 2 * r) r = h / 2;
737
+ canvasCtx.beginPath();
738
+ canvasCtx.moveTo(x + r, y);
739
+ canvasCtx.arcTo(x + w, y, x + w, y + h, r);
740
+ canvasCtx.arcTo(x + w, y + h, x, y + h, r);
741
+ canvasCtx.arcTo(x, y + h, x, y, r);
742
+ canvasCtx.arcTo(x, y, x + w, y, r);
743
+ canvasCtx.closePath();
744
+ canvasCtx.fill();
745
+ return canvas;
746
+ }
747
+ roundImage(image, r) {
748
+ const w = image.width;
749
+ const h = image.height;
750
+ const canvas = createCanvas(w, h);
751
+ const canvasCtx = canvas.getContext("2d");
752
+ const x = 0;
753
+ const y = 0;
754
+ if (w < 2 * r) r = w / 2;
755
+ if (h < 2 * r) r = h / 2;
756
+ canvasCtx.beginPath();
757
+ canvasCtx.moveTo(x + r, y);
758
+ canvasCtx.arcTo(x + w, y, x + w, y + h, r);
759
+ canvasCtx.arcTo(x + w, y + h, x, y + h, r);
760
+ canvasCtx.arcTo(x, y + h, x, y, r);
761
+ canvasCtx.arcTo(x, y, x + w, y, r);
762
+ canvasCtx.save();
763
+ canvasCtx.clip();
764
+ canvasCtx.closePath();
765
+ canvasCtx.drawImage(image, x, y);
766
+ canvasCtx.restore();
767
+ return canvas;
768
+ }
769
+ drawReplyLine(lineWidth, height, color) {
770
+ const canvas = createCanvas(20, height);
771
+ const context = canvas.getContext("2d");
772
+ context.beginPath();
773
+ context.moveTo(10, 0);
774
+ context.lineTo(10, height);
775
+ context.lineWidth = lineWidth;
776
+ context.strokeStyle = color;
777
+ context.stroke();
778
+ context.closePath();
779
+ return canvas;
780
+ }
781
+ trimNameOrNumber(text, maxWords = 2) {
782
+ const maxLength = 26;
783
+ const words = text.split(" ");
784
+ if (words.length > maxWords) {
785
+ text = words.slice(0, maxWords).join(" ");
786
+ } else if (text.length > maxLength) {
787
+ text = text.slice(0, maxLength);
788
+ }
789
+ return text;
790
+ }
791
+ formatPhoneNumber(text) {
792
+ text = text.replace(/\D/g, "");
793
+ if (text.startsWith("62")) {
794
+ return `+62 ${text.slice(2, 5)}-${text.slice(5, 9)}-${text.slice(9)}`;
795
+ }
796
+ return text.replace(/^(\d{1,4})(\d{1,4})(\d{1,4})(\d{1,4})$/, "+$1$2$3$4");
797
+ }
798
+ async drawAvatar(user) {
799
+ const avatarImage = await this.downloadAvatarImage(user);
800
+ if (avatarImage) {
801
+ const avatarSize = avatarImage.naturalHeight || avatarImage.height;
802
+ const canvas = createCanvas(avatarSize, avatarSize);
803
+ const canvasCtx = canvas.getContext("2d");
804
+ const avatarX = 0;
805
+ const avatarY = 0;
806
+ canvasCtx.save();
807
+ canvasCtx.beginPath();
808
+ canvasCtx.arc(
809
+ avatarX + avatarSize / 2,
810
+ avatarY + avatarSize / 2,
811
+ avatarSize / 2,
812
+ 0,
813
+ Math.PI * 2,
814
+ true
815
+ );
816
+ canvasCtx.clip();
817
+ canvasCtx.closePath();
818
+ canvasCtx.drawImage(
819
+ avatarImage,
820
+ avatarX,
821
+ avatarY,
822
+ avatarSize,
823
+ avatarSize
824
+ );
825
+ canvasCtx.restore();
826
+ return canvas;
827
+ }
828
+ return null;
829
+ }
830
+ async drawQuote(
831
+ scale,
832
+ backgroundColorOne,
833
+ backgroundColorTwo,
834
+ avatar,
835
+ replyName,
836
+ replyNameColor,
837
+ finalReplyTextCanvas,
838
+ replyNumber,
839
+ name,
840
+ number,
841
+ text,
842
+ media,
843
+ mediaType,
844
+ finalContentWidth,
845
+ replyMedia,
846
+ replyMediaType,
847
+ replyThumbnailSize,
848
+ fromTime,
849
+ emojiBrand = "apple",
850
+ gap
851
+ ) {
852
+ const avatarPosX = 0;
853
+ const avatarPosY = 5 * scale;
854
+ const avatarSize = 50 * scale;
855
+ const indent = 14 * scale;
856
+ const blockPosX = avatarSize + 10 * scale;
857
+ const width = blockPosX + finalContentWidth;
858
+ const quotedThumbW = replyMedia
859
+ ? Math.min(finalContentWidth * 0.25, replyMedia.width)
860
+ : 0;
861
+ const quotedThumbH = replyMedia
862
+ ? replyMedia.height * (quotedThumbW / replyMedia.width)
863
+ : 0;
864
+ const replyNameHeight = replyName?.height || 0;
865
+ const replyNumberHeight = replyNumber?.height || 0;
866
+ const quotedTextHeight = finalReplyTextCanvas?.height || 0;
867
+ const replyNameBarHeight = Math.max(replyNameHeight, replyNumberHeight, 1);
868
+ const replyQuotedHeight =
869
+ Math.max(replyNameBarHeight + quotedTextHeight, quotedThumbH) +
870
+ indent / 2 -
871
+ 25 * scale;
872
+ const replyBubbleWidth = finalContentWidth - indent * 2;
873
+ const namePosX = blockPosX + indent;
874
+ const textPosX = blockPosX + indent;
875
+ const nameHeight = name?.height || 0;
876
+ const numberHeight = number?.height || 0;
877
+ const nameBarHeight = Math.max(nameHeight, numberHeight, 1);
878
+ let namePosY = indent;
879
+ let currentY = namePosY + nameBarHeight - 25 * scale;
880
+ let rectHeight = currentY;
881
+ let replyBubblePosX = textPosX;
882
+ let replyBubblePosY = currentY;
883
+ if (replyName && (finalReplyTextCanvas || replyMedia)) {
884
+ currentY += replyQuotedHeight;
885
+ rectHeight = currentY;
886
+ }
887
+ let mediaPosX, mediaPosY, mediaWidth, mediaHeight;
888
+ if (media) {
889
+ mediaWidth = finalContentWidth - indent * 2;
890
+ mediaHeight = media.height * (mediaWidth / media.width);
891
+ mediaPosX = textPosX;
892
+ mediaPosY = currentY;
893
+ currentY += mediaHeight + indent;
894
+ rectHeight = currentY;
895
+ }
896
+ let textPosY = currentY;
897
+ if (text) {
898
+ currentY += text.height;
899
+ rectHeight = currentY;
900
+ }
901
+ const height = Math.max(rectHeight + indent, avatarSize + indent * 2);
902
+ const canvas = createCanvas(width, height);
903
+ const ctx = canvas.getContext("2d");
904
+ const rectWidth = width - blockPosX;
905
+ const rect =
906
+ backgroundColorOne === backgroundColorTwo
907
+ ? this.drawRoundRect(backgroundColorOne, rectWidth, height, 25 * scale)
908
+ : this.drawGradientRoundRect(
909
+ backgroundColorOne,
910
+ backgroundColorTwo,
911
+ rectWidth,
912
+ height,
913
+ 25 * scale
914
+ );
915
+ ctx.drawImage(rect, blockPosX, 0);
916
+ if (avatar) {
917
+ ctx.drawImage(avatar, avatarPosX, avatarPosY, avatarSize, avatarSize);
918
+ }
919
+ if (name) {
920
+ ctx.drawImage(
921
+ name,
922
+ namePosX,
923
+ namePosY + (nameBarHeight - name.height) / 2
924
+ );
925
+ }
926
+ if (number) {
927
+ let nomorX =
928
+ blockPosX + indent + (finalContentWidth - number.width - indent * 2);
929
+ ctx.drawImage(
930
+ number,
931
+ nomorX,
932
+ namePosY + (nameBarHeight - name.height) / 2
933
+ );
934
+ }
935
+ if (replyName && (finalReplyTextCanvas || replyMedia)) {
936
+ const replyBg = this.drawRoundRect(
937
+ replyNameColor,
938
+ replyBubbleWidth,
939
+ replyQuotedHeight,
940
+ 10 * scale
941
+ );
942
+ ctx.drawImage(replyBg, replyBubblePosX, replyBubblePosY);
943
+ ctx.save();
944
+ ctx.beginPath();
945
+ ctx.moveTo(replyBubblePosX + 10 * scale, replyBubblePosY);
946
+ ctx.arcTo(
947
+ replyBubblePosX + replyBubbleWidth,
948
+ replyBubblePosY,
949
+ replyBubblePosX + replyBubbleWidth,
950
+ replyBubblePosY + replyQuotedHeight,
951
+ 10 * scale
952
+ );
953
+ ctx.arcTo(
954
+ replyBubblePosX + replyBubbleWidth,
955
+ replyBubblePosY + replyQuotedHeight,
956
+ replyBubblePosX,
957
+ replyBubblePosY + replyQuotedHeight,
958
+ 10 * scale
959
+ );
960
+ ctx.arcTo(
961
+ replyBubblePosX,
962
+ replyBubblePosY + replyQuotedHeight,
963
+ replyBubblePosX,
964
+ replyBubblePosY,
965
+ 10 * scale
966
+ );
967
+ ctx.arcTo(
968
+ replyBubblePosX,
969
+ replyBubblePosY,
970
+ replyBubblePosX + replyBubbleWidth,
971
+ replyBubblePosY,
972
+ 10 * scale
973
+ );
974
+ ctx.closePath();
975
+ ctx.clip();
976
+ ctx.fillStyle = _colorLuminance(backgroundColorOne, 0.09);
977
+ ctx.fillRect(
978
+ replyBubblePosX + 7 * scale,
979
+ replyBubblePosY,
980
+ replyBubbleWidth * scale,
981
+ replyQuotedHeight * scale
982
+ );
983
+ ctx.restore();
984
+ let nameEndsAt = replyBubblePosX + indent;
985
+ if (replyName) {
986
+ const nameX = replyBubblePosX + indent;
987
+ const nameY =
988
+ replyBubblePosY +
989
+ (replyNameBarHeight - replyName.height) / 2 +
990
+ 5 * scale;
991
+ ctx.drawImage(replyName, nameX, nameY);
992
+ nameEndsAt = nameX + replyName.width;
993
+ }
994
+ let mediaStartsAt = replyBubblePosX + replyBubbleWidth - indent;
995
+ if (replyMedia) {
996
+ const mediaX =
997
+ replyBubblePosX + replyBubbleWidth - quotedThumbW - indent;
998
+ const mediaY = replyBubblePosY + (replyQuotedHeight - quotedThumbH) / 2;
999
+ ctx.drawImage(
1000
+ this.roundImage(replyMedia, 7 * scale),
1001
+ mediaX,
1002
+ mediaY,
1003
+ quotedThumbW,
1004
+ quotedThumbH
1005
+ );
1006
+ mediaStartsAt = mediaX;
1007
+ }
1008
+ if (replyNumber) {
1009
+ const leftBoundary = nameEndsAt + gap;
1010
+ const rightBoundary = mediaStartsAt - indent * 0.8;
1011
+ const numberX = rightBoundary - replyNumber.width;
1012
+ if (numberX > leftBoundary) {
1013
+ const numberY =
1014
+ replyBubblePosY +
1015
+ (replyNameBarHeight - replyName.height) / 2 +
1016
+ 5 * scale;
1017
+ ctx.drawImage(replyNumber, numberX, numberY);
1018
+ }
1019
+ }
1020
+ if (finalReplyTextCanvas) {
1021
+ ctx.drawImage(
1022
+ finalReplyTextCanvas,
1023
+ replyBubblePosX + indent,
1024
+ replyBubblePosY + replyNameBarHeight - 15 * scale
1025
+ );
1026
+ }
1027
+ }
1028
+ if (media) {
1029
+ ctx.drawImage(
1030
+ this.roundImage(media, 8 * scale),
1031
+ mediaPosX,
1032
+ mediaPosY + 15,
1033
+ mediaWidth,
1034
+ mediaHeight
1035
+ );
1036
+ }
1037
+ if (text) {
1038
+ ctx.drawImage(text, textPosX, textPosY + 10);
1039
+ }
1040
+ if (fromTime) {
1041
+ const timeFontSize = 15 * scale;
1042
+ ctx.font = `bold ${timeFontSize}px "Noto Sans"`;
1043
+ ctx.fillStyle = "#888";
1044
+ ctx.textAlign = "right";
1045
+ ctx.textBaseline = "bottom";
1046
+ ctx.fillText(
1047
+ fromTime,
1048
+ canvas.width - indent * 1.2,
1049
+ canvas.height - indent * 0.7
1050
+ );
1051
+ }
1052
+ return canvas;
1053
+ }
1054
+ async generate(
1055
+ backgroundColorOne,
1056
+ backgroundColorTwo,
1057
+ message,
1058
+ width = 512,
1059
+ height = 512,
1060
+ scale = 2,
1061
+ emojiBrand = "apple"
1062
+ ) {
1063
+ if (!scale) scale = 2;
1064
+ if (scale > 20) scale = 20;
1065
+ width = width || 512;
1066
+ height = height || 512;
1067
+ width *= scale;
1068
+ height *= scale;
1069
+ const backStyle = this.lightOrDark(backgroundColorOne);
1070
+ const gap = 15 * scale;
1071
+ const nameColorLight = [
1072
+ "#FC5C51",
1073
+ "#FA790F",
1074
+ "#895DD5",
1075
+ "#0FB297",
1076
+ "#D54FAF",
1077
+ "#0FC9D6",
1078
+ "#3CA5EC",
1079
+ ];
1080
+ const nameColorDark = [
1081
+ "#FF8E86",
1082
+ "#FFA357",
1083
+ "#B18FFF",
1084
+ "#4DD6BF",
1085
+ "#FF7FD5",
1086
+ "#45E8D1",
1087
+ "#7AC9FF",
1088
+ ];
1089
+ let nameIndex = 1;
1090
+ if (message.from && message.from.id) {
1091
+ nameIndex = Math.abs(message.from.id) % 7;
1092
+ }
1093
+ const nameColorArray =
1094
+ backStyle === "light" ? nameColorLight : nameColorDark;
1095
+ let nameColor = nameColorArray[nameIndex];
1096
+ const colorContrast = new ColorContrast();
1097
+ const contrast = colorContrast.getContrastRatio(
1098
+ this.colorLuminance(backgroundColorOne, 0.55),
1099
+ nameColor
1100
+ );
1101
+ if (contrast > 90 || contrast < 30) {
1102
+ nameColor = colorContrast.adjustContrast(
1103
+ this.colorLuminance(backgroundColorTwo, 0.55),
1104
+ nameColor
1105
+ );
1106
+ }
1107
+ const nameSize = 28 * scale;
1108
+ let textColor = backStyle === "light" ? "#000" : "#fff";
1109
+ const indent = 14 * scale;
1110
+ let nameText =
1111
+ message.from.name ||
1112
+ `${message.from.first_name || ""} ${message.from.last_name || ""}`.trim();
1113
+ if (!nameText) nameText = "Yanto Baut";
1114
+ const nameCanvas = await this.drawMultilineText(
1115
+ this.trimNameOrNumber(nameText, 2),
1116
+ [{ type: "bold", offset: 0, length: runes(nameText).length }],
1117
+ nameSize,
1118
+ nameColor,
1119
+ 0,
1120
+ nameSize,
1121
+ width,
1122
+ nameSize,
1123
+ emojiBrand
1124
+ );
1125
+ let numberCanvas = null;
1126
+ if (message.from && message.from.number) {
1127
+ const messageNumber = this.formatPhoneNumber(message.from.number);
1128
+ numberCanvas = await this.drawMultilineText(
1129
+ this.trimNameOrNumber(messageNumber, 2),
1130
+ [],
1131
+ Math.floor(nameSize * 0.6),
1132
+ nameColor,
1133
+ 0,
1134
+ nameSize,
1135
+ width,
1136
+ nameSize,
1137
+ emojiBrand
1138
+ );
1139
+ }
1140
+ let textCanvas;
1141
+ if (message.text) {
1142
+ textCanvas = await this.drawMultilineText(
1143
+ message.text,
1144
+ message.entities,
1145
+ 24 * scale,
1146
+ textColor,
1147
+ 0,
1148
+ 24 * scale,
1149
+ width,
1150
+ height,
1151
+ emojiBrand
1152
+ );
1153
+ }
1154
+ let avatarCanvas;
1155
+ if (message.avatar && message.from) {
1156
+ avatarCanvas = await this.drawAvatar(message.from);
1157
+ }
1158
+ let mediaCanvas;
1159
+ if (message.media) {
1160
+ mediaCanvas = await this.downloadMediaImage(message.media);
1161
+ }
1162
+ const mainNameBarWidth =
1163
+ (nameCanvas?.width || 0) + (numberCanvas?.width || 0) + indent * 1.5;
1164
+ const mainTextWidth = textCanvas?.width || 0;
1165
+ const mainMediaWidth = mediaCanvas ? width - indent * 4 : 0;
1166
+ const mainContentRequiredWidth = Math.max(
1167
+ mainNameBarWidth,
1168
+ mainTextWidth,
1169
+ mainMediaWidth
1170
+ );
1171
+ let replyContentRequiredWidth = 0;
1172
+ let replyNameCanvas,
1173
+ replyNumberCanvas,
1174
+ replyTextCanvas_forMeasure,
1175
+ replyMedia,
1176
+ replyMediaType,
1177
+ replyNameColor;
1178
+ if (
1179
+ message.replyMessage &&
1180
+ message.replyMessage.name &&
1181
+ message.replyMessage.text
1182
+ ) {
1183
+ try {
1184
+ const chatId = message.replyMessage.chatId || 0;
1185
+ const replyNameIndex = Math.abs(chatId) % 7;
1186
+ replyNameColor = nameColorArray[replyNameIndex];
1187
+ const replyNameFontSize = 27 * scale;
1188
+ replyNameCanvas = await this.drawMultilineText(
1189
+ this.trimNameOrNumber(message.replyMessage.name, 2),
1190
+ "bold",
1191
+ replyNameFontSize,
1192
+ replyNameColor,
1193
+ 0,
1194
+ replyNameFontSize,
1195
+ width,
1196
+ replyNameFontSize,
1197
+ emojiBrand
1198
+ );
1199
+ if (message.replyMessage.number) {
1200
+ const replyMessageNumber = this.formatPhoneNumber(
1201
+ message.replyMessage.number
1202
+ );
1203
+ replyNumberCanvas = await this.drawMultilineText(
1204
+ this.trimNameOrNumber(replyMessageNumber, 2),
1205
+ [],
1206
+ Math.floor(replyNameFontSize * 0.6),
1207
+ replyNameColor,
1208
+ 0,
1209
+ replyNameFontSize,
1210
+ width,
1211
+ replyNameFontSize,
1212
+ emojiBrand
1213
+ );
1214
+ }
1215
+ if (message.replyMessage.text) {
1216
+ replyTextCanvas_forMeasure = await this.drawMultilineText(
1217
+ message.replyMessage.text,
1218
+ message.replyMessage.entities,
1219
+ 22 * scale,
1220
+ textColor,
1221
+ 0,
1222
+ 22 * scale,
1223
+ width,
1224
+ height,
1225
+ emojiBrand
1226
+ );
1227
+ }
1228
+ if (message.replyMessage.media) {
1229
+ let rawReplyMedia = await this.downloadMediaImage(
1230
+ message.replyMessage.media
1231
+ );
1232
+ replyMediaType = message.replyMessage.mediaType;
1233
+ if (rawReplyMedia) {
1234
+ const targetSize = 60 * scale;
1235
+ const tempCanvas = createCanvas(
1236
+ rawReplyMedia.width,
1237
+ rawReplyMedia.height
1238
+ );
1239
+ const tempCtx = tempCanvas.getContext("2d");
1240
+ tempCtx.drawImage(rawReplyMedia, 0, 0);
1241
+ const canvasBuffer = tempCanvas.toBuffer("image/png");
1242
+ const resizedBuffer = await sharp(canvasBuffer)
1243
+ .resize(targetSize, targetSize, {
1244
+ fit: "fill",
1245
+ position: "center",
1246
+ })
1247
+ .png()
1248
+ .toBuffer();
1249
+ replyMedia = await loadImage(resizedBuffer);
1250
+ }
1251
+ }
1252
+ const replyNameBarWidth =
1253
+ (replyNameCanvas?.width || 0) +
1254
+ (replyNumberCanvas?.width || 0) +
1255
+ indent;
1256
+ const replyTextWidth = replyTextCanvas_forMeasure?.width || 0;
1257
+ replyContentRequiredWidth = Math.max(replyNameBarWidth, replyTextWidth);
1258
+ } catch (error) {
1259
+ console.error("Error generating reply message:", error);
1260
+ [
1261
+ replyNameCanvas,
1262
+ replyNumberCanvas,
1263
+ replyMedia,
1264
+ replyTextCanvas_forMeasure,
1265
+ replyNameColor,
1266
+ ] = Array(5).fill(null);
1267
+ }
1268
+ }
1269
+ const finalContentWidth =
1270
+ Math.max(mainContentRequiredWidth, replyContentRequiredWidth) +
1271
+ indent * 2;
1272
+ if (mediaCanvas && mediaCanvas.width > finalContentWidth - indent * 2) {
1273
+ const tempCanvas = createCanvas(mediaCanvas.width, mediaCanvas.height);
1274
+ const tempCtx = tempCanvas.getContext("2d");
1275
+ tempCtx.drawImage(mediaCanvas, 0, 0);
1276
+ const buffer = tempCanvas.toBuffer("image/png");
1277
+ const resizedBuffer = await sharp(buffer)
1278
+ .resize({ width: finalContentWidth - indent * 2 })
1279
+ .png()
1280
+ .toBuffer();
1281
+ mediaCanvas = await loadImage(resizedBuffer);
1282
+ }
1283
+ let quotedThumbW = 0;
1284
+ let quotedThumbH = 0;
1285
+ if (replyMedia) {
1286
+ quotedThumbW = Math.min(95 * scale, replyMedia.width);
1287
+ quotedThumbH = replyMedia.height * (quotedThumbW / replyMedia.width);
1288
+ }
1289
+ let finalReplyTextCanvas;
1290
+ if (replyTextCanvas_forMeasure) {
1291
+ const quotedTextMaxWidth = replyMedia
1292
+ ? finalContentWidth - indent * 3 - quotedThumbW
1293
+ : finalContentWidth - indent * 3;
1294
+ finalReplyTextCanvas = await this.drawTruncatedText(
1295
+ message.replyMessage.text,
1296
+ message.replyMessage.entities,
1297
+ 22 * scale,
1298
+ textColor,
1299
+ quotedTextMaxWidth,
1300
+ emojiBrand
1301
+ );
1302
+ }
1303
+ let finalTextCanvas;
1304
+ if (textCanvas) {
1305
+ const mainBubbleWidth = finalContentWidth - indent * 2;
1306
+ finalTextCanvas = await this.drawMultilineText(
1307
+ message.text,
1308
+ message.entities,
1309
+ 24 * scale,
1310
+ textColor,
1311
+ 0,
1312
+ 24 * scale,
1313
+ mainBubbleWidth,
1314
+ height,
1315
+ emojiBrand
1316
+ );
1317
+ }
1318
+ let fromTime = message.from?.time || null;
1319
+ const quote = await this.drawQuote(
1320
+ scale,
1321
+ backgroundColorOne,
1322
+ backgroundColorTwo,
1323
+ avatarCanvas,
1324
+ replyNameCanvas,
1325
+ replyNameColor,
1326
+ finalReplyTextCanvas,
1327
+ replyNumberCanvas,
1328
+ nameCanvas,
1329
+ numberCanvas,
1330
+ finalTextCanvas,
1331
+ mediaCanvas,
1332
+ message.mediaType,
1333
+ finalContentWidth,
1334
+ replyMedia,
1335
+ replyMediaType,
1336
+ quotedThumbH,
1337
+ fromTime,
1338
+ emojiBrand,
1339
+ gap
1340
+ );
1341
+ return quote;
1342
+ }
1343
+ }
1344
+ const imageAlpha = (image, alpha) => {
1345
+ const canvas = createCanvas(image.width, image.height);
1346
+ const canvasCtx = canvas.getContext("2d");
1347
+ canvasCtx.globalAlpha = alpha;
1348
+ canvasCtx.drawImage(image, 0, 0);
1349
+ return canvas;
1350
+ };
1351
+ module.exports = async (parm) => {
1352
+ await fontsLoadedPromise;
1353
+ if (!parm) {
1354
+ return {
1355
+ error: "query_empty",
1356
+ };
1357
+ }
1358
+ if (!parm.messages || parm.messages.length < 1) {
1359
+ return {
1360
+ error: "messages_empty",
1361
+ };
1362
+ }
1363
+ const quoteGenerate = new QuoteGenerate();
1364
+ const quoteImages = [];
1365
+ let backgroundColor = parm.backgroundColor || "//#292232";
1366
+ let backgroundColorOne, backgroundColorTwo;
1367
+ const backgroundColorSplit = backgroundColor.split("/");
1368
+ if (
1369
+ backgroundColorSplit &&
1370
+ backgroundColorSplit.length > 1 &&
1371
+ backgroundColorSplit[0] !== ""
1372
+ ) {
1373
+ backgroundColorOne = _normalizeColor(backgroundColorSplit[0]);
1374
+ backgroundColorTwo = _normalizeColor(backgroundColorSplit[1]);
1375
+ } else if (backgroundColor.startsWith("//")) {
1376
+ backgroundColor = _normalizeColor(backgroundColor.replace("//", ""));
1377
+ backgroundColorOne = _colorLuminance(backgroundColor, 0.35);
1378
+ backgroundColorTwo = _colorLuminance(backgroundColor, -0.15);
1379
+ } else {
1380
+ backgroundColor = _normalizeColor(backgroundColor);
1381
+ backgroundColorOne = backgroundColor;
1382
+ backgroundColorTwo = backgroundColor;
1383
+ }
1384
+ for (const key in parm.messages) {
1385
+ const message = parm.messages[key];
1386
+ if (message) {
1387
+ if (!message.from)
1388
+ message.from = {
1389
+ id: 0,
1390
+ };
1391
+ if (message.from.photo) {
1392
+ message.avatar = true;
1393
+ }
1394
+ if (
1395
+ !message.from.name &&
1396
+ (message.from.first_name || message.from.last_name)
1397
+ ) {
1398
+ message.from.name = [message.from.first_name, message.from.last_name]
1399
+ .filter(Boolean)
1400
+ .join(" ");
1401
+ }
1402
+ if (message.replyMessage) {
1403
+ if (!message.replyMessage.chatId)
1404
+ message.replyMessage.chatId = message.from?.id || 0;
1405
+ if (!message.replyMessage.entities) message.replyMessage.entities = [];
1406
+ if (!message.replyMessage.from) {
1407
+ message.replyMessage.from = {
1408
+ name: message.replyMessage.name,
1409
+ photo: {},
1410
+ };
1411
+ } else if (!message.replyMessage.from.photo) {
1412
+ message.replyMessage.from.photo = {};
1413
+ }
1414
+ }
1415
+ const canvasQuote = await quoteGenerate.generate(
1416
+ backgroundColorOne,
1417
+ backgroundColorTwo,
1418
+ message,
1419
+ parm.width,
1420
+ parm.height,
1421
+ parseFloat(parm.scale) || 2,
1422
+ parm.emojiBrand || "apple"
1423
+ );
1424
+ quoteImages.push(canvasQuote);
1425
+ }
1426
+ }
1427
+ if (quoteImages.length === 0) {
1428
+ return {
1429
+ error: "empty_messages",
1430
+ };
1431
+ }
1432
+ let canvasQuote;
1433
+ if (quoteImages.length > 1) {
1434
+ let width = 0,
1435
+ height = 0;
1436
+ for (let index = 0; index < quoteImages.length; index++) {
1437
+ if (quoteImages[index].width > width) width = quoteImages[index].width;
1438
+ height += quoteImages[index].height;
1439
+ }
1440
+ const quoteMargin = parm.scale ? 5 * parm.scale : 10;
1441
+ const canvas = createCanvas(
1442
+ width,
1443
+ height + quoteMargin * (quoteImages.length - 1)
1444
+ );
1445
+ const canvasCtx = canvas.getContext("2d");
1446
+ let imageY = 0;
1447
+ for (let index = 0; index < quoteImages.length; index++) {
1448
+ canvasCtx.drawImage(quoteImages[index], 0, imageY);
1449
+ imageY += quoteImages[index].height + quoteMargin;
1450
+ }
1451
+ canvasQuote = canvas;
1452
+ } else {
1453
+ canvasQuote = quoteImages[0];
1454
+ }
1455
+ let quoteImage;
1456
+ let { type } = parm;
1457
+ const scale = parseFloat(parm.scale) || 2;
1458
+ if (!type) {
1459
+ type = "quote";
1460
+ }
1461
+ if (type === "quote") {
1462
+ const downPadding = 75;
1463
+ const maxWidth = 512;
1464
+ const maxHeight = 512;
1465
+ const imageQuoteSharp = sharp(canvasQuote.toBuffer());
1466
+ if (canvasQuote.height > canvasQuote.width)
1467
+ imageQuoteSharp.resize({
1468
+ height: maxHeight,
1469
+ });
1470
+ else
1471
+ imageQuoteSharp.resize({
1472
+ width: maxWidth,
1473
+ });
1474
+ const canvasImage = await loadImage(await imageQuoteSharp.toBuffer());
1475
+ const canvasPadding = createCanvas(
1476
+ canvasImage.width,
1477
+ canvasImage.height + downPadding
1478
+ );
1479
+ const canvasPaddingCtx = canvasPadding.getContext("2d");
1480
+ canvasPaddingCtx.drawImage(canvasImage, 0, 0);
1481
+ const imageSharp = sharp(canvasPadding.toBuffer());
1482
+ if (canvasPadding.height >= canvasPadding.width)
1483
+ imageSharp.resize({
1484
+ height: maxHeight,
1485
+ });
1486
+ else
1487
+ imageSharp.resize({
1488
+ width: maxWidth,
1489
+ });
1490
+ quoteImage = await imageSharp.png().toBuffer();
1491
+ } else if (type === "image") {
1492
+ const heightPadding = 75 * scale;
1493
+ const widthPadding = 95 * scale;
1494
+ const canvasImage = await loadImage(canvasQuote.toBuffer());
1495
+ const canvasPic = createCanvas(
1496
+ canvasImage.width + widthPadding,
1497
+ canvasImage.height + heightPadding
1498
+ );
1499
+ const canvasPicCtx = canvasPic.getContext("2d");
1500
+ const gradient = canvasPicCtx.createRadialGradient(
1501
+ canvasPic.width / 2,
1502
+ canvasPic.height / 2,
1503
+ 0,
1504
+ canvasPic.width / 2,
1505
+ canvasPic.height / 2,
1506
+ canvasPic.width / 2
1507
+ );
1508
+ const patternColorOne = _colorLuminance(backgroundColorTwo, 0.15);
1509
+ const patternColorTwo = _colorLuminance(backgroundColorOne, 0.15);
1510
+ gradient.addColorStop(0, patternColorOne);
1511
+ gradient.addColorStop(1, patternColorTwo);
1512
+ canvasPicCtx.fillStyle = gradient;
1513
+ canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height);
1514
+ try {
1515
+ const canvasPatternImage = await loadImage(
1516
+ path.join(__dirname, "../assets/pattern_02.png")
1517
+ );
1518
+ const pattern = canvasPicCtx.createPattern(
1519
+ imageAlpha(canvasPatternImage, 0.3),
1520
+ "repeat"
1521
+ );
1522
+ canvasPicCtx.fillStyle = pattern;
1523
+ canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height);
1524
+ } catch (e) {
1525
+ console.log("Gagal memuat pattern. Melanjutkan tanpa pattern.");
1526
+ }
1527
+ canvasPicCtx.shadowOffsetX = 8;
1528
+ canvasPicCtx.shadowOffsetY = 8;
1529
+ canvasPicCtx.shadowBlur = 13;
1530
+ canvasPicCtx.shadowColor = "rgba(0, 0, 0, 0.5)";
1531
+ canvasPicCtx.drawImage(canvasImage, widthPadding / 2, heightPadding / 2);
1532
+ canvasPicCtx.shadowOffsetX = 0;
1533
+ canvasPicCtx.shadowOffsetY = 0;
1534
+ canvasPicCtx.shadowBlur = 0;
1535
+ canvasPicCtx.shadowColor = "rgba(0, 0, 0, 0)";
1536
+ canvasPicCtx.fillStyle = `rgba(0, 0, 0, 0.3)`;
1537
+ canvasPicCtx.font = `${8 * scale}px "Noto Sans"`;
1538
+ canvasPicCtx.textAlign = "right";
1539
+ quoteImage = await sharp(canvasPic.toBuffer())
1540
+ .png({
1541
+ lossless: true,
1542
+ force: true,
1543
+ })
1544
+ .toBuffer();
1545
+ } else if (type === "stories") {
1546
+ const canvasPic = createCanvas(720, 1280);
1547
+ const canvasPicCtx = canvasPic.getContext("2d");
1548
+ const gradient = canvasPicCtx.createRadialGradient(
1549
+ canvasPic.width / 2,
1550
+ canvasPic.height / 2,
1551
+ 0,
1552
+ canvasPic.width / 2,
1553
+ canvasPic.height / 2,
1554
+ canvasPic.width / 2
1555
+ );
1556
+ const patternColorOne = _colorLuminance(backgroundColorTwo, 0.25);
1557
+ const patternColorTwo = _colorLuminance(backgroundColorOne, 0.15);
1558
+ gradient.addColorStop(0, patternColorOne);
1559
+ gradient.addColorStop(1, patternColorTwo);
1560
+ canvasPicCtx.fillStyle = gradient;
1561
+ canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height);
1562
+ try {
1563
+ const canvasPatternImage = await loadImage(
1564
+ path.join(__dirname, "../assets/pattern_02.png")
1565
+ );
1566
+ const pattern = canvasPicCtx.createPattern(
1567
+ imageAlpha(canvasPatternImage, 0.3),
1568
+ "repeat"
1569
+ );
1570
+ canvasPicCtx.fillStyle = pattern;
1571
+ canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height);
1572
+ } catch (e) {
1573
+ console.log("Gagal memuat pattern. Melanjutkan tanpa pattern.");
1574
+ }
1575
+ canvasPicCtx.shadowOffsetX = 8;
1576
+ canvasPicCtx.shadowOffsetY = 8;
1577
+ canvasPicCtx.shadowBlur = 13;
1578
+ canvasPicCtx.shadowColor = "rgba(0, 0, 0, 0.5)";
1579
+ let canvasImage = await loadImage(canvasQuote.toBuffer());
1580
+ const minPadding = 110;
1581
+ if (
1582
+ canvasImage.width > canvasPic.width - minPadding * 2 ||
1583
+ canvasImage.height > canvasPic.height - minPadding * 2
1584
+ ) {
1585
+ canvasImage = await sharp(canvasQuote.toBuffer())
1586
+ .resize({
1587
+ width: canvasPic.width - minPadding * 2,
1588
+ height: canvasPic.height - minPadding * 2,
1589
+ fit: "contain",
1590
+ background: {
1591
+ r: 0,
1592
+ g: 0,
1593
+ b: 0,
1594
+ alpha: 0,
1595
+ },
1596
+ })
1597
+ .toBuffer();
1598
+ canvasImage = await loadImage(canvasImage);
1599
+ }
1600
+ const imageX = (canvasPic.width - canvasImage.width) / 2;
1601
+ const imageY = (canvasPic.height - canvasImage.height) / 2;
1602
+ canvasPicCtx.drawImage(canvasImage, imageX, imageY);
1603
+ canvasPicCtx.shadowOffsetX = 0;
1604
+ canvasPicCtx.shadowOffsetY = 0;
1605
+ canvasPicCtx.shadowBlur = 0;
1606
+ canvasPicCtx.fillStyle = `rgba(0, 0, 0, 0.4)`;
1607
+ canvasPicCtx.font = `${16 * scale}px "Noto Sans"`;
1608
+ canvasPicCtx.textAlign = "center";
1609
+ canvasPicCtx.translate(70, canvasPic.height / 2);
1610
+ canvasPicCtx.rotate(-Math.PI / 2);
1611
+ quoteImage = await sharp(canvasPic.toBuffer())
1612
+ .png({
1613
+ lossless: true,
1614
+ force: true,
1615
+ })
1616
+ .toBuffer();
1617
+ } else {
1618
+ quoteImage = canvasQuote.toBuffer("image/png");
1619
+ }
1620
+ return { image: quoteImage };
1621
+ };