Fixed Locale comma format
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Nicolai Ort 2021-07-19 17:22:03 +02:00
parent 4b4d66ae78
commit 2a3322612d
Signed by: niggl
GPG Key ID: 13AFA55AF62F269F

View File

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