re-implement SVG version from scratch

This implementation is using the bit matrix returned by zxing,
and then we do our own SVG rendering.

The SIX-supplied Swiss Cross SVG version is now used for the overlay.

The resulting SVG has been successfully tested in a number of different SVG
rendering engines:

• Google Chrome 86
• Firefox 82
• Emacs 26
• GIMP
• Inkscape
• Mobile Safari

When rendering the SVG onto 1265x1265 px at 600 dpi,
the resulting image matches the PNG version exactly.
This commit is contained in:
Michael Stapelberg
2020-11-07 13:20:39 +01:00
parent 11ded9c5ab
commit c63152fc39
7 changed files with 112 additions and 43 deletions

File diff suppressed because one or more lines are too long

View File

@@ -14,4 +14,4 @@
package qrbill package qrbill
//go:generate sh -c "go run third_party/goembed/goembed.go -package qrbill -var swisscross third_party/swiss-cross/CH-Kreuz_7mm/CH-Kreuz_7mm.png > GENERATED_swisscross.go && gofmt -w GENERATED_swisscross.go" //go:generate sh -c "go run third_party/goembed/goembed.go -package qrbill -var swisscross third_party/swiss-cross/CH-Kreuz_7mm/CH-Kreuz_7mm.png third_party/swiss-cross/CH-Kreuz_7mm/CH-Kreuz_7mm.svg > GENERATED_swisscross.go && gofmt -w GENERATED_swisscross.go"

3
go.mod
View File

@@ -3,13 +3,10 @@ module github.com/stapelberg/qrbill
go 1.14 go 1.14
require ( require (
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/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/makiuchi-d/gozxing v0.0.0-20200903113411-25f730ed83da github.com/makiuchi-d/gozxing v0.0.0-20200903113411-25f730ed83da
github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-isatty v0.0.12
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
golang.org/x/text v0.3.4 // indirect golang.org/x/text v0.3.4 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
) )

6
go.sum
View File

@@ -1,17 +1,11 @@
github.com/aaronarduino/goqrsvg v0.0.0-20170617203649-603647895681 h1:eZrVcUgy0P6+B6Vu7SKPh3UZQS5nEuyjhbkFyfz7I2I=
github.com/aaronarduino/goqrsvg v0.0.0-20170617203649-603647895681/go.mod h1:dytw+5qs+pdi61fO/S4OmXR7AuEq/HvNCuG03KxQHT4=
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA= github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA=
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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/makiuchi-d/gozxing v0.0.0-20200903113411-25f730ed83da h1:OgNu1PPD9EvZckyKDAc8DA4KymNXuc6vaCLsdOGyjOE= github.com/makiuchi-d/gozxing v0.0.0-20200903113411-25f730ed83da h1:OgNu1PPD9EvZckyKDAc8DA4KymNXuc6vaCLsdOGyjOE=
github.com/makiuchi-d/gozxing v0.0.0-20200903113411-25f730ed83da/go.mod h1:WoI7z45M7ZNA5BJxiJHaB+x7+k8S/3phW5Y13IR4yWY= github.com/makiuchi-d/gozxing v0.0.0-20200903113411-25f730ed83da/go.mod h1:WoI7z45M7ZNA5BJxiJHaB+x7+k8S/3phW5Y13IR4yWY=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
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=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=

View File

