From 9b0155c2ab50056ab473d8fc089a54eb8427e8ad Mon Sep 17 00:00:00 2001 From: Yeqown Date: Fri, 13 Mar 2026 16:28:06 +0800 Subject: [PATCH 01/13] feat(WIP): implementing kanji encoding mode --- encoder.go | 43 ++++++++++++++++++++++++++----------------- qrcode.go | 11 ++++++----- version.go | 5 +++-- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/encoder.go b/encoder.go index 73803ba..8dc4409 100644 --- a/encoder.go +++ b/encoder.go @@ -93,15 +93,17 @@ func newEncoder(m encMode, ec ecLevel, v version) *encoder { // Encode ... // 1. encode raw data into bitset // 2. append _defaultPadding data -func (e *encoder) Encode(byts []byte) (*binary.Binary, error) { +func (e *encoder) Encode(raw string) (*binary.Binary, error) { e.dst = binary.New() - e.data = byts + + // TODO: construct data []byte with encMode + e.data = []byte(raw) // append mode indicator symbol indicator := getEncodeModeIndicator(e.mode) e.dst.Append(indicator) // append chars length counter bits symbol - e.dst.AppendUint32(uint32(len(byts)), e.charCountBits()) + e.dst.AppendUint32(uint32(len(e.data)), e.charCountBits()) // encode data with specified mode switch e.mode { @@ -112,7 +114,7 @@ func (e *encoder) Encode(byts []byte) (*binary.Binary, error) { case EncModeByte: e.encodeByte() case EncModeJP: - panic("this has not been finished") + e.encodeKanji() } // fill and _defaultPadding bits @@ -179,6 +181,12 @@ func (e *encoder) encodeByte() { } } +// encodeKanji +// https://www.thonky.com/qr-code-tutorial/kanji-mode-encoding +func (e *encoder) encodeKanji() { + +} + // Break Up into 8-bit Codewords and Add Pad Bytes if Necessary func (e *encoder) breakUpInto8bit() error { // fill ending code (max 4bit) @@ -285,7 +293,7 @@ func encodeAlphanumericCharacter(v byte) uint32 { // analyzeEncFunc returns true is current byte matched in current mode, // otherwise means you should use a bigger character set to check. -type analyzeEncFunc func(byte) bool +type analyzeEncFunc func(rune) bool // analyzeEncodeModeFromRaw try to detect letter set of input data, // so that encoder can determine which mode should be use. @@ -295,31 +303,31 @@ type analyzeEncFunc func(byte) bool // case2: could not use EncModeNumeric, but you can find all of them in character mapping, use EncModeAlphanumeric. // case3: could not use EncModeAlphanumeric, but you can find all of them in ISO-8859-1 character set, use EncModeByte. // case4: could not use EncModeByte, use EncModeJP, no more choice. -func analyzeEncodeModeFromRaw(raw []byte) encMode { +func analyzeEncodeModeFromRaw(raw string) encMode { analyzeFnMapping := map[encMode]analyzeEncFunc{ EncModeNumeric: analyzeNum, EncModeAlphanumeric: analyzeAlphaNum, - EncModeByte: nil, + EncModeByte: analyzeByte, EncModeJP: nil, } var ( - f analyzeEncFunc - mode = EncModeNumeric + analyzeFn analyzeEncFunc + mode = EncModeNumeric ) // loop to check each character in raw data, // from low mode to higher while current mode could bearing the input data. for _, byt := range raw { reAnalyze: - if f = analyzeFnMapping[mode]; f == nil { + if analyzeFn = analyzeFnMapping[mode]; analyzeFn == nil { break } // issue#28 @borislavone reports this bug. // FIXED(@yeqown): next encMode analyzeVersionAuto func did not check the previous byte, // add goto statement to reanalyze previous byte which can't be analyzed in last encMode. - if !f(byt) { + if !analyzeFn(byt) { mode <<= 1 goto reAnalyze } @@ -329,12 +337,12 @@ func analyzeEncodeModeFromRaw(raw []byte) encMode { } // analyzeNum is byt in num encMode -func analyzeNum(byt byte) bool { +func analyzeNum(byt rune) bool { return byt >= '0' && byt <= '9' } // analyzeAlphaNum is byt in alpha number -func analyzeAlphaNum(byt byte) bool { +func analyzeAlphaNum(byt rune) bool { if (byt >= '0' && byt <= '9') || (byt >= 'A' && byt <= 'Z') { return true } @@ -345,7 +353,8 @@ func analyzeAlphaNum(byt byte) bool { return false } -//// analyzeByte is byt in bytes. -//func analyzeByte(byt byte) qrbool { -// return false -//} +// analyzeByte contains ISO-8859-1 character set +func analyzeByte(byt rune) bool { + // TODO: analyze input can be found in ISO-8859-1 character set. + return true +} diff --git a/qrcode.go b/qrcode.go index ee9b7a5..ea8c6a9 100644 --- a/qrcode.go +++ b/qrcode.go @@ -40,7 +40,7 @@ func toBytes[T ~string | ~[]byte](v T) []byte { func build(raw []byte, option *encodingOption) (*QRCode, error) { qrc := &QRCode{ - sourceRawBytes: raw, + sourceText: text, dataBSet: nil, mat: nil, ecBSet: nil, @@ -61,7 +61,8 @@ func build(raw []byte, option *encodingOption) (*QRCode, error) { // QRCode contains fields to generate QRCode matrix, outputImageOptions to Draw image, // etc. type QRCode struct { - sourceRawBytes []byte // raw Data to transfer + sourceText string // sourceText input text + // sourceRawBytes []byte // raw Data to transfer dataBSet *binary.Binary // final data bit stream of encode data mat *Matrix // matrix grid to store final bitmap @@ -98,7 +99,7 @@ func (q *QRCode) Dimension() int { func (q *QRCode) init() (err error) { // choose encode mode (num, alpha num, byte, Japanese) if q.encodingOption.EncMode == EncModeAuto { - q.encodingOption.EncMode = analyzeEncodeModeFromRaw(q.sourceRawBytes) + q.encodingOption.EncMode = analyzeEncodeModeFromRaw(q.sourceText) } // choose version @@ -149,7 +150,7 @@ func (q *QRCode) calcVersion() (ver *version, err error) { // automatically parse version if needAnalyze { // analyzeVersion the input data to choose to adapt version - analyzed, err2 := analyzeVersion(q.sourceRawBytes, opt.EcLevel, opt.EncMode) + analyzed, err2 := analyzeVersion(q.sourceText, opt.EcLevel, opt.EncMode) if err2 != nil { err = fmt.Errorf("calcVersion: analyzeVersionAuto failed: %v", err2) return nil, err @@ -180,7 +181,7 @@ func (q *QRCode) dataEncoding() (blocks []dataBlock, err error) { var ( bset *binary.Binary ) - bset, err = q.encoder.Encode(q.sourceRawBytes) + bset, err = q.encoder.Encode(q.sourceText) if err != nil { err = fmt.Errorf("could not encode data: %v", err) return diff --git a/version.go b/version.go index 641c65b..83cdee4 100644 --- a/version.go +++ b/version.go @@ -8,6 +8,7 @@ import ( // "github.com/skip2/go-qrcode/bitset" "github.com/yeqown/reedsolomon/binary" + "unicode/utf8" ) func init() { @@ -273,7 +274,7 @@ func loadVersion(lv int, ec ecLevel) version { // // check out http://muyuchengfeng.xyz/%E4%BA%8C%E7%BB%B4%E7%A0%81-%E5%AD%97%E7%AC%A6%E5%AE%B9%E9%87%8F%E8%A1%A8/ // for more details. -func analyzeVersion(raw []byte, ec ecLevel, mode encMode) (*version, error) { +func analyzeVersion(raw string, ec ecLevel, mode encMode) (*version, error) { step := 0 switch ec { case ErrorCorrectionLow: @@ -288,7 +289,7 @@ func analyzeVersion(raw []byte, ec ecLevel, mode encMode) (*version, error) { return nil, errInvalidErrorCorrectionLevel } - want, mark := len(raw), 0 + want, mark := utf8.RuneCountInString(raw), 0 for ; step < 160; step += 4 { switch mode { From de0a8e570b901af5c05bd1f3459e0105d9bb18cd Mon Sep 17 00:00:00 2001 From: yeqown Date: Sat, 18 May 2024 14:32:46 +0800 Subject: [PATCH 02/13] feat(chardet): split character analyze methods into chardet. WIP --- chardet.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ encoder.go | 68 -------------------------------------------------- 2 files changed, 73 insertions(+), 68 deletions(-) create mode 100644 chardet.go diff --git a/chardet.go b/chardet.go new file mode 100644 index 0000000..b155d36 --- /dev/null +++ b/chardet.go @@ -0,0 +1,73 @@ +package qrcode + +// TODO: +// chardet.go refer to https://github.com/chardet/chardet to detect input string's +// character set, to see any unsupported character encountered in the input string. + +// analyzeEncFunc returns true is current byte matched in current mode, +// otherwise means you should use a bigger character set to check. +type analyzeEncFunc func(rune) bool + +// analyzeEncodeModeFromRaw try to detect letter set of input data, +// so that encoder can determine which mode should be use. +// reference: https://en.wikipedia.org/wiki/QR_code +// +// case1: only numbers, use EncModeNumeric. +// case2: could not use EncModeNumeric, but you can find all of them in character mapping, use EncModeAlphanumeric. +// case3: could not use EncModeAlphanumeric, but you can find all of them in ISO-8859-1 character set, use EncModeByte. +// case4: could not use EncModeByte, use EncModeJP, no more choice. +func analyzeEncodeModeFromRaw(raw string) encMode { + analyzeFnMapping := map[encMode]analyzeEncFunc{ + EncModeNumeric: analyzeNum, + EncModeAlphanumeric: analyzeAlphaNum, + EncModeByte: analyzeByte, + EncModeJP: nil, + } + + var ( + analyzeFn analyzeEncFunc + mode = EncModeNumeric + ) + + // loop to check each character in raw data, + // from low mode to higher while current mode could bearing the input data. + for _, byt := range raw { + reAnalyze: + if analyzeFn = analyzeFnMapping[mode]; analyzeFn == nil { + break + } + + // issue#28 @borislavone reports this bug. + // FIXED(@yeqown): next encMode analyzeVersionAuto func did not check the previous byte, + // add goto statement to reanalyze previous byte which can't be analyzed in last encMode. + if !analyzeFn(byt) { + mode <<= 1 + goto reAnalyze + } + } + + return mode +} + +// analyzeNum is byt in num encMode +func analyzeNum(byt rune) bool { + return byt >= '0' && byt <= '9' +} + +// analyzeAlphaNum is byt in alpha number +func analyzeAlphaNum(byt rune) bool { + if (byt >= '0' && byt <= '9') || (byt >= 'A' && byt <= 'Z') { + return true + } + switch byt { + case ' ', '$', '%', '*', '+', '-', '.', '/', ':': + return true + } + return false +} + +// analyzeByte contains ISO-8859-1 character set +func analyzeByte(byt rune) bool { + // TODO: analyze input can be found in ISO-8859-1 character set. + return true +} diff --git a/encoder.go b/encoder.go index 8dc4409..fc706fc 100644 --- a/encoder.go +++ b/encoder.go @@ -290,71 +290,3 @@ func encodeAlphanumericCharacter(v byte) uint32 { return 0 } - -// analyzeEncFunc returns true is current byte matched in current mode, -// otherwise means you should use a bigger character set to check. -type analyzeEncFunc func(rune) bool - -// analyzeEncodeModeFromRaw try to detect letter set of input data, -// so that encoder can determine which mode should be use. -// reference: https://en.wikipedia.org/wiki/QR_code -// -// case1: only numbers, use EncModeNumeric. -// case2: could not use EncModeNumeric, but you can find all of them in character mapping, use EncModeAlphanumeric. -// case3: could not use EncModeAlphanumeric, but you can find all of them in ISO-8859-1 character set, use EncModeByte. -// case4: could not use EncModeByte, use EncModeJP, no more choice. -func analyzeEncodeModeFromRaw(raw string) encMode { - analyzeFnMapping := map[encMode]analyzeEncFunc{ - EncModeNumeric: analyzeNum, - EncModeAlphanumeric: analyzeAlphaNum, - EncModeByte: analyzeByte, - EncModeJP: nil, - } - - var ( - analyzeFn analyzeEncFunc - mode = EncModeNumeric - ) - - // loop to check each character in raw data, - // from low mode to higher while current mode could bearing the input data. - for _, byt := range raw { - reAnalyze: - if analyzeFn = analyzeFnMapping[mode]; analyzeFn == nil { - break - } - - // issue#28 @borislavone reports this bug. - // FIXED(@yeqown): next encMode analyzeVersionAuto func did not check the previous byte, - // add goto statement to reanalyze previous byte which can't be analyzed in last encMode. - if !analyzeFn(byt) { - mode <<= 1 - goto reAnalyze - } - } - - return mode -} - -// analyzeNum is byt in num encMode -func analyzeNum(byt rune) bool { - return byt >= '0' && byt <= '9' -} - -// analyzeAlphaNum is byt in alpha number -func analyzeAlphaNum(byt rune) bool { - if (byt >= '0' && byt <= '9') || (byt >= 'A' && byt <= 'Z') { - return true - } - switch byt { - case ' ', '$', '%', '*', '+', '-', '.', '/', ':': - return true - } - return false -} - -// analyzeByte contains ISO-8859-1 character set -func analyzeByte(byt rune) bool { - // TODO: analyze input can be found in ISO-8859-1 character set. - return true -} From e6538b9462120715a508263a8cee4b6177d9af8e Mon Sep 17 00:00:00 2001 From: yeqown Date: Sun, 9 Jun 2024 10:21:54 +0800 Subject: [PATCH 03/13] feat(encoder): remove data field from encoder, so that the Encode method could convert raw string into []byte according to the encode mode actually --- encoder.go | 78 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/encoder.go b/encoder.go index fc706fc..52585c5 100644 --- a/encoder.go +++ b/encoder.go @@ -7,13 +7,28 @@ import ( "log" "github.com/yeqown/reedsolomon/binary" + "strconv" ) -// encMode ... +// encMode indicates the encoding mode of the data to be encoded. +// The encoding mode is used to determine how the data should be encoded +// into bits for the QR code. This repository supports the following encoding +// modes: +// - EncModeNone: no encoding +// - EncModeNumeric: numeric encoding +// - EncModeAlphanumeric: alphanumeric encoding +// - EncModeByte: byte encoding +// - EncModeJP: japanese encoding +// +// The encoding mode is determined by the data to be encoded. For example, if +// the data to be encoded is all numeric, the encoding mode will be EncModeNumeric. +// If the data to be encoded is alphanumeric, the encoding mode will be EncModeAlphanumeric. +// You can also specify the encoding mode automatically by using EncModeAuto, which +// will automatically determine the encoding mode based on the data to be encoded. type encMode uint const ( - // a qrbool of EncModeAuto will trigger a detection of the letter set from the input data, + // EncModeAuto will trigger a detection of the letter set from the input data. EncModeAuto = 0 // EncModeNone mode ... EncModeNone encMode = 1 << iota @@ -46,7 +61,7 @@ func getEncModeName(mode encMode) string { case EncModeJP: return "japan" default: - return "unknown" + return "unknown(" + strconv.Itoa(int(mode)) + ")" } } @@ -69,8 +84,7 @@ func getEncodeModeIndicator(mode encMode) *binary.Binary { // encoder ... data to bit stream ... type encoder struct { // self init - dst *binary.Binary - data []byte // raw input data + dst *binary.Binary // initial params mode encMode // encode mode @@ -81,9 +95,14 @@ type encoder struct { } func newEncoder(m encMode, ec ecLevel, v version) *encoder { + switch m { + case EncModeNumeric, EncModeAlphanumeric, EncModeByte, EncModeJP: + default: + panic("unsupported data encoding mode in newEncoder()") + } + return &encoder{ dst: nil, - data: nil, mode: m, ecLv: ec, version: v, @@ -96,25 +115,34 @@ func newEncoder(m encMode, ec ecLevel, v version) *encoder { func (e *encoder) Encode(raw string) (*binary.Binary, error) { e.dst = binary.New() - // TODO: construct data []byte with encMode - e.data = []byte(raw) + var data []byte + switch e.mode { + case EncModeNumeric, EncModeAlphanumeric, EncModeByte: + data = []byte(raw) + case EncModeJP: + // TODO: construct data []byte from raw string + default: + log.Printf("unsupported encoding mode: %s", getEncModeName(e.mode)) + } // append mode indicator symbol indicator := getEncodeModeIndicator(e.mode) e.dst.Append(indicator) // append chars length counter bits symbol - e.dst.AppendUint32(uint32(len(e.data)), e.charCountBits()) + e.dst.AppendUint32(uint32(len(data)), e.charCountBits()) // encode data with specified mode switch e.mode { case EncModeNumeric: - e.encodeNumeric() + e.encodeNumeric(data) case EncModeAlphanumeric: - e.encodeAlphanumeric() + e.encodeAlphanumeric(data) case EncModeByte: - e.encodeByte() + e.encodeByte(data) case EncModeJP: - e.encodeKanji() + e.encodeKanji(data) + default: + log.Printf("unsupported encoding mode: %s", getEncModeName(e.mode)) } // fill and _defaultPadding bits @@ -126,20 +154,20 @@ func (e *encoder) Encode(raw string) (*binary.Binary, error) { } // 0001b mode indicator -func (e *encoder) encodeNumeric() { +func (e *encoder) encodeNumeric(data []byte) { if e.dst == nil { log.Println("e.dst is nil") return } - for i := 0; i < len(e.data); i += 3 { - charsRemaining := len(e.data) - i + for i := 0; i < len(data); i += 3 { + charsRemaining := len(data) - i var value uint32 bitsUsed := 1 for j := 0; j < charsRemaining && j < 3; j++ { value *= 10 - value += uint32(e.data[i+j] - 0x30) + value += uint32(data[i+j] - 0x30) bitsUsed += 3 } e.dst.AppendUint32(value, bitsUsed) @@ -147,18 +175,18 @@ func (e *encoder) encodeNumeric() { } // 0010b mode indicator -func (e *encoder) encodeAlphanumeric() { +func (e *encoder) encodeAlphanumeric(data []byte) { if e.dst == nil { log.Println("e.dst is nil") return } - for i := 0; i < len(e.data); i += 2 { - charsRemaining := len(e.data) - i + for i := 0; i < len(data); i += 2 { + charsRemaining := len(data) - i var value uint32 for j := 0; j < charsRemaining && j < 2; j++ { value *= 45 - value += encodeAlphanumericCharacter(e.data[i+j]) + value += encodeAlphanumericCharacter(data[i+j]) } bitsUsed := 6 @@ -171,20 +199,20 @@ func (e *encoder) encodeAlphanumeric() { } // 0100b mode indicator -func (e *encoder) encodeByte() { +func (e *encoder) encodeByte(data []byte) { if e.dst == nil { log.Println("e.dst is nil") return } - for _, b := range e.data { + for _, b := range data { _ = e.dst.AppendByte(b, 8) } } // encodeKanji // https://www.thonky.com/qr-code-tutorial/kanji-mode-encoding -func (e *encoder) encodeKanji() { - +func (e *encoder) encodeKanji(data []byte) { + // TODO: implement encodeKanji } // Break Up into 8-bit Codewords and Add Pad Bytes if Necessary From d8b99cfe55809ec98bf905443edca76993bdc937 Mon Sep 17 00:00:00 2001 From: yeqown Date: Sun, 9 Jun 2024 10:47:11 +0800 Subject: [PATCH 04/13] feat(encoder): chardet detect ISO-8859-1 --- chardet.go | 19 ++-- chardet_test.go | 231 ++++++++++++++++++++++++++++++++++++++++++++++++ encoder_test.go | 167 +--------------------------------- version_test.go | 14 +-- 4 files changed, 252 insertions(+), 179 deletions(-) create mode 100644 chardet_test.go diff --git a/chardet.go b/chardet.go index b155d36..4fb9fde 100644 --- a/chardet.go +++ b/chardet.go @@ -1,6 +1,5 @@ package qrcode -// TODO: // chardet.go refer to https://github.com/chardet/chardet to detect input string's // character set, to see any unsupported character encountered in the input string. @@ -50,16 +49,16 @@ func analyzeEncodeModeFromRaw(raw string) encMode { } // analyzeNum is byt in num encMode -func analyzeNum(byt rune) bool { - return byt >= '0' && byt <= '9' +func analyzeNum(r rune) bool { + return r >= '0' && r <= '9' } // analyzeAlphaNum is byt in alpha number -func analyzeAlphaNum(byt rune) bool { - if (byt >= '0' && byt <= '9') || (byt >= 'A' && byt <= 'Z') { +func analyzeAlphaNum(r rune) bool { + if (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') { return true } - switch byt { + switch r { case ' ', '$', '%', '*', '+', '-', '.', '/', ':': return true } @@ -67,7 +66,11 @@ func analyzeAlphaNum(byt rune) bool { } // analyzeByte contains ISO-8859-1 character set -func analyzeByte(byt rune) bool { - // TODO: analyze input can be found in ISO-8859-1 character set. +func analyzeByte(r rune) bool { + // ISO-8859-1 character set, if r > \u00ff, means it's not in ISO-8859-1. + if r > '\u00ff' { + return false + } + return true } diff --git a/chardet_test.go b/chardet_test.go new file mode 100644 index 0000000..e9ddac7 --- /dev/null +++ b/chardet_test.go @@ -0,0 +1,231 @@ +package qrcode + +import ( + "testing" +) + +func Test_analyzeNum(t *testing.T) { + type args struct { + byt rune + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "case 0", + args: args{byt: '0'}, + want: true, + }, + { + name: "case 1", + args: args{byt: 'a'}, + want: false, + }, + { + name: "case 2", + args: args{byt: 'A'}, + want: false, + }, + { + name: "case 3", + args: args{byt: '9'}, + want: true, + }, + { + name: "case 4", + args: args{byt: '*'}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := analyzeNum(tt.args.byt); got != tt.want { + t.Errorf("analyzeNum() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_analyzeAlphanum(t *testing.T) { + type args struct { + byt rune + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "case 0", + args: args{byt: '0'}, + want: true, + }, + { + name: "case 1", + args: args{byt: 'a'}, + want: false, + }, + { + name: "case 2", + args: args{byt: 'A'}, + want: true, + }, + { + name: "case 3", + args: args{byt: '9'}, + want: true, + }, + { + name: "case 4", + args: args{byt: '*'}, + want: true, + }, + { + name: "case 5", + args: args{byt: '?'}, + want: false, + }, + { + name: "case 6", + args: args{byt: '&'}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := analyzeAlphaNum(tt.args.byt); got != tt.want { + t.Errorf("analyzeAlphaNum() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_analyzeByte(t *testing.T) { + type args struct { + byt rune + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "case 0", + args: args{byt: '0'}, + want: true, + }, + { + name: "case 1", + args: args{byt: 'a'}, + want: true, + }, + { + name: "case 2", + args: args{byt: 'A'}, + want: true, + }, + { + name: "case 3", + args: args{byt: '9'}, + want: true, + }, + { + name: "case 4", + args: args{byt: '*'}, + want: true, + }, + { + name: "case 5", + args: args{byt: '?'}, + want: true, + }, + { + name: "case 6", + args: args{byt: '&'}, + want: true, + }, + { + name: "case 7", + args: args{byt: 'Ö'}, + want: true, + }, + { + name: "case 8", + args: args{byt: 'に'}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := analyzeByte(tt.args.byt); got != tt.want { + t.Errorf("analyzeByte() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_anlayzeMode(t *testing.T) { + type args struct { + raw string + } + tests := []struct { + name string + args args + want encMode + }{ + { + name: "case 0", + args: args{raw: "123120899231"}, + want: EncModeNumeric, + }, + { + name: "case 1", + args: args{raw: ":/1231H208*99231FBJO"}, + want: EncModeAlphanumeric, + }, + { + name: "case 2", + args: args{raw: "hahah1298312hG&^FBJO@jhgG*"}, + want: EncModeByte, + }, + { + name: "case 3", + args: args{raw: "JKAHDOIANKQOIHCMJKASJ"}, + want: EncModeAlphanumeric, + }, + { + name: "case 4", + args: args{raw: "https://baidu.com?keyword=_JSO==GA"}, + want: EncModeByte, + }, + { + name: "case 5", + args: args{raw: "这是汉字也应该是EncModeByte"}, + want: EncModeJP, + }, + { + name: "case 6 (swedish letter)", + args: args{raw: "Övrigt aksldjlk Övrigt should JP encMode?"}, + want: EncModeByte, + }, + { + name: "case 7 (japanese letter)", + args: args{raw: "にほんごのテスト"}, + want: EncModeJP, + }, + { + name: "issue#28", + args: args{raw: "a"}, + want: EncModeByte, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := analyzeEncodeModeFromRaw(tt.args.raw); got != tt.want { + t.Errorf("analyzeEncodeModeFromRaw() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/encoder_test.go b/encoder_test.go index c89a459..3bc30ed 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -4,10 +4,6 @@ import ( "testing" ) -// func init() { -// load(defaultVersionCfg) -// } - func TestEncodeNum(t *testing.T) { enc := encoder{ ecLv: ErrorCorrectionLow, @@ -15,7 +11,7 @@ func TestEncodeNum(t *testing.T) { version: loadVersion(1, ErrorCorrectionLow), } - b, err := enc.Encode([]byte("12312312")) + b, err := enc.Encode("12312312") if err != nil { t.Errorf("could not encode: %v", err) t.Fail() @@ -30,7 +26,7 @@ func TestEncodeAlphanum(t *testing.T) { version: loadVersion(1, ErrorCorrectionLow), } - b, err := enc.Encode([]byte("AKJA*:/")) + b, err := enc.Encode("AKJA*:/") if err != nil { t.Errorf("could not encode: %v", err) t.Fail() @@ -45,167 +41,10 @@ func TestEncodeByte(t *testing.T) { version: loadVersion(5, ErrorCorrectionQuart), } - b, err := enc.Encode([]byte("http://baidu.com?keyword=123123")) + b, err := enc.Encode("http://baidu.com?keyword=123123") if err != nil { t.Errorf("could not encode: %v", err) t.Fail() } t.Log(b, b.Len()) } - -func Test_analyzeNum(t *testing.T) { - type args struct { - byt byte - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "case 0", - args: args{byt: '0'}, - want: true, - }, - { - name: "case 1", - args: args{byt: 'a'}, - want: false, - }, - { - name: "case 2", - args: args{byt: 'A'}, - want: false, - }, - { - name: "case 3", - args: args{byt: '9'}, - want: true, - }, - { - name: "case 4", - args: args{byt: '*'}, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := analyzeNum(tt.args.byt); got != tt.want { - t.Errorf("analyzeNum() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_analyzeAlphanum(t *testing.T) { - type args struct { - byt byte - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "case 0", - args: args{byt: '0'}, - want: true, - }, - { - name: "case 1", - args: args{byt: 'a'}, - want: false, - }, - { - name: "case 2", - args: args{byt: 'A'}, - want: true, - }, - { - name: "case 3", - args: args{byt: '9'}, - want: true, - }, - { - name: "case 4", - args: args{byt: '*'}, - want: true, - }, - { - name: "case 5", - args: args{byt: '?'}, - want: false, - }, - { - name: "case 6", - args: args{byt: '&'}, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := analyzeAlphaNum(tt.args.byt); got != tt.want { - t.Errorf("analyzeAlphaNum() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_anlayzeMode(t *testing.T) { - type args struct { - raw []byte - } - tests := []struct { - name string - args args - want encMode - }{ - { - name: "case 0", - args: args{raw: []byte("123120899231")}, - want: EncModeNumeric, - }, - { - name: "case 1", - args: args{raw: []byte(":/1231H208*99231FBJO")}, - want: EncModeAlphanumeric, - }, - { - name: "case 2", - args: args{raw: []byte("hahah1298312hG&^FBJO@jhgG*")}, - want: EncModeByte, - }, - { - name: "case 3", - args: args{raw: []byte("JKAHDOIANKQOIHCMJKASJ")}, - want: EncModeAlphanumeric, - }, - { - name: "case 4", - args: args{raw: []byte("https://baidu.com?keyword=_JSO==GA")}, - want: EncModeByte, - }, - { - name: "case 5", - args: args{raw: []byte("这是汉字也应该是EncModeByte")}, - want: EncModeByte, - }, - { - name: "case 6 (swedish letter)", - args: args{raw: []byte("Övrigt aksldjlk Övrigt should JP encMode?")}, - want: EncModeByte, - }, - { - name: "issue#28", - args: args{raw: []byte("a")}, - want: EncModeByte, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := analyzeEncodeModeFromRaw(tt.args.raw); got != tt.want { - t.Errorf("analyzeEncodeModeFromRaw() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/version_test.go b/version_test.go index 10555f1..9af578f 100644 --- a/version_test.go +++ b/version_test.go @@ -70,7 +70,7 @@ func Test_analyzeVersion(t *testing.T) { v3 := loadVersion(23, ErrorCorrectionMedium) type args struct { - raw []byte + raw string ecLv ecLevel eMode encMode } @@ -83,7 +83,7 @@ func Test_analyzeVersion(t *testing.T) { { name: "case 0", args: args{ - raw: []byte("TEXT"), + raw: "TEXT", ecLv: ErrorCorrectionMedium, eMode: EncModeAlphanumeric, }, @@ -93,7 +93,7 @@ func Test_analyzeVersion(t *testing.T) { { name: "case 1", args: args{ - raw: []byte(strings.Repeat("TEXT", 30)), + raw: strings.Repeat("TEXT", 30), ecLv: ErrorCorrectionMedium, eMode: EncModeAlphanumeric, }, @@ -103,7 +103,7 @@ func Test_analyzeVersion(t *testing.T) { { name: "case 2", args: args{ - raw: []byte(strings.Repeat("TEXT", 300)), + raw: strings.Repeat("TEXT", 300), ecLv: ErrorCorrectionMedium, eMode: EncModeAlphanumeric, }, @@ -282,7 +282,7 @@ func Benchmark_loadVersion_bottom(b *testing.B) { } func Benchmark_analyzeVersion_short(b *testing.B) { - source := []byte("text") + source := "text" for i := 0; i < b.N; i++ { _, _ = analyzeVersion(source, ErrorCorrectionMedium, EncModeByte) @@ -290,7 +290,7 @@ func Benchmark_analyzeVersion_short(b *testing.B) { } func Benchmark_analyzeVersion_middle(b *testing.B) { - source := []byte(strings.Repeat("text", 30)) + source := strings.Repeat("text", 30) for i := 0; i < b.N; i++ { _, _ = analyzeVersion(source, ErrorCorrectionMedium, EncModeByte) @@ -298,7 +298,7 @@ func Benchmark_analyzeVersion_middle(b *testing.B) { } func Benchmark_analyzeVersion_long(b *testing.B) { - source := []byte(strings.Repeat("text", 300)) + source := strings.Repeat("text", 300) for i := 0; i < b.N; i++ { _, _ = analyzeVersion(source, ErrorCorrectionMedium, EncModeByte) From a30883d84792ba132589d8be396c5b60de7cbc07 Mon Sep 17 00:00:00 2001 From: yeqown Date: Sun, 9 Jun 2024 10:48:53 +0800 Subject: [PATCH 05/13] typo: test case name of analyzeMode --- chardet_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chardet_test.go b/chardet_test.go index e9ddac7..f5ac514 100644 --- a/chardet_test.go +++ b/chardet_test.go @@ -166,7 +166,7 @@ func Test_analyzeByte(t *testing.T) { } } -func Test_anlayzeMode(t *testing.T) { +func Test_analyzeMode(t *testing.T) { type args struct { raw string } From ed8002e47db743fe368f9c875a6a3dc3780ca098 Mon Sep 17 00:00:00 2001 From: yeqown Date: Sun, 9 Jun 2024 15:30:49 +0800 Subject: [PATCH 06/13] feat: support kanji encoding --- chardet.go | 70 ++++++++++++++++++++++++++++++++++++++----------- encoder.go | 68 ++++++++++++++++++++++++++++++++++++++++++++--- encoder_test.go | 26 ++++++++++++++++++ 3 files changed, 145 insertions(+), 19 deletions(-) diff --git a/chardet.go b/chardet.go index 4fb9fde..a2ca4b9 100644 --- a/chardet.go +++ b/chardet.go @@ -1,5 +1,9 @@ package qrcode +import ( + "log" +) + // chardet.go refer to https://github.com/chardet/chardet to detect input string's // character set, to see any unsupported character encountered in the input string. @@ -16,35 +20,53 @@ type analyzeEncFunc func(rune) bool // case3: could not use EncModeAlphanumeric, but you can find all of them in ISO-8859-1 character set, use EncModeByte. // case4: could not use EncModeByte, use EncModeJP, no more choice. func analyzeEncodeModeFromRaw(raw string) encMode { - analyzeFnMapping := map[encMode]analyzeEncFunc{ - EncModeNumeric: analyzeNum, - EncModeAlphanumeric: analyzeAlphaNum, - EncModeByte: analyzeByte, - EncModeJP: nil, - } - var ( analyzeFn analyzeEncFunc - mode = EncModeNumeric + mode = EncModeNone ) - // loop to check each character in raw data, - // from low mode to higher while current mode could bearing the input data. - for _, byt := range raw { - reAnalyze: - if analyzeFn = analyzeFnMapping[mode]; analyzeFn == nil { - break + getNextAnalyzeFn := func() analyzeEncFunc { + switch mode { + case EncModeNumeric: + return analyzeNum + case EncModeAlphanumeric: + return analyzeAlphaNum + case EncModeByte: + return analyzeByte + case EncModeJP: + return analyzeJP + default: } + return analyzeDefault + } + + next := func() { + // switch to next mode and get next analyze function. + mode <<= 1 + analyzeFn = getNextAnalyzeFn() + } + + next() + + // Loop to check each character in raw data, + // from low mode to higher while current mode could bear the input data. + for _, byt := range raw { + reAnalyze: // issue#28 @borislavone reports this bug. // FIXED(@yeqown): next encMode analyzeVersionAuto func did not check the previous byte, // add goto statement to reanalyze previous byte which can't be analyzed in last encMode. if !analyzeFn(byt) { - mode <<= 1 + next() goto reAnalyze } } + if mode > EncModeJP { + // If the mode overflow the EncModeJP, means we can't encode the input data. + log.Panicf("could not encode the input data: %s", raw) + } + return mode } @@ -74,3 +96,21 @@ func analyzeByte(r rune) bool { return true } + +// analyzeJP contains Kanji character set +// http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml +func analyzeJP(r rune) bool { + // Kanji character set + if r > 0x8140 && r < 0x9FFC { + return true + } + if r > 0xE040 && r < 0xEBBF { + return true + } + + return false +} + +func analyzeDefault(r rune) bool { + return false +} diff --git a/encoder.go b/encoder.go index 52585c5..7d3c947 100644 --- a/encoder.go +++ b/encoder.go @@ -5,9 +5,11 @@ package qrcode import ( "fmt" "log" + "strconv" "github.com/yeqown/reedsolomon/binary" - "strconv" + "golang.org/x/text/encoding/japanese" + "golang.org/x/text/transform" ) // encMode indicates the encoding mode of the data to be encoded. @@ -120,7 +122,7 @@ func (e *encoder) Encode(raw string) (*binary.Binary, error) { case EncModeNumeric, EncModeAlphanumeric, EncModeByte: data = []byte(raw) case EncModeJP: - // TODO: construct data []byte from raw string + data = toShiftJIS(raw) default: log.Printf("unsupported encoding mode: %s", getEncModeName(e.mode)) } @@ -209,10 +211,68 @@ func (e *encoder) encodeByte(data []byte) { } } -// encodeKanji +// toShiftJIS // https://www.thonky.com/qr-code-tutorial/kanji-mode-encoding +func toShiftJIS(raw string) []byte { + // FIXME: some character encoded into Shift JIS but not in the range of 0x8140-0x9FFC and 0xE040-0xEBBF. + enc := japanese.ShiftJIS.NewEncoder() + s2, _, err := transform.String(enc, raw) + if err != nil { + log.Printf("could not encode string to Shift JIS: %v", err) + return []byte{} + } + + data := []byte(s2) + if len(data)%2 != 0 { + log.Panicf("shift JIS encoded []byte must be times of 2, but got %d", len(data)) + } + + for i := 0; i < len(data); i += 2 { + data[i], data[i+1] = encodeShiftJIS(data[i], data[i+1]) + } + + return data +} + +func encodeShiftJIS(hi byte, lo byte) (byte, byte) { + r := uint16(hi)<<8 | uint16(lo) + + fmt.Printf("before: r=%x\n", r) + if r > 0x8140 && r < 0x9FFC { + r -= 0x8140 + } else if r > 0xE040 && r < 0xEBBF { + r -= 0xC140 + } else { + // Not a Shift JIS character out of range 0x8140-0x9FFC and 0xE040-0xEBBF + log.Printf("'%c'(0x%x) not a Shift JIS character out of range 0x8140-0x9FFC and 0xE040-0xEBBF", r, r) + return 0, 0 + } + + fmt.Printf("middle: r=%x\n", r) + hi = uint8(r >> 8) + lo = uint8(r & 0xFF) + + fmt.Printf("middle: high=%x, low=%x\n", hi, lo) + + r = uint16(hi)*uint16(0xC0) + uint16(lo) + fmt.Printf("after: r=%x\n", r) + + return byte(r >> 8), byte(r & 0xFF) +} + +// encodeKanji func (e *encoder) encodeKanji(data []byte) { - // TODO: implement encodeKanji + // data must be times of 2, since toShiftJIS encode 1 char to 2 bytes + if len(data)%2 != 0 { + log.Println("data must be times of 2") + } + + for i := 0; i < len(data); i += 2 { + // 2 bytes to 1 kanji + // 2 bytes to 13 bits + _ = e.dst.AppendByte(data[i]<<3, 5) + _ = e.dst.AppendByte(data[i+1], 8) + } } // Break Up into 8-bit Codewords and Add Pad Bytes if Necessary diff --git a/encoder_test.go b/encoder_test.go index 3bc30ed..da5d267 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -1,6 +1,7 @@ package qrcode import ( + "bytes" "testing" ) @@ -48,3 +49,28 @@ func TestEncodeByte(t *testing.T) { } t.Log(b, b.Len()) } + +func Test_toShiftJIS(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want []byte + }{ + { + name: "test 1", + args: args{"茗荷"}, + want: []byte{0x1A, 0xAA, 0x06, 0x97}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toShiftJIS(tt.args.s); !bytes.Equal(got, tt.want) { + t.Errorf("toShiftJIS() = %v, want %v", got, tt.want) + } + }) + } +} From 47e8948478014bf8f7d2c14af6ac83c53fc93827 Mon Sep 17 00:00:00 2001 From: yeqown Date: Sun, 9 Jun 2024 15:32:47 +0800 Subject: [PATCH 07/13] styles: fix lint --- chardet.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/chardet.go b/chardet.go index a2ca4b9..5e743f4 100644 --- a/chardet.go +++ b/chardet.go @@ -90,11 +90,7 @@ func analyzeAlphaNum(r rune) bool { // analyzeByte contains ISO-8859-1 character set func analyzeByte(r rune) bool { // ISO-8859-1 character set, if r > \u00ff, means it's not in ISO-8859-1. - if r > '\u00ff' { - return false - } - - return true + return r <= '\u00ff' } // analyzeJP contains Kanji character set From c3b2e26f23d6faae4a631bffdc24e20891b10625 Mon Sep 17 00:00:00 2001 From: yeqown Date: Sun, 9 Jun 2024 15:56:43 +0800 Subject: [PATCH 08/13] fix: analyzeMode blocked in loop --- chardet.go | 24 +++++++++++++----------- chardet_test.go | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/chardet.go b/chardet.go index 5e743f4..2d05432 100644 --- a/chardet.go +++ b/chardet.go @@ -38,28 +38,34 @@ func analyzeEncodeModeFromRaw(raw string) encMode { default: } - return analyzeDefault + return nil } - next := func() { - // switch to next mode and get next analyze function. + next := func() bool { + // switch to next mode and get next analyze function. if no more analyze function, return true. mode <<= 1 analyzeFn = getNextAnalyzeFn() + return analyzeFn == nil } next() // Loop to check each character in raw data, // from low mode to higher while current mode could bear the input data. - for _, byt := range raw { + for _, r := range raw { reAnalyze: // issue#28 @borislavone reports this bug. // FIXED(@yeqown): next encMode analyzeVersionAuto func did not check the previous byte, // add goto statement to reanalyze previous byte which can't be analyzed in last encMode. - if !analyzeFn(byt) { - next() - goto reAnalyze + if pass := analyzeFn(r); pass { + continue } + + if nomore := next(); nomore { + break + } + + goto reAnalyze } if mode > EncModeJP { @@ -106,7 +112,3 @@ func analyzeJP(r rune) bool { return false } - -func analyzeDefault(r rune) bool { - return false -} diff --git a/chardet_test.go b/chardet_test.go index f5ac514..1f7d46f 100644 --- a/chardet_test.go +++ b/chardet_test.go @@ -202,7 +202,7 @@ func Test_analyzeMode(t *testing.T) { }, { name: "case 5", - args: args{raw: "这是汉字也应该是EncModeByte"}, + args: args{raw: "茗荷"}, want: EncModeJP, }, { @@ -212,7 +212,7 @@ func Test_analyzeMode(t *testing.T) { }, { name: "case 7 (japanese letter)", - args: args{raw: "にほんごのテスト"}, + args: args{raw: "朸 朷 杆 杞 杠 杙 杣"}, want: EncModeJP, }, { From 056731fa129f3f713196d3822ec4b4f36eb5eef1 Mon Sep 17 00:00:00 2001 From: yeqown Date: Mon, 10 Jun 2024 10:31:08 +0800 Subject: [PATCH 09/13] feat: another way to analyze kanji character --- chardet.go | 158 ++++++++++++++++++++++++++++++++++++++++++++---- chardet_test.go | 94 ++++++++++++++++++++++++++++ encoder.go | 6 +- 3 files changed, 244 insertions(+), 14 deletions(-) diff --git a/chardet.go b/chardet.go index 2d05432..5798de4 100644 --- a/chardet.go +++ b/chardet.go @@ -4,6 +4,10 @@ import ( "log" ) +func init() { + restoreKanJi() +} + // chardet.go refer to https://github.com/chardet/chardet to detect input string's // character set, to see any unsupported character encountered in the input string. @@ -70,18 +74,18 @@ func analyzeEncodeModeFromRaw(raw string) encMode { if mode > EncModeJP { // If the mode overflow the EncModeJP, means we can't encode the input data. - log.Panicf("could not encode the input data: %s", raw) + log.Panicf("character set not supported, please check your input data.") } return mode } -// analyzeNum is byt in num encMode +// analyzeNum is r in num encMode func analyzeNum(r rune) bool { return r >= '0' && r <= '9' } -// analyzeAlphaNum is byt in alpha number +// analyzeAlphaNum is r in alpha number func analyzeAlphaNum(r rune) bool { if (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') { return true @@ -102,13 +106,145 @@ func analyzeByte(r rune) bool { // analyzeJP contains Kanji character set // http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml func analyzeJP(r rune) bool { - // Kanji character set - if r > 0x8140 && r < 0x9FFC { - return true - } - if r > 0xE040 && r < 0xEBBF { - return true - } + _, ok := __UNICODE_TO_QR_KANJI[uint32(r)] + return ok +} - return false +var ( + __UNICODE_TO_QR_KANJI map[uint32]struct{} +) + +func restoreKanJi() { + // private static short[] UNICODE_TO_QR_KANJI = new short[1 << 16]; + + // Arrays.fill(UNICODE_TO_QR_KANJI, (short)-1); + // byte[] bytes = Base64.getDecoder().decode(PACKED_QR_KANJI_TO_UNICODE); + // for (int i = 0; i < bytes.length; i += 2) { + // char c = (char)(((bytes[i] & 0xFF) << 8) | (bytes[i + 1] & 0xFF)); + // if (c == 0xFFFF) + // continue; + // assert UNICODE_TO_QR_KANJI[c] == -1; + // UNICODE_TO_QR_KANJI[c] = (short)(i / 2); + // } + + __UNICODE_TO_QR_KANJI = make(map[uint32]struct{}, 1<<16) + // restore from __PACKED_QR_KANJI_TO_UNICODE + for i := 0; i < len(__PACKED_QR_KANJI_TO_UNICODE); i += 2 { + c := (uint32(__PACKED_QR_KANJI_TO_UNICODE[i]) << 8) | uint32(__PACKED_QR_KANJI_TO_UNICODE[i+1]) + if c == 0xFFFF { + continue + } + __UNICODE_TO_QR_KANJI[c] = struct{}{} + } } + +var __PACKED_QR_KANJI_TO_UNICODE = "MAAwATAC/wz/DjD7/xr/G/8f/wEwmzCcALT/QACo/z7/4/8/MP0w/jCdMJ4wA07dMAUwBjAHMPwgFSAQ/w8AXDAcIBb/XCAmICUgGCAZIBwgHf8I/wkwFDAV/zv/Pf9b/10wCDAJMAowCzAMMA0wDjAPMBAwEf8LIhIAsQDX//8A9/8dImD/HP8eImYiZyIeIjQmQiZA" + + "ALAgMiAzIQP/5f8EAKIAo/8F/wP/Bv8K/yAApyYGJgUlyyXPJc4lxyXGJaEloCWzJbIlvSW8IDswEiGSIZAhkSGTMBP/////////////////////////////IggiCyKGIocigiKDIioiKf////////////////////8iJyIoAKwh0iHUIgAiA///////////////////" + + "//////////8iICKlIxIiAiIHImEiUiJqImsiGiI9Ih0iNSIrIiz//////////////////yErIDAmbyZtJmogICAhALb//////////yXv/////////////////////////////////////////////////xD/Ef8S/xP/FP8V/xb/F/8Y/xn///////////////////8h" + + "/yL/I/8k/yX/Jv8n/yj/Kf8q/yv/LP8t/y7/L/8w/zH/Mv8z/zT/Nf82/zf/OP85/zr///////////////////9B/0L/Q/9E/0X/Rv9H/0j/Sf9K/0v/TP9N/07/T/9Q/1H/Uv9T/1T/Vf9W/1f/WP9Z/1r//////////zBBMEIwQzBEMEUwRjBHMEgwSTBKMEswTDBN" + + "ME4wTzBQMFEwUjBTMFQwVTBWMFcwWDBZMFowWzBcMF0wXjBfMGAwYTBiMGMwZDBlMGYwZzBoMGkwajBrMGwwbTBuMG8wcDBxMHIwczB0MHUwdjB3MHgweTB6MHswfDB9MH4wfzCAMIEwgjCDMIQwhTCGMIcwiDCJMIowizCMMI0wjjCPMJAwkTCSMJP/////////////" + + "////////////////////////MKEwojCjMKQwpTCmMKcwqDCpMKowqzCsMK0wrjCvMLAwsTCyMLMwtDC1MLYwtzC4MLkwujC7MLwwvTC+ML8wwDDBMMIwwzDEMMUwxjDHMMgwyTDKMMswzDDNMM4wzzDQMNEw0jDTMNQw1TDWMNcw2DDZMNow2zDcMN0w3jDf//8w4DDh" + + "MOIw4zDkMOUw5jDnMOgw6TDqMOsw7DDtMO4w7zDwMPEw8jDzMPQw9TD2/////////////////////wORA5IDkwOUA5UDlgOXA5gDmQOaA5sDnAOdA54DnwOgA6EDowOkA6UDpgOnA6gDqf////////////////////8DsQOyA7MDtAO1A7YDtwO4A7kDugO7A7wDvQO+" + + "A78DwAPBA8MDxAPFA8YDxwPIA8n/////////////////////////////////////////////////////////////////////////////////////////////////////////////BBAEEQQSBBMEFAQVBAEEFgQXBBgEGQQaBBsEHAQdBB4EHwQgBCEEIgQjBCQEJQQm" + + "BCcEKAQpBCoEKwQsBC0ELgQv////////////////////////////////////////BDAEMQQyBDMENAQ1BFEENgQ3BDgEOQQ6BDsEPAQ9//8EPgQ/BEAEQQRCBEMERARFBEYERwRIBEkESgRLBEwETQROBE///////////////////////////////////yUAJQIlDCUQ" + + "JRglFCUcJSwlJCU0JTwlASUDJQ8lEyUbJRclIyUzJSslOyVLJSAlLyUoJTclPyUdJTAlJSU4JUL/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "/////////////////////////////////////06cVRZaA5Y/VMBhG2MoWfaQIoR1gxx6UGCqY+FuJWXthGaCppv1aJNXJ2WhYnFbm1nQhnuY9H1ifb6bjmIWfJ+It1uJXrVjCWaXaEiVx5eNZ09O5U8KT01PnVBJVvJZN1nUWgFcCWDfYQ9hcGYTaQVwunVPdXB5+32t" + + "fe+Aw4QOiGOLApBVkHpTO06VTqVX34CykMF4704AWPFuopA4ejKDKIKLnC9RQVNwVL1U4VbgWftfFZjybeuA5IUt////////lmKWcJagl/tUC1PzW4dwz3+9j8KW6FNvnVx6uk4ReJOB/G4mVhhVBGsdhRqcO1nlU6ltZnTclY9WQk6RkEuW8oNPmQxT4VW2WzBfcWYg" + + "ZvNoBGw4bPNtKXRbdsh6Tpg0gvGIW4pgku1tsnWrdsqZxWCmiwGNipWyaY5TrVGG//9XElgwWURbtF72YChjqWP0bL9vFHCOcRRxWXHVcz9+AYJ2gtGFl5BgkludG1hpZbxsWnUlUflZLlllX4Bf3GK8ZfpqKmsna7Rzi3/BiVadLJ0OnsRcoWyWg3tRBFxLYbaBxmh2" + + "cmFOWU/6U3hgaW4pek+X804LUxZO7k9VTz1PoU9zUqBT71YJWQ9awVu2W+F50WaHZ5xntmtMbLNwa3PCeY15vno8e4eCsYLbgwSDd4Pvg9OHZoqyVimMqI/mkE6XHoaKT8Rc6GIRcll1O4Hlgr2G/ozAlsWZE5nVTstPGonjVt5YSljKXvtf62AqYJRgYmHQYhJi0GU5" + + "////////m0FmZmiwbXdwcHVMdoZ9dYKlh/mVi5aOjJ1R8VK+WRZUs1uzXRZhaGmCba94jYTLiFeKcpOnmrhtbJmohtlXo2f/hs6SDlKDVodUBF7TYuFkuWg8aDhru3NyeLp6a4maidKNa48DkO2Vo5aUl2lbZlyzaX2YTZhOY5t7IGor//9qf2i2nA1vX1JyVZ1gcGLs" + + "bTtuB27RhFuJEI9EThScOVP2aRtqOpeEaCpRXHrDhLKR3JOMVludKGgigwWEMXylUgiCxXTmTn5Pg1GgW9JSClLYUudd+1WaWCpZ5luMW5hb215yXnlgo2EfYWNhvmPbZWJn0WhTaPprPmtTbFdvIm+Xb0V0sHUYduN3C3r/e6F8IX3pfzZ/8ICdgmaDnomzisyMq5CE" + + "lFGVk5WRlaKWZZfTmSiCGE44VCtcuF3Mc6l2THc8XKl/640LlsGYEZhUmFhPAU8OU3FVnFZoV/pZR1sJW8RckF4MXn5fzGPuZzpl12XiZx9oy2jE////////al9eMGvFbBdsfXV/eUhbY3oAfQBfvYmPihiMtI13jsyPHZjimg6bPE6AUH1RAFmTW5xiL2KAZOxrOnKg" + + "dZF5R3+ph/uKvItwY6yDypegVAlUA1WraFRqWIpweCdndZ7NU3RbooEahlCQBk4YTkVOx08RU8pUOFuuXxNgJWVR//9nPWxCbHJs43B4dAN6dnquewh9Gnz+fWZl53JbU7tcRV3oYtJi4GMZbiCGWooxjd2S+G8BeaabWk6oTqtOrE+bT6BQ0VFHevZRcVH2U1RTIVN/" + + "U+tVrFiDXOFfN19KYC9gUGBtYx9lWWpLbMFywnLtd++A+IEFggiFTpD3k+GX/5lXmlpO8FHdXC1mgWltXEBm8ml1c4loUHyBUMVS5FdHXf6TJmWkayNrPXQ0eYF5vXtLfcqCuYPMiH+JX4s5j9GR0VQfkoBOXVA2U+VTOnLXc5Z36YLmjq+ZxpnImdJRd2Eahl5VsHp6" + + "UHZb05BHloVOMmrbkedcUVxI////////Y5h6n2yTl3SPYXqqcYqWiHyCaBd+cGhRk2xS8lQbhauKE3+kjs2Q4VNmiIh5QU/CUL5SEVFEVVNXLXPqV4tZUV9iX4RgdWF2YWdhqWOyZDplbGZvaEJuE3Vmej18+31MfZl+S39rgw6DSobNigiKY4tmjv2YGp2PgriPzpvo" + + "//9Sh2IfZINvwJaZaEFQkWsgbHpvVHp0fVCIQIojZwhO9lA5UCZQZVF8UjhSY1WnVw9YBVrMXvphsmH4YvNjcmkcailyfXKscy54FHhvfXl3DICpiYuLGYzijtKQY5N1lnqYVZoTnnhRQ1OfU7Nee18mbhtukHOEc/59Q4I3igCK+pZQTk5QC1PkVHxW+lnRW2Rd8V6r" + + "XydiOGVFZ69uVnLQfMqItIChgOGD8IZOioeN6JI3lseYZ58TTpROkk8NU0hUSVQ+Wi9fjF+hYJ9op2qOdFp4gYqeiqSLd5GQTl6byU6kT3xPr1AZUBZRSVFsUp9SuVL+U5pT41QR////////VA5ViVdRV6JZfVtUW11bj13lXedd9154XoNeml63XxhgUmFMYpdi2GOn" + + "ZTtmAmZDZvRnbWghaJdpy2xfbSptaW4vbp11MnaHeGx6P3zgfQV9GH1efbGAFYADgK+AsYFUgY+CKoNSiEyIYYsbjKKM/JDKkXWScXg/kvyVpJZN//+YBZmZmtidO1JbUqtT91QIWNVi92/gjGqPX565UUtSO1RKVv16QJF3nWCe0nNEbwmBcHURX/1g2pqoctuPvGtk" + + "mANOylbwV2RYvlpaYGhhx2YPZgZoOWixbfd11X06gm6bQk6bT1BTyVUGXW9d5l3uZ/tsmXRzeAKKUJOWiN9XUF6nYytQtVCsUY1nAFTJWF5Zu1uwX2liTWOhaD1rc24IcH2Rx3KAeBV4JnltZY59MIPciMGPCZabUmRXKGdQf2qMoVG0V0KWKlg6aYqAtFSyXQ5X/HiV" + + "nfpPXFJKVItkPmYoZxRn9XqEe1Z9IpMvaFybrXs5UxlRilI3////////W99i9mSuZOZnLWu6hamW0XaQm9ZjTJMGm6t2v2ZSTglQmFPCXHFg6GSSZWNoX3Hmc8p1I3uXfoKGlYuDjNuReJkQZaxmq2uLTtVO1E86T39SOlP4U/JV41bbWOtZy1nJWf9bUFxNXgJeK1/X" + + "YB1jB2UvW1xlr2W9ZehnnWti//9re2wPc0V5SXnBfPh9GX0rgKKBAoHziZaKXoppimaKjIrujMeM3JbMmPxrb06LTzxPjVFQW1db+mFIYwFmQmshbstsu3I+dL111HjBeTqADIAzgeqElI+ebFCef18Pi1idK3r6jvhbjZbrTgNT8Vf3WTFayVukYIluf28Gdb6M6luf" + + "hQB74FByZ/SCnVxhhUp+HoIOUZlcBGNojWZlnHFueT59F4AFix2OypBuhseQqlAfUvpcOmdTcHxyNZFMkciTK4LlW8JfMWD5TjtT1luIYktnMWuKculz4HougWuNo5FSmZZRElPXVGpb/2OIajl9rJcAVtpTzlRo////////W5dcMV3eT+5hAWL+bTJ5wHnLfUJ+TX/S" + + "ge2CH4SQiEaJcouQjnSPL5AxkUuRbJbGkZxOwE9PUUVTQV+TYg5n1GxBbgtzY34mkc2Sg1PUWRlbv23ReV1+LnybWH5xn1H6iFOP8E/KXPtmJXeseuOCHJn/UcZfqmXsaW9riW3z//9ulm9kdv59FF3hkHWRh5gGUeZSHWJAZpFm2W4aXrZ90n9yZviFr4X3ivhSqVPZ" + + "WXNej1+QYFWS5JZkULdRH1LdUyBTR1PsVOhVRlUxVhdZaFm+WjxbtVwGXA9cEVwaXoReil7gX3Bif2KEYttjjGN3ZgdmDGYtZnZnfmiiah9qNWy8bYhuCW5YcTxxJnFndcd3AXhdeQF5ZXnweuB7EXynfTmAloPWhIuFSYhdiPOKH4o8ilSKc4xhjN6RpJJmk36UGJac" + + "l5hOCk4ITh5OV1GXUnBXzlg0WMxbIl44YMVk/mdhZ1ZtRHK2dXN6Y4S4i3KRuJMgVjFX9Jj+////////Yu1pDWuWce1+VIB3gnKJ5pjfh1WPsVw7TzhP4U+1VQdaIFvdW+lfw2FOYy9lsGZLaO5pm214bfF1M3W5dx95XnnmfTOB44KvhaqJqoo6jquPm5Aykd2XB066" + + "TsFSA1h1WOxcC3UaXD2BTooKj8WWY5dteyWKz5gIkWJW81Oo//+QF1Q5V4JeJWOobDRwindhfIt/4IhwkEKRVJMQkxiWj3RemsRdB11pZXBnoo2olttjbmdJaRmDxZgXlsCI/m+EZHpb+E4WcCx1XWYvUcRSNlLiWdNfgWAnYhBlP2V0Zh9mdGjyaBZrY24FcnJ1H3bb" + + "fL6AVljwiP2Jf4qgipOKy5AdkZKXUpdZZYl6DoEGlrteLWDcYhplpWYUZ5B383pNfE1+PoEKjKyNZI3hjl94qVIHYtljpWRCYpiKLXqDe8CKrJbqfXaCDIdJTtlRSFNDU2Bbo1wCXBZd3WImYkdksGgTaDRsyW1FbRdn029ccU5xfWXLen97rX3a////////fkp/qIF6" + + "ghuCOYWmim6Mzo31kHiQd5KtkpGVg5uuUk1VhG84cTZRaHmFflWBs3zOVkxYUVyoY6pm/mb9aVpy2XWPdY55DnlWed98l30gfUSGB4o0ljuQYZ8gUOdSdVPMU+JQCVWqWO5ZT3I9W4tcZFMdYONg82NcY4NjP2O7//9kzWXpZvld42nNaf1vFXHlTol16Xb4epN8333P" + + "fZyAYYNJg1iEbIS8hfuIxY1wkAGQbZOXlxyaElDPWJdhjoHThTWNCJAgT8NQdFJHU3Ngb2NJZ19uLI2zkB9P11xejMplz32aU1KIllF2Y8NbWFtrXApkDWdRkFxO1lkaWSpscIpRVT5YFVmlYPBiU2fBgjVpVZZAmcSaKE9TWAZb/oAQXLFeL1+FYCBhS2I0Zv9s8G7e" + + "gM6Bf4LUiIuMuJAAkC6Wip7bm9tO41PwWSd7LJGNmEyd+W7dcCdTU1VEW4ViWGKeYtNsom/vdCKKF5Q4b8GK/oM4UeeG+FPq////////U+lPRpBUj7BZaoExXf166o+/aNqMN3L4nEhqPYqwTjlTWFYGV2ZixWOiZeZrTm3hbltwrXfteu97qn27gD2AxobLipWTW1bj" + + "WMdfPmWtZpZqgGu1dTeKx1Akd+VXMF8bYGVmemxgdfR6Gn9ugfSHGJBFmbN7yXVcevl7UYTE//+QEHnpepKDNlrhd0BOLU7yW5lf4GK9Zjxn8WzohmuId4o7kU6S85nQahdwJnMqgueEV4yvTgFRRlHLVYtb9V4WXjNegV8UXzVfa1+0YfJjEWaiZx1vbnJSdTp3OoB0" + + "gTmBeId2ir+K3I2FjfOSmpV3mAKc5VLFY1d29GcVbIhzzYzDk66Wc20lWJxpDmnMj/2TmnXbkBpYWmgCY7Rp+09Dbyxn2I+7hSZ9tJNUaT9vcFdqWPdbLH0scipUCpHjnbROrU9OUFxQdVJDjJ5USFgkW5peHV6VXq1e918fYIxitWM6Y9Bor2xAeId5jnoLfeCCR4oC" + + "iuaORJAT////////kLiRLZHYnw5s5WRYZOJldW70doR7G5Bpk9FuulTyX7lkpI9Nj+2SRFF4WGtZKVxVXpdt+36PdRyMvI7imFtwuU8da79vsXUwlvtRTlQQWDVYV1msXGBfkmWXZ1xuIXZ7g9+M7ZAUkP2TTXgleDpSql6mVx9ZdGASUBJRWlGs//9RzVIAVRBYVFhY" + + "WVdblVz2XYtgvGKVZC1ncWhDaLxo33bXbdhub22bcG9xyF9Tddh5d3tJe1R7UnzWfXFSMIRjhWmF5IoOiwSMRo4PkAOQD5QZlnaYLZowldhQzVLVVAxYAlwOYadknm0ed7N65YD0hASQU5KFXOCdB1M/X5dfs22ccnl3Y3m/e+Rr0nLsiq1oA2phUfh6gWk0XEqc9oLr" + + "W8WRSXAeVnhcb2DHZWZsjIxakEGYE1RRZseSDVlIkKNRhU5NUeqFmYsOcFhjepNLaWKZtH4EdXdTV2lgjt+W42xdToxcPF8Qj+lTAozRgImGeV7/ZeVOc1Fl////////WYJcP5fuTvtZil/Nio1v4XmweWJb54RxcytxsV50X/Vje2SaccN8mE5DXvxOS1fcVqJgqW/D" + + "fQ2A/YEzgb+PsomXhqRd9GKKZK2Jh2d3bOJtPnQ2eDRaRn91gq2ZrE/zXsNi3WOSZVdnb3bDckyAzIC6jymRTVANV/lakmiF//9pc3Fkcv2Mt1jyjOCWapAZh3955HfnhClPL1JlU1pizWfPbMp2fXuUfJWCNoWEj+tm3W8gcgZ+G4OrmcGeplH9e7F4cnu4gId7SGro" + + "XmGAjHVRdWBRa5Jibox2epGXmupPEH9wYpx7T5WlnOlWelhZhuSWvE80UiRTSlPNU9teBmQsZZFnf2w+bE5ySHKvc+11VH5BgiyF6Yype8SRxnFpmBKY72M9Zml1anbkeNCFQ4buUypTUVQmWYNeh198YLJiSWJ5YqtlkGvUbMx1snaueJF52H3Lf3eApYirirmMu5B/" + + "l16Y22oLfDhQmVw+X65nh2vYdDV3CX+O////////nztnynoXUzl1i5rtX2aBnYPxgJhfPF/FdWJ7RpA8aGdZ61qbfRB2fossT/VfamoZbDdvAnTieWiIaIpVjHle32PPdcV50oLXkyiS8oSchu2cLVTBX2xljG1ccBWMp4zTmDtlT3T2Tg1O2FfgWStaZlvMUaheA16c" + + "YBZidmV3//9lp2ZubW5yNnsmgVCBmoKZi1yMoIzmjXSWHJZET65kq2tmgh6EYYVqkOhcAWlTmKiEeoVXTw9Sb1+pXkVnDXmPgXmJB4mGbfVfF2JVbLhOz3Jpm5JSBlQ7VnRYs2GkYm5xGllufIl83n0blvBlh4BeThlPdVF1WEBeY15zXwpnxE4mhT2ViZZbfHOYAVD7" + + "WMF2VninUiV3pYURe4ZQT1kJckd7x33oj7qP1JBNT79SyVopXwGXrU/dgheS6lcDY1VraXUriNyPFHpCUt9Yk2FVYgpmrmvNfD+D6VAjT/hTBVRGWDFZSVudXPBc710pXpZisWNnZT5luWcL////////bNVs4XD5eDJ+K4DegrOEDITshwKJEooqjEqQppLSmP2c851s" + + "Tk9OoVCNUlZXSlmoXj1f2F/ZYj9mtGcbZ9Bo0lGSfSGAqoGoiwCMjIy/kn6WMlQgmCxTF1DVU1xYqGSyZzRyZ3dmekaR5lLDbKFrhlgAXkxZVGcsf/tR4XbG//9kaXjom1Seu1fLWblmJ2eaa85U6WnZXlWBnGeVm6pn/pxSaF1Opk/jU8hiuWcrbKuPxE+tfm2ev04H" + + "YWJugG8rhRNUc2cqm0Vd83uVXKxbxoccbkqE0XoUgQhZmXyNbBF3IFLZWSJxIXJfd9uXJ51haQtaf1oYUaVUDVR9Zg5234/3kpic9Fnqcl1uxVFNaMl9v33sl2KeumR4aiGDAlmEW19r23MbdvJ9soAXhJlRMmcontl27mdiUv+ZBVwkYjt8foywVU9gtn0LlYBTAU5f" + + "UbZZHHI6gDaRzl8ld+JThF95fQSFrIozjo2XVmfzha6UU2EJYQhsuXZS////////iu2POFUvT1FRKlLHU8tbpV59YKBhgmPWZwln2m5nbYxzNnM3dTF5UIjVipiQSpCRkPWWxIeNWRVOiE9ZTg6KiY8/mBBQrV58WZZbuV64Y9pj+mTBZtxpSmnYbQtutnGUdSh6r3+K" + + "gACESYTJiYGLIY4KkGWWfZkKYX5ikWsy//9sg210f8x//G3Af4WHuoj4Z2WDsZg8lvdtG31hhD2Rak5xU3VdUGsEb+uFzYYtiadSKVQPXGVnTmiodAZ0g3XiiM+I4ZHMluKWeF+Lc4d6y4ROY6B1ZVKJbUFunHQJdVl4a3ySloZ63J+NT7ZhbmXFhlxOhk6uUNpOIVHM" + + "W+5lmWiBbbxzH3ZCd616HHzngm+K0pB8kc+WdZgYUpt90VArU5hnl23LcdB0M4HojyqWo5xXnp90YFhBbZl9L5heTuRPNk+LUbdSsV26YBxzsnk8gtOSNJa3lvaXCp6Xn2Jmpmt0UhdSo3DIiMJeyWBLYZBvI3FJfD599IBv////////hO6QI5MsVEKbb2rTcImMwo3v" + + "lzJStFpBXspfBGcXaXxplG1qbw9yYnL8e+2AAYB+h0uQzlFtnpN5hICLkzKK1lAtVIyKcWtqjMSBB2DRZ6Cd8k6ZTpicEIprhcGFaGkAbn54l4FV////////////////////////////////////////////////////////////////////////////////////////" + + "/////////////////////////////18MThBOFU4qTjFONk48Tj9OQk5WTlhOgk6FjGtOioISXw1Ojk6eTp9OoE6iTrBOs062Ts5OzU7ETsZOwk7XTt5O7U7fTvdPCU9aTzBPW09dT1dPR092T4hPj0+YT3tPaU9wT5FPb0+GT5ZRGE/UT99Pzk/YT9tP0U/aT9BP5E/l" + + "UBpQKFAUUCpQJVAFTxxP9lAhUClQLE/+T+9QEVAGUENQR2cDUFVQUFBIUFpQVlBsUHhQgFCaUIVQtFCy////////UMlQylCzUMJQ1lDeUOVQ7VDjUO5Q+VD1UQlRAVECURZRFVEUURpRIVE6UTdRPFE7UT9RQFFSUUxRVFFievhRaVFqUW5RgFGCVthRjFGJUY9RkVGT" + + "UZVRllGkUaZRolGpUapRq1GzUbFRslGwUbVRvVHFUclR21HghlVR6VHt//9R8FH1Uf5SBFILUhRSDlInUipSLlIzUjlST1JEUktSTFJeUlRSalJ0UmlSc1J/Un1SjVKUUpJScVKIUpGPqI+nUqxSrVK8UrVSwVLNUtdS3lLjUuaY7VLgUvNS9VL4UvlTBlMIdThTDVMQ" + + "Uw9TFVMaUyNTL1MxUzNTOFNAU0ZTRU4XU0lTTVHWU15TaVNuWRhTe1N3U4JTllOgU6ZTpVOuU7BTtlPDfBKW2VPfZvxx7lPuU+hT7VP6VAFUPVRAVCxULVQ8VC5UNlQpVB1UTlSPVHVUjlRfVHFUd1RwVJJUe1SAVHZUhFSQVIZUx1SiVLhUpVSsVMRUyFSo////////" + + "VKtUwlSkVL5UvFTYVOVU5lUPVRRU/VTuVO1U+lTiVTlVQFVjVUxVLlVcVUVVVlVXVThVM1VdVZlVgFSvVYpVn1V7VX5VmFWeVa5VfFWDValVh1WoVdpVxVXfVcRV3FXkVdRWFFX3VhZV/lX9VhtV+VZOVlBx31Y0VjZWMlY4//9Wa1ZkVi9WbFZqVoZWgFaKVqBWlFaP" + + "VqVWrla2VrRWwla8VsFWw1bAVshWzlbRVtNW11buVvlXAFb/VwRXCVcIVwtXDVcTVxhXFlXHVxxXJlc3VzhXTlc7V0BXT1dpV8BXiFdhV39XiVeTV6BXs1ekV6pXsFfDV8ZX1FfSV9NYClfWV+NYC1gZWB1YclghWGJYS1hwa8BYUlg9WHlYhVi5WJ9Yq1i6WN5Yu1i4" + + "WK5YxVjTWNFY11jZWNhY5VjcWORY31jvWPpY+Vj7WPxY/VkCWQpZEFkbaKZZJVksWS1ZMlk4WT560llVWVBZTllaWVhZYllgWWdZbFlp////////WXhZgVmdT15Pq1mjWbJZxlnoWdxZjVnZWdpaJVofWhFaHFoJWhpaQFpsWklaNVo2WmJaalqaWrxavlrLWsJavVrj" + + "Wtda5lrpWtZa+lr7WwxbC1sWWzJa0FsqWzZbPltDW0VbQFtRW1VbWltbW2VbaVtwW3NbdVt4ZYhbeluA//9bg1umW7hbw1vHW8lb1FvQW+Rb5lviW95b5VvrW/Bb9lvzXAVcB1wIXA1cE1wgXCJcKFw4XDlcQVxGXE5cU1xQXE9bcVxsXG5OYlx2XHlcjFyRXJRZm1yr" + + "XLtctly8XLdcxVy+XMdc2VzpXP1c+lztXYxc6l0LXRVdF11cXR9dG10RXRRdIl0aXRldGF1MXVJdTl1LXWxdc112XYddhF2CXaJdnV2sXa5dvV2QXbddvF3JXc1d013SXdZd213rXfJd9V4LXhpeGV4RXhteNl43XkReQ15AXk5eV15UXl9eYl5kXkdedV52XnqevF5/" + + "XqBewV7CXshe0F7P////////XtZe417dXtpe217iXuFe6F7pXuxe8V7zXvBe9F74Xv5fA18JX11fXF8LXxFfFl8pXy1fOF9BX0hfTF9OXy9fUV9WX1dfWV9hX21fc193X4Nfgl9/X4pfiF+RX4dfnl+ZX5hfoF+oX61fvF/WX/tf5F/4X/Ff3WCzX/9gIWBg//9gGWAQ" + + "YClgDmAxYBtgFWArYCZgD2A6YFpgQWBqYHdgX2BKYEZgTWBjYENgZGBCYGxga2BZYIFgjWDnYINgmmCEYJtglmCXYJJgp2CLYOFguGDgYNNgtF/wYL1gxmC1YNhhTWEVYQZg9mD3YQBg9GD6YQNhIWD7YPFhDWEOYUdhPmEoYSdhSmE/YTxhLGE0YT1hQmFEYXNhd2FY" + + "YVlhWmFrYXRhb2FlYXFhX2FdYVNhdWGZYZZhh2GsYZRhmmGKYZFhq2GuYcxhymHJYfdhyGHDYcZhumHLf3lhzWHmYeNh9mH6YfRh/2H9Yfxh/mIAYghiCWINYgxiFGIb////////Yh5iIWIqYi5iMGIyYjNiQWJOYl5iY2JbYmBiaGJ8YoJiiWJ+YpJik2KWYtRig2KU" + + "Ytdi0WK7Ys9i/2LGZNRiyGLcYsxiymLCYsdim2LJYwxi7mLxYydjAmMIYu9i9WNQYz5jTWQcY09jlmOOY4Bjq2N2Y6Njj2OJY59jtWNr//9jaWO+Y+ljwGPGY+NjyWPSY/ZjxGQWZDRkBmQTZCZkNmUdZBdkKGQPZGdkb2R2ZE5lKmSVZJNkpWSpZIhkvGTaZNJkxWTH" + + "ZLtk2GTCZPFk54IJZOBk4WKsZONk72UsZPZk9GTyZPplAGT9ZRhlHGUFZSRlI2UrZTRlNWU3ZTZlOHVLZUhlVmVVZU1lWGVeZV1lcmV4ZYJlg4uKZZtln2WrZbdlw2XGZcFlxGXMZdJl22XZZeBl4WXxZ3JmCmYDZftnc2Y1ZjZmNGYcZk9mRGZJZkFmXmZdZmRmZ2Zo" + + "Zl9mYmZwZoNmiGaOZolmhGaYZp1mwWa5Zslmvma8////////ZsRmuGbWZtpm4GY/ZuZm6WbwZvVm92cPZxZnHmcmZyeXOGcuZz9nNmdBZzhnN2dGZ15nYGdZZ2NnZGeJZ3BnqWd8Z2pnjGeLZ6ZnoWeFZ7dn72e0Z+xns2fpZ7hn5GfeZ91n4mfuZ7lnzmfGZ+dqnGge" + + "aEZoKWhAaE1oMmhO//9os2graFloY2h3aH9on2iPaK1olGidaJtog2quaLlodGi1aKBoumkPaI1ofmkBaMppCGjYaSJpJmjhaQxozWjUaOdo1Wk2aRJpBGjXaONpJWj5aOBo72koaSppGmkjaSFoxml5aXdpXGl4aWtpVGl+aW5pOWl0aT1pWWkwaWFpXmldaYFpammy" + + "aa5p0Gm/acFp02m+ac5b6GnKad1pu2nDaadqLmmRaaBpnGmVabRp3mnoagJqG2n/awpp+WnyaedqBWmxah5p7WoUaetqCmoSasFqI2oTakRqDGpyajZqeGpHamJqWWpmakhqOGoiapBqjWqgaoRqomqj////////apeGF2q7asNqwmq4arNqrGreatFq32qqatpq6mr7" + + "awWGFmr6axJrFpsxax9rOGs3dtxrOZjua0drQ2tJa1BrWWtUa1trX2tha3hreWt/a4BrhGuDa41rmGuVa55rpGuqa6trr2uya7Frs2u3a7xrxmvLa9Nr32vsa+tr82vv//+evmwIbBNsFGwbbCRsI2xebFVsYmxqbIJsjWyabIFsm2x+bGhsc2ySbJBsxGzxbNNsvWzX" + + "bMVs3WyubLFsvmy6bNts72zZbOptH4hNbTZtK209bThtGW01bTNtEm0MbWNtk21kbVpteW1ZbY5tlW/kbYVt+W4VbgpttW3HbeZtuG3Gbext3m3Mbeht0m3Fbfpt2W3kbdVt6m3ubi1ubm4ubhlucm5fbj5uI25rbitudm5Nbh9uQ246bk5uJG7/bh1uOG6CbqpumG7J" + + "brdu0269bq9uxG6ybtRu1W6PbqVuwm6fb0FvEXBMbuxu+G7+bz9u8m8xbu9vMm7M////////bz5vE273b4Zvem94b4FvgG9vb1tv829tb4JvfG9Yb45vkW/Cb2Zvs2+jb6FvpG+5b8Zvqm/fb9Vv7G/Ub9hv8W/ub9twCXALb/pwEXABcA9v/nAbcBpvdHAdcBhwH3Aw" + + "cD5wMnBRcGNwmXCScK9w8XCscLhws3CucN9wy3Dd//9w2XEJcP1xHHEZcWVxVXGIcWZxYnFMcVZxbHGPcftxhHGVcahxrHHXcblxvnHScclx1HHOceBx7HHncfVx/HH5cf9yDXIQchtyKHItcixyMHIycjtyPHI/ckByRnJLclhydHJ+coJygXKHcpJylnKicqdyuXKy" + + "csNyxnLEcs5y0nLicuBy4XL5cvdQD3MXcwpzHHMWcx1zNHMvcylzJXM+c05zT57Yc1dzanNoc3BzeHN1c3tzenPIc7NzznO7c8Bz5XPuc950onQFdG90JXP4dDJ0OnRVdD90X3RZdEF0XHRpdHB0Y3RqdHZ0fnSLdJ50p3TKdM901HPx////////dOB043TndOl07nTy" + + "dPB08XT4dPd1BHUDdQV1DHUOdQ11FXUTdR51JnUsdTx1RHVNdUp1SXVbdUZ1WnVpdWR1Z3VrdW11eHV2dYZ1h3V0dYp1iXWCdZR1mnWddaV1o3XCdbN1w3W1db11uHW8dbF1zXXKddJ12XXjdd51/nX///91/HYBdfB1+nXydfN2C3YNdgl2H3YndiB2IXYidiR2NHYw" + + "djt2R3ZIdkZ2XHZYdmF2YnZodml2anZndmx2cHZydnZ2eHZ8doB2g3aIdot2jnaWdpN2mXaadrB2tHa4drl2unbCds121nbSdt524Xbldud26oYvdvt3CHcHdwR3KXckdx53JXcmdxt3N3c4d0d3Wndod2t3W3dld393fnd5d453i3eRd6B3nnewd7Z3uXe/d7x3vXe7" + + "d8d3zXfXd9p33Hfjd+53/HgMeBJ5JnggeSp4RXiOeHR4hnh8eJp4jHijeLV4qniveNF4xnjLeNR4vni8eMV4ynjs////////eOd42nj9ePR5B3kSeRF5GXkseSt5QHlgeVd5X3laeVV5U3l6eX95inmdeaefS3mqea55s3m5ebp5yXnVeed57HnheeN6CHoNehh6GXog" + + "eh95gHoxejt6Pno3ekN6V3pJemF6Ynppn516cHp5en16iHqXepV6mHqWeql6yHqw//96tnrFesR6v5CDesd6ynrNes961XrTetl62nrdeuF64nrmeu168HsCew97CnsGezN7GHsZex57NXsoezZ7UHt6ewR7TXsLe0x7RXt1e2V7dHtne3B7cXtse257nXuYe597jXuc" + + "e5p7i3uSe497XXuZe8t7wXvMe897tHvGe9176XwRfBR75nvlfGB8AHwHfBN783v3fBd8DXv2fCN8J3wqfB98N3wrfD18THxDfFR8T3xAfFB8WHxffGR8VnxlfGx8dXyDfJB8pHytfKJ8q3yhfKh8s3yyfLF8rny5fL18wHzFfMJ82HzSfNx84ps7fO988nz0fPZ8+n0G" + + "////////fQJ9HH0VfQp9RX1LfS59Mn0/fTV9Rn1zfVZ9Tn1yfWh9bn1PfWN9k32JfVt9j319fZt9un2ufaN9tX3Hfb19q349faJ9r33cfbh9n32wfdh93X3kfd59+33yfeF+BX4KfiN+IX4SfjF+H34Jfgt+In5GfmZ+O341fjl+Q343//9+Mn46fmd+XX5Wfl5+WX5a" + + "fnl+an5pfnx+e36DfdV+fY+ufn9+iH6Jfox+kn6QfpN+lH6Wfo5+m36cfzh/On9Ff0x/TX9Of1B/UX9Vf1R/WH9ff2B/aH9pf2d/eH+Cf4Z/g3+If4d/jH+Uf55/nX+af6N/r3+yf7l/rn+2f7iLcX/Ff8Z/yn/Vf9R/4X/mf+l/83/5mNyABoAEgAuAEoAYgBmAHIAh" + + "gCiAP4A7gEqARoBSgFiAWoBfgGKAaIBzgHKAcIB2gHmAfYB/gISAhoCFgJuAk4CagK1RkICsgNuA5YDZgN2AxIDagNaBCYDvgPGBG4EpgSOBL4FL////////louBRoE+gVOBUYD8gXGBboFlgWaBdIGDgYiBioGAgYKBoIGVgaSBo4FfgZOBqYGwgbWBvoG4gb2BwIHC" + + "gbqByYHNgdGB2YHYgciB2oHfgeCB54H6gfuB/oIBggKCBYIHggqCDYIQghaCKYIrgjiCM4JAglmCWIJdglqCX4Jk//+CYoJogmqCa4IugnGCd4J4gn6CjYKSgquCn4K7gqyC4YLjgt+C0oL0gvOC+oOTgwOC+4L5gt6DBoLcgwmC2YM1gzSDFoMygzGDQIM5g1CDRYMv" + + "gyuDF4MYg4WDmoOqg5+DooOWgyODjoOHg4qDfIO1g3ODdYOgg4mDqIP0hBOD64POg/2EA4PYhAuDwYP3hAeD4IPyhA2EIoQgg72EOIUGg/uEbYQqhDyFWoSEhHeEa4SthG6EgoRphEaELIRvhHmENYTKhGKEuYS/hJ+E2YTNhLuE2oTQhMGExoTWhKGFIYT/hPSFF4UY" + + "hSyFH4UVhRSE/IVAhWOFWIVI////////hUGGAoVLhVWFgIWkhYiFkYWKhaiFbYWUhZuF6oWHhZyFd4V+hZCFyYW6hc+FuYXQhdWF3YXlhdyF+YYKhhOGC4X+hfqGBoYihhqGMIY/hk1OVYZUhl+GZ4ZxhpOGo4aphqqGi4aMhraGr4bEhsaGsIbJiCOGq4bUht6G6Ybs" + + "//+G34bbhu+HEocGhwiHAIcDhvuHEYcJhw2G+YcKhzSHP4c3hzuHJYcphxqHYIdfh3iHTIdOh3SHV4doh26HWYdTh2OHaogFh6KHn4eCh6+Hy4e9h8CH0JbWh6uHxIezh8eHxoe7h++H8ofgiA+IDYf+h/aH94gOh9KIEYgWiBWIIoghiDGINog5iCeIO4hEiEKIUohZ" + + "iF6IYohriIGIfoieiHWIfYi1iHKIgoiXiJKIroiZiKKIjYikiLCIv4ixiMOIxIjUiNiI2YjdiPmJAoj8iPSI6IjyiQSJDIkKiROJQ4keiSWJKokriUGJRIk7iTaJOIlMiR2JYIle////////iWaJZIltiWqJb4l0iXeJfomDiYiJiomTiZiJoYmpiaaJrImvibKJuom9" + + "ib+JwInaidyJ3YnnifSJ+IoDihaKEIoMihuKHYolijaKQYpbilKKRopIinyKbYpsimKKhYqCioSKqIqhipGKpYqmipqKo4rEis2KworaiuuK84rn//+K5IrxixSK4IriiveK3orbiwyLB4saiuGLFosQixeLIIszl6uLJosriz6LKItBi0yLT4tOi0mLVotbi1qLa4tf" + + "i2yLb4t0i32LgIuMi46LkouTi5aLmYuajDqMQYw/jEiMTIxOjFCMVYxijGyMeIx6jIKMiYyFjIqMjYyOjJSMfIyYYh2MrYyqjL2MsoyzjK6MtozIjMGM5IzjjNqM/Yz6jPuNBI0FjQqNB40PjQ2NEJ9OjROMzY0UjRaNZ41tjXGNc42BjZmNwo2+jbqNz43ajdaNzI3b" + + "jcuN6o3rjd+N4438jgiOCY3/jh2OHo4Qjh+OQo41jjCONI5K////////jkeOSY5MjlCOSI5ZjmSOYI4qjmOOVY52jnKOfI6BjoeOhY6EjouOio6TjpGOlI6ZjqqOoY6sjrCOxo6xjr6OxY7IjsuO247jjvyO+47rjv6PCo8FjxWPEo8ZjxOPHI8fjxuPDI8mjzOPO485" + + "j0WPQo8+j0yPSY9Gj06PV49c//+PYo9jj2SPnI+fj6OPrY+vj7eP2o/lj+KP6o/vkIeP9JAFj/mP+pARkBWQIZANkB6QFpALkCeQNpA1kDmP+JBPkFCQUZBSkA6QSZA+kFaQWJBekGiQb5B2lqiQcpCCkH2QgZCAkIqQiZCPkKiQr5CxkLWQ4pDkYkiQ25ECkRKRGZEy" + + "kTCRSpFWkViRY5FlkWmRc5FykYuRiZGCkaKRq5GvkaqRtZG0kbqRwJHBkcmRy5HQkdaR35HhkduR/JH1kfaSHpH/khSSLJIVkhGSXpJXkkWSSZJkkkiSlZI/kkuSUJKckpaSk5KbklqSz5K5kreS6ZMPkvqTRJMu////////kxmTIpMakyOTOpM1kzuTXJNgk3yTbpNW" + + "k7CTrJOtk5STuZPWk9eT6JPlk9iTw5Pdk9CTyJPklBqUFJQTlAOUB5QQlDaUK5Q1lCGUOpRBlFKURJRblGCUYpRelGqSKZRwlHWUd5R9lFqUfJR+lIGUf5WClYeVipWUlZaVmJWZ//+VoJWolaeVrZW8lbuVuZW+lcpv9pXDlc2VzJXVldSV1pXcleGV5ZXiliGWKJYu" + + "li+WQpZMlk+WS5Z3llyWXpZdll+WZpZylmyWjZaYlpWWl5aqlqeWsZaylrCWtJa2lriWuZbOlsuWyZbNiU2W3JcNltWW+ZcElwaXCJcTlw6XEZcPlxaXGZcklyqXMJc5lz2XPpdEl0aXSJdCl0mXXJdgl2SXZpdoUtKXa5dxl3mXhZd8l4GXepeGl4uXj5eQl5yXqJem" + + "l6OXs5e0l8OXxpfIl8uX3Jftn0+X8nrfl/aX9ZgPmAyYOJgkmCGYN5g9mEaYT5hLmGuYb5hw////////mHGYdJhzmKqYr5ixmLaYxJjDmMaY6ZjrmQOZCZkSmRSZGJkhmR2ZHpkkmSCZLJkumT2ZPplCmUmZRZlQmUuZUZlSmUyZVZmXmZiZpZmtma6ZvJnfmduZ3ZnY" + + "mdGZ7ZnumfGZ8pn7mfiaAZoPmgWZ4poZmiuaN5pFmkKaQJpD//+aPppVmk2aW5pXml+aYpplmmSaaZprmmqarZqwmryawJrPmtGa05rUmt6a35rimuOa5prvmuua7pr0mvGa95r7mwabGJsamx+bIpsjmyWbJ5somymbKpsumy+bMptEm0ObT5tNm06bUZtYm3Sbk5uD" + + "m5GblpuXm5+boJuom7SbwJvKm7mbxpvPm9Gb0pvjm+Kb5JvUm+GcOpvym/Gb8JwVnBScCZwTnAycBpwInBKcCpwEnC6cG5wlnCScIZwwnEecMpxGnD6cWpxgnGecdpx4nOec7JzwnQmdCJzrnQOdBp0qnSadr50jnR+dRJ0VnRKdQZ0/nT6dRp1I////////nV2dXp1k" + + "nVGdUJ1ZnXKdiZ2Hnaudb516nZqdpJ2pnbKdxJ3BnbuduJ26ncadz53Cndmd0534nead7Z3vnf2eGp4bnh6edZ55nn2egZ6InouejJ6SnpWekZ6dnqWeqZ64nqqerZdhnsyezp7PntCe1J7cnt6e3Z7gnuWe6J7v//+e9J72nvee+Z77nvye/Z8Hnwh2t58VnyGfLJ8+" + + "n0qfUp9Un2OfX59gn2GfZp9nn2yfap93n3Kfdp+Vn5yfoFgvaceQWXRkUdxxmf//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + + "/////////////////////////////////////////////w==" diff --git a/chardet_test.go b/chardet_test.go index 1f7d46f..4237448 100644 --- a/chardet_test.go +++ b/chardet_test.go @@ -166,6 +166,100 @@ func Test_analyzeByte(t *testing.T) { } } +func Test_analyzeJP(t *testing.T) { + type args struct { + r rune + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "case 0", + args: args{r: '0'}, + want: false, + }, + { + name: "case 1", + args: args{r: 'a'}, + want: false, + }, + { + name: "case 2", + args: args{r: 'A'}, + want: false, + }, + { + name: "case 3", + args: args{r: '9'}, + want: false, + }, + { + name: "case 4", + args: args{r: '*'}, + want: false, + }, + { + name: "case 5", + args: args{r: '?'}, + want: false, + }, + { + name: "case 6", + args: args{r: '&'}, + want: false, + }, + { + name: "case 7", + args: args{r: 'Ö'}, + want: false, + }, + { + name: "case 8", + args: args{r: 'に'}, + want: true, + }, + { + name: "case 9", + args: args{r: '茗'}, + want: true, + }, + { + name: "case 10", + args: args{r: '杆'}, + want: true, + }, + { + name: "case 11", + args: args{r: '荷'}, + want: true, + }, + { + name: "case 12", + args: args{r: '杠'}, + want: true, + }, + { + name: "case 13", + args: args{r: '杙'}, + want: true, + }, + { + name: "case 14", + args: args{r: '杣'}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := analyzeJP(tt.args.r); got != tt.want { + t.Errorf("analyzeJP(%c=0x%x) = %v, want %v", tt.args.r, tt.args.r, got, tt.want) + } + }) + } +} + func Test_analyzeMode(t *testing.T) { type args struct { raw string diff --git a/encoder.go b/encoder.go index 7d3c947..e0efa42 100644 --- a/encoder.go +++ b/encoder.go @@ -237,7 +237,7 @@ func toShiftJIS(raw string) []byte { func encodeShiftJIS(hi byte, lo byte) (byte, byte) { r := uint16(hi)<<8 | uint16(lo) - fmt.Printf("before: r=%x\n", r) + // fmt.Printf("before: r=%x\n", r) if r > 0x8140 && r < 0x9FFC { r -= 0x8140 } else if r > 0xE040 && r < 0xEBBF { @@ -252,10 +252,10 @@ func encodeShiftJIS(hi byte, lo byte) (byte, byte) { hi = uint8(r >> 8) lo = uint8(r & 0xFF) - fmt.Printf("middle: high=%x, low=%x\n", hi, lo) + // fmt.Printf("middle: high=%x, low=%x\n", hi, lo) r = uint16(hi)*uint16(0xC0) + uint16(lo) - fmt.Printf("after: r=%x\n", r) + // fmt.Printf("after: r=%x\n", r) return byte(r >> 8), byte(r & 0xFF) } From 2d330874639fb99a16a776aa21e6f0c24b903db2 Mon Sep 17 00:00:00 2001 From: yeqown Date: Sat, 6 Jul 2024 20:43:01 +0800 Subject: [PATCH 10/13] feat: kanji character parse and encode --- README.md | 4 +-- chardet.go | 22 +++++++++------- chardet_test.go | 66 ++++++++++++++++++++++++++++------------------- cmd/wasm/types.go | 4 +-- encoder.go | 17 +++++++----- qrcode.go | 5 +++- version.go | 5 ++-- 7 files changed, 73 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index bacfd77..2ac3d6f 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ const ( EncModeAlphanumeric // EncModeByte mode ... EncModeByte - // EncModeJP mode ... - EncModeJP + // EncModeKanji mode ... + EncModeKanji ) // WithEncodingMode sets the encoding mode. diff --git a/chardet.go b/chardet.go index 5798de4..0614bb8 100644 --- a/chardet.go +++ b/chardet.go @@ -1,13 +1,15 @@ package qrcode -import ( - "log" -) +import "errors" func init() { restoreKanJi() } +var ( + ErrNotSupportCharacter = errors.New("character set not supported, please check your input data.") +) + // chardet.go refer to https://github.com/chardet/chardet to detect input string's // character set, to see any unsupported character encountered in the input string. @@ -22,8 +24,8 @@ type analyzeEncFunc func(rune) bool // case1: only numbers, use EncModeNumeric. // case2: could not use EncModeNumeric, but you can find all of them in character mapping, use EncModeAlphanumeric. // case3: could not use EncModeAlphanumeric, but you can find all of them in ISO-8859-1 character set, use EncModeByte. -// case4: could not use EncModeByte, use EncModeJP, no more choice. -func analyzeEncodeModeFromRaw(raw string) encMode { +// case4: could not use EncModeByte, use EncModeKanji, no more choice. +func analyzeEncodeModeFromRaw(raw string) (encMode, error) { var ( analyzeFn analyzeEncFunc mode = EncModeNone @@ -37,7 +39,7 @@ func analyzeEncodeModeFromRaw(raw string) encMode { return analyzeAlphaNum case EncModeByte: return analyzeByte - case EncModeJP: + case EncModeKanji: return analyzeJP default: } @@ -72,12 +74,12 @@ func analyzeEncodeModeFromRaw(raw string) encMode { goto reAnalyze } - if mode > EncModeJP { - // If the mode overflow the EncModeJP, means we can't encode the input data. - log.Panicf("character set not supported, please check your input data.") + if mode > EncModeKanji { + // If the mode overflow the EncModeKanji, means we can't encode the input data. + return EncModeNone, ErrNotSupportCharacter } - return mode + return mode, nil } // analyzeNum is r in num encMode diff --git a/chardet_test.go b/chardet_test.go index 4237448..fa1287b 100644 --- a/chardet_test.go +++ b/chardet_test.go @@ -265,19 +265,22 @@ func Test_analyzeMode(t *testing.T) { raw string } tests := []struct { - name string - args args - want encMode + name string + args args + want encMode + wantErr bool }{ { - name: "case 0", - args: args{raw: "123120899231"}, - want: EncModeNumeric, + name: "case 0", + args: args{raw: "123120899231"}, + want: EncModeNumeric, + wantErr: false, }, { - name: "case 1", - args: args{raw: ":/1231H208*99231FBJO"}, - want: EncModeAlphanumeric, + name: "case 1", + args: args{raw: ":/1231H208*99231FBJO"}, + want: EncModeAlphanumeric, + wantErr: false, }, { name: "case 2", @@ -285,19 +288,22 @@ func Test_analyzeMode(t *testing.T) { want: EncModeByte, }, { - name: "case 3", - args: args{raw: "JKAHDOIANKQOIHCMJKASJ"}, - want: EncModeAlphanumeric, + name: "case 3", + args: args{raw: "JKAHDOIANKQOIHCMJKASJ"}, + want: EncModeAlphanumeric, + wantErr: false, }, { - name: "case 4", - args: args{raw: "https://baidu.com?keyword=_JSO==GA"}, - want: EncModeByte, + name: "case 4", + args: args{raw: "https://baidu.com?keyword=_JSO==GA"}, + want: EncModeByte, + wantErr: false, }, { - name: "case 5", - args: args{raw: "茗荷"}, - want: EncModeJP, + name: "case 5", + args: args{raw: "茗荷"}, + want: EncModeKanji, + wantErr: false, }, { name: "case 6 (swedish letter)", @@ -305,20 +311,28 @@ func Test_analyzeMode(t *testing.T) { want: EncModeByte, }, { - name: "case 7 (japanese letter)", - args: args{raw: "朸 朷 杆 杞 杠 杙 杣"}, - want: EncModeJP, + name: "case 7 (japanese letter)", + args: args{raw: "朸 朷 杆 杞 杠 杙 杣"}, + want: EncModeKanji, + wantErr: false, }, { - name: "issue#28", - args: args{raw: "a"}, - want: EncModeByte, + name: "issue#28", + args: args{raw: "a"}, + want: EncModeByte, + wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := analyzeEncodeModeFromRaw(tt.args.raw); got != tt.want { - t.Errorf("analyzeEncodeModeFromRaw() = %v, want %v", got, tt.want) + got, err := analyzeEncodeModeFromRaw(tt.args.raw) + if (err != nil) != tt.wantErr { + t.Errorf("analyzeMode() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if got != tt.want { + t.Errorf("analyzeMode() = %v, want %v", got, tt.want) } }) } diff --git a/cmd/wasm/types.go b/cmd/wasm/types.go index f81aae3..7c9783a 100644 --- a/cmd/wasm/types.go +++ b/cmd/wasm/types.go @@ -89,8 +89,8 @@ func (o *genOption) encodeOptions() []qrcode.EncodeOption { out = append(out, qrcode.WithEncodingMode(qrcode.EncModeNumeric)) case uint8(qrcode.EncModeByte): out = append(out, qrcode.WithEncodingMode(qrcode.EncModeByte)) - case uint8(qrcode.EncModeJP): - out = append(out, qrcode.WithEncodingMode(qrcode.EncModeJP)) + case uint8(qrcode.EncModeKanji): + out = append(out, qrcode.WithEncodingMode(qrcode.EncModeKanji)) } switch o.encodeOption.ecLevel { diff --git a/encoder.go b/encoder.go index e0efa42..fc30d07 100644 --- a/encoder.go +++ b/encoder.go @@ -20,7 +20,7 @@ import ( // - EncModeNumeric: numeric encoding // - EncModeAlphanumeric: alphanumeric encoding // - EncModeByte: byte encoding -// - EncModeJP: japanese encoding +// - EncModeKanji: japanese encoding // // The encoding mode is determined by the data to be encoded. For example, if // the data to be encoded is all numeric, the encoding mode will be EncModeNumeric. @@ -41,7 +41,9 @@ const ( // EncModeByte mode ... EncModeByte // EncModeJP mode ... + // @Deprecated use EncModeKanji instead EncModeJP + EncModeKanji = EncModeJP ) var ( @@ -60,8 +62,8 @@ func getEncModeName(mode encMode) string { return "alphanumeric" case EncModeByte: return "byte" - case EncModeJP: - return "japan" + case EncModeKanji: + return "kanji" default: return "unknown(" + strconv.Itoa(int(mode)) + ")" } @@ -76,7 +78,7 @@ func getEncodeModeIndicator(mode encMode) *binary.Binary { return binary.New(false, false, true, false) case EncModeByte: return binary.New(false, true, false, false) - case EncModeJP: + case EncModeKanji: return binary.New(true, false, false, false) default: panic("no indicator") @@ -98,7 +100,7 @@ type encoder struct { func newEncoder(m encMode, ec ecLevel, v version) *encoder { switch m { - case EncModeNumeric, EncModeAlphanumeric, EncModeByte, EncModeJP: + case EncModeNumeric, EncModeAlphanumeric, EncModeByte, EncModeKanji: default: panic("unsupported data encoding mode in newEncoder()") } @@ -121,7 +123,7 @@ func (e *encoder) Encode(raw string) (*binary.Binary, error) { switch e.mode { case EncModeNumeric, EncModeAlphanumeric, EncModeByte: data = []byte(raw) - case EncModeJP: + case EncModeKanji: data = toShiftJIS(raw) default: log.Printf("unsupported encoding mode: %s", getEncModeName(e.mode)) @@ -141,7 +143,7 @@ func (e *encoder) Encode(raw string) (*binary.Binary, error) { e.encodeAlphanumeric(data) case EncModeByte: e.encodeByte(data) - case EncModeJP: + case EncModeKanji: e.encodeKanji(data) default: log.Printf("unsupported encoding mode: %s", getEncModeName(e.mode)) @@ -224,6 +226,7 @@ func toShiftJIS(raw string) []byte { data := []byte(s2) if len(data)%2 != 0 { + // BUG: encode bytes with Shift JIS must be times of 2, cause panic here log.Panicf("shift JIS encoded []byte must be times of 2, but got %d", len(data)) } diff --git a/qrcode.go b/qrcode.go index ea8c6a9..311c127 100644 --- a/qrcode.go +++ b/qrcode.go @@ -99,7 +99,10 @@ func (q *QRCode) Dimension() int { func (q *QRCode) init() (err error) { // choose encode mode (num, alpha num, byte, Japanese) if q.encodingOption.EncMode == EncModeAuto { - q.encodingOption.EncMode = analyzeEncodeModeFromRaw(q.sourceText) + q.encodingOption.EncMode, err = analyzeEncodeModeFromRaw(q.sourceText) + if err != nil { + return fmt.Errorf("init: analyze encode mode failed: %v", err) + } } // choose version diff --git a/version.go b/version.go index 83cdee4..760c621 100644 --- a/version.go +++ b/version.go @@ -7,8 +7,9 @@ import ( "sync" // "github.com/skip2/go-qrcode/bitset" - "github.com/yeqown/reedsolomon/binary" "unicode/utf8" + + "github.com/yeqown/reedsolomon/binary" ) func init() { @@ -299,7 +300,7 @@ func analyzeVersion(raw string, ec ecLevel, mode encMode) (*version, error) { mark = versions[step].Cap.AlphaNumeric case EncModeByte: mark = versions[step].Cap.Byte - case EncModeJP: + case EncModeKanji: mark = versions[step].Cap.JP default: return nil, errMissMatchedEncodeType From 08441ae90d20f5a9cb1e80266aa1be8a9c993bbb Mon Sep 17 00:00:00 2001 From: yeqown Date: Thu, 18 Jul 2024 21:58:51 +0800 Subject: [PATCH 11/13] feat: adjust order between kanji and byte. --- chardet.go | 23 ++++++++++++----------- chardet_test.go | 6 +++--- encoder.go | 35 +++++++++++++++++++++-------------- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/chardet.go b/chardet.go index 0614bb8..7a54710 100644 --- a/chardet.go +++ b/chardet.go @@ -1,13 +1,15 @@ package qrcode -import "errors" +import ( + "errors" +) func init() { restoreKanJi() } var ( - ErrNotSupportCharacter = errors.New("character set not supported, please check your input data.") + ErrNotSupportCharacter = errors.New("character set not supported, please check your input data") ) // chardet.go refer to https://github.com/chardet/chardet to detect input string's @@ -22,9 +24,9 @@ type analyzeEncFunc func(rune) bool // reference: https://en.wikipedia.org/wiki/QR_code // // case1: only numbers, use EncModeNumeric. -// case2: could not use EncModeNumeric, but you can find all of them in character mapping, use EncModeAlphanumeric. -// case3: could not use EncModeAlphanumeric, but you can find all of them in ISO-8859-1 character set, use EncModeByte. -// case4: could not use EncModeByte, use EncModeKanji, no more choice. +// case2: could not use EncModeNumeric, but can find them all in character mapping, use EncModeAlphanumeric. +// case3: could not use EncModeAlphanumeric, but can find them all Shift JIS character set, use EncModeKanji. +// case4: could not use EncModeKanji, use EncModeByte. func analyzeEncodeModeFromRaw(raw string) (encMode, error) { var ( analyzeFn analyzeEncFunc @@ -37,10 +39,10 @@ func analyzeEncodeModeFromRaw(raw string) (encMode, error) { return analyzeNum case EncModeAlphanumeric: return analyzeAlphaNum - case EncModeByte: - return analyzeByte case EncModeKanji: return analyzeJP + case EncModeByte: + return analyzeByte default: } @@ -74,7 +76,7 @@ func analyzeEncodeModeFromRaw(raw string) (encMode, error) { goto reAnalyze } - if mode > EncModeKanji { + if mode > EncModeByte { // If the mode overflow the EncModeKanji, means we can't encode the input data. return EncModeNone, ErrNotSupportCharacter } @@ -99,10 +101,9 @@ func analyzeAlphaNum(r rune) bool { return false } -// analyzeByte contains ISO-8859-1 character set +// analyzeByte always return true, since byte (utf8) mode can encode all characters. func analyzeByte(r rune) bool { - // ISO-8859-1 character set, if r > \u00ff, means it's not in ISO-8859-1. - return r <= '\u00ff' + return true } // analyzeJP contains Kanji character set diff --git a/chardet_test.go b/chardet_test.go index fa1287b..9d71710 100644 --- a/chardet_test.go +++ b/chardet_test.go @@ -154,7 +154,7 @@ func Test_analyzeByte(t *testing.T) { { name: "case 8", args: args{byt: 'に'}, - want: false, + want: true, }, } for _, tt := range tests { @@ -312,12 +312,12 @@ func Test_analyzeMode(t *testing.T) { }, { name: "case 7 (japanese letter)", - args: args{raw: "朸 朷 杆 杞 杠 杙 杣"}, + args: args{raw: "嵋嶄"}, want: EncModeKanji, wantErr: false, }, { - name: "issue#28", + name: "issue#28 alphanum mode does not support lower case letter", args: args{raw: "a"}, want: EncModeByte, wantErr: false, diff --git a/encoder.go b/encoder.go index fc30d07..5effc53 100644 --- a/encoder.go +++ b/encoder.go @@ -19,8 +19,8 @@ import ( // - EncModeNone: no encoding // - EncModeNumeric: numeric encoding // - EncModeAlphanumeric: alphanumeric encoding +// - EncModeKanji: japanese kanji encoding // - EncModeByte: byte encoding -// - EncModeKanji: japanese encoding // // The encoding mode is determined by the data to be encoded. For example, if // the data to be encoded is all numeric, the encoding mode will be EncModeNumeric. @@ -32,18 +32,25 @@ type encMode uint const ( // EncModeAuto will trigger a detection of the letter set from the input data. EncModeAuto = 0 - // EncModeNone mode ... - EncModeNone encMode = 1 << iota - // EncModeNumeric mode ... - EncModeNumeric - // EncModeAlphanumeric mode ... - EncModeAlphanumeric - // EncModeByte mode ... - EncModeByte + + // EncModeNone mode represents no encoding, usually used as initial value of encMode + EncModeNone encMode = 2 + + // EncModeNumeric mode support only numeric character set (0-9) + EncModeNumeric encMode = 4 + + // EncModeAlphanumeric mode support only alphanumeric character set (0-9, A-Z, SP, $%*+-./ or :) + EncModeAlphanumeric encMode = 8 + // EncModeJP mode ... // @Deprecated use EncModeKanji instead - EncModeJP + EncModeJP encMode = 16 + // EncModeKanji mode support only Shift JIS encoding character set. + // From 0x8140 to 0x9FFC and 0xE040 to 0xEBBF. EncModeKanji = EncModeJP + + // EncModeByte mode support ISO-8859-1 character set by default, but also support UTF-8. + EncModeByte encMode = 32 ) var ( @@ -60,10 +67,10 @@ func getEncModeName(mode encMode) string { return "numeric" case EncModeAlphanumeric: return "alphanumeric" - case EncModeByte: - return "byte" case EncModeKanji: return "kanji" + case EncModeByte: + return "byte" default: return "unknown(" + strconv.Itoa(int(mode)) + ")" } @@ -141,10 +148,10 @@ func (e *encoder) Encode(raw string) (*binary.Binary, error) { e.encodeNumeric(data) case EncModeAlphanumeric: e.encodeAlphanumeric(data) - case EncModeByte: - e.encodeByte(data) case EncModeKanji: e.encodeKanji(data) + case EncModeByte: + e.encodeByte(data) default: log.Printf("unsupported encoding mode: %s", getEncModeName(e.mode)) } From 5bb225aabaaaa34d9cbaa5a9a03de624012788d3 Mon Sep 17 00:00:00 2001 From: Yeqown Date: Fri, 13 Mar 2026 17:55:03 +0800 Subject: [PATCH 12/13] fix: kanji encoding and analyze --- .issues/106/issue.go | 61 ++++ .issues/{issue69/issue69.go => 69/issue.go} | 0 chardet.go | 183 +++--------- chardet_test.go | 62 +++- docs/kanji-encoding.md | 309 ++++++++++++++++++++ encoder.go | 72 +++-- encoder_test.go | 228 ++++++++++++++- go.mod | 1 + go.sum | 2 + go.work.sum | 5 +- mask_test.go | 2 +- qrcode.go | 39 ++- qrcode_test.go | 132 ++++++++- version.go | 11 +- 14 files changed, 915 insertions(+), 192 deletions(-) create mode 100644 .issues/106/issue.go rename .issues/{issue69/issue69.go => 69/issue.go} (100%) create mode 100644 docs/kanji-encoding.md diff --git a/.issues/106/issue.go b/.issues/106/issue.go new file mode 100644 index 0000000..bab2fc8 --- /dev/null +++ b/.issues/106/issue.go @@ -0,0 +1,61 @@ +/* + * Link: https://github.com/yeqown/go-qrcode/issues/106 + * Title: Feature: Add Kanji encoding mode support + * Author: fdelbos(https://github.com/fdelbos) + */ + +package main + +import ( + "fmt" + + yeqown "github.com/yeqown/go-qrcode/v2" + "github.com/yeqown/go-qrcode/writer/compressed" +) + +/* +See https://github.com/yeqown/go-qrcode/issues/106 +Feature: Add Kanji encoding mode support for QR codes + +Results: +Content length: 3 // source text length (3 Kanji characters) +qr-kanji.png: bytes +*/ +func main() { + // Pure Kanji text with explicit Kanji mode + // But Shift-JIS only supports Kanji characters, not full-width alphanumeric, + // so we can't encode `https://google.com` in Kanji mode + content := "日本語" + + fmt.Printf("Content length: %d\n", len([]rune(content))) + + qrc, err := yeqown.NewWith(content, + yeqown.WithEncodingMode(yeqown.EncModeKanji), + ) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + return + } + + // Save to file + option := &compressed.Option{ + Padding: 4, + BlockSize: 1, + } + w, err := compressed.New("qr-kanji.png", option) + if err != nil { + panic(err) + } + defer w.Close() + + if err = qrc.Save(w); err != nil { + panic(err) + } + + fmt.Println("QR code saved to qr-kanji.png") + + // Note: If your input might contain non-Kanji characters, use EncModeAuto: + // qrc, err := yeqown.NewWith(anyText, + // yeqown.WithEncodingMode(yeqown.EncModeAuto), + // ) +} diff --git a/.issues/issue69/issue69.go b/.issues/69/issue.go similarity index 100% rename from .issues/issue69/issue69.go rename to .issues/69/issue.go diff --git a/chardet.go b/chardet.go index 7a54710..8035b9f 100644 --- a/chardet.go +++ b/chardet.go @@ -2,11 +2,10 @@ package qrcode import ( "errors" -) -func init() { - restoreKanJi() -} + "golang.org/x/text/encoding/japanese" + "golang.org/x/text/transform" +) var ( ErrNotSupportCharacter = errors.New("character set not supported, please check your input data") @@ -106,148 +105,42 @@ func analyzeByte(r rune) bool { return true } -// analyzeJP contains Kanji character set -// http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml +// analyzeJP checks if a character can be encoded in QR Code Kanji mode. +// A character is valid for Kanji mode if: +// 1. It is in the CJK Unified Ideographs block (U+4E00-U+9FFF) +// 2. It can be converted to Shift JIS +// 3. The resulting Shift JIS value is in the valid QR Code ranges: +// - 0x8140-0x9FFC (first range) +// - 0xE040-0xEBBF (second range) func analyzeJP(r rune) bool { - _, ok := __UNICODE_TO_QR_KANJI[uint32(r)] - return ok -} - -var ( - __UNICODE_TO_QR_KANJI map[uint32]struct{} -) + // Check if the character is in the CJK Unified Ideographs block + // This is a quick pre-check to avoid unnecessary conversion attempts + // U+4E00-U+9FFF: CJK Unified Ideographs + // U+3400-U+4DBF: CJK Unified Ideographs Extension A + // U+F900-U+FAFF: CJK Compatibility Ideographs + isCJK := (r >= 0x4E00 && r <= 0x9FFF) || + (r >= 0x3400 && r <= 0x4DBF) || + (r >= 0xF900 && r <= 0xFAFF) + + if !isCJK { + return false + } -func restoreKanJi() { - // private static short[] UNICODE_TO_QR_KANJI = new short[1 << 16]; - - // Arrays.fill(UNICODE_TO_QR_KANJI, (short)-1); - // byte[] bytes = Base64.getDecoder().decode(PACKED_QR_KANJI_TO_UNICODE); - // for (int i = 0; i < bytes.length; i += 2) { - // char c = (char)(((bytes[i] & 0xFF) << 8) | (bytes[i + 1] & 0xFF)); - // if (c == 0xFFFF) - // continue; - // assert UNICODE_TO_QR_KANJI[c] == -1; - // UNICODE_TO_QR_KANJI[c] = (short)(i / 2); - // } - - __UNICODE_TO_QR_KANJI = make(map[uint32]struct{}, 1<<16) - // restore from __PACKED_QR_KANJI_TO_UNICODE - for i := 0; i < len(__PACKED_QR_KANJI_TO_UNICODE); i += 2 { - c := (uint32(__PACKED_QR_KANJI_TO_UNICODE[i]) << 8) | uint32(__PACKED_QR_KANJI_TO_UNICODE[i+1]) - if c == 0xFFFF { - continue - } - __UNICODE_TO_QR_KANJI[c] = struct{}{} + // Try to convert the character to Shift JIS + // If conversion fails, it's not a valid Kanji character for QR Code + enc := japanese.ShiftJIS.NewEncoder() + s2, _, err := transform.String(enc, string(r)) + if err != nil || len(s2) != 2 { + return false } -} -var __PACKED_QR_KANJI_TO_UNICODE = "MAAwATAC/wz/DjD7/xr/G/8f/wEwmzCcALT/QACo/z7/4/8/MP0w/jCdMJ4wA07dMAUwBjAHMPwgFSAQ/w8AXDAcIBb/XCAmICUgGCAZIBwgHf8I/wkwFDAV/zv/Pf9b/10wCDAJMAowCzAMMA0wDjAPMBAwEf8LIhIAsQDX//8A9/8dImD/HP8eImYiZyIeIjQmQiZA" + - "ALAgMiAzIQP/5f8EAKIAo/8F/wP/Bv8K/yAApyYGJgUlyyXPJc4lxyXGJaEloCWzJbIlvSW8IDswEiGSIZAhkSGTMBP/////////////////////////////IggiCyKGIocigiKDIioiKf////////////////////8iJyIoAKwh0iHUIgAiA///////////////////" + - "//////////8iICKlIxIiAiIHImEiUiJqImsiGiI9Ih0iNSIrIiz//////////////////yErIDAmbyZtJmogICAhALb//////////yXv/////////////////////////////////////////////////xD/Ef8S/xP/FP8V/xb/F/8Y/xn///////////////////8h" + - "/yL/I/8k/yX/Jv8n/yj/Kf8q/yv/LP8t/y7/L/8w/zH/Mv8z/zT/Nf82/zf/OP85/zr///////////////////9B/0L/Q/9E/0X/Rv9H/0j/Sf9K/0v/TP9N/07/T/9Q/1H/Uv9T/1T/Vf9W/1f/WP9Z/1r//////////zBBMEIwQzBEMEUwRjBHMEgwSTBKMEswTDBN" + - "ME4wTzBQMFEwUjBTMFQwVTBWMFcwWDBZMFowWzBcMF0wXjBfMGAwYTBiMGMwZDBlMGYwZzBoMGkwajBrMGwwbTBuMG8wcDBxMHIwczB0MHUwdjB3MHgweTB6MHswfDB9MH4wfzCAMIEwgjCDMIQwhTCGMIcwiDCJMIowizCMMI0wjjCPMJAwkTCSMJP/////////////" + - "////////////////////////MKEwojCjMKQwpTCmMKcwqDCpMKowqzCsMK0wrjCvMLAwsTCyMLMwtDC1MLYwtzC4MLkwujC7MLwwvTC+ML8wwDDBMMIwwzDEMMUwxjDHMMgwyTDKMMswzDDNMM4wzzDQMNEw0jDTMNQw1TDWMNcw2DDZMNow2zDcMN0w3jDf//8w4DDh" + - "MOIw4zDkMOUw5jDnMOgw6TDqMOsw7DDtMO4w7zDwMPEw8jDzMPQw9TD2/////////////////////wORA5IDkwOUA5UDlgOXA5gDmQOaA5sDnAOdA54DnwOgA6EDowOkA6UDpgOnA6gDqf////////////////////8DsQOyA7MDtAO1A7YDtwO4A7kDugO7A7wDvQO+" + - "A78DwAPBA8MDxAPFA8YDxwPIA8n/////////////////////////////////////////////////////////////////////////////////////////////////////////////BBAEEQQSBBMEFAQVBAEEFgQXBBgEGQQaBBsEHAQdBB4EHwQgBCEEIgQjBCQEJQQm" + - "BCcEKAQpBCoEKwQsBC0ELgQv////////////////////////////////////////BDAEMQQyBDMENAQ1BFEENgQ3BDgEOQQ6BDsEPAQ9//8EPgQ/BEAEQQRCBEMERARFBEYERwRIBEkESgRLBEwETQROBE///////////////////////////////////yUAJQIlDCUQ" + - "JRglFCUcJSwlJCU0JTwlASUDJQ8lEyUbJRclIyUzJSslOyVLJSAlLyUoJTclPyUdJTAlJSU4JUL/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "/////////////////////////////////////06cVRZaA5Y/VMBhG2MoWfaQIoR1gxx6UGCqY+FuJWXthGaCppv1aJNXJ2WhYnFbm1nQhnuY9H1ifb6bjmIWfJ+It1uJXrVjCWaXaEiVx5eNZ09O5U8KT01PnVBJVvJZN1nUWgFcCWDfYQ9hcGYTaQVwunVPdXB5+32t" + - "fe+Aw4QOiGOLApBVkHpTO06VTqVX34CykMF4704AWPFuopA4ejKDKIKLnC9RQVNwVL1U4VbgWftfFZjybeuA5IUt////////lmKWcJagl/tUC1PzW4dwz3+9j8KW6FNvnVx6uk4ReJOB/G4mVhhVBGsdhRqcO1nlU6ltZnTclY9WQk6RkEuW8oNPmQxT4VW2WzBfcWYg" + - "ZvNoBGw4bPNtKXRbdsh6Tpg0gvGIW4pgku1tsnWrdsqZxWCmiwGNipWyaY5TrVGG//9XElgwWURbtF72YChjqWP0bL9vFHCOcRRxWXHVcz9+AYJ2gtGFl5BgkludG1hpZbxsWnUlUflZLlllX4Bf3GK8ZfpqKmsna7Rzi3/BiVadLJ0OnsRcoWyWg3tRBFxLYbaBxmh2" + - "cmFOWU/6U3hgaW4pek+X804LUxZO7k9VTz1PoU9zUqBT71YJWQ9awVu2W+F50WaHZ5xntmtMbLNwa3PCeY15vno8e4eCsYLbgwSDd4Pvg9OHZoqyVimMqI/mkE6XHoaKT8Rc6GIRcll1O4Hlgr2G/ozAlsWZE5nVTstPGonjVt5YSljKXvtf62AqYJRgYmHQYhJi0GU5" + - "////////m0FmZmiwbXdwcHVMdoZ9dYKlh/mVi5aOjJ1R8VK+WRZUs1uzXRZhaGmCba94jYTLiFeKcpOnmrhtbJmohtlXo2f/hs6SDlKDVodUBF7TYuFkuWg8aDhru3NyeLp6a4maidKNa48DkO2Vo5aUl2lbZlyzaX2YTZhOY5t7IGor//9qf2i2nA1vX1JyVZ1gcGLs" + - "bTtuB27RhFuJEI9EThScOVP2aRtqOpeEaCpRXHrDhLKR3JOMVludKGgigwWEMXylUgiCxXTmTn5Pg1GgW9JSClLYUudd+1WaWCpZ5luMW5hb215yXnlgo2EfYWNhvmPbZWJn0WhTaPprPmtTbFdvIm+Xb0V0sHUYduN3C3r/e6F8IX3pfzZ/8ICdgmaDnomzisyMq5CE" + - "lFGVk5WRlaKWZZfTmSiCGE44VCtcuF3Mc6l2THc8XKl/640LlsGYEZhUmFhPAU8OU3FVnFZoV/pZR1sJW8RckF4MXn5fzGPuZzpl12XiZx9oy2jE////////al9eMGvFbBdsfXV/eUhbY3oAfQBfvYmPihiMtI13jsyPHZjimg6bPE6AUH1RAFmTW5xiL2KAZOxrOnKg" + - "dZF5R3+ph/uKvItwY6yDypegVAlUA1WraFRqWIpweCdndZ7NU3RbooEahlCQBk4YTkVOx08RU8pUOFuuXxNgJWVR//9nPWxCbHJs43B4dAN6dnquewh9Gnz+fWZl53JbU7tcRV3oYtJi4GMZbiCGWooxjd2S+G8BeaabWk6oTqtOrE+bT6BQ0VFHevZRcVH2U1RTIVN/" + - "U+tVrFiDXOFfN19KYC9gUGBtYx9lWWpLbMFywnLtd++A+IEFggiFTpD3k+GX/5lXmlpO8FHdXC1mgWltXEBm8ml1c4loUHyBUMVS5FdHXf6TJmWkayNrPXQ0eYF5vXtLfcqCuYPMiH+JX4s5j9GR0VQfkoBOXVA2U+VTOnLXc5Z36YLmjq+ZxpnImdJRd2Eahl5VsHp6" + - "UHZb05BHloVOMmrbkedcUVxI////////Y5h6n2yTl3SPYXqqcYqWiHyCaBd+cGhRk2xS8lQbhauKE3+kjs2Q4VNmiIh5QU/CUL5SEVFEVVNXLXPqV4tZUV9iX4RgdWF2YWdhqWOyZDplbGZvaEJuE3Vmej18+31MfZl+S39rgw6DSobNigiKY4tmjv2YGp2PgriPzpvo" + - "//9Sh2IfZINvwJaZaEFQkWsgbHpvVHp0fVCIQIojZwhO9lA5UCZQZVF8UjhSY1WnVw9YBVrMXvphsmH4YvNjcmkcailyfXKscy54FHhvfXl3DICpiYuLGYzijtKQY5N1lnqYVZoTnnhRQ1OfU7Nee18mbhtukHOEc/59Q4I3igCK+pZQTk5QC1PkVHxW+lnRW2Rd8V6r" + - "XydiOGVFZ69uVnLQfMqItIChgOGD8IZOioeN6JI3lseYZ58TTpROkk8NU0hUSVQ+Wi9fjF+hYJ9op2qOdFp4gYqeiqSLd5GQTl6byU6kT3xPr1AZUBZRSVFsUp9SuVL+U5pT41QR////////VA5ViVdRV6JZfVtUW11bj13lXedd9154XoNeml63XxhgUmFMYpdi2GOn" + - "ZTtmAmZDZvRnbWghaJdpy2xfbSptaW4vbp11MnaHeGx6P3zgfQV9GH1efbGAFYADgK+AsYFUgY+CKoNSiEyIYYsbjKKM/JDKkXWScXg/kvyVpJZN//+YBZmZmtidO1JbUqtT91QIWNVi92/gjGqPX565UUtSO1RKVv16QJF3nWCe0nNEbwmBcHURX/1g2pqoctuPvGtk" + - "mANOylbwV2RYvlpaYGhhx2YPZgZoOWixbfd11X06gm6bQk6bT1BTyVUGXW9d5l3uZ/tsmXRzeAKKUJOWiN9XUF6nYytQtVCsUY1nAFTJWF5Zu1uwX2liTWOhaD1rc24IcH2Rx3KAeBV4JnltZY59MIPciMGPCZabUmRXKGdQf2qMoVG0V0KWKlg6aYqAtFSyXQ5X/HiV" + - "nfpPXFJKVItkPmYoZxRn9XqEe1Z9IpMvaFybrXs5UxlRilI3////////W99i9mSuZOZnLWu6hamW0XaQm9ZjTJMGm6t2v2ZSTglQmFPCXHFg6GSSZWNoX3Hmc8p1I3uXfoKGlYuDjNuReJkQZaxmq2uLTtVO1E86T39SOlP4U/JV41bbWOtZy1nJWf9bUFxNXgJeK1/X" + - "YB1jB2UvW1xlr2W9ZehnnWti//9re2wPc0V5SXnBfPh9GX0rgKKBAoHziZaKXoppimaKjIrujMeM3JbMmPxrb06LTzxPjVFQW1db+mFIYwFmQmshbstsu3I+dL111HjBeTqADIAzgeqElI+ebFCef18Pi1idK3r6jvhbjZbrTgNT8Vf3WTFayVukYIluf28Gdb6M6luf" + - "hQB74FByZ/SCnVxhhUp+HoIOUZlcBGNojWZlnHFueT59F4AFix2OypBuhseQqlAfUvpcOmdTcHxyNZFMkciTK4LlW8JfMWD5TjtT1luIYktnMWuKculz4HougWuNo5FSmZZRElPXVGpb/2OIajl9rJcAVtpTzlRo////////W5dcMV3eT+5hAWL+bTJ5wHnLfUJ+TX/S" + - "ge2CH4SQiEaJcouQjnSPL5AxkUuRbJbGkZxOwE9PUUVTQV+TYg5n1GxBbgtzY34mkc2Sg1PUWRlbv23ReV1+LnybWH5xn1H6iFOP8E/KXPtmJXeseuOCHJn/UcZfqmXsaW9riW3z//9ulm9kdv59FF3hkHWRh5gGUeZSHWJAZpFm2W4aXrZ90n9yZviFr4X3ivhSqVPZ" + - "WXNej1+QYFWS5JZkULdRH1LdUyBTR1PsVOhVRlUxVhdZaFm+WjxbtVwGXA9cEVwaXoReil7gX3Bif2KEYttjjGN3ZgdmDGYtZnZnfmiiah9qNWy8bYhuCW5YcTxxJnFndcd3AXhdeQF5ZXnweuB7EXynfTmAloPWhIuFSYhdiPOKH4o8ilSKc4xhjN6RpJJmk36UGJac" + - "l5hOCk4ITh5OV1GXUnBXzlg0WMxbIl44YMVk/mdhZ1ZtRHK2dXN6Y4S4i3KRuJMgVjFX9Jj+////////Yu1pDWuWce1+VIB3gnKJ5pjfh1WPsVw7TzhP4U+1VQdaIFvdW+lfw2FOYy9lsGZLaO5pm214bfF1M3W5dx95XnnmfTOB44KvhaqJqoo6jquPm5Aykd2XB066" + - "TsFSA1h1WOxcC3UaXD2BTooKj8WWY5dteyWKz5gIkWJW81Oo//+QF1Q5V4JeJWOobDRwindhfIt/4IhwkEKRVJMQkxiWj3RemsRdB11pZXBnoo2olttjbmdJaRmDxZgXlsCI/m+EZHpb+E4WcCx1XWYvUcRSNlLiWdNfgWAnYhBlP2V0Zh9mdGjyaBZrY24FcnJ1H3bb" + - "fL6AVljwiP2Jf4qgipOKy5AdkZKXUpdZZYl6DoEGlrteLWDcYhplpWYUZ5B383pNfE1+PoEKjKyNZI3hjl94qVIHYtljpWRCYpiKLXqDe8CKrJbqfXaCDIdJTtlRSFNDU2Bbo1wCXBZd3WImYkdksGgTaDRsyW1FbRdn029ccU5xfWXLen97rX3a////////fkp/qIF6" + - "ghuCOYWmim6Mzo31kHiQd5KtkpGVg5uuUk1VhG84cTZRaHmFflWBs3zOVkxYUVyoY6pm/mb9aVpy2XWPdY55DnlWed98l30gfUSGB4o0ljuQYZ8gUOdSdVPMU+JQCVWqWO5ZT3I9W4tcZFMdYONg82NcY4NjP2O7//9kzWXpZvld42nNaf1vFXHlTol16Xb4epN8333P" + - "fZyAYYNJg1iEbIS8hfuIxY1wkAGQbZOXlxyaElDPWJdhjoHThTWNCJAgT8NQdFJHU3Ngb2NJZ19uLI2zkB9P11xejMplz32aU1KIllF2Y8NbWFtrXApkDWdRkFxO1lkaWSpscIpRVT5YFVmlYPBiU2fBgjVpVZZAmcSaKE9TWAZb/oAQXLFeL1+FYCBhS2I0Zv9s8G7e" + - "gM6Bf4LUiIuMuJAAkC6Wip7bm9tO41PwWSd7LJGNmEyd+W7dcCdTU1VEW4ViWGKeYtNsom/vdCKKF5Q4b8GK/oM4UeeG+FPq////////U+lPRpBUj7BZaoExXf166o+/aNqMN3L4nEhqPYqwTjlTWFYGV2ZixWOiZeZrTm3hbltwrXfteu97qn27gD2AxobLipWTW1bj" + - "WMdfPmWtZpZqgGu1dTeKx1Akd+VXMF8bYGVmemxgdfR6Gn9ugfSHGJBFmbN7yXVcevl7UYTE//+QEHnpepKDNlrhd0BOLU7yW5lf4GK9Zjxn8WzohmuId4o7kU6S85nQahdwJnMqgueEV4yvTgFRRlHLVYtb9V4WXjNegV8UXzVfa1+0YfJjEWaiZx1vbnJSdTp3OoB0" + - "gTmBeId2ir+K3I2FjfOSmpV3mAKc5VLFY1d29GcVbIhzzYzDk66Wc20lWJxpDmnMj/2TmnXbkBpYWmgCY7Rp+09Dbyxn2I+7hSZ9tJNUaT9vcFdqWPdbLH0scipUCpHjnbROrU9OUFxQdVJDjJ5USFgkW5peHV6VXq1e918fYIxitWM6Y9Bor2xAeId5jnoLfeCCR4oC" + - "iuaORJAT////////kLiRLZHYnw5s5WRYZOJldW70doR7G5Bpk9FuulTyX7lkpI9Nj+2SRFF4WGtZKVxVXpdt+36PdRyMvI7imFtwuU8da79vsXUwlvtRTlQQWDVYV1msXGBfkmWXZ1xuIXZ7g9+M7ZAUkP2TTXgleDpSql6mVx9ZdGASUBJRWlGs//9RzVIAVRBYVFhY" + - "WVdblVz2XYtgvGKVZC1ncWhDaLxo33bXbdhub22bcG9xyF9Tddh5d3tJe1R7UnzWfXFSMIRjhWmF5IoOiwSMRo4PkAOQD5QZlnaYLZowldhQzVLVVAxYAlwOYadknm0ed7N65YD0hASQU5KFXOCdB1M/X5dfs22ccnl3Y3m/e+Rr0nLsiq1oA2phUfh6gWk0XEqc9oLr" + - "W8WRSXAeVnhcb2DHZWZsjIxakEGYE1RRZseSDVlIkKNRhU5NUeqFmYsOcFhjepNLaWKZtH4EdXdTV2lgjt+W42xdToxcPF8Qj+lTAozRgImGeV7/ZeVOc1Fl////////WYJcP5fuTvtZil/Nio1v4XmweWJb54RxcytxsV50X/Vje2SaccN8mE5DXvxOS1fcVqJgqW/D" + - "fQ2A/YEzgb+PsomXhqRd9GKKZK2Jh2d3bOJtPnQ2eDRaRn91gq2ZrE/zXsNi3WOSZVdnb3bDckyAzIC6jymRTVANV/lakmiF//9pc3Fkcv2Mt1jyjOCWapAZh3955HfnhClPL1JlU1pizWfPbMp2fXuUfJWCNoWEj+tm3W8gcgZ+G4OrmcGeplH9e7F4cnu4gId7SGro" + - "XmGAjHVRdWBRa5Jibox2epGXmupPEH9wYpx7T5WlnOlWelhZhuSWvE80UiRTSlPNU9teBmQsZZFnf2w+bE5ySHKvc+11VH5BgiyF6Yype8SRxnFpmBKY72M9Zml1anbkeNCFQ4buUypTUVQmWYNeh198YLJiSWJ5YqtlkGvUbMx1snaueJF52H3Lf3eApYirirmMu5B/" + - "l16Y22oLfDhQmVw+X65nh2vYdDV3CX+O////////nztnynoXUzl1i5rtX2aBnYPxgJhfPF/FdWJ7RpA8aGdZ61qbfRB2fossT/VfamoZbDdvAnTieWiIaIpVjHle32PPdcV50oLXkyiS8oSchu2cLVTBX2xljG1ccBWMp4zTmDtlT3T2Tg1O2FfgWStaZlvMUaheA16c" + - "YBZidmV3//9lp2ZubW5yNnsmgVCBmoKZi1yMoIzmjXSWHJZET65kq2tmgh6EYYVqkOhcAWlTmKiEeoVXTw9Sb1+pXkVnDXmPgXmJB4mGbfVfF2JVbLhOz3Jpm5JSBlQ7VnRYs2GkYm5xGllufIl83n0blvBlh4BeThlPdVF1WEBeY15zXwpnxE4mhT2ViZZbfHOYAVD7" + - "WMF2VninUiV3pYURe4ZQT1kJckd7x33oj7qP1JBNT79SyVopXwGXrU/dgheS6lcDY1VraXUriNyPFHpCUt9Yk2FVYgpmrmvNfD+D6VAjT/hTBVRGWDFZSVudXPBc710pXpZisWNnZT5luWcL////////bNVs4XD5eDJ+K4DegrOEDITshwKJEooqjEqQppLSmP2c851s" + - "Tk9OoVCNUlZXSlmoXj1f2F/ZYj9mtGcbZ9Bo0lGSfSGAqoGoiwCMjIy/kn6WMlQgmCxTF1DVU1xYqGSyZzRyZ3dmekaR5lLDbKFrhlgAXkxZVGcsf/tR4XbG//9kaXjom1Seu1fLWblmJ2eaa85U6WnZXlWBnGeVm6pn/pxSaF1Opk/jU8hiuWcrbKuPxE+tfm2ev04H" + - "YWJugG8rhRNUc2cqm0Vd83uVXKxbxoccbkqE0XoUgQhZmXyNbBF3IFLZWSJxIXJfd9uXJ51haQtaf1oYUaVUDVR9Zg5234/3kpic9Fnqcl1uxVFNaMl9v33sl2KeumR4aiGDAlmEW19r23MbdvJ9soAXhJlRMmcontl27mdiUv+ZBVwkYjt8foywVU9gtn0LlYBTAU5f" + - "UbZZHHI6gDaRzl8ld+JThF95fQSFrIozjo2XVmfzha6UU2EJYQhsuXZS////////iu2POFUvT1FRKlLHU8tbpV59YKBhgmPWZwln2m5nbYxzNnM3dTF5UIjVipiQSpCRkPWWxIeNWRVOiE9ZTg6KiY8/mBBQrV58WZZbuV64Y9pj+mTBZtxpSmnYbQtutnGUdSh6r3+K" + - "gACESYTJiYGLIY4KkGWWfZkKYX5ikWsy//9sg210f8x//G3Af4WHuoj4Z2WDsZg8lvdtG31hhD2Rak5xU3VdUGsEb+uFzYYtiadSKVQPXGVnTmiodAZ0g3XiiM+I4ZHMluKWeF+Lc4d6y4ROY6B1ZVKJbUFunHQJdVl4a3ySloZ63J+NT7ZhbmXFhlxOhk6uUNpOIVHM" + - "W+5lmWiBbbxzH3ZCd616HHzngm+K0pB8kc+WdZgYUpt90VArU5hnl23LcdB0M4HojyqWo5xXnp90YFhBbZl9L5heTuRPNk+LUbdSsV26YBxzsnk8gtOSNJa3lvaXCp6Xn2Jmpmt0UhdSo3DIiMJeyWBLYZBvI3FJfD599IBv////////hO6QI5MsVEKbb2rTcImMwo3v" + - "lzJStFpBXspfBGcXaXxplG1qbw9yYnL8e+2AAYB+h0uQzlFtnpN5hICLkzKK1lAtVIyKcWtqjMSBB2DRZ6Cd8k6ZTpicEIprhcGFaGkAbn54l4FV////////////////////////////////////////////////////////////////////////////////////////" + - "/////////////////////////////18MThBOFU4qTjFONk48Tj9OQk5WTlhOgk6FjGtOioISXw1Ojk6eTp9OoE6iTrBOs062Ts5OzU7ETsZOwk7XTt5O7U7fTvdPCU9aTzBPW09dT1dPR092T4hPj0+YT3tPaU9wT5FPb0+GT5ZRGE/UT99Pzk/YT9tP0U/aT9BP5E/l" + - "UBpQKFAUUCpQJVAFTxxP9lAhUClQLE/+T+9QEVAGUENQR2cDUFVQUFBIUFpQVlBsUHhQgFCaUIVQtFCy////////UMlQylCzUMJQ1lDeUOVQ7VDjUO5Q+VD1UQlRAVECURZRFVEUURpRIVE6UTdRPFE7UT9RQFFSUUxRVFFievhRaVFqUW5RgFGCVthRjFGJUY9RkVGT" + - "UZVRllGkUaZRolGpUapRq1GzUbFRslGwUbVRvVHFUclR21HghlVR6VHt//9R8FH1Uf5SBFILUhRSDlInUipSLlIzUjlST1JEUktSTFJeUlRSalJ0UmlSc1J/Un1SjVKUUpJScVKIUpGPqI+nUqxSrVK8UrVSwVLNUtdS3lLjUuaY7VLgUvNS9VL4UvlTBlMIdThTDVMQ" + - "Uw9TFVMaUyNTL1MxUzNTOFNAU0ZTRU4XU0lTTVHWU15TaVNuWRhTe1N3U4JTllOgU6ZTpVOuU7BTtlPDfBKW2VPfZvxx7lPuU+hT7VP6VAFUPVRAVCxULVQ8VC5UNlQpVB1UTlSPVHVUjlRfVHFUd1RwVJJUe1SAVHZUhFSQVIZUx1SiVLhUpVSsVMRUyFSo////////" + - "VKtUwlSkVL5UvFTYVOVU5lUPVRRU/VTuVO1U+lTiVTlVQFVjVUxVLlVcVUVVVlVXVThVM1VdVZlVgFSvVYpVn1V7VX5VmFWeVa5VfFWDValVh1WoVdpVxVXfVcRV3FXkVdRWFFX3VhZV/lX9VhtV+VZOVlBx31Y0VjZWMlY4//9Wa1ZkVi9WbFZqVoZWgFaKVqBWlFaP" + - "VqVWrla2VrRWwla8VsFWw1bAVshWzlbRVtNW11buVvlXAFb/VwRXCVcIVwtXDVcTVxhXFlXHVxxXJlc3VzhXTlc7V0BXT1dpV8BXiFdhV39XiVeTV6BXs1ekV6pXsFfDV8ZX1FfSV9NYClfWV+NYC1gZWB1YclghWGJYS1hwa8BYUlg9WHlYhVi5WJ9Yq1i6WN5Yu1i4" + - "WK5YxVjTWNFY11jZWNhY5VjcWORY31jvWPpY+Vj7WPxY/VkCWQpZEFkbaKZZJVksWS1ZMlk4WT560llVWVBZTllaWVhZYllgWWdZbFlp////////WXhZgVmdT15Pq1mjWbJZxlnoWdxZjVnZWdpaJVofWhFaHFoJWhpaQFpsWklaNVo2WmJaalqaWrxavlrLWsJavVrj" + - "Wtda5lrpWtZa+lr7WwxbC1sWWzJa0FsqWzZbPltDW0VbQFtRW1VbWltbW2VbaVtwW3NbdVt4ZYhbeluA//9bg1umW7hbw1vHW8lb1FvQW+Rb5lviW95b5VvrW/Bb9lvzXAVcB1wIXA1cE1wgXCJcKFw4XDlcQVxGXE5cU1xQXE9bcVxsXG5OYlx2XHlcjFyRXJRZm1yr" + - "XLtctly8XLdcxVy+XMdc2VzpXP1c+lztXYxc6l0LXRVdF11cXR9dG10RXRRdIl0aXRldGF1MXVJdTl1LXWxdc112XYddhF2CXaJdnV2sXa5dvV2QXbddvF3JXc1d013SXdZd213rXfJd9V4LXhpeGV4RXhteNl43XkReQ15AXk5eV15UXl9eYl5kXkdedV52XnqevF5/" + - "XqBewV7CXshe0F7P////////XtZe417dXtpe217iXuFe6F7pXuxe8V7zXvBe9F74Xv5fA18JX11fXF8LXxFfFl8pXy1fOF9BX0hfTF9OXy9fUV9WX1dfWV9hX21fc193X4Nfgl9/X4pfiF+RX4dfnl+ZX5hfoF+oX61fvF/WX/tf5F/4X/Ff3WCzX/9gIWBg//9gGWAQ" + - "YClgDmAxYBtgFWArYCZgD2A6YFpgQWBqYHdgX2BKYEZgTWBjYENgZGBCYGxga2BZYIFgjWDnYINgmmCEYJtglmCXYJJgp2CLYOFguGDgYNNgtF/wYL1gxmC1YNhhTWEVYQZg9mD3YQBg9GD6YQNhIWD7YPFhDWEOYUdhPmEoYSdhSmE/YTxhLGE0YT1hQmFEYXNhd2FY" + - "YVlhWmFrYXRhb2FlYXFhX2FdYVNhdWGZYZZhh2GsYZRhmmGKYZFhq2GuYcxhymHJYfdhyGHDYcZhumHLf3lhzWHmYeNh9mH6YfRh/2H9Yfxh/mIAYghiCWINYgxiFGIb////////Yh5iIWIqYi5iMGIyYjNiQWJOYl5iY2JbYmBiaGJ8YoJiiWJ+YpJik2KWYtRig2KU" + - "Ytdi0WK7Ys9i/2LGZNRiyGLcYsxiymLCYsdim2LJYwxi7mLxYydjAmMIYu9i9WNQYz5jTWQcY09jlmOOY4Bjq2N2Y6Njj2OJY59jtWNr//9jaWO+Y+ljwGPGY+NjyWPSY/ZjxGQWZDRkBmQTZCZkNmUdZBdkKGQPZGdkb2R2ZE5lKmSVZJNkpWSpZIhkvGTaZNJkxWTH" + - "ZLtk2GTCZPFk54IJZOBk4WKsZONk72UsZPZk9GTyZPplAGT9ZRhlHGUFZSRlI2UrZTRlNWU3ZTZlOHVLZUhlVmVVZU1lWGVeZV1lcmV4ZYJlg4uKZZtln2WrZbdlw2XGZcFlxGXMZdJl22XZZeBl4WXxZ3JmCmYDZftnc2Y1ZjZmNGYcZk9mRGZJZkFmXmZdZmRmZ2Zo" + - "Zl9mYmZwZoNmiGaOZolmhGaYZp1mwWa5Zslmvma8////////ZsRmuGbWZtpm4GY/ZuZm6WbwZvVm92cPZxZnHmcmZyeXOGcuZz9nNmdBZzhnN2dGZ15nYGdZZ2NnZGeJZ3BnqWd8Z2pnjGeLZ6ZnoWeFZ7dn72e0Z+xns2fpZ7hn5GfeZ91n4mfuZ7lnzmfGZ+dqnGge" + - "aEZoKWhAaE1oMmhO//9os2graFloY2h3aH9on2iPaK1olGidaJtog2quaLlodGi1aKBoumkPaI1ofmkBaMppCGjYaSJpJmjhaQxozWjUaOdo1Wk2aRJpBGjXaONpJWj5aOBo72koaSppGmkjaSFoxml5aXdpXGl4aWtpVGl+aW5pOWl0aT1pWWkwaWFpXmldaYFpammy" + - "aa5p0Gm/acFp02m+ac5b6GnKad1pu2nDaadqLmmRaaBpnGmVabRp3mnoagJqG2n/awpp+WnyaedqBWmxah5p7WoUaetqCmoSasFqI2oTakRqDGpyajZqeGpHamJqWWpmakhqOGoiapBqjWqgaoRqomqj////////apeGF2q7asNqwmq4arNqrGreatFq32qqatpq6mr7" + - "awWGFmr6axJrFpsxax9rOGs3dtxrOZjua0drQ2tJa1BrWWtUa1trX2tha3hreWt/a4BrhGuDa41rmGuVa55rpGuqa6trr2uya7Frs2u3a7xrxmvLa9Nr32vsa+tr82vv//+evmwIbBNsFGwbbCRsI2xebFVsYmxqbIJsjWyabIFsm2x+bGhsc2ySbJBsxGzxbNNsvWzX" + - "bMVs3WyubLFsvmy6bNts72zZbOptH4hNbTZtK209bThtGW01bTNtEm0MbWNtk21kbVpteW1ZbY5tlW/kbYVt+W4VbgpttW3HbeZtuG3Gbext3m3Mbeht0m3Fbfpt2W3kbdVt6m3ubi1ubm4ubhlucm5fbj5uI25rbitudm5Nbh9uQ246bk5uJG7/bh1uOG6CbqpumG7J" + - "brdu0269bq9uxG6ybtRu1W6PbqVuwm6fb0FvEXBMbuxu+G7+bz9u8m8xbu9vMm7M////////bz5vE273b4Zvem94b4FvgG9vb1tv829tb4JvfG9Yb45vkW/Cb2Zvs2+jb6FvpG+5b8Zvqm/fb9Vv7G/Ub9hv8W/ub9twCXALb/pwEXABcA9v/nAbcBpvdHAdcBhwH3Aw" + - "cD5wMnBRcGNwmXCScK9w8XCscLhws3CucN9wy3Dd//9w2XEJcP1xHHEZcWVxVXGIcWZxYnFMcVZxbHGPcftxhHGVcahxrHHXcblxvnHScclx1HHOceBx7HHncfVx/HH5cf9yDXIQchtyKHItcixyMHIycjtyPHI/ckByRnJLclhydHJ+coJygXKHcpJylnKicqdyuXKy" + - "csNyxnLEcs5y0nLicuBy4XL5cvdQD3MXcwpzHHMWcx1zNHMvcylzJXM+c05zT57Yc1dzanNoc3BzeHN1c3tzenPIc7NzznO7c8Bz5XPuc950onQFdG90JXP4dDJ0OnRVdD90X3RZdEF0XHRpdHB0Y3RqdHZ0fnSLdJ50p3TKdM901HPx////////dOB043TndOl07nTy" + - "dPB08XT4dPd1BHUDdQV1DHUOdQ11FXUTdR51JnUsdTx1RHVNdUp1SXVbdUZ1WnVpdWR1Z3VrdW11eHV2dYZ1h3V0dYp1iXWCdZR1mnWddaV1o3XCdbN1w3W1db11uHW8dbF1zXXKddJ12XXjdd51/nX///91/HYBdfB1+nXydfN2C3YNdgl2H3YndiB2IXYidiR2NHYw" + - "djt2R3ZIdkZ2XHZYdmF2YnZodml2anZndmx2cHZydnZ2eHZ8doB2g3aIdot2jnaWdpN2mXaadrB2tHa4drl2unbCds121nbSdt524Xbldud26oYvdvt3CHcHdwR3KXckdx53JXcmdxt3N3c4d0d3Wndod2t3W3dld393fnd5d453i3eRd6B3nnewd7Z3uXe/d7x3vXe7" + - "d8d3zXfXd9p33Hfjd+53/HgMeBJ5JnggeSp4RXiOeHR4hnh8eJp4jHijeLV4qniveNF4xnjLeNR4vni8eMV4ynjs////////eOd42nj9ePR5B3kSeRF5GXkseSt5QHlgeVd5X3laeVV5U3l6eX95inmdeaefS3mqea55s3m5ebp5yXnVeed57HnheeN6CHoNehh6GXog" + - "eh95gHoxejt6Pno3ekN6V3pJemF6Ynppn516cHp5en16iHqXepV6mHqWeql6yHqw//96tnrFesR6v5CDesd6ynrNes961XrTetl62nrdeuF64nrmeu168HsCew97CnsGezN7GHsZex57NXsoezZ7UHt6ewR7TXsLe0x7RXt1e2V7dHtne3B7cXtse257nXuYe597jXuc" + - "e5p7i3uSe497XXuZe8t7wXvMe897tHvGe9176XwRfBR75nvlfGB8AHwHfBN783v3fBd8DXv2fCN8J3wqfB98N3wrfD18THxDfFR8T3xAfFB8WHxffGR8VnxlfGx8dXyDfJB8pHytfKJ8q3yhfKh8s3yyfLF8rny5fL18wHzFfMJ82HzSfNx84ps7fO988nz0fPZ8+n0G" + - "////////fQJ9HH0VfQp9RX1LfS59Mn0/fTV9Rn1zfVZ9Tn1yfWh9bn1PfWN9k32JfVt9j319fZt9un2ufaN9tX3Hfb19q349faJ9r33cfbh9n32wfdh93X3kfd59+33yfeF+BX4KfiN+IX4SfjF+H34Jfgt+In5GfmZ+O341fjl+Q343//9+Mn46fmd+XX5Wfl5+WX5a" + - "fnl+an5pfnx+e36DfdV+fY+ufn9+iH6Jfox+kn6QfpN+lH6Wfo5+m36cfzh/On9Ff0x/TX9Of1B/UX9Vf1R/WH9ff2B/aH9pf2d/eH+Cf4Z/g3+If4d/jH+Uf55/nX+af6N/r3+yf7l/rn+2f7iLcX/Ff8Z/yn/Vf9R/4X/mf+l/83/5mNyABoAEgAuAEoAYgBmAHIAh" + - "gCiAP4A7gEqARoBSgFiAWoBfgGKAaIBzgHKAcIB2gHmAfYB/gISAhoCFgJuAk4CagK1RkICsgNuA5YDZgN2AxIDagNaBCYDvgPGBG4EpgSOBL4FL////////louBRoE+gVOBUYD8gXGBboFlgWaBdIGDgYiBioGAgYKBoIGVgaSBo4FfgZOBqYGwgbWBvoG4gb2BwIHC" + - "gbqByYHNgdGB2YHYgciB2oHfgeCB54H6gfuB/oIBggKCBYIHggqCDYIQghaCKYIrgjiCM4JAglmCWIJdglqCX4Jk//+CYoJogmqCa4IugnGCd4J4gn6CjYKSgquCn4K7gqyC4YLjgt+C0oL0gvOC+oOTgwOC+4L5gt6DBoLcgwmC2YM1gzSDFoMygzGDQIM5g1CDRYMv" + - "gyuDF4MYg4WDmoOqg5+DooOWgyODjoOHg4qDfIO1g3ODdYOgg4mDqIP0hBOD64POg/2EA4PYhAuDwYP3hAeD4IPyhA2EIoQgg72EOIUGg/uEbYQqhDyFWoSEhHeEa4SthG6EgoRphEaELIRvhHmENYTKhGKEuYS/hJ+E2YTNhLuE2oTQhMGExoTWhKGFIYT/hPSFF4UY" + - "hSyFH4UVhRSE/IVAhWOFWIVI////////hUGGAoVLhVWFgIWkhYiFkYWKhaiFbYWUhZuF6oWHhZyFd4V+hZCFyYW6hc+FuYXQhdWF3YXlhdyF+YYKhhOGC4X+hfqGBoYihhqGMIY/hk1OVYZUhl+GZ4ZxhpOGo4aphqqGi4aMhraGr4bEhsaGsIbJiCOGq4bUht6G6Ybs" + - "//+G34bbhu+HEocGhwiHAIcDhvuHEYcJhw2G+YcKhzSHP4c3hzuHJYcphxqHYIdfh3iHTIdOh3SHV4doh26HWYdTh2OHaogFh6KHn4eCh6+Hy4e9h8CH0JbWh6uHxIezh8eHxoe7h++H8ofgiA+IDYf+h/aH94gOh9KIEYgWiBWIIoghiDGINog5iCeIO4hEiEKIUohZ" + - "iF6IYohriIGIfoieiHWIfYi1iHKIgoiXiJKIroiZiKKIjYikiLCIv4ixiMOIxIjUiNiI2YjdiPmJAoj8iPSI6IjyiQSJDIkKiROJQ4keiSWJKokriUGJRIk7iTaJOIlMiR2JYIle////////iWaJZIltiWqJb4l0iXeJfomDiYiJiomTiZiJoYmpiaaJrImvibKJuom9" + - "ib+JwInaidyJ3YnnifSJ+IoDihaKEIoMihuKHYolijaKQYpbilKKRopIinyKbYpsimKKhYqCioSKqIqhipGKpYqmipqKo4rEis2KworaiuuK84rn//+K5IrxixSK4IriiveK3orbiwyLB4saiuGLFosQixeLIIszl6uLJosriz6LKItBi0yLT4tOi0mLVotbi1qLa4tf" + - "i2yLb4t0i32LgIuMi46LkouTi5aLmYuajDqMQYw/jEiMTIxOjFCMVYxijGyMeIx6jIKMiYyFjIqMjYyOjJSMfIyYYh2MrYyqjL2MsoyzjK6MtozIjMGM5IzjjNqM/Yz6jPuNBI0FjQqNB40PjQ2NEJ9OjROMzY0UjRaNZ41tjXGNc42BjZmNwo2+jbqNz43ajdaNzI3b" + - "jcuN6o3rjd+N4438jgiOCY3/jh2OHo4Qjh+OQo41jjCONI5K////////jkeOSY5MjlCOSI5ZjmSOYI4qjmOOVY52jnKOfI6BjoeOhY6EjouOio6TjpGOlI6ZjqqOoY6sjrCOxo6xjr6OxY7IjsuO247jjvyO+47rjv6PCo8FjxWPEo8ZjxOPHI8fjxuPDI8mjzOPO485" + - "j0WPQo8+j0yPSY9Gj06PV49c//+PYo9jj2SPnI+fj6OPrY+vj7eP2o/lj+KP6o/vkIeP9JAFj/mP+pARkBWQIZANkB6QFpALkCeQNpA1kDmP+JBPkFCQUZBSkA6QSZA+kFaQWJBekGiQb5B2lqiQcpCCkH2QgZCAkIqQiZCPkKiQr5CxkLWQ4pDkYkiQ25ECkRKRGZEy" + - "kTCRSpFWkViRY5FlkWmRc5FykYuRiZGCkaKRq5GvkaqRtZG0kbqRwJHBkcmRy5HQkdaR35HhkduR/JH1kfaSHpH/khSSLJIVkhGSXpJXkkWSSZJkkkiSlZI/kkuSUJKckpaSk5KbklqSz5K5kreS6ZMPkvqTRJMu////////kxmTIpMakyOTOpM1kzuTXJNgk3yTbpNW" + - "k7CTrJOtk5STuZPWk9eT6JPlk9iTw5Pdk9CTyJPklBqUFJQTlAOUB5QQlDaUK5Q1lCGUOpRBlFKURJRblGCUYpRelGqSKZRwlHWUd5R9lFqUfJR+lIGUf5WClYeVipWUlZaVmJWZ//+VoJWolaeVrZW8lbuVuZW+lcpv9pXDlc2VzJXVldSV1pXcleGV5ZXiliGWKJYu" + - "li+WQpZMlk+WS5Z3llyWXpZdll+WZpZylmyWjZaYlpWWl5aqlqeWsZaylrCWtJa2lriWuZbOlsuWyZbNiU2W3JcNltWW+ZcElwaXCJcTlw6XEZcPlxaXGZcklyqXMJc5lz2XPpdEl0aXSJdCl0mXXJdgl2SXZpdoUtKXa5dxl3mXhZd8l4GXepeGl4uXj5eQl5yXqJem" + - "l6OXs5e0l8OXxpfIl8uX3Jftn0+X8nrfl/aX9ZgPmAyYOJgkmCGYN5g9mEaYT5hLmGuYb5hw////////mHGYdJhzmKqYr5ixmLaYxJjDmMaY6ZjrmQOZCZkSmRSZGJkhmR2ZHpkkmSCZLJkumT2ZPplCmUmZRZlQmUuZUZlSmUyZVZmXmZiZpZmtma6ZvJnfmduZ3ZnY" + - "mdGZ7ZnumfGZ8pn7mfiaAZoPmgWZ4poZmiuaN5pFmkKaQJpD//+aPppVmk2aW5pXml+aYpplmmSaaZprmmqarZqwmryawJrPmtGa05rUmt6a35rimuOa5prvmuua7pr0mvGa95r7mwabGJsamx+bIpsjmyWbJ5somymbKpsumy+bMptEm0ObT5tNm06bUZtYm3Sbk5uD" + - "m5GblpuXm5+boJuom7SbwJvKm7mbxpvPm9Gb0pvjm+Kb5JvUm+GcOpvym/Gb8JwVnBScCZwTnAycBpwInBKcCpwEnC6cG5wlnCScIZwwnEecMpxGnD6cWpxgnGecdpx4nOec7JzwnQmdCJzrnQOdBp0qnSadr50jnR+dRJ0VnRKdQZ0/nT6dRp1I////////nV2dXp1k" + - "nVGdUJ1ZnXKdiZ2Hnaudb516nZqdpJ2pnbKdxJ3BnbuduJ26ncadz53Cndmd0534nead7Z3vnf2eGp4bnh6edZ55nn2egZ6InouejJ6SnpWekZ6dnqWeqZ64nqqerZdhnsyezp7PntCe1J7cnt6e3Z7gnuWe6J7v//+e9J72nvee+Z77nvye/Z8Hnwh2t58VnyGfLJ8+" + - "n0qfUp9Un2OfX59gn2GfZp9nn2yfap93n3Kfdp+Vn5yfoFgvaceQWXRkUdxxmf//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////" + - "/////////////////////////////////////////////w==" + // Check if the resulting Shift JIS value is in the valid QR Code Kanji ranges + data := []byte(s2) + hi := uint16(data[0]) + lo := uint16(data[1]) + code := hi<<8 | lo + + // QR Code Kanji mode supports Shift JIS ranges: + // 0x8140-0x9FFC and 0xE040-0xEBBF + return (code >= 0x8140 && code <= 0x9FFC) || (code >= 0xE040 && code <= 0xEBBF) +} diff --git a/chardet_test.go b/chardet_test.go index 9d71710..7774c07 100644 --- a/chardet_test.go +++ b/chardet_test.go @@ -218,7 +218,7 @@ func Test_analyzeJP(t *testing.T) { { name: "case 8", args: args{r: 'に'}, - want: true, + want: false, // Hiragana is NOT supported in Kanji mode }, { name: "case 9", @@ -322,6 +322,66 @@ func Test_analyzeMode(t *testing.T) { want: EncModeByte, wantErr: false, }, + { + name: "hiragana only - should use Byte mode", + args: args{raw: "これはひらがなです"}, + want: EncModeByte, + wantErr: false, + }, + { + name: "katakana only - should use Byte mode", + args: args{raw: "コンニチハ"}, + want: EncModeByte, + wantErr: false, + }, + { + name: "mixed kanji and hiragana - should use Byte mode", + args: args{raw: "漢字ひらがな"}, + want: EncModeByte, + wantErr: false, + }, + { + name: "single kanji character", + args: args{raw: "漢"}, + want: EncModeKanji, + wantErr: false, + }, + { + name: "kanji sentence", + args: args{raw: "日本語"}, + want: EncModeKanji, + wantErr: false, + }, + { + name: "kanji world characters", + args: args{raw: "世界"}, + want: EncModeKanji, + wantErr: false, + }, + { + name: "long kanji text", + args: args{raw: "東京京都大阪北海道沖縄鹿児島"}, + want: EncModeKanji, + wantErr: false, + }, + { + name: "mixed kanji and alphanumeric - should use Byte mode", + args: args{raw: "漢字ABC123"}, + want: EncModeByte, + wantErr: false, + }, + { + name: "CJK Extension A character - not in Shift JIS, should use Byte mode", + args: args{raw: "㐀"}, // U+3400, CJK Extension A - not in Shift JIS + want: EncModeByte, + wantErr: false, + }, + { + name: "CJK Compatibility Ideograph - not in Shift JIS, should use Byte mode", + args: args{raw: "豈"}, // U+F900, CJK Compatibility Ideographs - not in Shift JIS + want: EncModeByte, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/docs/kanji-encoding.md b/docs/kanji-encoding.md new file mode 100644 index 0000000..80757c4 --- /dev/null +++ b/docs/kanji-encoding.md @@ -0,0 +1,309 @@ +# Kanji Encoding Mode + +## Overview + +Kanji mode is a specialized encoding mode in QR Code designed to efficiently encode Japanese Kanji characters. It provides significant space savings compared to byte mode by leveraging the structure of Shift JIS (Japanese Industrial Standards) character encoding. + +### Benefits + +- **Compact Encoding**: Each Kanji character is encoded in 13 bits (vs. 16 bits in UTF-16 or 8 bytes per character in UTF-8) +- **Efficient Storage**: Reduces QR Code size for Japanese text by approximately 50% compared to byte mode +- **Optimized for Japanese**: Specifically designed for the Japanese writing system + +## Specifications + +| Parameter | Value | +|-----------|-------| +| Mode Indicator | `1000` (4 bits) | +| Bits per Character | 13 bits | +| Character Encoding | Shift JIS (JIS X 0208) | + +### Character Count Indicator Bits + +The number of bits used to store the character count varies by QR Code version: + +| QR Code Version | Character Count Bits | +|-----------------|---------------------| +| 1-9 | 8 bits | +| 10-26 | 10 bits | +| 27-40 | 12 bits | + +## Character Set + +Kanji mode supports characters from the JIS X 0208 character set, encoded using Shift JIS. The valid Kanji characters fall within two ranges: + +### Shift JIS Ranges + +| Range | Start | End | Description | +|-------|-------|-----|-------------| +| Range 1 | 0x8140 | 0x9FFC | First Kanji block | +| Range 2 | 0xE040 | 0xEBBF | Second Kanji block | + +### Unicode to Shift JIS Mapping + +Modern applications typically work with Unicode characters. To use Kanji mode, Unicode characters must first be converted to their Shift JIS byte representation. + +Example mappings: +- Unicode `U+4E16` (世) → Shift JIS `0x90 0xB6` +- Unicode `U+754C` (界) → Shift JIS `0x8A 0x79` + +## Encoding Algorithm + +### Step-by-Step Process + +1. **Input**: Unicode Kanji character(s) +2. **Convert**: Transform each Unicode character to its Shift JIS 2-byte representation +3. **Adjust**: Apply the adjustment formula based on the Shift JIS value +4. **Encode**: Compress to 13-bit representation + +### Mathematical Formula + +Given a Shift JIS code `code` (2 bytes): + +``` +// Step 1: Adjust the base +if (code >= 0x8140 && code <= 0x9FFC) { + adjusted = code - 0x8140 +} else if (code >= 0xE040 && code <= 0xEBBF) { + adjusted = code - 0xC140 +} + +// Step 2: Split into high and low bytes +high = adjusted >> 8 // Upper byte +low = adjusted & 0xFF // Lower byte + +// Step 3: Calculate encoded value (13-bit result) +encoded = (high × 0xC0) + low +``` + +### Why 0xC0? + +The multiplier `0xC0` (192 in decimal) is derived from the Shift JIS encoding structure: +- In the valid ranges, the lower byte can be `0x40-0xFC` (except `0x7F`) +- This gives 188 possible values per high byte +- The encoding packs these efficiently: `high × 192 + low` results in at most 13 bits + +### Encoding Example + +Let's encode the Kanji character "世" (Unicode `U+4E16`): + +1. **Convert to Shift JIS**: `0x90B6` +2. **Check range**: `0x90B6` is in range 1 (0x8140-0x9FFC) +3. **Adjust**: `0x90B6 - 0x8140 = 0x0F76` +4. **Split**: `high = 0x0F`, `low = 0x76` +5. **Encode**: `(0x0F × 0xC0) + 0x76 = 0x1176` +6. **Binary**: `1000101110110` (13 bits) + +## Character Detection + +To determine if a character is eligible for Kanji mode encoding: + +### Detection Algorithm + +1. **Check if character is Kanji**: The character must be in the Japanese Kanji Unicode ranges (primarily U+4E00-U+9FFF for CJK Unified Ideographs) +2. **Convert to Shift JIS**: Attempt conversion from Unicode to Shift JIS +3. **Validate range**: The resulting Shift JIS value must be in: + - `0x8140` to `0x9FFC`, OR + - `0xE040` to `0xEBBF` +4. **Check byte length**: Each character must encode to exactly 2 bytes in Shift JIS + +### Detection Criteria Summary + +``` +IsKanji(character) { + shiftJIS = UnicodeToShiftJIS(character) + + if (shiftJIS.length != 2) { + return false + } + + code = (shiftJIS[0] << 8) | shiftJIS[1] + + return (code >= 0x8140 && code <= 0x9FFC) || + (code >= 0xE040 && code <= 0xEBBF) +} +``` + +### Automatic Mode Selection + +When encoding mixed content, use Kanji mode only when: +- ALL characters in the data are valid Kanji characters +- Each character successfully converts to a valid Shift JIS value in the allowed ranges +- The content is primarily Japanese text + +If any character fails validation, fall back to a compatible mode (typically byte mode with UTF-8). + +## Practical Considerations + +### When to Use Kanji Mode + +- Japanese text containing primarily Kanji characters +- When minimizing QR Code size is critical +- When target scanners support Kanji mode decoding + +### Limitations + +- Only supports JIS X 0208 characters +- Hiragana and Katakana are NOT supported (use byte mode) +- Some rare Kanji characters outside the ranges cannot be encoded +- Requires proper Shift JIS conversion capability + +### Compatibility + +Most modern QR Code scanners support Kanji mode, but for maximum compatibility with older scanners, consider using byte mode with UTF-8 encoding, especially for international applications. + +--- + +# Kanji 编码模式 + +## 概述 + +Kanji 模式是 QR 码中专门设计的一种编码模式,用于高效编码日文汉字字符。通过利用 Shift JIS(日本工业标准)字符编码的结构,它相比字节模式能显著节省空间。 + +### 优势 + +- **紧凑编码**: 每个汉字字符编码为 13 位(相比 UTF-16 的 16 位或 UTF-8 的每字符 8 字节) +- **高效存储**: 相比字节模式,可将日文文本的 QR 码大小减少约 50% +- **专为日文优化**: 专门针对日文字符系统设计 + +## 规范说明 + +| 参数 | 值 | +|------|-----| +| 模式指示器 | `1000` (4 位) | +| 每字符位数 | 13 位 | +| 字符编码 | Shift JIS (JIS X 0208) | + +### 字符计数指示器位数 + +用于存储字符计数的位数随 QR 码版本变化: + +| QR 码版本 | 字符计数位数 | +|-----------|-------------| +| 1-9 | 8 位 | +| 10-26 | 10 位 | +| 27-40 | 12 位 | + +## 字符集 + +Kanji 模式支持 JIS X 0208 字符集中的字符,使用 Shift JIS 编码。有效的汉字字符落在两个范围内: + +### Shift JIS 范围 + +| 范围 | 起始 | 结束 | 描述 | +|------|------|------|------| +| 范围 1 | 0x8140 | 0x9FFC | 第一汉字块 | +| 范围 2 | 0xE040 | 0xEBBF | 第二汉字块 | + +### Unicode 到 Shift JIS 映射 + +现代应用通常使用 Unicode 字符。要使用 Kanji 模式,Unicode 字符必须首先转换为其 Shift JIS 双字节表示。 + +映射示例: +- Unicode `U+4E16` (世) → Shift JIS `0x90 0xB6` +- Unicode `U+754C` (界) → Shift JIS `0x8A 0x79` + +## 编码算法 + +### 逐步流程 + +1. **输入**: Unicode 汉字字符 +2. **转换**: 将每个 Unicode 字符转换为其 Shift JIS 双字节表示 +3. **调整**: 根据 Shift JIS 值应用调整公式 +4. **编码**: 压缩为 13 位表示 + +### 数学公式 + +给定 Shift JIS 代码 `code`(2 字节): + +``` +// 步骤 1: 调整基数 +if (code >= 0x8140 && code <= 0x9FFC) { + adjusted = code - 0x8140 +} else if (code >= 0xE040 && code <= 0xEBBF) { + adjusted = code - 0xC140 +} + +// 步骤 2: 拆分为高位和低位字节 +high = adjusted >> 8 // 高位字节 +low = adjusted & 0xFF // 低位字节 + +// 步骤 3: 计算编码值(13 位结果) +encoded = (high × 0xC0) + low +``` + +### 为什么是 0xC0? + +乘数 `0xC0`(十进制 192)源自 Shift JIS 编码结构: +- 在有效范围内,低位字节可以是 `0x40-0xFC`(除 `0x7F` 外) +- 这为每个高位字节提供 188 个可能值 +- 编码将其高效打包:`high × 192 + low` 结果最多为 13 位 + +### 编码示例 + +让我们编码汉字 "世"(Unicode `U+4E16`): + +1. **转换为 Shift JIS**: `0x90B6` +2. **检查范围**: `0x90B6` 在范围 1 内 (0x8140-0x9FFC) +3. **调整**: `0x90B6 - 0x8140 = 0x0F76` +4. **拆分**: `high = 0x0F`, `low = 0x76` +5. **编码**: `(0x0F × 0xC0) + 0x76 = 0x1176` +6. **二进制**: `1000101110110` (13 位) + +## 字符检测 + +判断字符是否符合 Kanji 模式编码条件: + +### 检测算法 + +1. **检查是否为汉字**: 字符必须在日文汉字 Unicode 范围内(主要是 U+4E00-U+9FFF 的 CJK 统一表意文字) +2. **转换为 Shift JIS**: 尝试从 Unicode 转换为 Shift JIS +3. **验证范围**: 结果 Shift JIS 值必须在: + - `0x8140` 到 `0x9FFC`,或 + - `0xE040` 到 `0xEBBF` +4. **检查字节长度**: 每个字符在 Shift JIS 中必须编码为恰好 2 字节 + +### 检测标准总结 + +``` +IsKanji(character) { + shiftJIS = UnicodeToShiftJIS(character) + + if (shiftJIS.length != 2) { + return false + } + + code = (shiftJIS[0] << 8) | shiftJIS[1] + + return (code >= 0x8140 && code <= 0x9FFC) || + (code >= 0xE040 && code <= 0xEBBF) +} +``` + +### 自动模式选择 + +编码混合内容时,仅当满足以下条件时使用 Kanji 模式: +- 数据中的所有字符都是有效的汉字字符 +- 每个字符都能成功转换为允许范围内的有效 Shift JIS 值 +- 内容主要是日文文本 + +如果任何字符验证失败,则回退到兼容模式(通常为 UTF-8 字节模式)。 + +## 实际考虑 + +### 何时使用 Kanji 模式 + +- 主要包含汉字字符的日文文本 +- 最小化 QR 码大小至关重要时 +- 目标扫描器支持 Kanji 模式解码 + +### 限制 + +- 仅支持 JIS X 0208 字符 +- 不支持平假名和片假名(使用字节模式) +- 范围外的某些罕见汉字无法编码 +- 需要正确的 Shift JIS 转换能力 + +### 兼容性 + +大多数现代 QR 码扫描器支持 Kanji 模式,但为了与旧扫描器实现最大兼容性,对于国际应用可考虑使用带 UTF-8 编码的字节模式。 diff --git a/encoder.go b/encoder.go index 5effc53..9292b7c 100644 --- a/encoder.go +++ b/encoder.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "strconv" + "unicode/utf8" "github.com/yeqown/reedsolomon/binary" "golang.org/x/text/encoding/japanese" @@ -126,12 +127,18 @@ func newEncoder(m encMode, ec ecLevel, v version) *encoder { func (e *encoder) Encode(raw string) (*binary.Binary, error) { e.dst = binary.New() - var data []byte + var ( + data []byte + charCount = 0 // Character count for the character count indicator + ) switch e.mode { case EncModeNumeric, EncModeAlphanumeric, EncModeByte: data = []byte(raw) + charCount = len(data) case EncModeKanji: data = toShiftJIS(raw) + // For Kanji mode, charCount is the number of Kanji characters, not bytes + charCount = utf8.RuneCountInString(raw) default: log.Printf("unsupported encoding mode: %s", getEncModeName(e.mode)) } @@ -140,7 +147,7 @@ func (e *encoder) Encode(raw string) (*binary.Binary, error) { indicator := getEncodeModeIndicator(e.mode) e.dst.Append(indicator) // append chars length counter bits symbol - e.dst.AppendUint32(uint32(len(data)), e.charCountBits()) + e.dst.AppendUint32(uint32(charCount), e.charCountBits()) // encode data with specified mode switch e.mode { @@ -220,10 +227,10 @@ func (e *encoder) encodeByte(data []byte) { } } -// toShiftJIS -// https://www.thonky.com/qr-code-tutorial/kanji-mode-encoding +// toShiftJIS converts Unicode string to Shift JIS and applies Kanji encoding. +// Each character is encoded as 13 bits using the QR Code Kanji mode algorithm. +// Reference: https://www.thonky.com/qr-code-tutorial/kanji-mode-encoding func toShiftJIS(raw string) []byte { - // FIXME: some character encoded into Shift JIS but not in the range of 0x8140-0x9FFC and 0xE040-0xEBBF. enc := japanese.ShiftJIS.NewEncoder() s2, _, err := transform.String(enc, raw) if err != nil { @@ -233,12 +240,19 @@ func toShiftJIS(raw string) []byte { data := []byte(s2) if len(data)%2 != 0 { - // BUG: encode bytes with Shift JIS must be times of 2, cause panic here - log.Panicf("shift JIS encoded []byte must be times of 2, but got %d", len(data)) + // Kanji characters must encode to exactly 2 bytes in Shift JIS + log.Printf("shift JIS encoded data must be a multiple of 2, but got %d", len(data)) + return []byte{} } for i := 0; i < len(data); i += 2 { - data[i], data[i+1] = encodeShiftJIS(data[i], data[i+1]) + hi, lo := encodeShiftJIS(data[i], data[i+1]) + if hi == 0 && lo == 0 { + // Invalid character encountered + log.Printf("invalid Kanji character at position %d", i/2) + return []byte{} + } + data[i], data[i+1] = hi, lo } return data @@ -247,41 +261,45 @@ func toShiftJIS(raw string) []byte { func encodeShiftJIS(hi byte, lo byte) (byte, byte) { r := uint16(hi)<<8 | uint16(lo) - // fmt.Printf("before: r=%x\n", r) - if r > 0x8140 && r < 0x9FFC { + // QR Code Kanji mode supports Shift JIS ranges: + // 0x8140-0x9FFC and 0xE040-0xEBBF + if r >= 0x8140 && r <= 0x9FFC { r -= 0x8140 - } else if r > 0xE040 && r < 0xEBBF { + } else if r >= 0xE040 && r <= 0xEBBF { r -= 0xC140 } else { - // Not a Shift JIS character out of range 0x8140-0x9FFC and 0xE040-0xEBBF - log.Printf("'%c'(0x%x) not a Shift JIS character out of range 0x8140-0x9FFC and 0xE040-0xEBBF", r, r) + // Not a valid QR Code Kanji character + log.Printf("'%c'(0x%x) not a valid QR Code Kanji character (must be in 0x8140-0x9FFC or 0xE040-0xEBBF)", r, r) return 0, 0 } - fmt.Printf("middle: r=%x\n", r) hi = uint8(r >> 8) lo = uint8(r & 0xFF) - // fmt.Printf("middle: high=%x, low=%x\n", hi, lo) - + // Compress to 13-bit value: (high × 0xC0) + low r = uint16(hi)*uint16(0xC0) + uint16(lo) - // fmt.Printf("after: r=%x\n", r) return byte(r >> 8), byte(r & 0xFF) } -// encodeKanji +// encodeKanji encodes Kanji data (already processed by encodeShiftJIS). +// Each Kanji character is encoded as 13 bits: the data contains pairs of bytes +// where data[i] contains the high 5 bits and data[i+1] contains the low 8 bits. func (e *encoder) encodeKanji(data []byte) { - // data must be times of 2, since toShiftJIS encode 1 char to 2 bytes + // data must be a multiple of 2, since toShiftJIS encodes 1 char to 2 bytes if len(data)%2 != 0 { - log.Println("data must be times of 2") + log.Println("data must be a multiple of 2") + return } for i := 0; i < len(data); i += 2 { - // 2 bytes to 1 kanji - // 2 bytes to 13 bits - _ = e.dst.AppendByte(data[i]<<3, 5) - _ = e.dst.AppendByte(data[i+1], 8) + // Reconstruct the 13-bit value: (high 5 bits << 8) | low 8 bits + // data[i] contains the high 5 bits of the 13-bit result + // data[i+1] contains the low 8 bits of the 13-bit result + value := uint32(data[i])<<8 | uint32(data[i+1]) + + // Append the 13-bit value to the bitstream + e.dst.AppendUint32(value, 13) } } @@ -327,15 +345,15 @@ var charCountMap = map[string]int{ "9_numeric": 10, "9_alphanumeric": 9, "9_byte": 8, - "9_japan": 8, + "9_kanji": 8, "26_numeric": 12, "26_alphanumeric": 11, "26_byte": 16, - "26_japan": 10, + "26_kanji": 10, "40_numeric": 14, "40_alphanumeric": 13, "40_byte": 16, - "40_japan": 12, + "40_kanji": 12, } // charCountBits diff --git a/encoder_test.go b/encoder_test.go index da5d267..0def202 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -60,17 +60,241 @@ func Test_toShiftJIS(t *testing.T) { want []byte }{ { - name: "test 1", + name: "test 茗荷", args: args{"茗荷"}, want: []byte{0x1A, 0xAA, 0x06, 0x97}, }, + { + name: "test 世", + args: args{"世"}, + // Shift JIS: 0x90A2 + // 0x90A2 - 0x8140 = 0x0F62, hi=0x0F, lo=0x62 + // encoded = 0x0F*0xC0 + 0x62 = 0xBA2 + // high byte = 0x0B, low byte = 0xA2 + want: []byte{0x0B, 0xA2}, + }, + { + name: "test 世界", + args: args{"世界"}, + // "世": [0x0B, 0xA2], "界": [0x06, 0xC5] + want: []byte{0x0B, 0xA2, 0x06, 0xC5}, + }, + { + name: "test 日本語", + args: args{"日本語"}, + // "日": [0x0E, 0x3A], "本": [0x0F, 0xFB], "語": [0x08, 0xEA] + want: []byte{0x0E, 0x3A, 0x0F, 0xFB, 0x08, 0xEA}, + }, + { + name: "test 漢字", + args: args{"漢字"}, + // "漢": [0x07, 0x3F], "字": [0x0A, 0x1A] + want: []byte{0x07, 0x3F, 0x0A, 0x1A}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := toShiftJIS(tt.args.s); !bytes.Equal(got, tt.want) { - t.Errorf("toShiftJIS() = %v, want %v", got, tt.want) + t.Errorf("toShiftJIS(%q) = %v, want %v", tt.args.s, got, tt.want) + } + }) + } +} + +func Test_encodeShiftJIS(t *testing.T) { + type args struct { + hi byte + lo byte + } + tests := []struct { + name string + args args + wantHi byte + wantLo byte + }{ + // Range 1: 0x8140-0x9FFC + { + name: "lower boundary of range 1", + args: args{0x81, 0x40}, + wantHi: 0x00, + wantLo: 0x00, + }, + { + name: "middle of range 1 (世)", + args: args{0x90, 0xA2}, // "世" in Shift JIS + // 0x90A2 - 0x8140 = 0x0F62 + // high=0x0F, low=0x62 + // encoded = 0x0F*0xC0 + 0x62 = 0xBA2 + wantHi: 0x0B, + wantLo: 0xA2, + }, + { + name: "upper boundary of range 1", + args: args{0x9F, 0xFC}, + // 0x9FFC - 0x8140 = 0x1EBC + // high=0x1E, low=0xBC + // encoded = 0x1E*0xC0 + 0xBC = 0x173C + wantHi: 0x17, + wantLo: 0x3C, + }, + // Range 2: 0xE040-0xEBBF + { + name: "lower boundary of range 2", + args: args{0xE0, 0x40}, + // 0xE040 - 0xC140 = 0x1F00 + // high=0x1F, low=0x00 + // encoded = 0x1F*0xC0 + 0x00 = 0x1740 + wantHi: 0x17, + wantLo: 0x40, + }, + { + name: "middle of range 2", + args: args{0xE4, 0xAA}, + // 0xE4AA - 0xC140 = 0x236A + // high=0x23, low=0x6A + // encoded = 0x23*0xC0 + 0x6A = 0x1AAA + wantHi: 0x1A, + wantLo: 0xAA, + }, + { + name: "upper boundary of range 2", + args: args{0xEB, 0xBF}, + // 0xEBBF - 0xC140 = 0x2A7F + // high=0x2A, low=0x7F + // encoded = 0x2A*0xC0 + 0x7F = 0x1FFF + wantHi: 0x1F, + wantLo: 0xFF, + }, + // Invalid ranges + { + name: "below range 1", + args: args{0x80, 0x00}, + wantHi: 0x00, + wantLo: 0x00, + }, + { + name: "between ranges", + args: args{0x9F, 0xFD}, + wantHi: 0x00, + wantLo: 0x00, + }, + { + name: "above range 2", + args: args{0xEC, 0x00}, + wantHi: 0x00, + wantLo: 0x00, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotHi, gotLo := encodeShiftJIS(tt.args.hi, tt.args.lo) + if gotHi != tt.wantHi || gotLo != tt.wantLo { + t.Errorf("encodeShiftJIS(0x%02X, 0x%02X) = (0x%02X, 0x%02X), want (0x%02X, 0x%02X)", + tt.args.hi, tt.args.lo, gotHi, gotLo, tt.wantHi, tt.wantLo) + } + }) + } +} + +func TestEncodeKanji(t *testing.T) { + tests := []struct { + name string + input string + wantLen int // Expected bit length for encoded data (13 bits per character) + }{ + { + name: "single character 世", + input: "世", + wantLen: 13, + }, + { + name: "two characters 世界", + input: "世界", + wantLen: 26, + }, + { + name: "four characters 日本語", + input: "日本語", + wantLen: 39, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc := encoder{ + ecLv: ErrorCorrectionLow, + mode: EncModeKanji, + version: loadVersion(1, ErrorCorrectionLow), + } + + b, err := enc.Encode(tt.input) + if err != nil { + t.Errorf("could not encode: %v", err) + t.Fail() } + + // The total length includes mode indicator (4), char count (8 for v1-9), + // data bits (wantLen), and padding to fill the codeword capacity + t.Logf("Encode(%q) total bits: %d", tt.input, b.Len()) + + // Check that we successfully encoded the Kanji data + // The mode indicator is 1000 (4 bits), char count is 8 bits for version 1 + // So the first 12 bits should be: 1000 (mode) + char count (8 bits) + // For a single character, char count = 1 = 00000001 + // First 12 bits: 1000 00000001 + }) + } +} + +func TestEncodeKanji_Version(t *testing.T) { + tests := []struct { + name string + input string + version int + expectedCharCountBits int + }{ + { + name: "version 1", + input: "漢字", + version: 1, + expectedCharCountBits: 8, + }, + { + name: "version 10", + input: "漢字", + version: 10, + expectedCharCountBits: 10, + }, + { + name: "version 27", + input: "漢字", + version: 27, + expectedCharCountBits: 12, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc := encoder{ + ecLv: ErrorCorrectionLow, + mode: EncModeKanji, + version: loadVersion(tt.version, ErrorCorrectionLow), + } + + charCountBits := enc.charCountBits() + if charCountBits != tt.expectedCharCountBits { + t.Errorf("charCountBits() = %d, want %d", charCountBits, tt.expectedCharCountBits) + } + + b, err := enc.Encode(tt.input) + if err != nil { + t.Errorf("could not encode: %v", err) + t.Fail() + } + + t.Logf("Encode(%q) with version %d = %v, total bits: %d", tt.input, tt.version, b, b.Len()) }) } } diff --git a/go.mod b/go.mod index 12f16e9..1d65435 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index ff1802e..ae1592c 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0= github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work.sum b/go.work.sum index 9bb2472..2c37ecc 100644 --- a/go.work.sum +++ b/go.work.sum @@ -52,20 +52,21 @@ golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/mask_test.go b/mask_test.go index 220bef1..691a775 100644 --- a/mask_test.go +++ b/mask_test.go @@ -8,7 +8,7 @@ import ( func TestMask(t *testing.T) { qrc := &QRCode{ - sourceRawBytes: []byte("baidu.com google.com qq.com sina.com apple.com"), + sourceText: "baidu.com google.com qq.com sina.com apple.com", encodingOption: DefaultEncodingOption(), } err := qrc.init() diff --git a/qrcode.go b/qrcode.go index 311c127..0852331 100644 --- a/qrcode.go +++ b/qrcode.go @@ -38,9 +38,38 @@ func toBytes[T ~string | ~[]byte](v T) []byte { } } +// validateEncodingMode checks if the specified encoding mode is compatible with the input text. +// Returns an error if the text contains characters that cannot be encoded in the specified mode. +func validateEncodingMode(mode encMode, text string) error { + var analyzeFn analyzeEncFunc + + switch mode { + case EncModeNumeric: + analyzeFn = analyzeNum + case EncModeAlphanumeric: + analyzeFn = analyzeAlphaNum + case EncModeKanji: + analyzeFn = analyzeJP + case EncModeByte: + // Byte mode can encode any character + return nil + default: + return nil + } + + for _, r := range text { + if !analyzeFn(r) { + return fmt.Errorf("character '%c' (U+%04X) cannot be encoded in %s mode", + r, r, getEncModeName(mode)) + } + } + + return nil +} + func build(raw []byte, option *encodingOption) (*QRCode, error) { qrc := &QRCode{ - sourceText: text, + sourceText: string(raw), dataBSet: nil, mat: nil, ecBSet: nil, @@ -62,7 +91,6 @@ func build(raw []byte, option *encodingOption) (*QRCode, error) { // etc. type QRCode struct { sourceText string // sourceText input text - // sourceRawBytes []byte // raw Data to transfer dataBSet *binary.Binary // final data bit stream of encode data mat *Matrix // matrix grid to store final bitmap @@ -80,7 +108,7 @@ func (q *QRCode) Save(w Writer) error { defer func() { if err := w.Close(); err != nil { - log.Printf("[WARNNING] [go-qrcode] close writer failed: %v\n", err) + log.Printf("[WARNING] [go-qrcode] close writer failed: %v\n", err) } }() @@ -103,6 +131,11 @@ func (q *QRCode) init() (err error) { if err != nil { return fmt.Errorf("init: analyze encode mode failed: %v", err) } + } else { + // Validate that the specified encoding mode is compatible with the input + if err = validateEncodingMode(q.encodingOption.EncMode, q.sourceText); err != nil { + return err + } } // choose version diff --git a/qrcode_test.go b/qrcode_test.go index 9feb555..441905e 100644 --- a/qrcode_test.go +++ b/qrcode_test.go @@ -23,17 +23,12 @@ func Test_NewWith(t *testing.T) { qrc.mat.print() } -// Test_NewWithConfig_UnmatchedEncodeMode NewWith will panic while encMode is -// not matched to Config.EncMode, for example: -// cfg.EncMode is EncModeAlphanumeric but source text is bytes encoding. +// Test_NewWithConfig_UnmatchedEncodeMode tests that explicit encoding mode +// returns error when input contains characters that cannot be encoded. func Test_NewWithConfig_UnmatchedEncodeMode(t *testing.T) { - assert.Panics(t, func() { - _, err := NewWith("abcs", WithEncodingMode(EncModeAlphanumeric)) - if err != nil { - t.Errorf("could not generate QRCode: %v", err) - t.Fail() - } - }) + // Lowercase letters with Alphanumeric mode should return error + _, err := NewWith("abcs", WithEncodingMode(EncModeAlphanumeric)) + assert.Error(t, err, "expected error when using lowercase letters with Alphanumeric mode") } func Benchmark_NewQRCode_1KB(b *testing.B) { @@ -148,3 +143,120 @@ func Test_NewWith_MinimumVersion_WithExplicitVersion(t *testing.T) { // WithVersion takes precedence, so version should be 10 assert.Equal(t, 10, qrc.v.Ver) } + +// Test_NewWith_Kanji_EncMode tests Kanji mode encoding with explicit mode setting +func Test_NewWith_Kanji_EncMode(t *testing.T) { + tests := []struct { + name string + text string + }{ + { + name: "single Kanji character", + text: "漢", + }, + { + name: "multiple Kanji characters", + text: "漢字", + }, + { + name: "Kanji sentence", + text: "日本語", + }, + { + name: "Kanji mixed characters", + text: "世界", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + qrc, err := NewWith(tt.text, + WithEncodingMode(EncModeKanji), + WithErrorCorrectionLevel(ErrorCorrectionLow), + ) + require.NoError(t, err) + assert.NotNil(t, qrc) + + // Verify the encoding mode is Kanji + assert.Equal(t, EncModeKanji, qrc.encoder.mode) + + t.Logf("Kanji QR code for '%s': version=%d", tt.text, qrc.v.Ver) + }) + } +} + +// Test_NewWith_Kanji_AutoMode tests automatic Kanji mode detection +func Test_NewWith_Kanji_AutoMode(t *testing.T) { + tests := []struct { + name string + text string + expected encMode + }{ + { + name: "Kanji only - auto detect", + text: "漢字", + expected: EncModeKanji, + }, + { + name: "Kanji characters - auto detect", + text: "世界", + expected: EncModeKanji, + }, + { + name: "Mixed ASCII and Katakana - auto detect Byte mode", + text: "QRコード123", + expected: EncModeByte, + }, + { + name: "Pure Kanji text - auto detect", + text: "金木水火土日月星", + expected: EncModeKanji, + }, + { + name: "Long Kanji text - auto detect", + text: "東京京都大阪北海道沖縄鹿児島", + expected: EncModeKanji, + }, + { + name: "Hiragana only - auto detect Byte mode", + text: "これはひらがなです", + expected: EncModeByte, // Hiragana is not supported in Kanji mode + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Use EncModeAuto to let the library detect the mode automatically + qrc, err := NewWith(tt.text, + WithEncodingMode(EncModeAuto), + WithErrorCorrectionLevel(ErrorCorrectionLow), + ) + require.NoError(t, err) + assert.NotNil(t, qrc) + + // Verify the detected mode matches expectation + assert.Equal(t, tt.expected, qrc.encoder.mode, + "Expected mode %v for text '%s', got %v", tt.expected, tt.text, qrc.encoder.mode) + + t.Logf("Auto-detected mode for '%s': %v, version=%d", tt.text, getEncModeName(qrc.encoder.mode), qrc.v.Ver) + }) + } +} + +// Test_NewWith_Kanji_Version10 tests Kanji encoding with specific version +func Test_NewWith_Kanji_Version10(t *testing.T) { + qrc, err := NewWith("漢字文字試験", + WithEncodingMode(EncModeKanji), + WithVersion(10), + WithErrorCorrectionLevel(ErrorCorrectionLow), + ) + require.NoError(t, err) + assert.NotNil(t, qrc) + + // Verify version is set correctly + assert.Equal(t, 10, qrc.v.Ver) + // Verify encoding mode is Kanji + assert.Equal(t, EncModeKanji, qrc.encoder.mode) + + t.Logf("Kanji QR code with version 10: matrix dimension=%d", qrc.mat.Width()) +} diff --git a/version.go b/version.go index 760c621..cad7d2a 100644 --- a/version.go +++ b/version.go @@ -290,7 +290,16 @@ func analyzeVersion(raw string, ec ecLevel, mode encMode) (*version, error) { return nil, errInvalidErrorCorrectionLevel } - want, mark := utf8.RuneCountInString(raw), 0 + // Byte mode capacity is measured in bytes, not characters + // Numeric, Alphanumeric, and Kanji modes are character-based + var want int + if mode == EncModeByte { + want = len(raw) + } else { + want = utf8.RuneCountInString(raw) + } + + mark := 0 for ; step < 160; step += 4 { switch mode { From 7d2e113d583548709975aee109ef7144b456428c Mon Sep 17 00:00:00 2001 From: Yeqown Date: Fri, 13 Mar 2026 18:07:14 +0800 Subject: [PATCH 13/13] tests: add test case for not supported kanji character cases --- qrcode_test.go | 74 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/qrcode_test.go b/qrcode_test.go index 441905e..c7aa8bd 100644 --- a/qrcode_test.go +++ b/qrcode_test.go @@ -147,24 +147,68 @@ func Test_NewWith_MinimumVersion_WithExplicitVersion(t *testing.T) { // Test_NewWith_Kanji_EncMode tests Kanji mode encoding with explicit mode setting func Test_NewWith_Kanji_EncMode(t *testing.T) { tests := []struct { - name string - text string + name string + text string + wantErr bool + expectedErr string }{ + // Valid Kanji input { - name: "single Kanji character", - text: "漢", + name: "single Kanji character", + text: "漢", + wantErr: false, }, { - name: "multiple Kanji characters", - text: "漢字", + name: "multiple Kanji characters", + text: "漢字", + wantErr: false, }, { - name: "Kanji sentence", - text: "日本語", + name: "Kanji sentence", + text: "日本語", + wantErr: false, }, { - name: "Kanji mixed characters", - text: "世界", + name: "Kanji mixed characters", + text: "世界", + wantErr: false, + }, + // Invalid input for Kanji mode + { + name: "ASCII characters", + text: "https://google.com", + wantErr: true, + expectedErr: "cannot be encoded in kanji mode", + }, + { + name: "numbers with Kanji mode", + text: "漢字123", + wantErr: true, + expectedErr: "cannot be encoded in kanji mode", + }, + { + name: "Hiragana with Kanji mode", + text: "こんにちは", + wantErr: true, + expectedErr: "cannot be encoded in kanji mode", + }, + { + name: "Katakana with Kanji mode", + text: "コンニチハ", + wantErr: true, + expectedErr: "cannot be encoded in kanji mode", + }, + { + name: "mixed Kanji and ASCII", + text: "漢字test", + wantErr: true, + expectedErr: "cannot be encoded in kanji mode", + }, + { + name: "CJK Extension A character", + text: "㐀", + wantErr: true, + expectedErr: "cannot be encoded in kanji mode", }, } @@ -174,12 +218,16 @@ func Test_NewWith_Kanji_EncMode(t *testing.T) { WithEncodingMode(EncModeKanji), WithErrorCorrectionLevel(ErrorCorrectionLow), ) + + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErr) + return + } + require.NoError(t, err) assert.NotNil(t, qrc) - - // Verify the encoding mode is Kanji assert.Equal(t, EncModeKanji, qrc.encoder.mode) - t.Logf("Kanji QR code for '%s': version=%d", tt.text, qrc.v.Ver) }) }