| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- <?php
- /**
- * Class MaskPattern
- *
- * @created 19.01.2021
- * @author ZXing Authors
- * @author Smiley <smiley@chillerlan.net>
- * @copyright 2021 Smiley
- * @license Apache-2.0
- */
- namespace chillerlan\QRCode\Common;
- use chillerlan\QRCode\QRCodeException;
- use chillerlan\QRCode\Data\QRMatrix;
- use Closure;
- use function abs, array_column, array_search, intdiv, min;
- /**
- * ISO/IEC 18004:2000 Section 8.8.1
- * ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results
- *
- * @see http://www.thonky.com/qr-code-tutorial/data-masking
- * @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java
- */
- final class MaskPattern{
- /**
- * @see \chillerlan\QRCode\QROptionsTrait::$maskPattern
- *
- * @var int
- */
- public const AUTO = -1;
- public const PATTERN_000 = 0b000;
- public const PATTERN_001 = 0b001;
- public const PATTERN_010 = 0b010;
- public const PATTERN_011 = 0b011;
- public const PATTERN_100 = 0b100;
- public const PATTERN_101 = 0b101;
- public const PATTERN_110 = 0b110;
- public const PATTERN_111 = 0b111;
- /**
- * @var int[]
- */
- public const PATTERNS = [
- self::PATTERN_000,
- self::PATTERN_001,
- self::PATTERN_010,
- self::PATTERN_011,
- self::PATTERN_100,
- self::PATTERN_101,
- self::PATTERN_110,
- self::PATTERN_111,
- ];
- /*
- * Penalty scores
- *
- * ISO/IEC 18004:2000 Section 8.8.1 - Table 24
- */
- private const PENALTY_N1 = 3;
- private const PENALTY_N2 = 3;
- private const PENALTY_N3 = 40;
- private const PENALTY_N4 = 10;
- /**
- * The current mask pattern value (0-7)
- */
- private int $maskPattern;
- /**
- * MaskPattern constructor.
- *
- * @throws \chillerlan\QRCode\QRCodeException
- */
- public function __construct(int $maskPattern){
- if(($maskPattern & 0b111) !== $maskPattern){
- throw new QRCodeException('invalid mask pattern');
- }
- $this->maskPattern = $maskPattern;
- }
- /**
- * Returns the current mask pattern
- */
- public function getPattern():int{
- return $this->maskPattern;
- }
- /**
- * Returns a closure that applies the mask for the chosen mask pattern.
- *
- * Note that the diagram in section 6.8.1 is misleading since it indicates that $i is column position
- * and $j is row position. In fact, as the text says, $i is row position and $j is column position.
- *
- * @see https://www.thonky.com/qr-code-tutorial/mask-patterns
- * @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java#L32-L117
- */
- public function getMask():Closure{
- // $x = column (width), $y = row (height)
- return [
- self::PATTERN_000 => fn(int $x, int $y):bool => (($x + $y) % 2) === 0,
- self::PATTERN_001 => fn(int $x, int $y):bool => ($y % 2) === 0,
- self::PATTERN_010 => fn(int $x, int $y):bool => ($x % 3) === 0,
- self::PATTERN_011 => fn(int $x, int $y):bool => (($x + $y) % 3) === 0,
- self::PATTERN_100 => fn(int $x, int $y):bool => ((intdiv($y, 2) + intdiv($x, 3)) % 2) === 0,
- self::PATTERN_101 => fn(int $x, int $y):bool => (($x * $y) % 6) === 0,
- self::PATTERN_110 => fn(int $x, int $y):bool => (($x * $y) % 6) < 3,
- self::PATTERN_111 => fn(int $x, int $y):bool => (($x + $y + (($x * $y) % 3)) % 2) === 0,
- ][$this->maskPattern];
- }
- /**
- * Evaluates the matrix of the given data interface and returns a new mask pattern instance for the best result
- */
- public static function getBestPattern(QRMatrix $QRMatrix):self{
- $penalties = [];
- $size = $QRMatrix->getSize();
- foreach(self::PATTERNS as $pattern){
- $mp = new self($pattern);
- $matrix = (clone $QRMatrix)->setFormatInfo($mp)->mask($mp)->getMatrix(true);
- $penalty = 0;
- for($level = 1; $level <= 4; $level++){
- $penalty += self::{'testRule'.$level}($matrix, $size, $size);
- }
- $penalties[$pattern] = (int)$penalty;
- }
- return new self(array_search(min($penalties), $penalties, true));
- }
- /**
- * Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and
- * give penalty to them. Example: 00000 or 11111.
- *
- * @param bool[][] $matrix
- */
- public static function testRule1(array $matrix, int $height, int $width):int{
- $penalty = 0;
- // horizontal
- foreach($matrix as $row){
- $penalty += self::applyRule1($row);
- }
- // vertical
- for($x = 0; $x < $width; $x++){
- $penalty += self::applyRule1(array_column($matrix, $x));
- }
- return $penalty;
- }
- /**
- * @param bool[] $rc
- */
- private static function applyRule1(array $rc):int{
- $penalty = 0;
- $numSameBitCells = 0;
- $prevBit = null;
- foreach($rc as $val){
- if($val === $prevBit){
- $numSameBitCells++;
- }
- else{
- if($numSameBitCells >= 5){
- $penalty += (self::PENALTY_N1 + $numSameBitCells - 5);
- }
- $numSameBitCells = 1; // Include the cell itself.
- $prevBit = $val;
- }
- }
- if($numSameBitCells >= 5){
- $penalty += (self::PENALTY_N1 + $numSameBitCells - 5);
- }
- return $penalty;
- }
- /**
- * Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give
- * penalty to them. This is actually equivalent to the spec's rule, which is to find MxN blocks and give a
- * penalty proportional to (M-1)x(N-1), because this is the number of 2x2 blocks inside such a block.
- *
- * @param bool[][] $matrix
- */
- public static function testRule2(array $matrix, int $height, int $width):int{
- $penalty = 0;
- foreach($matrix as $y => $row){
- if($y > ($height - 2)){
- break;
- }
- foreach($row as $x => $val){
- if($x > ($width - 2)){
- break;
- }
- if(
- $val === $row[($x + 1)]
- && $val === $matrix[($y + 1)][$x]
- && $val === $matrix[($y + 1)][($x + 1)]
- ){
- $penalty++;
- }
- }
- }
- return (self::PENALTY_N2 * $penalty);
- }
- /**
- * Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4
- * starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them. If we
- * find patterns like 000010111010000, we give penalty once.
- *
- * @param bool[][] $matrix
- */
- public static function testRule3(array $matrix, int $height, int $width):int{
- $penalties = 0;
- foreach($matrix as $y => $row){
- foreach($row as $x => $val){
- if(
- ($x + 6) < $width
- && $val
- && !$row[($x + 1)]
- && $row[($x + 2)]
- && $row[($x + 3)]
- && $row[($x + 4)]
- && !$row[($x + 5)]
- && $row[($x + 6)]
- && (
- self::isWhiteHorizontal($row, $width, ($x - 4), $x)
- || self::isWhiteHorizontal($row, $width, ($x + 7), ($x + 11))
- )
- ){
- $penalties++;
- }
- if(
- ($y + 6) < $height
- && $val
- && !$matrix[($y + 1)][$x]
- && $matrix[($y + 2)][$x]
- && $matrix[($y + 3)][$x]
- && $matrix[($y + 4)][$x]
- && !$matrix[($y + 5)][$x]
- && $matrix[($y + 6)][$x]
- && (
- self::isWhiteVertical($matrix, $height, $x, ($y - 4), $y)
- || self::isWhiteVertical($matrix, $height, $x, ($y + 7), ($y + 11))
- )
- ){
- $penalties++;
- }
- }
- }
- return ($penalties * self::PENALTY_N3);
- }
- /**
- * @param bool[] $row
- */
- private static function isWhiteHorizontal(array $row, int $width, int $from, int $to):bool{
- if($from < 0 || $width < $to){
- return false;
- }
- for($x = $from; $x < $to; $x++){
- if($row[$x]){
- return false;
- }
- }
- return true;
- }
- /**
- * @param bool[][] $matrix
- */
- private static function isWhiteVertical(array $matrix, int $height, int $x, int $from, int $to):bool{
- if($from < 0 || $height < $to){
- return false;
- }
- for($y = $from; $y < $to; $y++){
- if($matrix[$y][$x] === true){
- return false;
- }
- }
- return true;
- }
- /**
- * Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give
- * penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance.
- *
- * @param bool[][] $matrix
- */
- public static function testRule4(array $matrix, int $height, int $width):int{
- $darkCells = 0;
- $totalCells = ($height * $width);
- foreach($matrix as $row){
- foreach($row as $val){
- if($val === true){
- $darkCells++;
- }
- }
- }
- return (intdiv((abs($darkCells * 2 - $totalCells) * 10), $totalCells) * self::PENALTY_N4);
- }
- }
|