mirror of
https://github.com/chillerlan/php-qrcode.git
synced 2026-08-25 13:17:57 +00:00
298 lines
7.8 KiB
PHP
298 lines
7.8 KiB
PHP
<?php
|
|
/**
|
|
* Class MaskPattern
|
|
*
|
|
* @created 19.01.2021
|
|
* @author smiley <smiley@chillerlan.net>
|
|
* @copyright 2021 smiley
|
|
* @license MIT
|
|
*/
|
|
|
|
namespace chillerlan\QRCode\Common;
|
|
|
|
use chillerlan\QRCode\Data\QRData;
|
|
use chillerlan\QRCode\QRCodeException;
|
|
use Closure;
|
|
use function abs, array_search, count, 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
|
|
*/
|
|
final class MaskPattern{
|
|
|
|
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,
|
|
];
|
|
|
|
/**
|
|
* The current mask pattern value (0-7)
|
|
*/
|
|
private int $maskPattern;
|
|
|
|
/**
|
|
* MaskPattern constructor.
|
|
*
|
|
* @throws \chillerlan\QRCode\QRCodeException
|
|
*/
|
|
public function __construct(int $maskPattern){
|
|
|
|
if((0b111 & $maskPattern) !== $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.
|
|
*
|
|
* Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations
|
|
* of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix,
|
|
* including areas used for finder patterns, timing patterns, etc. These areas should be unused
|
|
* after the point they are unmasked anyway.
|
|
*
|
|
* 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 => (((int)($y / 2) + (int)($x / 3)) % 2) === 0,
|
|
self::PATTERN_101 => fn(int $x, int $y):bool => ($x * $y) % 6 === 0, // ((($x * $y) % 2) + (($x * $y) % 3)) === 0,
|
|
self::PATTERN_110 => fn(int $x, int $y):bool => (($x * $y) % 6) < 3, // (((($x * $y) % 2) + (($x * $y) % 3)) % 2) === 0,
|
|
self::PATTERN_111 => fn(int $x, int $y):bool => (($x + $y + (($x * $y) % 3)) % 2) === 0, // (((($x * $y) % 3) + (($x + $y) % 2)) % 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(QRData $dataInterface):self{
|
|
$penalties = [];
|
|
|
|
foreach(self::PATTERNS as $pattern){
|
|
$matrix = $dataInterface->writeMatrix(new self($pattern))->matrix(true);
|
|
$penalty = 0;
|
|
|
|
for($level = 1; $level <= 4; $level++){
|
|
$penalty += self::{'testRule'.$level}($matrix, count($matrix), count($matrix[0]));
|
|
}
|
|
|
|
$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.
|
|
*/
|
|
public static function testRule1(array $matrix, int $height, int $width):int{
|
|
return self::applyRule1($matrix, $height, $width, true) + self::applyRule1($matrix, $height, $width, false);
|
|
}
|
|
|
|
private static function applyRule1(array $matrix, int $height, int $width, bool $isHorizontal):int{
|
|
$penalty = 0;
|
|
$iLimit = $isHorizontal ? $height : $width;
|
|
$jLimit = $isHorizontal ? $width : $height;
|
|
|
|
for($i = 0; $i < $iLimit; $i++){
|
|
$numSameBitCells = 0;
|
|
$prevBit = -1;
|
|
|
|
for($j = 0; $j < $jLimit; $j++){
|
|
$bit = $isHorizontal ? $matrix[$i][$j] : $matrix[$j][$i];
|
|
|
|
if($bit === $prevBit){
|
|
$numSameBitCells++;
|
|
}
|
|
else{
|
|
|
|
if($numSameBitCells >= 5){
|
|
$penalty += 3 + ($numSameBitCells - 5);
|
|
}
|
|
|
|
$numSameBitCells = 1; // Include the cell itself.
|
|
$prevBit = $bit;
|
|
}
|
|
}
|
|
if($numSameBitCells >= 5){
|
|
$penalty += 3 + ($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.
|
|
*/
|
|
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 3 * $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.
|
|
*/
|
|
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 * 40;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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]){
|
|
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.
|
|
*/
|
|
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){
|
|
$darkCells++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (int)(abs($darkCells * 2 - $totalCells) * 10 / $totalCells) * 10;
|
|
}
|
|
|
|
}
|