baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+236
View File
@@ -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)