-
-
+
点击下方按钮,将小票二维码对准取景框即可自动识别;也可从相册选择二维码图片。
-
-
+
+
+
+
{
去系统设置开启相机权限
-
-
-
-
-
-
-
-
将二维码放入框内,自动识别
-
-
diff --git a/tests/images/二维码1.jpg b/tests/images/二维码1.jpg
new file mode 100644
index 0000000..d6abe8a
Binary files /dev/null and b/tests/images/二维码1.jpg differ
diff --git a/tests/images/二维码2.jpg b/tests/images/二维码2.jpg
new file mode 100644
index 0000000..bab4281
Binary files /dev/null and b/tests/images/二维码2.jpg differ
diff --git a/tests/qrscan.test.js b/tests/qrscan.test.js
index 0f4fba3..b9b5776 100644
--- a/tests/qrscan.test.js
+++ b/tests/qrscan.test.js
@@ -1,8 +1,9 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from "vitest";
import QRCode from "qrcode";
import {
extractUrl,
decodeQrFromImageData,
+ scaleImageData,
} from "../src/qrscan.js";
/**
@@ -18,11 +19,10 @@ function renderQrImageData(text, scale = 8, quiet = 4) {
const modules = qr.modules.data;
const dim = (size + quiet * 2) * scale;
const data = new Uint8ClampedArray(dim * dim * 4);
- // 先整体填白(含静区)。
data.fill(255);
for (let row = 0; row < size; row += 1) {
for (let col = 0; col < size; col += 1) {
- if (!modules[row * size + col]) continue; // 0 = 浅色,跳过
+ if (!modules[row * size + col]) continue;
const x0 = (col + quiet) * scale;
const y0 = (row + quiet) * scale;
for (let dy = 0; dy < scale; dy += 1) {
@@ -39,6 +39,45 @@ function renderQrImageData(text, scale = 8, quiet = 4) {
return { data, width: dim, height: dim };
}
+/**
+ * 将小二维码嵌入大画布中央,模拟手机拍摄的远景照片(二维码占比小)。
+ * @param {string} text 二维码内容。
+ * @param {number} canvasW 大画布宽度。
+ * @param {number} canvasH 大画布高度。
+ * @returns {ImageData} 大画布像素数据。
+ */
+function embedSmallQr(text, canvasW = 600, canvasH = 400) {
+ const qr = QRCode.create(text, { errorCorrectionLevel: "M" });
+ const size = qr.modules.size;
+ const modules = qr.modules.data;
+ const scale = 3; // 每个模块 3px,二维码约 80px
+ const quiet = 1;
+ const qrW = (size + quiet * 2) * scale;
+ const qrH = qrW;
+ const ox = Math.floor((canvasW - qrW) / 2);
+ const oy = Math.floor((canvasH - qrH) / 2);
+
+ const data = new Uint8ClampedArray(canvasW * canvasH * 4);
+ data.fill(255);
+ for (let row = 0; row < size; row += 1) {
+ for (let col = 0; col < size; col += 1) {
+ if (!modules[row * size + col]) continue;
+ const x0 = ox + (col + quiet) * scale;
+ const y0 = oy + (row + quiet) * scale;
+ for (let dy = 0; dy < scale; dy += 1) {
+ for (let dx = 0; dx < scale; dx += 1) {
+ const idx = ((y0 + dy) * canvasW + (x0 + dx)) * 4;
+ data[idx] = 0;
+ data[idx + 1] = 0;
+ data[idx + 2] = 0;
+ data[idx + 3] = 255;
+ }
+ }
+ }
+ }
+ return { data, width: canvasW, height: canvasH };
+}
+
describe("qrscan", () => {
// 4-1 网址校验:接受 http/https,拒绝其它内容。
it("4-1 extractUrl 仅接受 http/https 网址", () => {
@@ -68,4 +107,25 @@ describe("qrscan", () => {
expect(decodeQrFromImageData(blank)).toBeNull();
expect(decodeQrFromImageData(null)).toBeNull();
});
+
+ // 4-4 多级缩放回退:大画布中小二维码,原始尺寸失败但缩小后成功。
+ it("4-4 多级缩放回退能识别大图中的小二维码", () => {
+ const url = "https://inv.example.com/small";
+ const imageData = embedSmallQr(url, 800, 600);
+ const result = decodeQrFromImageData(imageData);
+ expect(result).toBe(url);
+ });
+
+ // 4-5 scaleImageData 等比缩放正确。
+ it("4-5 scaleImageData 等比缩放", () => {
+ const src = renderQrImageData("test", 4);
+ const half = scaleImageData(src, 0.5);
+ expect(half.width).toBe(Math.floor(src.width * 0.5));
+ expect(half.height).toBe(Math.floor(src.height * 0.5));
+ expect(half.data).toBeInstanceOf(Uint8ClampedArray);
+ expect(half.data.length).toBe(half.width * half.height * 4);
+ expect(half.height).toBe(Math.floor(src.height * 0.5));
+ });
});
+
+