Compare commits

...

22 Commits
v0.5.1 ... main

Author SHA1 Message Date
Nicolai Ort 57a84c256a Merge pull request 'Fixed decimal separator' (#47) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: #47
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-07-19 16:15:35 +00:00
Nicolai Ort b78534e1b0 🧾New changelog file version [CI SKIP] [skip ci] 2021-07-19 16:14:59 +00:00
Nicolai Ort 9b85b54da5 Merge branch 'dev' of git.odit.services:lfk/document-server into dev
continuous-integration/drone/push Build is passing Details
2021-07-19 18:14:03 +02:00
Nicolai Ort 4b5a86282d 🚀Bumped version to v0.5.4 2021-07-19 18:13:53 +02:00
Nicolai Ort c9cb03ea95
Fixed decimal separator in docker 2021-07-19 18:13:37 +02:00
Philipp Dormann c7f57548f3 🧾New changelog file version [CI SKIP] [skip ci] 2021-07-19 15:53:52 +00:00
Philipp Dormann 8d00307170 Merge pull request 'Hotfixes' (#46) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: #46
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-07-19 15:53:40 +00:00
Nicolai Ort 5e92b9a48f 🧾New changelog file version [CI SKIP] [skip ci] 2021-07-19 15:50:28 +00:00
Nicolai Ort 01e1323555 🚀Bumped version to v0.5.3
continuous-integration/drone/push Build is passing Details
2021-07-19 17:49:35 +02:00
Nicolai Ort f8465721cd 🧾New changelog file version [CI SKIP] [skip ci] 2021-07-19 15:41:59 +00:00
Nicolai Ort 4cea7cb32f Merge branch 'dev' of git.odit.services:lfk/document-server into dev
continuous-integration/drone/push Build is passing Details
2021-07-19 17:41:13 +02:00
Nicolai Ort 72303b1105
Fix for runner donation array 2021-07-19 17:39:52 +02:00
Nicolai Ort 451b7fbe05 🧾New changelog file version [CI SKIP] [skip ci] 2021-07-19 15:23:19 +00:00
Nicolai Ort 2a3322612d
Fixed Locale comma format
continuous-integration/drone/push Build is passing Details
2021-07-19 17:22:03 +02:00
Philipp Dormann 4b4d66ae78 🧾New changelog file version [CI SKIP] [skip ci] 2021-07-04 11:42:27 +00:00
Philipp Dormann c935950eb0 Merge pull request 'v0.5.2: hotfix TypeError in Runner Certificate generation' (#45) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: #45
2021-07-04 11:41:55 +00:00
Philipp Dormann 573b921197 🧾New changelog file version [CI SKIP] [skip ci] 2021-07-04 11:40:08 +00:00
Philipp Dormann 274c13e358
🚀Bumped version to v0.5.2
continuous-integration/drone/push Build is passing Details
2021-07-04 13:39:06 +02:00
Philipp Dormann ff0421da2f 🧾New changelog file version [CI SKIP] [skip ci] 2021-07-04 11:34:41 +00:00
Philipp Dormann 915baa6efa Merge branch 'bugfix/44-runner-certificates-result-in-a-status-500' into dev
continuous-integration/drone/push Build is passing Details
close #44
2021-07-04 13:33:50 +02:00
Philipp Dormann bac004d74e wrap distanceDonations.reduce in array length check
ref #44
2021-07-04 13:32:53 +02:00
Nicolai Ort b7b7f6a0ae 🧾New changelog file version [CI SKIP] [skip ci] 2021-04-22 16:16:50 +00:00
4 changed files with 348 additions and 314 deletions

View File

@ -2,8 +2,32 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v0.5.4](https://git.odit.services/lfk/document-server/compare/v0.5.1...v0.5.4)
- Merge branch 'bugfix/44-runner-certificates-result-in-a-status-500' into dev [`#44`](https://git.odit.services/lfk/document-server/issues/44)
- Fixed Locale comma format [`2a33226`](https://git.odit.services/lfk/document-server/commit/2a3322612d473bd9002cf8d6f9807f9dc7d687da)
- wrap distanceDonations.reduce in array length check [`bac004d`](https://git.odit.services/lfk/document-server/commit/bac004d74eb954d1753d4efcdb927822b89fa757)
- 🧾New changelog file version [CI SKIP] [skip ci] [`ff0421d`](https://git.odit.services/lfk/document-server/commit/ff0421da2f16a8f79f9987dabea7bdcb4ef88c05)
- 🧾New changelog file version [CI SKIP] [skip ci] [`5e92b9a`](https://git.odit.services/lfk/document-server/commit/5e92b9a48fcbb8ab6a179c53f180a7a6bd743ae7)
- Fixed decimal separator in docker [`c9cb03e`](https://git.odit.services/lfk/document-server/commit/c9cb03ea95acccd6cdc8b86c747c787938840d07)
- 🧾New changelog file version [CI SKIP] [skip ci] [`f846572`](https://git.odit.services/lfk/document-server/commit/f8465721cddfb55d51eb30d29d74ef63d825b5ac)
- Fix for runner donation array [`72303b1`](https://git.odit.services/lfk/document-server/commit/72303b11052276ad15373887f9e04183841f56f4)
- 🧾New changelog file version [CI SKIP] [skip ci] [`451b7fb`](https://git.odit.services/lfk/document-server/commit/451b7fbe0543991e8a203e38daa350a954ae0e11)
- 🧾New changelog file version [CI SKIP] [skip ci] [`573b921`](https://git.odit.services/lfk/document-server/commit/573b9211972a55df0a38742cb6eb789d6fd3717b)
- 🚀Bumped version to v0.5.4 [`4b5a862`](https://git.odit.services/lfk/document-server/commit/4b5a86282d0618a0c5b00fc796efe77db2103356)
- 🧾New changelog file version [CI SKIP] [skip ci] [`c7f5754`](https://git.odit.services/lfk/document-server/commit/c7f57548f316e1bb6635bd56bd269d80ac1e220f)
- Merge pull request 'Hotfixes' (#46) from dev into main [`8d00307`](https://git.odit.services/lfk/document-server/commit/8d003071704b5a6d7b0d70aff2f9cb05c3660b78)
- 🚀Bumped version to v0.5.3 [`01e1323`](https://git.odit.services/lfk/document-server/commit/01e1323555fe67f6f0ce3c18163e475035bd1cdd)
- 🧾New changelog file version [CI SKIP] [skip ci] [`4b4d66a`](https://git.odit.services/lfk/document-server/commit/4b4d66ae784150f7e1cc491a3fc5d84c93273aee)
- Merge pull request 'v0.5.2: hotfix TypeError in Runner Certificate generation' (#45) from dev into main [`c935950`](https://git.odit.services/lfk/document-server/commit/c935950eb052bce71185fc74c750ec77f081e7df)
- 🚀Bumped version to v0.5.2 [`274c13e`](https://git.odit.services/lfk/document-server/commit/274c13e358f16207fe8bb5cdc1b9ede0582ecb46)
- 🧾New changelog file version [CI SKIP] [skip ci] [`b7b7f6a`](https://git.odit.services/lfk/document-server/commit/b7b7f6a0ae304d24f90a3de3931f53cf08770060)
#### [v0.5.1](https://git.odit.services/lfk/document-server/compare/v0.5.0...v0.5.1)
> 22 April 2021
- Merge pull request 'Release 0.5.1' (#43) from dev into main [`11efdeb`](https://git.odit.services/lfk/document-server/commit/11efdebacf076ecfe0e10cdcda37ac07464901ce)
- Quick callstack fix🛠 [`76418f6`](https://git.odit.services/lfk/document-server/commit/76418f65e1e111e83838f0d42c541ae6a8063a09)
- Fixed barcode generation for runenrcard pdfs🐞 [`f78037c`](https://git.odit.services/lfk/document-server/commit/f78037c0f15162d5b98986edf20d263961f43e69)
- Updated docker-compose example🐳 [`a4c8dad`](https://git.odit.services/lfk/document-server/commit/a4c8dade23e448d4d4caefe304a6cd9195c873a4)
@ -12,6 +36,7 @@ All notable changes to this project will be documented in this file. Dates are d
- 🧾New changelog file version [CI SKIP] [skip ci] [`2ee4c06`](https://git.odit.services/lfk/document-server/commit/2ee4c060557a44db1974a015412288f7942ebe72)
- more typo fixes [`981bae4`](https://git.odit.services/lfk/document-server/commit/981bae4786a2fa12a1355122e8c5a1e95e29cf32)
- You can now configure the card's code format distinct from the others [`ac9be79`](https://git.odit.services/lfk/document-server/commit/ac9be793bd598771174f5313ef8288240306ba5c)
- 🧾New changelog file version [CI SKIP] [skip ci] [`0f2d6f5`](https://git.odit.services/lfk/document-server/commit/0f2d6f58d6a8a8888263778cf1a14c73b28e774e)
- 🚀Bumped version to v0.5.1 [`22fb3ed`](https://git.odit.services/lfk/document-server/commit/22fb3edd7836ba4ca35e6b208ab6f6620da60f4a)
- Emoji+Chinese fixes🌍 [`b6fc069`](https://git.odit.services/lfk/document-server/commit/b6fc069042dc9c5d7ec97f2660568e8e105780b9)
- 🧾New changelog file version [CI SKIP] [skip ci] [`cc4a2b4`](https://git.odit.services/lfk/document-server/commit/cc4a2b4ab4c2cb9976797f93e8348607fb88ea7d)

View File

@ -1,6 +1,6 @@
{
"name": "@odit/lfk-document-server",
"version": "0.5.1",
"version": "0.5.4",
"description": "The document generation server for the LfK! runner system. This generates certificates, sponsoring aggreements and more",
"main": "src/app.ts",
"scripts": {

View File

@ -1,308 +1,308 @@
import axios from 'axios';
import cheerio from "cheerio";
import fs from "fs";
import Handlebars from 'handlebars';
import i18next from "i18next";
import Backend from 'i18next-fs-backend';
import mime from "mime-types";
import path from 'path';
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';
/**
* This class is responsible for all things pdf creation.
* This uses the html templates from src/templates.
*/
export class PdfCreator {
private templateDir = path.join(__dirname, '/templates');
private browser;
private static interpolations = { eventname: config.eventname, sponsoring_receipt_minimum_amount: config.sponsoring_receipt_minimum_amount, currency_symbol: config.currency_symbol }
private static contractsPerRunner = config.contracts_per_runner;
/**
* Main constructor.
* Initializes i18n(ext), Handlebars and puppeteer.
*/
constructor() {
this.init();
}
/**
* Main constructor.
* Initializes i18n(ext), Handlebars and puppeteer.
*/
public async init() {
const minimal_args = [
'--autoplay-policy=user-gesture-required',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-update',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-domain-reliability',
'--disable-extensions',
'--disable-features=AudioServiceOutOfProcess',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-notifications',
'--disable-offer-store-unmasked-wallet-cards',
'--disable-popup-blocking',
'--disable-print-preview',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-speech-api',
'--disable-sync',
'--hide-scrollbars',
'--ignore-gpu-blacklist',
'--metrics-recording-only',
'--mute-audio',
'--no-default-browser-check',
'--no-first-run',
'--no-pings',
'--no-zygote',
'--password-store=basic',
'--use-gl=swiftshader',
'--no-sandbox'
];
await i18next
.use(Backend)
.init({
fallbackLng: 'en',
lng: 'en',
backend: {
loadPath: path.join(__dirname, '/locales/{{lng}}.json')
}
});
await Handlebars.registerHelper(helpers);
await Handlebars.registerHelper('__',
function (str) {
return i18next.t(str, PdfCreator.interpolations).toString();
}
);
await Handlebars.registerHelper('--sponsor',
function (str) {
const index = (parseInt(str) % config.sponor_logos.length);
if (isNaN(index)) {
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg=="
}
return config.sponor_logos[index];
}
);
await Handlebars.registerHelper('--format_kilometers',
function (str) {
let meters = parseInt(str);
return ((meters / 1000).toFixed(3).toString())
}
);
await Handlebars.registerHelper('--format_currency',
function (str) {
let meters = parseInt(str);
return ((meters / 100).toFixed(2).toString())
}
);
this.browser = await puppeteer.launch({ headless: true, args: minimal_args });
}
/**
* 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 generateSponsoringContract(runners: Runner[], locale: string = "en", codeformat: string = config.codeformat): Promise<Buffer> {
if (runners.length == 1 && Object.keys(runners[0]).length == 0) {
runners[0] = this.generateEmptyRunner();
}
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.generateSponsoringContract(chunk, locale));
}
const pdfs = await Promise.all(pdf_promises);
return await this.mergePdfs(pdfs);
}
for (var i = 1; i < PdfCreator.contractsPerRunner; i++) {
runners = runners.reduce(function (res, current, index, array) {
return res.concat([current, current]);
}, []);
}
await i18next.changeLanguage(locale);
const template_source = fs.readFileSync(`${this.templateDir}/sponsoring_contract.html`, 'utf8');
const template = Handlebars.compile(template_source);
let result = template({ runners, codeformat, disclaimer: config.disclaimer_text });
result = await awaitAsyncHandlebarHelpers(result);
const pdf = await this.renderPdf(result, { format: "A5", landscape: true });
return pdf
}
/**
* Generate runner card pdfs.
* @param cards The runner cars you want to generate the cards for.
* @param locale The locale used for the cards (default:en)
*/
public async generateRunnerCards(cards: RunnerCard[], locale: string = "en", codeformat: string = config.codeformat_cards): Promise<Buffer> {
if (cards.length > 10) {
let pdf_promises = new Array<Promise<Buffer>>();
let i, j;
for (i = 0, j = cards.length; i < j; i += 10) {
let chunk = cards.slice(i, i + 10);
pdf_promises.push(this.generateRunnerCards(chunk, locale, codeformat));
}
const pdfs = await Promise.all(pdf_promises);
return await this.mergePdfs(pdfs);
}
const cards_swapped = this.swapArrayPairs(cards);
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: config.eventname, codeformat: codeformat, card_subtitle: config.card_subtitle })
result = await awaitAsyncHandlebarHelpers(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<Buffer>();
let i, j;
for (i = 0, j = runners.length; i < j; i += 50) {
let chunk = runners.slice(i, i + 50);
pdf_promises.push(await this.generateRunnerCertficates(chunk, locale));
}
return await this.mergePdfs(pdf_promises);
}
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, currency_symbol: config.currency_symbol, donations_footer_text: config.donations_footer_text });
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.
* @param html The html string whoms images shall get replaced.
*/
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")) {
return;
}
const img_type = mime.lookup(imgsrc);
if (!(img_type.includes("image"))) {
throw new Error("File is not image mime type");
}
let image;
if (imgsrc.startsWith("http")) {
image = (await axios.get(imgsrc)).data;
image = Buffer.from(image).toString('base64');
}
else {
if (imgsrc.startsWith("./")) {
imgsrc = imgsrc.replace("./", "");
}
image = fs.readFileSync(`${this.templateDir}/${imgsrc}`, { encoding: "base64" });
}
image = `data:${img_type};base64,${image}`
$(element).attr("src", image)
});
return $.html();
}
/**
* This method manages the creation of pdfs via puppeteer.
* @param html The HTML that should get rendered.
* @param options Puppeteer PDF option (eg: {format: "A4"})
*/
public async renderPdf(html: string, options): Promise<any> {
html = await this.imgToBase64(html);
let page = await this.browser.newPage();
await page.setContent(html);
const pdf = await page.pdf(options);
await page.close();
return pdf;
}
/**
* Merges multiple pdfs into one.
* @param pdfs The pdfs you want to merge as an buffer array.
* @returns The merged pdf as a buffer.
*/
private async mergePdfs(pdfs: Buffer[]): Promise<Buffer> {
const mergedPdf = await PDFDocument.create();
for (const pdfBuffer of pdfs) {
const pdf = await PDFDocument.load(pdfBuffer);
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach((page) => {
mergedPdf.addPage(page);
});
}
return <Buffer>(await mergedPdf.save());
}
/**
* Generates a new dummy runner with halfspaces for all strings.
* Can be used to generate empty sponsoring contracts.
* @returns A new runner object that apears to be empty.
*/
private generateEmptyRunner(): Runner {
let group = new RunnerGroup();
group.id = 0;
group.name = "";
let runner = new Runner();
runner.id = 0;
runner.firstname = "";
runner.lastname = "";
runner.group = group;
return runner;
}
/**
* Swaps pairs (0/1, 2/3, ...) of elements in an array recursively.
* If the last element has no partner it inserts an empty element at the end and swaps the two
* This is needed to generate pdfs with front- and backside that get printet on one paper.
* @param array The array which's pairs shall get switched.
* @returns Array with swapped pairs,
*/
private swapArrayPairs(array): Array<any> {
if (array.length == 1) {
return [null, array[0]];
}
if (array.length == 0) {
return null;
}
const rest = this.swapArrayPairs(array.slice(2))
if (!rest) {
return [array[1], array[0]]
}
return [array[1], array[0]].concat(rest);
}
import axios from 'axios';
import cheerio from "cheerio";
import fs from "fs";
import Handlebars from 'handlebars';
import i18next from "i18next";
import Backend from 'i18next-fs-backend';
import mime from "mime-types";
import path from 'path';
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';
/**
* This class is responsible for all things pdf creation.
* This uses the html templates from src/templates.
*/
export class PdfCreator {
private templateDir = path.join(__dirname, '/templates');
private browser;
private static interpolations = { eventname: config.eventname, sponsoring_receipt_minimum_amount: config.sponsoring_receipt_minimum_amount, currency_symbol: config.currency_symbol }
private static contractsPerRunner = config.contracts_per_runner;
/**
* Main constructor.
* Initializes i18n(ext), Handlebars and puppeteer.
*/
constructor() {
this.init();
}
/**
* Main constructor.
* Initializes i18n(ext), Handlebars and puppeteer.
*/
public async init() {
const minimal_args = [
'--autoplay-policy=user-gesture-required',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-update',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-domain-reliability',
'--disable-extensions',
'--disable-features=AudioServiceOutOfProcess',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-notifications',
'--disable-offer-store-unmasked-wallet-cards',
'--disable-popup-blocking',
'--disable-print-preview',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-speech-api',
'--disable-sync',
'--hide-scrollbars',
'--ignore-gpu-blacklist',
'--metrics-recording-only',
'--mute-audio',
'--no-default-browser-check',
'--no-first-run',
'--no-pings',
'--no-zygote',
'--password-store=basic',
'--use-gl=swiftshader',
'--no-sandbox'
];
await i18next
.use(Backend)
.init({
fallbackLng: 'en',
lng: 'en',
backend: {
loadPath: path.join(__dirname, '/locales/{{lng}}.json')
}
});
await Handlebars.registerHelper(helpers);
await Handlebars.registerHelper('__',
function (str) {
return i18next.t(str, PdfCreator.interpolations).toString();
}
);
await Handlebars.registerHelper('--sponsor',
function (str) {
const index = (parseInt(str) % config.sponor_logos.length);
if (isNaN(index)) {
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg=="
}
return config.sponor_logos[index];
}
);
await Handlebars.registerHelper('--format_kilometers',
function (str) {
let meters = parseInt(str);
return ((meters / 1000).toLocaleString("en-EN", { minimumFractionDigits: 1, maximumFractionDigits: 3 }).replace(".", ","));
}
);
await Handlebars.registerHelper('--format_currency',
function (str) {
let meters = parseInt(str);
return ((meters / 100).toLocaleString("en-EN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).replace(".", ","));
}
);
this.browser = await puppeteer.launch({ headless: true, args: minimal_args });
}
/**
* 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 generateSponsoringContract(runners: Runner[], locale: string = "en", codeformat: string = config.codeformat): Promise<Buffer> {
if (runners.length == 1 && Object.keys(runners[0]).length == 0) {
runners[0] = this.generateEmptyRunner();
}
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.generateSponsoringContract(chunk, locale));
}
const pdfs = await Promise.all(pdf_promises);
return await this.mergePdfs(pdfs);
}
for (var i = 1; i < PdfCreator.contractsPerRunner; i++) {
runners = runners.reduce(function (res, current, index, array) {
return res.concat([current, current]);
}, []);
}
await i18next.changeLanguage(locale);
const template_source = fs.readFileSync(`${this.templateDir}/sponsoring_contract.html`, 'utf8');
const template = Handlebars.compile(template_source);
let result = template({ runners, codeformat, disclaimer: config.disclaimer_text });
result = await awaitAsyncHandlebarHelpers(result);
const pdf = await this.renderPdf(result, { format: "A5", landscape: true });
return pdf
}
/**
* Generate runner card pdfs.
* @param cards The runner cars you want to generate the cards for.
* @param locale The locale used for the cards (default:en)
*/
public async generateRunnerCards(cards: RunnerCard[], locale: string = "en", codeformat: string = config.codeformat_cards): Promise<Buffer> {
if (cards.length > 10) {
let pdf_promises = new Array<Promise<Buffer>>();
let i, j;
for (i = 0, j = cards.length; i < j; i += 10) {
let chunk = cards.slice(i, i + 10);
pdf_promises.push(this.generateRunnerCards(chunk, locale, codeformat));
}
const pdfs = await Promise.all(pdf_promises);
return await this.mergePdfs(pdfs);
}
const cards_swapped = this.swapArrayPairs(cards);
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: config.eventname, codeformat: codeformat, card_subtitle: config.card_subtitle })
result = await awaitAsyncHandlebarHelpers(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<Buffer>();
let i, j;
for (i = 0, j = runners.length; i < j; i += 50) {
let chunk = runners.slice(i, i + 50);
pdf_promises.push(await this.generateRunnerCertficates(chunk, locale));
}
return await this.mergePdfs(pdf_promises);
}
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, currency_symbol: config.currency_symbol, donations_footer_text: config.donations_footer_text });
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.
* @param html The html string whoms images shall get replaced.
*/
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")) {
return;
}
const img_type = mime.lookup(imgsrc);
if (!(img_type.includes("image"))) {
throw new Error("File is not image mime type");
}
let image;
if (imgsrc.startsWith("http")) {
image = (await axios.get(imgsrc)).data;
image = Buffer.from(image).toString('base64');
}
else {
if (imgsrc.startsWith("./")) {
imgsrc = imgsrc.replace("./", "");
}
image = fs.readFileSync(`${this.templateDir}/${imgsrc}`, { encoding: "base64" });
}
image = `data:${img_type};base64,${image}`
$(element).attr("src", image)
});
return $.html();
}
/**
* This method manages the creation of pdfs via puppeteer.
* @param html The HTML that should get rendered.
* @param options Puppeteer PDF option (eg: {format: "A4"})
*/
public async renderPdf(html: string, options): Promise<any> {
html = await this.imgToBase64(html);
let page = await this.browser.newPage();
await page.setContent(html);
const pdf = await page.pdf(options);
await page.close();
return pdf;
}
/**
* Merges multiple pdfs into one.
* @param pdfs The pdfs you want to merge as an buffer array.
* @returns The merged pdf as a buffer.
*/
private async mergePdfs(pdfs: Buffer[]): Promise<Buffer> {
const mergedPdf = await PDFDocument.create();
for (const pdfBuffer of pdfs) {
const pdf = await PDFDocument.load(pdfBuffer);
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach((page) => {
mergedPdf.addPage(page);
});
}
return <Buffer>(await mergedPdf.save());
}
/**
* Generates a new dummy runner with halfspaces for all strings.
* Can be used to generate empty sponsoring contracts.
* @returns A new runner object that apears to be empty.
*/
private generateEmptyRunner(): Runner {
let group = new RunnerGroup();
group.id = 0;
group.name = "";
let runner = new Runner();
runner.id = 0;
runner.firstname = "";
runner.lastname = "";
runner.group = group;
return runner;
}
/**
* Swaps pairs (0/1, 2/3, ...) of elements in an array recursively.
* If the last element has no partner it inserts an empty element at the end and swaps the two
* This is needed to generate pdfs with front- and backside that get printet on one paper.
* @param array The array which's pairs shall get switched.
* @returns Array with swapped pairs,
*/
private swapArrayPairs(array): Array<any> {
if (array.length == 1) {
return [null, array[0]];
}
if (array.length == 0) {
return null;
}
const rest = this.swapArrayPairs(array.slice(2))
if (!rest) {
return [array[1], array[0]]
}
return [array[1], array[0]].concat(rest);
}
}

View File

@ -97,12 +97,21 @@ export class PdfController {
else {
runner.group.fullName = `${runner.group.parentGroup.name}/${runner.group.name}`;
}
runner.donationPerDistanceTotal = runner.distanceDonations.reduce(function (sum, current) {
return sum + current.amountPerDistance;
}, 0);
runner.donationTotal = runner.distanceDonations.reduce(function (sum, current) {
return sum + current.amount;
}, 0);
runner.donationPerDistanceTotal = 0;
if (!Array.isArray(runner.distanceDonations)){
runner.distanceDonations = [].concat(runner.distanceDonations)
}
if (runner.distanceDonations.length > 0) {
runner.donationPerDistanceTotal += runner.distanceDonations.reduce(function (sum, current) {
return sum + current.amountPerDistance;
}, 0);
}
runner.donationTotal = 0;
if (runner.distanceDonations.length > 0) {
runner.donationTotal += runner.distanceDonations.reduce(function (sum, current) {
return sum + current.amount;
}, 0);
}
response.push(runner)
}
return response;