diff --git a/cmd/qrbill-example/example.go b/cmd/qrbill-example/example.go
index a0c73a6..06d4042 100644
--- a/cmd/qrbill-example/example.go
+++ b/cmd/qrbill-example/example.go
@@ -1,14 +1,178 @@
package main
import (
+ "bytes"
+ "flag"
+ "fmt"
+ "image/png"
+ "io"
"log"
+ "net/http"
+ "regexp"
+ "strings"
+ "github.com/davecgh/go-spew/spew"
"github.com/stapelberg/qrbill"
+
+ _ "net/http/pprof"
)
+func ifEmpty(s, alternative string) string {
+ if s == "" {
+ return alternative
+ }
+ return s
+}
+
+func qrchFromRequest(r *http.Request) *qrbill.QRCH {
+ return &qrbill.QRCH{
+ CdtrInf: qrbill.QRCHCdtrInf{
+ IBAN: ifEmpty(r.FormValue("criban"), "CH0209000000870913543"),
+ Cdtr: qrbill.Address{
+ AdrTp: qrbill.AddressTypeStructured,
+ Name: ifEmpty(r.FormValue("crname"), "Legalize it!"),
+ StrtNmOrAdrLine1: ifEmpty(r.FormValue("craddr1"), "Quellenstrasse 25"),
+ BldgNbOrAdrLine2: ifEmpty(r.FormValue("craddr2"), ""),
+ PstCd: ifEmpty(r.FormValue("crpost"), "8005"),
+ TwnNm: ifEmpty(r.FormValue("crcity"), "Zürich"),
+ Ctry: ifEmpty(r.FormValue("crcountry"), "CH"),
+ },
+ },
+ CcyAmt: qrbill.QRCHCcyAmt{
+ Amt: "",
+ Ccy: "CHF",
+ },
+ UltmtDbtr: qrbill.Address{
+ AdrTp: qrbill.AddressTypeStructured,
+ Name: ifEmpty(r.FormValue("udname"), "Michael Stapelberg"),
+ StrtNmOrAdrLine1: ifEmpty(r.FormValue("udaddr1"), "Brahmsstrasse 21"),
+ BldgNbOrAdrLine2: ifEmpty(r.FormValue("udaddr2"), ""),
+ PstCd: ifEmpty(r.FormValue("udpost"), "8003"),
+ TwnNm: ifEmpty(r.FormValue("udcity"), "Zürich"),
+ Ctry: ifEmpty(r.FormValue("udcountry"), "CH"),
+ },
+ RmtInf: qrbill.QRCHRmtInf{
+ Tp: "NON", // Reference type
+ Ref: "", // Reference
+ AddInf: qrbill.QRCHRmtInfAddInf{
+ Ustrd: ifEmpty(r.FormValue("message"), "Spende 6141"),
+ },
+ },
+ }
+}
+
+var fieldNameRe = regexp.MustCompile(`
( )*([^:]+):`)
+var stringLiteralRe = regexp.MustCompile(`"([^"]*)"`)
+
+func qrHandler(format string) http.Handler {
+ if format != "png" &&
+ format != "svg" &&
+ format != "txt" &&
+ format != "html" {
+ log.Fatalf("BUG: format must be either png, svg, txt or html")
+ }
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ prefix := "[" + r.RemoteAddr + "]"
+ log.Printf("%s handling request for %s", prefix, r.URL.Path)
+ defer log.Printf("%s done: %s", prefix, r.URL.Path)
+
+ qrch := qrchFromRequest(r)
+
+ bill, err := qrch.Encode()
+ if err != nil {
+ log.Print(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ var b []byte
+ switch format {
+ case "png":
+ code, err := bill.EncodeToImage()
+ if err != nil {
+ log.Print(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ var buf bytes.Buffer
+ if err := png.Encode(&buf, code); err != nil {
+ log.Print(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ b = buf.Bytes()
+ w.Header().Add("Content-Type", "image/png")
+
+ case "svg":
+ var err error
+ b, err = bill.EncodeToSVG()
+ if err != nil {
+ log.Print(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Add("Content-Type", "image/svg+xml")
+
+ case "txt":
+ w.Header().Add("Content-Type", "text/plain; charset=utf-8")
+ spew.Fdump(w, qrch.Fill())
+
+ case "html":
+ w.Header().Add("Content-Type", "text/html; charset=utf-8")
+ fmt.Fprintf(w, `
+
+ QR Bill HTML Debug Page
+
+
+
+`)
+ sp := spew.Sdump(qrch.Fill())
+ sp = strings.ReplaceAll(sp, "\n", "
")
+ sp = strings.ReplaceAll(sp, " ", " ")
+ sp = stringLiteralRe.ReplaceAllStringFunc(sp, func(stringLiteral string) string {
+ return `` + stringLiteral + ""
+ })
+ sp = fieldNameRe.ReplaceAllStringFunc(sp, func(fieldName string) string {
+ return `` + fieldName + ""
+ })
+ fmt.Fprintf(w, "%s", sp)
+ }
+
+ // TODO: add cache control headers
+ if _, err := io.Copy(w, bytes.NewReader(b)); err != nil {
+ log.Print(err)
+ return
+ }
+ })
+}
+
func logic() error {
- return qrbill.Generate()
- //return nil
+ var listen = flag.String("listen", "localhost:9933", "[host]:port to listen on")
+ flag.Parse()
+ http.Handle("/qr.png", qrHandler("png"))
+ http.Handle("/qr.svg", qrHandler("svg"))
+ http.Handle("/qr.txt", qrHandler("txt"))
+ http.Handle("/qr.html", qrHandler("html"))
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ }
+ w.Header().Add("Content-Type", "text/html; charset=utf-8")
+ // TODO: add explanation for how to construt a URL
+ // e.g. for usage in filemaker web view
+ fmt.Fprintf(w, "")
+ fmt.Fprintf(w, `- PNG referenz: qr.png`+"\n")
+ fmt.Fprintf(w, `
- SVG scalable: qr.svg`+"\n")
+ fmt.Fprintf(w, `
- debug: qr.txt`+"\n")
+ })
+ log.Printf("listening on http://%s", *listen)
+ return http.ListenAndServe(*listen, nil)
}
func main() {
diff --git a/go.mod b/go.mod
index 565563b..af2d0d3 100644
--- a/go.mod
+++ b/go.mod
@@ -6,5 +6,6 @@ require (
github.com/aaronarduino/goqrsvg v0.0.0-20170617203649-603647895681
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca
github.com/boombuler/barcode v1.0.0
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
)
diff --git a/go.sum b/go.sum
index 3997f08..c4d77af 100644
--- a/go.sum
+++ b/go.sum
@@ -4,5 +4,7 @@ github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
diff --git a/qrbill.go b/qrbill.go
index 38af90c..76d57e2 100644
--- a/qrbill.go
+++ b/qrbill.go
@@ -9,14 +9,12 @@
package qrbill
import (
+ "bytes"
"image"
- "log"
- "os"
"strings"
"github.com/aaronarduino/goqrsvg"
svg "github.com/ajstarks/svgo"
- "github.com/boombuler/barcode"
"github.com/boombuler/barcode/qr"
// We currently read the swiss cross PNG version.
@@ -34,17 +32,16 @@ import (
// https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#317-swissqrcode-iso-20022
const (
- // QRType is an unambiguous indicator for the Swiss QR Code. Fixed value
- // "SPC".
+ // QRType is an unambiguous indicator for the Swiss QR Code. Fixed value.
QRType = "SPC" // Swiss Payments Code
// Version contains the version of the specifications (Implementation
// Guidelines) in use on the date on which the Swiss QR Code was
// created. The first two positions indicate the main version, the following
- // two positions the sub-version. Fixed value of "0200" for Version 2.0.
+ // two positions the sub-version. Fixed value.
Version = "0200" // Version 2.0
- // CodingType is the character set code. Fixed value "1".
+ // CodingType is the character set code. Fixed value.
CodingType = "1" // UTF-8 restricted to the Latin character set
)
@@ -58,7 +55,6 @@ const (
// - fixed length: 21 alphanumeric characters
// - only IBANs with CH or LI country code permitted
-var iban = "CH0209000000870913543"
type Address struct {
AdrTp AddressType
@@ -106,176 +102,93 @@ type QRCH struct {
RmtInf QRCHRmtInf // Payment reference
}
-func (q *QRCH) QRContents() string {
- return strings.Join([]string{
- q.Header.QRType,
- q.Header.Version,
- q.Header.Coding,
-
- q.CdtrInf.IBAN,
-
- string(q.CdtrInf.Cdtr.AdrTp),
- q.CdtrInf.Cdtr.Name,
- q.CdtrInf.Cdtr.StrtNmOrAdrLine1,
- q.CdtrInf.Cdtr.BldgNbOrAdrLine2,
- q.CdtrInf.Cdtr.PstCd,
- q.CdtrInf.Cdtr.TwnNm,
- q.CdtrInf.Cdtr.Ctry,
-
- string(q.UltmtCdtr.AdrTp),
- q.UltmtCdtr.Name,
- q.UltmtCdtr.StrtNmOrAdrLine1,
- q.UltmtCdtr.BldgNbOrAdrLine2,
- q.UltmtCdtr.PstCd,
- q.UltmtCdtr.TwnNm,
- q.UltmtCdtr.Ctry,
-
- q.CcyAmt.Amt,
- q.CcyAmt.Ccy,
-
- string(q.UltmtDbtr.AdrTp),
- q.UltmtDbtr.Name,
- q.UltmtDbtr.StrtNmOrAdrLine1,
- q.UltmtDbtr.BldgNbOrAdrLine2,
- q.UltmtDbtr.PstCd,
- q.UltmtDbtr.TwnNm,
- q.UltmtDbtr.Ctry,
-
- q.RmtInf.Tp,
- q.RmtInf.Ref,
- q.RmtInf.AddInf.Ustrd,
- q.RmtInf.AddInf.Trailer,
- }, "\n")
+func (q *QRCH) Fill() *QRCH {
+ clone := &QRCH{}
+ *clone = *q
+ clone.Header.QRType = QRType
+ clone.Header.Version = Version
+ clone.Header.Coding = CodingType
+ clone.RmtInf.AddInf.Trailer = "EPD" // TODO: constant
+ return clone
}
-// https://www.paymentstandards.ch/dam/downloads/qrcodegenerator.java
-func generateSwissQrCode(content string) error {
- // generate the qr code from the payload
- code, err := qr.Encode(content, qr.M, qr.Auto)
- if err != nil {
- return err
- }
+func (q *QRCH) Encode() (*Bill, error) {
+ f := q.Fill()
+ //f := q.Fill()
+ // TODO: data content must be no more than 997 characters
+ // TODO: truncate fields where necessary
+ return &Bill{
+ qrcontents: strings.Join([]string{
+ f.Header.QRType,
+ f.Header.Version,
+ f.Header.Coding,
- // overlay the qr code with a Swiss Cross
- combined, err := overlayWithSwissCross(code)
- if err != nil {
- return err
- }
- _ = combined
+ f.CdtrInf.IBAN,
- return nil
+ string(f.CdtrInf.Cdtr.AdrTp),
+ f.CdtrInf.Cdtr.Name,
+ f.CdtrInf.Cdtr.StrtNmOrAdrLine1,
+ f.CdtrInf.Cdtr.BldgNbOrAdrLine2,
+ f.CdtrInf.Cdtr.PstCd,
+ f.CdtrInf.Cdtr.TwnNm,
+ f.CdtrInf.Cdtr.Ctry,
+
+ string(f.UltmtCdtr.AdrTp),
+ f.UltmtCdtr.Name,
+ f.UltmtCdtr.StrtNmOrAdrLine1,
+ f.UltmtCdtr.BldgNbOrAdrLine2,
+ f.UltmtCdtr.PstCd,
+ f.UltmtCdtr.TwnNm,
+ f.UltmtCdtr.Ctry,
+
+ f.CcyAmt.Amt,
+ f.CcyAmt.Ccy,
+
+ string(f.UltmtDbtr.AdrTp),
+ f.UltmtDbtr.Name,
+ f.UltmtDbtr.StrtNmOrAdrLine1,
+ f.UltmtDbtr.BldgNbOrAdrLine2,
+ f.UltmtDbtr.PstCd,
+ f.UltmtDbtr.TwnNm,
+ f.UltmtDbtr.Ctry,
+
+ f.RmtInf.Tp,
+ f.RmtInf.Ref,
+ f.RmtInf.AddInf.Ustrd,
+ "EPD", // TODO: constant
+ }, "\n"),
+ }, nil
}
-func overlayWithSwissCross(code barcode.Barcode) (image.Image, error) {
- // TODO: bundle a swiss cross image
- const swissCrossPath = "/home/michael/go/src/github.com/stapelberg/qrbill/third_party/swiss-cross/CH-Kreuz_7mm/CH-Kreuz_7mm.png"
-
- // TODO: read swiss cross image
- f, err := os.Open(swissCrossPath)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- m, _, err := image.Decode(f)
- if err != nil {
- return nil, err
- }
- log.Printf("bounds: %+v", m.Bounds())
- return nil, nil
+type Bill struct {
+ qrcontents string
}
-func Generate() error {
- log.Printf("hey!")
-
- content := (&QRCH{
- Header: QRCHHeader{
- QRType: QRType,
- Version: Version,
- Coding: CodingType,
- },
- CdtrInf: QRCHCdtrInf{
- IBAN: iban,
- Cdtr: Address{
- AdrTp: AddressTypeStructured, // CR AddressTyp
- Name: "Legalize it!", // CR Name
- StrtNmOrAdrLine1: "Quellenstrasse 25", // CR Street or address line 1
- BldgNbOrAdrLine2: "", // CR Building number or address line 2
- PstCd: "8005", // CR Postal code
- TwnNm: "Zürich", // CR City
- Ctry: "CH", // CR Country
- },
- },
- CcyAmt: QRCHCcyAmt{
- Amt: "",
- Ccy: "CHF",
- },
- UltmtDbtr: Address{
- "S", // UD AddressTyp
- "Michael Stapelberg", // UD Name
- "Brahmsstrasse 21", // UD Street or address line 1
- "", // UD Building number or address line 2
- "8003", // Postal code
- "Zürich", // City
- "CH", // Country
- },
- RmtInf: QRCHRmtInf{
- Tp: "NON", // Reference type
- Ref: "", // Reference
- AddInf: QRCHRmtInfAddInf{
- Ustrd: "Spende 6141",
- Trailer: "EPD",
- },
- },
- }).QRContents()
-
+func (b *Bill) EncodeToSVG() ([]byte, error) {
// as per https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf, section 5.1:
// Error correction level M (redundancy of around 15%)
- // TODO: data content must be no more than 997 characters
+ // Section 4.2.1: Character set:
+ // UTF-8 should be used for encoding
- // https://www.PaymentStandards.CH/FAQ)
- // TODO: auf version 24 (46mm x 46mm) skalieren
-
- // version 25 with 117 x 117 modules
-
- // minimum module size of 0.4mm (recommended for printing)
-
- // TODO: overlay the swiss cross logo!
- // TODO: verify dimensions when printed
-
- // TODO: ensure UTF-8
- code, err := qr.Encode(content, qr.M, qr.Auto)
+ code, err := qr.Encode(b.qrcontents, qr.M, qr.Unicode)
if err != nil {
- return err
+ return nil, err
}
- f, err := os.Create("/tmp/code.svg")
- if err != nil {
- return err
- }
- defer f.Close()
- s := svg.New(f)
+ var buf bytes.Buffer
+ s := svg.New(&buf)
qrsvg := goqrsvg.NewQrSVG(code, 5)
qrsvg.StartQrSVG(s)
if err := qrsvg.WriteQrSVG(s); err != nil {
- return err
+ return nil, err
}
+
+ // TODO: overlay the swiss cross logo!
+
s.End()
- if err := f.Close(); err != nil {
- return err
- }
-
- if err := generateSwissQrCode(content); err != nil {
- return err
- }
-
- /*
- code, err := qrcode.NewWithForcedVersion(content, 25, qrcode.Medium)
- if err != nil {
- return err
- }
- log.Printf("code: %v", code)
- const pixelsPerMillimeter = 10
- return code.WriteFile(-2, "/tmp/code.png")
- */
- return nil
+ return buf.Bytes(), nil
+}
+
+func (b *Bill) EncodeToImage() (image.Image, error) {
+ return generateSwissQrCode(b.qrcontents)
}
diff --git a/qrcodegenerator.go b/qrcodegenerator.go
new file mode 100644
index 0000000..8487f76
--- /dev/null
+++ b/qrcodegenerator.go
@@ -0,0 +1,90 @@
+package qrbill
+
+import (
+ "image"
+ "image/draw"
+ "os"
+
+ "github.com/boombuler/barcode"
+ "github.com/boombuler/barcode/qr"
+)
+
+// This is a port of the Java 1.7 reference example from paymentstandards.ch:
+// https://www.paymentstandards.ch/dam/downloads/qrcodegenerator.java
+//
+// The priority was to write idiomatic Go code first, and match the reference
+// example as good as possible second.
+
+const (
+ swissCrossEdgeSidePx = 166
+
+ swissCrossEdgeSideMm = 7
+
+ // The edge length of the qrcode inclusive its white border.
+ qrCodeEdgeSideMm = 42 + 13
+
+ qrCodeEdgeSidePx = swissCrossEdgeSidePx / swissCrossEdgeSideMm * qrCodeEdgeSideMm
+)
+
+func generateSwissQrCode(payload string) (image.Image, error) {
+ // generate the qr code from the payload
+ qrCodeImage, err := generateQrCodeImage(payload)
+ if err != nil {
+ return nil, err
+ }
+
+ // overlay the qr code with a Swiss Cross
+ return overlayWithSwissCross(qrCodeImage)
+}
+
+func generateQrCodeImage(payload string) (image.Image, error) {
+ code, err := qr.Encode(payload, qr.M, qr.Unicode)
+ if err != nil {
+ return nil, err
+ }
+ qrcode, err := barcode.Scale(code, qrCodeEdgeSidePx, qrCodeEdgeSidePx)
+ if err != nil {
+ return nil, err
+ }
+ return qrcode, nil
+}
+
+func overlayWithSwissCross(qrCodeImage image.Image) (image.Image, error) {
+ // TODO: bundle the swiss cross image instead of reading it
+ const swissCrossPath = "/home/michael/go/src/github.com/stapelberg/qrbill/third_party/swiss-cross/CH-Kreuz_7mm/CH-Kreuz_7mm.png"
+
+ f, err := os.Open(swissCrossPath)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ swissCrossImage, _, err := image.Decode(f)
+ if err != nil {
+ return nil, err
+ }
+
+ combinedQrCodeImage := image.NewRGBA(qrCodeImage.Bounds())
+
+ {
+ sr := qrCodeImage.Bounds() // source rect
+ destRect := image.Rectangle{image.Point{0, 0}, sr.Size()}
+ draw.Draw(combinedQrCodeImage, destRect, qrCodeImage, sr.Min, draw.Src)
+ }
+
+ {
+ sr := swissCrossImage.Bounds() // source rect
+ const swissCrossPosition = (qrCodeEdgeSidePx / 2) - (swissCrossEdgeSidePx / 2)
+ destPoint := image.Point{
+ X: swissCrossPosition,
+ Y: swissCrossPosition,
+ }
+ // Convert the source image bounds into the destination image’s coordinate
+ // space:
+ destRect := image.Rectangle{
+ destPoint,
+ destPoint.Add(sr.Size()),
+ }
+ draw.Draw(combinedQrCodeImage, destRect, swissCrossImage, sr.Min, draw.Src)
+ }
+ return combinedQrCodeImage, nil
+}