import type { FastifyInstance } from "fastify"; import sharp from "sharp"; import { z } from "zod"; import { resolveOutputFormat } from "../../lib/output-format.js"; import { createToolRoute } from "../tool-factory.js"; const settingsSchema = z.object({ text: z.string().min(1).max(500), fontSize: z.number().min(8).max(200).default(48), color: z .string() .regex(/^#[0-9a-fA-F]{6}$/) .default("#FFFFFF"), position: z.enum(["top", "center", "bottom"]).default("bottom"), backgroundBox: z.boolean().default(false), backgroundColor: z .string() .regex(/^#[0-9a-fA-F]{6}$/) .default("#000000"), shadow: z.boolean().default(true), }); function escapeXml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } export function registerTextOverlay(app: FastifyInstance) { createToolRoute(app, { toolId: "text-overlay", settingsSchema, process: async (inputBuffer, settings, filename) => { const outputFormat = await resolveOutputFormat(inputBuffer, filename); const image = sharp(inputBuffer); const metadata = await image.metadata(); const width = metadata.width ?? 800; const height = metadata.height ?? 600; const escapedText = escapeXml(settings.text); let y: number; const pad = settings.fontSize; switch (settings.position) { case "top": y = pad + settings.fontSize; break; case "center": y = height / 2; break; default: y = height - pad; break; } const x = width / 2; // Build SVG text element with optional effects let filter = ""; let filterRef = ""; if (settings.shadow) { filter = ``; filterRef = ' filter="url(#shadow)"'; } let bgRect = ""; if (settings.backgroundBox) { const boxH = settings.fontSize * 1.8; const boxY = y - settings.fontSize * 0.9; bgRect = ``; } const svgOverlay = ` ${filter} ${bgRect} ${escapedText} `; const svgBuffer = Buffer.from(svgOverlay); const result = await image.composite([{ input: svgBuffer, top: 0, left: 0 }]); const buffer = await result .toFormat(outputFormat.format, { quality: outputFormat.quality }) .toBuffer(); return { buffer, filename, contentType: outputFormat.contentType }; }, }); }