Compare commits

...
15 Commits
8 changed files with 221 additions and 40 deletions

No files matched your search

+6
View File
@@ -2,9 +2,15 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v0.4.3](https://git.odit.services/lfk/document-server/compare/v0.4.3...v0.4.3)
#### [v0.4.3](https://git.odit.services/lfk/document-server/compare/v0.4.2...v0.4.3)
> 30 March 2021
- Merge pull request 'Release 0.4.3' (#40) from dev into main [`c8dc998`](https://git.odit.services/lfk/document-server/commit/c8dc998ecdccc7fc4348ecc0db552a3d7bc2eb52)
- 🧾New changelog file version [CI SKIP] [skip ci] [`289a0d8`](https://git.odit.services/lfk/document-server/commit/289a0d8671575dda911c64f79d24726d3bbee071)
- 🧾New changelog file version [CI SKIP] [skip ci] [`3df3d26`](https://git.odit.services/lfk/document-server/commit/3df3d26708aab12590cd9c1f697cfdea8017ace4)
- 🧾New changelog file version [CI SKIP] [skip ci] [`457ea26`](https://git.odit.services/lfk/document-server/commit/457ea26cf8124009084415d12c7a0e31912a3eb1)
- 🚀Bumped version to v0.4.3 [`c3beb3e`](https://git.odit.services/lfk/document-server/commit/c3beb3e1032492f9a8304c4b099a700ad0d1d2cf)
- Pipeline mtu fix [`c2d2b66`](https://git.odit.services/lfk/document-server/commit/c2d2b66f2f6fbd30c2027fd3dab393db5219eb44)
+29 -3
View File
@@ -10,6 +10,7 @@ import { PDFDocument } from 'pdf-lib';
import puppeteer from "puppeteer";
import { awaitAsyncHandlebarHelpers, helpers } from './asyncHelpers';
import { config } from './config';
import { CertificateRunner } from './models/CertificateRunner';
import { Runner } from './models/Runner';
import { RunnerCard } from './models/RunnerCard';
import { RunnerGroup } from './models/RunnerGroup';
@@ -153,13 +154,37 @@ export class PdfCreator {
await i18next.changeLanguage(locale);
const template_source = fs.readFileSync(`${this.templateDir}/runner_card.html`, 'utf8');
const template = Handlebars.compile(template_source);
let result = template({ cards, cards_swapped, eventname: "LfK! 2069", codeformat: "qrcode" })
let result = template({ cards, cards_swapped, eventname: config.eventname, codeformat: codeformat })
result = await awaitAsyncHandlebarHelpers(result);
fs.writeFileSync("lelelelele.tmp", result);
const pdf = await this.renderPdf(result, { format: "A4", landscape: false });
return pdf
}
/**
* Generate sponsoring contract pdfs.
* @param runner The runner you want to generate the contracts for.
* @param locale The locale used for the contracts (default:en)
*/
public async generateRunnerCertficates(runners: CertificateRunner[], locale: string = "en"): Promise<Buffer> {
if (runners.length > 50) {
let pdf_promises = new Array<Promise<Buffer>>();
let i, j;
for (i = 0, j = runners.length; i < j; i += 50) {
let chunk = runners.slice(i, i + 50);
pdf_promises.push(this.generateRunnerCertficates(chunk, locale));
}
const pdfs = await Promise.all(pdf_promises);
return await this.mergePdfs(pdfs);
}
await i18next.changeLanguage(locale);
const template_source = fs.readFileSync(`${this.templateDir}/runner_certificate.html`, 'utf8');
const template = Handlebars.compile(template_source);
let result = template({ runners, eventname: config.eventname });
result = await awaitAsyncHandlebarHelpers(result);
const pdf = await this.renderPdf(result, { format: "A4", landscape: false, printBackground: true });
return pdf;
}
/**
* Converts all images in html to base64.
* Works with image files in the template directory or images from urls.
@@ -167,6 +192,7 @@ export class PdfCreator {
*/
public async imgToBase64(html): Promise<string> {
const $ = cheerio.load(html)
$('img').each(async (index, element) => {
let imgsrc = $(element).attr("src");
if (imgsrc.startsWith("data:image")) {
@@ -192,7 +218,7 @@ export class PdfCreator {
image = `data:${img_type};base64,${image}`
$(element).attr("src", image)
})
});
return $.html();
}
+34
View File
@@ -1,5 +1,6 @@
import { Authorized, Body, JsonController, Post, QueryParam, Res } from 'routing-controllers';
import { OpenAPI } from 'routing-controllers-openapi';
import { CertificateRunner } from '../models/CertificateRunner';
import { Runner } from '../models/Runner';
import { RunnerCard } from '../models/RunnerCard';
import { PdfCreator } from '../PdfCreator';
@@ -54,6 +55,25 @@ export class PdfController {
return contracts;
}
@Post('/certificates')
@OpenAPI({ description: "Generate runner certificate pdfs from certificate runner objects.<br>You can choose your prefered locale by passing the 'locale' query-param.<br> If you provide more than 100 runenrs this could take a moment or two (we tested up to 1000 runners in about 70sec so far)." })
async generateCertificates(@Body({ validate: true, options: { limit: "500mb" } }) runners: CertificateRunner[], @Res() res: any, @QueryParam("locale") locale: string, @QueryParam("download") download: boolean) {
if (!this.initialized) {
await this.pdf.init();
this.initialized = true;
}
if (!Array.isArray(runners)) {
runners = [runners];
}
runners = this.mapCertificatRunnersGroupNames(runners)
const certificates = await this.pdf.generateRunnerCertficates(runners, locale);
res.setHeader('content-type', 'application/pdf');
if (download) {
res.setHeader('Content-Disposition', 'attachment; filename="certificates.pdf"')
}
return certificates;
}
private mapRunnerGroupNames(runners: Runner[]): Runner[] {
let response = new Array<Runner>();
for (let runner of runners) {
@@ -68,6 +88,20 @@ export class PdfController {
return response;
}
private mapCertificatRunnersGroupNames(runners: CertificateRunner[]): CertificateRunner[] {
let response = new Array<CertificateRunner>();
for (let runner of runners) {
if (!runner.group.parentGroup) {
runner.group.fullName = runner.group.name;
}
else {
runner.group.fullName = `${runner.group.parentGroup.name}/${runner.group.name}`;
}
response.push(runner)
}
return response;
}
private mapCardGroupNames(cards: RunnerCard[]): RunnerCard[] {
let response = new Array<RunnerCard>();
for (let card of cards) {
+27 -19
View File
@@ -1,20 +1,28 @@
{
"address": "Adresse",
"city": "Stadt",
"date": "Datum",
"firstname": "Vorname",
"group": "Team/Klasse",
"house_number": "Hausnummer",
"id": "ID",
"lastname": "Nachname",
"location": "Ort",
"please_use_blockletters": "Bitte in DRUCKBUCHSTABEN schreiben",
"postalcode": "Postleitzahl",
"signature": "Unterschrift",
"sponsor": "Sponsor",
"sponsoring_address_condition": "Muss ausgefüllt werden, wenn Sie eine Spendenquittung benötigen - Spendenquittungen können erst ab einem Gesamtbetrag von {{sponsoring_receipt_minimum_amount}}{{currency_symbol}} ausgestellt werden",
"sponsoring_amount_per_distance": "mit einem Betrag von _____{{currency_symbol}} pro gelaufenem Kilometer zu unterstützen.",
"sponsoring_subtitle": "Ich/Wir sind bereit anlässlich des {{eventname}}",
"sponsoring_title": "Sponsoringerklärung",
"street": "Straße"
}
"address": "Adresse",
"city": "Stadt",
"date": "Datum",
"firstname": "Vorname",
"group": "Team/Klasse",
"house_number": "Hausnummer",
"id": "ID",
"lastname": "Nachname",
"location": "Ort",
"please_use_blockletters": "Bitte in DRUCKBUCHSTABEN schreiben",
"postalcode": "Postleitzahl",
"signature": "Unterschrift",
"sponsor": "Sponsor",
"sponsoring_address_condition": "Muss ausgefüllt werden, wenn Sie eine Spendenquittung benötigen - Spendenquittungen können erst ab einem Gesamtbetrag von {{sponsoring_receipt_minimum_amount}}{{currency_symbol}} ausgestellt werden",
"sponsoring_amount_per_distance": "mit einem Betrag von _____{{currency_symbol}} pro gelaufenem Kilometer zu unterstützen.",
"sponsoring_subtitle": "Ich/Wir sind bereit anlässlich des {{eventname}}",
"sponsoring_title": "Sponsoringerklärung",
"street": "Straße",
"urkunde": "Urkunde",
"hat-beim-eventname": "Hat beim {{../eventname}}",
"fuer-den-guten-zweck-zurueckgelegt": "für den guten Zweck zurückgelegt",
"sponsorings": "Sponsorings",
"sponsor-in": "Sponsor:in",
"betrag-km": "Betrag/KM",
"gesamtbetrag": "Gesamtbetrag",
"gesamt": "Gesamt"
}
+27 -18
View File
@@ -1,19 +1,28 @@
{
"address": "Address",
"city": "City",
"date": "date",
"firstname": "First name",
"group": "Team/class",
"house_number": "House number",
"lastname": "Last name",
"location": "Location",
"please_use_blockletters": "Please write in BLOCK LETTERS.",
"postalcode": "Postal code",
"signature": "Signature",
"sponsor": "sponsor",
"sponsoring_address_condition": "You have to provide an address if you want a donation receipt - Donation receipts can't be issued for total donation amounts under {{sponsoring_receipt_minimum_amount}}{{currency_symbol}}",
"sponsoring_amount_per_distance": "with the amount of _____{{currency_symbol}} per kilometer run.",
"sponsoring_subtitle": "On the ocation of the {{eventname}} I/We want to support",
"sponsoring_title": "Sponsoring contract",
"street": "Street"
}
"address": "Address",
"city": "City",
"date": "date",
"firstname": "First name",
"group": "Team/class",
"house_number": "House number",
"lastname": "Last name",
"location": "Location",
"please_use_blockletters": "Please write in BLOCK LETTERS.",
"postalcode": "Postal code",
"signature": "Signature",
"sponsor": "sponsor",
"sponsoring_address_condition": "You have to provide an address if you want a donation receipt - Donation receipts can't be issued for total donation amounts under {{sponsoring_receipt_minimum_amount}}{{currency_symbol}}",
"sponsoring_amount_per_distance": "with the amount of _____{{currency_symbol}} per kilometer run.",
"sponsoring_subtitle": "On the ocation of the {{eventname}} I/We want to support",
"sponsoring_title": "Sponsoring contract",
"street": "Street",
"urkunde": "Certifcate",
"fuer-den-guten-zweck-zurueckgelegt": "for our good cuse at the {eventname}",
"hat-beim-eventname": "Ran",
"id": "ID",
"sponsorings": "Donations",
"sponsor-in": "Donor",
"betrag-km": "Amount/KM",
"gesamtbetrag": "Total",
"gesamt": "Combined"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+98
View File
@@ -0,0 +1,98 @@
<html>
<head>
<meta charset="utf8">
<title>Sponsoring contract</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
<style>
.sheet {
margin: 0;
overflow: hidden;
position: relative;
box-sizing: border-box;
page-break-after: always;
padding: 1.2cm 2cm 1.2cm 2cm;
background-image: url("data:image/png;base64,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 truncated
background-repeat: no-repeat;
background-size: 11cm;
background-position: 5cm 5cm;
background-color: rgba(255, 255, 255, 0.7);
background-blend-mode: lighten;
/* border-style: solid; */
}
body.A4 .sheet {
width: 210mm;
height: 296mm
}
.certificate-footer {
position: absolute;
bottom: 0cm;
}
td {
border: 1px solid black;
font-size: large;
font-weight: 600;
}
</style>
</head>
<body class="A4 landscape">
{{#each runners}}
<article class="sheet">
<header class="content has-text-centered">
<h1 style="font-size: 3cm; font-weight: bold;">{{__ "urkunde"}}</h1>
</header>
<main class="content has-text-centered" style="padding-top: 3cm;">
<p style="width: 50%; font-size: 4vw; font-weight: bold; margin-bottom: 0; display: inline;">{{this.firstname}}
{{this.middlename}} {{this.lastname}}
</p>
<p style="font-size: 1cm; margin-bottom: 0;">{{__ "hat-beim-eventname"}}</p>
<p style="font-size: 2cm; font-weight: bold; margin-bottom: 0;">{{this.distance}}km</p>
<p style="font-size: 1cm;">{{__ "fuer-den-guten-zweck-zurueckgelegt"}}</p>
</main>
<footer class="certificate-footer">
<img src="./certificate_footer.png">
</footer>
</article>
<article class="sheet">
<header class="content has-text-centered">
<h1 style="font-size: 2.5cm;">{{__ "sponsorings"}}</h1>
</header>
<main>
<table style="border: solid; width: 17cm;">
<thead>
<td class=".td-head">{{__ "sponsor-in"}}</td>
<td>{{__ "betrag-km"}}</td>
<td>{{__ "gesamtbetrag"}}</td>
</thead>
<tbody>
{{#each this.distanceDonations}}
<tr>
<td>{{this.donor.firstname}} {{this.donor.middlename}} {{this.donor.lastname}}</td>
<td>{{this.amountPerDistance}}</td>
<td>{{this.amount}}]</td>
</tr>
{{/each}}
</tbody>
<tfoot>
<td>{{__ "gesamt"}}</td>
<td>0.5€</td>
<td>1€</td>
</tfoot>
</table>
</main>
<footer class="certificate-footer">
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
</footer>
</article>
{{/each}}
</body>
</html>