import { ApplicationRef, ComponentFactoryResolver, ElementRef, Injectable, Injector, Type } from '@angular/core'; import { PDFResources } from '@core/resources/pdf.resources'; import { FileUploadForPDF, GeneratePdfPayload } from '@core/typings/pdf.typing'; import { ApplicationPdfComponent } from '@features/application-pdf/application-pdf/application-pdf.component'; import { DownloadFormPdfComponent } from '@features/configure-forms/download-form-pdf/download-form-pdf.component'; import { EmailPdfComponent } from '@features/system-emails/email-pdf/email-pdf.component'; import { ApplicationEmailPdf, EmailPdfType } from '@features/system-emails/email.typing'; import { FileService } from '@yourcause/common'; import JSZip from 'jszip'; import { ApplicationFileService } from './application-file.service'; import { JsZipService } from './js-zip.service'; export type Without = { [P in Exclude]: T[P] }; @Injectable({ providedIn: 'root' }) export class PDFService { constructor ( private pdfResources: PDFResources, private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector, private appRef: ApplicationRef, private applicationFileService: ApplicationFileService, private fileService: FileService, private jsZipService: JsZipService ) { } /** * * @param props: Inputs on the component we are making a PDF of * @param ComponentClass: Component for PDF * @param stylesheets: Stylesheets for component * @param additionalFooterTemplate: Additional footer template string * @returns PDF payload info */ generatePdfPayload ( props: Without, ComponentClass: Type, stylesheets: string[], additionalFooterTemplate?: string ): GeneratePdfPayload { const compFactory = this.componentFactoryResolver.resolveComponentFactory( ComponentClass ); const comp = compFactory.create(this.injector); this.appRef.attachView(comp.hostView); Object.assign(comp.instance, props); comp.changeDetectorRef.detectChanges(); this.addStyles(stylesheets, comp.location); const htmlElement: HTMLElement = comp.location.nativeElement; const html = this.replaceHtml(htmlElement); // run `window.DEBUG_PDF = true` in the console // and it will open the pdf in a new window for you to debug without constantly creating new PDFs if ((window as any).DEBUG_PDF) { const subWindow = window.open(); subWindow.document.body.innerHTML = html; return null; } else { return { margins: { marginTop: '.5in', marginBottom: '.75in', marginLeft: '.25in', marginRight: '.25in' }, headerTemplate: '
', footerTemplate: `
`, htmlContent: htmlElement.innerHTML }; } } /** * simple minification, remove comments, new lines, and extra spaces * * @param htmlElement: the HTML element from PDF * @returns the html element with replacements */ replaceHtml (htmlElement: HTMLElement) { return htmlElement.innerHTML .replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '') // css comments .replace(//g, '') // HTML comments .replace(/\r?\n/g, '') // new lines .replace(/ +/g, ' '); // one or more spaces } /** * * @param stylesheets: Stylesheets * @param view: Element Ref */ addStyles ( stylesheets: string[], view: ElementRef ) { stylesheets.forEach((stylesheet) => { const styleTag = document.createElement('style'); styleTag.innerHTML = stylesheet.replace(/(\/\*[\w\'\s\r\n\*]*\*\/)|(\/\/[\w\s\']*)|(\\/]*\>)/, ''); view.nativeElement.appendChild(styleTag); }); } /** * * @param props: Inputs for ApplicationPdfComponent * @returns The generated pdf url */ async generateApplicationPdf ( props: Without, additionalFooterTemplate: string ): Promise { const stylesheet = require( '../../features/application-pdf/application-pdf/application-pdf.component.scss' ); const payload = this.generatePdfPayload( props, ApplicationPdfComponent, [stylesheet], additionalFooterTemplate ); if (payload) { const { url } = await this.pdfResources.applicationHtmlToPdf( payload, props.applicationId ); return url; } return null; } /** * * @param downloadUrl: download url for pdf * @returns the pdf blob */ getPdfBlob (downloadUrl: string) { return this.pdfResources.getPdfBlobFromDownloadUrl(downloadUrl); } async getEmailPdfBlobForApplication ( applicationId: number, emails: ApplicationEmailPdf[], emailPdfType: EmailPdfType ): Promise { const downloadUrl = await this.generateEmailPdf( { emails, emailPdfType }, applicationId ); return this.getPdfBlob(downloadUrl); } async handleFileUploads ( fileUploads: FileUploadForPDF[], attachmentsFolder: JSZip ) { // track used file names to allow multiple with the same name const usedFileNames: Record = {}; for (const fileUpload of fileUploads) { const { accessUrl } = await this.applicationFileService.getFile( +fileUpload.applicationId, +fileUpload.applicationFormId, +fileUpload.fileId ); const blob = await this.getPdfBlob(accessUrl); const originalFileName = fileUpload.fileName; let fileNameForZip = originalFileName; // if we have already used the original file name // increment the counter and append the counter to the file name used in the zip // otherwise, start the tracker at 1 if (originalFileName in usedFileNames) { ++usedFileNames[originalFileName]; // split the file into pieces const originalFileParts = originalFileName.split('.'); // pull off the last piece of the filename (pop mutates the array) const extension = originalFileParts.pop(); // join the remaining parts back together, put the increment in the filename, and add the extension to the end fileNameForZip = `${originalFileParts.join('.')} (${usedFileNames[originalFileName]})${extension}`; } else { usedFileNames[originalFileName] = 1; } this.jsZipService.addFile(attachmentsFolder, fileNameForZip, blob); } } /** * * @param zip: The zip to download * @param fileName: File name for the zip */ async handleZipDownload ( zip: JSZip, fileName: string ) { let zipBlob = await this.jsZipService.generateAsync(zip); try { zipBlob = new File([zipBlob], fileName, { type: 'application/pdf' }); } catch { } this.fileService.saveAs(zipBlob, fileName); } /** * * @param props: Inputs for DownloadFormPdfComponent * @returns The generated pdf url */ async generateFormPdf ( props: Without ) { const stylesheet = require( '../../features/configure-forms/download-form-pdf/download-form-pdf.component.scss' ); const payload = this.generatePdfPayload(props, DownloadFormPdfComponent, [stylesheet]); if (payload) { const { url } = await this.pdfResources.formHtmlToPdf( payload, props.form.id ); return url; } return null; } /** * * @param props: Inputs for EmailPdfComponent * @returns The generated pdf url */ async generateEmailPdf ( props: Without, recordId: number ) { const stylesheet1 = require( '../../features/system-emails/email-pdf/email-pdf.component.scss' ); const stylesheet2 = require( '../../features/system-emails/email-header-block/email-header-block.component.scss' ); const stylesheet3 = require( '../../../styles/_emails.scss' ); const stylesheets = [ stylesheet1, stylesheet2, stylesheet3 ]; const payload = this.generatePdfPayload(props, EmailPdfComponent, stylesheets); if (payload) { const { url } = await this.pdfResources.emailHtmlToPdf( payload, props.emailPdfType, recordId ); return url; } return null; } }