upiqr 1.3.26 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,9 @@
7
7
 
8
8
  Generate NPCI's UPI QR code (BASE64) along with UPI intent link, By using it any payment is possible from UPI enabled apps.
9
9
 
10
+ **Zero runtime dependencies.** QR generation is implemented from scratch (no `qrcode` package or
11
+ any other npm dependency) - smaller install size and no third-party supply-chain risk to inherit.
12
+
10
13
  ## Supports
11
14
 
12
15
  <div id="header" align="center">
@@ -19,16 +22,28 @@ This package will work on client and server.
19
22
 
20
23
  ```js
21
24
 
22
- import upiqr from "upiqr";
25
+ import { upiqrSync } from "upiqr";
23
26
 
24
- upiqr({
27
+ const { qr } = upiqrSync({
25
28
  payeeVPA: "bhar4t@upi",
26
29
  payeeName: "Bharat Sahu"
27
30
  })
28
- .then(({ qr }) => {
29
- console.log(qr) // data:image/png;base64,eR0lGODP...
30
- })
31
- .catch(console.error)
31
+
32
+ console.log(qr) // data:image/png;base64,eR0lGODP...
33
+
34
+ ```
35
+
36
+ `upiqrSync` is synchronous - no `Promise`/`await` needed. The previous default export (`import
37
+ upiqr from "upiqr"`) still works exactly as before but is **deprecated** and logs a one-time
38
+ `console.warn`; it will be removed in a future major version:
39
+
40
+ ```js
41
+
42
+ import upiqr from "upiqr"; // deprecated, use upiqrSync instead
43
+
44
+ upiqr({ payeeVPA: "bhar4t@upi", payeeName: "Bharat Sahu" })
45
+ .then(({ qr }) => console.log(qr))
46
+ .catch(console.error)
32
47
 
33
48
  ```
34
49
 
