> */ private array $modules = []; /** @var list> */ private array $isFunction = []; private function __construct(string $text) { $bytes = array_values(unpack('C*', $text) ?: []); $this->version = $this->chooseVersion(count($bytes)); $this->size = $this->version * 4 + 17; $row = array_fill(0, $this->size, false); $this->modules = array_fill(0, $this->size, $row); $this->isFunction = array_fill(0, $this->size, $row); $this->drawFunctionPatterns(); $this->drawCodewords($this->addEccAndInterleave($this->dataCodewords($bytes))); $this->applyBestMask(); } public static function svg(string $text, int $scale = 8, int $quietZone = 4): string { if ($text === '') { throw new InvalidArgumentException('二维码内容不能为空。'); } $qr = new self($text); $dimension = $qr->size + $quietZone * 2; $path = ''; for ($y = 0; $y < $qr->size; $y++) { for ($x = 0; $x < $qr->size; $x++) { if ($qr->modules[$y][$x]) { $path .= sprintf('M%d %dh1v1h-1z', $x + $quietZone, $y + $quietZone); } } } $pixels = $dimension * $scale; return sprintf( '' . '', $dimension, $pixels, $path, ); } /** * @return list> 供测试校验的模块矩阵(true 为黑)。 */ public static function matrix(string $text): array { return (new self($text))->modules; } private function chooseVersion(int $byteCount): int { for ($version = 1; $version <= 40; $version++) { $capacityBits = $this->numDataCodewords($version) * 8; $usedBits = 4 + ($version < 10 ? 8 : 16) + $byteCount * 8; if ($usedBits <= $capacityBits) { return $version; } } throw new InvalidArgumentException('二维码内容过长。'); } /** * @param list $bytes * @return list */ private function dataCodewords(array $bytes): array { $bits = []; $append = static function (int $value, int $length) use (&$bits): void { for ($i = $length - 1; $i >= 0; $i--) { $bits[] = ($value >> $i) & 1; } }; $append(0b0100, 4); $append(count($bytes), $this->version < 10 ? 8 : 16); foreach ($bytes as $byte) { $append($byte, 8); } $capacityBits = $this->numDataCodewords($this->version) * 8; $append(0, min(4, $capacityBits - count($bits))); $append(0, (8 - count($bits) % 8) % 8); $codewords = []; foreach (array_chunk($bits, 8) as $chunk) { $value = 0; foreach ($chunk as $bit) { $value = ($value << 1) | $bit; } $codewords[] = $value; } for ($pad = 0xEC; count($codewords) < $capacityBits / 8; $pad ^= 0xEC ^ 0x11) { $codewords[] = $pad; } return $codewords; } /** * @param list $data * @return list */ private function addEccAndInterleave(array $data): array { $numBlocks = self::NUM_ERROR_CORRECTION_BLOCKS[$this->version]; $blockEccLength = self::ECC_CODEWORDS_PER_BLOCK[$this->version]; $rawCodewords = intdiv($this->numRawDataModules($this->version), 8); $numShortBlocks = $numBlocks - $rawCodewords % $numBlocks; $shortBlockLength = intdiv($rawCodewords, $numBlocks); $divisor = $this->reedSolomonDivisor($blockEccLength); $blocks = []; $offset = 0; for ($i = 0; $i < $numBlocks; $i++) { $length = $shortBlockLength - $blockEccLength + ($i < $numShortBlocks ? 0 : 1); $block = array_slice($data, $offset, $length); $offset += $length; $ecc = $this->reedSolomonRemainder($block, $divisor); if ($i < $numShortBlocks) { $block[] = 0; } $blocks[] = array_merge($block, $ecc); } $result = []; $blockLength = count($blocks[0]); for ($i = 0; $i < $blockLength; $i++) { foreach ($blocks as $j => $block) { if ($i !== $shortBlockLength - $blockEccLength || $j >= $numShortBlocks) { $result[] = $block[$i]; } } } return $result; } private function drawFunctionPatterns(): void { for ($i = 0; $i < $this->size; $i++) { $this->setFunction(6, $i, $i % 2 === 0); $this->setFunction($i, 6, $i % 2 === 0); } $this->drawFinder(3, 3); $this->drawFinder($this->size - 4, 3); $this->drawFinder(3, $this->size - 4); $positions = $this->alignmentPositions(); $count = count($positions); foreach ($positions as $i => $x) { foreach ($positions as $j => $y) { if (($i === 0 && $j === 0) || ($i === 0 && $j === $count - 1) || ($i === $count - 1 && $j === 0)) { continue; } for ($dy = -2; $dy <= 2; $dy++) { for ($dx = -2; $dx <= 2; $dx++) { $this->setFunction($x + $dx, $y + $dy, max(abs($dx), abs($dy)) !== 1); } } } } $this->drawFormatBits(0); $this->drawVersion(); } private function drawFinder(int $x, int $y): void { for ($dy = -4; $dy <= 4; $dy++) { for ($dx = -4; $dx <= 4; $dx++) { $xx = $x + $dx; $yy = $y + $dy; if ($xx >= 0 && $xx < $this->size && $yy >= 0 && $yy < $this->size) { $distance = max(abs($dx), abs($dy)); $this->setFunction($xx, $yy, $distance !== 2 && $distance !== 4); } } } } private function drawFormatBits(int $mask): void { $data = (self::FORMAT_BITS_M << 3) | $mask; $remainder = $data; for ($i = 0; $i < 10; $i++) { $remainder = ($remainder << 1) ^ (($remainder >> 9) * 0x537); } $bits = (($data << 10) | $remainder) ^ 0x5412; $bit = static fn (int $index): bool => (($bits >> $index) & 1) !== 0; for ($i = 0; $i <= 5; $i++) { $this->setFunction(8, $i, $bit($i)); } $this->setFunction(8, 7, $bit(6)); $this->setFunction(8, 8, $bit(7)); $this->setFunction(7, 8, $bit(8)); for ($i = 9; $i < 15; $i++) { $this->setFunction(14 - $i, 8, $bit($i)); } for ($i = 0; $i < 8; $i++) { $this->setFunction($this->size - 1 - $i, 8, $bit($i)); } for ($i = 8; $i < 15; $i++) { $this->setFunction(8, $this->size - 15 + $i, $bit($i)); } $this->setFunction(8, $this->size - 8, true); } private function drawVersion(): void { if ($this->version < 7) { return; } $remainder = $this->version; for ($i = 0; $i < 12; $i++) { $remainder = ($remainder << 1) ^ (($remainder >> 11) * 0x1F25); } $bits = ($this->version << 12) | $remainder; for ($i = 0; $i < 18; $i++) { $dark = (($bits >> $i) & 1) !== 0; $a = $this->size - 11 + $i % 3; $b = intdiv($i, 3); $this->setFunction($a, $b, $dark); $this->setFunction($b, $a, $dark); } } /** * @param list $codewords */ private function drawCodewords(array $codewords): void { $totalBits = count($codewords) * 8; $index = 0; for ($right = $this->size - 1; $right >= 1; $right -= 2) { if ($right === 6) { $right = 5; } for ($vertical = 0; $vertical < $this->size; $vertical++) { for ($j = 0; $j < 2; $j++) { $x = $right - $j; $upward = (($right + 1) & 2) === 0; $y = $upward ? $this->size - 1 - $vertical : $vertical; if (! $this->isFunction[$y][$x] && $index < $totalBits) { $this->modules[$y][$x] = (($codewords[$index >> 3] >> (7 - ($index & 7))) & 1) !== 0; $index++; } } } } } private function applyBestMask(): void { $bestMask = 0; $bestPenalty = PHP_INT_MAX; for ($mask = 0; $mask < 8; $mask++) { $this->applyMask($mask); $this->drawFormatBits($mask); $penalty = $this->penaltyScore(); if ($penalty < $bestPenalty) { $bestPenalty = $penalty; $bestMask = $mask; } $this->applyMask($mask); } $this->applyMask($bestMask); $this->drawFormatBits($bestMask); } private function applyMask(int $mask): void { for ($y = 0; $y < $this->size; $y++) { for ($x = 0; $x < $this->size; $x++) { if ($this->isFunction[$y][$x]) { continue; } $invert = match ($mask) { 0 => ($x + $y) % 2 === 0, 1 => $y % 2 === 0, 2 => $x % 3 === 0, 3 => ($x + $y) % 3 === 0, 4 => (intdiv($x, 3) + intdiv($y, 2)) % 2 === 0, 5 => $x * $y % 2 + $x * $y % 3 === 0, 6 => ($x * $y % 2 + $x * $y % 3) % 2 === 0, default => (($x + $y) % 2 + $x * $y % 3) % 2 === 0, }; if ($invert) { $this->modules[$y][$x] = ! $this->modules[$y][$x]; } } } } private function penaltyScore(): int { $size = $this->size; $penalty = 0; $dark = 0; $lineScore = function (callable $cell) use ($size): int { $score = 0; for ($line = 0; $line < $size; $line++) { $run = 1; $sequence = ''; for ($i = 0; $i < $size; $i++) { $current = $cell($line, $i); $sequence .= $current ? '1' : '0'; if ($i > 0 && $current === $cell($line, $i - 1)) { $run++; } else { if ($run >= 5) { $score += $run - 2; } $run = 1; } } if ($run >= 5) { $score += $run - 2; } $padded = '0000' . $sequence . '0000'; $score += 40 * (substr_count($padded, '00001011101') + substr_count($padded, '10111010000')); } return $score; }; $penalty += $lineScore(fn (int $line, int $i): bool => $this->modules[$line][$i]); $penalty += $lineScore(fn (int $line, int $i): bool => $this->modules[$i][$line]); for ($y = 0; $y < $size; $y++) { for ($x = 0; $x < $size; $x++) { if ($this->modules[$y][$x]) { $dark++; } if ($x < $size - 1 && $y < $size - 1) { $color = $this->modules[$y][$x]; if ($color === $this->modules[$y][$x + 1] && $color === $this->modules[$y + 1][$x] && $color === $this->modules[$y + 1][$x + 1]) { $penalty += 3; } } } } $total = $size * $size; $k = intdiv(abs($dark * 20 - $total * 10) + $total - 1, $total) - 1; return $penalty + max(0, $k) * 10; } /** * @return list */ private function alignmentPositions(): array { if ($this->version === 1) { return []; } $count = intdiv($this->version, 7) + 2; $step = $this->version === 32 ? 26 : (int) ceil(($this->version * 4 + 4) / ($count * 2 - 2)) * 2; $positions = [6]; for ($position = $this->size - 7; count($positions) < $count; $position -= $step) { array_splice($positions, 1, 0, [$position]); } return $positions; } private function setFunction(int $x, int $y, bool $dark): void { $this->modules[$y][$x] = $dark; $this->isFunction[$y][$x] = true; } private function numRawDataModules(int $version): int { $result = (16 * $version + 128) * $version + 64; if ($version >= 2) { $alignmentCount = intdiv($version, 7) + 2; $result -= (25 * $alignmentCount - 10) * $alignmentCount - 55; if ($version >= 7) { $result -= 36; } } return $result; } private function numDataCodewords(int $version): int { return intdiv($this->numRawDataModules($version), 8) - self::ECC_CODEWORDS_PER_BLOCK[$version] * self::NUM_ERROR_CORRECTION_BLOCKS[$version]; } /** * @return list */ private function reedSolomonDivisor(int $degree): array { $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] = $this->gfMultiply($result[$j], $root); if ($j + 1 < $degree) { $result[$j] ^= $result[$j + 1]; } } $root = $this->gfMultiply($root, 0x02); } return $result; } /** * @param list $data * @param list $divisor * @return list */ private function reedSolomonRemainder(array $data, array $divisor): array { $result = array_fill(0, count($divisor), 0); foreach ($data as $byte) { $factor = $byte ^ array_shift($result); $result[] = 0; foreach ($divisor as $i => $coefficient) { $result[$i] ^= $this->gfMultiply($coefficient, $factor); } } return $result; } private function gfMultiply(int $x, int $y): int { $z = 0; for ($i = 7; $i >= 0; $i--) { $z = ($z << 1) ^ (($z >> 7) * 0x11D); $z ^= (($y >> $i) & 1) * $x; } return $z & 0xFF; } }