Merge pull request 'Alpha Release 0.2.0 - The barcode release' (#22) from dev into main
Reviewed-on: #22 Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
This commit was merged in pull request #22.
	This commit is contained in:
		
							
								
								
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,12 +4,28 @@ All notable changes to this project will be documented in this file. Dates are d
 | 
			
		||||
 | 
			
		||||
#### [v0.1.3](https://git.odit.services/lfk/document-server/compare/v0.1.2...v0.1.3)
 | 
			
		||||
 | 
			
		||||
- Merge pull request 'Barcode generation feature/13-barcode_generation' (#21) from feature/13-barcode_generation into dev [`ff36b48`](https://git.odit.services/lfk/document-server/commit/ff36b4871f2d696c0b86883d529365ee8f1c6132)
 | 
			
		||||
- 🚀Bumped version to v0.1.3 [`6a14232`](https://git.odit.services/lfk/document-server/commit/6a142328898d5b89fa11eaf033372971d1093b0c)
 | 
			
		||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`ad9a8a4`](https://git.odit.services/lfk/document-server/commit/ad9a8a4fe0649d48db924771be8ecb4cbf5c162a)
 | 
			
		||||
- Implemented async barcode generation using async helpers [`edc846a`](https://git.odit.services/lfk/document-server/commit/edc846ab05319a4e60422625678f204bc145884c)
 | 
			
		||||
- Reworked template layout for barcode [`1c06689`](https://git.odit.services/lfk/document-server/commit/1c066898009883f510fa204c66800e5f6228a15d)
 | 
			
		||||
- Added basic barcode generation [`8072d0b`](https://git.odit.services/lfk/document-server/commit/8072d0b1940ef6f316ce78dcbcb9e5af5bab04e7)
 | 
			
		||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`03f63e3`](https://git.odit.services/lfk/document-server/commit/03f63e3777381a4475910e6fa4a3986f87b73f39)
 | 
			
		||||
- Merge pull request 'Alpha Release 0.1.3 - More env vars' (#20) from dev into main [`ecd02a1`](https://git.odit.services/lfk/document-server/commit/ecd02a1af7431d0bf615c4ec064f64e023946e49)
 | 
			
		||||
- Now with working code scaleing [`4b79b29`](https://git.odit.services/lfk/document-server/commit/4b79b29ee6319559c9d68ddb11f831d25f12b3da)
 | 
			
		||||
- Now loading barcode format from env with overwrite via query param [`9a7c1d6`](https://git.odit.services/lfk/document-server/commit/9a7c1d64fdbdadbd104739133a87773e4d2bca01)
 | 
			
		||||
- Added fallback error image [`5023457`](https://git.odit.services/lfk/document-server/commit/502345782f26895ccf3089d15c3817709b62dfcc)
 | 
			
		||||
- First part of the handlebars barcode generation [`a35f8cf`](https://git.odit.services/lfk/document-server/commit/a35f8cfd3aa94923968fd77425c074844d28ec0d)
 | 
			
		||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`b6296b8`](https://git.odit.services/lfk/document-server/commit/b6296b8d97cda943dfb5e11bc9dfbb2f363f5b81)
 | 
			
		||||
- Merge pull request 'Load more stuff from env feature/16-env_vars' (#17) from feature/16-env_vars into dev [`bc4d16e`](https://git.odit.services/lfk/document-server/commit/bc4d16e6f8959ed35d7e87647de84584cdfddd7b)
 | 
			
		||||
- Added todo [`75d2ac3`](https://git.odit.services/lfk/document-server/commit/75d2ac3c5f80f8440b6d48c33b15ef17565559b3)
 | 
			
		||||
- Removed promise [`e1ec193`](https://git.odit.services/lfk/document-server/commit/e1ec193a4ff1cd618da90f5f2d029ec848a6f669)
 | 
			
		||||
- Added new env vars to config [`3bb322e`](https://git.odit.services/lfk/document-server/commit/3bb322ede5db15a147c0d7a8db2a68ccb7fa2112)
 | 
			
		||||
- Added new env vars to readme [`b77bb3a`](https://git.odit.services/lfk/document-server/commit/b77bb3ad9dba9d73c2c81215ba57936192155a9a)
 | 
			
		||||
- Now loading interpolation vars from config/env [`b4ebae2`](https://git.odit.services/lfk/document-server/commit/b4ebae283b472b2f0c6e28caed49b30edb119585)
 | 
			
		||||
- Fixed broken mime-type [`4187a8e`](https://git.odit.services/lfk/document-server/commit/4187a8e82015495c0e0362e957e236ed6935a908)
 | 
			
		||||
- Merge pull request 'Load more stuff from env feature/16-env_vars' (#17) from feature/16-env_vars into dev [`bc4d16e`](https://git.odit.services/lfk/document-server/commit/bc4d16e6f8959ed35d7e87647de84584cdfddd7b)
 | 
			
		||||
- Added new env vars to readme [`b77bb3a`](https://git.odit.services/lfk/document-server/commit/b77bb3ad9dba9d73c2c81215ba57936192155a9a)
 | 
			
		||||
- Switched to using the current runner's id as the barcode text [`3e2b011`](https://git.odit.services/lfk/document-server/commit/3e2b011d2887d261fb9c36820982095d6dd6d847)
 | 
			
		||||
- Added barcode field to template [`27d1d69`](https://git.odit.services/lfk/document-server/commit/27d1d69360c8513079abcfe3a6fc2a50309a2b61)
 | 
			
		||||
- 🧾New changelog file version [CI SKIP] [skip ci] [`a306009`](https://git.odit.services/lfk/document-server/commit/a30600943d01116b99e946cb705a16d0372b5095)
 | 
			
		||||
 | 
			
		||||
#### [v0.1.2](https://git.odit.services/lfk/document-server/compare/v0.1.1...v0.1.2)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								licenses.md
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								licenses.md
									
									
									
									
									
								
							@@ -27,6 +27,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
SOFTWARE.
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
# async-helpers
 | 
			
		||||
**Author**: Brian Woodward (https://github.com/doowb)
 | 
			
		||||
**Repo**: doowb/async-helpers
 | 
			
		||||
**License**: MIT
 | 
			
		||||
**Description**: Use async helpers in templates with engines that typically only handle sync helpers. Handlebars and Lodash have been tested.
 | 
			
		||||
## License Text
 | 
			
		||||
The MIT License (MIT)
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2015-2017, Brian Woodward.
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in
 | 
			
		||||
all copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
			
		||||
THE SOFTWARE.
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
# axios
 | 
			
		||||
**Author**: Matt Zabriskie
 | 
			
		||||
**Repo**: [object Object]
 | 
			
		||||
@@ -54,6 +83,38 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
			
		||||
THE SOFTWARE.
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
# bwip-js
 | 
			
		||||
**Author**: Mark Warren <mwarren@metafloor.com>
 | 
			
		||||
**Repo**: [object Object]
 | 
			
		||||
**License**: MIT
 | 
			
		||||
**Description**: JavaScript barcode generator supporting over 100 types and standards.
 | 
			
		||||
## License Text
 | 
			
		||||
bwip-js : Barcode Writer in Pure JavaScript
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2011-2019 Mark Warren
 | 
			
		||||
 | 
			
		||||
The MIT License
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in
 | 
			
		||||
all copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
			
		||||
THE SOFTWARE.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
# cheerio
 | 
			
		||||
**Author**: Matt Mueller <mattmuelle@gmail.com> (mat.io)
 | 
			
		||||
**Repo**: [object Object]
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,9 @@
 | 
			
		||||
  "license": "CC-BY-NC-SA-4.0",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@odit/class-validator-jsonschema": "^2.1.1",
 | 
			
		||||
    "async-helpers": "^0.3.17",
 | 
			
		||||
    "axios": "^0.21.1",
 | 
			
		||||
    "bwip-js": "^2.0.12",
 | 
			
		||||
    "cheerio": "^1.0.0-rc.5",
 | 
			
		||||
    "class-transformer": "0.3.1",
 | 
			
		||||
    "class-validator": "^0.13.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,199 +1,206 @@
 | 
			
		||||
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 { config } from './config';
 | 
			
		||||
import { Runner } from './models/Runner';
 | 
			
		||||
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 }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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('__',
 | 
			
		||||
            function (str) {
 | 
			
		||||
                return i18next.t(str, PdfCreator.interpolations).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"): 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);
 | 
			
		||||
        }
 | 
			
		||||
        await i18next.changeLanguage(locale);
 | 
			
		||||
        const template_source = fs.readFileSync(`${this.templateDir}/sponsoring_contract.html`, 'utf8');
 | 
			
		||||
        const template = Handlebars.compile(template_source);
 | 
			
		||||
        const result = template({ runners })
 | 
			
		||||
        const pdf = await this.renderPdf(result, { format: "A5", landscape: 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");
 | 
			
		||||
            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;
 | 
			
		||||
    }
 | 
			
		||||
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 { Runner } from './models/Runner';
 | 
			
		||||
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 }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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();
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
        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);
 | 
			
		||||
        }
 | 
			
		||||
        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 });
 | 
			
		||||
        result = await awaitAsyncHandlebarHelpers(result);
 | 
			
		||||
        const pdf = await this.renderPdf(result, { format: "A5", landscape: 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								src/asyncHelpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/asyncHelpers.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,19 +1,20 @@
 | 
			
		||||
import { config as configDotenv } from 'dotenv';
 | 
			
		||||
 | 
			
		||||
configDotenv();
 | 
			
		||||
export const config = {
 | 
			
		||||
    internal_port: parseInt(process.env.APP_PORT) || 4010,
 | 
			
		||||
    development: process.env.NODE_ENV === "production",
 | 
			
		||||
    version: process.env.VERSION || require('../package.json').version,
 | 
			
		||||
    eventname: process.env.EVENT_NAME || "Please set the event name",
 | 
			
		||||
    currency_symbol: process.env.CURRENCY_SYMBOL || "€",
 | 
			
		||||
    sponsoring_receipt_minimum_amount: process.env.SPONSORING_RECEIPT_MINIMUM_AMOUNT || "10"
 | 
			
		||||
}
 | 
			
		||||
let errors = 0
 | 
			
		||||
if (typeof config.internal_port !== "number") {
 | 
			
		||||
    errors++
 | 
			
		||||
}
 | 
			
		||||
if (typeof config.development !== "boolean") {
 | 
			
		||||
    errors++
 | 
			
		||||
}
 | 
			
		||||
import { config as configDotenv } from 'dotenv';
 | 
			
		||||
 | 
			
		||||
configDotenv();
 | 
			
		||||
export const config = {
 | 
			
		||||
    internal_port: parseInt(process.env.APP_PORT) || 4010,
 | 
			
		||||
    development: process.env.NODE_ENV === "production",
 | 
			
		||||
    version: process.env.VERSION || require('../package.json').version,
 | 
			
		||||
    eventname: process.env.EVENT_NAME || "Please set the event name",
 | 
			
		||||
    currency_symbol: process.env.CURRENCY_SYMBOL || "€",
 | 
			
		||||
    sponsoring_receipt_minimum_amount: process.env.SPONSORING_RECEIPT_MINIMUM_AMOUNT || "10",
 | 
			
		||||
    codeformat: process.env.CODEFORMAT || "qrcode"
 | 
			
		||||
}
 | 
			
		||||
let errors = 0
 | 
			
		||||
if (typeof config.internal_port !== "number") {
 | 
			
		||||
    errors++
 | 
			
		||||
}
 | 
			
		||||
if (typeof config.development !== "boolean") {
 | 
			
		||||
    errors++
 | 
			
		||||
}
 | 
			
		||||
export let e = errors
 | 
			
		||||
@@ -15,7 +15,7 @@ export class PdfController {
 | 
			
		||||
 | 
			
		||||
    @Post('/contracts')
 | 
			
		||||
    @OpenAPI({ description: "Generate Sponsoring contract pdfs from 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 generateContracts(@Body({ validate: true, options: { limit: "500mb" } }) runners: Runner | Runner[], @Res() res: any, @QueryParam("locale") locale: string) {
 | 
			
		||||
    async generateContracts(@Body({ validate: true, options: { limit: "500mb" } }) runners: Runner | Runner[], @Res() res: any, @QueryParam("locale") locale: string, @QueryParam("codeformat") codeformat: string) {
 | 
			
		||||
        if (!this.initialized) {
 | 
			
		||||
            await this.pdf.init();
 | 
			
		||||
            this.initialized = true;
 | 
			
		||||
@@ -23,7 +23,7 @@ export class PdfController {
 | 
			
		||||
        if (!Array.isArray(runners)) {
 | 
			
		||||
            runners = [runners];
 | 
			
		||||
        }
 | 
			
		||||
        const contracts = await this.pdf.generateSponsoringContract(runners, locale);
 | 
			
		||||
        const contracts = await this.pdf.generateSponsoringContract(runners, locale, codeformat);
 | 
			
		||||
        res.setHeader('content-type', 'application/pdf');
 | 
			
		||||
        return contracts;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -29,31 +29,36 @@
 | 
			
		||||
  <div class="sheet">
 | 
			
		||||
    <img id="header_img" width="100%" src="sponsoringheader.png" />
 | 
			
		||||
    <div style=" padding: 0 1rem 0 1rem;">
 | 
			
		||||
      <div class="columns" style="padding-bottom: 0;">
 | 
			
		||||
        <div class="column is-two-fifths">
 | 
			
		||||
          <p style="font-size: large; font-weight: bold;">{{__ "sponsoring_title"}}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column">
 | 
			
		||||
          <p style="font-size: x-small; vertical-align: revert; margin-top: auto;">{{__ "please_use_blockletters"}}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p> {{__ "sponsoring_subtitle"}} </p>
 | 
			
		||||
      <div class="columns" style="padding-top: 0;">
 | 
			
		||||
        <div class="column is-8">
 | 
			
		||||
          <span style="border-bottom: 1px solid; width: 100%; display: block;">{{this.firstname}}
 | 
			
		||||
            {{this.middlename}}</span>
 | 
			
		||||
          <p style="font-size: x-small; display: block;">{{__ "firstname"}}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column is-2">
 | 
			
		||||
          <span style="border-bottom: 1px solid; width: 100%; display: block;">{{this.id}}</span>
 | 
			
		||||
          <p style="font-size: x-small; display: block;">ID</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column">
 | 
			
		||||
          <!-- CODE Here -->
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="columns">
 | 
			
		||||
        <div class="column is-10">
 | 
			
		||||
          <div class="columns" style="padding-bottom: 0;">
 | 
			
		||||
            <div class="column is-two-fifths">
 | 
			
		||||
              <p style="font-size: large; font-weight: bold;">{{__ "sponsoring_title"}}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="column">
 | 
			
		||||
              <p style="font-size: x-small; vertical-align: revert; margin-top: auto;">{{__ "please_use_blockletters"}}
 | 
			
		||||
              </p>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <p> {{__ "sponsoring_subtitle"}} </p>
 | 
			
		||||
          <div class="columns">
 | 
			
		||||
            <div class="column is-9">
 | 
			
		||||
              <span style="border-bottom: 1px solid; width: 100%; display: block;">{{this.firstname}}
 | 
			
		||||
                {{this.middlename}}</span>
 | 
			
		||||
              <p style="font-size: x-small; display: block;">{{__ "firstname"}}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="column is-3">
 | 
			
		||||
              <span style="border-bottom: 1px solid; width: 100%; display: block;">{{this.id}}</span>
 | 
			
		||||
              <p style="font-size: x-small; display: block;">ID</p>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="column">
 | 
			
		||||
          <img style="vertical-align: revert; margin-top: auto; object-fit: cover; max-height: 2cm;"
 | 
			
		||||
            src="{{--bc this.id ../codeformat}}" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="columns" style="padding-top: 1rem;">
 | 
			
		||||
        <div class="column is-6">
 | 
			
		||||
          <span style="border-bottom: 1px solid; width: 100%; display: block;">{{this.lastname}}</span>
 | 
			
		||||
          <p style="font-size: x-small; display: block;">{{__ "lastname"}}</p>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user