Passed
Push — main ( 739579...b8f287 )
by smiley
12:32
created

QRData::setBitBuffer()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 15
rs 10
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
	 * Sets a BitBuffer object
92
	 *
93
	 * this can be used instead of setData(), however, the version auto detection is not available in this case.
94
	 *
95
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
96
	 */
97
	public function setBitBuffer(BitBuffer $bitBuffer):self{
98
99
		if($this->options->version === Version::AUTO){
100
			throw new QRCodeDataException('version auto detection is not available');
101
		}
102
103
		if($bitBuffer->getLength() === 0){
104
			throw new QRCodeDataException('the given BitBuffer is empty');
105
		}
106
107
		$this->dataSegments = [];
108
		$this->bitBuffer    = $bitBuffer;
109
		$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

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

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