codemasher
2021-01-25 00:42:26 +01:00
parent 93618e83ff
commit 84eb31696c
32 changed files with 4016 additions and 2 deletions
+361
View File
@@ -0,0 +1,361 @@
<?php
/**
* Class Binarizer
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use RuntimeException;
use function array_fill, count, max;
/**
* This class implements a local thresholding algorithm, which while slower than the
* GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for
* high frequency images of barcodes with black data on white backgrounds. For this application,
* it does a much better job than a global blackpoint with severe shadows and gradients.
* However it tends to produce artifacts on lower frequency images and is therefore not
* a good general purpose binarizer for uses outside ZXing.
*
* This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers,
* and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already
* inherently local, and only fails for horizontal gradients. We can revisit that problem later,
* but for now it was not a win to use local blocks for 1D.
*
* This Binarizer is the default for the unit tests and the recommended class for library users.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
final class Binarizer{
// This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels.
// So this is the smallest dimension in each axis we can accept.
private const BLOCK_SIZE_POWER = 3;
private const BLOCK_SIZE = 8; // ...0100...00
private const BLOCK_SIZE_MASK = 7; // ...0011...11
private const MINIMUM_DIMENSION = 40;
private const MIN_DYNAMIC_RANGE = 24;
# private const LUMINANCE_BITS = 5;
private const LUMINANCE_SHIFT = 3;
private const LUMINANCE_BUCKETS = 32;
private LuminanceSource $source;
public function __construct(LuminanceSource $source){
$this->source = $source;
}
/**
* @throws \RuntimeException
*/
private function estimateBlackPoint(array $buckets):int{
// Find the tallest peak in the histogram.
$numBuckets = count($buckets);
$maxBucketCount = 0;
$firstPeak = 0;
$firstPeakSize = 0;
for($x = 0; $x < $numBuckets; $x++){
if($buckets[$x] > $firstPeakSize){
$firstPeak = $x;
$firstPeakSize = $buckets[$x];
}
if($buckets[$x] > $maxBucketCount){
$maxBucketCount = $buckets[$x];
}
}
// Find the second-tallest peak which is somewhat far from the tallest peak.
$secondPeak = 0;
$secondPeakScore = 0;
for($x = 0; $x < $numBuckets; $x++){
$distanceToBiggest = $x - $firstPeak;
// Encourage more distant second peaks by multiplying by square of distance.
$score = $buckets[$x] * $distanceToBiggest * $distanceToBiggest;
if($score > $secondPeakScore){
$secondPeak = $x;
$secondPeakScore = $score;
}
}
// Make sure firstPeak corresponds to the black peak.
if($firstPeak > $secondPeak){
$temp = $firstPeak;
$firstPeak = $secondPeak;
$secondPeak = $temp;
}
// If there is too little contrast in the image to pick a meaningful black point, throw rather
// than waste time trying to decode the image, and risk false positives.
if($secondPeak - $firstPeak <= $numBuckets / 16){
throw new RuntimeException('no meaningful dark point found');
}
// Find a valley between them that is low and closer to the white peak.
$bestValley = $secondPeak - 1;
$bestValleyScore = -1;
for($x = $secondPeak - 1; $x > $firstPeak; $x--){
$fromFirst = $x - $firstPeak;
$score = $fromFirst * $fromFirst * ($secondPeak - $x) * ($maxBucketCount - $buckets[$x]);
if($score > $bestValleyScore){
$bestValley = $x;
$bestValleyScore = $score;
}
}
return $bestValley << self::LUMINANCE_SHIFT;
}
/**
* Calculates the final BitMatrix once for all requests. This could be called once from the
* constructor instead, but there are some advantages to doing it lazily, such as making
* profiling easier, and not doing heavy lifting when callers don't expect it.
*
* Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive
* and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or
* may not apply sharpening. Therefore, a row from this matrix may not be identical to one
* fetched using getBlackRow(), so don't mix and match between them.
*
* @return \chillerlan\QRCode\Decoder\BitMatrix The 2D array of bits for the image (true means black).
*/
public function getBlackMatrix():BitMatrix{
$width = $this->source->getWidth();
$height = $this->source->getHeight();
if($width >= self::MINIMUM_DIMENSION && $height >= self::MINIMUM_DIMENSION){
$subWidth = $width >> self::BLOCK_SIZE_POWER;
if(($width & self::BLOCK_SIZE_MASK) !== 0){
$subWidth++;
}
$subHeight = $height >> self::BLOCK_SIZE_POWER;
if(($height & self::BLOCK_SIZE_MASK) !== 0){
$subHeight++;
}
return $this->calculateThresholdForBlock($subWidth, $subHeight, $width, $height);
}
// If the image is too small, fall back to the global histogram approach.
return $this->getHistogramBlackMatrix($width, $height);
}
public function getHistogramBlackMatrix(int $width, int $height):BitMatrix{
$matrix = new BitMatrix(max($width, $height));
// Quickly calculates the histogram by sampling four rows from the image. This proved to be
// more robust on the blackbox tests than sampling a diagonal as we used to do.
$buckets = array_fill(0, self::LUMINANCE_BUCKETS, 0);
for($y = 1; $y < 5; $y++){
$row = (int)($height * $y / 5);
$localLuminances = $this->source->getRow($row);
$right = (int)(($width * 4) / 5);
for($x = (int)($width / 5); $x < $right; $x++){
$pixel = $localLuminances[(int)$x] & 0xff;
$buckets[$pixel >> self::LUMINANCE_SHIFT]++;
}
}
$blackPoint = $this->estimateBlackPoint($buckets);
// We delay reading the entire image luminance until the black point estimation succeeds.
// Although we end up reading four rows twice, it is consistent with our motto of
// "fail quickly" which is necessary for continuous scanning.
$localLuminances = $this->source->getMatrix();
for($y = 0; $y < $height; $y++){
$offset = $y * $width;
for($x = 0; $x < $width; $x++){
$pixel = (int)($localLuminances[$offset + $x] & 0xff);
if($pixel < $blackPoint){
$matrix->set($x, $y);
}
}
}
return $matrix;
}
/**
* Calculates a single black point for each block of pixels and saves it away.
* See the following thread for a discussion of this algorithm:
*
* @see http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0
*/
private function calculateBlackPoints(array $luminances, int $subWidth, int $subHeight, int $width, int $height):array{
$blackPoints = array_fill(0, $subHeight, 0);
foreach($blackPoints as $key => $point){
$blackPoints[$key] = array_fill(0, $subWidth, 0);
}
for($y = 0; $y < $subHeight; $y++){
$yoffset = ($y << self::BLOCK_SIZE_POWER);
$maxYOffset = $height - self::BLOCK_SIZE;
if($yoffset > $maxYOffset){
$yoffset = $maxYOffset;
}
for($x = 0; $x < $subWidth; $x++){
$xoffset = ($x << self::BLOCK_SIZE_POWER);
$maxXOffset = $width - self::BLOCK_SIZE;
if($xoffset > $maxXOffset){
$xoffset = $maxXOffset;
}
$sum = 0;
$min = 255;
$max = 0;
for($yy = 0, $offset = $yoffset * $width + $xoffset; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
$pixel = (int)($luminances[(int)($offset + $xx)]) & 0xff;
$sum += $pixel;
// still looking for good contrast
if($pixel < $min){
$min = $pixel;
}
if($pixel > $max){
$max = $pixel;
}
}
// short-circuit min/max tests once dynamic range is met
if($max - $min > self::MIN_DYNAMIC_RANGE){
// finish the rest of the rows quickly
for($yy++, $offset += $width; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
$sum += $luminances[$offset + $xx] & 0xff;
}
}
}
}
// The default estimate is the average of the values in the block.
$average = $sum >> (self::BLOCK_SIZE_POWER * 2);
if($max - $min <= self::MIN_DYNAMIC_RANGE){
// If variation within the block is low, assume this is a block with only light or only
// dark pixels. In that case we do not want to use the average, as it would divide this
// low contrast area into black and white pixels, essentially creating data out of noise.
//
// The default assumption is that the block is light/background. Since no estimate for
// the level of dark pixels exists locally, use half the min for the block.
$average = (int)($min / 2);
if($y > 0 && $x > 0){
// Correct the "white background" assumption for blocks that have neighbors by comparing
// the pixels in this block to the previously calculated black points. This is based on
// the fact that dark barcode symbology is always surrounded by some amount of light
// background for which reasonable black point estimates were made. The bp estimated at
// the boundaries is used for the interior.
// The (min < bp) is arbitrary but works better than other heuristics that were tried.
$averageNeighborBlackPoint = (int)(($blackPoints[$y - 1][$x] + (2 * $blackPoints[$y][$x - 1]) + $blackPoints[$y - 1][$x - 1]) / 4);
if($min < $averageNeighborBlackPoint){
$average = $averageNeighborBlackPoint;
}
}
}
$blackPoints[$y][$x] = (int)($average);
}
}
return $blackPoints;
}
/**
* For each block in the image, calculate the average black point using a 5x5 grid
* of the blocks around it. Also handles the corner cases (fractional blocks are computed based
* on the last pixels in the row/column which are also used in the previous block).
*/
private function calculateThresholdForBlock(
int $subWidth,
int $subHeight,
int $width,
int $height
):BitMatrix{
$matrix = new BitMatrix(max($width, $height));
$luminances = $this->source->getMatrix();
$blackPoints = $this->calculateBlackPoints($luminances, $subWidth, $subHeight, $width, $height);
for($y = 0; $y < $subHeight; $y++){
$yoffset = ($y << self::BLOCK_SIZE_POWER);
$maxYOffset = $height - self::BLOCK_SIZE;
if($yoffset > $maxYOffset){
$yoffset = $maxYOffset;
}
for($x = 0; $x < $subWidth; $x++){
$xoffset = ($x << self::BLOCK_SIZE_POWER);
$maxXOffset = $width - self::BLOCK_SIZE;
if($xoffset > $maxXOffset){
$xoffset = $maxXOffset;
}
$left = $this->cap($x, 2, $subWidth - 3);
$top = $this->cap($y, 2, $subHeight - 3);
$sum = 0;
for($z = -2; $z <= 2; $z++){
$blackRow = $blackPoints[$top + $z];
$sum += $blackRow[$left - 2] + $blackRow[$left - 1] + $blackRow[$left] + $blackRow[$left + 1] + $blackRow[$left + 2];
}
$average = (int)($sum / 25);
// Applies a single threshold to a block of pixels.
for($j = 0, $o = $yoffset * $width + $xoffset; $j < self::BLOCK_SIZE; $j++, $o += $width){
for($i = 0; $i < self::BLOCK_SIZE; $i++){
// Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0.
if(($luminances[$o + $i] & 0xff) <= $average){
$matrix->set($xoffset + $i, $yoffset + $j);
}
}
}
}
}
return $matrix;
}
private function cap(int $value, int $min, int $max):int{
if($value < $min){
return $min;
}
if($value > $max){
return $max;
}
return $value;
}
}
+204
View File
@@ -0,0 +1,204 @@
<?php
/**
* Class BitMatrix
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{MaskPattern, Version};
use InvalidArgumentException;
use function chillerlan\QRCode\Common\uRShift;
use function array_fill, count;
final class BitMatrix{
private int $dimension;
private int $rowSize;
private array $bits;
public function __construct(int $dimension){
$this->dimension = $dimension;
$this->rowSize = ((int)(($this->dimension + 0x1f) / 0x20));
$this->bits = array_fill(0, $this->rowSize * $this->dimension, 0);
}
/**
* <p>Sets the given bit to true.</p>
*
* @param int $x ; The horizontal component (i.e. which column)
* @param int $y ; The vertical component (i.e. which row)
*/
public function set(int $x, int $y):void{
$offset = (int)($y * $this->rowSize + ($x / 0x20));
$this->bits[$offset] ??= 0;
$this->bits[$offset] |= ($this->bits[$offset] |= 1 << ($x & 0x1f));
}
/**
* <p>Flips the given bit. 1 << (0xf9 & 0x1f)</p>
*
* @param int $x ; The horizontal component (i.e. which column)
* @param int $y ; The vertical component (i.e. which row)
*/
public function flip(int $x, int $y):void{
$offset = $y * $this->rowSize + (int)($x / 0x20);
$this->bits[$offset] = ($this->bits[$offset] ^ (1 << ($x & 0x1f)));
}
/**
* <p>Sets a square region of the bit matrix to true.</p>
*
* @param int $left ; The horizontal position to begin at (inclusive)
* @param int $top ; The vertical position to begin at (inclusive)
* @param int $width ; The width of the region
* @param int $height ; The height of the region
*
* @throws \InvalidArgumentException
*/
public function setRegion(int $left, int $top, int $width, int $height):void{
if($top < 0 || $left < 0){
throw new InvalidArgumentException('Left and top must be nonnegative');
}
if($height < 1 || $width < 1){
throw new InvalidArgumentException('Height and width must be at least 1');
}
$right = $left + $width;
$bottom = $top + $height;
if($bottom > $this->dimension || $right > $this->dimension){
throw new InvalidArgumentException('The region must fit inside the matrix');
}
for($y = $top; $y < $bottom; $y++){
$yOffset = $y * $this->rowSize;
for($x = $left; $x < $right; $x++){
$xOffset = $yOffset + (int)($x / 0x20);
$this->bits[$xOffset] = ($this->bits[$xOffset] |= 1 << ($x & 0x1f));
}
}
}
/**
* @return int The dimension (width/height) of the matrix
*/
public function getDimension():int{
return $this->dimension;
}
/**
* <p>Gets the requested bit, where true means black.</p>
*
* @param int $x The horizontal component (i.e. which column)
* @param int $y The vertical component (i.e. which row)
*
* @return bool value of given bit in matrix
*/
public function get(int $x, int $y):bool{
$offset = (int)($y * $this->rowSize + ($x / 0x20));
$this->bits[$offset] ??= 0;
return (uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0;
}
/**
* See ISO 18004:2006 Annex E
*/
public function buildFunctionPattern(Version $version):BitMatrix{
$dimension = $version->getDimension();
// @todo
$bitMatrix = new self($dimension);
// Top left finder pattern + separator + format
$bitMatrix->setRegion(0, 0, 9, 9);
// Top right finder pattern + separator + format
$bitMatrix->setRegion($dimension - 8, 0, 8, 9);
// Bottom left finder pattern + separator + format
$bitMatrix->setRegion(0, $dimension - 8, 9, 8);
// Alignment patterns
$apc = $version->getAlignmentPattern();
$max = count($apc);
for($x = 0; $x < $max; $x++){
$i = $apc[$x] - 2;
for($y = 0; $y < $max; $y++){
if(($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)){
// No alignment patterns near the three finder paterns
continue;
}
$bitMatrix->setRegion($apc[$y] - 2, $i, 5, 5);
}
}
// Vertical timing pattern
$bitMatrix->setRegion(6, 9, 1, $dimension - 17);
// Horizontal timing pattern
$bitMatrix->setRegion(9, 6, $dimension - 17, 1);
if($version->getVersionNumber() > 6){
// Version info, top right
$bitMatrix->setRegion($dimension - 11, 0, 3, 6);
// Version info, bottom left
$bitMatrix->setRegion(0, $dimension - 11, 6, 3);
}
return $bitMatrix;
}
/**
* Mirror the bit matrix in order to attempt a second reading.
*/
public function mirror():void{
for($x = 0; $x < $this->dimension; $x++){
for($y = $x + 1; $y < $this->dimension; $y++){
if($this->get($x, $y) !== $this->get($y, $x)){
$this->flip($y, $x);
$this->flip($x, $y);
}
}
}
}
/**
* <p>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.</p>
*
* <p>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.</p>
*
* <p>Implementations of this method reverse the data masking process applied to a QR Code and
* make its bits ready to read.</p>
*/
public function unmask(int $dimension, MaskPattern $maskPattern):void{
$mask = $maskPattern->getMask();
for($y = 0; $y < $dimension; $y++){
for($x = 0; $x < $dimension; $x++){
if($mask($x, $y) === 0){
$this->flip($x, $y);
}
}
}
}
}
+333
View File
@@ -0,0 +1,333 @@
<?php
/**
* Class BitMatrixParser
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use RuntimeException;
use chillerlan\QRCode\Common\{Version, FormatInformation};
use function chillerlan\QRCode\Common\numBitsDiffering;
use const PHP_INT_MAX;
/**
* @author Sean Owen
*/
final class BitMatrixParser{
private BitMatrix $bitMatrix;
private ?Version $parsedVersion = null;
private ?FormatInformation $parsedFormatInfo = null;
private bool $mirror = false;
/**
* @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix
*
* @throws \RuntimeException if dimension is not >= 21 and 1 mod 4
*/
public function __construct(BitMatrix $bitMatrix){
$dimension = $bitMatrix->getDimension();
if($dimension < 21 || ($dimension % 4) !== 1){
throw new RuntimeException('dimension is not >= 21, dimension mod 4 not 1');
}
$this->bitMatrix = $bitMatrix;
}
/**
* Prepare the parser for a mirrored operation.
* This flag has effect only on the {@link #readFormatInformation()} and the
* {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the
* {@link #mirror()} method should be called.
*
* @param bool mirror Whether to read version and format information mirrored.
*/
public function setMirror(bool $mirror):void{
$this->parsedVersion = null;
$this->parsedFormatInfo = null;
$this->mirror = $mirror;
}
/**
* Mirror the bit matrix in order to attempt a second reading.
*/
public function mirror():void{
$this->bitMatrix->mirror();
}
private function copyBit(int $i, int $j, int $versionBits):int{
$bit = $this->mirror
? $this->bitMatrix->get($j, $i)
: $this->bitMatrix->get($i, $j);
return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1;
}
/**
* <p>Reads the bits in the {@link BitMatrix} representing the finder pattern in the
* correct order in order to reconstruct the codewords bytes contained within the
* QR Code.</p>
*
* @return array bytes encoded within the QR Code
* @throws \RuntimeException if the exact number of bytes expected is not read
*/
public function readCodewords():array{
$formatInfo = $this->readFormatInformation();
$version = $this->readVersion();
// Get the data mask for the format used in this QR Code. This will exclude
// some bits from reading as we wind through the bit matrix.
$dimension = $this->bitMatrix->getDimension();
$this->bitMatrix->unmask($dimension, $formatInfo->getDataMask());
$functionPattern = $this->bitMatrix->buildFunctionPattern($version);
$readingUp = true;
$result = [];
$resultOffset = 0;
$currentByte = 0;
$bitsRead = 0;
// Read columns in pairs, from right to left
for($j = $dimension - 1; $j > 0; $j -= 2){
if($j === 6){
// Skip whole column with vertical alignment pattern;
// saves time and makes the other code proceed more cleanly
$j--;
}
// Read alternatingly from bottom to top then top to bottom
for($count = 0; $count < $dimension; $count++){
$i = $readingUp ? $dimension - 1 - $count : $count;
for($col = 0; $col < 2; $col++){
// Ignore bits covered by the function pattern
if(!$functionPattern->get($j - $col, $i)){
// Read a bit
$bitsRead++;
$currentByte <<= 1;
if($this->bitMatrix->get($j - $col, $i)){
$currentByte |= 1;
}
// If we've made a whole byte, save it off
if($bitsRead === 8){
$result[$resultOffset++] = $currentByte; //(byte)
$bitsRead = 0;
$currentByte = 0;
}
}
}
}
$readingUp = !$readingUp; // switch directions
}
if($resultOffset !== $version->getTotalCodewords()){
throw new RuntimeException('offset differs from total codewords for version');
}
return $result;
}
/**
* <p>Reads format information from one of its two locations within the QR Code.</p>
*
* @return \chillerlan\QRCode\Common\FormatInformation encapsulating the QR Code's format info
* @throws \RuntimeException if both format information locations cannot be parsed as
* the valid encoding of format information
*/
public function readFormatInformation():FormatInformation{
if($this->parsedFormatInfo !== null){
return $this->parsedFormatInfo;
}
// Read top-left format info bits
$formatInfoBits1 = 0;
for($i = 0; $i < 6; $i++){
$formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1);
}
// .. and skip a bit in the timing pattern ...
$formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1);
$formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1);
$formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1);
// .. and skip a bit in the timing pattern ...
for($j = 5; $j >= 0; $j--){
$formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1);
}
// Read the top-right/bottom-left pattern too
$dimension = $this->bitMatrix->getDimension();
$formatInfoBits2 = 0;
$jMin = $dimension - 7;
for($j = $dimension - 1; $j >= $jMin; $j--){
$formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2);
}
for($i = $dimension - 8; $i < $dimension; $i++){
$formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2);
}
$this->parsedFormatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2);
if($this->parsedFormatInfo !== null){
return $this->parsedFormatInfo;
}
// Should return null, but, some QR codes apparently do not mask this info.
// Try again by actually masking the pattern first.
$this->parsedFormatInfo = $this->doDecodeFormatInformation(
$formatInfoBits1 ^ FormatInformation::MASK_QR,
$formatInfoBits2 ^ FormatInformation::MASK_QR
);
if($this->parsedFormatInfo !== null){
return $this->parsedFormatInfo;
}
throw new RuntimeException('failed to read format info');
}
/**
* @param int $maskedFormatInfo1 format info indicator, with mask still applied
* @param int $maskedFormatInfo2 second copy of same info; both are checked at the same time
* to establish best match
*
* @return \chillerlan\QRCode\Common\FormatInformation information about the format it specifies, or {@code null}
* if doesn't seem to match any known pattern
*/
private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?FormatInformation{
// Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing
$bestDifference = PHP_INT_MAX;
$bestFormatInfo = 0;
foreach(FormatInformation::DECODE_LOOKUP as $decodeInfo){
[$maskedBits, $dataBits] = $decodeInfo;
if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){
// Found an exact match
return new FormatInformation($maskedBits);
}
$bitsDifference = numBitsDiffering($maskedFormatInfo1, $dataBits);
if($bitsDifference < $bestDifference){
$bestFormatInfo = $maskedBits;
$bestDifference = $bitsDifference;
}
if($maskedFormatInfo1 !== $maskedFormatInfo2){
// also try the other option
$bitsDifference = numBitsDiffering($maskedFormatInfo2, $dataBits);
if($bitsDifference < $bestDifference){
$bestFormatInfo = $maskedBits;
$bestDifference = $bitsDifference;
}
}
}
// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match
if($bestDifference <= 3){
return new FormatInformation($bestFormatInfo);
}
return null;
}
/**
* <p>Reads version information from one of its two locations within the QR Code.</p>
*
* @return \chillerlan\QRCode\Common\Version encapsulating the QR Code's version
* @throws \RuntimeException if both version information locations cannot be parsed as
* the valid encoding of version information
*/
public function readVersion():Version{
if($this->parsedVersion !== null){
return $this->parsedVersion;
}
$dimension = $this->bitMatrix->getDimension();
$provisionalVersion = ($dimension - 17) / 4;
if($provisionalVersion <= 6){
return new Version($provisionalVersion);
}
// Read top-right version info: 3 wide by 6 tall
$versionBits = 0;
$ijMin = $dimension - 11;
for($j = 5; $j >= 0; $j--){
for($i = $dimension - 9; $i >= $ijMin; $i--){
$versionBits = $this->copyBit($i, $j, $versionBits);
}
}
$this->parsedVersion = $this->decodeVersionInformation($versionBits);
if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){
return $this->parsedVersion;
}
// Hmm, failed. Try bottom left: 6 wide by 3 tall
$versionBits = 0;
for($i = 5; $i >= 0; $i--){
for($j = $dimension - 9; $j >= $ijMin; $j--){
$versionBits = $this->copyBit($i, $j, $versionBits);
}
}
$this->parsedVersion = $this->decodeVersionInformation($versionBits);
if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){
return $this->parsedVersion;
}
throw new RuntimeException('failed to read version');
}
private function decodeVersionInformation(int $versionBits):?Version{
$bestDifference = PHP_INT_MAX;
$bestVersion = 0;
for($i = 7; $i <= 40; $i++){
$targetVersion = new Version($i);
$targetVersionPattern = $targetVersion->getVersionPattern();
// Do the version info bits match exactly? done.
if($targetVersionPattern === $versionBits){
return $targetVersion;
}
// Otherwise see if this is the closest to a real version info bit string
// we have seen so far
$bitsDifference = numBitsDiffering($versionBits, $targetVersionPattern);
if($bitsDifference < $bestDifference){
$bestVersion = $i;
$bestDifference = $bitsDifference;
}
}
// We can tolerate up to 3 bits of error since no two version info codewords will
// differ in less than 8 bits.
if($bestDifference <= 3){
return new Version($bestVersion);
}
// If we didn't find a close enough match, fail
return null;
}
}
+336
View File
@@ -0,0 +1,336 @@
<?php
/**
* Class Decoder
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use Exception, InvalidArgumentException, RuntimeException;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, ECICharset, Mode, ReedSolomonDecoder, Version};
use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number};
use chillerlan\QRCode\Detector\Detector;
use function count, array_fill, mb_convert_encoding, mb_detect_encoding;
/**
* <p>The main class which implements QR Code decoding -- as opposed to locating and extracting
* the QR Code from an image.</p>
*
* @author Sean Owen
*/
final class Decoder{
# private const GB2312_SUBSET = 1;
/**
* <p>Decodes a QR Code represented as a {@link \chillerlan\QRCode\Decoder\BitMatrix}.
* A 1 or "true" is taken to mean a black module.</p>
*
* @param \chillerlan\QRCode\Decoder\LuminanceSource $source
*
* @return \chillerlan\QRCode\Decoder\DecoderResult text and bytes encoded within the QR Code
* @throws \Exception if the QR Code cannot be decoded
*/
public function decode(LuminanceSource $source):DecoderResult{
$matrix = (new Binarizer($source))->getBlackMatrix();
$bitMatrix = (new Detector($matrix))->detect();
$fe = null;
try{
// Construct a parser and read version, error-correction level
// clone the BitMatrix to avoid errors in case we run into mirroring
return $this->decodeParser(new BitMatrixParser(clone $bitMatrix));
}
catch(Exception $e){
$fe = $e;
}
try{
$parser = new BitMatrixParser(clone $bitMatrix);
// Will be attempting a mirrored reading of the version and format info.
$parser->setMirror(true);
// Preemptively read the version.
# $parser->readVersion();
// Preemptively read the format information.
# $parser->readFormatInformation();
/*
* Since we're here, this means we have successfully detected some kind
* of version and format information when mirrored. This is a good sign,
* that the QR code may be mirrored, and we should try once more with a
* mirrored content.
*/
// Prepare for a mirrored reading.
$parser->mirror();
return $this->decodeParser($parser);
}
catch(Exception $e){
// Throw the exception from the original reading
if($fe instanceof Exception){
throw $fe;
}
throw $e;
}
}
/**
* @param \chillerlan\QRCode\Decoder\BitMatrixParser $parser
*
* @return \chillerlan\QRCode\Decoder\DecoderResult
*/
private function decodeParser(BitMatrixParser $parser):DecoderResult{
$version = $parser->readVersion();
$eccLevel = $parser->readFormatInformation()->getErrorCorrectionLevel();
// Read raw codewords
$rawCodewords = $parser->readCodewords();
// Separate into data blocks
$dataBlocks = $this->getDataBlocks($rawCodewords, $version, $eccLevel);
$resultBytes = [];
$resultOffset = 0;
// Error-correct and copy data blocks together into a stream of bytes
foreach($dataBlocks as $dataBlock){
[$numDataCodewords, $codewordBytes] = $dataBlock;
$corrected = $this->correctErrors($codewordBytes, $numDataCodewords);
for($i = 0; $i < $numDataCodewords; $i++){
$resultBytes[$resultOffset++] = $corrected[$i];
}
}
// Decode the contents of that stream of bytes
return $this->decodeBitStream($resultBytes, $version, $eccLevel);
}
/**
* <p>When QR Codes use multiple data blocks, they are actually interleaved.
* That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This
* method will separate the data into original blocks.</p>
*
* @param array $rawCodewords bytes as read directly from the QR Code
* @param \chillerlan\QRCode\Common\Version $version version of the QR Code
* @param \chillerlan\QRCode\Common\EccLevel $eccLevel error-correction level of the QR Code
*
* @return array DataBlocks containing original bytes, "de-interleaved" from representation in the QR Code
* @throws \InvalidArgumentException
*/
private function getDataBlocks(array $rawCodewords, Version $version, EccLevel $eccLevel):array{
if(count($rawCodewords) !== $version->getTotalCodewords()){
throw new InvalidArgumentException('$rawCodewords differ from total codewords for version');
}
// Figure out the number and size of data blocks used by this version and
// error correction level
[$numEccCodewords, $eccBlocks] = $version->getRSBlocks($eccLevel);
// Now establish DataBlocks of the appropriate size and number of data codewords
$result = [];//new DataBlock[$totalBlocks];
$numResultBlocks = 0;
foreach($eccBlocks as $blockData){
[$numEccBlocks, $eccPerBlock] = $blockData;
for($i = 0; $i < $numEccBlocks; $i++, $numResultBlocks++){
$result[$numResultBlocks] = [$eccPerBlock, array_fill(0, $numEccCodewords + $eccPerBlock, 0)];
}
}
// All blocks have the same amount of data, except that the last n
// (where n may be 0) have 1 more byte. Figure out where these start.
$shorterBlocksTotalCodewords = count($result[0][1]);
$longerBlocksStartAt = count($result) - 1;
while($longerBlocksStartAt >= 0){
$numCodewords = count($result[$longerBlocksStartAt][1]);
if($numCodewords == $shorterBlocksTotalCodewords){
break;
}
$longerBlocksStartAt--;
}
$longerBlocksStartAt++;
$shorterBlocksNumDataCodewords = $shorterBlocksTotalCodewords - $numEccCodewords;
// The last elements of result may be 1 element longer;
// first fill out as many elements as all of them have
$rawCodewordsOffset = 0;
for($i = 0; $i < $shorterBlocksNumDataCodewords; $i++){
for($j = 0; $j < $numResultBlocks; $j++){
$result[$j][1][$i] = $rawCodewords[$rawCodewordsOffset++];
}
}
// Fill out the last data block in the longer ones
for($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++){
$result[$j][1][$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++];
}
// Now add in error correction blocks
$max = count($result[0][1]);
for($i = $shorterBlocksNumDataCodewords; $i < $max; $i++){
for($j = 0; $j < $numResultBlocks; $j++){
$iOffset = $j < $longerBlocksStartAt ? $i : $i + 1;
$result[$j][1][$iOffset] = $rawCodewords[$rawCodewordsOffset++];
}
}
return $result;
}
/**
* <p>Given data and error-correction codewords received, possibly corrupted by errors, attempts to
* correct the errors in-place using Reed-Solomon error correction.</p>
*/
private function correctErrors(array $codewordBytes, int $numDataCodewords):array{
// First read into an array of ints
$codewordsInts = [];
foreach($codewordBytes as $i => $codewordByte){
$codewordsInts[$i] = $codewordByte & 0xFF;
}
$decoded = (new ReedSolomonDecoder)->decode($codewordsInts, (count($codewordBytes) - $numDataCodewords));
// Copy back into array of bytes -- only need to worry about the bytes that were data
// We don't care about errors in the error-correction codewords
for($i = 0; $i < $numDataCodewords; $i++){
$codewordBytes[$i] = $decoded[$i];
}
return $codewordBytes;
}
/**
* @throws \RuntimeException
*/
private function decodeBitStream(array $bytes, Version $version, EccLevel $ecLevel):DecoderResult{
$bits = new BitBuffer($bytes);
$symbolSequence = -1;
$parityData = -1;
$versionNumber = $version->getVersionNumber();
$result = '';
$eciCharset = null;
# $fc1InEffect = false;
// While still another segment to read...
while($bits->available() >= 4){
$datamode = $bits->read(4); // mode is encoded by 4 bits
// OK, assume we're done. Really, a TERMINATOR mode should have been recorded here
if($datamode === Mode::DATA_TERMINATOR){
break;
}
if($datamode === Mode::DATA_ECI){
// Count doesn't apply to ECI
$value = ECI::parseValue($bits);
$eciCharset = new ECICharset($value);
}
/** @noinspection PhpStatementHasEmptyBodyInspection */
elseif($datamode === Mode::DATA_FNC1_FIRST || $datamode === Mode::DATA_FNC1_SECOND){
// We do little with FNC1 except alter the parsed result a bit according to the spec
# $fc1InEffect = true;
}
elseif($datamode === Mode::DATA_STRCTURED_APPEND){
if($bits->available() < 16){
throw new RuntimeException('structured append: not enough bits left');
}
// sequence number and parity is added later to the result metadata
// Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue
$symbolSequence = $bits->read(8);
$parityData = $bits->read(8);
}
else{
// First handle Hanzi mode which does not start with character count
/* if($datamode === Mode::DATA_HANZI){
//chinese mode contains a sub set indicator right after mode indicator
$subset = $bits->read(4);
$length = $bits->read(Mode::getLengthBitsForVersion($datamode, $versionNumber));
if($subset === self::GB2312_SUBSET){
$result .= $this->decodeHanziSegment($bits, $length);
}
}*/
# else{
// "Normal" QR code modes:
if($datamode === Mode::DATA_NUMBER){
$result .= Number::decodeSegment($bits, $versionNumber);
}
elseif($datamode === Mode::DATA_ALPHANUM){
$str = AlphaNum::decodeSegment($bits, $versionNumber);
// See section 6.4.8.1, 6.4.8.2
/* if($fc1InEffect){
$start = \strlen($str);
// We need to massage the result a bit if in an FNC1 mode:
for($i = $start; $i < $start; $i++){
if($str[$i] === '%'){
if($i < $start - 1 && $str[$i + 1] === '%'){
// %% is rendered as %
$str = \substr_replace($str, '', $i + 1, 1);//deleteCharAt(i + 1);
}
# else{
// In alpha mode, % should be converted to FNC1 separator 0x1D @todo
# $str = setCharAt($i, \chr(0x1D)); // ???
# }
}
}
}
*/
$result .= $str;
}
elseif($datamode === Mode::DATA_BYTE){
$str = Byte::decodeSegment($bits, $versionNumber);
if($eciCharset !== null){
$encoding = $eciCharset->getName();
if($encoding === null){
// The spec isn't clear on this mode; see
// section 6.4.5: t does not say which encoding to assuming
// upon decoding. I have seen ISO-8859-1 used as well as
// Shift_JIS -- without anything like an ECI designator to
// give a hint.
$encoding = mb_detect_encoding($str, ['ISO-8859-1', 'SJIS', 'UTF-8']);
}
$eciCharset = null;
$str = mb_convert_encoding($str, $encoding);
}
$result .= $str;
}
elseif($datamode === Mode::DATA_KANJI){
$result .= Kanji::decodeSegment($bits, $versionNumber);
}
else{
throw new RuntimeException('invalid data mode');
}
# }
}
}
return new DecoderResult($bytes, $result, $version, $ecLevel, $symbolSequence, $parityData);
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
/**
* Class DecoderResult
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{EccLevel, Version};
/**
* <p>Encapsulates the result of decoding a matrix of bits. This typically
* applies to 2D barcode formats. For now it contains the raw bytes obtained,
* as well as a String interpretation of those bytes, if applicable.</p>
*
* @author Sean Owen
*/
final class DecoderResult{
private array $rawBytes;
private string $text;
private Version $version;
private EccLevel $eccLevel;
private int $structuredAppendParity;
private int $structuredAppendSequenceNumber;
public function __construct(
array $rawBytes,
string $text,
Version $version,
EccLevel $eccLevel,
int $saSequence = -1,
int $saParity = -1
){
$this->rawBytes = $rawBytes;
$this->text = $text;
$this->version = $version;
$this->eccLevel = $eccLevel;
$this->structuredAppendParity = $saParity;
$this->structuredAppendSequenceNumber = $saSequence;
}
/**
* @return int[] raw bytes encoded by the barcode, if applicable, otherwise {@code null}
*/
public function getRawBytes():array{
return $this->rawBytes;
}
/**
* @return string raw text encoded by the barcode
*/
public function getText():string{
return $this->text;
}
public function __toString():string{
return $this->text;
}
public function getVersion():Version{
return $this->version;
}
public function getEccLevel():EccLevel{
return $this->eccLevel;
}
public function hasStructuredAppend():bool{
return $this->structuredAppendParity >= 0 && $this->structuredAppendSequenceNumber >= 0;
}
public function getStructuredAppendParity():int{
return $this->structuredAppendParity;
}
public function getStructuredAppendSequenceNumber():int{
return $this->structuredAppendSequenceNumber;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* Class GDLuminanceSource
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Decoder;
use InvalidArgumentException;
use function get_resource_type, imagecolorat, imagecolorsforindex, imagesx, imagesy, is_resource;
use const PHP_MAJOR_VERSION;
/**
* This class is used to help decode images from files which arrive as GD Resource
* It does not support rotation.
*/
final class GDLuminanceSource extends LuminanceSource{
/**
* @var resource|\GdImage
* @phan-suppress PhanUndeclaredTypeProperty
*/
private $gdImage;
/**
* GDLuminanceSource constructor.
*
* @param resource|\GdImage $gdImage
* @phan-suppress PhanUndeclaredTypeParameter
*
* @throws \InvalidArgumentException
*/
public function __construct($gdImage){
/**
* @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection
* @phan-suppress PhanUndeclaredClassInstanceof
*/
if(
(PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage)
|| (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd'))
){
throw new InvalidArgumentException('Invalid GD image source.');
}
parent::__construct(imagesx($gdImage), imagesy($gdImage));
$this->gdImage = $gdImage;
$this->setLuminancePixels();
}
private function setLuminancePixels():void{
for($j = 0; $j < $this->height; $j++){
for($i = 0; $i < $this->width; $i++){
$argb = imagecolorat($this->gdImage, $i, $j);
$pixel = imagecolorsforindex($this->gdImage, $argb);
$this->setLuminancePixel($pixel['red'], $pixel['green'], $pixel['blue']);
}
}
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* Class IMagickLuminanceSource
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Decoder;
use Imagick, InvalidArgumentException;
use function count;
/**
* This class is used to help decode images from files which arrive as Imagick Resource
* It does not support rotation.
*/
final class IMagickLuminanceSource extends LuminanceSource{
private Imagick $imagick;
/**
* IMagickLuminanceSource constructor.
*
* @param \Imagick $imagick
*
* @throws \InvalidArgumentException
*/
public function __construct(Imagick $imagick){
parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight());
$this->imagick = $imagick;
$this->setLuminancePixels();
}
private function setLuminancePixels():void{
$this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY);
$pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR);
$countPixels = count($pixels);
for($i = 0; $i < $countPixels; $i += 3){
$this->setLuminancePixel($pixels[$i] & 0xff, $pixels[$i + 1] & 0xff, $pixels[$i + 2] & 0xff);
}
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
/**
* Class LuminanceSource
*
* @created 24.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use InvalidArgumentException;
use function chillerlan\QRCode\Common\arraycopy;
/**
* The purpose of this class hierarchy is to abstract different bitmap implementations across
* platforms into a standard interface for requesting greyscale luminance values. The interface
* only provides immutable methods; therefore crop and rotation create copies. This is to ensure
* that one Reader does not modify the original luminance source and leave it in an unknown state
* for other Readers in the chain.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
abstract class LuminanceSource{
protected array $luminances;
protected int $width;
protected int $height;
public function __construct(int $width, int $height){
$this->width = $width;
$this->height = $height;
// In order to measure pure decoding speed, we convert the entire image to a greyscale array
// up front, which is the same as the Y channel of the YUVLuminanceSource in the real app.
$this->luminances = [];
// @todo: grayscale?
//$this->luminances = $this->grayScaleToBitmap($this->grayscale());
}
/**
* Fetches luminance data for the underlying bitmap. Values should be fetched using:
* {@code int luminance = array[y * width + x] & 0xff}
*
* @return array A row-major 2D array of luminance values. Do not use result.length as it may be
* larger than width * height bytes on some platforms. Do not modify the contents
* of the result.
*/
public function getMatrix():array{
return $this->luminances;
}
/**
* @return int The width of the bitmap.
*/
public function getWidth():int{
return $this->width;
}
/**
* @return int The height of the bitmap.
*/
public function getHeight():int{
return $this->height;
}
/**
* Fetches one row of luminance data from the underlying platform's bitmap. Values range from
* 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have
* to bitwise and with 0xff for each value. It is preferable for implementations of this method
* to only fetch this row rather than the whole image, since no 2D Readers may be installed and
* getMatrix() may never be called.
*
* @param int $y The row to fetch, which must be in [0,getHeight())
*
* @return array An array containing the luminance data.
*/
public function getRow(int $y):array{
if($y < 0 || $y >= $this->getHeight()){
throw new InvalidArgumentException('Requested row is outside the image: '.$y);
}
return arraycopy($this->luminances, $y * $this->width, [], 0, $this->width);
}
/**
* @param int $r
* @param int $g
* @param int $b
*
* @return void
*/
protected function setLuminancePixel(int $r, int $g, int $b):void{
$this->luminances[] = $r === $g && $g === $b
// Image is already greyscale, so pick any channel.
? $r // (($r + 128) % 256) - 128;
// Calculate luminance cheaply, favoring green.
: ($r + 2 * $g + $b) / 4; // (((($r + 2 * $g + $b) / 4) + 128) % 256) - 128;
}
}