All files / qr-code/src/encode bitstream.ts

100% Statements 60/60
100% Branches 21/21
100% Functions 6/6
100% Lines 53/53

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136                                304x 304x       885x         10347x 2x   10345x 82850x 82850x 82850x 82850x 82850x           300x           331x 331x   32x 32x 361x   361x   32x 32x 24x 8x 7x   32x     142x 142x 1243x   142x 142x     7345x 157x                                       293x 293x   293x 4x 4x     293x 325x 325x 1x         324x 324x 324x     292x 1x                 291x 291x   291x 291x 291x 291x 25731x   291x    
import type { ErrorCorrectionLevel, QRSegment } from "../types";
import { QRCapacityError } from "../errors";
import { MODE_INDICATOR, charCountBits, dataCapacityBits } from "./capacity";
import { ECI_UTF8, alphanumericValue } from "./segment";
 
/**
 * Bit-level assembly of the data codewords (ISO/IEC 18004 §7.4).
 *
 * Segments are written most-significant-bit first, then the stream is closed
 * with a terminator, padded to a byte boundary, and filled to the version's
 * exact data capacity with the alternating 0xEC / 0x11 pad codewords the spec
 * mandates.
 */
 
/** An MSB-first bit accumulator. */
export class BitWriter {
  private readonly bytes: number[] = [];
  private bits = 0;
 
  /** Bits written so far. */
  get length(): number {
    return this.bits;
  }
 
  /** Append the low `count` bits of `value`, most-significant first. */
  push(value: number, count: number): void {
    if (count < 0 || count > 32) {
      throw new RangeError(`Cannot write ${count} bits at once`);
    }
    for (let i = count - 1; i >= 0; i--) {
      const bit = (value >>> i) & 1;
      const byteIndex = this.bits >>> 3;
      if (byteIndex === this.bytes.length) this.bytes.push(0);
      this.bytes[byteIndex]! |= bit << (7 - (this.bits & 7));
      this.bits++;
    }
  }
 
  /** The written bits as bytes, zero-padded to the next byte boundary. */
  toUint8Array(): Uint8Array {
    return Uint8Array.from(this.bytes);
  }
}
 
/** Write one segment's payload (no mode indicator, no character count). */
export function writeSegmentPayload(writer: BitWriter, segment: QRSegment): void {
  const { data } = segment;
  switch (segment.mode) {
    case "numeric": {
      let i = 0;
      for (; i + 3 <= data.length; i += 3) {
        const value =
          (data[i]! - 0x30) * 100 + (data[i + 1]! - 0x30) * 10 + (data[i + 2]! - 0x30);
        writer.push(value, 10);
      }
      const rest = data.length - i;
      if (rest === 2) {
        writer.push((data[i]! - 0x30) * 10 + (data[i + 1]! - 0x30), 7);
      } else if (rest === 1) {
        writer.push(data[i]! - 0x30, 4);
      }
      break;
    }
    case "alphanumeric": {
      let i = 0;
      for (; i + 2 <= data.length; i += 2) {
        writer.push(alphanumericValue(data[i]!) * 45 + alphanumericValue(data[i + 1]!), 11);
      }
      if (i < data.length) writer.push(alphanumericValue(data[i]!), 6);
      break;
    }
    case "byte": {
      for (let i = 0; i < data.length; i++) writer.push(data[i]!, 8);
      break;
    }
  }
}
 
/**
 * Segments → the full data-codeword block for a version and level.
 *
 * @param segments - Mode-split payload.
 * @param version - Symbol version 1–40.
 * @param level - Error-correction level.
 * @param eci - Prefix an ECI 26 (UTF-8) header.
 * @throws {QRCapacityError} when the segments exceed the version's capacity.
 */
export function buildBitStream(
  segments: readonly QRSegment[],
  version: number,
  level: ErrorCorrectionLevel,
  eci: boolean,
): Uint8Array {
  const capacityBits = dataCapacityBits(version, level);
  const writer = new BitWriter();
 
  if (eci) {
    writer.push(0b0111, 4);
    writer.push(ECI_UTF8, 8);
  }
 
  for (const segment of segments) {
    const countBits = charCountBits(segment.mode, version);
    if (segment.charCount >= 1 << countBits) {
      throw new RangeError(
        `Segment of ${segment.charCount} ${segment.mode} characters exceeds the ` +
          `${countBits}-bit character count field at version ${version}`,
      );
    }
    writer.push(MODE_INDICATOR[segment.mode], 4);
    writer.push(segment.charCount, countBits);
    writeSegmentPayload(writer, segment);
  }
 
  if (writer.length > capacityBits) {
    throw new QRCapacityError({
      needed: writer.length,
      capacity: capacityBits,
      level,
      maxVersion: version,
    });
  }
 
  // Terminator (up to four zero bits), then pad to a whole byte.
  writer.push(0, Math.min(4, capacityBits - writer.length));
  writer.push(0, (8 - (writer.length % 8)) % 8);
 
  const written = writer.toUint8Array();
  const codewords = new Uint8Array(capacityBits / 8);
  codewords.set(written);
  for (let i = written.length, pad = 0xec; i < codewords.length; i++, pad ^= 0xec ^ 0x11) {
    codewords[i] = pad;
  }
  return codewords;
}