1.0
This commit is contained in:
@@ -21,6 +21,66 @@ function esc(str) {
|
||||
}
|
||||
|
||||
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 `<img class="cover-sticker" style="left:${leftPct}%; top:${topPct}%; width:${sizeMm}mm; height:${sizeMm}mm; transform: rotate(${rot}deg);" src="${uri}" />`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<section class="page cover-page">
|
||||
<div class="cover-stickers">${stickers}</div>
|
||||
<div class="cover-title-wrap">
|
||||
<div class="tape" style="position:static; margin: 0 auto 6mm; transform: rotate(-4deg);"></div>
|
||||
<h1 class="cover-title">${esc(title)}</h1>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function buildFriendPageHtml(friend, folder) {
|
||||
const uploadsDir = path.join(storage.friendFolderPath(folder), 'uploads');
|
||||
@@ -77,7 +137,8 @@ function buildFriendPageHtml(friend, folder) {
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function buildFullHtml(friends) {
|
||||
function buildFullHtml(friends, bookTitle) {
|
||||
const coverPage = buildCoverPageHtml(bookTitle || 'Unser Freundesbuch', friends);
|
||||
const pages = friends
|
||||
.map((f) => buildFriendPageHtml(f, f.folderName))
|
||||
.join('\n');
|
||||
@@ -140,9 +201,24 @@ function buildFullHtml(friends) {
|
||||
.audio-block { width: 58mm; min-height: 30mm; background: #e8f3ec; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; font-size: 13px; padding: 3mm; }
|
||||
.audio-cover { width: 100%; height: 30mm; object-fit: cover; border-radius: 4px; margin-bottom: 2mm; }
|
||||
.cap { font-family: Georgia, serif; font-size: 11px; color: #6b4f8c; margin-top: 2mm; text-align: center; }
|
||||
|
||||
/* Deckblatt */
|
||||
.cover-page { display: flex; align-items: center; justify-content: center; padding: 0; }
|
||||
.cover-stickers { position: absolute; inset: 0; }
|
||||
.cover-sticker {
|
||||
position: absolute; object-fit: cover; border: 4px solid #fff; border-radius: 6px;
|
||||
box-shadow: 0 3px 10px rgba(0,0,0,0.25);
|
||||
}
|
||||
.cover-title-wrap { position: relative; z-index: 5; text-align: center; padding: 0 12mm; }
|
||||
.cover-title {
|
||||
display: inline-block; font-size: 52px; background: rgba(255,253,247,0.94);
|
||||
padding: 6mm 10mm; border-radius: 8px; box-shadow: 0 4px 14px rgba(0,0,0,0.2);
|
||||
transform: rotate(-2deg); color: #6b4f8c; max-width: 160mm;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${coverPage}
|
||||
${pages || '<section class="page"><h1>Noch keine Freunde angelegt.</h1></section>'}
|
||||
</body>
|
||||
</html>`;
|
||||
@@ -150,7 +226,8 @@ ${pages || '<section class="page"><h1>Noch keine Freunde angelegt.</h1></section
|
||||
|
||||
async function generateBookPdf() {
|
||||
const friends = storage.getAllFriendsMeta();
|
||||
const html = buildFullHtml(friends);
|
||||
const settings = storage.readSettings();
|
||||
const html = buildFullHtml(friends, settings.bookTitle);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
|
||||
|
||||
Reference in New Issue
Block a user