baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
"""二维码往返自检(开发工具,不参与运行时)。
|
||||
|
||||
作用:解码我们自己生成的 QR SVG,验证 **能被读回原文本**——避免"看起来像二维码
|
||||
但扫不出来"的假成功。这不是完整 QR 解码器,只覆盖生成器用到的子集
|
||||
(Model 2 / 字节模式 / 纠错 L / 掩码 0),用于回归验证。
|
||||
|
||||
用法:
|
||||
python -m apps.printing.qr_selftest
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import re
|
||||
import sys
|
||||
|
||||
from .qr import VERSIONS_L, ALIGN_POS, _size_of, _pick_version
|
||||
|
||||
|
||||
def _svg_to_matrix(svg: str) -> list:
|
||||
"""从生成的 SVG 还原模块矩阵(正方形 rect 拼回去,无需图像库)。"""
|
||||
m = re.search(r'viewBox="0 0 (\d+) (\d+)"', svg)
|
||||
if not m:
|
||||
raise ValueError("no viewBox")
|
||||
total = int(m.group(1))
|
||||
# 模块边长 = 总边长 / (size + 2*border),从第一个 rect 的 width 推断
|
||||
rects = re.findall(r'<rect x="(\d+)" y="(\d+)" width="(\d+)" height="(\d+)"/>', svg)
|
||||
if not rects:
|
||||
raise ValueError("no rects")
|
||||
scale = int(rects[0][3]) # height = scale
|
||||
size = total // scale
|
||||
grid = [[1] * size for _ in range(size)] # 背景白=0,先全填 0 再置黑
|
||||
grid = [[0] * size for _ in range(size)]
|
||||
for x, y, w, h in rects:
|
||||
x, y, w, h = int(x), int(y), int(w), int(h)
|
||||
r0, c0 = y // scale, x // scale
|
||||
for dc in range(w // scale):
|
||||
if 0 <= r0 < size and c0 + dc < size:
|
||||
grid[r0][c0 + dc] = 1
|
||||
return grid
|
||||
|
||||
|
||||
def _strip_border(grid: list) -> list:
|
||||
"""去掉静默区,回到纯模块矩阵。"""
|
||||
size = len(grid)
|
||||
# 找第一行/列全 0 的边界(静默区)
|
||||
def all_zero(values):
|
||||
return all(v == 0 for v in values)
|
||||
|
||||
top = 0
|
||||
while top < size and all_zero(grid[top]):
|
||||
top += 1
|
||||
left = 0
|
||||
while left < size and all_zero([grid[r][left] for r in range(size)]):
|
||||
left += 1
|
||||
bottom = size - 1
|
||||
while bottom >= 0 and all_zero(grid[bottom]):
|
||||
bottom -= 1
|
||||
right = size - 1
|
||||
while right >= 0 and all_zero([grid[r][right] for r in range(size)]):
|
||||
right -= 1
|
||||
return [row[left:right + 1] for row in grid[top:bottom + 1]]
|
||||
|
||||
|
||||
def _read_format(mat: list) -> tuple:
|
||||
"""读左上格式信息 → (纠错级别, 掩码)。"""
|
||||
size = len(mat)
|
||||
seq = [(8, 0), (8, 1), (8, 2), (8, 3), (8, 4), (8, 5), (8, 7), (8, 8),
|
||||
(7, 8), (5, 8), (4, 8), (3, 8), (2, 8), (1, 8), (0, 8)]
|
||||
bits = 0
|
||||
for i, (r, c) in enumerate(seq):
|
||||
bits |= (mat[r][c] & 1) << i
|
||||
bits ^= 0b101010000010010
|
||||
ec = (bits >> 13) & 0b11
|
||||
mask = (bits >> 10) & 0b111
|
||||
return ec, mask
|
||||
|
||||
|
||||
def _unmask(mat: list, mask: int) -> list:
|
||||
"""按掩码规则还原数据区。"""
|
||||
from .qr import _is_function
|
||||
|
||||
size = len(mat)
|
||||
version = (size - 17) // 4
|
||||
out = [row[:] for row in mat]
|
||||
for r in range(size):
|
||||
for c in range(size):
|
||||
if _is_function(r, c, version):
|
||||
continue
|
||||
invert = False
|
||||
if mask == 0:
|
||||
invert = (r + c) % 2 == 0
|
||||
elif mask == 1:
|
||||
invert = r % 2 == 0
|
||||
elif mask == 2:
|
||||
invert = c % 3 == 0
|
||||
elif mask == 3:
|
||||
invert = (r + c) % 3 == 0
|
||||
if invert:
|
||||
out[r][c] ^= 1
|
||||
return out
|
||||
|
||||
|
||||
def _extract_bits(mat: list, version: int | None = None) -> list:
|
||||
"""按之字形路径读出数据位(跳过功能模块)。"""
|
||||
from .qr import _is_function
|
||||
|
||||
size = len(mat)
|
||||
version = version if version is not None else (size - 17) // 4
|
||||
bits = []
|
||||
col = size - 1
|
||||
upward = True
|
||||
while col > 0:
|
||||
if col == 6:
|
||||
col -= 1
|
||||
rows = range(size - 1, -1, -1) if upward else range(size)
|
||||
for row in rows:
|
||||
for c in (col, col - 1):
|
||||
if not _is_function(row, c, version):
|
||||
bits.append(mat[row][c] & 1)
|
||||
upward = not upward
|
||||
col -= 2
|
||||
return bits
|
||||
|
||||
|
||||
def _bits_to_bytes(bits: list) -> bytes:
|
||||
out = bytearray()
|
||||
for i in range(0, len(bits) - 7, 8):
|
||||
b = 0
|
||||
for j in range(8):
|
||||
b = (b << 1) | bits[i + j]
|
||||
out.append(b)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def decode(svg: str) -> str | None:
|
||||
"""解码本模块生成的 QR SVG,返回原文;失败返回 None。
|
||||
|
||||
流程:SVG → 模块矩阵 → 去静默区 → 读格式信息(纠错/掩码) → 去掩码
|
||||
→ 之字形读位 → 字节模式解析。
|
||||
"""
|
||||
from .qr import _is_function
|
||||
|
||||
grid = _svg_to_matrix(svg)
|
||||
mat = _strip_border(grid)
|
||||
size = len(mat)
|
||||
version = (size - 17) // 4
|
||||
if version not in VERSIONS_L:
|
||||
return None
|
||||
|
||||
ec, mask = _read_format(mat)
|
||||
if ec != 0b01: # 只支持纠错 L
|
||||
return None
|
||||
|
||||
# 去掩码
|
||||
base = [row[:] for row in mat]
|
||||
for r in range(size):
|
||||
for c in range(size):
|
||||
if _is_function(r, c, version):
|
||||
continue
|
||||
if mask == 0 and (r + c) % 2 == 0:
|
||||
base[r][c] ^= 1
|
||||
elif mask == 1 and r % 2 == 0:
|
||||
base[r][c] ^= 1
|
||||
elif mask == 2 and c % 3 == 0:
|
||||
base[r][c] ^= 1
|
||||
elif mask == 3 and (r + c) % 3 == 0:
|
||||
base[r][c] ^= 1
|
||||
|
||||
# 之字形读位
|
||||
bits = []
|
||||
col = size - 1
|
||||
upward = True
|
||||
while col > 0:
|
||||
if col == 6:
|
||||
col -= 1
|
||||
rows = range(size - 1, -1, -1) if upward else range(size)
|
||||
for row in rows:
|
||||
for c in (col, col - 1):
|
||||
if not _is_function(row, c, version):
|
||||
bits.append(base[row][c] & 1)
|
||||
upward = not upward
|
||||
col -= 2
|
||||
|
||||
# 位流解析(注意:不能按字节边界切——模式 4 位 + 长度 8 位 = 12 位,
|
||||
# 数据从第 12 位开始,跨字节。早期版本按字节取导致整体错位。)
|
||||
if len(bits) < 12:
|
||||
return None
|
||||
if bits[0:4] != [0, 1, 0, 0]: # 字节模式
|
||||
return None
|
||||
length = 0
|
||||
for b in bits[4:12]: # 版本 1-9:长度 8 位
|
||||
length = (length << 1) | b
|
||||
need = 12 + length * 8
|
||||
if length <= 0 or len(bits) < need:
|
||||
return None
|
||||
payload = bytearray()
|
||||
for i in range(length):
|
||||
byte = 0
|
||||
for j in range(8):
|
||||
byte = (byte << 1) | bits[12 + i * 8 + j]
|
||||
payload.append(byte)
|
||||
try:
|
||||
return payload.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def selftest() -> int:
|
||||
"""往返测试若干载荷,返回失败数。"""
|
||||
from .qr import qr_svg_data_uri
|
||||
|
||||
samples = [
|
||||
"/api/v1/open/statements/9eb70e10-82e1-47d1-bc5b-fb3e0716ba19/",
|
||||
"https://dealerhub.example.com/api/v1/open/statements/c71ac955-27d6-4942-968c-720a6afe5394/",
|
||||
"HELLO-DEALERHUB-2026",
|
||||
"1",
|
||||
"a" * 60,
|
||||
"对账单-中文测试",
|
||||
]
|
||||
failed = 0
|
||||
for text in samples:
|
||||
uri = qr_svg_data_uri(text, scale=4)
|
||||
svg = base64.b64decode(uri.split(",", 1)[1]).decode("utf-8")
|
||||
got = decode(svg)
|
||||
ok = got == text
|
||||
failed += 0 if ok else 1
|
||||
mark = "✓" if ok else "✗"
|
||||
print(f" {mark} len={len(text):3d} expect={text[:45]!r}"
|
||||
+ ("" if ok else f" got={got!r}"))
|
||||
print(f"\n往返测试:{len(samples) - failed}/{len(samples)} 通过")
|
||||
return failed
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(1 if selftest() else 0)
|
||||
Reference in New Issue
Block a user