| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* Class QRMatrix |
| 5 |
* |
| 6 |
* @created 15.11.2017 |
| 7 |
* @author Smiley <[email protected]> |
| 8 |
* @copyright 2017 Smiley |
| 9 |
* @license MIT |
| 10 |
*/ |
| 11 |
namespace WCPOS\Vendor\chillerlan\QRCode\Data; |
| 12 |
|
| 13 |
use WCPOS\Vendor\chillerlan\QRCode\Common\BitBuffer; |
| 14 |
use WCPOS\Vendor\chillerlan\QRCode\Common\EccLevel; |
| 15 |
use WCPOS\Vendor\chillerlan\QRCode\Common\MaskPattern; |
| 16 |
use WCPOS\Vendor\chillerlan\QRCode\Common\Version; |
| 17 |
use function array_fill, array_map, array_reverse, count, intdiv; |
| 18 |
/** |
| 19 |
* Holds an array representation of the final QR Code that contains numerical values for later output modifications; |
| 20 |
* maps the ECC coded binary data and applies the mask pattern |
| 21 |
* |
| 22 |
* @see http://www.thonky.com/qr-code-tutorial/format-version-information |
| 23 |
*/ |
| 24 |
class QRMatrix |
| 25 |
{ |
| 26 |
/* |
| 27 |
* special values |
| 28 |
*/ |
| 29 |
/** @var int */ |
| 30 |
public const IS_DARK = 0b100000000000; |
| 31 |
/** @var int */ |
| 32 |
public const M_NULL = 0b0; |
| 33 |
/** @var int */ |
| 34 |
public const M_LOGO = 0b1000000000; |
| 35 |
/** @var int */ |
| 36 |
public const M_LOGO_DARK = 0b101000000000; |
| 37 |
/* |
| 38 |
* light values |
| 39 |
*/ |
| 40 |
/** @var int */ |
| 41 |
public const M_DATA = 0b10; |
| 42 |
/** @var int */ |
| 43 |
public const M_FINDER = 0b100; |
| 44 |
/** @var int */ |
| 45 |
public const M_SEPARATOR = 0b1000; |
| 46 |
/** @var int */ |
| 47 |
public const M_ALIGNMENT = 0b10000; |
| 48 |
/** @var int */ |
| 49 |
public const M_TIMING = 0b100000; |
| 50 |
/** @var int */ |
| 51 |
public const M_FORMAT = 0b1000000; |
| 52 |
/** @var int */ |
| 53 |
public const M_VERSION = 0b10000000; |
| 54 |
/** @var int */ |
| 55 |
public const M_QUIETZONE = 0b100000000; |
| 56 |
/* |
| 57 |
* dark values |
| 58 |
*/ |
| 59 |
/** @var int */ |
| 60 |
public const M_DARKMODULE = 0b100000000001; |
| 61 |
/** @var int */ |
| 62 |
public const M_DATA_DARK = 0b100000000010; |
| 63 |
/** @var int */ |
| 64 |
public const M_FINDER_DARK = 0b100000000100; |
| 65 |
/** @var int */ |
| 66 |
public const M_ALIGNMENT_DARK = 0b100000010000; |
| 67 |
/** @var int */ |
| 68 |
public const M_TIMING_DARK = 0b100000100000; |
| 69 |
/** @var int */ |
| 70 |
public const M_FORMAT_DARK = 0b100001000000; |
| 71 |
/** @var int */ |
| 72 |
public const M_VERSION_DARK = 0b100010000000; |
| 73 |
/** @var int */ |
| 74 |
public const M_FINDER_DOT = 0b110000000000; |
| 75 |
/* |
| 76 |
* values used for reversed reflectance |
| 77 |
*/ |
| 78 |
/** @var int */ |
| 79 |
public const M_DARKMODULE_LIGHT = 0b1; |
| 80 |
/** @var int */ |
| 81 |
public const M_FINDER_DOT_LIGHT = 0b10000000000; |
| 82 |
/** @var int */ |
| 83 |
public const M_SEPARATOR_DARK = 0b100000001000; |
| 84 |
/** @var int */ |
| 85 |
public const M_QUIETZONE_DARK = 0b100100000000; |
| 86 |
/** |
| 87 |
* Map of flag => coord |
| 88 |
* |
| 89 |
* @see \chillerlan\QRCode\Data\QRMatrix::checkNeighbours() |
| 90 |
* |
| 91 |
* @var array |
| 92 |
*/ |
| 93 |
protected const neighbours = [0b1 => [-1, -1], 0b10 => [0, -1], 0b100 => [1, -1], 0b1000 => [1, 0], 0b10000 => [1, 1], 0b100000 => [0, 1], 0b1000000 => [-1, 1], 0b10000000 => [-1, 0]]; |
| 94 |
/** |
| 95 |
* the matrix version - always set in QRMatrix, may be null in BitMatrix |
| 96 |
*/ |
| 97 |
protected ?Version $version = null; |
| 98 |
/** |
| 99 |
* the current ECC level - always set in QRMatrix, may be null in BitMatrix |
| 100 |
*/ |
| 101 |
protected ?EccLevel $eccLevel = null; |
| 102 |
/** |
| 103 |
* the mask pattern that was used in the most recent operation, set via: |
| 104 |
* |
| 105 |
* - QRMatrix::setFormatInfo() |
| 106 |
* - QRMatrix::mask() |
| 107 |
* - BitMatrix::readFormatInformation() |
| 108 |
*/ |
| 109 |
protected ?MaskPattern $maskPattern = null; |
| 110 |
/** |
| 111 |
* the size (side length) of the matrix, including quiet zone (if created) |
| 112 |
*/ |
| 113 |
protected int $moduleCount; |
| 114 |
/** |
| 115 |
* the actual matrix data array |
| 116 |
* |
| 117 |
* @var int[][] |
| 118 |
*/ |
| 119 |
protected array $matrix; |
| 120 |
/** |
| 121 |
* QRMatrix constructor. |
| 122 |
*/ |
| 123 |
public function __construct(Version $version, EccLevel $eccLevel) |
| 124 |
{ |
| 125 |
$this->version = $version; |
| 126 |
$this->eccLevel = $eccLevel; |
| 127 |
$this->moduleCount = $this->version->getDimension(); |
| 128 |
$this->matrix = $this->createMatrix($this->moduleCount, $this::M_NULL); |
| 129 |
} |
| 130 |
/** |
| 131 |
* Creates a 2-dimensional array (square) of the given $size |
| 132 |
*/ |
| 133 |
protected function createMatrix(int $size, int $value) : array |
| 134 |
{ |
| 135 |
return array_fill(0, $size, array_fill(0, $size, $value)); |
| 136 |
} |
| 137 |
/** |
| 138 |
* shortcut to initialize the functional patterns |
| 139 |
*/ |
| 140 |
public function initFunctionalPatterns() : self |
| 141 |
{ |
| 142 |
return $this->setFinderPattern()->setSeparators()->setAlignmentPattern()->setTimingPattern()->setDarkModule()->setVersionNumber()->setFormatInfo(); |
| 143 |
} |
| 144 |
/** |
| 145 |
* Returns the data matrix, returns a pure boolean representation if $boolean is set to true |
| 146 |
* |
| 147 |
* @return int[][]|bool[][] |
| 148 |
*/ |
| 149 |
public function getMatrix(?bool $boolean = null) : array |
| 150 |
{ |
| 151 |
if ($boolean !== \true) { |
| 152 |
return $this->matrix; |
| 153 |
} |
| 154 |
$matrix = $this->matrix; |
| 155 |
foreach ($matrix as &$row) { |
| 156 |
$row = array_map([$this, 'isDark'], $row); |
| 157 |
} |
| 158 |
return $matrix; |
| 159 |
} |
| 160 |
/** |
| 161 |
* @deprecated 5.0.0 use QRMatrix::getMatrix() instead |
| 162 |
* @see \chillerlan\QRCode\Data\QRMatrix::getMatrix() |
| 163 |
* @codeCoverageIgnore |
| 164 |
*/ |
| 165 |
public function matrix(?bool $boolean = null) : array |
| 166 |
{ |
| 167 |
return $this->getMatrix($boolean); |
| 168 |
} |
| 169 |
/** |
| 170 |
* Returns the current version number |
| 171 |
*/ |
| 172 |
public function getVersion() : ?Version |
| 173 |
{ |
| 174 |
return $this->version; |
| 175 |
} |
| 176 |
/** |
| 177 |
* @deprecated 5.0.0 use QRMatrix::getVersion() instead |
| 178 |
* @see \chillerlan\QRCode\Data\QRMatrix::getVersion() |
| 179 |
* @codeCoverageIgnore |
| 180 |
*/ |
| 181 |
public function version() : ?Version |
| 182 |
{ |
| 183 |
return $this->getVersion(); |
| 184 |
} |
| 185 |
/** |
| 186 |
* Returns the current ECC level |
| 187 |
*/ |
| 188 |
public function getEccLevel() : ?EccLevel |
| 189 |
{ |
| 190 |
return $this->eccLevel; |
| 191 |
} |
| 192 |
/** |
| 193 |
* @deprecated 5.0.0 use QRMatrix::getEccLevel() instead |
| 194 |
* @see \chillerlan\QRCode\Data\QRMatrix::getEccLevel() |
| 195 |
* @codeCoverageIgnore |
| 196 |
*/ |
| 197 |
public function eccLevel() : ?EccLevel |
| 198 |
{ |
| 199 |
return $this->getEccLevel(); |
| 200 |
} |
| 201 |
/** |
| 202 |
* Returns the current mask pattern |
| 203 |
*/ |
| 204 |
public function getMaskPattern() : ?MaskPattern |
| 205 |
{ |
| 206 |
return $this->maskPattern; |
| 207 |
} |
| 208 |
/** |
| 209 |
* @deprecated 5.0.0 use QRMatrix::getMaskPattern() instead |
| 210 |
* @see \chillerlan\QRCode\Data\QRMatrix::getMaskPattern() |
| 211 |
* @codeCoverageIgnore |
| 212 |
*/ |
| 213 |
public function maskPattern() : ?MaskPattern |
| 214 |
{ |
| 215 |
return $this->getMaskPattern(); |
| 216 |
} |
| 217 |
/** |
| 218 |
* Returns the absoulute size of the matrix, including quiet zone (after setting it). |
| 219 |
* |
| 220 |
* size = version * 4 + 17 [ + 2 * quietzone size] |
| 221 |
*/ |
| 222 |
public function getSize() : int |
| 223 |
{ |
| 224 |
return $this->moduleCount; |
| 225 |
} |
| 226 |
/** |
| 227 |
* @deprecated 5.0.0 use QRMatrix::getSize() instead |
| 228 |
* @see \chillerlan\QRCode\Data\QRMatrix::getSize() |
| 229 |
* @codeCoverageIgnore |
| 230 |
*/ |
| 231 |
public function size() : int |
| 232 |
{ |
| 233 |
return $this->getSize(); |
| 234 |
} |
| 235 |
/** |
| 236 |
* Returns the value of the module at position [$x, $y] or -1 if the coordinate is outside the matrix |
| 237 |
*/ |
| 238 |
public function get(int $x, int $y) : int |
| 239 |
{ |
| 240 |
if (!isset($this->matrix[$y][$x])) { |
| 241 |
return -1; |
| 242 |
} |
| 243 |
return $this->matrix[$y][$x]; |
| 244 |
} |
| 245 |
/** |
| 246 |
* Sets the $M_TYPE value for the module at position [$x, $y] |
| 247 |
* |
| 248 |
* true => $M_TYPE | 0x800 |
| 249 |
* false => $M_TYPE |
| 250 |
*/ |
| 251 |
public function set(int $x, int $y, bool $value, int $M_TYPE) : self |
| 252 |
{ |
| 253 |
if (isset($this->matrix[$y][$x])) { |
| 254 |
// we don't know whether the input is dark, so we remove the dark bit |
| 255 |
$M_TYPE &= ~$this::IS_DARK; |
| 256 |
if ($value === \true) { |
| 257 |
$M_TYPE |= $this::IS_DARK; |
| 258 |
} |
| 259 |
$this->matrix[$y][$x] = $M_TYPE; |
| 260 |
} |
| 261 |
return $this; |
| 262 |
} |
| 263 |
/** |
| 264 |
* Fills an area of $width * $height, from the given starting point [$startX, $startY] (top left) with $value for $M_TYPE. |
| 265 |
*/ |
| 266 |
public function setArea(int $startX, int $startY, int $width, int $height, bool $value, int $M_TYPE) : self |
| 267 |
{ |
| 268 |
for ($y = $startY; $y < $startY + $height; $y++) { |
| 269 |
for ($x = $startX; $x < $startX + $width; $x++) { |
| 270 |
$this->set($x, $y, $value, $M_TYPE); |
| 271 |
} |
| 272 |
} |
| 273 |
return $this; |
| 274 |
} |
| 275 |
/** |
| 276 |
* Flips the value of the module at ($x, $y) |
| 277 |
*/ |
| 278 |
public function flip(int $x, int $y) : self |
| 279 |
{ |
| 280 |
if (isset($this->matrix[$y][$x])) { |
| 281 |
$this->matrix[$y][$x] ^= $this::IS_DARK; |
| 282 |
} |
| 283 |
return $this; |
| 284 |
} |
| 285 |
/** |
| 286 |
* Checks whether the module at ($x, $y) is of the given $M_TYPE |
| 287 |
* |
| 288 |
* true => $value & $M_TYPE === $M_TYPE |
| 289 |
* |
| 290 |
* Also, returns false if the given coordinates are out of range. |
| 291 |
*/ |
| 292 |
public function checkType(int $x, int $y, int $M_TYPE) : bool |
| 293 |
{ |
| 294 |
if (isset($this->matrix[$y][$x])) { |
| 295 |
return ($this->matrix[$y][$x] & $M_TYPE) === $M_TYPE; |
| 296 |
} |
| 297 |
return \false; |
| 298 |
} |
| 299 |
/** |
| 300 |
* Checks whether the module at ($x, $y) is in the given array of $M_TYPES, |
| 301 |
* returns true if a match is found, otherwise false. |
| 302 |
*/ |
| 303 |
public function checkTypeIn(int $x, int $y, array $M_TYPES) : bool |
| 304 |
{ |
| 305 |
foreach ($M_TYPES as $type) { |
| 306 |
if ($this->checkType($x, $y, $type)) { |
| 307 |
return \true; |
| 308 |
} |
| 309 |
} |
| 310 |
return \false; |
| 311 |
} |
| 312 |
/** |
| 313 |
* Checks whether the module at ($x, $y) is true (dark) or false (light) |
| 314 |
* |
| 315 |
* Also, returns false if the given coordinates are out of range. |
| 316 |
*/ |
| 317 |
public function check(int $x, int $y) : bool |
| 318 |
{ |
| 319 |
if (isset($this->matrix[$y][$x])) { |
| 320 |
return $this->isDark($this->matrix[$y][$x]); |
| 321 |
} |
| 322 |
return \false; |
| 323 |
} |
| 324 |
/** |
| 325 |
* Checks whether the given $M_TYPE is a dark value |
| 326 |
*/ |
| 327 |
public function isDark(int $M_TYPE) : bool |
| 328 |
{ |
| 329 |
return ($M_TYPE & $this::IS_DARK) === $this::IS_DARK; |
| 330 |
} |
| 331 |
/** |
| 332 |
* Checks the status of the neighbouring modules for the module at ($x, $y) and returns a bitmask with the results. |
| 333 |
* |
| 334 |
* The 8 flags of the bitmask represent the status of each of the neighbouring fields, |
| 335 |
* starting with the lowest bit for top left, going clockwise: |
| 336 |
* |
| 337 |
* 0 1 2 |
| 338 |
* 7 # 3 |
| 339 |
* 6 5 4 |
| 340 |
*/ |
| 341 |
public function checkNeighbours(int $x, int $y, ?int $M_TYPE = null) : int |
| 342 |
{ |
| 343 |
$bits = 0; |
| 344 |
foreach ($this::neighbours as $bit => [$ix, $iy]) { |
| 345 |
$ix += $x; |
| 346 |
$iy += $y; |
| 347 |
// $M_TYPE is given, skip if the field is not the same type |
| 348 |
if ($M_TYPE !== null && !$this->checkType($ix, $iy, $M_TYPE)) { |
| 349 |
continue; |
| 350 |
} |
| 351 |
if ($this->checkType($ix, $iy, $this::IS_DARK)) { |
| 352 |
$bits |= $bit; |
| 353 |
} |
| 354 |
} |
| 355 |
return $bits; |
| 356 |
} |
| 357 |
/** |
| 358 |
* Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder |
| 359 |
* |
| 360 |
* 4 * version + 9 or moduleCount - 8 |
| 361 |
*/ |
| 362 |
public function setDarkModule() : self |
| 363 |
{ |
| 364 |
$this->set(8, $this->moduleCount - 8, \true, $this::M_DARKMODULE); |
| 365 |
return $this; |
| 366 |
} |
| 367 |
/** |
| 368 |
* Draws the 7x7 finder patterns in the corners top left/right and bottom left |
| 369 |
* |
| 370 |
* ISO/IEC 18004:2000 Section 7.3.2 |
| 371 |
*/ |
| 372 |
public function setFinderPattern() : self |
| 373 |
{ |
| 374 |
$pos = [ |
| 375 |
[0, 0], |
| 376 |
// top left |
| 377 |
[$this->moduleCount - 7, 0], |
| 378 |
// top right |
| 379 |
[0, $this->moduleCount - 7], |
| 380 |
]; |
| 381 |
foreach ($pos as $c) { |
| 382 |
$this->setArea($c[0], $c[1], 7, 7, \true, $this::M_FINDER)->setArea($c[0] + 1, $c[1] + 1, 5, 5, \false, $this::M_FINDER)->setArea($c[0] + 2, $c[1] + 2, 3, 3, \true, $this::M_FINDER_DOT); |
| 383 |
} |
| 384 |
return $this; |
| 385 |
} |
| 386 |
/** |
| 387 |
* Draws the separator lines around the finder patterns |
| 388 |
* |
| 389 |
* ISO/IEC 18004:2000 Section 7.3.3 |
| 390 |
*/ |
| 391 |
public function setSeparators() : self |
| 392 |
{ |
| 393 |
$h = [[7, 0], [$this->moduleCount - 8, 0], [7, $this->moduleCount - 8]]; |
| 394 |
$v = [[7, 7], [$this->moduleCount - 1, 7], [7, $this->moduleCount - 8]]; |
| 395 |
for ($c = 0; $c < 3; $c++) { |
| 396 |
for ($i = 0; $i < 8; $i++) { |
| 397 |
// phpcs:ignore |
| 398 |
$this->set($h[$c][0], $h[$c][1] + $i, \false, $this::M_SEPARATOR); |
| 399 |
$this->set($v[$c][0] - $i, $v[$c][1], \false, $this::M_SEPARATOR); |
| 400 |
} |
| 401 |
} |
| 402 |
return $this; |
| 403 |
} |
| 404 |
/** |
| 405 |
* Draws the 5x5 alignment patterns |
| 406 |
* |
| 407 |
* ISO/IEC 18004:2000 Section 7.3.5 |
| 408 |
*/ |
| 409 |
public function setAlignmentPattern() : self |
| 410 |
{ |
| 411 |
$alignmentPattern = $this->version->getAlignmentPattern(); |
| 412 |
foreach ($alignmentPattern as $y) { |
| 413 |
foreach ($alignmentPattern as $x) { |
| 414 |
// skip existing patterns |
| 415 |
if ($this->matrix[$y][$x] !== $this::M_NULL) { |
| 416 |
continue; |
| 417 |
} |
| 418 |
$this->setArea($x - 2, $y - 2, 5, 5, \true, $this::M_ALIGNMENT)->setArea($x - 1, $y - 1, 3, 3, \false, $this::M_ALIGNMENT)->set($x, $y, \true, $this::M_ALIGNMENT); |
| 419 |
} |
| 420 |
} |
| 421 |
return $this; |
| 422 |
} |
| 423 |
/** |
| 424 |
* Draws the timing pattern (h/v checkered line between the finder patterns) |
| 425 |
* |
| 426 |
* ISO/IEC 18004:2000 Section 7.3.4 |
| 427 |
*/ |
| 428 |
public function setTimingPattern() : self |
| 429 |
{ |
| 430 |
for ($i = 8; $i < $this->moduleCount - 8; $i++) { |
| 431 |
if ($this->matrix[6][$i] !== $this::M_NULL || $this->matrix[$i][6] !== $this::M_NULL) { |
| 432 |
continue; |
| 433 |
} |
| 434 |
$v = $i % 2 === 0; |
| 435 |
$this->set($i, 6, $v, $this::M_TIMING); |
| 436 |
// h |
| 437 |
$this->set(6, $i, $v, $this::M_TIMING); |
| 438 |
// v |
| 439 |
} |
| 440 |
return $this; |
| 441 |
} |
| 442 |
/** |
| 443 |
* Draws the version information, 2x 3x6 pixel |
| 444 |
* |
| 445 |
* ISO/IEC 18004:2000 Section 8.10 |
| 446 |
*/ |
| 447 |
public function setVersionNumber() : self |
| 448 |
{ |
| 449 |
$bits = $this->version->getVersionPattern(); |
| 450 |
if ($bits !== null) { |
| 451 |
for ($i = 0; $i < 18; $i++) { |
| 452 |
$a = intdiv($i, 3); |
| 453 |
$b = $i % 3 + ($this->moduleCount - 8 - 3); |
| 454 |
$v = ($bits >> $i & 1) === 1; |
| 455 |
$this->set($b, $a, $v, $this::M_VERSION); |
| 456 |
// ne |
| 457 |
$this->set($a, $b, $v, $this::M_VERSION); |
| 458 |
// sw |
| 459 |
} |
| 460 |
} |
| 461 |
return $this; |
| 462 |
} |
| 463 |
/** |
| 464 |
* Draws the format info along the finder patterns. If no $maskPattern, all format info modules will be set to false. |
| 465 |
* |
| 466 |
* ISO/IEC 18004:2000 Section 8.9 |
| 467 |
*/ |
| 468 |
public function setFormatInfo(?MaskPattern $maskPattern = null) : self |
| 469 |
{ |
| 470 |
$this->maskPattern = $maskPattern; |
| 471 |
$bits = 0; |
| 472 |
// sets all format fields to false (test mode) |
| 473 |
if ($this->maskPattern instanceof MaskPattern) { |
| 474 |
$bits = $this->eccLevel->getformatPattern($this->maskPattern); |
| 475 |
} |
| 476 |
for ($i = 0; $i < 15; $i++) { |
| 477 |
$v = ($bits >> $i & 1) === 1; |
| 478 |
if ($i < 6) { |
| 479 |
$this->set(8, $i, $v, $this::M_FORMAT); |
| 480 |
} elseif ($i < 8) { |
| 481 |
$this->set(8, $i + 1, $v, $this::M_FORMAT); |
| 482 |
} else { |
| 483 |
$this->set(8, $this->moduleCount - 15 + $i, $v, $this::M_FORMAT); |
| 484 |
} |
| 485 |
if ($i < 8) { |
| 486 |
$this->set($this->moduleCount - $i - 1, 8, $v, $this::M_FORMAT); |
| 487 |
} elseif ($i < 9) { |
| 488 |
$this->set(15 - $i, 8, $v, $this::M_FORMAT); |
| 489 |
} else { |
| 490 |
$this->set(15 - $i - 1, 8, $v, $this::M_FORMAT); |
| 491 |
} |
| 492 |
} |
| 493 |
return $this; |
| 494 |
} |
| 495 |
/** |
| 496 |
* Draws the "quiet zone" of $size around the matrix |
| 497 |
* |
| 498 |
* ISO/IEC 18004:2000 Section 7.3.7 |
| 499 |
* |
| 500 |
* @throws \chillerlan\QRCode\Data\QRCodeDataException |
| 501 |
*/ |
| 502 |
public function setQuietZone(int $quietZoneSize) : self |
| 503 |
{ |
| 504 |
// early exit if there's nothing to add |
| 505 |
if ($quietZoneSize < 1) { |
| 506 |
return $this; |
| 507 |
} |
| 508 |
if ($this->matrix[$this->moduleCount - 1][$this->moduleCount - 1] === $this::M_NULL) { |
| 509 |
throw new QRCodeDataException('use only after writing data'); |
| 510 |
} |
| 511 |
// create a matrix with the new size |
| 512 |
$newSize = $this->moduleCount + $quietZoneSize * 2; |
| 513 |
$newMatrix = $this->createMatrix($newSize, $this::M_QUIETZONE); |
| 514 |
// copy over the current matrix |
| 515 |
foreach ($this->matrix as $y => $row) { |
| 516 |
foreach ($row as $x => $val) { |
| 517 |
$newMatrix[$y + $quietZoneSize][$x + $quietZoneSize] = $val; |
| 518 |
} |
| 519 |
} |
| 520 |
// set the new values |
| 521 |
$this->moduleCount = $newSize; |
| 522 |
$this->matrix = $newMatrix; |
| 523 |
return $this; |
| 524 |
} |
| 525 |
/** |
| 526 |
* Rotates the matrix by 90 degrees clock wise |
| 527 |
*/ |
| 528 |
public function rotate90() : self |
| 529 |
{ |
| 530 |
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */ |
| 531 |
$this->matrix = array_map(fn(int ...$a): array => array_reverse($a), ...$this->matrix); |
| 532 |
return $this; |
| 533 |
} |
| 534 |
/** |
| 535 |
* Inverts the values of the whole matrix |
| 536 |
* |
| 537 |
* ISO/IEC 18004:2015 Section 6.2 - Reflectance reversal |
| 538 |
*/ |
| 539 |
public function invert() : self |
| 540 |
{ |
| 541 |
foreach ($this->matrix as $y => $row) { |
| 542 |
foreach ($row as $x => $val) { |
| 543 |
// skip null fields |
| 544 |
if ($val === $this::M_NULL) { |
| 545 |
continue; |
| 546 |
} |
| 547 |
$this->flip($x, $y); |
| 548 |
} |
| 549 |
} |
| 550 |
return $this; |
| 551 |
} |
| 552 |
/** |
| 553 |
* Clears a space of $width * $height in order to add a logo or text. |
| 554 |
* If no $height is given, the space will be assumed a square of $width. |
| 555 |
* |
| 556 |
* Additionally, the logo space can be positioned within the QR Code using $startX and $startY. |
| 557 |
* If either of these are null, the logo space will be centered in that direction. |
| 558 |
* ECC level "H" (30%) is required. |
| 559 |
* |
| 560 |
* The coordinates of $startX and $startY do not include the quiet zone: |
| 561 |
* [0, 0] is always the top left module of the top left finder pattern, negative values go into the quiet zone top and left. |
| 562 |
* |
| 563 |
* Please note that adding a logo space minimizes the error correction capacity of the QR Code and |
| 564 |
* created images may become unreadable, especially when printed with a chance to receive damage. |
| 565 |
* Please test thoroughly before using this feature in production. |
| 566 |
* |
| 567 |
* This method should be called from within an output module (after the matrix has been filled with data). |
| 568 |
* Note that there is no restiction on how many times this method could be called on the same matrix instance. |
| 569 |
* |
| 570 |
* @link https://github.com/chillerlan/php-qrcode/issues/52 |
| 571 |
* |
| 572 |
* @throws \chillerlan\QRCode\Data\QRCodeDataException |
| 573 |
*/ |
| 574 |
public function setLogoSpace(int $width, ?int $height = null, ?int $startX = null, ?int $startY = null) : self |
| 575 |
{ |
| 576 |
$height ??= $width; |
| 577 |
// if width and height happen to be negative or 0 (default value), just return - nothing to do |
| 578 |
if ($width <= 0 || $height <= 0) { |
| 579 |
return $this; |
| 580 |
// @codeCoverageIgnore |
| 581 |
} |
| 582 |
// for logos, we operate in ECC H (30%) only |
| 583 |
if ($this->eccLevel->getLevel() !== EccLevel::H) { |
| 584 |
throw new QRCodeDataException('ECC level "H" required to add logo space'); |
| 585 |
} |
| 586 |
// $this->moduleCount includes the quiet zone (if created), we need the QR size here |
| 587 |
$dimension = $this->version->getDimension(); |
| 588 |
// throw if the size exceeds the qrcode size |
| 589 |
if ($width > $dimension || $height > $dimension) { |
| 590 |
throw new QRCodeDataException('logo dimensions exceed matrix size'); |
| 591 |
} |
| 592 |
// we need uneven sizes to center the logo space, adjust if needed |
| 593 |
if ($startX === null && $width % 2 === 0) { |
| 594 |
$width++; |
| 595 |
} |
| 596 |
if ($startY === null && $height % 2 === 0) { |
| 597 |
$height++; |
| 598 |
} |
| 599 |
// throw if the logo space exceeds the maximum error correction capacity |
| 600 |
if ($width * $height > (int) ($dimension * $dimension * 0.25)) { |
| 601 |
throw new QRCodeDataException('logo space exceeds the maximum error correction capacity'); |
| 602 |
} |
| 603 |
$quietzone = ($this->moduleCount - $dimension) / 2; |
| 604 |
$end = $this->moduleCount - $quietzone; |
| 605 |
// determine start coordinates |
| 606 |
$startX ??= ($dimension - $width) / 2; |
| 607 |
$startY ??= ($dimension - $height) / 2; |
| 608 |
$endX = $quietzone + $startX + $width; |
| 609 |
$endY = $quietzone + $startY + $height; |
| 610 |
// clear the space |
| 611 |
for ($y = $quietzone + $startY; $y < $endY; $y++) { |
| 612 |
for ($x = $quietzone + $startX; $x < $endX; $x++) { |
| 613 |
// out of bounds, skip |
| 614 |
if ($x < $quietzone || $y < $quietzone || $x >= $end || $y >= $end) { |
| 615 |
continue; |
| 616 |
} |
| 617 |
$this->set($x, $y, \false, $this::M_LOGO); |
| 618 |
} |
| 619 |
} |
| 620 |
return $this; |
| 621 |
} |
| 622 |
/** |
| 623 |
* Maps the interleaved binary $data on the matrix |
| 624 |
*/ |
| 625 |
public function writeCodewords(BitBuffer $bitBuffer) : self |
| 626 |
{ |
| 627 |
$data = (new ReedSolomonEncoder($this->version, $this->eccLevel))->interleaveEcBytes($bitBuffer); |
| 628 |
$byteCount = count($data); |
| 629 |
$iByte = 0; |
| 630 |
$iBit = 7; |
| 631 |
$direction = \true; |
| 632 |
for ($i = $this->moduleCount - 1; $i > 0; $i -= 2) { |
| 633 |
// skip vertical alignment pattern |
| 634 |
if ($i === 6) { |
| 635 |
$i--; |
| 636 |
} |
| 637 |
for ($count = 0; $count < $this->moduleCount; $count++) { |
| 638 |
$y = $count; |
| 639 |
if ($direction) { |
| 640 |
$y = $this->moduleCount - 1 - $count; |
| 641 |
} |
| 642 |
for ($col = 0; $col < 2; $col++) { |
| 643 |
$x = $i - $col; |
| 644 |
// skip functional patterns |
| 645 |
if ($this->matrix[$y][$x] !== $this::M_NULL) { |
| 646 |
continue; |
| 647 |
} |
| 648 |
$this->matrix[$y][$x] = $this::M_DATA; |
| 649 |
if ($iByte < $byteCount && ($data[$iByte] >> $iBit-- & 1) === 1) { |
| 650 |
$this->matrix[$y][$x] |= $this::IS_DARK; |
| 651 |
} |
| 652 |
if ($iBit === -1) { |
| 653 |
$iByte++; |
| 654 |
$iBit = 7; |
| 655 |
} |
| 656 |
} |
| 657 |
} |
| 658 |
$direction = !$direction; |
| 659 |
// switch directions |
| 660 |
} |
| 661 |
return $this; |
| 662 |
} |
| 663 |
/** |
| 664 |
* Applies/reverses the mask pattern |
| 665 |
* |
| 666 |
* ISO/IEC 18004:2000 Section 8.8.1 |
| 667 |
*/ |
| 668 |
public function mask(MaskPattern $maskPattern) : self |
| 669 |
{ |
| 670 |
$this->maskPattern = $maskPattern; |
| 671 |
$mask = $this->maskPattern->getMask(); |
| 672 |
foreach ($this->matrix as $y => $row) { |
| 673 |
foreach ($row as $x => $val) { |
| 674 |
// skip non-data modules |
| 675 |
if (($val & $this::M_DATA) === $this::M_DATA && $mask($x, $y)) { |
| 676 |
$this->flip($x, $y); |
| 677 |
} |
| 678 |
} |
| 679 |
} |
| 680 |
return $this; |
| 681 |
} |
| 682 |
} |
| 683 |
|