commit b2bf4ca7688a171afdb85dadc2f735e1295e4763 Author: erinn Date: Wed Dec 18 11:53:51 2019 -0800 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecdf2d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor +.idea diff --git a/csv.go b/csv.go new file mode 100644 index 0000000..453ea99 --- /dev/null +++ b/csv.go @@ -0,0 +1,57 @@ +package main + +import ( + "encoding/csv" + "os" +) + +type CSV struct { + index map[string]int + records [][]string +} + +func LoadCSVFromFile(filename string) CSV { + f, err := os.Open(filename) + if err != nil { + panic(err) + } + + r := csv.NewReader(f) + records, err := r.ReadAll() + index := make(map[string]int) + if err != nil { + panic(err) + } + + for i := 0; i < len(records[0]); i++ { + index[records[0][i]] = i + } + + csvo := CSV{index: index, records: records[1:]} + return csvo +} + +func (this *CSV) NumRecords() int { + return len(this.records) +} + +func (this *CSV) Record(index int) map[string]string { + ret := make(map[string]string) + for k, idx := range this.index { + ret[k] = this.records[index][idx] + } + return ret +} + +func (this *CSV) GetField(index int, fieldLabel string) string { + return this.records[index][this.indexOfField(fieldLabel)] +} + +func (this *CSV) PatreonEmptyAddress(index int) bool { + r := this.Record(index) + return r["Addressee"] == "" && r["Street"] == "" && r["City"] == "" && r["State"] == "" && r["Zip"] == "" && r["Country"] == "" +} + +func (this *CSV) indexOfField(fieldLabel string) int { + return this.index[fieldLabel] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6ec6492 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.openprivacy.ca/erinn/pdftest + +go 1.12 + +require ( + git.openprivacy.ca/openprivacy/libricochet-go v1.0.8 // indirect + github.com/jung-kurt/gofpdf v1.16.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1b029e7 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +git.openprivacy.ca/openprivacy/libricochet-go v1.0.8 h1:HVoyxfivFaEtkfO5K3piD6oq6MQB1qGF5IB2EYXeCW8= +git.openprivacy.ca/openprivacy/libricochet-go v1.0.8/go.mod h1:6I+vO9Aagv3/yUWv+e7A57H8tgXgR67FCjfSj9Pp970= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= +github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= +github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..cd3a89c --- /dev/null +++ b/main.go @@ -0,0 +1,123 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "os" + "strings" + "text/template" + + "github.com/jung-kurt/gofpdf" +) + +var ( + flagInputCsv = flag.String("csv", "", "CSV file (downloaded from Patreon)") + flagOutputPdf = flag.String("o", "out.pdf", "filename to output PDF to") + flagPortrait = flag.Bool("portrait", true, "portrait mode?") + flagUnitStr = flag.String("unitStr", "mm", "units to use for other measurements") + flagPaperType = flag.String("paper", "Letter", "paper size") + flagCols = flag.Int("cols", 3, "number of columns") + flagRows = flag.Int("row", 10, "number of rows") + flagSkipLabels = flag.Int("skip", 0, "skip labels (useful for printing on partially-printed label sheets") + flagOffsetX = flag.Float64("x", 6.5, "left margin") + flagOffsetY = flag.Float64("y", 12.7, "top margin") + flagLabelWidth = flag.Float64("w", 66.8, "label width") + flagLabelHeight = flag.Float64("h", 25.4, "label height") + flagGapX = flag.Float64("gapx", 3.4, "gap between columns") + flagGapY = flag.Float64("gapy", 0., "gap between rows") + flagDrawLines = flag.Bool("lines", false, "draw lines?") + flagFontSize = flag.Float64("fontsize", 10, "font size") + flagFontFamily = flag.String("fontfamily", "Arial", "font family") + flagFontStyle = flag.String("fontstyle", "", "font style (B for bold)") + flagTemplate = flag.String("template", "{{.Addressee}}\n{{.Street}}\n{{.City}}, {{.State}}\n{{.Zip}}, {{.Country}}\n", "mailing label template") + lineHeight = 4.0 +) + +func main() { + flag.Parse() + + if *flagInputCsv == "" { + fmt.Println("Must specify input csv with -csv") + flag.Usage() + os.Exit(1) + } + + pL := "P" + if !*flagPortrait { + pL = "L" + } + + pdf := gofpdf.New(pL, *flagUnitStr, *flagPaperType, "") + pdf.SetFont(*flagFontFamily, *flagFontStyle, *flagFontSize) + data := LoadCSVFromFile(*flagInputCsv) + DoTheThing(pdf, data) + + err := pdf.OutputFileAndClose(*flagOutputPdf) + if err != nil { + panic(err) + } +} + +func DoTheThing(pdf *gofpdf.Fpdf, data CSV) { + tpl, err := template.New("mailing labels").Parse(*flagTemplate) + if err != nil { + panic(err) + } + + tr := pdf.UnicodeTranslatorFromDescriptor("") + for i := 0; i < data.NumRecords(); { + AddGriddedPage(pdf) + + for x := 0; x < *flagCols; x++ { + for y := 0; y < *flagRows; y++ { + if *flagSkipLabels > 0 { + *flagSkipLabels-- + continue + } + for ; i < data.NumRecords() && data.PatreonEmptyAddress(i); i++ { + fmt.Printf("Warning: skipping empty addressee: %s\n", data.GetField(i, "Name")) + } + if i >= data.NumRecords() { + return + } + + lx := *flagOffsetX + (*flagLabelWidth + *flagGapX) * float64(x) + 4. + ly := *flagOffsetY + (*flagLabelHeight + *flagGapY) * float64(y) + 8. + + var buf bytes.Buffer + err := tpl.Execute(&buf, data.Record(i)) + if err != nil { + panic(err) + } + + DrawText(pdf, tr(buf.String()), lx, ly) + + i++ + } + } + } +} + +func AddGriddedPage(pdf *gofpdf.Fpdf) { + pdf.AddPage() + if !*flagDrawLines { + return + } + + for x := 0; x < *flagCols; x++ { + for y := 0; y < *flagRows; y++ { + lx := *flagOffsetX + (*flagLabelWidth + *flagGapX) * float64(x) + ly := *flagOffsetY + (*flagLabelHeight + *flagGapY) * float64(y) + pdf.Rect(lx, ly, *flagLabelWidth, *flagLabelHeight, "D") + } + } +} + +func DrawText(pdf *gofpdf.Fpdf, str string, x, y float64) { + lines := strings.Split(str, "\n") + for _, s := range lines { + pdf.Text(x, y, s) + y += lineHeight + } +} \ No newline at end of file