Skip to content

Commit 05eaf4e

Browse files
committed
:octocat: ECI mode rework
1 parent 9eba4ad commit 05eaf4e

File tree

6 files changed

+337
-66
lines changed

6 files changed

+337
-66
lines changed

src/Common/ECICharset.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
namespace chillerlan\QRCode\Common;
1313

1414
use chillerlan\QRCode\QRCodeException;
15-
use function array_key_exists;
15+
use function sprintf;
1616

1717
/**
1818
* ISO/IEC 18004:2000 - 8.4.1 Extended Channel Interpretation (ECI) Mode
@@ -98,8 +98,8 @@ final class ECICharset{
9898
*/
9999
public function __construct(int $charsetID){
100100

101-
if(!array_key_exists($charsetID, self::MB_ENCODINGS)){
102-
throw new QRCodeException('invalid charset id: '.$charsetID);
101+
if($charsetID < 0 || $charsetID > 999999){
102+
throw new QRCodeException(sprintf('invalid charset id: "%s"', $charsetID));
103103
}
104104

105105
$this->charsetID = $charsetID;
@@ -119,7 +119,7 @@ public function getID():int{
119119
* @see \iconv()
120120
*/
121121
public function getName():?string{
122-
return self::MB_ENCODINGS[$this->charsetID];
122+
return (self::MB_ENCODINGS[$this->charsetID] ?? null);
123123
}
124124

125125
}

src/Common/Mode.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
namespace chillerlan\QRCode\Common;
1212

13-
use chillerlan\QRCode\Data\{AlphaNum, Byte, Hanzi, Kanji, Number};
13+
use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number};
1414
use chillerlan\QRCode\QRCodeException;
1515

1616
/**
@@ -48,10 +48,11 @@ final class Mode{
4848
*/
4949
public const LENGTH_BITS = [
5050
self::NUMBER => [10, 12, 14],
51-
self::ALPHANUM => [9, 11, 13],
52-
self::BYTE => [8, 16, 16],
53-
self::KANJI => [8, 10, 12],
54-
self::HANZI => [8, 10, 12],
51+
self::ALPHANUM => [ 9, 11, 13],
52+
self::BYTE => [ 8, 16, 16],
53+
self::KANJI => [ 8, 10, 12],
54+
self::HANZI => [ 8, 10, 12],
55+
self::ECI => [ 0, 0, 0],
5556
];
5657

5758
/**

src/Data/ECI.php

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
namespace chillerlan\QRCode\Data;
1212

1313
use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode};
14+
use function mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, sprintf;
1415

1516
/**
1617
* Adds an ECI Designator
1718
*
19+
* ISO/IEC 18004:2000 8.4.1.1
20+
*
1821
* Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment()
1922
*/
2023
final class ECI extends QRDataModeAbstract{
@@ -34,48 +37,76 @@ final class ECI extends QRDataModeAbstract{
3437
* @noinspection PhpMissingParentConstructorInspection
3538
*/
3639
public function __construct(int $encoding){
40+
41+
if($encoding < 0 || $encoding > 999999){
42+
throw new QRCodeDataException(sprintf('invalid encoding id: "%s"', $encoding));
43+
}
44+
3745
$this->encoding = $encoding;
3846
}
3947

4048
/**
4149
* @inheritDoc
4250
*/
4351
public function getLengthInBits():int{
44-
return 8;
52+
53+
if($this->encoding < 128){
54+
return 8;
55+
}
56+
57+
if($this->encoding < 16384){
58+
return 16;
59+
}
60+
61+
return 24;
4562
}
4663

4764
/**
65+
* Writes an ECI designator to the bitbuffer
66+
*
4867
* @inheritDoc
4968
*/
50-
public function write(BitBuffer $bitBuffer, int $versionNumber):void{
51-
$bitBuffer
52-
->put($this::$datamode, 4)
53-
->put($this->encoding, 8)
54-
;
69+
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
70+
$bitBuffer->put($this::$datamode, 4);
71+
72+
if($this->encoding < 128){
73+
$bitBuffer->put($this->encoding, 8);
74+
}
75+
elseif($this->encoding < 16384){
76+
$bitBuffer->put(($this->encoding | 0x8000), 16);
77+
}
78+
elseif($this->encoding < 1000000){
79+
$bitBuffer->put(($this->encoding | 0xC00000), 24);
80+
}
81+
82+
return $this;
5583
}
5684

5785
/**
86+
* Reads and parses the value of an ECI designator
87+
*
5888
* @throws \chillerlan\QRCode\Data\QRCodeDataException
5989
*/
6090
public static function parseValue(BitBuffer $bitBuffer):ECICharset{
6191
$firstByte = $bitBuffer->read(8);
6292

63-
if(($firstByte & 0x80) === 0){
64-
// just one byte
65-
return new ECICharset($firstByte & 0x7f);
93+
// just one byte
94+
if(($firstByte & 0b10000000) === 0){
95+
$id = ($firstByte & 0b01111111);
6696
}
67-
68-
if(($firstByte & 0xc0) === 0x80){
69-
// two bytes
70-
return new ECICharset((($firstByte & 0x3f) << 8) | $bitBuffer->read(8));
97+
// two bytes
98+
elseif(($firstByte & 0b11000000) === 0b10000000){
99+
$id = ((($firstByte & 0b00111111) << 8) | $bitBuffer->read(8));
71100
}
72-
73-
if(($firstByte & 0xe0) === 0xC0){
74-
// three bytes
75-
return new ECICharset((($firstByte & 0x1f) << 16) | $bitBuffer->read(16));
101+
// three bytes
102+
elseif(($firstByte & 0b11100000) === 0b11000000){
103+
$id = ((($firstByte & 0b00011111) << 16) | $bitBuffer->read(16));
104+
}
105+
else{
106+
throw new QRCodeDataException(sprintf('error decoding ECI value first byte: %08b', $firstByte)); // @codeCoverageIgnore
76107
}
77108

78-
throw new QRCodeDataException('error decoding ECI value');
109+
return new ECICharset($id);
79110
}
80111

81112
/**
@@ -86,10 +117,35 @@ public static function validateString(string $string):bool{
86117
}
87118

88119
/**
89-
* @codeCoverageIgnore Unused, but required as per interface
120+
* Reads and decodes the ECI designator including the following byte sequence
121+
*
122+
* @throws \chillerlan\QRCode\Data\QRCodeDataException
90123
*/
91124
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
92-
return '';
125+
$eciCharset = self::parseValue($bitBuffer);
126+
$nextMode = $bitBuffer->read(4);
127+
128+
if($nextMode !== Mode::BYTE){
129+
throw new QRCodeDataException(sprintf('ECI designator followed by invalid mode: "%04b"', $nextMode));
130+
}
131+
132+
$data = Byte::decodeSegment($bitBuffer, $versionNumber);
133+
$encoding = $eciCharset->getName();
134+
135+
if($encoding === null){
136+
// The spec isn't clear on this mode; see
137+
// section 6.4.5: t does not say which encoding to assuming
138+
// upon decoding. I have seen ISO-8859-1 used as well as
139+
// Shift_JIS -- without anything like an ECI designator to
140+
// give a hint.
141+
$encoding = mb_detect_encoding($data, ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8'], true);
142+
143+
if($encoding === false){
144+
throw new QRCodeDataException('could not determine encoding in ECI mode'); // @codeCoverageIgnore
145+
}
146+
}
147+
148+
return mb_convert_encoding($data, mb_internal_encoding(), $encoding);
93149
}
94150

95151
}

src/Decoder/Decoder.php

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111

1212
namespace chillerlan\QRCode\Decoder;
1313

14-
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, ECICharset, MaskPattern, Mode, ReedSolomonDecoder, Version};
14+
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Mode, ReedSolomonDecoder, Version};
1515
use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number};
1616
use chillerlan\QRCode\Detector\Detector;
1717
use Throwable;
18-
use function chr, mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, str_replace;
18+
use function chr, str_replace;
1919

2020
/**
2121
* The main class which implements QR Code decoding -- as opposed to locating and extracting
@@ -29,7 +29,6 @@ final class Decoder{
2929
private ?EccLevel $eccLevel = null;
3030
private ?MaskPattern $maskPattern = null;
3131
private BitBuffer $bitBuffer;
32-
private ?ECICharset $eciCharset = null;
3332

3433
/**
3534
* Decodes a QR Code represented as a BitMatrix.
@@ -92,7 +91,6 @@ private function decodeMatrix(BitMatrix $matrix):DecoderResult{
9291
*/
9392
private function decodeBitStream(BitBuffer $bitBuffer):DecoderResult{
9493
$this->bitBuffer = $bitBuffer;
95-
$this->eciCharset = null;
9694
$versionNumber = $this->version->getVersionNumber();
9795
$symbolSequence = -1;
9896
$parityData = -1;
@@ -103,12 +101,12 @@ private function decodeBitStream(BitBuffer $bitBuffer):DecoderResult{
103101
while($this->bitBuffer->available() >= 4){
104102
$datamode = $this->bitBuffer->read(4); // mode is encoded by 4 bits
105103

106-
// OK, assume we're done. Really, a TERMINATOR mode should have been recorded here
104+
// OK, assume we're done
107105
if($datamode === Mode::TERMINATOR){
108106
break;
109107
}
110108
elseif($datamode === Mode::ECI){
111-
$this->eciCharset = ECI::parseValue($this->bitBuffer);
109+
$result .= ECI::decodeSegment($this->bitBuffer, $versionNumber);
112110
}
113111
elseif($datamode === Mode::FNC1_FIRST || $datamode === Mode::FNC1_SECOND){
114112
// We do little with FNC1 except alter the parsed result a bit according to the spec
@@ -131,7 +129,7 @@ private function decodeBitStream(BitBuffer $bitBuffer):DecoderResult{
131129
$result .= $this->decodeAlphanumSegment($versionNumber, $fc1InEffect);
132130
}
133131
elseif($datamode === Mode::BYTE){
134-
$result .= $this->decodeByteSegment($versionNumber);
132+
$result .= Byte::decodeSegment($this->bitBuffer, $versionNumber);
135133
}
136134
elseif($datamode === Mode::KANJI){
137135
$result .= Kanji::decodeSegment($this->bitBuffer, $versionNumber);
@@ -172,34 +170,4 @@ private function decodeAlphanumSegment(int $versionNumber, bool $fc1InEffect):st
172170
return $str;
173171
}
174172

175-
/**
176-
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
177-
*/
178-
private function decodeByteSegment(int $versionNumber):string{
179-
$str = Byte::decodeSegment($this->bitBuffer, $versionNumber);
180-
181-
if($this->eciCharset === null){
182-
return $str;
183-
}
184-
185-
$encoding = $this->eciCharset->getName();
186-
187-
if($encoding === null){
188-
// The spec isn't clear on this mode; see
189-
// section 6.4.5: t does not say which encoding to assuming
190-
// upon decoding. I have seen ISO-8859-1 used as well as
191-
// Shift_JIS -- without anything like an ECI designator to
192-
// give a hint.
193-
$encoding = mb_detect_encoding($str, ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8'], true);
194-
195-
if($encoding === false){
196-
throw new QRCodeDecoderException('could not determine encoding in ECI mode');
197-
}
198-
}
199-
200-
$this->eciCharset = null;
201-
202-
return mb_convert_encoding($str, mb_internal_encoding(), $encoding);
203-
}
204-
205173
}

tests/Common/ECICharsetTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
/**
3+
* ECICharsetTest.php
4+
*
5+
* @created 13.03.2023
6+
* @author smiley <[email protected]>
7+
* @copyright 2023 smiley
8+
* @license MIT
9+
*/
10+
11+
namespace chillerlan\QRCodeTest\Common;
12+
13+
use chillerlan\QRCode\Common\ECICharset;
14+
use chillerlan\QRCode\QRCodeException;
15+
use PHPUnit\Framework\TestCase;
16+
17+
class ECICharsetTest extends TestCase{
18+
19+
public static function invalidIdProvider():array{
20+
return [[-1], [1000000]];
21+
}
22+
23+
/**
24+
* @dataProvider invalidIdProvider
25+
*/
26+
public function testInvalidDataException(int $id):void{
27+
$this->expectException(QRCodeException::class);
28+
$this->expectExceptionMessage('invalid charset id:');
29+
/** @phan-suppress-next-line PhanNoopNew */
30+
new ECICharset($id);
31+
}
32+
33+
public function encodingProvider():array{
34+
$params = [];
35+
36+
foreach(ECICharset::MB_ENCODINGS as $id => $name){
37+
$params[] = [$id, $name];
38+
}
39+
40+
return $params;
41+
}
42+
43+
/**
44+
* @dataProvider encodingProvider
45+
*/
46+
public function testGetName(int $id, string $name = null):void{
47+
$eciCharset = new ECICharset($id);
48+
49+
$this::assertSame($id, $eciCharset->getID());
50+
$this::assertSame($name, $eciCharset->getName());
51+
}
52+
53+
}

0 commit comments

Comments
 (0)