const fs = require('fs'); const path = require('path'); const puppeteer = require('puppeteer'); const storage = require('./utils/storage'); function fileToDataUri(fullPath) { 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 (e) { 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; } 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 stickers = files .map((filePath, i) => { const uri = fileToDataUri(filePath); 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 ``; }) .join(''); return `
${stickers}

${esc(title)}

`; } function buildFriendPageHtml(friend, folder) { const uploadsDir = path.join(storage.friendFolderPath(folder), 'uploads'); const profileUri = friend.profileImage ? fileToDataUri(path.join(uploadsDir, friend.profileImage)) : null; const steckbriefRows = Object.entries(friend.steckbrief || {}) .filter(([, v]) => v && String(v).trim()) .map(([k, v]) => `
${esc(k)}${esc(v)}
`) .join(''); const blocks = [...(friend.blocks || [])].sort((a, b) => a.order - b.order); const blockHtml = blocks .map((b, i) => { const rot = ROTATIONS[i % ROTATIONS.length]; if (b.type === 'text') { return `

${esc(b.content).replace(/\n/g, '
')}

${b.caption ? `
${esc(b.caption)}
` : ''}
`; } if (b.type === 'image') { const uri = fileToDataUri(path.join(uploadsDir, b.filename)); if (!uri) return ''; return `
${b.caption ? `
${esc(b.caption)}
` : ''}
`; } if (b.type === 'audio') { const coverUri = b.coverFilename ? fileToDataUri(path.join(uploadsDir, b.coverFilename)) : null; return `
${coverUri ? `` : ''}
🎵 Audio-Erinnerung${b.caption ? ': ' + esc(b.caption) : ''}
(im Web-Freundesbuch anhörbar)
`; } return ''; }) .join('\n'); return `
${profileUri ? `` : '
?
'}

${esc(friend.name)}

Steckbrief

${steckbriefRows || '

Noch nichts eingetragen.

'}
${blockHtml || '

Noch keine Erinnerungen hinzugefügt.

'}
`; } function buildFullHtml(friends, bookTitle) { const coverPage = buildCoverPageHtml(bookTitle || 'Unser Freundesbuch', friends); const pages = friends .map((f) => buildFriendPageHtml(f, f.folderName)) .join('\n'); return ` ${coverPage} ${pages || '

Noch keine Freunde angelegt.

'} `; } async function generateBookPdf() { const friends = storage.getAllFriendsMeta(); const settings = storage.readSettings(); const html = buildFullHtml(friends, settings.bookTitle); const browser = await puppeteer.launch({ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); try { const page = await browser.newPage(); await page.setContent(html, { waitUntil: 'networkidle0' }); const buffer = await page.pdf({ format: 'A4', printBackground: true }); return buffer; } finally { await browser.close(); } } module.exports = { generateBookPdf, buildFullHtml };