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.
- package/assets/fonts/Chirp-Regular.ttf +0 -0
- package/assets/fonts/Noto-Bold.ttf +0 -0
- package/assets/fonts/Noto-Emoji.ttf +0 -0
- package/assets/fonts/Noto-Regular.ttf +0 -0
- package/assets/fonts/SfProDisplay.ttf +0 -0
- package/assets/images/android/background-call.jpg +0 -0
- package/assets/images/google/google-lyrics.jpg +0 -0
- package/assets/images/google/google-search.jpg +0 -0
- package/assets/images/instagram/fakestory.png +0 -0
- package/assets/images/instagram/instagram-verified.png +0 -0
- package/assets/images/iphone/background-call.jpg +0 -0
- package/assets/images/iphone/background.jpg +0 -0
- package/assets/images/iphone/battery.png +0 -0
- package/assets/images/iphone/copy.png +0 -0
- package/assets/images/iphone/delete.png +0 -0
- package/assets/images/iphone/forward.png +0 -0
- package/assets/images/iphone/pin.png +0 -0
- package/assets/images/iphone/plus.png +0 -0
- package/assets/images/iphone/reply.png +0 -0
- package/assets/images/iphone/report.png +0 -0
- package/assets/images/iphone/signal.png +0 -0
- package/assets/images/iphone/star.png +0 -0
- package/assets/images/iphone/wifi.png +0 -0
- package/assets/images/pattern_02.png +0 -0
- package/assets/images/tiktok/tiktok-verified.png +0 -0
- package/assets/images/tweet/like.png +0 -0
- package/assets/images/tweet/other.png +0 -0
- package/assets/images/tweet/reply.png +0 -0
- package/assets/images/tweet/retweet.png +0 -0
- package/assets/images/tweet/share.png +0 -0
- package/assets/images/tweet/twitter-verified.png +0 -0
- package/assets/images/youtube/youtube-verified.png +0 -0
- package/index.js +39 -0
- package/lib/generators.js +3220 -0
- package/lib/quote-generator.js +1621 -0
- package/lib/utils.js +166 -0
- 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
|
+
};
|