File

app/core/services/pdf-download-service/pdf-document.ts

Description

jsPDF wrapper for PDF generation.

Index

Properties
Methods

Constructor

constructor(margins: literal type)
Parameters :
Name Type Optional
margins literal type No

Properties

addPerPageFunc
Type : function
cursorX
Type : number
cursorY
Type : number
Private doc
Type : any
Public margins
Type : literal type
pageHeight
Type : number
pageWidth
Type : number
tableUniqueId
Type : number

Methods

Public addBox
addBox(left: number, top: number, width: number, height: number)

Adds a box.

Parameters :
Name Type Optional
left number No
top number No
width number No
height number No
Returns : void
Public addImage
addImage(image: any, width: number, height: number)

Adds an image from an element scaled to fit inside the given box and advances the cursor.

Parameters :
Name Type Optional
image any No
width number No
height number No
Returns : void
Public addImageAt
addImageAt(image: any, left: number, top: number, width: number, height: number)

Adds an image from an element scaled to fit inside the given box at an absolute position (left, top).

Parameters :
Name Type Optional
image any No
left number No
top number No
width number No
height number No
Returns : void
Public addLine
addLine(fromLeft: number, fromTop: number, toLeft: number, toTop: number)

Adds a line between two points.

Parameters :
Name Type Optional
fromLeft number No
fromTop number No
toLeft number No
toTop number No
Returns : void
Public addLink
addLink(left: number, top: number, width: number, height: number, url: string)

Adds a link to the document.

Parameters :
Name Type Optional
left number No
top number No
width number No
height number No
url string No
Returns : void
Public addPage
addPage()

Adds a new page to the document.

Returns : void
Public addPerPage
addPerPage(func: (page: number) => void)

Adds content on every page using a function which receives the page number

Parameters :
Name Type Optional
func function No
Returns : void
Public addTable
addTable(element: any, caption: string, width: number)

Adds a table to the PDF from an element.

Parameters :
Name Type Optional
element any No
caption string No
width number No
Returns : void
Public addText
addText(text: string | string[], size: number, color: any, align: string)

Adds a text and advances the cursor.

Parameters :
Name Type Optional Default value
text string | string[] No
size number No
color any No '#282828'
align string No 'left'
Returns : void
Public addTextAt
addTextAt(text: string | string[], left: number, top: number, size: number, color: any, align: string)

Adds a text at an absolute position (left, top). Returns the wrapped text.

Parameters :
Name Type Optional Default value
text string | string[] No
left number No
top number No
size number No
color any No '#282828'
align string No 'left'
Returns : string[]
Public getCurrentPage
getCurrentPage()

Returns the current page of the cursor.

Returns : number
Public getTotalPages
getTotalPages()

Returns the total amount of pages.

Returns : number
Public pageBreak
pageBreak(size: number)

Checks a number of pixels ahead if we need to add a page break.

Parameters :
Name Type Optional
size number No
Returns : boolean
Public parseHtml
parseHtml(html: string, tags: string[], tagIndex: number)

Parses a HTML string containing tags and returns a list of tag/text pairs.

Parameters :
Name Type Optional Default value
html string No
tags string[] No
tagIndex number No 0
Returns : literal type[]
Public save
save(filename: string)

Saves the document into a downloaded file.

Parameters :
Name Type Optional
filename string No
Returns : void
Public setCurrentPage
setCurrentPage(page: number)

Sets the current page of the cursor.

Parameters :
Name Type Optional
page number No
Returns : void
Public wordWrap
wordWrap(text: string, width: number)

Returns a word wrapped text.

Parameters :
Name Type Optional
text string No
width number No
Returns : string[]
import { pdfFonts } from './pdf-fonts';
import jsPDF from 'jspdf';
import canvg from 'canvg';
import 'jspdf-autotable';
import 'canvg';

/**
 * jsPDF wrapper for PDF generation.
 * */
export class PdfDocument {

	private doc: any;
	cursorX: number;
	cursorY: number;
	pageWidth: number;
	pageHeight: number;
	tableUniqueId: number;
	addPerPageFunc: (number) => void;
	
	constructor(
		public margins: {
			top: number,
			bottom: number,
			left: number
		}
	) {
		// Init document
		this.doc = new jsPDF('p', 'pt', 'a4');
		this.pageWidth = this.doc.internal.pageSize.getWidth();
		this.pageHeight = this.doc.internal.pageSize.getHeight();
		this.cursorX = this.cursorY = 0;
		this.tableUniqueId = 0;
		
		// Normal font
		this.doc.addFileToVFS('skfchevinot-webfont-normal.ttf', pdfFonts.normal);
		this.doc.addFont('skfchevinot-webfont-normal.ttf', 'skfchevinot-webfont', 'normal', 'WinAnsiEncoding');
		this.doc.setFont('skfchevinot-webfont');
		this.doc.setFontStyle('normal');
	}

