"""二维码往返自检(开发工具,不参与运行时)。 作用:解码我们自己生成的 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'', 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)