Nugget post: WKHTMLTOPDF not loading JavaScript

WKHTMLTOPDF is an extremely popular tool to convert existing html pages (or strings) to a PDF. This comes in handy for CyberSift’s Tutela product, where we are required to generate PDF reports programmatically. A significant aspect of these reports are graphs and charts to more easily visualize data. When using newer charting libraries, we ended up with blank pages instead of charts.

To build some context, assume we have the following GoLang file which:

  • Uses the “go-wkhtmltopdf” library to turn html into pdf
  • Lines 54-55 show how easy it is to add a simple “Hi!” to a page in the pdf
  • Line 57 points to a function which uses the “go-echarts” library. This golang library outputs an html page which in turn contains the actual Javascript Apache eCharts library to generate a chart. The html file is temporarily stored on disk in line 59
  • In line 65 we proceed to add this generated html page to our PDF using wkhtmltopdf
package main
import (
"bytes"
"fmt"
"log"
"math/rand"
"os"
"strings"
"time"
"github.com/SebastiaanKlippert/go-wkhtmltopdf"
"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/opts"
)
// generate random data for bar chart
func generateBarItems() []opts.BarData {
items := make([]opts.BarData, 0)
for i := 0; i < 7; i++ {
items = append(items, opts.BarData{Value: rand.Intn(300)})
}
return items
}
func exampleBarChart() bytes.Buffer {
// create a new bar instance
bar := charts.NewBar()
// set some global options like Title/Legend/ToolTip or anything else
bar.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
Title: "My first bar chart generated by go-echarts",
Subtitle: "It's extremely easy to use, right?",
}))
// Put data into instance
bar.SetXAxis([]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}).
AddSeries("Category A", generateBarItems()).
AddSeries("Category B", generateBarItems())
// Where the magic happens
var result bytes.Buffer
bar.Render(&result)
return result
}
func ExampleNewPDFGenerator() {
// Create new PDF generator
pdfg, err := wkhtmltopdf.NewPDFGenerator()
if err != nil {
log.Fatal(err)
}
html := "<html>Hi</html>"
pdfg.AddPage(wkhtmltopdf.NewPageReader(strings.NewReader(html)))
chart := exampleBarChart()
f, _ := os.Create(fmt.Sprintf("bar-%d.html", time.Now().UnixMicro()))
f.WriteString(chart.String())
f.Close()
defer os.Remove(f.Name())
pdfg.AddPage(wkhtmltopdf.NewPage(f.Name()))
// Create PDF document in internal buffer
err = pdfg.Create()
if err != nil {
log.Fatal(err)
}
// Write buffer contents to file on disk
err = pdfg.WriteFile("./simplesample.pdf")
if err != nil {
log.Fatal(err)
}
fmt.Println("Done")
// Output: Done
}
func main() {
ExampleNewPDFGenerator()
}

Simple enough, and a PDF is generated, but when opened, you can see the “Hi!” in the first page, but nothing in the second page where our chart is supposed to be:

Returning to first principles

In troubleshooting this, it’s useful to “break it down”. The problem can be broken down into two stages:

  • Either the echarts library isn’t generating the right HTML
  • Or wkhtmltopdf is not successfully generating the PDF

Tackling the first problem is easy. Prevent the program from deleting the temporary echarts HTML file in line 63 by commenting out the “defer os.Remove(f.Name())” line. After running this program, you should be left with a bar-….html file, which can be opened in the browser:

So the echarts library isn’t the problem. Must be WKHTMLTOPDF. To remove even more cofounding variables, let’s return to basics and use wkhtmltopdf directly on the html page:

It seems to run properly, but the “troubleshooting.pdf” file shows the same issue… a blank page. Definitely a wkhtmltopdf problem then.

Hunting around for debug or logging flags, we come across a promising flag:

wkhtmltopdf.exe --debug-javascript

Running that now gives us a clue in the warning:

Syntax error in line 17 of our echarts generated HTML file….? Taking a peek at that:

Doesnt look like much, and it looks like valid javascript.

BUT it struck me that older browsers dont understand newer JS keywords like “let” and “const” (the benefits of getting older I guess…), instead only var was used. Let’s test that theory by turning any “let” occurrences in the HTML to “var”:

And running wkhtmltopdf.exe again:

No errors this time, and the troubleshooting.pdf file is correctly showing the chart! So let’s modify our program and insert a simple search and replace:

  • Line 56 generates the chart as before
  • Line 57 replaces any occurrences of “let ” (note the trailing space) to “var “
  • We proceed as before but remember to uncomment our os.Remove in line 63

Compile, run, and:

Success! The final version of the program follows below. I hope this saves some hair pulling while offering some insight into how we tend to troubleshoot issues

package main
import (
"bytes"
"fmt"
"log"
"math/rand"
"os"
"strings"
"time"
"github.com/SebastiaanKlippert/go-wkhtmltopdf"
"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/opts"
)
// generate random data for bar chart
func generateBarItems() []opts.BarData {
items := make([]opts.BarData, 0)
for i := 0; i < 7; i++ {
items = append(items, opts.BarData{Value: rand.Intn(300)})
}
return items
}
func exampleBarChart() bytes.Buffer {
// create a new bar instance
bar := charts.NewBar()
// set some global options like Title/Legend/ToolTip or anything else
bar.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
Title: "My first bar chart generated by go-echarts",
Subtitle: "It's extremely easy to use, right?",
}))
// Put data into instance
bar.SetXAxis([]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}).
AddSeries("Category A", generateBarItems()).
AddSeries("Category B", generateBarItems())
// Where the magic happens
var result bytes.Buffer
bar.Render(&result)
return result
}
func ExampleNewPDFGenerator() {
// Create new PDF generator
pdfg, err := wkhtmltopdf.NewPDFGenerator()
if err != nil {
log.Fatal(err)
}
html := "<html>Hi</html>"
pdfg.AddPage(wkhtmltopdf.NewPageReader(strings.NewReader(html)))
chart := exampleBarChart()
chartString := strings.ReplaceAll(chart.String(), "let ", "var ")
f, _ := os.Create(fmt.Sprintf("bar-%d.html", time.Now().UnixMicro()))
f.WriteString(chartString)
f.Close()
defer os.Remove(f.Name())
pdfg.AddPage(wkhtmltopdf.NewPage(f.Name()))
// Create PDF document in internal buffer
err = pdfg.Create()
if err != nil {
log.Fatal(err)
}
// Write buffer contents to file on disk
err = pdfg.WriteFile("./simplesample.pdf")
if err != nil {
log.Fatal(err)
}
fmt.Println("Done")
// Output: Done
}
func main() {
ExampleNewPDFGenerator()
}