Source: PDFBuilder.js

import PDFDocument from 'pdfkit';
import blobStream from 'blob-stream';

/*
eslint no-underscore-dangle: ["error", {
  "allow": [
  "_setupHeader",
  "_setupFooter"]
}]
*/

/**
 * Builds the PDF
 */
class PDFBuilder {
  /**
   * @param {Object} options The options used to build the PDF
   * @param {string} options.title The title of the document
   * @param {boolean} options.includePageNumber=true Document includes page numbers
   * @param {string} options.documentFont=Helvetica The default font style for document
   * @param {string} options.documentBoldFont=Helvetica-Bold The default bold font style for document
   * @param {string} options.headingFont=options.documentBoldFont The font style of the page heading
   * @param {number} options.headingFontSize=16 The heading font size
   * @param {number} options.headingLineGap=18 The heading line gap
   * @param {string} options.subHeadingFont=options.documentBoldFont The font style of the subheading
   * @param {number} options.subHeadingFontSize=14 The subheading font size
   * @param {number} options.subHeadingLineGap=12 The subheading line gap
   * @param {( portrait | landscape )} options.layout=portrait The page layout style
   * @param {Margin} options.margin Margin of the page
   * @param {( letter )} options.size The size of the generated PDF
   * @param {metadata} options.metadata Additional data stored on the object.
   */
  constructor(options = {}) {
    this.title = options.title || '';
    this.metadata = options.metadata || {};

    this.doc = new PDFDocument({
      autoFirstPage: false,
      bufferPages: true,
    });

    let documentTitle = this.title;
    if (this.metadata.year) {
      documentTitle = `${this.metadata.year} ${documentTitle}`;
    }

    if (!this.metadata.filename) {
      this.metadata.filename = `${documentTitle}.pdf`;
    }

    this.doc.info.Title = documentTitle;
    this.doc.info.Producer = 'Harvest Profit';
    this.doc.info.Creator = 'Harvest Profit';

    this.stream = this.doc.pipe(blobStream());

    this.header = options.header;
    this.footer = options.footer;
    this.documentFont = options.documentFont || 'Helvetica';
    this.documentBoldFont = options.documentBoldFont || 'Helvetica-Bold';
    this.includePageNumber = (typeof options.includePageNumber === 'undefined') ? true : options.includePageNumber;
    this.headingFont = options.headingFont || this.documentBoldFont;
    this.headingFontSize = options.headingFontSize || 16;
    this.headingFontColor = options.headingFontColor || '#000000';
    this.headingLineGap = options.headingLineGap || 18;
    this.subHeadingFont = options.subHeadingFont || this.documentBoldFont;
    this.subHeadingFontSize = options.subHeadingFontSize || 14;
    this.subHeadingFontColor = options.subHeadingFontColor || '#000000';
    this.subHeadingLineGap = options.subHeadingLineGap || 12;
    this.layout = options.layout || 'portrait';
    const defaultMargins = {
      bottom: 30,
      left: 30,
      right: 30,
      top: 30,
    };
    this.margins = options.margins || defaultMargins;
    this.size = options.size || 'letter';

    if (this.header != null) {
      this._setupHeader();
    }

    this.currentPage = 0;
    if (this.footer != null) {
      this._setupFooter();
    }

    this.addPage();
  }

  /**
  * Add a new page to the PDF document.
  */
  addPage() {
    this.doc.addPage({
      layout: this.layout,
      margins: this.margins,
    });
    this.resetPosition();
  }

  /**
  * Adds a new page to the PDF document if close to the end of the content area.
  * @param {number} currentPage The current page of the document
  * @param {number} pageCount The number of pages
  * @return {boolean} Was a new page added
  */
  addPageIfNeeded(currentPage, pageCount) {
    if (this.doc.y / this.contentHeight() > 0.95) {
      // Don't add a new page if we can just switch to the next page that's already been created
      if (currentPage == null || pageCount == null || currentPage === pageCount - 1) {
        this.addPage();
      } else {
        this.doc.switchToPage(currentPage + 1);
        this.resetPosition();
      }

      return true;
    }

    return false;
  }

