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>/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;