Search code examples
javascriptphpsymfonychartswkhtmltopdf

KnpLabs/snappy bundle (wkhtmltopdf) not render Chart.js charts


I trying to generate PDF with charts generated by Chart.js library but javascript does not render at all.

Do you see something I had missed?

   # config/packages/knp_snappy.yaml
    knp_snappy:
        pdf:
            enabled:    true
            binary:     '%env(WKHTMLTOPDF_PATH)%'
            options:
              debug-javascript: true
              enable-javascript: true
              javascript-delay: 200
              no-stop-slow-scripts: true

        image:
            enabled:    true
            binary:     '%env(WKHTMLTOIMAGE_PATH)%'
            options:    []

PHP Symfony part

    /**
     * Download pdf.
     *
     * @param string $slug
     * @param string $language
     *
     * @return Response
     *
     * @throws \Psr\Cache\CacheException
     * @throws \Psr\Cache\InvalidArgumentException
     */
    public function __invoke(string $slug, string $language): Response
    {
            $this->pdfFilesService->setPdfLanguage($language);
            $base64Images = $this->pdfFilesService->getBase64Images($slug);
            $profile      = $this->profileData->execute($slug, $language);
            $filename     = $profile['name'].'-report.pdf';
            $path         = $this->parameterBag->get('app.private_pdf_storage').'/'.$filename;

            $html = $this->templating->render('download_pdf.twig', \array_merge($base64Images, [
                'profile'     => $profile,
                'language'    => $language,
            ]));


            $this->pdf->generateFromHtml([$html], $path, [], true);

            return new Response(
                $path,
                200,
                [
                    'Content-Type'        => 'application/pdf',
                    'Content-Disposition' => 'inline; filename="'.$path.'"',
                ]
            );

    }

Twig

   <section>
       <div id="barMulti"></div>
   </section>
   <script type="text/javascript" src="{{ chartJs }}"></script>
   <script type="text/javascript">
      function getDataMulti(type) {
         return {
            // The type of chart we want to create
            type,

            // The data for our dataset
            data: {
               labels: [ ... ],
               datasets: [
                  {
                     backgroundColor: "#F4F7F9",
                     data: [3,7,4,5,5,2,6,8,9,7]
                  },
                  {
                     backgroundColor: "#66C4E0",
                     data: [3,7,4,5,5,2,6,8,9,7]
                  },
                  {
                     backgroundColor: "#009DCD",
                     data: [3,7,4,5,5,2,6,8,9,7]
                  },
               ]
            },

            // Configuration options go here
            options: {
               legend: {
                  display: false,
               },
               animation: {
                  duration: 0
               },
               scales: {
                  yAxes: [{
                     gridLines: {
                        color: "#454D57",
                     },
                     ticks: {
                        padding: 20,
                        fontStyle: 'bold',
                        fontColor: '#F4F7F9',
                        min: 0,
                        max: 100,
                     }
                  }],
                  xAxes: [
                     {
                        gridLines: {
                           color: "#454D57"
                        },
                        ticks: {
                           fontStyle: 'bold',
                           fontColor: '#F4F7F9',
                           padding: 20,
                           callback: function(label) {
                              if (/\s/.test(label)) {
                                 return label.split(" ");
                              }

                              return label;
                           }
                        }
                     }
                  ]
               }
            }
         }
      }

      // var barMulti = document.getElementById('barMulti');
      var barMulti = document.getElementById('barMulti');
      new Chart(barMulti, getDataMulti('bar'));
   </script>

Solution

  • wkhtmltopdf is not a perfect library but it is opensource and free to use, so we need to be grateful and try to help its usage or improve by contributing.

    wkhtmltopdf GitHub Repo

    As the first step, you need to test your HTML/TWIG template for working Javascript.

    <script>
      document.body.innerHTML = "Testing JavaScript PDF Rendering"
    </script>
    

    If this is not working then check your wkhtmltopdf configuration

      # config/packages/knp_snappy.yaml
        knp_snappy:
            pdf:
                enabled:    true
                binary:     '%env(WKHTMLTOPDF_PATH)%'
                options:
                  debug-javascript: true
                  enable-javascript: true
                  javascript-delay: 1500
                  no-stop-slow-scripts: true
    

    After you are sure that Javascript works in wkhtmltopdf

    Most important thing. You need to wrap your canvas element with styled div

     <div class="reportGraph">
         <canvas id="barMulti"></canvas>
     </div>
    

    and in head or css put styled class

       .reportGraph {
          width:850px
       }
    

    or maybe style canvas container by inline css

    Then add this script file after including of chart.js library

    <script>
      // wkhtmltopdf 0.12.5 crash fix.
      // https://github.com/wkhtmltopdf/wkhtmltopdf/issues/3242#issuecomment-518099192
      'use strict';
      (function(setLineDash) {
        CanvasRenderingContext2D.prototype.setLineDash = function() {
          if(!arguments[0].length){
            arguments[0] = [1,0];
          }
          // Now, call the original method
          return setLineDash.apply(this, arguments);
        };
      })(CanvasRenderingContext2D.prototype.setLineDash);
      Function.prototype.bind = Function.prototype.bind || function (thisp) {
        var fn = this;
        return function () {
          return fn.apply(thisp, arguments);
        };
      };
    </script>
    

    Then add another script tag in which you will render your chart.

    <script>
    function drawGraphs() {
        new Chart(
            document.getElementById("canvas"), {
                "responsive": false,
                "type":"line",
                "data":{"labels":["January","February","March","April","May","June","July"],"datasets":[{"label":"My First Dataset","data":[65,59,80,81,56,55,40],"fill":false,"borderColor":"rgb(75, 192, 192)","lineTension":0.1}]},
                "options":{}
            }
        );
    }
    window.onload = function() {
        drawGraphs();
    };
    </script>