282 lines
7.2 KiB
Go
282 lines
7.2 KiB
Go
// Package qrbill implements the Swiss QR-bill standard.
|
|
//
|
|
// More specifically, the most recent standard version at the time of writing
|
|
// was the Swiss Payment Standards 2019 Swiss Implementation Guidelines QR-bill
|
|
// Version 2.1, to be found at:
|
|
//
|
|
// https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf (English)
|
|
// https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-de.pdf (German)
|
|
package qrbill
|
|
|
|
import (
|
|
"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.
|
|
_ "image/png"
|
|
)
|
|
|
|
// As per section 4.1: In general:
|
|
// Oriented upon the Swiss Implementation Guidelines for Credit Transfers for
|
|
// the ISO 20022 Customer Credit Transfer Initiation message (pain.001).
|
|
|
|
// Section 4.2.1: Character set:
|
|
// UTF-8 should be used for encoding
|
|
|
|
// see also:
|
|
// 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 = "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.
|
|
Version = "0200" // Version 2.0
|
|
|
|
// CodingType is the character set code. Fixed value "1".
|
|
CodingType = "1" // UTF-8 restricted to the Latin character set
|
|
)
|
|
|
|
// AddressType corresponds to AdrTp in ISO20022.
|
|
type AddressType string
|
|
|
|
const (
|
|
AddressTypeStructured AddressType = "S"
|
|
AddressTypeCombined = "K"
|
|
)
|
|
|
|
// - fixed length: 21 alphanumeric characters
|
|
// - only IBANs with CH or LI country code permitted
|
|
var iban = "CH0209000000870913543"
|
|
|
|
type Address struct {
|
|
AdrTp AddressType
|
|
Name string // Name, max 70. chars, first name + last name, or company name
|
|
StrtNmOrAdrLine1 string // Street or address line 1
|
|
BldgNbOrAdrLine2 string // Building number or address line 2
|
|
PstCd string // Postal code, max 16 chars, must be provided without a country code prefix
|
|
TwnNm string // Town, max. 35 chars
|
|
Ctry string // Country, two-digit country code according to ISO 3166-1
|
|
}
|
|
|
|
type QRCHHeader struct {
|
|
QRType string
|
|
Version string
|
|
Coding string
|
|
}
|
|
|
|
type QRCHCdtrInf struct {
|
|
IBAN string
|
|
Cdtr Address // Creditor
|
|
}
|
|
|
|
type QRCHCcyAmt struct {
|
|
Amt string // Amount
|
|
Ccy string // Currency
|
|
}
|
|
|
|
type QRCHRmtInfAddInf struct {
|
|
Ustrd string // Unstructured message
|
|
Trailer string // Trailer
|
|
}
|
|
|
|
type QRCHRmtInf struct {
|
|
Tp string // Reference type
|
|
Ref string // Reference
|
|
AddInf QRCHRmtInfAddInf // Additional information
|
|
}
|
|
|
|
type QRCH struct {
|
|
Header QRCHHeader // Header
|
|
CdtrInf QRCHCdtrInf // Creditor information (Account / Payable to)
|
|
UltmtCdtr Address // (must not be filled in, for Future Use)
|
|
CcyAmt QRCHCcyAmt // Paymount amount information
|
|
UltmtDbtr Address // Ultimate Debtor
|
|
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")
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// overlay the qr code with a Swiss Cross
|
|
combined, err := overlayWithSwissCross(code)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = combined
|
|
|
|
return 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
|
|
}
|
|
|
|
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()
|
|
|
|
// 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
|
|
|
|
// 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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f, err := os.Create("/tmp/code.svg")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
s := svg.New(f)
|
|
qrsvg := goqrsvg.NewQrSVG(code, 5)
|
|
qrsvg.StartQrSVG(s)
|
|
if err := qrsvg.WriteQrSVG(s); err != nil {
|
|
return err
|
|
}
|
|
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
|
|
}
|