From c63152fc39f885f2f6df3a171b2015a10a2905c0 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 7 Nov 2020 13:20:39 +0100 Subject: [PATCH] re-implement SVG version from scratch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- GENERATED_swisscross.go | 2 ++ generate.go | 2 +- go.mod | 3 -- go.sum | 6 ---- qrbill.go | 43 +++++++++-------------- qrcodegenerator.go | 23 +++++++++---- qrcodegeneratorsvg.go | 76 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 112 insertions(+), 43 deletions(-) create mode 100644 qrcodegeneratorsvg.go diff --git a/GENERATED_swisscross.go b/GENERATED_swisscross.go index b54ffae..adeab15 100644 --- a/GENERATED_swisscross.go +++ b/GENERATED_swisscross.go @@ -3,5 +3,7 @@ package qrbill // Table of contents var swisscross = map[string][]byte{ "third_party/swiss-cross/CH-Kreuz_7mm/CH-Kreuz_7mm.png": swisscross_0, + "third_party/swiss-cross/CH-Kreuz_7mm/CH-Kreuz_7mm.svg": swisscross_1, } var swisscross_0 = []byte("\x89PNG \n\n\x00\x00\x00 IHDR\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\x00x\xf3\xc9\xda\x00\x00\x00 pHYs\x00\x00\\F\x00\x00\\F\x94CA\x00\x0080iTXtXML:com.adobe.xmp\x00\x00\x00\x00\x00\n\n \n \n Adobe Photoshop CC 2015.5 (Macintosh)\n 2017-06-07T09:36:22+02:00\n 2017-06-07T09:43:10+02:00\n 2017-06-07T09:43:10+02:00\n image/png\n 0\n xmp.iid:d37a2394-3f20-4130-9fc2-6cccaed7bb7f\n xmp.did:d37a2394-3f20-4130-9fc2-6cccaed7bb7f\n xmp.did:d37a2394-3f20-4130-9fc2-6cccaed7bb7f\n \n \n \n created\n xmp.iid:d37a2394-3f20-4130-9fc2-6cccaed7bb7f\n 2017-06-07T09:36:22+02:00\n Adobe Photoshop CC 2015.5 (Macintosh)\n \n \n \n 1\n 6000000/10000\n 6000000/10000\n 2\n 65535\n 166\n 166\n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\x98\xfd\xb9B\x00\x00\x00 cHRM\x00\x00\x87\n\x00\x00\x8c \x00\xb7\x00\x00\x80\xe8\x00\x00R\x00[\x00\x006\xaf\x00\x00A\xd0\xdci\x00\x00\x00\xb9IDATx\xda\xec\x97K\xc20 Dm\x87}\x8f\x94\xabs\xa4r\x80tX\xf0i\xde \x83Y\xa0d=Yv2\x9e\xb8\xaaÒ\xdcR\xea)E\xebD5\"6\xa7\x83\xa9fX\xdf`Q-W'\x88X\xfa2yM\xa6!\xeb\xe0D/\xd2wϻ\x008\xe2_\xc6})mD5\xc6}\x90\xdff\xfb\xa9SM\xf6/s +Rry\x9b\xdc\xe3\x9b: \x80\xd5\xcc\xfa\xddÍ8\x99>K\x82\xb8\xc5V\xd4!\xdd;U\xd3ȋ\xce0\x88^|az>\xbf\x85\x879\xd9!\xabE\x9d\x9ftҗuzl.>ՙ\xb4\x8c\xca(B\xaf\x00\x89\xabA\x00\x80\x85S\x00\x00\x00\x00IEND\xaeB`\x82") +var swisscross_1 = []byte("\n\n\n\n\n\n\n\n\n") diff --git a/generate.go b/generate.go index 1d31ef3..050155c 100644 --- a/generate.go +++ b/generate.go @@ -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" diff --git a/go.mod b/go.mod index 2e1d763..4b02798 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 3a9d933..bf9c193 100644 --- a/go.sum +++ b/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= diff --git a/qrbill.go b/qrbill.go index fb5a2b9..a0bde52 100644 --- a/qrbill.go +++ b/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 element: + cross = bytes.ReplaceAll(cross, []byte(``), nil) + // Overwrite position and size of the embedded 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 document: + return bytes.ReplaceAll(qrCodeSVG, []byte(``), append(cross, []byte("")...)), nil } func (b *Bill) EncodeToImage() (image.Image, error) { diff --git a/qrcodegenerator.go b/qrcodegenerator.go index d45fd3a..3ebe46b 100644 --- a/qrcodegenerator.go +++ b/qrcodegenerator.go @@ -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 } diff --git a/qrcodegeneratorsvg.go b/qrcodegeneratorsvg.go new file mode 100644 index 0000000..3202f54 --- /dev/null +++ b/qrcodegeneratorsvg.go @@ -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 +}