You are here

Exporting Highcharts JS to PDF & DOCX with TCPDF and PHPWord


For integration into Drupal I tried to export Highcharts JS graphics to PDF and DOCX.
The actual integration into Drupal was done by a colleague, so this is once again a simple proof of concept.

Technologies used are: Highcharts JSTCPDFPHPWordBatik rasterizer

The Highcharts JS site provided already some decent examples I could start from.

The idea is basically this: I have a XML with a predefined structure which will have all text and numbers in there (i only needed pie charts btw), this XML is used to render the page (also the Highcharts). When a user clicks either the pdf or docx button, the svg is pulled from the Highcharts object and send to a executable (Batik) on the (php) server which converts it into a image. These images are placed in a pdf or docx with their according title(s) and text(s).

Example XML:

<?xml version="1.0" encoding="UTF-8"?>
  		<name>Status projecten</name>
        <categorie><a href="">OK</a></categorie>
        <categorie>met vertraging</categorie>
        <categorie>niet OK</categorie>
  	  chart title
  	  chart text

The first part of the XML element "chart" is more or less what the examples from Highcharts provided, I added the URL element so a click through on the corresponding pie could be implemented.

The last part with "title" and "text", well, are the title and text per chart.

This HTML/JS is used for the page rendering:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "">
<html xmlns="">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />

<title>jQuery Highcharts</title>

<link href="" rel="stylesheet" type="text/css">

<script src="" type="text/javascript"></script>

<script src="" type="text/javascript"></script>
<script src="" type="text/javascript"></script>
<script src="" type="text/javascript"></script>
var charts;

