How to create beautiful PDFs with CSS within Sugar

Francesca Shiekh —  August 15, 2016 — Leave a comment

Here is a guest post from Sugar Developer Francesca Shiekh who works for our customer Wolfram Research. She also happens to be one of the most active developers in the Sugar Developer Community! In this post, Francesca and Wolfram Research Systems Administrator Trey Mitchell share how you can customize Sugar to use an alternative PDF generator for the creation of PDF documents in Sugar.

Creating better PDFs with CSS

Screen Shot 2016-08-05 at 2.41.58 PM

We wanted a custom Contracts module that allowed specific styling to be applied to all Contract PDFs.  We determined that we could use CSS to apply a consistent style to any and all PDF documents associated with this new custom Contracts module. But we could not achieve this with SugarPDF due to the complexity of our CSS. Also overriding sugarpdf.pfdmanager.php was not an option as it is an extension of TCPDF and TCPDF has limited CSS support.

It was therefore necessary for use to look at alternatives that could better handle CSS.  We found a promising open source tool called wkhtmltopdf that uses WebKit.  WebKit should be familiar because it is the same layout engine used in Safari and Google Chrome, and is therefore very accurate in rendering CSS.

We still wanted to leverage the PDF Manager module to create and maintain our PDFs. We planned to develop our Contract templates in HTML with our own custom CSS and then copy them into the HTML editor that was already available in PDF Manager.

Read more below to find out how we made it work.

Step 1: Create a custom PDF rowaction

A custom field type called wpdfaction provides the rowaction needed for the dropdown that contains all the active PDF Manager templates for the current module. This new rowaction is dynamically included in the buttons section of your module’s record view much like the out of the box pdfaction.

custom/modules/*/clients/base/views/record/record.php

              array (
                'type' => 'wpdfaction',
                'name' => 'download-pdf',
                'label' => 'LBL_PDF_VIEW',
                'action' => 'download',
                'acl_action' => 'view',
              ),

The field type wpdfaction is a slight modification of the clients/base/fields/pdfaction field where the template is unchanged and the JavaScript is modified to call a custom API that in turn generates the PDF to be downloaded.

custom/clients/base/fields/wpdfaction/wpdfaction.js


/**
* @extends View.Fields.Base.RowactionField
*/
({
  extendsFrom: 'RowactionField',
  events: {
    'click [data-action=link]': 'linkClicked',
    'click [data-action=download]': 'downloadClicked',
  },
  /**
   * PDF Template collection.
   *
   * @type {Data.BeanCollection}
   */

   templateCollection: null,

   /**
   * Visibility property for available template links.
   *
   * @property {Boolean}
   */

  fetchCalled: false,

  /**
   * {@inheritDoc}
   * Create PDF Template collection in order to get available template list.
   */

  initialize: function(options) {
    this._super('initialize', [options]);
    this.templateCollection = app.data.createBeanCollection('PdfManager');
    this._fetchTemplate();
  },

  /**
   * {@inheritDoc}
   * @private
   */

  _render: function() {
    this._super('_render');
  },

  /**
   * Define proper filter for PDF template list.
   * Fetch the collection to get available template list.
   * @private
   */

  _fetchTemplate: function() {
    this.fetchCalled = true;
    var collection = this.templateCollection;
    collection.filterDef = {'$and': [{
      'base_module': this.module
    }, {
      'published': 'yes'
    }]};
      collection.fetch();
  },

  /**
   * Build download link url.
   *
   * @param {String} templateId PDF Template id.
   * @return {string} Link url.
   * @private
   */

  _buildDownloadLink: function(templateId) {
      var url = app.api.buildURL(this.module+'/'+this.model.id+'/'+'generateWPDF/'+templateId);
      return url;
  },

  /**
   * Handle the button click event.
   * Stop event propagation in order to keep the dropdown box.
   *
   * @param {Event} evt Mouse event.
   */

  linkClicked: function(evt) {
    evt.preventDefault();
    evt.stopPropagation();
    if (this.templateCollection.dataFetched) {
      this.fetchCalled = !this.fetchCalled;
    } else {
      this._fetchTemplate();
    }
    this.render();
  },

  /**
   * Handles download pdf link.
   * @param {Event} evt The `click` event.
   */

  downloadClicked: function(evt) {
    var templateId = this.$(evt.currentTarget).data('id');
    this._triggerDownload(this._buildDownloadLink(templateId));
  },

  /**
   * do the actual downloading
   * @param {String} uri The location of the cached file.
   **/

  startDownload: function(uri) {
    app.api.fileDownload(uri, {
      error: function(e) {
        app.error.handleHttpError(e, {});
      }
    }, {iframe: this.$el});
  },

  /**
   * Download the file.
   *
   * @param {String} url The url for the file download API.
   * @protected
   */

  _triggerDownload: function(url) {
    var self = this;
    app.api.call('GET', url, null, {
      success: function(o){
        self.startDownload(o);
      },
      error: function (e){
        app.error.handleHttpError(e, {});
      },
    });
  },

  /**
   * {@inheritDoc}
   * Bind listener for template collection.
   */

  bindDataChange: function() {
    this.templateCollection.on('reset', this.render, this);
    this._super('bindDataChange');
  },

  /**
   * {@inheritDoc}
   * Dispose safe for templateCollection listeners.
   */

  unbindData: function() {
    this.templateCollection.off(null, null, this);
    this.templateCollection = null;
    this._super('unbindData');
  },

  /**
   * {@inheritDoc}
   * Check additional access for PdfManager Module.
   */

  hasAccess: function() {
    var pdfAccess = app.acl.hasAccess('view', 'PdfManager');
    return pdfAccess && this._super('hasAccess');
  }
})

