| const puppeteer = require('puppeteer'); |
| |
| const { createCanvas, registerFont } = require('canvas'); |
| const fs = require('fs'); |
| const fsPromises = require('fs').promises; |
| const path = require('path'); |
| const sharp = require('sharp'); |
| const axios = require('axios'); |
| require('dotenv').config |
| |
|
|
| |
| const BOT_TOKEN = process.env.BOT_TOKEN; |
|
|
| |
| const fontMap = { |
| '/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf': 'Noto Sans', |
| '/usr/share/fonts/truetype/noto/NotoSansSinhala-Regular.ttf': 'Noto Sans Sinhala', |
| '/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf': 'Noto Color Emoji', |
| '/usr/share/fonts/truetype/noto/NotoSansSymbols-Regular.ttf': 'Noto Sans Symbols', |
| '/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf': 'Noto Sans Symbols 2', |
| '/usr/share/fonts/truetype/noto/NotoSansMath-Regular.ttf': 'Noto Sans Math', |
| '/usr/share/fonts/truetype/noto/NotoSansMeeteiMayek-Regular.ttf': 'Noto Sans Meetei Mayek' |
| }; |
|
|
| |
| console.log('Registering fonts for node-canvas...'); |
| Object.entries(fontMap).forEach(([fontPath, familyName]) => { |
| try { |
| if (fs.existsSync(fontPath)) { |
| registerFont(fontPath, { family: familyName }); |
| console.log(`Registered for node-canvas: ${fontPath} as "${familyName}"`); |
| } else { |
| console.warn(`⚠️ node-canvas: Font file not found at ${fontPath}`); |
| } |
| } catch (e) { |
| console.warn(`⚠️ node-canvas: Could not register font at ${fontPath}: ${e.message}`); |
| } |
| }); |
|
|
| |
| |
|
|
| |
| |
| const FONT_STACK = "'Noto Sans', 'Noto Sans Sinhala', 'Noto Sans Meetei Mayek', 'Noto Sans Math', 'Noto Sans Symbols', 'Noto Sans Symbols 2', 'Noto Color Emoji'"; |
| |
| const DUMMY_AVATAR_FONT_STACK = "'Noto Color Emoji', 'Noto Sans'"; |
|
|
|
|
| |
|
|
| function getTelegramDarkThemeColor(id) { const map = new Map([[0, '#FF516A'], [1, '#FF9442'], [2, '#C66FFF'], [3, '#50D892'], [4, '#64D4F5'], [5, '#5095ED'], [6, '#FF66A6'], [7, '#FF8280'], [8, '#EDD64E'], [9, '#C66FFF']]); return map.get(id) || '#00ffff'; } |
|
|
| |
| async function createDummyAvatarBuffer(f, l, c, scale = 1) { |
| const avatarSize = 140 * scale; |
|
|
| |
| let initialText = ''; |
| const firstChar = f ? (Array.from(f)[0] || '') : ''; |
| const isFirstCharEmoji = /\p{Emoji}/u.test(firstChar); |
|
|
| if (isFirstCharEmoji) { |
| initialText = firstChar; |
| } else { |
| const firstInitial = firstChar; |
| const lastInitial = l ? (Array.from(l)[0] || '') : ''; |
| initialText = (firstInitial + lastInitial).toUpperCase().trim(); |
| } |
|
|
| if (!initialText) { |
| initialText = '?'; |
| } |
|
|
| |
| const graphemeCount = Array.from(initialText).length; |
| const isSingleEmoji = graphemeCount === 1 && /\p{Emoji}/u.test(initialText); |
|
|
| let fontSize; |
| let fontWeight = 'bold'; |
| if (isSingleEmoji) { |
| fontSize = 72 * scale; |
| fontWeight = 'normal'; |
| } else if (graphemeCount === 1) { |
| fontSize = 64 * scale; |
| } else { |
| fontSize = 48 * scale; |
| } |
|
|
| |
| const htmlContent = ` |
| <html><head><style> |
| /* --- Font-face rules removed --- */ |
| |
| body { |
| margin: 0; |
| padding: 0; |
| width: ${avatarSize}px; |
| height: ${avatarSize}px; |
| font-family: ${DUMMY_AVATAR_FONT_STACK}; /* Use the font stack */ |
| } |
| #avatar { |
| width: 100%; |
| height: 100%; |
| background-color: ${c}; |
| border-radius: 50%; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| color: #FFF; |
| font-size: ${fontSize}px; |
| font-weight: ${fontWeight}; |
| line-height: 1; |
| text-align: center; |
| overflow: hidden; /* Just in case */ |
| } |
| </style></head> |
| <body> |
| <div id="avatar">${escapeHtml(initialText)}</div> |
| </body></html> |
| `; |
|
|
| |
| let browser; |
| let pngBuffer; |
| try { |
| browser = await puppeteer.launch({ headless: true,executablePath: '/usr/bin/google-chrome', args: ['--no-sandbox', '--disable-gpu'] }); |
| const page = await browser.newPage(); |
| await page.setViewport({ width: avatarSize, height: avatarSize }); |
| await page.setContent(htmlContent, { waitUntil: 'domcontentloaded' }); |
|
|
| const element = await page.$('#avatar'); |
| pngBuffer = await element.screenshot({ omitBackground: true }); |
| |
| } catch (e) { |
| console.error("❌ Error creating dummy avatar with Puppeteer:", e.message); |
| return null; |
| } finally { |
| if (browser) { |
| await browser.close(); |
| } |
| } |
| |
| return pngBuffer; |
| } |
|
|
|
|
| |
| const EMOJI_STATUS_CACHE_DIR = './emoji_status'; |
| if (!fs.existsSync(EMOJI_STATUS_CACHE_DIR)) { |
| fs.mkdirSync(EMOJI_STATUS_CACHE_DIR); |
| } |
| const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); |
|
|
| async function getEmojiStatusBuffer(emojiId) { |
| const cachePath = `${EMOJI_STATUS_CACHE_DIR}/${emojiId}.png`; |
| |
| |
| if (fs.existsSync(cachePath)) { |
| return fs.readFileSync(cachePath); |
| } |
|
|
| const PROXY_URL = 'https://telegram-proxy.resident.workers.dev'; |
| const maxRetries = 6; |
|
|
| for (let attempt = 1; attempt <= maxRetries; attempt++) { |
| try { |
| |
| const axiosConfig = { |
| headers: { |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', |
| 'Accept': 'application/json', |
| 'Host': 'telegram-proxy.resident.workers.dev' |
| }, |
| timeout: 15000 |
| }; |
|
|
| |
| const stickerApiUrl = `${PROXY_URL}/bot${BOT_TOKEN}/getCustomEmojiStickers`; |
| const stickerResponse = await axios.post(stickerApiUrl, |
| { custom_emoji_ids: [emojiId] }, |
| axiosConfig |
| ); |
|
|
| const stickers = stickerResponse.data.result; |
| if (!stickers || stickers.length === 0) throw new Error("Emoji not found in Telegram."); |
|
|
| const sticker = stickers[0]; |
|
|
| |
| |
| |
| |
| |
| let targetFileId = null; |
| if (sticker.thumbnail && sticker.thumbnail.file_id) { |
| targetFileId = sticker.thumbnail.file_id; |
| } else if (sticker.thumb && sticker.thumb.file_id) { |
| targetFileId = sticker.thumb.file_id; |
| } else { |
| |
| if (sticker.is_video || sticker.is_animated) { |
| throw new Error("No static thumbnail found for animated/video emoji."); |
| } |
| targetFileId = sticker.file_id; |
| } |
|
|
| |
| const fileApiUrl = `${PROXY_URL}/bot${BOT_TOKEN}/getFile`; |
| const fileResponse = await axios.post(fileApiUrl, |
| { file_id: targetFileId }, |
| axiosConfig |
| ); |
| |
| const filePath = fileResponse.data.result.file_path; |
|
|
| |
| const fileUrl = `${PROXY_URL}/file/bot${BOT_TOKEN}/${filePath}`; |
| const imageResponse = await axios.get(fileUrl, { |
| ...axiosConfig, |
| responseType: 'arraybuffer' |
| }); |
|
|
| |
| |
| const pngBuffer = await sharp(imageResponse.data) |
| .resize(100, 100, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }) |
| .png() |
| .toBuffer(); |
|
|
| |
| fs.writeFileSync(cachePath, pngBuffer); |
| console.log(`✅ Successfully cached emoji: ${emojiId}`); |
| return pngBuffer; |
|
|
| } catch (error) { |
| const errorMsg = error.response ? JSON.stringify(error.response.data) : error.message; |
| console.warn(`[Attempt ${attempt}/${maxRetries}] Emoji Fetch Error: ${errorMsg}`); |
|
|
| if (attempt === maxRetries) { |
| console.error(`❌ Permanent failure for Emoji ID ${emojiId}. Returning null.`); |
| return null; |
| } |
|
|
| |
| await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); |
| } |
| } |
| return null; |
| } |
|
|
| function escapeHtml(text) { |
| if (!text) return ''; |
| |
| return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); |
| } |
|
|
| |
| function createTextChunkImageBuffer(text, { fontSize = 20, color = '#FFFFFF' }) { |
| const canvas = createCanvas(1, 1); |
| const ctx = canvas.getContext('2d'); |
| |
| ctx.font = `bold ${fontSize}px ${FONT_STACK}`; |
| const metrics = ctx.measureText(text); |
| const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; |
| |
| const padding = 0; |
| |
| |
| const canvasWidth = Math.max(1, metrics.width + 2 * padding); |
| const canvasHeight = Math.max(1, textHeight + 2 * padding); |
| |
| const textCanvas = createCanvas(canvasWidth, canvasHeight); |
| const textCtx = textCanvas.getContext('2d'); |
| textCtx.font = `bold ${fontSize}px ${FONT_STACK}`; |
| textCtx.fillStyle = color; |
| textCtx.textBaseline = 'alphabetic'; |
| |
| if (metrics.width > 0) { |
| textCtx.fillText(text, padding, metrics.actualBoundingBoxAscent + padding); |
| } |
| return textCanvas.toBuffer('image/png'); |
| } |
|
|
| |
| |
| function generateNameHtml(text, color, fontSize) { |
| const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; |
| |
| const chunks = text.split(/(\s+|(?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]))/).filter(Boolean); |
| let html = ''; |
|
|
| for (const chunk of chunks) { |
| if (chunk.match(emojiRegex)) { |
| |
| html += `<span class="name-emoji">${escapeHtml(chunk)}</span>`; |
| |
| |
| } else if (chunk.match(/^\s+$/)) { |
| |
| |
| html += `<span class="name-whitespace" style="white-space: pre;">${escapeHtml(chunk)}</span>`; |
| } else { |
| |
| const trimmedChunk = chunk.trim(); |
| if (trimmedChunk) { |
| const chunkImageBuffer = createTextChunkImageBuffer(trimmedChunk, { fontSize: fontSize, color: color }); |
| const chunkImageBase64 = `data:image/png;base64,${chunkImageBuffer.toString('base64')}`; |
| html += `<img class="name-chunk-image" src="${chunkImageBase64}" />`; |
| } |
| } |
| } |
| return html; |
| } |
|
|
|
|
| function wrapTextSmartly(text, maxWidth, font) { |
| |
| |
| const { createCanvas } = require('canvas'); |
| const canvas = createCanvas(1, 1); |
| const ctx = canvas.getContext('2d'); |
| ctx.font = font; |
|
|
| const sanitizedText = text.replace(/\u200B/g, ' ').trim(); |
| if (!sanitizedText) return ''; |
|
|
| const words = sanitizedText.split(/\s+/); |
| let line = ''; |
| let result = ''; |
|
|
| for (let n = 0; n < words.length; n++) { |
| const word = words[n]; |
| const testLine = (line ? line + ' ' : '') + word; |
| const metrics = ctx.measureText(testLine); |
| const testWidth = metrics.width; |
|
|
| if ((testWidth > maxWidth && line) || metrics.width > maxWidth) { |
| result += line.trim() + '\n'; |
| line = word + ' '; |
| } else { |
| line = testLine + ' '; |
| } |
| } |
| result += line.trim(); |
| return result; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function highlightTextPatterns(wrappedText) { |
| |
| const patternRegex = /(https?:\/\/[^\s]+|www\.[^\s]+|@\w+|\/\w+)/g; |
| |
| const parts = wrappedText.split(patternRegex).filter(p => p !== undefined && p !== null && p !== ''); |
|
|
| let outputHtml = ''; |
| const highlightColor = '#6ab8ed'; |
| |
| for (const part of parts) { |
| |
| if (part.match(/^(https?:\/\/[^\s]+|www\.[^\s]+|@\w+|(\/)\w+)$/)) { |
| |
| |
| const escapedContent = escapeHtml(part); |
| |
| outputHtml += `<span style="color: ${highlightColor}; text-decoration: underline;">${escapedContent}</span>`; |
| } else { |
| |
| |
| const escapedText = escapeHtml(part); |
| |
| |
| outputHtml += escapedText.replace(/\n/g, '<br/>'); |
| } |
| } |
| return outputHtml; |
| } |
|
|
|
|
| |
| async function createImage(firstName, lastName, customemojiid, message, nameColorId, inputImageBuffer, replySender, replyMessage,replysendercolor) { |
| const scale = 4; |
| const username = `${firstName} ${lastName}`.trim().replace(/\u200B/g, ''); |
| const nameColor = getTelegramDarkThemeColor(nameColorId); |
|
|
| |
| |
| const UNIVERSAL_FONT_SIZE = 26 * scale; |
| |
| |
| let messageFontSize = UNIVERSAL_FONT_SIZE; |
| let nameImageFontSize = UNIVERSAL_FONT_SIZE; |
| let replySenderFontSize = UNIVERSAL_FONT_SIZE; |
| let replyMessageFontSize = UNIVERSAL_FONT_SIZE; |
|
|
| |
| let nameEmojiFontSize = nameImageFontSize; |
|
|
| |
| let nameLineHeight = 34 * scale; |
| let nameMarginBottom = 12 * scale; |
| let messageLineHeight = 1.4; |
| const replyLineHeight = 1.3; |
| const replyMarginBottom = 10 * scale; |
| |
|
|
|
|
| |
| const DEFAULT_MESSAGE_MAX_WIDTH = 650 * scale; |
| const BUBBLE_MIN_WIDTH = 250 * scale; |
| const REPLY_MESSAGE_MAX_LENGTH = 50; |
| |
| |
| |
| const highlightedMessageHtml = highlightTextPatterns(message); |
| |
|
|
| |
| const nameContentHtml = generateNameHtml(username, nameColor, nameImageFontSize); |
|
|
| |
| let replySenderHtml = ''; |
| const replySenderColor = getTelegramDarkThemeColor(replysendercolor); |
| if (replySender) { |
| replySenderHtml = generateNameHtml(replySender.replace(/\u200B/g, ''), replySenderColor, replySenderFontSize); |
| } |
|
|
| |
| let processedReplyMessage = replyMessage; |
| if (replySender && processedReplyMessage && processedReplyMessage.length > REPLY_MESSAGE_MAX_LENGTH) { |
| |
| processedReplyMessage = processedReplyMessage.substring(0, REPLY_MESSAGE_MAX_LENGTH); |
| } |
|
|
| let avatarBuffer = inputImageBuffer ? await sharp(inputImageBuffer).png().toBuffer() : await createDummyAvatarBuffer(firstName, lastName, nameColor, scale); |
| const avatarBase64 = `data:image/png;base64,${avatarBuffer.toString('base64')}`; |
| const emojiStatusBuffer = customemojiid ? await getEmojiStatusBuffer(customemojiid) : null; |
| const emojiStatusBase64 = emojiStatusBuffer ? `data:image/png;base64,${emojiStatusBuffer.toString('base64')}` : null; |
|
|
| const htmlContent = ` |
| <html><head><style> |
| /* --- Font-face rules removed --- */ |
| |
| body { |
| margin: 0; |
| padding: ${30 * scale}px; /* Body padding for spacing */ |
| font-family: ${FONT_STACK}, sans-serif; /* Use the full font stack */ |
| display: flex; |
| justify-content: flex-start; /* Align content to start */ |
| align-items: flex-start; |
| min-height: 100vh; |
| background-color: transparent; |
| } |
| |
| .container { |
| display: flex; |
| align-items: flex-end; |
| } |
| |
| .avatar { |
| width: ${70 * scale}px; |
| height: ${70 * scale}px; |
| border-radius: 50%; |
| margin-right: ${15 * scale}px; |
| flex-shrink: 0; |
| object-fit: cover; |
| } |
| |
| .bubble { |
| background-color: #2a2233; |
| border-radius: ${20 * scale}px ${20 * scale}px ${20 * scale}px 0; |
| padding: ${18 * scale}px ${25 * scale}px ${12 * scale}px ${25 * scale}px; |
| position: relative; |
| min-width: ${BUBBLE_MIN_WIDTH}px; /* Ensure bubble isn't too small */ |
| box-sizing: border-box; |
| flex-grow: 1; |
| display: flex; |
| flex-direction: column; |
| align-items: flex-start; |
| } |
| |
| .bubble::before { |
| content: ''; |
| position: absolute; |
| bottom: 0; |
| left: -${20 * scale}px; |
| width: ${20 * scale}px; |
| height: ${20 * scale}px; |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='%232a2233' d='M20 0 V20 H0 C10 20 20 10 20 0 Z'/%3E%3C/svg%3E"); |
| background-size: contain; |
| background-repeat: no-repeat; |
| } |
| |
| /* FIX: Remove flex, rely on inline-block alignment */ |
| .name-line { |
| display: flex; |
| align-items: center; /* Vertically center all items */ |
| margin-bottom: ${nameMarginBottom}px; |
| min-height: ${nameLineHeight}px; |
| font-size: ${nameEmojiFontSize}px; /* Base size for 'em' unit */ |
| font-weight: bold; |
| color: ${nameColor}; |
| line-height: 1; |
| white-space: nowrap; |
| } |
| |
| /* * This targets all children (img, span) to make |
| * them align properly as flex items. |
| */ |
| .name-line > * { |
| display: block; /* Make them blocks for flex alignment */ |
| } |
| |
| /* * KEY FIX: Set the max-height of the text-image to 1em |
| * This will scale it down to match the font-size. |
| */ |
| .name-line > .name-chunk-image { |
| max-height: 1em; /* 1em = ${nameEmojiFontSize}px */ |
| width: auto; /* Let width scale with aspect ratio */ |
| } |
| |
| /* FIX: Use inline-block and vertical-align. Remove forced height. */ |
| .name-chunk-image, .name-emoji { |
| /* height: 1em; REMOVED */ |
| display: inline-block; |
| vertical-align: middle; |
| } |
| .name-emoji { |
| /* No specific rules needed */ |
| } |
| |
| /* FIX: Add this new rule for the whitespace span */ |
| .name-whitespace { |
| display: inline-block; |
| vertical-align: middle; |
| } |
| |
| /* This targets the <img> tags generated by generateNameHtml for the name */ |
| .name-line .name-chunk-image { |
| /* height: ${nameImageFontSize}px !important; REMOVED -- THIS FIXES THE SIZE */ |
| } |
| |
| .emoji-status { |
| width: ${nameEmojiFontSize * 1.5}px; |
| height: ${nameEmojiFontSize * 1.5}px; |
| margin-left: ${8 * scale}px; /* Keep margin for status emoji */ |
| vertical-align: middle; |
| border-radius:15%; |
| display: inline-block; /* Keep for status emoji alignment */ |
| } |
| |
| .message { |
| font-size: ${messageFontSize}px; /* UNIVERSAL SIZE */ |
| line-height: ${messageLineHeight}; |
| color: #fefcff; |
| word-break: break-word; |
| padding-bottom: ${10 * scale}px; |
| text-align: left; |
| /* max-width is now set dynamically in Puppeteer */ |
| width: 100%; |
| box-sizing: border-box; |
| } |
| |
| .reply { |
| background-color: ${replySenderColor}10; |
| border-radius: ${10 * scale}px; |
| position: relative; |
| padding-left: ${12 * scale}px; |
| padding-top: ${8 * scale}px; |
| padding-bottom: ${8 * scale}px; |
| padding-right: ${10 * scale}px; |
| margin-bottom: ${replyMarginBottom}px; |
| display: flex; |
| flex-direction: column; |
| align-items: flex-start; |
| width: 100%; |
| box-sizing: border-box; |
| /* gap removed */ |
| } |
| |
| .reply::before { |
| content: ''; |
| position: absolute; |
| left: ${4 * scale}px; |
| background-color: ${replySenderColor}; |
| border-radius: ${2 * scale}px; |
| width: ${4 * scale}px; |
| top: ${8 * scale}px; |
| bottom: ${8 * scale}px; |
| } |
| |
| /* FIX: Remove flex, rely on inline-block alignment */ |
| .reply-sender { |
| display: flex; |
| align-items: center; |
| font-size: ${replySenderFontSize}px; /* Base size for 'em' unit */ |
| font-weight: bold; |
| color: ${replySenderColor}; |
| margin-bottom: ${4 * scale}px; |
| line-height: ${replyLineHeight}; |
| text-align: left; |
| padding-left: ${10 * scale}px; |
| white-space: nowrap; |
| } |
| |
| /* * Make all children of reply-sender align properly |
| */ |
| .reply-sender > * { |
| display: block; |
| } |
| |
| /* * KEY FIX (Repeated): Scale the reply sender's |
| * text-image down to match the font-size. |
| */ |
| .reply-sender > .name-chunk-image { |
| max-height: 1em; |
| width: auto; |
| } |
| |
| /* FIX: Use inline-block and vertical-align. Remove forced height. */ |
| .reply-sender .name-chunk-image, .reply-sender .name-emoji { |
| /* height: 1em; REMOVED */ |
| display: inline-block; |
| vertical-align: middle; |
| } |
| |
| .reply-sender .name-chunk-image { |
| /* height: ${replySenderFontSize}px !important; REMOVED -- THIS FIXES THE SIZE */ |
| } |
| |
| .reply-message { |
| font-size: ${replyMessageFontSize}px; /* UNIVERSAL SIZE */ |
| line-height: ${replyLineHeight}; |
| color: #b0b0b0; |
| white-space: nowrap; |
| overflow: hidden; |
| /* Replaced ellipsis with a fade-out gradient mask */ |
| -webkit-mask-image: linear-gradient(to right, black 90%, transparent 100%); |
| mask-image: linear-gradient(to right, black 90%, transparent 100%); |
| text-align: left; |
| padding-left: ${10 * scale}px; |
| width: 100%; |
| box-sizing: border-box; |
| } |
| </style></head> |
| <body><div class="container" id="capture"> |
| <img src="${avatarBase64}" class="avatar" /> |
| <div class="bubble"> |
| |
| <div class="name-line"> |
| ${nameContentHtml} |
| ${emojiStatusBase64 ? `<img src="${emojiStatusBase64}" class="emoji-status" />` : ''} |
| </div> |
| |
| ${replySender && replyMessage ? ` |
| <div class="reply"> |
| |
| <div class="reply-sender">${replySenderHtml}</div> |
| <div class="reply-message">${escapeHtml(processedReplyMessage)}</div> |
| </div> |
| ` : ''} |
| <div class="message">${highlightedMessageHtml}</div> |
| </div> |
| </div></body></html>`; |
|
|
| |
| const AVATAR_WIDTH = 70 * scale; |
| const AVATAR_MARGIN_RIGHT = 15 * scale; |
| const BUBBLE_PADDING_HORIZONTAL = (25 + 25) * scale; |
| const BUBBLE_TAIL_WIDTH = 20 * scale; |
| const BODY_PADDING_HORIZONTAL = (30 + 30) * scale; |
|
|
| const ESTIMATED_MAX_NAME_WIDTH = DEFAULT_MESSAGE_MAX_WIDTH * 1.5; |
| |
| const VIEWPORT_WIDTH = BODY_PADDING_HORIZONTAL + AVATAR_WIDTH + AVATAR_MARGIN_RIGHT + BUBBLE_TAIL_WIDTH + Math.max(ESTIMATED_MAX_NAME_WIDTH, DEFAULT_MESSAGE_MAX_WIDTH + BUBBLE_PADDING_HORIZONTAL) + (50 * scale); |
| const VIEWPORT_HEIGHT = 1200 * scale; |
|
|
| const browser = await puppeteer.launch({ headless: true,executablePath: '/usr/bin/google-chrome', args: ['--no-sandbox', '--disable-gpu','--no-proxy-server'] }); |
| const page = await browser.newPage(); |
| await page.setViewport({ width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT }); |
| |
| await page.setContent(htmlContent, { waitUntil: 'domcontentloaded' }); |
|
|
| |
| await page.evaluate((defaultMessageWidth) => { |
| |
| const nameWidth = document.querySelector('.name-line')?.scrollWidth || 0; |
| |
| const replySenderElement = document.querySelector('.reply-sender'); |
| const replyWidth = replySenderElement?.scrollWidth || 0; |
| |
| |
| const contentWidth = Math.max(nameWidth, replyWidth); |
|
|
| |
| const newMaxWidth = Math.max(contentWidth, defaultMessageWidth); |
|
|
| const messageElement = document.querySelector('.message'); |
| if (messageElement) { |
| messageElement.style.maxWidth = newMaxWidth + 'px'; |
| } |
| }, DEFAULT_MESSAGE_MAX_WIDTH); |
|
|
| const element = await page.$('#capture'); |
| const finalPngBuffer = await element.screenshot({ omitBackground: true }); |
| await browser.close(); |
|
|
| |
|
|
| |
| const stickerWidth = 2048; |
| |
| const scaledPngBuffer = await sharp(finalPngBuffer) |
| .resize({ width: stickerWidth, fit: 'inside', withoutEnlargement: true }) |
| .toBuffer(); |
|
|
| const scaledMetadata = await sharp(scaledPngBuffer).metadata(); |
| const bubbleWidth = scaledMetadata.width || 0; |
|
|
| const padding = Math.floor((stickerWidth - bubbleWidth) / 2); |
| |
| const webpBuffer = await sharp(scaledPngBuffer) |
| .extend({ |
| top: 0, |
| bottom: 0, |
| left: padding > 0 ? padding : 0, |
| right: padding > 0 ? padding : 0, |
| background: { r: 0, g: 0, b: 0, alpha: 0 } |
| }) |
| .webp({ quality: 90 }) |
| .toBuffer(); |
| |
| return webpBuffer; |
| } |
|
|
| module.exports = createImage; |