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;

Komentar