javascript-snippet
fake-gopay
Lines: 225Chars: 7100Size: 6.93 KB
javascript-snippet
1import { Canvas, loadImage, FontLibrary } from 'skia-canvas';
2import fs from 'fs';
3import https from 'https';
4import path from 'path';
5import { fileURLToPath } from 'url';
6import express from "express";
7
8const router = express.Router();
9const __filename = fileURLToPath(import.meta.url);
10const __dirname = path.dirname(__filename);
11
12const CONFIG = {
13 data: {
14 saldo: '890',
15 koin: '159',
16 terpakai: '0',
17 bulan: 'Mei',
18 },
19 pos: {
20 saldo: { x: 62, y: 325 },
21 koin: { x: 115, y: 400 },
22 pill: { x: 50, y: 510 },
23 },
24 fontSize: {
25 rp: 34,
26 saldo: 95,
27 koin: 34,
28 pill: 34,
29 },
30 icon: {
31 eye: { w: 60, h: 60 },
32 report: { w: 30, h: 30 },
33 next: { w: 30, h: 30 },
34 },
35 pill: {
36 height: 48,
37 paddingLeft: 14,
38 paddingRight: 14,
39 gapIconText: 10,
40 gapTextArrow: 16,
41 },
42 gap: {
43 rpToAngka: 8,
44 angkaToEye: 20,
45 eyeOffsetY: 12,
46 },
47 color: {
48 report: 'rgba(196, 227, 245)',
49 eye: 'rgba(204, 226, 240)',
50 },
51 baseUrl: 'https://raw.githubusercontent.com/Ditzzx-vibecoder/Assets/main',
52};
53
54function downloadAsset(url, dest) {
55 return new Promise((resolve, reject) => {
56 const doGet = (targetUrl) => {
57 https.get(targetUrl, (res) => {
58 if ([301, 302].includes(res.statusCode)) {
59 return doGet(res.headers.location);
60 }
61 const file = fs.createWriteStream(dest);
62 res.pipe(file);
63 file.on('finish', () => file.close(resolve));
64 file.on('error', (err) => {
65 fs.unlink(dest, () => reject(err));
66 });
67 }).on('error', (err) => {
68 fs.unlink(dest, () => reject(err));
69 });
70 };
71 doGet(url);
72 });
73}
74
75function roundRect(ctx, x, y, w, h, r) {
76 ctx.beginPath();
77 ctx.moveTo(x+r,y);
78 ctx.lineTo(x+w-r,y);
79 ctx.quadraticCurveTo(x+w,y,x+w,y+r);
80 ctx.lineTo(x+w,y+h-r);
81 ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
82 ctx.lineTo(x+r,y+h);
83 ctx.quadraticCurveTo(x,y+h,x,y+h-r);
84 ctx.lineTo(x,y+r);
85 ctx.quadraticCurveTo(x,y,x+r,y);
86 ctx.closePath();
87}
88
89function tintIcon(ctx, img, x, y, w, h, color) {
90 const off = new Canvas(w, h);
91 const octx = off.getContext('2d');
92 octx.drawImage(img, 0, 0, w, h);
93 octx.globalCompositeOperation = 'source-in';
94 octx.fillStyle = color;
95 octx.fillRect(0, 0, w, h);
96 ctx.drawImage(off, x, y, w, h);
97}
98
99async function generate(data) {
100 const fontDir = path.join(__dirname, '../fonts');
101 const imgDir = path.join(__dirname, '../assets');
102
103 if (!fs.existsSync(fontDir)) fs.mkdirSync(fontDir, { recursive: true });
104 if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
105
106 const B = CONFIG.baseUrl;
107
108 const assets = {
109 bg: { url: `${B}/Image/quality_restoration_20260501080321276.jpg`, path: path.join(imgDir, 'bg.jpg') },
110 fontReg: { url: `${B}/Font/rupa_sans_regular.ttf`, path: path.join(fontDir, 'reg.ttf') },
111 fontSb: { url: `${B}/Font/rupa_sans_semi_bold.ttf`, path: path.join(fontDir, 'sb.ttf') },
112 fontSerif: { url: `${B}/Font/rupa_serif_semi_bold.ttf`, path: path.join(fontDir, 'serif.ttf') },
113 iconReport: { url: `${B}/Image/bar-chart_6687624.svg`, path: path.join(imgDir, 'report.svg') },
114 iconEye: { url: `${B}/Image/icChat16ReadMessage.svg`, path: path.join(imgDir, 'eye.svg') },
115 iconNext: { url: `${B}/Image/icNavigation16NextIos.svg`, path: path.join(imgDir, 'next.svg') },
116 };
117
118 if (fs.existsSync(assets.iconReport.path)) {
119 fs.unlinkSync(assets.iconReport.path);
120 }
121
122 for (const asset of Object.values(assets)) {
123 if (!fs.existsSync(asset.path)) {
124 await downloadAsset(asset.url, asset.path);
125 }
126 }
127
128 FontLibrary.use('RupaSans', [assets.fontReg.path, assets.fontSb.path]);
129 FontLibrary.use('RupaSerif', [assets.fontSerif.path]);
130
131 const bg = await loadImage(assets.bg.path);
132 const iconReport = await loadImage(assets.iconReport.path);
133 const iconEye = await loadImage(assets.iconEye.path);
134 const iconNext = await loadImage(assets.iconNext.path);
135
136 const canvas = new Canvas(bg.width, bg.height);
137 const ctx = canvas.getContext('2d');
138
139 const { pos, fontSize, icon, pill, gap, color } = CONFIG;
140
141 ctx.drawImage(bg, 0, 0);
142
143 ctx.fillStyle = '#fff';
144
145 ctx.font = `800 ${fontSize.rp}px RupaSans`;
146 ctx.fillText('Rp', pos.saldo.x, pos.saldo.y - 38);
147 const rpW = ctx.measureText('Rp').width;
148
149 ctx.font = `800 ${fontSize.saldo}px RupaSerif`;
150 const angkaX = pos.saldo.x + rpW + gap.rpToAngka;
151 ctx.fillText(data.saldo, angkaX, pos.saldo.y);
152 const angkaW = ctx.measureText(data.saldo).width;
153
154 const eyeX = angkaX + angkaW + gap.angkaToEye;
155 const eyeMidY = pos.saldo.y - (fontSize.saldo / 2) + gap.eyeOffsetY;
156 tintIcon(ctx, iconEye, eyeX, eyeMidY - (icon.eye.h / 2), icon.eye.w, icon.eye.h, color.eye);
157
158 ctx.fillStyle = '#fff';
159
160 ctx.font = `800 ${fontSize.koin}px RupaSans`;
161 ctx.fillText(data.koin, pos.koin.x, pos.koin.y);
162 const koinAngkaW = ctx.measureText(data.koin).width;
163
164 ctx.font = `400 ${fontSize.koin}px RupaSans`;
165 ctx.fillText(' Coins', pos.koin.x + koinAngkaW, pos.koin.y);
166
167 ctx.font = `800 ${fontSize.pill}px RupaSans`;
168 const rpTerpakaiText = `Rp${data.terpakai}`;
169 const rpTerpakaiW = ctx.measureText(rpTerpakaiText).width;
170
171 ctx.font = `400 ${fontSize.pill}px RupaSans`;
172 const sisaText = ` udah terpakai di ${data.bulan}`;
173 const sisaW = ctx.measureText(sisaText).width;
174
175 const textW = rpTerpakaiW + sisaW;
176 const pillW = pill.paddingLeft + icon.report.w + pill.gapIconText + textW + pill.gapTextArrow + icon.next.w + pill.paddingRight;
177
178 const pillCenterY = pos.pill.y + (pill.height / 2);
179 const textBaseY = pillCenterY + (fontSize.pill / 3);
180 const textStartX = pos.pill.x + pill.paddingLeft + icon.report.w + pill.gapIconText;
181
182 tintIcon(ctx, iconReport, pos.pill.x + pill.paddingLeft, pillCenterY - (icon.report.h / 2), icon.report.w, icon.report.h, color.report);
183 tintIcon(ctx, iconNext, pos.pill.x + pillW - pill.paddingRight - icon.next.w, pillCenterY - (icon.next.h / 2), icon.next.w, icon.next.h, '#fff');
184
185 ctx.fillStyle = '#fff';
186 ctx.font = `600 ${fontSize.pill}px RupaSans`;
187 ctx.fillText(rpTerpakaiText, textStartX, textBaseY);
188
189 ctx.font = `400 ${fontSize.pill}px RupaSans`;
190 ctx.fillText(sisaText, textStartX + rpTerpakaiW, textBaseY);
191
192 return await canvas.toBuffer('png');
193}
194
195router.post(
196 "/",
197 async (req, res) => {
198
199 try {
200
201 const buffer =
202 await generate(
203 req.body
204 );
205
206 res.setHeader(
207 "Content-Type",
208 "image/png"
209 );
210
211 res.send(buffer);
212
213 } catch (e) {
214
215 res.status(500).json({
216 status: false,
217 message: e.message
218 });
219
220 }
221
222 }
223);
224
225export default router;