  /**
  * Add a heading to the PDF document.
  * @param {string} text The heading to add
  */
  addHeading(text) {
    this.addPageIfNeeded();

    const { doc } = this;
    const lineGap = this.headingLineGap;
    const x = this.pageLeft();
    const { y } = doc;

    doc.font(this.headingFont);
    doc.fontSize(this.headingFontSize);
    doc.fillColor(this.headingFontColor);
    doc.text(text.toUpperCase(), x, y, { lineGap });
  }

  /**
  * Add a sub heading to the PDF document.
  * @param {String} text The subheading text to add
  */
  addSubHeading(text) {
    this.addPageIfNeeded();

    const { doc } = this;
    const lineGap = this.subHeadingLineGap;
    const x = this.pageLeft();
    const { y } = doc;

    doc.font(this.subHeadingFont);
    doc.fontSize(this.subHeadingFontSize);
    doc.fillColor(this.subHeadingFontColor);
    doc.text(text.toUpperCase(), x, y, { lineGap });
  }

  /**
  * Add a table to the PDF document.
  * @param {PDFTable} table The table to add to the PDF
  */
  addTable(table) {
    this.addPageIfNeeded();
    table.addToPDF(this);
  }

  /**
  * Returns the height of the page inside the page margins.
  * @return {number}
  */
  contentHeight() {
    const topMargin = this.header == null ? this.margins.top : this.header.height;
    return this.doc.page.height - topMargin - this.margins.bottom;
  }

  /**
  * Returns the width of the page inside the page margins.
  * @return {number} The width of the page inside the page margins
  */
  contentWidth() {
    return this.doc.page.width - this.margins.left - this.margins.right;
  }

  /**
  * Closes the document and generates blob URL that can be saved to disk.
  * @example
  * let pdfBuilder = new PDFBuilder();
  * let saveAs = require('file-saver');
  * pdfBuilder.generateBlob().then(function(blob) {
  *   saveAs(blob, 'document.pdf');
  * });
  * @return {Promise} Resolves when the PDF has been generated
  */
  generateBlob() {
    const { doc, stream } = this;

    return new Promise((resolve) => {
      doc.flushPages();
      doc.end();

      stream.on('finish', () => {
        const blob = stream.toBlob('application/pdf');
        resolve(blob);
      });
    });
  }

  /**
  * Closes the document and generates blob URL that can be loaded by a web browser.
  * @example
  * let pdfBuilder = new PDFBuilder();
  * pdfBuilder.generateBlobURL().then(function(url) {
  *   $('iframe')[0].src = url;
  * });
  * @return {Promise} Resolves when the PDF Url has been generated
  */
  generateBlobURL() {
    const { doc, stream } = this;

    return new Promise((resolve) => {
      doc.flushPages();
      doc.end();

      stream.on('finish', () => {
        const url = stream.toBlobURL('application/pdf');
        resolve(url);
      });
    });
  }

  /**
  * Returns the position of the left edge of the page based on left margin.
  * @return {number} The position of the left edge of the page
  */
  pageLeft() {
    return this.margins.left;
  }

  /**
  * Returns the position of the right edge of the page based on the width and right margin.
  * @return {number} The position of the right edge of the page
  */
  pageRight() {
    return this.doc.page.width - this.margins.right;
  }

  /**
  * Sets the current print position to the top-left of the document + margins.
  */
  resetPosition() {
    this.doc.x = this.margins.left;
    this.doc.y = this.header == null ? this.margins.top : this.header.height;
  }

  _setupHeader() {
    const builder = this;
    const { header } = this;

    this.doc.on('pageAdded', () => {
      header.onPageAdded(builder);
    });
  }

  _setupFooter() {
    const builder = this;
    const { footer } = this;

    this.doc.on('pageAdded', () => {
      builder.currentPage += 1;
      footer.onPageAdded(builder);
    });
  }
}

export default PDFBuilder;