MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、JS空集合保护、 会话过期体验优化(403 JSON)、display_errors 关闭、 固定 key 根治 Integrity check failed 等全部改动 注意:config.inc.php/.htaccess/.user.ini 含敏感信息, 通过 .gitignore 排除,勿推送到公开仓库。
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* Class AlphaNum
|
||||
*
|
||||
* @created 25.11.2015
|
||||
* @author Smiley <smiley@chillerlan.net>
|
||||
* @copyright 2015 Smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\{BitBuffer, Mode};
|
||||
use function ceil;
|
||||
use function intdiv;
|
||||
use function preg_match;
|
||||
use function strpos;
|
||||
|
||||
/**
|
||||
* Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / :
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 8.3.3
|
||||
* ISO/IEC 18004:2000 Section 8.4.3
|
||||
*/
|
||||
final class AlphaNum extends QRDataModeAbstract{
|
||||
|
||||
/**
|
||||
* ISO/IEC 18004:2000 Table 5
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const CHAR_MAP = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public const DATAMODE = Mode::ALPHANUM;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLengthInBits():int{
|
||||
return (int)ceil($this->getCharCount() * (11 / 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function validateString(string $string):bool{
|
||||
return (bool)preg_match('/^[A-Z\d %$*+-.:\/]+$/', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
|
||||
$len = $this->getCharCount();
|
||||
|
||||
$bitBuffer
|
||||
->put(self::DATAMODE, 4)
|
||||
->put($len, $this::getLengthBits($versionNumber))
|
||||
;
|
||||
|
||||
// encode 2 characters in 11 bits
|
||||
for($i = 0; ($i + 1) < $len; $i += 2){
|
||||
$bitBuffer->put(
|
||||
($this->ord($this->data[$i]) * 45 + $this->ord($this->data[($i + 1)])),
|
||||
11,
|
||||
);
|
||||
}
|
||||
|
||||
// encode a remaining character in 6 bits
|
||||
if($i < $len){
|
||||
$bitBuffer->put($this->ord($this->data[$i]), 6);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
|
||||
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
|
||||
$result = '';
|
||||
// Read two characters at a time
|
||||
while($length > 1){
|
||||
|
||||
if($bitBuffer->available() < 11){
|
||||
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$nextTwoCharsBits = $bitBuffer->read(11);
|
||||
$result .= self::chr(intdiv($nextTwoCharsBits, 45));
|
||||
$result .= self::chr($nextTwoCharsBits % 45);
|
||||
$length -= 2;
|
||||
}
|
||||
|
||||
if($length === 1){
|
||||
// special case: one character left
|
||||
if($bitBuffer->available() < 6){
|
||||
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$result .= self::chr($bitBuffer->read(6));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
private function ord(string $chr):int{
|
||||
/** @phan-suppress-next-line PhanParamSuspiciousOrder */
|
||||
$ord = strpos(self::CHAR_MAP, $chr);
|
||||
|
||||
if($ord === false){
|
||||
throw new QRCodeDataException('invalid character'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return $ord;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
private static function chr(int $ord):string{
|
||||
|
||||
if($ord < 0 || $ord > 44){
|
||||
throw new QRCodeDataException('invalid character code'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return self::CHAR_MAP[$ord];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Byte
|
||||
*
|
||||
* @created 25.11.2015
|
||||
* @author Smiley <smiley@chillerlan.net>
|
||||
* @copyright 2015 Smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\{BitBuffer, Mode};
|
||||
use function chr;
|
||||
use function ord;
|
||||
|
||||
/**
|
||||
* 8-bit Byte mode, ISO-8859-1 or UTF-8
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 8.3.4
|
||||
* ISO/IEC 18004:2000 Section 8.4.4
|
||||
*/
|
||||
final class Byte extends QRDataModeAbstract{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public const DATAMODE = Mode::BYTE;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLengthInBits():int{
|
||||
return ($this->getCharCount() * 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function validateString(string $string):bool{
|
||||
return $string !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
|
||||
$len = $this->getCharCount();
|
||||
|
||||
$bitBuffer
|
||||
->put(self::DATAMODE, 4)
|
||||
->put($len, $this::getLengthBits($versionNumber))
|
||||
;
|
||||
|
||||
$i = 0;
|
||||
|
||||
while($i < $len){
|
||||
$bitBuffer->put(ord($this->data[$i]), 8);
|
||||
$i++;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
|
||||
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
|
||||
|
||||
if($bitBuffer->available() < (8 * $length)){
|
||||
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$readBytes = '';
|
||||
|
||||
for($i = 0; $i < $length; $i++){
|
||||
$readBytes .= chr($bitBuffer->read(8));
|
||||
}
|
||||
|
||||
return $readBytes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ECI
|
||||
*
|
||||
* @created 20.11.2020
|
||||
* @author smiley <smiley@chillerlan.net>
|
||||
* @copyright 2020 smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode};
|
||||
use function mb_convert_encoding;
|
||||
use function mb_detect_encoding;
|
||||
use function mb_internal_encoding;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Adds an ECI Designator
|
||||
*
|
||||
* ISO/IEC 18004:2000 8.4.1.1
|
||||
*
|
||||
* Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment()
|
||||
*/
|
||||
final class ECI extends QRDataModeAbstract{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public const DATAMODE = Mode::ECI;
|
||||
|
||||
/**
|
||||
* The current ECI encoding id
|
||||
*/
|
||||
private int $encoding;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
* @noinspection PhpMissingParentConstructorInspection
|
||||
*/
|
||||
public function __construct(int $encoding){
|
||||
|
||||
if($encoding < 0 || $encoding > 999999){
|
||||
throw new QRCodeDataException(sprintf('invalid encoding id: "%s"', $encoding));
|
||||
}
|
||||
|
||||
$this->encoding = $encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLengthInBits():int{
|
||||
|
||||
if($this->encoding < 128){
|
||||
return 8;
|
||||
}
|
||||
|
||||
if($this->encoding < 16384){
|
||||
return 16;
|
||||
}
|
||||
|
||||
return 24;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an ECI designator to the bitbuffer
|
||||
*
|
||||
* @inheritDoc
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
|
||||
$bitBuffer->put(self::DATAMODE, 4);
|
||||
|
||||
if($this->encoding < 128){
|
||||
$bitBuffer->put($this->encoding, 8);
|
||||
}
|
||||
elseif($this->encoding < 16384){
|
||||
$bitBuffer->put(($this->encoding | 0x8000), 16);
|
||||
}
|
||||
elseif($this->encoding < 1000000){
|
||||
$bitBuffer->put(($this->encoding | 0xC00000), 24);
|
||||
}
|
||||
else{
|
||||
throw new QRCodeDataException('invalid ECI ID');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses the value of an ECI designator
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function parseValue(BitBuffer $bitBuffer):ECICharset{
|
||||
$firstByte = $bitBuffer->read(8);
|
||||
|
||||
// just one byte
|
||||
if(($firstByte & 0b10000000) === 0){
|
||||
$id = ($firstByte & 0b01111111);
|
||||
}
|
||||
// two bytes
|
||||
elseif(($firstByte & 0b11000000) === 0b10000000){
|
||||
$id = ((($firstByte & 0b00111111) << 8) | $bitBuffer->read(8));
|
||||
}
|
||||
// three bytes
|
||||
elseif(($firstByte & 0b11100000) === 0b11000000){
|
||||
$id = ((($firstByte & 0b00011111) << 16) | $bitBuffer->read(16));
|
||||
}
|
||||
else{
|
||||
throw new QRCodeDataException(sprintf('error decoding ECI value first byte: %08b', $firstByte));// @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return new ECICharset($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore Unused, but required as per interface
|
||||
*/
|
||||
public static function validateString(string $string):bool{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and decodes the ECI designator including the following byte sequence
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
|
||||
$eciCharset = self::parseValue($bitBuffer);
|
||||
$nextMode = $bitBuffer->read(4);
|
||||
$data = self::decodeModeSegment($nextMode, $bitBuffer, $versionNumber);
|
||||
$encoding = $eciCharset->getName();
|
||||
|
||||
if($encoding === null){
|
||||
// The spec isn't clear on this mode; see
|
||||
// section 6.4.5: it does not say which encoding to assuming
|
||||
// upon decoding. I have seen ISO-8859-1 used as well as
|
||||
// Shift_JIS -- without anything like an ECI designator to
|
||||
// give a hint.
|
||||
$encoding = mb_detect_encoding($data, ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8'], true);
|
||||
|
||||
if($encoding === false){
|
||||
throw new QRCodeDataException('could not determine encoding in ECI mode'); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
|
||||
return mb_convert_encoding($data, mb_internal_encoding(), $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
private static function decodeModeSegment(int $mode, BitBuffer $bitBuffer, int $versionNumber):string{
|
||||
|
||||
switch(true){
|
||||
case $mode === Mode::NUMBER: return Number::decodeSegment($bitBuffer, $versionNumber);
|
||||
case $mode === Mode::ALPHANUM: return AlphaNum::decodeSegment($bitBuffer, $versionNumber);
|
||||
case $mode === Mode::BYTE: return Byte::decodeSegment($bitBuffer, $versionNumber);
|
||||
}
|
||||
|
||||
throw new QRCodeDataException(sprintf('ECI designator followed by invalid mode: "%04b"', $mode));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Hanzi
|
||||
*
|
||||
* @created 19.11.2020
|
||||
* @author smiley <smiley@chillerlan.net>
|
||||
* @copyright 2020 smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\{BitBuffer, Mode};
|
||||
use Throwable;
|
||||
use function chr;
|
||||
use function implode;
|
||||
use function intdiv;
|
||||
use function is_string;
|
||||
use function mb_convert_encoding;
|
||||
use function mb_detect_encoding;
|
||||
use function mb_detect_order;
|
||||
use function mb_internal_encoding;
|
||||
use function mb_strlen;
|
||||
use function ord;
|
||||
use function sprintf;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Hanzi (simplified Chinese) mode, GBT18284-2000: 13-bit double-byte characters from the GB2312/GB18030 character set
|
||||
*
|
||||
* Please note that this is not part of the QR Code specification and may not be supported by all readers (ZXing-based ones do).
|
||||
*
|
||||
* @see https://en.wikipedia.org/wiki/GB_2312
|
||||
* @see http://www.herongyang.com/GB2312/Introduction-of-GB2312.html
|
||||
* @see https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding
|
||||
* @see https://gist.github.com/codemasher/91da33c44bfb48a81a6c1426bb8e4338
|
||||
* @see https://github.com/zxing/zxing/blob/dfb06fa33b17a9e68321be151c22846c7b78048f/core/src/main/java/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java#L172-L209
|
||||
* @see https://www.chinesestandard.net/PDF/English.aspx/GBT18284-2000
|
||||
*/
|
||||
final class Hanzi extends QRDataModeAbstract{
|
||||
|
||||
/**
|
||||
* possible values: GB2312, GB18030
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ENCODING = 'GB18030';
|
||||
|
||||
/**
|
||||
* @todo: other subsets???
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const GB2312_SUBSET = 0b0001;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public const DATAMODE = Mode::HANZI;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getCharCount():int{
|
||||
return mb_strlen($this->data, self::ENCODING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLengthInBits():int{
|
||||
return ($this->getCharCount() * 13);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function convertEncoding(string $string):string{
|
||||
mb_detect_order([mb_internal_encoding(), 'UTF-8', 'GB2312', 'GB18030', 'CP936', 'EUC-CN', 'HZ']);
|
||||
|
||||
$detected = mb_detect_encoding($string, null, true);
|
||||
|
||||
if($detected === false){
|
||||
throw new QRCodeDataException('mb_detect_encoding error');
|
||||
}
|
||||
|
||||
if($detected === self::ENCODING){
|
||||
return $string;
|
||||
}
|
||||
|
||||
$string = mb_convert_encoding($string, self::ENCODING, $detected);
|
||||
|
||||
if(!is_string($string)){
|
||||
throw new QRCodeDataException('mb_convert_encoding error');
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if a string qualifies as Hanzi/GB2312
|
||||
*/
|
||||
public static function validateString(string $string):bool{
|
||||
|
||||
try{
|
||||
$string = self::convertEncoding($string);
|
||||
}
|
||||
catch(Throwable $e){
|
||||
return false;
|
||||
}
|
||||
|
||||
$len = strlen($string);
|
||||
|
||||
if($len < 2 || ($len % 2) !== 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
for($i = 0; $i < $len; $i += 2){
|
||||
$byte1 = ord($string[$i]);
|
||||
$byte2 = ord($string[($i + 1)]);
|
||||
|
||||
// byte 1 unused ranges
|
||||
if($byte1 < 0xa1 || ($byte1 > 0xa9 && $byte1 < 0xb0) || $byte1 > 0xf7){
|
||||
return false;
|
||||
}
|
||||
|
||||
// byte 2 unused ranges
|
||||
if($byte2 < 0xa1 || $byte2 > 0xfe){
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
|
||||
*/
|
||||
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
|
||||
|
||||
$bitBuffer
|
||||
->put(self::DATAMODE, 4)
|
||||
->put($this::GB2312_SUBSET, 4)
|
||||
->put($this->getCharCount(), $this::getLengthBits($versionNumber))
|
||||
;
|
||||
|
||||
$len = strlen($this->data);
|
||||
|
||||
for($i = 0; ($i + 1) < $len; $i += 2){
|
||||
$c = (((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[($i + 1)])));
|
||||
|
||||
if($c >= 0xa1a1 && $c <= 0xaafe){
|
||||
$c -= 0x0a1a1;
|
||||
}
|
||||
elseif($c >= 0xb0a1 && $c <= 0xfafe){
|
||||
$c -= 0x0a6a1;
|
||||
}
|
||||
else{
|
||||
throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c));
|
||||
}
|
||||
|
||||
$bitBuffer->put((((($c >> 8) & 0xff) * 0x060) + ($c & 0xff)), 13);
|
||||
}
|
||||
|
||||
if($i < $len){
|
||||
throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1)));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* See specification GBT 18284-2000
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
|
||||
|
||||
// Hanzi mode contains a subset indicator right after mode indicator
|
||||
if($bitBuffer->read(4) !== self::GB2312_SUBSET){
|
||||
throw new QRCodeDataException('ecpected subset indicator for Hanzi mode');
|
||||
}
|
||||
|
||||
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
|
||||
|
||||
if($bitBuffer->available() < ($length * 13)){
|
||||
throw new QRCodeDataException('not enough bits available');
|
||||
}
|
||||
|
||||
// Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as GB2312 afterwards
|
||||
$buffer = [];
|
||||
$offset = 0;
|
||||
|
||||
while($length > 0){
|
||||
// Each 13 bits encodes a 2-byte character
|
||||
$twoBytes = $bitBuffer->read(13);
|
||||
$assembledTwoBytes = ((intdiv($twoBytes, 0x060) << 8) | ($twoBytes % 0x060));
|
||||
|
||||
$assembledTwoBytes += ($assembledTwoBytes < 0x00a00) // 0x003BF
|
||||
? 0x0a1a1 // In the 0xA1A1 to 0xAAFE range
|
||||
: 0x0a6a1; // In the 0xB0A1 to 0xFAFE range
|
||||
|
||||
$buffer[$offset] = chr(0xff & ($assembledTwoBytes >> 8));
|
||||
$buffer[($offset + 1)] = chr(0xff & $assembledTwoBytes);
|
||||
$offset += 2;
|
||||
$length--;
|
||||
}
|
||||
|
||||
return mb_convert_encoding(implode('', $buffer), mb_internal_encoding(), self::ENCODING);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Kanji
|
||||
*
|
||||
* @created 25.11.2015
|
||||
* @author Smiley <smiley@chillerlan.net>
|
||||
* @copyright 2015 Smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\{BitBuffer, Mode};
|
||||
use Throwable;
|
||||
use function chr;
|
||||
use function implode;
|
||||
use function intdiv;
|
||||
use function is_string;
|
||||
use function mb_convert_encoding;
|
||||
use function mb_detect_encoding;
|
||||
use function mb_detect_order;
|
||||
use function mb_internal_encoding;
|
||||
use function mb_strlen;
|
||||
use function ord;
|
||||
use function sprintf;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Kanji mode: 13-bit double-byte characters from the Shift-JIS character set
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 8.3.5
|
||||
* ISO/IEC 18004:2000 Section 8.4.5
|
||||
*
|
||||
* @see https://en.wikipedia.org/wiki/Shift_JIS#As_defined_in_JIS_X_0208:1997
|
||||
* @see http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml
|
||||
* @see https://gist.github.com/codemasher/d07d3e6e9346c08e7a41b8b978784952
|
||||
*/
|
||||
final class Kanji extends QRDataModeAbstract{
|
||||
|
||||
/**
|
||||
* possible values: SJIS, SJIS-2004
|
||||
*
|
||||
* SJIS-2004 may produce errors in PHP < 8
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ENCODING = 'SJIS';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public const DATAMODE = Mode::KANJI;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getCharCount():int{
|
||||
return mb_strlen($this->data, self::ENCODING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLengthInBits():int{
|
||||
return ($this->getCharCount() * 13);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function convertEncoding(string $string):string{
|
||||
mb_detect_order([mb_internal_encoding(), 'UTF-8', 'SJIS', 'SJIS-2004']);
|
||||
|
||||
$detected = mb_detect_encoding($string, null, true);
|
||||
|
||||
if($detected === false){
|
||||
throw new QRCodeDataException('mb_detect_encoding error');
|
||||
}
|
||||
|
||||
if($detected === self::ENCODING){
|
||||
return $string;
|
||||
}
|
||||
|
||||
$string = mb_convert_encoding($string, self::ENCODING, $detected);
|
||||
|
||||
if(!is_string($string)){
|
||||
throw new QRCodeDataException(sprintf('invalid encoding: %s', $detected));
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if a string qualifies as SJIS Kanji
|
||||
*/
|
||||
public static function validateString(string $string):bool{
|
||||
|
||||
try{
|
||||
$string = self::convertEncoding($string);
|
||||
}
|
||||
catch(Throwable $e){
|
||||
return false;
|
||||
}
|
||||
|
||||
$len = strlen($string);
|
||||
|
||||
if($len < 2 || ($len % 2) !== 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
for($i = 0; $i < $len; $i += 2){
|
||||
$byte1 = ord($string[$i]);
|
||||
$byte2 = ord($string[($i + 1)]);
|
||||
|
||||
// byte 1 unused and vendor ranges
|
||||
if($byte1 < 0x81 || ($byte1 > 0x84 && $byte1 < 0x88) || ($byte1 > 0x9f && $byte1 < 0xe0) || $byte1 > 0xea){
|
||||
return false;
|
||||
}
|
||||
|
||||
// byte 2 unused ranges
|
||||
if($byte2 < 0x40 || $byte2 === 0x7f || $byte2 > 0xfc){
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
|
||||
*/
|
||||
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
|
||||
|
||||
$bitBuffer
|
||||
->put(self::DATAMODE, 4)
|
||||
->put($this->getCharCount(), $this::getLengthBits($versionNumber))
|
||||
;
|
||||
|
||||
$len = strlen($this->data);
|
||||
|
||||
for($i = 0; ($i + 1) < $len; $i += 2){
|
||||
$c = (((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[($i + 1)])));
|
||||
|
||||
if($c >= 0x8140 && $c <= 0x9ffc){
|
||||
$c -= 0x8140;
|
||||
}
|
||||
elseif($c >= 0xe040 && $c <= 0xebbf){
|
||||
$c -= 0xc140;
|
||||
}
|
||||
else{
|
||||
throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c));
|
||||
}
|
||||
|
||||
$bitBuffer->put((((($c >> 8) & 0xff) * 0xc0) + ($c & 0xff)), 13);
|
||||
}
|
||||
|
||||
if($i < $len){
|
||||
throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1)));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
|
||||
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
|
||||
|
||||
if($bitBuffer->available() < ($length * 13)){
|
||||
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as SJIS afterwards
|
||||
$buffer = [];
|
||||
$offset = 0;
|
||||
|
||||
while($length > 0){
|
||||
// Each 13 bits encodes a 2-byte character
|
||||
$twoBytes = $bitBuffer->read(13);
|
||||
$assembledTwoBytes = ((intdiv($twoBytes, 0x0c0) << 8) | ($twoBytes % 0x0c0));
|
||||
|
||||
$assembledTwoBytes += ($assembledTwoBytes < 0x01f00)
|
||||
? 0x08140 // In the 0x8140 to 0x9FFC range
|
||||
: 0x0c140; // In the 0xE040 to 0xEBBF range
|
||||
|
||||
$buffer[$offset] = chr(0xff & ($assembledTwoBytes >> 8));
|
||||
$buffer[($offset + 1)] = chr(0xff & $assembledTwoBytes);
|
||||
$offset += 2;
|
||||
$length--;
|
||||
}
|
||||
|
||||
return mb_convert_encoding(implode('', $buffer), mb_internal_encoding(), self::ENCODING);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Number
|
||||
*
|
||||
* @created 26.11.2015
|
||||
* @author Smiley <smiley@chillerlan.net>
|
||||
* @copyright 2015 Smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\{BitBuffer, Mode};
|
||||
use function ceil;
|
||||
use function intdiv;
|
||||
use function substr;
|
||||
use function unpack;
|
||||
|
||||
/**
|
||||
* Numeric mode: decimal digits 0 to 9
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 8.3.2
|
||||
* ISO/IEC 18004:2000 Section 8.4.2
|
||||
*/
|
||||
final class Number extends QRDataModeAbstract{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public const DATAMODE = Mode::NUMBER;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLengthInBits():int{
|
||||
return (int)ceil($this->getCharCount() * (10 / 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function validateString(string $string):bool{
|
||||
return (bool)preg_match('/^\d+$/', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
|
||||
$len = $this->getCharCount();
|
||||
|
||||
$bitBuffer
|
||||
->put(self::DATAMODE, 4)
|
||||
->put($len, $this::getLengthBits($versionNumber))
|
||||
;
|
||||
|
||||
$i = 0;
|
||||
|
||||
// encode numeric triplets in 10 bits
|
||||
while(($i + 2) < $len){
|
||||
$bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10);
|
||||
$i += 3;
|
||||
}
|
||||
|
||||
if($i < $len){
|
||||
|
||||
// encode 2 remaining numbers in 7 bits
|
||||
if(($len - $i) === 2){
|
||||
$bitBuffer->put($this->parseInt(substr($this->data, $i, 2)), 7);
|
||||
}
|
||||
// encode one remaining number in 4 bits
|
||||
elseif(($len - $i) === 1){
|
||||
$bitBuffer->put($this->parseInt(substr($this->data, $i, 1)), 4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the code for the given numeric string
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
private function parseInt(string $string):int{
|
||||
$num = 0;
|
||||
|
||||
$ords = unpack('C*', $string);
|
||||
|
||||
if($ords === false){
|
||||
throw new QRCodeDataException('unpack() error');
|
||||
}
|
||||
|
||||
foreach($ords as $ord){
|
||||
$num = ($num * 10 + $ord - 48);
|
||||
}
|
||||
|
||||
return $num;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
|
||||
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
|
||||
$result = '';
|
||||
// Read three digits at a time
|
||||
while($length >= 3){
|
||||
// Each 10 bits encodes three digits
|
||||
if($bitBuffer->available() < 10){
|
||||
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$threeDigitsBits = $bitBuffer->read(10);
|
||||
|
||||
if($threeDigitsBits >= 1000){
|
||||
throw new QRCodeDataException('error decoding numeric value');
|
||||
}
|
||||
|
||||
$result .= intdiv($threeDigitsBits, 100);
|
||||
$result .= (intdiv($threeDigitsBits, 10) % 10);
|
||||
$result .= ($threeDigitsBits % 10);
|
||||
|
||||
$length -= 3;
|
||||
}
|
||||
|
||||
if($length === 2){
|
||||
// Two digits left over to read, encoded in 7 bits
|
||||
if($bitBuffer->available() < 7){
|
||||
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$twoDigitsBits = $bitBuffer->read(7);
|
||||
|
||||
if($twoDigitsBits >= 100){
|
||||
throw new QRCodeDataException('error decoding numeric value');
|
||||
}
|
||||
|
||||
$result .= intdiv($twoDigitsBits, 10);
|
||||
$result .= ($twoDigitsBits % 10);
|
||||
}
|
||||
elseif($length === 1){
|
||||
// One digit left over to read
|
||||
if($bitBuffer->available() < 4){
|
||||
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$digitBits = $bitBuffer->read(4);
|
||||
|
||||
if($digitBits >= 10){
|
||||
throw new QRCodeDataException('error decoding numeric value');
|
||||
}
|
||||
|
||||
$result .= $digitBits;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Class QRCodeDataException
|
||||
*
|
||||
* @created 09.12.2015
|
||||
* @author Smiley <smiley@chillerlan.net>
|
||||
* @copyright 2015 Smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\QRCodeException;
|
||||
|
||||
/**
|
||||
* An exception container
|
||||
*/
|
||||
final class QRCodeDataException extends QRCodeException{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
/**
|
||||
* Class QRData
|
||||
*
|
||||
* @created 25.11.2015
|
||||
* @author Smiley <smiley@chillerlan.net>
|
||||
* @copyright 2015 Smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, Version};
|
||||
use chillerlan\Settings\SettingsContainerInterface;
|
||||
use function count;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Processes the binary data and maps it on a QRMatrix which is then being returned
|
||||
*/
|
||||
final class QRData{
|
||||
|
||||
/**
|
||||
* the options instance
|
||||
*
|
||||
* @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
|
||||
*/
|
||||
private SettingsContainerInterface $options;
|
||||
|
||||
/**
|
||||
* a BitBuffer instance
|
||||
*/
|
||||
private BitBuffer $bitBuffer;
|
||||
|
||||
/**
|
||||
* an EccLevel instance
|
||||
*/
|
||||
private EccLevel $eccLevel;
|
||||
|
||||
/**
|
||||
* current QR Code version
|
||||
*/
|
||||
private Version $version;
|
||||
|
||||
/**
|
||||
* @var \chillerlan\QRCode\Data\QRDataModeInterface[]
|
||||
*/
|
||||
private array $dataSegments = [];
|
||||
|
||||
/**
|
||||
* Max bits for the current ECC mode
|
||||
*
|
||||
* @var int[]
|
||||
*/
|
||||
private array $maxBitsForEcc;
|
||||
|
||||
/**
|
||||
* QRData constructor.
|
||||
*/
|
||||
public function __construct(SettingsContainerInterface $options, array $dataSegments = []){
|
||||
$this->options = $options;
|
||||
$this->bitBuffer = new BitBuffer;
|
||||
$this->eccLevel = new EccLevel($this->options->eccLevel);
|
||||
$this->maxBitsForEcc = $this->eccLevel->getMaxBits();
|
||||
|
||||
$this->setData($dataSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data string (internally called by the constructor)
|
||||
*
|
||||
* Subsequent calls will overwrite the current state - use the QRCode::add*Segement() method instead
|
||||
*
|
||||
* @param \chillerlan\QRCode\Data\QRDataModeInterface[] $dataSegments
|
||||
*/
|
||||
public function setData(array $dataSegments):self{
|
||||
$this->dataSegments = $dataSegments;
|
||||
$this->version = $this->getMinimumVersion();
|
||||
|
||||
$this->bitBuffer->clear();
|
||||
$this->writeBitBuffer();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BitBuffer instance
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getBitBuffer():BitBuffer{
|
||||
return $this->bitBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a BitBuffer object
|
||||
*
|
||||
* This can be used instead of setData(), however, the version auto-detection is not available in this case.
|
||||
* The version needs to match the length bits range for the data mode the data has been encoded with,
|
||||
* additionally the bit array needs to contain enough pad bits.
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public function setBitBuffer(BitBuffer $bitBuffer):self{
|
||||
|
||||
if($this->options->version === Version::AUTO){
|
||||
throw new QRCodeDataException('version auto detection is not available');
|
||||
}
|
||||
|
||||
if($bitBuffer->getLength() === 0){
|
||||
throw new QRCodeDataException('the given BitBuffer is empty');
|
||||
}
|
||||
|
||||
$this->dataSegments = [];
|
||||
$this->bitBuffer = $bitBuffer;
|
||||
$this->version = new Version($this->options->version);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a fresh matrix object with the data written and masked with the given $maskPattern
|
||||
*/
|
||||
public function writeMatrix():QRMatrix{
|
||||
return (new QRMatrix($this->version, $this->eccLevel))
|
||||
->initFunctionalPatterns()
|
||||
->writeCodewords($this->bitBuffer)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* estimates the total length of the several mode segments in order to guess the minimum version
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public function estimateTotalBitLength():int{
|
||||
$length = 0;
|
||||
|
||||
foreach($this->dataSegments as $segment){
|
||||
// data length of the current segment
|
||||
$length += $segment->getLengthInBits();
|
||||
// +4 bits for the mode descriptor
|
||||
$length += 4;
|
||||
// Hanzi mode sets an additional 4 bit long subset identifier
|
||||
if($segment instanceof Hanzi){
|
||||
$length += 4;
|
||||
}
|
||||
}
|
||||
|
||||
$provisionalVersion = null;
|
||||
|
||||
foreach($this->maxBitsForEcc as $version => $maxBits){
|
||||
|
||||
if($length <= $maxBits){
|
||||
$provisionalVersion = $version;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if($provisionalVersion !== null){
|
||||
|
||||
// add character count indicator bits for the provisional version
|
||||
foreach($this->dataSegments as $segment){
|
||||
$length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion);
|
||||
}
|
||||
|
||||
// it seems that in some cases the estimated total length is not 100% accurate,
|
||||
// so we substract 4 bits from the total when not in mixed mode
|
||||
if(count($this->dataSegments) <= 1){
|
||||
$length -= 4;
|
||||
}
|
||||
|
||||
// we've got a match!
|
||||
// or let's see if there's a higher version number available
|
||||
if($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])){
|
||||
return $length;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the minimum version number for the given string
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public function getMinimumVersion():Version{
|
||||
|
||||
if($this->options->version !== Version::AUTO){
|
||||
return new Version($this->options->version);
|
||||
}
|
||||
|
||||
$total = $this->estimateTotalBitLength();
|
||||
|
||||
// guess the version number within the given range
|
||||
for($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++){
|
||||
if($total <= ($this->maxBitsForEcc[$version] - 4)){
|
||||
return new Version($version);
|
||||
}
|
||||
}
|
||||
|
||||
// it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
|
||||
throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a BitBuffer and writes the string data to it
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException on data overflow
|
||||
*/
|
||||
private function writeBitBuffer():void{
|
||||
$MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version);
|
||||
|
||||
foreach($this->dataSegments as $segment){
|
||||
$segment->write($this->bitBuffer, $this->version->getVersionNumber());
|
||||
}
|
||||
|
||||
// overflow, likely caused due to invalid version setting
|
||||
if($this->bitBuffer->getLength() > $MAX_BITS){
|
||||
throw new QRCodeDataException(
|
||||
sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
|
||||
);
|
||||
}
|
||||
|
||||
// add terminator (ISO/IEC 18004:2000 Table 2)
|
||||
if(($this->bitBuffer->getLength() + 4) <= $MAX_BITS){
|
||||
$this->bitBuffer->put(Mode::TERMINATOR, 4);
|
||||
}
|
||||
|
||||
// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
|
||||
|
||||
// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
|
||||
// by the addition of padding bits with binary value 0
|
||||
while(($this->bitBuffer->getLength() % 8) !== 0){
|
||||
|
||||
if($this->bitBuffer->getLength() === $MAX_BITS){
|
||||
break;
|
||||
}
|
||||
|
||||
$this->bitBuffer->putBit(false);
|
||||
}
|
||||
|
||||
// The message bit stream shall then be extended to fill the data capacity of the symbol
|
||||
// corresponding to the Version and Error Correction Level, by the addition of the Pad
|
||||
// Codewords 11101100 and 00010001 alternately.
|
||||
$alternate = false;
|
||||
|
||||
while(($this->bitBuffer->getLength() + 8) <= $MAX_BITS){
|
||||
$this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8);
|
||||
|
||||
$alternate = !$alternate;
|
||||
}
|
||||
|
||||
// In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
|
||||
// to the end of the message in order exactly to fill the symbol capacity
|
||||
while($this->bitBuffer->getLength() <= $MAX_BITS){
|
||||
$this->bitBuffer->putBit(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* Class QRDataModeAbstract
|
||||
*
|
||||
* @created 19.11.2020
|
||||
* @author smiley <smiley@chillerlan.net>
|
||||
* @copyright 2020 smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\Mode;
|
||||
|
||||
/**
|
||||
* abstract methods for the several data modes
|
||||
*/
|
||||
abstract class QRDataModeAbstract implements QRDataModeInterface{
|
||||
|
||||
/**
|
||||
* The data to write
|
||||
*/
|
||||
protected string $data;
|
||||
|
||||
/**
|
||||
* QRDataModeAbstract constructor.
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public function __construct(string $data){
|
||||
$data = $this::convertEncoding($data);
|
||||
|
||||
if(!$this::validateString($data)){
|
||||
throw new QRCodeDataException('invalid data');
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the character count of the $data string
|
||||
*/
|
||||
protected function getCharCount():int{
|
||||
return strlen($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function convertEncoding(string $string):string{
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* shortcut
|
||||
*/
|
||||
protected static function getLengthBits(int $versionNumber):int{
|
||||
return Mode::getLengthBitsForVersion(static::DATAMODE, $versionNumber);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface QRDataModeInterface
|
||||
*
|
||||
* @created 01.12.2015
|
||||
* @author Smiley <smiley@chillerlan.net>
|
||||
* @copyright 2015 Smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\BitBuffer;
|
||||
|
||||
/**
|
||||
* Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji)
|
||||
*/
|
||||
interface QRDataModeInterface{
|
||||
|
||||
/**
|
||||
* the current data mode: Number, Alphanum, Kanji, Hanzi, Byte, ECI
|
||||
*
|
||||
* tbh I hate this constant here, but it's part of the interface, so I can't just declare it in the abstract class.
|
||||
* (phan will complain about a PhanAccessOverridesFinalConstant)
|
||||
*
|
||||
* @see https://wiki.php.net/rfc/final_class_const
|
||||
*
|
||||
* @var int
|
||||
* @see \chillerlan\QRCode\Common\Mode
|
||||
* @internal do not call this constant from the interface, but rather from one of the child classes
|
||||
*/
|
||||
public const DATAMODE = -1;
|
||||
|
||||
/**
|
||||
* retruns the length in bits of the data string
|
||||
*/
|
||||
public function getLengthInBits():int;
|
||||
|
||||
/**
|
||||
* encoding conversion helper
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public static function convertEncoding(string $string):string;
|
||||
|
||||
/**
|
||||
* checks if the given string qualifies for the encoder module
|
||||
*/
|
||||
public static function validateString(string $string):bool;
|
||||
|
||||
/**
|
||||
* writes the actual data string to the BitBuffer, uses the given version to determine the length bits
|
||||
*
|
||||
* @see \chillerlan\QRCode\Data\QRData::writeBitBuffer()
|
||||
*/
|
||||
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface;
|
||||
|
||||
/**
|
||||
* reads a segment from the BitBuffer and decodes in the current data mode
|
||||
*/
|
||||
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,817 @@
|
||||
<?php
|
||||
/**
|
||||
* Class QRMatrix
|
||||
*
|
||||
* @created 15.11.2017
|
||||
* @author Smiley <smiley@chillerlan.net>
|
||||
* @copyright 2017 Smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Version};
|
||||
use function array_fill;
|
||||
use function array_map;
|
||||
use function array_reverse;
|
||||
use function count;
|
||||
use function intdiv;
|
||||
|
||||
/**
|
||||
* Holds an array representation of the final QR Code that contains numerical values for later output modifications;
|
||||
* maps the ECC coded binary data and applies the mask pattern
|
||||
*
|
||||
* @see http://www.thonky.com/qr-code-tutorial/format-version-information
|
||||
*/
|
||||
class QRMatrix{
|
||||
|
||||
/*
|
||||
* special values
|
||||
*/
|
||||
|
||||
/** @var int */
|
||||
public const IS_DARK = 0b100000000000;
|
||||
/** @var int */
|
||||
public const M_NULL = 0b000000000000;
|
||||
/** @var int */
|
||||
public const M_LOGO = 0b001000000000;
|
||||
/** @var int */
|
||||
public const M_LOGO_DARK = 0b101000000000;
|
||||
|
||||
/*
|
||||
* light values
|
||||
*/
|
||||
|
||||
/** @var int */
|
||||
public const M_DATA = 0b000000000010;
|
||||
/** @var int */
|
||||
public const M_FINDER = 0b000000000100;
|
||||
/** @var int */
|
||||
public const M_SEPARATOR = 0b000000001000;
|
||||
/** @var int */
|
||||
public const M_ALIGNMENT = 0b000000010000;
|
||||
/** @var int */
|
||||
public const M_TIMING = 0b000000100000;
|
||||
/** @var int */
|
||||
public const M_FORMAT = 0b000001000000;
|
||||
/** @var int */
|
||||
public const M_VERSION = 0b000010000000;
|
||||
/** @var int */
|
||||
public const M_QUIETZONE = 0b000100000000;
|
||||
|
||||
/*
|
||||
* dark values
|
||||
*/
|
||||
|
||||
/** @var int */
|
||||
public const M_DARKMODULE = 0b100000000001;
|
||||
/** @var int */
|
||||
public const M_DATA_DARK = 0b100000000010;
|
||||
/** @var int */
|
||||
public const M_FINDER_DARK = 0b100000000100;
|
||||
/** @var int */
|
||||
public const M_ALIGNMENT_DARK = 0b100000010000;
|
||||
/** @var int */
|
||||
public const M_TIMING_DARK = 0b100000100000;
|
||||
/** @var int */
|
||||
public const M_FORMAT_DARK = 0b100001000000;
|
||||
/** @var int */
|
||||
public const M_VERSION_DARK = 0b100010000000;
|
||||
/** @var int */
|
||||
public const M_FINDER_DOT = 0b110000000000;
|
||||
|
||||
/*
|
||||
* values used for reversed reflectance
|
||||
*/
|
||||
|
||||
/** @var int */
|
||||
public const M_DARKMODULE_LIGHT = 0b000000000001;
|
||||
/** @var int */
|
||||
public const M_FINDER_DOT_LIGHT = 0b010000000000;
|
||||
/** @var int */
|
||||
public const M_SEPARATOR_DARK = 0b100000001000;
|
||||
/** @var int */
|
||||
public const M_QUIETZONE_DARK = 0b100100000000;
|
||||
|
||||
/**
|
||||
* Map of flag => coord
|
||||
*
|
||||
* @see \chillerlan\QRCode\Data\QRMatrix::checkNeighbours()
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected const neighbours = [
|
||||
0b00000001 => [-1, -1],
|
||||
0b00000010 => [ 0, -1],
|
||||
0b00000100 => [ 1, -1],
|
||||
0b00001000 => [ 1, 0],
|
||||
0b00010000 => [ 1, 1],
|
||||
0b00100000 => [ 0, 1],
|
||||
0b01000000 => [-1, 1],
|
||||
0b10000000 => [-1, 0],
|
||||
];
|
||||
|
||||
/**
|
||||
* the matrix version - always set in QRMatrix, may be null in BitMatrix
|
||||
*/
|
||||
protected ?Version $version = null;
|
||||
|
||||
/**
|
||||
* the current ECC level - always set in QRMatrix, may be null in BitMatrix
|
||||
*/
|
||||
protected ?EccLevel $eccLevel = null;
|
||||
|
||||
/**
|
||||
* the mask pattern that was used in the most recent operation, set via:
|
||||
*
|
||||
* - QRMatrix::setFormatInfo()
|
||||
* - QRMatrix::mask()
|
||||
* - BitMatrix::readFormatInformation()
|
||||
*/
|
||||
protected ?MaskPattern $maskPattern = null;
|
||||
|
||||
/**
|
||||
* the size (side length) of the matrix, including quiet zone (if created)
|
||||
*/
|
||||
protected int $moduleCount;
|
||||
|
||||
/**
|
||||
* the actual matrix data array
|
||||
*
|
||||
* @var int[][]
|
||||
*/
|
||||
protected array $matrix;
|
||||
|
||||
/**
|
||||
* QRMatrix constructor.
|
||||
*/
|
||||
public function __construct(Version $version, EccLevel $eccLevel){
|
||||
$this->version = $version;
|
||||
$this->eccLevel = $eccLevel;
|
||||
$this->moduleCount = $this->version->getDimension();
|
||||
$this->matrix = $this->createMatrix($this->moduleCount, $this::M_NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a 2-dimensional array (square) of the given $size
|
||||
*/
|
||||
protected function createMatrix(int $size, int $value):array{
|
||||
return array_fill(0, $size, array_fill(0, $size, $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* shortcut to initialize the functional patterns
|
||||
*/
|
||||
public function initFunctionalPatterns():self{
|
||||
return $this
|
||||
->setFinderPattern()
|
||||
->setSeparators()
|
||||
->setAlignmentPattern()
|
||||
->setTimingPattern()
|
||||
->setDarkModule()
|
||||
->setVersionNumber()
|
||||
->setFormatInfo()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data matrix, returns a pure boolean representation if $boolean is set to true
|
||||
*
|
||||
* @return int[][]|bool[][]
|
||||
*/
|
||||
public function getMatrix(?bool $boolean = null):array{
|
||||
|
||||
if($boolean !== true){
|
||||
return $this->matrix;
|
||||
}
|
||||
|
||||
$matrix = $this->matrix;
|
||||
|
||||
foreach($matrix as &$row){
|
||||
$row = array_map([$this, 'isDark'], $row);
|
||||
}
|
||||
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 5.0.0 use QRMatrix::getMatrix() instead
|
||||
* @see \chillerlan\QRCode\Data\QRMatrix::getMatrix()
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function matrix(?bool $boolean = null):array{
|
||||
return $this->getMatrix($boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current version number
|
||||
*/
|
||||
public function getVersion():?Version{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 5.0.0 use QRMatrix::getVersion() instead
|
||||
* @see \chillerlan\QRCode\Data\QRMatrix::getVersion()
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function version():?Version{
|
||||
return $this->getVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current ECC level
|
||||
*/
|
||||
public function getEccLevel():?EccLevel{
|
||||
return $this->eccLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 5.0.0 use QRMatrix::getEccLevel() instead
|
||||
* @see \chillerlan\QRCode\Data\QRMatrix::getEccLevel()
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function eccLevel():?EccLevel{
|
||||
return $this->getEccLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current mask pattern
|
||||
*/
|
||||
public function getMaskPattern():?MaskPattern{
|
||||
return $this->maskPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 5.0.0 use QRMatrix::getMaskPattern() instead
|
||||
* @see \chillerlan\QRCode\Data\QRMatrix::getMaskPattern()
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function maskPattern():?MaskPattern{
|
||||
return $this->getMaskPattern();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absoulute size of the matrix, including quiet zone (after setting it).
|
||||
*
|
||||
* size = version * 4 + 17 [ + 2 * quietzone size]
|
||||
*/
|
||||
public function getSize():int{
|
||||
return $this->moduleCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 5.0.0 use QRMatrix::getSize() instead
|
||||
* @see \chillerlan\QRCode\Data\QRMatrix::getSize()
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function size():int{
|
||||
return $this->getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the module at position [$x, $y] or -1 if the coordinate is outside the matrix
|
||||
*/
|
||||
public function get(int $x, int $y):int{
|
||||
|
||||
if(!isset($this->matrix[$y][$x])){
|
||||
return -1;
|
||||
}
|
||||
|
||||
return $this->matrix[$y][$x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the $M_TYPE value for the module at position [$x, $y]
|
||||
*
|
||||
* true => $M_TYPE | 0x800
|
||||
* false => $M_TYPE
|
||||
*/
|
||||
public function set(int $x, int $y, bool $value, int $M_TYPE):self{
|
||||
|
||||
if(isset($this->matrix[$y][$x])){
|
||||
// we don't know whether the input is dark, so we remove the dark bit
|
||||
$M_TYPE &= ~$this::IS_DARK;
|
||||
|
||||
if($value === true){
|
||||
$M_TYPE |= $this::IS_DARK;
|
||||
}
|
||||
|
||||
$this->matrix[$y][$x] = $M_TYPE;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an area of $width * $height, from the given starting point [$startX, $startY] (top left) with $value for $M_TYPE.
|
||||
*/
|
||||
public function setArea(int $startX, int $startY, int $width, int $height, bool $value, int $M_TYPE):self{
|
||||
|
||||
for($y = $startY; $y < ($startY + $height); $y++){
|
||||
for($x = $startX; $x < ($startX + $width); $x++){
|
||||
$this->set($x, $y, $value, $M_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips the value of the module at ($x, $y)
|
||||
*/
|
||||
public function flip(int $x, int $y):self{
|
||||
|
||||
if(isset($this->matrix[$y][$x])){
|
||||
$this->matrix[$y][$x] ^= $this::IS_DARK;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the module at ($x, $y) is of the given $M_TYPE
|
||||
*
|
||||
* true => $value & $M_TYPE === $M_TYPE
|
||||
*
|
||||
* Also, returns false if the given coordinates are out of range.
|
||||
*/
|
||||
public function checkType(int $x, int $y, int $M_TYPE):bool{
|
||||
|
||||
if(isset($this->matrix[$y][$x])){
|
||||
return ($this->matrix[$y][$x] & $M_TYPE) === $M_TYPE;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the module at ($x, $y) is in the given array of $M_TYPES,
|
||||
* returns true if a match is found, otherwise false.
|
||||
*/
|
||||
public function checkTypeIn(int $x, int $y, array $M_TYPES):bool{
|
||||
|
||||
foreach($M_TYPES as $type){
|
||||
if($this->checkType($x, $y, $type)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the module at ($x, $y) is true (dark) or false (light)
|
||||
*
|
||||
* Also, returns false if the given coordinates are out of range.
|
||||
*/
|
||||
public function check(int $x, int $y):bool{
|
||||
|
||||
if(isset($this->matrix[$y][$x])){
|
||||
return $this->isDark($this->matrix[$y][$x]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given $M_TYPE is a dark value
|
||||
*/
|
||||
public function isDark(int $M_TYPE):bool{
|
||||
return ($M_TYPE & $this::IS_DARK) === $this::IS_DARK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the status of the neighbouring modules for the module at ($x, $y) and returns a bitmask with the results.
|
||||
*
|
||||
* The 8 flags of the bitmask represent the status of each of the neighbouring fields,
|
||||
* starting with the lowest bit for top left, going clockwise:
|
||||
*
|
||||
* 0 1 2
|
||||
* 7 # 3
|
||||
* 6 5 4
|
||||
*/
|
||||
public function checkNeighbours(int $x, int $y, ?int $M_TYPE = null):int{
|
||||
$bits = 0;
|
||||
|
||||
foreach($this::neighbours as $bit => [$ix, $iy]){
|
||||
$ix += $x;
|
||||
$iy += $y;
|
||||
|
||||
// $M_TYPE is given, skip if the field is not the same type
|
||||
if($M_TYPE !== null && !$this->checkType($ix, $iy, $M_TYPE)){
|
||||
continue;
|
||||
}
|
||||
|
||||
if($this->checkType($ix, $iy, $this::IS_DARK)){
|
||||
$bits |= $bit;
|
||||
}
|
||||
}
|
||||
|
||||
return $bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder
|
||||
*
|
||||
* 4 * version + 9 or moduleCount - 8
|
||||
*/
|
||||
public function setDarkModule():self{
|
||||
$this->set(8, ($this->moduleCount - 8), true, $this::M_DARKMODULE);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the 7x7 finder patterns in the corners top left/right and bottom left
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 7.3.2
|
||||
*/
|
||||
public function setFinderPattern():self{
|
||||
|
||||
$pos = [
|
||||
[0, 0], // top left
|
||||
[($this->moduleCount - 7), 0], // top right
|
||||
[0, ($this->moduleCount - 7)], // bottom left
|
||||
];
|
||||
|
||||
foreach($pos as $c){
|
||||
$this
|
||||
->setArea( $c[0] , $c[1] , 7, 7, true, $this::M_FINDER)
|
||||
->setArea(($c[0] + 1), ($c[1] + 1), 5, 5, false, $this::M_FINDER)
|
||||
->setArea(($c[0] + 2), ($c[1] + 2), 3, 3, true, $this::M_FINDER_DOT)
|
||||
;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the separator lines around the finder patterns
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 7.3.3
|
||||
*/
|
||||
public function setSeparators():self{
|
||||
|
||||
$h = [
|
||||
[7, 0],
|
||||
[($this->moduleCount - 8), 0],
|
||||
[7, ($this->moduleCount - 8)],
|
||||
];
|
||||
|
||||
$v = [
|
||||
[7, 7],
|
||||
[($this->moduleCount - 1), 7],
|
||||
[7, ($this->moduleCount - 8)],
|
||||
];
|
||||
|
||||
for($c = 0; $c < 3; $c++){
|
||||
for($i = 0; $i < 8; $i++){
|
||||
// phpcs:ignore
|
||||
$this->set( $h[$c][0] , ($h[$c][1] + $i), false, $this::M_SEPARATOR);
|
||||
$this->set(($v[$c][0] - $i), $v[$c][1] , false, $this::M_SEPARATOR);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Draws the 5x5 alignment patterns
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 7.3.5
|
||||
*/
|
||||
public function setAlignmentPattern():self{
|
||||
$alignmentPattern = $this->version->getAlignmentPattern();
|
||||
|
||||
foreach($alignmentPattern as $y){
|
||||
foreach($alignmentPattern as $x){
|
||||
|
||||
// skip existing patterns
|
||||
if($this->matrix[$y][$x] !== $this::M_NULL){
|
||||
continue;
|
||||
}
|
||||
|
||||
$this
|
||||
->setArea(($x - 2), ($y - 2), 5, 5, true, $this::M_ALIGNMENT)
|
||||
->setArea(($x - 1), ($y - 1), 3, 3, false, $this::M_ALIGNMENT)
|
||||
->set($x, $y, true, $this::M_ALIGNMENT)
|
||||
;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Draws the timing pattern (h/v checkered line between the finder patterns)
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 7.3.4
|
||||
*/
|
||||
public function setTimingPattern():self{
|
||||
|
||||
for($i = 8; $i < ($this->moduleCount - 8); $i++){
|
||||
|
||||
if($this->matrix[6][$i] !== $this::M_NULL || $this->matrix[$i][6] !== $this::M_NULL){
|
||||
continue;
|
||||
}
|
||||
|
||||
$v = ($i % 2) === 0;
|
||||
|
||||
$this->set($i, 6, $v, $this::M_TIMING); // h
|
||||
$this->set(6, $i, $v, $this::M_TIMING); // v
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the version information, 2x 3x6 pixel
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 8.10
|
||||
*/
|
||||
public function setVersionNumber():self{
|
||||
$bits = $this->version->getVersionPattern();
|
||||
|
||||
if($bits !== null){
|
||||
|
||||
for($i = 0; $i < 18; $i++){
|
||||
$a = intdiv($i, 3);
|
||||
$b = (($i % 3) + ($this->moduleCount - 8 - 3));
|
||||
$v = (($bits >> $i) & 1) === 1;
|
||||
|
||||
$this->set($b, $a, $v, $this::M_VERSION); // ne
|
||||
$this->set($a, $b, $v, $this::M_VERSION); // sw
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the format info along the finder patterns. If no $maskPattern, all format info modules will be set to false.
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 8.9
|
||||
*/
|
||||
public function setFormatInfo(?MaskPattern $maskPattern = null):self{
|
||||
$this->maskPattern = $maskPattern;
|
||||
$bits = 0; // sets all format fields to false (test mode)
|
||||
|
||||
if($this->maskPattern instanceof MaskPattern){
|
||||
$bits = $this->eccLevel->getformatPattern($this->maskPattern);
|
||||
}
|
||||
|
||||
for($i = 0; $i < 15; $i++){
|
||||
$v = (($bits >> $i) & 1) === 1;
|
||||
|
||||
if($i < 6){
|
||||
$this->set(8, $i, $v, $this::M_FORMAT);
|
||||
}
|
||||
elseif($i < 8){
|
||||
$this->set(8, ($i + 1), $v, $this::M_FORMAT);
|
||||
}
|
||||
else{
|
||||
$this->set(8, ($this->moduleCount - 15 + $i), $v, $this::M_FORMAT);
|
||||
}
|
||||
|
||||
if($i < 8){
|
||||
$this->set(($this->moduleCount - $i - 1), 8, $v, $this::M_FORMAT);
|
||||
}
|
||||
elseif($i < 9){
|
||||
$this->set(((15 - $i)), 8, $v, $this::M_FORMAT);
|
||||
}
|
||||
else{
|
||||
$this->set((15 - $i - 1), 8, $v, $this::M_FORMAT);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the "quiet zone" of $size around the matrix
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 7.3.7
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public function setQuietZone(int $quietZoneSize):self{
|
||||
|
||||
// early exit if there's nothing to add
|
||||
if($quietZoneSize < 1){
|
||||
return $this;
|
||||
}
|
||||
|
||||
if($this->matrix[($this->moduleCount - 1)][($this->moduleCount - 1)] === $this::M_NULL){
|
||||
throw new QRCodeDataException('use only after writing data');
|
||||
}
|
||||
|
||||
// create a matrix with the new size
|
||||
$newSize = ($this->moduleCount + ($quietZoneSize * 2));
|
||||
$newMatrix = $this->createMatrix($newSize, $this::M_QUIETZONE);
|
||||
|
||||
// copy over the current matrix
|
||||
foreach($this->matrix as $y => $row){
|
||||
foreach($row as $x => $val){
|
||||
$newMatrix[($y + $quietZoneSize)][($x + $quietZoneSize)] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
// set the new values
|
||||
$this->moduleCount = $newSize;
|
||||
$this->matrix = $newMatrix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the matrix by 90 degrees clock wise
|
||||
*/
|
||||
public function rotate90():self{
|
||||
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
|
||||
$this->matrix = array_map((fn(int ...$a):array => array_reverse($a)), ...$this->matrix);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverts the values of the whole matrix
|
||||
*
|
||||
* ISO/IEC 18004:2015 Section 6.2 - Reflectance reversal
|
||||
*/
|
||||
public function invert():self{
|
||||
|
||||
foreach($this->matrix as $y => $row){
|
||||
foreach($row as $x => $val){
|
||||
|
||||
// skip null fields
|
||||
if($val === $this::M_NULL){
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->flip($x, $y);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a space of $width * $height in order to add a logo or text.
|
||||
* If no $height is given, the space will be assumed a square of $width.
|
||||
*
|
||||
* Additionally, the logo space can be positioned within the QR Code using $startX and $startY.
|
||||
* If either of these are null, the logo space will be centered in that direction.
|
||||
* ECC level "H" (30%) is required.
|
||||
*
|
||||
* The coordinates of $startX and $startY do not include the quiet zone:
|
||||
* [0, 0] is always the top left module of the top left finder pattern, negative values go into the quiet zone top and left.
|
||||
*
|
||||
* Please note that adding a logo space minimizes the error correction capacity of the QR Code and
|
||||
* created images may become unreadable, especially when printed with a chance to receive damage.
|
||||
* Please test thoroughly before using this feature in production.
|
||||
*
|
||||
* This method should be called from within an output module (after the matrix has been filled with data).
|
||||
* Note that there is no restiction on how many times this method could be called on the same matrix instance.
|
||||
*
|
||||
* @link https://github.com/chillerlan/php-qrcode/issues/52
|
||||
*
|
||||
* @throws \chillerlan\QRCode\Data\QRCodeDataException
|
||||
*/
|
||||
public function setLogoSpace(int $width, ?int $height = null, ?int $startX = null, ?int $startY = null):self{
|
||||
$height ??= $width;
|
||||
|
||||
// if width and height happen to be negative or 0 (default value), just return - nothing to do
|
||||
if($width <= 0 || $height <= 0){
|
||||
return $this; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// for logos, we operate in ECC H (30%) only
|
||||
if($this->eccLevel->getLevel() !== EccLevel::H){
|
||||
throw new QRCodeDataException('ECC level "H" required to add logo space');
|
||||
}
|
||||
|
||||
// $this->moduleCount includes the quiet zone (if created), we need the QR size here
|
||||
$dimension = $this->version->getDimension();
|
||||
|
||||
// throw if the size exceeds the qrcode size
|
||||
if($width > $dimension || $height > $dimension){
|
||||
throw new QRCodeDataException('logo dimensions exceed matrix size');
|
||||
}
|
||||
|
||||
// we need uneven sizes to center the logo space, adjust if needed
|
||||
if($startX === null && ($width % 2) === 0){
|
||||
$width++;
|
||||
}
|
||||
|
||||
if($startY === null && ($height % 2) === 0){
|
||||
$height++;
|
||||
}
|
||||
|
||||
// throw if the logo space exceeds the maximum error correction capacity
|
||||
if(($width * $height) > (int)($dimension * $dimension * 0.25)){
|
||||
throw new QRCodeDataException('logo space exceeds the maximum error correction capacity');
|
||||
}
|
||||
|
||||
$quietzone = (($this->moduleCount - $dimension) / 2);
|
||||
$end = ($this->moduleCount - $quietzone);
|
||||
|
||||
// determine start coordinates
|
||||
$startX ??= (($dimension - $width) / 2);
|
||||
$startY ??= (($dimension - $height) / 2);
|
||||
$endX = ($quietzone + $startX + $width);
|
||||
$endY = ($quietzone + $startY + $height);
|
||||
|
||||
// clear the space
|
||||
for($y = ($quietzone + $startY); $y < $endY; $y++){
|
||||
for($x = ($quietzone + $startX); $x < $endX; $x++){
|
||||
// out of bounds, skip
|
||||
if($x < $quietzone || $y < $quietzone ||$x >= $end || $y >= $end){
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->set($x, $y, false, $this::M_LOGO);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the interleaved binary $data on the matrix
|
||||
*/
|
||||
public function writeCodewords(BitBuffer $bitBuffer):self{
|
||||
$data = (new ReedSolomonEncoder($this->version, $this->eccLevel))->interleaveEcBytes($bitBuffer);
|
||||
$byteCount = count($data);
|
||||
$iByte = 0;
|
||||
$iBit = 7;
|
||||
$direction = true;
|
||||
|
||||
for($i = ($this->moduleCount - 1); $i > 0; $i -= 2){
|
||||
|
||||
// skip vertical alignment pattern
|
||||
if($i === 6){
|
||||
$i--;
|
||||
}
|
||||
|
||||
for($count = 0; $count < $this->moduleCount; $count++){
|
||||
$y = $count;
|
||||
|
||||
if($direction){
|
||||
$y = ($this->moduleCount - 1 - $count);
|
||||
}
|
||||
|
||||
for($col = 0; $col < 2; $col++){
|
||||
$x = ($i - $col);
|
||||
|
||||
// skip functional patterns
|
||||
if($this->matrix[$y][$x] !== $this::M_NULL){
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->matrix[$y][$x] = $this::M_DATA;
|
||||
|
||||
if($iByte < $byteCount && (($data[$iByte] >> $iBit--) & 1) === 1){
|
||||
$this->matrix[$y][$x] |= $this::IS_DARK;
|
||||
}
|
||||
|
||||
if($iBit === -1){
|
||||
$iByte++;
|
||||
$iBit = 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$direction = !$direction; // switch directions
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies/reverses the mask pattern
|
||||
*
|
||||
* ISO/IEC 18004:2000 Section 8.8.1
|
||||
*/
|
||||
public function mask(MaskPattern $maskPattern):self{
|
||||
$this->maskPattern = $maskPattern;
|
||||
$mask = $this->maskPattern->getMask();
|
||||
|
||||
foreach($this->matrix as $y => $row){
|
||||
foreach($row as $x => $val){
|
||||
// skip non-data modules
|
||||
if(($val & $this::M_DATA) === $this::M_DATA && $mask($x, $y)){
|
||||
$this->flip($x, $y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ReedSolomonEncoder
|
||||
*
|
||||
* @created 07.01.2021
|
||||
* @author smiley <smiley@chillerlan.net>
|
||||
* @copyright 2021 smiley
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace chillerlan\QRCode\Data;
|
||||
|
||||
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, GenericGFPoly, GF256, Version};
|
||||
use function array_fill;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function max;
|
||||
|
||||
/**
|
||||
* Reed-Solomon encoding - ISO/IEC 18004:2000 Section 8.5 ff
|
||||
*
|
||||
* @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
|
||||
*/
|
||||
final class ReedSolomonEncoder{
|
||||
|
||||
private Version $version;
|
||||
private EccLevel $eccLevel;
|
||||
|
||||
private array $interleavedData;
|
||||
private int $interleavedDataIndex;
|
||||
|
||||
/**
|
||||
* ReedSolomonDecoder constructor
|
||||
*/
|
||||
public function __construct(Version $version, EccLevel $eccLevel){
|
||||
$this->version = $version;
|
||||
$this->eccLevel = $eccLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* ECC encoding and interleaving
|
||||
*
|
||||
* @throws \chillerlan\QRCode\QRCodeException
|
||||
*/
|
||||
public function interleaveEcBytes(BitBuffer $bitBuffer):array{
|
||||
[$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $this->version->getRSBlocks($this->eccLevel);
|
||||
|
||||
$rsBlocks = array_fill(0, $l1, [($numEccCodewords + $b1), $b1]);
|
||||
|
||||
if($l2 > 0){
|
||||
$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [($numEccCodewords + $b2), $b2]));
|
||||
}
|
||||
|
||||
$bitBufferData = $bitBuffer->getBuffer();
|
||||
$dataBytes = [];
|
||||
$ecBytes = [];
|
||||
$maxDataBytes = 0;
|
||||
$maxEcBytes = 0;
|
||||
$dataByteOffset = 0;
|
||||
|
||||
foreach($rsBlocks as $key => [$rsBlockTotal, $dataByteCount]){
|
||||
$dataBytes[$key] = [];
|
||||
|
||||
for($i = 0; $i < $dataByteCount; $i++){
|
||||
$dataBytes[$key][$i] = ($bitBufferData[($i + $dataByteOffset)] & 0xff);
|
||||
}
|
||||
|
||||
$ecByteCount = ($rsBlockTotal - $dataByteCount);
|
||||
$ecBytes[$key] = $this->encode($dataBytes[$key], $ecByteCount);
|
||||
$maxDataBytes = max($maxDataBytes, $dataByteCount);
|
||||
$maxEcBytes = max($maxEcBytes, $ecByteCount);
|
||||
$dataByteOffset += $dataByteCount;
|
||||
}
|
||||
|
||||
$this->interleavedData = array_fill(0, $this->version->getTotalCodewords(), 0);
|
||||
$this->interleavedDataIndex = 0;
|
||||
$numRsBlocks = ($l1 + $l2);
|
||||
|
||||
$this->interleave($dataBytes, $maxDataBytes, $numRsBlocks);
|
||||
$this->interleave($ecBytes, $maxEcBytes, $numRsBlocks);
|
||||
|
||||
return $this->interleavedData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function encode(array $dataBytes, int $ecByteCount):array{
|
||||
$rsPoly = new GenericGFPoly([1]);
|
||||
|
||||
for($i = 0; $i < $ecByteCount; $i++){
|
||||
$rsPoly = $rsPoly->multiply(new GenericGFPoly([1, GF256::exp($i)]));
|
||||
}
|
||||
|
||||
$rsPolyDegree = $rsPoly->getDegree();
|
||||
|
||||
$modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree))
|
||||
->mod($rsPoly)
|
||||
->getCoefficients()
|
||||
;
|
||||
|
||||
$ecBytes = array_fill(0, $rsPolyDegree, 0);
|
||||
$count = (count($modCoefficients) - $rsPolyDegree);
|
||||
|
||||
foreach($ecBytes as $i => &$val){
|
||||
$modIndex = ($i + $count);
|
||||
$val = 0;
|
||||
|
||||
if($modIndex >= 0){
|
||||
$val = $modCoefficients[$modIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return $ecBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function interleave(array $byteArray, int $maxBytes, int $numRsBlocks):void{
|
||||
for($x = 0; $x < $maxBytes; $x++){
|
||||
for($y = 0; $y < $numRsBlocks; $y++){
|
||||
if($x < count($byteArray[$y])){
|
||||
$this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user