import type { FastifyInstance } from "fastify"; import sharp from "sharp"; import { z } from "zod"; import { createToolRoute } from "../tool-factory.js"; const settingsSchema = z.object({ borderWidth: z.number().min(0).max(200).default(10), borderColor: z .string() .regex(/^#[0-9a-fA-F]{6}$/) .default("#000000"), cornerRadius: z.number().min(0).max(500).default(0), padding: z.number().min(0).max(200).default(0), shadowBlur: z.number().min(0).max(50).default(0), shadowColor: z .string() .regex(/^#[0-9a-fA-F]{6,8}$/) .default("#00000080"), }); export function registerBorder(app: FastifyInstance) { createToolRoute(app, { toolId: "border", settingsSchema, process: async (inputBuffer, settings, filename) => { const image = sharp(inputBuffer); const meta = await image.metadata(); const w = meta.width ?? 100; const h = meta.height ?? 100; // Parse border color const br = parseInt(settings.borderColor.slice(1, 3), 16); const bg = parseInt(settings.borderColor.slice(3, 5), 16); const bb = parseInt(settings.borderColor.slice(5, 7), 16); const totalBorder = settings.borderWidth + settings.padding; const shadowPad = settings.shadowBlur > 0 ? settings.shadowBlur * 2 : 0; // Extend image with border let result = sharp(inputBuffer).extend({ top: totalBorder + shadowPad, bottom: totalBorder + shadowPad, left: totalBorder + shadowPad, right: totalBorder + shadowPad, background: { r: br, g: bg, b: bb, alpha: 1 }, }); // If inner padding, overlay a background-colored rectangle for padding area if (settings.padding > 0 && settings.borderWidth > 0) { const _outerW = w + totalBorder * 2 + shadowPad * 2; const _outerH = h + totalBorder * 2 + shadowPad * 2; // Create a white padding region behind the image const paddingRect = await sharp({ create: { width: w + settings.padding * 2, height: h + settings.padding * 2, channels: 4, background: { r: 255, g: 255, b: 255, alpha: 1 }, }, }) .png() .toBuffer(); const currentBuf = await result.toBuffer(); result = sharp(currentBuf).composite([ { input: paddingRect, top: settings.borderWidth + shadowPad, left: settings.borderWidth + shadowPad, }, { input: inputBuffer, top: totalBorder + shadowPad, left: totalBorder + shadowPad, }, ]); } // Apply rounded corners via SVG mask if (settings.cornerRadius > 0) { const buf = await result.ensureAlpha().toBuffer(); const bufMeta = await sharp(buf).metadata(); const maskW = bufMeta.width ?? w; const maskH = bufMeta.height ?? h; const r = Math.min(settings.cornerRadius, maskW / 2, maskH / 2); const roundedMask = Buffer.from( ` `, ); const maskBuffer = await sharp(roundedMask).resize(maskW, maskH).toBuffer(); result = sharp(buf).composite([{ input: maskBuffer, blend: "dest-in" }]); } const buffer = await result.png().toBuffer(); return { buffer, filename, contentType: "image/png" }; }, }); }