QRData.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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, Version};
  12. use chillerlan\Settings\SettingsContainerInterface;
  13. use function count;
  14. use function sprintf;
  15. /**
  16. * Processes the binary data and maps it on a QRMatrix 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. public function __construct(SettingsContainerInterface $options, array $dataSegments = []){
  51. $this->options = $options;
  52. $this->bitBuffer = new BitBuffer;
  53. $this->eccLevel = new EccLevel($this->options->eccLevel);
  54. $this->maxBitsForEcc = $this->eccLevel->getMaxBits();
  55. $this->setData($dataSegments);
  56. }
  57. /**
  58. * Sets the data string (internally called by the constructor)
  59. *
  60. * Subsequent calls will overwrite the current state - use the QRCode::add*Segement() method instead
  61. *
  62. * @param \chillerlan\QRCode\Data\QRDataModeInterface[] $dataSegments
  63. */
  64. public function setData(array $dataSegments):self{
  65. $this->dataSegments = $dataSegments;
  66. $this->version = $this->getMinimumVersion();
  67. $this->bitBuffer->clear();
  68. $this->writeBitBuffer();
  69. return $this;
  70. }
  71. /**
  72. * Returns the current BitBuffer instance
  73. *
  74. * @codeCoverageIgnore
  75. */
  76. public function getBitBuffer():BitBuffer{
  77. return $this->bitBuffer;
  78. }
  79. /**
  80. * Sets a BitBuffer object
  81. *
  82. * This can be used instead of setData(), however, the version auto-detection is not available in this case.
  83. * The version needs to match the length bits range for the data mode the data has been encoded with,
  84. * additionally the bit array needs to contain enough pad bits.
  85. *
  86. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  87. */
  88. public function setBitBuffer(BitBuffer $bitBuffer):self{
  89. if($this->options->version === Version::AUTO){
  90. throw new QRCodeDataException('version auto detection is not available');
  91. }
  92. if($bitBuffer->getLength() === 0){
  93. throw new QRCodeDataException('the given BitBuffer is empty');
  94. }
  95. $this->dataSegments = [];
  96. $this->bitBuffer = $bitBuffer;
  97. $this->version = new Version($this->options->version);
  98. return $this;
  99. }
  100. /**
  101. * returns a fresh matrix object with the data written and masked with the given $maskPattern
  102. */
  103. public function writeMatrix(MaskPattern $maskPattern):QRMatrix{
  104. return (new QRMatrix($this->version, $this->eccLevel))
  105. ->initFunctionalPatterns()
  106. ->writeCodewords($this->bitBuffer)
  107. ->setFormatInfo($maskPattern)
  108. ->mask($maskPattern)
  109. ;
  110. }
  111. /**
  112. * estimates the total length of the several mode segments in order to guess the minimum version
  113. *
  114. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  115. */
  116. public function estimateTotalBitLength():int{
  117. $length = 0;
  118. foreach($this->dataSegments as $segment){
  119. // data length of the current segment
  120. $length += $segment->getLengthInBits();
  121. // +4 bits for the mode descriptor
  122. $length += 4;
  123. // Hanzi mode sets an additional 4 bit long subset identifier
  124. if($segment instanceof Hanzi){
  125. $length += 4;
  126. }
  127. }
  128. $provisionalVersion = null;
  129. foreach($this->maxBitsForEcc as $version => $maxBits){
  130. if($length <= $maxBits){
  131. $provisionalVersion = $version;
  132. }
  133. }
  134. if($provisionalVersion !== null){
  135. // add character count indicator bits for the provisional version
  136. foreach($this->dataSegments as $segment){
  137. $length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion);
  138. }
  139. // it seems that in some cases the estimated total length is not 100% accurate,
  140. // so we substract 4 bits from the total when not in mixed mode
  141. if(count($this->dataSegments) <= 1){
  142. $length -= 4;
  143. }
  144. // we've got a match!
  145. // or let's see if there's a higher version number available
  146. if($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])){
  147. return $length;
  148. }
  149. }
  150. throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
  151. }
  152. /**
  153. * returns the minimum version number for the given string
  154. *
  155. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  156. */
  157. public function getMinimumVersion():Version{
  158. if($this->options->version !== Version::AUTO){
  159. return new Version($this->options->version);
  160. }
  161. $total = $this->estimateTotalBitLength();
  162. // guess the version number within the given range
  163. for($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++){
  164. if($total <= $this->maxBitsForEcc[$version]){
  165. return new Version($version);
  166. }
  167. }
  168. // it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
  169. throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
  170. }
  171. /**
  172. * creates a BitBuffer and writes the string data to it
  173. *
  174. * @throws \chillerlan\QRCode\QRCodeException on data overflow
  175. */
  176. private function writeBitBuffer():void{
  177. $MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version);
  178. foreach($this->dataSegments as $segment){
  179. $segment->write($this->bitBuffer, $this->version->getVersionNumber());
  180. }
  181. // overflow, likely caused due to invalid version setting
  182. if($this->bitBuffer->getLength() > $MAX_BITS){
  183. throw new QRCodeDataException(
  184. sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
  185. );
  186. }
  187. // add terminator (ISO/IEC 18004:2000 Table 2)
  188. if(($this->bitBuffer->getLength() + 4) <= $MAX_BITS){
  189. $this->bitBuffer->put(Mode::TERMINATOR, 4);
  190. }
  191. // Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
  192. // if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
  193. // by the addition of padding bits with binary value 0
  194. while(($this->bitBuffer->getLength() % 8) !== 0){
  195. if($this->bitBuffer->getLength() === $MAX_BITS){
  196. break;
  197. }
  198. $this->bitBuffer->putBit(false);
  199. }
  200. // The message bit stream shall then be extended to fill the data capacity of the symbol
  201. // corresponding to the Version and Error Correction Level, by the addition of the Pad
  202. // Codewords 11101100 and 00010001 alternately.
  203. $alternate = false;
  204. while(($this->bitBuffer->getLength() + 8) <= $MAX_BITS){
  205. $this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8);
  206. $alternate = !$alternate;
  207. }
  208. // In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
  209. // to the end of the message in order exactly to fill the symbol capacity
  210. while($this->bitBuffer->getLength() <= $MAX_BITS){
  211. $this->bitBuffer->putBit(false);
  212. }
  213. }
  214. }