codemasher 5 лет назад
Родитель
Сommit
9f5d0bc060

+ 4 - 1
examples/custom_output.php

@@ -22,7 +22,10 @@ $options = new QROptions([
 	'eccLevel'     => QRCode::ECC_L,
 ]);
 
-$qrOutputInterface = new MyCustomOutput($options, (new QRCode($options))->getMatrix($data));
+$qrcode = new QRCode($options);
+$qrcode->addByteSegment($data);
+
+$qrOutputInterface = new MyCustomOutput($options, $qrcode->getMatrix());
 
 var_dump($qrOutputInterface->dump());
 

+ 4 - 1
examples/imageWithLogo.php

@@ -36,9 +36,12 @@ $options->logoHeight       = 13;
 $options->scale            = 5;
 $options->imageTransparent = false;
 
+$qrcode = new QRCode($options);
+$qrcode->addByteSegment($data);
+
 header('Content-type: image/png');
 
-$qrOutputInterface = new QRImageWithLogo($options, (new QRCode($options))->getMatrix($data));
+$qrOutputInterface = new QRImageWithLogo($options, $qrcode->getMatrix());
 
 // dump the output, with an additional logo
 echo $qrOutputInterface->dump(null, __DIR__.'/octocat.png');

+ 4 - 1
examples/imageWithText.php

@@ -25,9 +25,12 @@ $options = new QROptions([
 	'imageBase64'  => false,
 ]);
 
+$qrcode = new QRCode($options);
+$qrcode->addByteSegment($data);
+
 header('Content-type: image/png');
 
-$qrOutputInterface = new QRImageWithText($options, (new QRCode($options))->getMatrix($data));
+$qrOutputInterface = new QRImageWithText($options, $qrcode->getMatrix());
 
 // dump the output, with additional text
 echo $qrOutputInterface->dump(null, 'example text');

+ 46 - 9
src/Data/AlphaNum.php

@@ -14,7 +14,7 @@ namespace chillerlan\QRCode\Data;
 
 use chillerlan\QRCode\QRCode;
 
-use function ord, sprintf;
+use function ceil, ord, sprintf, str_split;
 
 /**
  * Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / :
@@ -22,7 +22,21 @@ use function ord, sprintf;
  * ISO/IEC 18004:2000 Section 8.3.3
  * ISO/IEC 18004:2000 Section 8.4.3
  */
