import { NextResponse } from "next/server" import { createCanvas, createImageData, loadImage, registerFont, CanvasRenderingContext2D } from "canvas" import { Path2D, applyPath2DToCanvasRenderingContext } from "path2d" import { drawText } from "canvas-txt" import baseFonts from "./baseFonts.js" import titleFonts from "./titleFonts.js" import { catYuriIcon, catYaoiIcon, catHetIcon, catGenIcon, catMultiIcon, catOtherIcon } from "./categoryIcons.js" import { ratingNoRatingIcon, ratingExplicitIcon, ratingMatureIcon, ratingTeenIcon, ratingGeneralIcon } from "./ratingIcons.js" import { warningChoseNotToWarnIcon, warningWarningsIcon, warningNoWarningsIcon } from "./warningIcons.js" import { checkItem, checkToggle } from '@/lib/propUtils.js' export default async function OGImage ({ theme, baseFont, titleFont, image, addr, opts }) { applyPath2DToCanvasRenderingContext(CanvasRenderingContext2D) baseFonts[baseFont].defs.forEach((font) => { registerFont(process.cwd()+font.path, {family: baseFonts[baseFont].displayName, style: font.style, weight: font.weight}) }) titleFonts[titleFont].defs.forEach((font) => { registerFont(process.cwd()+font.path, {family: titleFonts[titleFont].displayName, style: font.style, weight: font.weight}) }) const canvas = createCanvas(1600, 900) const ctx = canvas.getContext('2d') ctx.fillStyle = theme.background ctx.fillRect(0, 0, 1600, 900) let yCursor = 40; ctx.fillStyle = theme.color const iconCount = (checkToggle('category', image.props) ? 1 : 0) + (checkToggle('rating', image.props) ? 1 : 0) + (checkToggle('warnings', image.props) ? 1 : 0) if (iconCount > 0) { let iconCursor = 800 - (36*iconCount + 12*(iconCount - 1))/2 if (checkToggle('category', image.props)) { let catIcon; ctx.fillStyle = theme.accent ctx.beginPath() ctx.ellipse(iconCursor, yCursor, 18, 18, 0, 0, 2*Math.PI); ctx.fill() ctx.closePath() ctx.beginPath() ctx.fillStyle = theme.background switch (image.category) { case 'F': catIcon = catYuriIcon break case 'M': catIcon = catYaoiIcon break case 'FM': catIcon = catHetIcon break case 'G': catIcon = catGenIcon break case 'MX': catIcon = catMultiIcon break case 'O': default: catIcon = catOtherIcon break } ctx.translate(iconCursor - 18, yCursor - 18) const catPath = new Path2D(catIcon) ctx.fill(catPath) ctx.closePath() ctx.setTransform(1, 0, 0, 1, 0, 0); iconCursor += 48 } if (checkToggle('rating', image.props)) { let ratingIcon; ctx.beginPath() ctx.fillStyle = theme.accent2 ctx.ellipse(iconCursor, yCursor, 18, 18, 0, 0, 360); ctx.fill() ctx.closePath() ctx.beginPath() ctx.fillStyle = theme.background switch (image.rating) { case 'Not Rated': ratingIcon = ratingNoRatingIcon break case 'Explicit': ratingIcon = ratingExplicitIcon break case 'Mature': ratingIcon = ratingMatureIcon break case 'Teen': ratingIcon = ratingTeenIcon break case 'General': default: ratingIcon = ratingGeneralIcon break } ctx.translate(iconCursor - 18, yCursor - 18) const ratingPath = new Path2D(ratingIcon) ctx.fill(ratingPath) ctx.closePath() ctx.setTransform(1, 0, 0, 1, 0, 0); iconCursor += 48 } if (checkToggle('warnings', image.props)) { let warningIcon; ctx.beginPath() ctx.fillStyle = theme.accent3 ctx.ellipse(iconCursor, yCursor, 18, 18, 0, 0, 360); ctx.fill() ctx.closePath() ctx.beginPath() ctx.fillStyle = theme.background switch (image.rating) { case 'CNTW': warningIcon = warningChoseNotToWarnIcon break case 'NW': warningIcon = warningWarningIcon break case 'W': default: warningIcon = warningNoWarningsIcon } ctx.translate(iconCursor - 18, yCursor - 18) const warningPath = new Path2D(warningIcon) ctx.fill(warningPath) ctx.closePath() ctx.setTransform(1, 0, 0, 1, 0, 0); } yCursor += 36 } ctx.fillStyle = theme.color const topline = drawText(ctx, image.topLine, { x: 40, y: yCursor, width: 1520, height: 24, fontSize: 24, vAlign: 'top', fill: theme.color, font: baseFonts[baseFont].displayName }) yCursor += topline.height + 24 const title = drawText(ctx, image.titleLine, { x: 40, y: yCursor, width: 1520, height: 54, fontSize: 54, vAlign: 'top', fill: theme.color, font: titleFonts[titleFont].displayName }) yCursor += title.height + 24 const author = drawText(ctx, `by ${image.authorLine}`, { x: 40, y: yCursor, width: 1520, height: 42, fontSize: 42, vAlign: 'top', font: titleFonts[titleFont].displayName }) yCursor += author.height + 24 if (image.chapterLine) { const chapter = drawText(ctx, image.chapterLine, { x: 40, y: yCursor, width: 1520, height: 36, fontSize: 36, vAlign: 'top', font: titleFonts[titleFont].displayName }) yCursor += chapter.height + 24 } ctx.fillStyle = theme.descBackground const descRect = ctx.fillRect(40, yCursor, 1520, (860 - yCursor)) ctx.fillStyle = theme.descColor yCursor += 40 if (checkToggle('charTags', image.props)) { let cxCursor = 80 let cyCursor = yCursor ctx.font = "18px "+baseFonts[baseFont].displayName image.charTags.forEach((c) => { const metrics = ctx.measureText(c) if (metrics.width + cxCursor > 1520) { cxCursor = 80 cyCursor += 36 } ctx.fillStyle = theme.accent2 ctx.beginPath() ctx.roundRect(cxCursor, cyCursor, metrics.width+16, 32, 5); ctx.fill() ctx.closePath() ctx.fillStyle = theme.accent2Color drawText(ctx, c, { x: cxCursor + 8, y: cyCursor + 4, fontSize: 18, align: 'left', vAlign: 'top', font: baseFonts[baseFont].displayName }) cxCursor = cxCursor + metrics.width + 16 + 8 }) yCursor = cyCursor + 36 } if (checkToggle('relTags', image.props)) { let rxCursor = 80 let ryCursor = yCursor ctx.font = "18px "+baseFonts[baseFont].displayName image.relTags.forEach((r) => { const metrics = ctx.measureText(r) if (metrics.width + rxCursor > 1520) { cxCursor = 80 cyCursor += 36 } ctx.fillStyle = theme.accent3 ctx.beginPath() ctx.roundRect(rxCursor, ryCursor, metrics.width+16, 32, 5); ctx.fill() ctx.fillStyle = theme.accent3Color drawText(ctx, r, { x: rxCursor + 8, y: ryCursor + 4, fontSize: 18, align: 'left', vAlign: 'top', font: baseFonts[baseFont].displayName }) rxCursor = rxCursor + metrics.width + 16 + 8 }) yCursor = ryCursor + 36 } if (checkToggle('freeTags', image.props)) { let fxCursor = 80 let fyCursor = yCursor ctx.font = "18px "+baseFonts[baseFont].displayName image.freeTags.forEach((f) => { const metrics = ctx.measureText(f) if (metrics.width + fxCursor > 1520) { fxCursor = 80 fyCursor += 36 } ctx.fillStyle = theme.accent4 ctx.beginPath() ctx.roundRect(fxCursor, fyCursor, metrics.width+16, 32, 5); ctx.fill() ctx.fillStyle = theme.accent4Color drawText(ctx, f, { x: fxCursor + 8, y: fyCursor + 4, fontSize: 18, align: 'left', vAlign: 'top', font: baseFonts[baseFont].displayName }) fxCursor = fxCursor + metrics.width + 16 + 8 }) yCursor = fyCursor + 36 } yCursor += 12 ctx.fillStyle = theme.descColor if (checkToggle('summary', image.props)) { image.summary.forEach((l) => { const line = drawText(ctx, l, { x: 80, y: yCursor, width: 1440, height: 1, lineHeight: 30, fontSize: 24, align: 'left', vAlign: 'top', font: baseFonts[baseFont].displayName }) yCursor += line.height + 12 }) } const bottomText = `${checkToggle('wordcount', image.props) ? `${image.words} words • ` : ''}${(checkToggle('chapters', image.props) && image.chapterCount !== null) ? `${image.chapterCount} chapters • ` : ''}${checkToggle('postedAt', image.props) ? `posted on ${image.postedAt} • ` : ''}${checkToggle('updatedAt', image.props) ? `updated on ${image.updatedAt} • ` : ''}${addr}`; ctx.fillStyle = theme.accent2 const bottomLine = drawText(ctx, bottomText, { x: 80, y: 820, width: 1440, height: 18, lineHeight: 18, fontSize: 18, align: 'right', vAlign: 'bottom', font: baseFonts[baseFont].displayName }) const headers = new Headers(); headers.set('Content-Type', 'image/png'); return new Response(canvas.toBuffer(), { headers: { 'Content-Type': 'image/png' } }) }