This commit is contained in:
cnwhy
2026-08-02 18:11:21 +08:00
parent bfea903e80
commit c6f33bf5c9
8 changed files with 12938 additions and 42 deletions
+162 -5
View File
@@ -1,5 +1,5 @@
/*!
* qr-decode v0.0.4
* qr-decode v0.0.5
* (c) cnwhy <w.why@163.com>
* Released under the ISC License.
*/
@@ -2432,6 +2432,130 @@ function decode(imageDate, debugfn) {
};
return process(imageDate.data, base);
}
// ECI 声明中常见的字符集(QR 规范:ECI 编号 -> 字符集)
var ECI_CHARSET = {
3: 'iso-8859-1',
20: 'shift_jis',
25: 'utf-16be',
26: 'utf-8',
27: 'us-ascii'
};
// UTF-8 字节数组转字符串
var decodeUTF8 = function decodeUTF8(bytes) {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8').decode(new Uint8Array(bytes));
}
var str = "";
var i = 0;
while (i < bytes.length) {
var code = bytes[i++] & 0xFF;
if (code < 0x80) {
str += String.fromCharCode(code);
} else if (code < 0xE0) {
str += String.fromCharCode((code & 0x1F) << 6 | bytes[i++] & 0x3F);
} else if (code < 0xF0) {
str += String.fromCharCode((code & 0x0F) << 12 | (bytes[i++] & 0x3F) << 6 | bytes[i++] & 0x3F);
} else {
var cp = (code & 0x07) << 18 | (bytes[i++] & 0x3F) << 12 | (bytes[i++] & 0x3F) << 6 | bytes[i++] & 0x3F;
cp -= 0x10000;
str += String.fromCharCode((cp >> 10) + 0xD800, (cp & 0x3FF) + 0xDC00);
}
}
return str;
};
// UTF-16 字节数组转字符串(isLE=false 时先把 BE 转成 LE 统一处理)
var decodeUTF16 = function decodeUTF16(bytes, isLE) {
if (!isLE) {
// UTF-16BE -> 交换字节序
for (var i = 0; i + 1 < bytes.length; i += 2) {
var t = bytes[i];
bytes[i] = bytes[i + 1];
bytes[i + 1] = t;
}
}
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-16le').decode(new Uint8Array(bytes));
}
// 兜底:手写 UTF-16LE 解码(含代理对)
var str = "";
for (var i = 0; i + 1 < bytes.length; i += 2) {
var code = bytes[i] | bytes[i + 1] << 8;
if (code >= 0xD800 && code <= 0xDBFF && i + 3 < bytes.length) {
var low = bytes[i + 2] | bytes[i + 3] << 8;
if (low >= 0xDC00 && low <= 0xDFFF) {
str += String.fromCharCode(code, low);
i += 2;
continue;
}
}
str += String.fromCharCode(code);
}
return str;
};
// 8bit 字节段解码:按 ECI 声明 -> BOM -> 启发式检测 -> UTF-8 的顺序判断编码方式
var bytesToString = function bytesToString(bytes, eci) {
var len = bytes.length;
if (len == 0) {
return "";
}
// 1. 优先按 ECI 声明的字符集解码
if (eci && ECI_CHARSET[eci]) {
var charset = ECI_CHARSET[eci];
if (charset == 'utf-16be') {
return decodeUTF16(bytes, false);
}
if (charset == 'utf-8') {
return decodeUTF8(bytes);
}
if (typeof TextDecoder !== 'undefined') {
try {
return new TextDecoder(charset).decode(new Uint8Array(bytes));
} catch (e) {
// 环境不支持该字符集时继续走下面的检测
}
}
}
// 2. BOM 检测
if (len >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE) {
return decodeUTF16(bytes.slice(2), true); // UTF-16LE
}
if (len >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF) {
return decodeUTF16(bytes.slice(2), false); // UTF-16BE
}
if (len >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) {
return decodeUTF8(bytes.slice(3)); // UTF-8 with BOM
}
// 3. 无 BOM 时启发式检测 UTF-16(偶数长度且奇/偶位存在成片 0x00 字节)
if (len >= 4 && len % 2 == 0) {
var evenZeros = 0;
var oddZeros = 0;
for (var i = 0; i < len; i += 2) {
if (bytes[i] == 0) {
evenZeros++;
}
if (bytes[i + 1] == 0) {
oddZeros++;
}
}
var half = len / 2;
if (oddZeros > half * 0.7) {
return decodeUTF16(bytes, true); // 奇数位大量 0x00 -> UTF-16LE
}
if (evenZeros > half * 0.7) {
return decodeUTF16(bytes, false); // 偶数位大量 0x00 -> UTF-16BE
}
}
// 4. 兜底默认 UTF-8
return decodeUTF8(bytes);
};
var process = function process(data, base) {
// var start = new Date().getTime();
// var image = grayScaleToBitmap(grayscale(),base);
@@ -2453,11 +2577,44 @@ var process = function process(data, base) {
// 解析QR矩阵
var reader = Decoder_1.decode(qRCodeMatrix.bits);
// console.log(reader);
var data = reader.DataByte;
// 先读取编码方式(mode indicator),按编码方式将数据直接转成字符串
var MODE_NUMBER = 1;
var MODE_ROMAN_AND_NUMBER = 2;
var MODE_8BIT_BYTE = 4;
var MODE_ECI = 7;
var MODE_KANJI = 8;
var str = "";
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].length; j++) str += String.fromCharCode(data[i][j]);
var eci = 0; // ECI 声明的字符集编号,作用于后续字节段
while (true) {
var mode = reader.NextMode();
if (mode == 0) break;
if (mode == MODE_ECI) {
eci = reader.parseECIValue();
continue;
}
var dataLength = reader.getDataLength(mode);
if (dataLength < 1) throw "Invalid data length: " + dataLength;
switch (mode) {
case MODE_NUMBER:
// 数字模式
str += reader.getFigureString(dataLength);
break;
case MODE_ROMAN_AND_NUMBER:
// 字母数字模式
str += reader.getRomanAndFigureString(dataLength);
break;
case MODE_8BIT_BYTE:
// 8bit 字节模式,先判断编码方式(ECI/BOM/启发式)再解码
str += bytesToString(reader.get8bitByteArray(dataLength), eci);
break;
case MODE_KANJI:
// 汉字(日文)模式
str += reader.getKanjiString(dataLength);
break;
default:
throw "Invalid mode: " + mode;
}
}
// var end = new Date().getTime();
+162 -5
View File
@@ -1,5 +1,5 @@
/*!
* qr-decode v0.0.4
* qr-decode v0.0.5
* (c) cnwhy <w.why@163.com>
* Released under the ISC License.
*/
@@ -2438,6 +2438,130 @@
};
return process(imageDate.data, base);
}
// ECI 声明中常见的字符集(QR 规范:ECI 编号 -> 字符集)
var ECI_CHARSET = {
3: 'iso-8859-1',
20: 'shift_jis',
25: 'utf-16be',
26: 'utf-8',
27: 'us-ascii'
};
// UTF-8 字节数组转字符串
var decodeUTF8 = function decodeUTF8(bytes) {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8').decode(new Uint8Array(bytes));
}
var str = "";
var i = 0;
while (i < bytes.length) {
var code = bytes[i++] & 0xFF;
if (code < 0x80) {
str += String.fromCharCode(code);
} else if (code < 0xE0) {
str += String.fromCharCode((code & 0x1F) << 6 | bytes[i++] & 0x3F);
} else if (code < 0xF0) {
str += String.fromCharCode((code & 0x0F) << 12 | (bytes[i++] & 0x3F) << 6 | bytes[i++] & 0x3F);
} else {
var cp = (code & 0x07) << 18 | (bytes[i++] & 0x3F) << 12 | (bytes[i++] & 0x3F) << 6 | bytes[i++] & 0x3F;
cp -= 0x10000;
str += String.fromCharCode((cp >> 10) + 0xD800, (cp & 0x3FF) + 0xDC00);
}
}
return str;
};
// UTF-16 字节数组转字符串(isLE=false 时先把 BE 转成 LE 统一处理)
var decodeUTF16 = function decodeUTF16(bytes, isLE) {
if (!isLE) {
// UTF-16BE -> 交换字节序
for (var i = 0; i + 1 < bytes.length; i += 2) {
var t = bytes[i];
bytes[i] = bytes[i + 1];
bytes[i + 1] = t;
}
}
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-16le').decode(new Uint8Array(bytes));
}
// 兜底:手写 UTF-16LE 解码(含代理对)
var str = "";
for (var i = 0; i + 1 < bytes.length; i += 2) {
var code = bytes[i] | bytes[i + 1] << 8;
if (code >= 0xD800 && code <= 0xDBFF && i + 3 < bytes.length) {
var low = bytes[i + 2] | bytes[i + 3] << 8;
if (low >= 0xDC00 && low <= 0xDFFF) {
str += String.fromCharCode(code, low);
i += 2;
continue;
}
}
str += String.fromCharCode(code);
}
return str;
};
// 8bit 字节段解码:按 ECI 声明 -> BOM -> 启发式检测 -> UTF-8 的顺序判断编码方式
var bytesToString = function bytesToString(bytes, eci) {
var len = bytes.length;
if (len == 0) {
return "";
}
// 1. 优先按 ECI 声明的字符集解码
if (eci && ECI_CHARSET[eci]) {
var charset = ECI_CHARSET[eci];
if (charset == 'utf-16be') {
return decodeUTF16(bytes, false);
}
if (charset == 'utf-8') {
return decodeUTF8(bytes);
}
if (typeof TextDecoder !== 'undefined') {
try {
return new TextDecoder(charset).decode(new Uint8Array(bytes));
} catch (e) {
// 环境不支持该字符集时继续走下面的检测
}
}
}
// 2. BOM 检测
if (len >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE) {
return decodeUTF16(bytes.slice(2), true); // UTF-16LE
}
if (len >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF) {
return decodeUTF16(bytes.slice(2), false); // UTF-16BE
}
if (len >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) {
return decodeUTF8(bytes.slice(3)); // UTF-8 with BOM
}
// 3. 无 BOM 时启发式检测 UTF-16(偶数长度且奇/偶位存在成片 0x00 字节)
if (len >= 4 && len % 2 == 0) {
var evenZeros = 0;
var oddZeros = 0;
for (var i = 0; i < len; i += 2) {
if (bytes[i] == 0) {
evenZeros++;
}
if (bytes[i + 1] == 0) {
oddZeros++;
}
}
var half = len / 2;
if (oddZeros > half * 0.7) {
return decodeUTF16(bytes, true); // 奇数位大量 0x00 -> UTF-16LE
}
if (evenZeros > half * 0.7) {
return decodeUTF16(bytes, false); // 偶数位大量 0x00 -> UTF-16BE
}
}
// 4. 兜底默认 UTF-8
return decodeUTF8(bytes);
};
var process = function process(data, base) {
// var start = new Date().getTime();
// var image = grayScaleToBitmap(grayscale(),base);
@@ -2459,11 +2583,44 @@
// 解析QR矩阵
var reader = Decoder_1.decode(qRCodeMatrix.bits);
// console.log(reader);
var data = reader.DataByte;
// 先读取编码方式(mode indicator),按编码方式将数据直接转成字符串
var MODE_NUMBER = 1;
var MODE_ROMAN_AND_NUMBER = 2;
var MODE_8BIT_BYTE = 4;
var MODE_ECI = 7;
var MODE_KANJI = 8;
var str = "";
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].length; j++) str += String.fromCharCode(data[i][j]);
var eci = 0; // ECI 声明的字符集编号,作用于后续字节段
while (true) {
var mode = reader.NextMode();
if (mode == 0) break;
if (mode == MODE_ECI) {
eci = reader.parseECIValue();
continue;
}
var dataLength = reader.getDataLength(mode);
if (dataLength < 1) throw "Invalid data length: " + dataLength;
switch (mode) {
case MODE_NUMBER:
// 数字模式
str += reader.getFigureString(dataLength);
break;
case MODE_ROMAN_AND_NUMBER:
// 字母数字模式
str += reader.getRomanAndFigureString(dataLength);
break;
case MODE_8BIT_BYTE:
// 8bit 字节模式,先判断编码方式(ECI/BOM/启发式)再解码
str += bytesToString(reader.get8bitByteArray(dataLength), eci);
break;
case MODE_KANJI:
// 汉字(日文)模式
str += reader.getKanjiString(dataLength);
break;
default:
throw "Invalid mode: " + mode;
}
}
// var end = new Date().getTime();
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long