const sharp = require('sharp'); const canvas = require('canvas'); const crypto = require('crypto'); const path = require('path'); const randint = (min, max) => { return crypto.randomInt(min, max) }; canvas.registerFont(path.join(__dirname, 'asset/lobster.ttf'), { family: 'Lobster' }); /** @param {import('fastify').FastifyRequest} req */ const getSettings = (req) => { const q = req.query; const settings = { // Width and height are ratio numbers; actual width & height is calculated from text length & padding width: q.width ?? 3, height: q.height ?? 1, padding: q.padding ?? 20, format: q.format ?? 'png', dpi: q.dpi ?? 250 }; for (const key in settings) { if (['format'].indexOf(key) !== -1) continue; settings[key] = parseInt(settings[key]); } return settings; } const get_size = (ratio, dpi, txtlen, padding) => { return { width: Math.floor((ratio[0] * dpi * 0.12) + (txtlen * dpi * 0.08)) + (padding * 2), height: Math.floor(ratio[1] * dpi * 0.2) + (padding * 2) } } const createImage = async (width, height, bg = 'white', options = {}) => { return sharp({ create: { width: width, height: height, channels: 3, background: bg, ...options } }) } const drawText = (pic, text, dpi, blend, options = {}) => { return pic.composite([{ input: { text: { text, font: 'Open Sans', dpi, rgba: true, ...options } }, blend }]) } /** @param {import('fastify').FastifyInstance} fastify */ module.exports = (fastify) => { fastify.get('/captcha/v1/:text', async (req, res) => { const text = req.params.text; const settings = getSettings(req); const size = get_size([settings.width, settings.height], settings.dpi, text.length, settings.padding); console.log(size) console.log(settings) let pic = await createImage(size.width, size.height); drawText(pic, text, settings.dpi, 'over'); const textpic = await pic .ensureAlpha() .toFormat('png', { compressionLevel: 0 }) .toBuffer(); const draw = canvas.createCanvas(size.width, size.height); const ctx = draw.getContext('2d'); ctx.drawImage(await canvas.loadImage(textpic), 0, 0, size.width, size.height); const strokes = randint(25, 40); for (let i = 0; i < strokes; i++) { ctx.strokeStyle = `rgba(0,0,0,${10})`; ctx.beginPath(); ctx.bezierCurveTo(randint(0, size.width), randint(0, size.height), randint(0, size.width), randint(0, size.height), randint(0, size.width), randint(0, size.height)); ctx.stroke(); } res.header('Content-Type', 'png'); return draw.toBuffer(); }); fastify.get('/captcha/v2/:text', async (req, res) => { const text = req.params.text; const settings = getSettings(req); const size = get_size([settings.width, settings.height], settings.dpi, text.length, settings.padding); size.height *= 0.5 const draw = canvas.createCanvas(size.width, size.height); const ctx = draw.getContext('2d'); const arcs = randint(5, 32); for (let i = 0; i != arcs; i++) { ctx.beginPath(); ctx.strokeStyle = `rgba(${randint(0,255)},${randint(0,255)},${randint(0,255)},${randint(0,255)})`; ctx.arc(randint(0, size.width), randint(0, size.height), randint(10, 25), 0, 360); ctx.stroke(); ctx.closePath(); } // let i = 0; ctx.textDrawingMode = 'path'; const textStyles = ['#f00', '#2a4', '#00f', '#9a0', '#f0f']; ctx.strokeStyle = textStyles[randint(0, textStyles.length)]; ctx.font = `${settings.dpi * 0.2}px sans-serif`; textStyles.splice(textStyles.indexOf(ctx.strokeStyle), 1); const gap = randint(50, 64); for (const i in text) { ctx.fillText(text[i], settings.padding + (i * randint(40,48)), randint(Math.floor(settings.padding + settings.dpi * 0.05), size.height - settings.padding + Math.floor(settings.dpi * 0.1)), 200); ctx.stroke() } const fauxletters = randint(20,40); let alphabet = 'abcdefghijklmnopqrstuvwxyz'; alphabet += alphabet.toUpperCase(); alphabet += '0123456789'; for (let i = 0; i !== fauxletters; i++) { ctx.strokeStyle = textStyles[randint(0,textStyles.length)] + '2'; ctx.font = `${settings.dpi * 0.2}px serif` ctx.strokeText(alphabet[randint(0, alphabet.length)], randint(0, size.width), randint(0, size.height), 200); ctx.stroke() } res.header('Content-Type', 'image/png'); return draw.toBuffer(); }) }