With the Text tool, Klammertext's codes give letters a keyboard lacks: ^o" is ö, ^s^ ß, ^a1 ā, ^130ED^ a code point of 4 to 6 digits, ^9053 53EF^ a sequence, ^^ a caret, replaced as each code is completed. The public site and relay moved to app.kokkoja.com; the old name redirects. The Markdown documentation is withdrawn from the release while it is converted to Klammertext; the README's links to it are stale until then. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
86 lines
3.4 KiB
Python
86 lines
3.4 KiB
Python
"""The client's QR encoder against Nayuki's reference encoder (qrcodegen).
|
|
|
|
Every version from 1 to 40 at every level, and the invitation link's
|
|
shape, must give the same modules, mask choice included. Needs node
|
|
(the type checker's) and qrcodegen (tests/requirements.txt). Two other
|
|
encoders were tried as the reference: python-qrcode agrees with ours at
|
|
a given mask but chooses masks by its own reading of the penalty rules,
|
|
and segno's modules differ even at the same mask.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import unittest
|
|
|
|
from qrcodegen import QrCode, QrSegment
|
|
|
|
CLIENT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "client", "js")
|
|
LEVELS = {"L": QrCode.Ecc.LOW, "M": QrCode.Ecc.MEDIUM, "Q": QrCode.Ecc.QUARTILE, "H": QrCode.Ecc.HIGH}
|
|
|
|
HARNESS = """
|
|
import { qrModules } from %s;
|
|
const texts = JSON.parse(await new Promise((resolve) => {
|
|
let data = '';
|
|
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
process.stdin.on('end', () => resolve(data));
|
|
}));
|
|
console.log(JSON.stringify(texts.map(([text, level]) =>
|
|
qrModules(text, level).map((row) => row.map((dark) => dark ? 1 : 0)))));
|
|
"""
|
|
|
|
|
|
def encode_in_client(texts):
|
|
"""[(text, level)] -> list of module matrices (lists of 0/1 rows)."""
|
|
harness = HARNESS % json.dumps(os.path.join(CLIENT, "qr_code.js"))
|
|
result = subprocess.run(["node", "--input-type=module", "-e", harness],
|
|
input=json.dumps(texts), capture_output=True, text=True, check=True)
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
def reference(data, level):
|
|
"""Nayuki's code for those bytes in byte mode: (version, mask, matrix)."""
|
|
code = QrCode.encode_segments([QrSegment.make_bytes(data)], LEVELS[level], boostecl=False)
|
|
size = code.get_size()
|
|
matrix = [[1 if code.get_module(x, y) else 0 for x in range(size)] for y in range(size)]
|
|
return code.get_version(), code.get_mask(), matrix
|
|
|
|
|
|
def longest_text(version, level):
|
|
"""The longest text of that version at that level in byte mode."""
|
|
bits = QrCode._get_num_data_codewords(version, LEVELS[level]) * 8
|
|
length = (bits - 4 - (8 if version < 10 else 16)) // 8
|
|
assert reference(b"x" * length, level)[0] == version
|
|
return "".join(chr(33 + (i * 7) % 90) for i in range(length))
|
|
|
|
|
|
class QrCodeTest(unittest.TestCase):
|
|
|
|
def check(self, texts):
|
|
matrices = encode_in_client(texts)
|
|
for (text, level), matrix in zip(texts, matrices):
|
|
version, mask, expected = reference(text.encode("utf-8"), level)
|
|
with self.subTest(level=level, length=len(text.encode("utf-8")), version=version, mask=mask):
|
|
self.assertEqual(matrix, expected)
|
|
|
|
def test_invitation_link(self):
|
|
link = ("https://app.kokkoja.com/#join=wss://app.kokkoja.com/relay%23"
|
|
"AAAAAAAAAAAAAAAAAAAAAA.BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
|
|
self.check([(link, "M"), ("", "M"), ("Kokkoja", "M"), ("Mahendra & Artha", "L"),
|
|
("Übung 1: äöü", "M")])
|
|
|
|
def test_every_version(self):
|
|
texts = []
|
|
for level in "LMQH":
|
|
for version in range(1, 41):
|
|
# The longest text of that version, so that the block
|
|
# structure is filled, and a short one with much padding.
|
|
full = longest_text(version, level)
|
|
texts.append((full, level))
|
|
texts.append((full[: len(full) // 2 + 1], level))
|
|
self.check(texts)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|