Kokkoja as of 2026-10-10: the teacher's requests, the Session window, notices, the QR code, the license
This commit is contained in:
1 parent
95c80d04c9
commit
1d087af633
32 files changed
+4917
-890
No files matched your search
@@ -0,0 +1,85 @@
|
||||
"""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://kokkoja.andykopra.com/#join=wss://kokkoja.andykopra.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()
|
||||
Reference in new issue
Block a user