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
|
||||
|
||||
//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
|
||||
|
||||
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
|
||||
github.com/makiuchi-d/gozxing v0.0.0-20200903113411-25f730ed83da
|
||||
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/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/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/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/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
|
||||
43
qrbill.go
43
qrbill.go
@@ -32,9 +32,8 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/aaronarduino/goqrsvg"
|
||||
svg "github.com/ajstarks/svgo"
|
||||
"github.com/boombuler/barcode/qr"
|
||||
"github.com/makiuchi-d/gozxing/qrcode/decoder"
|
||||
"github.com/makiuchi-d/gozxing/qrcode/encoder"
|
||||
|
||||
// We currently read the swiss cross PNG version.
|
||||
_ "image/png"
|
||||
@@ -265,37 +264,27 @@ type Bill struct {
|
||||
}
|
||||
|
||||
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%)
|
||||
|
||||
// Section 4.2.1: Character set:
|
||||
// UTF-8 should be used for encoding
|
||||
|
||||
code, err := qr.Encode(b.qrcontents, qr.M, qr.Unicode)
|
||||
var err error
|
||||
code, err := encoder.Encoder_encode(b.qrcontents, decoder.ErrorCorrectionLevel_M, qrEncodeHints())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
s := svg.New(&buf)
|
||||
qrsvg := goqrsvg.NewQrSVG(code, 5)
|
||||
qrsvg.StartQrSVG(s)
|
||||
if err := qrsvg.WriteQrSVG(s); err != nil {
|
||||
|
||||
const quietzone = 4
|
||||
qrCodeSVG, err := renderResultSVG(code, qrCodeEdgeSidePx, qrCodeEdgeSidePx, quietzone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Overlay the swiss cross (not entirely scaled correctly, but should be
|
||||
// good enough):
|
||||
const px = 5
|
||||
x := (30 - 4) * px
|
||||
y := (30 - 4) * px
|
||||
// overlay the swiss cross
|
||||
cross := swisscross["third_party/swiss-cross/CH-Kreuz_7mm/CH-Kreuz_7mm.svg"]
|
||||
// Remove XML document header, we embed the <svg> element:
|
||||
cross = bytes.ReplaceAll(cross, []byte(`<?xml version="1.0" encoding="utf-8"?>`), nil)
|
||||
// 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")
|
||||
s.Rect(x+0*px+2, y+0*px+2, 8*px-4, 8*px-4, "fill:#000000")
|
||||
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
|
||||
// Inject the swiss cross into the <svg> document:
|
||||
return bytes.ReplaceAll(qrCodeSVG, []byte(`</g>`), append(cross, []byte("</g>")...)), nil
|
||||
}
|
||||
|
||||
func (b *Bill) EncodeToImage() (image.Image, error) {
|
||||
|
||||
@@ -53,14 +53,25 @@ func generateSwissQrCode(payload string) (image.Image, error) {
|
||||
return overlayWithSwissCross(qrCodeImage)
|
||||
}
|
||||
|
||||
func generateQrCodeImage(payload string) (image.Image, error) {
|
||||
|
||||
w := qrcode.NewQRCodeWriter()
|
||||
hints := map[gozxing.EncodeHintType]interface{}{
|
||||
func qrEncodeHints() map[gozxing.EncodeHintType]interface{} {
|
||||
return map[gozxing.EncodeHintType]interface{}{
|
||||
// as per https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf, section 5.1:
|
||||
// Error correction level M (redundancy of around 15%)
|
||||
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 {
|
||||
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