Number.php 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. <?php
  2. /**
  3. * Class Number
  4. *
  5. * @created 26.11.2015
  6. * @author Smiley <smiley@chillerlan.net>
  7. * @copyright 2015 Smiley
  8. * @license MIT
  9. */
  10. namespace chillerlan\QRCode\Data;
  11. use chillerlan\QRCode\Common\{BitBuffer, Mode};
  12. use function array_flip, ceil, intdiv, str_split, substr, unpack;
  13. /**
  14. * Numeric mode: decimal digits 0 to 9
  15. *
  16. * ISO/IEC 18004:2000 Section 8.3.2
  17. * ISO/IEC 18004:2000 Section 8.4.2
  18. */
  19. final class Number extends QRDataModeAbstract{
  20. /**
  21. * @var int[]
  22. */
  23. private const NUMBER_TO_ORD = [
  24. '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
  25. ];
  26. /**
  27. * @inheritDoc
  28. */
  29. public const DATAMODE = Mode::NUMBER;
  30. /**
  31. * @inheritDoc
  32. */
  33. public function getLengthInBits():int{
  34. return (int)ceil($this->getCharCount() * (10 / 3));
  35. }
  36. /**
  37. * @inheritDoc
  38. */
  39. public static function validateString(string $string):bool{
  40. if($string === ''){
  41. return false;
  42. }
  43. foreach(str_split($string) as $chr){
  44. if(!isset(self::NUMBER_TO_ORD[$chr])){
  45. return false;
  46. }
  47. }
  48. return true;
  49. }
  50. /**
  51. * @inheritDoc
  52. */
  53. public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
  54. $len = $this->getCharCount();
  55. $bitBuffer
  56. ->put(self::DATAMODE, 4)
  57. ->put($len, $this::getLengthBits($versionNumber))
  58. ;
  59. $i = 0;
  60. // encode numeric triplets in 10 bits
  61. while(($i + 2) < $len){
  62. $bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10);
  63. $i += 3;
  64. }
  65. if($i < $len){
  66. // encode 2 remaining numbers in 7 bits
  67. if(($len - $i) === 2){
  68. $bitBuffer->put($this->parseInt(substr($this->data, $i, 2)), 7);
  69. }
  70. // encode one remaining number in 4 bits
  71. elseif(($len - $i) === 1){
  72. $bitBuffer->put($this->parseInt(substr($this->data, $i, 1)), 4);
  73. }
  74. }
  75. return $this;
  76. }
  77. /**
  78. * get the code for the given numeric string
  79. */
  80. private function parseInt(string $string):int{
  81. $num = 0;
  82. foreach(unpack('C*', $string) as $chr){
  83. $num = ($num * 10 + $chr - 48);
  84. }
  85. return $num;
  86. }
  87. /**
  88. * @inheritDoc
  89. *
  90. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  91. */
  92. public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
  93. $length = $bitBuffer->read(self::getLengthBits($versionNumber));
  94. $charmap = array_flip(self::NUMBER_TO_ORD);
  95. // @todo
  96. $toNumericChar = function(int $ord) use ($charmap):string{
  97. if(isset($charmap[$ord])){
  98. return $charmap[$ord];
  99. }
  100. throw new QRCodeDataException('invalid character value: '.$ord);
  101. };
  102. $result = '';
  103. // Read three digits at a time
  104. while($length >= 3){
  105. // Each 10 bits encodes three digits
  106. if($bitBuffer->available() < 10){
  107. throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
  108. }
  109. $threeDigitsBits = $bitBuffer->read(10);
  110. if($threeDigitsBits >= 1000){
  111. throw new QRCodeDataException('error decoding numeric value');
  112. }
  113. $result .= $toNumericChar(intdiv($threeDigitsBits, 100));
  114. $result .= $toNumericChar(intdiv($threeDigitsBits, 10) % 10);
  115. $result .= $toNumericChar($threeDigitsBits % 10);
  116. $length -= 3;
  117. }
  118. if($length === 2){
  119. // Two digits left over to read, encoded in 7 bits
  120. if($bitBuffer->available() < 7){
  121. throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
  122. }
  123. $twoDigitsBits = $bitBuffer->read(7);
  124. if($twoDigitsBits >= 100){
  125. throw new QRCodeDataException('error decoding numeric value');
  126. }
  127. $result .= $toNumericChar(intdiv($twoDigitsBits, 10));
  128. $result .= $toNumericChar($twoDigitsBits % 10);
  129. }
  130. elseif($length === 1){
  131. // One digit left over to read
  132. if($bitBuffer->available() < 4){
  133. throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
  134. }
  135. $digitBits = $bitBuffer->read(4);
  136. if($digitBits >= 10){
  137. throw new QRCodeDataException('error decoding numeric value');
  138. }
  139. $result .= $toNumericChar($digitBits);
  140. }
  141. return $result;
  142. }
  143. }