import sharp, { OverlayOptions } from "sharp"; import path from "path"; import fs from "fs"; /** * Pure JavaScript image watermarking via Sharp (no Python required). * Applies: * 1. School logo in top-left corner * 2. WeChat-scannable QR code in bottom-right corner with white backing * 3. Bottom typography banner: '上海健志空手道馆 · Shanghai Karate Club · Est. 2008' */ export async function applyWatermarkToFile(filePath: string): Promise { if (!fs.existsSync(filePath)) { return; } try { const baseDir = process.cwd(); const logoPath = path.join(baseDir, "public", "assets", "brand", "shanghai_karate_logo.png"); const qrPath = path.join(baseDir, "public", "assets", "brand", "dojo_qr.png"); const baseImage = sharp(filePath); const metadata = await baseImage.metadata(); const bw = metadata.width || 1024; const bh = metadata.height || 1024; const margin = Math.round(bw * 0.035); const compositeLayers: OverlayOptions[] = []; // 1. School Logo (Top-Left) if (fs.existsSync(logoPath)) { const targetLogoW = Math.round(bw * 0.28); const resizedLogoBuffer = await sharp(logoPath) .resize({ width: targetLogoW }) .png() .toBuffer(); compositeLayers.push({ input: resizedLogoBuffer, left: margin, top: margin, }); } // 2. Scannable WeChat QR Code (Bottom-Right) if (fs.existsSync(qrPath)) { const targetQrW = Math.round(bw * 0.17); const qrPad = 6; const totalQrW = targetQrW + qrPad * 2; // Clean white backing with slight rounded corners for maximum readability const whiteBackingSvg = Buffer.from( ` ` ); const resizedQrBuffer = await sharp(qrPath) .resize({ width: targetQrW, height: targetQrW }) .png() .toBuffer(); const qrWithBg = await sharp(whiteBackingSvg) .composite([{ input: resizedQrBuffer, left: qrPad, top: qrPad }]) .png() .toBuffer(); const qrPosX = bw - totalQrW - margin; const qrPosY = bh - totalQrW - Math.round(margin * 1.5); compositeLayers.push({ input: qrWithBg, left: qrPosX, top: qrPosY, }); } // 3. Bottom Text: '上海健志空手道馆 · Shanghai Karate Club · Est. 2008' const fontSize = Math.max(14, Math.round(bw * 0.026)); const textSvg = Buffer.from( ` 上海健志空手道馆 · Shanghai Karate Club · Est. 2008 上海健志空手道馆 · Shanghai Karate Club · Est. 2008 ` ); const textPosY = bh - Math.round(margin * 1.2) - fontSize; compositeLayers.push({ input: textSvg, left: margin, top: Math.max(0, textPosY), }); // Apply all overlays and save to file const outputBuffer = await baseImage .composite(compositeLayers) .jpeg({ quality: 95 }) .toBuffer(); fs.writeFileSync(filePath, outputBuffer); console.log(`[Watermark] Pure JS watermark successfully applied to ${filePath}`); } catch (err) { console.warn("[Watermark] Failed to apply watermark in JavaScript:", err); } }