/**
* @created 11.07.2022
* @author smiley <smiley@chillerlan.net>
* @copyright 2022 smiley
* @license MIT
*/
import BitBuffer from '../Common/BitBuffer.js';
import EccLevel from '../Common/EccLevel.js';
import Version from '../Common/Version.js';
import QRMatrix from './QRMatrix.js';
import Mode from '../Common/Mode.js';
import QRCodeDataException from './QRCodeDataException.js';
import {VERSION_AUTO} from '../Common/constants.js';
/**
* Processes the binary data and maps it on a matrix which is then being returned
*/
export default class QRData{
/**
* the options instance
*
* @type {QROptions}
* @private
*/
options;
/**
* a BitBuffer instance
*
* @type {BitBuffer}
* @private
*/
bitBuffer;
/**
* an EccLevel instance
*
* @type {EccLevel}
* @private
*/
eccLevel;
/**
* current QR Code version
*
* @type {Version}
* @private
*/
version;
/**
* @type {QRDataModeAbstract[]|Array}
* @private
*/
dataSegments = [];
/**
* Max bits for the current ECC mode
*
* @type {number[]|int[]}
* @private
*/
maxBitsForEcc;
/**
* QRData constructor.
*
* @param {QROptions} $options
* @param {QRDataModeAbstract[]} $dataSegments
*/
constructor($options, $dataSegments = []){
this.options = $options;
this.bitBuffer = new BitBuffer;
this.eccLevel = new EccLevel(this.options.eccLevel);
this.maxBitsForEcc = this.eccLevel.getMaxBits();
this.setData($dataSegments);
}
/**
* Sets the data string (internally called by the constructor)
*
* @param {QRDataModeAbstract[]} $dataSegments
*
* @returns {QRData}
*/
setData($dataSegments){
this.dataSegments = $dataSegments;
this.version = this.getMinimumVersion();
this.bitBuffer.clear();
this.writeBitBuffer();
return this;
}
/**
* returns a fresh matrix object with the data written and masked with the given $maskPattern
*
* @returns {QRMatrix}
*/
writeMatrix(){
return (new QRMatrix(this.version, this.eccLevel))
.initFunctionalPatterns()
.writeCodewords(this.bitBuffer)
;
}
/**
* estimates the total length of the several mode segments in order to guess the minimum version
*
* @returns {number|int}
* @throws {QRCodeDataException}
* @private
*/
estimateTotalBitLength(){
let $length = 0;
let $segment;
for($segment of this.dataSegments){
// data length of the current segment
$length += $segment.getLengthInBits();
// +4 bits for the mode descriptor
$length += 4;
}
let $provisionalVersion = null;
for(let $version in this.maxBitsForEcc){
if($version === 0){ // JS array/object weirdness vs php arrays...
continue;
}
if($length <= this.maxBitsForEcc[$version]){
$provisionalVersion = $version;
}
}
if($provisionalVersion !== null){
// add character count indicator bits for the provisional version
for($segment of this.dataSegments){
$length += Mode.getLengthBitsForVersion($segment.datamode, $provisionalVersion);
}
// it seems that in some cases the estimated total length is not 100% accurate,
// so we substract 4 bits from the total when not in mixed mode
if(this.dataSegments.length <= 1){
$length -= 4;
}
// we've got a match!
// or let's see if there's a higher version number available
if($length <= this.maxBitsForEcc[$provisionalVersion] || this.maxBitsForEcc[($provisionalVersion + 1)]){
return $length;
}
}
throw new QRCodeDataException(`estimated data exceeds ${$length} bits`);
}
/**
* returns the minimum version number for the given string
*
* @return {Version}
* @throws {QRCodeDataException}
* @private
*/
getMinimumVersion(){
if(this.options.version !== VERSION_AUTO){
return new Version(this.options.version);
}
let $total = this.estimateTotalBitLength();
// guess the version number within the given range
for(let $version = this.options.versionMin; $version <= this.options.versionMax; $version++){
if($total <= (this.maxBitsForEcc[$version] - 4)){
return new Version($version);
}
}
/* c8 ignore next 2 */
// it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
throw new QRCodeDataException('failed to guess minimum version');
}
/**
* creates a BitBuffer and writes the string data to it
*
* @returns {void}
* @throws {QRCodeException} on data overflow
* @private
*/
writeBitBuffer(){
let $MAX_BITS = this.eccLevel.getMaxBitsForVersion(this.version);
for(let $i = 0; $i < this.dataSegments.length; $i++){
this.dataSegments[$i].write(this.bitBuffer, this.version.getVersionNumber());
}
// overflow, likely caused due to invalid version setting
if(this.bitBuffer.getLength() > $MAX_BITS){
throw new QRCodeDataException(`code length overflow. (${this.bitBuffer.getLength()} > ${$MAX_BITS} bit)`);
}
// add terminator (ISO/IEC 18004:2000 Table 2)
if(this.bitBuffer.getLength() + 4 <= $MAX_BITS){
this.bitBuffer.put(0, 4);
}
// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
// by the addition of padding bits with binary value 0
while(this.bitBuffer.getLength() % 8 !== 0){
if(this.bitBuffer.getLength() === $MAX_BITS){
break;
}
this.bitBuffer.putBit(false);
}
// The message bit stream shall then be extended to fill the data capacity of the symbol
// corresponding to the Version and Error Correction Level, by the addition of the Pad
// Codewords 11101100 and 00010001 alternately.
let $alternate = false;
while(this.bitBuffer.getLength() <= $MAX_BITS){
this.bitBuffer.put($alternate ? 0b00010001 : 0b11101100, 8);
$alternate = !$alternate;
}
// In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
// to the end of the message in order exactly to fill the symbol capacity
while(this.bitBuffer.getLength() <= $MAX_BITS){
this.bitBuffer.putBit(false);
}
}
}