| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* Class MaskPattern |
| 5 |
* |
| 6 |
* @created 19.01.2021 |
| 7 |
* @author ZXing Authors |
| 8 |
* @author Smiley <smiley@chillerlan.net> |
| 9 |
* @copyright 2021 Smiley |
| 10 |
* @license Apache-2.0 |
| 11 |
*/ |
| 12 |
namespace WCPOS\Vendor\chillerlan\QRCode\Common; |
| 13 |
|
| 14 |
use WCPOS\Vendor\chillerlan\QRCode\QRCodeException; |
| 15 |
use WCPOS\Vendor\chillerlan\QRCode\Data\QRMatrix; |
| 16 |
use Closure; |
| 17 |
use function abs, array_column, array_search, intdiv, min; |
| 18 |
/** |
| 19 |
* ISO/IEC 18004:2000 Section 8.8.1 |
| 20 |
* ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results |
| 21 |
* |
| 22 |
* @see http://www.thonky.com/qr-code-tutorial/data-masking |
| 23 |
* @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java |
| 24 |
*/ |
| 25 |
final class MaskPattern |
| 26 |
{ |
| 27 |
/** |
| 28 |
* @see \chillerlan\QRCode\QROptionsTrait::$maskPattern |
| 29 |
* |
| 30 |
* @var int |
| 31 |
*/ |
| 32 |
public const AUTO = -1; |
| 33 |
public const PATTERN_000 = 0b0; |
| 34 |
public const PATTERN_001 = 0b1; |
| 35 |
public const PATTERN_010 = 0b10; |
| 36 |
public const PATTERN_011 = 0b11; |
| 37 |
public const PATTERN_100 = 0b100; |
| 38 |
public const PATTERN_101 = 0b101; |
| 39 |
public const PATTERN_110 = 0b110; |
| 40 |
public const PATTERN_111 = 0b111; |
| 41 |
/** |
| 42 |
* @var int[] |
| 43 |
*/ |
| 44 |
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]; |
| 45 |
/* |
| 46 |
* Penalty scores |
| 47 |
* |
| 48 |
* ISO/IEC 18004:2000 Section 8.8.1 - Table 24 |
| 49 |
*/ |
| 50 |
private const PENALTY_N1 = 3; |
| 51 |
private const PENALTY_N2 = 3; |
| 52 |
private const PENALTY_N3 = 40; |
| 53 |
private const PENALTY_N4 = 10; |
| 54 |
/** |
| 55 |
* The current mask pattern value (0-7) |
| 56 |
*/ |
| 57 |
private int $maskPattern; |
| 58 |
/** |
| 59 |
* MaskPattern constructor. |
| 60 |
* |
| 61 |
* @throws \chillerlan\QRCode\QRCodeException |
| 62 |
*/ |
| 63 |
public function __construct(int $maskPattern) |
| 64 |
{ |
| 65 |
if (($maskPattern & 0b111) !== $maskPattern) { |
| 66 |
throw new QRCodeException('invalid mask pattern'); |
| 67 |
} |
| 68 |
$this->maskPattern = $maskPattern; |
| 69 |
} |
| 70 |
/** |
| 71 |
* Returns the current mask pattern |
| 72 |
*/ |
| 73 |
public function getPattern() : int |
| 74 |
{ |
| 75 |
return $this->maskPattern; |
| 76 |
} |
| 77 |
/** |
| 78 |
* Returns a closure that applies the mask for the chosen mask pattern. |
| 79 |
* |
| 80 |
* Note that the diagram in section 6.8.1 is misleading since it indicates that $i is column position |
| 81 |
* and $j is row position. In fact, as the text says, $i is row position and $j is column position. |
| 82 |
* |
| 83 |
* @see https://www.thonky.com/qr-code-tutorial/mask-patterns |
| 84 |
* @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java#L32-L117 |
| 85 |
*/ |
| 86 |
public function getMask() : Closure |
| 87 |
{ |
| 88 |
// $x = column (width), $y = row (height) |
| 89 |
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]; |
| 90 |
} |
| 91 |
/** |
| 92 |
* Evaluates the matrix of the given data interface and returns a new mask pattern instance for the best result |
| 93 |
*/ |
| 94 |
public static function getBestPattern(QRMatrix $QRMatrix) : self |
| 95 |
{ |
| 96 |
$penalties = []; |
| 97 |
$size = $QRMatrix->getSize(); |
| 98 |
foreach (self::PATTERNS as $pattern) { |
| 99 |
$mp = new self($pattern); |
| 100 |
$matrix = (clone $QRMatrix)->setFormatInfo($mp)->mask($mp)->getMatrix(\true); |
| 101 |
$penalty = 0; |
| 102 |
for ($level = 1; $level <= 4; $level++) { |
| 103 |
$penalty += self::{'testRule' . $level}($matrix, $size, $size); |
| 104 |
} |
| 105 |
$penalties[$pattern] = (int) $penalty; |
| 106 |
} |
| 107 |
return new self(array_search(min($penalties), $penalties, \true)); |
| 108 |
} |
| 109 |
/** |
| 110 |
* Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and |
| 111 |
* give penalty to them. Example: 00000 or 11111. |
| 112 |
*/ |
| 113 |
public static function testRule1(array $matrix, int $height, int $width) : int |
| 114 |
{ |
| 115 |
$penalty = 0; |
| 116 |
// horizontal |
| 117 |
foreach ($matrix as $row) { |
| 118 |
$penalty += self::applyRule1($row); |
| 119 |
} |
| 120 |
// vertical |
| 121 |
for ($x = 0; $x < $width; $x++) { |
| 122 |
$penalty += self::applyRule1(array_column($matrix, $x)); |
| 123 |
} |
| 124 |
return $penalty; |
| 125 |
} |
| 126 |
/** |
| 127 |
* |
| 128 |
*/ |
| 129 |
private static function applyRule1(array $rc) : int |
| 130 |
{ |
| 131 |
$penalty = 0; |
| 132 |
$numSameBitCells = 0; |
| 133 |
$prevBit = null; |
| 134 |
foreach ($rc as $val) { |
| 135 |
if ($val === $prevBit) { |
| 136 |
$numSameBitCells++; |
| 137 |
} else { |
| 138 |
if ($numSameBitCells >= 5) { |
| 139 |
$penalty += self::PENALTY_N1 + $numSameBitCells - 5; |
| 140 |
} |
| 141 |
$numSameBitCells = 1; |
| 142 |
// Include the cell itself. |
| 143 |
$prevBit = $val; |
| 144 |
} |
| 145 |
} |
| 146 |
if ($numSameBitCells >= 5) { |
| 147 |
$penalty += self::PENALTY_N1 + $numSameBitCells - 5; |
| 148 |
} |
| 149 |
return $penalty; |
| 150 |
} |
| 151 |
/** |
| 152 |
* Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give |
| 153 |
* penalty to them. This is actually equivalent to the spec's rule, which is to find MxN blocks and give a |
| 154 |
* penalty proportional to (M-1)x(N-1), because this is the number of 2x2 blocks inside such a block. |
| 155 |
*/ |
| 156 |
public static function testRule2(array $matrix, int $height, int $width) : int |
| 157 |
{ |
| 158 |
$penalty = 0; |
| 159 |
foreach ($matrix as $y => $row) { |
| 160 |
if ($y > $height - 2) { |
| 161 |
break; |
| 162 |
} |
| 163 |
foreach ($row as $x => $val) { |
| 164 |
if ($x > $width - 2) { |
| 165 |
break; |
| 166 |
} |
| 167 |
if ($val === $row[$x + 1] && $val === $matrix[$y + 1][$x] && $val === $matrix[$y + 1][$x + 1]) { |
| 168 |
$penalty++; |
| 169 |
} |
| 170 |
} |
| 171 |
} |
| 172 |
return self::PENALTY_N2 * $penalty; |
| 173 |
} |
| 174 |
/** |
| 175 |
* Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4 |
| 176 |
* starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them. If we |
| 177 |
* find patterns like 000010111010000, we give penalty once. |
| 178 |
*/ |
| 179 |
public static function testRule3(array $matrix, int $height, int $width) : int |
| 180 |
{ |
| 181 |
$penalties = 0; |
| 182 |
foreach ($matrix as $y => $row) { |
| 183 |
foreach ($row as $x => $val) { |
| 184 |
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))) { |
| 185 |
$penalties++; |
| 186 |
} |
| 187 |
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))) { |
| 188 |
$penalties++; |
| 189 |
} |
| 190 |
} |
| 191 |
} |
| 192 |
return $penalties * self::PENALTY_N3; |
| 193 |
} |
| 194 |
/** |
| 195 |
* |
| 196 |
*/ |
| 197 |
private static function isWhiteHorizontal(array $row, int $width, int $from, int $to) : bool |
| 198 |
{ |
| 199 |
if ($from < 0 || $width < $to) { |
| 200 |
return \false; |
| 201 |
} |
| 202 |
for ($x = $from; $x < $to; $x++) { |
| 203 |
if ($row[$x]) { |
| 204 |
return \false; |
| 205 |
} |
| 206 |
} |
| 207 |
return \true; |
| 208 |
} |
| 209 |
/** |
| 210 |
* |
| 211 |
*/ |
| 212 |
private static function isWhiteVertical(array $matrix, int $height, int $x, int $from, int $to) : bool |
| 213 |
{ |
| 214 |
if ($from < 0 || $height < $to) { |
| 215 |
return \false; |
| 216 |
} |
| 217 |
for ($y = $from; $y < $to; $y++) { |
| 218 |
if ($matrix[$y][$x] === \true) { |
| 219 |
return \false; |
| 220 |
} |
| 221 |
} |
| 222 |
return \true; |
| 223 |
} |
| 224 |
/** |
| 225 |
* Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give |
| 226 |
* penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance. |
| 227 |
*/ |
| 228 |
public static function testRule4(array $matrix, int $height, int $width) : int |
| 229 |
{ |
| 230 |
$darkCells = 0; |
| 231 |
$totalCells = $height * $width; |
| 232 |
foreach ($matrix as $row) { |
| 233 |
foreach ($row as $val) { |
| 234 |
if ($val === \true) { |
| 235 |
$darkCells++; |
| 236 |
} |
| 237 |
} |
| 238 |
} |
| 239 |
return intdiv(abs($darkCells * 2 - $totalCells) * 10, $totalCells) * self::PENALTY_N4; |
| 240 |
} |
| 241 |
} |
| 242 |
|