diff --git a/main-site/photo-gallery-app/backend/bpb-watermark.svg b/main-site/photo-gallery-app/backend/bpb-watermark.svg new file mode 100644 index 0000000..960828f --- /dev/null +++ b/main-site/photo-gallery-app/backend/bpb-watermark.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + diff --git a/main-site/photo-gallery-app/backend/routes/photos.js b/main-site/photo-gallery-app/backend/routes/photos.js index 399a05a..a152efd 100644 --- a/main-site/photo-gallery-app/backend/routes/photos.js +++ b/main-site/photo-gallery-app/backend/routes/photos.js @@ -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(` - - - - - - - - - - - BEACH PARTY - BALLOONS - - - - `); - 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) {