git.sophuwu.com > qrstr
made text encoder, html still to do
sophuwu sophie@skisiel.com
Thu, 08 May 2025 18:28:06 +0200
commit

5889a953806b5c61bd01835c93d973faf1ab0dc6

4 files changed, 252 insertions(+), 0 deletions(-)

jump to
A .gitignore

@@ -0,0 +1,3 @@

+.idea +build/ +test/
A go.mod

@@ -0,0 +1,5 @@

+module git.sophuwu.com/qrstr + +go 1.24.2 + +require github.com/boombuler/barcode v1.0.2
A go.sum

@@ -0,0 +1,2 @@

+github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= +github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
A qr.go

@@ -0,0 +1,242 @@

+package qrstr + +/* + * This file is to turn strings into unicode or html qr codes. + * Author: sophuwu <sophie@skisiel.com> + * Feel free to use this code in any way you want. + * Just call QR("string", header bool, html bool) to get a qr code. + * The header will display the string above the qr code. + */ + +import ( + "fmt" + "github.com/boombuler/barcode/qr" + "image" + "image/color" + "slices" + "strings" +) + +// utf8r aliases rune +type utf8r rune + +// String returns the plain text or html representation of the rune +func (u utf8r) String(html bool) string { + if html { + return fmt.Sprintf("<td>&#x%x;</td>", u) + } + return string(u) +} + +const blank rune = ' ' +const upper rune = '▀' +const lower rune = '▄' +const whole rune = '█' + +// pad returns a string with the given length +func pad(n int, r ...rune) string { + if n == 0 { + return "" + } + if n < 0 { + n = 1 + } + if len(r) == 0 { + return strings.Repeat(string(whole), n) + } + return strings.Repeat(string(r[0]), n) +} + +type runeCol []rune + +func (c *runeCol) getRune(top, bot color.Color) rune { + return (*c)[func() int { + i := 0 + if top == color.Black { + i |= 1 + } + if bot == color.Black { + i |= 2 + } + return i + }()] +} + +func (c *runeCol) addRune(s *string, top, bot color.Color) { + *s += string(c.getRune(top, bot)) +} + +var lightMode = runeCol{blank, upper, lower, whole} +var darkMode = runeCol{whole, lower, upper, blank} + +// wrap wraps text around a newline, hard wrapping at the given width +// but trying to soft wrap if possible +func wrap(w int, s ...string) []string { + var line = "" + var lines, b []string + var v string + var i, j int + w-- + for _, l := range s { + if len(l) < w { + lines = append(lines, l) + continue + } + + b = strings.Split(l, " ") + for i, v = range b { + if len(v) > w { + for j = 0; j < len(v); j += w { + if j+w < len(v) { + lines = append(lines, v[j:j+w]+"-") + } else { + line = v[j:] + " " + } + } + continue + } + if len(line)+len(v) < w { + line += v + " " + } else { + lines = append(lines, strings.TrimSuffix(line, " ")) + line = "" + continue + } + if i == len(b)-1 { + lines = append(lines, strings.TrimSuffix(line, " ")) + line = "" + continue + } + } + } + + return slices.Clip(lines) +} + +type qrEncoder struct { + strFunc func(rc *runeCol, code *image.Image, headers *[]string) (string, error) + rc *runeCol + errCorr errorCorrectionLevel +} + +func (q *qrEncoder) Encode(data string, header ...string) (string, error) { + strFunc := q.strFunc + if strFunc == nil { + return "", fmt.Errorf("encoder misconfigured, use NewEncoder when creating it") + } + var code image.Image + var err error + code, err = qr.Encode(data, qr.ErrorCorrectionLevel((*q).errCorr), qr.Auto) + if err != nil { + return "", err + } + return strFunc(q.rc, &code, &header) +} + +func text(rc *runeCol, code *image.Image, headers *[]string) (string, error) { + if rc == nil || code == nil { + return "", fmt.Errorf("encoder misconfigured, use NewEncoder when creating it") + } + var output = "" + dx := (*code).Bounds().Dx() + dy := (*code).Bounds().Dy() + wr := rc.getRune(color.White, color.White) + prefix := "" + suffix := "\n" + + hashead := headers != nil && len(*headers) > 0 + + if hashead { + output += fmt.Sprintln(string(whole) + pad(dx+2, upper) + string(whole)) + for _, v := range wrap(dx, *headers...) { + v = v + pad(dx-len(v)+1, blank) + string(whole) + v = string(whole) + string(blank) + v + output += v + "\n" + } + + output += string(whole) + pad(dx+2, lower) + string(whole) + "\n" + string(whole) + pad(dx+2, wr) + string(whole) + "\n" + prefix = string(whole) + string(wr) + suffix = string(wr) + string(whole) + "\n" + } + + output += prefix + prefix = suffix + prefix + + var y, x int + for y = 0; y < dy-dy%2; y += 2 { + for x = 0; x < dx; x++ { + rc.addRune(&output, (*code).At(x, y), (*code).At(x, y+1)) + } + output += prefix + } + if dy%2 == 1 { + for x = 0; x < dx; x++ { + rc.addRune(&output, (*code).At(x, y), color.White) + } + output += suffix + } else { + output = strings.TrimSuffix(output, prefix) + suffix + } + + if hashead { + output += string(whole) + pad(dx+2, lower) + string(whole) + "\n" + } + + return output, nil +} + +func html(rc *runeCol, code *image.Image, headers *[]string) (string, error) { + var output = "" + return output, nil +} + +type encoderType int +type errorCorrectionLevel qr.ErrorCorrectionLevel + +const ( + // TextDarkMode makes qr codes for printing on dark backgrounds with white text, + // like a terminal or a screen with dark/night mode enabled. + // MUST BE PRINTED/DISPLAYED USING A MONOSPACE FONT. + TextDarkMode encoderType = 0 + // TextLightMode makes qr codes for printing on light backgrounds with black text, + // like paper or a screen with light mode. + // MUST BE PRINTED/DISPLAYED USING A MONOSPACE FONT. + TextLightMode encoderType = 1 + // HTMLMode makes qr codes for embedding in HTML documents or web pages. + // Generates a table using HTML tags, does not require a monospace font. + HTMLMode encoderType = 2 + + // ErrorCorrection7Percent indicates 7% of lost data can be recovered, makes the qr code smaller + ErrorCorrection7Percent errorCorrectionLevel = 0 + // ErrorCorrection15Percent indicates 15% of lost data can be recovered, default + ErrorCorrection15Percent errorCorrectionLevel = 1 + // ErrorCorrection25Percent indicates 25% of lost data can be recovered, makes the qr code bigger + ErrorCorrection25Percent errorCorrectionLevel = 2 + // ErrorCorrection30Percent indicates 30% of lost data can be recovered, makes the qr code very big + ErrorCorrection30Percent errorCorrectionLevel = 3 +) + +// NewEncoder returns a new qrEncoder code with the given data and headers +func NewEncoder(encoderType encoderType, errorCorrectionLevel errorCorrectionLevel) (*qrEncoder, error) { + var q qrEncoder + switch encoderType { + case TextDarkMode: + q.rc = &darkMode + q.strFunc = text + break + case TextLightMode: + q.rc = &lightMode + q.strFunc = text + break + case HTMLMode: + q.strFunc = html + break + default: + return nil, fmt.Errorf("invalid encoder type: %d", encoderType) + } + if errorCorrectionLevel < 0 || errorCorrectionLevel > 3 { + return nil, fmt.Errorf("invalid error correction level: %d", errorCorrectionLevel) + } + q.errCorr = errorCorrectionLevel + return &q, nil +}