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

IgniteUI / igniteui-webcomponents / 26950945697

04 Jun 2026 12:12PM UTC coverage: 98.389% (+0.05%) from 98.337%
26950945697

Pull #2242

github

web-flow
Merge b79ea6de0 into 989edd66d
Pull Request #2242: feat: Added QR code component with encoding and rendering capabilities

6144 of 6451 branches covered (95.24%)

Branch coverage included in aggregate %.

2480 of 2491 new or added lines in 8 files covered. (99.56%)

7 existing lines in 1 file now uncovered.

43941 of 44454 relevant lines covered (98.85%)

1704.37 hits per line

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

96.5
/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 {
44✔
19
  return /^\d+$/.test(str);
44✔
20
}
44✔
21

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

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

9✔
37
function getCharacterCountBits(mode: QrEncodingMode, version: number): number {
56✔
38
  switch (mode) {
56✔
39
    case 'numeric':
56✔
40
      return version < 10 ? 10 : version < 27 ? 12 : 14;
6!
41
    case 'alphanumeric':
56✔
42
      return version < 10 ? 9 : version < 27 ? 11 : 13;
16✔
43
    case 'byte':
56✔
44
      return version < 10 ? 8 : version < 27 ? 16 : 16;
34!
45
    default:
56!
NEW
46
      throw new Error(`Unsupported encoding mode: ${mode}`);
×
47
  }
56✔
48
}
56✔
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 {
1,145✔
57
  for (let i = length - 1; i >= 0; i--) {
1,145✔
58
    bits.push((value >> i) & 1);
9,664✔
59
  }
9,664✔
60
}
1,145✔
61

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

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

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

56✔
75
  switch (mode) {
56✔
76
    case 'numeric':
56✔
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':
56✔
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:
56✔
99
      for (const byte of byteEncoded!) {
34✔
100
        pushBits(bits, byte, 8);
712✔
101
      }
712✔
102
      break;
34✔
103
  }
56✔
104

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

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

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

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

41✔
129
  let padIndex = 0;
41✔
130
  while (result.length < totalBytes) {
41✔
131
    result.push(PAD_BYTES[padIndex % 2]);
761✔
132
    padIndex++;
761✔
133
  }
761✔
134
  return result;
41✔
135
}
41✔
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,
45✔
158
  ecLevel: QrErrorCorrectionLevel = 'M',
45✔
159
  requestedVersion?: number
45✔
160
): EncodeResult {
45✔
161
  if (data.length === 0) {
45✔
162
    throw new Error('Data cannot be empty');
1✔
163
  }
1✔
164

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

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

44✔
171
  if (requestedVersion != null) {
45✔
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 {
45✔
185
    bits = [];
36✔
186
    for (let v = 1; v <= 40; v++) {
36✔
187
      const candidateBits = encodeData(data, mode, v);
50✔
188
      if (
50✔
189
        Math.ceil((candidateBits.length + 4) / 8) <=
50✔
190
        getDataCodewordsCount(v, ecIndex)
50✔
191
      ) {
50✔
192
        version = v;
36✔
193
        bits = candidateBits;
36✔
194
        break;
36✔
195
      }
36✔
196
    }
50✔
197
  }
36✔
198

41✔
199
  const capacity = getDataCodewordsCount(version, ecIndex);
41✔
200

41✔
201
  const maxBits = capacity * 8;
41✔
202
  const terminatorLength = Math.min(4, maxBits - bits.length);
41✔
203
  for (let i = 0; i < terminatorLength; i++) {
45✔
204
    bits.push(0);
164✔
205
  }
164✔
206

41✔
207
  while (bits.length % 8 !== 0) {
45✔
208
    bits.push(0);
39✔
209
  }
39✔
210

41✔
211
  const dataBytes = bitsToBytes(bits);
41✔
212
  const paddedData = padData(dataBytes, capacity);
41✔
213
  const codewords = interleaveBlocks(paddedData, version, ecIndex);
41✔
214

41✔
215
  return {
41✔
216
    codewords,
41✔
217
    mode,
41✔
218
    version,
41✔
219
    ecLevelIndex: ecIndex,
41✔
220
  };
41✔
221
}
41✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc