From f2f14f20cc4c6b19935453aa03bf5fe908539636 Mon Sep 17 00:00:00 2001 From: yeqown Date: Thu, 22 Sep 2022 13:48:08 +0800 Subject: [PATCH] feat(issue#69): implement a compressed writer, ref issue#69 (#72) implement a compressed writer, ref issue#69 --- .issues/go.mod | 9 +++ .issues/issue69/issue69.go | 141 ++++++++++++++++++++++++++++++++++++ go.work | 9 +++ writer/compressed/go.mod | 7 ++ writer/compressed/writer.go | 91 +++++++++++++++++++++++ 5 files changed, 257 insertions(+) create mode 100644 .issues/go.mod create mode 100644 .issues/issue69/issue69.go create mode 100644 go.work create mode 100644 writer/compressed/go.mod create mode 100644 writer/compressed/writer.go diff --git a/.issues/go.mod b/.issues/go.mod new file mode 100644 index 0000000..18695f1 --- /dev/null +++ b/.issues/go.mod @@ -0,0 +1,9 @@ +module issues + +go 1.19 + +require ( + github.com/yeqown/go-qrcode/v2 v2.2.0 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e +) + diff --git a/.issues/issue69/issue69.go b/.issues/issue69/issue69.go new file mode 100644 index 0000000..49518fe --- /dev/null +++ b/.issues/issue69/issue69.go @@ -0,0 +1,141 @@ +/* + * Link: https://github.com/yeqown/go-qrcode/issues/69 + * Title: Feature(image-compression): PNG bit depth must be 1 + * Author: stokito(https://github.com/stokito) + */ + +package main + +import ( + "fmt" + "image" + "image/png" + "io" + "os" + "strings" + + skip2 "github.com/skip2/go-qrcode" + + yeqown "github.com/yeqown/go-qrcode/v2" + yeqownwstd "github.com/yeqown/go-qrcode/writer/compressed" +) + +/* +See https://github.com/yeqown/go-qrcode/issues/69 +Results: (unit: B) +Content length: 158 // source text length +qr-skip2-best.png: 641 // skip2 generated size +qr-yeqown-best.png: 958 // yeqown best compression level +*/ +func main() { + // some json + content := "{\n \"id\": \"beb6c04733d1a2da0f87c910a384\",\n \"tmax\": 895,\n \"cur\": [\n \"USD\"\n ],\n \"imp\": [\n {\n \"id\": \"21292\",\n \"instl\": 0,\n \"secure\": 0}\n" + // strip json to alpha num just for a test + content = strings.Replace(content, "{", "$", -1) + content = strings.Replace(content, "}", "%", -1) + content = strings.Replace(content, "\n", " ", -1) + content = strings.Replace(content, "\"", "*", -1) + content = strings.Replace(content, ",", "/", -1) + content = strings.Replace(content, "[", "/", -1) + content = strings.Replace(content, "]", "/", -1) + content = strings.ToUpper(content) + fmt.Printf("Content lenght: %d\n", len(content)) + //imgEncoderNone := CustomPngEncoder{png.NoCompression} + imgEncoderBest := CustomPngEncoder{png.BestCompression} + + encodeWithYeqown(content, "qr-yeqown.png") + //encodeWithYeqownCompression(content, "qr-yeqown-none.png", imgEncoderNone) + encodeWithYeqownCompression(content, "qr-yeqown-best.png", imgEncoderBest) + encodeWithSkip2(content, "qr-skip2-best.png") +} + +func encodeWithSkip2(content, name string) { + //err := skip2.WriteFile(content, skip2.Highest, 0, name) + //if err != nil { + // log.Fatal(err) + //} + q, err := skip2.New(content, skip2.Highest) + if err != nil { + panic(err) + } + err = q.WriteFile(0, name) + if err != nil { + panic(err) + } + stat, _ := os.Stat(name) + fmt.Printf("%s: %v\n", name, stat.Size()) +} + +type CustomPngEncoder struct { + CompressionLevel png.CompressionLevel +} + +func (j CustomPngEncoder) Encode(w io.Writer, img image.Image) error { + pngEncoder := png.Encoder{CompressionLevel: j.CompressionLevel} + return pngEncoder.Encode(w, img) +} + +func encodeWithYeqown(content, name string) { + qrc, err := yeqown.NewWith(content, + yeqown.WithEncodingMode(yeqown.EncModeAlphanumeric), + yeqown.WithErrorCorrectionLevel(yeqown.ErrorCorrectionHighest), + ) + if err != nil { + panic(err) + } + + option := yeqownwstd.Option{ + Padding: 4, + BlockSize: 1, + } + stdw, err := yeqownwstd.New(name, &option) + if err != nil { + panic(err) + } + + if err := qrc.Save(stdw); err != nil { + panic(err) + } + + stat, _ := os.Stat(name) + fmt.Printf("%s: %v\n", name, stat.Size()) +} + +func encodeWithYeqownCompression(content, name string, imageEncoder CustomPngEncoder) { + //cfg := yeqown.DefaultConfig() + //cfg.EncMode = yeqown.EncModeAlphanumeric + ////cfg.EncMode = yeqown.EncModeByte + //cfg.EcLevel = yeqown.ErrorCorrectionHighest + // + //imgOpts := yeqown.WithCustomImageEncoder(imageEncoder) + //imgOpts2 := yeqown.WithQRWidth(1) + //imgOpts3 := yeqown.WithBorderWidth(4) + //qrc, err := yeqown.NewWithConfig(content, cfg, imgOpts, imgOpts2, imgOpts3) + //if err != nil { + // panic(err) + //} + + qrc, err := yeqown.NewWith(content, + yeqown.WithEncodingMode(yeqown.EncModeAlphanumeric), + yeqown.WithErrorCorrectionLevel(yeqown.ErrorCorrectionHighest), + ) + if err != nil { + panic(err) + } + + option := yeqownwstd.Option{ + Padding: 4, + BlockSize: 1, + } + stdw, err := yeqownwstd.New(name, &option) + if err != nil { + panic(err) + } + + if err := qrc.Save(stdw); err != nil { + panic(err) + } + + stat, _ := os.Stat(name) + fmt.Printf("%s: %v\n", name, stat.Size()) +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..29de310 --- /dev/null +++ b/go.work @@ -0,0 +1,9 @@ +go 1.19 + +use ( + . + ./.issues + ./writer/compressed + ./writer/standard + ./writer/terminal +) diff --git a/writer/compressed/go.mod b/writer/compressed/go.mod new file mode 100644 index 0000000..f400c7f --- /dev/null +++ b/writer/compressed/go.mod @@ -0,0 +1,7 @@ +module github.com/yeqown/go-qrcode/writer/compressed + +go 1.19 + +require ( + github.com/yeqown/go-qrcode/v2 v2.2.0 +) \ No newline at end of file diff --git a/writer/compressed/writer.go b/writer/compressed/writer.go new file mode 100644 index 0000000..b732151 --- /dev/null +++ b/writer/compressed/writer.go @@ -0,0 +1,91 @@ +package compressed + +import ( + "image" + "image/color" + "image/png" + "io" + "os" + + "github.com/yeqown/go-qrcode/v2" +) + +type Option struct { + Padding int + BlockSize int +} + +// compressedWriter implements issue#69, generating compressed images +// in some special situations, such as, network transferring. +// https://github.com/yeqown/go-qrcode/issues/69 +type compressedWriter struct { + fd io.WriteCloser + + option *Option +} + +var ( + backgroundColor = color.Gray{Y: 0xff} + foregroundColor = color.Gray{Y: 0x00} +) + +func New(filename string, opt *Option) (qrcode.Writer, error) { + fd, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return nil, err + } + + return compressedWriter{fd: fd, option: opt}, nil +} + +func (w compressedWriter) Write(mat qrcode.Matrix) error { + padding := w.option.Padding + blockWidth := w.option.BlockSize + width := mat.Width()*blockWidth + 2*padding + height := width + + img := image.NewPaletted( + image.Rect(0, 0, width, height), + color.Palette([]color.Color{backgroundColor, foregroundColor}), + ) + bgColor := uint8(img.Palette.Index(backgroundColor)) + fgColor := uint8(img.Palette.Index(foregroundColor)) + + rectangle := func(x1, y1 int, x2, y2 int, img *image.Paletted, color uint8) { + for x := x1; x < x2; x++ { + for y := y1; y < y2; y++ { + pos := img.PixOffset(x, y) + img.Pix[pos] = color + } + } + } + + // background + rectangle(0, 0, width, height, img, bgColor) + + mat.Iterate(qrcode.IterDirection_COLUMN, func(x int, y int, v qrcode.QRValue) { + sx := x*blockWidth + padding + sy := y*blockWidth + padding + es := (x+1)*blockWidth + padding + ey := (y+1)*blockWidth + padding + + if v.IsSet() { + rectangle(sx, sy, es, ey, img, fgColor) + } + + //switch v.IsSet() { + //case false: + // gray = backgroundColor + //default: + // gray = foregroundColor + //} + + }) + + encoder := png.Encoder{CompressionLevel: png.BestCompression} + return encoder.Encode(w.fd, img) +} + +func (w compressedWriter) Close() error { + return w.fd.Close() +}