$(document).ready(function() {
    type: 'GET',
    url: 'data.xml',
    dataType : 'xml',
    success: function(data) {
      charts=new Array();
      // Iterate over the lines and add categories or series
      $(data).find('chart').each(function(id) {
        var chart, title, subtitle, containerChart, renderTo;
        var options = {
          chart: {renderTo: ''},
          title: null,
          subtitle: null,
          tooltip: {
            formatter: function() {
              return '<b>' + + '</b>: ' + this.percentage + ' %';
          plotOptions: {
            pie: {
              allowPointSelect: true,
              cursor: 'pointer',
              dataLabels: {
                enabled: true,
                formatter: function() {
                  return '<b>' + + '</b>: ' + this.percentage + ' %';
              showInLegend: true,
              events: {
                click: function(e) {
          navigation: {
            buttonOptions: true
          series: [],
          navigation: {
            buttonOptions: {
              enabled: false
          exporting: {
            type: 'image/jpeg',
          credits: {
            enabled: false
        containerChart = 'containerChart-'+id;
        renderTo = 'chart-'+id;
        $('#container').append('<div class="chart" id="' + containerChart + '"></div>');
          .append('<h2 class="title">'+$(this).find('title').text()+'</h2>')
          .append('<div id="' + renderTo + '"></div>')
          .append('<div class="text">'+$(this).find('text').text()+'</div');
        options.chart.renderTo = renderTo;
        $(this).find('series').each(function() {
          var serie = new Object();
          serie.type = $(this).find('type').text();
 = $(this).find('name').text();         
 = new Array();
          $(this).find('data').each(function() {
            var categorie = $(this).find('categorie').text();
            var point = parseFloat($(this).find('point').text());
            var url=$(this).find('url').text();
            var item = {
              name: categorie,
              y: point,
        // Create the chart
        chart = new Highcharts.Chart(options);
        /*ADD CHART DATA TO ARRAY, getSVG for exporting*/

function getDocument(type){
  var title='title';
  var header='header';
  var footer='footer';
  var json={
    'data': $.toJSON(charts)


function download(url, data, method){
  //url and data options required
  if( url && data ){ 
    //data can be string of parameters or array/object
    data = typeof data == 'string' ? data : jQuery.param(data);
    //split params into form inputs
    var inputs = '';
    jQuery.each(data.split('&'), function(){ 
      var pair = this.split('=');
      inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />'; 
    //send request
    jQuery('<form action="" method="'+ (method||'post') +'">'+inputs+'</form>')

  <button id="pdf">PDF</button>
  <button id="doc">DOC</button>
  <div id="container" style=""></div>

When the page is constructed, the title, text and svg data are pushed into an array (line 126)

When the user clicks one of the buttons, a document will be generated with all the charts (getDocument, line 135): a json object is passed with the document title, header, footer and the json data per chart (and the type = pdf/doc), this is send to docgen.php, based on the exporting-server Highcharts example.

require_once 'PHPWord/PHPWord.php';

define ('BATIK_PATH', 'exporting-server/batik-rasterizer.jar');
define ('TEMP_PATH', 'exporting-server/temp/');

if(isset($_POST['type']) && isset($_POST['title']) && isset($_POST['header']) && isset($_POST['footer']) && isset($_POST['data'])){
  for($i=0; $i < count($data); $i++){
  foreach($items as $item){
  print 'ERROR docgen.php: no post data!';

function svgToJpg($item){
  ini_set('magic_quotes_gpc', 'off');
  $filename =  isset($_POST['filename']) ? $_POST['filename'] : 'chart';
  $width =  isset($_POST['width']) ? $_POST['width'] : 300;
  if (get_magic_quotes_gpc()) {
    $svg = stripslashes($svg);  
  $tempName = md5(rand());
  $typeString = '-m image/jpeg';
  $ext = '.jpg';
  $outfile = TEMP_PATH.$tempName.$ext;
  if (isset($typeString)) {
    // size
   $width = "-w $width";
    // generate the temporary file
    if (!file_put_contents(TEMP_PATH.$tempName.".svg", $svg)) { 
      die("Couldn't create temporary file. Check that the directory permissions for
        the /temp directory are set to 777.");
    // do the conversion
    shell_exec("chmod 777 ".TEMP_PATH.$tempName.".svg");
    $output = shell_exec("java -jar ". BATIK_PATH ." $typeString -d $outfile $width ".TEMP_PATH.$tempName.".svg");
    // catch error
    if (!is_file($outfile) || filesize($outfile) < 10) {
      echo "<pre>$output</pre>";
      echo "Error while converting SVG. ";
      if (strpos($output, 'SVGConverter.error.while.rasterizing.file') !== false) {
        echo "SVG code for debugging: <hr/>";
        echo htmlentities($svg);
    // stream it
    else {
      return $item;
    // delete it
  // SVG can be streamed directly back
  } else {
    echo "Invalid type";

function doDoc($title,$headertext,$footertext,$items){
  // New Word Document
  $PHPWord = new PHPWord();
  // New portrait section
  $section = $PHPWord->createSection();
  // Add header
  $header = $section->createHeader();
  $table = $header->addTable();
  // Add footer
  $footer = $section->createFooter();
  //$footer->addPreserveText('Page {PAGE} of {NUMPAGES}.', array('align'=>'center'));
  $footer->addPreserveText($footertext, array('align'=>'center'));
  // Title styles
  $PHPWord->addTitleStyle(1, array('size'=>20, 'color'=>'333333', 'bold'=>true));
  $PHPWord->addTitleStyle(2, array('size'=>16, 'color'=>'666666'));
  $section->addTitle($title, 1);
  foreach($items as $item){
    $section->addTitle($item->title, 2);
  $objWriter = PHPWord_IOFactory::createWriter($PHPWord, 'Word2007');
  header('Content-Type: application/');
  header('Content-Disposition: attachment;filename="'.$title.'.docx"');
  header('Cache-Control: max-age=0');
  // At least write the document to webspace:
  $objWriter = PHPWord_IOFactory::createWriter($PHPWord, 'Word2007');

function doPDF($title,$headertext,$footertext,$items){
  // create new PDF document
  // set default header data
  $pdf->SetHeaderData(NULL, NULL, $headertext, NULL);
  // set header and footer fonts
  $pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN));
  $pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA));
  // set default monospaced font
  //set margins
  //set auto page breaks
  $pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);
  //set image scale factor
  //set some language-dependent strings
  // ---------------------------------------------------------
  // set font
  $pdf->SetFont('helvetica', '', 10);
  // add a page
  $html = '<h1>'.$title.'</h1>';
  foreach($items as $item){
    $html .= '<h2>'.$item->title.'</h2>';
    $html .= '<p>'.$item->text.'</p>';
    $html .= '<img src="" />';
  $pdf->writeHTML($html, true, false, true, false, '');
  //Close and output PDF document
  $pdf->Output($title.'.pdf', 'D');

This will convert all the charts to a jpg (svgToJpg, line 39): the svg string is converted to a temp svg file, which is converted to an image using Batik (this is serverside, so check your resources if you plan to use this in a production environment), the generated image paths are passed to the item array which is used to generate the pdf/docx.

I won't go into detail about the document generation itself, as this is pretty basic and the used libraries possibly provide better examples, but the reason I didn't use any HTML to PDF/DOCX generation (which would have been a little more generic) is that the whole idea was that a table of contents could be generated from the predefined titles in the DOCX. I didn't find any tools who could do this from HTML code, so I decided to loop through the XML data. And since I was doing this custom for the docx, why not for the pdf..

So that's it very very very roughly, happy to answer any questions that pop into your mind.