Pure data is for computers and nerds like you and me. Anyone else likes nicely formatted reports. Go and a good PDF package can help.

The above video is available on the Applied Go channel on YouTube.

This article examines the code used in the video. Numbers have an important story to tell. They rely on you to give them a clear and convincing voice. (Stephen Few) In the last article we exported orders from a spreadsheet and turned them into a report that we saved as CSV data. Now our boss requests this data as a shiny PDF report on her desk. Let’s do that, no problem! But wait, the standard library has no PDF package, right? Luckily, after a short search, we find a package called gofpdf on GitHub. After go get ting the package, we can start coding right away.

package main import ( "encoding/csv" "log" "os" "time" "github.com/jung-kurt/gofpdf" ) func main () { data := loadCSV ( path ()) pdf := newReport () pdf = header (pdf, data[ 0 ]) pdf = table (pdf, data[ 1 :]) pdf = image (pdf) if pdf. Err () { log. Fatalf ( "Failed creating PDF report: %s

" , pdf. Error ()) } err := savePDF (pdf) if err != nil { log. Fatalf ( "Cannot save PDF: %s|n" , err) } }

Side Note: gofpdf Error Handling A somewhat unusual feature of gofpdf is that it does not return errors in the usual way. Instead, if one of the methods of the Fpdf object triggers any error, the Fpdf object stores this error and lets all subsequent method calls “fall through”. The error can then be verified by calling Fpdf 's Err() method, and printed by calling its Error() method. if pdf. Err () { log. Fatalf ( "Cannot create PDF: %s

" , pdf. Error ()) } We make use of this error mechanism in main() , after all PDF processing is done.

func loadCSV (path string ) [][] string { f, err := os. Open (path) if err != nil { log. Fatalf ( "Cannot open '%s': %s

" , path, err. Error ()) } defer f. Close () r := csv. NewReader (f) rows, err := r. ReadAll () if err != nil { log. Fatalln ( "Cannot read CSV data:" , err. Error ()) } return rows } func path () string { if len (os.Args) < 2 { return "ordersReport.csv" } return os.Args[ 1 ] } func newReport () * gofpdf.Fpdf { pdf := gofpdf. New ( "L" , "mm" , "Letter" , "" ) pdf. AddPage () pdf. SetFont ( "Times" , "B" , 28 ) pdf. Cell ( 40 , 10 , "Daily Report" ) pdf. Ln ( 12 ) pdf. SetFont ( "Times" , "" , 20 ) pdf. Cell ( 40 , 10 , time. Now (). Format ( "Mon Jan 2, 2006" )) pdf. Ln ( 20 ) return pdf }

How Cell() and Ln() advance the output position As mentioned in the comments, the Cell() method takes no coordinates. Instead, the PDF document maintains the current output position internally, and advances it to the right by the length of the cell being written. Method Ln() moves the output position back to the left border and down by the provided value. (Passing -1 uses the height of the recently written cell.) Please enable JavaScript to view the animation.

func header (pdf * gofpdf.Fpdf, hdr [] string ) * gofpdf.Fpdf { pdf. SetFont ( "Times" , "B" , 16 ) pdf. SetFillColor ( 240 , 240 , 240 ) for _, str := range hdr { pdf. CellFormat ( 40 , 7 , str, "1" , 0 , "" , true , 0 , "" ) } pdf. Ln ( - 1 ) return pdf } func table (pdf * gofpdf.Fpdf, tbl [][] string ) * gofpdf.Fpdf { pdf. SetFont ( "Times" , "" , 16 ) pdf. SetFillColor ( 255 , 255 , 255 ) align := [] string { "L" , "C" , "L" , "R" , "R" , "R" } for _, line := range tbl { for i, str := range line { pdf. CellFormat ( 40 , 7 , str, "1" , 0 , align[i], false , 0 , "" ) } pdf. Ln ( - 1 ) } return pdf } func image (pdf * gofpdf.Fpdf) * gofpdf.Fpdf { pdf. ImageOptions ( "stats.png" , 225 , 10 , 25 , 25 , false , gofpdf.ImageOptions{ImageType: "PNG" , ReadDpi: true }, 0 , "" ) return pdf } func savePDF (pdf * gofpdf.Fpdf) error { return pdf. OutputFileAndClose ( "report.pdf" ) }