• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-webcomponents / 30922726647

04 Aug 2026 03:09PM UTC coverage: 98.348% (+0.03%) from 98.316%
30922726647

push

github

web-flow
feat: Added QR code component with encoding and rendering capabilities (#2242)

---------

Co-authored-by: Simeon Simeonoff <sim.simeonoff@gmail.com>

6455 of 6788 branches covered (95.09%)

Branch coverage included in aggregate %.

2810 of 2835 new or added lines in 12 files covered. (99.12%)

1 existing line in 1 file now uncovered.

46122 of 46672 relevant lines covered (98.82%)

1828.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.98
/src/components/qr-code/model/encode.ts
1
import type { QrEncodingMode, QrErrorCorrectionLevel } from '../types.js';
9✔
2
import { getDataCodewordsCount, interleaveBlocks } from './error-correction.js';
9✔
3

9✔
4
const EC_LEVEL_INDEX = { L: 0, M: 1, Q: 2, H: 3 } as const;
9✔
5
const ALPHANUMERIC_MAP = new Map<string, number>(
9✔
6
  [...'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'].map((char, index) => [
9✔
7
    char,
405✔
8
    index,
405✔
9
  ])
9✔
10
);
9✔
11
const PAD_BYTES = [0xec, 0x11];
9✔
12
const TEXT_ENCODER = new TextEncoder();
9✔
13

9✔
14
function getAlphanumericValue(char: string): number {
217✔
15
  return ALPHANUMERIC_MAP.get(char) ?? -1;
217!
16
}
217✔
17

9✔
18
function isNumeric(str: string): boolean {
75✔
19
  return /^\d+$/.test(str);
75✔
20
}
75✔
21

9✔
22
function isAlphanumeric(str: string): boolean {
72✔
23
  for (const char of str) {
72✔
24
    if (!ALPHANUMERIC_MAP.has(char)) {
181✔
25
      return false;
56✔
26
    }
56✔
27
  }
181✔
28
  return true;
16✔
29
}
16✔
30

9✔
31
function detectEncodingMode(data: string): QrEncodingMode {
75✔
32
  if (isNumeric(data)) return 'numeric';
75✔
33
  if (isAlphanumeric(data)) return 'alphanumeric';
75✔
34
  return 'byte';
56✔
35
}
56✔
36

9✔
37
function getCharacterCountBits(mode: QrEncodingMode, version: number): number {
133✔
38
  switch (mode) {
133✔
39
    case 'numeric':
133✔
40
      return version < 10 ? 10 : version < 27 ? 12 : 14;
6!
41
    case 'alphanumeric':
133✔
42
      return version < 10 ? 9 : version < 27 ? 11 : 13;
16✔
43
    case 'byte':
133✔
44
      return version < 10 ? 8 : version < 27 ? 16 : 16;
111✔
45
    default:
133!
NEW
46
      throw new Error(`Unsupported encoding mode: ${mode}`);
×
47
  }
133✔
48
}
133✔
49

9✔
50
const MODE_INDICATORS: Record<QrEncodingMode, number> = {
9✔
51
  numeric: 0b0001,
9✔
52
  alphanumeric: 0b0010,
9✔
53
  byte: 0b0100,
9✔
54
};
9✔
55

9✔
56
function pushBits(bits: number[], value: number, length: number): void {
81,648✔
57
  for (let i = length - 1; i >= 0; i--) {
81,648✔
58
    bits.push((value >> i) & 1);
653,628✔
59
  }
653,628✔
60
}
81,648✔
61

9✔
62
function encodeData(
133✔
63
  data: string,
133✔
64
  mode: QrEncodingMode,
133✔
65
  version: number
133✔
66
): number[] {
133✔
67
  const bits: number[] = [];
133✔
68
  const byteEncoded = mode === 'byte' ? TEXT_ENCODER.encode(data) : null;
133✔
69

133✔
70
  pushBits(bits, MODE_INDICATORS[mode], 4);
133✔
71

133✔
72
  const charCount = byteEncoded ? byteEncoded.length : data.length;
133✔
73
  pushBits(bits, charCount, getCharacterCountBits(mode, version));
133✔
74

133✔
75
  switch (mode) {
133✔
76
    case 'numeric':
133✔
77
      for (let i = 0; i < data.length; i += 3) {
6✔
78
        const chunk = data.slice(i, i + 3);
209✔
79
        const value = Number.parseInt(chunk, 10);
209✔
80

209✔
81
        if (chunk.length === 3) pushBits(bits, value, 10);
209✔
82
        else if (chunk.length === 2) pushBits(bits, value, 7);
6✔
83
        else pushBits(bits, value, 4);
1✔
84
      }
209✔
85
      break;
6✔
86
    case 'alphanumeric':
133✔
87
      for (let i = 0; i < data.length; i += 2) {
16✔
88
        if (i + 1 < data.length) {
112✔
89
          const value =
105✔
90
            getAlphanumericValue(data[i]) * 45 +
105✔
91
            getAlphanumericValue(data[i + 1]);
105✔
92
          pushBits(bits, value, 11);
105✔
93
        } else {
112✔
94
          pushBits(bits, getAlphanumericValue(data[i]), 6);
7✔
95
        }
7✔
96
      }
112✔
97
      break;
16✔
98
    default:
133✔
99
      for (const byte of byteEncoded!) {
111✔
100
        pushBits(bits, byte, 8);
81,061✔
101
      }
81,061✔
102
      break;
111✔
103
  }
133✔
104

133✔
105
  return bits;
133✔
106
}
133✔
107

9✔
108
function bitsToBytes(bits: number[]): number[] {
71✔
109
  const bytes: number[] = [];
71✔
110
  for (let i = 0; i < bits.length; i += 8) {
71✔
111
    let byte = 0;
786✔
112
    for (let j = 0; j < 8 && i + j < bits.length; j++) {
786✔
113
      byte = (byte << 1) | bits[i + j];
6,288✔
114
    }
6,288✔
115
    bytes.push(byte);
786✔
116
  }
786✔
117
  return bytes;
71✔
118
}
71✔
119

9✔
120
function padData(data: number[], totalBytes: number): number[] {
71✔
121
  const result = data.slice();
71✔
122

71✔
123
  if (result.length > totalBytes) {
71!
NEW
124
    throw new Error(
×
NEW
125
      'Data exceeds maximum capacity for this version and error correction level'
×
NEW
126
    );
×
NEW
127
  }
×
128

71✔
129
  let padIndex = 0;
71✔
130
  while (result.length < totalBytes) {
71✔
131
    result.push(PAD_BYTES[padIndex % 2]);
1,025✔
132
    padIndex++;
1,025✔
133
  }
1,025✔
134
  return result;
71✔
135
}
71✔
136

9✔
137
/** Result produced by `encodeQR`. */
9✔
138
export type EncodeResult = {
9✔
139
  /** Interleaved data + ECC codewords ready for matrix placement. */
9✔
140
  codewords: number[];
9✔
141
  /** Encoding mode that was applied to the input string. */
9✔
142
  mode: QrEncodingMode;
9✔
143
  /** QR version (1–40) used for this code. */
9✔
144
  version: number;
9✔
145
  /** Numeric index of the error correction level (L=0, M=1, Q=2, H=3). */
9✔
146
  ecLevelIndex: number;
9✔
147
};
9✔
148

9✔
149
/**
9✔
150
 * Encodes a string into QR codewords (data + error correction), selecting the
9✔
151
 * smallest version that fits unless `requestedVersion` is specified.
9✔
152
 *
9✔
153
 * @throws When `data` is empty, the requested version is out of range, or the
9✔
154
 * data exceeds the capacity of the requested version.
9✔
155
 */
9✔
156
export function encodeQR(
9✔
157
  data: string,
76✔
158
  ecLevel: QrErrorCorrectionLevel = 'M',
76✔
159
  requestedVersion?: number
76✔
160
): EncodeResult {
76✔
161
  if (data.length === 0) {
76✔
162
    throw new Error('Data cannot be empty');
1✔
163
  }
1✔
164

75✔
165
  const ecIndex = EC_LEVEL_INDEX[ecLevel];
75✔
166
  const mode = detectEncodingMode(data);
75✔
167

75✔
168
  let version = 1;
75✔
169
  let bits: number[];
75✔
170

75✔
171
  if (requestedVersion != null) {
76✔
172
    if (requestedVersion < 1 || requestedVersion > 40) {
8✔
173
      throw new Error('Requested version must be between 1 and 40');
2✔
174
    }
2✔
175
    version = requestedVersion;
6✔
176
    bits = encodeData(data, mode, version);
6✔
177
    if (
6✔
178
      Math.ceil((bits.length + 4) / 8) > getDataCodewordsCount(version, ecIndex)
6✔
179
    ) {
8✔
180
      throw new Error(
1✔
181
        `Data too long for version ${version} and error correction level ${ecLevel}`
1✔
182
      );
1✔
183
    }
1✔
184
  } else {
76✔
185
    bits = [];
67✔
186
    for (let v = 1; v <= 40; v++) {
67✔
187
      const candidateBits = encodeData(data, mode, v);
127✔
188
      if (
127✔
189
        Math.ceil((candidateBits.length + 4) / 8) <=
127✔
190
        getDataCodewordsCount(v, ecIndex)
127✔
191
      ) {
127✔
192
        version = v;
66✔
193
        bits = candidateBits;
66✔
194
        break;
66✔
195
      }
66✔
196
    }
127✔
197

67✔
198
    // encodeData() always produces a non-empty bit sequence, so an empty
67✔
199
    // `bits` here means no version (1-40) could fit the data.
67✔
200
    if (bits.length === 0) {
67✔
201
      throw new Error(
1✔
202
        `Data too long to fit in any QR version (1-40) at error correction level '${ecLevel}'`
1✔
203
      );
1✔
204
    }
1✔
205
  }
67✔
206

71✔
207
  const capacity = getDataCodewordsCount(version, ecIndex);
71✔
208

71✔
209
  const maxBits = capacity * 8;
71✔
210
  const terminatorLength = Math.min(4, maxBits - bits.length);
71✔
211
  for (let i = 0; i < terminatorLength; i++) {
76✔
212
    bits.push(0);
284✔
213
  }
284✔
214

71✔
215
  while (bits.length % 8 !== 0) {
76✔
216
    bits.push(0);
39✔
217
  }
39✔
218

71✔
219
  const dataBytes = bitsToBytes(bits);
71✔
220
  const paddedData = padData(dataBytes, capacity);
71✔
221
  const codewords = interleaveBlocks(paddedData, version, ecIndex);
71✔
222

71✔
223
  return {
71✔
224
    codewords,
71✔
225
    mode,
71✔
226
    version,
71✔
227
    ecLevelIndex: ecIndex,
71✔
228
  };
71✔
229
}
71✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc