mirror of
https://github.com/chillerlan/php-qrcode.git
synced 2026-10-16 16:55:28 +00:00
✨ init from https://github.com/codemasher/php-qrcode-decoder/commit/5798a53268f14eb1d7d70098d23db0a81b25f98b
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user