qc-generator-whatsapp
Version:
Advanced generator for dynamic quote images and animations for WhatsApp
413 lines (404 loc) • 17.5 kB
JavaScript
const fs = require("fs");
const path = require("path");
const sharp = require("sharp");
const EmojiDbLib = require("emoji-db");
const ffmpeg = require('fluent-ffmpeg');
const { createCanvas, loadImage } = require('canvas');
const emojiImageByBrandPromise = require("emoji-cache");
let emojiDb;
try {
emojiDb = new EmojiDbLib({ useDefaultDb: true });
if (!emojiDb || typeof emojiDb.searchFromText !== 'function') throw new Error('Failed to initialize emoji database');
} catch (error) {
console.error('Error initializing emoji database:', error);
throw error;
}
function randomChoice(arr) {
try {
if (!Array.isArray(arr)) throw new TypeError('Input must be an array');
if (arr.length === 0) throw new Error('Array cannot be empty');
return arr[Math.floor(Math.random() * arr.length)];
} catch (error) {
console.error('Error in randomChoice: ', error);
throw error;
}
}
function isHighlighted(highlightWords, word) {
try {
if (!Array.isArray(highlightWords)) throw new TypeError('highlightWords must be an array');
if (typeof word !== 'string') throw new TypeError('word must be a string');
return highlightWords.includes(word.toLowerCase());
} catch (error) {
console.error('Error in isHighlighted: ', error);
throw error;
}
}
function parseTextToSegments(text, ctx, fontSize) {
try {
if (typeof text !== 'string') throw new TypeError('Text must be a string');
if (typeof fontSize !== 'number' || fontSize <= 0) throw new TypeError('Font size must be a positive number');
if (!ctx || typeof ctx.measureText !== 'function') throw new TypeError('Invalid canvas context');
const segments = [];
const emojiSize = fontSize * 1.2;
const emojiData = emojiDb.searchFromText({ input: text, fixCodePoints: true });
let currentIndex = 0;
const processPlainText = (plainText) => {
if (!plainText) return;
const tokenizerRegex = /\*([^*]+)\*|(\s+)|([^\s*]+)/g;
let match;
while ((match = tokenizerRegex.exec(plainText)) !== null) {
const [fullMatch, boldContent, whitespaceContent, textContent] = match;
if (boldContent) {
ctx.font = `bold ${fontSize}px Arial`;
segments.push({
type: 'bold',
content: boldContent,
width: ctx.measureText(boldContent).width
});
ctx.font = `${fontSize}px Arial`;
} else if (whitespaceContent) {
segments.push({
type: 'whitespace',
content: ' ',
width: ctx.measureText(' ').width * whitespaceContent.length
});
} else if (textContent) {
segments.push({
type: 'text',
content: textContent,
width: ctx.measureText(textContent).width
});
}
}
};
emojiData.forEach(emojiInfo => {
if (emojiInfo.offset > currentIndex) {
const plainText = text.substring(currentIndex, emojiInfo.offset);
processPlainText(plainText);
}
segments.push({
type: 'emoji',
content: emojiInfo.found,
width: emojiSize,
});
currentIndex = emojiInfo.offset + emojiInfo.length;
});
if (currentIndex < text.length) {
const remainingText = text.substring(currentIndex);
processPlainText(remainingText);
}
return segments;
} catch (error) {
console.error('Error in parseTextToSegments:', error);
throw error;
}
}
function rebuildLinesFromSegments(segments, maxWidth) {
try {
if (!Array.isArray(segments)) {
throw new TypeError('Segments must be an array');
}
if (typeof maxWidth !== 'number' || maxWidth <= 0) {
throw new TypeError('Max width must be a positive number');
}
const lines = [];
if (segments.length === 0) return lines;
let currentLine = [];
let currentLineWidth = 0;
segments.forEach(segment => {
if (!segment || typeof segment.width !== 'number') throw new TypeError('Invalid segment format');
if (currentLineWidth + segment.width > maxWidth && currentLine.length > 0) {
lines.push(currentLine);
currentLine = [];
currentLineWidth = 0;
}
if (segment.type === 'whitespace' && currentLine.length === 0) {
return;
}
currentLine.push(segment);
currentLineWidth += segment.width;
});
if (currentLine.length > 0) {
lines.push(currentLine);
}
return lines;
} catch (error) {
console.error('Error in rebuildLinesFromSegments: ', error);
throw error;
}
}
function generateAnimatedBratVid(tempFrameDir, outputPath) {
return new Promise((resolve, reject) => {
try {
if (typeof tempFrameDir !== 'string' || typeof outputPath !== 'string') {
throw new TypeError('Directory and path must be strings');
}
if (!fs.existsSync(tempFrameDir)) {
throw new Error(`Temporary frame directory not found: ${tempFrameDir}`);
}
const command = ffmpeg()
.input(path.join(tempFrameDir, 'frame_%d.png'))
.inputOptions('-framerate', '1.5')
.outputOptions('-vf', 'scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:(ow-iw)/2:(oh-ih)/2')
.output(outputPath)
.videoCodec('libwebp')
.outputOptions('-loop', '0', '-q:v', '80', '-preset', 'default', '-an')
.on('end', () => {
resolve();
})
.on('error', (err) => {
console.error('Error while processing video:', err);
reject(err);
});
command.run();
} catch (error) {
console.error('Error in generateAnimatedBratVid:', error);
reject(error);
}
});
}
async function bratVidGenerator(text, width, height, bgColor = "#FFFFFF", textColor = "#000000", highlightWords = []) {
try {
if (typeof text !== 'string' || text.trim().length === 0) {
throw new Error('Text must be a non-empty string');
}
if (!Array.isArray(highlightWords)) {
throw new TypeError('highlightWords must be an array.');
}
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
throw new Error('Width and height must be positive integers');
}
if (!/^#[0-9A-F]{6}$/i.test(bgColor) || !/^#[0-9A-F]{6}$/i.test(textColor)) {
throw new Error('Colors must be in hex format (#RRGGBB)');
}
const allEmojiImages = await emojiImageByBrandPromise;
const emojiCache = allEmojiImages["apple"] || {};
const padding = 20;
const availableWidth = width - (padding * 2);
const tempCanvas = createCanvas(1, 1);
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) throw new Error('Failed to create canvas context');
const allSegments = parseTextToSegments(text, tempCtx, 100).filter(seg => seg.type !== 'whitespace');
if (allSegments.length === 0) throw new Error('No valid content segments found in the text');
let frames = [];
for (let segmentCount = 1; segmentCount <= allSegments.length; segmentCount++) {
const currentSegments = allSegments.slice(0, segmentCount);
let fontSize = 200;
let finalLines = [];
let lineHeight = 0;
const lineHeightMultiplier = 1.3;
let sizeFound = false;
while (fontSize > 10) {
tempCtx.font = `${fontSize}px Arial`;
const segmentsForSizing = currentSegments.map(seg => {
if (seg.type === 'text') {
return { ...seg, width: tempCtx.measureText(seg.content).width };
}
if (seg.type === 'bold') {
tempCtx.font = `bold ${fontSize}px Arial`;
const boldWidth = tempCtx.measureText(seg.content).width;
tempCtx.font = `${fontSize}px Arial`;
return { ...seg, width: boldWidth };
}
if (seg.type === 'emoji') {
return { ...seg, width: fontSize * 1.2 };
}
return seg;
});
const lines = rebuildLinesFromSegments(segmentsForSizing, availableWidth);
let isTooWide = lines.some(line => line.reduce((sum, seg) => sum + seg.width, 0) > availableWidth);
const currentLineHeight = fontSize * lineHeightMultiplier;
const totalTextHeight = lines.length * currentLineHeight;
if (totalTextHeight <= height - (padding * 2) && !isTooWide) {
finalLines = lines;
lineHeight = currentLineHeight;
sizeFound = true;
break;
}
fontSize -= 2;
}
if (!sizeFound) {
fontSize = 10;
tempCtx.font = `${fontSize}px Arial`;
const segmentsForSizing = currentSegments.map(seg => {
if (seg.type === 'text') return { ...seg, width: tempCtx.measureText(seg.content).width };
if (seg.type === 'bold') {
tempCtx.font = `bold ${fontSize}px Arial`;
const boldWidth = tempCtx.measureText(seg.content).width;
tempCtx.font = `${fontSize}px Arial`;
return { ...seg, width: boldWidth };
}
if (seg.type === 'emoji') return { ...seg, width: fontSize * 1.2 };
return seg;
});
finalLines = rebuildLinesFromSegments(segmentsForSizing, availableWidth);
lineHeight = fontSize * lineHeightMultiplier;
}
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Failed to create canvas context');
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
ctx.textBaseline = 'top';
const totalTextBlockHeight = finalLines.length * lineHeight;
const startY = (height - totalTextBlockHeight) / 2;
for (let j = 0; j < finalLines.length; j++) {
const line = finalLines[j];
const positionY = startY + (j * lineHeight);
const contentSegments = line.filter(seg => seg.type !== 'whitespace');
if (contentSegments.length <= 1) {
let positionX = padding;
for (const segment of line) {
ctx.font = segment.type === 'bold' ? `bold ${fontSize}px Arial` : `${fontSize}px Arial`;
ctx.fillStyle = isHighlighted(highlightWords, segment.content) ? "red" : textColor;
if (segment.type === 'text' || segment.type === 'bold') {
ctx.fillText(segment.content, positionX, positionY);
} else if (segment.type === 'emoji') {
const emojiSize = fontSize * 1.2;
const emojiY = positionY + (lineHeight - emojiSize) / 2;
if (!emojiCache[segment.content]) throw new Error(`Emoji ${segment.content} tidak ditemukan di cache`);
const emojiImg = await loadImage(Buffer.from(emojiCache[segment.content], 'base64'));
ctx.drawImage(emojiImg, positionX, emojiY, emojiSize, emojiSize);
}
positionX += segment.width;
}
} else {
const totalContentWidth = contentSegments.reduce((sum, seg) => sum + seg.width, 0);
const numberOfGaps = contentSegments.length - 1;
const spaceBetween = numberOfGaps > 0 ? (availableWidth - totalContentWidth) / numberOfGaps : 0;
let positionX = padding;
for (const segment of line) {
ctx.font = segment.type === 'bold' ? `bold ${fontSize}px Arial` : `${fontSize}px Arial`;
ctx.fillStyle = isHighlighted(highlightWords, segment.content) ? "red" : textColor;
if (segment.type === 'text' || segment.type === 'bold') {
ctx.fillText(segment.content, positionX, positionY);
positionX += segment.width;
} else if (segment.type === 'emoji') {
const emojiSize = fontSize * 1.2;
const emojiY = positionY + (lineHeight - emojiSize) / 2;
if (!emojiCache[segment.content]) throw new Error(`Emoji ${segment.content} tidak ditemukan di cache`);
const emojiImg = await loadImage(Buffer.from(emojiCache[segment.content], 'base64'));
ctx.drawImage(emojiImg, positionX, emojiY, emojiSize, emojiSize);
positionX += segment.width;
}
positionX += spaceBetween;
}
}
}
const buffer = canvas.toBuffer('image/png');
const blurredBuffer = await sharp(buffer).blur(3).toBuffer();
frames.push(blurredBuffer);
}
return frames;
} catch (error) {
console.error('Error in bratVidGenerator:', error);
throw error;
}
}
async function bratGenerator(teks, highlightWords = []) {
try {
if (typeof teks !== 'string' || teks.trim().length === 0) throw new Error('Teks tidak boleh kosong.');
if (!Array.isArray(highlightWords)) throw new TypeError('highlightWords harus berupa array.');
const allEmojiImages = await emojiImageByBrandPromise;
const emojiCache = allEmojiImages["apple"] || {};
let width = 512;
let height = 512;
let margin = 8;
let verticalPadding = 8;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error('Gagal membuat konteks kanvas.');
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
let fontSize = 200;
let lineHeightMultiplier = 1.3;
ctx.textAlign = "left";
ctx.textBaseline = "top";
const availableWidth = width - 2 * margin;
let finalLines = [];
let finalFontSize = 0;
let lineHeight = 0;
while (fontSize > 10) {
ctx.font = `${fontSize}px Arial`;
const segments = parseTextToSegments(teks, ctx, fontSize);
const lines = rebuildLinesFromSegments(segments, availableWidth);
let isTooWide = lines.some(line => line.reduce((sum, seg) => sum + seg.width, 0) > availableWidth);
const currentLineHeight = fontSize * lineHeightMultiplier;
const totalTextHeight = lines.length * currentLineHeight;
if (totalTextHeight <= height - 2 * verticalPadding && !isTooWide) {
finalLines = lines;
finalFontSize = fontSize;
lineHeight = currentLineHeight;
break;
}
fontSize -= 2;
}
if (finalLines.length === 1 && finalLines[0].length === 1 && finalLines[0][0].type === 'text') {
const theOnlyWord = finalLines[0][0].content;
const heightBasedSize = (height - 2 * verticalPadding) / lineHeightMultiplier;
ctx.font = `200px Arial`;
const referenceWidth = ctx.measureText(theOnlyWord).width;
const widthBasedSize = (availableWidth / referenceWidth) * 200;
finalFontSize = Math.floor(Math.min(heightBasedSize, widthBasedSize));
lineHeight = finalFontSize * lineHeightMultiplier;
}
const totalFinalHeight = finalLines.length * lineHeight;
let y = (finalLines.length === 1) ? verticalPadding : (height - totalFinalHeight) / 2;
for (const line of finalLines) {
let x = margin;
const contentSegments = line.filter(seg => seg.type !== 'whitespace');
if (contentSegments.length <= 1) {
for (const segment of line) {
ctx.font = segment.type === 'bold' ? `bold ${finalFontSize}px Arial` : `${finalFontSize}px Arial`;
ctx.fillStyle = isHighlighted(highlightWords, segment.content) ? "red" : "black";
if (segment.type === 'text' || segment.type === 'bold') {
ctx.fillText(segment.content, x, y);
} else if (segment.type === 'emoji') {
const emojiSize = finalFontSize * 1.2;
const emojiY = y + (lineHeight - emojiSize) / 2;
if (!emojiCache[segment.content]) throw new Error(`Emoji ${segment.content} tidak ditemukan di cache`);
const emojiImg = await loadImage(Buffer.from(emojiCache[segment.content], 'base64'));
ctx.drawImage(emojiImg, x, emojiY, emojiSize, emojiSize);
}
x += segment.width;
}
} else {
const totalContentWidth = contentSegments.reduce((sum, seg) => sum + seg.width, 0);
const numberOfGaps = contentSegments.length - 1;
const spacePerGap = (availableWidth - totalContentWidth) / numberOfGaps;
let currentX = margin;
for (let i = 0; i < contentSegments.length; i++) {
const segment = contentSegments[i];
ctx.font = segment.type === 'bold' ? `bold ${finalFontSize}px Arial` : `${finalFontSize}px Arial`;
ctx.fillStyle = isHighlighted(highlightWords, segment.content) ? "red" : "black";
if (segment.type === 'text' || segment.type === 'bold') {
ctx.fillText(segment.content, currentX, y);
} else if (segment.type === 'emoji') {
const emojiSize = finalFontSize * 1.2;
const emojiY = y + (lineHeight - emojiSize) / 2;
if (!emojiCache[segment.content]) throw new Error(`Emoji ${segment.content} tidak ditemukan di cache`);
const emojiImg = await loadImage(Buffer.from(emojiCache[segment.content], 'base64'));
ctx.drawImage(emojiImg, currentX, emojiY, emojiSize, emojiSize);
}
currentX += segment.width;
if (i < numberOfGaps) {
currentX += spacePerGap;
}
}
}
y += lineHeight;
}
const buffer = canvas.toBuffer("image/png");
const blurredBuffer = await sharp(buffer).blur(3).toBuffer();
return blurredBuffer;
} catch (error) {
console.error('Terjadi error di bratGenerator:', error);
return error.message;
}
}
module.exports = {
randomChoice,
bratGenerator,
bratVidGenerator,
generateAnimatedBratVid
};