Step 2: Create a custom API that generates the new PDFs

The custom API is what generates the new PDFs from HTML stored in the PDF Manager module.

You can put the following REST API implementation in custom/clients/base/api/generateWPDFApi.php for use in multiple modules or custom/modules/<module>/clients/base/api/generateWPDFApi.php for use in a specific module only.

We have to include SugarpdfHelper because it is required by PdfManagerHelper to parse the SugarBean fields needed for SugarSmarty to replace our field variables with data from the record.

The outputWPdf function can be changed to use your favorite PDF generator. We were able to try a few different implementations before we settled on the ease of use of wkhtmltopdf.

The final PDF is stored in Sugar’s cache directory and ultimately downloaded using app.api.fileDownload() in the client.

If testing in Google Chrome, you need to disable the PDF Viewer to ensure that it is downloaded correctly.  To do this go to chrome://plugins and find and disable “Chrome PDF Viewer” in the list.


<php class generateWPDFApi extends SugarApi {   public function registerApiRest() {     return array(       'generateWPDF' => array(
        'reqType' => 'GET',
        'path' => array('<module>','?','generateWPDF','?'),
        'pathVars' => array('module','record','','pdf_template_id'),
        'method' => 'generateWPDF',
        'shortHelp' => 'CustomWR: Generate PDF file from PdfManager template with specified ID',
        'longHelp' => '',
      ),
    );
  }   

  function generateWPDF($api, $args)
  {
    require_once 'include/Sugarpdf/SugarpdfHelper.php'; //needed by PdfManagerHelper
    require_once 'modules/PdfManager/PdfManagerHelper.php';
    global $sugar_config;
    $this->requireArgs($args,array('module','record','pdf_template_id'));
    $this->_initSmartyInstance();
    // settings for disable smarty php tags
    $this->ss->security_settings['PHP_TAGS'] = false;
    $this->ss->security = true;
    if (defined('SUGAR_SHADOW_PATH')) {
      $this->ss->secure_dir[] = SUGAR_SHADOW_PATH;
    }   

    $pdfTemplate = BeanFactory::retrieveBean('PdfManager' , $args['pdf_template_id']);
    $this->bean = BeanFactory::retrieveBean($args['module'],$args['record']);
    $this->pdfFilename = $pdfTemplate->name . '_' . $this->bean->name;
    $this->templateLocation = $this->buildTemplateFile($pdfTemplate);
    $fields = PdfManagerHelper::parseBeanFields($this->bean, true);
    //assign values to merge fields
    $this->ss->assign('fields', $fields);
    //write the pdf from html into a cached file for downloading
    return $this->outputWPdf();
  }

  private function outputWPdf()
  {
    //using wkthmltopdf as our converter of choice.

    global $sugar_config;
    $host = 'https://'.parse_url($sugar_config['site_url'], PHP_URL_HOST);
    if(!empty($this->templateLocation)){
      //get the HTML
      $html = $this->ss->fetch($this->templateLocation);

      //add the css to the HTML and any other headers you wish to add
      $css_url = '<YOUR CSS URL HERE>';
      $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"><head>			<link rel="stylesheet" type="text/css" href="'.$css_url.'"/></head>'.$html.'</html>';

      // Run wkhtmltopdf
      $descriptorspec = array(
        0 => array('pipe', 'r'), // stdin
        1 => array('pipe', 'w'), // stdout
        2 => array('pipe', 'w'), // stderr
      );

      $page_size = 'Letter';
      // print-media-type option is essential to proper pagination during conversion so page-break-xxxx css property is respected
      // -q = quiet
      // - -  allows for the pipes to be used
      $options =  " --print-media-type "; //gets proper pagination
      $options .= " -q "; //quiet
      $options .= " --footer-spacing 5 "; //space between footer and text
      $options .= " --header-spacing 5 "; //space between header and text
      $options .= " --page-size ". $page_size;

      $cmd = '/usr/local/bin/wkhtmltopdf ' . $options . ' - - ';
      $process = proc_open( $cmd  , $descriptorspec, $pipes);

      if(is_resource($process)){
        // Send the HTML on stdin
        fwrite($pipes[0], $html);
        fclose($pipes[0]);

        // Read the outputs
        $this->pdf = stream_get_contents($pipes[1]);

        // Any errors that may arise
        $errors = stream_get_contents($pipes[2]);

        // Close all pipes and close the process
        fclose($pipes[1]);
        $return_value = proc_close($process);

        // Check for errors and Output the results
        if ($errors) {
          throw new SugarApiException('PDF generation failed:' . $errors);
        }else {
          //stamp filename with user and date stamp
          $filenamestamp = '';
          if(isset($current_user)){
            $filenamestamp .= '_'.$current_user->user_name;
          }

          $filenamestamp .= '_'.time();
          //avoid special characters in the filename
          $cr = array(' ',"\r", "\n","/","-");
          $filename = str_replace($cr, '_', $this->pdfFilename. $filenamestamp.'.pdf');
          //create the file in sugar cache
          $cachefile = sugar_cached('tmp/').$filename;
          $fp = sugar_fopen($cachefile, 'w');
          fwrite($fp, $this->pdf);
          fclose($fp);
          return($cachefile);
        }
      }else{
        throw new SugarApiException('PDF generation failed: wkhtmlpdf resource error ');
      }
    }else{
      throw new SugarApiException('PDF generation failed: No Template Location ');
    }
  }
  private function buildTemplateFile($pdfTemplate, $previewMode = FALSE)
  {
    if (!empty($pdfTemplate)) {
      if ( ! file_exists(sugar_cached('modules/PdfManager/tpls')) ) {
        mkdir_recursive(sugar_cached('modules/PdfManager/tpls'));
      }
      $tpl_filename = sugar_cached('modules/PdfManager/tpls/' . $pdfTemplate->id . '.tpl');
      $pdfTemplate->body_html = from_html($pdfTemplate->body_html);
      sugar_file_put_contents($tpl_filename, $pdfTemplate->body_html);
      return $tpl_filename;
    }
    return '';
  }

  /**
   * Init the Sugar_Smarty object.
   */
  private function _initSmartyInstance()
  {
    if ( !($this->ss instanceof Sugar_Smarty) ) {
      $this->ss = new Sugar_Smarty();
      $this->ss->security = true;
      if (defined('SUGAR_SHADOW_PATH')) {
        $this->ss->secure_dir[] = SUGAR_SHADOW_PATH;
      }
      $this->ss->assign('MOD', $GLOBALS['mod_strings']);
      $this->ss->assign('APP', $GLOBALS['app_strings']);
    }
  }
}

