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:
File diff suppressed because one or more lines are too long
@@ -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
3
go.mod
@@ -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
6
go.sum
@@ -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=
|
||||||
|
|||||||
43
qrbill.go
43
qrbill.go
@@ -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) {
|
||||||
|
|||||||
@@ -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
76
qrcodegeneratorsvg.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user