redirecter for ao3 that adds opengraph metadata
at main 297 lines 9.3 kB view raw
1import { NextResponse } from "next/server" 2import { createCanvas, createImageData, loadImage, registerFont, CanvasRenderingContext2D } from "canvas" 3import { Path2D, applyPath2DToCanvasRenderingContext } from "path2d" 4import { drawText } from "canvas-txt" 5import baseFonts from "./baseFonts.js" 6import titleFonts from "./titleFonts.js" 7import { catYuriIcon, catYaoiIcon, catHetIcon, catGenIcon, catMultiIcon, catOtherIcon } from "./categoryIcons.js" 8import { ratingNoRatingIcon, ratingExplicitIcon, ratingMatureIcon, ratingTeenIcon, ratingGeneralIcon } from "./ratingIcons.js" 9import { warningChoseNotToWarnIcon, warningWarningsIcon, warningNoWarningsIcon } from "./warningIcons.js" 10import { checkItem, checkToggle } from '@/lib/propUtils.js' 11 12export default async function OGImage ({ theme, baseFont, titleFont, image, addr, opts }) { 13 applyPath2DToCanvasRenderingContext(CanvasRenderingContext2D) 14 baseFonts[baseFont].defs.forEach((font) => { 15 registerFont(process.cwd()+font.path, {family: baseFonts[baseFont].displayName, style: font.style, weight: font.weight}) 16 }) 17 titleFonts[titleFont].defs.forEach((font) => { 18 registerFont(process.cwd()+font.path, {family: titleFonts[titleFont].displayName, style: font.style, weight: font.weight}) 19 }) 20 const canvas = createCanvas(1600, 900) 21 const ctx = canvas.getContext('2d') 22 ctx.fillStyle = theme.background 23 ctx.fillRect(0, 0, 1600, 900) 24 let yCursor = 40; 25 ctx.fillStyle = theme.color 26 const iconCount = (checkToggle('category', image.props) ? 1 : 0) + (checkToggle('rating', image.props) ? 1 : 0) + (checkToggle('warnings', image.props) ? 1 : 0) 27 if (iconCount > 0) { 28 let iconCursor = 800 - (36*iconCount + 12*(iconCount - 1))/2 29 if (checkToggle('category', image.props)) { 30 let catIcon; 31 ctx.fillStyle = theme.accent 32 ctx.beginPath() 33 ctx.ellipse(iconCursor, yCursor, 18, 18, 0, 0, 2*Math.PI); 34 ctx.fill() 35 ctx.closePath() 36 ctx.beginPath() 37 ctx.fillStyle = theme.background 38 switch (image.category) { 39 case 'F': 40 catIcon = catYuriIcon 41 break 42 case 'M': 43 catIcon = catYaoiIcon 44 break 45 case 'FM': 46 catIcon = catHetIcon 47 break 48 case 'G': 49 catIcon = catGenIcon 50 break 51 case 'MX': 52 catIcon = catMultiIcon 53 break 54 case 'O': 55 default: 56 catIcon = catOtherIcon 57 break 58 } 59 ctx.translate(iconCursor - 18, yCursor - 18) 60 const catPath = new Path2D(catIcon) 61 ctx.fill(catPath) 62 ctx.closePath() 63 ctx.setTransform(1, 0, 0, 1, 0, 0); 64 iconCursor += 48 65 } 66 if (checkToggle('rating', image.props)) { 67 let ratingIcon; 68 ctx.beginPath() 69 ctx.fillStyle = theme.accent2 70 ctx.ellipse(iconCursor, yCursor, 18, 18, 0, 0, 360); 71 ctx.fill() 72 ctx.closePath() 73 ctx.beginPath() 74 ctx.fillStyle = theme.background 75 switch (image.rating) { 76 case 'Not Rated': 77 ratingIcon = ratingNoRatingIcon 78 break 79 case 'Explicit': 80 ratingIcon = ratingExplicitIcon 81 break 82 case 'Mature': 83 ratingIcon = ratingMatureIcon 84 break 85 case 'Teen': 86 ratingIcon = ratingTeenIcon 87 break 88 case 'General': 89 default: 90 ratingIcon = ratingGeneralIcon 91 break 92 } 93 ctx.translate(iconCursor - 18, yCursor - 18) 94 const ratingPath = new Path2D(ratingIcon) 95 ctx.fill(ratingPath) 96 ctx.closePath() 97 ctx.setTransform(1, 0, 0, 1, 0, 0); 98 iconCursor += 48 99 } 100 if (checkToggle('warnings', image.props)) { 101 let warningIcon; 102 ctx.beginPath() 103 ctx.fillStyle = theme.accent3 104 ctx.ellipse(iconCursor, yCursor, 18, 18, 0, 0, 360); 105 ctx.fill() 106 ctx.closePath() 107 ctx.beginPath() 108 ctx.fillStyle = theme.background 109 switch (image.rating) { 110 case 'CNTW': 111 warningIcon = warningChoseNotToWarnIcon 112 break 113 case 'NW': 114 warningIcon = warningWarningIcon 115 break 116 case 'W': 117 default: 118 warningIcon = warningNoWarningsIcon 119 } 120 ctx.translate(iconCursor - 18, yCursor - 18) 121 const warningPath = new Path2D(warningIcon) 122 ctx.fill(warningPath) 123 ctx.closePath() 124 ctx.setTransform(1, 0, 0, 1, 0, 0); 125 } 126 yCursor += 36 127 } 128 ctx.fillStyle = theme.color 129 const topline = drawText(ctx, image.topLine, { 130 x: 40, 131 y: yCursor, 132 width: 1520, 133 height: 24, 134 fontSize: 24, 135 vAlign: 'top', 136 fill: theme.color, 137 font: baseFonts[baseFont].displayName 138 }) 139 yCursor += topline.height + 24 140 const title = drawText(ctx, image.titleLine, { 141 x: 40, 142 y: yCursor, 143 width: 1520, 144 height: 54, 145 fontSize: 54, 146 vAlign: 'top', 147 fill: theme.color, 148 font: titleFonts[titleFont].displayName 149 }) 150 yCursor += title.height + 24 151 const author = drawText(ctx, `by ${image.authorLine}`, { 152 x: 40, 153 y: yCursor, 154 width: 1520, 155 height: 42, 156 fontSize: 42, 157 vAlign: 'top', 158 font: titleFonts[titleFont].displayName 159 }) 160 yCursor += author.height + 24 161 if (image.chapterLine) { 162 const chapter = drawText(ctx, image.chapterLine, { 163 x: 40, 164 y: yCursor, 165 width: 1520, 166 height: 36, 167 fontSize: 36, 168 vAlign: 'top', 169 font: titleFonts[titleFont].displayName 170 }) 171 yCursor += chapter.height + 24 172 } 173 ctx.fillStyle = theme.descBackground 174 const descRect = ctx.fillRect(40, yCursor, 1520, (860 - yCursor)) 175 ctx.fillStyle = theme.descColor 176 yCursor += 40 177 if (checkToggle('charTags', image.props)) { 178 let cxCursor = 80 179 let cyCursor = yCursor 180 ctx.font = "18px "+baseFonts[baseFont].displayName 181 image.charTags.forEach((c) => { 182 const metrics = ctx.measureText(c) 183 if (metrics.width + cxCursor > 1520) { 184 cxCursor = 80 185 cyCursor += 36 186 } 187 ctx.fillStyle = theme.accent2 188 ctx.beginPath() 189 ctx.roundRect(cxCursor, cyCursor, metrics.width+16, 32, 5); 190 ctx.fill() 191 ctx.closePath() 192 ctx.fillStyle = theme.accent2Color 193 drawText(ctx, c, { 194 x: cxCursor + 8, 195 y: cyCursor + 4, 196 fontSize: 18, 197 align: 'left', 198 vAlign: 'top', 199 font: baseFonts[baseFont].displayName 200 }) 201 cxCursor = cxCursor + metrics.width + 16 + 8 202 }) 203 yCursor = cyCursor + 36 204 } 205 if (checkToggle('relTags', image.props)) { 206 let rxCursor = 80 207 let ryCursor = yCursor 208 ctx.font = "18px "+baseFonts[baseFont].displayName 209 image.relTags.forEach((r) => { 210 const metrics = ctx.measureText(r) 211 if (metrics.width + rxCursor > 1520) { 212 cxCursor = 80 213 cyCursor += 36 214 } 215 ctx.fillStyle = theme.accent3 216 ctx.beginPath() 217 ctx.roundRect(rxCursor, ryCursor, metrics.width+16, 32, 5); 218 ctx.fill() 219 ctx.fillStyle = theme.accent3Color 220 drawText(ctx, r, { 221 x: rxCursor + 8, 222 y: ryCursor + 4, 223 fontSize: 18, 224 align: 'left', 225 vAlign: 'top', 226 font: baseFonts[baseFont].displayName 227 }) 228 rxCursor = rxCursor + metrics.width + 16 + 8 229 }) 230 yCursor = ryCursor + 36 231 } 232 if (checkToggle('freeTags', image.props)) { 233 let fxCursor = 80 234 let fyCursor = yCursor 235 ctx.font = "18px "+baseFonts[baseFont].displayName 236 image.freeTags.forEach((f) => { 237 const metrics = ctx.measureText(f) 238 if (metrics.width + fxCursor > 1520) { 239 fxCursor = 80 240 fyCursor += 36 241 } 242 ctx.fillStyle = theme.accent4 243 ctx.beginPath() 244 ctx.roundRect(fxCursor, fyCursor, metrics.width+16, 32, 5); 245 ctx.fill() 246 ctx.fillStyle = theme.accent4Color 247 drawText(ctx, f, { 248 x: fxCursor + 8, 249 y: fyCursor + 4, 250 fontSize: 18, 251 align: 'left', 252 vAlign: 'top', 253 font: baseFonts[baseFont].displayName 254 }) 255 fxCursor = fxCursor + metrics.width + 16 + 8 256 }) 257 yCursor = fyCursor + 36 258 } 259 yCursor += 12 260 ctx.fillStyle = theme.descColor 261 if (checkToggle('summary', image.props)) { 262 image.summary.forEach((l) => { 263 const line = drawText(ctx, l, { 264 x: 80, 265 y: yCursor, 266 width: 1440, 267 height: 1, 268 lineHeight: 30, 269 fontSize: 24, 270 align: 'left', 271 vAlign: 'top', 272 font: baseFonts[baseFont].displayName 273 }) 274 yCursor += line.height + 12 275 }) 276 } 277 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}`; 278 ctx.fillStyle = theme.accent2 279 const bottomLine = drawText(ctx, bottomText, { 280 x: 80, 281 y: 820, 282 width: 1440, 283 height: 18, 284 lineHeight: 18, 285 fontSize: 18, 286 align: 'right', 287 vAlign: 'bottom', 288 font: baseFonts[baseFont].displayName 289 }) 290 const headers = new Headers(); 291 headers.set('Content-Type', 'image/png'); 292 return new Response(canvas.toBuffer(), { 293 headers: { 294 'Content-Type': 'image/png' 295 } 296 }) 297}