'\\\\', ';' => '\\;', ',' => '\\,', ':' => '\\:', '"' => '\\"', ]); } function qr_entry_to_payload(string $key, mixed $entry): ?array { if (is_string($entry)) { return [ 'payload' => $entry, 'title' => $key, 'subtitle' => 'Scan this QR code.', 'link' => filter_var($entry, FILTER_VALIDATE_URL) ? $entry : null, ]; } if (!is_array($entry)) { return null; } $type = strtolower((string)($entry['type'] ?? '')); $label = (string)($entry['label'] ?? $key); if ($type === 'wifi' || isset($entry['ssid'])) { $ssid = (string)($entry['ssid'] ?? ''); if ($ssid === '') { return null; } $security = strtoupper((string)($entry['security'] ?? $entry['encryption'] ?? 'WPA')); if (in_array($security, ['', 'NONE', 'OPEN', 'NOPASS'], true)) { $security = 'nopass'; } $hidden = !empty($entry['hidden']) ? 'true' : 'false'; $payload = 'WIFI:T:' . wifi_escape($security) . ';S:' . wifi_escape($ssid) . ';'; if ($security !== 'nopass') { $payload .= 'P:' . wifi_escape((string)($entry['password'] ?? '')) . ';'; } $payload .= 'H:' . $hidden . ';;'; return [ 'payload' => $payload, 'title' => $label, 'subtitle' => 'Scan to connect to ' . $ssid, 'link' => null, ]; } $payload = $entry['url'] ?? $entry['text'] ?? $entry['payload'] ?? null; if (!is_string($payload) || $payload === '') { return null; } return [ 'payload' => $payload, 'title' => $label, 'subtitle' => filter_var($payload, FILTER_VALIDATE_URL) ? $payload : 'Scan this QR code.', 'link' => filter_var($payload, FILTER_VALIDATE_URL) ? $payload : null, ]; } final class QrCode { private const ECC_CODEWORDS_PER_BLOCK = [1 => 10, 2 => 16, 3 => 26, 4 => 18, 5 => 24, 6 => 16, 7 => 18, 8 => 22, 9 => 22, 10 => 26]; private const DATA_CODEWORDS_PER_BLOCK = [ 1 => [16], 2 => [28], 3 => [44], 4 => [32, 32], 5 => [43, 43], 6 => [27, 27, 27, 27], 7 => [31, 31, 31, 31], 8 => [38, 38, 39, 39], 9 => [36, 36, 36, 37, 37], 10 => [43, 43, 43, 43, 44], ]; private const ALIGNMENT_POSITIONS = [ 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], ]; /** @var array */ private static array $gfExp = []; /** @var array */ private static array $gfLog = []; private int $version; private int $size; /** @var array> */ private array $modules; /** @var array> */ private array $isFunction; private function __construct(int $version) { $this->version = $version; $this->size = 21 + 4 * ($version - 1); $row = array_fill(0, $this->size, false); $this->modules = array_fill(0, $this->size, $row); $this->isFunction = array_fill(0, $this->size, $row); } public static function fromText(string $text): self { $bytes = array_values(unpack('C*', $text) ?: []); $version = self::chooseVersion(count($bytes)); $qr = new self($version); $qr->drawFunctionPatterns(); $dataCodewords = $qr->makeDataCodewords($bytes); $allCodewords = self::addEccAndInterleave($version, $dataCodewords); $qr->drawCodewords($allCodewords, 0); $qr->drawFormatBits(0); return $qr; } public function toSvg(string $centerText = '', int $border = 4): string { $fullSize = $this->size + $border * 2; $svg = ''; $svg .= ''; $svg .= ''; for ($y = 0; $y < $this->size; $y++) { for ($x = 0; $x < $this->size; $x++) { if ($this->modules[$y][$x]) { $svg .= ''; } } } $svg .= ''; if ($centerText !== '') { $safeText = htmlspecialchars($centerText, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $chars = max(1, strlen($centerText)); $badgeWidth = min($this->size * 0.48, max($this->size * 0.25, $chars * 1.15 + 4)); $badgeHeight = max(4.8, min(6.2, $this->size * 0.18)); $x = ($fullSize - $badgeWidth) / 2; $y = ($fullSize - $badgeHeight) / 2; $fontSize = min(3.2, max(2.1, ($badgeWidth - 2) / ($chars * 0.62))); $svg .= ''; $svg .= '' . $safeText . ''; } $svg .= ''; return $svg; } private static function chooseVersion(int $byteLength): int { for ($version = 1; $version <= 10; $version++) { $countBits = $version <= 9 ? 8 : 16; $capacityBits = array_sum(self::DATA_CODEWORDS_PER_BLOCK[$version]) * 8; if (4 + $countBits + $byteLength * 8 <= $capacityBits) { return $version; } } fail(413, 'QR payload is too long.'); } /** @param array $bytes @return array */ private function makeDataCodewords(array $bytes): array { $capacityBits = array_sum(self::DATA_CODEWORDS_PER_BLOCK[$this->version]) * 8; $bits = []; self::appendBits($bits, 0b0100, 4); // Byte mode self::appendBits($bits, count($bytes), $this->version <= 9 ? 8 : 16); foreach ($bytes as $byte) { self::appendBits($bits, $byte, 8); } $terminator = min(4, $capacityBits - count($bits)); for ($i = 0; $i < $terminator; $i++) { $bits[] = 0; } while (count($bits) % 8 !== 0) { $bits[] = 0; } $result = []; for ($i = 0; $i < count($bits); $i += 8) { $byte = 0; for ($j = 0; $j < 8; $j++) { $byte = ($byte << 1) | $bits[$i + $j]; } $result[] = $byte; } $pad = [0xEC, 0x11]; for ($i = 0; count($result) < intdiv($capacityBits, 8); $i++) { $result[] = $pad[$i % 2]; } return $result; } /** @param array $bits */ private static function appendBits(array &$bits, int $value, int $length): void { for ($i = $length - 1; $i >= 0; $i--) { $bits[] = ($value >> $i) & 1; } } /** @param array $data @return array */ private static function addEccAndInterleave(int $version, array $data): array { $blockLengths = self::DATA_CODEWORDS_PER_BLOCK[$version]; $eccLength = self::ECC_CODEWORDS_PER_BLOCK[$version]; $divisor = self::reedSolomonDivisor($eccLength); $blocks = []; $offset = 0; foreach ($blockLengths as $length) { $blockData = array_slice($data, $offset, $length); $offset += $length; $blocks[] = ['data' => $blockData, 'ecc' => self::reedSolomonRemainder($blockData, $divisor)]; } $result = []; $maxData = max($blockLengths); for ($i = 0; $i < $maxData; $i++) { foreach ($blocks as $block) { if ($i < count($block['data'])) { $result[] = $block['data'][$i]; } } } for ($i = 0; $i < $eccLength; $i++) { foreach ($blocks as $block) { $result[] = $block['ecc'][$i]; } } return $result; } /** @return array */ private static function reedSolomonDivisor(int $degree): array { self::initGalois(); $result = array_fill(0, $degree, 0); $result[$degree - 1] = 1; $root = 1; for ($i = 0; $i < $degree; $i++) { for ($j = 0; $j < $degree; $j++) { $result[$j] = self::gfMultiply($result[$j], $root); if ($j + 1 < $degree) { $result[$j] ^= $result[$j + 1]; } } $root = self::gfMultiply($root, 0x02); } return $result; } /** @param array $data @param array $divisor @return array */ private static function reedSolomonRemainder(array $data, array $divisor): array { $result = array_fill(0, count($divisor), 0); foreach ($data as $byte) { $factor = $byte ^ $result[0]; array_shift($result); $result[] = 0; foreach ($divisor as $i => $coefficient) { $result[$i] ^= self::gfMultiply($coefficient, $factor); } } return $result; } private static function initGalois(): void { if (self::$gfExp !== []) { return; } $x = 1; for ($i = 0; $i < 255; $i++) { self::$gfExp[$i] = $x; self::$gfLog[$x] = $i; $x <<= 1; if (($x & 0x100) !== 0) { $x ^= 0x11D; } } for ($i = 255; $i < 512; $i++) { self::$gfExp[$i] = self::$gfExp[$i - 255]; } } private static function gfMultiply(int $x, int $y): int { if ($x === 0 || $y === 0) { return 0; } return self::$gfExp[self::$gfLog[$x] + self::$gfLog[$y]]; } private function drawFunctionPatterns(): void { $this->drawFinderPattern(3, 3); $this->drawFinderPattern($this->size - 4, 3); $this->drawFinderPattern(3, $this->size - 4); for ($i = 0; $i < $this->size; $i++) { if (!$this->isFunction[6][$i]) { $this->setFunctionModule($i, 6, $i % 2 === 0); } if (!$this->isFunction[$i][6]) { $this->setFunctionModule(6, $i, $i % 2 === 0); } } foreach (self::ALIGNMENT_POSITIONS[$this->version] as $x) { foreach (self::ALIGNMENT_POSITIONS[$this->version] as $y) { if (!$this->isFunction[$y][$x]) { $this->drawAlignmentPattern($x, $y); } } } $this->drawFormatBits(0); if ($this->version >= 7) { $this->drawVersionBits(); } } private function drawFinderPattern(int $cx, int $cy): void { for ($dy = -4; $dy <= 4; $dy++) { for ($dx = -4; $dx <= 4; $dx++) { $x = $cx + $dx; $y = $cy + $dy; if ($x < 0 || $x >= $this->size || $y < 0 || $y >= $this->size) { continue; } $dist = max(abs($dx), abs($dy)); $this->setFunctionModule($x, $y, $dist !== 2 && $dist !== 4); } } } private function drawAlignmentPattern(int $cx, int $cy): void { for ($dy = -2; $dy <= 2; $dy++) { for ($dx = -2; $dx <= 2; $dx++) { $this->setFunctionModule($cx + $dx, $cy + $dy, max(abs($dx), abs($dy)) !== 1); } } } private function drawFormatBits(int $mask): void { $data = $mask; // Medium ECC has format bits 00. $rem = $data; for ($i = 0; $i < 10; $i++) { $rem = ($rem << 1) ^ ((($rem >> 9) & 1) * 0x537); } $bits = (($data << 10) | ($rem & 0x3FF)) ^ 0x5412; for ($i = 0; $i <= 5; $i++) { $this->setFunctionModule(8, $i, (($bits >> $i) & 1) !== 0); } $this->setFunctionModule(8, 7, (($bits >> 6) & 1) !== 0); $this->setFunctionModule(8, 8, (($bits >> 7) & 1) !== 0); $this->setFunctionModule(7, 8, (($bits >> 8) & 1) !== 0); for ($i = 9; $i < 15; $i++) { $this->setFunctionModule(14 - $i, 8, (($bits >> $i) & 1) !== 0); } for ($i = 0; $i < 8; $i++) { $this->setFunctionModule($this->size - 1 - $i, 8, (($bits >> $i) & 1) !== 0); } for ($i = 8; $i < 15; $i++) { $this->setFunctionModule(8, $this->size - 15 + $i, (($bits >> $i) & 1) !== 0); } $this->setFunctionModule(8, $this->size - 8, true); } private function drawVersionBits(): void { $rem = $this->version; for ($i = 0; $i < 12; $i++) { $rem = ($rem << 1) ^ ((($rem >> 11) & 1) * 0x1F25); } $bits = ($this->version << 12) | ($rem & 0xFFF); for ($i = 0; $i < 18; $i++) { $bit = (($bits >> $i) & 1) !== 0; $a = $this->size - 11 + ($i % 3); $b = intdiv($i, 3); $this->setFunctionModule($a, $b, $bit); $this->setFunctionModule($b, $a, $bit); } } /** @param array $codewords */ private function drawCodewords(array $codewords, int $mask): void { $bits = []; foreach ($codewords as $byte) { self::appendBits($bits, $byte, 8); } $i = 0; $upward = true; for ($right = $this->size - 1; $right >= 1; $right -= 2) { if ($right === 6) { $right = 5; } for ($vert = 0; $vert < $this->size; $vert++) { $y = $upward ? $this->size - 1 - $vert : $vert; for ($j = 0; $j < 2; $j++) { $x = $right - $j; if ($this->isFunction[$y][$x]) { continue; } $bit = $i < count($bits) && $bits[$i] === 1; $i++; if ($this->maskBit($mask, $x, $y)) { $bit = !$bit; } $this->modules[$y][$x] = $bit; } } $upward = !$upward; } } private function maskBit(int $mask, int $x, int $y): bool { return match ($mask) { 0 => (($x + $y) % 2) === 0, default => false, }; } private function setFunctionModule(int $x, int $y, bool $dark): void { $this->modules[$y][$x] = $dark; $this->isFunction[$y][$x] = true; } } $what = (string)($_GET['what'] ?? ''); if ($what === '') { fail(400, 'Missing required query parameter: what'); } if (!preg_match('/^[A-Za-z0-9_.-]+$/', $what)) { not_found(); } if (!is_readable(DATA_FILE)) { not_found(); } $config = json_decode((string)file_get_contents(DATA_FILE), true); if (!is_array($config)) { fail(500, 'Invalid what.json'); } if (!array_key_exists($what, $config)) { not_found(); } $entry = qr_entry_to_payload($what, $config[$what]); if ($entry === null) { not_found(); } $qr = QrCode::fromText($entry['payload']); $title = (string)$entry['title']; $subtitle = (string)$entry['subtitle']; $link = is_string($entry['link']) ? $entry['link'] : null; header('Content-Type: text/html; charset=utf-8'); ?> <?= h($title) ?> QR

toSvg($what) ?>