QRData::setData()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 8
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, Mode, Version};
14
use chillerlan\Settings\SettingsContainerInterface;
15
16
use function count;
17
use function sprintf;
18
19
/**
20
 * Processes the binary data and maps it on a QRMatrix 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
	public function __construct(SettingsContainerInterface $options, array $dataSegments = []){
62
		$this->options       = $options;
63
		$this->bitBuffer     = new BitBuffer;
64
		$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

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

117
		$this->version      = new Version(/** @scrutinizer ignore-type */ $this->options->version);
Loading history...
118
119
		return $this;
120
	}
121
122
	/**
123
	 * returns a fresh matrix object with the data written and masked with the given $maskPattern
124
	 */
125
	public function writeMatrix():QRMatrix{
126
		return (new QRMatrix($this->version, $this->eccLevel))
127
			->initFunctionalPatterns()
128
			->writeCodewords($this->bitBuffer)
129
		;
130
	}
131
132
	/**
133
	 * estimates the total length of the several mode segments in order to guess the minimum version
134
	 *
135
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
136
	 */
137
	public function estimateTotalBitLength():int{
138
		$length = 0;
139
140
		foreach($this->dataSegments as $segment){
141
			// data length of the current segment
142
			$length += $segment->getLengthInBits();
143
			// +4 bits for the mode descriptor
144
			$length += 4;
145
			// Hanzi mode sets an additional 4 bit long subset identifier
146
			if($segment instanceof Hanzi){
147
				$length += 4;
148
			}
149
		}
150
151
		$provisionalVersion = null;
152
153
		foreach($this->maxBitsForEcc as $version => $maxBits){
154
155
			if($length <= $maxBits){
156
				$provisionalVersion = $version;
157
			}
158
159
		}
160
161
		if($provisionalVersion !== null){
162
163
			// add character count indicator bits for the provisional version
164
			foreach($this->dataSegments as $segment){
165
				$length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion);
166
			}
167
168
			// it seems that in some cases the estimated total length is not 100% accurate,
169
			// so we substract 4 bits from the total when not in mixed mode
170
			if(count($this->dataSegments) <= 1){
171
				$length -= 4;
172
			}
173
174
			// we've got a match!
175
			// or let's see if there's a higher version number available
176
			if($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])){
177
				return $length;
178
			}
179
180
		}
181
182
		throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
183
	}
184
185
	/**
186
	 * returns the minimum version number for the given string
187
	 *
188
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
189
	 */
190
	public function getMinimumVersion():Version{
191
192
		if($this->options->version !== Version::AUTO){
193
			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

193
			return new Version(/** @scrutinizer ignore-type */ $this->options->version);
Loading history...
194
		}
195
196
		$total = $this->estimateTotalBitLength();
197
198
		// guess the version number within the given range
199
		for($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++){
200
			if($total <= $this->maxBitsForEcc[$version]){
201
				return new Version($version);
202
			}
203
		}
204
205
		// it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
206
		throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
207
	}
208
209
	/**
210
	 * creates a BitBuffer and writes the string data to it
211
	 *
212
	 * @throws \chillerlan\QRCode\QRCodeException on data overflow
213
	 */
214
	private function writeBitBuffer():void{
215
		$MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version);
216
217
		foreach($this->dataSegments as $segment){
218
			$segment->write($this->bitBuffer, $this->version->getVersionNumber());
219
		}
220
221
		// overflow, likely caused due to invalid version setting
222
		if($this->bitBuffer->getLength() > $MAX_BITS){
223
			throw new QRCodeDataException(
224
				sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
225
			);
226
		}
227
228
		// add terminator (ISO/IEC 18004:2000 Table 2)
229
		if(($this->bitBuffer->getLength() + 4) <= $MAX_BITS){
230
			$this->bitBuffer->put(Mode::TERMINATOR, 4);
231
		}
232
233
		// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
234
235
		// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
236
		// by the addition of padding bits with binary value 0
237
		while(($this->bitBuffer->getLength() % 8) !== 0){
238
239
			if($this->bitBuffer->getLength() === $MAX_BITS){
240
				break;
241
			}
242
243
			$this->bitBuffer->putBit(false);
244
		}
245
246
		// The message bit stream shall then be extended to fill the data capacity of the symbol
247
		// corresponding to the Version and Error Correction Level, by the addition of the Pad
248
		// Codewords 11101100 and 00010001 alternately.
249
		$alternate = false;
250
251
		while(($this->bitBuffer->getLength() + 8) <= $MAX_BITS){
252
			$this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8);
253
254
			$alternate = !$alternate;
0 ignored issues
show
introduced by
The condition $alternate is always false.
Loading history...
255
		}
256
257
		// In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
258
		// to the end of the message in order exactly to fill the symbol capacity
259
		while($this->bitBuffer->getLength() <= $MAX_BITS){
260
			$this->bitBuffer->putBit(false);
261
		}
262
263
	}
264
265
}
266