feat: replace text watermark with BPB logo SVG overlay on gallery uploads
Loads bpb-watermark.svg at startup, converts black fills to semi-transparent white (35% opacity), and composites it centered at 45% image width over every uploaded photo. Removes the old diagonal "BEACH PARTY BALLOONS" text overlay. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
92cf44e5f5
commit
2723a6d954
56
main-site/photo-gallery-app/backend/bpb-watermark.svg
Normal file
56
main-site/photo-gallery-app/backend/bpb-watermark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 55 KiB |
@ -40,6 +40,19 @@ const VARIANTS = {
|
||||
thumb: { size: 640, quality: 76, suffix: '-sm' },
|
||||
};
|
||||
|
||||
// Watermark logo — loaded once at startup, black fills replaced with semi-transparent white
|
||||
let WM_LOGO_BUF = null;
|
||||
(function loadWatermarkLogo() {
|
||||
const wmPath = path.join(__dirname, '..', 'bpb-watermark.svg');
|
||||
try {
|
||||
const raw = fs.readFileSync(wmPath, 'utf8');
|
||||
const styled = raw.replace(/style="fill:#000000"/g, 'style="fill:#ffffff;fill-opacity:0.35"');
|
||||
WM_LOGO_BUF = Buffer.from(styled);
|
||||
} catch (e) {
|
||||
console.warn('[watermark] bpb-watermark.svg not found — watermark disabled.');
|
||||
}
|
||||
}());
|
||||
|
||||
const HEIF_BRANDS = new Set([
|
||||
'heic', 'heix', 'hevc', 'heim', 'heis', 'hevm', 'hevs', 'mif1', 'msf1', 'avif', 'avis'
|
||||
]);
|
||||
@ -191,48 +204,27 @@ router.route('/upload').post(requireAuth, upload.array('photos'), async (req, re
|
||||
parseInt(hash.substring(4, 6), 16),
|
||||
];
|
||||
|
||||
const diagonalOverlay = Buffer.from(`
|
||||
<svg width="2400" height="2400" viewBox="0 0 2400 2400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="diagGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="rgba(255,255,255,0.22)" />
|
||||
<stop offset="50%" stop-color="rgba(255,255,255,0.33)" />
|
||||
<stop offset="100%" stop-color="rgba(255,255,255,0.22)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(1200 1200) rotate(-32)">
|
||||
<text x="0" y="-80" text-anchor="middle" dominant-baseline="middle"
|
||||
fill="url(#diagGrad)" stroke="rgba(0,0,0,0.16)" stroke-width="8"
|
||||
font-family="Arial Black, Arial, sans-serif" font-size="260" letter-spacing="6" textLength="1800" lengthAdjust="spacingAndGlyphs">
|
||||
BEACH PARTY
|
||||
<tspan x="0" dy="280">BALLOONS</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
let buffer;
|
||||
try {
|
||||
// Prepare base image first so we know its post-resize dimensions, then scale overlay slightly smaller to avoid size conflicts
|
||||
const base = sharp(inputBuffer)
|
||||
.rotate()
|
||||
.resize({ width: VARIANTS.main.size, height: VARIANTS.main.size, fit: 'inside', withoutEnlargement: true })
|
||||
.toColorspace('srgb');
|
||||
|
||||
const { data: baseBuffer, info } = await base.toBuffer({ resolveWithObject: true });
|
||||
const targetWidth = Math.max(Math.floor((info.width || VARIANTS.main.size) * 0.98), 1);
|
||||
const targetHeight = Math.max(Math.floor((info.height || VARIANTS.main.size) * 0.98), 1);
|
||||
|
||||
// Scale the diagonal overlay to slightly smaller than the image to ensure it composites cleanly
|
||||
const overlayBuffer = await sharp(diagonalOverlay, { density: 300 })
|
||||
.resize({ width: targetWidth, height: targetHeight, fit: 'cover' })
|
||||
.png()
|
||||
.toBuffer();
|
||||
const compositeInputs = [];
|
||||
if (WM_LOGO_BUF) {
|
||||
const wmW = Math.max(Math.round(info.width * 0.45), 150);
|
||||
const wmBuf = await sharp(WM_LOGO_BUF, { density: 300 })
|
||||
.resize({ width: wmW, fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.png()
|
||||
.toBuffer();
|
||||
compositeInputs.push({ input: wmBuf, gravity: 'center' });
|
||||
}
|
||||
|
||||
buffer = await sharp(baseBuffer)
|
||||
.composite([
|
||||
{ input: overlayBuffer, gravity: 'center' },
|
||||
])
|
||||
.composite(compositeInputs)
|
||||
.toFormat('webp', { quality: VARIANTS.main.quality, effort: 5 })
|
||||
.toBuffer();
|
||||
} catch (err) {
|
||||
@ -247,19 +239,19 @@ router.route('/upload').post(requireAuth, upload.array('photos'), async (req, re
|
||||
.toColorspace('srgb');
|
||||
|
||||
const { data: baseBufferRetry, info: infoRetry } = await baseRetry.toBuffer({ resolveWithObject: true });
|
||||
const overlayRetry = await sharp(diagonalOverlay, { density: 300 })
|
||||
.resize({
|
||||
width: Math.max(Math.floor((infoRetry.width || VARIANTS.main.size) * 0.98), 1),
|
||||
height: Math.max(Math.floor((infoRetry.height || VARIANTS.main.size) * 0.98), 1),
|
||||
fit: 'cover'
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const compositeRetry = [];
|
||||
if (WM_LOGO_BUF) {
|
||||
const wmWRetry = Math.max(Math.round(infoRetry.width * 0.45), 150);
|
||||
const wmBufRetry = await sharp(WM_LOGO_BUF, { density: 300 })
|
||||
.resize({ width: wmWRetry, fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.png()
|
||||
.toBuffer();
|
||||
compositeRetry.push({ input: wmBufRetry, gravity: 'center' });
|
||||
}
|
||||
|
||||
buffer = await sharp(baseBufferRetry)
|
||||
.composite([
|
||||
{ input: overlayRetry, gravity: 'center' },
|
||||
])
|
||||
.composite(compositeRetry)
|
||||
.toFormat('webp', { quality: VARIANTS.main.quality, effort: 5 })
|
||||
.toBuffer();
|
||||
} catch (secondErr) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user