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' },
|
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([
|
const HEIF_BRANDS = new Set([
|
||||||
'heic', 'heix', 'hevc', 'heim', 'heis', 'hevm', 'hevs', 'mif1', 'msf1', 'avif', 'avis'
|
'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),
|
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;
|
let buffer;
|
||||||
try {
|
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)
|
const base = sharp(inputBuffer)
|
||||||
.rotate()
|
.rotate()
|
||||||
.resize({ width: VARIANTS.main.size, height: VARIANTS.main.size, fit: 'inside', withoutEnlargement: true })
|
.resize({ width: VARIANTS.main.size, height: VARIANTS.main.size, fit: 'inside', withoutEnlargement: true })
|
||||||
.toColorspace('srgb');
|
.toColorspace('srgb');
|
||||||
|
|
||||||
const { data: baseBuffer, info } = await base.toBuffer({ resolveWithObject: true });
|
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 compositeInputs = [];
|
||||||
const overlayBuffer = await sharp(diagonalOverlay, { density: 300 })
|
if (WM_LOGO_BUF) {
|
||||||
.resize({ width: targetWidth, height: targetHeight, fit: 'cover' })
|
const wmW = Math.max(Math.round(info.width * 0.45), 150);
|
||||||
.png()
|
const wmBuf = await sharp(WM_LOGO_BUF, { density: 300 })
|
||||||
.toBuffer();
|
.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)
|
buffer = await sharp(baseBuffer)
|
||||||
.composite([
|
.composite(compositeInputs)
|
||||||
{ input: overlayBuffer, gravity: 'center' },
|
|
||||||
])
|
|
||||||
.toFormat('webp', { quality: VARIANTS.main.quality, effort: 5 })
|
.toFormat('webp', { quality: VARIANTS.main.quality, effort: 5 })
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -247,19 +239,19 @@ router.route('/upload').post(requireAuth, upload.array('photos'), async (req, re
|
|||||||
.toColorspace('srgb');
|
.toColorspace('srgb');
|
||||||
|
|
||||||
const { data: baseBufferRetry, info: infoRetry } = await baseRetry.toBuffer({ resolveWithObject: true });
|
const { data: baseBufferRetry, info: infoRetry } = await baseRetry.toBuffer({ resolveWithObject: true });
|
||||||
const overlayRetry = await sharp(diagonalOverlay, { density: 300 })
|
|
||||||
.resize({
|
const compositeRetry = [];
|
||||||
width: Math.max(Math.floor((infoRetry.width || VARIANTS.main.size) * 0.98), 1),
|
if (WM_LOGO_BUF) {
|
||||||
height: Math.max(Math.floor((infoRetry.height || VARIANTS.main.size) * 0.98), 1),
|
const wmWRetry = Math.max(Math.round(infoRetry.width * 0.45), 150);
|
||||||
fit: 'cover'
|
const wmBufRetry = await sharp(WM_LOGO_BUF, { density: 300 })
|
||||||
})
|
.resize({ width: wmWRetry, fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
compositeRetry.push({ input: wmBufRetry, gravity: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
buffer = await sharp(baseBufferRetry)
|
buffer = await sharp(baseBufferRetry)
|
||||||
.composite([
|
.composite(compositeRetry)
|
||||||
{ input: overlayRetry, gravity: 'center' },
|
|
||||||
])
|
|
||||||
.toFormat('webp', { quality: VARIANTS.main.quality, effort: 5 })
|
.toFormat('webp', { quality: VARIANTS.main.quality, effort: 5 })
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
} catch (secondErr) {
|
} catch (secondErr) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user