Wkhtmltopdf installation and setup

As stated above, we needed an HTML to PDF solution that would allow us to compose documents using a CSS and would render them correctly in PDF and chose wkhtmltopdf for its precision and ease of use.

We provide here a walk through for a simple manual installation of wkhtmltopdf on CentOS. Steps on other Red Hat based distributions should be identical.
On Debian derivatives dependency package names may differ, and you will use apt-get instead of yum to install them.

You will need root access or sudo to do this.
In the following writing ,commands requiring root access will be preceded with an asterisk (*).

First you need to ensure you have the proper wkhtmltopdf dependencies:

*[root@sugarcrm-test ~]# yum -y install zlib fontconfig freetype libX11 libXext libXrender

Download the latest copy of wkhtmltopdf from the project’s download page.

We will be using the 64-bit version here:

[root@sugarcrm-test ~]# wget http://download.gna.org/wkhtmltopdf/0.12/0.12.3/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz

It’s also advisable to verify the integrity of the file you downloaded:

[root@sugarcrm-test ~]# wget http://download.gna.org/wkhtmltopdf/0.12/0.12.3/SHA256SUMS
[root@sugarcrm-test ~]# sha256sum -c SHA256SUMS

You will see a lot of errors because we didn’t download all the files listed in SHA256SUMS. What you are looking to verify is an OK next to the file you downloaded like this:

