const fs = require('fs');
const path = require('path');
const puppeteer = require('puppeteer');
const sharp = require('sharp');
const storage = require('./utils/storage');
// Bilder werden vor dem Einbetten verkleinert/komprimiert (JPEG). Das hält die
// generierte HTML-Seite klein genug, damit Chromium sie zuverlässig und schnell
// rendert (große Original-Fotos als Base64 können sonst zu Timeouts/Abstürzen führen).
async function fileToDataUri(fullPath, maxWidth = 900) {
try {
const resized = await sharp(fullPath)
.rotate() // EXIF-Ausrichtung berücksichtigen
.resize({ width: maxWidth, withoutEnlargement: true })
.jpeg({ quality: 74 })
.toBuffer();
return `data:image/jpeg;base64,${resized.toString('base64')}`;
} catch (e) {
// Fallback: Originaldatei unverändert einbetten (z. B. bei nicht unterstützten Formaten)
try {
const buf = fs.readFileSync(fullPath);
const ext = path.extname(fullPath).toLowerCase().replace('.', '');
const mime = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', gif: 'gif', webp: 'webp' }[ext] || 'jpeg';
return `data:image/${mime};base64,${buf.toString('base64')}`;
} catch (e2) {
return null;
}
}
}
function esc(str) {
return String(str || '').replace(/[&<>"']/g, (c) => (
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
));
}
const ROTATIONS = [-4, 3, -2, 5, -3, 2, -5, 4];
const COVER_MAX_STICKERS = 30;
// Gleicher deterministischer Pseudo-Zufallsgenerator wie im Buch-Viewer (book.js)
function seededRandom(seed) {
let t = (seed += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
function collectStickerImages(friends) {
const imgs = [];
friends.forEach((f) => {
const uploadsDir = path.join(storage.friendFolderPath(f.folderName), 'uploads');
if (f.profileImage) imgs.push(path.join(uploadsDir, f.profileImage));
(f.blocks || []).forEach((b) => {
if (b.type === 'image' && b.filename) imgs.push(path.join(uploadsDir, b.filename));
if (b.type === 'audio' && b.coverFilename) imgs.push(path.join(uploadsDir, b.coverFilename));
});
});
return imgs;
}
async function buildCoverPageHtml(title, friends) {
let files = collectStickerImages(friends);
if (files.length > COVER_MAX_STICKERS) {
const step = files.length / COVER_MAX_STICKERS;
files = Array.from({ length: COVER_MAX_STICKERS }, (_, i) => files[Math.floor(i * step)]);
}
const cols = Math.min(6, Math.max(3, Math.ceil(Math.sqrt(files.length * 1.6)) || 3));
const rows = Math.max(1, Math.ceil(files.length / cols));
const stickerHtmlList = await Promise.all(
files.map(async (filePath, i) => {
const uri = await fileToDataUri(filePath, 300); // Sticker sind klein -> niedrige Auflösung reicht
if (!uri) return '';
const cellX = i % cols;
const cellY = Math.floor(i / cols);
const rx = seededRandom(i * 13 + 1);
const ry = seededRandom(i * 17 + 2);
const rr = seededRandom(i * 7 + 3);
const rs = seededRandom(i * 5 + 4);
const leftPct = ((cellX + 0.1 + rx * 0.8) / cols) * 100;
const topPct = ((cellY + 0.1 + ry * 0.8) / rows) * 100;
const rot = (rr * 50 - 25).toFixed(1);
const sizeMm = Math.round(28 + rs * 20);
return ``;
})
);
return `
${esc(title)}
${esc(b.content).replace(/\n/g, '
')}
Noch nichts eingetragen.
'}Noch keine Erinnerungen hinzugefügt.
'}