Tuesday, April 5, 2016

Creating PDF using Pdfmake - Advanced

Styling

pdfmake makes it possible to style any paragraph or its part:
var docDefinition = {
  content: [
    // if you don't need styles, you can use a simple string to define a paragraph
    'This is a standard paragraph, using default style',

    // using a { text: '...' } object lets you set styling properties
    { text: 'This paragraph will have a bigger font', fontSize: 15 },

    // if you set the value of text to an array instead of a string, you'll be able
    // to style any part individually
    {
      text: [
        'This paragraph is defined as an array of elements to make it possible to ',
        { text: 'restyle part of it and make it bigger ', fontSize: 15 },
        'than the rest.'
      ]
    }
  ]
};

Style dictionary

It's also possible to define a dictionary of reusable styles:
var docDefinition = {
  content: [
    { text: 'This is a header', style: 'header' },
    'No styling here, this is a standard paragraph',
    { text: 'Report No. 001', style: 'anotherstyle' },    { text: 'Created Date', style: [ 'header', 'anotherstyle' ] }
    { text: 'This is report body, style: 'textstyle' }  ],

  styles: {
    header: {
       fontSize: 22,
       bold: true,
       alignment: "center",
    },
    anotherstyle: {
       fontSize: 14,
       bold:true,
       alignment: 'right',
   },
   textstyle: {
       fontSize: 14,
       alignment: 'justify',
   }
};

Columns

By default paragraphs are rendered as a vertical stack of elements (one below another). It is possible however to divide available space into columns. Note Column content is not limited to a simple text. It can actually contain any valid pdfmake element.
var docDefinition = {
  content: [
    'This paragraph fills full width, as there are no columns. Next paragraph however consists of three columns',
    {
      columns: [
        {
          // auto-sized columns have their widths based on their content
          width: 'auto',
          text: 'First column'
        },
        {
          // star-sized columns fill the remaining space
          // if there's more than one star-column, available width is divided equally
          width: '*',
          text: 'Second column'
        },
        {
          // fixed width
          width: 100,
          text: 'Third column'
        },
        {
          // % width
          width: '20%',
          text: 'Fourth column'
        }
      ],
      // optional space between columns
      columnGap: 10
    },
    'This paragraph goes below all columns and has full width'
  ]
};

Tables

Conceptually tables are similar to columns. They can however have headers, borders and cells spanning over multiple columns/rows.
var docDefinition = {
    content: [
        {
            table: {
                // headers are automatically repeated if the table spans over multiple pages
                // you can declare how many rows should be treated as headers
                headerRows: 1,
                widths: [ '*', 'auto', 100, '*' ],
                body: [
                    [ 'First', 'Second', 'Third', 'The last one' ],
                    [ 'Value 1', 'Value 2', 'Value 3', 'Value 4' ],
                    [ { text: 'Bold value', bold: true }, 'Val 2', 'Val 3', 'Val 4' ]
                ]
            }
        }
    ]
};  

Styling tables 

You can provide a custom style for the table. Currently it supports: 
  • line widths 
  • line colors 
  • cell paddings
var styles = {
    header: {
        fontSize: 18,
        bold: true,
        margin: [0, 0, 0, 10]
    },
    subheader: {
        fontSize: 16,
        bold: true,
        margin: [0, 10, 0, 5]
    },
    tableExample: {
        margin: [0, 5, 0, 15]
    },
    tableHeader: {
        bold: true,
        fontSize: 13,
        color: 'black'
    }
};
    
var content = [
    { text: 'Tables', style: 'header' },
    { text: 'A simple table', style: 'subheader' },
    'The following table has nothing more than a body array',
    {
        style: 'tableExample',
        table: {
            body: [
                ['Column 1', 'Column 2', 'Column 3'],
                ['One value goes here', 'Another one here', 'OK?']
            ]
        }
    },
    { text: 'noBorders:', fontSize: 14, bold: true, pageBreak: 'before', margin: [0, 0, 0, 8] },
    {
        style: 'tableExample',
        table: {
            headerRows: 1,
            body: [
             [{ text: 'Header 1', style: 'tableHeader' }, { text: 'Header 2', style: 'tableHeader'}, { text: 'Header 3', 
style: 'tableHeader' }],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
            ]
        },
        layout: 'noBorders'
    },
    { text: 'headerLineOnly:', fontSize: 14, bold: true, margin: [0, 20, 0, 8] },
    {
        style: 'tableExample',
        table: {
            headerRows: 1,
            body: [
             [{ text: 'Header 1', style: 'tableHeader' }, { text: 'Header 2', style: 'tableHeader'}, { text: 'Header 3', 
style: 'tableHeader' }],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
            ]
        },
        layout: 'headerLineOnly'
    },
    { text: 'lightHorizontalLines:', fontSize: 14, bold: true, margin: [0, 20, 0, 8] },
    {
        style: 'tableExample',
        table: {
            headerRows: 1,
            body: [
             [{ text: 'Header 1', style: 'tableHeader' }, { text: 'Header 2', style: 'tableHeader'}, { text: 'Header 3', 

style: 'tableHeader' }],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
            ]
        },
        layout: 'lightHorizontalLines'
    },
    
    { text: 'but you can provide a custom styler as well', margin: [0, 20, 0, 8] },
    {
        style: 'tableExample',
        table: {
            headerRows: 1,
            body: [
             [{ text: 'Header 1', style: 'tableHeader' }, { text: 'Header 2', style: 'tableHeader'}, { text: 'Header 3', 
style: 'tableHeader' }],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
             [ 'Sample value 1', 'Sample value 2', 'Sample value 3' ],
            ]
        },
        layout: {
            hLineWidth: function(i, node) {
             return (i === 0 || i === node.table.body.length) ? 2 : 1;
            },
            vLineWidth: function(i, node) {
             return (i === 0 || i === node.table.widths.length) ? 2 : 1;
            },
            hLineColor: function(i, node) {
             return (i === 0 || i === node.table.body.length) ? 'black' : 'gray';
            },
            vLineColor: function(i, node) {
             return (i === 0 || i === node.table.widths.length) ? 'black' : 'gray';
            },
            // paddingLeft: function(i, node) { return 4; },
            // paddingRight: function(i, node) { return 4; },
            // paddingTop: function(i, node) { return 2; },
            // paddingBottom: function(i, node) { return 2; }
        }
    }
];
    
var docDefinition = {
    // a string or { width: number, height: number }
    pageSize: 'A4',
    // by default we use portrait, you can change it to landscape here
    pageOrientation: 'portrait', //can change to landscape
    // [left, top, right, bottom] or [horizontal, vertical] or just a number for equal margins
    pageMargins: [ 40, 60, 40, 60 ], 
    styles: styles,
    content: content,
    defaultStyle: {
        // alignment: 'justify'
    }    
};

Lists

pdfMake supports both numbered and bulleted lists:
var docDefinition = {
  content: [
    'Bulleted list example:',
    {
      // to treat a paragraph as a bulleted list, set an array of items under the ul key
      ul: [
        'Item 1',
        'Item 2',
        'Item 3',
        { text: 'Item 4', bold: true },
      ]
    },

    'Numbered list example:',
    {
      // for numbered lists set the ol key
      ol: [
        'Item 1',
        'Item 2',
        'Item 3'
      ]
    }
  ]
};

Headers and footers

Page headers and footers in pdfmake can be: static or dynamic.They use the same syntax:
var docDefinition = {
  header: 'simple text',

  footer: {
    columns: [
      'Left part',
      { text: 'Right part', alignment: 'right' }
    ]
  },

  content: (...)
};
For dynamically generated content (including page numbers and page count) you can pass a function to the header or footer:
var docDefinition = {
  footer: function(currentPage, pageCount) { return currentPage.toString() + ' of ' + pageCount; },
  header: function(currentPage, pageCount) {
    // you can apply any logic and return any valid pdfmake element

    return { text: 'simple text', alignment: (currentPage % 2) ? 'left' : 'right' };
  },
  (...)
};

Background-layer

The background-layer will be added on every page.
var docDefinition = {
  background: 'simple text',

  content: (...)
};
It may contain any other object as well (images, tables, ...) or be dynamically generated:
var docDefinition = {
  background: function(currentPage) {
    return 'simple text on page ' + currentPage
  },

  content: (...)
};

Margins

Any element in pdfMake can have a margin:
(...)
// margin: [left, top, right, bottom]
{ text: 'sample', margin: [ 5, 2, 10, 20 ] },

// margin: [horizontal, vertical]
{ text: 'another text', margin: [5, 2] },

// margin: equalLeftTopRightBottom
{ text: 'last one', margin: 5 }
(...)

Stack of paragraphs

You could have figured out by now (from the examples), that if you set the content key to an array, the document becomes a stack of paragraphs. You'll quite often reuse this structure in a nested element, like in the following example:
var docDefinition = {
  content: [
    'paragraph 1',
    'paragraph 2',
    {
      columns: [
        'first column is a simple text',
        [
          // second column consists of paragraphs
          'paragraph A',
          'paragraph B',
          'these paragraphs will be rendered one below another inside the column'
        ]
      ]
    }
  ]
};
The problem with an array is that you cannot add styling properties to it (to change font size for example). The good news is - array is just a shortcut in pdfMake for { stack: [] }, so if you want to restyle the whole stack, you can do it using the expanded definition:
var docDefinition = {
  content: [
    'paragraph 1',
    'paragraph 2',
    {
      columns: [
        'first column is a simple text',
        {
          stack: [
            // second column consists of paragraphs
            'paragraph A',
            'paragraph B',
            'these paragraphs will be rendered one below another inside the column'
          ],
          fontSize: 15
        }
      ]
    }
  ]
};

Images

This is simple. Just use the { image: '...' } node type. JPEG and PNG formats are supported.
var docDefinition = {
  content: [
    {
      // you'll most often use dataURI images on the browser side
      // if no width/height/fit is provided, the original size will be used
      image: 'data:image/jpeg;base64,...encodedContent...'
    },
    {
      // if you specify width, image will scale proportionally
      image: 'data:image/jpeg;base64,...encodedContent...',
      width: 150
    },
    {
      // if you specify both width and height - image will be stretched
      image: 'data:image/jpeg;base64,...encodedContent...',
      width: 150,
      height: 150
    },
    {
      // you can also fit the image inside a rectangle
      image: 'data:image/jpeg;base64,...encodedContent...',
      fit: [100, 100]
    },
    {
      // if you reuse the same image in multiple nodes,
      // you should put it to to images dictionary and reference it by name
      image: 'mySuperImage'
    },
    {
      // under NodeJS (or in case you use virtual file system provided by pdfmake)
      // you can also pass file names here
      image: 'myImageDictionary/image1.jpg'
    }
  ],

  images: {
    mySuperImage: 'data:image/jpeg;base64,...content...'
  }
};

Page dimensions, orientation and margins

var docDefinition = {
  // a string or { width: number, height: number }
  pageSize: 'A5',

  // by default we use portrait, you can change it to landscape if you wish
  pageOrientation: 'landscape',

  // [left, top, right, bottom] or [horizontal, vertical] or just a number for equal margins
  pageMargins: [ 40, 60, 40, 60 ],
};
If you set pageSize to a string, you can use one of the following values:
  • '4A0', '2A0', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10',
  • 'B0', 'B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B9', 'B10',
  • 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9', 'C10',
  • 'RA0', 'RA1', 'RA2', 'RA3', 'RA4',
  • 'SRA0', 'SRA1', 'SRA2', 'SRA3', 'SRA4',
  • 'EXECUTIVE', 'FOLIO', 'LEGAL', 'LETTER', 'TABLOID'
To change page orientation within a document, add a page break with the new page orientation.
{
  pageOrientation: 'portrait',
  content: [
    {text: 'Text on Portrait'},
    {text: 'Text on Landscape', pageOrientation: 'landscape', pageBreak: 'before'},
    {text: 'Text on Landscape 2', pageOrientation: 'portrait', pageBreak: 'after'},
    {text: 'Text on Portrait 2'},
  ]
}

Document Metadata

PDF documents can have various metadata associated with them, such as the title, or author of the document. You can add that information by adding it to the document definition
var docDefinition = {
  info: {
    title: 'awesome Document',
    author: 'john doe',
    subject: 'subject of document',
    keywords: 'keywords for document',
  },
  content:  'This is an sample PDF printed with pdfMake' 
}

Source: 

Creating PDF using Pdfmake - Introduction

Pdfmake follows a declarative approach. You don't need to calculate positions manually like: writeText(text, x, y), moveDown etc..., as you other libraries do.

You just need to create document-definition-object like this:
var docDefinition = { content: 'This is an sample PDF printed with pdfMake' };

You can add more stuffs to the object with multi-level tables, images, lists, paragraphs, margins, styles etc.  After you made the object, you can create the pdf and download it.

// open the PDF in a new window
pdfMake.createPdf(docDefinition).open();
// print the PDF (not working in this version, will be added back in a couple of days)
// pdfMake.createPdf(docDefinition).print();
// download the PDF
pdfMake.createPdf(docDefinition).download();

Pdfmake has following features:
  • line-wrapping,
  • text-alignments (left, right, centered, justified),
  • numbered and bulleted lists,
  • tables and columns
    • auto/fixed/star-sized widths,
    • col-spans and row-spans,
    • headers automatically repeated in case of a page-break,
  • images and vector graphics,
  • convenient styling and style inheritance,
  • page headers and footers:
    • static or dynamic content,
    • access to current page number and page count,
  • background-layer
  • page dimensions and orientations,
  • margins,
  • custom page breaks,
  • font embedding,
  • support for complex, multi-level (nested) structures,
  • helper methods for opening/printing/downloading the generated PDF.
  • setting of PDF metadata (e.g. author, subject)
Source: 

Generating PDF using jsPDF - a whole example

Pdfmake vs jsPDF
There are 2 popular javascript tools to generate PDF on the client side. One is jsPDF, the other is pdfmake. The pdfmake is a layout engine on top of pdfkit, which supports a nice declarative json document description format. Pdfmake has better documentation, and jsPDF only have minimal document. jsPDF doesn't trim each line like Pdfmake does. Pdfmake supports new pages, which jsPDF doesn't. However pdfmake is still under beta version currently. The jsPDF has very simple documentation so be patient to apply it on complex project.

 jsPDF prints using:
    var doc = new jsPDF();
    doc.setFontSize(40);
    doc.text(35, 25, "Paranyan loves jsPDF");
    doc.addImage(imgData, 'JPEG', 15, 40, 180, 160);

Pdfmake on the other hand:
    var dd = {
        content: [
            'First paragraph',
            'Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines'
        ]
    }

Note datatable javascript plugin applied pdfmake to export table data to pdf. So If your application used datatable pdf export feature, you don't need to add another javascript library.

The Flyer Builder Example
The flyer builder is a user interface applying jsPDF to allow the user to insert some basic data (a title, an abstract and a price). Optionally, an image can be added, otherwise a grey-boxed Special Offer title is displayed. This example is from this link and could be a very good start for learning how to generate PDF using jsPDF. 
There is one demo for this interface. Note the demo used iFrame to hold the interface and it only worked for Firefox.  

The PDF generation first creates a new instance of jsPDF object with these options: portrait orientation (p), millimeters units (mm),‘A4’ format.
var pdf = new jsPDF('p', 'mm', 'a4');
Images can be added using pdf.addImage  function. Note every object placed inside the pdf should be exactly positioned. Thus the coordinates of each object based on the unites defined above should be declared correctly.
// pdf.addImage(base64_source, image format, X, Y, width, height)
pdf.addImage(agency_logo.src, 'PNG', logo_sizes.centered_x, _y, logo_sizes.w, logo_sizes.h);
Also those images must be Base64 encoded: the agency logo is embedded in the script in this format, while the image loaded by the user is encoded using the readAsDataURL method in the $('#flyer-image').change listener.

The title is added using the textAlign function. Note that this function is not part of the jsPDF core, but, as suggested by the author in his examples, the library can be easily expanded using its API. You can find the textAlign() function at the top of flyer builder script:
pdf.textAlign(flyer_title, {align: "center"}, 0, _y);
This function calculates the X coordinate of the the text string to make it centered, and then calls the native text() method:
pdf.text(text string, X, Y);
To change text properties, you can use following methods:
setFontSize()setFont()setTextColor()setFontType().
To set a 20pt Times Bold red string, for example, you need to type this:
pdf.setFontSize(20);
pdf.setFont("times");
pdf.setFontType("bold");
pdf.setTextColor(255, 0, 0);
pdf.text(10,10, 'This is a 20pt Times Bold red string');
The Special offer grey box and the price circle use two similar methods: roundedRect() and circle(). Both of them require top-left coordinates, size values (the width and height in the first case and the radius in the second one):
pdf.roundedRect( X, Y, width, height, radius along X axis, radius along Y axis, style);
pdf.circle( X, Y, radius, style);
The style parameters refers to the fill and stroke properties of the object. Valid styles are: S [default] for stroke, F for fill, and DF (or FD) for fill and stroke.

Fill and stroke properties must be set in advance using setFillColor and setDrawColor, which require a RGB value and setLineWidth that requires the line width value in the unit declared at inception of PDF document.

The full source code:
(function (API) {
    API.textAlign = function (txt, options, x, y) {
        options = options || {};
        // Use the options align property to specify desired text alignment
        // Param x will be ignored if desired text alignment is 'center'.
        // Usage of options can easily extend the function to apply different text
        // styles and sizes

        // Get current font size
        var fontSize = this.internal.getFontSize();

        // Get page width
        var pageWidth = this.internal.pageSize.width;

        // Get the actual text's width
        // You multiply the unit width of your string by your font size and divide
        // by the internal scale factor. The division is necessary
        // for the case where you use units other than 'pt' in the constructor
        // of jsPDF.
        var txtWidth = this.getStringUnitWidth(txt) * fontSize / this.internal.scaleFactor;

        if (options.align === "center") {
            // Calculate text's x coordinate
            x = (pageWidth - txtWidth) / 2;
        }
        else if (options.align === "centerAtX") { // center on X value
            x = x - (txtWidth / 2);
        }
        else if (options.align === "right") {
            x = x - txtWidth;
        }

        // Draw text at x,y
        this.text(txt, x, y);
    };
    /*
        API.textWidth = function(txt) {
         var fontSize = this.internal.getFontSize();
            return this.getStringUnitWidth(txt)*fontSize / this.internal.scaleFactor;
        };
    */
    API.getLineHeight = function (txt) {
        return this.internal.getLineHeight();
    };
})(jsPDF.API);

(function () {
    "use strict";
    /*
     Refs:
     http://mrrio.github.io/jsPDF/
     https://github.com/MrRio/jsPDF
     https://mrrio.github.io/jsPDF/doc/symbols/jsPDF.html
    */

    // some variables
    var agency_logo = {
      src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAEdCAMAAADD1fMAAAAAGXRFWHRTb2Z0d2' + 
           'FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABhQTFRFAHC6RpjOtdbr1uj0f7jd7/b7////IYLCvV4sVgAAIv' +
           '1JREFUeNrsnYvao6oOQE0I5v3feFqvqIQEqv61k5z9nT17tIrAIhcCdL1LiwTGR5UXAb3RWqTzKmgS6rqOwkMKG4m7Drz'
        w: 800,
        h: 285
    },

    agency_name = 'Travel & Holidays',
    agency_site_url = 'www.travelandholidays.com',
    footer = agency_name + ' - ' + agency_site_url,

    page_size = 'a4',
    page_width = 210, // mm
    page_margin = 10, // mm
    content_width = page_width - (page_margin * 2), // available width for the content

    _y, _x, // drawing coord
    color_array, _string, lineHeight, y_correction, // some variables
    vspace = 10 // standard vertical space between elements
    ;

    // some variables
    var can_display_preview = true, // if true a preview of the PDF can be displayed in the iframe,
        // this value is set to false if the browser can't display the preview
        preview_container = $('#pdf_preview'),
        update_preview_button = $('#flyer_preview_btn'),
        download_button = $('#flyer_download_btn');

    // preview can be displayed?
    if (navigator.msSaveBlob) { // older IE
        update_preview_button.prop('disabled', true);
        can_display_preview = false;
        preview_container.replaceWith(
          '<div class="no_iframe">' +
          '<div>' +
          "The preview can't be displayed" +
          '</div>' +
          '</div>'
        );
    }

    // utilities
    var hex2rgb = function (hex_string) {
        if (/^#/.test(hex_string)) {
            hex_string = hex_string.substr(1);
        }
        if (hex_string.length === 3) {
            hex_string = hex_string.replace(/\w/, function (match) {
                return String(match) + String(match);
            });
        }
        return {
            red: parseInt(hex_string.substr(0, 2), 16),
            green: parseInt(hex_string.substr(2, 2), 16),
            blue: parseInt(hex_string.substr(4, 2), 16)
        };
    }
    px2mm = function (pixel) {
        // px to inches
        var inches = pixel / 72;
        return inches * 25.4;
    },

    mm2px = function (millimeters) {
        // mm to inches
        var inches = millimeters / 25.4;
        return inches * 72;
    },

    // function to calculate and check img sizes
    imgSizes = function (img_w, img_h, img_mm_w) {
        /* img_w and img_h represent the original image size, in pixel
           img_mm_w is the desidered rendered image size, in millimeters */

        if (img_mm_w > content_width) { // this should be never used...
            img_mm_w = content_width;
        }

        if (mm2px(img_mm_w) > img_w) {
            throw 'The `img_mm_w` parameter is too big';
        }

        var img_mm_h = Math.round((px2mm(img_h) * img_mm_w) / px2mm(img_w));

        return {
            w: img_mm_w,
            h: img_mm_h,
            centered_x: (page_width - img_mm_w) / 2
        };
    };

    try {
        // image reading
        // More info at https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL
        // for more examples about file api
        // take a look at https://scotch.io/tutorials/use-the-html5-file-api-to-work-with-files-locally-in-the-browser
        var flyer_img = $('#flyer-image'),
            img_data = null;

        $('#flyer-image').change(function () {
            // temporary disabling buttons while parsing image
            update_preview_button.prop('disabled', true);
            download_button.prop('disabled', true);
            /*
             getting the file
             flyer_img[0] : transforms the jQuery reference to a DOM object
             files[0]     : refers to the file the the user has chosen
             use `console.log(user_file);` to show some info about the file
            */
            var user_file = flyer_img[0].files[0];
            img_data = {
                type: user_file.type === 'image/jpeg' ? 'JPEG' : 'PNG' // maybe you should add some controls to prevent loading of other file types
            };

            var reader = new FileReader();
            reader.onload = function (event) {
                img_data.src = event.target.result;
                // we need this to get img dimensions
                var user_img = new Image();
                user_img.onload = function () {
                    img_data.w = user_img.width;
                    img_data.h = user_img.height;

                    // restoring buttons
                    download_button.prop('disabled', false);
                    if (can_display_preview) {
                        update_preview_button.prop('disabled', false);
                    }
                };
                user_img.src = img_data.src;
            };
            //when the file is read it triggers the onload event above.
            reader.readAsDataURL(user_file);
        });

        //!pdf builder
        var createPDF = function (update_preview) {
            /*
             update_preview:
              ==> true: shows pdf online
              ==> false: downloads the pdf
             */
            _y = page_margin; // vertical starting point

            // form data
            var flyer_title = $('#flyer-title').val(),
                flyer_title_size = $('#flyer-title-size').val(),
                flyer_title_color = $('#flyer-title-color').val(),
                flyer_description = $('#flyer-description').val(),
                flyer_price = $('#flyer-price').val(),
                flyer_price_currency = $('#flyer-price-currency').val(),
                flyer_price_color = $('#flyer-price-color').val();

            var pdf = new jsPDF('p', 'mm', page_size),
                text_baseline,
                // some colors:
                light_grey = 237,
                grey = 128,
                black = 0,
                white = 255;

            // Optional - set properties of the document
            pdf.setProperties({
                title: flyer_title,
                subject: footer,
                author: 'me',
                creator: 'Flyer Builder & jsPDF'
            });

            // !logo
            var logo_sizes = imgSizes(agency_logo.w, agency_logo.h, 60);
            pdf.addImage(agency_logo.src, 'PNG', logo_sizes.centered_x, _y, logo_sizes.w, logo_sizes.h);

            // fonts initializing
            pdf.setFont("helvetica");
            pdf.setFontType("bold");

            // !main title
            color_array = hex2rgb(flyer_title_color);
            pdf.setTextColor(color_array.red, color_array.green, color_array.blue);

            pdf.setFontSize(flyer_title_size);

            lineHeight = px2mm(pdf.getLineHeight(flyer_title));

            _y += (logo_sizes.h + vspace + lineHeight);

            pdf.textAlign(flyer_title, {
                align: "center"
            }, 0, _y);

            _y += vspace;

            // !user image
            if (img_data) {
                var img_sizes = imgSizes(img_data.w, img_data.h, content_width);
                pdf.addImage(img_data.src, img_data.type, img_sizes.centered_x, _y, img_sizes.w, img_sizes.h);
                _y += img_sizes.h;

            }
            else {
                // if we haven't an image, a grey box with a text is added
                var box_height = 80;

                pdf.setFillColor(light_grey);
                pdf.roundedRect(page_margin, _y, content_width, box_height, 5, 5, 'F');
                pdf.setFontSize(60);
                pdf.setTextColor(white);
                _string = 'SPECIAL OFFER';
                lineHeight = px2mm(pdf.getLineHeight(_string));

                // y_correction: value to be added to y coord of the grey box to have text vertically centered
                // it is empirically calculated adding 1/3 of text line height to half box height
                y_correction = box_height / 2 + lineHeight / 3;

                pdf.textAlign(_string, {
                    align: "center"
                }, 0, _y + y_correction);
                _y += box_height;
            }

            // !price
            // first: creating a circle that overlaps the bottom side of the image
            var circle_radius = 30;
            color_array = hex2rgb(flyer_price_color);
            pdf.setFillColor(color_array.red, color_array.green, color_array.blue);

            // _x and _y refer to center of the circle
            _x = content_width - circle_radius; // circle ends at `page_margin` millimeters from the image right side

            pdf.circle(_x, _y, circle_radius, 'F'); // circle overlaps image for 1/2 of its height
            pdf.setFontSize(60);
            pdf.setFont("times");
            pdf.setFontType("bold");

            _string = flyer_price_currency + parseInt(flyer_price); // decimals are removed
            lineHeight = px2mm(pdf.getLineHeight(_string));
            y_correction = lineHeight / 3;

            pdf.setTextColor(white);
            pdf.textAlign(_string, {
                align: "centerAtX"
            }, _x, _y + y_correction);

            // !description
            if (flyer_description) {
                pdf.setFontSize(20);
                pdf.setFont("helvetica");
                pdf.setFontType("italic");
                pdf.setTextColor(grey);

                var lineWidth = content_width - (circle_radius * 2) - (page_margin * 2);
                _y += page_margin;

                var line_height = 12; // mm

                var description_lines = pdf.splitTextToSize(flyer_description, lineWidth);
                //pdf.text(page_margin, _y, description_lines); // doesn't allows to change line spacing

                for (var i = 0; i < description_lines.length; i++) {
                    pdf.text(page_margin, _y, description_lines[i]);
                    _y += line_height;
                }
            }

            // !footer
            _y = 287;
            pdf.setFontSize(9);
            pdf.setFontType("normal");
            pdf.setTextColor(black);
            pdf.textAlign(footer, {
                align: "center"
            }, 0, _y);

            // ****************************
            // output
            if (update_preview) {
                preview_container.attr('src', pdf.output('datauristring'));
            }
            else {
                pdf.save('flyer ' + flyer_title + '.pdf');
            }
        }; // end createPDF

        // !buttons
        update_preview_button.click(function () {
            createPDF(true);
        });

        $('#flyer_download_btn').click(function () {
            createPDF(false);
        });
    }
    catch (e) {
        console.log(e);
    }
})();
You can check the demo for whole html, css, and javascript codes.

Source: