implement web handler for QR code generation
This commit is contained in:
@@ -1,14 +1,178 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/stapelberg/qrbill"
|
"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>( )*([^:]+):`)
|
||||||
|
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, " ", " ")
|
||||||
|
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 {
|
func logic() error {
|
||||||
return qrbill.Generate()
|
var listen = flag.String("listen", "localhost:9933", "[host]:port to listen on")
|
||||||
//return nil
|
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() {
|
func main() {
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -6,5 +6,6 @@ require (
|
|||||||
github.com/aaronarduino/goqrsvg v0.0.0-20170617203649-603647895681
|
github.com/aaronarduino/goqrsvg v0.0.0-20170617203649-603647895681
|
||||||
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca
|
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca
|
||||||
github.com/boombuler/barcode v1.0.0
|
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
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
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/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 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc=
|
||||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
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 h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
|
|||||||
235
qrbill.go
235
qrbill.go
@@ -9,14 +9,12 @@
|
|||||||
package qrbill
|
package qrbill
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"image"
|
"image"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aaronarduino/goqrsvg"
|
"github.com/aaronarduino/goqrsvg"
|
||||||
svg "github.com/ajstarks/svgo"
|
svg "github.com/ajstarks/svgo"
|
||||||
"github.com/boombuler/barcode"
|
|
||||||
"github.com/boombuler/barcode/qr"
|
"github.com/boombuler/barcode/qr"
|
||||||
|
|
||||||
// We currently read the swiss cross PNG version.
|
// 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
|
// https://github.com/codebude/QRCoder/wiki/Advanced-usage---Payload-generators#317-swissqrcode-iso-20022
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// QRType is an unambiguous indicator for the Swiss QR Code. Fixed value
|
// QRType is an unambiguous indicator for the Swiss QR Code. Fixed value.
|
||||||
// "SPC".
|
|
||||||
QRType = "SPC" // Swiss Payments Code
|
QRType = "SPC" // Swiss Payments Code
|
||||||
|
|
||||||
// Version contains the version of the specifications (Implementation
|
// Version contains the version of the specifications (Implementation
|
||||||
// Guidelines) in use on the date on which the Swiss QR Code was
|
// Guidelines) in use on the date on which the Swiss QR Code was
|
||||||
// created. The first two positions indicate the main version, the following
|
// 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
|
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
|
CodingType = "1" // UTF-8 restricted to the Latin character set
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,7 +55,6 @@ const (
|
|||||||
|
|
||||||
// - fixed length: 21 alphanumeric characters
|
// - fixed length: 21 alphanumeric characters
|
||||||
// - only IBANs with CH or LI country code permitted
|
// - only IBANs with CH or LI country code permitted
|
||||||
var iban = "CH0209000000870913543"
|
|
||||||
|
|
||||||
type Address struct {
|
type Address struct {
|
||||||
AdrTp AddressType
|
AdrTp AddressType
|
||||||
@@ -106,176 +102,93 @@ type QRCH struct {
|
|||||||
RmtInf QRCHRmtInf // Payment reference
|
RmtInf QRCHRmtInf // Payment reference
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QRCH) QRContents() string {
|
func (q *QRCH) Fill() *QRCH {
|
||||||
return strings.Join([]string{
|
clone := &QRCH{}
|
||||||
q.Header.QRType,
|
*clone = *q
|
||||||
q.Header.Version,
|
clone.Header.QRType = QRType
|
||||||
q.Header.Coding,
|
clone.Header.Version = Version
|
||||||
|
clone.Header.Coding = CodingType
|
||||||
q.CdtrInf.IBAN,
|
clone.RmtInf.AddInf.Trailer = "EPD" // TODO: constant
|
||||||
|
return clone
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://www.paymentstandards.ch/dam/downloads/qrcodegenerator.java
|
func (q *QRCH) Encode() (*Bill, error) {
|
||||||
func generateSwissQrCode(content string) error {
|
f := q.Fill()
|
||||||
// generate the qr code from the payload
|
//f := q.Fill()
|
||||||
code, err := qr.Encode(content, qr.M, qr.Auto)
|
// TODO: data content must be no more than 997 characters
|
||||||
if err != nil {
|
// TODO: truncate fields where necessary
|
||||||
return err
|
return &Bill{
|
||||||
}
|
qrcontents: strings.Join([]string{
|
||||||
|
f.Header.QRType,
|
||||||
|
f.Header.Version,
|
||||||
|
f.Header.Coding,
|
||||||
|
|
||||||
// overlay the qr code with a Swiss Cross
|
f.CdtrInf.IBAN,
|
||||||
combined, err := overlayWithSwissCross(code)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = combined
|
|
||||||
|
|
||||||
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) {
|
type Bill struct {
|
||||||
// TODO: bundle a swiss cross image
|
qrcontents string
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Generate() error {
|
func (b *Bill) EncodeToSVG() ([]byte, 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()
|
|
||||||
|
|
||||||
// as per https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf, section 5.1:
|
// as per https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf, section 5.1:
|
||||||
// Error correction level M (redundancy of around 15%)
|
// 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)
|
code, err := qr.Encode(b.qrcontents, qr.M, qr.Unicode)
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
f, err := os.Create("/tmp/code.svg")
|
var buf bytes.Buffer
|
||||||
if err != nil {
|
s := svg.New(&buf)
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
s := svg.New(f)
|
|
||||||
qrsvg := goqrsvg.NewQrSVG(code, 5)
|
qrsvg := goqrsvg.NewQrSVG(code, 5)
|
||||||
qrsvg.StartQrSVG(s)
|
qrsvg.StartQrSVG(s)
|
||||||
if err := qrsvg.WriteQrSVG(s); err != nil {
|
if err := qrsvg.WriteQrSVG(s); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: overlay the swiss cross logo!
|
||||||
|
|
||||||
s.End()
|
s.End()
|
||||||
if err := f.Close(); err != nil {
|
return buf.Bytes(), nil
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
|
func (b *Bill) EncodeToImage() (image.Image, error) {
|
||||||
if err := generateSwissQrCode(content); err != nil {
|
return generateSwissQrCode(b.qrcontents)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
90
qrcodegenerator.go
Normal file
90
qrcodegenerator.go
Normal 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 image’s coordinate
|
||||||
|
// space:
|
||||||
|
destRect := image.Rectangle{
|
||||||
|
destPoint,
|
||||||
|
destPoint.Add(sr.Size()),
|
||||||
|
}
|
||||||
|
draw.Draw(combinedQrCodeImage, destRect, swissCrossImage, sr.Min, draw.Src)
|
||||||
|
}
|
||||||
|
return combinedQrCodeImage, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user