Passed
Push — main ( bb73f7...8c75d8 )
by smiley
03:09
created

Decoder::decodeBitStream()   C

Complexity

Conditions 14
Paths 4

Size

Total Lines 114
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 14
eloc 45
nc 4
nop 1
dl 0
loc 114
rs 6.2666
c 4
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Class Decoder
4
 *
5
 * @created      17.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\Decoder;
13
14
use Throwable;
15
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, FormatInformation, Mode, ReedSolomonDecoder, Version};
16
use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number};
17
use chillerlan\QRCode\Detector\Detector;
18
use function count, array_fill, mb_convert_encoding, mb_detect_encoding;
19
20
/**
21
 * The main class which implements QR Code decoding -- as opposed to locating and extracting
22
 * the QR Code from an image.
23
 *
24
 * @author Sean Owen
25
 */
26
final class Decoder{
27
28
#	private const GB2312_SUBSET = 1;
29
30
	private ?Version $version = null;
31
	private ?FormatInformation $formatInfo = null;
32
	private EccLevel $eccLevel;
33
34
	/**
35
	 * Decodes a QR Code represented as a BitMatrix.
36
	 * A 1 or "true" is taken to mean a black module.
37
	 *
38
	 * @param \chillerlan\QRCode\Decoder\LuminanceSourceInterface $source
39
	 *
40
	 * @return \chillerlan\QRCode\Decoder\DecoderResult                     text and bytes encoded within the QR Code
41
	 * @throws \Throwable|\chillerlan\QRCode\Decoder\QRCodeDecoderException if the QR Code cannot be decoded
42
	 */
43
	public function decode(LuminanceSourceInterface $source):DecoderResult{
44
		$bitMatrix = (new Detector($source))->detect();
45
46
		try{
47
			// clone the BitMatrix to avoid errors in case we run into mirroring
48
			return $this->decodeMatrix(clone $bitMatrix);
49
		}
50
		catch(Throwable $e){
51
52
			try{
53
				/*
54
				 * Prepare for a mirrored reading.
55
				 *
56
				 * Since we're here, this means we have successfully detected some kind
57
				 * of version and format information when mirrored. This is a good sign,
58
				 * that the QR code may be mirrored, and we should try once more with a
59
				 * mirrored content.
60
				 */
61
				return $this->decodeMatrix($bitMatrix->setMirror(true)->mirror());
62
			}
63
			catch(Throwable $f){
64
				// Throw the exception from the original reading
65
				throw $e;
66
			}
67
68
		}
69
70
	}
71
72
	/**
73
	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
74
	 */
75
	private function decodeMatrix(BitMatrix $bitMatrix):DecoderResult{
76
		// Read raw codewords
77
		$rawCodewords     = $bitMatrix->readCodewords();
78
		$this->version    = $bitMatrix->getVersion();
79
		$this->formatInfo = $bitMatrix->getFormatInfo();
80
81
		if($this->version === null || $this->formatInfo === null){
82
			throw new QRCodeDecoderException('unable to read version or format info'); // @codeCoverageIgnore
83
		}
84
85
		$this->eccLevel = $this->formatInfo->getErrorCorrectionLevel();
86
		$resultBytes    = (new ReedSolomonDecoder)->decode($this->getDataBlocks($rawCodewords));
87
		// Decode the contents of that stream of bytes
88
		return $this->decodeBitStream($resultBytes);
89
	}
90
91
	/**
92
	 * When QR Codes use multiple data blocks, they are actually interleaved.
93
	 * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This
94
	 * method will separate the data into original blocks.
95
	 *
96
	 * @param array $rawCodewords bytes as read directly from the QR Code
97
	 *
98
	 * @return array DataBlocks containing original bytes, "de-interleaved" from representation in the QR Code
99
	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
100
	 */
101
	private function getDataBlocks(array $rawCodewords):array{
102
103
		// Figure out the number and size of data blocks used by this version and
104
		// error correction level
105
		[$numEccCodewords, $eccBlocks] = $this->version->getRSBlocks($this->eccLevel);
0 ignored issues
show
Bug introduced by
The method getRSBlocks() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
		/** @scrutinizer ignore-call */ 
106
  [$numEccCodewords, $eccBlocks] = $this->version->getRSBlocks($this->eccLevel);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
106
107
		// Now establish DataBlocks of the appropriate size and number of data codewords
108
		$result          = [];//new DataBlock[$totalBlocks];
109
		$numResultBlocks = 0;
110
111
		foreach($eccBlocks as $blockData){
112
			[$numEccBlocks, $eccPerBlock] = $blockData;
113
114
			for($i = 0; $i < $numEccBlocks; $i++, $numResultBlocks++){
115
				$result[$numResultBlocks] = [$eccPerBlock, array_fill(0, $numEccCodewords + $eccPerBlock, 0)];
116
			}
117
		}
118
119
		// All blocks have the same amount of data, except that the last n
120
		// (where n may be 0) have 1 more byte. Figure out where these start.
121
		/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
122
		$shorterBlocksTotalCodewords = count($result[0][1]);
123
		$longerBlocksStartAt         = count($result) - 1;
124
125
		while($longerBlocksStartAt >= 0){
126
			$numCodewords = count($result[$longerBlocksStartAt][1]);
127
128
			if($numCodewords == $shorterBlocksTotalCodewords){
129
				break;
130
			}
131
132
			$longerBlocksStartAt--;
133
		}
134
135
		$longerBlocksStartAt++;
136
137
		$shorterBlocksNumDataCodewords = $shorterBlocksTotalCodewords - $numEccCodewords;
138
		// The last elements of result may be 1 element longer;
139
		// first fill out as many elements as all of them have
140
		$rawCodewordsOffset = 0;
141
142
		for($i = 0; $i < $shorterBlocksNumDataCodewords; $i++){
143
			for($j = 0; $j < $numResultBlocks; $j++){
144
				$result[$j][1][$i] = $rawCodewords[$rawCodewordsOffset++];
145
			}
146
		}
147
148
		// Fill out the last data block in the longer ones
149
		for($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++){
150
			$result[$j][1][$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++];
151
		}
152
153
		// Now add in error correction blocks
154
		/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
155
		$max = count($result[0][1]);
156
157
		for($i = $shorterBlocksNumDataCodewords; $i < $max; $i++){
158
			for($j = 0; $j < $numResultBlocks; $j++){
159
				$iOffset                 = $j < $longerBlocksStartAt ? $i : $i + 1;
160
				$result[$j][1][$iOffset] = $rawCodewords[$rawCodewordsOffset++];
161
			}
162
		}
163
164
		return $result;
165
	}
166
167
	/**
168
	 * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
169
	 */
170
	private function decodeBitStream(array $bytes):DecoderResult{
171
		$bits           = new BitBuffer($bytes);
172
		$symbolSequence = -1;
173
		$parityData     = -1;
174
		$versionNumber  = $this->version->getVersionNumber();
175
176
		$result      = '';
177
		$eciCharset  = null;
178
#		$fc1InEffect = false;
179
180
		// While still another segment to read...
181
		while($bits->available() >= 4){
182
			$datamode = $bits->read(4); // mode is encoded by 4 bits
183
184
			// OK, assume we're done. Really, a TERMINATOR mode should have been recorded here
185
			if($datamode === Mode::TERMINATOR){
186
				break;
187
			}
188
189
			if($datamode === Mode::ECI){
190
				// Count doesn't apply to ECI
191
				$eciCharset = ECI::parseValue($bits);
192
			}
193
			/** @noinspection PhpStatementHasEmptyBodyInspection */
194
			elseif($datamode === Mode::FNC1_FIRST || $datamode === Mode::FNC1_SECOND){
195
				// We do little with FNC1 except alter the parsed result a bit according to the spec
196
#				$fc1InEffect = true;
197
			}
198
			elseif($datamode === Mode::STRCTURED_APPEND){
199
				if($bits->available() < 16){
200
					throw new QRCodeDecoderException('structured append: not enough bits left');
201
				}
202
				// sequence number and parity is added later to the result metadata
203
				// Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue
204
				$symbolSequence = $bits->read(8);
205
				$parityData     = $bits->read(8);
206
			}
207
			else{
208
				// First handle Hanzi mode which does not start with character count
209
/*				if($datamode === Mode::DATA_HANZI){
210
					//chinese mode contains a sub set indicator right after mode indicator
211
					$subset = $bits->read(4);
212
					$length = $bits->read(Mode::getLengthBitsForVersion($datamode, $versionNumber));
213
					if($subset === self::GB2312_SUBSET){
214
						$result .= $this->decodeHanziSegment($bits, $length);
215
					}
216
				}*/
217
#				else{
218
					// "Normal" QR code modes:
219
					if($datamode === Mode::NUMBER){
220
						$result .= Number::decodeSegment($bits, $versionNumber);
221
					}
222
					elseif($datamode === Mode::ALPHANUM){
223
						$str = AlphaNum::decodeSegment($bits, $versionNumber);
224
225
						// See section 6.4.8.1, 6.4.8.2
226
/*						if($fc1InEffect){
227
							$start = \strlen($str);
228
							// We need to massage the result a bit if in an FNC1 mode:
229
							for($i = $start; $i < $start; $i++){
230
								if($str[$i] === '%'){
231
									if($i < $start - 1 && $str[$i + 1] === '%'){
232
										// %% is rendered as %
233
										$str = \substr_replace($str, '', $i + 1, 1);//deleteCharAt(i + 1);
234
									}
235
#									else{
236
										// In alpha mode, % should be converted to FNC1 separator 0x1D @todo
237
#										$str = setCharAt($i, \chr(0x1D)); // ???
238
#									}
239
								}
240
							}
241
						}
242
*/
243
						$result .= $str;
244
					}
245
					elseif($datamode === Mode::BYTE){
246
						$str = Byte::decodeSegment($bits, $versionNumber);
247
248
						if($eciCharset !== null){
249
							$encoding = $eciCharset->getName();
250
251
							if($encoding === null){
252
								// The spec isn't clear on this mode; see
253
								// section 6.4.5: t does not say which encoding to assuming
254
								// upon decoding. I have seen ISO-8859-1 used as well as
255
								// Shift_JIS -- without anything like an ECI designator to
256
								// give a hint.
257
								$encoding = mb_detect_encoding($str, ['ISO-8859-1', 'SJIS', 'UTF-8']);
258
							}
259
260
							$eciCharset = null;
261
							$str = mb_convert_encoding($str, $encoding);
262
						}
263
264
						$result .= $str;
265
					}
266
					elseif($datamode === Mode::KANJI){
267
						$result .= Kanji::decodeSegment($bits, $versionNumber);
268
					}
269
					else{
270
						throw new QRCodeDecoderException('invalid data mode');
271
					}
272
#				}
273
			}
274
		}
275
276
		return new DecoderResult([
277
			'rawBytes'                 => $bytes,
278
			'data'                     => $result,
279
			'version'                  => $this->version,
280
			'eccLevel'                 => $this->eccLevel,
281
			'maskPattern'              => $this->formatInfo->getMaskPattern(),
0 ignored issues
show
Bug introduced by
The method getMaskPattern() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

281
			'maskPattern'              => $this->formatInfo->/** @scrutinizer ignore-call */ getMaskPattern(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
282
			'structuredAppendParity'   => $parityData,
283
			'structuredAppendSequence' => $symbolSequence
284
		]);
285
	}
286
287
}
288