Skip to content

Commit b79bf85

Browse files
committed
:octocat: improve minimum version detection and related tests
1 parent b30c369 commit b79bf85

File tree

9 files changed

+209
-85
lines changed

9 files changed

+209
-85
lines changed

src/Common/EccLevel.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,18 @@ public function getformatPattern(MaskPattern $maskPattern):int{
207207
* @return int[]
208208
*/
209209
public function getMaxBits():array{
210-
return array_column(self::MAX_BITS, $this->getOrdinal());
210+
$col = array_column(self::MAX_BITS, $this->getOrdinal());
211+
212+
unset($col[0]); // remove the inavlid index 0
213+
214+
return $col;
215+
}
216+
217+
/**
218+
* Returns the maximum bit length for the given version and current ECC level
219+
*/
220+
public function getMaxBitsForVersion(Version $version):int{
221+
return self::MAX_BITS[$version->getVersionNumber()][$this->getOrdinal()];
211222
}
212223

213224
}

src/Data/QRData.php

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Mode, Version};
1414
use chillerlan\Settings\SettingsContainerInterface;
1515

16+
use function count;
1617
use function sprintf;
1718

1819
/**
@@ -135,35 +136,48 @@ public function writeMatrix(MaskPattern $maskPattern):QRMatrix{
135136
* @throws \chillerlan\QRCode\Data\QRCodeDataException
136137
*/
137138
public function estimateTotalBitLength():int{
138-
$length = 4; // 4 bits for the terminator
139-
$margin = 0;
139+
$length = 0;
140140

141141
foreach($this->dataSegments as $segment){
142-
// data length in bits of the current segment +4 bits for each mode descriptor
143-
$length += ($segment->getLengthInBits() + Mode::getLengthBitsForVersion($segment::DATAMODE, 1) + 4);
144-
145-
if(!$segment instanceof ECI){
146-
// mode length bits margin to the next breakpoint
147-
$margin += ($segment instanceof Byte) ? 8 : 2;
148-
}
149-
142+
// data length of the current segment
143+
$length += $segment->getLengthInBits();
144+
// +4 bits for the mode descriptor
145+
$length += 4;
146+
// Hanzi mode sets an additional 4 bit long subset identifier
150147
if($segment instanceof Hanzi){
151-
// Hanzi mode sets an additional 4 bit long subset identifier
152148
$length += 4;
153149
}
150+
}
151+
152+
$provisionalVersion = null;
153+
154+
foreach($this->maxBitsForEcc as $version => $maxBits){
155+
156+
if($length <= $maxBits){
157+
$provisionalVersion = $version;
158+
}
154159

155160
}
156161

157-
foreach([9, 26, 40] as $breakpoint){
162+
if($provisionalVersion !== null){
163+
164+
// add character count indicator bits for the provisional version
165+
foreach($this->dataSegments as $segment){
166+
$length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion);
167+
}
158168

159-
// length bits for the first breakpoint have already been added
160-
if($breakpoint > 9){
161-
$length += $margin;
169+
// it seems that in some cases the estimated total length is not 100% accurate,
170+
// so we substract 4 bits from the total when not in mixed mode
171+
if(count($this->dataSegments) <= 1){
172+
$length -= 4;
162173
}
163174

164-
if($length < $this->maxBitsForEcc[$breakpoint]){
175+
// we've got a match!
176+
// or let's see if there's a higher version number available
177+
if($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])){
165178
return $length;
166179
}
180+
167181
}
168182

169183
throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
@@ -199,11 +213,10 @@ public function getMinimumVersion():Version{
199213
* @throws \chillerlan\QRCode\QRCodeException on data overflow
200214
*/
201215
private function writeBitBuffer():void{
202-
$version = $this->version->getVersionNumber();
203-
$MAX_BITS = $this->maxBitsForEcc[$version];
216+
$MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version);
204217

205218
foreach($this->dataSegments as $segment){
206-
$segment->write($this->bitBuffer, $version);
219+
$segment->write($this->bitBuffer, $this->version->getVersionNumber());
207220
}
208221

209222
// overflow, likely caused due to invalid version setting
@@ -215,14 +228,19 @@ private function writeBitBuffer():void{
215228

216229
// add terminator (ISO/IEC 18004:2000 Table 2)
217230
if(($this->bitBuffer->getLength() + 4) <= $MAX_BITS){
218-
$this->bitBuffer->put(0, 4);
231+
$this->bitBuffer->put(Mode::TERMINATOR, 4);
219232
}
220233

221234
// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
222235

223236
// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
224237
// by the addition of padding bits with binary value 0
225238
while(($this->bitBuffer->getLength() % 8) !== 0){
239+
240+
if($this->bitBuffer->getLength() === $MAX_BITS){
241+
break;
242+
}
243+
226244
$this->bitBuffer->putBit(false);
227245
}
228246

@@ -231,12 +249,18 @@ private function writeBitBuffer():void{
231249
// Codewords 11101100 and 00010001 alternately.
232250
$alternate = false;
233251

234-
while($this->bitBuffer->getLength() <= $MAX_BITS){
252+
while(($this->bitBuffer->getLength() + 8) <= $MAX_BITS){
235253
$this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8);
236254

237255
$alternate = !$alternate;
238256
}
239257

258+
// In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
259+
// to the end of the message in order exactly to fill the symbol capacity
260+
while($this->bitBuffer->getLength() <= $MAX_BITS){
261+
$this->bitBuffer->putBit(false);
262+
}
263+
240264
}
241265

242266
}

tests/Data/AlphaNumTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
*/
1818
final class AlphaNumTest extends DataInterfaceTestAbstract{
1919

20-
protected string $FQN = AlphaNum::class;
21-
protected string $testdata = '0 $%*+-./:';
20+
protected static string $FQN = AlphaNum::class;
21+
protected static string $testdata = '0 $%*+-./:';
2222

2323
/**
2424
* isAlphaNum() should pass on the 45 defined characters and fail on anything else (e.g. lowercase)

tests/Data/ByteTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
*/
1818
final class ByteTest extends DataInterfaceTestAbstract{
1919

20-
protected string $FQN = Byte::class;
21-
protected string $testdata = '[¯\_(ツ)_/¯]';
20+
protected static string $FQN = Byte::class;
21+
protected static string $testdata = '[¯\_(ツ)_/¯]';
2222

2323
/**
2424
* isByte() passses any binary string and only fails on empty strings

0 commit comments

Comments
 (0)