174 lines
4.4 KiB
JavaScript
174 lines
4.4 KiB
JavaScript
import express from 'express';
|
|
import { Resvg } from '@resvg/resvg-js';
|
|
import * as fs from 'fs/promises';
|
|
import { escape } from 'html-escaper';
|
|
import { request } from 'undici';
|
|
import { lookup as mimeLookup } from 'mime-types';
|
|
|
|
const errorSVG = 'cards/error.svg';
|
|
|
|
const cards = {
|
|
player: {
|
|
cards: {
|
|
front: {
|
|
name: 'cards/player-front.svg',
|
|
parameters: [
|
|
{ name: 'name', type: 'string' },
|
|
{ name: 'dmg', type: 'number' },
|
|
{ name: 'hp', type: 'number' },
|
|
{ name: 'image', type: 'image', optional: true },
|
|
],
|
|
},
|
|
back: {
|
|
name: 'cards/skilled-back.png',
|
|
ignoreParameters: true,
|
|
}
|
|
},
|
|
special: {
|
|
help: 'cards/player-help.svg',
|
|
},
|
|
}
|
|
}
|
|
|
|
async function svg(filename, params) {
|
|
let svg = await fs.readFile(filename, 'utf8');
|
|
|
|
for (const [key, value] of Object.entries(params)) {
|
|
svg = svg.replaceAll('$' + key + '$', escape(value));
|
|
}
|
|
|
|
return svg;
|
|
}
|
|
|
|
async function render(svg) {
|
|
const opts = {
|
|
font: {
|
|
fontFiles: [
|
|
'fonts/Atkinson-Hyperlegible-Bold.otf',
|
|
'fonts/Atkinson-Hyperlegible-BoldItalic.otf',
|
|
'fonts/Atkinson-Hyperlegible-Italic.otf',
|
|
'fonts/Atkinson-Hyperlegible-Regular.otf',
|
|
'fonts/Minecraft.otf',
|
|
'fonts/Minecraft-Bold.otf',
|
|
],
|
|
loadSystemFonts: false, // It will be faster to disable loading system fonts.
|
|
defaultFontFamily: 'Atkinson Hyperlegible',
|
|
},
|
|
};
|
|
|
|
const resvg = new Resvg(svg, opts);
|
|
const pngData = resvg.render();
|
|
const pngBuffer = pngData.asPng();
|
|
|
|
return pngBuffer;
|
|
}
|
|
|
|
|
|
const server = express();
|
|
|
|
async function returns(req, res, filename, params) {
|
|
const ext = req.params.ext;
|
|
|
|
const s = await svg(filename, params);
|
|
if (ext === 'png') {
|
|
const r = await render(s);
|
|
res.type('png');
|
|
res.setHeader('Content-Length', r.byteLength);
|
|
res.send(r);
|
|
} else if (ext === 'svg') {
|
|
res.type('svg');
|
|
res.setHeader('Content-Length', s.length);
|
|
res.send(s);
|
|
} else {
|
|
res.status(400).send(`Unknown filetype '${ext}'`);
|
|
}
|
|
}
|
|
|
|
async function err(req, res, error) {
|
|
returns(req, res, errorSVG, { error });
|
|
}
|
|
|
|
const userAgent = 'cardgen/0.0.0 (https://oat.zone/)';
|
|
|
|
async function resolveImg(url) {
|
|
const { body, statusCode } = await request(url, {
|
|
headers: { 'user-agent': userAgent },
|
|
});
|
|
if (statusCode !== 200) throw `Image returned non-200 error code`;
|
|
|
|
const blob = await body.blob();
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
const b64 = Buffer.from(arrayBuffer).toString('base64');
|
|
|
|
const mime = mimeLookup(url) || 'image/png';
|
|
|
|
return 'data:' + mime + ';base64,' + b64;
|
|
}
|
|
|
|
function resolveNumber(str) {
|
|
const int = parseInt(str);
|
|
if (isNaN(int)) throw 'Invalid number provided';
|
|
return int;
|
|
}
|
|
|
|
server.get('/:type/:name.:ext', async (req, res) => {
|
|
if (req.headers['user-agent'] === userAgent) return err(req, res, 'Stop that.');
|
|
|
|
const type = req.params.type;
|
|
const name = req.params.name;
|
|
const ext = req.params.ext;
|
|
|
|
const card = cards[type];
|
|
|
|
if (!card) return err(req, res, `No card named ${type} found!`);
|
|
|
|
const tex = card.cards[name];
|
|
|
|
if (!tex) return err(req, res, `Card ${type} does not have ${name}`);
|
|
|
|
let params = {};
|
|
|
|
if (card.special.help && Object.entries(req.query).length === 0 && tex.parameters)
|
|
return returns(req, res, card.special.help, {});
|
|
|
|
if (tex.name.endsWith('.png')) {
|
|
if (ext !== 'png') {
|
|
res.status(400).send(`Cannot offer ${ext}; only a png of this texture xists`);
|
|
}
|
|
return res.sendFile(tex.name, { root: './' });
|
|
}
|
|
|
|
for (const param of (tex.parameters || [])) {
|
|
const name = param.name;
|
|
const type = param.type;
|
|
const isOptional = param.optional;
|
|
|
|
const value = req.query[name];
|
|
|
|
if (!isOptional && (value === undefined || value === null || value === '')) {
|
|
return err(req, res, `Required argument ${name} missing`);
|
|
}
|
|
|
|
try {
|
|
switch (type) {
|
|
case 'string':
|
|
params[name] = value || '';
|
|
break;
|
|
case 'number':
|
|
params[name] = value ? resolveNumber(value) : 0;
|
|
break;
|
|
case 'image':
|
|
params[name] = value ? await resolveImg(value) : '';
|
|
break;
|
|
}
|
|
} catch(e) {
|
|
return err(req, res, `${name}: ${e}`);
|
|
}
|
|
}
|
|
|
|
await returns(req, res, tex.name, params);
|
|
});
|
|
|
|
const port = process.env.PORT || 8080;
|
|
server.listen(port);
|
|
console.log(`listening on port ${port}`); |