-final class AlphaNum extends QRDataAbstract{
+final class AlphaNum extends QRDataModeAbstract{
+
+	/**
+	 * ISO/IEC 18004:2000 Table 5
+	 *
+	 * @var int[]
+	 */
+	protected const CHAR_MAP_ALPHANUM = [
+		'0' =>  0, '1' =>  1, '2' =>  2, '3' =>  3, '4' =>  4, '5' =>  5, '6' =>  6, '7' =>  7,
+		'8' =>  8, '9' =>  9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15,
+		'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23,
+		'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31,
+		'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, '$' => 37, '%' => 38, '*' => 39,
+		'+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44,
+	];
 
 	protected int $datamode = QRCode::DATA_ALPHANUM;
 
@@ -31,14 +45,37 @@ final class AlphaNum extends QRDataAbstract{
 	/**
 	 * @inheritdoc
 	 */
-	protected function write(string $data):void{
+	public function getLengthInBits():int{
+		return (int)ceil($this->getLength() * (11 / 2));
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public static function validateString(string $string):bool{
+
+		foreach(str_split($string) as $chr){
+			if(!isset(self::CHAR_MAP_ALPHANUM[$chr])){
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public function write(int $version):void{
+		$this->writeSegmentHeader($version);
+		$len = $this->getLength();
 
-		for($i = 0; $i + 1 < $this->strlen; $i += 2){
-			$this->bitBuffer->put($this->getCharCode($data[$i]) * 45 + $this->getCharCode($data[$i + 1]), 11);
+		for($i = 0; $i + 1 < $len; $i += 2){
+			$this->bitBuffer->put($this->getCharCode($this->data[$i]) * 45 + $this->getCharCode($this->data[$i + 1]), 11);
 		}
 
-		if($i < $this->strlen){
-			$this->bitBuffer->put($this->getCharCode($data[$i]), 6);
+		if($i < $len){
+			$this->bitBuffer->put($this->getCharCode($this->data[$i]), 6);
 		}
 
 	}
@@ -50,11 +87,11 @@ final class AlphaNum extends QRDataAbstract{
 	 */
 	protected function getCharCode(string $chr):int{
 
-		if(!isset($this::CHAR_MAP_ALPHANUM[$chr])){
+		if(!isset(self::CHAR_MAP_ALPHANUM[$chr])){
 			throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, ord($chr)));
 		}
 
-		return $this::CHAR_MAP_ALPHANUM[$chr];
+		return self::CHAR_MAP_ALPHANUM[$chr];
 	}
 
 }

+ 21 - 5
src/Data/Byte.php

@@ -22,7 +22,7 @@ use function ord;
  * ISO/IEC 18004:2000 Section 8.3.4
  * ISO/IEC 18004:2000 Section 8.4.4
  */
-final class Byte extends QRDataAbstract{
+final class Byte extends QRDataModeAbstract{
 
 	protected int $datamode = QRCode::DATA_BYTE;
 
@@ -31,11 +31,27 @@ final class Byte extends QRDataAbstract{
 	/**
 	 * @inheritdoc
 	 */
-	protected function write(string $data):void{
-		$i = 0;
+	public function getLengthInBits():int{
+		return $this->getLength() * 8;
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public static function validateString(string $string):bool{
+		return !empty($string);
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public function write(int $version):void{
+		$this->writeSegmentHeader($version);
+		$len = $this->getLength();
+		$i   = 0;
 
-		while($i < $this->strlen){
-			$this->bitBuffer->put(ord($data[$i]), 8);
+		while($i < $len){
+			$this->bitBuffer->put(ord($this->data[$i]), 8);
 			$i++;
 		}
 

+ 43 - 7
src/Data/Kanji.php

@@ -12,9 +12,10 @@
 
 namespace chillerlan\QRCode\Data;
 
+use chillerlan\QRCode\Helpers\BitBuffer;
 use chillerlan\QRCode\QRCode;
 
-use function mb_strlen, ord, sprintf, strlen;
+use function mb_convert_encoding, mb_detect_encoding, mb_strlen, ord, sprintf, strlen;
 
 /**
  * Kanji mode: double-byte characters from the Shift JIS character set
@@ -22,17 +23,51 @@ use function mb_strlen, ord, sprintf, strlen;
  * ISO/IEC 18004:2000 Section 8.3.5
  * ISO/IEC 18004:2000 Section 8.4.5
  */
-final class Kanji extends QRDataAbstract{
+final class Kanji extends QRDataModeAbstract{
 
 	protected int $datamode = QRCode::DATA_KANJI;
 
 	protected array $lengthBits = [8, 10, 12];
 
+	public function __construct(BitBuffer $bitBuffer, string $data){
+		parent::__construct($bitBuffer, $data);
+
+		/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
+		$this->data = mb_convert_encoding($this->data, 'SJIS', mb_detect_encoding($this->data));
+	}
+
 	/**
 	 * @inheritdoc
 	 */
-	protected function getLength(string $data):int{
-		return mb_strlen($data, 'SJIS');
+	protected function getLength():int{
+		return mb_strlen($this->data, 'SJIS');
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public function getLengthInBits():int{
+		return $this->getLength() * 13;
+	}
+
+	/**
+	 * checks if a string qualifies as Kanji
+	 */
+	public static function validateString(string $string):bool{
+		$i   = 0;
+		$len = strlen($string);
+
+		while($i + 1 < $len){
+			$c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1]));
+
+			if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
+				return false;
+			}
+
+			$i += 2;
+		}
+
+		return $i >= $len;
 	}
 
 	/**
@@ -40,11 +75,12 @@ final class Kanji extends QRDataAbstract{
 	 *
 	 * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
 	 */
-	protected function write(string $data):void{
-		$len = strlen($data);
+	public function write(int $version):void{
+		$this->writeSegmentHeader($version);
+		$len = strlen($this->data); // not self::getLength() - we need 8-bit length
 
 		for($i = 0; $i + 1 < $len; $i += 2){
-			$c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1]));
+			$c = ((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[$i + 1]));
 
 			if($c >= 0x8140 && $c <= 0x9FFC){
 				$c -= 0x8140;

+ 6 - 6
src/Data/MaskPatternTester.php

@@ -17,7 +17,7 @@ namespace chillerlan\QRCode\Data;
 use function abs, array_search, call_user_func_array, min;
 
 /**
- * Receives a QRDataInterface object and runs the mask pattern tests on it.
+ * Receives a QRData object and runs the mask pattern tests on it.
  *
  * ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results
  *
@@ -28,16 +28,16 @@ final class MaskPatternTester{
 	/**
 	 * The data interface that contains the data matrix to test
 	 */
-	protected QRDataInterface $dataInterface;
+	protected QRData $qrData;
 
 	/**
-	 * Receives the QRDataInterface
+	 * Receives the QRData object
 	 *
 	 * @see \chillerlan\QRCode\QROptions::$maskPattern
 	 * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern
 	 */
-	public function __construct(QRDataInterface $dataInterface){
-		$this->dataInterface = $dataInterface;
+	public function __construct(QRData $qrData){
+		$this->qrData = $qrData;
 	}
 
 	/**
@@ -62,7 +62,7 @@ final class MaskPatternTester{
 	 * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern
 	 */
 	public function testPattern(int $pattern):int{
-		$matrix  = $this->dataInterface->initMatrix($pattern, true);
+		$matrix  = $this->qrData->initMatrix($pattern, true);
 		$penalty = 0;
 
 		for($level = 1; $level <= 4; $level++){

+ 42 - 12
src/Data/Number.php

@@ -14,7 +14,7 @@ namespace chillerlan\QRCode\Data;
 
 use chillerlan\QRCode\QRCode;
 
-use function ord, sprintf, str_split, substr;
+use function ceil, ord, sprintf, str_split, substr;
 
 /**
  * Numeric mode: decimal digits 0 to 9
@@ -22,7 +22,14 @@ use function ord, sprintf, str_split, substr;
  * ISO/IEC 18004:2000 Section 8.3.2
  * ISO/IEC 18004:2000 Section 8.4.2
  */
-final class Number extends QRDataAbstract{
+final class Number extends QRDataModeAbstract{
+
+	/**
+	 * @var int[]
+	 */
+	protected const CHAR_MAP_NUMBER = [
+		'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
+	];
 
 	protected int $datamode = QRCode::DATA_NUMBER;
 
@@ -31,21 +38,44 @@ final class Number extends QRDataAbstract{
 	/**
 	 * @inheritdoc
 	 */
-	protected function write(string $data):void{
-		$i = 0;
+	public function getLengthInBits():int{
+		return (int)ceil($this->getLength() * (10 / 3));
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public static function validateString(string $string):bool{
+
+		foreach(str_split($string) as $chr){
+			if(!isset(self::CHAR_MAP_NUMBER[$chr])){
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public function write(int $version):void{
+		$this->writeSegmentHeader($version);
+		$len = $this->getLength();
+		$i   = 0;
 
-		while($i + 2 < $this->strlen){
-			$this->bitBuffer->put($this->parseInt(substr($data, $i, 3)), 10);
+		while($i + 2 < $len){
+			$this->bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10);
 			$i += 3;
 		}
 
-		if($i < $this->strlen){
+		if($i < $len){
 
-			if($this->strlen - $i === 1){
-				$this->bitBuffer->put($this->parseInt(substr($data, $i, $i + 1)), 4);
+			if($len - $i === 1){
+				$this->bitBuffer->put($this->parseInt(substr($this->data, $i, $i + 1)), 4);
 			}
-			elseif($this->strlen - $i === 2){
-				$this->bitBuffer->put($this->parseInt(substr($data, $i, $i + 2)), 7);
+			elseif($len - $i === 2){
+				$this->bitBuffer->put($this->parseInt(substr($this->data, $i, $i + 2)), 7);
 			}
 
 		}
@@ -63,7 +93,7 @@ final class Number extends QRDataAbstract{
 		foreach(str_split($string) as $chr){
 			$c = ord($chr);
 
-			if(!isset($this::CHAR_MAP_NUMBER[$chr])){
+			if(!isset(self::CHAR_MAP_NUMBER[$chr])){
 				throw new QRCodeDataException(sprintf('illegal char: "%s" [%d]', $chr, $c));
 			}
 

+ 408 - 0
src/Data/QRData.php

@@ -0,0 +1,408 @@
+<?php
+/**
+ * Class QRData
+ *
+ * @filesource   QRData.php
+ * @created      25.11.2015
+ * @package      chillerlan\QRCode\Data
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2015 Smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Data;
+
+use chillerlan\QRCode\QRCode;
+use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial};
+use chillerlan\Settings\SettingsContainerInterface;
+
+use function array_column, array_combine, array_fill, array_keys, array_merge, count, max, range, sprintf;
+
+/**
+ * Processes the binary data and maps it on a matrix which is then being returned
+ */
+class QRData{
+
+	/**
+	 * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
+	 *
+	 * @var int [][]
+	 */
+	const MAX_BITS = [
+		// version => [L, M, Q, H ]
+		1  => [  152,   128,   104,    72],
+		2  => [  272,   224,   176,   128],
+		3  => [  440,   352,   272,   208],
+		4  => [  640,   512,   384,   288],
+		5  => [  864,   688,   496,   368],
+		6  => [ 1088,   864,   608,   480],
+		7  => [ 1248,   992,   704,   528],
+		8  => [ 1552,  1232,   880,   688],
+		9  => [ 1856,  1456,  1056,   800],
+		10 => [ 2192,  1728,  1232,   976],
+		11 => [ 2592,  2032,  1440,  1120],
+		12 => [ 2960,  2320,  1648,  1264],
+		13 => [ 3424,  2672,  1952,  1440],
+		14 => [ 3688,  2920,  2088,  1576],
+		15 => [ 4184,  3320,  2360,  1784],
+		16 => [ 4712,  3624,  2600,  2024],
+		17 => [ 5176,  4056,  2936,  2264],
+		18 => [ 5768,  4504,  3176,  2504],
+		19 => [ 6360,  5016,  3560,  2728],
+		20 => [ 6888,  5352,  3880,  3080],
+		21 => [ 7456,  5712,  4096,  3248],
+		22 => [ 8048,  6256,  4544,  3536],
+		23 => [ 8752,  6880,  4912,  3712],
+		24 => [ 9392,  7312,  5312,  4112],
+		25 => [10208,  8000,  5744,  4304],
+		26 => [10960,  8496,  6032,  4768],
+		27 => [11744,  9024,  6464,  5024],
+		28 => [12248,  9544,  6968,  5288],
+		29 => [13048, 10136,  7288,  5608],
+		30 => [13880, 10984,  7880,  5960],
+		31 => [14744, 11640,  8264,  6344],
+		32 => [15640, 12328,  8920,  6760],
+		33 => [16568, 13048,  9368,  7208],
+		34 => [17528, 13800,  9848,  7688],
+		35 => [18448, 14496, 10288,  7888],
+		36 => [19472, 15312, 10832,  8432],
+		37 => [20528, 15936, 11408,  8768],
+		38 => [21616, 16816, 12016,  9136],
+		39 => [22496, 17728, 12656,  9776],
+		40 => [23648, 18672, 13328, 10208],
+	];
+
+	/**
+	 * @see http://www.thonky.com/qr-code-tutorial/error-correction-table
+	 *
+	 * @var int [][][]
+	 */
+	const RSBLOCKS = [
+		1  => [[ 1,  0,  26,  19], [ 1,  0, 26, 16], [ 1,  0, 26, 13], [ 1,  0, 26,  9]],
+		2  => [[ 1,  0,  44,  34], [ 1,  0, 44, 28], [ 1,  0, 44, 22], [ 1,  0, 44, 16]],
+		3  => [[ 1,  0,  70,  55], [ 1,  0, 70, 44], [ 2,  0, 35, 17], [ 2,  0, 35, 13]],
+		4  => [[ 1,  0, 100,  80], [ 2,  0, 50, 32], [ 2,  0, 50, 24], [ 4,  0, 25,  9]],
+		5  => [[ 1,  0, 134, 108], [ 2,  0, 67, 43], [ 2,  2, 33, 15], [ 2,  2, 33, 11]],
+		6  => [[ 2,  0,  86,  68], [ 4,  0, 43, 27], [ 4,  0, 43, 19], [ 4,  0, 43, 15]],
+		7  => [[ 2,  0,  98,  78], [ 4,  0, 49, 31], [ 2,  4, 32, 14], [ 4,  1, 39, 13]],
+		8  => [[ 2,  0, 121,  97], [ 2,  2, 60, 38], [ 4,  2, 40, 18], [ 4,  2, 40, 14]],
+		9  => [[ 2,  0, 146, 116], [ 3,  2, 58, 36], [ 4,  4, 36, 16], [ 4,  4, 36, 12]],
+		10 => [[ 2,  2,  86,  68], [ 4,  1, 69, 43], [ 6,  2, 43, 19], [ 6,  2, 43, 15]],
+		11 => [[ 4,  0, 101,  81], [ 1,  4, 80, 50], [ 4,  4, 50, 22], [ 3,  8, 36, 12]],
+		12 => [[ 2,  2, 116,  92], [ 6,  2, 58, 36], [ 4,  6, 46, 20], [ 7,  4, 42, 14]],
+		13 => [[ 4,  0, 133, 107], [ 8,  1, 59, 37], [ 8,  4, 44, 20], [12,  4, 33, 11]],
+		14 => [[ 3,  1, 145, 115], [ 4,  5, 64, 40], [11,  5, 36, 16], [11,  5, 36, 12]],
+		15 => [[ 5,  1, 109,  87], [ 5,  5, 65, 41], [ 5,  7, 54, 24], [11,  7, 36, 12]],
+		16 => [[ 5,  1, 122,  98], [ 7,  3, 73, 45], [15,  2, 43, 19], [ 3, 13, 45, 15]],
+		17 => [[ 1,  5, 135, 107], [10,  1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]],
+		18 => [[ 5,  1, 150, 120], [ 9,  4, 69, 43], [17,  1, 50, 22], [ 2, 19, 42, 14]],
+		19 => [[ 3,  4, 141, 113], [ 3, 11, 70, 44], [17,  4, 47, 21], [ 9, 16, 39, 13]],
+		20 => [[ 3,  5, 135, 107], [ 3, 13, 67, 41], [15,  5, 54, 24], [15, 10, 43, 15]],
+		21 => [[ 4,  4, 144, 116], [17,  0, 68, 42], [17,  6, 50, 22], [19,  6, 46, 16]],
+		22 => [[ 2,  7, 139, 111], [17,  0, 74, 46], [ 7, 16, 54, 24], [34,  0, 37, 13]],
+		23 => [[ 4,  5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]],
+		24 => [[ 6,  4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30,  2, 46, 16]],
+		25 => [[ 8,  4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]],
+		26 => [[10,  2, 142, 114], [19,  4, 74, 46], [28,  6, 50, 22], [33,  4, 46, 16]],
+		27 => [[ 8,  4, 152, 122], [22,  3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]],
+		28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]],
+		29 => [[ 7,  7, 146, 116], [21,  7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]],
+		30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]],
+		31 => [[13,  3, 145, 115], [ 2, 29, 74, 46], [42,  1, 54, 24], [23, 28, 45, 15]],
+		32 => [[17,  0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]],
+		33 => [[17,  1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]],
+		34 => [[13,  6, 145, 115], [14, 23, 74, 46], [44,  7, 54, 24], [59,  1, 46, 16]],
+		35 => [[12,  7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]],
+		36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]],
+		37 => [[17,  4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]],
+		38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]],
+		39 => [[20,  4, 147, 117], [40,  7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]],
+		40 => [[19,  6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]],
+	];
+
+	/**
+	 * current QR Code version
+	 */
+	protected int $version;
+
+	/**
+	 * ECC temp data
+	 */
+	protected array $ecdata;
+
+	/**
+	 * ECC temp data
+	 */
+	protected array $dcdata;
+
+	/**
+	 * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
+	 */
+	protected array $dataSegments = [];
+
+	/**
+	 * Max bits for the current ECC mode
+	 *
+	 * @var int[]
+	 */
+	protected array $maxBitsForEcc;
+
+	/**
+	 * the options instance
+	 *
+	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
+	 */
+	protected SettingsContainerInterface $options;
+
+	/**
+	 * a BitBuffer instance
+	 */
+	protected BitBuffer $bitBuffer;
+
+	/**
+	 * QRData constructor.
+	 *
+	 * @param \chillerlan\Settings\SettingsContainerInterface $options
+	 * @param array|null                                      $dataSegments
+	 */
+	public function __construct(SettingsContainerInterface $options, array $dataSegments = null){
+		$this->options   = $options;
+		$this->bitBuffer = new BitBuffer;
+
+		$this->maxBitsForEcc = array_combine(
+			array_keys($this::MAX_BITS),
+			array_column($this::MAX_BITS, QRCode::ECC_MODES[$this->options->eccLevel])
+		);
+
+		if(!empty($dataSegments)){
+			$this->setData($dataSegments);
+		}
+
+	}
+
+	/**
+	 * Sets the data string (internally called by the constructor)
+	 */
+	public function setData(array $dataSegments):QRData{
+
+		foreach($dataSegments as $segment){
+			[$class, $data] = $segment;
+
+			$this->dataSegments[] = new $class($this->bitBuffer, $data);
+		}
+
+		$this->version = $this->options->version === QRCode::VERSION_AUTO
+			? $this->getMinimumVersion()
+			: $this->options->version;
+
+		$this->writeBitBuffer();
+
+		return $this;
+	}
+
+	/**
+	 * returns a fresh matrix object with the data written for the given $maskPattern
+	 */
+	public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{
+		return (new QRMatrix($this->version, $this->options->eccLevel))
+			->init($maskPattern, $test)
+			->mapData($this->maskECC(), $maskPattern)
+		;
+	}
+
+	/**
+	 * estimates the total length of the several mode segments in order to guess the minimum version
+	 *
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 */
+	protected function estimateTotalBitLength():int{
+		$length = 0;
+		$margin = 0;
+
+		foreach($this->dataSegments as $segment){
+			// data length in bits of the current segment +4 bits for each mode descriptor
+			$length += ($segment->getLengthInBits() + $segment->getLengthBits(0) + 4);
+			// mode length bits margin to the next breakpoint
+			$margin += ($segment instanceof Byte ? 8 : 2);
+		}
+
+		foreach([9, 26, 40] as $breakpoint){
+
+			// length bits for the first breakpoint have already been added
+			if($breakpoint > 9){
+				$length += $margin;
+			}
+
+			if($length < $this->maxBitsForEcc[$breakpoint]){
+				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
+	 */
+	protected function getMinimumVersion():int{
+		$total = $this->estimateTotalBitLength();
+
+		// guess the version number within the given range
+		foreach(range($this->options->versionMin, $this->options->versionMax) as $version){
+
+			if($total <= $this->maxBitsForEcc[$version]){
+				return $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\QRCodeException on data overflow
+	 */
+	protected function writeBitBuffer():void{
+		$MAX_BITS = $this->maxBitsForEcc[$this->version];
+
+		foreach($this->dataSegments as $segment){
+			$segment->write($this->version);
+		}
+
+		// 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(0b0000, 4);
+		}
+
+		// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
+		while($this->bitBuffer->getLength() % 8 !== 0){
+			$this->bitBuffer->putBit(false);
+		}
+
+		while(true){
+
+			if($this->bitBuffer->getLength() >= $MAX_BITS){
+				break;
+			}
+
+			$this->bitBuffer->put(0b11101100, 8);
+
+			if($this->bitBuffer->getLength() >= $MAX_BITS){
+				break;
+			}
+
+			$this->bitBuffer->put(0b00010001, 8);
+		}
+
+	}
+
+	/**
+	 * ECC masking
+	 *
+	 * ISO/IEC 18004:2000 Section 8.5 ff
+	 *
+	 * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
+	 */
+	protected function maskECC():array{
+		[$l1, $l2, $b1, $b2] = $this::RSBLOCKS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
+
+		$rsBlocks     = array_fill(0, $l1, [$b1, $b2]);
+		$rsCount      = $l1 + $l2;
+		$this->ecdata = array_fill(0, $rsCount, []);
+		$this->dcdata = $this->ecdata;
+
+		if($l2 > 0){
+			$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1]));
+		}
+
+		$totalCodeCount = 0;
+		$maxDcCount     = 0;
+		$maxEcCount     = 0;
+		$offset         = 0;
+
+		$bitBuffer = $this->bitBuffer->getBuffer();
+
+		foreach($rsBlocks as $key => $block){
+			[$rsBlockTotal, $dcCount] = $block;
+
+			$ecCount            = $rsBlockTotal - $dcCount;
+			$maxDcCount         = max($maxDcCount, $dcCount);
+			$maxEcCount         = max($maxEcCount, $ecCount);
+			$this->dcdata[$key] = array_fill(0, $dcCount, null);
+
+			foreach($this->dcdata[$key] as $a => $_z){
+				$this->dcdata[$key][$a] = 0xff & $bitBuffer[$a + $offset];
+			}
+
+			[$num, $add] = $this->poly($key, $ecCount);
+
+			foreach($this->ecdata[$key] as $c => $_){
+				$modIndex               = $c + $add;
+				$this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0;
+			}
+
+			$offset         += $dcCount;
+			$totalCodeCount += $rsBlockTotal;
+		}
+
+		$data  = array_fill(0, $totalCodeCount, null);
+		$index = 0;
+
+		$mask = function(array $arr, int $count) use (&$data, &$index, $rsCount):void{
+			for($x = 0; $x < $count; $x++){
+				for($y = 0; $y < $rsCount; $y++){
+					if($x < count($arr[$y])){
+						$data[$index] = $arr[$y][$x];
+						$index++;
+					}
+				}
+			}
+		};
+
+		$mask($this->dcdata, $maxDcCount);
+		$mask($this->ecdata, $maxEcCount);
+
+		return $data;
+	}
+
+	/**
+	 * helper method for the polynomial operations
+	 */
+	protected function poly(int $key, int $count):array{
+		$rsPoly  = new Polynomial;
+		$modPoly = new Polynomial;
+
+		for($i = 0; $i < $count; $i++){
+			$modPoly->setNum([1, $modPoly->gexp($i)]);
+			$rsPoly->multiply($modPoly->getNum());
+		}
+
+		$rsPolyCount = count($rsPoly->getNum());
+
+		$modPoly
+			->setNum($this->dcdata[$key], $rsPolyCount - 1)
+			->mod($rsPoly->getNum())
+		;
+
+		$this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null);
+		$num                = $modPoly->getNum();
+
+		return [
+			$num,
+			count($num) - count($this->ecdata[$key]),
+		];
+	}
+
+}

+ 0 - 311
src/Data/QRDataAbstract.php

@@ -1,311 +0,0 @@
-<?php
-/**
- * Class QRDataAbstract
- *
- * @filesource   QRDataAbstract.php
- * @created      25.11.2015
- * @package      chillerlan\QRCode\Data
- * @author       Smiley <smiley@chillerlan.net>
- * @copyright    2015 Smiley
- * @license      MIT
- */
-
-namespace chillerlan\QRCode\Data;
-
-use chillerlan\QRCode\QRCode;
-use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial};
-use chillerlan\Settings\SettingsContainerInterface;
-
-use function array_fill, array_merge, count, max, mb_convert_encoding, mb_detect_encoding, range, sprintf, strlen;
-
-/**
- * Processes the binary data and maps it on a matrix which is then being returned
- */
-abstract class QRDataAbstract implements QRDataInterface{
-
-	/**
-	 * the string byte count
-	 */
-	protected ?int $strlen = null;
-
-	/**
-	 * the current data mode: Num, Alphanum, Kanji, Byte
-	 */
-	protected int $datamode;
-
-	/**
-	 * mode length bits for the version breakpoints 1-9, 10-26 and 27-40
-	 *
-	 * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator
-	 */
-	protected array $lengthBits = [0, 0, 0];
-
-	/**
-	 * current QR Code version
-	 */
-	protected int $version;
-
-	/**
-	 * ECC temp data
-	 */
-	protected array $ecdata;
-
-	/**
-	 * ECC temp data
-	 */
-	protected array $dcdata;
-
-	/**
-	 * the options instance
-	 *
-	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
-	 */
-	protected SettingsContainerInterface $options;
-
-	/**
-	 * a BitBuffer instance
-	 */
-	protected BitBuffer $bitBuffer;
-
-	/**
-	 * QRDataInterface constructor.
-	 */
-	public function __construct(SettingsContainerInterface $options, string $data = null){
-		$this->options = $options;
-
-		if($data !== null){
-			$this->setData($data);
-		}
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	public function setData(string $data):QRDataInterface{
-
-		if($this->datamode === QRCode::DATA_KANJI){
-			$data = mb_convert_encoding($data, 'SJIS', mb_detect_encoding($data));
-		}
-
-		$this->strlen  = $this->getLength($data);
-		$this->version = $this->options->version === QRCode::VERSION_AUTO
-			? $this->getMinimumVersion()
-			: $this->options->version;
-
-		$this->writeBitBuffer($data);
-
-		return $this;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{
-		return (new QRMatrix($this->version, $this->options->eccLevel))
-			->init($maskPattern, $test)
-			->mapData($this->maskECC(), $maskPattern)
-		;
-	}
-
-	/**
-	 * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40
-	 *
-	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
-	 * @codeCoverageIgnore
-	 */
-	protected function getLengthBits():int{
-
-		 foreach([9, 26, 40] as $key => $breakpoint){
-			 if($this->version <= $breakpoint){
-				 return $this->lengthBits[$key];
-			 }
-		 }
-
-		throw new QRCodeDataException(sprintf('invalid version number: %d', $this->version));
-	}
-
-	/**
-	 * returns the byte count of the $data string
-	 */
-	protected function getLength(string $data):int{
-		return strlen($data);
-	}
-
-	/**
-	 * returns the minimum version number for the given string
-	 *
-	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
-	 */
-	protected function getMinimumVersion():int{
-		$maxlength = 0;
-
-		// guess the version number within the given range
-		$dataMode = QRCode::DATA_MODES[$this->datamode];
-		$eccMode  = QRCode::ECC_MODES[$this->options->eccLevel];
-
-		foreach(range($this->options->versionMin, $this->options->versionMax) as $version){
-			$maxlength = $this::MAX_LENGTH[$version][$dataMode][$eccMode];
-
-			if($this->strlen <= $maxlength){
-				return $version;
-			}
-		}
-
-		throw new QRCodeDataException(sprintf('data exceeds %d characters', $maxlength));
-	}
-
-	/**
-	 * writes the actual data string to the BitBuffer
-	 *
-	 * @see \chillerlan\QRCode\Data\QRDataAbstract::writeBitBuffer()
-	 */
-	abstract protected function write(string $data):void;
-
-	/**
-	 * creates a BitBuffer and writes the string data to it
-	 *
-	 * @throws \chillerlan\QRCode\QRCodeException on data overflow
-	 */
-	protected function writeBitBuffer(string $data):void{
-		$this->bitBuffer = new BitBuffer;
-
-		$MAX_BITS = $this::MAX_BITS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
-
-		$this->bitBuffer
-			->put($this->datamode, 4)
-			->put($this->strlen, $this->getLengthBits())
-		;
-
-		$this->write($data);
-
-		// 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(0, 4);
-		}
-
-		// padding
-		while($this->bitBuffer->getLength() % 8 !== 0){
-			$this->bitBuffer->putBit(false);
-		}
-
-		// padding
-		while(true){
-
-			if($this->bitBuffer->getLength() >= $MAX_BITS){
-				break;
-			}
-
-			$this->bitBuffer->put(0xEC, 8);
-
-			if($this->bitBuffer->getLength() >= $MAX_BITS){
-				break;
-			}
-
-			$this->bitBuffer->put(0x11, 8);
-		}
-
-	}
-
-	/**
-	 * ECC masking
-	 *
-	 * ISO/IEC 18004:2000 Section 8.5 ff
-	 *
-	 * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
-	 */
-	protected function maskECC():array{
-		[$l1, $l2, $b1, $b2] = $this::RSBLOCKS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
-
-		$rsBlocks     = array_fill(0, $l1, [$b1, $b2]);
-		$rsCount      = $l1 + $l2;
-		$this->ecdata = array_fill(0, $rsCount, []);
-		$this->dcdata = $this->ecdata;
-
-		if($l2 > 0){
-			$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1]));
-		}
-
-		$totalCodeCount = 0;
-		$maxDcCount     = 0;
-		$maxEcCount     = 0;
-		$offset         = 0;
-
-		$bitBuffer = $this->bitBuffer->getBuffer();
-
-		foreach($rsBlocks as $key => $block){
-			[$rsBlockTotal, $dcCount] = $block;
-
-			$ecCount            = $rsBlockTotal - $dcCount;
-			$maxDcCount         = max($maxDcCount, $dcCount);
-			$maxEcCount         = max($maxEcCount, $ecCount);
-			$this->dcdata[$key] = array_fill(0, $dcCount, null);
-
-			foreach($this->dcdata[$key] as $a => $_z){
-				$this->dcdata[$key][$a] = 0xff & $bitBuffer[$a + $offset];
-			}
-
-			[$num, $add] = $this->poly($key, $ecCount);
-
-			foreach($this->ecdata[$key] as $c => $_){
-				$modIndex               = $c + $add;
-				$this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0;
-			}
-
-			$offset         += $dcCount;
-			$totalCodeCount += $rsBlockTotal;
-		}
-
-		$data  = array_fill(0, $totalCodeCount, null);
-		$index = 0;
-
-		$mask = function(array $arr, int $count) use (&$data, &$index, $rsCount):void{
-			for($x = 0; $x < $count; $x++){
-				for($y = 0; $y < $rsCount; $y++){
-					if($x < count($arr[$y])){
-						$data[$index] = $arr[$y][$x];
-						$index++;
-					}
-				}
-			}
-		};
-
-		$mask($this->dcdata, $maxDcCount);
-		$mask($this->ecdata, $maxEcCount);
-
-		return $data;
-	}
-
-	/**
-	 * helper method for the polynomial operations
-	 */
-	protected function poly(int $key, int $count):array{
-		$rsPoly  = new Polynomial;
-		$modPoly = new Polynomial;
-
-		for($i = 0; $i < $count; $i++){
-			$modPoly->setNum([1, $modPoly->gexp($i)]);
-			$rsPoly->multiply($modPoly->getNum());
-		}
-
-		$rsPolyCount = count($rsPoly->getNum());
-
-		$modPoly
-			->setNum($this->dcdata[$key], $rsPolyCount - 1)
-			->mod($rsPoly->getNum())
-		;
-
-		$this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null);
-		$num                = $modPoly->getNum();
-
-		return [
-			$num,
-			count($num) - count($this->ecdata[$key]),
-		];
-	}
-
-}

+ 0 - 200
src/Data/QRDataInterface.php

@@ -1,200 +0,0 @@
-<?php
-/**
- * Interface QRDataInterface
- *
- * @filesource   QRDataInterface.php
- * @created      01.12.2015
- * @package      chillerlan\QRCode\Data
- * @author       Smiley <smiley@chillerlan.net>
- * @copyright    2015 Smiley
- * @license      MIT
- */
-
-namespace chillerlan\QRCode\Data;
-
-/**
- * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji)
- * and holds version information in several constants
- */
-interface QRDataInterface{
-
-	/**
-	 * @var int[]
-	 */
-	const CHAR_MAP_NUMBER = [
-		'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
-	];
-
-	/**
-	 * ISO/IEC 18004:2000 Table 5
-	 *
-	 * @var int[]
-	 */
-	const CHAR_MAP_ALPHANUM = [
-		'0' =>  0, '1' =>  1, '2' =>  2, '3' =>  3, '4' =>  4, '5' =>  5, '6' =>  6, '7' =>  7,
-		'8' =>  8, '9' =>  9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15,
-		'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23,
-		'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31,
-		'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, '$' => 37, '%' => 38, '*' => 39,
-		'+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44,
-	];
-
-	/**
-	 * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
-	 *
-	 * @see http://www.qrcode.com/en/about/version.html
-	 *
-	 * @var int [][][]
-	 */
-	const MAX_LENGTH =[
-	//	v  => [NUMERIC => [L, M, Q, H ], ALPHANUM => [L, M, Q, H], BINARY => [L, M, Q, H  ], KANJI => [L, M, Q, H   ]]  // modules
-		1  => [[  41,   34,   27,   17], [  25,   20,   16,   10], [  17,   14,   11,    7], [  10,    8,    7,    4]], //  21
-		2  => [[  77,   63,   48,   34], [  47,   38,   29,   20], [  32,   26,   20,   14], [  20,   16,   12,    8]], //  25
-		3  => [[ 127,  101,   77,   58], [  77,   61,   47,   35], [  53,   42,   32,   24], [  32,   26,   20,   15]], //  29
-		4  => [[ 187,  149,  111,   82], [ 114,   90,   67,   50], [  78,   62,   46,   34], [  48,   38,   28,   21]], //  33
-		5  => [[ 255,  202,  144,  106], [ 154,  122,   87,   64], [ 106,   84,   60,   44], [  65,   52,   37,   27]], //  37
-		6  => [[ 322,  255,  178,  139], [ 195,  154,  108,   84], [ 134,  106,   74,   58], [  82,   65,   45,   36]], //  41
-		7  => [[ 370,  293,  207,  154], [ 224,  178,  125,   93], [ 154,  122,   86,   64], [  95,   75,   53,   39]], //  45
-		8  => [[ 461,  365,  259,  202], [ 279,  221,  157,  122], [ 192,  152,  108,   84], [ 118,   93,   66,   52]], //  49
-		9  => [[ 552,  432,  312,  235], [ 335,  262,  189,  143], [ 230,  180,  130,   98], [ 141,  111,   80,   60]], //  53
-		10 => [[ 652,  513,  364,  288], [ 395,  311,  221,  174], [ 271,  213,  151,  119], [ 167,  131,   93,   74]], //  57
-		11 => [[ 772,  604,  427,  331], [ 468,  366,  259,  200], [ 321,  251,  177,  137], [ 198,  155,  109,   85]], //  61
-		12 => [[ 883,  691,  489,  374], [ 535,  419,  296,  227], [ 367,  287,  203,  155], [ 226,  177,  125,   96]], //  65
-		13 => [[1022,  796,  580,  427], [ 619,  483,  352,  259], [ 425,  331,  241,  177], [ 262,  204,  149,  109]], //  69 NICE!
-		14 => [[1101,  871,  621,  468], [ 667,  528,  376,  283], [ 458,  362,  258,  194], [ 282,  223,  159,  120]], //  73
-		15 => [[1250,  991,  703,  530], [ 758,  600,  426,  321], [ 520,  412,  292,  220], [ 320,  254,  180,  136]], //  77
-		16 => [[1408, 1082,  775,  602], [ 854,  656,  470,  365], [ 586,  450,  322,  250], [ 361,  277,  198,  154]], //  81
-		17 => [[1548, 1212,  876,  674], [ 938,  734,  531,  408], [ 644,  504,  364,  280], [ 397,  310,  224,  173]], //  85
-		18 => [[1725, 1346,  948,  746], [1046,  816,  574,  452], [ 718,  560,  394,  310], [ 442,  345,  243,  191]], //  89
-		19 => [[1903, 1500, 1063,  813], [1153,  909,  644,  493], [ 792,  624,  442,  338], [ 488,  384,  272,  208]], //  93
-		20 => [[2061, 1600, 1159,  919], [1249,  970,  702,  557], [ 858,  666,  482,  382], [ 528,  410,  297,  235]], //  97
-		21 => [[2232, 1708, 1224,  969], [1352, 1035,  742,  587], [ 929,  711,  509,  403], [ 572,  438,  314,  248]], // 101
-		22 => [[2409, 1872, 1358, 1056], [1460, 1134,  823,  640], [1003,  779,  565,  439], [ 618,  480,  348,  270]], // 105
-		23 => [[2620, 2059, 1468, 1108], [1588, 1248,  890,  672], [1091,  857,  611,  461], [ 672,  528,  376,  284]], // 109
-		24 => [[2812, 2188, 1588, 1228], [1704, 1326,  963,  744], [1171,  911,  661,  511], [ 721,  561,  407,  315]], // 113
-		25 => [[3057, 2395, 1718, 1286], [1853, 1451, 1041,  779], [1273,  997,  715,  535], [ 784,  614,  440,  330]], // 117
-		26 => [[3283, 2544, 1804, 1425], [1990, 1542, 1094,  864], [1367, 1059,  751,  593], [ 842,  652,  462,  365]], // 121
-		27 => [[3517, 2701, 1933, 1501], [2132, 1637, 1172,  910], [1465, 1125,  805,  625], [ 902,  692,  496,  385]], // 125
-		28 => [[3669, 2857, 2085, 1581], [2223, 1732, 1263,  958], [1528, 1190,  868,  658], [ 940,  732,  534,  405]], // 129
-		29 => [[3909, 3035, 2181, 1677], [2369, 1839, 1322, 1016], [1628, 1264,  908,  698], [1002,  778,  559,  430]], // 133
-		30 => [[4158, 3289, 2358, 1782], [2520, 1994, 1429, 1080], [1732, 1370,  982,  742], [1066,  843,  604,  457]], // 137
-		31 => [[4417, 3486, 2473, 1897], [2677, 2113, 1499, 1150], [1840, 1452, 1030,  790], [1132,  894,  634,  486]], // 141
-		32 => [[4686, 3693, 2670, 2022], [2840, 2238, 1618, 1226], [1952, 1538, 1112,  842], [1201,  947,  684,  518]], // 145
-		33 => [[4965, 3909, 2805, 2157], [3009, 2369, 1700, 1307], [2068, 1628, 1168,  898], [1273, 1002,  719,  553]], // 149
-		34 => [[5253, 4134, 2949, 2301], [3183, 2506, 1787, 1394], [2188, 1722, 1228,  958], [1347, 1060,  756,  590]], // 153
-		35 => [[5529, 4343, 3081, 2361], [3351, 2632, 1867, 1431], [2303, 1809, 1283,  983], [1417, 1113,  790,  605]], // 157
-		36 => [[5836, 4588, 3244, 2524], [3537, 2780, 1966, 1530], [2431, 1911, 1351, 1051], [1496, 1176,  832,  647]], // 161
-		37 => [[6153, 4775, 3417, 2625], [3729, 2894, 2071, 1591], [2563, 1989, 1423, 1093], [1577, 1224,  876,  673]], // 165
-		38 => [[6479, 5039, 3599, 2735], [3927, 3054, 2181, 1658], [2699, 2099, 1499, 1139], [1661, 1292,  923,  701]], // 169
-		39 => [[6743, 5313, 3791, 2927], [4087, 3220, 2298, 1774], [2809, 2213, 1579, 1219], [1729, 1362,  972,  750]], // 173
-		40 => [[7089, 5596, 3993, 3057], [4296, 3391, 2420, 1852], [2953, 2331, 1663, 1273], [1817, 1435, 1024,  784]], // 177
-	];
-
-	/**
-	 * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
-	 *
-	 * @var int [][]
-	 */
-	const MAX_BITS = [
-		// version => [L, M, Q, H ]
-		1  => [  152,   128,   104,    72],
-		2  => [  272,   224,   176,   128],
-		3  => [  440,   352,   272,   208],
-		4  => [  640,   512,   384,   288],
-		5  => [  864,   688,   496,   368],
-		6  => [ 1088,   864,   608,   480],
-		7  => [ 1248,   992,   704,   528],
-		8  => [ 1552,  1232,   880,   688],
-		9  => [ 1856,  1456,  1056,   800],
-		10 => [ 2192,  1728,  1232,   976],
-		11 => [ 2592,  2032,  1440,  1120],
-		12 => [ 2960,  2320,  1648,  1264],
-		13 => [ 3424,  2672,  1952,  1440],
-		14 => [ 3688,  2920,  2088,  1576],
-		15 => [ 4184,  3320,  2360,  1784],
-		16 => [ 4712,  3624,  2600,  2024],
-		17 => [ 5176,  4056,  2936,  2264],
-		18 => [ 5768,  4504,  3176,  2504],
-		19 => [ 6360,  5016,  3560,  2728],
-		20 => [ 6888,  5352,  3880,  3080],
-		21 => [ 7456,  5712,  4096,  3248],
-		22 => [ 8048,  6256,  4544,  3536],
-		23 => [ 8752,  6880,  4912,  3712],
-		24 => [ 9392,  7312,  5312,  4112],
-		25 => [10208,  8000,  5744,  4304],
-		26 => [10960,  8496,  6032,  4768],
-		27 => [11744,  9024,  6464,  5024],
-		28 => [12248,  9544,  6968,  5288],
-		29 => [13048, 10136,  7288,  5608],
-		30 => [13880, 10984,  7880,  5960],
-		31 => [14744, 11640,  8264,  6344],
-		32 => [15640, 12328,  8920,  6760],
-		33 => [16568, 13048,  9368,  7208],
-		34 => [17528, 13800,  9848,  7688],
-		35 => [18448, 14496, 10288,  7888],
-		36 => [19472, 15312, 10832,  8432],
-		37 => [20528, 15936, 11408,  8768],
-		38 => [21616, 16816, 12016,  9136],
-		39 => [22496, 17728, 12656,  9776],
-		40 => [23648, 18672, 13328, 10208],
-	];
-
-	/**
-	 * @see http://www.thonky.com/qr-code-tutorial/error-correction-table
-	 *
-	 * @var int [][][]
-	 */
-	const RSBLOCKS = [
-		1  => [[ 1,  0,  26,  19], [ 1,  0, 26, 16], [ 1,  0, 26, 13], [ 1,  0, 26,  9]],
-		2  => [[ 1,  0,  44,  34], [ 1,  0, 44, 28], [ 1,  0, 44, 22], [ 1,  0, 44, 16]],
-		3  => [[ 1,  0,  70,  55], [ 1,  0, 70, 44], [ 2,  0, 35, 17], [ 2,  0, 35, 13]],
-		4  => [[ 1,  0, 100,  80], [ 2,  0, 50, 32], [ 2,  0, 50, 24], [ 4,  0, 25,  9]],
-		5  => [[ 1,  0, 134, 108], [ 2,  0, 67, 43], [ 2,  2, 33, 15], [ 2,  2, 33, 11]],
-		6  => [[ 2,  0,  86,  68], [ 4,  0, 43, 27], [ 4,  0, 43, 19], [ 4,  0, 43, 15]],
-		7  => [[ 2,  0,  98,  78], [ 4,  0, 49, 31], [ 2,  4, 32, 14], [ 4,  1, 39, 13]],
-		8  => [[ 2,  0, 121,  97], [ 2,  2, 60, 38], [ 4,  2, 40, 18], [ 4,  2, 40, 14]],
-		9  => [[ 2,  0, 146, 116], [ 3,  2, 58, 36], [ 4,  4, 36, 16], [ 4,  4, 36, 12]],
-		10 => [[ 2,  2,  86,  68], [ 4,  1, 69, 43], [ 6,  2, 43, 19], [ 6,  2, 43, 15]],
-		11 => [[ 4,  0, 101,  81], [ 1,  4, 80, 50], [ 4,  4, 50, 22], [ 3,  8, 36, 12]],
-		12 => [[ 2,  2, 116,  92], [ 6,  2, 58, 36], [ 4,  6, 46, 20], [ 7,  4, 42, 14]],
-		13 => [[ 4,  0, 133, 107], [ 8,  1, 59, 37], [ 8,  4, 44, 20], [12,  4, 33, 11]],
-		14 => [[ 3,  1, 145, 115], [ 4,  5, 64, 40], [11,  5, 36, 16], [11,  5, 36, 12]],
-		15 => [[ 5,  1, 109,  87], [ 5,  5, 65, 41], [ 5,  7, 54, 24], [11,  7, 36, 12]],
-		16 => [[ 5,  1, 122,  98], [ 7,  3, 73, 45], [15,  2, 43, 19], [ 3, 13, 45, 15]],
-		17 => [[ 1,  5, 135, 107], [10,  1, 74, 46], [ 1, 15, 50, 22], [ 2, 17, 42, 14]],
-		18 => [[ 5,  1, 150, 120], [ 9,  4, 69, 43], [17,  1, 50, 22], [ 2, 19, 42, 14]],
-		19 => [[ 3,  4, 141, 113], [ 3, 11, 70, 44], [17,  4, 47, 21], [ 9, 16, 39, 13]],
-		20 => [[ 3,  5, 135, 107], [ 3, 13, 67, 41], [15,  5, 54, 24], [15, 10, 43, 15]],
-		21 => [[ 4,  4, 144, 116], [17,  0, 68, 42], [17,  6, 50, 22], [19,  6, 46, 16]],
-		22 => [[ 2,  7, 139, 111], [17,  0, 74, 46], [ 7, 16, 54, 24], [34,  0, 37, 13]],
-		23 => [[ 4,  5, 151, 121], [ 4, 14, 75, 47], [11, 14, 54, 24], [16, 14, 45, 15]],
-		24 => [[ 6,  4, 147, 117], [ 6, 14, 73, 45], [11, 16, 54, 24], [30,  2, 46, 16]],
-		25 => [[ 8,  4, 132, 106], [ 8, 13, 75, 47], [ 7, 22, 54, 24], [22, 13, 45, 15]],
-		26 => [[10,  2, 142, 114], [19,  4, 74, 46], [28,  6, 50, 22], [33,  4, 46, 16]],
-		27 => [[ 8,  4, 152, 122], [22,  3, 73, 45], [ 8, 26, 53, 23], [12, 28, 45, 15]],
-		28 => [[ 3, 10, 147, 117], [ 3, 23, 73, 45], [ 4, 31, 54, 24], [11, 31, 45, 15]],
-		29 => [[ 7,  7, 146, 116], [21,  7, 73, 45], [ 1, 37, 53, 23], [19, 26, 45, 15]],
-		30 => [[ 5, 10, 145, 115], [19, 10, 75, 47], [15, 25, 54, 24], [23, 25, 45, 15]],
-		31 => [[13,  3, 145, 115], [ 2, 29, 74, 46], [42,  1, 54, 24], [23, 28, 45, 15]],
-		32 => [[17,  0, 145, 115], [10, 23, 74, 46], [10, 35, 54, 24], [19, 35, 45, 15]],
-		33 => [[17,  1, 145, 115], [14, 21, 74, 46], [29, 19, 54, 24], [11, 46, 45, 15]],
-		34 => [[13,  6, 145, 115], [14, 23, 74, 46], [44,  7, 54, 24], [59,  1, 46, 16]],
-		35 => [[12,  7, 151, 121], [12, 26, 75, 47], [39, 14, 54, 24], [22, 41, 45, 15]],
-		36 => [[ 6, 14, 151, 121], [ 6, 34, 75, 47], [46, 10, 54, 24], [ 2, 64, 45, 15]],
-		37 => [[17,  4, 152, 122], [29, 14, 74, 46], [49, 10, 54, 24], [24, 46, 45, 15]],
-		38 => [[ 4, 18, 152, 122], [13, 32, 74, 46], [48, 14, 54, 24], [42, 32, 45, 15]],
-		39 => [[20,  4, 147, 117], [40,  7, 75, 47], [43, 22, 54, 24], [10, 67, 45, 15]],
-		40 => [[19,  6, 148, 118], [18, 31, 75, 47], [34, 34, 54, 24], [20, 61, 45, 15]],
-	];
-
-	/**
-	 * Sets the data string (internally called by the constructor)
-	 */
-	public function setData(string $data):QRDataInterface;
-
-	/**
-	 * returns a fresh matrix object with the data written for the given $maskPattern
-	 */
-	public function initMatrix(int $maskPattern, bool $test = null):QRMatrix;
-
-}

+ 101 - 0
src/Data/QRDataModeAbstract.php

@@ -0,0 +1,101 @@
+<?php
+/**
+ * Class QRDataModeAbstract
+ *
+ * @filesource   QRDataModeAbstract.php
+ * @created      19.11.2020
+ * @package      chillerlan\QRCode\Data
+ * @author       smiley <smiley@chillerlan.net>
+ * @copyright    2020 smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Data;
+
+use chillerlan\QRCode\Helpers\BitBuffer;
+
+/**
+ */
+abstract class QRDataModeAbstract implements QRDataModeInterface{
+
+	/**
+	 * the current data mode: Num, Alphanum, Kanji, Byte
+	 */
+	protected int $datamode;
+
+	/**
+	 * mode length bits for the version breakpoints 1-9, 10-26 and 27-40
+	 *
+	 * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator
+	 */
+	protected array $lengthBits = [0, 0, 0];
+
+	/**
+	 * The data to write
+	 */
+	protected string $data;
+
+	/**
+	 * a BitBuffer instance
+	 */
+	protected BitBuffer $bitBuffer;
+
+	/**
+	 * QRDataModeAbstract constructor.
+	 *
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 */
+	public function __construct(BitBuffer $bitBuffer, string $data){
+		// do we need this here? we check during write anyways.
+#		if(!static::validateString($data)){
+#			throw new QRCodeDataException('invalid data string');
+#		}
+
+		$this->bitBuffer = $bitBuffer;
+		$this->data      = $data;
+	}
+
+	/**
+	 * returns the character count of the $data string
+	 */
+	protected function getLength():int{
+		return strlen($this->data);
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getLengthBits(int $k):int{
+		return $this->lengthBits[$k] ?? 0;
+	}
+
+	/**
+	 * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40
+	 *
+	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
+	 * @codeCoverageIgnore
+	 */
+	protected function getLengthBitsForVersion(int $version):int{
+
+		foreach([9, 26, 40] as $key => $breakpoint){
+			if($version <= $breakpoint){
+				return $this->getLengthBits($key);
+			}
+		}
+
+		throw new QRCodeDataException(sprintf('invalid version number: %d', $version));
+	}
+
+	/**
+	 *
+	 */
+	protected function writeSegmentHeader(int $version):void{
+
+		$this->bitBuffer
+			->put($this->datamode, 4)
+			->put($this->getLength(), $this->getLengthBitsForVersion($version))
+		;
+
+	}
+
+}

+ 43 - 0
src/Data/QRDataModeInterface.php

@@ -0,0 +1,43 @@
+<?php
+/**
+ * Interface QRDataModeInterface
+ *
+ * @filesource   QRDataModeInterface.php
+ * @created      01.12.2015
+ * @package      chillerlan\QRCode\Data
+ * @author       Smiley <smiley@chillerlan.net>
+ * @copyright    2015 Smiley
+ * @license      MIT
+ */
+
+namespace chillerlan\QRCode\Data;
+
+/**
+ * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji)
+ * and holds version information in several constants
+ */
+interface QRDataModeInterface{
+
+	/**
+	 * returns the length bits for the given breakpoint [0,1,2]
+	 */
+	public function getLengthBits(int $k):int;
+
+	/**
+	 * retruns the length in bits of the data string
+	 */
+	public function getLengthInBits():int;
+
+	/**
+	 * 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(int $version):void;
+
+}

+ 2 - 2
src/Data/QRMatrix.php

@@ -641,10 +641,10 @@ final class QRMatrix{
 	}
 
 	/**
-	 * Maps the binary $data array from QRDataInterface::maskECC() on the matrix,
+	 * Maps the binary $data array from QRData::maskECC() on the matrix,
 	 * masking the data using $maskPattern (ISO/IEC 18004:2000 Section 8.8)
 	 *
-	 * @see \chillerlan\QRCode\Data\QRDataAbstract::maskECC()
+	 * @see \chillerlan\QRCode\Data\QRData::maskECC()
 	 *
 	 * @param int[] $data
 	 * @param int   $maskPattern

+ 86 - 91
src/QRCode.php

@@ -13,14 +13,14 @@
 namespace chillerlan\QRCode;
 
 use chillerlan\QRCode\Data\{
-	AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRCodeDataException, QRDataInterface, QRMatrix
+	AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRData, QRCodeDataException, QRMatrix
 };
 use chillerlan\QRCode\Output\{
 	QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString
 };
 use chillerlan\Settings\SettingsContainerInterface;
 
-use function call_user_func_array, class_exists, in_array, ord, strlen, strtolower, str_split;
+use function class_exists, in_array;
 
 /**
  * Turns a text string into a Model 2 QR Code
@@ -49,20 +49,6 @@ class QRCode{
 	/** @var int */
 	public const DATA_KANJI    = 0b1000;
 
-	/**
-	 * References to the keys of the following tables:
-	 *
-	 * @see \chillerlan\QRCode\Data\QRDataInterface::MAX_LENGTH
-	 *
-	 * @var int[]
-	 */
-	public const DATA_MODES = [
-		self::DATA_NUMBER   => 0,
-		self::DATA_ALPHANUM => 1,
-		self::DATA_BYTE     => 2,
-		self::DATA_KANJI    => 3,
-	];
-
 	// ISO/IEC 18004:2000 Tables 12, 25
 
 	/** @var int */
@@ -77,8 +63,8 @@ class QRCode{
 	/**
 	 * References to the keys of the following tables:
 	 *
-	 * @see \chillerlan\QRCode\Data\QRDataInterface::MAX_BITS
-	 * @see \chillerlan\QRCode\Data\QRDataInterface::RSBLOCKS
+	 * @see \chillerlan\QRCode\Data\QRData::MAX_BITS
+	 * @see \chillerlan\QRCode\Data\QRData::RSBLOCKS
 	 * @see \chillerlan\QRCode\Data\QRMatrix::formatPattern
 	 *
 	 * @var int[]
@@ -134,22 +120,31 @@ class QRCode{
 			self::OUTPUT_IMAGICK,
 		],
 		QRFpdf::class => [
-			self::OUTPUT_FPDF
-		]
+			self::OUTPUT_FPDF,
+		],
 	];
 
 	/**
-	 * Map of data mode => interface
+	 * Map of data mode => interface (detection order)
 	 *
 	 * @var string[]
 	 */
 	protected const DATA_INTERFACES = [
-		'number'   => Number::class,
-		'alphanum' => AlphaNum::class,
-		'kanji'    => Kanji::class,
-		'byte'     => Byte::class,
+		self::DATA_NUMBER   => Number::class,
+		self::DATA_ALPHANUM => AlphaNum::class,
+		self::DATA_KANJI    => Kanji::class,
+		self::DATA_BYTE     => Byte::class,
 	];
 
+	/**
+	 * A collection of one or more data segments of [classname, data] to write
+	 *
+	 * @see \chillerlan\QRCode\Data\QRDataModeInterface
+	 *
+	 * @var string[][]|int[][]
+	 */
+	protected array $dataSegments = [];
+
 	/**
 	 * The settings container
 	 *
@@ -160,7 +155,7 @@ class QRCode{
 	/**
 	 * The selected data interface (Number, AlphaNum, Kanji, Byte)
 	 */
-	protected QRDataInterface $dataInterface;
+	protected QRData $dataInterface;
 
 	/**
 	 * QRCode constructor.
@@ -176,22 +171,39 @@ class QRCode{
 	 *
 	 * @return mixed
 	 */
-	public function render(string $data, string $file = null){
-		return $this->initOutputInterface($data)->dump($file);
+	public function render(string $data = null, string $file = null){
+
+		if($data !== null){
+			/** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */
+			foreach($this::DATA_INTERFACES as $dataInterface){
+
+				if($dataInterface::validateString($data)){
+					$this->addSegment($data, $dataInterface);
+
+					break;
+				}
+
+			}
+
+		}
+
+		return $this->initOutputInterface()->dump($file);
 	}
 
+
+
 	/**
 	 * Returns a QRMatrix object for the given $data and current QROptions
 	 *
 	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
 	 */
-	public function getMatrix(string $data):QRMatrix{
+	public function getMatrix():QRMatrix{
 
-		if(empty($data)){
+		if(empty($this->dataSegments)){
 			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
 		}
 
-		$this->dataInterface = $this->initDataInterface($data);
+		$this->dataInterface = new QRData($this->options, $this->dataSegments);
 
 		$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
 			? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern()
@@ -206,47 +218,21 @@ class QRCode{
 		return $matrix;
 	}
 
-	/**
-	 * returns a fresh QRDataInterface for the given $data
-	 *
-	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
-	 */
-	public function initDataInterface(string $data):QRDataInterface{
-
-		// allow forcing the data mode
-		// see https://github.com/chillerlan/php-qrcode/issues/39
-		$interface = $this::DATA_INTERFACES[strtolower($this->options->dataModeOverride)] ?? null;
-
-		if($interface !== null){
-			return new $interface($this->options, $data);
-		}
-
-		foreach($this::DATA_INTERFACES as $mode => $dataInterface){
-
-			if(call_user_func_array([$this, 'is'.$mode], [$data])){
-				return new $dataInterface($this->options, $data);
-			}
-
-		}
-
-		throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore
-	}
-
 	/**
 	 * returns a fresh (built-in) QROutputInterface
 	 *
 	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
 	 */
-	protected function initOutputInterface(string $data):QROutputInterface{
+	protected function initOutputInterface():QROutputInterface{
 
 		if($this->options->outputType === $this::OUTPUT_CUSTOM && class_exists($this->options->outputInterface)){
-			return new $this->options->outputInterface($this->options, $this->getMatrix($data));
+			return new $this->options->outputInterface($this->options, $this->getMatrix());
 		}
 
 		foreach($this::OUTPUT_MODES as $outputInterface => $modes){
 
 			if(in_array($this->options->outputType, $modes, true) && class_exists($outputInterface)){
-				return new $outputInterface($this->options, $this->getMatrix($data));
+				return new $outputInterface($this->options, $this->getMatrix());
 			}
 
 		}
@@ -255,58 +241,67 @@ class QRCode{
 	}
 
 	/**
-	 * checks if a string qualifies as numeric
+	 * checks if a string qualifies as numeric (convenience method)
+	 *
+	 * @see Number::validateString()
 	 */
 	public function isNumber(string $string):bool{
-		return $this->checkString($string, QRDataInterface::CHAR_MAP_NUMBER);
+		return Number::validateString($string);
 	}
 
 	/**
-	 * checks if a string qualifies as alphanumeric
+	 * checks if a string qualifies as alphanumeric (convenience method)
+	 *
+	 * @see AlphaNum::validateString()
 	 */
 	public function isAlphaNum(string $string):bool{
-		return $this->checkString($string, QRDataInterface::CHAR_MAP_ALPHANUM);
+		return AlphaNum::validateString($string);
 	}
 
 	/**
-	 * checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence.
+	 * checks if a string qualifies as Kanji (convenience method)
+	 *
+	 * @see Kanji::validateString()
 	 */
-	protected function checkString(string $string, array $charmap):bool{
-
-		foreach(str_split($string) as $chr){
-			if(!isset($charmap[$chr])){
-				return false;
-			}
-		}
-
-		return true;
+	public function isKanji(string $string):bool{
+		return Kanji::validateString($string);
 	}
 
 	/**
-	 * checks if a string qualifies as Kanji
+	 * a dummy (convenience method)
+	 *
+	 * @see Byte::validateString()
 	 */
-	public function isKanji(string $string):bool{
-		$i   = 0;
-		$len = strlen($string);
+	public function isByte(string $string):bool{
+		return Byte::validateString($string);
+	}
 
-		while($i + 1 < $len){
-			$c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1]));
+	protected function addSegment(string $data, string $classname):void{
+		$this->dataSegments[] = [$classname, $data];
+	}
 
-			if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
-				return false;
-			}
+	public function addNumberSegment(string $data):QRCode{
+		$this->addSegment($data, Number::class);
 
-			$i += 2;
-		}
+		return $this;
+	}
 
-		return $i >= $len;
+	public function addAlphaNumSegment(string $data):QRCode{
+		$this->addSegment($data, AlphaNum::class);
+
+		return $this;
 	}
 
-	/**
-	 * a dummy
-	 */
-	public function isByte(string $data):bool{
-		return !empty($data);
+	public function addKanjiSegment(string $data):QRCode{
+		$this->addSegment($data, Kanji::class);
+
+		return $this;
+	}
+
+	public function addByteSegment(string $data):QRCode{
+		$this->addSegment($data, Byte::class);
+
+		return $this;
 	}
 
 }

+ 0 - 1
src/QROptions.php

@@ -24,7 +24,6 @@ use chillerlan\Settings\SettingsContainerAbstract;
  * @property int         $maskPattern
  * @property bool        $addQuietzone
  * @property int         $quietzoneSize
- * @property string|null $dataModeOverride
  * @property string      $outputType
  * @property string|null $outputInterface
  * @property string|null $cachefile

+ 0 - 9
src/QROptionsTrait.php

@@ -71,15 +71,6 @@ trait QROptionsTrait{
 	 */
 	protected int $quietzoneSize = 4;
 
-	/**
-	 * Use this to circumvent the data mode detection and force the usage of the given mode.
-	 *
-	 * valid modes are: Number, AlphaNum, Kanji, Byte (case insensitive)
-	 *
-	 * @see https://github.com/chillerlan/php-qrcode/issues/39
-	 */
-	protected ?string $dataModeOverride = null;
-
 	/**
 	 * The output type
 	 *

+ 3 - 12
tests/Data/AlphaNumTest.php

@@ -12,8 +12,7 @@
 
 namespace chillerlan\QRCodeTest\Data;
 
-use chillerlan\QRCode\Data\{AlphaNum, QRCodeDataException, QRDataInterface};
-use chillerlan\QRCode\QROptions;
+use chillerlan\QRCode\Data\{AlphaNum, QRCodeDataException};
 
 /**
  * Tests the AlphaNum class
@@ -21,7 +20,7 @@ use chillerlan\QRCode\QROptions;
 final class AlphaNumTest extends DatainterfaceTestAbstract{
 
 	/** @internal */
-	protected string $testdata  = '0 $%*+-./:';
+	protected array $testdata  = [AlphaNum::class, '0 $%*+-./:'];
 
 	/** @internal */
 	protected array  $expected  = [
@@ -40,14 +39,6 @@ final class AlphaNumTest extends DatainterfaceTestAbstract{
 		92, 112, 20, 198, 27
 	];
 
-	/**
-	 * @inheritDoc
-	 * @internal
-	 */
-	protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{
-		return new AlphaNum($options);
-	}
-
 	/**
 	 * Tests if an exception is thrown when an invalid character is encountered
 	 */
@@ -55,7 +46,7 @@ final class AlphaNumTest extends DatainterfaceTestAbstract{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('illegal char: "#" [35]');
 
-		$this->dataInterface->setData('#');
+		$this->dataInterface->setData([[AlphaNum::class, '#']]);
 	}
 
 }

+ 1 - 11
tests/Data/ByteTest.php

@@ -13,8 +13,6 @@
 namespace chillerlan\QRCodeTest\Data;
 
 use chillerlan\QRCode\Data\Byte;
-use chillerlan\QRCode\Data\QRDataInterface;
-use chillerlan\QRCode\QROptions;
 
 /**
  * Tests the Byte class
@@ -22,7 +20,7 @@ use chillerlan\QRCode\QROptions;
 final class ByteTest extends DatainterfaceTestAbstract{
 
 	/** @internal */
-	protected string $testdata = '[¯\_(ツ)_/¯]';
+	protected array $testdata = [Byte::class, '[¯\_(ツ)_/¯]'];
 
 	/** @internal */
 	protected array  $expected = [
@@ -41,12 +39,4 @@ final class ByteTest extends DatainterfaceTestAbstract{
 		21, 47, 250, 101
 	];
 
-	/**
-	 * @inheritDoc
-	 * @internal
-	 */
-	protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{
-		return new Byte($options);
-	}
-
 }

+ 16 - 19
tests/Data/DatainterfaceTestAbstract.php

@@ -15,7 +15,7 @@ namespace chillerlan\QRCodeTest\Data;
 use chillerlan\QRCode\QRCode;
 use chillerlan\QRCode\QROptions;
 use PHPUnit\Framework\TestCase;
-use chillerlan\QRCode\Data\{QRCodeDataException, QRDataInterface, QRMatrix};
+use chillerlan\QRCode\Data\{QRCodeDataException, QRData, QRMatrix};
 use ReflectionClass;
 
 use function str_repeat;
@@ -28,39 +28,32 @@ abstract class DatainterfaceTestAbstract extends TestCase{
 	/** @internal */
 	protected ReflectionClass $reflection;
 	/** @internal */
-	protected QRDataInterface $dataInterface;
+	protected QRData $dataInterface;
 	/** @internal */
-	protected string $testdata;
+	protected array $testdata;
 	/** @internal */
-	protected array  $expected;
+	protected array $expected;
 
 	/**
 	 * @internal
 	 */
 	protected function setUp():void{
-		$this->dataInterface = $this->getDataInterfaceInstance(new QROptions(['version' => 4]));
+		$this->dataInterface = new QRData(new QROptions(['version' => 4]), []);
 		$this->reflection    = new ReflectionClass($this->dataInterface);
 	}
 
-	/**
-	 * Returns a data interface instance
-	 *
-	 * @internal
-	 */
-	abstract protected function getDataInterfaceInstance(QROptions $options):QRDataInterface;
-
 	/**
 	 * Verifies the data interface instance
 	 */
 	public function testInstance():void{
-		$this::assertInstanceOf(QRDataInterface::class, $this->dataInterface);
+		$this::assertInstanceOf(QRData::class, $this->dataInterface);
 	}
 
 	/**
 	 * Tests ecc masking and verifies against a sample
 	 */
 	public function testMaskEcc():void{
-		$this->dataInterface->setData($this->testdata);
+		$this->dataInterface->setData([$this->testdata]);
 
 		$maskECC = $this->reflection->getMethod('maskECC');
 		$maskECC->setAccessible(true);
@@ -83,7 +76,7 @@ abstract class DatainterfaceTestAbstract extends TestCase{
 	 * @dataProvider MaskPatternProvider
 	 */
 	public function testInitMatrix(int $maskPattern):void{
-		$this->dataInterface->setData($this->testdata);
+		$this->dataInterface->setData([$this->testdata]);
 
 		$matrix = $this->dataInterface->initMatrix($maskPattern);
 
@@ -95,7 +88,7 @@ abstract class DatainterfaceTestAbstract extends TestCase{
 	 * Tests getting the minimum QR version for the given data
 	 */
 	public function testGetMinimumVersion():void{
-		$this->dataInterface->setData($this->testdata);
+		$this->dataInterface->setData([$this->testdata]);
 
 		$getMinimumVersion = $this->reflection->getMethod('getMinimumVersion');
 		$getMinimumVersion->setAccessible(true);
@@ -109,9 +102,12 @@ abstract class DatainterfaceTestAbstract extends TestCase{
 	public function testGetMinimumVersionException():void{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('data exceeds');
+		[$class, $data] = $this->testdata;
 
-		$this->dataInterface = $this->getDataInterfaceInstance(new QROptions(['version' => QRCode::VERSION_AUTO]));
-		$this->dataInterface->setData(str_repeat($this->testdata, 1337));
+		$this->dataInterface = new QRData(
+			new QROptions(['version' => QRCode::VERSION_AUTO]),
+			[[$class, str_repeat($data, 1337)]]
+		);
 	}
 
 	/**
@@ -120,8 +116,9 @@ abstract class DatainterfaceTestAbstract extends TestCase{
 	public function testCodeLengthOverflowException():void{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('code length overflow');
+		[$class, $data] = $this->testdata;
 
-		$this->dataInterface->setData(str_repeat($this->testdata, 1337));
+		$this->dataInterface->setData([[$class, str_repeat($data, 1337)]]);
 	}
 
 }

+ 4 - 13
tests/Data/KanjiTest.php

@@ -12,8 +12,7 @@
 
 namespace chillerlan\QRCodeTest\Data;
 
-use chillerlan\QRCode\QROptions;
-use chillerlan\QRCode\Data\{Kanji, QRCodeDataException, QRDataInterface};
+use chillerlan\QRCode\Data\{Kanji, QRCodeDataException};
 
 /**
  * Tests the Kanji class
@@ -21,7 +20,7 @@ use chillerlan\QRCode\Data\{Kanji, QRCodeDataException, QRDataInterface};
 final class KanjiTest extends DatainterfaceTestAbstract{
 
 	/** @internal */
-	protected string $testdata = '茗荷茗荷茗荷茗荷茗荷';
+	protected array $testdata = [Kanji::class, '茗荷茗荷茗荷茗荷茗荷'];
 
 	/** @internal */
 	protected array  $expected = [
@@ -40,14 +39,6 @@ final class KanjiTest extends DatainterfaceTestAbstract{
 		96, 113, 54, 191
 	];
 
-	/**
-	 * @inheritDoc
-	 * @internal
-	 */
-	protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{
-		return new Kanji($options);
-	}
-
 	/**
 	 * Tests if an exception is thrown when an invalid character is encountered
 	 */
@@ -55,7 +46,7 @@ final class KanjiTest extends DatainterfaceTestAbstract{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('illegal char at 1 [16191]');
 
-		$this->dataInterface->setData('ÃÃ');
+		$this->dataInterface->setData([[Kanji::class, 'ÃÃ']]);
 	}
 
 	/**
@@ -65,7 +56,7 @@ final class KanjiTest extends DatainterfaceTestAbstract{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('illegal char at 1');
 
-		$this->dataInterface->setData('Ã');
+		$this->dataInterface->setData([[Kanji::class, 'Ã']]);
 	}
 
 }

+ 3 - 3
tests/Data/MaskPatternTesterTest.php

@@ -13,7 +13,7 @@
 namespace chillerlan\QRCodeTest\Data;
 
 use chillerlan\QRCode\QROptions;
-use chillerlan\QRCode\Data\{Byte, MaskPatternTester};
+use chillerlan\QRCode\Data\{Byte, MaskPatternTester, QRData};
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -25,7 +25,7 @@ final class MaskPatternTesterTest extends TestCase{
 	 * Tests getting the best mask pattern
 	 */
 	public function testMaskpattern():void{
-		$dataInterface = new Byte(new QROptions(['version' => 10]), 'test');
+		$dataInterface = new QRData(new QROptions(['version' => 10]), [[Byte::class, 'test']]);
 
 		$this::assertSame(3, (new MaskPatternTester($dataInterface))->getBestMaskPattern());
 	}
@@ -34,7 +34,7 @@ final class MaskPatternTesterTest extends TestCase{
 	 * Tests getting the penalty value for a given mask pattern
 	 */
 	public function testMaskpatternID():void{
-		$dataInterface = new Byte(new QROptions(['version' => 10]), 'test');
+		$dataInterface = new QRData(new QROptions(['version' => 10]), [[Byte::class, 'test']]);
 
 		$this::assertSame(4243, (new MaskPatternTester($dataInterface))->testPattern(3));
 	}

+ 3 - 12
tests/Data/NumberTest.php

@@ -12,8 +12,7 @@
 
 namespace chillerlan\QRCodeTest\Data;
 
-use chillerlan\QRCode\QROptions;
-use chillerlan\QRCode\Data\{Number, QRCodeDataException, QRDataInterface};
+use chillerlan\QRCode\Data\{Number, QRCodeDataException};
 
 /**
  * Tests the Number class
@@ -21,7 +20,7 @@ use chillerlan\QRCode\Data\{Number, QRCodeDataException, QRDataInterface};
 final class NumberTest extends DatainterfaceTestAbstract{
 
 	/** @internal */
-	protected string $testdata  = '0123456789';
+	protected array $testdata  = [Number::class, '0123456789'];
 
 	/** @internal */
 	protected array $expected = [
@@ -40,14 +39,6 @@ final class NumberTest extends DatainterfaceTestAbstract{
 		89, 63, 168, 151
 	];
 
-	/**
-	 * @inheritDoc
-	 * @internal
-	 */
-	protected function getDataInterfaceInstance(QROptions $options):QRDataInterface{
-		return new Number($options);
-	}
-
 	/**
 	 * Tests if an exception is thrown when an invalid character is encountered
 	 */
@@ -55,7 +46,7 @@ final class NumberTest extends DatainterfaceTestAbstract{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('illegal char: "#" [35]');
 
-		$this->dataInterface->setData('#');
+		$this->dataInterface->setData([[Number::class, '#']]);
 	}
 
 }

+ 4 - 4
tests/Data/QRMatrixTest.php

@@ -315,7 +315,7 @@ final class QRMatrixTest extends TestCase{
 		$o->eccLevel     = QRCode::ECC_H;
 		$o->addQuietzone = false;
 
-		$matrix = (new QRCode($o))->getMatrix('testdata');
+		$matrix = (new QRCode($o))->addByteSegment('testdata')->getMatrix();
 		// also testing size adjustment to uneven numbers
 		$matrix->setLogoSpace(20, 14);
 
@@ -335,7 +335,7 @@ final class QRMatrixTest extends TestCase{
 		$o->addQuietzone  = true;
 		$o->quietzoneSize = 10;
 
-		$m = (new QRCode($o))->getMatrix('testdata');
+		$m = (new QRCode($o))->addByteSegment('testdata')->getMatrix();
 
 		// logo space should not overwrite quiet zone & function patterns
 		$m->setLogoSpace(21, 21, -10, -10);
@@ -360,7 +360,7 @@ final class QRMatrixTest extends TestCase{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('ECC level "H" required to add logo space');
 
-		(new QRCode)->getMatrix('testdata')->setLogoSpace(50, 50);
+		(new QRCode)->addByteSegment('testdata')->getMatrix()->setLogoSpace(50, 50);
 	}
 
 	public function testSetLogoSpaceMaxSizeException():void{
@@ -371,7 +371,7 @@ final class QRMatrixTest extends TestCase{
 		$o->version  = 5;
 		$o->eccLevel = QRCode::ECC_H;
 
-		(new QRCode($o))->getMatrix('testdata')->setLogoSpace(50, 50);
+		(new QRCode($o))->addByteSegment('testdata')->getMatrix()->setLogoSpace(50, 50);
 	}
 
 }

+ 2 - 2
tests/Output/QROutputTestAbstract.php

@@ -13,7 +13,7 @@
 namespace chillerlan\QRCodeTest\Output;
 
 use chillerlan\QRCode\{QRCode, QROptions};
-use chillerlan\QRCode\Data\{Byte, QRMatrix};
+use chillerlan\QRCode\Data\{Byte, QRData, QRMatrix};
 use chillerlan\QRCode\Output\{QRCodeOutputException, QROutputInterface};
 use PHPUnit\Framework\TestCase;
 
@@ -48,7 +48,7 @@ abstract class QROutputTestAbstract extends TestCase{
 		}
 
 		$this->options         = new QROptions;
-		$this->matrix          = (new Byte($this->options, 'testdata'))->initMatrix(0);
+		$this->matrix          = (new QRData($this->options, [[Byte::class, 'testdata']]))->initMatrix(0);
 		$this->outputInterface = $this->getOutputInterface($this->options);
 	}
 

+ 2 - 52
tests/QRCodeTest.php

@@ -13,12 +13,10 @@
 namespace chillerlan\QRCodeTest;
 
 use chillerlan\QRCode\{QROptions, QRCode};
-use chillerlan\QRCode\Data\{AlphaNum, Byte, Kanji, Number, QRCodeDataException};
+use chillerlan\QRCode\Data\QRCodeDataException;
 use chillerlan\QRCode\Output\QRCodeOutputException;
 use PHPUnit\Framework\TestCase;
 
-use function random_bytes;
-
 /**
  * Tests basic functions of the QRCode class
  */
@@ -97,55 +95,7 @@ class QRCodeTest extends TestCase{
 		$this->expectException(QRCodeDataException::class);
 		$this->expectExceptionMessage('QRCode::getMatrix() No data given.');
 
-		$this->qrcode->getMatrix('');
-	}
-
-	/**
-	 * test whether stings are trimmed (they are not) - i'm still torn on that (see isByte)
-	 */
-	public function testAvoidTrimming():void{
-		$m1 = $this->qrcode->getMatrix('hello')->matrix();
-		$m2 = $this->qrcode->getMatrix('hello ')->matrix(); // added space
-
-		$this::assertNotSame($m1, $m2);
-	}
-
-	/**
-	 * tests if the data mode is overriden if QROptions::$dataModeOverride is set to a valid value
-	 *
-	 * @see https://github.com/chillerlan/php-qrcode/issues/39
-	 */
-	public function testDataModeOverride():void{
-
-		// no (or invalid) value set - auto detection
-		$this->options->dataModeOverride = 'foo';
-		$this->qrcode = new QRCode;
-
-		$this::assertInstanceOf(Number::class, $this->qrcode->initDataInterface('123'));
-		$this::assertInstanceOf(AlphaNum::class, $this->qrcode->initDataInterface('ABC123'));
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface(random_bytes(32)));
-		$this::assertInstanceOf(Kanji::class, $this->qrcode->initDataInterface('茗荷'));
-
-		// data mode set: force the given data mode
-		$this->options->dataModeOverride = 'Byte';
-		$this->qrcode = new QRCode($this->options);
-
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface('123'));
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface('ABC123'));
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface(random_bytes(32)));
-		$this::assertInstanceOf(Byte::class, $this->qrcode->initDataInterface('茗荷'));
-	}
-
-	/**
-	 * tests if an exception is thrown when an invalid character occurs when forcing a data mode other than Byte
-	 */
-	public function testDataModeOverrideError():void{
-		$this->expectException(QRCodeDataException::class);
-		$this->expectExceptionMessage('illegal char:');
-
-		$this->options->dataModeOverride = 'AlphaNum';
-
-		(new QRCode($this->options))->initDataInterface(random_bytes(32));
+		$this->qrcode->getMatrix();
 	}
 
 }