正在显示
32 个修改的文件
包含
4882 行增加
和
0 行删除
addons/qrcode/.addonrc
0 → 100644
1 | +{"files":[],"license":"regular","licenseto":"15629","licensekey":"HiXjw0UmOMWv1CJ4 p+tHvep\/xS0jC5mBuNA3jQ==","domains":["netcar.com"],"licensecodes":[],"validations":["9c3b042fdd8c2e4e21e4da30dcc79ba1"]} |
addons/qrcode/Qrcode.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace addons\qrcode; | ||
4 | + | ||
5 | +use think\Addons; | ||
6 | +use think\Loader; | ||
7 | + | ||
8 | +/** | ||
9 | + * 二维码生成 | ||
10 | + */ | ||
11 | +class Qrcode extends Addons | ||
12 | +{ | ||
13 | + | ||
14 | + /** | ||
15 | + * 插件安装方法 | ||
16 | + * @return bool | ||
17 | + */ | ||
18 | + public function install() | ||
19 | + { | ||
20 | + return true; | ||
21 | + } | ||
22 | + | ||
23 | + /** | ||
24 | + * 插件卸载方法 | ||
25 | + * @return bool | ||
26 | + */ | ||
27 | + public function uninstall() | ||
28 | + { | ||
29 | + return true; | ||
30 | + } | ||
31 | + | ||
32 | + /** | ||
33 | + * 添加命名空间 | ||
34 | + */ | ||
35 | + public function appInit() | ||
36 | + { | ||
37 | + if (!class_exists('\BaconQrCode\Writer')) { | ||
38 | + Loader::addNamespace('BaconQrCode', ADDON_PATH . 'qrcode' . DS . 'library' . DS . 'BaconQrCode' . DS); | ||
39 | + } | ||
40 | + if (!class_exists('\Endroid\QrCode\QrCode')) { | ||
41 | + Loader::addNamespace('Endroid', ADDON_PATH . 'qrcode' . DS . 'library' . DS . 'Endroid' . DS); | ||
42 | + } | ||
43 | + if (!class_exists('\MyCLabs\Enum\Enum')) { | ||
44 | + Loader::addNamespace('MyCLabs', ADDON_PATH . 'qrcode' . DS . 'library' . DS . 'MyCLabs' . DS); | ||
45 | + } | ||
46 | + if (!class_exists('\DASPRiD\Enum\EnumMap')) { | ||
47 | + Loader::addNamespace('DASPRiD', ADDON_PATH . 'qrcode' . DS . 'library' . DS . 'DASPRiD' . DS); | ||
48 | + } | ||
49 | + } | ||
50 | + | ||
51 | +} |
addons/qrcode/config.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +return [ | ||
4 | + [ | ||
5 | + 'name' => 'text', | ||
6 | + 'title' => '默认文本', | ||
7 | + 'type' => 'string', | ||
8 | + 'content' => [], | ||
9 | + 'value' => 'Hello world!', | ||
10 | + 'rule' => 'required', | ||
11 | + 'msg' => '', | ||
12 | + 'tip' => '', | ||
13 | + 'ok' => '', | ||
14 | + 'extend' => '', | ||
15 | + ], | ||
16 | + [ | ||
17 | + 'name' => 'size', | ||
18 | + 'title' => '默认宽高', | ||
19 | + 'type' => 'number', | ||
20 | + 'content' => [], | ||
21 | + 'value' => '300', | ||
22 | + 'rule' => 'required', | ||
23 | + 'msg' => '', | ||
24 | + 'tip' => '', | ||
25 | + 'ok' => '', | ||
26 | + 'extend' => '', | ||
27 | + ], | ||
28 | + [ | ||
29 | + 'name' => 'padding', | ||
30 | + 'title' => '默认边距', | ||
31 | + 'type' => 'number', | ||
32 | + 'content' => [], | ||
33 | + 'value' => '15', | ||
34 | + 'rule' => 'required', | ||
35 | + 'msg' => '', | ||
36 | + 'tip' => '', | ||
37 | + 'ok' => '', | ||
38 | + 'extend' => '', | ||
39 | + ], | ||
40 | + [ | ||
41 | + 'name' => 'format', | ||
42 | + 'title' => '默认格式', | ||
43 | + 'type' => 'radio', | ||
44 | + 'content' => [ | ||
45 | + 'png' => 'PNG', | ||
46 | + 'svg' => 'SVG(不支持标签)', | ||
47 | + ], | ||
48 | + 'value' => 'png', | ||
49 | + 'rule' => 'required', | ||
50 | + 'msg' => '', | ||
51 | + 'tip' => '', | ||
52 | + 'ok' => '', | ||
53 | + 'extend' => '', | ||
54 | + ], | ||
55 | + [ | ||
56 | + 'name' => 'errorlevel', | ||
57 | + 'title' => '容错级别', | ||
58 | + 'type' => 'radio', | ||
59 | + 'content' => [ | ||
60 | + 'low' => '低', | ||
61 | + 'medium' => '中', | ||
62 | + 'quartile' => '高', | ||
63 | + 'high' => '超高', | ||
64 | + ], | ||
65 | + 'value' => 'medium', | ||
66 | + 'rule' => 'required', | ||
67 | + 'msg' => '', | ||
68 | + 'tip' => '', | ||
69 | + 'ok' => '', | ||
70 | + 'extend' => '', | ||
71 | + ], | ||
72 | + [ | ||
73 | + 'name' => 'foreground', | ||
74 | + 'title' => '前景色', | ||
75 | + 'type' => 'string', | ||
76 | + 'content' => [], | ||
77 | + 'value' => '#000000', | ||
78 | + 'rule' => 'required', | ||
79 | + 'msg' => '', | ||
80 | + 'tip' => '', | ||
81 | + 'ok' => '', | ||
82 | + 'extend' => '', | ||
83 | + ], | ||
84 | + [ | ||
85 | + 'name' => 'background', | ||
86 | + 'title' => '背景色', | ||
87 | + 'type' => 'string', | ||
88 | + 'content' => [], | ||
89 | + 'value' => '#ffffff', | ||
90 | + 'rule' => 'required', | ||
91 | + 'msg' => '', | ||
92 | + 'tip' => '', | ||
93 | + 'ok' => '', | ||
94 | + 'extend' => '', | ||
95 | + ], | ||
96 | + [ | ||
97 | + 'name' => 'label', | ||
98 | + 'title' => '默认标签', | ||
99 | + 'type' => 'string', | ||
100 | + 'content' => [], | ||
101 | + 'value' => '', | ||
102 | + 'rule' => '', | ||
103 | + 'msg' => '', | ||
104 | + 'tip' => '', | ||
105 | + 'ok' => '', | ||
106 | + 'extend' => '', | ||
107 | + ], | ||
108 | + [ | ||
109 | + 'name' => 'labelfontsize', | ||
110 | + 'title' => '标签字体大小', | ||
111 | + 'type' => 'number', | ||
112 | + 'content' => [], | ||
113 | + 'value' => '14', | ||
114 | + 'rule' => '', | ||
115 | + 'msg' => '', | ||
116 | + 'tip' => '', | ||
117 | + 'ok' => '', | ||
118 | + 'extend' => '', | ||
119 | + ], | ||
120 | + [ | ||
121 | + 'name' => 'labelfontpath', | ||
122 | + 'title' => '标签字体', | ||
123 | + 'type' => 'file', | ||
124 | + 'content' => [], | ||
125 | + 'value' => '/assets/fonts/SourceHanSansK-Regular.ttf', | ||
126 | + 'rule' => 'required', | ||
127 | + 'msg' => '', | ||
128 | + 'tip' => '', | ||
129 | + 'ok' => '', | ||
130 | + 'extend' => '', | ||
131 | + ], | ||
132 | + [ | ||
133 | + 'name' => 'labelalignment', | ||
134 | + 'title' => '标签对齐方式', | ||
135 | + 'type' => 'radio', | ||
136 | + 'content' => [ | ||
137 | + 'left' => '左', | ||
138 | + 'center' => '居中', | ||
139 | + 'right' => '右', | ||
140 | + ], | ||
141 | + 'value' => 'center', | ||
142 | + 'rule' => '', | ||
143 | + 'msg' => '', | ||
144 | + 'tip' => '', | ||
145 | + 'ok' => '', | ||
146 | + 'extend' => '', | ||
147 | + ], | ||
148 | + [ | ||
149 | + 'name' => 'logo', | ||
150 | + 'title' => '默认显示Logo', | ||
151 | + 'type' => 'radio', | ||
152 | + 'content' => [ | ||
153 | + '否', | ||
154 | + '是', | ||
155 | + ], | ||
156 | + 'value' => '0', | ||
157 | + 'rule' => 'required', | ||
158 | + 'msg' => '', | ||
159 | + 'tip' => '', | ||
160 | + 'ok' => '', | ||
161 | + 'extend' => '', | ||
162 | + ], | ||
163 | + [ | ||
164 | + 'name' => 'logopath', | ||
165 | + 'title' => 'Logo图片', | ||
166 | + 'type' => 'image', | ||
167 | + 'content' => [], | ||
168 | + 'value' => '/assets/img/qrcode.png', | ||
169 | + 'rule' => 'required', | ||
170 | + 'msg' => '', | ||
171 | + 'tip' => '', | ||
172 | + 'ok' => '', | ||
173 | + 'extend' => '', | ||
174 | + ], | ||
175 | + [ | ||
176 | + 'name' => 'logosize', | ||
177 | + 'title' => 'Logo大小', | ||
178 | + 'type' => 'number', | ||
179 | + 'content' => [], | ||
180 | + 'value' => '50', | ||
181 | + 'rule' => 'required', | ||
182 | + 'msg' => '', | ||
183 | + 'tip' => '', | ||
184 | + 'ok' => '', | ||
185 | + 'extend' => '', | ||
186 | + ], | ||
187 | + [ | ||
188 | + 'name' => 'writefile', | ||
189 | + 'title' => '写入文件', | ||
190 | + 'type' => 'radio', | ||
191 | + 'content' => [ | ||
192 | + '否', | ||
193 | + '是', | ||
194 | + ], | ||
195 | + 'value' => '0', | ||
196 | + 'rule' => 'required', | ||
197 | + 'msg' => '', | ||
198 | + 'tip' => '', | ||
199 | + 'ok' => '', | ||
200 | + 'extend' => '', | ||
201 | + ], | ||
202 | + [ | ||
203 | + 'name' => 'limitreferer', | ||
204 | + 'title' => '防盗链配置', | ||
205 | + 'type' => 'radio', | ||
206 | + 'content' => [ | ||
207 | + '否', | ||
208 | + '是', | ||
209 | + ], | ||
210 | + 'value' => '0', | ||
211 | + 'rule' => 'required', | ||
212 | + 'msg' => '', | ||
213 | + 'tip' => '', | ||
214 | + 'ok' => '', | ||
215 | + 'extend' => '', | ||
216 | + ], | ||
217 | + [ | ||
218 | + 'name' => 'allowemptyreferer', | ||
219 | + 'title' => '允许空referer', | ||
220 | + 'visible' => 'limitreferer=1', | ||
221 | + 'type' => 'radio', | ||
222 | + 'content' => [ | ||
223 | + '否', | ||
224 | + '是', | ||
225 | + ], | ||
226 | + 'value' => '0', | ||
227 | + 'rule' => 'required', | ||
228 | + 'msg' => '', | ||
229 | + 'tip' => '', | ||
230 | + 'ok' => '', | ||
231 | + 'extend' => '', | ||
232 | + ], | ||
233 | + [ | ||
234 | + 'name' => 'allowrefererlist', | ||
235 | + 'title' => '允许的域名列表', | ||
236 | + 'visible' => 'limitreferer=1', | ||
237 | + 'type' => 'text', | ||
238 | + 'content' => [ | ||
239 | + ], | ||
240 | + 'value' => '', | ||
241 | + 'rule' => '', | ||
242 | + 'msg' => '', | ||
243 | + 'tip' => '一行一个域名,支持泛域名,*表示所有域名', | ||
244 | + 'ok' => '', | ||
245 | + 'extend' => '', | ||
246 | + ], | ||
247 | + [ | ||
248 | + 'name' => 'rewrite', | ||
249 | + 'title' => '伪静态', | ||
250 | + 'type' => 'array', | ||
251 | + 'content' => [], | ||
252 | + 'value' => [ | ||
253 | + 'index/index' => '/qrcode$', | ||
254 | + 'index/build' => '/qrcode/build$', | ||
255 | + ], | ||
256 | + 'rule' => 'required', | ||
257 | + 'msg' => '', | ||
258 | + 'tip' => '', | ||
259 | + 'ok' => '', | ||
260 | + 'extend' => '', | ||
261 | + ], | ||
262 | +]; |
addons/qrcode/controller/Index.php
0 → 100644
1 | +<?php | ||
2 | + | ||
3 | +namespace addons\qrcode\controller; | ||
4 | + | ||
5 | +use think\addons\Controller; | ||
6 | +use think\exception\HttpResponseException; | ||
7 | +use think\Response; | ||
8 | + | ||
9 | +/** | ||
10 | + * 二维码生成 | ||
11 | + * | ||
12 | + */ | ||
13 | +class Index extends Controller | ||
14 | +{ | ||
15 | + public function index() | ||
16 | + { | ||
17 | + return $this->view->fetch(); | ||
18 | + } | ||
19 | + | ||
20 | + // 生成二维码 | ||
21 | + public function build() | ||
22 | + { | ||
23 | + $config = get_addon_config('qrcode'); | ||
24 | + if (isset($config['limitreferer']) && $config['limitreferer']) { | ||
25 | + $referer = $this->request->server('HTTP_REFERER', ''); | ||
26 | + $refererInfo = parse_url($referer); | ||
27 | + $refererHost = $referer && $refererInfo ? $refererInfo['host'] : ''; | ||
28 | + $refererHostArr = explode('.', $refererHost); | ||
29 | + $wildcardDomain = ''; | ||
30 | + if (count($refererHostArr) > 2) { | ||
31 | + $refererHostArr[0] = '*'; | ||
32 | + $wildcardDomain = implode('.', $refererHostArr); | ||
33 | + } | ||
34 | + $allowRefererList = $config['allowrefererlist'] ?? ''; | ||
35 | + $domainArr = explode("\n", str_replace("\r", "", $allowRefererList)); | ||
36 | + $domainArr = array_filter(array_unique($domainArr)); | ||
37 | + $domainArr[] = request()->host(true); | ||
38 | + | ||
39 | + $inAllowList = false; | ||
40 | + | ||
41 | + if (in_array('*', $domainArr) || ($refererHost && in_array($refererHost, $domainArr)) || ($wildcardDomain && in_array($wildcardDomain, $domainArr))) { | ||
42 | + $inAllowList = true; | ||
43 | + } | ||
44 | + | ||
45 | + if (!$inAllowList && (!$referer && $config['allowemptyreferer'])) { | ||
46 | + $inAllowList = true; | ||
47 | + } | ||
48 | + | ||
49 | + if (!$inAllowList) { | ||
50 | + $response = Response::create('暂无权限', 'html', 403); | ||
51 | + throw new HttpResponseException($response); | ||
52 | + } | ||
53 | + } | ||
54 | + $params = $this->request->get(); | ||
55 | + $params = array_intersect_key($params, array_flip(['text', 'size', 'padding', 'errorlevel', 'foreground', 'background', 'logo', 'logosize', 'logopath', 'label', 'labelfontsize', 'labelalignment'])); | ||
56 | + | ||
57 | + $params['text'] = $this->request->get('text', $config['text'], 'trim'); | ||
58 | + $params['label'] = $this->request->get('label', $config['label'], 'trim'); | ||
59 | + | ||
60 | + $qrCode = \addons\qrcode\library\Service::qrcode($params); | ||
61 | + | ||
62 | + $mimetype = $config['format'] == 'png' ? 'image/png' : 'image/svg+xml'; | ||
63 | + | ||
64 | + $response = Response::create()->header("Content-Type", $mimetype); | ||
65 | + | ||
66 | + // 直接显示二维码 | ||
67 | + header('Content-Type: ' . $qrCode->getContentType()); | ||
68 | + $response->content($qrCode->writeString()); | ||
69 | + | ||
70 | + // 写入到文件 | ||
71 | + if ($config['writefile']) { | ||
72 | + $qrcodePath = ROOT_PATH . 'public/uploads/qrcode/'; | ||
73 | + if (!is_dir($qrcodePath)) { | ||
74 | + @mkdir($qrcodePath); | ||
75 | + } | ||
76 | + if (is_really_writable($qrcodePath)) { | ||
77 | + $filePath = $qrcodePath . md5(implode('', $params)) . '.' . $config['format']; | ||
78 | + $qrCode->writeFile($filePath); | ||
79 | + } | ||
80 | + } | ||
81 | + | ||
82 | + return $response; | ||
83 | + } | ||
84 | +} |
addons/qrcode/info.ini
0 → 100644
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +use BaconQrCode\Exception\InvalidArgumentException; | ||
7 | +use SplFixedArray; | ||
8 | + | ||
9 | +/** | ||
10 | + * A simple, fast array of bits. | ||
11 | + */ | ||
12 | +final class BitArray | ||
13 | +{ | ||
14 | + /** | ||
15 | + * Bits represented as an array of integers. | ||
16 | + * | ||
17 | + * @var SplFixedArray<int> | ||
18 | + */ | ||
19 | + private $bits; | ||
20 | + | ||
21 | + /** | ||
22 | + * Size of the bit array in bits. | ||
23 | + * | ||
24 | + * @var int | ||
25 | + */ | ||
26 | + private $size; | ||
27 | + | ||
28 | + /** | ||
29 | + * Creates a new bit array with a given size. | ||
30 | + */ | ||
31 | + public function __construct(int $size = 0) | ||
32 | + { | ||
33 | + $this->size = $size; | ||
34 | + $this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0)); | ||
35 | + } | ||
36 | + | ||
37 | + /** | ||
38 | + * Gets the size in bits. | ||
39 | + */ | ||
40 | + public function getSize() : int | ||
41 | + { | ||
42 | + return $this->size; | ||
43 | + } | ||
44 | + | ||
45 | + /** | ||
46 | + * Gets the size in bytes. | ||
47 | + */ | ||
48 | + public function getSizeInBytes() : int | ||
49 | + { | ||
50 | + return ($this->size + 7) >> 3; | ||
51 | + } | ||
52 | + | ||
53 | + /** | ||
54 | + * Ensures that the array has a minimum capacity. | ||
55 | + */ | ||
56 | + public function ensureCapacity(int $size) : void | ||
57 | + { | ||
58 | + if ($size > count($this->bits) << 5) { | ||
59 | + $this->bits->setSize(($size + 31) >> 5); | ||
60 | + } | ||
61 | + } | ||
62 | + | ||
63 | + /** | ||
64 | + * Gets a specific bit. | ||
65 | + */ | ||
66 | + public function get(int $i) : bool | ||
67 | + { | ||
68 | + return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f))); | ||
69 | + } | ||
70 | + | ||
71 | + /** | ||
72 | + * Sets a specific bit. | ||
73 | + */ | ||
74 | + public function set(int $i) : void | ||
75 | + { | ||
76 | + $this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f); | ||
77 | + } | ||
78 | + | ||
79 | + /** | ||
80 | + * Flips a specific bit. | ||
81 | + */ | ||
82 | + public function flip(int $i) : void | ||
83 | + { | ||
84 | + $this->bits[$i >> 5] ^= 1 << ($i & 0x1f); | ||
85 | + } | ||
86 | + | ||
87 | + /** | ||
88 | + * Gets the next set bit position from a given position. | ||
89 | + */ | ||
90 | + public function getNextSet(int $from) : int | ||
91 | + { | ||
92 | + if ($from >= $this->size) { | ||
93 | + return $this->size; | ||
94 | + } | ||
95 | + | ||
96 | + $bitsOffset = $from >> 5; | ||
97 | + $currentBits = $this->bits[$bitsOffset]; | ||
98 | + $bitsLength = count($this->bits); | ||
99 | + $currentBits &= ~((1 << ($from & 0x1f)) - 1); | ||
100 | + | ||
101 | + while (0 === $currentBits) { | ||
102 | + if (++$bitsOffset === $bitsLength) { | ||
103 | + return $this->size; | ||
104 | + } | ||
105 | + | ||
106 | + $currentBits = $this->bits[$bitsOffset]; | ||
107 | + } | ||
108 | + | ||
109 | + $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits); | ||
110 | + return $result > $this->size ? $this->size : $result; | ||
111 | + } | ||
112 | + | ||
113 | + /** | ||
114 | + * Gets the next unset bit position from a given position. | ||
115 | + */ | ||
116 | + public function getNextUnset(int $from) : int | ||
117 | + { | ||
118 | + if ($from >= $this->size) { | ||
119 | + return $this->size; | ||
120 | + } | ||
121 | + | ||
122 | + $bitsOffset = $from >> 5; | ||
123 | + $currentBits = ~$this->bits[$bitsOffset]; | ||
124 | + $bitsLength = count($this->bits); | ||
125 | + $currentBits &= ~((1 << ($from & 0x1f)) - 1); | ||
126 | + | ||
127 | + while (0 === $currentBits) { | ||
128 | + if (++$bitsOffset === $bitsLength) { | ||
129 | + return $this->size; | ||
130 | + } | ||
131 | + | ||
132 | + $currentBits = ~$this->bits[$bitsOffset]; | ||
133 | + } | ||
134 | + | ||
135 | + $result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits); | ||
136 | + return $result > $this->size ? $this->size : $result; | ||
137 | + } | ||
138 | + | ||
139 | + /** | ||
140 | + * Sets a bulk of bits. | ||
141 | + */ | ||
142 | + public function setBulk(int $i, int $newBits) : void | ||
143 | + { | ||
144 | + $this->bits[$i >> 5] = $newBits; | ||
145 | + } | ||
146 | + | ||
147 | + /** | ||
148 | + * Sets a range of bits. | ||
149 | + * | ||
150 | + * @throws InvalidArgumentException if end is smaller than start | ||
151 | + */ | ||
152 | + public function setRange(int $start, int $end) : void | ||
153 | + { | ||
154 | + if ($end < $start) { | ||
155 | + throw new InvalidArgumentException('End must be greater or equal to start'); | ||
156 | + } | ||
157 | + | ||
158 | + if ($end === $start) { | ||
159 | + return; | ||
160 | + } | ||
161 | + | ||
162 | + --$end; | ||
163 | + | ||
164 | + $firstInt = $start >> 5; | ||
165 | + $lastInt = $end >> 5; | ||
166 | + | ||
167 | + for ($i = $firstInt; $i <= $lastInt; ++$i) { | ||
168 | + $firstBit = $i > $firstInt ? 0 : $start & 0x1f; | ||
169 | + $lastBit = $i < $lastInt ? 31 : $end & 0x1f; | ||
170 | + | ||
171 | + if (0 === $firstBit && 31 === $lastBit) { | ||
172 | + $mask = 0x7fffffff; | ||
173 | + } else { | ||
174 | + $mask = 0; | ||
175 | + | ||
176 | + for ($j = $firstBit; $j < $lastBit; ++$j) { | ||
177 | + $mask |= 1 << $j; | ||
178 | + } | ||
179 | + } | ||
180 | + | ||
181 | + $this->bits[$i] = $this->bits[$i] | $mask; | ||
182 | + } | ||
183 | + } | ||
184 | + | ||
185 | + /** | ||
186 | + * Clears the bit array, unsetting every bit. | ||
187 | + */ | ||
188 | + public function clear() : void | ||
189 | + { | ||
190 | + $bitsLength = count($this->bits); | ||
191 | + | ||
192 | + for ($i = 0; $i < $bitsLength; ++$i) { | ||
193 | + $this->bits[$i] = 0; | ||
194 | + } | ||
195 | + } | ||
196 | + | ||
197 | + /** | ||
198 | + * Checks if a range of bits is set or not set. | ||
199 | + | ||
200 | + * @throws InvalidArgumentException if end is smaller than start | ||
201 | + */ | ||
202 | + public function isRange(int $start, int $end, bool $value) : bool | ||
203 | + { | ||
204 | + if ($end < $start) { | ||
205 | + throw new InvalidArgumentException('End must be greater or equal to start'); | ||
206 | + } | ||
207 | + | ||
208 | + if ($end === $start) { | ||
209 | + return true; | ||
210 | + } | ||
211 | + | ||
212 | + --$end; | ||
213 | + | ||
214 | + $firstInt = $start >> 5; | ||
215 | + $lastInt = $end >> 5; | ||
216 | + | ||
217 | + for ($i = $firstInt; $i <= $lastInt; ++$i) { | ||
218 | + $firstBit = $i > $firstInt ? 0 : $start & 0x1f; | ||
219 | + $lastBit = $i < $lastInt ? 31 : $end & 0x1f; | ||
220 | + | ||
221 | + if (0 === $firstBit && 31 === $lastBit) { | ||
222 | + $mask = 0x7fffffff; | ||
223 | + } else { | ||
224 | + $mask = 0; | ||
225 | + | ||
226 | + for ($j = $firstBit; $j <= $lastBit; ++$j) { | ||
227 | + $mask |= 1 << $j; | ||
228 | + } | ||
229 | + } | ||
230 | + | ||
231 | + if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) { | ||
232 | + return false; | ||
233 | + } | ||
234 | + } | ||
235 | + | ||
236 | + return true; | ||
237 | + } | ||
238 | + | ||
239 | + /** | ||
240 | + * Appends a bit to the array. | ||
241 | + */ | ||
242 | + public function appendBit(bool $bit) : void | ||
243 | + { | ||
244 | + $this->ensureCapacity($this->size + 1); | ||
245 | + | ||
246 | + if ($bit) { | ||
247 | + $this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f)); | ||
248 | + } | ||
249 | + | ||
250 | + ++$this->size; | ||
251 | + } | ||
252 | + | ||
253 | + /** | ||
254 | + * Appends a number of bits (up to 32) to the array. | ||
255 | + | ||
256 | + * @throws InvalidArgumentException if num bits is not between 0 and 32 | ||
257 | + */ | ||
258 | + public function appendBits(int $value, int $numBits) : void | ||
259 | + { | ||
260 | + if ($numBits < 0 || $numBits > 32) { | ||
261 | + throw new InvalidArgumentException('Num bits must be between 0 and 32'); | ||
262 | + } | ||
263 | + | ||
264 | + $this->ensureCapacity($this->size + $numBits); | ||
265 | + | ||
266 | + for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) { | ||
267 | + $this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1); | ||
268 | + } | ||
269 | + } | ||
270 | + | ||
271 | + /** | ||
272 | + * Appends another bit array to this array. | ||
273 | + */ | ||
274 | + public function appendBitArray(self $other) : void | ||
275 | + { | ||
276 | + $otherSize = $other->getSize(); | ||
277 | + $this->ensureCapacity($this->size + $other->getSize()); | ||
278 | + | ||
279 | + for ($i = 0; $i < $otherSize; ++$i) { | ||
280 | + $this->appendBit($other->get($i)); | ||
281 | + } | ||
282 | + } | ||
283 | + | ||
284 | + /** | ||
285 | + * Makes an exclusive-or comparision on the current bit array. | ||
286 | + * | ||
287 | + * @throws InvalidArgumentException if sizes don't match | ||
288 | + */ | ||
289 | + public function xorBits(self $other) : void | ||
290 | + { | ||
291 | + $bitsLength = count($this->bits); | ||
292 | + $otherBits = $other->getBitArray(); | ||
293 | + | ||
294 | + if ($bitsLength !== count($otherBits)) { | ||
295 | + throw new InvalidArgumentException('Sizes don\'t match'); | ||
296 | + } | ||
297 | + | ||
298 | + for ($i = 0; $i < $bitsLength; ++$i) { | ||
299 | + $this->bits[$i] = $this->bits[$i] ^ $otherBits[$i]; | ||
300 | + } | ||
301 | + } | ||
302 | + | ||
303 | + /** | ||
304 | + * Converts the bit array to a byte array. | ||
305 | + * | ||
306 | + * @return SplFixedArray<int> | ||
307 | + */ | ||
308 | + public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray | ||
309 | + { | ||
310 | + $bytes = new SplFixedArray($numBytes); | ||
311 | + | ||
312 | + for ($i = 0; $i < $numBytes; ++$i) { | ||
313 | + $byte = 0; | ||
314 | + | ||
315 | + for ($j = 0; $j < 8; ++$j) { | ||
316 | + if ($this->get($bitOffset)) { | ||
317 | + $byte |= 1 << (7 - $j); | ||
318 | + } | ||
319 | + | ||
320 | + ++$bitOffset; | ||
321 | + } | ||
322 | + | ||
323 | + $bytes[$i] = $byte; | ||
324 | + } | ||
325 | + | ||
326 | + return $bytes; | ||
327 | + } | ||
328 | + | ||
329 | + /** | ||
330 | + * Gets the internal bit array. | ||
331 | + * | ||
332 | + * @return SplFixedArray<int> | ||
333 | + */ | ||
334 | + public function getBitArray() : SplFixedArray | ||
335 | + { | ||
336 | + return $this->bits; | ||
337 | + } | ||
338 | + | ||
339 | + /** | ||
340 | + * Reverses the array. | ||
341 | + */ | ||
342 | + public function reverse() : void | ||
343 | + { | ||
344 | + $newBits = new SplFixedArray(count($this->bits)); | ||
345 | + | ||
346 | + for ($i = 0; $i < $this->size; ++$i) { | ||
347 | + if ($this->get($this->size - $i - 1)) { | ||
348 | + $newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f)); | ||
349 | + } | ||
350 | + } | ||
351 | + | ||
352 | + $this->bits = $newBits; | ||
353 | + } | ||
354 | + | ||
355 | + /** | ||
356 | + * Returns a string representation of the bit array. | ||
357 | + */ | ||
358 | + public function __toString() : string | ||
359 | + { | ||
360 | + $result = ''; | ||
361 | + | ||
362 | + for ($i = 0; $i < $this->size; ++$i) { | ||
363 | + if (0 === ($i & 0x07)) { | ||
364 | + $result .= ' '; | ||
365 | + } | ||
366 | + | ||
367 | + $result .= $this->get($i) ? 'X' : '.'; | ||
368 | + } | ||
369 | + | ||
370 | + return $result; | ||
371 | + } | ||
372 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +use BaconQrCode\Exception\InvalidArgumentException; | ||
7 | +use SplFixedArray; | ||
8 | + | ||
9 | +/** | ||
10 | + * Bit matrix. | ||
11 | + * | ||
12 | + * Represents a 2D matrix of bits. In function arguments below, and throughout | ||
13 | + * the common module, x is the column position, and y is the row position. The | ||
14 | + * ordering is always x, y. The origin is at the top-left. | ||
15 | + */ | ||
16 | +class BitMatrix | ||
17 | +{ | ||
18 | + /** | ||
19 | + * Width of the bit matrix. | ||
20 | + * | ||
21 | + * @var int | ||
22 | + */ | ||
23 | + private $width; | ||
24 | + | ||
25 | + /** | ||
26 | + * Height of the bit matrix. | ||
27 | + * | ||
28 | + * @var int | ||
29 | + */ | ||
30 | + private $height; | ||
31 | + | ||
32 | + /** | ||
33 | + * Size in bits of each individual row. | ||
34 | + * | ||
35 | + * @var int | ||
36 | + */ | ||
37 | + private $rowSize; | ||
38 | + | ||
39 | + /** | ||
40 | + * Bits representation. | ||
41 | + * | ||
42 | + * @var SplFixedArray<int> | ||
43 | + */ | ||
44 | + private $bits; | ||
45 | + | ||
46 | + /** | ||
47 | + * @throws InvalidArgumentException if a dimension is smaller than zero | ||
48 | + */ | ||
49 | + public function __construct(int $width, int $height = null) | ||
50 | + { | ||
51 | + if (null === $height) { | ||
52 | + $height = $width; | ||
53 | + } | ||
54 | + | ||
55 | + if ($width < 1 || $height < 1) { | ||
56 | + throw new InvalidArgumentException('Both dimensions must be greater than zero'); | ||
57 | + } | ||
58 | + | ||
59 | + $this->width = $width; | ||
60 | + $this->height = $height; | ||
61 | + $this->rowSize = ($width + 31) >> 5; | ||
62 | + $this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0)); | ||
63 | + } | ||
64 | + | ||
65 | + /** | ||
66 | + * Gets the requested bit, where true means black. | ||
67 | + */ | ||
68 | + public function get(int $x, int $y) : bool | ||
69 | + { | ||
70 | + $offset = $y * $this->rowSize + ($x >> 5); | ||
71 | + return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1); | ||
72 | + } | ||
73 | + | ||
74 | + /** | ||
75 | + * Sets the given bit to true. | ||
76 | + */ | ||
77 | + public function set(int $x, int $y) : void | ||
78 | + { | ||
79 | + $offset = $y * $this->rowSize + ($x >> 5); | ||
80 | + $this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f)); | ||
81 | + } | ||
82 | + | ||
83 | + /** | ||
84 | + * Flips the given bit. | ||
85 | + */ | ||
86 | + public function flip(int $x, int $y) : void | ||
87 | + { | ||
88 | + $offset = $y * $this->rowSize + ($x >> 5); | ||
89 | + $this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f)); | ||
90 | + } | ||
91 | + | ||
92 | + /** | ||
93 | + * Clears all bits (set to false). | ||
94 | + */ | ||
95 | + public function clear() : void | ||
96 | + { | ||
97 | + $max = count($this->bits); | ||
98 | + | ||
99 | + for ($i = 0; $i < $max; ++$i) { | ||
100 | + $this->bits[$i] = 0; | ||
101 | + } | ||
102 | + } | ||
103 | + | ||
104 | + /** | ||
105 | + * Sets a square region of the bit matrix to true. | ||
106 | + * | ||
107 | + * @throws InvalidArgumentException if left or top are negative | ||
108 | + * @throws InvalidArgumentException if width or height are smaller than 1 | ||
109 | + * @throws InvalidArgumentException if region does not fit into the matix | ||
110 | + */ | ||
111 | + public function setRegion(int $left, int $top, int $width, int $height) : void | ||
112 | + { | ||
113 | + if ($top < 0 || $left < 0) { | ||
114 | + throw new InvalidArgumentException('Left and top must be non-negative'); | ||
115 | + } | ||
116 | + | ||
117 | + if ($height < 1 || $width < 1) { | ||
118 | + throw new InvalidArgumentException('Width and height must be at least 1'); | ||
119 | + } | ||
120 | + | ||
121 | + $right = $left + $width; | ||
122 | + $bottom = $top + $height; | ||
123 | + | ||
124 | + if ($bottom > $this->height || $right > $this->width) { | ||
125 | + throw new InvalidArgumentException('The region must fit inside the matrix'); | ||
126 | + } | ||
127 | + | ||
128 | + for ($y = $top; $y < $bottom; ++$y) { | ||
129 | + $offset = $y * $this->rowSize; | ||
130 | + | ||
131 | + for ($x = $left; $x < $right; ++$x) { | ||
132 | + $index = $offset + ($x >> 5); | ||
133 | + $this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f)); | ||
134 | + } | ||
135 | + } | ||
136 | + } | ||
137 | + | ||
138 | + /** | ||
139 | + * A fast method to retrieve one row of data from the matrix as a BitArray. | ||
140 | + */ | ||
141 | + public function getRow(int $y, BitArray $row = null) : BitArray | ||
142 | + { | ||
143 | + if (null === $row || $row->getSize() < $this->width) { | ||
144 | + $row = new BitArray($this->width); | ||
145 | + } | ||
146 | + | ||
147 | + $offset = $y * $this->rowSize; | ||
148 | + | ||
149 | + for ($x = 0; $x < $this->rowSize; ++$x) { | ||
150 | + $row->setBulk($x << 5, $this->bits[$offset + $x]); | ||
151 | + } | ||
152 | + | ||
153 | + return $row; | ||
154 | + } | ||
155 | + | ||
156 | + /** | ||
157 | + * Sets a row of data from a BitArray. | ||
158 | + */ | ||
159 | + public function setRow(int $y, BitArray $row) : void | ||
160 | + { | ||
161 | + $bits = $row->getBitArray(); | ||
162 | + | ||
163 | + for ($i = 0; $i < $this->rowSize; ++$i) { | ||
164 | + $this->bits[$y * $this->rowSize + $i] = $bits[$i]; | ||
165 | + } | ||
166 | + } | ||
167 | + | ||
168 | + /** | ||
169 | + * This is useful in detecting the enclosing rectangle of a 'pure' barcode. | ||
170 | + * | ||
171 | + * @return int[]|null | ||
172 | + */ | ||
173 | + public function getEnclosingRectangle() : ?array | ||
174 | + { | ||
175 | + $left = $this->width; | ||
176 | + $top = $this->height; | ||
177 | + $right = -1; | ||
178 | + $bottom = -1; | ||
179 | + | ||
180 | + for ($y = 0; $y < $this->height; ++$y) { | ||
181 | + for ($x32 = 0; $x32 < $this->rowSize; ++$x32) { | ||
182 | + $bits = $this->bits[$y * $this->rowSize + $x32]; | ||
183 | + | ||
184 | + if (0 !== $bits) { | ||
185 | + if ($y < $top) { | ||
186 | + $top = $y; | ||
187 | + } | ||
188 | + | ||
189 | + if ($y > $bottom) { | ||
190 | + $bottom = $y; | ||
191 | + } | ||
192 | + | ||
193 | + if ($x32 * 32 < $left) { | ||
194 | + $bit = 0; | ||
195 | + | ||
196 | + while (($bits << (31 - $bit)) === 0) { | ||
197 | + $bit++; | ||
198 | + } | ||
199 | + | ||
200 | + if (($x32 * 32 + $bit) < $left) { | ||
201 | + $left = $x32 * 32 + $bit; | ||
202 | + } | ||
203 | + } | ||
204 | + } | ||
205 | + | ||
206 | + if ($x32 * 32 + 31 > $right) { | ||
207 | + $bit = 31; | ||
208 | + | ||
209 | + while (0 === BitUtils::unsignedRightShift($bits, $bit)) { | ||
210 | + --$bit; | ||
211 | + } | ||
212 | + | ||
213 | + if (($x32 * 32 + $bit) > $right) { | ||
214 | + $right = $x32 * 32 + $bit; | ||
215 | + } | ||
216 | + } | ||
217 | + } | ||
218 | + } | ||
219 | + | ||
220 | + $width = $right - $left; | ||
221 | + $height = $bottom - $top; | ||
222 | + | ||
223 | + if ($width < 0 || $height < 0) { | ||
224 | + return null; | ||
225 | + } | ||
226 | + | ||
227 | + return [$left, $top, $width, $height]; | ||
228 | + } | ||
229 | + | ||
230 | + /** | ||
231 | + * Gets the most top left set bit. | ||
232 | + * | ||
233 | + * This is useful in detecting a corner of a 'pure' barcode. | ||
234 | + * | ||
235 | + * @return int[]|null | ||
236 | + */ | ||
237 | + public function getTopLeftOnBit() : ?array | ||
238 | + { | ||
239 | + $bitsOffset = 0; | ||
240 | + | ||
241 | + while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) { | ||
242 | + ++$bitsOffset; | ||
243 | + } | ||
244 | + | ||
245 | + if (count($this->bits) === $bitsOffset) { | ||
246 | + return null; | ||
247 | + } | ||
248 | + | ||
249 | + $x = intdiv($bitsOffset, $this->rowSize); | ||
250 | + $y = ($bitsOffset % $this->rowSize) << 5; | ||
251 | + | ||
252 | + $bits = $this->bits[$bitsOffset]; | ||
253 | + $bit = 0; | ||
254 | + | ||
255 | + while (0 === ($bits << (31 - $bit))) { | ||
256 | + ++$bit; | ||
257 | + } | ||
258 | + | ||
259 | + $x += $bit; | ||
260 | + | ||
261 | + return [$x, $y]; | ||
262 | + } | ||
263 | + | ||
264 | + /** | ||
265 | + * Gets the most bottom right set bit. | ||
266 | + * | ||
267 | + * This is useful in detecting a corner of a 'pure' barcode. | ||
268 | + * | ||
269 | + * @return int[]|null | ||
270 | + */ | ||
271 | + public function getBottomRightOnBit() : ?array | ||
272 | + { | ||
273 | + $bitsOffset = count($this->bits) - 1; | ||
274 | + | ||
275 | + while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) { | ||
276 | + --$bitsOffset; | ||
277 | + } | ||
278 | + | ||
279 | + if ($bitsOffset < 0) { | ||
280 | + return null; | ||
281 | + } | ||
282 | + | ||
283 | + $x = intdiv($bitsOffset, $this->rowSize); | ||
284 | + $y = ($bitsOffset % $this->rowSize) << 5; | ||
285 | + | ||
286 | + $bits = $this->bits[$bitsOffset]; | ||
287 | + $bit = 0; | ||
288 | + | ||
289 | + while (0 === BitUtils::unsignedRightShift($bits, $bit)) { | ||
290 | + --$bit; | ||
291 | + } | ||
292 | + | ||
293 | + $x += $bit; | ||
294 | + | ||
295 | + return [$x, $y]; | ||
296 | + } | ||
297 | + | ||
298 | + /** | ||
299 | + * Gets the width of the matrix, | ||
300 | + */ | ||
301 | + public function getWidth() : int | ||
302 | + { | ||
303 | + return $this->width; | ||
304 | + } | ||
305 | + | ||
306 | + /** | ||
307 | + * Gets the height of the matrix. | ||
308 | + */ | ||
309 | + public function getHeight() : int | ||
310 | + { | ||
311 | + return $this->height; | ||
312 | + } | ||
313 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +/** | ||
7 | + * General bit utilities. | ||
8 | + * | ||
9 | + * All utility methods are based on 32-bit integers and also work on 64-bit | ||
10 | + * systems. | ||
11 | + */ | ||
12 | +final class BitUtils | ||
13 | +{ | ||
14 | + private function __construct() | ||
15 | + { | ||
16 | + } | ||
17 | + | ||
18 | + /** | ||
19 | + * Performs an unsigned right shift. | ||
20 | + * | ||
21 | + * This is the same as the unsigned right shift operator ">>>" in other | ||
22 | + * languages. | ||
23 | + */ | ||
24 | + public static function unsignedRightShift(int $a, int $b) : int | ||
25 | + { | ||
26 | + return ( | ||
27 | + $a >= 0 | ||
28 | + ? $a >> $b | ||
29 | + : (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1)) | ||
30 | + ); | ||
31 | + } | ||
32 | + | ||
33 | + /** | ||
34 | + * Gets the number of trailing zeros. | ||
35 | + */ | ||
36 | + public static function numberOfTrailingZeros(int $i) : int | ||
37 | + { | ||
38 | + $lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1'); | ||
39 | + return $lastPos === false ? 32 : 31 - $lastPos; | ||
40 | + } | ||
41 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +use BaconQrCode\Exception\InvalidArgumentException; | ||
7 | +use DASPRiD\Enum\AbstractEnum; | ||
8 | + | ||
9 | +/** | ||
10 | + * Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004. | ||
11 | + * | ||
12 | + * @method static self CP437() | ||
13 | + * @method static self ISO8859_1() | ||
14 | + * @method static self ISO8859_2() | ||
15 | + * @method static self ISO8859_3() | ||
16 | + * @method static self ISO8859_4() | ||
17 | + * @method static self ISO8859_5() | ||
18 | + * @method static self ISO8859_6() | ||
19 | + * @method static self ISO8859_7() | ||
20 | + * @method static self ISO8859_8() | ||
21 | + * @method static self ISO8859_9() | ||
22 | + * @method static self ISO8859_10() | ||
23 | + * @method static self ISO8859_11() | ||
24 | + * @method static self ISO8859_12() | ||
25 | + * @method static self ISO8859_13() | ||
26 | + * @method static self ISO8859_14() | ||
27 | + * @method static self ISO8859_15() | ||
28 | + * @method static self ISO8859_16() | ||
29 | + * @method static self SJIS() | ||
30 | + * @method static self CP1250() | ||
31 | + * @method static self CP1251() | ||
32 | + * @method static self CP1252() | ||
33 | + * @method static self CP1256() | ||
34 | + * @method static self UNICODE_BIG_UNMARKED() | ||
35 | + * @method static self UTF8() | ||
36 | + * @method static self ASCII() | ||
37 | + * @method static self BIG5() | ||
38 | + * @method static self GB18030() | ||
39 | + * @method static self EUC_KR() | ||
40 | + */ | ||
41 | +final class CharacterSetEci extends AbstractEnum | ||
42 | +{ | ||
43 | + protected const CP437 = [[0, 2]]; | ||
44 | + protected const ISO8859_1 = [[1, 3], 'ISO-8859-1']; | ||
45 | + protected const ISO8859_2 = [[4], 'ISO-8859-2']; | ||
46 | + protected const ISO8859_3 = [[5], 'ISO-8859-3']; | ||
47 | + protected const ISO8859_4 = [[6], 'ISO-8859-4']; | ||
48 | + protected const ISO8859_5 = [[7], 'ISO-8859-5']; | ||
49 | + protected const ISO8859_6 = [[8], 'ISO-8859-6']; | ||
50 | + protected const ISO8859_7 = [[9], 'ISO-8859-7']; | ||
51 | + protected const ISO8859_8 = [[10], 'ISO-8859-8']; | ||
52 | + protected const ISO8859_9 = [[11], 'ISO-8859-9']; | ||
53 | + protected const ISO8859_10 = [[12], 'ISO-8859-10']; | ||
54 | + protected const ISO8859_11 = [[13], 'ISO-8859-11']; | ||
55 | + protected const ISO8859_12 = [[14], 'ISO-8859-12']; | ||
56 | + protected const ISO8859_13 = [[15], 'ISO-8859-13']; | ||
57 | + protected const ISO8859_14 = [[16], 'ISO-8859-14']; | ||
58 | + protected const ISO8859_15 = [[17], 'ISO-8859-15']; | ||
59 | + protected const ISO8859_16 = [[18], 'ISO-8859-16']; | ||
60 | + protected const SJIS = [[20], 'Shift_JIS']; | ||
61 | + protected const CP1250 = [[21], 'windows-1250']; | ||
62 | + protected const CP1251 = [[22], 'windows-1251']; | ||
63 | + protected const CP1252 = [[23], 'windows-1252']; | ||
64 | + protected const CP1256 = [[24], 'windows-1256']; | ||
65 | + protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig']; | ||
66 | + protected const UTF8 = [[26], 'UTF-8']; | ||
67 | + protected const ASCII = [[27, 170], 'US-ASCII']; | ||
68 | + protected const BIG5 = [[28]]; | ||
69 | + protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK']; | ||
70 | + protected const EUC_KR = [[30], 'EUC-KR']; | ||
71 | + | ||
72 | + /** | ||
73 | + * @var int[] | ||
74 | + */ | ||
75 | + private $values; | ||
76 | + | ||
77 | + /** | ||
78 | + * @var string[] | ||
79 | + */ | ||
80 | + private $otherEncodingNames; | ||
81 | + | ||
82 | + /** | ||
83 | + * @var array<int, self>|null | ||
84 | + */ | ||
85 | + private static $valueToEci; | ||
86 | + | ||
87 | + /** | ||
88 | + * @var array<string, self>|null | ||
89 | + */ | ||
90 | + private static $nameToEci; | ||
91 | + | ||
92 | + public function __construct(array $values, string ...$otherEncodingNames) | ||
93 | + { | ||
94 | + $this->values = $values; | ||
95 | + $this->otherEncodingNames = $otherEncodingNames; | ||
96 | + } | ||
97 | + | ||
98 | + /** | ||
99 | + * Returns the primary value. | ||
100 | + */ | ||
101 | + public function getValue() : int | ||
102 | + { | ||
103 | + return $this->values[0]; | ||
104 | + } | ||
105 | + | ||
106 | + /** | ||
107 | + * Gets character set ECI by value. | ||
108 | + * | ||
109 | + * Returns the representing ECI of a given value, or null if it is legal but unsupported. | ||
110 | + * | ||
111 | + * @throws InvalidArgumentException if value is not between 0 and 900 | ||
112 | + */ | ||
113 | + public static function getCharacterSetEciByValue(int $value) : ?self | ||
114 | + { | ||
115 | + if ($value < 0 || $value >= 900) { | ||
116 | + throw new InvalidArgumentException('Value must be between 0 and 900'); | ||
117 | + } | ||
118 | + | ||
119 | + $valueToEci = self::valueToEci(); | ||
120 | + | ||
121 | + if (! array_key_exists($value, $valueToEci)) { | ||
122 | + return null; | ||
123 | + } | ||
124 | + | ||
125 | + return $valueToEci[$value]; | ||
126 | + } | ||
127 | + | ||
128 | + /** | ||
129 | + * Returns character set ECI by name. | ||
130 | + * | ||
131 | + * Returns the representing ECI of a given name, or null if it is legal but unsupported | ||
132 | + */ | ||
133 | + public static function getCharacterSetEciByName(string $name) : ?self | ||
134 | + { | ||
135 | + $nameToEci = self::nameToEci(); | ||
136 | + $name = strtolower($name); | ||
137 | + | ||
138 | + if (! array_key_exists($name, $nameToEci)) { | ||
139 | + return null; | ||
140 | + } | ||
141 | + | ||
142 | + return $nameToEci[$name]; | ||
143 | + } | ||
144 | + | ||
145 | + private static function valueToEci() : array | ||
146 | + { | ||
147 | + if (null !== self::$valueToEci) { | ||
148 | + return self::$valueToEci; | ||
149 | + } | ||
150 | + | ||
151 | + self::$valueToEci = []; | ||
152 | + | ||
153 | + foreach (self::values() as $eci) { | ||
154 | + foreach ($eci->values as $value) { | ||
155 | + self::$valueToEci[$value] = $eci; | ||
156 | + } | ||
157 | + } | ||
158 | + | ||
159 | + return self::$valueToEci; | ||
160 | + } | ||
161 | + | ||
162 | + private static function nameToEci() : array | ||
163 | + { | ||
164 | + if (null !== self::$nameToEci) { | ||
165 | + return self::$nameToEci; | ||
166 | + } | ||
167 | + | ||
168 | + self::$nameToEci = []; | ||
169 | + | ||
170 | + foreach (self::values() as $eci) { | ||
171 | + self::$nameToEci[strtolower($eci->name())] = $eci; | ||
172 | + | ||
173 | + foreach ($eci->otherEncodingNames as $name) { | ||
174 | + self::$nameToEci[strtolower($name)] = $eci; | ||
175 | + } | ||
176 | + } | ||
177 | + | ||
178 | + return self::$nameToEci; | ||
179 | + } | ||
180 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +/** | ||
7 | + * Encapsulates the parameters for one error-correction block in one symbol version. | ||
8 | + * | ||
9 | + * This includes the number of data codewords, and the number of times a block with these parameters is used | ||
10 | + * consecutively in the QR code version's format. | ||
11 | + */ | ||
12 | +final class EcBlock | ||
13 | +{ | ||
14 | + /** | ||
15 | + * How many times the block is used. | ||
16 | + * | ||
17 | + * @var int | ||
18 | + */ | ||
19 | + private $count; | ||
20 | + | ||
21 | + /** | ||
22 | + * Number of data codewords. | ||
23 | + * | ||
24 | + * @var int | ||
25 | + */ | ||
26 | + private $dataCodewords; | ||
27 | + | ||
28 | + public function __construct(int $count, int $dataCodewords) | ||
29 | + { | ||
30 | + $this->count = $count; | ||
31 | + $this->dataCodewords = $dataCodewords; | ||
32 | + } | ||
33 | + | ||
34 | + /** | ||
35 | + * Returns how many times the block is used. | ||
36 | + */ | ||
37 | + public function getCount() : int | ||
38 | + { | ||
39 | + return $this->count; | ||
40 | + } | ||
41 | + | ||
42 | + /** | ||
43 | + * Returns the number of data codewords. | ||
44 | + */ | ||
45 | + public function getDataCodewords() : int | ||
46 | + { | ||
47 | + return $this->dataCodewords; | ||
48 | + } | ||
49 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +/** | ||
7 | + * Encapsulates a set of error-correction blocks in one symbol version. | ||
8 | + * | ||
9 | + * Most versions will use blocks of differing sizes within one version, so, this encapsulates the parameters for each | ||
10 | + * set of blocks. It also holds the number of error-correction codewords per block since it will be the same across all | ||
11 | + * blocks within one version. | ||
12 | + */ | ||
13 | +final class EcBlocks | ||
14 | +{ | ||
15 | + /** | ||
16 | + * Number of EC codewords per block. | ||
17 | + * | ||
18 | + * @var int | ||
19 | + */ | ||
20 | + private $ecCodewordsPerBlock; | ||
21 | + | ||
22 | + /** | ||
23 | + * List of EC blocks. | ||
24 | + * | ||
25 | + * @var EcBlock[] | ||
26 | + */ | ||
27 | + private $ecBlocks; | ||
28 | + | ||
29 | + public function __construct(int $ecCodewordsPerBlock, EcBlock ...$ecBlocks) | ||
30 | + { | ||
31 | + $this->ecCodewordsPerBlock = $ecCodewordsPerBlock; | ||
32 | + $this->ecBlocks = $ecBlocks; | ||
33 | + } | ||
34 | + | ||
35 | + /** | ||
36 | + * Returns the number of EC codewords per block. | ||
37 | + */ | ||
38 | + public function getEcCodewordsPerBlock() : int | ||
39 | + { | ||
40 | + return $this->ecCodewordsPerBlock; | ||
41 | + } | ||
42 | + | ||
43 | + /** | ||
44 | + * Returns the total number of EC block appearances. | ||
45 | + */ | ||
46 | + public function getNumBlocks() : int | ||
47 | + { | ||
48 | + $total = 0; | ||
49 | + | ||
50 | + foreach ($this->ecBlocks as $ecBlock) { | ||
51 | + $total += $ecBlock->getCount(); | ||
52 | + } | ||
53 | + | ||
54 | + return $total; | ||
55 | + } | ||
56 | + | ||
57 | + /** | ||
58 | + * Returns the total count of EC codewords. | ||
59 | + */ | ||
60 | + public function getTotalEcCodewords() : int | ||
61 | + { | ||
62 | + return $this->ecCodewordsPerBlock * $this->getNumBlocks(); | ||
63 | + } | ||
64 | + | ||
65 | + /** | ||
66 | + * Returns the EC blocks included in this collection. | ||
67 | + * | ||
68 | + * @return EcBlock[] | ||
69 | + */ | ||
70 | + public function getEcBlocks() : array | ||
71 | + { | ||
72 | + return $this->ecBlocks; | ||
73 | + } | ||
74 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +use BaconQrCode\Exception\OutOfBoundsException; | ||
7 | +use DASPRiD\Enum\AbstractEnum; | ||
8 | + | ||
9 | +/** | ||
10 | + * Enum representing the four error correction levels. | ||
11 | + * | ||
12 | + * @method static self L() ~7% correction | ||
13 | + * @method static self M() ~15% correction | ||
14 | + * @method static self Q() ~25% correction | ||
15 | + * @method static self H() ~30% correction | ||
16 | + */ | ||
17 | +final class ErrorCorrectionLevel extends AbstractEnum | ||
18 | +{ | ||
19 | + protected const L = [0x01]; | ||
20 | + protected const M = [0x00]; | ||
21 | + protected const Q = [0x03]; | ||
22 | + protected const H = [0x02]; | ||
23 | + | ||
24 | + /** | ||
25 | + * @var int | ||
26 | + */ | ||
27 | + private $bits; | ||
28 | + | ||
29 | + protected function __construct(int $bits) | ||
30 | + { | ||
31 | + $this->bits = $bits; | ||
32 | + } | ||
33 | + | ||
34 | + /** | ||
35 | + * @throws OutOfBoundsException if number of bits is invalid | ||
36 | + */ | ||
37 | + public static function forBits(int $bits) : self | ||
38 | + { | ||
39 | + switch ($bits) { | ||
40 | + case 0: | ||
41 | + return self::M(); | ||
42 | + | ||
43 | + case 1: | ||
44 | + return self::L(); | ||
45 | + | ||
46 | + case 2: | ||
47 | + return self::H(); | ||
48 | + | ||
49 | + case 3: | ||
50 | + return self::Q(); | ||
51 | + } | ||
52 | + | ||
53 | + throw new OutOfBoundsException('Invalid number of bits'); | ||
54 | + } | ||
55 | + | ||
56 | + /** | ||
57 | + * Returns the two bits used to encode this error correction level. | ||
58 | + */ | ||
59 | + public function getBits() : int | ||
60 | + { | ||
61 | + return $this->bits; | ||
62 | + } | ||
63 | +} |
1 | +<?php | ||
2 | +/** | ||
3 | + * BaconQrCode | ||
4 | + * | ||
5 | + * @link http://github.com/Bacon/BaconQrCode For the canonical source repository | ||
6 | + * @copyright 2013 Ben 'DASPRiD' Scholzen | ||
7 | + * @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License | ||
8 | + */ | ||
9 | + | ||
10 | +namespace BaconQrCode\Common; | ||
11 | + | ||
12 | +/** | ||
13 | + * Encapsulates a QR Code's format information, including the data mask used and error correction level. | ||
14 | + */ | ||
15 | +class FormatInformation | ||
16 | +{ | ||
17 | + /** | ||
18 | + * Mask for format information. | ||
19 | + */ | ||
20 | + private const FORMAT_INFO_MASK_QR = 0x5412; | ||
21 | + | ||
22 | + /** | ||
23 | + * Lookup table for decoding format information. | ||
24 | + * | ||
25 | + * See ISO 18004:2006, Annex C, Table C.1 | ||
26 | + */ | ||
27 | + private const FORMAT_INFO_DECODE_LOOKUP = [ | ||
28 | + [0x5412, 0x00], | ||
29 | + [0x5125, 0x01], | ||
30 | + [0x5e7c, 0x02], | ||
31 | + [0x5b4b, 0x03], | ||
32 | + [0x45f9, 0x04], | ||
33 | + [0x40ce, 0x05], | ||
34 | + [0x4f97, 0x06], | ||
35 | + [0x4aa0, 0x07], | ||
36 | + [0x77c4, 0x08], | ||
37 | + [0x72f3, 0x09], | ||
38 | + [0x7daa, 0x0a], | ||
39 | + [0x789d, 0x0b], | ||
40 | + [0x662f, 0x0c], | ||
41 | + [0x6318, 0x0d], | ||
42 | + [0x6c41, 0x0e], | ||
43 | + [0x6976, 0x0f], | ||
44 | + [0x1689, 0x10], | ||
45 | + [0x13be, 0x11], | ||
46 | + [0x1ce7, 0x12], | ||
47 | + [0x19d0, 0x13], | ||
48 | + [0x0762, 0x14], | ||
49 | + [0x0255, 0x15], | ||
50 | + [0x0d0c, 0x16], | ||
51 | + [0x083b, 0x17], | ||
52 | + [0x355f, 0x18], | ||
53 | + [0x3068, 0x19], | ||
54 | + [0x3f31, 0x1a], | ||
55 | + [0x3a06, 0x1b], | ||
56 | + [0x24b4, 0x1c], | ||
57 | + [0x2183, 0x1d], | ||
58 | + [0x2eda, 0x1e], | ||
59 | + [0x2bed, 0x1f], | ||
60 | + ]; | ||
61 | + | ||
62 | + /** | ||
63 | + * Offset i holds the number of 1 bits in the binary representation of i. | ||
64 | + * | ||
65 | + * @var array | ||
66 | + */ | ||
67 | + private const BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4]; | ||
68 | + | ||
69 | + /** | ||
70 | + * Error correction level. | ||
71 | + * | ||
72 | + * @var ErrorCorrectionLevel | ||
73 | + */ | ||
74 | + private $ecLevel; | ||
75 | + | ||
76 | + /** | ||
77 | + * Data mask. | ||
78 | + * | ||
79 | + * @var int | ||
80 | + */ | ||
81 | + private $dataMask; | ||
82 | + | ||
83 | + protected function __construct(int $formatInfo) | ||
84 | + { | ||
85 | + $this->ecLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x3); | ||
86 | + $this->dataMask = $formatInfo & 0x7; | ||
87 | + } | ||
88 | + | ||
89 | + /** | ||
90 | + * Checks how many bits are different between two integers. | ||
91 | + */ | ||
92 | + public static function numBitsDiffering(int $a, int $b) : int | ||
93 | + { | ||
94 | + $a ^= $b; | ||
95 | + | ||
96 | + return ( | ||
97 | + self::BITS_SET_IN_HALF_BYTE[$a & 0xf] | ||
98 | + + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 4) & 0xf)] | ||
99 | + + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 8) & 0xf)] | ||
100 | + + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 12) & 0xf)] | ||
101 | + + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 16) & 0xf)] | ||
102 | + + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 20) & 0xf)] | ||
103 | + + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 24) & 0xf)] | ||
104 | + + self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 28) & 0xf)] | ||
105 | + ); | ||
106 | + } | ||
107 | + | ||
108 | + /** | ||
109 | + * Decodes format information. | ||
110 | + */ | ||
111 | + public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self | ||
112 | + { | ||
113 | + $formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2); | ||
114 | + | ||
115 | + if (null !== $formatInfo) { | ||
116 | + return $formatInfo; | ||
117 | + } | ||
118 | + | ||
119 | + // Should return null, but, some QR codes apparently do not mask this info. Try again by actually masking the | ||
120 | + // pattern first. | ||
121 | + return self::doDecodeFormatInformation( | ||
122 | + $maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR, | ||
123 | + $maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR | ||
124 | + ); | ||
125 | + } | ||
126 | + | ||
127 | + /** | ||
128 | + * Internal method for decoding format information. | ||
129 | + */ | ||
130 | + private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self | ||
131 | + { | ||
132 | + $bestDifference = PHP_INT_MAX; | ||
133 | + $bestFormatInfo = 0; | ||
134 | + | ||
135 | + foreach (self::FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) { | ||
136 | + $targetInfo = $decodeInfo[0]; | ||
137 | + | ||
138 | + if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) { | ||
139 | + // Found an exact match | ||
140 | + return new self($decodeInfo[1]); | ||
141 | + } | ||
142 | + | ||
143 | + $bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo); | ||
144 | + | ||
145 | + if ($bitsDifference < $bestDifference) { | ||
146 | + $bestFormatInfo = $decodeInfo[1]; | ||
147 | + $bestDifference = $bitsDifference; | ||
148 | + } | ||
149 | + | ||
150 | + if ($maskedFormatInfo1 !== $maskedFormatInfo2) { | ||
151 | + // Also try the other option | ||
152 | + $bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo); | ||
153 | + | ||
154 | + if ($bitsDifference < $bestDifference) { | ||
155 | + $bestFormatInfo = $decodeInfo[1]; | ||
156 | + $bestDifference = $bitsDifference; | ||
157 | + } | ||
158 | + } | ||
159 | + } | ||
160 | + | ||
161 | + // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match. | ||
162 | + if ($bestDifference <= 3) { | ||
163 | + return new self($bestFormatInfo); | ||
164 | + } | ||
165 | + | ||
166 | + return null; | ||
167 | + } | ||
168 | + | ||
169 | + /** | ||
170 | + * Returns the error correction level. | ||
171 | + */ | ||
172 | + public function getErrorCorrectionLevel() : ErrorCorrectionLevel | ||
173 | + { | ||
174 | + return $this->ecLevel; | ||
175 | + } | ||
176 | + | ||
177 | + /** | ||
178 | + * Returns the data mask. | ||
179 | + */ | ||
180 | + public function getDataMask() : int | ||
181 | + { | ||
182 | + return $this->dataMask; | ||
183 | + } | ||
184 | + | ||
185 | + /** | ||
186 | + * Hashes the code of the EC level. | ||
187 | + */ | ||
188 | + public function hashCode() : int | ||
189 | + { | ||
190 | + return ($this->ecLevel->getBits() << 3) | $this->dataMask; | ||
191 | + } | ||
192 | + | ||
193 | + /** | ||
194 | + * Verifies if this instance equals another one. | ||
195 | + */ | ||
196 | + public function equals(self $other) : bool | ||
197 | + { | ||
198 | + return ( | ||
199 | + $this->ecLevel === $other->ecLevel | ||
200 | + && $this->dataMask === $other->dataMask | ||
201 | + ); | ||
202 | + } | ||
203 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +use DASPRiD\Enum\AbstractEnum; | ||
7 | + | ||
8 | +/** | ||
9 | + * Enum representing various modes in which data can be encoded to bits. | ||
10 | + * | ||
11 | + * @method static self TERMINATOR() | ||
12 | + * @method static self NUMERIC() | ||
13 | + * @method static self ALPHANUMERIC() | ||
14 | + * @method static self STRUCTURED_APPEND() | ||
15 | + * @method static self BYTE() | ||
16 | + * @method static self ECI() | ||
17 | + * @method static self KANJI() | ||
18 | + * @method static self FNC1_FIRST_POSITION() | ||
19 | + * @method static self FNC1_SECOND_POSITION() | ||
20 | + * @method static self HANZI() | ||
21 | + */ | ||
22 | +final class Mode extends AbstractEnum | ||
23 | +{ | ||
24 | + protected const TERMINATOR = [[0, 0, 0], 0x00]; | ||
25 | + protected const NUMERIC = [[10, 12, 14], 0x01]; | ||
26 | + protected const ALPHANUMERIC = [[9, 11, 13], 0x02]; | ||
27 | + protected const STRUCTURED_APPEND = [[0, 0, 0], 0x03]; | ||
28 | + protected const BYTE = [[8, 16, 16], 0x04]; | ||
29 | + protected const ECI = [[0, 0, 0], 0x07]; | ||
30 | + protected const KANJI = [[8, 10, 12], 0x08]; | ||
31 | + protected const FNC1_FIRST_POSITION = [[0, 0, 0], 0x05]; | ||
32 | + protected const FNC1_SECOND_POSITION = [[0, 0, 0], 0x09]; | ||
33 | + protected const HANZI = [[8, 10, 12], 0x0d]; | ||
34 | + | ||
35 | + /** | ||
36 | + * @var int[] | ||
37 | + */ | ||
38 | + private $characterCountBitsForVersions; | ||
39 | + | ||
40 | + /** | ||
41 | + * @var int | ||
42 | + */ | ||
43 | + private $bits; | ||
44 | + | ||
45 | + protected function __construct(array $characterCountBitsForVersions, int $bits) | ||
46 | + { | ||
47 | + $this->characterCountBitsForVersions = $characterCountBitsForVersions; | ||
48 | + $this->bits = $bits; | ||
49 | + } | ||
50 | + | ||
51 | + /** | ||
52 | + * Returns the number of bits used in a specific QR code version. | ||
53 | + */ | ||
54 | + public function getCharacterCountBits(Version $version) : int | ||
55 | + { | ||
56 | + $number = $version->getVersionNumber(); | ||
57 | + | ||
58 | + if ($number <= 9) { | ||
59 | + $offset = 0; | ||
60 | + } elseif ($number <= 26) { | ||
61 | + $offset = 1; | ||
62 | + } else { | ||
63 | + $offset = 2; | ||
64 | + } | ||
65 | + | ||
66 | + return $this->characterCountBitsForVersions[$offset]; | ||
67 | + } | ||
68 | + | ||
69 | + /** | ||
70 | + * Returns the four bits used to encode this mode. | ||
71 | + */ | ||
72 | + public function getBits() : int | ||
73 | + { | ||
74 | + return $this->bits; | ||
75 | + } | ||
76 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +use BaconQrCode\Exception\InvalidArgumentException; | ||
7 | +use BaconQrCode\Exception\RuntimeException; | ||
8 | +use SplFixedArray; | ||
9 | + | ||
10 | +/** | ||
11 | + * Reed-Solomon codec for 8-bit characters. | ||
12 | + * | ||
13 | + * Based on libfec by Phil Karn, KA9Q. | ||
14 | + */ | ||
15 | +final class ReedSolomonCodec | ||
16 | +{ | ||
17 | + /** | ||
18 | + * Symbol size in bits. | ||
19 | + * | ||
20 | + * @var int | ||
21 | + */ | ||
22 | + private $symbolSize; | ||
23 | + | ||
24 | + /** | ||
25 | + * Block size in symbols. | ||
26 | + * | ||
27 | + * @var int | ||
28 | + */ | ||
29 | + private $blockSize; | ||
30 | + | ||
31 | + /** | ||
32 | + * First root of RS code generator polynomial, index form. | ||
33 | + * | ||
34 | + * @var int | ||
35 | + */ | ||
36 | + private $firstRoot; | ||
37 | + | ||
38 | + /** | ||
39 | + * Primitive element to generate polynomial roots, index form. | ||
40 | + * | ||
41 | + * @var int | ||
42 | + */ | ||
43 | + private $primitive; | ||
44 | + | ||
45 | + /** | ||
46 | + * Prim-th root of 1, index form. | ||
47 | + * | ||
48 | + * @var int | ||
49 | + */ | ||
50 | + private $iPrimitive; | ||
51 | + | ||
52 | + /** | ||
53 | + * RS code generator polynomial degree (number of roots). | ||
54 | + * | ||
55 | + * @var int | ||
56 | + */ | ||
57 | + private $numRoots; | ||
58 | + | ||
59 | + /** | ||
60 | + * Padding bytes at front of shortened block. | ||
61 | + * | ||
62 | + * @var int | ||
63 | + */ | ||
64 | + private $padding; | ||
65 | + | ||
66 | + /** | ||
67 | + * Log lookup table. | ||
68 | + * | ||
69 | + * @var SplFixedArray | ||
70 | + */ | ||
71 | + private $alphaTo; | ||
72 | + | ||
73 | + /** | ||
74 | + * Anti-Log lookup table. | ||
75 | + * | ||
76 | + * @var SplFixedArray | ||
77 | + */ | ||
78 | + private $indexOf; | ||
79 | + | ||
80 | + /** | ||
81 | + * Generator polynomial. | ||
82 | + * | ||
83 | + * @var SplFixedArray | ||
84 | + */ | ||
85 | + private $generatorPoly; | ||
86 | + | ||
87 | + /** | ||
88 | + * @throws InvalidArgumentException if symbol size ist not between 0 and 8 | ||
89 | + * @throws InvalidArgumentException if first root is invalid | ||
90 | + * @throws InvalidArgumentException if num roots is invalid | ||
91 | + * @throws InvalidArgumentException if padding is invalid | ||
92 | + * @throws RuntimeException if field generator polynomial is not primitive | ||
93 | + */ | ||
94 | + public function __construct( | ||
95 | + int $symbolSize, | ||
96 | + int $gfPoly, | ||
97 | + int $firstRoot, | ||
98 | + int $primitive, | ||
99 | + int $numRoots, | ||
100 | + int $padding | ||
101 | + ) { | ||
102 | + if ($symbolSize < 0 || $symbolSize > 8) { | ||
103 | + throw new InvalidArgumentException('Symbol size must be between 0 and 8'); | ||
104 | + } | ||
105 | + | ||
106 | + if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) { | ||
107 | + throw new InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize)); | ||
108 | + } | ||
109 | + | ||
110 | + if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) { | ||
111 | + throw new InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize)); | ||
112 | + } | ||
113 | + | ||
114 | + if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) { | ||
115 | + throw new InvalidArgumentException( | ||
116 | + 'Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots) | ||
117 | + ); | ||
118 | + } | ||
119 | + | ||
120 | + $this->symbolSize = $symbolSize; | ||
121 | + $this->blockSize = (1 << $symbolSize) - 1; | ||
122 | + $this->padding = $padding; | ||
123 | + $this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false); | ||
124 | + $this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false); | ||
125 | + | ||
126 | + // Generate galous field lookup table | ||
127 | + $this->indexOf[0] = $this->blockSize; | ||
128 | + $this->alphaTo[$this->blockSize] = 0; | ||
129 | + | ||
130 | + $sr = 1; | ||
131 | + | ||
132 | + for ($i = 0; $i < $this->blockSize; ++$i) { | ||
133 | + $this->indexOf[$sr] = $i; | ||
134 | + $this->alphaTo[$i] = $sr; | ||
135 | + | ||
136 | + $sr <<= 1; | ||
137 | + | ||
138 | + if ($sr & (1 << $symbolSize)) { | ||
139 | + $sr ^= $gfPoly; | ||
140 | + } | ||
141 | + | ||
142 | + $sr &= $this->blockSize; | ||
143 | + } | ||
144 | + | ||
145 | + if (1 !== $sr) { | ||
146 | + throw new RuntimeException('Field generator polynomial is not primitive'); | ||
147 | + } | ||
148 | + | ||
149 | + // Form RS code generator polynomial from its roots | ||
150 | + $this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false); | ||
151 | + $this->firstRoot = $firstRoot; | ||
152 | + $this->primitive = $primitive; | ||
153 | + $this->numRoots = $numRoots; | ||
154 | + | ||
155 | + // Find prim-th root of 1, used in decoding | ||
156 | + for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize) { | ||
157 | + } | ||
158 | + | ||
159 | + $this->iPrimitive = intdiv($iPrimitive, $primitive); | ||
160 | + | ||
161 | + $this->generatorPoly[0] = 1; | ||
162 | + | ||
163 | + for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; ++$i, $root += $primitive) { | ||
164 | + $this->generatorPoly[$i + 1] = 1; | ||
165 | + | ||
166 | + for ($j = $i; $j > 0; $j--) { | ||
167 | + if ($this->generatorPoly[$j] !== 0) { | ||
168 | + $this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[ | ||
169 | + $this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root) | ||
170 | + ]; | ||
171 | + } else { | ||
172 | + $this->generatorPoly[$j] = $this->generatorPoly[$j - 1]; | ||
173 | + } | ||
174 | + } | ||
175 | + | ||
176 | + $this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)]; | ||
177 | + } | ||
178 | + | ||
179 | + // Convert generator poly to index form for quicker encoding | ||
180 | + for ($i = 0; $i <= $numRoots; ++$i) { | ||
181 | + $this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]]; | ||
182 | + } | ||
183 | + } | ||
184 | + | ||
185 | + /** | ||
186 | + * Encodes data and writes result back into parity array. | ||
187 | + */ | ||
188 | + public function encode(SplFixedArray $data, SplFixedArray $parity) : void | ||
189 | + { | ||
190 | + for ($i = 0; $i < $this->numRoots; ++$i) { | ||
191 | + $parity[$i] = 0; | ||
192 | + } | ||
193 | + | ||
194 | + $iterations = $this->blockSize - $this->numRoots - $this->padding; | ||
195 | + | ||
196 | + for ($i = 0; $i < $iterations; ++$i) { | ||
197 | + $feedback = $this->indexOf[$data[$i] ^ $parity[0]]; | ||
198 | + | ||
199 | + if ($feedback !== $this->blockSize) { | ||
200 | + // Feedback term is non-zero | ||
201 | + $feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback); | ||
202 | + | ||
203 | + for ($j = 1; $j < $this->numRoots; ++$j) { | ||
204 | + $parity[$j] = $parity[$j] ^ $this->alphaTo[ | ||
205 | + $this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j]) | ||
206 | + ]; | ||
207 | + } | ||
208 | + } | ||
209 | + | ||
210 | + for ($j = 0; $j < $this->numRoots - 1; ++$j) { | ||
211 | + $parity[$j] = $parity[$j + 1]; | ||
212 | + } | ||
213 | + | ||
214 | + if ($feedback !== $this->blockSize) { | ||
215 | + $parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])]; | ||
216 | + } else { | ||
217 | + $parity[$this->numRoots - 1] = 0; | ||
218 | + } | ||
219 | + } | ||
220 | + } | ||
221 | + | ||
222 | + /** | ||
223 | + * Decodes received data. | ||
224 | + */ | ||
225 | + public function decode(SplFixedArray $data, SplFixedArray $erasures = null) : ?int | ||
226 | + { | ||
227 | + // This speeds up the initialization a bit. | ||
228 | + $numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false); | ||
229 | + $numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false); | ||
230 | + | ||
231 | + $lambda = clone $numRootsPlusOne; | ||
232 | + $b = clone $numRootsPlusOne; | ||
233 | + $t = clone $numRootsPlusOne; | ||
234 | + $omega = clone $numRootsPlusOne; | ||
235 | + $root = clone $numRoots; | ||
236 | + $loc = clone $numRoots; | ||
237 | + | ||
238 | + $numErasures = (null !== $erasures ? count($erasures) : 0); | ||
239 | + | ||
240 | + // Form the Syndromes; i.e., evaluate data(x) at roots of g(x) | ||
241 | + $syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false); | ||
242 | + | ||
243 | + for ($i = 1; $i < $this->blockSize - $this->padding; ++$i) { | ||
244 | + for ($j = 0; $j < $this->numRoots; ++$j) { | ||
245 | + if ($syndromes[$j] === 0) { | ||
246 | + $syndromes[$j] = $data[$i]; | ||
247 | + } else { | ||
248 | + $syndromes[$j] = $data[$i] ^ $this->alphaTo[ | ||
249 | + $this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive) | ||
250 | + ]; | ||
251 | + } | ||
252 | + } | ||
253 | + } | ||
254 | + | ||
255 | + // Convert syndromes to index form, checking for nonzero conditions | ||
256 | + $syndromeError = 0; | ||
257 | + | ||
258 | + for ($i = 0; $i < $this->numRoots; ++$i) { | ||
259 | + $syndromeError |= $syndromes[$i]; | ||
260 | + $syndromes[$i] = $this->indexOf[$syndromes[$i]]; | ||
261 | + } | ||
262 | + | ||
263 | + if (! $syndromeError) { | ||
264 | + // If syndrome is zero, data[] is a codeword and there are no errors to correct, so return data[] | ||
265 | + // unmodified. | ||
266 | + return 0; | ||
267 | + } | ||
268 | + | ||
269 | + $lambda[0] = 1; | ||
270 | + | ||
271 | + if ($numErasures > 0) { | ||
272 | + // Init lambda to be the erasure locator polynomial | ||
273 | + $lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))]; | ||
274 | + | ||
275 | + for ($i = 1; $i < $numErasures; ++$i) { | ||
276 | + $u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i])); | ||
277 | + | ||
278 | + for ($j = $i + 1; $j > 0; --$j) { | ||
279 | + $tmp = $this->indexOf[$lambda[$j - 1]]; | ||
280 | + | ||
281 | + if ($tmp !== $this->blockSize) { | ||
282 | + $lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)]; | ||
283 | + } | ||
284 | + } | ||
285 | + } | ||
286 | + } | ||
287 | + | ||
288 | + for ($i = 0; $i <= $this->numRoots; ++$i) { | ||
289 | + $b[$i] = $this->indexOf[$lambda[$i]]; | ||
290 | + } | ||
291 | + | ||
292 | + // Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial | ||
293 | + $r = $numErasures; | ||
294 | + $el = $numErasures; | ||
295 | + | ||
296 | + while (++$r <= $this->numRoots) { | ||
297 | + // Compute discrepancy at the r-th step in poly form | ||
298 | + $discrepancyR = 0; | ||
299 | + | ||
300 | + for ($i = 0; $i < $r; ++$i) { | ||
301 | + if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) { | ||
302 | + $discrepancyR ^= $this->alphaTo[ | ||
303 | + $this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1]) | ||
304 | + ]; | ||
305 | + } | ||
306 | + } | ||
307 | + | ||
308 | + $discrepancyR = $this->indexOf[$discrepancyR]; | ||
309 | + | ||
310 | + if ($discrepancyR === $this->blockSize) { | ||
311 | + $tmp = $b->toArray(); | ||
312 | + array_unshift($tmp, $this->blockSize); | ||
313 | + array_pop($tmp); | ||
314 | + $b = SplFixedArray::fromArray($tmp, false); | ||
315 | + continue; | ||
316 | + } | ||
317 | + | ||
318 | + $t[0] = $lambda[0]; | ||
319 | + | ||
320 | + for ($i = 0; $i < $this->numRoots; ++$i) { | ||
321 | + if ($b[$i] !== $this->blockSize) { | ||
322 | + $t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])]; | ||
323 | + } else { | ||
324 | + $t[$i + 1] = $lambda[$i + 1]; | ||
325 | + } | ||
326 | + } | ||
327 | + | ||
328 | + if (2 * $el <= $r + $numErasures - 1) { | ||
329 | + $el = $r + $numErasures - $el; | ||
330 | + | ||
331 | + for ($i = 0; $i <= $this->numRoots; ++$i) { | ||
332 | + $b[$i] = ( | ||
333 | + $lambda[$i] === 0 | ||
334 | + ? $this->blockSize | ||
335 | + : $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize) | ||
336 | + ); | ||
337 | + } | ||
338 | + } else { | ||
339 | + $tmp = $b->toArray(); | ||
340 | + array_unshift($tmp, $this->blockSize); | ||
341 | + array_pop($tmp); | ||
342 | + $b = SplFixedArray::fromArray($tmp, false); | ||
343 | + } | ||
344 | + | ||
345 | + $lambda = clone $t; | ||
346 | + } | ||
347 | + | ||
348 | + // Convert lambda to index form and compute deg(lambda(x)) | ||
349 | + $degLambda = 0; | ||
350 | + | ||
351 | + for ($i = 0; $i <= $this->numRoots; ++$i) { | ||
352 | + $lambda[$i] = $this->indexOf[$lambda[$i]]; | ||
353 | + | ||
354 | + if ($lambda[$i] !== $this->blockSize) { | ||
355 | + $degLambda = $i; | ||
356 | + } | ||
357 | + } | ||
358 | + | ||
359 | + // Find roots of the error+erasure locator polynomial by Chien search. | ||
360 | + $reg = clone $lambda; | ||
361 | + $reg[0] = 0; | ||
362 | + $count = 0; | ||
363 | + $i = 1; | ||
364 | + | ||
365 | + for ($k = $this->iPrimitive - 1; $i <= $this->blockSize; ++$i, $k = $this->modNn($k + $this->iPrimitive)) { | ||
366 | + $q = 1; | ||
367 | + | ||
368 | + for ($j = $degLambda; $j > 0; $j--) { | ||
369 | + if ($reg[$j] !== $this->blockSize) { | ||
370 | + $reg[$j] = $this->modNn($reg[$j] + $j); | ||
371 | + $q ^= $this->alphaTo[$reg[$j]]; | ||
372 | + } | ||
373 | + } | ||
374 | + | ||
375 | + if ($q !== 0) { | ||
376 | + // Not a root | ||
377 | + continue; | ||
378 | + } | ||
379 | + | ||
380 | + // Store root (index-form) and error location number | ||
381 | + $root[$count] = $i; | ||
382 | + $loc[$count] = $k; | ||
383 | + | ||
384 | + if (++$count === $degLambda) { | ||
385 | + break; | ||
386 | + } | ||
387 | + } | ||
388 | + | ||
389 | + if ($degLambda !== $count) { | ||
390 | + // deg(lambda) unequal to number of roots: uncorrectable error detected | ||
391 | + return null; | ||
392 | + } | ||
393 | + | ||
394 | + // Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo x**numRoots). In index form. Also find | ||
395 | + // deg(omega). | ||
396 | + $degOmega = $degLambda - 1; | ||
397 | + | ||
398 | + for ($i = 0; $i <= $degOmega; ++$i) { | ||
399 | + $tmp = 0; | ||
400 | + | ||
401 | + for ($j = $i; $j >= 0; --$j) { | ||
402 | + if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) { | ||
403 | + $tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])]; | ||
404 | + } | ||
405 | + } | ||
406 | + | ||
407 | + $omega[$i] = $this->indexOf[$tmp]; | ||
408 | + } | ||
409 | + | ||
410 | + // Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(firstRoot-1) and | ||
411 | + // den = lambda_pr(inv(X(l))) all in poly form. | ||
412 | + for ($j = $count - 1; $j >= 0; --$j) { | ||
413 | + $num1 = 0; | ||
414 | + | ||
415 | + for ($i = $degOmega; $i >= 0; $i--) { | ||
416 | + if ($omega[$i] !== $this->blockSize) { | ||
417 | + $num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])]; | ||
418 | + } | ||
419 | + } | ||
420 | + | ||
421 | + $num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)]; | ||
422 | + $den = 0; | ||
423 | + | ||
424 | + // lambda[i+1] for i even is the formal derivativelambda_pr of lambda[i] | ||
425 | + for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) { | ||
426 | + if ($lambda[$i + 1] !== $this->blockSize) { | ||
427 | + $den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])]; | ||
428 | + } | ||
429 | + } | ||
430 | + | ||
431 | + // Apply error to data | ||
432 | + if ($num1 !== 0 && $loc[$j] >= $this->padding) { | ||
433 | + $data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ ( | ||
434 | + $this->alphaTo[ | ||
435 | + $this->modNn( | ||
436 | + $this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den] | ||
437 | + ) | ||
438 | + ] | ||
439 | + ); | ||
440 | + } | ||
441 | + } | ||
442 | + | ||
443 | + if (null !== $erasures) { | ||
444 | + if (count($erasures) < $count) { | ||
445 | + $erasures->setSize($count); | ||
446 | + } | ||
447 | + | ||
448 | + for ($i = 0; $i < $count; $i++) { | ||
449 | + $erasures[$i] = $loc[$i]; | ||
450 | + } | ||
451 | + } | ||
452 | + | ||
453 | + return $count; | ||
454 | + } | ||
455 | + | ||
456 | + /** | ||
457 | + * Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow divide. | ||
458 | + */ | ||
459 | + private function modNn(int $x) : int | ||
460 | + { | ||
461 | + while ($x >= $this->blockSize) { | ||
462 | + $x -= $this->blockSize; | ||
463 | + $x = ($x >> $this->symbolSize) + ($x & $this->blockSize); | ||
464 | + } | ||
465 | + | ||
466 | + return $x; | ||
467 | + } | ||
468 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Common; | ||
5 | + | ||
6 | +use BaconQrCode\Exception\InvalidArgumentException; | ||
7 | +use SplFixedArray; | ||
8 | + | ||
9 | +/** | ||
10 | + * Version representation. | ||
11 | + */ | ||
12 | +final class Version | ||
13 | +{ | ||
14 | + private const VERSION_DECODE_INFO = [ | ||
15 | + 0x07c94, | ||
16 | + 0x085bc, | ||
17 | + 0x09a99, | ||
18 | + 0x0a4d3, | ||
19 | + 0x0bbf6, | ||
20 | + 0x0c762, | ||
21 | + 0x0d847, | ||
22 | + 0x0e60d, | ||
23 | + 0x0f928, | ||
24 | + 0x10b78, | ||
25 | + 0x1145d, | ||
26 | + 0x12a17, | ||
27 | + 0x13532, | ||
28 | + 0x149a6, | ||
29 | + 0x15683, | ||
30 | + 0x168c9, | ||
31 | + 0x177ec, | ||
32 | + 0x18ec4, | ||
33 | + 0x191e1, | ||
34 | + 0x1afab, | ||
35 | + 0x1b08e, | ||
36 | + 0x1cc1a, | ||
37 | + 0x1d33f, | ||
38 | + 0x1ed75, | ||
39 | + 0x1f250, | ||
40 | + 0x209d5, | ||
41 | + 0x216f0, | ||
42 | + 0x228ba, | ||
43 | + 0x2379f, | ||
44 | + 0x24b0b, | ||
45 | + 0x2542e, | ||
46 | + 0x26a64, | ||
47 | + 0x27541, | ||
48 | + 0x28c69, | ||
49 | + ]; | ||
50 | + | ||
51 | + /** | ||
52 | + * Version number of this version. | ||
53 | + * | ||
54 | + * @var int | ||
55 | + */ | ||
56 | + private $versionNumber; | ||
57 | + | ||
58 | + /** | ||
59 | + * Alignment pattern centers. | ||
60 | + * | ||
61 | + * @var SplFixedArray | ||
62 | + */ | ||
63 | + private $alignmentPatternCenters; | ||
64 | + | ||
65 | + /** | ||
66 | + * Error correction blocks. | ||
67 | + * | ||
68 | + * @var EcBlocks[] | ||
69 | + */ | ||
70 | + private $ecBlocks; | ||
71 | + | ||
72 | + /** | ||
73 | + * Total number of codewords. | ||
74 | + * | ||
75 | + * @var int | ||
76 | + */ | ||
77 | + private $totalCodewords; | ||
78 | + | ||
79 | + /** | ||
80 | + * Cached version instances. | ||
81 | + * | ||
82 | + * @var array<int, self>|null | ||
83 | + */ | ||
84 | + private static $versions; | ||
85 | + | ||
86 | + /** | ||
87 | + * @param int[] $alignmentPatternCenters | ||
88 | + */ | ||
89 | + private function __construct( | ||
90 | + int $versionNumber, | ||
91 | + array $alignmentPatternCenters, | ||
92 | + EcBlocks ...$ecBlocks | ||
93 | + ) { | ||
94 | + $this->versionNumber = $versionNumber; | ||
95 | + $this->alignmentPatternCenters = $alignmentPatternCenters; | ||
96 | + $this->ecBlocks = $ecBlocks; | ||
97 | + | ||
98 | + $totalCodewords = 0; | ||
99 | + $ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock(); | ||
100 | + | ||
101 | + foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) { | ||
102 | + $totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords); | ||
103 | + } | ||
104 | + | ||
105 | + $this->totalCodewords = $totalCodewords; | ||
106 | + } | ||
107 | + | ||
108 | + /** | ||
109 | + * Returns the version number. | ||
110 | + */ | ||
111 | + public function getVersionNumber() : int | ||
112 | + { | ||
113 | + return $this->versionNumber; | ||
114 | + } | ||
115 | + | ||
116 | + /** | ||
117 | + * Returns the alignment pattern centers. | ||
118 | + * | ||
119 | + * @return int[] | ||
120 | + */ | ||
121 | + public function getAlignmentPatternCenters() : array | ||
122 | + { | ||
123 | + return $this->alignmentPatternCenters; | ||
124 | + } | ||
125 | + | ||
126 | + /** | ||
127 | + * Returns the total number of codewords. | ||
128 | + */ | ||
129 | + public function getTotalCodewords() : int | ||
130 | + { | ||
131 | + return $this->totalCodewords; | ||
132 | + } | ||
133 | + | ||
134 | + /** | ||
135 | + * Calculates the dimension for the current version. | ||
136 | + */ | ||
137 | + public function getDimensionForVersion() : int | ||
138 | + { | ||
139 | + return 17 + 4 * $this->versionNumber; | ||
140 | + } | ||
141 | + | ||
142 | + /** | ||
143 | + * Returns the number of EC blocks for a specific EC level. | ||
144 | + */ | ||
145 | + public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel) : EcBlocks | ||
146 | + { | ||
147 | + return $this->ecBlocks[$ecLevel->ordinal()]; | ||
148 | + } | ||
149 | + | ||
150 | + /** | ||
151 | + * Gets a provisional version number for a specific dimension. | ||
152 | + * | ||
153 | + * @throws InvalidArgumentException if dimension is not 1 mod 4 | ||
154 | + */ | ||
155 | + public static function getProvisionalVersionForDimension(int $dimension) : self | ||
156 | + { | ||
157 | + if (1 !== $dimension % 4) { | ||
158 | + throw new InvalidArgumentException('Dimension is not 1 mod 4'); | ||
159 | + } | ||
160 | + | ||
161 | + return self::getVersionForNumber(intdiv($dimension - 17, 4)); | ||
162 | + } | ||
163 | + | ||
164 | + /** | ||
165 | + * Gets a version instance for a specific version number. | ||
166 | + * | ||
167 | + * @throws InvalidArgumentException if version number is out of range | ||
168 | + */ | ||
169 | + public static function getVersionForNumber(int $versionNumber) : self | ||
170 | + { | ||
171 | + if ($versionNumber < 1 || $versionNumber > 40) { | ||
172 | + throw new InvalidArgumentException('Version number must be between 1 and 40'); | ||
173 | + } | ||
174 | + | ||
175 | + return self::versions()[$versionNumber - 1]; | ||
176 | + } | ||
177 | + | ||
178 | + /** | ||
179 | + * Decodes version information from an integer and returns the version. | ||
180 | + */ | ||
181 | + public static function decodeVersionInformation(int $versionBits) : ?self | ||
182 | + { | ||
183 | + $bestDifference = PHP_INT_MAX; | ||
184 | + $bestVersion = 0; | ||
185 | + | ||
186 | + foreach (self::VERSION_DECODE_INFO as $i => $targetVersion) { | ||
187 | + if ($targetVersion === $versionBits) { | ||
188 | + return self::getVersionForNumber($i + 7); | ||
189 | + } | ||
190 | + | ||
191 | + $bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion); | ||
192 | + | ||
193 | + if ($bitsDifference < $bestDifference) { | ||
194 | + $bestVersion = $i + 7; | ||
195 | + $bestDifference = $bitsDifference; | ||
196 | + } | ||
197 | + } | ||
198 | + | ||
199 | + if ($bestDifference <= 3) { | ||
200 | + return self::getVersionForNumber($bestVersion); | ||
201 | + } | ||
202 | + | ||
203 | + return null; | ||
204 | + } | ||
205 | + | ||
206 | + /** | ||
207 | + * Builds the function pattern for the current version. | ||
208 | + */ | ||
209 | + public function buildFunctionPattern() : BitMatrix | ||
210 | + { | ||
211 | + $dimension = $this->getDimensionForVersion(); | ||
212 | + $bitMatrix = new BitMatrix($dimension); | ||
213 | + | ||
214 | + // Top left finder pattern + separator + format | ||
215 | + $bitMatrix->setRegion(0, 0, 9, 9); | ||
216 | + // Top right finder pattern + separator + format | ||
217 | + $bitMatrix->setRegion($dimension - 8, 0, 8, 9); | ||
218 | + // Bottom left finder pattern + separator + format | ||
219 | + $bitMatrix->setRegion(0, $dimension - 8, 9, 8); | ||
220 | + | ||
221 | + // Alignment patterns | ||
222 | + $max = count($this->alignmentPatternCenters); | ||
223 | + | ||
224 | + for ($x = 0; $x < $max; ++$x) { | ||
225 | + $i = $this->alignmentPatternCenters[$x] - 2; | ||
226 | + | ||
227 | + for ($y = 0; $y < $max; ++$y) { | ||
228 | + if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) { | ||
229 | + // No alignment patterns near the three finder paterns | ||
230 | + continue; | ||
231 | + } | ||
232 | + | ||
233 | + $bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5); | ||
234 | + } | ||
235 | + } | ||
236 | + | ||
237 | + // Vertical timing pattern | ||
238 | + $bitMatrix->setRegion(6, 9, 1, $dimension - 17); | ||
239 | + // Horizontal timing pattern | ||
240 | + $bitMatrix->setRegion(9, 6, $dimension - 17, 1); | ||
241 | + | ||
242 | + if ($this->versionNumber > 6) { | ||
243 | + // Version info, top right | ||
244 | + $bitMatrix->setRegion($dimension - 11, 0, 3, 6); | ||
245 | + // Version info, bottom left | ||
246 | + $bitMatrix->setRegion(0, $dimension - 11, 6, 3); | ||
247 | + } | ||
248 | + | ||
249 | + return $bitMatrix; | ||
250 | + } | ||
251 | + | ||
252 | + /** | ||
253 | + * Returns a string representation for the version. | ||
254 | + */ | ||
255 | + public function __toString() : string | ||
256 | + { | ||
257 | + return (string) $this->versionNumber; | ||
258 | + } | ||
259 | + | ||
260 | + /** | ||
261 | + * Build and cache a specific version. | ||
262 | + * | ||
263 | + * See ISO 18004:2006 6.5.1 Table 9. | ||
264 | + * | ||
265 | + * @return array<int, self> | ||
266 | + */ | ||
267 | + private static function versions() : array | ||
268 | + { | ||
269 | + if (null !== self::$versions) { | ||
270 | + return self::$versions; | ||
271 | + } | ||
272 | + | ||
273 | + return self::$versions = [ | ||
274 | + new self( | ||
275 | + 1, | ||
276 | + [], | ||
277 | + new EcBlocks(7, new EcBlock(1, 19)), | ||
278 | + new EcBlocks(10, new EcBlock(1, 16)), | ||
279 | + new EcBlocks(13, new EcBlock(1, 13)), | ||
280 | + new EcBlocks(17, new EcBlock(1, 9)) | ||
281 | + ), | ||
282 | + new self( | ||
283 | + 2, | ||
284 | + [6, 18], | ||
285 | + new EcBlocks(10, new EcBlock(1, 34)), | ||
286 | + new EcBlocks(16, new EcBlock(1, 28)), | ||
287 | + new EcBlocks(22, new EcBlock(1, 22)), | ||
288 | + new EcBlocks(28, new EcBlock(1, 16)) | ||
289 | + ), | ||
290 | + new self( | ||
291 | + 3, | ||
292 | + [6, 22], | ||
293 | + new EcBlocks(15, new EcBlock(1, 55)), | ||
294 | + new EcBlocks(26, new EcBlock(1, 44)), | ||
295 | + new EcBlocks(18, new EcBlock(2, 17)), | ||
296 | + new EcBlocks(22, new EcBlock(2, 13)) | ||
297 | + ), | ||
298 | + new self( | ||
299 | + 4, | ||
300 | + [6, 26], | ||
301 | + new EcBlocks(20, new EcBlock(1, 80)), | ||
302 | + new EcBlocks(18, new EcBlock(2, 32)), | ||
303 | + new EcBlocks(26, new EcBlock(3, 24)), | ||
304 | + new EcBlocks(16, new EcBlock(4, 9)) | ||
305 | + ), | ||
306 | + new self( | ||
307 | + 5, | ||
308 | + [6, 30], | ||
309 | + new EcBlocks(26, new EcBlock(1, 108)), | ||
310 | + new EcBlocks(24, new EcBlock(2, 43)), | ||
311 | + new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)), | ||
312 | + new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12)) | ||
313 | + ), | ||
314 | + new self( | ||
315 | + 6, | ||
316 | + [6, 34], | ||
317 | + new EcBlocks(18, new EcBlock(2, 68)), | ||
318 | + new EcBlocks(16, new EcBlock(4, 27)), | ||
319 | + new EcBlocks(24, new EcBlock(4, 19)), | ||
320 | + new EcBlocks(28, new EcBlock(4, 15)) | ||
321 | + ), | ||
322 | + new self( | ||
323 | + 7, | ||
324 | + [6, 22, 38], | ||
325 | + new EcBlocks(20, new EcBlock(2, 78)), | ||
326 | + new EcBlocks(18, new EcBlock(4, 31)), | ||
327 | + new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)), | ||
328 | + new EcBlocks(26, new EcBlock(4, 13), new EcBlock(1, 14)) | ||
329 | + ), | ||
330 | + new self( | ||
331 | + 8, | ||
332 | + [6, 24, 42], | ||
333 | + new EcBlocks(24, new EcBlock(2, 97)), | ||
334 | + new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)), | ||
335 | + new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)), | ||
336 | + new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15)) | ||
337 | + ), | ||
338 | + new self( | ||
339 | + 9, | ||
340 | + [6, 26, 46], | ||
341 | + new EcBlocks(30, new EcBlock(2, 116)), | ||
342 | + new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)), | ||
343 | + new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)), | ||
344 | + new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13)) | ||
345 | + ), | ||
346 | + new self( | ||
347 | + 10, | ||
348 | + [6, 28, 50], | ||
349 | + new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)), | ||
350 | + new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)), | ||
351 | + new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)), | ||
352 | + new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16)) | ||
353 | + ), | ||
354 | + new self( | ||
355 | + 11, | ||
356 | + [6, 30, 54], | ||
357 | + new EcBlocks(20, new EcBlock(4, 81)), | ||
358 | + new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)), | ||
359 | + new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)), | ||
360 | + new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13)) | ||
361 | + ), | ||
362 | + new self( | ||
363 | + 12, | ||
364 | + [6, 32, 58], | ||
365 | + new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)), | ||
366 | + new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)), | ||
367 | + new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)), | ||
368 | + new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15)) | ||
369 | + ), | ||
370 | + new self( | ||
371 | + 13, | ||
372 | + [6, 34, 62], | ||
373 | + new EcBlocks(26, new EcBlock(4, 107)), | ||
374 | + new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)), | ||
375 | + new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)), | ||
376 | + new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12)) | ||
377 | + ), | ||
378 | + new self( | ||
379 | + 14, | ||
380 | + [6, 26, 46, 66], | ||
381 | + new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)), | ||
382 | + new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)), | ||
383 | + new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)), | ||
384 | + new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13)) | ||
385 | + ), | ||
386 | + new self( | ||
387 | + 15, | ||
388 | + [6, 26, 48, 70], | ||
389 | + new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)), | ||
390 | + new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)), | ||
391 | + new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)), | ||
392 | + new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13)) | ||
393 | + ), | ||
394 | + new self( | ||
395 | + 16, | ||
396 | + [6, 26, 50, 74], | ||
397 | + new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)), | ||
398 | + new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)), | ||
399 | + new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)), | ||
400 | + new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16)) | ||
401 | + ), | ||
402 | + new self( | ||
403 | + 17, | ||
404 | + [6, 30, 54, 78], | ||
405 | + new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)), | ||
406 | + new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)), | ||
407 | + new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)), | ||
408 | + new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15)) | ||
409 | + ), | ||
410 | + new self( | ||
411 | + 18, | ||
412 | + [6, 30, 56, 82], | ||
413 | + new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)), | ||
414 | + new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)), | ||
415 | + new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)), | ||
416 | + new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15)) | ||
417 | + ), | ||
418 | + new self( | ||
419 | + 19, | ||
420 | + [6, 30, 58, 86], | ||
421 | + new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)), | ||
422 | + new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)), | ||
423 | + new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)), | ||
424 | + new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14)) | ||
425 | + ), | ||
426 | + new self( | ||
427 | + 20, | ||
428 | + [6, 34, 62, 90], | ||
429 | + new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)), | ||
430 | + new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)), | ||
431 | + new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)), | ||
432 | + new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16)) | ||
433 | + ), | ||
434 | + new self( | ||
435 | + 21, | ||
436 | + [6, 28, 50, 72, 94], | ||
437 | + new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)), | ||
438 | + new EcBlocks(26, new EcBlock(17, 42)), | ||
439 | + new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)), | ||
440 | + new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17)) | ||
441 | + ), | ||
442 | + new self( | ||
443 | + 22, | ||
444 | + [6, 26, 50, 74, 98], | ||
445 | + new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)), | ||
446 | + new EcBlocks(28, new EcBlock(17, 46)), | ||
447 | + new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)), | ||
448 | + new EcBlocks(24, new EcBlock(34, 13)) | ||
449 | + ), | ||
450 | + new self( | ||
451 | + 23, | ||
452 | + [6, 30, 54, 78, 102], | ||
453 | + new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)), | ||
454 | + new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)), | ||
455 | + new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)), | ||
456 | + new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16)) | ||
457 | + ), | ||
458 | + new self( | ||
459 | + 24, | ||
460 | + [6, 28, 54, 80, 106], | ||
461 | + new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)), | ||
462 | + new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)), | ||
463 | + new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)), | ||
464 | + new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17)) | ||
465 | + ), | ||
466 | + new self( | ||
467 | + 25, | ||
468 | + [6, 32, 58, 84, 110], | ||
469 | + new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)), | ||
470 | + new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)), | ||
471 | + new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)), | ||
472 | + new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16)) | ||
473 | + ), | ||
474 | + new self( | ||
475 | + 26, | ||
476 | + [6, 30, 58, 86, 114], | ||
477 | + new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)), | ||
478 | + new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)), | ||
479 | + new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)), | ||
480 | + new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17)) | ||
481 | + ), | ||
482 | + new self( | ||
483 | + 27, | ||
484 | + [6, 34, 62, 90, 118], | ||
485 | + new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)), | ||
486 | + new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)), | ||
487 | + new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)), | ||
488 | + new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16)) | ||
489 | + ), | ||
490 | + new self( | ||
491 | + 28, | ||
492 | + [6, 26, 50, 74, 98, 122], | ||
493 | + new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)), | ||
494 | + new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)), | ||
495 | + new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)), | ||
496 | + new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16)) | ||
497 | + ), | ||
498 | + new self( | ||
499 | + 29, | ||
500 | + [6, 30, 54, 78, 102, 126], | ||
501 | + new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)), | ||
502 | + new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)), | ||
503 | + new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)), | ||
504 | + new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16)) | ||
505 | + ), | ||
506 | + new self( | ||
507 | + 30, | ||
508 | + [6, 26, 52, 78, 104, 130], | ||
509 | + new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)), | ||
510 | + new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)), | ||
511 | + new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)), | ||
512 | + new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16)) | ||
513 | + ), | ||
514 | + new self( | ||
515 | + 31, | ||
516 | + [6, 30, 56, 82, 108, 134], | ||
517 | + new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)), | ||
518 | + new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)), | ||
519 | + new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)), | ||
520 | + new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16)) | ||
521 | + ), | ||
522 | + new self( | ||
523 | + 32, | ||
524 | + [6, 34, 60, 86, 112, 138], | ||
525 | + new EcBlocks(30, new EcBlock(17, 115)), | ||
526 | + new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)), | ||
527 | + new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)), | ||
528 | + new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16)) | ||
529 | + ), | ||
530 | + new self( | ||
531 | + 33, | ||
532 | + [6, 30, 58, 86, 114, 142], | ||
533 | + new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)), | ||
534 | + new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)), | ||
535 | + new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)), | ||
536 | + new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16)) | ||
537 | + ), | ||
538 | + new self( | ||
539 | + 34, | ||
540 | + [6, 34, 62, 90, 118, 146], | ||
541 | + new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)), | ||
542 | + new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)), | ||
543 | + new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)), | ||
544 | + new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17)) | ||
545 | + ), | ||
546 | + new self( | ||
547 | + 35, | ||
548 | + [6, 30, 54, 78, 102, 126, 150], | ||
549 | + new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)), | ||
550 | + new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)), | ||
551 | + new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)), | ||
552 | + new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16)) | ||
553 | + ), | ||
554 | + new self( | ||
555 | + 36, | ||
556 | + [6, 24, 50, 76, 102, 128, 154], | ||
557 | + new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)), | ||
558 | + new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)), | ||
559 | + new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)), | ||
560 | + new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16)) | ||
561 | + ), | ||
562 | + new self( | ||
563 | + 37, | ||
564 | + [6, 28, 54, 80, 106, 132, 158], | ||
565 | + new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)), | ||
566 | + new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)), | ||
567 | + new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)), | ||
568 | + new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16)) | ||
569 | + ), | ||
570 | + new self( | ||
571 | + 38, | ||
572 | + [6, 32, 58, 84, 110, 136, 162], | ||
573 | + new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)), | ||
574 | + new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)), | ||
575 | + new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)), | ||
576 | + new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16)) | ||
577 | + ), | ||
578 | + new self( | ||
579 | + 39, | ||
580 | + [6, 26, 54, 82, 110, 138, 166], | ||
581 | + new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)), | ||
582 | + new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)), | ||
583 | + new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)), | ||
584 | + new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16)) | ||
585 | + ), | ||
586 | + new self( | ||
587 | + 40, | ||
588 | + [6, 30, 58, 86, 114, 142, 170], | ||
589 | + new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)), | ||
590 | + new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)), | ||
591 | + new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)), | ||
592 | + new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16)) | ||
593 | + ), | ||
594 | + ]; | ||
595 | + } | ||
596 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Encoder; | ||
5 | + | ||
6 | +use SplFixedArray; | ||
7 | + | ||
8 | +/** | ||
9 | + * Block pair. | ||
10 | + */ | ||
11 | +final class BlockPair | ||
12 | +{ | ||
13 | + /** | ||
14 | + * Data bytes in the block. | ||
15 | + * | ||
16 | + * @var SplFixedArray<int> | ||
17 | + */ | ||
18 | + private $dataBytes; | ||
19 | + | ||
20 | + /** | ||
21 | + * Error correction bytes in the block. | ||
22 | + * | ||
23 | + * @var SplFixedArray<int> | ||
24 | + */ | ||
25 | + private $errorCorrectionBytes; | ||
26 | + | ||
27 | + /** | ||
28 | + * Creates a new block pair. | ||
29 | + * | ||
30 | + * @param SplFixedArray<int> $data | ||
31 | + * @param SplFixedArray<int> $errorCorrection | ||
32 | + */ | ||
33 | + public function __construct(SplFixedArray $data, SplFixedArray $errorCorrection) | ||
34 | + { | ||
35 | + $this->dataBytes = $data; | ||
36 | + $this->errorCorrectionBytes = $errorCorrection; | ||
37 | + } | ||
38 | + | ||
39 | + /** | ||
40 | + * Gets the data bytes. | ||
41 | + * | ||
42 | + * @return SplFixedArray<int> | ||
43 | + */ | ||
44 | + public function getDataBytes() : SplFixedArray | ||
45 | + { | ||
46 | + return $this->dataBytes; | ||
47 | + } | ||
48 | + | ||
49 | + /** | ||
50 | + * Gets the error correction bytes. | ||
51 | + * | ||
52 | + * @return SplFixedArray<int> | ||
53 | + */ | ||
54 | + public function getErrorCorrectionBytes() : SplFixedArray | ||
55 | + { | ||
56 | + return $this->errorCorrectionBytes; | ||
57 | + } | ||
58 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Encoder; | ||
5 | + | ||
6 | +use SplFixedArray; | ||
7 | +use Traversable; | ||
8 | + | ||
9 | +/** | ||
10 | + * Byte matrix. | ||
11 | + */ | ||
12 | +final class ByteMatrix | ||
13 | +{ | ||
14 | + /** | ||
15 | + * Bytes in the matrix, represented as array. | ||
16 | + * | ||
17 | + * @var SplFixedArray<SplFixedArray<int>> | ||
18 | + */ | ||
19 | + private $bytes; | ||
20 | + | ||
21 | + /** | ||
22 | + * Width of the matrix. | ||
23 | + * | ||
24 | + * @var int | ||
25 | + */ | ||
26 | + private $width; | ||
27 | + | ||
28 | + /** | ||
29 | + * Height of the matrix. | ||
30 | + * | ||
31 | + * @var int | ||
32 | + */ | ||
33 | + private $height; | ||
34 | + | ||
35 | + public function __construct(int $width, int $height) | ||
36 | + { | ||
37 | + $this->height = $height; | ||
38 | + $this->width = $width; | ||
39 | + $this->bytes = new SplFixedArray($height); | ||
40 | + | ||
41 | + for ($y = 0; $y < $height; ++$y) { | ||
42 | + $this->bytes[$y] = SplFixedArray::fromArray(array_fill(0, $width, 0)); | ||
43 | + } | ||
44 | + } | ||
45 | + | ||
46 | + /** | ||
47 | + * Gets the width of the matrix. | ||
48 | + */ | ||
49 | + public function getWidth() : int | ||
50 | + { | ||
51 | + return $this->width; | ||
52 | + } | ||
53 | + | ||
54 | + /** | ||
55 | + * Gets the height of the matrix. | ||
56 | + */ | ||
57 | + public function getHeight() : int | ||
58 | + { | ||
59 | + return $this->height; | ||
60 | + } | ||
61 | + | ||
62 | + /** | ||
63 | + * Gets the internal representation of the matrix. | ||
64 | + * | ||
65 | + * @return SplFixedArray<SplFixedArray<int>> | ||
66 | + */ | ||
67 | + public function getArray() : SplFixedArray | ||
68 | + { | ||
69 | + return $this->bytes; | ||
70 | + } | ||
71 | + | ||
72 | + /** | ||
73 | + * @return Traversable<int> | ||
74 | + */ | ||
75 | + public function getBytes() : Traversable | ||
76 | + { | ||
77 | + foreach ($this->bytes as $row) { | ||
78 | + foreach ($row as $byte) { | ||
79 | + yield $byte; | ||
80 | + } | ||
81 | + } | ||
82 | + } | ||
83 | + | ||
84 | + /** | ||
85 | + * Gets the byte for a specific position. | ||
86 | + */ | ||
87 | + public function get(int $x, int $y) : int | ||
88 | + { | ||
89 | + return $this->bytes[$y][$x]; | ||
90 | + } | ||
91 | + | ||
92 | + /** | ||
93 | + * Sets the byte for a specific position. | ||
94 | + */ | ||
95 | + public function set(int $x, int $y, int $value) : void | ||
96 | + { | ||
97 | + $this->bytes[$y][$x] = $value; | ||
98 | + } | ||
99 | + | ||
100 | + /** | ||
101 | + * Clears the matrix with a specific value. | ||
102 | + */ | ||
103 | + public function clear(int $value) : void | ||
104 | + { | ||
105 | + for ($y = 0; $y < $this->height; ++$y) { | ||
106 | + for ($x = 0; $x < $this->width; ++$x) { | ||
107 | + $this->bytes[$y][$x] = $value; | ||
108 | + } | ||
109 | + } | ||
110 | + } | ||
111 | + | ||
112 | + public function __clone() | ||
113 | + { | ||
114 | + $this->bytes = clone $this->bytes; | ||
115 | + | ||
116 | + foreach ($this->bytes as $index => $row) { | ||
117 | + $this->bytes[$index] = clone $row; | ||
118 | + } | ||
119 | + } | ||
120 | + | ||
121 | + /** | ||
122 | + * Returns a string representation of the matrix. | ||
123 | + */ | ||
124 | + public function __toString() : string | ||
125 | + { | ||
126 | + $result = ''; | ||
127 | + | ||
128 | + for ($y = 0; $y < $this->height; $y++) { | ||
129 | + for ($x = 0; $x < $this->width; $x++) { | ||
130 | + switch ($this->bytes[$y][$x]) { | ||
131 | + case 0: | ||
132 | + $result .= ' 0'; | ||
133 | + break; | ||
134 | + | ||
135 | + case 1: | ||
136 | + $result .= ' 1'; | ||
137 | + break; | ||
138 | + | ||
139 | + default: | ||
140 | + $result .= ' '; | ||
141 | + break; | ||
142 | + } | ||
143 | + } | ||
144 | + | ||
145 | + $result .= "\n"; | ||
146 | + } | ||
147 | + | ||
148 | + return $result; | ||
149 | + } | ||
150 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Encoder; | ||
5 | + | ||
6 | +use BaconQrCode\Common\BitArray; | ||
7 | +use BaconQrCode\Common\CharacterSetEci; | ||
8 | +use BaconQrCode\Common\ErrorCorrectionLevel; | ||
9 | +use BaconQrCode\Common\Mode; | ||
10 | +use BaconQrCode\Common\ReedSolomonCodec; | ||
11 | +use BaconQrCode\Common\Version; | ||
12 | +use BaconQrCode\Exception\WriterException; | ||
13 | +use SplFixedArray; | ||
14 | + | ||
15 | +/** | ||
16 | + * Encoder. | ||
17 | + */ | ||
18 | +final class Encoder | ||
19 | +{ | ||
20 | + /** | ||
21 | + * Default byte encoding. | ||
22 | + */ | ||
23 | + public const DEFAULT_BYTE_MODE_ECODING = 'ISO-8859-1'; | ||
24 | + | ||
25 | + /** | ||
26 | + * The original table is defined in the table 5 of JISX0510:2004 (p.19). | ||
27 | + */ | ||
28 | + private const ALPHANUMERIC_TABLE = [ | ||
29 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0x0f | ||
30 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0x1f | ||
31 | + 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0x2f | ||
32 | + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0x3f | ||
33 | + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0x4f | ||
34 | + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0x5f | ||
35 | + ]; | ||
36 | + | ||
37 | + /** | ||
38 | + * Codec cache. | ||
39 | + * | ||
40 | + * @var array | ||
41 | + */ | ||
42 | + private static $codecs = []; | ||
43 | + | ||
44 | + /** | ||
45 | + * Encodes "content" with the error correction level "ecLevel". | ||
46 | + */ | ||
47 | + public static function encode( | ||
48 | + string $content, | ||
49 | + ErrorCorrectionLevel $ecLevel, | ||
50 | + string $encoding = self::DEFAULT_BYTE_MODE_ECODING | ||
51 | + ) : QrCode { | ||
52 | + // Pick an encoding mode appropriate for the content. Note that this | ||
53 | + // will not attempt to use multiple modes / segments even if that were | ||
54 | + // more efficient. Would be nice. | ||
55 | + $mode = self::chooseMode($content, $encoding); | ||
56 | + | ||
57 | + // This will store the header information, like mode and length, as well | ||
58 | + // as "header" segments like an ECI segment. | ||
59 | + $headerBits = new BitArray(); | ||
60 | + | ||
61 | + // Append ECI segment if applicable | ||
62 | + if (Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ECODING !== $encoding) { | ||
63 | + $eci = CharacterSetEci::getCharacterSetEciByName($encoding); | ||
64 | + | ||
65 | + if (null !== $eci) { | ||
66 | + self::appendEci($eci, $headerBits); | ||
67 | + } | ||
68 | + } | ||
69 | + | ||
70 | + // (With ECI in place,) Write the mode marker | ||
71 | + self::appendModeInfo($mode, $headerBits); | ||
72 | + | ||
73 | + // Collect data within the main segment, separately, to count its size | ||
74 | + // if needed. Don't add it to main payload yet. | ||
75 | + $dataBits = new BitArray(); | ||
76 | + self::appendBytes($content, $mode, $dataBits, $encoding); | ||
77 | + | ||
78 | + // Hard part: need to know version to know how many bits length takes. | ||
79 | + // But need to know how many bits it takes to know version. First we | ||
80 | + // take a guess at version by assuming version will be the minimum, 1: | ||
81 | + $provisionalBitsNeeded = $headerBits->getSize() | ||
82 | + + $mode->getCharacterCountBits(Version::getVersionForNumber(1)) | ||
83 | + + $dataBits->getSize(); | ||
84 | + $provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel); | ||
85 | + | ||
86 | + // Use that guess to calculate the right version. I am still not sure | ||
87 | + // this works in 100% of cases. | ||
88 | + $bitsNeeded = $headerBits->getSize() | ||
89 | + + $mode->getCharacterCountBits($provisionalVersion) | ||
90 | + + $dataBits->getSize(); | ||
91 | + $version = self::chooseVersion($bitsNeeded, $ecLevel); | ||
92 | + | ||
93 | + $headerAndDataBits = new BitArray(); | ||
94 | + $headerAndDataBits->appendBitArray($headerBits); | ||
95 | + | ||
96 | + // Find "length" of main segment and write it. | ||
97 | + $numLetters = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : strlen($content)); | ||
98 | + self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits); | ||
99 | + | ||
100 | + // Put data together into the overall payload. | ||
101 | + $headerAndDataBits->appendBitArray($dataBits); | ||
102 | + $ecBlocks = $version->getEcBlocksForLevel($ecLevel); | ||
103 | + $numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords(); | ||
104 | + | ||
105 | + // Terminate the bits properly. | ||
106 | + self::terminateBits($numDataBytes, $headerAndDataBits); | ||
107 | + | ||
108 | + // Interleave data bits with error correction code. | ||
109 | + $finalBits = self::interleaveWithEcBytes( | ||
110 | + $headerAndDataBits, | ||
111 | + $version->getTotalCodewords(), | ||
112 | + $numDataBytes, | ||
113 | + $ecBlocks->getNumBlocks() | ||
114 | + ); | ||
115 | + | ||
116 | + // Choose the mask pattern. | ||
117 | + $dimension = $version->getDimensionForVersion(); | ||
118 | + $matrix = new ByteMatrix($dimension, $dimension); | ||
119 | + $maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix); | ||
120 | + | ||
121 | + // Build the matrix. | ||
122 | + MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix); | ||
123 | + | ||
124 | + return new QrCode($mode, $ecLevel, $version, $maskPattern, $matrix); | ||
125 | + } | ||
126 | + | ||
127 | + /** | ||
128 | + * Gets the alphanumeric code for a byte. | ||
129 | + */ | ||
130 | + private static function getAlphanumericCode(int $code) : int | ||
131 | + { | ||
132 | + if (isset(self::ALPHANUMERIC_TABLE[$code])) { | ||
133 | + return self::ALPHANUMERIC_TABLE[$code]; | ||
134 | + } | ||
135 | + | ||
136 | + return -1; | ||
137 | + } | ||
138 | + | ||
139 | + /** | ||
140 | + * Chooses the best mode for a given content. | ||
141 | + */ | ||
142 | + private static function chooseMode(string $content, string $encoding = null) : Mode | ||
143 | + { | ||
144 | + if (null !== $encoding && 0 === strcasecmp($encoding, 'SHIFT-JIS')) { | ||
145 | + return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE(); | ||
146 | + } | ||
147 | + | ||
148 | + $hasNumeric = false; | ||
149 | + $hasAlphanumeric = false; | ||
150 | + $contentLength = strlen($content); | ||
151 | + | ||
152 | + for ($i = 0; $i < $contentLength; ++$i) { | ||
153 | + $char = $content[$i]; | ||
154 | + | ||
155 | + if (ctype_digit($char)) { | ||
156 | + $hasNumeric = true; | ||
157 | + } elseif (-1 !== self::getAlphanumericCode(ord($char))) { | ||
158 | + $hasAlphanumeric = true; | ||
159 | + } else { | ||
160 | + return Mode::BYTE(); | ||
161 | + } | ||
162 | + } | ||
163 | + | ||
164 | + if ($hasAlphanumeric) { | ||
165 | + return Mode::ALPHANUMERIC(); | ||
166 | + } elseif ($hasNumeric) { | ||
167 | + return Mode::NUMERIC(); | ||
168 | + } | ||
169 | + | ||
170 | + return Mode::BYTE(); | ||
171 | + } | ||
172 | + | ||
173 | + /** | ||
174 | + * Calculates the mask penalty for a matrix. | ||
175 | + */ | ||
176 | + private static function calculateMaskPenalty(ByteMatrix $matrix) : int | ||
177 | + { | ||
178 | + return ( | ||
179 | + MaskUtil::applyMaskPenaltyRule1($matrix) | ||
180 | + + MaskUtil::applyMaskPenaltyRule2($matrix) | ||
181 | + + MaskUtil::applyMaskPenaltyRule3($matrix) | ||
182 | + + MaskUtil::applyMaskPenaltyRule4($matrix) | ||
183 | + ); | ||
184 | + } | ||
185 | + | ||
186 | + /** | ||
187 | + * Checks if content only consists of double-byte kanji characters. | ||
188 | + */ | ||
189 | + private static function isOnlyDoubleByteKanji(string $content) : bool | ||
190 | + { | ||
191 | + $bytes = @iconv('utf-8', 'SHIFT-JIS', $content); | ||
192 | + | ||
193 | + if (false === $bytes) { | ||
194 | + return false; | ||
195 | + } | ||
196 | + | ||
197 | + $length = strlen($bytes); | ||
198 | + | ||
199 | + if (0 !== $length % 2) { | ||
200 | + return false; | ||
201 | + } | ||
202 | + | ||
203 | + for ($i = 0; $i < $length; $i += 2) { | ||
204 | + $byte = $bytes[$i] & 0xff; | ||
205 | + | ||
206 | + if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) { | ||
207 | + return false; | ||
208 | + } | ||
209 | + } | ||
210 | + | ||
211 | + return true; | ||
212 | + } | ||
213 | + | ||
214 | + /** | ||
215 | + * Chooses the best mask pattern for a matrix. | ||
216 | + */ | ||
217 | + private static function chooseMaskPattern( | ||
218 | + BitArray $bits, | ||
219 | + ErrorCorrectionLevel $ecLevel, | ||
220 | + Version $version, | ||
221 | + ByteMatrix $matrix | ||
222 | + ) : int { | ||
223 | + $minPenalty = PHP_INT_MAX; | ||
224 | + $bestMaskPattern = -1; | ||
225 | + | ||
226 | + for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; ++$maskPattern) { | ||
227 | + MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix); | ||
228 | + $penalty = self::calculateMaskPenalty($matrix); | ||
229 | + | ||
230 | + if ($penalty < $minPenalty) { | ||
231 | + $minPenalty = $penalty; | ||
232 | + $bestMaskPattern = $maskPattern; | ||
233 | + } | ||
234 | + } | ||
235 | + | ||
236 | + return $bestMaskPattern; | ||
237 | + } | ||
238 | + | ||
239 | + /** | ||
240 | + * Chooses the best version for the input. | ||
241 | + * | ||
242 | + * @throws WriterException if data is too big | ||
243 | + */ | ||
244 | + private static function chooseVersion(int $numInputBits, ErrorCorrectionLevel $ecLevel) : Version | ||
245 | + { | ||
246 | + for ($versionNum = 1; $versionNum <= 40; ++$versionNum) { | ||
247 | + $version = Version::getVersionForNumber($versionNum); | ||
248 | + $numBytes = $version->getTotalCodewords(); | ||
249 | + | ||
250 | + $ecBlocks = $version->getEcBlocksForLevel($ecLevel); | ||
251 | + $numEcBytes = $ecBlocks->getTotalEcCodewords(); | ||
252 | + | ||
253 | + $numDataBytes = $numBytes - $numEcBytes; | ||
254 | + $totalInputBytes = intdiv($numInputBits + 8, 8); | ||
255 | + | ||
256 | + if ($numDataBytes >= $totalInputBytes) { | ||
257 | + return $version; | ||
258 | + } | ||
259 | + } | ||
260 | + | ||
261 | + throw new WriterException('Data too big'); | ||
262 | + } | ||
263 | + | ||
264 | + /** | ||
265 | + * Terminates the bits in a bit array. | ||
266 | + * | ||
267 | + * @throws WriterException if data bits cannot fit in the QR code | ||
268 | + * @throws WriterException if bits size does not equal the capacity | ||
269 | + */ | ||
270 | + private static function terminateBits(int $numDataBytes, BitArray $bits) : void | ||
271 | + { | ||
272 | + $capacity = $numDataBytes << 3; | ||
273 | + | ||
274 | + if ($bits->getSize() > $capacity) { | ||
275 | + throw new WriterException('Data bits cannot fit in the QR code'); | ||
276 | + } | ||
277 | + | ||
278 | + for ($i = 0; $i < 4 && $bits->getSize() < $capacity; ++$i) { | ||
279 | + $bits->appendBit(false); | ||
280 | + } | ||
281 | + | ||
282 | + $numBitsInLastByte = $bits->getSize() & 0x7; | ||
283 | + | ||
284 | + if ($numBitsInLastByte > 0) { | ||
285 | + for ($i = $numBitsInLastByte; $i < 8; ++$i) { | ||
286 | + $bits->appendBit(false); | ||
287 | + } | ||
288 | + } | ||
289 | + | ||
290 | + $numPaddingBytes = $numDataBytes - $bits->getSizeInBytes(); | ||
291 | + | ||
292 | + for ($i = 0; $i < $numPaddingBytes; ++$i) { | ||
293 | + $bits->appendBits(0 === ($i & 0x1) ? 0xec : 0x11, 8); | ||
294 | + } | ||
295 | + | ||
296 | + if ($bits->getSize() !== $capacity) { | ||
297 | + throw new WriterException('Bits size does not equal capacity'); | ||
298 | + } | ||
299 | + } | ||
300 | + | ||
301 | + /** | ||
302 | + * Gets number of data- and EC bytes for a block ID. | ||
303 | + * | ||
304 | + * @return int[] | ||
305 | + * @throws WriterException if block ID is too large | ||
306 | + * @throws WriterException if EC bytes mismatch | ||
307 | + * @throws WriterException if RS blocks mismatch | ||
308 | + * @throws WriterException if total bytes mismatch | ||
309 | + */ | ||
310 | + private static function getNumDataBytesAndNumEcBytesForBlockId( | ||
311 | + int $numTotalBytes, | ||
312 | + int $numDataBytes, | ||
313 | + int $numRsBlocks, | ||
314 | + int $blockId | ||
315 | + ) : array { | ||
316 | + if ($blockId >= $numRsBlocks) { | ||
317 | + throw new WriterException('Block ID too large'); | ||
318 | + } | ||
319 | + | ||
320 | + $numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks; | ||
321 | + $numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2; | ||
322 | + $numTotalBytesInGroup1 = intdiv($numTotalBytes, $numRsBlocks); | ||
323 | + $numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1; | ||
324 | + $numDataBytesInGroup1 = intdiv($numDataBytes, $numRsBlocks); | ||
325 | + $numDataBytesInGroup2 = $numDataBytesInGroup1 + 1; | ||
326 | + $numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1; | ||
327 | + $numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2; | ||
328 | + | ||
329 | + if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) { | ||
330 | + throw new WriterException('EC bytes mismatch'); | ||
331 | + } | ||
332 | + | ||
333 | + if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) { | ||
334 | + throw new WriterException('RS blocks mismatch'); | ||
335 | + } | ||
336 | + | ||
337 | + if ($numTotalBytes !== | ||
338 | + (($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1) | ||
339 | + + (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2) | ||
340 | + ) { | ||
341 | + throw new WriterException('Total bytes mismatch'); | ||
342 | + } | ||
343 | + | ||
344 | + if ($blockId < $numRsBlocksInGroup1) { | ||
345 | + return [$numDataBytesInGroup1, $numEcBytesInGroup1]; | ||
346 | + } else { | ||
347 | + return [$numDataBytesInGroup2, $numEcBytesInGroup2]; | ||
348 | + } | ||
349 | + } | ||
350 | + | ||
351 | + /** | ||
352 | + * Interleaves data with EC bytes. | ||
353 | + * | ||
354 | + * @throws WriterException if number of bits and data bytes does not match | ||
355 | + * @throws WriterException if data bytes does not match offset | ||
356 | + * @throws WriterException if an interleaving error occurs | ||
357 | + */ | ||
358 | + private static function interleaveWithEcBytes( | ||
359 | + BitArray $bits, | ||
360 | + int $numTotalBytes, | ||
361 | + int $numDataBytes, | ||
362 | + int $numRsBlocks | ||
363 | + ) : BitArray { | ||
364 | + if ($bits->getSizeInBytes() !== $numDataBytes) { | ||
365 | + throw new WriterException('Number of bits and data bytes does not match'); | ||
366 | + } | ||
367 | + | ||
368 | + $dataBytesOffset = 0; | ||
369 | + $maxNumDataBytes = 0; | ||
370 | + $maxNumEcBytes = 0; | ||
371 | + | ||
372 | + $blocks = new SplFixedArray($numRsBlocks); | ||
373 | + | ||
374 | + for ($i = 0; $i < $numRsBlocks; ++$i) { | ||
375 | + list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId( | ||
376 | + $numTotalBytes, | ||
377 | + $numDataBytes, | ||
378 | + $numRsBlocks, | ||
379 | + $i | ||
380 | + ); | ||
381 | + | ||
382 | + $size = $numDataBytesInBlock; | ||
383 | + $dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size); | ||
384 | + $ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock); | ||
385 | + $blocks[$i] = new BlockPair($dataBytes, $ecBytes); | ||
386 | + | ||
387 | + $maxNumDataBytes = max($maxNumDataBytes, $size); | ||
388 | + $maxNumEcBytes = max($maxNumEcBytes, count($ecBytes)); | ||
389 | + $dataBytesOffset += $numDataBytesInBlock; | ||
390 | + } | ||
391 | + | ||
392 | + if ($numDataBytes !== $dataBytesOffset) { | ||
393 | + throw new WriterException('Data bytes does not match offset'); | ||
394 | + } | ||
395 | + | ||
396 | + $result = new BitArray(); | ||
397 | + | ||
398 | + for ($i = 0; $i < $maxNumDataBytes; ++$i) { | ||
399 | + foreach ($blocks as $block) { | ||
400 | + $dataBytes = $block->getDataBytes(); | ||
401 | + | ||
402 | + if ($i < count($dataBytes)) { | ||
403 | + $result->appendBits($dataBytes[$i], 8); | ||
404 | + } | ||
405 | + } | ||
406 | + } | ||
407 | + | ||
408 | + for ($i = 0; $i < $maxNumEcBytes; ++$i) { | ||
409 | + foreach ($blocks as $block) { | ||
410 | + $ecBytes = $block->getErrorCorrectionBytes(); | ||
411 | + | ||
412 | + if ($i < count($ecBytes)) { | ||
413 | + $result->appendBits($ecBytes[$i], 8); | ||
414 | + } | ||
415 | + } | ||
416 | + } | ||
417 | + | ||
418 | + if ($numTotalBytes !== $result->getSizeInBytes()) { | ||
419 | + throw new WriterException( | ||
420 | + 'Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ' | ||
421 | + ); | ||
422 | + } | ||
423 | + | ||
424 | + return $result; | ||
425 | + } | ||
426 | + | ||
427 | + /** | ||
428 | + * Generates EC bytes for given data. | ||
429 | + * | ||
430 | + * @param SplFixedArray<int> $dataBytes | ||
431 | + * @return SplFixedArray<int> | ||
432 | + */ | ||
433 | + private static function generateEcBytes(SplFixedArray $dataBytes, int $numEcBytesInBlock) : SplFixedArray | ||
434 | + { | ||
435 | + $numDataBytes = count($dataBytes); | ||
436 | + $toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock); | ||
437 | + | ||
438 | + for ($i = 0; $i < $numDataBytes; $i++) { | ||
439 | + $toEncode[$i] = $dataBytes[$i] & 0xff; | ||
440 | + } | ||
441 | + | ||
442 | + $ecBytes = new SplFixedArray($numEcBytesInBlock); | ||
443 | + $codec = self::getCodec($numDataBytes, $numEcBytesInBlock); | ||
444 | + $codec->encode($toEncode, $ecBytes); | ||
445 | + | ||
446 | + return $ecBytes; | ||
447 | + } | ||
448 | + | ||
449 | + /** | ||
450 | + * Gets an RS codec and caches it. | ||
451 | + */ | ||
452 | + private static function getCodec(int $numDataBytes, int $numEcBytesInBlock) : ReedSolomonCodec | ||
453 | + { | ||
454 | + $cacheId = $numDataBytes . '-' . $numEcBytesInBlock; | ||
455 | + | ||
456 | + if (isset(self::$codecs[$cacheId])) { | ||
457 | + return self::$codecs[$cacheId]; | ||
458 | + } | ||
459 | + | ||
460 | + return self::$codecs[$cacheId] = new ReedSolomonCodec( | ||
461 | + 8, | ||
462 | + 0x11d, | ||
463 | + 0, | ||
464 | + 1, | ||
465 | + $numEcBytesInBlock, | ||
466 | + 255 - $numDataBytes - $numEcBytesInBlock | ||
467 | + ); | ||
468 | + } | ||
469 | + | ||
470 | + /** | ||
471 | + * Appends mode information to a bit array. | ||
472 | + */ | ||
473 | + private static function appendModeInfo(Mode $mode, BitArray $bits) : void | ||
474 | + { | ||
475 | + $bits->appendBits($mode->getBits(), 4); | ||
476 | + } | ||
477 | + | ||
478 | + /** | ||
479 | + * Appends length information to a bit array. | ||
480 | + * | ||
481 | + * @throws WriterException if num letters is bigger than expected | ||
482 | + */ | ||
483 | + private static function appendLengthInfo(int $numLetters, Version $version, Mode $mode, BitArray $bits) : void | ||
484 | + { | ||
485 | + $numBits = $mode->getCharacterCountBits($version); | ||
486 | + | ||
487 | + if ($numLetters >= (1 << $numBits)) { | ||
488 | + throw new WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1)); | ||
489 | + } | ||
490 | + | ||
491 | + $bits->appendBits($numLetters, $numBits); | ||
492 | + } | ||
493 | + | ||
494 | + /** | ||
495 | + * Appends bytes to a bit array in a specific mode. | ||
496 | + * | ||
497 | + * @throws WriterException if an invalid mode was supplied | ||
498 | + */ | ||
499 | + private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void | ||
500 | + { | ||
501 | + switch ($mode) { | ||
502 | + case Mode::NUMERIC(): | ||
503 | + self::appendNumericBytes($content, $bits); | ||
504 | + break; | ||
505 | + | ||
506 | + case Mode::ALPHANUMERIC(): | ||
507 | + self::appendAlphanumericBytes($content, $bits); | ||
508 | + break; | ||
509 | + | ||
510 | + case Mode::BYTE(): | ||
511 | + self::append8BitBytes($content, $bits, $encoding); | ||
512 | + break; | ||
513 | + | ||
514 | + case Mode::KANJI(): | ||
515 | + self::appendKanjiBytes($content, $bits); | ||
516 | + break; | ||
517 | + | ||
518 | + default: | ||
519 | + throw new WriterException('Invalid mode: ' . $mode); | ||
520 | + } | ||
521 | + } | ||
522 | + | ||
523 | + /** | ||
524 | + * Appends numeric bytes to a bit array. | ||
525 | + */ | ||
526 | + private static function appendNumericBytes(string $content, BitArray $bits) : void | ||
527 | + { | ||
528 | + $length = strlen($content); | ||
529 | + $i = 0; | ||
530 | + | ||
531 | + while ($i < $length) { | ||
532 | + $num1 = (int) $content[$i]; | ||
533 | + | ||
534 | + if ($i + 2 < $length) { | ||
535 | + // Encode three numeric letters in ten bits. | ||
536 | + $num2 = (int) $content[$i + 1]; | ||
537 | + $num3 = (int) $content[$i + 2]; | ||
538 | + $bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10); | ||
539 | + $i += 3; | ||
540 | + } elseif ($i + 1 < $length) { | ||
541 | + // Encode two numeric letters in seven bits. | ||
542 | + $num2 = (int) $content[$i + 1]; | ||
543 | + $bits->appendBits($num1 * 10 + $num2, 7); | ||
544 | + $i += 2; | ||
545 | + } else { | ||
546 | + // Encode one numeric letter in four bits. | ||
547 | + $bits->appendBits($num1, 4); | ||
548 | + ++$i; | ||
549 | + } | ||
550 | + } | ||
551 | + } | ||
552 | + | ||
553 | + /** | ||
554 | + * Appends alpha-numeric bytes to a bit array. | ||
555 | + * | ||
556 | + * @throws WriterException if an invalid alphanumeric code was found | ||
557 | + */ | ||
558 | + private static function appendAlphanumericBytes(string $content, BitArray $bits) : void | ||
559 | + { | ||
560 | + $length = strlen($content); | ||
561 | + $i = 0; | ||
562 | + | ||
563 | + while ($i < $length) { | ||
564 | + $code1 = self::getAlphanumericCode(ord($content[$i])); | ||
565 | + | ||
566 | + if (-1 === $code1) { | ||
567 | + throw new WriterException('Invalid alphanumeric code'); | ||
568 | + } | ||
569 | + | ||
570 | + if ($i + 1 < $length) { | ||
571 | + $code2 = self::getAlphanumericCode(ord($content[$i + 1])); | ||
572 | + | ||
573 | + if (-1 === $code2) { | ||
574 | + throw new WriterException('Invalid alphanumeric code'); | ||
575 | + } | ||
576 | + | ||
577 | + // Encode two alphanumeric letters in 11 bits. | ||
578 | + $bits->appendBits($code1 * 45 + $code2, 11); | ||
579 | + $i += 2; | ||
580 | + } else { | ||
581 | + // Encode one alphanumeric letter in six bits. | ||
582 | + $bits->appendBits($code1, 6); | ||
583 | + ++$i; | ||
584 | + } | ||
585 | + } | ||
586 | + } | ||
587 | + | ||
588 | + /** | ||
589 | + * Appends regular 8-bit bytes to a bit array. | ||
590 | + * | ||
591 | + * @throws WriterException if content cannot be encoded to target encoding | ||
592 | + */ | ||
593 | + private static function append8BitBytes(string $content, BitArray $bits, string $encoding) : void | ||
594 | + { | ||
595 | + $bytes = @iconv('utf-8', $encoding, $content); | ||
596 | + | ||
597 | + if (false === $bytes) { | ||
598 | + throw new WriterException('Could not encode content to ' . $encoding); | ||
599 | + } | ||
600 | + | ||
601 | + $length = strlen($bytes); | ||
602 | + | ||
603 | + for ($i = 0; $i < $length; $i++) { | ||
604 | + $bits->appendBits(ord($bytes[$i]), 8); | ||
605 | + } | ||
606 | + } | ||
607 | + | ||
608 | + /** | ||
609 | + * Appends KANJI bytes to a bit array. | ||
610 | + * | ||
611 | + * @throws WriterException if content does not seem to be encoded in SHIFT-JIS | ||
612 | + * @throws WriterException if an invalid byte sequence occurs | ||
613 | + */ | ||
614 | + private static function appendKanjiBytes(string $content, BitArray $bits) : void | ||
615 | + { | ||
616 | + if (strlen($content) % 2 > 0) { | ||
617 | + // We just do a simple length check here. The for loop will check | ||
618 | + // individual characters. | ||
619 | + throw new WriterException('Content does not seem to be encoded in SHIFT-JIS'); | ||
620 | + } | ||
621 | + | ||
622 | + $length = strlen($content); | ||
623 | + | ||
624 | + for ($i = 0; $i < $length; $i += 2) { | ||
625 | + $byte1 = ord($content[$i]) & 0xff; | ||
626 | + $byte2 = ord($content[$i + 1]) & 0xff; | ||
627 | + $code = ($byte1 << 8) | $byte2; | ||
628 | + | ||
629 | + if ($code >= 0x8140 && $code <= 0x9ffc) { | ||
630 | + $subtracted = $code - 0x8140; | ||
631 | + } elseif ($code >= 0xe040 && $code <= 0xebbf) { | ||
632 | + $subtracted = $code - 0xc140; | ||
633 | + } else { | ||
634 | + throw new WriterException('Invalid byte sequence'); | ||
635 | + } | ||
636 | + | ||
637 | + $encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff); | ||
638 | + | ||
639 | + $bits->appendBits($encoded, 13); | ||
640 | + } | ||
641 | + } | ||
642 | + | ||
643 | + /** | ||
644 | + * Appends ECI information to a bit array. | ||
645 | + */ | ||
646 | + private static function appendEci(CharacterSetEci $eci, BitArray $bits) : void | ||
647 | + { | ||
648 | + $mode = Mode::ECI(); | ||
649 | + $bits->appendBits($mode->getBits(), 4); | ||
650 | + $bits->appendBits($eci->getValue(), 8); | ||
651 | + } | ||
652 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Encoder; | ||
5 | + | ||
6 | +use BaconQrCode\Common\BitUtils; | ||
7 | +use BaconQrCode\Exception\InvalidArgumentException; | ||
8 | + | ||
9 | +/** | ||
10 | + * Mask utility. | ||
11 | + */ | ||
12 | +final class MaskUtil | ||
13 | +{ | ||
14 | + /**#@+ | ||
15 | + * Penalty weights from section 6.8.2.1 | ||
16 | + */ | ||
17 | + const N1 = 3; | ||
18 | + const N2 = 3; | ||
19 | + const N3 = 40; | ||
20 | + const N4 = 10; | ||
21 | + /**#@-*/ | ||
22 | + | ||
23 | + private function __construct() | ||
24 | + { | ||
25 | + } | ||
26 | + | ||
27 | + /** | ||
28 | + * Applies mask penalty rule 1 and returns the penalty. | ||
29 | + * | ||
30 | + * Finds repetitive cells with the same color and gives penalty to them. | ||
31 | + * Example: 00000 or 11111. | ||
32 | + */ | ||
33 | + public static function applyMaskPenaltyRule1(ByteMatrix $matrix) : int | ||
34 | + { | ||
35 | + return ( | ||
36 | + self::applyMaskPenaltyRule1Internal($matrix, true) | ||
37 | + + self::applyMaskPenaltyRule1Internal($matrix, false) | ||
38 | + ); | ||
39 | + } | ||
40 | + | ||
41 | + /** | ||
42 | + * Applies mask penalty rule 2 and returns the penalty. | ||
43 | + * | ||
44 | + * Finds 2x2 blocks with the same color and gives penalty to them. This is | ||
45 | + * actually equivalent to the spec's rule, which is to find MxN blocks and | ||
46 | + * give a penalty proportional to (M-1)x(N-1), because this is the number of | ||
47 | + * 2x2 blocks inside such a block. | ||
48 | + */ | ||
49 | + public static function applyMaskPenaltyRule2(ByteMatrix $matrix) : int | ||
50 | + { | ||
51 | + $penalty = 0; | ||
52 | + $array = $matrix->getArray(); | ||
53 | + $width = $matrix->getWidth(); | ||
54 | + $height = $matrix->getHeight(); | ||
55 | + | ||
56 | + for ($y = 0; $y < $height - 1; ++$y) { | ||
57 | + for ($x = 0; $x < $width - 1; ++$x) { | ||
58 | + $value = $array[$y][$x]; | ||
59 | + | ||
60 | + if ($value === $array[$y][$x + 1] | ||
61 | + && $value === $array[$y + 1][$x] | ||
62 | + && $value === $array[$y + 1][$x + 1] | ||
63 | + ) { | ||
64 | + ++$penalty; | ||
65 | + } | ||
66 | + } | ||
67 | + } | ||
68 | + | ||
69 | + return self::N2 * $penalty; | ||
70 | + } | ||
71 | + | ||
72 | + /** | ||
73 | + * Applies mask penalty rule 3 and returns the penalty. | ||
74 | + * | ||
75 | + * Finds consecutive cells of 00001011101 or 10111010000, and gives penalty | ||
76 | + * to them. If we find patterns like 000010111010000, we give penalties | ||
77 | + * twice (i.e. 40 * 2). | ||
78 | + */ | ||
79 | + public static function applyMaskPenaltyRule3(ByteMatrix $matrix) : int | ||
80 | + { | ||
81 | + $penalty = 0; | ||
82 | + $array = $matrix->getArray(); | ||
83 | + $width = $matrix->getWidth(); | ||
84 | + $height = $matrix->getHeight(); | ||
85 | + | ||
86 | + for ($y = 0; $y < $height; ++$y) { | ||
87 | + for ($x = 0; $x < $width; ++$x) { | ||
88 | + if ($x + 6 < $width | ||
89 | + && 1 === $array[$y][$x] | ||
90 | + && 0 === $array[$y][$x + 1] | ||
91 | + && 1 === $array[$y][$x + 2] | ||
92 | + && 1 === $array[$y][$x + 3] | ||
93 | + && 1 === $array[$y][$x + 4] | ||
94 | + && 0 === $array[$y][$x + 5] | ||
95 | + && 1 === $array[$y][$x + 6] | ||
96 | + && ( | ||
97 | + ( | ||
98 | + $x + 10 < $width | ||
99 | + && 0 === $array[$y][$x + 7] | ||
100 | + && 0 === $array[$y][$x + 8] | ||
101 | + && 0 === $array[$y][$x + 9] | ||
102 | + && 0 === $array[$y][$x + 10] | ||
103 | + ) | ||
104 | + || ( | ||
105 | + $x - 4 >= 0 | ||
106 | + && 0 === $array[$y][$x - 1] | ||
107 | + && 0 === $array[$y][$x - 2] | ||
108 | + && 0 === $array[$y][$x - 3] | ||
109 | + && 0 === $array[$y][$x - 4] | ||
110 | + ) | ||
111 | + ) | ||
112 | + ) { | ||
113 | + $penalty += self::N3; | ||
114 | + } | ||
115 | + | ||
116 | + if ($y + 6 < $height | ||
117 | + && 1 === $array[$y][$x] | ||
118 | + && 0 === $array[$y + 1][$x] | ||
119 | + && 1 === $array[$y + 2][$x] | ||
120 | + && 1 === $array[$y + 3][$x] | ||
121 | + && 1 === $array[$y + 4][$x] | ||
122 | + && 0 === $array[$y + 5][$x] | ||
123 | + && 1 === $array[$y + 6][$x] | ||
124 | + && ( | ||
125 | + ( | ||
126 | + $y + 10 < $height | ||
127 | + && 0 === $array[$y + 7][$x] | ||
128 | + && 0 === $array[$y + 8][$x] | ||
129 | + && 0 === $array[$y + 9][$x] | ||
130 | + && 0 === $array[$y + 10][$x] | ||
131 | + ) | ||
132 | + || ( | ||
133 | + $y - 4 >= 0 | ||
134 | + && 0 === $array[$y - 1][$x] | ||
135 | + && 0 === $array[$y - 2][$x] | ||
136 | + && 0 === $array[$y - 3][$x] | ||
137 | + && 0 === $array[$y - 4][$x] | ||
138 | + ) | ||
139 | + ) | ||
140 | + ) { | ||
141 | + $penalty += self::N3; | ||
142 | + } | ||
143 | + } | ||
144 | + } | ||
145 | + | ||
146 | + return $penalty; | ||
147 | + } | ||
148 | + | ||
149 | + /** | ||
150 | + * Applies mask penalty rule 4 and returns the penalty. | ||
151 | + * | ||
152 | + * Calculates the ratio of dark cells and gives penalty if the ratio is far | ||
153 | + * from 50%. It gives 10 penalty for 5% distance. | ||
154 | + */ | ||
155 | + public static function applyMaskPenaltyRule4(ByteMatrix $matrix) : int | ||
156 | + { | ||
157 | + $numDarkCells = 0; | ||
158 | + | ||
159 | + $array = $matrix->getArray(); | ||
160 | + $width = $matrix->getWidth(); | ||
161 | + $height = $matrix->getHeight(); | ||
162 | + | ||
163 | + for ($y = 0; $y < $height; ++$y) { | ||
164 | + $arrayY = $array[$y]; | ||
165 | + | ||
166 | + for ($x = 0; $x < $width; ++$x) { | ||
167 | + if (1 === $arrayY[$x]) { | ||
168 | + ++$numDarkCells; | ||
169 | + } | ||
170 | + } | ||
171 | + } | ||
172 | + | ||
173 | + $numTotalCells = $height * $width; | ||
174 | + $darkRatio = $numDarkCells / $numTotalCells; | ||
175 | + $fixedPercentVariances = (int) (abs($darkRatio - 0.5) * 20); | ||
176 | + | ||
177 | + return $fixedPercentVariances * self::N4; | ||
178 | + } | ||
179 | + | ||
180 | + /** | ||
181 | + * Returns the mask bit for "getMaskPattern" at "x" and "y". | ||
182 | + * | ||
183 | + * See 8.8 of JISX0510:2004 for mask pattern conditions. | ||
184 | + * | ||
185 | + * @throws InvalidArgumentException if an invalid mask pattern was supplied | ||
186 | + */ | ||
187 | + public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool | ||
188 | + { | ||
189 | + switch ($maskPattern) { | ||
190 | + case 0: | ||
191 | + $intermediate = ($y + $x) & 0x1; | ||
192 | + break; | ||
193 | + | ||
194 | + case 1: | ||
195 | + $intermediate = $y & 0x1; | ||
196 | + break; | ||
197 | + | ||
198 | + case 2: | ||
199 | + $intermediate = $x % 3; | ||
200 | + break; | ||
201 | + | ||
202 | + case 3: | ||
203 | + $intermediate = ($y + $x) % 3; | ||
204 | + break; | ||
205 | + | ||
206 | + case 4: | ||
207 | + $intermediate = (BitUtils::unsignedRightShift($y, 1) + ($x / 3)) & 0x1; | ||
208 | + break; | ||
209 | + | ||
210 | + case 5: | ||
211 | + $temp = $y * $x; | ||
212 | + $intermediate = ($temp & 0x1) + ($temp % 3); | ||
213 | + break; | ||
214 | + | ||
215 | + case 6: | ||
216 | + $temp = $y * $x; | ||
217 | + $intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1; | ||
218 | + break; | ||
219 | + | ||
220 | + case 7: | ||
221 | + $temp = $y * $x; | ||
222 | + $intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1; | ||
223 | + break; | ||
224 | + | ||
225 | + default: | ||
226 | + throw new InvalidArgumentException('Invalid mask pattern: ' . $maskPattern); | ||
227 | + } | ||
228 | + | ||
229 | + return 0 == $intermediate; | ||
230 | + } | ||
231 | + | ||
232 | + /** | ||
233 | + * Helper function for applyMaskPenaltyRule1. | ||
234 | + * | ||
235 | + * We need this for doing this calculation in both vertical and horizontal | ||
236 | + * orders respectively. | ||
237 | + */ | ||
238 | + private static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, bool $isHorizontal) : int | ||
239 | + { | ||
240 | + $penalty = 0; | ||
241 | + $iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth(); | ||
242 | + $jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight(); | ||
243 | + $array = $matrix->getArray(); | ||
244 | + | ||
245 | + for ($i = 0; $i < $iLimit; ++$i) { | ||
246 | + $numSameBitCells = 0; | ||
247 | + $prevBit = -1; | ||
248 | + | ||
249 | + for ($j = 0; $j < $jLimit; $j++) { | ||
250 | + $bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i]; | ||
251 | + | ||
252 | + if ($bit === $prevBit) { | ||
253 | + ++$numSameBitCells; | ||
254 | + } else { | ||
255 | + if ($numSameBitCells >= 5) { | ||
256 | + $penalty += self::N1 + ($numSameBitCells - 5); | ||
257 | + } | ||
258 | + | ||
259 | + $numSameBitCells = 1; | ||
260 | + $prevBit = $bit; | ||
261 | + } | ||
262 | + } | ||
263 | + | ||
264 | + if ($numSameBitCells >= 5) { | ||
265 | + $penalty += self::N1 + ($numSameBitCells - 5); | ||
266 | + } | ||
267 | + } | ||
268 | + | ||
269 | + return $penalty; | ||
270 | + } | ||
271 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Encoder; | ||
5 | + | ||
6 | +use BaconQrCode\Common\BitArray; | ||
7 | +use BaconQrCode\Common\ErrorCorrectionLevel; | ||
8 | +use BaconQrCode\Common\Version; | ||
9 | +use BaconQrCode\Exception\RuntimeException; | ||
10 | +use BaconQrCode\Exception\WriterException; | ||
11 | + | ||
12 | +/** | ||
13 | + * Matrix utility. | ||
14 | + */ | ||
15 | +final class MatrixUtil | ||
16 | +{ | ||
17 | + /** | ||
18 | + * Position detection pattern. | ||
19 | + */ | ||
20 | + private const POSITION_DETECTION_PATTERN = [ | ||
21 | + [1, 1, 1, 1, 1, 1, 1], | ||
22 | + [1, 0, 0, 0, 0, 0, 1], | ||
23 | + [1, 0, 1, 1, 1, 0, 1], | ||
24 | + [1, 0, 1, 1, 1, 0, 1], | ||
25 | + [1, 0, 1, 1, 1, 0, 1], | ||
26 | + [1, 0, 0, 0, 0, 0, 1], | ||
27 | + [1, 1, 1, 1, 1, 1, 1], | ||
28 | + ]; | ||
29 | + | ||
30 | + /** | ||
31 | + * Position adjustment pattern. | ||
32 | + */ | ||
33 | + private const POSITION_ADJUSTMENT_PATTERN = [ | ||
34 | + [1, 1, 1, 1, 1], | ||
35 | + [1, 0, 0, 0, 1], | ||
36 | + [1, 0, 1, 0, 1], | ||
37 | + [1, 0, 0, 0, 1], | ||
38 | + [1, 1, 1, 1, 1], | ||
39 | + ]; | ||
40 | + | ||
41 | + /** | ||
42 | + * Coordinates for position adjustment patterns for each version. | ||
43 | + */ | ||
44 | + private const POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = [ | ||
45 | + [null, null, null, null, null, null, null], // Version 1 | ||
46 | + [ 6, 18, null, null, null, null, null], // Version 2 | ||
47 | + [ 6, 22, null, null, null, null, null], // Version 3 | ||
48 | + [ 6, 26, null, null, null, null, null], // Version 4 | ||
49 | + [ 6, 30, null, null, null, null, null], // Version 5 | ||
50 | + [ 6, 34, null, null, null, null, null], // Version 6 | ||
51 | + [ 6, 22, 38, null, null, null, null], // Version 7 | ||
52 | + [ 6, 24, 42, null, null, null, null], // Version 8 | ||
53 | + [ 6, 26, 46, null, null, null, null], // Version 9 | ||
54 | + [ 6, 28, 50, null, null, null, null], // Version 10 | ||
55 | + [ 6, 30, 54, null, null, null, null], // Version 11 | ||
56 | + [ 6, 32, 58, null, null, null, null], // Version 12 | ||
57 | + [ 6, 34, 62, null, null, null, null], // Version 13 | ||
58 | + [ 6, 26, 46, 66, null, null, null], // Version 14 | ||
59 | + [ 6, 26, 48, 70, null, null, null], // Version 15 | ||
60 | + [ 6, 26, 50, 74, null, null, null], // Version 16 | ||
61 | + [ 6, 30, 54, 78, null, null, null], // Version 17 | ||
62 | + [ 6, 30, 56, 82, null, null, null], // Version 18 | ||
63 | + [ 6, 30, 58, 86, null, null, null], // Version 19 | ||
64 | + [ 6, 34, 62, 90, null, null, null], // Version 20 | ||
65 | + [ 6, 28, 50, 72, 94, null, null], // Version 21 | ||
66 | + [ 6, 26, 50, 74, 98, null, null], // Version 22 | ||
67 | + [ 6, 30, 54, 78, 102, null, null], // Version 23 | ||
68 | + [ 6, 28, 54, 80, 106, null, null], // Version 24 | ||
69 | + [ 6, 32, 58, 84, 110, null, null], // Version 25 | ||
70 | + [ 6, 30, 58, 86, 114, null, null], // Version 26 | ||
71 | + [ 6, 34, 62, 90, 118, null, null], // Version 27 | ||
72 | + [ 6, 26, 50, 74, 98, 122, null], // Version 28 | ||
73 | + [ 6, 30, 54, 78, 102, 126, null], // Version 29 | ||
74 | + [ 6, 26, 52, 78, 104, 130, null], // Version 30 | ||
75 | + [ 6, 30, 56, 82, 108, 134, null], // Version 31 | ||
76 | + [ 6, 34, 60, 86, 112, 138, null], // Version 32 | ||
77 | + [ 6, 30, 58, 86, 114, 142, null], // Version 33 | ||
78 | + [ 6, 34, 62, 90, 118, 146, null], // Version 34 | ||
79 | + [ 6, 30, 54, 78, 102, 126, 150], // Version 35 | ||
80 | + [ 6, 24, 50, 76, 102, 128, 154], // Version 36 | ||
81 | + [ 6, 28, 54, 80, 106, 132, 158], // Version 37 | ||
82 | + [ 6, 32, 58, 84, 110, 136, 162], // Version 38 | ||
83 | + [ 6, 26, 54, 82, 110, 138, 166], // Version 39 | ||
84 | + [ 6, 30, 58, 86, 114, 142, 170], // Version 40 | ||
85 | + ]; | ||
86 | + | ||
87 | + /** | ||
88 | + * Type information coordinates. | ||
89 | + */ | ||
90 | + private const TYPE_INFO_COORDINATES = [ | ||
91 | + [8, 0], | ||
92 | + [8, 1], | ||
93 | + [8, 2], | ||
94 | + [8, 3], | ||
95 | + [8, 4], | ||
96 | + [8, 5], | ||
97 | + [8, 7], | ||
98 | + [8, 8], | ||
99 | + [7, 8], | ||
100 | + [5, 8], | ||
101 | + [4, 8], | ||
102 | + [3, 8], | ||
103 | + [2, 8], | ||
104 | + [1, 8], | ||
105 | + [0, 8], | ||
106 | + ]; | ||
107 | + | ||
108 | + /** | ||
109 | + * Version information polynomial. | ||
110 | + */ | ||
111 | + private const VERSION_INFO_POLY = 0x1f25; | ||
112 | + | ||
113 | + /** | ||
114 | + * Type information polynomial. | ||
115 | + */ | ||
116 | + private const TYPE_INFO_POLY = 0x537; | ||
117 | + | ||
118 | + /** | ||
119 | + * Type information mask pattern. | ||
120 | + */ | ||
121 | + private const TYPE_INFO_MASK_PATTERN = 0x5412; | ||
122 | + | ||
123 | + /** | ||
124 | + * Clears a given matrix. | ||
125 | + */ | ||
126 | + public static function clearMatrix(ByteMatrix $matrix) : void | ||
127 | + { | ||
128 | + $matrix->clear(-1); | ||
129 | + } | ||
130 | + | ||
131 | + /** | ||
132 | + * Builds a complete matrix. | ||
133 | + */ | ||
134 | + public static function buildMatrix( | ||
135 | + BitArray $dataBits, | ||
136 | + ErrorCorrectionLevel $level, | ||
137 | + Version $version, | ||
138 | + int $maskPattern, | ||
139 | + ByteMatrix $matrix | ||
140 | + ) : void { | ||
141 | + self::clearMatrix($matrix); | ||
142 | + self::embedBasicPatterns($version, $matrix); | ||
143 | + self::embedTypeInfo($level, $maskPattern, $matrix); | ||
144 | + self::maybeEmbedVersionInfo($version, $matrix); | ||
145 | + self::embedDataBits($dataBits, $maskPattern, $matrix); | ||
146 | + } | ||
147 | + | ||
148 | + /** | ||
149 | + * Removes the position detection patterns from a matrix. | ||
150 | + * | ||
151 | + * This can be useful if you need to render those patterns separately. | ||
152 | + */ | ||
153 | + public static function removePositionDetectionPatterns(ByteMatrix $matrix) : void | ||
154 | + { | ||
155 | + $pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]); | ||
156 | + | ||
157 | + self::removePositionDetectionPattern(0, 0, $matrix); | ||
158 | + self::removePositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix); | ||
159 | + self::removePositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix); | ||
160 | + } | ||
161 | + | ||
162 | + /** | ||
163 | + * Embeds type information into a matrix. | ||
164 | + */ | ||
165 | + private static function embedTypeInfo(ErrorCorrectionLevel $level, int $maskPattern, ByteMatrix $matrix) : void | ||
166 | + { | ||
167 | + $typeInfoBits = new BitArray(); | ||
168 | + self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits); | ||
169 | + | ||
170 | + $typeInfoBitsSize = $typeInfoBits->getSize(); | ||
171 | + | ||
172 | + for ($i = 0; $i < $typeInfoBitsSize; ++$i) { | ||
173 | + $bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i); | ||
174 | + | ||
175 | + $x1 = self::TYPE_INFO_COORDINATES[$i][0]; | ||
176 | + $y1 = self::TYPE_INFO_COORDINATES[$i][1]; | ||
177 | + | ||
178 | + $matrix->set($x1, $y1, (int) $bit); | ||
179 | + | ||
180 | + if ($i < 8) { | ||
181 | + $x2 = $matrix->getWidth() - $i - 1; | ||
182 | + $y2 = 8; | ||
183 | + } else { | ||
184 | + $x2 = 8; | ||
185 | + $y2 = $matrix->getHeight() - 7 + ($i - 8); | ||
186 | + } | ||
187 | + | ||
188 | + $matrix->set($x2, $y2, (int) $bit); | ||
189 | + } | ||
190 | + } | ||
191 | + | ||
192 | + /** | ||
193 | + * Generates type information bits and appends them to a bit array. | ||
194 | + * | ||
195 | + * @throws RuntimeException if bit array resulted in invalid size | ||
196 | + */ | ||
197 | + private static function makeTypeInfoBits(ErrorCorrectionLevel $level, int $maskPattern, BitArray $bits) : void | ||
198 | + { | ||
199 | + $typeInfo = ($level->getBits() << 3) | $maskPattern; | ||
200 | + $bits->appendBits($typeInfo, 5); | ||
201 | + | ||
202 | + $bchCode = self::calculateBchCode($typeInfo, self::TYPE_INFO_POLY); | ||
203 | + $bits->appendBits($bchCode, 10); | ||
204 | + | ||
205 | + $maskBits = new BitArray(); | ||
206 | + $maskBits->appendBits(self::TYPE_INFO_MASK_PATTERN, 15); | ||
207 | + $bits->xorBits($maskBits); | ||
208 | + | ||
209 | + if (15 !== $bits->getSize()) { | ||
210 | + throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize()); | ||
211 | + } | ||
212 | + } | ||
213 | + | ||
214 | + /** | ||
215 | + * Embeds version information if required. | ||
216 | + */ | ||
217 | + private static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix) : void | ||
218 | + { | ||
219 | + if ($version->getVersionNumber() < 7) { | ||
220 | + return; | ||
221 | + } | ||
222 | + | ||
223 | + $versionInfoBits = new BitArray(); | ||
224 | + self::makeVersionInfoBits($version, $versionInfoBits); | ||
225 | + | ||
226 | + $bitIndex = 6 * 3 - 1; | ||
227 | + | ||
228 | + for ($i = 0; $i < 6; ++$i) { | ||
229 | + for ($j = 0; $j < 3; ++$j) { | ||
230 | + $bit = $versionInfoBits->get($bitIndex); | ||
231 | + --$bitIndex; | ||
232 | + | ||
233 | + $matrix->set($i, $matrix->getHeight() - 11 + $j, (int) $bit); | ||
234 | + $matrix->set($matrix->getHeight() - 11 + $j, $i, (int) $bit); | ||
235 | + } | ||
236 | + } | ||
237 | + } | ||
238 | + | ||
239 | + /** | ||
240 | + * Generates version information bits and appends them to a bit array. | ||
241 | + * | ||
242 | + * @throws RuntimeException if bit array resulted in invalid size | ||
243 | + */ | ||
244 | + private static function makeVersionInfoBits(Version $version, BitArray $bits) : void | ||
245 | + { | ||
246 | + $bits->appendBits($version->getVersionNumber(), 6); | ||
247 | + | ||
248 | + $bchCode = self::calculateBchCode($version->getVersionNumber(), self::VERSION_INFO_POLY); | ||
249 | + $bits->appendBits($bchCode, 12); | ||
250 | + | ||
251 | + if (18 !== $bits->getSize()) { | ||
252 | + throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize()); | ||
253 | + } | ||
254 | + } | ||
255 | + | ||
256 | + /** | ||
257 | + * Calculates the BCH code for a value and a polynomial. | ||
258 | + */ | ||
259 | + private static function calculateBchCode(int $value, int $poly) : int | ||
260 | + { | ||
261 | + $msbSetInPoly = self::findMsbSet($poly); | ||
262 | + $value <<= $msbSetInPoly - 1; | ||
263 | + | ||
264 | + while (self::findMsbSet($value) >= $msbSetInPoly) { | ||
265 | + $value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly); | ||
266 | + } | ||
267 | + | ||
268 | + return $value; | ||
269 | + } | ||
270 | + | ||
271 | + /** | ||
272 | + * Finds and MSB set. | ||
273 | + */ | ||
274 | + private static function findMsbSet(int $value) : int | ||
275 | + { | ||
276 | + $numDigits = 0; | ||
277 | + | ||
278 | + while (0 !== $value) { | ||
279 | + $value >>= 1; | ||
280 | + ++$numDigits; | ||
281 | + } | ||
282 | + | ||
283 | + return $numDigits; | ||
284 | + } | ||
285 | + | ||
286 | + /** | ||
287 | + * Embeds basic patterns into a matrix. | ||
288 | + */ | ||
289 | + private static function embedBasicPatterns(Version $version, ByteMatrix $matrix) : void | ||
290 | + { | ||
291 | + self::embedPositionDetectionPatternsAndSeparators($matrix); | ||
292 | + self::embedDarkDotAtLeftBottomCorner($matrix); | ||
293 | + self::maybeEmbedPositionAdjustmentPatterns($version, $matrix); | ||
294 | + self::embedTimingPatterns($matrix); | ||
295 | + } | ||
296 | + | ||
297 | + /** | ||
298 | + * Embeds position detection patterns and separators into a byte matrix. | ||
299 | + */ | ||
300 | + private static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix) : void | ||
301 | + { | ||
302 | + $pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]); | ||
303 | + | ||
304 | + self::embedPositionDetectionPattern(0, 0, $matrix); | ||
305 | + self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix); | ||
306 | + self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix); | ||
307 | + | ||
308 | + $hspWidth = 8; | ||
309 | + | ||
310 | + self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix); | ||
311 | + self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix); | ||
312 | + self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix); | ||
313 | + | ||
314 | + $vspSize = 7; | ||
315 | + | ||
316 | + self::embedVerticalSeparationPattern($vspSize, 0, $matrix); | ||
317 | + self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix); | ||
318 | + self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix); | ||
319 | + } | ||
320 | + | ||
321 | + /** | ||
322 | + * Embeds a single position detection pattern into a byte matrix. | ||
323 | + */ | ||
324 | + private static function embedPositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void | ||
325 | + { | ||
326 | + for ($y = 0; $y < 7; ++$y) { | ||
327 | + for ($x = 0; $x < 7; ++$x) { | ||
328 | + $matrix->set($xStart + $x, $yStart + $y, self::POSITION_DETECTION_PATTERN[$y][$x]); | ||
329 | + } | ||
330 | + } | ||
331 | + } | ||
332 | + | ||
333 | + private static function removePositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void | ||
334 | + { | ||
335 | + for ($y = 0; $y < 7; ++$y) { | ||
336 | + for ($x = 0; $x < 7; ++$x) { | ||
337 | + $matrix->set($xStart + $x, $yStart + $y, 0); | ||
338 | + } | ||
339 | + } | ||
340 | + } | ||
341 | + | ||
342 | + /** | ||
343 | + * Embeds a single horizontal separation pattern. | ||
344 | + * | ||
345 | + * @throws RuntimeException if a byte was already set | ||
346 | + */ | ||
347 | + private static function embedHorizontalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void | ||
348 | + { | ||
349 | + for ($x = 0; $x < 8; $x++) { | ||
350 | + if (-1 !== $matrix->get($xStart + $x, $yStart)) { | ||
351 | + throw new RuntimeException('Byte already set'); | ||
352 | + } | ||
353 | + | ||
354 | + $matrix->set($xStart + $x, $yStart, 0); | ||
355 | + } | ||
356 | + } | ||
357 | + | ||
358 | + /** | ||
359 | + * Embeds a single vertical separation pattern. | ||
360 | + * | ||
361 | + * @throws RuntimeException if a byte was already set | ||
362 | + */ | ||
363 | + private static function embedVerticalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void | ||
364 | + { | ||
365 | + for ($y = 0; $y < 7; $y++) { | ||
366 | + if (-1 !== $matrix->get($xStart, $yStart + $y)) { | ||
367 | + throw new RuntimeException('Byte already set'); | ||
368 | + } | ||
369 | + | ||
370 | + $matrix->set($xStart, $yStart + $y, 0); | ||
371 | + } | ||
372 | + } | ||
373 | + | ||
374 | + /** | ||
375 | + * Embeds a dot at the left bottom corner. | ||
376 | + * | ||
377 | + * @throws RuntimeException if a byte was already set to 0 | ||
378 | + */ | ||
379 | + private static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix) : void | ||
380 | + { | ||
381 | + if (0 === $matrix->get(8, $matrix->getHeight() - 8)) { | ||
382 | + throw new RuntimeException('Byte already set to 0'); | ||
383 | + } | ||
384 | + | ||
385 | + $matrix->set(8, $matrix->getHeight() - 8, 1); | ||
386 | + } | ||
387 | + | ||
388 | + /** | ||
389 | + * Embeds position adjustment patterns if required. | ||
390 | + */ | ||
391 | + private static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix) : void | ||
392 | + { | ||
393 | + if ($version->getVersionNumber() < 2) { | ||
394 | + return; | ||
395 | + } | ||
396 | + | ||
397 | + $index = $version->getVersionNumber() - 1; | ||
398 | + | ||
399 | + $coordinates = self::POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[$index]; | ||
400 | + $numCoordinates = count($coordinates); | ||
401 | + | ||
402 | + for ($i = 0; $i < $numCoordinates; ++$i) { | ||
403 | + for ($j = 0; $j < $numCoordinates; ++$j) { | ||
404 | + $y = $coordinates[$i]; | ||
405 | + $x = $coordinates[$j]; | ||
406 | + | ||
407 | + if (null === $x || null === $y) { | ||
408 | + continue; | ||
409 | + } | ||
410 | + | ||
411 | + if (-1 === $matrix->get($x, $y)) { | ||
412 | + self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix); | ||
413 | + } | ||
414 | + } | ||
415 | + } | ||
416 | + } | ||
417 | + | ||
418 | + /** | ||
419 | + * Embeds a single position adjustment pattern. | ||
420 | + */ | ||
421 | + private static function embedPositionAdjustmentPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void | ||
422 | + { | ||
423 | + for ($y = 0; $y < 5; $y++) { | ||
424 | + for ($x = 0; $x < 5; $x++) { | ||
425 | + $matrix->set($xStart + $x, $yStart + $y, self::POSITION_ADJUSTMENT_PATTERN[$y][$x]); | ||
426 | + } | ||
427 | + } | ||
428 | + } | ||
429 | + | ||
430 | + /** | ||
431 | + * Embeds timing patterns into a matrix. | ||
432 | + */ | ||
433 | + private static function embedTimingPatterns(ByteMatrix $matrix) : void | ||
434 | + { | ||
435 | + $matrixWidth = $matrix->getWidth(); | ||
436 | + | ||
437 | + for ($i = 8; $i < $matrixWidth - 8; ++$i) { | ||
438 | + $bit = ($i + 1) % 2; | ||
439 | + | ||
440 | + if (-1 === $matrix->get($i, 6)) { | ||
441 | + $matrix->set($i, 6, $bit); | ||
442 | + } | ||
443 | + | ||
444 | + if (-1 === $matrix->get(6, $i)) { | ||
445 | + $matrix->set(6, $i, $bit); | ||
446 | + } | ||
447 | + } | ||
448 | + } | ||
449 | + | ||
450 | + /** | ||
451 | + * Embeds "dataBits" using "getMaskPattern". | ||
452 | + * | ||
453 | + * For debugging purposes, it skips masking process if "getMaskPattern" is -1. See 8.7 of JISX0510:2004 (p.38) for | ||
454 | + * how to embed data bits. | ||
455 | + * | ||
456 | + * @throws WriterException if not all bits could be consumed | ||
457 | + */ | ||
458 | + private static function embedDataBits(BitArray $dataBits, int $maskPattern, ByteMatrix $matrix) : void | ||
459 | + { | ||
460 | + $bitIndex = 0; | ||
461 | + $direction = -1; | ||
462 | + | ||
463 | + // Start from the right bottom cell. | ||
464 | + $x = $matrix->getWidth() - 1; | ||
465 | + $y = $matrix->getHeight() - 1; | ||
466 | + | ||
467 | + while ($x > 0) { | ||
468 | + // Skip vertical timing pattern. | ||
469 | + if (6 === $x) { | ||
470 | + --$x; | ||
471 | + } | ||
472 | + | ||
473 | + while ($y >= 0 && $y < $matrix->getHeight()) { | ||
474 | + for ($i = 0; $i < 2; $i++) { | ||
475 | + $xx = $x - $i; | ||
476 | + | ||
477 | + // Skip the cell if it's not empty. | ||
478 | + if (-1 !== $matrix->get($xx, $y)) { | ||
479 | + continue; | ||
480 | + } | ||
481 | + | ||
482 | + if ($bitIndex < $dataBits->getSize()) { | ||
483 | + $bit = $dataBits->get($bitIndex); | ||
484 | + ++$bitIndex; | ||
485 | + } else { | ||
486 | + // Padding bit. If there is no bit left, we'll fill the | ||
487 | + // left cells with 0, as described in 8.4.9 of | ||
488 | + // JISX0510:2004 (p. 24). | ||
489 | + $bit = false; | ||
490 | + } | ||
491 | + | ||
492 | + // Skip masking if maskPattern is -1. | ||
493 | + if (-1 !== $maskPattern && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) { | ||
494 | + $bit = ! $bit; | ||
495 | + } | ||
496 | + | ||
497 | + $matrix->set($xx, $y, (int) $bit); | ||
498 | + } | ||
499 | + | ||
500 | + $y += $direction; | ||
501 | + } | ||
502 | + | ||
503 | + $direction = -$direction; | ||
504 | + $y += $direction; | ||
505 | + $x -= 2; | ||
506 | + } | ||
507 | + | ||
508 | + // All bits should be consumed | ||
509 | + if ($dataBits->getSize() !== $bitIndex) { | ||
510 | + throw new WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')'); | ||
511 | + } | ||
512 | + } | ||
513 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Encoder; | ||
5 | + | ||
6 | +use BaconQrCode\Common\ErrorCorrectionLevel; | ||
7 | +use BaconQrCode\Common\Mode; | ||
8 | +use BaconQrCode\Common\Version; | ||
9 | + | ||
10 | +/** | ||
11 | + * QR code. | ||
12 | + */ | ||
13 | +final class QrCode | ||
14 | +{ | ||
15 | + /** | ||
16 | + * Number of possible mask patterns. | ||
17 | + */ | ||
18 | + public const NUM_MASK_PATTERNS = 8; | ||
19 | + | ||
20 | + /** | ||
21 | + * Mode of the QR code. | ||
22 | + * | ||
23 | + * @var Mode | ||
24 | + */ | ||
25 | + private $mode; | ||
26 | + | ||
27 | + /** | ||
28 | + * EC level of the QR code. | ||
29 | + * | ||
30 | + * @var ErrorCorrectionLevel | ||
31 | + */ | ||
32 | + private $errorCorrectionLevel; | ||
33 | + | ||
34 | + /** | ||
35 | + * Version of the QR code. | ||
36 | + * | ||
37 | + * @var Version | ||
38 | + */ | ||
39 | + private $version; | ||
40 | + | ||
41 | + /** | ||
42 | + * Mask pattern of the QR code. | ||
43 | + * | ||
44 | + * @var int | ||
45 | + */ | ||
46 | + private $maskPattern = -1; | ||
47 | + | ||
48 | + /** | ||
49 | + * Matrix of the QR code. | ||
50 | + * | ||
51 | + * @var ByteMatrix | ||
52 | + */ | ||
53 | + private $matrix; | ||
54 | + | ||
55 | + public function __construct( | ||
56 | + Mode $mode, | ||
57 | + ErrorCorrectionLevel $errorCorrectionLevel, | ||
58 | + Version $version, | ||
59 | + int $maskPattern, | ||
60 | + ByteMatrix $matrix | ||
61 | + ) { | ||
62 | + $this->mode = $mode; | ||
63 | + $this->errorCorrectionLevel = $errorCorrectionLevel; | ||
64 | + $this->version = $version; | ||
65 | + $this->maskPattern = $maskPattern; | ||
66 | + $this->matrix = $matrix; | ||
67 | + } | ||
68 | + | ||
69 | + /** | ||
70 | + * Gets the mode. | ||
71 | + */ | ||
72 | + public function getMode() : Mode | ||
73 | + { | ||
74 | + return $this->mode; | ||
75 | + } | ||
76 | + | ||
77 | + /** | ||
78 | + * Gets the EC level. | ||
79 | + */ | ||
80 | + public function getErrorCorrectionLevel() : ErrorCorrectionLevel | ||
81 | + { | ||
82 | + return $this->errorCorrectionLevel; | ||
83 | + } | ||
84 | + | ||
85 | + /** | ||
86 | + * Gets the version. | ||
87 | + */ | ||
88 | + public function getVersion() : Version | ||
89 | + { | ||
90 | + return $this->version; | ||
91 | + } | ||
92 | + | ||
93 | + /** | ||
94 | + * Gets the mask pattern. | ||
95 | + */ | ||
96 | + public function getMaskPattern() : int | ||
97 | + { | ||
98 | + return $this->maskPattern; | ||
99 | + } | ||
100 | + | ||
101 | + /** | ||
102 | + * Gets the matrix. | ||
103 | + * | ||
104 | + * @return ByteMatrix | ||
105 | + */ | ||
106 | + public function getMatrix() | ||
107 | + { | ||
108 | + return $this->matrix; | ||
109 | + } | ||
110 | + | ||
111 | + /** | ||
112 | + * Validates whether a mask pattern is valid. | ||
113 | + */ | ||
114 | + public static function isValidMaskPattern(int $maskPattern) : bool | ||
115 | + { | ||
116 | + return $maskPattern > 0 && $maskPattern < self::NUM_MASK_PATTERNS; | ||
117 | + } | ||
118 | + | ||
119 | + /** | ||
120 | + * Returns a string representation of the QR code. | ||
121 | + */ | ||
122 | + public function __toString() : string | ||
123 | + { | ||
124 | + $result = "<<\n" | ||
125 | + . ' mode: ' . $this->mode . "\n" | ||
126 | + . ' ecLevel: ' . $this->errorCorrectionLevel . "\n" | ||
127 | + . ' version: ' . $this->version . "\n" | ||
128 | + . ' maskPattern: ' . $this->maskPattern . "\n"; | ||
129 | + | ||
130 | + if ($this->matrix === null) { | ||
131 | + $result .= " matrix: null\n"; | ||
132 | + } else { | ||
133 | + $result .= " matrix:\n"; | ||
134 | + $result .= $this->matrix; | ||
135 | + } | ||
136 | + | ||
137 | + $result .= ">>\n"; | ||
138 | + | ||
139 | + return $result; | ||
140 | + } | ||
141 | +} |
addons/qrcode/library/BaconQrCode/LICENSE
0 → 100644
1 | +Copyright (c) 2017, Ben Scholzen 'DASPRiD' | ||
2 | +All rights reserved. | ||
3 | + | ||
4 | +Redistribution and use in source and binary forms, with or without | ||
5 | +modification, are permitted provided that the following conditions are met: | ||
6 | + | ||
7 | +1. Redistributions of source code must retain the above copyright notice, this | ||
8 | + list of conditions and the following disclaimer. | ||
9 | +2. Redistributions in binary form must reproduce the above copyright notice, | ||
10 | + this list of conditions and the following disclaimer in the documentation | ||
11 | + and/or other materials provided with the distribution. | ||
12 | + | ||
13 | +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
14 | +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
15 | +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
16 | +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
17 | +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
18 | +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
19 | +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
20 | +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
21 | +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
22 | +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Renderer\Color; | ||
5 | + | ||
6 | +use BaconQrCode\Exception; | ||
7 | + | ||
8 | +final class Alpha implements ColorInterface | ||
9 | +{ | ||
10 | + /** | ||
11 | + * @var int | ||
12 | + */ | ||
13 | + private $alpha; | ||
14 | + | ||
15 | + /** | ||
16 | + * @var ColorInterface | ||
17 | + */ | ||
18 | + private $baseColor; | ||
19 | + | ||
20 | + /** | ||
21 | + * @param int $alpha the alpha value, 0 to 100 | ||
22 | + */ | ||
23 | + public function __construct(int $alpha, ColorInterface $baseColor) | ||
24 | + { | ||
25 | + if ($alpha < 0 || $alpha > 100) { | ||
26 | + throw new Exception\InvalidArgumentException('Alpha must be between 0 and 100'); | ||
27 | + } | ||
28 | + | ||
29 | + $this->alpha = $alpha; | ||
30 | + $this->baseColor = $baseColor; | ||
31 | + } | ||
32 | + | ||
33 | + public function getAlpha() : int | ||
34 | + { | ||
35 | + return $this->alpha; | ||
36 | + } | ||
37 | + | ||
38 | + public function getBaseColor() : ColorInterface | ||
39 | + { | ||
40 | + return $this->baseColor; | ||
41 | + } | ||
42 | + | ||
43 | + public function toRgb() : Rgb | ||
44 | + { | ||
45 | + return $this->baseColor->toRgb(); | ||
46 | + } | ||
47 | + | ||
48 | + public function toCmyk() : Cmyk | ||
49 | + { | ||
50 | + return $this->baseColor->toCmyk(); | ||
51 | + } | ||
52 | + | ||
53 | + public function toGray() : Gray | ||
54 | + { | ||
55 | + return $this->baseColor->toGray(); | ||
56 | + } | ||
57 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Renderer\Color; | ||
5 | + | ||
6 | +use BaconQrCode\Exception; | ||
7 | + | ||
8 | +final class Cmyk implements ColorInterface | ||
9 | +{ | ||
10 | + /** | ||
11 | + * @var int | ||
12 | + */ | ||
13 | + private $cyan; | ||
14 | + | ||
15 | + /** | ||
16 | + * @var int | ||
17 | + */ | ||
18 | + private $magenta; | ||
19 | + | ||
20 | + /** | ||
21 | + * @var int | ||
22 | + */ | ||
23 | + private $yellow; | ||
24 | + | ||
25 | + /** | ||
26 | + * @var int | ||
27 | + */ | ||
28 | + private $black; | ||
29 | + | ||
30 | + /** | ||
31 | + * @param int $cyan the cyan amount, 0 to 100 | ||
32 | + * @param int $magenta the magenta amount, 0 to 100 | ||
33 | + * @param int $yellow the yellow amount, 0 to 100 | ||
34 | + * @param int $black the black amount, 0 to 100 | ||
35 | + */ | ||
36 | + public function __construct(int $cyan, int $magenta, int $yellow, int $black) | ||
37 | + { | ||
38 | + if ($cyan < 0 || $cyan > 100) { | ||
39 | + throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100'); | ||
40 | + } | ||
41 | + | ||
42 | + if ($magenta < 0 || $magenta > 100) { | ||
43 | + throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100'); | ||
44 | + } | ||
45 | + | ||
46 | + if ($yellow < 0 || $yellow > 100) { | ||
47 | + throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100'); | ||
48 | + } | ||
49 | + | ||
50 | + if ($black < 0 || $black > 100) { | ||
51 | + throw new Exception\InvalidArgumentException('Black must be between 0 and 100'); | ||
52 | + } | ||
53 | + | ||
54 | + $this->cyan = $cyan; | ||
55 | + $this->magenta = $magenta; | ||
56 | + $this->yellow = $yellow; | ||
57 | + $this->black = $black; | ||
58 | + } | ||
59 | + | ||
60 | + public function getCyan() : int | ||
61 | + { | ||
62 | + return $this->cyan; | ||
63 | + } | ||
64 | + | ||
65 | + public function getMagenta() : int | ||
66 | + { | ||
67 | + return $this->magenta; | ||
68 | + } | ||
69 | + | ||
70 | + public function getYellow() : int | ||
71 | + { | ||
72 | + return $this->yellow; | ||
73 | + } | ||
74 | + | ||
75 | + public function getBlack() : int | ||
76 | + { | ||
77 | + return $this->black; | ||
78 | + } | ||
79 | + | ||
80 | + public function toRgb() : Rgb | ||
81 | + { | ||
82 | + $k = $this->black / 100; | ||
83 | + $c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100; | ||
84 | + $m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100; | ||
85 | + $y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100; | ||
86 | + | ||
87 | + return new Rgb( | ||
88 | + (int) (-$c * 255 + 255), | ||
89 | + (int) (-$m * 255 + 255), | ||
90 | + (int) (-$y * 255 + 255) | ||
91 | + ); | ||
92 | + } | ||
93 | + | ||
94 | + public function toCmyk() : Cmyk | ||
95 | + { | ||
96 | + return $this; | ||
97 | + } | ||
98 | + | ||
99 | + public function toGray() : Gray | ||
100 | + { | ||
101 | + return $this->toRgb()->toGray(); | ||
102 | + } | ||
103 | +} |
1 | +<?php | ||
2 | +declare(strict_types = 1); | ||
3 | + | ||
4 | +namespace BaconQrCode\Renderer\Color; | ||
5 | + | ||
6 | +interface ColorInterface | ||
7 | +{ | ||
8 | + /** | ||
9 | + * Converts the color to RGB. | ||
10 | + */ | ||
11 | + public function toRgb() : Rgb; | ||
12 | + | ||
13 | + /** | ||
14 | + * Converts the color to CMYK. | ||
15 | + */ | ||
16 | + public function toCmyk() : Cmyk; | ||
17 | + | ||
18 | + /** | ||
19 | + * Converts the color to gray. | ||
20 | + */ | ||
21 | + public function toGray() : Gray; | ||
22 | +} |
-
请 注册 或 登录 后发表评论