QRData.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. /**
  3. * Class QRData
  4. *
  5. * @filesource QRData.php
  6. * @created 25.11.2015
  7. * @package chillerlan\QRCode\Data
  8. * @author Smiley <smiley@chillerlan.net>
  9. * @copyright 2015 Smiley
  10. * @license MIT
  11. */
  12. namespace chillerlan\QRCode\Data;
  13. use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Mode, ReedSolomonEncoder, Version};
  14. use chillerlan\QRCode\QRCode;
  15. use chillerlan\Settings\SettingsContainerInterface;
  16. use function range, sprintf;
  17. /**
  18. * Processes the binary data and maps it on a matrix which is then being returned
  19. */
  20. final class QRData{
  21. /**
  22. * the options instance
  23. *
  24. * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
  25. */
  26. protected SettingsContainerInterface $options;
  27. /**
  28. * a BitBuffer instance
  29. */
  30. protected BitBuffer $bitBuffer;
  31. /**
  32. * an EccLevel instance
  33. */
  34. protected EccLevel $eccLevel;
  35. /**
  36. * current QR Code version
  37. */
  38. protected Version $version;
  39. /**
  40. * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
  41. */
  42. protected array $dataSegments = [];
  43. /**
  44. * Max bits for the current ECC mode
  45. *
  46. * @var int[]
  47. */
  48. protected array $maxBitsForEcc;
  49. /**
  50. * QRData constructor.
  51. *
  52. * @param \chillerlan\Settings\SettingsContainerInterface $options
  53. * @param \chillerlan\QRCode\Data\QRDataModeInterface[]|null $dataSegments
  54. */
  55. public function __construct(SettingsContainerInterface $options, array $dataSegments = null){
  56. $this->options = $options;
  57. $this->bitBuffer = new BitBuffer;
  58. $this->eccLevel = new EccLevel($this->options->eccLevel);
  59. $this->maxBitsForEcc = $this->eccLevel->getMaxBits();
  60. if(!empty($dataSegments)){
  61. $this->setData($dataSegments);
  62. }
  63. }
  64. /**
  65. * Sets the data string (internally called by the constructor)
  66. */
  67. public function setData(array $dataSegments):QRData{
  68. $this->dataSegments = $dataSegments;
  69. $version = $this->options->version === QRCode::VERSION_AUTO
  70. ? $this->getMinimumVersion()
  71. : $this->options->version;
  72. $this->version = new Version($version);
  73. $this->writeBitBuffer();
  74. return $this;
  75. }
  76. /**
  77. * returns a fresh matrix object with the data written for the given $maskPattern
  78. */
  79. public function writeMatrix(MaskPattern $maskPattern, bool $test = null):QRMatrix{
  80. $data = (new ReedSolomonEncoder)->interleaveEcBytes($this->bitBuffer, $this->version, $this->eccLevel);
  81. return (new QRMatrix($this->version, $this->eccLevel))
  82. ->init($maskPattern, $test)
  83. ->mapData($data)
  84. ->mask($maskPattern)
  85. ;
  86. }
  87. /**
  88. * estimates the total length of the several mode segments in order to guess the minimum version
  89. *
  90. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  91. */
  92. protected function estimateTotalBitLength():int{
  93. $length = 0;
  94. $margin = 0;
  95. foreach($this->dataSegments as $segment){
  96. // data length in bits of the current segment +4 bits for each mode descriptor
  97. $length += ($segment->getLengthInBits() + Mode::getLengthBitsForMode($segment->getDataMode())[0] + 4);
  98. if(!$segment instanceof ECI){
  99. // mode length bits margin to the next breakpoint
  100. $margin += ($segment instanceof Byte ? 8 : 2);
  101. }
  102. }
  103. foreach([9, 26, 40] as $breakpoint){
  104. // length bits for the first breakpoint have already been added
  105. if($breakpoint > 9){
  106. $length += $margin;
  107. }
  108. if($length < $this->maxBitsForEcc[$breakpoint]){
  109. return $length;
  110. }
  111. }
  112. throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
  113. }
  114. /**
  115. * returns the minimum version number for the given string
  116. *
  117. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  118. */
  119. protected function getMinimumVersion():int{
  120. $total = $this->estimateTotalBitLength();
  121. // guess the version number within the given range
  122. foreach(range($this->options->versionMin, $this->options->versionMax) as $version){
  123. if($total <= $this->maxBitsForEcc[$version]){
  124. return $version;
  125. }
  126. }
  127. // it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
  128. throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
  129. }
  130. /**
  131. * creates a BitBuffer and writes the string data to it
  132. *
  133. * @throws \chillerlan\QRCode\QRCodeException on data overflow
  134. */
  135. protected function writeBitBuffer():void{
  136. $version = $this->version->getVersionNumber();
  137. $MAX_BITS = $this->maxBitsForEcc[$version];
  138. foreach($this->dataSegments as $segment){
  139. $segment->write($this->bitBuffer, $version);
  140. }
  141. // overflow, likely caused due to invalid version setting
  142. if($this->bitBuffer->getLength() > $MAX_BITS){
  143. throw new QRCodeDataException(
  144. sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
  145. );
  146. }
  147. // add terminator (ISO/IEC 18004:2000 Table 2)
  148. if($this->bitBuffer->getLength() + 4 <= $MAX_BITS){
  149. $this->bitBuffer->put(0b0000, 4);
  150. }
  151. // Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
  152. // if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
  153. // by the addition of padding bits with binary value 0
  154. while($this->bitBuffer->getLength() % 8 !== 0){
  155. $this->bitBuffer->putBit(false);
  156. }
  157. // The message bit stream shall then be extended to fill the data capacity of the symbol
  158. // corresponding to the Version and Error Correction Level, by the addition of the Pad
  159. // Codewords 11101100 and 00010001 alternately.
  160. while(true){
  161. if($this->bitBuffer->getLength() >= $MAX_BITS){
  162. break;
  163. }
  164. $this->bitBuffer->put(0b11101100, 8);
  165. if($this->bitBuffer->getLength() >= $MAX_BITS){
  166. break;
  167. }
  168. $this->bitBuffer->put(0b00010001, 8);
  169. }
  170. }
  171. }