Kanji::convertEncoding()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 4
nop 1
dl 0
loc 20
rs 9.9332
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class Kanji
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, Mode};
14
15
use Throwable;
16
use function chr, implode, is_string, mb_convert_encoding, mb_detect_encoding,
17
	mb_detect_order, mb_internal_encoding, mb_strlen, ord, sprintf, strlen;
18
19
/**
20
 * Kanji mode: 13-bit double-byte characters from the Shift-JIS character set
21
 *
22
 * ISO/IEC 18004:2000 Section 8.3.5
23
 * ISO/IEC 18004:2000 Section 8.4.5
24
 *
25
 * @see https://en.wikipedia.org/wiki/Shift_JIS#As_defined_in_JIS_X_0208:1997
26
 * @see http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml
27
 * @see https://gist.github.com/codemasher/d07d3e6e9346c08e7a41b8b978784952
28
 */
29
final class Kanji extends QRDataModeAbstract{
30
31
	/**
32
	 * possible values: SJIS, SJIS-2004
33
	 *
34
	 * SJIS-2004 may produce errors in PHP < 8
35
	 *
36
	 * @var string
37
	 */
38
	public const ENCODING = 'SJIS';
39
40
	/**
41
	 * @inheritDoc
42
	 */
43
	public const DATAMODE = Mode::KANJI;
44
45
	/**
46
	 * @inheritDoc
47
	 */
48
	protected function getCharCount():int{
49
		return mb_strlen($this->data, self::ENCODING);
50
	}
51
52
	/**
53
	 * @inheritDoc
54
	 */
55
	public function getLengthInBits():int{
56
		return ($this->getCharCount() * 13);
57
	}
58
59
	/**
60
	 * @inheritDoc
61
	 */
62
	public static function convertEncoding(string $string):string{
63
		mb_detect_order([mb_internal_encoding(), 'UTF-8', 'SJIS', 'SJIS-2004']);
64
65
		$detected = mb_detect_encoding($string, null, true);
66
67
		if($detected === false){
68
			throw new QRCodeDataException('mb_detect_encoding error');
69
		}
70
71
		if($detected === self::ENCODING){
72
			return $string;
73
		}
74
75
		$string = mb_convert_encoding($string, self::ENCODING, $detected);
76
77
		if(!is_string($string)){
78
			throw new QRCodeDataException(sprintf('invalid encoding: %s', $detected));
79
		}
80
81
		return $string;
82
	}
83
84
	/**
85
	 * checks if a string qualifies as SJIS Kanji
86
	 */
87
	public static function validateString(string $string):bool{
88
89
		try{
90
			$string = self::convertEncoding($string);
91
		}
92
		catch(Throwable $e){
93
			return false;
94
		}
95
96
		$len = strlen($string);
97
98
		if($len < 2 || ($len % 2) !== 0){
99
			return false;
100
		}
101
102
		for($i = 0; $i < $len; $i += 2){
103
			$byte1 = ord($string[$i]);
104
			$byte2 = ord($string[($i + 1)]);
105
106
			// byte 1 unused and vendor ranges
107
			if($byte1 < 0x81 || ($byte1 > 0x84 && $byte1 < 0x88) || ($byte1 > 0x9f && $byte1 < 0xe0) ||  $byte1 > 0xea){
108
				return false;
109
			}
110
111
			// byte 2 unused ranges
112
			if($byte2 < 0x40 || $byte2 === 0x7f || $byte2 > 0xfc){
113
				return false;
114
			}
115
116
		}
117
118
		return true;
119
	}
120
121
	/**
122
	 * @inheritDoc
123
	 *
124
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
125
	 */
126
	public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
127
128
		$bitBuffer
129
			->put(self::DATAMODE, 4)
130
			->put($this->getCharCount(), $this::getLengthBits($versionNumber))
131
		;
132
133
		$len = strlen($this->data);
134
135
		for($i = 0; ($i + 1) < $len; $i += 2){
136
			$c = (((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[($i + 1)])));
137
138
			if($c >= 0x8140 && $c <= 0x9ffc){
139
				$c -= 0x8140;
140
			}
141
			elseif($c >= 0xe040 && $c <= 0xebbf){
142
				$c -= 0xc140;
143
			}
144
			else{
145
				throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c));
146
			}
147
148
			$bitBuffer->put((((($c >> 8) & 0xff) * 0xc0) + ($c & 0xff)), 13);
149
		}
150
151
		if($i < $len){
152
			throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1)));
153
		}
154
155
		return $this;
156
	}
157
158
	/**
159
	 * @inheritDoc
160
	 *
161
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
162
	 */
163
	public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
164
		$length = $bitBuffer->read(self::getLengthBits($versionNumber));
165
166
		if($bitBuffer->available() < ($length * 13)){
167
			throw new QRCodeDataException('not enough bits available');  // @codeCoverageIgnore
168
		}
169
170
		// Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as SJIS afterwards
171
		$buffer = [];
172
		$offset = 0;
173
174
		while($length > 0){
175
			// Each 13 bits encodes a 2-byte character
176
			$twoBytes          = $bitBuffer->read(13);
177
			$assembledTwoBytes = ((((int)($twoBytes / 0x0c0)) << 8) | ($twoBytes % 0x0c0));
178
179
			$assembledTwoBytes += ($assembledTwoBytes < 0x01f00)
180
				? 0x08140  // In the 0x8140 to 0x9FFC range
181
				: 0x0c140; // In the 0xE040 to 0xEBBF range
182
183
			$buffer[$offset]       = chr(0xff & ($assembledTwoBytes >> 8));
184
			$buffer[($offset + 1)] = chr(0xff & $assembledTwoBytes);
185
			$offset                += 2;
186
			$length--;
187
		}
188
189
		return mb_convert_encoding(implode($buffer), mb_internal_encoding(), self::ENCODING);
0 ignored issues
show
Bug introduced by
It seems like mb_internal_encoding() can also be of type true; however, parameter $to_encoding of mb_convert_encoding() does only seem to accept string, 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

189
		return mb_convert_encoding(implode($buffer), /** @scrutinizer ignore-type */ mb_internal_encoding(), self::ENCODING);
Loading history...
190
	}
191
192
}
193