Card generation feature/14-card_generation #24
@ -34,6 +34,7 @@ The basic generation mechanism makes the templates and routes interchangeable (i
|
|||||||
| EVENT_NAME | String | "Please set the event name" | The event's name - used to generate pdf text.
|
| EVENT_NAME | String | "Please set the event name" | The event's name - used to generate pdf text.
|
||||||
| CURRENCY_SYMBOL | String | "€" | The your currency's symbol - used to generate pdf text.
|
| CURRENCY_SYMBOL | String | "€" | The your currency's symbol - used to generate pdf text.
|
||||||
| SPONSORING_RECEIPT_MINIMUM_AMOUNT | String | "10" | The mimimum total donation amount a sponsor has to donate to be able to receive a donation receipt - used to generate pdf text.
|
| SPONSORING_RECEIPT_MINIMUM_AMOUNT | String | "10" | The mimimum total donation amount a sponsor has to donate to be able to receive a donation receipt - used to generate pdf text.
|
||||||
|
| SPONOR_LOGOS | Array<String> | Empty png | The sponsor images you want to loop through. You can provide them via http url, local file or base64-encoded image.
|
||||||
|
|
||||||
## Templates
|
## Templates
|
||||||
> The document server uses html templates to generate various pdf documents.
|
> The document server uses html templates to generate various pdf documents.
|
||||||
|
@ -1,206 +1,265 @@
|
|||||||
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 { Runner } from './models/Runner';
|
import { Runner } from './models/Runner';
|
||||||
import { RunnerGroup } from './models/RunnerGroup';
|
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.
|
* 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');
|
export class PdfCreator {
|
||||||
private browser;
|
private templateDir = path.join(__dirname, '/templates');
|
||||||
private static interpolations = { eventname: config.eventname, sponsoring_receipt_minimum_amount: config.sponsoring_receipt_minimum_amount, currency_symbol: config.currency_symbol }
|
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.
|
* Main constructor.
|
||||||
*/
|
* Initializes i18n(ext), Handlebars and puppeteer.
|
||||||
constructor() {
|
*/
|
||||||
this.init();
|
constructor() {
|
||||||
}
|
this.init();
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Main constructor.
|
/**
|
||||||
* Initializes i18n(ext), Handlebars and puppeteer.
|
* Main constructor.
|
||||||
*/
|
* Initializes i18n(ext), Handlebars and puppeteer.
|
||||||
public async init() {
|
*/
|
||||||
const minimal_args = [
|
public async init() {
|
||||||
'--autoplay-policy=user-gesture-required',
|
const minimal_args = [
|
||||||
'--disable-background-networking',
|
'--autoplay-policy=user-gesture-required',
|
||||||
'--disable-background-timer-throttling',
|
'--disable-background-networking',
|
||||||
'--disable-backgrounding-occluded-windows',
|
'--disable-background-timer-throttling',
|
||||||
'--disable-breakpad',
|
'--disable-backgrounding-occluded-windows',
|
||||||
'--disable-client-side-phishing-detection',
|
'--disable-breakpad',
|
||||||
'--disable-component-update',
|
'--disable-client-side-phishing-detection',
|
||||||
'--disable-default-apps',
|
'--disable-component-update',
|
||||||
'--disable-dev-shm-usage',
|
'--disable-default-apps',
|
||||||
'--disable-domain-reliability',
|
'--disable-dev-shm-usage',
|
||||||
'--disable-extensions',
|
'--disable-domain-reliability',
|
||||||
'--disable-features=AudioServiceOutOfProcess',
|
'--disable-extensions',
|
||||||
'--disable-hang-monitor',
|
'--disable-features=AudioServiceOutOfProcess',
|
||||||
'--disable-ipc-flooding-protection',
|
'--disable-hang-monitor',
|
||||||
'--disable-notifications',
|
'--disable-ipc-flooding-protection',
|
||||||
'--disable-offer-store-unmasked-wallet-cards',
|
'--disable-notifications',
|
||||||
'--disable-popup-blocking',
|
'--disable-offer-store-unmasked-wallet-cards',
|
||||||
'--disable-print-preview',
|
'--disable-popup-blocking',
|
||||||
'--disable-prompt-on-repost',
|
'--disable-print-preview',
|
||||||
'--disable-renderer-backgrounding',
|
'--disable-prompt-on-repost',
|
||||||
'--disable-speech-api',
|
'--disable-renderer-backgrounding',
|
||||||
'--disable-sync',
|
'--disable-speech-api',
|
||||||
'--hide-scrollbars',
|
'--disable-sync',
|
||||||
'--ignore-gpu-blacklist',
|
'--hide-scrollbars',
|
||||||
'--metrics-recording-only',
|
'--ignore-gpu-blacklist',
|
||||||
'--mute-audio',
|
'--metrics-recording-only',
|
||||||
'--no-default-browser-check',
|
'--mute-audio',
|
||||||
'--no-first-run',
|
'--no-default-browser-check',
|
||||||
'--no-pings',
|
'--no-first-run',
|
||||||
'--no-zygote',
|
'--no-pings',
|
||||||
'--password-store=basic',
|
'--no-zygote',
|
||||||
'--use-gl=swiftshader',
|
'--password-store=basic',
|
||||||
'--no-sandbox'
|
'--use-gl=swiftshader',
|
||||||
];
|
'--no-sandbox'
|
||||||
await i18next
|
];
|
||||||
.use(Backend)
|
await i18next
|
||||||
.init({
|
.use(Backend)
|
||||||
fallbackLng: 'en',
|
.init({
|
||||||
lng: 'en',
|
fallbackLng: 'en',
|
||||||
backend: {
|
lng: 'en',
|
||||||
loadPath: path.join(__dirname, '/locales/{{lng}}.json')
|
backend: {
|
||||||
}
|
loadPath: path.join(__dirname, '/locales/{{lng}}.json')
|
||||||
});
|
}
|
||||||
|
});
|
||||||
await Handlebars.registerHelper(helpers);
|
|
||||||
await Handlebars.registerHelper('__',
|
await Handlebars.registerHelper(helpers);
|
||||||
function (str) {
|
await Handlebars.registerHelper('__',
|
||||||
return i18next.t(str, PdfCreator.interpolations).toString();
|
function (str) {
|
||||||
}
|
return i18next.t(str, PdfCreator.interpolations).toString();
|
||||||
);
|
}
|
||||||
this.browser = await puppeteer.launch({ headless: true, args: minimal_args });
|
);
|
||||||
}
|
await Handlebars.registerHelper('--sponsor',
|
||||||
|
function (str) {
|
||||||
/**
|
const index = (parseInt(str) % config.sponor_logos.length);
|
||||||
* Generate sponsoring contract pdfs.
|
if (isNaN(index)) {
|
||||||
* @param runner The runner you want to generate the contracts for.
|
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg=="
|
||||||
* @param locale The locale used for the contracts (default:en)
|
}
|
||||||
*/
|
return config.sponor_logos[index];
|
||||||
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();
|
this.browser = await puppeteer.launch({ headless: true, args: minimal_args });
|
||||||
}
|
}
|
||||||
if (runners.length > 50) {
|
|
||||||
let pdf_promises = new Array<Promise<Buffer>>();
|
/**
|
||||||
let i, j;
|
* Generate sponsoring contract pdfs.
|
||||||
for (i = 0, j = runners.length; i < j; i += 50) {
|
* @param runner The runner you want to generate the contracts for.
|
||||||
let chunk = runners.slice(i, i + 50);
|
* @param locale The locale used for the contracts (default:en)
|
||||||
pdf_promises.push(this.generateSponsoringContract(chunk, locale));
|
*/
|
||||||
}
|
public async generateSponsoringContract(runners: Runner[], locale: string = "en", codeformat: string = config.codeformat): Promise<Buffer> {
|
||||||
const pdfs = await Promise.all(pdf_promises);
|
if (runners.length == 1 && Object.keys(runners[0]).length == 0) {
|
||||||
return await this.mergePdfs(pdfs);
|
runners[0] = this.generateEmptyRunner();
|
||||||
}
|
}
|
||||||
await i18next.changeLanguage(locale);
|
if (runners.length > 50) {
|
||||||
const template_source = fs.readFileSync(`${this.templateDir}/sponsoring_contract.html`, 'utf8');
|
let pdf_promises = new Array<Promise<Buffer>>();
|
||||||
const template = Handlebars.compile(template_source);
|
let i, j;
|
||||||
let result = template({ runners, codeformat });
|
for (i = 0, j = runners.length; i < j; i += 50) {
|
||||||
result = await awaitAsyncHandlebarHelpers(result);
|
let chunk = runners.slice(i, i + 50);
|
||||||
const pdf = await this.renderPdf(result, { format: "A5", landscape: true });
|
pdf_promises.push(this.generateSponsoringContract(chunk, locale));
|
||||||
return pdf
|
}
|
||||||
}
|
const pdfs = await Promise.all(pdf_promises);
|
||||||
|
return await this.mergePdfs(pdfs);
|
||||||
/**
|
}
|
||||||
* Converts all images in html to base64.
|
await i18next.changeLanguage(locale);
|
||||||
* Works with image files in the template directory or images from urls.
|
const template_source = fs.readFileSync(`${this.templateDir}/sponsoring_contract.html`, 'utf8');
|
||||||
* @param html The html string whoms images shall get replaced.
|
const template = Handlebars.compile(template_source);
|
||||||
*/
|
let result = template({ runners, codeformat });
|
||||||
public async imgToBase64(html): Promise<string> {
|
result = await awaitAsyncHandlebarHelpers(result);
|
||||||
const $ = cheerio.load(html)
|
const pdf = await this.renderPdf(result, { format: "A5", landscape: true });
|
||||||
$('img').each(async (index, element) => {
|
return pdf
|
||||||
let imgsrc = $(element).attr("src");
|
}
|
||||||
if (imgsrc.startsWith("data:image")) {
|
|
||||||
return;
|
/**
|
||||||
}
|
* Generate runner card pdfs.
|
||||||
const img_type = mime.lookup(imgsrc);
|
* @param cards The runner cars you want to generate the cards for.
|
||||||
|
* @param locale The locale used for the cards (default:en)
|
||||||
if (!(img_type.includes("image"))) {
|
*/
|
||||||
throw new Error("File is not image mime type");
|
public async generateRunnerCards(cards: RunnerCard[], locale: string = "en", codeformat: string = config.codeformat): Promise<Buffer> {
|
||||||
}
|
if (cards.length > 10) {
|
||||||
|
let pdf_promises = new Array<Promise<Buffer>>();
|
||||||
let image;
|
let i, j;
|
||||||
if (imgsrc.startsWith("http")) {
|
for (i = 0, j = cards.length; i < j; i += 10) {
|
||||||
image = (await axios.get(imgsrc)).data;
|
let chunk = cards.slice(i, i + 10);
|
||||||
image = Buffer.from(image).toString('base64');
|
pdf_promises.push(this.generateRunnerCards(chunk, locale));
|
||||||
}
|
}
|
||||||
else {
|
const pdfs = await Promise.all(pdf_promises);
|
||||||
if (imgsrc.startsWith("./")) {
|
return await this.mergePdfs(pdfs);
|
||||||
imgsrc = imgsrc.replace("./", "");
|
}
|
||||||
}
|
const cards_swapped = this.swapArrayPairs(cards);
|
||||||
image = fs.readFileSync(`${this.templateDir}/${imgsrc}`, { encoding: "base64" });
|
await i18next.changeLanguage(locale);
|
||||||
}
|
const template_source = fs.readFileSync(`${this.templateDir}/runner_card.html`, 'utf8');
|
||||||
|
const template = Handlebars.compile(template_source);
|
||||||
image = `data:${img_type};base64,${image}`
|
let result = template({ cards, cards_swapped, eventname: "LfK! 2069", codeformat: "qrcode" })
|
||||||
$(element).attr("src", image)
|
result = await awaitAsyncHandlebarHelpers(result);
|
||||||
})
|
fs.writeFileSync("lelelelele.tmp", result);
|
||||||
|
const pdf = await this.renderPdf(result, { format: "A4", landscape: false });
|
||||||
return $.html();
|
return pdf
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method manages the creation of pdfs via puppeteer.
|
* Converts all images in html to base64.
|
||||||
* @param html The HTML that should get rendered.
|
* Works with image files in the template directory or images from urls.
|
||||||
* @param options Puppeteer PDF option (eg: {format: "A4"})
|
* @param html The html string whoms images shall get replaced.
|
||||||
*/
|
*/
|
||||||
public async renderPdf(html: string, options): Promise<any> {
|
public async imgToBase64(html): Promise<string> {
|
||||||
html = await this.imgToBase64(html);
|
const $ = cheerio.load(html)
|
||||||
let page = await this.browser.newPage();
|
$('img').each(async (index, element) => {
|
||||||
await page.setContent(html);
|
let imgsrc = $(element).attr("src");
|
||||||
const pdf = await page.pdf(options);
|
if (imgsrc.startsWith("data:image")) {
|
||||||
await page.close();
|
return;
|
||||||
return pdf;
|
}
|
||||||
}
|
const img_type = mime.lookup(imgsrc);
|
||||||
|
|
||||||
/**
|
if (!(img_type.includes("image"))) {
|
||||||
* Merges multiple pdfs into one.
|
throw new Error("File is not image mime type");
|
||||||
* @param pdfs The pdfs you want to merge as an buffer array.
|
}
|
||||||
* @returns The merged pdf as a buffer.
|
|
||||||
*/
|
let image;
|
||||||
private async mergePdfs(pdfs: Buffer[]): Promise<Buffer> {
|
if (imgsrc.startsWith("http")) {
|
||||||
const mergedPdf = await PDFDocument.create();
|
image = (await axios.get(imgsrc)).data;
|
||||||
|
image = Buffer.from(image).toString('base64');
|
||||||
for (const pdfBuffer of pdfs) {
|
}
|
||||||
const pdf = await PDFDocument.load(pdfBuffer);
|
else {
|
||||||
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
|
if (imgsrc.startsWith("./")) {
|
||||||
copiedPages.forEach((page) => {
|
imgsrc = imgsrc.replace("./", "");
|
||||||
mergedPdf.addPage(page);
|
}
|
||||||
});
|
image = fs.readFileSync(`${this.templateDir}/${imgsrc}`, { encoding: "base64" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Buffer>(await mergedPdf.save());
|
image = `data:${img_type};base64,${image}`
|
||||||
}
|
$(element).attr("src", image)
|
||||||
|
})
|
||||||
/**
|
|
||||||
* Generates a new dummy runner with halfspaces for all strings.
|
return $.html();
|
||||||
* Can be used to generate empty sponsoring contracts.
|
}
|
||||||
* @returns A new runner object that apears to be empty.
|
|
||||||
*/
|
/**
|
||||||
private generateEmptyRunner(): Runner {
|
* This method manages the creation of pdfs via puppeteer.
|
||||||
let group = new RunnerGroup();
|
* @param html The HTML that should get rendered.
|
||||||
group.id = 0;
|
* @param options Puppeteer PDF option (eg: {format: "A4"})
|
||||||
group.name = " ";
|
*/
|
||||||
let runner = new Runner();
|
public async renderPdf(html: string, options): Promise<any> {
|
||||||
runner.id = 0;
|
html = await this.imgToBase64(html);
|
||||||
runner.firstname = " ";
|
let page = await this.browser.newPage();
|
||||||
runner.lastname = " ";
|
await page.setContent(html);
|
||||||
runner.group = group;
|
const pdf = await page.pdf(options);
|
||||||
return runner;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
@ -8,7 +8,8 @@ export const config = {
|
|||||||
eventname: process.env.EVENT_NAME || "Please set the event name",
|
eventname: process.env.EVENT_NAME || "Please set the event name",
|
||||||
currency_symbol: process.env.CURRENCY_SYMBOL || "€",
|
currency_symbol: process.env.CURRENCY_SYMBOL || "€",
|
||||||
sponsoring_receipt_minimum_amount: process.env.SPONSORING_RECEIPT_MINIMUM_AMOUNT || "10",
|
sponsoring_receipt_minimum_amount: process.env.SPONSORING_RECEIPT_MINIMUM_AMOUNT || "10",
|
||||||
codeformat: process.env.CODEFORMAT || "qrcode"
|
codeformat: process.env.CODEFORMAT || "qrcode",
|
||||||
|
sponor_logos: getSponsorLogos()
|
||||||
}
|
}
|
||||||
let errors = 0
|
let errors = 0
|
||||||
if (typeof config.internal_port !== "number") {
|
if (typeof config.internal_port !== "number") {
|
||||||
@ -17,4 +18,13 @@ if (typeof config.internal_port !== "number") {
|
|||||||
if (typeof config.development !== "boolean") {
|
if (typeof config.development !== "boolean") {
|
||||||
errors++
|
errors++
|
||||||
}
|
}
|
||||||
|
function getSponsorLogos(): string[] {
|
||||||
|
try {
|
||||||
|
const logos = JSON.parse(process.env.SPONOR_LOGOS);
|
||||||
|
if (!Array.isArray(logos)) { throw new Error("Not an array.") }
|
||||||
|
return logos;
|
||||||
|
} catch (error) {
|
||||||
|
return ["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg=="];
|
||||||
|
}
|
||||||
|
}
|
||||||
export let e = errors
|
export let e = errors
|
@ -1,30 +1,46 @@
|
|||||||
import { Body, JsonController, Post, QueryParam, Res } from 'routing-controllers';
|
import { Body, JsonController, Post, QueryParam, Res } from 'routing-controllers';
|
||||||
import { OpenAPI } from 'routing-controllers-openapi';
|
import { OpenAPI } from 'routing-controllers-openapi';
|
||||||
import { Runner } from '../models/Runner';
|
import { Runner } from '../models/Runner';
|
||||||
import { PdfCreator } from '../PdfCreator';
|
import { RunnerCard } from '../models/RunnerCard';
|
||||||
|
import { PdfCreator } from '../PdfCreator';
|
||||||
/**
|
|
||||||
* The pdf controller handels all endpoints concerning pdf generation.
|
/**
|
||||||
* It therefore is the hearth of the document-generation server's endpoints.
|
* The pdf controller handels all endpoints concerning pdf generation.
|
||||||
* All endpoints have to accept a locale query-param to support i18n.
|
* It therefore is the hearth of the document-generation server's endpoints.
|
||||||
*/
|
* All endpoints have to accept a locale query-param to support i18n.
|
||||||
@JsonController()
|
*/
|
||||||
export class PdfController {
|
@JsonController()
|
||||||
private pdf: PdfCreator = new PdfCreator();
|
export class PdfController {
|
||||||
private initialized: boolean = false;
|
private pdf: PdfCreator = new PdfCreator();
|
||||||
|
private initialized: boolean = false;
|
||||||
@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)." })
|
@Post('/contracts')
|
||||||
async generateContracts(@Body({ validate: true, options: { limit: "500mb" } }) runners: Runner | Runner[], @Res() res: any, @QueryParam("locale") locale: string, @QueryParam("codeformat") codeformat: string) {
|
@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)." })
|
||||||
if (!this.initialized) {
|
async generateContracts(@Body({ validate: true, options: { limit: "500mb" } }) runners: Runner | Runner[], @Res() res: any, @QueryParam("locale") locale: string, @QueryParam("codeformat") codeformat: string) {
|
||||||
await this.pdf.init();
|
if (!this.initialized) {
|
||||||
this.initialized = true;
|
await this.pdf.init();
|
||||||
}
|
this.initialized = true;
|
||||||
if (!Array.isArray(runners)) {
|
}
|
||||||
runners = [runners];
|
if (!Array.isArray(runners)) {
|
||||||
}
|
runners = [runners];
|
||||||
const contracts = await this.pdf.generateSponsoringContract(runners, locale, codeformat);
|
}
|
||||||
res.setHeader('content-type', 'application/pdf');
|
const contracts = await this.pdf.generateSponsoringContract(runners, locale, codeformat);
|
||||||
return contracts;
|
res.setHeader('content-type', 'application/pdf');
|
||||||
}
|
return contracts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/cards')
|
||||||
|
@OpenAPI({ description: "Generate runner card pdfs from runner card objects.<br>You can choose your prefered locale by passing the 'locale' query-param." })
|
||||||
|
async generateCards(@Body({ validate: true, options: { limit: "500mb" } }) cards: RunnerCard | RunnerCard[], @Res() res: any, @QueryParam("locale") locale: string) {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.pdf.init();
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(cards)) {
|
||||||
|
cards = [cards];
|
||||||
|
}
|
||||||
|
const contracts = await this.pdf.generateRunnerCards(cards, locale);
|
||||||
|
res.setHeader('content-type', 'application/pdf');
|
||||||
|
return contracts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
71
src/templates/runner_card.html
Normal file
71
src/templates/runner_card.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<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
|
||||||
|
}
|
||||||
|
|
||||||
|
body.A4 .sheet {
|
||||||
|
width: 210mm;
|
||||||
|
height: 296mm
|
||||||
|
}
|
||||||
|
|
||||||
|
.runnercard {
|
||||||
|
border: 1px solid;
|
||||||
|
height: 5.5cm;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="A4 landscape">
|
||||||
|
<div class="sheet">
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
{{#each cards}}
|
||||||
|
<div class="column is-half runnercard">
|
||||||
|
<p class="title is-5" style="text-align: center; padding-bottom: 0; margin-top: -0.75rem;">{{../eventname}}</p>
|
||||||
|
<p style="text-align: center; margin-top: -1.5rem; font-size: small;">lauf-fuer-kaya.de - am 01.01.2021</p>
|
||||||
|
<p style="font-size: small;">Mit unterstützung von:</p>
|
||||||
|
<div class="columns" style="height: 6rem; overflow: hidden;">
|
||||||
|
<div class="column is-two-thirds">
|
||||||
|
<!--SPONSOR LOGO HERE-->
|
||||||
|
<img style="vertical-align: revert; margin-top: auto; object-fit: cover; max-height: 2cm;"
|
||||||
|
src="{{--sponsor this.id}}" />
|
||||||
|
</div>
|
||||||
|
<div class="column is-one-third">
|
||||||
|
<!--BARCODE HERE-->
|
||||||
|
<img style="vertical-align: revert; margin-top: auto; object-fit: cover; max-height: 2cm;"
|
||||||
|
src="{{--bc this.id ../codeformat}}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>{{this.runner.lastname}}, {{this.runner.firstname}} {{this.runner.middlename}}</p>
|
||||||
|
<p>{{this.runner.group.name}}</p>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sheet">
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
{{#each cards_swapped}}
|
||||||
|
<div class="column is-half runnercard" style="justify-content: center; align-items: center; text-align: center;">
|
||||||
|
<!--SPONSOR LOGO FIRST-->
|
||||||
|
<div style="height: 2cm; padding: 0 0 2.25cm 0">
|
||||||
|
<img style="object-fit: cover; max-height: 2cm;" src="{{--sponsor this.id}}" />
|
||||||
|
</div>
|
||||||
|
<img style="object-fit: cover; max-height: 2.5cm; position: relative;" src="{{--bc this.id ../codeformat}}" />
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -1,79 +1,128 @@
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import faker from "faker"
|
import faker from "faker"
|
||||||
import { Runner } from '../models/Runner'
|
import { Runner } from '../models/Runner'
|
||||||
import { RunnerGroup } from '../models/RunnerGroup'
|
import { RunnerCard } from '../models/RunnerCard'
|
||||||
|
import { RunnerGroup } from '../models/RunnerGroup'
|
||||||
const baseurl = "http://localhost:4010"
|
|
||||||
|
const baseurl = "http://localhost:4010"
|
||||||
axios.interceptors.request.use((config) => {
|
|
||||||
config.headers['request-startTime'] = process.hrtime()
|
axios.interceptors.request.use((config) => {
|
||||||
return config
|
config.headers['request-startTime'] = process.hrtime()
|
||||||
})
|
return config
|
||||||
|
})
|
||||||
axios.interceptors.response.use((response) => {
|
|
||||||
const start = response.config.headers['request-startTime']
|
axios.interceptors.response.use((response) => {
|
||||||
const end = process.hrtime(start)
|
const start = response.config.headers['request-startTime']
|
||||||
const milliseconds = Math.round((end[0] * 1000) + (end[1] / 1000000))
|
const end = process.hrtime(start)
|
||||||
response.headers['request-duration'] = milliseconds
|
const milliseconds = Math.round((end[0] * 1000) + (end[1] / 1000000))
|
||||||
return response
|
response.headers['request-duration'] = milliseconds
|
||||||
})
|
return response
|
||||||
|
})
|
||||||
function generateRunners(amount: number): Runner[] {
|
|
||||||
let runners: Runner[] = new Array<Runner>();
|
function generateRunners(amount: number): Runner[] {
|
||||||
let group = new RunnerGroup();
|
let runners: Runner[] = new Array<Runner>();
|
||||||
let runner = new Runner();
|
let group = new RunnerGroup();
|
||||||
for (var i = 0; i < amount; i++) {
|
let runner = new Runner();
|
||||||
group.name = faker.company.bsBuzz();
|
for (var i = 0; i < amount; i++) {
|
||||||
group.id = Math.floor(Math.random() * (9999999 - 1) + 1);
|
group.name = faker.company.bsBuzz();
|
||||||
runner.firstname = faker.name.firstName();
|
group.id = Math.floor(Math.random() * (9999999 - 1) + 1);
|
||||||
runner.lastname = faker.name.lastName();
|
runner.firstname = faker.name.firstName();
|
||||||
runner.id = Math.floor(Math.random() * (9999999 - 1) + 1);
|
runner.lastname = faker.name.lastName();
|
||||||
runners.push(runner);
|
runner.id = Math.floor(Math.random() * (9999999 - 1) + 1);
|
||||||
}
|
runners.push(runner);
|
||||||
return runners;
|
}
|
||||||
}
|
return runners;
|
||||||
|
}
|
||||||
async function postContracts(runners: Runner[]): Promise<Measurement> {
|
|
||||||
const res = await axios.post(`${baseurl}/contracts`, runners);
|
function generateCards(amount: number): RunnerCard[] {
|
||||||
return new Measurement("contract", runners.length, parseInt(res.headers['request-duration']))
|
let cards: RunnerCard[] = new Array<RunnerCard>();
|
||||||
}
|
let card = new RunnerCard();
|
||||||
|
for (let runner of generateRunners(amount)) {
|
||||||
async function testContracts(sizes): Promise<Measurement[]> {
|
card.id = runner.id;
|
||||||
let measurements = new Array<Measurement>();
|
card.code = idToEan13(card.id);
|
||||||
console.log("#### Testing contracts ####");
|
card.runner = runner;
|
||||||
|
cards.push(card);
|
||||||
for (let size of sizes) {
|
}
|
||||||
const m = await postContracts(generateRunners(size));
|
return cards;
|
||||||
console.log(m.toString());
|
}
|
||||||
measurements.push(m);
|
|
||||||
}
|
function idToEan13(id): string {
|
||||||
return measurements;
|
const multiply = [1, 3];
|
||||||
}
|
id = id.toString();
|
||||||
|
|
||||||
async function main() {
|
if (id.length > 12) {
|
||||||
const sizes = [0, 1, 10, 50, 100, 200, 500, 1000]
|
throw new Error("id too long");
|
||||||
console.log("########### Speedtest ###########");
|
}
|
||||||
console.log(`Document server version (according to the api): ${(await axios.get("http://localhost:4010/version")).data.version}`);
|
while (id.length < 12) { id = '0' + id; }
|
||||||
console.log("####### Running tests #######");
|
|
||||||
const contractResults = await testContracts(sizes);
|
let total = 0;
|
||||||
console.log("####### Results #######");
|
id.split('').forEach((letter, index) => {
|
||||||
console.table(contractResults);
|
total += parseInt(letter, 10) * multiply[index % 2];
|
||||||
}
|
});
|
||||||
|
const checkSum = (Math.ceil(total / 10) * 10) - total;
|
||||||
main();
|
return id + checkSum.toString();
|
||||||
|
}
|
||||||
class Measurement {
|
|
||||||
public type: string;
|
async function postContracts(runners: Runner[]): Promise<Measurement> {
|
||||||
public inputcount: number;
|
const res = await axios.post(`${baseurl}/contracts`, runners);
|
||||||
public responsetime: number;
|
return new Measurement("contract", runners.length, parseInt(res.headers['request-duration']))
|
||||||
|
}
|
||||||
constructor(type: string, input: number, time: number) {
|
|
||||||
this.type = type;
|
async function postCards(cards: RunnerCard[]): Promise<Measurement> {
|
||||||
this.inputcount = input;
|
const res = await axios.post(`${baseurl}/cards`, cards);
|
||||||
this.responsetime = time;
|
return new Measurement("card", cards.length, parseInt(res.headers['request-duration']))
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string {
|
async function testContracts(sizes): Promise<Measurement[]> {
|
||||||
return `It took ${this.responsetime}ms to generate ${this.inputcount} pdfs for the type ${this.type}.`
|
let measurements = new Array<Measurement>();
|
||||||
}
|
console.log("#### Testing contracts ####");
|
||||||
|
|
||||||
|
for (let size of sizes) {
|
||||||
|
const m = await postContracts(generateRunners(size));
|
||||||
|
console.log(m.toString());
|
||||||
|
measurements.push(m);
|
||||||
|
}
|
||||||
|
return measurements;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testCards(sizes): Promise<Measurement[]> {
|
||||||
|
let measurements = new Array<Measurement>();
|
||||||
|
console.log("#### Testing Cards ####");
|
||||||
|
|
||||||
|
for (let size of sizes) {
|
||||||
|
const m = await postCards(generateCards(size));
|
||||||
|
console.log(m.toString());
|
||||||
|
measurements.push(m);
|
||||||
|
}
|
||||||
|
return measurements;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const sizes = [0, 1, 10, 50, 100, 200, 500, 1000]
|
||||||
|
console.log("########### Speedtest ###########");
|
||||||
|
console.log(`Document server version (according to the api): ${(await axios.get("http://localhost:4010/version")).data.version}`);
|
||||||
|
console.log("####### Running tests #######");
|
||||||
|
const contractResults = await testContracts(sizes);
|
||||||
|
const cardResults = await testCards(sizes);
|
||||||
|
console.log("####### Results #######");
|
||||||
|
console.table(contractResults);
|
||||||
|
console.table(cardResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
||||||
|
class Measurement {
|
||||||
|
public type: string;
|
||||||
|
public inputcount: number;
|
||||||
|
public responsetime: number;
|
||||||
|
|
||||||
|
constructor(type: string, input: number, time: number) {
|
||||||
|
this.type = type;
|
||||||
|
this.inputcount = input;
|
||||||
|
this.responsetime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString(): string {
|
||||||
|
return `It took ${this.responsetime}ms to generate ${this.inputcount} pdfs for the type ${this.type}.`
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user