@@ -32,9 +32,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/aaronarduino/goqrsvg" "github.com/makiuchi-d/gozxing/qrcode/decoder"
svg "github.com/ajstarks/svgo" "github.com/makiuchi-d/gozxing/qrcode/encoder"
"github.com/boombuler/barcode/qr"
// We currently read the swiss cross PNG version. // We currently read the swiss cross PNG version.
_ "image/png" _ "image/png"
@@ -265,37 +264,27 @@ type Bill struct {
} }
func (b *Bill) EncodeToSVG() ([]byte, error) { func (b *Bill) EncodeToSVG() ([]byte, error) {
// as per https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf, section 5.1: var err error
// Error correction level M (redundancy of around 15%) code, err := encoder.Encoder_encode(b.qrcontents, decoder.ErrorCorrectionLevel_M, qrEncodeHints())
// Section 4.2.1: Character set:
// UTF-8 should be used for encoding
code, err := qr.Encode(b.qrcontents, qr.M, qr.Unicode)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var buf bytes.Buffer
s := svg.New(&buf) const quietzone = 4
qrsvg := goqrsvg.NewQrSVG(code, 5) qrCodeSVG, err := renderResultSVG(code, qrCodeEdgeSidePx, qrCodeEdgeSidePx, quietzone)
qrsvg.StartQrSVG(s) if err != nil {
if err := qrsvg.WriteQrSVG(s); err != nil {
return nil, err return nil, err
} }
// Overlay the swiss cross (not entirely scaled correctly, but should be // overlay the swiss cross
// good enough): cross := swisscross["third_party/swiss-cross/CH-Kreuz_7mm/CH-Kreuz_7mm.svg"]
const px = 5 // Remove XML document header, we embed the <svg> element:
x := (30 - 4) * px cross = bytes.ReplaceAll(cross, []byte(`<?xml version="1.0" encoding="utf-8"?>`), nil)
y := (30 - 4) * px // Overwrite position and size of the embedded <svg> element:
cross = bytes.ReplaceAll(cross, []byte(`x="0px" y="0px"`), []byte(`x="549" y="549" width="166" height="166"`))
s.Rect(x+0*px, y+0*px, 8*px, 8*px, "fill:#FFFFFF") // Inject the swiss cross into the <svg> document:
s.Rect(x+0*px+2, y+0*px+2, 8*px-4, 8*px-4, "fill:#000000") return bytes.ReplaceAll(qrCodeSVG, []byte(`</g>`), append(cross, []byte("</g>")...)), nil
s.Rect(x+3*px, y+1*px, 2*px, 6*px, "fill:#FFFFFF")
s.Rect(x+1*px, y+3*px, 6*px, 2*px, "fill:#FFFFFF")
s.End()
return buf.Bytes(), nil
} }
func (b *Bill) EncodeToImage() (image.Image, error) { func (b *Bill) EncodeToImage() (image.Image, error) {

View File

@@ -53,14 +53,25 @@ func generateSwissQrCode(payload string) (image.Image, error) {
return overlayWithSwissCross(qrCodeImage) return overlayWithSwissCross(qrCodeImage)
} }
func generateQrCodeImage(payload string) (image.Image, error) { func qrEncodeHints() map[gozxing.EncodeHintType]interface{} {
return map[gozxing.EncodeHintType]interface{}{
w := qrcode.NewQRCodeWriter() // as per https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf, section 5.1:
hints := map[gozxing.EncodeHintType]interface{}{ // Error correction level M (redundancy of around 15%)
gozxing.EncodeHintType_ERROR_CORRECTION: decoder.ErrorCorrectionLevel_M, gozxing.EncodeHintType_ERROR_CORRECTION: decoder.ErrorCorrectionLevel_M,
gozxing.EncodeHintType_CHARACTER_SET: common.CharacterSetECI_UTF8,
// Section 4.2.1: Character set:
// UTF-8 should be used for encoding
gozxing.EncodeHintType_CHARACTER_SET: common.CharacterSetECI_UTF8,
} }
matrix, err := w.Encode(payload, gozxing.BarcodeFormat_QR_CODE, qrCodeEdgeSidePx, qrCodeEdgeSidePx, hints) }
func generateQrCodeImage(payload string) (image.Image, error) {
matrix, err := qrcode.NewQRCodeWriter().Encode(
payload, // contents
gozxing.BarcodeFormat_QR_CODE, // format
qrCodeEdgeSidePx, // width
qrCodeEdgeSidePx, // height
qrEncodeHints()) // hints
if err != nil { if err != nil {
return nil, err return nil, err
} }

76
qrcodegeneratorsvg.go Normal file
View File

@@ -0,0 +1,76 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package qrbill
import (
"bytes"
svg "github.com/ajstarks/svgo"
"github.com/makiuchi-d/gozxing"
"github.com/makiuchi-d/gozxing/qrcode/encoder"
)
// renderResultSVG is a copy of renderResult from
// gozxing/qrcode/qrcode_writer.go, adapted to output to SVG.
func renderResultSVG(code *encoder.QRCode, width, height, quietZone int) ([]byte, error) {
input := code.GetMatrix()
if input == nil {
return nil, gozxing.NewWriterException("IllegalStateException")
}
inputWidth := input.GetWidth()
inputHeight := input.GetHeight()
qrWidth := inputWidth + (quietZone * 2)
qrHeight := inputHeight + (quietZone * 2)
outputWidth := qrWidth
if outputWidth < width {
outputWidth = width
}
outputHeight := qrHeight
if outputHeight < height {
outputHeight = height
}
multiple := outputWidth / qrWidth
if h := outputHeight / qrHeight; multiple > h {
multiple = h
}
// Padding includes both the quiet zone and the extra white pixels to accommodate the requested
// dimensions. For example, if input is 25x25 the QR will be 33x33 including the quiet zone.
// If the requested size is 200x160, the multiple will be 4, for a QR of 132x132. These will
// handle all the padding from 100x100 (the actual QR) up to 200x160.
leftPadding := (outputWidth - (inputWidth * multiple)) / 2
topPadding := (outputHeight - (inputHeight * multiple)) / 2
var buf bytes.Buffer
s := svg.New(&buf)
s.Start(outputWidth, outputHeight)
s.Rect(0, 0, outputWidth, outputHeight, "fill:white;stroke:white")
s.Group(`shape-rendering="crispEdges"`)
for inputY, outputY := 0, topPadding; inputY < inputHeight; inputY, outputY = inputY+1, outputY+multiple {
// Write the contents of this row of the barcode
for inputX, outputX := 0, leftPadding; inputX < inputWidth; inputX, outputX = inputX+1, outputX+multiple {
if input.Get(inputX, inputY) == 1 {
s.Rect(outputX, outputY, multiple, multiple, "fill:black;stroke:none;")
}
}
}
s.Gend()
s.End()
return buf.Bytes(), nil
}