| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Lib; |
| 39 | |
| 40 | use Std\Lib\Image\Bitmap; |
| 41 | |
| 42 | /** |
| 43 | * QRコード(ISO/IEC 18004) |
| 44 | * |
| 45 | * 型番1~40、誤り訂正L・M・Q・H、数字・英数字・バイト(UTF-8)のモード、8つのマスクから減点の一番少ない物。 |
| 46 | * 画像にはbitmap()(PNG等はImage::encodeで)。 |
| 47 | */ |
| 48 | final class Qr { |
| 49 | /** 誤り訂正のレベル => [形式情報のビット, 1ブロックの誤り訂正の数(型番1~40), ブロックの数] */ |
| 50 | private const EC = [ |
| 51 | 'L' => [1, [7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], |
| 52 | [1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25]], |
| 53 | 'M' => [0, [10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28], |
| 54 | [1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49]], |
| 55 | 'Q' => [3, [13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], |
| 56 | [1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68]], |
| 57 | 'H' => [2, [17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], |
| 58 | [1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]], |
| 59 | ]; |
| 60 | private const ALNUM = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; |
| 61 | |
| 62 | public int $version; |
| 63 | public int $size; |
| 64 | public int $mask; |
| 65 | public string $level; |
| 66 | /** 行毎のモジュール(trueが黒) */ |
| 67 | public array $modules = []; |
| 68 | /** 機能パターン(データを置かない所) */ |
| 69 | private array $function = []; |
| 70 | |
| 71 | /** |
| 72 | * @param string $text 中身(数字だけ、英大文字等だけなら小さいモードを使う) |
| 73 | * @param string $level 誤り訂正(L:7%、M:15%、Q:25%、H:30%) |
| 74 | * @param int $minVersion 最小の型番 |
| 75 | * @throws \InvalidArgumentException 入り切らない場合 |
| 76 | */ |
| 77 | public function __construct(string $text, string $level = 'M', int $minVersion = 1) { |
| 78 | if (!isset(self::EC[$level])) throw new \InvalidArgumentException('誤り訂正のレベルはL・M・Q・Hです。'); |
| 79 | $this->level = $level; |
| 80 | [$mode, $count] = match (true) { |
| 81 | preg_match('/^\d*$/', $text) === 1 => [1, strlen($text)], |
| 82 | strspn($text, self::ALNUM) === strlen($text) => [2, strlen($text)], |
| 83 | default => [4, strlen($text)], |
| 84 | }; |
| 85 | for ($v = max(1, $minVersion); $v <= 40; $v++) { |
| 86 | $bits = self::segment($text, $mode, $count, $v); |
| 87 | if ($bits !== null && strlen($bits) <= self::dataCodewords($v, $level) * 8) break; |
| 88 | } |
| 89 | if ($v > 40) throw new \InvalidArgumentException('QRコードに入り切りません。'); |
| 90 | $this->version = $v; |
| 91 | $this->size = $v * 4 + 17; |
| 92 | |
| 93 | // 終端(4ビットまで)、8ビットに揃え、残りは0xEC・0x11の繰り返し |
| 94 | $cap = self::dataCodewords($v, $level) * 8; |
| 95 | $bits .= str_repeat('0', min(4, $cap - strlen($bits))); |
| 96 | $bits .= str_repeat('0', (8 - strlen($bits) % 8) % 8); |
| 97 | for ($pad = 0xec; strlen($bits) < $cap; $pad ^= 0xec ^ 0x11) $bits .= sprintf('%08b', $pad); |
| 98 | $data = array_map('bindec', str_split($bits, 8)); |
| 99 | |
| 100 | $this->modules = array_fill(0, $this->size, array_fill(0, $this->size, false)); |
| 101 | $this->function = $this->modules; |
| 102 | $this->drawFunctions(); |
| 103 | $this->drawCodewords(self::interleave($data, $v, $level)); |
| 104 | |
| 105 | // 減点の一番少ないマスク |
| 106 | $best = null; |
| 107 | for ($m = 0; $m < 8; $m++) { |
| 108 | $this->applyMask($m); |
| 109 | $this->drawFormat($m); |
| 110 | $p = $this->penalty(); |
| 111 | if ($best === null || $p < $best[0]) $best = [$p, $m]; |
| 112 | $this->applyMask($m); |
| 113 | } |
| 114 | $this->mask = $best[1]; |
| 115 | $this->applyMask($this->mask); |
| 116 | $this->drawFormat($this->mask); |
| 117 | unset($this->function); |
| 118 | } |
| 119 | |
| 120 | /** |
| 121 | * 画像にする |
| 122 | * |
| 123 | * @param int $scale 1モジュールの画素数 |
| 124 | * @param int $margin 周りの白(モジュール、規格では4以上) |
| 125 | */ |
| 126 | public function bitmap(int $scale = 4, int $margin = 4): Bitmap { |
| 127 | $w = ($this->size + 2 * $margin) * $scale; |
| 128 | $white = str_repeat("\xff\xff\xff\xff", $w); |
| 129 | $data = str_repeat($white, $margin * $scale); |
| 130 | foreach ($this->modules as $row) { |
| 131 | $line = str_repeat("\xff\xff\xff\xff", $margin * $scale); |
| 132 | foreach ($row as $dark) $line .= str_repeat($dark ? "\0\0\0\xff" : "\xff\xff\xff\xff", $scale); |
| 133 | $line .= str_repeat("\xff\xff\xff\xff", $margin * $scale); |
| 134 | $data .= str_repeat($line, $scale); |
| 135 | } |
| 136 | $data .= str_repeat($white, $margin * $scale); |
| 137 | return new Bitmap($w, $w, $data); |
| 138 | } |
| 139 | |
| 140 | /** |
| 141 | * モード、文字数、データのビット列(文字数が型番の欄に入らなければnull) |
| 142 | */ |
| 143 | private static function segment(string $text, int $mode, int $count, int $v): ?string { |
| 144 | $cbits = match ($mode) { |
| 145 | 1 => $v < 10 ? 10 : ($v < 27 ? 12 : 14), |
| 146 | 2 => $v < 10 ? 9 : ($v < 27 ? 11 : 13), |
| 147 | default => $v < 10 ? 8 : 16, |
| 148 | }; |
| 149 | if ($count >= 1 << $cbits) return null; |
| 150 | $bits = sprintf('%04b', $mode).str_pad(decbin($count), $cbits, '0', STR_PAD_LEFT); |
| 151 | if ($mode === 1) { |
| 152 | foreach (str_split($text, 3) as $g) if ($g !== '') $bits .= str_pad(decbin((int)$g), [1 => 4, 2 => 7, 3 => 10][strlen($g)], '0', STR_PAD_LEFT); |
| 153 | } else if ($mode === 2) { |
| 154 | foreach (str_split($text, 2) as $g) { |
| 155 | if ($g === '') continue; |
| 156 | $bits .= strlen($g) === 2 |
| 157 | ? str_pad(decbin(strpos(self::ALNUM, $g[0]) * 45 + strpos(self::ALNUM, $g[1])), 11, '0', STR_PAD_LEFT) |
| 158 | : str_pad(decbin(strpos(self::ALNUM, $g)), 6, '0', STR_PAD_LEFT); |
| 159 | } |
| 160 | } else { |
| 161 | for ($i = 0; $i < $count; $i++) $bits .= sprintf('%08b', ord($text[$i])); |
| 162 | } |
| 163 | return $bits; |
| 164 | } |
| 165 | |
| 166 | /** データを置けるモジュールの数(機能パターンと形式・型番情報を除く) */ |
| 167 | private static function rawModules(int $v): int { |
| 168 | $n = (16 * $v + 128) * $v + 64; |
| 169 | if ($v >= 2) { |
| 170 | $align = intdiv($v, 7) + 2; |
| 171 | $n -= (25 * $align - 10) * $align - 55; |
| 172 | if ($v >= 7) $n -= 36; |
| 173 | } |
| 174 | return $n; |
| 175 | } |
| 176 | |
| 177 | private static function dataCodewords(int $v, string $level): int { |
| 178 | [, $ecc, $blocks] = self::EC[$level]; |
| 179 | return intdiv(self::rawModules($v), 8) - $ecc[$v - 1] * $blocks[$v - 1]; |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * ブロックに分けて誤り訂正を付け、交互に並べる |
| 184 | */ |
| 185 | private static function interleave(array $data, int $v, string $level): array { |
| 186 | [, $eccs, $nblocks] = self::EC[$level]; |
| 187 | $ecc = $eccs[$v - 1]; |
| 188 | $n = $nblocks[$v - 1]; |
| 189 | $total = intdiv(self::rawModules($v), 8); |
| 190 | $short = $n - $total % $n; |
| 191 | $shortLen = intdiv($total, $n); |
| 192 | $gen = self::generator($ecc); |
| 193 | $blocks = []; |
| 194 | $pos = 0; |
| 195 | for ($i = 0; $i < $n; $i++) { |
| 196 | $len = $shortLen - $ecc + ($i < $short ? 0 : 1); |
| 197 | $d = array_slice($data, $pos, $len); |
| 198 | $pos += $len; |
| 199 | $blocks[] = [$d, self::remainder($d, $gen)]; |
| 200 | } |
| 201 | $out = []; |
| 202 | for ($i = 0; $i <= $shortLen - $ecc; $i++) { |
| 203 | foreach ($blocks as [$d]) if (isset($d[$i])) $out[] = $d[$i]; |
| 204 | } |
| 205 | for ($i = 0; $i < $ecc; $i++) { |
| 206 | foreach ($blocks as [, $e]) $out[] = $e[$i]; |
| 207 | } |
| 208 | return $out; |
| 209 | } |
| 210 | |
| 211 | /** GF(2^8)(x^8 + x^4 + x^3 + x^2 + 1)の掛け算 */ |
| 212 | private static function mul(int $x, int $y): int { |
| 213 | $z = 0; |
| 214 | for ($i = 7; $i >= 0; $i--) { |
| 215 | $z = ($z << 1) ^ (($z >> 7) * 0x11d); |
| 216 | $z ^= (($y >> $i) & 1) * $x; |
| 217 | } |
| 218 | return $z; |
| 219 | } |
| 220 | |
| 221 | /** リード・ソロモンの生成多項式(最高次の係数は省く) */ |
| 222 | private static function generator(int $degree): array { |
| 223 | $g = array_fill(0, $degree, 0); |
| 224 | $g[$degree - 1] = 1; |
| 225 | $root = 1; |
| 226 | for ($i = 0; $i < $degree; $i++) { |
| 227 | for ($j = 0; $j < $degree; $j++) { |
| 228 | $g[$j] = self::mul($g[$j], $root); |
| 229 | if ($j + 1 < $degree) $g[$j] ^= $g[$j + 1]; |
| 230 | } |
| 231 | $root = self::mul($root, 0x02); |
| 232 | } |
| 233 | return $g; |
| 234 | } |
| 235 | |
| 236 | private static function remainder(array $data, array $gen): array { |
| 237 | $r = array_fill(0, count($gen), 0); |
| 238 | foreach ($data as $b) { |
| 239 | $f = $b ^ array_shift($r); |
| 240 | $r[] = 0; |
| 241 | foreach ($gen as $i => $c) $r[$i] ^= self::mul($c, $f); |
| 242 | } |
| 243 | return $r; |
| 244 | } |
| 245 | |
| 246 | private function set(int $x, int $y, bool $dark): void { |
| 247 | $this->modules[$y][$x] = $dark; |
| 248 | $this->function[$y][$x] = true; |
| 249 | } |
| 250 | |
| 251 | /** 位置検出、タイミング、位置合わせのパターン、型番情報(形式情報の場所は先に取っておく) */ |
| 252 | private function drawFunctions(): void { |
| 253 | $s = $this->size; |
| 254 | for ($i = 0; $i < $s; $i++) { |
| 255 | $this->set(6, $i, $i % 2 === 0); |
| 256 | $this->set($i, 6, $i % 2 === 0); |
| 257 | } |
| 258 | foreach ([[3, 3], [$s - 4, 3], [3, $s - 4]] as [$cx, $cy]) { |
| 259 | for ($dy = -4; $dy <= 4; $dy++) { |
| 260 | for ($dx = -4; $dx <= 4; $dx++) { |
| 261 | $x = $cx + $dx; |
| 262 | $y = $cy + $dy; |
| 263 | if ($x < 0 || $y < 0 || $x >= $s || $y >= $s) continue; |
| 264 | $d = max(abs($dx), abs($dy)); |
| 265 | $this->set($x, $y, $d !== 2 && $d !== 4); |
| 266 | } |
| 267 | } |
| 268 | } |
| 269 | // 位置合わせ(位置検出と重なる3つの角以外) |
| 270 | if ($this->version > 1) { |
| 271 | $n = intdiv($this->version, 7) + 2; |
| 272 | $step = intdiv($this->version * 8 + $n * 3 + 5, $n * 4 - 4) * 2; |
| 273 | $pos = [6]; |
| 274 | for ($i = $n - 1; $i >= 1; $i--) $pos[$i] = $s - 7 - ($n - 1 - $i) * $step; |
| 275 | ksort($pos); |
| 276 | foreach ($pos as $i => $x) { |
| 277 | foreach ($pos as $j => $y) { |
| 278 | if (($i === 0 && $j === 0) || ($i === 0 && $j === $n - 1) || ($i === $n - 1 && $j === 0)) continue; |
| 279 | for ($dy = -2; $dy <= 2; $dy++) for ($dx = -2; $dx <= 2; $dx++) $this->set($x + $dx, $y + $dy, max(abs($dx), abs($dy)) !== 1); |
| 280 | } |
| 281 | } |
| 282 | } |
| 283 | $this->drawFormat(0); |
| 284 | if ($this->version >= 7) { |
| 285 | $rem = $this->version; |
| 286 | for ($i = 0; $i < 12; $i++) $rem = ($rem << 1) ^ (($rem >> 11) * 0x1f25); |
| 287 | $bits = ($this->version << 12) | $rem; |
| 288 | for ($i = 0; $i < 18; $i++) { |
| 289 | $dark = (($bits >> $i) & 1) === 1; |
| 290 | $a = $s - 11 + $i % 3; |
| 291 | $b = intdiv($i, 3); |
| 292 | $this->set($a, $b, $dark); |
| 293 | $this->set($b, $a, $dark); |
| 294 | } |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | /** 形式情報(誤り訂正のレベルとマスク)の2つの写し */ |
| 299 | private function drawFormat(int $mask): void { |
| 300 | $data = (self::EC[$this->level][0] << 3) | $mask; |
| 301 | $rem = $data; |
| 302 | for ($i = 0; $i < 10; $i++) $rem = ($rem << 1) ^ (($rem >> 9) * 0x537); |
| 303 | $bits = (($data << 10) | $rem) ^ 0x5412; |
| 304 | $bit = fn(int $i): bool => (($bits >> $i) & 1) === 1; |
| 305 | $s = $this->size; |
| 306 | for ($i = 0; $i <= 5; $i++) $this->set(8, $i, $bit($i)); |
| 307 | $this->set(8, 7, $bit(6)); |
| 308 | $this->set(8, 8, $bit(7)); |
| 309 | $this->set(7, 8, $bit(8)); |
| 310 | for ($i = 9; $i < 15; $i++) $this->set(14 - $i, 8, $bit($i)); |
| 311 | for ($i = 0; $i < 8; $i++) $this->set($s - 1 - $i, 8, $bit($i)); |
| 312 | for ($i = 8; $i < 15; $i++) $this->set(8, $s - 15 + $i, $bit($i)); |
| 313 | $this->set(8, $s - 8, true); |
| 314 | } |
| 315 | |
| 316 | /** 右下から2列ずつ、上下にジグザグに置く */ |
| 317 | private function drawCodewords(array $data): void { |
| 318 | $s = $this->size; |
| 319 | $total = count($data) * 8; |
| 320 | $i = 0; |
| 321 | for ($right = $s - 1; $right >= 1; $right -= 2) { |
| 322 | if ($right === 6) $right = 5; |
| 323 | $up = (($right + 1) & 2) === 0; |
| 324 | for ($vert = 0; $vert < $s; $vert++) { |
| 325 | $y = $up ? $s - 1 - $vert : $vert; |
| 326 | for ($j = 0; $j < 2; $j++) { |
| 327 | $x = $right - $j; |
| 328 | if ($this->function[$y][$x]) continue; |
| 329 | // 残りのモジュールは0(白) |
| 330 | if ($i < $total) $this->modules[$y][$x] = (($data[$i >> 3] >> (7 - ($i & 7))) & 1) === 1; |
| 331 | $i++; |
| 332 | } |
| 333 | } |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | /** マスク(2回掛けると元に戻る) */ |
| 338 | private function applyMask(int $m): void { |
| 339 | for ($y = 0; $y < $this->size; $y++) { |
| 340 | for ($x = 0; $x < $this->size; $x++) { |
| 341 | if ($this->function[$y][$x]) continue; |
| 342 | $flip = match ($m) { |
| 343 | 0 => ($x + $y) % 2 === 0, |
| 344 | 1 => $y % 2 === 0, |
| 345 | 2 => $x % 3 === 0, |
| 346 | 3 => ($x + $y) % 3 === 0, |
| 347 | 4 => (intdiv($x, 3) + intdiv($y, 2)) % 2 === 0, |
| 348 | 5 => $x * $y % 2 + $x * $y % 3 === 0, |
| 349 | 6 => ($x * $y % 2 + $x * $y % 3) % 2 === 0, |
| 350 | default => (($x + $y) % 2 + $x * $y % 3) % 2 === 0, |
| 351 | }; |
| 352 | if ($flip) $this->modules[$y][$x] = !$this->modules[$y][$x]; |
| 353 | } |
| 354 | } |
| 355 | } |
| 356 | |
| 357 | /** |
| 358 | * 減点(同じ色の5つ以上の並び、2x2の塊、位置検出に似た模様、黒の割合) |
| 359 | */ |
| 360 | private function penalty(): int { |
| 361 | $s = $this->size; |
| 362 | $p = 0; |
| 363 | $lines = []; |
| 364 | for ($y = 0; $y < $s; $y++) { |
| 365 | $lines[] = implode('', array_map(fn($d) => $d ? '1' : '0', $this->modules[$y])); |
| 366 | } |
| 367 | $cols = []; |
| 368 | for ($x = 0; $x < $s; $x++) { |
| 369 | $c = ''; |
| 370 | for ($y = 0; $y < $s; $y++) $c .= $lines[$y][$x]; |
| 371 | $cols[] = $c; |
| 372 | } |
| 373 | $dark = 0; |
| 374 | foreach ([...$lines, ...$cols] as $k => $line) { |
| 375 | if ($k < $s) $dark += substr_count($line, '1'); |
| 376 | if (preg_match_all('/0{5,}|1{5,}/', $line, $m)) foreach ($m[0] as $run) $p += strlen($run) - 2; |
| 377 | // 1:1:3:1:1の両側に4つの白(外側は白とみなす) |
| 378 | $padded = '0000'.$line.'0000'; |
| 379 | $p += 40 * (preg_match_all('/(?=00001011101)/', $padded) + preg_match_all('/(?=10111010000)/', $padded)); |
| 380 | } |
| 381 | for ($y = 0; $y < $s - 1; $y++) { |
| 382 | for ($x = 0; $x < $s - 1; $x++) { |
| 383 | $c = $lines[$y][$x]; |
| 384 | if ($c === $lines[$y][$x + 1] && $c === $lines[$y + 1][$x] && $c === $lines[$y + 1][$x + 1]) $p += 3; |
| 385 | } |
| 386 | } |
| 387 | $total = $s * $s; |
| 388 | $p += 10 * max(0, intdiv(abs($dark * 20 - $total * 10) + $total - 1, $total) - 1); |
| 389 | return $p; |
| 390 | } |
| 391 | } |