diff --git a/src/Common/ReedSolomon.php b/src/Common/ReedSolomon.php new file mode 100644 index 000000000..855ecfbcd --- /dev/null +++ b/src/Common/ReedSolomon.php @@ -0,0 +1,125 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\QRCode\Common; + +use chillerlan\QRCode\Helpers\BitBuffer; +use chillerlan\QRCode\Helpers\Polynomial; + +use SplFixedArray; +use function array_fill, array_merge, count, max; + +/** + * ISO/IEC 18004:2000 Section 8.5 ff + * + * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding + */ +final class ReedSolomon{ + + private Version $version; + private EccLevel $eccLevel; + + private SplFixedArray $interleavedData; + private int $interleavedDataIndex; + + /** + * ReedSolomon constructor. + */ + public function __construct(Version $version, EccLevel $eccLevel){ + $this->version = $version; + $this->eccLevel = $eccLevel; + } + + /** + * ECC interleaving + * + * @return \SplFixedArray + */ + public function interleaveEcBytes(BitBuffer $bitBuffer):SplFixedArray{ + [$l1, $l2, $b1, $b2] = $this->version->getRSBlocks($this->eccLevel->getLevel()); + + $numRsBlocks = $l1 + $l2; + $ecBytes = new SplFixedArray($numRsBlocks); + $rsBlocks = array_fill(0, $l1, [$b1, $b2]); + + if($l2 > 0){ + $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1])); + } + + $dataBytes = SplFixedArray::fromArray($rsBlocks); + $maxDataBytes = 0; + $maxEcBytes = 0; + $dataByteOffset = 0; + $bitBufferData = $bitBuffer->getBuffer(); + + foreach($rsBlocks as $key => $block){ + [$rsBlockTotal, $dataByteCount] = $block; + + $ecByteCount = $rsBlockTotal - $dataByteCount; + $maxDataBytes = max($maxDataBytes, $dataByteCount); + $maxEcBytes = max($maxEcBytes, $ecByteCount); + $dataBytes[$key] = new SplFixedArray($dataByteCount); + + foreach($dataBytes[$key] as $i => $_){ + $dataBytes[$key][$i] = $bitBufferData[$i + $dataByteOffset] & 0xff; + } + + $rsPoly = new Polynomial; + $modPoly = new Polynomial; + + for($i = 0; $i < $ecByteCount; $i++){ + $modPoly->setNum([1, $modPoly->gexp($i)]); + $rsPoly->multiply($modPoly->getNum()); + } + + $rsPolyCount = count($rsPoly->getNum()) - 1; + + $modPoly + ->setNum($dataBytes[$key]->toArray(), $rsPolyCount) + ->mod($rsPoly->getNum()) + ; + + $ecBytes[$key] = new SplFixedArray($rsPolyCount); + $num = $modPoly->getNum(); + $count = count($num) - count($ecBytes[$key]); + + foreach($ecBytes[$key] as $i => $_){ + $modIndex = $i + $count; + $ecBytes[$key][$i] = $modIndex >= 0 ? $num[$modIndex] : 0; + } + + $dataByteOffset += $dataByteCount; + } + + $this->interleavedData = new SplFixedArray($this->version->getTotalCodewords()); + $this->interleavedDataIndex = 0; + + $this->interleave($dataBytes, $maxDataBytes, $numRsBlocks); + $this->interleave($ecBytes, $maxEcBytes, $numRsBlocks); + + return $this->interleavedData; + } + + /** + * + */ + private function interleave(SplFixedArray $byteArray, int $maxBytes, int $numRsBlocks):void{ + for($x = 0; $x < $maxBytes; $x++){ + for($y = 0; $y < $numRsBlocks; $y++){ + if($x < count($byteArray[$y])){ + $this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x]; + } + } + } + } + +} diff --git a/src/Data/QRData.php b/src/Data/QRData.php index f42000333..7df57874d 100644 --- a/src/Data/QRData.php +++ b/src/Data/QRData.php @@ -12,12 +12,12 @@ namespace chillerlan\QRCode\Data; -use chillerlan\QRCode\Common\{EccLevel, Mode, Version}; +use chillerlan\QRCode\Common\{EccLevel, Mode, ReedSolomon, Version}; use chillerlan\QRCode\QRCode; -use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial}; +use chillerlan\QRCode\Helpers\BitBuffer; use chillerlan\Settings\SettingsContainerInterface; -use function array_fill, array_merge, count, max, range, sprintf; +use function range, sprintf; /** * Processes the binary data and maps it on a matrix which is then being returned @@ -29,16 +29,6 @@ class QRData{ */ protected Version $version; - /** - * ECC temp data - */ - protected array $ecdata; - - /** - * ECC temp data - */ - protected array $dcdata; - /** * @var \chillerlan\QRCode\Data\QRDataModeInterface[] */ @@ -109,9 +99,12 @@ class QRData{ * returns a fresh matrix object with the data written for the given $maskPattern */ public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{ + $rs = new ReedSolomon($this->version, $this->eccLevel); + $data = $rs->interleaveEcBytes($this->bitBuffer); + return (new QRMatrix($this->version, $this->eccLevel)) ->init($maskPattern, $test) - ->mapData($this->maskECC(), $maskPattern) + ->mapData($data, $maskPattern) ; } @@ -223,101 +216,4 @@ class QRData{ } - /** - * ECC masking - * - * ISO/IEC 18004:2000 Section 8.5 ff - * - * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding - */ - protected function maskECC():array{ - [$l1, $l2, $b1, $b2] = $this->eccLevel->getRSBlocks($this->version->getVersionNumber()); - - $rsBlocks = array_fill(0, $l1, [$b1, $b2]); - $rsCount = $l1 + $l2; - $this->ecdata = array_fill(0, $rsCount, []); - $this->dcdata = $this->ecdata; - - if($l2 > 0){ - $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1])); - } - - $totalCodeCount = 0; - $maxDcCount = 0; - $maxEcCount = 0; - $offset = 0; - - $bitBuffer = $this->bitBuffer->getBuffer(); - - foreach($rsBlocks as $key => $block){ - [$rsBlockTotal, $dcCount] = $block; - - $ecCount = $rsBlockTotal - $dcCount; - $maxDcCount = max($maxDcCount, $dcCount); - $maxEcCount = max($maxEcCount, $ecCount); - $this->dcdata[$key] = array_fill(0, $dcCount, null); - - foreach($this->dcdata[$key] as $a => $_z){ - $this->dcdata[$key][$a] = 0xff & $bitBuffer[$a + $offset]; - } - - [$num, $add] = $this->poly($key, $ecCount); - - foreach($this->ecdata[$key] as $c => $_){ - $modIndex = $c + $add; - $this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0; - } - - $offset += $dcCount; - $totalCodeCount += $rsBlockTotal; - } - - $data = array_fill(0, $totalCodeCount, null); - $index = 0; - - $mask = function(array $arr, int $count) use (&$data, &$index, $rsCount):void{ - for($x = 0; $x < $count; $x++){ - for($y = 0; $y < $rsCount; $y++){ - if($x < count($arr[$y])){ - $data[$index] = $arr[$y][$x]; - $index++; - } - } - } - }; - - $mask($this->dcdata, $maxDcCount); - $mask($this->ecdata, $maxEcCount); - - return $data; - } - - /** - * helper method for the polynomial operations - */ - protected function poly(int $key, int $count):array{ - $rsPoly = new Polynomial; - $modPoly = new Polynomial; - - for($i = 0; $i < $count; $i++){ - $modPoly->setNum([1, $modPoly->gexp($i)]); - $rsPoly->multiply($modPoly->getNum()); - } - - $rsPolyCount = count($rsPoly->getNum()); - - $modPoly - ->setNum($this->dcdata[$key], $rsPolyCount - 1) - ->mod($rsPoly->getNum()) - ; - - $this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null); - $num = $modPoly->getNum(); - - return [ - $num, - count($num) - count($this->ecdata[$key]), - ]; - } - } diff --git a/src/Data/QRMatrix.php b/src/Data/QRMatrix.php index cb9def0f0..bb7bf82d3 100755 --- a/src/Data/QRMatrix.php +++ b/src/Data/QRMatrix.php @@ -16,6 +16,7 @@ use chillerlan\QRCode\Common\{EccLevel, Version}; use chillerlan\QRCode\QRCode; use Closure; +use SplFixedArray; use function array_fill, array_push, array_unshift, count, floor, max, min, range; /** @@ -493,14 +494,14 @@ final class QRMatrix{ * * @see \chillerlan\QRCode\Data\QRData::maskECC() * - * @param int[] $data - * @param int $maskPattern + * @param \SplFixedArray $data + * @param int $maskPattern * * @return \chillerlan\QRCode\Data\QRMatrix */ - public function mapData(array $data, int $maskPattern):QRMatrix{ + public function mapData(SplFixedArray $data, int $maskPattern):QRMatrix{ $this->maskPattern = $maskPattern; - $byteCount = count($data); + $byteCount = $data->count(); $y = $this->moduleCount - 1; $inc = -1; $byteIndex = 0; diff --git a/tests/Data/DatainterfaceTestAbstract.php b/tests/Data/DatainterfaceTestAbstract.php index b69fad70b..0e0361dfc 100644 --- a/tests/Data/DatainterfaceTestAbstract.php +++ b/tests/Data/DatainterfaceTestAbstract.php @@ -52,14 +52,18 @@ abstract class DatainterfaceTestAbstract extends TestCase{ /** * Tests ecc masking and verifies against a sample */ - public function testMaskEcc():void{ +/* public function testMaskEcc():void{ $this->dataInterface->setData([$this->testdata]); $maskECC = $this->reflection->getMethod('maskECC'); $maskECC->setAccessible(true); - $this::assertSame($this->expected, $maskECC->invoke($this->dataInterface)); - } + $bitBuffer = $this->reflection->getProperty('bitBuffer'); + $bitBuffer->setAccessible(true); + $bb = $bitBuffer->getValue($this->dataInterface); + + $this::assertSame($this->expected, $maskECC->invokeArgs($this->dataInterface, [$bb->getBuffer()])); + }*/ /** * @see testInitMatrix()