Passed
Push — main ( 9e04d2...06a3ca )
by smiley
02:18
created

QRData::writeBitBuffer()   B

Complexity

Conditions 7
Paths 18

Size

Total Lines 37
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
c 3
b 0
f 0
nc 18
nop 0
dl 0
loc 37
rs 8.8333
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, Version};
14
use chillerlan\Settings\SettingsContainerInterface;
15
16
use function sprintf;
17
18
/**
19
 * Processes the binary data and maps it on a matrix which is then being returned
20
 */
21
final class QRData{
22
23
	/**
24
	 * the options instance
25
	 *
26
	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
27
	 */
28
	private SettingsContainerInterface $options;
29
30
	/**
31
	 * a BitBuffer instance
32
	 */
33
	private BitBuffer $bitBuffer;
34
35
	/**
36
	 * an EccLevel instance
37
	 */
38
	private EccLevel $eccLevel;
39
40
	/**
41
	 * current QR Code version
42
	 */
43
	private Version $version;
44
45
	/**
46
	 * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
47
	 */
48
	private array $dataSegments = [];
49
50
	/**
51
	 * Max bits for the current ECC mode
52
	 *
53
	 * @var int[]
54
	 */
55
	private array $maxBitsForEcc;
56
57
	/**
58
	 * QRData constructor.
59
	 *
60
	 * @param \chillerlan\Settings\SettingsContainerInterface    $options
61
	 * @param \chillerlan\QRCode\Data\QRDataModeInterface[]|null $dataSegments
62
	 */
63
	public function __construct(SettingsContainerInterface $options, array $dataSegments = null){
64
		$this->options       = $options;
65
		$this->bitBuffer     = new BitBuffer;
66
		$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

66
		$this->eccLevel      = new EccLevel(/** @scrutinizer ignore-type */ $this->options->eccLevel);
Loading history...
67
		$this->maxBitsForEcc = $this->eccLevel->getMaxBits();
68
69
		if(!empty($dataSegments)){
70
			$this->setData($dataSegments);
71
		}
72
73
	}
74
75
	/**
76
	 * Sets the data string (internally called by the constructor)
77
	 *
78
	 * Subsequent calls will overwrite the current state - use the QRCode::add*Segement() method instead
79
	 */
80
	public function setData(array $dataSegments):self{
81
		$this->dataSegments = $dataSegments;
82
		$this->version      = $this->getMinimumVersion();
83
84
		$this->bitBuffer->clear();
85
		$this->writeBitBuffer();
86
87
		return $this;
88
	}
89
90
	/**
91
	 * Returns the current BitBuffer instance
92
	 *
93
	 * @codeCoverageIgnore
94
	 */
95
	public function getBitBuffer():BitBuffer{
96
		return $this->bitBuffer;
97
	}
98
99
	/**
100
	 * Sets a BitBuffer object
101
	 *
102
	 * This can be used instead of setData(), however, the version auto detection is not available in this case.
103
	 * The version needs match the length bits range for the data mode the data has been encoded with,
104
	 * additionally the bit array needs to contain enough pad bits.
105
	 *
106
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
107
	 */
108
	public function setBitBuffer(BitBuffer $bitBuffer):self{
109
110
		if($this->options->version === Version::AUTO){
111
			throw new QRCodeDataException('version auto detection is not available');
112
		}
113
114
		if($bitBuffer->getLength() === 0){
115
			throw new QRCodeDataException('the given BitBuffer is empty');
116
		}
117
118
		$this->dataSegments = [];
119
		$this->bitBuffer    = $bitBuffer;
120
		$this->version      = new Version($this->options->version);
0 ignored issues
show
Bug introduced by
It seems like $this->options->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

120
		$this->version      = new Version(/** @scrutinizer ignore-type */ $this->options->version);
Loading history...
121
122
		return $this;
123
	}
124
125
	/**
126
	 * returns a fresh matrix object with the data written and masked with the given $maskPattern
127
	 */
128
	public function writeMatrix(MaskPattern $maskPattern):QRMatrix{
129
		return (new QRMatrix($this->version, $this->eccLevel, $maskPattern))
130
			->initFunctionalPatterns()
131
			->writeCodewords($this->bitBuffer)
132
			->mask()
133
		;
134
	}
135
136
	/**
137
	 * estimates the total length of the several mode segments in order to guess the minimum version
138
	 *
139
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
140
	 */
141
	private function estimateTotalBitLength():int{
142
		$length = 0;
143
		$margin = 0;
144
145
		foreach($this->dataSegments as $segment){
146
			// data length in bits of the current segment +4 bits for each mode descriptor
147
			$length += ($segment->getLengthInBits() + Mode::getLengthBitsForMode($segment->getDataMode())[0] + 4);
148
149
			if(!$segment instanceof ECI){
150
				// mode length bits margin to the next breakpoint
151
				$margin += ($segment instanceof Byte ? 8 : 2);
152
			}
153
		}
154
155
		foreach([9, 26, 40] as $breakpoint){
156
157
			// length bits for the first breakpoint have already been added
158
			if($breakpoint > 9){
159
				$length += $margin;
160
			}
161
162
			if($length < $this->maxBitsForEcc[$breakpoint]){
163
				return $length;
164
			}
165
		}
166
167
		throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
168
	}
169
170
	/**
171
	 * returns the minimum version number for the given string
172
	 *
173
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
174
	 */
175
	private function getMinimumVersion():Version{
176
177
		if($this->options->version !== Version::AUTO){
178
			return new Version($this->options->version);
0 ignored issues
show
Bug introduced by
It seems like $this->options->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

178
			return new Version(/** @scrutinizer ignore-type */ $this->options->version);
Loading history...
179
		}
180
181
		$total = $this->estimateTotalBitLength();
182
183
		// guess the version number within the given range
184
		for($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++){
185
			if($total <= $this->maxBitsForEcc[$version]){
186
				return new Version($version);
187
			}
188
		}
189
190
		// it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
191
		throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
192
	}
193
194
	/**
195
	 * creates a BitBuffer and writes the string data to it
196
	 *
197
	 * @throws \chillerlan\QRCode\QRCodeException on data overflow
198
	 */
199
	private function writeBitBuffer():void{
200
		$version  = $this->version->getVersionNumber();
201
		$MAX_BITS = $this->maxBitsForEcc[$version];
202
203
		foreach($this->dataSegments as $segment){
204
			$segment->write($this->bitBuffer, $version);
205
		}
206
207
		// overflow, likely caused due to invalid version setting
208
		if($this->bitBuffer->getLength() > $MAX_BITS){
209
			throw new QRCodeDataException(
210
				sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
211
			);
212
		}
213
214
		// add terminator (ISO/IEC 18004:2000 Table 2)
215
		if($this->bitBuffer->getLength() + 4 <= $MAX_BITS){
216
			$this->bitBuffer->put(0, 4);
217
		}
218
219
		// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
220
221
		// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
222
		// by the addition of padding bits with binary value 0
223
		while($this->bitBuffer->getLength() % 8 !== 0){
224
			$this->bitBuffer->putBit(false);
225
		}
226
227
		// The message bit stream shall then be extended to fill the data capacity of the symbol
228
		// corresponding to the Version and Error Correction Level, by the addition of the Pad
229
		// Codewords 11101100 and 00010001 alternately.
230
		$alternate = false;
231
232
		while($this->bitBuffer->getLength() <= $MAX_BITS){
233
			$this->bitBuffer->put($alternate ? 0b00010001 : 0b11101100, 8);
234
235
			$alternate = !$alternate;
0 ignored issues
show
introduced by
The condition $alternate is always false.
Loading history...
236
		}
237
238
	}
239
240
}
241