import { createCanvas, loadImage, registerFont } from 'canvas'; import SQL from 'sql-template-strings'; import { PermissionAreas } from '#shared/helpers.ts'; import { availableGenders, availableNumeri, resolveFirstDeclension, iconUnicodesByGender, longIdentifierByGender, numeri, symbolsByNumeri, } from '#shared/nouns.ts'; import { getLocale, loadConfig, loadNounsData, loadTranslator } from '~~/server/data.ts'; import { registerLocaleFont } from '~~/server/localeFont.ts'; import { buildNoun, parseNounRow } from '~~/server/nouns.ts'; import type { NounRow } from '~~/server/nouns.ts'; export default defineEventHandler(async (event) => { const locale = getLocale(event); const [config, translator, nounsData] = await Promise.all([loadConfig(locale), loadTranslator(locale), loadNounsData(locale)]); checkIsConfigEnabledOr404(await loadConfig(locale), 'nouns'); const { isGranted } = await useAuthentication(event); if (!getRouterParam(event, 'id')?.endsWith('.png')) { throw createError({ status: 404, statusMessage: 'Not Found', }); } const id = getRouterParam(event, 'id')!.replace(/\.png$/, ''); const db = useDatabase(); const nounRow = await db.get(SQL` SELECT * FROM nouns WHERE locale = ${locale} AND id = ${id} AND approved >= ${isGranted(PermissionAreas.Nouns) ? 0 : 1} AND deleted = 0 `); if (!nounRow) { throw createError({ status: 404, statusMessage: 'Not Found', }); } const noun = buildNoun(parseNounRow(nounRow), config, nounsData); const genders = availableGenders(config); let maxItems = 0; genders.forEach((gender) => { let items = 0; for (const numerus of numeri) { items += noun.words[gender]?.[numerus]?.length ?? 0; } if (items > maxItems) { maxItems = items; } }); const padding = 48; const width = genders.length * 400; const height = padding * 2.5 + (maxItems + 1) * 48 + padding; const mime = 'image/png'; const fontName = registerLocaleFont(config, 'fontHeadings', ['regular', 'bold']); registerFont( 'node_modules/@fortawesome/fontawesome-pro/webfonts/fa-light-300.ttf', { family: 'FontAwesome', weight: 'regular' }, ); const canvas = createCanvas(width, height); const context = canvas.getContext('2d'); const bg = await loadImage('public/bg.png'); context.drawImage(bg, 0, 0, width, height); context.font = `bold 64pt '${fontName}'`; genders.forEach((gender, column) => { context.font = '24pt FontAwesome'; context.fillText( iconUnicodesByGender[gender], column * (width - 2 * padding) / genders.length + padding, padding * 1.5, ); context.font = `bold 24pt '${fontName}'`; const header = translator.translate(`nouns.${longIdentifierByGender[gender]}`); context.fillText(header, column * (width - 2 * padding) / genders.length + padding + 36, padding * 1.5); }); context.font = `24pt '${fontName}'`; genders.forEach((gender, column) => { let i = 0; for (const numerus of availableNumeri(config)) { const symbol = symbolsByNumeri[numerus]; noun.words[gender]?.[numerus]?.forEach((word) => { context.fillText( `${symbol} ${resolveFirstDeclension(word, numerus, nounsData)}`, column * (width - 2 * padding) / genders.length + padding, padding * 2.5 + i * 48, ); i++; }); } }); context.fillStyle = '#C71585'; context.font = '16pt FontAwesome'; context.fillText('\uf02c', padding, height - padding + 12); context.font = `16pt '${fontName}'`; context.fillText( `${translator.translate('domain')}/${config.nouns.routeMain || config.nouns.route}`, padding + 36, height - padding + 10, ); setResponseHeader(event, 'content-type', mime); return canvas.toBuffer(mime); });