QRData.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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\QROptions;
  12. use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, Version};
  13. use chillerlan\Settings\SettingsContainerInterface;
  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. private SettingsContainerInterface|QROptions $options;
  23. /**
  24. * a BitBuffer instance
  25. */
  26. private BitBuffer $bitBuffer;
  27. /**
  28. * an EccLevel instance
  29. */
  30. private EccLevel $eccLevel;
  31. /**
  32. * current QR Code version
  33. */
  34. private Version $version;
  35. /**
  36. * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
  37. */
  38. private array $dataSegments = [];
  39. /**
  40. * Max bits for the current ECC mode
  41. *
  42. * @var int[]
  43. */
  44. private array $maxBitsForEcc;
  45. /**
  46. * QRData constructor.
  47. *
  48. * @param \chillerlan\QRCode\Data\QRDataModeInterface[] $dataSegments
  49. */
  50. public function __construct(SettingsContainerInterface|QROptions $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():QRMatrix{
  104. return (new QRMatrix($this->version, $this->eccLevel))
  105. ->initFunctionalPatterns()
  106. ->writeCodewords($this->bitBuffer)
  107. ;
  108. }
  109. /**
  110. * estimates the total length of the several mode segments in order to guess the minimum version
  111. *
  112. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  113. */
  114. public function estimateTotalBitLength():int{
  115. $length = 0;
  116. foreach($this->dataSegments as $segment){
  117. // data length of the current segment
  118. $length += $segment->getLengthInBits();
  119. // +4 bits for the mode descriptor
  120. $length += 4;
  121. // Hanzi mode sets an additional 4 bit long subset identifier
  122. if($segment instanceof Hanzi){
  123. $length += 4;
  124. }
  125. }
  126. $provisionalVersion = null;
  127. /** @var int $version */
  128. foreach($this->maxBitsForEcc as $version => $maxBits){
  129. if($length <= $maxBits){
  130. $provisionalVersion = $version;
  131. }
  132. }
  133. if($provisionalVersion !== null){
  134. // add character count indicator bits for the provisional version
  135. foreach($this->dataSegments as $segment){
  136. $length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion);
  137. }
  138. // it seems that in some cases the estimated total length is not 100% accurate,
  139. // so we substract 4 bits from the total when not in mixed mode
  140. if(count($this->dataSegments) <= 1){
  141. $length -= 4;
  142. }
  143. // we've got a match!
  144. // or let's see if there's a higher version number available
  145. if($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])){
  146. return $length;
  147. }
  148. }
  149. throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
  150. }
  151. /**
  152. * returns the minimum version number for the given string
  153. *
  154. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  155. */
  156. public function getMinimumVersion():Version{
  157. if($this->options->version !== Version::AUTO){
  158. return new Version($this->options->version);
  159. }
  160. $total = $this->estimateTotalBitLength();
  161. // guess the version number within the given range
  162. for($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++){
  163. if($total <= ($this->maxBitsForEcc[$version] - 4)){
  164. return new Version($version);
  165. }
  166. }
  167. // it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
  168. throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
  169. }
  170. /**
  171. * creates a BitBuffer and writes the string data to it
  172. *
  173. * @throws \chillerlan\QRCode\QRCodeException on data overflow
  174. */
  175. private function writeBitBuffer():void{
  176. $MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version);
  177. foreach($this->dataSegments as $segment){
  178. $segment->write($this->bitBuffer, $this->version->getVersionNumber());
  179. }
  180. // overflow, likely caused due to invalid version setting
  181. if($this->bitBuffer->getLength() > $MAX_BITS){
  182. throw new QRCodeDataException(
  183. sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
  184. );
  185. }
  186. // add terminator (ISO/IEC 18004:2000 Table 2)
  187. if(($this->bitBuffer->getLength() + 4) <= $MAX_BITS){
  188. $this->bitBuffer->put(Mode::TERMINATOR, 4);
  189. }
  190. // Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
  191. // if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
  192. // by the addition of padding bits with binary value 0
  193. while(($this->bitBuffer->getLength() % 8) !== 0){
  194. if($this->bitBuffer->getLength() === $MAX_BITS){
  195. break;
  196. }
  197. $this->bitBuffer->putBit(false);
  198. }
  199. // The message bit stream shall then be extended to fill the data capacity of the symbol
  200. // corresponding to the Version and Error Correction Level, by the addition of the Pad
  201. // Codewords 11101100 and 00010001 alternately.
  202. $alternate = false;
  203. while(($this->bitBuffer->getLength() + 8) <= $MAX_BITS){
  204. $this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8);
  205. $alternate = !$alternate;
  206. }
  207. // In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
  208. // to the end of the message in order exactly to fill the symbol capacity
  209. while($this->bitBuffer->getLength() <= $MAX_BITS){
  210. $this->bitBuffer->putBit(false);
  211. }
  212. }
  213. }