implement web handler for QR code generation

This commit is contained in:
Michael Stapelberg
2020-06-20 09:39:06 +02:00
parent 8410782ab8
commit 8bf9d10964
5 changed files with 333 additions and 163 deletions

View File

@@ -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(`<br>(&nbsp;)*([^:]+):`)
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, `<html lang="en">
<head>
<title>QR Bill HTML Debug Page</title>
<style type="text/css">
.fieldname { font-weight: bold; }
.stringliteral { color: blue; }
</style>
</head>
<body style="font-family: monospace">
`)
sp := spew.Sdump(qrch.Fill())
sp = strings.ReplaceAll(sp, "\n", "<br>")
sp = strings.ReplaceAll(sp, " ", "&nbsp;")
sp = stringLiteralRe.ReplaceAllStringFunc(sp, func(stringLiteral string) string {
return `<span class="stringliteral">` + stringLiteral + "</span>"
})
sp = fieldNameRe.ReplaceAllStringFunc(sp, func(fieldName string) string {
return `<span class="fieldname">` + fieldName + "</span>"
})
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, "<ul>")
fmt.Fprintf(w, `<li>PNG referenz: <a href="/qr.png">qr.png</a>`+"\n")
fmt.Fprintf(w, `<li>SVG scalable: <a href="/qr.svg">qr.svg</a>`+"\n")
fmt.Fprintf(w, `<li>debug: <a href="/qr.txt">qr.txt</a>`+"\n")
})
log.Printf("listening on http://%s", *listen)
return http.ListenAndServe(*listen, nil)
}
func main() {

1
go.mod
View File

@@ -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
)

2
go.sum
View File

@@ -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=

235
qrbill.go
View File

@@ -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)
}

90
qrcodegenerator.go Normal file
View File

@@ -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 images coordinate
// space:
destRect := image.Rectangle{
destPoint,
destPoint.Add(sr.Size()),
}
draw.Draw(combinedQrCodeImage, destRect, swissCrossImage, sr.Min, draw.Src)
}
return combinedQrCodeImage, nil
}