QRData.php 5.4 KB

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