Files
badgedex/src/api/badge.js
T

155 lines
7.0 KiB
JavaScript

const router = require('express').Router();
const axios = require('axios');
const { escapeSvg, safeColor } = require('../lib/sanitize');
const { parseSize, parseStyle, parseLabelpos, parseFontweight } = require('../lib/validate');
const { FONT_FAMILY, calcFontSize, estimateTextWidth } = require('../lib/svg-utils');
const logger = require('../lib/logger');
router.get('/badge', async (req, res) => {
const icon = req.query.icon || 'simpleicons';
const label = req.query.label || 'Simple Icons';
const style = parseStyle(req.query.style);
const bgicon = req.query.bgicon || 'none';
const bglabel = req.query.bglabel || 'none';
const color = req.query.color || '#000000';
const size = parseSize(req.query.size);
const labelpos = parseLabelpos(req.query.labelpos);
const fontweight = parseFontweight(req.query.fontweight);
try {
// download icon SVG from SimpleIcons CDN
const safeIcon = escapeSvg(icon);
const iconUrl = `https://cdn.simpleicons.org/${safeIcon}?viewbox=auto&size=${size}`;
const iconResponse = await axios.get(iconUrl, { responseType: 'text' });
const iconSVG = iconResponse.data;
// if there is no label query, use the title from the SVG
let effectiveLabel = label;
if (!req.query.label || req.query.label === 'Simple Icons') {
const titleMatch = iconSVG.match(/<title>([^<]+)<\/title>/i);
if (titleMatch && titleMatch[1]) {
effectiveLabel = titleMatch[1];
}
}
// Badge size
const safeLabel = escapeSvg(effectiveLabel);
const safeBgIcon = safeColor(bgicon, 'none');
const safeBgLabel = safeColor(bglabel, 'none');
const safeFontWeight = escapeSvg(fontweight);
const padding = 10;
const iconSize = size;
const fontSize = calcFontSize(iconSize);
const textPadding = Math.round(iconSize * 0.4);
const radius = style === 'flat' ? 3 : 0;
const textWidth = estimateTextWidth(effectiveLabel, fontSize);
// Kinyerjük az ikon SVG-ből a width és height attribútumokat (ha vannak)
let iconViewBoxWidth = iconSize;
let iconViewBoxHeight = iconSize;
const widthMatch = iconSVG.match(/width="(\d+\.?\d*)"/);
const heightMatch = iconSVG.match(/height="(\d+\.?\d*)"/);
if (widthMatch) iconViewBoxWidth = parseFloat(widthMatch[1]);
if (heightMatch) iconViewBoxHeight = parseFloat(heightMatch[1]);
// Ikon doboz mérete
const iconBoxWidth = iconViewBoxWidth + 5;
const iconBoxHeight = iconViewBoxHeight + 5;
// Ha a color paraméter nincs megadva, próbáljuk kinyerni az ikon színét
let effectiveColor = color;
if (!req.query.color || req.query.color === '#000000') {
const match = iconSVG.match(/fill="([^"]+)"/);
if (match && match[1]) {
effectiveColor = match[1];
} else {
effectiveColor = "#222"; // fallback
}
}
effectiveColor = safeColor(effectiveColor, '#222');
// doboz magasság 5 pixellel nagyobb legyen az ikon méreténél
const iconSizeAndPadding = iconSize + 5;
const labelRectHeight = fontSize + padding;
let width, height, iconGroup, textElem;
const fontFamily = FONT_FAMILY;
const fontWeight = safeFontWeight; // paraméterből
if (labelpos === 'left') {
// Külön dobozok: padding csak a széleken kell, a dobozokon belül nem!
textElem = `<rect x="0" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgLabel}"/>
<text x="${textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${safeLabel}</text>`;
iconGroup = `
<rect x="${textWidth}" y="0" width="${iconBoxWidth + 5}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgIcon}"/>
<g transform="translate(${textWidth + (5 / 2)}, ${(iconBoxHeight - iconSize) / 2})">${iconSVG}</g>
`;
} else if (labelpos === 'above' || labelpos === 'below') {
// Felső vagy alsó label: ikon doboz magassága = iconSize + 5, szélessége = szöveg doboz szélessége
width = Math.max(iconSizeAndPadding, textWidth);
const iconRectY = labelpos === 'above'
? labelRectHeight
: 0;
const labelRectY = labelpos === 'above'
? 0
: iconSizeAndPadding;
height = iconSizeAndPadding + textPadding + labelRectHeight;
// Label háttér
const labelRect = `<rect x="0" y="${labelRectY}" width="${width}" height="${labelRectHeight}" rx="${radius}" fill="${safeBgLabel}"/>`;
// Ikon háttér
const iconRect = `<rect x="0" y="${iconRectY}" width="${width}" height="${iconSize + 5}" rx="${radius}" fill="${safeBgIcon}"/>`;
// Szöveg
const textY = labelpos === 'above'
? labelRectY + 5 + labelRectHeight / 2
: labelRectY + 5 + labelRectHeight / 2;
textElem = `
${labelRect}
<text x="${width / 2}" y="${textY}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${safeLabel}</text>
`;
// Ikon
iconGroup = `
${iconRect}
<g transform="translate(${(width - iconSize) / 2 + 2.5}, ${iconRectY + (iconSize + 5 - iconSize) / 2})">${iconSVG}</g>
`;
} else {
// Alapértelmezett: ikon balra, szöveg jobbra (függőleges közép)
iconGroup = `
<rect x="0" y="0" width="${iconBoxWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgIcon}"/>
<g transform="translate(${(iconBoxWidth - iconViewBoxWidth) / 2}, ${(iconBoxHeight - iconViewBoxHeight) / 2})">${iconSVG}</g>
`;
textElem = `<rect x="${iconBoxWidth}" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgLabel}"/>
<text x="${iconBoxWidth + textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${safeLabel}</text>`;
}
// SVG badge string összefűzéssel, szöveg árnyékkal
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${(labelpos === 'left' || labelpos === 'right') ? iconBoxWidth + textWidth : width}" height="${(labelpos === 'left' || labelpos === 'right') ? iconBoxHeight : labelRectHeight+iconSize + 5}" style="background:none">
<defs>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="1" stdDeviation="1" flood-color="#000" flood-opacity="0.25"/>
</filter>
</defs>
${iconGroup}
${textElem.replace('<text ', '<text filter="url(#shadow)" ')}
</svg>
`.trim();
res.setHeader('Content-Type', 'image/svg+xml');
res.send(svg);
} catch (err) {
if (err.response && err.response.status === 404) {
return res.status(404).send('Icon not found');
} else {
logger.error({ err, icon, url: req.url }, 'Badge generation failed');
res.status(500).send('Internal Server Error');
}
}
});
module.exports = router;