javascript
501 lines ยท 18,542 chars
// Author IQC : Ditzzx & Rin
// Sumber : https://whatsapp.com/channel/0029VbCHRSDAzNboLatr0W0o
import { createCanvas, loadImage, GlobalFonts } from '@napi-rs/canvas';
import { writeFile, readFile, mkdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import fs from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pluginConfig = {
name: "iqcpink",
alias: [],
category: "canvas",
description: "Membuat Fake Quote iOS style versi pink.",
usage: ".iqcpink [text/reply]",
isOwner: false,
isPremium: false,
isGroup: false,
isPrivate: false,
cooldown: 5,
energi: 2,
isEnabled: true,
};
let fontsLoaded = false;
const BG_URL = "https://files.catbox.moe/2fsv03.png";
let bgImgBuffer = null;
const INTER_FONTS = [
{ url: 'https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2', file: 'Inter-Regular.ttf' },
{ url: 'https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fAZ9hiJ-Ek-_EeA.woff2', file: 'Inter-Medium.ttf' },
{ url: 'https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hiJ-Ek-_EeA.woff2', file: 'Inter-SemiBold.ttf' },
];
const APPLE_EMOJI_JSON_URL = 'https://media.githubusercontent.com/media/Ditzzx-vibecoder/entahlah/main/emoji-apple.json';
const BG_W = 906;
const BG_H = 1736;
const SX = BG_W / 1080;
const SY = BG_H / 2280;
const state = {
text: "Kesendirian adalah teman terbaik ku๐๐",
time: "22.54",
bubbleColor: "#ffc5d5",
textColor: "#111111",
timeColor: "#5e4146",
tickColor: "#8c1d2c",
fontSize: Math.round(45 * SX),
bubbleWidth: Math.round(746 * SX),
showReaction: true,
emojiSize: Math.round(90 * SX),
emojiSpacing: Math.round(110 * SX),
emojiXOffset: Math.round(15 * SX),
emojiYOffset: -15,
reactionScale: 1.0,
emojis: ["๐", "โค๏ธ", "๐", "๐ฎ", "๐ข", "๐"],
offsetX: 20,
offsetY: 0,
};
let appleEmojiMap = null;
async function downloadFile(url) {
const axios = (await import('axios')).default;
const res = await axios.get(url, {
responseType: 'arraybuffer',
headers: { 'User-Agent': 'Mozilla/5.0' },
maxRedirects: 5,
});
return Buffer.from(res.data);
}
function emojiToUnicode(emoji) {
return [...emoji].map(c => c.codePointAt(0).toString(16)).join('-');
}
async function loadAppleEmojiMap() {
if (appleEmojiMap) return appleEmojiMap;
const buf = await downloadFile(APPLE_EMOJI_JSON_URL);
appleEmojiMap = JSON.parse(buf.toString('utf-8'));
return appleEmojiMap;
}
async function drawAppleEmoji(ctx, emoji, x, y, size) {
const map = await loadAppleEmojiMap();
const base = emojiToUnicode(emoji);
const variants = [
base,
base.replace(/-fe0f/g, ''),
base.toUpperCase(),
base.replace(/-fe0f/g, '').toUpperCase(),
];
let b64 = null;
for (const v of variants) {
if (map[v]) { b64 = map[v]; break; }
}
if (!b64) {
ctx.fillText(emoji, x, y);
return;
}
const buf = Buffer.from(b64, 'base64');
const img = await loadImage(buf);
ctx.drawImage(img, x - size / 2, y - size / 2, size, size);
}
async function ensureAssets() {
if (!fontsLoaded) {
for (const f of INTER_FONTS) {
const buf = await downloadFile(f.url);
GlobalFonts.register(buf, 'Inter');
}
fontsLoaded = true;
}
await loadAppleEmojiMap();
if (!bgImgBuffer) {
bgImgBuffer = await downloadFile(BG_URL);
}
}
function drawRoundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function measureTextCustom(ctx, text, fontSize) {
const parts = text.split(/(\p{Extended_Pictographic})/gu);
let totalWidth = 0;
for (const part of parts) {
if (!part) continue;
if (/\p{Extended_Pictographic}/u.test(part)) {
totalWidth += fontSize * 1.05;
} else {
totalWidth += ctx.measureText(part).width;
}
}
return totalWidth;
}
async function drawTextWithEmojis(ctx, text, x, y, fontSize) {
const parts = text.split(/(\p{Extended_Pictographic})/gu);
let currentX = x;
for (const part of parts) {
if (!part) continue;
if (/\p{Extended_Pictographic}/u.test(part)) {
const emojiSize = fontSize * 1.05;
const emojiCX = currentX + emojiSize / 2;
const emojiCY = y;
await drawAppleEmoji(ctx, part, emojiCX, emojiCY, emojiSize);
currentX += emojiSize;
} else {
ctx.fillText(part, currentX, y);
currentX += ctx.measureText(part).width;
}
}
}
function wrapText(ctx, text, maxWidth, fontSize) {
ctx.font = `500 ${fontSize}px Inter`;
const words = text.split(" ");
const lines = [];
let cur = "";
for (let i = 0; i < words.length; i++) {
const word = words[i];
if (word.includes('\n')) {
const parts = word.split('\n');
for (let j = 0; j < parts.length; j++) {
const test = cur + (cur ? " " : "") + parts[j];
if (measureTextCustom(ctx, test, fontSize) > maxWidth && cur) {
lines.push(cur); cur = parts[j];
} else { cur = test; }
if (j < parts.length - 1) { lines.push(cur); cur = ""; }
}
continue;
}
const test = cur + (cur ? " " : "") + word;
if (measureTextCustom(ctx, test, fontSize) > maxWidth && i > 0) {
lines.push(cur); cur = word;
} else { cur = test; }
}
if (cur) lines.push(cur);
return lines;
}
async function render(text, time, imgUrl, outputPath) {
await ensureAssets();
const s = { ...state, text: text ?? state.text, time: time ?? state.time };
let imgObj = null;
if (imgUrl) {
imgObj = await loadImage(imgUrl);
}
const canvas = createCanvas(BG_W, BG_H);
const ctx = canvas.getContext('2d');
const bgImg = await loadImage(bgImgBuffer);
ctx.drawImage(bgImg, 0, 0, BG_W, BG_H);
const rightPadding = Math.round(80 * SX);
const textPaddingX = Math.round(36 * SX);
const paddingTop = Math.round(28 * SY);
const paddingBottom = Math.round(28 * SY);
const bRadius = Math.round(32 * SX);
const menuTopBorderY = Math.round(1276 * SY);
const timeFontSize = Math.round(23 * SX);
ctx.font = `600 ${timeFontSize}px Inter`;
const timeMetrics = ctx.measureText(s.time);
const ticksWidth = Math.round(34 * SX);
const timestampWidth = timeMetrics.width + ticksWidth + Math.round(12 * SX);
const timestampHeight = timeFontSize;
const textLimitW = s.bubbleWidth - (textPaddingX * 2);
ctx.font = `500 ${s.fontSize}px Inter`;
let textLines = [];
if (s.text) textLines = wrapText(ctx, s.text, textLimitW, s.fontSize);
const lineWidths = textLines.map(line => measureTextCustom(ctx, line, s.fontSize));
const maxLineWidth = Math.max(...lineWidths, 0);
let bubbleActualW = 0;
let timestampOnNewRow = false;
const minBubbleW = Math.round(280 * SX);
let imgDrawH = 0;
if (imgObj) {
const imgAspect = imgObj.width / imgObj.height;
bubbleActualW = Math.min(Math.max(imgObj.width, minBubbleW), s.bubbleWidth);
if (s.text && textLines.length > 0) {
bubbleActualW = Math.max(bubbleActualW, maxLineWidth + (textPaddingX * 2));
}
imgDrawH = Math.round(bubbleActualW / imgAspect);
} else {
if (textLines.length === 1) {
bubbleActualW = maxLineWidth + (textPaddingX * 2) + timestampWidth + Math.round(35 * SX);
} else if (textLines.length > 1) {
const lastLineWidth = lineWidths[textLines.length - 1] || 0;
if (lastLineWidth + timestampWidth + Math.round(35 * SX) <= maxLineWidth) {
bubbleActualW = maxLineWidth + (textPaddingX * 2);
} else if (lastLineWidth + timestampWidth + Math.round(35 * SX) <= textLimitW) {
bubbleActualW = lastLineWidth + timestampWidth + Math.round(35 * SX) + (textPaddingX * 2);
} else {
bubbleActualW = maxLineWidth + (textPaddingX * 2);
timestampOnNewRow = true;
}
}
}
if (bubbleActualW < minBubbleW) bubbleActualW = minBubbleW;
if (bubbleActualW > s.bubbleWidth) bubbleActualW = s.bubbleWidth;
if (imgObj && bubbleActualW < timestampWidth + Math.round(50 * SX)) bubbleActualW = timestampWidth + Math.round(50 * SX);
const bubbleX = BG_W - bubbleActualW - rightPadding;
const lineGap = Math.round(12 * SY);
const textTotalHeight = (textLines.length * s.fontSize) + (Math.max(0, textLines.length - 1) * lineGap);
let bubbleHeight = 0;
if (timestampOnNewRow || !s.text) {
bubbleHeight = paddingTop + textTotalHeight + Math.round(16 * SY) + timestampHeight + paddingBottom;
} else {
bubbleHeight = paddingTop + textTotalHeight + paddingBottom;
}
if (imgObj) {
bubbleHeight += imgDrawH;
if (s.text) bubbleHeight += Math.round(12 * SY);
}
const currentBubbleY = menuTopBorderY - bubbleHeight - Math.round(28 * SY);
ctx.save();
ctx.translate(s.offsetX, s.offsetY);
ctx.save();
ctx.shadowColor = "rgba(0,0,0,0.05)";
ctx.shadowBlur = 20;
ctx.shadowOffsetY = 6;
ctx.fillStyle = s.bubbleColor;
drawRoundedRect(ctx, bubbleX, currentBubbleY, bubbleActualW, bubbleHeight, bRadius);
ctx.fill();
ctx.restore();
if (imgObj) {
ctx.save();
drawRoundedRect(ctx, bubbleX, currentBubbleY, bubbleActualW, bubbleHeight, bRadius);
ctx.clip();
ctx.drawImage(imgObj, bubbleX, currentBubbleY, bubbleActualW, imgDrawH);
ctx.restore();
}
ctx.save();
ctx.beginPath();
ctx.moveTo(bubbleX + bubbleActualW - Math.round(15 * SX), currentBubbleY + bubbleHeight - 5);
ctx.lineTo(bubbleX + bubbleActualW + Math.round(10 * SX), currentBubbleY + bubbleHeight - 5);
ctx.quadraticCurveTo(
bubbleX + bubbleActualW + Math.round(2 * SX),
currentBubbleY + bubbleHeight - Math.round(20 * SY),
bubbleX + bubbleActualW - Math.round(1 * SX),
currentBubbleY + bubbleHeight - Math.round(32 * SY)
);
ctx.closePath();
ctx.fillStyle = s.bubbleColor;
ctx.fill();
ctx.restore();
if (s.text) {
ctx.save();
ctx.fillStyle = s.textColor;
ctx.font = `400 ${s.fontSize}px Inter`;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
let textStartY = currentBubbleY + paddingTop;
if (imgObj) textStartY = currentBubbleY + imgDrawH + Math.round(12 * SY);
for (let i = 0; i < textLines.length; i++) {
const lineY = textStartY + (i * (s.fontSize + lineGap)) + (s.fontSize / 2);
await drawTextWithEmojis(ctx, textLines[i], bubbleX + textPaddingX, lineY, s.fontSize);
}
ctx.restore();
}
ctx.save();
let timeX = bubbleX + bubbleActualW - textPaddingX - timestampWidth;
let timeY = 0;
if (timestampOnNewRow || !s.text) {
timeY = currentBubbleY + bubbleHeight - paddingBottom - timestampHeight + Math.round(4 * SY);
} else {
let textStartY = currentBubbleY + paddingTop;
if (imgObj) textStartY = currentBubbleY + imgDrawH + Math.round(12 * SY);
const lastLineTop = textStartY + ((textLines.length - 1) * (s.fontSize + lineGap));
timeY = lastLineTop + s.fontSize - timestampHeight + Math.round(2 * SY);
}
ctx.fillStyle = s.timeColor;
ctx.font = `600 ${timeFontSize}px Inter`;
ctx.textBaseline = "top";
ctx.fillText(s.time, timeX, timeY);
const tickX = timeX + timeMetrics.width + Math.round(10 * SX);
const t = (n) => Math.round(n * SX);
const tickY = timeY + (timeFontSize / 2) - t(8);
ctx.strokeStyle = s.tickColor;
ctx.lineWidth = 3.6 * SX;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(tickX, tickY + t(8));
ctx.lineTo(tickX + t(6), tickY + t(14));
ctx.lineTo(tickX + t(16), tickY + t(2));
ctx.stroke();
ctx.beginPath();
ctx.moveTo(tickX + t(7), tickY + t(8));
ctx.lineTo(tickX + t(7) + t(6), tickY + t(14));
ctx.lineTo(tickX + t(7) + t(16), tickY + t(2));
ctx.stroke();
ctx.restore();
if (s.showReaction) {
ctx.save();
const emojiNum = s.emojis.length;
const startPad = Math.round(52 * SX);
const plusBtnW = Math.round(80 * SX);
const rxHeight = Math.round(160 * SX);
const rxWidth = startPad + ((emojiNum - 1) * s.emojiSpacing) + s.emojiSpacing * 0.5 + plusBtnW + startPad * 0.5;
const rxX = bubbleX + bubbleActualW - rxWidth + s.emojiXOffset;
const rxY = currentBubbleY - rxHeight + s.emojiYOffset;
const rxRadius = rxHeight / 2;
const rxPivotX = rxX + rxWidth - Math.round(80 * SX);
const rxPivotY = rxY + rxHeight / 2;
ctx.translate(rxPivotX, rxPivotY);
ctx.scale(s.reactionScale, s.reactionScale);
ctx.translate(-rxPivotX, -rxPivotY);
ctx.save();
ctx.shadowColor = "rgba(0,0,0,0.10)";
ctx.shadowBlur = 36;
ctx.shadowOffsetY = 16;
ctx.fillStyle = "#FFFFFF";
drawRoundedRect(ctx, rxX, rxY, rxWidth, rxHeight, rxRadius);
ctx.fill();
ctx.restore();
const emojiCY = rxY + rxHeight / 2;
for (let i = 0; i < emojiNum; i++) {
await drawAppleEmoji(ctx, s.emojis[i], rxX + startPad + (i * s.emojiSpacing), emojiCY, s.emojiSize);
}
const plusX = rxX + startPad + (emojiNum - 1) * s.emojiSpacing + Math.round(90 * SX);
const plusY = emojiCY;
const plusR = Math.round(38 * SX);
const arm = Math.round(13 * SX);
ctx.beginPath();
ctx.arc(plusX, plusY, plusR, 0, Math.PI * 2);
ctx.fillStyle = "#E5E5EA";
ctx.fill();
ctx.strokeStyle = "#8E8E93";
ctx.lineWidth = 4.5 * SX;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(plusX - arm, plusY); ctx.lineTo(plusX + arm, plusY);
ctx.moveTo(plusX, plusY - arm); ctx.lineTo(plusX, plusY + arm);
ctx.stroke();
ctx.restore();
}
ctx.restore();
const pngData = await canvas.encode('png');
const out = outputPath ?? join(process.cwd(), 'temp', `iqcpink_${Date.now()}.png`);
await writeFile(out, pngData);
return out;
}
async function handler(m, { sock, text }) {
try {
const targetText = text || (m.quoted && m.quoted.text ? m.quoted.text : "");
let targetImgBuffer = null;
try {
if (m.quoted && typeof m.quoted.download === 'function') {
targetImgBuffer = await m.quoted.download();
} else if (typeof m.download === 'function' && (m.isMedia || m.mtype === 'imageMessage' || m.type === 'imageMessage')) {
targetImgBuffer = await m.download();
}
} catch (e) {}
if (!targetText && !targetImgBuffer) {
return m.reply(
`๐ฉท *FITUR FAKE QUOTE iOS PINK*\n\n` +
`Sistem ini akan mengubah teks yang kamu berikan menjadi gambar *quote* bergaya iOS dengan tema Pink yang cantik dan elegan.\n\n` +
`*CARA PENGGUNAAN:*\n` +
`- Ketik \`${m.prefix}iqcpink <teks kamu>\`\n` +
`- Atau balas (*reply*) pesan orang lain dengan perintah \`${m.prefix}iqcpink\`\n\n` +
`_Pesan yang dibalas akan otomatis diubah menjadi quote menggunakan nama dan foto profil pembuat pesan tersebut!_`
);
}
let ts = m.messageTimestamp;
if (ts && typeof ts === 'object' && ts.low !== undefined) ts = ts.low;
ts = Number(ts) * 1000;
const date = isNaN(ts) ? new Date() : new Date(ts);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const timeStr = `${hours}.${minutes}`;
await m.react('๐');
let imgUrl = "";
let caption = targetText || "";
const tmpImgPath = join(process.cwd(), 'temp', `iqcpink_${Date.now()}.png`);
if (targetImgBuffer) {
await writeFile(tmpImgPath, targetImgBuffer);
imgUrl = tmpImgPath;
}
const outPath = join(process.cwd(), 'temp', `iqcpink_out_${Date.now()}.png`);
await mkdir(join(process.cwd(), 'temp'), { recursive: true });
const resultPath = await render(caption, timeStr, imgUrl, outPath);
await sock.sendMessage(m.chat, { image: { url: resultPath } }, { quoted: m });
if (existsSync(resultPath)) fs.unlinkSync(resultPath);
if (imgUrl && existsSync(imgUrl)) fs.unlinkSync(imgUrl);
await m.react('โ
');
} catch (error) {
bgImgBuffer = null;
fontsLoaded = false;
appleEmojiMap = null;
console.error("[iqcpink error]:", error);
await m.react('โ');
m.reply(`โ *GAGAL MEMPROSES QUOTE*\n\nMaaf, terjadi kesalahan saat mencoba membuat gambar quote pink. Silakan coba lagi beberapa saat.`);
}
}
export { pluginConfig as config, handler };