Fix PDF-Export
This commit is contained in:
@@ -31,7 +31,9 @@ Admin-Passwort für dich, und pro Freund ein frei wählbares Freigabe-Passwort z
|
|||||||
aus allen Profilbildern und Bild-Blöcken der Freunde.
|
aus allen Profilbildern und Bild-Blöcken der Freunde.
|
||||||
- **Eigene Reihenfolge**: Im Admin-Bereich lässt sich die Reihenfolge der Freunde per Pfeil-Buttons
|
- **Eigene Reihenfolge**: Im Admin-Bereich lässt sich die Reihenfolge der Freunde per Pfeil-Buttons
|
||||||
festlegen – sie bestimmt die Seitenfolge im Freundesbuch (nach dem Deckblatt) sowie im PDF.
|
festlegen – sie bestimmt die Seitenfolge im Freundesbuch (nach dem Deckblatt) sowie im PDF.
|
||||||
- **PDF-Export** des gesamten Freundesbuchs inkl. Deckblatt (serverseitig über Puppeteer/Chromium gerendert).
|
- **PDF-Export** des gesamten Freundesbuchs inkl. Deckblatt (serverseitig über Puppeteer/Chromium
|
||||||
|
gerendert; Fotos werden dabei automatisch verkleinert/komprimiert, damit auch viele/große
|
||||||
|
Bilder zuverlässig und schnell zu einem PDF werden).
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|||||||
460
backend/package-lock.json
generated
460
backend/package-lock.json
generated
@@ -13,7 +13,8 @@
|
|||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"multer": "^2.0.0",
|
"multer": "^2.0.0",
|
||||||
"node-id3": "^0.2.6",
|
"node-id3": "^0.2.6",
|
||||||
"puppeteer": "^22.15.0"
|
"puppeteer": "^22.15.0",
|
||||||
|
"sharp": "^0.33.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -39,6 +40,377 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-wasm32": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/runtime": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@puppeteer/browsers": {
|
"node_modules/@puppeteer/browsers": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz",
|
||||||
@@ -476,6 +848,19 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/color": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1",
|
||||||
|
"color-string": "^1.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -494,6 +879,16 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/color-string": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "^1.0.0",
|
||||||
|
"simple-swizzle": "^0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-stream": {
|
"node_modules/concat-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
@@ -650,6 +1045,15 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/devtools-protocol": {
|
"node_modules/devtools-protocol": {
|
||||||
"version": "0.0.1312386",
|
"version": "0.0.1312386",
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
|
||||||
@@ -1946,6 +2350,45 @@
|
|||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sharp": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"color": "^4.2.3",
|
||||||
|
"detect-libc": "^2.0.3",
|
||||||
|
"semver": "^7.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-darwin-arm64": "0.33.5",
|
||||||
|
"@img/sharp-darwin-x64": "0.33.5",
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.0.4",
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.0.4",
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.0.5",
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.0.4",
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.0.4",
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.0.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
|
||||||
|
"@img/sharp-linux-arm": "0.33.5",
|
||||||
|
"@img/sharp-linux-arm64": "0.33.5",
|
||||||
|
"@img/sharp-linux-s390x": "0.33.5",
|
||||||
|
"@img/sharp-linux-x64": "0.33.5",
|
||||||
|
"@img/sharp-linuxmusl-arm64": "0.33.5",
|
||||||
|
"@img/sharp-linuxmusl-x64": "0.33.5",
|
||||||
|
"@img/sharp-wasm32": "0.33.5",
|
||||||
|
"@img/sharp-win32-ia32": "0.33.5",
|
||||||
|
"@img/sharp-win32-x64": "0.33.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||||
@@ -2018,6 +2461,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-swizzle": {
|
||||||
|
"version": "0.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||||
|
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arrayish": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/smart-buffer": {
|
"node_modules/smart-buffer": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"multer": "^2.0.0",
|
"multer": "^2.0.0",
|
||||||
"node-id3": "^0.2.6",
|
"node-id3": "^0.2.6",
|
||||||
"puppeteer": "^22.15.0"
|
"puppeteer": "^22.15.0",
|
||||||
|
"sharp": "^0.33.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const puppeteer = require('puppeteer');
|
const puppeteer = require('puppeteer');
|
||||||
|
const sharp = require('sharp');
|
||||||
const storage = require('./utils/storage');
|
const storage = require('./utils/storage');
|
||||||
|
|
||||||
function fileToDataUri(fullPath) {
|
// 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 {
|
try {
|
||||||
const buf = fs.readFileSync(fullPath);
|
const buf = fs.readFileSync(fullPath);
|
||||||
const ext = path.extname(fullPath).toLowerCase().replace('.', '');
|
const ext = path.extname(fullPath).toLowerCase().replace('.', '');
|
||||||
const mime = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', gif: 'gif', webp: 'webp' }[ext] || 'jpeg';
|
const mime = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', gif: 'gif', webp: 'webp' }[ext] || 'jpeg';
|
||||||
return `data:image/${mime};base64,${buf.toString('base64')}`;
|
return `data:image/${mime};base64,${buf.toString('base64')}`;
|
||||||
} catch (e) {
|
} catch (e2) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function esc(str) {
|
function esc(str) {
|
||||||
@@ -44,7 +58,7 @@ function collectStickerImages(friends) {
|
|||||||
return imgs;
|
return imgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCoverPageHtml(title, friends) {
|
async function buildCoverPageHtml(title, friends) {
|
||||||
let files = collectStickerImages(friends);
|
let files = collectStickerImages(friends);
|
||||||
if (files.length > COVER_MAX_STICKERS) {
|
if (files.length > COVER_MAX_STICKERS) {
|
||||||
const step = files.length / COVER_MAX_STICKERS;
|
const step = files.length / COVER_MAX_STICKERS;
|
||||||
@@ -54,9 +68,9 @@ function buildCoverPageHtml(title, friends) {
|
|||||||
const cols = Math.min(6, Math.max(3, Math.ceil(Math.sqrt(files.length * 1.6)) || 3));
|
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 rows = Math.max(1, Math.ceil(files.length / cols));
|
||||||
|
|
||||||
const stickers = files
|
const stickerHtmlList = await Promise.all(
|
||||||
.map((filePath, i) => {
|
files.map(async (filePath, i) => {
|
||||||
const uri = fileToDataUri(filePath);
|
const uri = await fileToDataUri(filePath, 300); // Sticker sind klein -> niedrige Auflösung reicht
|
||||||
if (!uri) return '';
|
if (!uri) return '';
|
||||||
const cellX = i % cols;
|
const cellX = i % cols;
|
||||||
const cellY = Math.floor(i / cols);
|
const cellY = Math.floor(i / cols);
|
||||||
@@ -70,11 +84,11 @@ function buildCoverPageHtml(title, friends) {
|
|||||||
const sizeMm = Math.round(28 + rs * 20);
|
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}" />`;
|
return `<img class="cover-sticker" style="left:${leftPct}%; top:${topPct}%; width:${sizeMm}mm; height:${sizeMm}mm; transform: rotate(${rot}deg);" src="${uri}" />`;
|
||||||
})
|
})
|
||||||
.join('');
|
);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<section class="page cover-page">
|
<section class="page cover-page">
|
||||||
<div class="cover-stickers">${stickers}</div>
|
<div class="cover-stickers">${stickerHtmlList.join('')}</div>
|
||||||
<div class="cover-title-wrap">
|
<div class="cover-title-wrap">
|
||||||
<div class="tape" style="position:static; margin: 0 auto 6mm; transform: rotate(-4deg);"></div>
|
<div class="tape" style="position:static; margin: 0 auto 6mm; transform: rotate(-4deg);"></div>
|
||||||
<h1 class="cover-title">${esc(title)}</h1>
|
<h1 class="cover-title">${esc(title)}</h1>
|
||||||
@@ -82,9 +96,11 @@ function buildCoverPageHtml(title, friends) {
|
|||||||
</section>`;
|
</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFriendPageHtml(friend, folder) {
|
async function buildFriendPageHtml(friend, folder) {
|
||||||
const uploadsDir = path.join(storage.friendFolderPath(folder), 'uploads');
|
const uploadsDir = path.join(storage.friendFolderPath(folder), 'uploads');
|
||||||
const profileUri = friend.profileImage ? fileToDataUri(path.join(uploadsDir, friend.profileImage)) : null;
|
const profileUri = friend.profileImage
|
||||||
|
? await fileToDataUri(path.join(uploadsDir, friend.profileImage), 500)
|
||||||
|
: null;
|
||||||
|
|
||||||
const steckbriefRows = Object.entries(friend.steckbrief || {})
|
const steckbriefRows = Object.entries(friend.steckbrief || {})
|
||||||
.filter(([, v]) => v && String(v).trim())
|
.filter(([, v]) => v && String(v).trim())
|
||||||
@@ -92,8 +108,8 @@ function buildFriendPageHtml(friend, folder) {
|
|||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
const blocks = [...(friend.blocks || [])].sort((a, b) => a.order - b.order);
|
const blocks = [...(friend.blocks || [])].sort((a, b) => a.order - b.order);
|
||||||
const blockHtml = blocks
|
const blockHtmlList = await Promise.all(
|
||||||
.map((b, i) => {
|
blocks.map(async (b, i) => {
|
||||||
const rot = ROTATIONS[i % ROTATIONS.length];
|
const rot = ROTATIONS[i % ROTATIONS.length];
|
||||||
if (b.type === 'text') {
|
if (b.type === 'text') {
|
||||||
return `<div class="pdf-block text-block" style="transform: rotate(${rot * 0.4}deg)">
|
return `<div class="pdf-block text-block" style="transform: rotate(${rot * 0.4}deg)">
|
||||||
@@ -102,7 +118,7 @@ function buildFriendPageHtml(friend, folder) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (b.type === 'image') {
|
if (b.type === 'image') {
|
||||||
const uri = fileToDataUri(path.join(uploadsDir, b.filename));
|
const uri = await fileToDataUri(path.join(uploadsDir, b.filename), 700);
|
||||||
if (!uri) return '';
|
if (!uri) return '';
|
||||||
return `<div class="pdf-block image-block polaroid" style="transform: rotate(${rot}deg)">
|
return `<div class="pdf-block image-block polaroid" style="transform: rotate(${rot}deg)">
|
||||||
<img src="${uri}" />
|
<img src="${uri}" />
|
||||||
@@ -110,7 +126,7 @@ function buildFriendPageHtml(friend, folder) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (b.type === 'audio') {
|
if (b.type === 'audio') {
|
||||||
const coverUri = b.coverFilename ? fileToDataUri(path.join(uploadsDir, b.coverFilename)) : null;
|
const coverUri = b.coverFilename ? await fileToDataUri(path.join(uploadsDir, b.coverFilename), 500) : null;
|
||||||
return `<div class="pdf-block audio-block" style="transform: rotate(${rot * 0.5}deg)">
|
return `<div class="pdf-block audio-block" style="transform: rotate(${rot * 0.5}deg)">
|
||||||
${coverUri ? `<img class="audio-cover" src="${coverUri}" />` : ''}
|
${coverUri ? `<img class="audio-cover" src="${coverUri}" />` : ''}
|
||||||
<div class="audio-note">🎵 Audio-Erinnerung${b.caption ? ': ' + esc(b.caption) : ''}<br/><small>(im Web-Freundesbuch anhörbar)</small></div>
|
<div class="audio-note">🎵 Audio-Erinnerung${b.caption ? ': ' + esc(b.caption) : ''}<br/><small>(im Web-Freundesbuch anhörbar)</small></div>
|
||||||
@@ -118,7 +134,8 @@ function buildFriendPageHtml(friend, folder) {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
})
|
})
|
||||||
.join('\n');
|
);
|
||||||
|
const blockHtml = blockHtmlList.join('\n');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<section class="page">
|
<section class="page">
|
||||||
@@ -137,11 +154,10 @@ function buildFriendPageHtml(friend, folder) {
|
|||||||
</section>`;
|
</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFullHtml(friends, bookTitle) {
|
async function buildFullHtml(friends, bookTitle) {
|
||||||
const coverPage = buildCoverPageHtml(bookTitle || 'Unser Freundesbuch', friends);
|
const coverPage = await buildCoverPageHtml(bookTitle || 'Unser Freundesbuch', friends);
|
||||||
const pages = friends
|
const friendPages = await Promise.all(friends.map((f) => buildFriendPageHtml(f, f.folderName)));
|
||||||
.map((f) => buildFriendPageHtml(f, f.folderName))
|
const pages = friendPages.join('\n');
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
@@ -227,17 +243,25 @@ ${pages || '<section class="page"><h1>Noch keine Freunde angelegt.</h1></section
|
|||||||
async function generateBookPdf() {
|
async function generateBookPdf() {
|
||||||
const friends = storage.getAllFriendsMeta();
|
const friends = storage.getAllFriendsMeta();
|
||||||
const settings = storage.readSettings();
|
const settings = storage.readSettings();
|
||||||
const html = buildFullHtml(friends, settings.bookTitle);
|
const html = await buildFullHtml(friends, settings.bookTitle);
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
|
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-dev-shm-usage', // wichtig in Docker: /dev/shm ist oft zu klein und lässt Chromium sonst hängen
|
||||||
|
'--disable-gpu',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setContent(html, { waitUntil: 'networkidle0' });
|
// "domcontentloaded" reicht hier völlig: die Seite ist vollständig selbst enthalten
|
||||||
const buffer = await page.pdf({ format: 'A4', printBackground: true });
|
// (Bilder als Base64 eingebettet, keine externen Ressourcen) -> "networkidle0" kann
|
||||||
|
// in Docker ohne echten Netzwerkverkehr unnötig lange bzw. nie sauber auflösen.
|
||||||
|
await page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||||
|
const buffer = await page.pdf({ format: 'A4', printBackground: true, timeout: 60000 });
|
||||||
return buffer;
|
return buffer;
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user