@@ -54,18 +69,28 @@ For a complete list of supported fields, refer to the [NPCI UPI Linking Specs][u
54
69
 
55
70
  ### Customization:
56
71
 
57
- Internally using `qrcode` for QR Generation. And, it accepts [second optional param object][qr-code-options] for cusomizing the QR code.
72
+ QR generation is implemented from scratch with **zero runtime dependencies** (a dependency-free
73
+ QR encoder plus PNG rendering built on the platform's native APIs - Node's built-in `zlib` on the
74
+ server, the Canvas API in the browser). It accepts a second optional options object for customizing
75
+ the QR code:
58
76
 
59
77
  ```js
60
- upiqr(intentFields, {
78
+ upiqrSync(intentFields, {
79
+ errorCorrectionLevel: 'M', // 'L' | 'M' | 'Q' | 'H' (or 'low' | 'medium' | 'quartile' | 'high'), default 'M'
80
+ margin: 4, // quiet zone width in modules, default 4
81
+ scale: 4, // pixels per module, default 4
82
+ width: 300, // forces an output width in pixels; takes precedence over `scale`
61
83
  color: {
62
- dark: '#000000',
63
- light: '#FFFFFF'
64
- }
84
+ dark: '#000000ff', // dark module color (hex, RGB or RGBA), default '#000000ff'
85
+ light: '#ffffffff', // light module color (hex, RGB or RGBA), default '#ffffffff'
86
+ },
65
87
  })
66
88
 
67
89
  ```
68
90
 
91
+ Note: only PNG output is produced (SVG/JPEG/WebP are not supported), and byte-mode QR versions 1-10
92
+ are supported (comfortably covering typical UPI intent URLs).
93
+
69
94
  <!--- [![Code Coverage][codecov-img]][codecov-url] --->
70
95
 
71
96
  [build-img]:https://github.com/bhar4t/upiqr/actions/workflows/main.yml/badge.svg
@@ -82,5 +107,4 @@ Internally using `qrcode` for QR Generation. And, it accepts [second optional pa
82
107
  [semantic-release-url]:https://github.com/semantic-release/semantic-release
83
108
  [commitizen-img]:https://img.shields.io/badge/commitizen-friendly-brightgreen.svg
84
109
  [commitizen-url]:http://commitizen.github.io/cz-cli/
85
- [qr-code-options]:https://www.npmjs.com/package/qrcode#qr-code-options
86
110
  [upi-specs-url]:https://www.npci.org.in/PDF/npci/upi/circular/2017/Circular18_BankCompliances_to_enbaleUPIMerchantecosystem_0.pdf
@@ -0,0 +1 @@
1
+ import{QRResult,UPIIntentParams,UpiqrRenderOptions}from'./types/upiqr.js';export declare function upiqrSync(params:UPIIntentParams,qrOptions?:UpiqrRenderOptions):QRResult;export default function upiqr(params:UPIIntentParams,qrOptions?:UpiqrRenderOptions):Promise<QRResult>;
@@ -0,0 +1 @@
1
+ var __awaiter=this&&this.__awaiter||function(r,c,e,a){function s(n){return n instanceof e?n:new e(function(r){r(n)})}return new(e||(e=Promise))(function(n,e){function t(r){try{i(a.next(r))}catch(r){e(r)}}function o(r){try{i(a["throw"](r))}catch(r){e(r)}}function i(r){r.done?n(r.value):s(r.value).then(t,o)}i((a=a.apply(r,c||[])).next())})};import{buildUPIIntent}from"./qr/intent.js";import{encodeQRMatrix,normalizeErrorCorrectionLevel}from"./qr/encoder.js";import{renderModulesToRGBA}from"./qr/pixels.js";import{resolveRenderOptions}from"./qr/options.js";import{encodeCanvasPNG}from"./qr/canvas.js";import{warnDeprecatedAsyncDefault}from"./qr/deprecation.js";export function upiqrSync(r,n){const e=buildUPIIntent(r);try{const t=normalizeErrorCorrectionLevel(n===null||n===void 0?void 0:n.errorCorrectionLevel);const{matrix:o}=encodeQRMatrix(e,t);const i=resolveRenderOptions(o.length,n);const c=renderModulesToRGBA(o,i);const a=encodeCanvasPNG(c);return{qr:a,intent:e}}catch(r){throw new Error("Unable to generate UPI QR Code.\n"+r)}}export default function upiqr(r,n){return __awaiter(this,void 0,void 0,function*(){warnDeprecatedAsyncDefault();return upiqrSync(r,n)})}
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- import QRCode from'qrcode';import{QRResult,UPIIntentParams}from'./types/upiqr';export default function upiqr({payeeVPA:pa,payeeName:pn,payeeMerchantCode:mc,transactionId:tid,transactionRef:tr,transactionNote:tn,amount:am,minimumAmount:mam,currency:cu,}:UPIIntentParams,qrOptions?:QRCode.QRCodeToDataURLOptions):Promise<QRResult>;
1
+ import{QRResult,UPIIntentParams,UpiqrRenderOptions}from'./types/upiqr.js';export declare function upiqrSync(params:UPIIntentParams,qrOptions?:UpiqrRenderOptions):QRResult;export default function upiqr(params:UPIIntentParams,qrOptions?:UpiqrRenderOptions):Promise<QRResult>;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var __awaiter=this&&this.__awaiter||function(t,o,e,c){function u(n){return n instanceof e?n:new e(function(t){t(n)})}return new(e||(e=Promise))(function(n,e){function r(t){try{i(c.next(t))}catch(t){e(t)}}function a(t){try{i(c["throw"](t))}catch(t){e(t)}}function i(t){t.done?n(t.value):u(t.value).then(r,a)}i((c=c.apply(t,o||[])).next())})};import QRCode from"qrcode";function validate({pa:t,pn:n}){if(!t||!n)return"Virtual payee's address/name is compulsory";if(t.length<5||n.length<4)return"Virtual payee's address/name is too short.";return""}export default function upiqr(t,n){return __awaiter(this,arguments,void 0,function*({payeeVPA:t,payeeName:n,payeeMerchantCode:e,transactionId:r,transactionRef:a,transactionNote:i,amount:o,minimumAmount:c,currency:u},f){const s={pa:t,pn:n};if(o)s["am"]=o;if(c)s["mam"]=c;if(u)s["cu"]=u;if(e)s["mc"]=e;if(r)s["tid"]=r;if(a)s["tr"]=a;if(i)s["tn"]=i;const p=validate({pa:t,pn:n});if(p)throw new Error(p);const d="upi://pay?"+new URLSearchParams(s).toString();try{const m=yield QRCode.toDataURL(d,f);return{qr:m,intent:d}}catch(t){throw new Error("Unable to generate UPI QR Code.\n"+t)}})}
1
+ var __awaiter=this&&this.__awaiter||function(r,c,n,a){function s(e){return e instanceof n?e:new n(function(r){r(e)})}return new(n||(n=Promise))(function(e,n){function t(r){try{i(a.next(r))}catch(r){n(r)}}function o(r){try{i(a["throw"](r))}catch(r){n(r)}}function i(r){r.done?e(r.value):s(r.value).then(t,o)}i((a=a.apply(r,c||[])).next())})};import{buildUPIIntent}from"./qr/intent.js";import{encodeQRMatrix,normalizeErrorCorrectionLevel}from"./qr/encoder.js";import{renderModulesToRGBA}from"./qr/pixels.js";import{resolveRenderOptions}from"./qr/options.js";import{encodePNG}from"./qr/png.js";import{warnDeprecatedAsyncDefault}from"./qr/deprecation.js";export function upiqrSync(r,e){const n=buildUPIIntent(r);try{const t=normalizeErrorCorrectionLevel(e===null||e===void 0?void 0:e.errorCorrectionLevel);const{matrix:o}=encodeQRMatrix(n,t);const i=resolveRenderOptions(o.length,e);const c=renderModulesToRGBA(o,i);const a=`data:image/png;base64,${encodePNG(c).toString("base64")}`;return{qr:a,intent:n}}catch(r){throw new Error("Unable to generate UPI QR Code.\n"+r)}}export default function upiqr(r,e){return __awaiter(this,void 0,void 0,function*(){warnDeprecatedAsyncDefault();return upiqrSync(r,e)})}
@@ -0,0 +1,4 @@
1
+ import { RGBAImage } from './pixels.js';
2
+ import { Base64, ImageType } from '../types/upiqr.js';
3
+ /** Renders an RGBA pixel buffer to a PNG data URL using the browser's native Canvas API. */
4
+ export declare function encodeCanvasPNG(image: RGBAImage): Base64<ImageType>;
@@ -0,0 +1,11 @@
1
+ /** Renders an RGBA pixel buffer to a PNG data URL using the browser's native Canvas API. */
2
+ export function encodeCanvasPNG(image) {
3
+ const canvas = document.createElement('canvas');
4
+ canvas.width = image.width;
5
+ canvas.height = image.height;
6
+ const ctx = canvas.getContext('2d');
7
+ if (!ctx)
8
+ throw new Error('Unable to acquire a 2D canvas context for QR rendering.');
9
+ ctx.putImageData(new ImageData(image.data, image.width, image.height), 0, 0);
10
+ return canvas.toDataURL('image/png');
11
+ }
@@ -0,0 +1,8 @@
1
+ export interface RGBA {
2
+ r: number;
3
+ g: number;
4
+ b: number;
5
+ a: number;
6
+ }
7
+ /** Parses a `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa` hex color string into RGBA components. */
8
+ export declare function parseHexColor(input: string): RGBA;
@@ -0,0 +1,21 @@
1
+ const HEX_COLOR_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
2
+ /** Parses a `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa` hex color string into RGBA components. */
3
+ export function parseHexColor(input) {
4
+ const match = HEX_COLOR_RE.exec(input);
5
+ if (!match) {
6
+ throw new Error(`Invalid color "${input}". Expected hex format like #000000 or #000000ff.`);
7
+ }
8
+ let hex = match[1];
9
+ if (hex.length === 3 || hex.length === 4) {
10
+ hex = hex.split('').map(ch => ch + ch).join('');
11
+ }
12
+ if (hex.length === 6)
13
+ hex += 'ff';
14
+ const int = parseInt(hex, 16);
15
+ return {
16
+ r: (int >>> 24) & 0xff,
17
+ g: (int >>> 16) & 0xff,
18
+ b: (int >>> 8) & 0xff,
19
+ a: int & 0xff,
20
+ };
21
+ }
@@ -0,0 +1,2 @@
1
+ /** Emits a one-time console warning when the deprecated async default export is used. */
2
+ export declare function warnDeprecatedAsyncDefault(): void;
@@ -0,0 +1,10 @@
1
+ let warned = false;
2
+ /** Emits a one-time console warning when the deprecated async default export is used. */
3
+ export function warnDeprecatedAsyncDefault() {
4
+ if (warned)
5
+ return;
6
+ warned = true;
7
+ // eslint-disable-next-line no-console
8
+ console.warn('[upiqr] The default export (async) is deprecated and will be removed in a future major version. ' +
9
+ 'Use `import { upiqrSync } from "upiqr"` instead - it is synchronous and returns the result directly, no Promise required.');
10
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Dependency-free QR Code encoder (byte mode, versions 1-10, ECC levels L/M/Q/H).
3
+ * Ported from a from-scratch reference implementation and fixed/hardened for production use.
4
+ *
5
+ * Known scope limits (by design, matching the original reference implementation):
6
+ * - Only byte mode (UTF-8) is supported, no numeric/alphanumeric/kanji optimization.
7
+ * - Only versions 1-10 are supported (max ~213 data bytes at ECC M), which comfortably
8
+ * covers UPI intent URLs but will reject unusually large payloads.
9
+ */
10
+ export type ECCLevel = 'L' | 'M' | 'Q' | 'H';
11
+ export interface QRMatrixResult {
12
+ /** Square matrix of 0/1 values. No cell is left unresolved. */
13
+ matrix: number[][];
14
+ version: number;
15
+ }
16
+ declare function normalizeErrorCorrectionLevel(level?: string): ECCLevel;
17
+ /** Encodes `text` (byte/UTF-8 mode) into the smallest fitting QR matrix (versions 1-10). */
18
+ export declare function encodeQRMatrix(text: string, ecc: ECCLevel): QRMatrixResult;
19
+ export { normalizeErrorCorrectionLevel };
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Dependency-free QR Code encoder (byte mode, versions 1-10, ECC levels L/M/Q/H).
3
+ * Ported from a from-scratch reference implementation and fixed/hardened for production use.
4
+ *
5
+ * Known scope limits (by design, matching the original reference implementation):
6
+ * - Only byte mode (UTF-8) is supported, no numeric/alphanumeric/kanji optimization.
7
+ * - Only versions 1-10 are supported (max ~213 data bytes at ECC M), which comfortably
8
+ * covers UPI intent URLs but will reject unusually large payloads.
9
+ */
10
+ /** Thrown internally when a payload does not fit in the given version/ECC combination. */
11
+ class CapacityExceededError extends Error {
12
+ }
13
+ // Reed-Solomon block layout per version/ECC level (ISO/IEC 18004 Annex tables, versions 1-10).
14
+ const RS_BLOCKS = {
15
+ 1: { L: [[1, 26, 19]], M: [[1, 26, 16]], Q: [[1, 26, 13]], H: [[1, 26, 9]] },
16
+ 2: { L: [[1, 44, 34]], M: [[1, 44, 28]], Q: [[1, 44, 22]], H: [[1, 44, 16]] },
17
+ 3: { L: [[1, 70, 55]], M: [[1, 70, 44]], Q: [[2, 35, 17]], H: [[2, 35, 13]] },
18
+ 4: { L: [[1, 100, 80]], M: [[2, 50, 32]], Q: [[2, 50, 24]], H: [[4, 25, 9]] },
19
+ 5: { L: [[1, 134, 108]], M: [[2, 67, 43]], Q: [[2, 33, 15], [2, 34, 16]], H: [[2, 33, 11], [2, 34, 12]] },
20
+ 6: { L: [[2, 86, 68]], M: [[4, 43, 27]], Q: [[4, 43, 19]], H: [[4, 43, 15]] },
21
+ 7: { L: [[2, 98, 78]], M: [[4, 49, 31]], Q: [[2, 32, 14], [4, 33, 15]], H: [[4, 39, 13], [1, 40, 14]] },
22
+ 8: { L: [[2, 121, 97]], M: [[2, 60, 38], [2, 61, 39]], Q: [[4, 40, 18], [2, 41, 19]], H: [[4, 40, 14], [2, 41, 15]] },
23
+ 9: { L: [[2, 146, 116]], M: [[3, 58, 36], [2, 59, 37]], Q: [[4, 36, 16], [4, 37, 17]], H: [[4, 36, 12], [4, 37, 13]] },
24
+ 10: { L: [[2, 86, 68], [2, 87, 69]], M: [[4, 69, 43], [1, 70, 44]], Q: [[6, 43, 19], [2, 44, 20]], H: [[6, 43, 15], [2, 44, 16]] },
25
+ };
26
+ // Alignment pattern center coordinates per version (versions 1-10).
27
+ const ALIGN = {
28
+ 1: [], 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30],
29
+ 6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50],
30
+ };
31
+ // ECC indicator bits used in the 15-bit format info (ISO/IEC 18004 Table 25).
32
+ const ECC_FORMAT = { L: 1, M: 0, Q: 3, H: 2 };
33
+ // The 8 standard QR mask patterns (ISO/IEC 18004 Table 20).
34
+ const MASKS = [
35
+ (r, c) => (r + c) % 2 === 0,
36
+ (r, _c) => r % 2 === 0,
37
+ (_r, c) => c % 3 === 0,
38
+ (r, c) => (r + c) % 3 === 0,
39
+ (r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
40
+ (r, c) => ((r * c) % 2 + (r * c) % 3) === 0,
41
+ (r, c) => ((r * c) % 2 + (r * c) % 3) % 2 === 0,
42
+ (r, c) => ((r * c) % 3 + (r + c) % 2) % 2 === 0,
43
+ ];
44
+ function normalizeErrorCorrectionLevel(level) {
45
+ if (!level)
46
+ return 'M';
47
+ const map = {
48
+ low: 'L', medium: 'M', quartile: 'Q', high: 'H',
49
+ l: 'L', m: 'M', q: 'Q', h: 'H',
50
+ };
51
+ const normalized = map[level.toLowerCase()];
52
+ if (!normalized) {
53
+ throw new Error(`Invalid errorCorrectionLevel "${level}". Expected one of: L, M, Q, H, low, medium, quartile, high.`);
54
+ }
55
+ return normalized;
56
+ }
57
+ // GF(256) arithmetic (irreducible polynomial x^8+x^4+x^3+x^2+1 = 0x11d), as used by QR's Reed-Solomon codes.
58
+ function gfMul(x, y) {
59
+ let a = x;
60
+ let b = y;
61
+ let z = 0;
62
+ while (b) {
63
+ if (b & 1)
64
+ z ^= a;
65
+ a <<= 1;
66
+ if (a & 0x100)
67
+ a ^= 0x11d;
68
+ b >>>= 1;
69
+ }
70
+ return z;
71
+ }
72
+ const EXP = new Uint8Array(512);
73
+ const LOG = new Uint16Array(256);
74
+ (() => {
75
+ let x = 1;
76
+ for (let i = 0; i < 255; i++) {
77
+ EXP[i] = x;
78
+ LOG[x] = i;
79
+ x <<= 1;
80
+ if (x & 0x100)
81
+ x ^= 0x11d;
82
+ }
83
+ for (let i = 255; i < 512; i++)
84
+ EXP[i] = EXP[i - 255];
85
+ })();
86
+ function rsGenerator(degree) {
87
+ let g = [1];
88
+ for (let i = 0; i < degree; i++) {
89
+ const next = new Array(g.length + 1).fill(0);
90
+ const a = EXP[i];
91
+ for (let j = 0; j < g.length; j++) {
92
+ next[j] ^= g[j];
93
+ next[j + 1] ^= gfMul(g[j], a);
94
+ }
95
+ g = next;
96
+ }
97
+ return g;
98
+ }
99
+ const GEN_CACHE = new Map();
100
+ function reedSolomon(data, degree) {
101
+ let gen = GEN_CACHE.get(degree);
102
+ if (!gen) {
103
+ gen = rsGenerator(degree);
104
+ GEN_CACHE.set(degree, gen);
105
+ }
106
+ const ecc = new Array(degree).fill(0);
107
+ for (const b of data) {
108
+ const f = b ^ ecc[0];
109
+ for (let i = 0; i < degree - 1; i++) {
110
+ ecc[i] = ecc[i + 1] ^ gfMul(gen[i + 1], f);
111
+ }
112
+ ecc[degree - 1] = gfMul(gen[degree], f);
113
+ }
114
+ return ecc;
115
+ }
116
+ function rsBlocks(version, ecc) {
117
+ const groups = RS_BLOCKS[version][ecc];
118
+ const out = [];
119
+ for (const [count, total, data] of groups) {
120
+ for (let i = 0; i < count; i++)
121
+ out.push({ total, data, ec: total - data });
122
+ }
123
+ return out;
124
+ }
125
+ function bitAppend(bits, value, len) {
126
+ for (let i = len - 1; i >= 0; i--)
127
+ bits.push((value >>> i) & 1);
128
+ }
129
+ function makeDataBytes(text, version, ecc) {
130
+ const bytes = [...new TextEncoder().encode(text)];
131
+ const blocks = rsBlocks(version, ecc);
132
+ const capacity = blocks.reduce((n, b) => n + b.data, 0);
133
+ const countBits = version < 10 ? 8 : 16;
134
+ const bits = [];
135
+ bitAppend(bits, 0b0100, 4); // byte mode indicator
136
+ bitAppend(bits, bytes.length, countBits);
137
+ for (const b of bytes)
138
+ bitAppend(bits, b, 8);
139
+ if (bits.length > capacity * 8) {
140
+ throw new CapacityExceededError(`Text is too long for version ${version} / ECC ${ecc}`);
141
+ }
142
+ for (let i = 0; i < 4 && bits.length < capacity * 8; i++)
143
+ bits.push(0);
144
+ while (bits.length % 8)
145
+ bits.push(0);
146
+ let pad = 0;
147
+ while (bits.length < capacity * 8) {
148
+ bitAppend(bits, pad++ % 2 ? 0x11 : 0xec, 8);
149
+ }
150
+ const data = [];
151
+ for (let i = 0; i < bits.length; i += 8) {
152
+ let v = 0;
153
+ for (let j = 0; j < 8; j++)
154
+ v = (v << 1) | bits[i + j];
155
+ data.push(v);
156
+ }
157
+ const dataBlocks = [];
158
+ const eccBlocks = [];
159
+ let pos = 0;
160
+ for (const block of blocks) {
161
+ const d = data.slice(pos, pos + block.data);
162
+ pos += block.data;
163
+ dataBlocks.push(d);
164
+ eccBlocks.push(reedSolomon(d, block.ec));
165
+ }
166
+ const interleaved = [];
167
+ const maxD = Math.max(...blocks.map(b => b.data));
168
+ const maxE = Math.max(...blocks.map(b => b.ec));
169
+ for (let i = 0; i < maxD; i++)
170
+ for (const d of dataBlocks)
171
+ if (i < d.length)
172
+ interleaved.push(d[i]);
173
+ for (let i = 0; i < maxE; i++)
174
+ for (const e of eccBlocks)
175
+ if (i < e.length)
176
+ interleaved.push(e[i]);
177
+ return interleaved;
178
+ }
179
+ function bitLen(x) {
180
+ let n = 0;
181
+ let v = x;
182
+ while (v) {
183
+ n++;
184
+ v >>>= 1;
185
+ }
186
+ return n;
187
+ }
188
+ function bchFormat(data) {
189
+ let d = data << 10;
190
+ const g = 0x537;
191
+ while (bitLen(d) - bitLen(g) >= 0)
192
+ d ^= g << (bitLen(d) - bitLen(g));
193
+ return ((data << 10) | d) ^ 0x5412;
194
+ }
195
+ function bchVersion(data) {
196
+ let d = data << 12;
197
+ const g = 0x1f25;
198
+ while (bitLen(d) - bitLen(g) >= 0)
199
+ d ^= g << (bitLen(d) - bitLen(g));
200
+ return (data << 12) | d;
201
+ }
202
+ function getCell(m, r, c) {
203
+ return m[r][c];
204
+ }
205
+ function setCell(m, r, c, v) {
206
+ m[r][c] = v;
207
+ }
208
+ function matrixBase(version) {
209
+ const n = version * 4 + 17;
210
+ const m = Array.from({ length: n }, () => new Array(n).fill(-1));
211
+ function finder(r0, c0) {
212
+ for (let dr = -1; dr <= 7; dr++) {
213
+ for (let dc = -1; dc <= 7; dc++) {
214
+ const r = r0 + dr;
215
+ const c = c0 + dc;
216
+ if (r < 0 || r >= n || c < 0 || c >= n)
217
+ continue;
218
+ const dark = dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6 &&
219
+ (dr === 0 || dr === 6 || dc === 0 || dc === 6 || (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4));
220
+ setCell(m, r, c, dark ? 1 : 0);
221
+ }
222
+ }
223
+ }
224
+ finder(0, 0);
225
+ finder(n - 7, 0);
226
+ finder(0, n - 7);
227
+ for (let i = 8; i < n - 8; i++) {
228
+ if (getCell(m, 6, i) === -1)
229
+ setCell(m, 6, i, i % 2 === 0 ? 1 : 0);
230
+ if (getCell(m, i, 6) === -1)
231
+ setCell(m, i, 6, i % 2 === 0 ? 1 : 0);
232
+ }
233
+ if (version >= 2) {
234
+ const positions = ALIGN[version];
235
+ const first = positions[0];
236
+ const last = positions[positions.length - 1];
237
+ for (const r of positions) {
238
+ for (const c of positions) {
239
+ // Skip the 3 corner combinations that overlap a finder pattern; every other
240
+ // combination must be drawn unconditionally (it legitimately overrides the
241
+ // timing pattern where the two intersect, e.g. (6, 22) for version 7).
242
+ const overlapsFinder = (r === first && c === first) || (r === first && c === last) || (r === last && c === first);
243
+ if (overlapsFinder)
244
+ continue;
245
+ for (let dr = -2; dr <= 2; dr++) {
246
+ for (let dc = -2; dc <= 2; dc++) {
247
+ const dark = Math.max(Math.abs(dr), Math.abs(dc)) !== 1;
248
+ setCell(m, r + dr, c + dc, dark ? 1 : 0);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ // Reserve format information cells (filled in later by drawFormat).
255
+ for (let i = 0; i < 15; i++) {
256
+ const fr = i < 6 ? i : i < 8 ? i + 1 : n - 15 + i;
257
+ const fc = i < 8 ? n - i - 1 : i < 9 ? 15 - i : 14 - i;
258
+ setCell(m, fr, 8, 0);
259
+ setCell(m, 8, fc, 0);
260
+ }
261
+ setCell(m, n - 8, 8, 1); // fixed dark module
262
+ if (version >= 7) {
263
+ for (let i = 0; i < 18; i++) {
264
+ const r = Math.floor(i / 3);
265
+ const c = (i % 3) + n - 11;
266
+ setCell(m, r, c, 0);
267
+ setCell(m, c, r, 0);
268
+ }
269
+ }
270
+ return m;
271
+ }
272
+ function drawFormat(m, version, ecc, mask) {
273
+ const n = m.length;
274
+ const bits = bchFormat((ECC_FORMAT[ecc] << 3) | mask);
275
+ for (let i = 0; i < 15; i++) {
276
+ const bit = (bits >>> i) & 1;
277
+ const r = i < 6 ? i : i < 8 ? i + 1 : n - 15 + i;
278
+ setCell(m, r, 8, bit);
279
+ const c = i < 8 ? n - i - 1 : i < 9 ? 15 - i : 14 - i;
280
+ setCell(m, 8, c, bit);
281
+ }
282
+ setCell(m, n - 8, 8, 1);
283
+ if (version >= 7) {
284
+ const vb = bchVersion(version);
285
+ for (let i = 0; i < 18; i++) {
286
+ const bit = (vb >>> i) & 1;
287
+ const r = Math.floor(i / 3);
288
+ const c = (i % 3) + n - 11;
289
+ setCell(m, r, c, bit);
290
+ setCell(m, c, r, bit);
291
+ }
292
+ }
293
+ }
294
+ function makeMatrix(version, ecc, data, mask) {
295
+ const m = matrixBase(version);
296
+ const n = m.length;
297
+ drawFormat(m, version, ecc, mask);
298
+ const maskFn = MASKS[mask];
299
+ let bi = 0;
300
+ let upward = true;
301
+ for (let right = n - 1; right >= 1; right -= 2) {
302
+ if (right === 6)
303
+ right--;
304
+ for (let k = 0; k < n; k++) {
305
+ const r = upward ? n - 1 - k : k;
306
+ for (let j = 0; j < 2; j++) {
307
+ const c = right - j;
308
+ if (getCell(m, r, c) !== -1)
309
+ continue;
310
+ let bit = 0;
311
+ if (bi < data.length * 8) {
312
+ const b = data[bi >>> 3];
313
+ bit = (b >>> (7 - (bi & 7))) & 1;
314
+ bi++;
315
+ }
316
+ if (maskFn(r, c))
317
+ bit ^= 1;
318
+ setCell(m, r, c, bit);
319
+ }
320
+ }
321
+ upward = !upward;
322
+ }
323
+ return m;
324
+ }
325
+ function penalty(m) {
326
+ const n = m.length;
327
+ let dark = 0;
328
+ for (const row of m)
329
+ for (const v of row)
330
+ dark += v;
331
+ let p = 0;
332
+ for (let r = 0; r < n; r++) {
333
+ let run = 1;
334
+ for (let c = 1; c < n; c++) {
335
+ if (getCell(m, r, c) === getCell(m, r, c - 1))
336
+ run++;
337
+ else {
338
+ if (run >= 5)
339
+ p += 3 + run - 5;
340
+ run = 1;
341
+ }
342
+ }
343
+ if (run >= 5)
344
+ p += 3 + run - 5;
345
+ }
346
+ for (let c = 0; c < n; c++) {
347
+ let run = 1;
348
+ for (let r = 1; r < n; r++) {
349
+ if (getCell(m, r, c) === getCell(m, r - 1, c))
350
+ run++;
351
+ else {
352
+ if (run >= 5)
353
+ p += 3 + run - 5;
354
+ run = 1;
355
+ }
356
+ }
357
+ if (run >= 5)
358
+ p += 3 + run - 5;
359
+ }
360
+ for (let r = 0; r < n - 1; r++) {
361
+ for (let c = 0; c < n - 1; c++) {
362
+ const x = getCell(m, r, c);
363
+ if (x === getCell(m, r, c + 1) && x === getCell(m, r + 1, c) && x === getCell(m, r + 1, c + 1))
364
+ p += 3;
365
+ }
366
+ }
367
+ const pattern = [1, 0, 1, 1, 1, 0, 1];
368
+ function matchLine(arr) {
369
+ for (let i = 0; i <= arr.length - 7; i++) {
370
+ let ok = true;
371
+ for (let j = 0; j < 7; j++) {
372
+ if (arr[i + j] !== pattern[j]) {
373
+ ok = false;
374
+ break;
375
+ }
376
+ }
377
+ if (ok)
378
+ p += 40;
379
+ }
380
+ }
381
+ for (let r = 0; r < n; r++)
382
+ matchLine([...m[r]]);
383
+ for (let c = 0; c < n; c++)
384
+ matchLine(Array.from({ length: n }, (_, r) => getCell(m, r, c)));
385
+ p += Math.floor(Math.abs((100 * dark) / (n * n) - 50) / 5) * 10;
386
+ return p;
387
+ }
388
+ function chooseMask(version, ecc, data) {
389
+ let best = null;
390
+ for (let mask = 0; mask < 8; mask++) {
391
+ const m = makeMatrix(version, ecc, data, mask);
392
+ const score = penalty(m);
393
+ if (!best || score < best.score)
394
+ best = { m, score };
395
+ }
396
+ return best.m;
397
+ }
398
+ /** Encodes `text` (byte/UTF-8 mode) into the smallest fitting QR matrix (versions 1-10). */
399
+ export function encodeQRMatrix(text, ecc) {
400
+ for (let version = 1; version <= 10; version++) {
401
+ try {
402
+ const data = makeDataBytes(text, version, ecc);
403
+ return { matrix: chooseMask(version, ecc, data), version };
404
+ }
405
+ catch (e) {
406
+ if (!(e instanceof CapacityExceededError))
407
+ throw e;
408
+ }
409
+ }
410
+ throw new Error('Input is too long to encode. This implementation supports QR versions 1-10 (byte mode); reduce the payload length or lower the error correction level.');
411
+ }
412
+ export { normalizeErrorCorrectionLevel };
@@ -0,0 +1,3 @@
1
+ import { UPIIntentParams } from '../types/upiqr.js';
2
+ /** Validates UPI intent params and builds the `upi://pay?...` deep link. */
3
+ export declare function buildUPIIntent({ payeeVPA: pa, payeeName: pn, payeeMerchantCode: mc, transactionId: tid, transactionRef: tr, transactionNote: tn, amount: am, minimumAmount: mam, currency: cu, }: UPIIntentParams): string;
@@ -0,0 +1,29 @@
1
+ function validate({ pa, pn }) {
2
+ if (!pa || !pn)
3
+ return "Virtual payee's address/name is compulsory";
4
+ if (pa.length < 5 || pn.length < 4)
5
+ return "Virtual payee's address/name is too short.";
6
+ return '';
7
+ }
8
+ /** Validates UPI intent params and builds the `upi://pay?...` deep link. */
9
+ export function buildUPIIntent({ payeeVPA: pa, payeeName: pn, payeeMerchantCode: mc, transactionId: tid, transactionRef: tr, transactionNote: tn, amount: am, minimumAmount: mam, currency: cu, }) {
10
+ const error = validate({ pa, pn });
11
+ if (error)
12
+ throw new Error(error);
13
+ const params = { pa, pn };
14
+ if (am)
15
+ params['am'] = am;
16
+ if (mam)
17
+ params['mam'] = mam;
18
+ if (cu)
19
+ params['cu'] = cu;
20
+ if (mc)
21
+ params['mc'] = mc;
22
+ if (tid)
23
+ params['tid'] = tid;
24
+ if (tr)
25
+ params['tr'] = tr;
26
+ if (tn)
27
+ params['tn'] = tn;
28
+ return 'upi://pay?' + new URLSearchParams(params).toString();
29
+ }
@@ -0,0 +1,4 @@
1
+ import { UpiqrRenderOptions } from '../types/upiqr.js';
2
+ import { RenderOptions } from './pixels.js';
3
+ /** Resolves user-supplied renderer options against defaults matching the previous `qrcode` package behavior. */
4
+ export declare function resolveRenderOptions(moduleCount: number, options?: UpiqrRenderOptions): RenderOptions;
@@ -0,0 +1,21 @@
1
+ import { parseHexColor } from './color.js';
2
+ /** Resolves user-supplied renderer options against defaults matching the previous `qrcode` package behavior. */
3
+ export function resolveRenderOptions(moduleCount, options) {
4
+ var _a, _b;
5
+ var _c, _d, _e, _f;
6
+ const margin = (_c = options === null || options === void 0 ? void 0 : options.margin) !== null && _c !== void 0 ? _c : 4;
7
+ if (margin < 0)
8
+ throw new Error('margin must be a non-negative number.');
9
+ const dark = parseHexColor((_d = (_a = options === null || options === void 0 ? void 0 : options.color) === null || _a === void 0 ? void 0 : _a.dark) !== null && _d !== void 0 ? _d : '#000000ff');
10
+ const light = parseHexColor((_e = (_b = options === null || options === void 0 ? void 0 : options.color) === null || _b === void 0 ? void 0 : _b.light) !== null && _e !== void 0 ? _e : '#ffffffff');
11
+ let scale = (_f = options === null || options === void 0 ? void 0 : options.scale) !== null && _f !== void 0 ? _f : 4;
12
+ if (scale <= 0)
13
+ throw new Error('scale must be a positive number.');
14
+ if (options === null || options === void 0 ? void 0 : options.width) {
15
+ const totalModules = moduleCount + margin * 2;
16
+ const computed = Math.floor(options.width / totalModules);
17
+ if (computed > 0)
18
+ scale = computed;
19
+ }
20
+ return { scale, margin, dark, light };
21
+ }
@@ -0,0 +1,15 @@
1
+ import { RGBA } from './color.js';
2
+ export interface RenderOptions {
3
+ scale: number;
4
+ margin: number;
5
+ dark: RGBA;
6
+ light: RGBA;
7
+ }
8
+ export interface RGBAImage {
9
+ width: number;
10
+ height: number;
11
+ /** Row-major RGBA pixel data, 4 bytes per pixel. */
12
+ data: Uint8ClampedArray<ArrayBuffer>;
13
+ }
14
+ /** Rasterizes a QR module matrix into an RGBA pixel buffer, applying scale/margin/colors. */
15
+ export declare function renderModulesToRGBA(matrix: readonly (readonly number[])[], options: RenderOptions): RGBAImage;
@@ -0,0 +1,22 @@
1
+ /** Rasterizes a QR module matrix into an RGBA pixel buffer, applying scale/margin/colors. */
2
+ export function renderModulesToRGBA(matrix, options) {
3
+ const { scale, margin, dark, light } = options;
4
+ const n = matrix.length;
5
+ const size = (n + margin * 2) * scale;
6
+ const data = new Uint8ClampedArray(size * size * 4);
7
+ for (let y = 0; y < size; y++) {
8
+ const moduleRow = Math.floor(y / scale) - margin;
9
+ const row = moduleRow >= 0 && moduleRow < n ? matrix[moduleRow] : undefined;
10
+ for (let x = 0; x < size; x++) {
11
+ const moduleCol = Math.floor(x / scale) - margin;
12
+ const isDark = row !== undefined && moduleCol >= 0 && moduleCol < n && row[moduleCol] === 1;
13
+ const color = isDark ? dark : light;
14
+ const idx = (y * size + x) * 4;
15
+ data[idx] = color.r;
16
+ data[idx + 1] = color.g;
17
+ data[idx + 2] = color.b;
18
+ data[idx + 3] = color.a;
19
+ }
20
+ }
21
+ return { width: size, height: size, data };
22
+ }
@@ -0,0 +1,3 @@
1
+ import { RGBAImage } from './pixels.js';
2
+ /** Encodes an RGBA pixel buffer as a PNG (truecolor + alpha), using only Node's built-in zlib. */
3
+ export declare function encodePNG(image: RGBAImage): Buffer;
package/dist/qr/png.js ADDED
@@ -0,0 +1,54 @@
1
+ import { deflateSync } from 'node:zlib';
2
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
3
+ function buildCrcTable() {
4
+ const table = new Uint32Array(256);
5
+ for (let n = 0; n < 256; n++) {
6
+ let c = n;
7
+ for (let k = 0; k < 8; k++)
8
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
9
+ table[n] = c >>> 0;
10
+ }
11
+ return table;
12
+ }
13
+ const CRC_TABLE = buildCrcTable();
14
+ function crc32(buf) {
15
+ let crc = 0xffffffff;
16
+ for (const byte of buf)
17
+ crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
18
+ return (crc ^ 0xffffffff) >>> 0;
19
+ }
20
+ function chunk(type, data) {
21
+ const length = Buffer.alloc(4);
22
+ length.writeUInt32BE(data.length, 0);
23
+ const typeAndData = Buffer.concat([Buffer.from(type, 'ascii'), data]);
24
+ const crc = Buffer.alloc(4);
25
+ crc.writeUInt32BE(crc32(typeAndData), 0);
26
+ return Buffer.concat([length, typeAndData, crc]);
27
+ }
28
+ /** Encodes an RGBA pixel buffer as a PNG (truecolor + alpha), using only Node's built-in zlib. */
29
+ export function encodePNG(image) {
30
+ const { width, height, data } = image;
31
+ const ihdr = Buffer.alloc(13);
32
+ ihdr.writeUInt32BE(width, 0);
33
+ ihdr.writeUInt32BE(height, 4);
34
+ ihdr.writeUInt8(8, 8); // bit depth
35
+ ihdr.writeUInt8(6, 9); // color type: truecolor + alpha
36
+ ihdr.writeUInt8(0, 10); // compression method
37
+ ihdr.writeUInt8(0, 11); // filter method
38
+ ihdr.writeUInt8(0, 12); // interlace method
39
+ const stride = width * 4;
40
+ const rgba = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
41
+ const raw = Buffer.alloc((stride + 1) * height);
42
+ for (let y = 0; y < height; y++) {
43
+ const destStart = y * (stride + 1);
44
+ raw[destStart] = 0; // filter type: none
45
+ rgba.copy(raw, destStart + 1, y * stride, y * stride + stride);
46
+ }
47
+ const idat = deflateSync(raw, { level: 9 });
48
+ return Buffer.concat([
49
+ PNG_SIGNATURE,
50
+ chunk('IHDR', ihdr),
51
+ chunk('IDAT', idat),
52
+ chunk('IEND', Buffer.alloc(0)),
53
+ ]);
54
+ }
@@ -1,3 +1,5 @@
1
1
 
2
- export type ImageType='png'|'jpeg'|'webp';export type Base64<imageType extends ImageType>=`data:image/${imageType};base64,${string}`;export interface UPIIntentParams{payeeVPA:string;payeeName:string;payeeMerchantCode?:string;transactionId?:string;transactionRef?:string;transactionNote?:string;amount?:string;minimumAmount?:string;currency?:string;transactionRefUrl?:string;}
2
+ export type ImageType='png';export type Base64<imageType extends ImageType>=`data:image/${imageType};base64,${string}`;export type ErrorCorrectionLevel='low'|'medium'|'quartile'|'high'|'L'|'M'|'Q'|'H';export interface UpiqrColorOptions{dark?:string;light?:string;}
3
+ export interface UpiqrRenderOptions{errorCorrectionLevel?:ErrorCorrectionLevel;margin?:number;scale?:number;width?:number;color?:UpiqrColorOptions;}
4
+ export interface UPIIntentParams{payeeVPA:string;payeeName:string;payeeMerchantCode?:string;transactionId?:string;transactionRef?:string;transactionNote?:string;amount?:string;minimumAmount?:string;currency?:string;transactionRefUrl?:string;}
3
5
  export interface QRResult{qr:Base64<ImageType>;intent:string;}
package/package.json CHANGED
@@ -1,9 +1,21 @@
1
1
  {
2
2
  "name": "upiqr",
3
- "version": "1.3.26",
4
- "description": "Generate NPCI's UPI QR code along with UPI intent link, By using it any payment is possible from UPI enabled apps.",
3
+ "version": "1.4.1",
4
+ "description": "Zero-dependency UPI QR code (PNG) and intent link generator for NPCI-compliant payments - works in Node.js and the browser, written in TypeScript.",
5
+ "homepage": "https://github.com/bhar4t/upiqr#readme",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
8
+ "browser": {
9
+ "./dist/index.js": "./dist/index.browser.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "browser": "./dist/index.browser.js",
15
+ "node": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
7
19
  "type": "module",
8
20
  "files": [
9
21
  "dist",
@@ -13,8 +25,9 @@
13
25
  ],
14
26
  "scripts": {
15
27
  "clean": "rimraf dist",
16
- "minify": "jsmin -o dist/types/upiqr.d.ts dist/types/upiqr.d.ts && jsmin -o dist/index.d.ts dist/index.d.ts && uglifyjs -o dist/index.js dist/index.js --mangle",
17
- "build": "npm run clean && tsc && npm run minify"
28
+ "minify": "jsmin -o dist/types/upiqr.d.ts dist/types/upiqr.d.ts && jsmin -o dist/index.d.ts dist/index.d.ts && jsmin -o dist/index.browser.d.ts dist/index.browser.d.ts && uglifyjs -o dist/index.js dist/index.js --mangle && uglifyjs -o dist/index.browser.js dist/index.browser.js --mangle",
29
+ "build": "npm run clean && tsc && npm run minify",
30
+ "test": "tsx --test src/__tests__/encoder.test.ts src/__tests__/roundtrip.test.ts src/__tests__/index.test.ts src/__tests__/canvas.test.ts"
18
31
  },
19
32
  "repository": {
20
33
  "type": "git",
@@ -24,12 +37,25 @@
24
37
  "upi",
25
38
  "upiqr",
26
39
  "qrcode",
40
+ "qr code",
41
+ "qr code generator",
42
+ "qr code npm",
43
+ "zero dependency",
44
+ "zero dependencies",
45
+ "dependency-free",
46
+ "typescript",
27
47
  "payment",
28
48
  "accept payment",
49
+ "upi payment",
29
50
  "upi qr code generator",
30
- "qr code generator",
31
51
  "upi qr code",
32
- "NPCI"
52
+ "upi qr generator",
53
+ "upi id",
54
+ "vpa",
55
+ "bharat qr",
56
+ "india payments",
57
+ "npci",
58
+ "npci upi"
33
59
  ],
34
60
  "publishConfig": {
35
61
  "provenance": true
@@ -40,14 +66,12 @@
40
66
  "type": "github",
41
67
  "url": "https://github.com/sponsors/bhar4t/"
42
68
  },
43
- "dependencies": {
44
- "qrcode": "^1.4.4"
45
- },
46
69
  "devDependencies": {
47
70
  "@types/node": "^26.6.2",
48
- "@types/qrcode": "^1.5.1",
49
71
  "jsmin": "^1.0.1",
72
+ "jsqr": "^1.4.0",
50
73
  "rimraf": "^6.1.3",
74
+ "tsx": "^4.23.15",
51
75
  "typescript": "^7.0.2",
52
76
  "uglify-js": "^3.19.3"
53
77
  }