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