目标:用 Go 解析 base64 二维码图片,并把结果解析成 TLV 结构,便于后续业务处理。

常见流程:

  1. base64 解码
  2. 解析成 image.Image
  3. QR 解码得到字符串
  4. TLV 拆解

如果二维码是 jpg/png 文件,也可以直接读取本地文件拿到 image.Image

示例(Go 版)

package main

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"image"
	"_ "image/jpeg"
	"_ "image/png"
	"strings"

	"github.com/makiuchi-d/gozxing"
	"github.com/makiuchi-d/gozxing/qrcode"
)

func decodeBase64Image(input string) (image.Image, error) {
	// 兼容 data:image/png;base64,... 这类前缀
	if i := strings.IndexByte(input, ','); i > 0 && strings.Contains(input[:i], "base64") {
		input = input[i+1:]
	}

	data, err := base64.StdEncoding.DecodeString(input)
	if err != nil {
		return nil, fmt.Errorf("decode base64: %w", err)
	}

	img, _, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		return nil, fmt.Errorf("decode image: %w", err)
	}
	return img, nil
}

func recognizeQR(input string) (string, error) {
	img, err := decodeBase64Image(input)
	if err != nil {
		return "", err
	}

	bmp, err := gozxing.NewBinaryBitmapFromImage(img)
	if err != nil {
		return "", fmt.Errorf("build bitmap: %w", err)
	}

	reader := qrcode.NewQRCodeReader()
	result, err := reader.Decode(bmp, nil)
	if err != nil {
		return "", fmt.Errorf("decode qr: %w", err)
	}
	return result.String(), nil
}

type TLV struct {
	Tag    string
	Length int
	Value  string
}

func parseTLV(input string) ([]TLV, error) {
	if len(input)%2 != 0 {
		return nil, errors.New("invalid TLV length")
	}

	var out []TLV
	for i := 0; i < len(input); {
		if i+4 > len(input) {
			return nil, errors.New("invalid TLV header")
		}
		tag := input[i : i+2]
		length := 0
		_, err := fmt.Sscanf(input[i+2:i+4], "%02d", &length)
		if err != nil {
			return nil, fmt.Errorf("invalid TLV length: %w", err)
		}
		i += 4
		if i+length > len(input) {
			return nil, errors.New("invalid TLV value length")
		}
		value := input[i : i+length]
		i += length
		out = append(out, TLV{Tag: tag, Length: length, Value: value})
	}
	return out, nil
}

func main() {
	qrBase64 := "iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAQMAAAD58POIAAAABlBMVEX///8AAABVwtN+AAACU0lEQVR42tTSsY3mKhQFYEhMC5cEWjOJaQESAwluARLTGiS4BUjM0/warfRm7hawJ/wkW9yjQ/7lyBV5e3LInYc1MABa5lWJe/nTPGDAZ/C88tCiUCXhEHV7ibuFzH+DMlso08sQcADqBdGJP96G73f8ALnyn3zf8gO+ItiQXXfzp4L/Ac9eXq+cocoKOwYbAXGSU3fbaUeBzdetaJMLucyEAb0J6+LYVDzIuWMgXLldtbmCXRUwoP0AvvphZyVuYED08gdfud2CZxTY2E/eya7as6rBQOg188tfPqNQAwMCZCP7qUJb0aEAMrTQWSlVlVwx4Ldaa12vOQihAwOgQ8UN7DNDsijIF+iMuu96DuYx4C23fMN+Mi9DwoBsdL2nnFXYbj0GtLxATjlAGDl2DPgAWyKAbTMJg4GwJRFjc7llvgkKIMwmADY3QOwoyLluVmZpoXxK/wW8gxDgSjwP3SoG7AmV+a/jr++d/oTt4F4cLA9dQXkU1JV4eQlNxHzm8AtkIsaVTmc1RHsM6CxDCD6fSFgYGADokI5D+699Ggw2OnZ1qxKGDdfAQEbWVTx2oe/z88kvIMxvqnUih55px4C+rg0g5jwPNSsKqzyJ5tC8SmZHobsb4NDvrqMgGAjnaW7p5NE+CQXmzelKN3LIaisGRL0gvmYJdi0cCGzSA6jXJTAYyBVWe3me3ubsMQB6Ne8GqJuNz7R/AZ83S5uct7rKpyAEItmOTT7zKuEvcNu0gUxEmAMwABoS8zrqyD///A1ylavbKluE3QAG/2z+CwAA//+3BLtVXLXAxAAAAABJRU5ErkJggg=="

	content, err := recognizeQR(qrBase64)
	if err != nil {
		panic(err)
	}
	fmt.Println("QR:", content)

	tags, err := parseTLV(content)
	if err != nil {
		panic(err)
	}
	fmt.Printf("TLV: %+v\n", tags)
}

JS 版 TLV 解析

const qris = "00020101021226680016ID.CO.TELKOM.WWW011893600898025599662702150001952559966270303UMI51440014ID.CO.QRIS.WWW0215ID10200211817450303UMI520457325303360540825578.005502015802ID5916InterActive Corp6013KOTA SURABAYA61056013662130509413255111630439B7";

const tags = [];
let i = 0;

while (i < qris.length) {
  const tag = qris.substring(i, i + 2);
  i += 2;

  const valueLength = Number(qris.substring(i, i + 2));
  i += 2;

  const value = qris.substring(i, i + valueLength);
  i += valueLength;

  tags.push({ tag, length: valueLength, value });
}

console.log(tags);

小结:解码二维码本质是“图片解析 + QR 解码”,TLV 只是字符串拆分。把流程拆开,就很好维护。