Passed
Push — main ( 06a3ca...9c0f6a )
by smiley
01:54
created

ReedSolomonDecoder   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 115
c 3
b 0
f 0
dl 0
loc 280
rs 9.2
wmc 40

8 Methods

Rating   Name   Duplication   Size   Complexity  
A correctErrors() 0 17 3
A __construct() 0 3 1
A findErrorMagnitudes() 0 23 5
B decodeWords() 0 39 6
C deinterleaveRawBytes() 0 64 11
A runEuclideanAlgorithm() 0 39 5
A decode() 0 15 3
A findErrorLocations() 0 23 6

How to fix   Complexity   

Complex Class

Complex classes like ReedSolomonDecoder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ReedSolomonDecoder, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Class ReedSolomonDecoder
4
 *
5
 * @created      24.01.2021
6
 * @author       ZXing Authors
7
 * @author       Smiley <[email protected]>
8
 * @copyright    2021 Smiley
9
 * @license      Apache-2.0
10
 */
11
12
namespace chillerlan\QRCode\Common;
13
14
use chillerlan\QRCode\QRCodeException;
15
use function array_fill, array_reverse, count;
16
17
/**
18
 * Implements Reed-Solomon decoding, as the name implies.
19
 *
20
 * The algorithm will not be explained here, but the following references were helpful
21
 * in creating this implementation:
22
 *
23
 * - Bruce Maggs "Decoding Reed-Solomon Codes" (see discussion of Forney's Formula)
24
 *   http://www.cs.cmu.edu/afs/cs.cmu.edu/project/pscico-guyb/realworld/www/rs_decode.ps
25
 * - J.I. Hall. "Chapter 5. Generalized Reed-Solomon Codes" (see discussion of Euclidean algorithm)
26
 *   https://users.math.msu.edu/users/halljo/classes/codenotes/GRS.pdf
27
 *
28
 * Much credit is due to William Rucklidge since portions of this code are an indirect
29
 * port of his C++ Reed-Solomon implementation.
30
 *
31
 * @author Sean Owen
32
 * @author William Rucklidge
33
 * @author sanfordsquires
34
 */