	/**
	 * Parses a HTML string containing tags and returns a list of tag/text pairs.
	 */
	public parseHtml(html: string, tags: string[], tagIndex: number = 0): { tag: string, text: string }[] {

		// Split using the start tag
		let tag = tags[tagIndex];
		let startSplit = html.split('<' + tag + '>');
		let out = [];

		startSplit.forEach((text, index) => {

			// Before start tag, store as regular text
			if (index == 0 && text != '') {
				out.push({ tag: '', text: text });
				return;
			}
			
			// Split remaining by end tag
			let endSplit = text.split('</' + tag + '>');
			endSplit.forEach((text, index) => {

				// Inside tag
				if (index == 0 && text != '') {
					out.push({ tag: tag, text: text });

				// Outside tag, split recursively using the next tag in the list
				} else if (tagIndex < tags.length - 1) {
					out.push.apply(this.parseHtml(text, tags, tagIndex + 1));
				}
			});
		});

		return out;
	}
	
	/**
	 * Adds a table to the PDF from an element.
	 */
	public addTable(element: any, caption: string, width: number): void {

		// Caption
		this.addText(caption, 12);

		// Table
		const marginBottom = 30;
		const fontSizes = {
			'': 10,
			'sup': 8,
			'sub': 8
		};
		const textOffsets = {
			'': 0,
			'sup': -5,
			'sub': 5
		}

		element.setAttribute('id', 'pdf-table-id-' + this.tableUniqueId++)
		this.doc.autoTable({
			html: '#' + element.getAttribute('id'),
			startY: this.cursorY,
			styles: {
				font: 'skfchevinot-webfont',
				textColor: "#333a3e"
			},
			tableWidth: width,
			margin: {
				left: this.cursorX,
				top: this.margins.top,
				bottom: this.margins.bottom
			},
			didParseCell: (hookData) => {
				hookData.cell.styles.halign = (parseInt(hookData.column.index) == hookData.table.columns.length - 1) ? "right": "left";
				hookData.cell.tags = [];

				if (hookData.cell.raw)
				{
					// Look for and parse <sub> and <sup>, we will render them ourselves
					let span = hookData.cell.raw.querySelector('span');
					if (span && (span.innerHTML.includes('<sub>') || span.innerHTML.includes('<sup>'))) {
						hookData.cell.tags = this.parseHtml(span.innerHTML, [ 'sub', 'sup' ]);
						hookData.cell.text = '';
					}
				}
			},
			didDrawCell: (hookData) => {
				// Render sub/superscript manually
				if (hookData.cell.text == '' && hookData.cell.tags.length) {

					// Apply offset and font size to each tag
					hookData.cell.tags.forEach(tagText => {
						this.addTextAt(tagText.text, hookData.cell.textPos.x, hookData.cell.textPos.y + textOffsets[tagText.tag], fontSizes[tagText.tag], hookData.cell.styles.textColor);
						hookData.cell.textPos.x += this.doc.getStringUnitWidth(tagText.text) * fontSizes[tagText.tag];
					});
				}
			}
		});
		
		this.cursorY = this.doc.autoTable.previous.finalY + marginBottom;
	}

	/**
	 * Checks a number of pixels ahead if we need to add a page break.
	 */
	public pageBreak(size: number): boolean {
		if (this.cursorY + size > this.pageHeight - this.margins.bottom) {

			// We're on the last page, add a new one
			if (this.getCurrentPage() == this.getTotalPages()) {
				this.addPage();
			}

			// Advance to next page and move cursor to top
			this.setCurrentPage(this.getCurrentPage() + 1);
			this.cursorY = this.margins.top;

			return true;
		}

		return false;
	}

	/**
	 * Returns a word wrapped text.
	 * */
	public wordWrap(text: string, width: number): string[] {
		return this.doc.splitTextToSize(text, width);
	}
	
	/**
	 * Adds a text and advances the cursor.
	 * */
	public addText(text: string | string[], size: number, color: any = '#282828', align: string = 'left'): void {
		const spaceSize = 3;
		const marginBottom = 10;

		this.pageBreak(size);
		let textWrapped = this.addTextAt(text, this.cursorX, this.cursorY, size, color, align);
		this.cursorY += textWrapped.length * (size + spaceSize) + marginBottom;
	}

