#!/usr/bin/env python3 """ qr.py — Dibuja un código QR sin depender de nada: del texto a la cuadrícula de módulos oscuros y claros. Solo lo que Caudal necesita: texto como bytes (UTF-8), nivel de corrección M y versiones 1 a 12 (hasta 287 bytes), que da de sobra para una dirección y un código de enlace. Sigue ISO/IEC 18004: Reed-Solomon sobre GF(256), bloques entrelazados, las ocho máscaras y la de menor penalización. matriz("hola") → lista de filas, cada una una lista de bool (True = oscuro) """ from __future__ import annotations # Versión → (codewords de corrección por bloque, [(bloques, codewords de datos por bloque), …]) para el nivel M _BLOQUES = { 1: (10, [(1, 16)]), 2: (16, [(1, 28)]), 3: (26, [(1, 44)]), 4: (18, [(2, 32)]), 5: (24, [(2, 43)]), 6: (16, [(4, 27)]), 7: (18, [(4, 31)]), 8: (22, [(2, 38), (2, 39)]), 9: (22, [(3, 36), (2, 37)]), 10: (26, [(4, 43), (1, 44)]), 11: (30, [(1, 50), (4, 51)]), 12: (22, [(6, 36), (2, 37)]), } _ALINEACION = {1: [], 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30], 6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50], 11: [6, 30, 54], 12: [6, 32, 58]} _MASCARAS = ( lambda x, y: (x + y) % 2 == 0, lambda x, y: y % 2 == 0, lambda x, y: x % 3 == 0, lambda x, y: (x + y) % 3 == 0, lambda x, y: (x // 3 + y // 2) % 2 == 0, lambda x, y: x * y % 2 + x * y % 3 == 0, lambda x, y: (x * y % 2 + x * y % 3) % 2 == 0, lambda x, y: ((x + y) % 2 + x * y % 3) % 2 == 0, ) class TextoDemasiadoLargo(ValueError): pass def capacidad(version: int) -> int: """Bytes de texto que caben en esa versión.""" datos = sum(n * d for n, d in _BLOQUES[version][1]) return datos - (2 if version < 10 else 3) # 4 bits de modo + 8 o 16 de longitud, redondeado # ── Reed-Solomon en GF(256), polinomio 0x11D ── _EXP = [0] * 512 _LOG = [0] * 256 _x = 1 for _i in range(255): _EXP[_i] = _x _LOG[_x] = _i _x <<= 1 if _x & 0x100: _x ^= 0x11D for _i in range(255, 512): _EXP[_i] = _EXP[_i - 255] def _mul(a: int, b: int) -> int: return 0 if a == 0 or b == 0 else _EXP[_LOG[a] + _LOG[b]] def _generador(grado: int) -> list[int]: g = [1] for i in range(grado): g = [(_mul(g[j], _EXP[i]) if j < len(g) else 0) ^ (g[j - 1] if j else 0) for j in range(len(g) + 1)] return g[::-1] # de mayor a menor grado, mónico def _correccion(datos: list[int], grado: int) -> list[int]: g = _generador(grado) resto = [0] * grado for b in datos: factor = b ^ resto[0] resto = resto[1:] + [0] for i in range(grado): resto[i] ^= _mul(g[i + 1], factor) return resto def _codewords(texto: bytes, version: int) -> list[int]: ec, grupos = _BLOQUES[version] total_datos = sum(n * d for n, d in grupos) bits: list[int] = [] def mete(valor: int, n: int) -> None: bits.extend((valor >> i) & 1 for i in range(n - 1, -1, -1)) mete(0b0100, 4) mete(len(texto), 8 if version < 10 else 16) for b in texto: mete(b, 8) mete(0, min(4, total_datos * 8 - len(bits))) mete(0, -len(bits) % 8) datos = [int("".join(map(str, bits[i:i + 8])), 2) for i in range(0, len(bits), 8)] for relleno in (0xEC, 0x11) * total_datos: if len(datos) >= total_datos: break datos.append(relleno) bloques, pos = [], 0 for n, largo in grupos: for _ in range(n): bloques.append(datos[pos:pos + largo]) pos += largo correcciones = [_correccion(b, ec) for b in bloques] salida = [b[i] for i in range(max(map(len, bloques))) for b in bloques if i < len(b)] salida += [c[i] for i in range(ec) for c in correcciones] return salida def _bch(valor: int, bits: int, generador: int) -> int: resto = valor for _ in range(bits): resto = (resto << 1) ^ ((resto >> (generador.bit_length() - 2)) * generador) return resto def _base(version: int) -> tuple[list[list[bool]], list[list[bool]]]: """La cuadrícula con los patrones fijos y cuáles de sus módulos están reservados.""" n = 17 + 4 * version m = [[False] * n for _ in range(n)] fijo = [[False] * n for _ in range(n)] def pon(x: int, y: int, oscuro: bool) -> None: if 0 <= x < n and 0 <= y < n: m[y][x] = oscuro fijo[y][x] = True for i in range(n): # líneas de sincronía pon(6, i, i % 2 == 0) pon(i, 6, i % 2 == 0) for cx, cy in ((3, 3), (n - 4, 3), (3, n - 4)): # los tres ojos con su borde claro for dy in range(-4, 5): for dx in range(-4, 5): pon(cx + dx, cy + dy, max(abs(dx), abs(dy)) not in (2, 4)) centros = _ALINEACION[version] for cy in centros: # patrones de alineación for cx in centros: if (cx, cy) in ((6, 6), (6, n - 7), (n - 7, 6)): continue for dy in range(-2, 3): for dx in range(-2, 3): pon(cx + dx, cy + dy, max(abs(dx), abs(dy)) != 1) for i in range(9): # sitio de la información de formato for x, y in ((8, i), (i, 8)): if not fijo[y][x]: pon(x, y, False) for i in range(8): pon(n - 1 - i, 8, False) pon(8, n - 1 - i, False) pon(8, n - 8, True) # el módulo siempre oscuro if version >= 7: # información de versión dato = version << 12 | _bch(version, 12, 0x1F25) for i in range(18): a, b = n - 11 + i % 3, i // 3 pon(a, b, bool(dato >> i & 1)) pon(b, a, bool(dato >> i & 1)) return m, fijo def _formato(m: list[list[bool]], mascara: int) -> None: n = len(m) dato = (mascara << 10 | _bch(mascara, 10, 0x537)) ^ 0x5412 # nivel M = 00 bit = [bool(dato >> i & 1) for i in range(15)] for i in range(6): m[i][8] = bit[i] m[7][8], m[8][8], m[8][7] = bit[6], bit[7], bit[8] for i in range(9, 15): m[8][14 - i] = bit[i] for i in range(8): m[8][n - 1 - i] = bit[i] for i in range(8, 15): m[n - 15 + i][8] = bit[i] def _penalizacion(m: list[list[bool]]) -> int: n = len(m) total = 0 lineas = [list(f) for f in m] + [[m[y][x] for y in range(n)] for x in range(n)] for linea in lineas: racha, color = 0, None for v in linea: # rachas de cinco o más del mismo color if v == color: racha += 1 if racha == 5: total += 3 elif racha > 5: total += 1 else: color, racha = v, 1 cadena = "".join("1" if v else "0" for v in linea) # el dibujo de un ojo con claro al lado for patron in ("10111010000", "00001011101"): inicio = cadena.find(patron) while inicio != -1: total += 40 inicio = cadena.find(patron, inicio + 1) for y in range(n - 1): # bloques de 2×2 del mismo color for x in range(n - 1): if m[y][x] == m[y][x + 1] == m[y + 1][x] == m[y + 1][x + 1]: total += 3 oscuros = sum(map(sum, m)) # proporción de oscuros lejos de la mitad total += (abs(oscuros * 20 - n * n * 10) + n * n - 1) // (n * n) * 10 - 10 if oscuros * 2 != n * n else 0 return total def matriz(texto: str | bytes, mascara: int | None = None) -> list[list[bool]]: """La cuadrícula del QR (sin el margen claro, que pone quien la dibuja).""" crudo = texto.encode("utf-8") if isinstance(texto, str) else bytes(texto) version = next((v for v in sorted(_BLOQUES) if len(crudo) <= capacidad(v)), None) if version is None: raise TextoDemasiadoLargo(f"El texto no cabe en un QR de los que dibuja Caudal ({capacidad(max(_BLOQUES))} bytes).") base, fijo = _base(version) n = len(base) datos = _codewords(crudo, version) i = 0 derecha = n - 1 while derecha >= 1: # zigzag de dos columnas, de abajo arriba y vuelta if derecha == 6: derecha = 5 for vert in range(n): for j in (0, 1): x = derecha - j y = n - 1 - vert if (derecha + 1) & 2 == 0 else vert if not fijo[y][x] and i < len(datos) * 8: base[y][x] = bool(datos[i >> 3] >> (7 - (i & 7)) & 1) i += 1 derecha -= 2 mejor = None for k in (range(8) if mascara is None else (mascara,)): m = [[base[y][x] ^ (not fijo[y][x] and _MASCARAS[k](x, y)) for x in range(n)] for y in range(n)] _formato(m, k) p = _penalizacion(m) if mejor is None or p < mejor[0]: mejor = (p, m) return mejor[1] def filas(texto: str | bytes) -> list[str]: """Lo mismo como texto, una cadena de «1» y «0» por fila: lo que se manda a la página.""" return ["".join("1" if v else "0" for v in fila) for fila in matriz(texto)]