wkhtmltox-0.12.3_linux-generic-amd64.tar.xz: OK

Next we want to extract the archive and place the files in the proper locations.

This next command will do a few things
– Extract the archive’s contents into /usr/local
– Print all the files that were created.
– write a file called wkhtmltox-files with a list of files that were extracted.

This is useful if you wish to remove or otherwise cleanup the install later.

*[root@sugarcrm-test ~]# tar xvJf wkhtmltox-0.12.3_linux-generic-amd64.tar.xz --strip-components=1 -C /usr/local|grep -v \/$|sed -e 's/^wkhtmltox/\/usr\/local/'|tee wkhtmtox-files

All permissions should be correct out of the box but if you wish to verify you can run the following command and check against the output below.

[root@sugarcrm-test ~]# for file in `cat wkhtmtox-files`;do ls -l $file;done
-rwxr-xr-x 1 root root 45072792 Jan 20 02:32 /usr/local/lib/libwkhtmltox.so.0.12.3
lrwxrwxrwx 1 root root 22 Jul 15 15:56 /usr/local/lib/libwkhtmltox.so.0 -&amp;amp;gt; libwkhtmltox.so.0.12.3
lrwxrwxrwx 1 root root 22 Jul 15 15:56 /usr/local/lib/libwkhtmltox.so.0.12 -&amp;amp;gt; libwkhtmltox.so.0.12.3
lrwxrwxrwx 1 root root 22 Jul 15 15:56 /usr/local/lib/libwkhtmltox.so -&amp;amp;gt; libwkhtmltox.so.0.12.3
-rw-r--r-- 1 root root 2546 Jan 20 02:34 /usr/local/share/man/man1/wkhtmltoimage.1.gz
-rw-r--r-- 1 root root 6774 Jan 20 02:33 /usr/local/share/man/man1/wkhtmltopdf.1.gz
-rw-r--r-- 1 root root 3980 Jul  7  2015 /usr/local/include/wkhtmltox/pdf.h
-rw-r--r-- 1 root root 911 Jul  7  2015 /usr/local/include/wkhtmltox/dllend.inc
-rw-r--r-- 1 root root 1475 Jul  7  2015 /usr/local/include/wkhtmltox/dllbegin.inc
-rw-r--r-- 1 root root 3407 Jul  7  2015 /usr/local/include/wkhtmltox/image.h
-rwxr-xr-x 1 root root 39666344 Jan 20 02:34 /usr/local/bin/wkhtmltoimage
-rwxr-xr-x 1 root root 39745960 Jan 20 02:33 /usr/local/bin/wkhtmltopdf

That should be it. You can verify the binary is working from command line:

[root@sugarcrm-test ~]# wkhtmltopdf http://google.com google.pdf

If you ever wish to remove the application you can do so using the file we generated upon extraction:

*[root@sugarcrm-test ~]# for file in `cat wkhtmtox-files`;do rm -I $file;done

There will be a stray empty directory to remove as well:

*[root@sugarcrm-test ~]# rmdir /usr/local/include/wkhtmltox/

Francesca Shiekh

Posts

Business Systems Software Engineer at Wolfram Research

No Comments

Be the first to start the conversation!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s