import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; import type { RenderedImage, ResvgRenderOptions } from "@resvg/resvg-js"; export interface BookingOgImageInput { title?: string | null; description?: string | null; duration?: number | null; durations?: number[] | null; username?: string | null; ownerEmail?: string | null; bookingPageTitle?: string | null; profileImageDataUrl?: string | null; } const WIDTH = 1200; const HEIGHT = 630; const BRAND_BLUE = "#00B5FF"; const BRAND_MINT = "#48FFE4"; const BG = "#000000"; const SURFACE = "#0a0a0a"; const BORDER = "#1f1f1f"; const FG = "#ededed"; const MUTED = "#a0a0a0"; const FONT_FAMILY = "Liberation Sans, Arial, system-ui, sans-serif"; const FONT_SOURCE_PATHS = [ "../../assets/fonts/LiberationSans-Regular.ttf", "../../assets/fonts/LiberationSans-Bold.ttf", ] as const; const AVATAR_CX = 996; const AVATAR_CY = 170; const AVATAR_SIZE = 172; const LOGO_MARK = ` `; function escapeSvg(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function cleanText(value: string | null | undefined): string { return String(value ?? "") .replace(/\s+/g, " ") .trim(); } function titleCase(value: string): string { return value .split(/[\s._-]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .join(" "); } function displayNameFromIdentifier( username?: string | null, ownerEmail?: string | null, ): string { const usernameName = titleCase(cleanText(username)); const emailName = titleCase(cleanText(ownerEmail).split("@")[0]); if (emailName.split(/\s+/).length > usernameName.split(/\s+/).length) { return emailName; } return usernameName || emailName || "Host"; } function hostNameFromBookingPageTitle(title?: string | null): string | null { const clean = cleanText(title); const match = clean.match(/^book(?:\s+a)?\s+meeting\s+with\s+(.+)$/i); if (match?.[1]) return cleanText(match[1]); const meetMatch = clean.match(/^meet\s+(.+)$/i); if (meetMatch?.[1]) return cleanText(meetMatch[1]); return null; } function isGenericMeetingTitle(title: string): boolean { return /^(book\s+a\s+meeting|meeting)$/i.test(title); } function displayTitle(input: BookingOgImageInput, hostName: string): string { const title = cleanText(input.title); const pageTitle = cleanText(input.bookingPageTitle); if (title && !isGenericMeetingTitle(title)) return title; return hostName ? `Meet with ${hostName}` : pageTitle || title || "Book a meeting"; } function durationLabel(input: BookingOgImageInput): string { const durations = Array.isArray(input.durations) ? input.durations.filter((value) => Number.isFinite(value) && value > 0) : []; if (durations.length > 1) { return `${durations.slice(0, 3).join(" / ")} min options`; } const duration = durations[0] ?? input.duration ?? 30; return `${duration} min meeting`; } function initialsFor(name: string): string { const parts = name.split(/\s+/).filter(Boolean); if (parts.length === 0) return "AN"; if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase(); } function validProfileImageDataUrl(value: string | null | undefined): string { const dataUrl = cleanText(value); return /^data:image\/[a-z0-9.+-]+;base64,/i.test(dataUrl) ? dataUrl : ""; } function estimateTextWidth(value: string, fontSize: number): number { let units = 0; for (const char of value) { if (char === " ") { units += 0.28; } else if (/[MW@#%&]/.test(char)) { units += 0.86; } else if (/[A-Z]/.test(char)) { units += 0.64; } else if (/[ilI.,:;|!']/u.test(char)) { units += 0.26; } else if (/[0-9]/.test(char)) { units += 0.56; } else { units += 0.54; } } return units * fontSize; } function trimTextToWidth( value: string, fontSize: number, maxWidth: number, ): string { const ellipsis = "..."; let trimmed = value.trim(); while ( trimmed.length > 0 && estimateTextWidth(`${trimmed}${ellipsis}`, fontSize) > maxWidth ) { trimmed = trimmed.slice(0, -1).trimEnd(); } return trimmed ? `${trimmed}${ellipsis}` : ellipsis; } function wrapText( value: string, fontSize: number, maxWidth: number, maxLines: number, ): string[] { const words = value.split(/\s+/).filter(Boolean); const lines: string[] = []; let current = ""; for (const word of words) { const next = current ? `${current} ${word}` : word; if (estimateTextWidth(next, fontSize) <= maxWidth) { current = next; continue; } if (!current) { lines.push(trimTextToWidth(word, fontSize, maxWidth)); current = ""; } else { lines.push(current); current = word; } if (lines.length === maxLines) break; } if (current && lines.length < maxLines) lines.push(current); const usedWordCount = lines.join(" ").split(/\s+/).filter(Boolean).length; if (usedWordCount < words.length && lines.length > 0) { lines[lines.length - 1] = trimTextToWidth( lines[lines.length - 1], fontSize, maxWidth, ); } return lines.length ? lines : [trimTextToWidth(value, fontSize, maxWidth)]; } function textBlock({ lines, x, y, fontSize, lineHeight, weight, fill, }: { lines: string[]; x: number; y: number; fontSize: number; lineHeight: number; weight: number; fill: string; }): string { return `${lines .map( (line, index) => `${escapeSvg(line)}`, ) .join("")}`; } export function renderBookingOgImageSvg(input: BookingOgImageInput): string { const inferredHost = hostNameFromBookingPageTitle(input.bookingPageTitle) ?? displayNameFromIdentifier(input.username, input.ownerEmail); const title = displayTitle(input, inferredHost); const titleFitsSingleLine = estimateTextWidth(title, 82) <= 820; const titleLines = titleFitsSingleLine ? [title] : wrapText(title, 66, 820, 2); const duration = durationLabel(input); const initials = initialsFor(inferredHost); const profileImageDataUrl = validProfileImageDataUrl( input.profileImageDataUrl, ); const titleFontSize = titleFitsSingleLine ? 82 : 66; const titleLineHeight = titleLines.length > 1 ? 76 : 92; const titleGroupY = titleLines.length > 1 ? 332 : 382; const durationBadge = titleLines.length === 1 ? ` ${escapeSvg(duration)} ` : ""; const avatarContent = profileImageDataUrl ? `` : ` ${escapeSvg(initials)}`; return ` Agent-Native Calendar booking link ${LOGO_MARK} Agent-Native Calendar ${avatarContent} ${textBlock({ lines: titleLines, x: 0, y: 0, fontSize: titleFontSize, lineHeight: titleLineHeight, weight: 800, fill: FG, })} ${durationBadge} `; } interface BookingOgRenderOptions { fontFiles?: string[]; } function resolveSourceFontFiles(): string[] { try { const metaUrl = import.meta.url; if (!metaUrl.startsWith("file:")) return []; return FONT_SOURCE_PATHS.map((sourcePath) => fileURLToPath(new URL(sourcePath, metaUrl)), ).filter((fontFile) => existsSync(fontFile)); } catch { return []; } } function bookingOgResvgOptions( options: BookingOgRenderOptions = {}, ): ResvgRenderOptions { const fontFiles = ( options.fontFiles?.length ? options.fontFiles : resolveSourceFontFiles() ).filter((fontFile) => existsSync(fontFile)); const hasBundledFonts = fontFiles.length > 0; return { fitTo: { mode: "width", value: WIDTH }, font: { loadSystemFonts: !hasBundledFonts, ...(hasBundledFonts ? { fontFiles } : {}), defaultFontFamily: "Liberation Sans", sansSerifFamily: "Liberation Sans", }, }; } async function loadResvg(): Promise { return import(/* @vite-ignore */ "@resvg/resvg-js"); } export async function renderBookingOgImage( input: BookingOgImageInput, options: BookingOgRenderOptions = {}, ): Promise { const { Resvg } = await loadResvg(); return new Resvg(renderBookingOgImageSvg(input), { ...bookingOgResvgOptions(options), }).render(); } export async function renderBookingOgImagePng( input: BookingOgImageInput, options: BookingOgRenderOptions = {}, ): Promise { return (await renderBookingOgImage(input, options)).asPng(); }