package qrstr /* * This file is to turn strings into unicode or html qr codes. * Author: sophuwu * Feel free to use this code in any way you want. * 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("&#x%x;", 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 Encoder struct { strFunc func(rc *runeCol, code *image.Image, headers *[]string) (string, error) rc *runeCol errCorr ErrorCorrectionLevel } // Encode encodes data with configuration from NewEncoder into a qr code string. // If headers are provided, they will be displayed above the qr code in the output. func (q *Encoder) Encode(data string, headers ...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, &headers) } 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 := string(wr) suffix := string(wr) + "\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" } else { output += pad(dx+2, wr) + "\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 += pad(dx+4, wr) + "\n" } else { output += pad(dx+2, wr) + "\n" } return output, nil } const css = ` ` func html(rc *runeCol, code *image.Image, headers *[]string) (string, error) { var output = "
\n" + css if code == nil { return "", fmt.Errorf("encoder misconfigured, use NewEncoder when creating it") } hashead := headers != nil && len(*headers) > 0 if hashead { for _, v := range *headers { output += "

" + v + "

\n" } output += "
\n" } dx := (*code).Bounds().Dx() dy := (*code).Bounds().Dy() output += "\n" for y := 0; y < dy; y++ { output += "\n" for x := 0; x < dx; x++ { if (*code).At(x, y) == color.Black { output += "\n" } else { output += "\n" } } output += "\n" } output += "
\n" 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. // This mode is not recommended for terminals, use TerminalMode instead. // MUST BE PRINTED/DISPLAYED USING A MONOSPACE FONT. TextDarkMode EncoderType = 0 // TextLightMode makes qr codes for printing on light backgrounds with black text, // like white paper or a screen with light mode. // This mode is not recommended for terminals, use TerminalMode instead. // MUST BE PRINTED/DISPLAYED USING A MONOSPACE FONT. TextLightMode EncoderType = 1 // HTMLMode makes qr codes for embedding in HTML documents or web pages. // Colours are set automatically with this mode. // Generates a table using HTML tags, does not require a monospace font. HTMLMode EncoderType = 2 // TerminalMode makes qr codes for printing on xterm terminals with auto color. // Colours are set automatically with this mode. // MUST BE PRINTED/DISPLAYED USING A MONOSPACE FONT. TerminalMode EncoderType = 3 // 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 qr encoder with the given type and error correction level. // The encoder type determines the output format of the qr code. // The error correction level determines the amount of data that can be recovered from the qr code. // The encoder type must be one of the following: TextDarkMode, TextLightMode, HTMLMode // The error correction level must be one of the following: ErrorCorrection7Percent, ErrorCorrection15Percent, ErrorCorrection25Percent, ErrorCorrection30Percent func NewEncoder(encoderType EncoderType, errorCorrectionLevel ErrorCorrectionLevel) (*Encoder, error) { var q Encoder 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 case TerminalMode: q.rc = &darkMode q.strFunc = func(rc *runeCol, code *image.Image, headers *[]string) (string, error) { s, e := text(rc, code, headers) if e != nil { return "", e } front := "\033[40;97m" back := "\033[0m\n" s = strings.ReplaceAll(s, "\n", back+front) return front + strings.TrimSuffix(s, front), nil } 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 }