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

IgniteUI / igniteui-webcomponents / 28792371834

06 Jul 2026 12:43PM UTC coverage: 98.375% (+0.04%) from 98.333%
28792371834

Pull #2242

github

web-flow
Merge dcbd3d500 into 0792d307c
Pull Request #2242: feat: Added QR code component with encoding and rendering capabilities

6258 of 6571 branches covered (95.24%)

Branch coverage included in aggregate %.

2772 of 2793 new or added lines in 12 files covered. (99.25%)

44789 of 45319 relevant lines covered (98.83%)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

84✔
215
  return {
84✔
216
    codewords,
84✔
217
    mode,
84✔
218
    version,
84✔
219
    ecLevelIndex: ecIndex,
84✔
220
  };
84✔
221
}
84✔
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