Passed
Push — v5 ( 043137...387bee )
by smiley
09:56
created

QRData::setData()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 9
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 17
rs 9.9666
1
<?php
2
/**
3
 * Class QRData
4
 *
5
 * @filesource   QRData.php
6
 * @created      25.11.2015
7
 * @package      chillerlan\QRCode\Data
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2015 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\QRCode\Data;
14
15
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, ReedSolomonEncoder, Version};
16
use chillerlan\QRCode\QRCode;
17
use chillerlan\Settings\SettingsContainerInterface;
18
19
use function range, sprintf;
20
21
/**
22
 * Processes the binary data and maps it on a matrix which is then being returned
23
 */
24
final class QRData{
25
26
	/**
27
	 * the options instance
28
	 *
29
	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
30
	 */
31
	protected SettingsContainerInterface $options;
32
33
	/**
34
	 * a BitBuffer instance
35
	 */
36
	protected BitBuffer $bitBuffer;
37
38
	/**
39
	 * an EccLevel instance
40
	 */
41
	protected EccLevel $eccLevel;
42
43
	/**
44
	 * current QR Code version
45
	 */
46
	protected Version $version;
47
48
	/**
49
	 * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
50
	 */
51
	protected array $dataSegments = [];
52
53
	/**
54
	 * Max bits for the current ECC mode
55
	 *
56
	 * @var int[]
57
	 */
58
	protected array $maxBitsForEcc;
59
60
	/**
61
	 * QRData constructor.
62
	 *
63
	 * @param \chillerlan\Settings\SettingsContainerInterface $options
64
	 * @param array|null                                      $dataSegments
65
	 */
66
	public function __construct(SettingsContainerInterface $options, array $dataSegments = null){
67
		$this->options       = $options;
68
		$this->bitBuffer     = new BitBuffer;
69
		$this->eccLevel      = new EccLevel($this->options->eccLevel);
70
		$this->maxBitsForEcc = $this->eccLevel->getMaxBits();
71
72
		if(!empty($dataSegments)){
73
			$this->setData($dataSegments);
74
		}
75
76
	}
77
78
	/**
79
	 * Sets the data string (internally called by the constructor)
80
	 */
81
	public function setData(array $dataSegments):QRData{
82
83
		foreach($dataSegments as $segment){
84
			[$class, $data] = $segment;
85
86
			$this->dataSegments[] = new $class($data);
87
		}
88
89
		$version = $this->options->version === QRCode::VERSION_AUTO
90
			? $this->getMinimumVersion()
91
			: $this->options->version;
92
93
		$this->version = new Version($version);
94
95
		$this->writeBitBuffer();
96
97
		return $this;
98
	}
99
100
	/**
101
	 * returns a fresh matrix object with the data written for the given $maskPattern
102
	 */
103
	public function writeMatrix(int $maskPattern, bool $test = null):QRMatrix{
104
		$data = (new ReedSolomonEncoder)->interleaveEcBytes($this->bitBuffer, $this->version, $this->eccLevel);
105
106
		return (new QRMatrix($this->version, $this->eccLevel))
107
			->init($maskPattern, $test)
108
			->mapData($data, $maskPattern)
109
		;
110
	}
111
112
	/**
113
	 * estimates the total length of the several mode segments in order to guess the minimum version
114
	 *
115
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
116
	 */
117
	protected function estimateTotalBitLength():int{
118
		$length = 0;
119
		$margin = 0;
120
121
		foreach($this->dataSegments as $segment){
122
			// data length in bits of the current segment +4 bits for each mode descriptor
123
			$length += ($segment->getLengthInBits() + Mode::getLengthBitsForMode($segment->getDataMode())[0] + 4);
124
125
			if(!$segment instanceof ECI){
126
				// mode length bits margin to the next breakpoint
127
				$margin += ($segment instanceof Byte ? 8 : 2);
128
			}
129
		}
130
131
		foreach([9, 26, 40] as $breakpoint){
132
133
			// length bits for the first breakpoint have already been added
134
			if($breakpoint > 9){
135
				$length += $margin;
136
			}
137
138
			if($length < $this->maxBitsForEcc[$breakpoint]){
139
				return $length;
140
			}
141
		}
142
143
		throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
144
	}
145
146
	/**
147
	 * returns the minimum version number for the given string
148
	 *
149
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
150
	 */
151
	protected function getMinimumVersion():int{
152
		$total = $this->estimateTotalBitLength();
153
154
		// guess the version number within the given range
155
		foreach(range($this->options->versionMin, $this->options->versionMax) as $version){
156
157
			if($total <= $this->maxBitsForEcc[$version]){
158
				return $version;
159
			}
160
		}
161
162
		// it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
163
		throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
164
	}
165
166
	/**
167
	 * creates a BitBuffer and writes the string data to it
168
	 *
169
	 * @throws \chillerlan\QRCode\QRCodeException on data overflow
170
	 */
171
	protected function writeBitBuffer():void{
172
		$version  = $this->version->getVersionNumber();
173
		$MAX_BITS = $this->maxBitsForEcc[$version];
174
175
		foreach($this->dataSegments as $segment){
176
			$segment->write($this->bitBuffer, $version);
177
		}
178
179
		// overflow, likely caused due to invalid version setting
180
		if($this->bitBuffer->getLength() > $MAX_BITS){
181
			throw new QRCodeDataException(
182
				sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
183
			);
184
		}
185
186
		// add terminator (ISO/IEC 18004:2000 Table 2)
187
		if($this->bitBuffer->getLength() + 4 <= $MAX_BITS){
188
			$this->bitBuffer->put(0b0000, 4);
189
		}
190
191
		// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
192
193
		// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
194
		// by the addition of padding bits with binary value 0
195
		while($this->bitBuffer->getLength() % 8 !== 0){
196
			$this->bitBuffer->putBit(false);
197
		}
198
199
		// The message bit stream shall then be extended to fill the data capacity of the symbol
200
		// corresponding to the Version and Error Correction Level, by the addition of the Pad
201
		// Codewords 11101100 and 00010001 alternately.
202
		while(true){
203
204
			if($this->bitBuffer->getLength() >= $MAX_BITS){
205
				break;
206
			}
207
208
			$this->bitBuffer->put(0b11101100, 8);
209
210
			if($this->bitBuffer->getLength() >= $MAX_BITS){
211
				break;
212
			}
213
214
			$this->bitBuffer->put(0b00010001, 8);
215
		}
216
217
	}
218
219
}
220