Passed
Push — main ( b62539...4e03cd )
by smiley
01:53
created

QRData   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 187
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 59
c 2
b 0
f 0
dl 0
loc 187
rs 10
wmc 23

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A setData() 0 12 2
A writeMatrix() 0 7 1
B estimateTotalBitLength() 0 27 7
A getMinimumVersion() 0 13 3
B writeBitBuffer() 0 44 8
1
<?php
2
/**
3
 * Class QRData
4
 *
5
 * @created      25.11.2015
6
 * @author       Smiley <[email protected]>
7
 * @copyright    2015 Smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\QRCode\Data;
12
13
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Mode, ReedSolomonEncoder, Version};
14
use chillerlan\QRCode\QRCode;
15
use chillerlan\Settings\SettingsContainerInterface;
16
17
use function range, sprintf;
18
19
/**
20
 * Processes the binary data and maps it on a matrix which is then being returned
21
 */
22
final class QRData{
23
24
	/**
25
	 * the options instance
26
	 *
27
	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
28
	 */
29
	private SettingsContainerInterface $options;
30
31
	/**
32
	 * a BitBuffer instance
33
	 */
34
	private BitBuffer $bitBuffer;
35
36
	/**
37
	 * an EccLevel instance
38
	 */
39
	private EccLevel $eccLevel;
40
41
	/**
42
	 * current QR Code version
43
	 */
44
	private Version $version;
45
46
	/**
47
	 * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
48
	 */
49
	private array $dataSegments = [];
50
51
	/**
52
	 * Max bits for the current ECC mode
53
	 *
54
	 * @var int[]
55
	 */
56
	private array $maxBitsForEcc;
57
58
	/**
59
	 * QRData constructor.
60
	 *
61
	 * @param \chillerlan\Settings\SettingsContainerInterface    $options
62
	 * @param \chillerlan\QRCode\Data\QRDataModeInterface[]|null $dataSegments
63
	 */
64
	public function __construct(SettingsContainerInterface $options, array $dataSegments = null){
65
		$this->options       = $options;
66
		$this->bitBuffer     = new BitBuffer;
67
		$this->eccLevel      = new EccLevel($this->options->eccLevel);
0 ignored issues
show
Bug introduced by
It seems like $this->options->eccLevel can also be of type null; however, parameter $eccLevel of chillerlan\QRCode\Common\EccLevel::__construct() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

67
		$this->eccLevel      = new EccLevel(/** @scrutinizer ignore-type */ $this->options->eccLevel);
Loading history...
68
		$this->maxBitsForEcc = $this->eccLevel->getMaxBits();
69
70
		if(!empty($dataSegments)){
71
			$this->setData($dataSegments);
72
		}
73
74
	}
75
76
	/**
77
	 * Sets the data string (internally called by the constructor)
78
	 */
79
	public function setData(array $dataSegments):QRData{
80
		$this->dataSegments = $dataSegments;
81
82
		$version = $this->options->version === QRCode::VERSION_AUTO
83
			? $this->getMinimumVersion()
84
			: $this->options->version;
85
86
		$this->version = new Version($version);
0 ignored issues
show
Bug introduced by
It seems like $version can also be of type null; however, parameter $version of chillerlan\QRCode\Common\Version::__construct() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

86
		$this->version = new Version(/** @scrutinizer ignore-type */ $version);
Loading history...
87
88
		$this->writeBitBuffer();
89
90
		return $this;
91
	}
92
93
	/**
94
	 * returns a fresh matrix object with the data written for the given $maskPattern
95
	 */
96
	public function writeMatrix(MaskPattern $maskPattern, bool $test = null):QRMatrix{
97
		$data = (new ReedSolomonEncoder)->interleaveEcBytes($this->bitBuffer, $this->version, $this->eccLevel);
98
99
		return (new QRMatrix($this->version, $this->eccLevel))
100
			->init($maskPattern, $test)
101
			->mapData($data)
102
			->mask($maskPattern)
103
		;
104
	}
105
106
	/**
107
	 * estimates the total length of the several mode segments in order to guess the minimum version
108
	 *
109
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
110
	 */
111
	private function estimateTotalBitLength():int{
112
		$length = 0;
113
		$margin = 0;
114
115
		foreach($this->dataSegments as $segment){
116
			// data length in bits of the current segment +4 bits for each mode descriptor
117
			$length += ($segment->getLengthInBits() + Mode::getLengthBitsForMode($segment->getDataMode())[0] + 4);
118
119
			if(!$segment instanceof ECI){
120
				// mode length bits margin to the next breakpoint
121
				$margin += ($segment instanceof Byte ? 8 : 2);
122
			}
123
		}
124
125
		foreach([9, 26, 40] as $breakpoint){
126
127
			// length bits for the first breakpoint have already been added
128
			if($breakpoint > 9){
129
				$length += $margin;
130
			}
131
132
			if($length < $this->maxBitsForEcc[$breakpoint]){
133
				return $length;
134
			}
135
		}
136
137
		throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
138
	}
139
140
	/**
141
	 * returns the minimum version number for the given string
142
	 *
143
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
144
	 */
145
	private function getMinimumVersion():int{
146
		$total = $this->estimateTotalBitLength();
147
148
		// guess the version number within the given range
149
		foreach(range($this->options->versionMin, $this->options->versionMax) as $version){
150
151
			if($total <= $this->maxBitsForEcc[$version]){
152
				return $version;
153
			}
154
		}
155
156
		// it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
157
		throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
158
	}
159
160
	/**
161
	 * creates a BitBuffer and writes the string data to it
162
	 *
163
	 * @throws \chillerlan\QRCode\QRCodeException on data overflow
164
	 */
165
	private function writeBitBuffer():void{
166
		$version  = $this->version->getVersionNumber();
167
		$MAX_BITS = $this->maxBitsForEcc[$version];
168
169
		foreach($this->dataSegments as $segment){
170
			$segment->write($this->bitBuffer, $version);
171
		}
172
173
		// overflow, likely caused due to invalid version setting
174
		if($this->bitBuffer->getLength() > $MAX_BITS){
175
			throw new QRCodeDataException(
176
				sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
177
			);
178
		}
179
180
		// add terminator (ISO/IEC 18004:2000 Table 2)
181
		if($this->bitBuffer->getLength() + 4 <= $MAX_BITS){
182
			$this->bitBuffer->put(0b0000, 4);
183
		}
184
185
		// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
186
187
		// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
188
		// by the addition of padding bits with binary value 0
189
		while($this->bitBuffer->getLength() % 8 !== 0){
190
			$this->bitBuffer->putBit(false);
191
		}
192
193
		// The message bit stream shall then be extended to fill the data capacity of the symbol
194
		// corresponding to the Version and Error Correction Level, by the addition of the Pad
195
		// Codewords 11101100 and 00010001 alternately.
196
		while(true){
197
198
			if($this->bitBuffer->getLength() >= $MAX_BITS){
199
				break;
200
			}
201
202
			$this->bitBuffer->put(0b11101100, 8);
203
204
			if($this->bitBuffer->getLength() >= $MAX_BITS){
205
				break;
206
			}
207
208
			$this->bitBuffer->put(0b00010001, 8);
209
		}
210
211
	}
212
213
}
214