Source: Data/QRData.js

  1. /**
  2. * @created 11.07.2022
  3. * @author smiley <smiley@chillerlan.net>
  4. * @copyright 2022 smiley
  5. * @license MIT
  6. */
  7. import BitBuffer from '../Common/BitBuffer.js';
  8. import EccLevel from '../Common/EccLevel.js';
  9. import Version from '../Common/Version.js';
  10. import QRMatrix from './QRMatrix.js';
  11. import Mode from '../Common/Mode.js';
  12. import QRCodeDataException from './QRCodeDataException.js';
  13. import {VERSION_AUTO} from '../Common/constants.js';
  14. /**
  15. * Processes the binary data and maps it on a matrix which is then being returned
  16. */
  17. export default class QRData{
  18. /**
  19. * the options instance
  20. *
  21. * @type {QROptions}
  22. * @private
  23. */
  24. options;
  25. /**
  26. * a BitBuffer instance
  27. *
  28. * @type {BitBuffer}
  29. * @private
  30. */
  31. bitBuffer;
  32. /**
  33. * an EccLevel instance
  34. *
  35. * @type {EccLevel}
  36. * @private
  37. */
  38. eccLevel;
  39. /**
  40. * current QR Code version
  41. *
  42. * @type {Version}
  43. * @private
  44. */
  45. version;
  46. /**
  47. * @type {QRDataModeAbstract[]|Array}
  48. * @private
  49. */
  50. dataSegments = [];
  51. /**
  52. * Max bits for the current ECC mode
  53. *
  54. * @type {number[]|int[]}
  55. * @private
  56. */
  57. maxBitsForEcc;
  58. /**
  59. * QRData constructor.
  60. *
  61. * @param {QROptions} $options
  62. * @param {QRDataModeAbstract[]} $dataSegments
  63. */
  64. constructor($options, $dataSegments = []){
  65. this.options = $options;
  66. this.bitBuffer = new BitBuffer;
  67. this.eccLevel = new EccLevel(this.options.eccLevel);
  68. this.maxBitsForEcc = this.eccLevel.getMaxBits();
  69. this.setData($dataSegments);
  70. }
  71. /**
  72. * Sets the data string (internally called by the constructor)
  73. *
  74. * @param {QRDataModeAbstract[]} $dataSegments
  75. *
  76. * @returns {QRData}
  77. */
  78. setData($dataSegments){
  79. this.dataSegments = $dataSegments;
  80. this.version = this.getMinimumVersion();
  81. this.bitBuffer.clear();
  82. this.writeBitBuffer();
  83. return this;
  84. }
  85. /**
  86. * returns a fresh matrix object with the data written and masked with the given $maskPattern
  87. *
  88. * @returns {QRMatrix}
  89. */
  90. writeMatrix(){
  91. return (new QRMatrix(this.version, this.eccLevel))
  92. .initFunctionalPatterns()
  93. .writeCodewords(this.bitBuffer)
  94. ;
  95. }
  96. /**
  97. * estimates the total length of the several mode segments in order to guess the minimum version
  98. *
  99. * @returns {number|int}
  100. * @throws {QRCodeDataException}
  101. * @private
  102. */
  103. estimateTotalBitLength(){
  104. let $length = 0;
  105. let $segment;
  106. for($segment of this.dataSegments){
  107. // data length of the current segment
  108. $length += $segment.getLengthInBits();
  109. // +4 bits for the mode descriptor
  110. $length += 4;
  111. }
  112. let $provisionalVersion = null;
  113. for(let $version in this.maxBitsForEcc){
  114. if($version === 0){ // JS array/object weirdness vs php arrays...
  115. continue;
  116. }
  117. if($length <= this.maxBitsForEcc[$version]){
  118. $provisionalVersion = $version;
  119. }
  120. }
  121. if($provisionalVersion !== null){
  122. // add character count indicator bits for the provisional version
  123. for($segment of this.dataSegments){
  124. $length += Mode.getLengthBitsForVersion($segment.datamode, $provisionalVersion);
  125. }
  126. // it seems that in some cases the estimated total length is not 100% accurate,
  127. // so we substract 4 bits from the total when not in mixed mode
  128. if(this.dataSegments.length <= 1){
  129. $length -= 4;
  130. }
  131. // we've got a match!
  132. // or let's see if there's a higher version number available
  133. if($length <= this.maxBitsForEcc[$provisionalVersion] || this.maxBitsForEcc[($provisionalVersion + 1)]){
  134. return $length;
  135. }
  136. }
  137. throw new QRCodeDataException(`estimated data exceeds ${$length} bits`);
  138. }
  139. /**
  140. * returns the minimum version number for the given string
  141. *
  142. * @return {Version}
  143. * @throws {QRCodeDataException}
  144. * @private
  145. */
  146. getMinimumVersion(){
  147. if(this.options.version !== VERSION_AUTO){
  148. return new Version(this.options.version);
  149. }
  150. let $total = this.estimateTotalBitLength();
  151. // guess the version number within the given range
  152. for(let $version = this.options.versionMin; $version <= this.options.versionMax; $version++){
  153. if($total <= (this.maxBitsForEcc[$version] - 4)){
  154. return new Version($version);
  155. }
  156. }
  157. /* c8 ignore next 2 */
  158. // it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
  159. throw new QRCodeDataException('failed to guess minimum version');
  160. }
  161. /**
  162. * creates a BitBuffer and writes the string data to it
  163. *
  164. * @returns {void}
  165. * @throws {QRCodeException} on data overflow
  166. * @private
  167. */
  168. writeBitBuffer(){
  169. let $MAX_BITS = this.eccLevel.getMaxBitsForVersion(this.version);
  170. for(let $i = 0; $i < this.dataSegments.length; $i++){
  171. this.dataSegments[$i].write(this.bitBuffer, this.version.getVersionNumber());
  172. }
  173. // overflow, likely caused due to invalid version setting
  174. if(this.bitBuffer.getLength() > $MAX_BITS){
  175. throw new QRCodeDataException(`code length overflow. (${this.bitBuffer.getLength()} > ${$MAX_BITS} bit)`);
  176. }
  177. // add terminator (ISO/IEC 18004:2000 Table 2)
  178. if(this.bitBuffer.getLength() + 4 <= $MAX_BITS){
  179. this.bitBuffer.put(0, 4);
  180. }
  181. // Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
  182. // if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
  183. // by the addition of padding bits with binary value 0
  184. while(this.bitBuffer.getLength() % 8 !== 0){
  185. if(this.bitBuffer.getLength() === $MAX_BITS){
  186. break;
  187. }
  188. this.bitBuffer.putBit(false);
  189. }
  190. // The message bit stream shall then be extended to fill the data capacity of the symbol
  191. // corresponding to the Version and Error Correction Level, by the addition of the Pad
  192. // Codewords 11101100 and 00010001 alternately.
  193. let $alternate = false;
  194. while(this.bitBuffer.getLength() <= $MAX_BITS){
  195. this.bitBuffer.put($alternate ? 0b00010001 : 0b11101100, 8);
  196. $alternate = !$alternate;
  197. }
  198. // In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
  199. // to the end of the message in order exactly to fill the symbol capacity
  200. while(this.bitBuffer.getLength() <= $MAX_BITS){
  201. this.bitBuffer.putBit(false);
  202. }
  203. }
  204. }