35
final class ReedSolomonDecoder{
36
37
	private Version  $version;
38
	private EccLevel $eccLevel;
39
40
	/**
41
	 * ReedSolomonDecoder constructor
42
	 */
43
	public function __construct(Version $version, EccLevel $eccLevel){
44
		$this->version  = $version;
45
		$this->eccLevel = $eccLevel;
46
	}
47
48
	/**
49
	 * Error-correct and copy data blocks together into a stream of bytes
50
	 */
51
	public function decode(array $rawCodewords):BitBuffer{
52
		$dataBlocks  = $this->deinterleaveRawBytes($rawCodewords);
53
		$dataBytes   = [];
54
55
		foreach($dataBlocks as $dataBlock){
56
			[$numDataCodewords, $codewordBytes] = $dataBlock;
57
58
			$corrected = $this->correctErrors($codewordBytes, $numDataCodewords);
59
60
			for($i = 0; $i < $numDataCodewords; $i++){
61
				$dataBytes[] = $corrected[$i];
62
			}
63
		}
64
65
		return new BitBuffer($dataBytes);
66
	}
67
68
	/**
69
	 * When QR Codes use multiple data blocks, they are actually interleaved.
70
	 * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This
71
	 * method will separate the data into original blocks.
72
	 *
73
	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
74
	 */
75
	private function deinterleaveRawBytes(array $rawCodewords):array{
76
		// Figure out the number and size of data blocks used by this version and
77
		// error correction level
78
		[$numEccCodewords, $eccBlocks] = $this->version->getRSBlocks($this->eccLevel);
79
80
		// Now establish DataBlocks of the appropriate size and number of data codewords
81
		$result          = [];//new DataBlock[$totalBlocks];
82
		$numResultBlocks = 0;
83
84
		foreach($eccBlocks as $blockData){
85
			[$numEccBlocks, $eccPerBlock] = $blockData;
86
87
			for($i = 0; $i < $numEccBlocks; $i++, $numResultBlocks++){
88
				$result[$numResultBlocks] = [$eccPerBlock, array_fill(0, $numEccCodewords + $eccPerBlock, 0)];
89
			}
90
		}
91
92
		// All blocks have the same amount of data, except that the last n
93
		// (where n may be 0) have 1 more byte. Figure out where these start.
94
		/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
95
		$shorterBlocksTotalCodewords = count($result[0][1]);
96
		$longerBlocksStartAt         = count($result) - 1;
97
98
		while($longerBlocksStartAt >= 0){
99
			$numCodewords = count($result[$longerBlocksStartAt][1]);
100
101
			if($numCodewords == $shorterBlocksTotalCodewords){
102
				break;
103
			}
104
105
			$longerBlocksStartAt--;
106
		}
107
108
		$longerBlocksStartAt++;
109
110
		$shorterBlocksNumDataCodewords = $shorterBlocksTotalCodewords - $numEccCodewords;
111
		// The last elements of result may be 1 element longer;
112
		// first fill out as many elements as all of them have
113
		$rawCodewordsOffset = 0;
114
115
		for($i = 0; $i < $shorterBlocksNumDataCodewords; $i++){
116
			for($j = 0; $j < $numResultBlocks; $j++){
117
				$result[$j][1][$i] = $rawCodewords[$rawCodewordsOffset++];
118
			}
119
		}
120
121
		// Fill out the last data block in the longer ones
122
		for($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++){
123
			$result[$j][1][$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++];
124
		}
125
126
		// Now add in error correction blocks
127
		/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
128
		$max = count($result[0][1]);
129
130
		for($i = $shorterBlocksNumDataCodewords; $i < $max; $i++){
131
			for($j = 0; $j < $numResultBlocks; $j++){
132
				$iOffset                 = $j < $longerBlocksStartAt ? $i : $i + 1;
133
				$result[$j][1][$iOffset] = $rawCodewords[$rawCodewordsOffset++];
134
			}
135
		}
136
137
		// DataBlocks containing original bytes, "de-interleaved" from representation in the QR Code
138
		return $result;
139
	}
140
141
	/**
142
	 * Given data and error-correction codewords received, possibly corrupted by errors, attempts to
143
	 * correct the errors in-place using Reed-Solomon error correction.
144
	 */
145
	private function correctErrors(array $codewordBytes, int $numDataCodewords):array{
146
		// First read into an array of ints
147
		$codewordsInts = [];
148
149
		foreach($codewordBytes as $codewordByte){
150
			$codewordsInts[] = $codewordByte & 0xFF;
151
		}
152
153
		$decoded = $this->decodeWords($codewordsInts, (count($codewordBytes) - $numDataCodewords));
154
155
		// Copy back into array of bytes -- only need to worry about the bytes that were data
156
		// We don't care about errors in the error-correction codewords
157
		for($i = 0; $i < $numDataCodewords; $i++){
158
			$codewordBytes[$i] = $decoded[$i];
159
		}
160
161
		return $codewordBytes;
162
	}
163
164
	/**
165
	 * Decodes given set of received codewords, which include both data and error-correction
166
	 * codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place,
167
	 * in the input.
168
	 *
169
	 * @param array $received        data and error-correction codewords
170
	 * @param int   $numEccCodewords number of error-correction codewords available
171
	 *
172
	 * @return int[]
173
	 * @throws \chillerlan\QRCode\QRCodeException if decoding fails for any reason
174
	 */
175
	private function decodeWords(array $received, int $numEccCodewords):array{
176
		$poly                 = new GenericGFPoly($received);
177
		$syndromeCoefficients = [];
178
		$error                = false;
179
180
		for($i = 0; $i < $numEccCodewords; $i++){
181
			$syndromeCoefficients[$i] = $poly->evaluateAt(GF256::exp($i));
182
183
			if($syndromeCoefficients[$i] !== 0){
184
				$error = true;
185
			}
186
		}
187
188
		if(!$error){
0 ignored issues
show
introduced by
The condition $error is always false.
Loading history...
189
			return $received;
190
		}
191
192
		[$sigma, $omega] = $this->runEuclideanAlgorithm(
193
			GF256::buildMonomial($numEccCodewords, 1),
194
			new GenericGFPoly(array_reverse($syndromeCoefficients)),
195
			$numEccCodewords
196
		);
197
198
		$errorLocations      = $this->findErrorLocations($sigma);
199
		$errorMagnitudes     = $this->findErrorMagnitudes($omega, $errorLocations);
200
		$errorLocationsCount = count($errorLocations);
201
		$receivedCount       = count($received);
202
203
		for($i = 0; $i < $errorLocationsCount; $i++){
204
			$position = $receivedCount - 1 - GF256::log($errorLocations[$i]);
205
206
			if($position < 0){
207
				throw new QRCodeException('Bad error location');
208
			}
209
210
			$received[$position] ^= $errorMagnitudes[$i];
211
		}
212
213
		return $received;
214
	}
215
216
	/**
217
	 * @return \chillerlan\QRCode\Common\GenericGFPoly[] [sigma, omega]
218
	 * @throws \chillerlan\QRCode\QRCodeException
219
	 */
220
	private function runEuclideanAlgorithm(GenericGFPoly $a, GenericGFPoly $b, int $R):array{
221
		// Assume a's degree is >= b's
222
		if($a->getDegree() < $b->getDegree()){
223
			$temp = $a;
224
			$a    = $b;
225
			$b    = $temp;
226
		}
227
228
		$rLast = $a;
229
		$r     = $b;
230
		$tLast = new GenericGFPoly([0]);
231
		$t     = new GenericGFPoly([1]);
232
233
		// Run Euclidean algorithm until r's degree is less than R/2
234
		while(2 * $r->getDegree() >= $R){
235
			$rLastLast = $rLast;
236
			$tLastLast = $tLast;
237
			$rLast     = $r;
238
			$tLast     = $t;
239
240
			// Divide rLastLast by rLast, with quotient in q and remainder in r
241
			[$q, $r] = $rLastLast->divide($rLast);
242
243
			$t = $q->multiply($tLast)->addOrSubtract($tLastLast);
244
245
			if($r->getDegree() >= $rLast->getDegree()){
246
				throw new QRCodeException('Division algorithm failed to reduce polynomial?');
247
			}
248
		}
249
250
		$sigmaTildeAtZero = $t->getCoefficient(0);
251
252
		if($sigmaTildeAtZero === 0){
253
			throw new QRCodeException('sigmaTilde(0) was zero');
254
		}
255
256
		$inverse = GF256::inverse($sigmaTildeAtZero);
257
258
		return [$t->multiplyInt($inverse), $r->multiplyInt($inverse)];
259
	}
260
261
	/**
262
	 * @throws \chillerlan\QRCode\QRCodeException
263
	 */
264
	private function findErrorLocations(GenericGFPoly $errorLocator):array{
265
		// This is a direct application of Chien's search
266
		$numErrors = $errorLocator->getDegree();
267
268
		if($numErrors === 1){ // shortcut
269
			return [$errorLocator->getCoefficient(1)];
270
		}
271
272
		$result = array_fill(0, $numErrors, 0);
273
		$e      = 0;
274
275
		for($i = 1; $i < 256 && $e < $numErrors; $i++){
276
			if($errorLocator->evaluateAt($i) === 0){
277
				$result[$e] = GF256::inverse($i);
278
				$e++;
279
			}
280
		}
281
282
		if($e !== $numErrors){
283
			throw new QRCodeException('Error locator degree does not match number of roots');
284
		}
285
286
		return $result;
287
	}
288
289
	/**
290
	 *
291
	 */
292
	private function findErrorMagnitudes(GenericGFPoly $errorEvaluator, array $errorLocations):array{
293
		// This is directly applying Forney's Formula
294
		$s      = count($errorLocations);
295
		$result = [];
296
297
		for($i = 0; $i < $s; $i++){
298
			$xiInverse   = GF256::inverse($errorLocations[$i]);
299
			$denominator = 1;
300
301
			for($j = 0; $j < $s; $j++){
302
				if($i !== $j){
303
#					$denominator = GF256::multiply($denominator, GF256::addOrSubtract(1, GF256::multiply($errorLocations[$j], $xiInverse)));
304
					// Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug.
305
					// Below is a funny-looking workaround from Steven Parkes
306
					$term        = GF256::multiply($errorLocations[$j], $xiInverse);
307
					$denominator = GF256::multiply($denominator, (($term & 0x1) === 0 ? $term | 1 : $term & ~1));
308
				}
309
			}
310
311
			$result[$i] = GF256::multiply($errorEvaluator->evaluateAt($xiInverse), GF256::inverse($denominator));
312
		}
313
314
		return $result;
315
	}
316
317
}
318