Files
karate_halloween/lib/watermark.ts
T
Ajasra 6772e0cf37 perf(booth): optimize assets and result flow
WebP reduces UI payload; JPEG keeps portrait saves compatible.\nFinish ends the visit without prompting another portrait.
2026-10-30 00:00:16 +08:00

119 lines
3.9 KiB
TypeScript

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<void> {
if (!fs.existsSync(filePath)) {
return;
}
try {
const baseDir = process.cwd();
const logoPath = path.join(baseDir, "public", "assets", "brand", "shanghai_karate_logo.webp");
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(
`<svg width="${totalQrW}" height="${totalQrW}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" rx="6" fill="#ffffff" fill-opacity="0.96"/>
</svg>`
);
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(
`<svg width="${bw}" height="${Math.round(fontSize * 2.5)}" xmlns="http://www.w3.org/2000/svg">
<style>
.text {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: ${fontSize}px;
font-weight: 700;
}
.shadow {
fill: rgba(0, 0, 0, 0.85);
}
.main {
fill: #fff0c8;
}
</style>
<text x="2" y="${fontSize + 2}" class="text shadow">上海健志空手道馆 · Shanghai Karate Club · Est. 2008</text>
<text x="0" y="${fontSize}" class="text main">上海健志空手道馆 · Shanghai Karate Club · Est. 2008</text>
</svg>`
);
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);
}
}