Skip to content

Commit

Permalink
feat: svg generator
Browse files Browse the repository at this point in the history
  • Loading branch information
piglig committed Dec 24, 2023
1 parent 7e225ce commit d82145c
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 15 deletions.
87 changes: 81 additions & 6 deletions qr_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"image/png"
"math"
"os"
"path/filepath"
"strings"
)

// Ecc is the representation of an error correction level in a QR Code symbol.
Expand Down Expand Up @@ -80,6 +82,18 @@ func NewQrCodeImgConfig(scale int, border int) *QrCodeImgConfig {
return &QrCodeImgConfig{scale: scale, border: border, light: color.White, dark: color.Black}
}

func (q *QrCodeImgConfig) Valid() error {
if q.scale <= 0 {
return errors.New("scale must be positive")
}

if q.border < 0 {
return errors.New("border must be non-negative")
}

Check warning on line 92 in qr_code.go

View check run for this annotation

Codecov / codecov/patch

qr_code.go#L91-L92

Added lines #L91 - L92 were not covered by tests

return nil
}

// Light gets light color from QrCodeImgConfig
func (q *QrCodeImgConfig) Light() color.Color {
return q.light
Expand Down Expand Up @@ -563,12 +577,9 @@ func (q *QrCode) drawFormatBits(msk int) {

// PNG generates a PNG image file for the QR code with QrCodeImgConfig and saves it to given file path
func (q *QrCode) PNG(config *QrCodeImgConfig, filePath string) error {
if q == nil || config == nil {
return errors.New("qr code or config is nil")
}

if config.scale <= 0 || config.border < 0 {
return errors.New("invalid input")
err := config.Valid()
if err != nil {
return err
}

// Ensure that the border size combined with QR code size does not exceed the maximum allowed integer value after scaling.
Expand Down Expand Up @@ -615,6 +626,70 @@ func (q *QrCode) writePng(img *image.RGBA, filepath string) error {
return nil
}

// SVG generates a SVG file for the QR code with QrCodeImgConfig, light, dark color and saves it to given file path
func (q *QrCode) SVG(config *QrCodeImgConfig, filePath, light, dark string) error {
err := config.Valid()
if err != nil {
return err
}

if ext := filepath.Ext(filePath); ext != ".svg" {
return fmt.Errorf("file type:%v invalid", ext)
}

svg := q.toSVGString(config, light, dark)
return q.writeSVG(svg, filePath)
}

// toSVGString generates a SVG string image with QrCodeImgConfig, light and dark color
func (q *QrCode) toSVGString(config *QrCodeImgConfig, lightColor, darkColor string) string {
brd := int64(config.border)
scl := int64(config.scale)
size := int64(q.GetSize())

sb := strings.Builder{}
sb.Grow(128)
sb.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
sb.WriteString("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
sb.WriteString(fmt.Sprintf("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 %d %d\" stroke=\"none\">\n",
(size*scl)+brd*2, (size*scl)+brd*2))
sb.WriteString(fmt.Sprintf("\t<rect width=\"%d\" height=\"%d\" fill=\"%s\"/>\n", (size*scl)+brd*2, (size*scl)+brd*2, lightColor))
sb.WriteString("\t<path d=\"")

for y := int64(0); y < size; y++ {
for x := int64(0); x < size; x++ {
if q.GetModule(int(x), int(y)) {
sb.WriteString(fmt.Sprintf("M%d,%dh%dv%dh-%dz ", (x*scl)+brd, (y*scl)+brd, scl, scl, scl))
}
}
}

// Trim the last space for neatness
pathData := strings.TrimSpace(sb.String())
sb.Reset() // Reset the builder before writing the final path data
sb.WriteString(pathData)

sb.WriteString(fmt.Sprintf("\" fill=\"%s\"/>\n", darkColor))
sb.WriteString("</svg>\n")

return sb.String()
}

// writeSVG writes a SVG file to the path of the SVG file
func (q *QrCode) writeSVG(svgStr, filePath string) error {
svgFile, err := os.Create(filePath)
if err != nil {
return err
}

Check warning on line 683 in qr_code.go

View check run for this annotation

Codecov / codecov/patch

qr_code.go#L682-L683

Added lines #L682 - L683 were not covered by tests
defer svgFile.Close()

_, err = svgFile.WriteString(svgStr)
if err != nil {
return fmt.Errorf("failed to write SVG: %w", err)
}

Check warning on line 689 in qr_code.go

View check run for this annotation

Codecov / codecov/patch

qr_code.go#L688-L689

Added lines #L688 - L689 were not covered by tests
return nil
}

// EncodeText takes a string and an error correction level (ecl),
// encodes the text to segments and returns a QR code or an error.
func EncodeText(text string, ecl Ecc) (*QrCode, error) {
Expand Down
82 changes: 73 additions & 9 deletions qr_code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,6 @@ func TestQrCode_PNG(t *testing.T) {
dest: "",
config: NewQrCodeImgConfig(10, 3),
},
{
text: "",
wantErr: true,
ecl: Low,
dest: "",
config: nil,
},
}

for _, tt := range tests {
Expand All @@ -295,14 +288,14 @@ func TestQrCode_PNG(t *testing.T) {
dest := filepath.Join(tempDir, tt.dest)
err = qr.PNG(tt.config, dest)
if (err != nil) != tt.wantErr {
t.Errorf("TestQrCode_ToPNG() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("TestQrCode_PNG() error = %v, wantErr %v", err, tt.wantErr)
return
}

if err == nil {
_, err = os.Stat(dest)
if err != nil {
t.Errorf("TestQrCode_ToPNG() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("TestQrCode_PNG() error = %v, wantErr %v", err, tt.wantErr)
return
}
}
Expand Down Expand Up @@ -408,3 +401,74 @@ func TestNewQrCodeImgConfig(t *testing.T) {
})
}
}

func TestQrCode_SVG(t *testing.T) {
tempDir := t.TempDir()
defer os.RemoveAll(tempDir)
tests := []struct {
text string
wantErr bool
ecl Ecc
dest string
config *QrCodeImgConfig
}{
{
text: "Hello, world!",
wantErr: false,
ecl: Low,
dest: "hello-world-QR.svg",
config: NewQrCodeImgConfig(10, 4),
},
{
text: "",
wantErr: false,
ecl: Low,
dest: "empty-QR.svg",
config: NewQrCodeImgConfig(10, 4),
},
{
text: "こんにちwa、世界! αβγδ",
wantErr: false,
ecl: Quartile,
dest: "unicode-QR.svg",
config: NewQrCodeImgConfig(10, 3),
},
{
text: "aabbcc",
wantErr: true,
ecl: Quartile,
dest: "aabbcc-QR.svg",
config: NewQrCodeImgConfig(-10, -3),
},
{
text: "aabbcc",
wantErr: true,
ecl: Low,
dest: "",
config: NewQrCodeImgConfig(10, 3),
},
}

for _, tt := range tests {
qr, err := EncodeText(tt.text, tt.ecl)
if err != nil {
t.Errorf("EncodeText() error = %v", err)
return
}

dest := filepath.Join(tempDir, tt.dest)
err = qr.SVG(tt.config, dest, "#FFFFFF", "#000000")
if (err != nil) != tt.wantErr {
t.Errorf("TestQrCode_SVG() error = %v, text = %v, wantErr %v", err, tt.text, tt.wantErr)
return
}

if err == nil {
_, err = os.Stat(dest)
if err != nil {
t.Errorf("TestQrCode_SVG() error = %v, wantErr %v", err, tt.wantErr)
return
}
}
}
}

0 comments on commit d82145c

Please sign in to comment.