	/**
	 * Adds a text at an absolute position (left, top). Returns the wrapped text.
	 * */
	public addTextAt(text: string | string[], left: number, top: number, size: number, color: any = '#282828', align = 'left'): string[] {
		this.doc.setFontSize(size);
		this.doc.setTextColor(color);
		
		// Wrap depending on alignment
		let textWrapped: string[];
		if (typeof text === "string")
		{
			if (align === 'left') {
				textWrapped = this.wordWrap(text, this.pageWidth - this.margins.left - left);
			} else if (align === 'right') {
				textWrapped = this.wordWrap(text, left - this.margins.left);
			} else {
				textWrapped = [ text ];
			}
		} else {
			textWrapped = text;
		}

		this.doc.text(textWrapped, left, top, { align: align, baseline: 'top' });
		return textWrapped;
	}

	/** 
	 * Adds an image from an element scaled to fit inside the given box and advances the cursor. 
	 */
	public addImage(image: any, width: number, height: number): void {
		this.pageBreak(height);
		this.addImageAt(image, this.cursorX, this.cursorY, width, height);
		this.cursorY += height;
	}

	/**
	 * Adds a box.
	 */
	public addBox(left: number, top: number, width: number, height: number): void {
		this.doc.rect(left, top, width, height);
	}

	/**
	 * Adds a line between two points.
	 */
	public addLine(fromLeft: number, fromTop: number, toLeft: number, toTop: number): void {
		this.doc.line(fromLeft, fromTop, toLeft, toTop);
	}

	/** Adds a link to the document. */
	public addLink(left: number, top: number, width: number, height: number, url: string): void {
		this.doc.link(left, top, width, height, { url: url });
	}

	/** 
	 * Adds an image from an element scaled to fit inside the given box at an absolute position (left, top). 
	 */
	public addImageAt(image: any, left: number, top: number, width: number, height: number): void {
		let data = image;

		// Load SVG using canvg
		if (image.src.endsWith('.svg')) {
			let tmpCanvas = document.createElement('canvas');
			canvg(tmpCanvas, image.src);
			
			// To get rid of the gray border
			let realCanvas = document.createElement('canvas');
			realCanvas.width = tmpCanvas.width;
			realCanvas.height = tmpCanvas.height;
			var context = realCanvas.getContext('2d');
			context.fillStyle = '#FFFFFF';
			context.fillRect(0, 0, realCanvas.width, realCanvas.height);
			context.drawImage(tmpCanvas, 0, 0);

			data = realCanvas.toDataURL('image/png');
			image = realCanvas;
		}

		// Center the image in the box by the largest dimension (width or height)
		let ratio = image.width / image.height;
		let imgWidth, imgHeight;

		// Landscape, center vertically
		if (image.width > image.height) {
			imgWidth = Math.min(image.width, width);
			imgHeight = imgWidth / ratio;
			top += height / 2 - imgHeight / 2;
		}

		// Portrait, center horizontally
		else {
			imgHeight = Math.min(image.height, height);
			imgWidth = imgHeight * ratio;
			left += width / 2 - imgWidth / 2;
		}

		this.doc.addImage(data, 'PNG', left, top, imgWidth, imgHeight);
	}

	/**
	 * Adds content on every page using a function which receives the page number
	*/
	public addPerPage(func: (page: number) => void): void {
		this.addPerPageFunc = func;
	}

	/** 
	 * Returns the current page of the cursor.
	 */
	public getCurrentPage(): number {
		return this.doc.internal.getCurrentPageInfo().pageNumber;
	}

	/** Sets the current page of the cursor. */
	public setCurrentPage(page: number): void {
		this.doc.setPage(page);
	}

	/**
	 * Adds a new page to the document.
	 * */
	public addPage(): void {
		this.doc.addPage();
	}

	/**
	 * Returns the total amount of pages.
	 */
	public getTotalPages(): number {
		return this.doc.internal.getNumberOfPages();
	}

	/**
	 * Saves the document into a downloaded file.
	 */
	public save(filename: string): void {
		this.doc.fromHTML(
			document.createElement('div'),
			this.margins.left,
			this.margins.top,
			{},
			function (dispose) {
				let totalPages = this.getTotalPages();
				for (let i = 1; i <= totalPages; i++) {
					this.doc.setPage(i);
					this.addPerPageFunc(i);
				}
			}.bind(this),
			this.margins
		);
		this.doc.save(filename);
	}
}

result-matching ""

    No results matching ""