wahdx-api 1.0.2 → 1.0.4

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.
@@ -1,199 +1,208 @@
1
- const QRCode = require("qrcode");
2
- const { createCanvas, loadImage } = require("canvas");
3
- const fs = require("fs");
4
- const sharp = require("sharp");
5
- const jsQR = require("jsqr");
6
- const path = require("path");
7
- // CJS sudah menyediakan __filename dan __dirname secara otomatis
8
-
9
- class QRISGenerator {
10
- constructor(config = {}) {
11
- this.config = {
12
- baseQrString: config.baseQrString || '',
13
- logoPath: config.logoPath || null,
14
- defaultQrPath: config.defaultQrPath || 'QRIS.png'
15
- };
16
- }
17
-
18
- /**
19
- * Membaca QR code dari gambar dan mengekstrak baseQrString
20
- * @param {string} imagePath - Path relatif atau absolut ke file gambar QR
21
- * @returns {Promise<string>} - Promise yang menghasilkan baseQrString
22
- */
23
- async readQRFromImage(imagePath) {
24
- try {
25
- // Konversi path relatif ke absolut jika diperlukan
26
- const absolutePath = path.isAbsolute(imagePath)
27
- ? imagePath
28
- : path.resolve(process.cwd(), imagePath);
29
-
30
- // Periksa apakah file ada
31
- if (!fs.existsSync(absolutePath)) {
32
- throw new Error(`File tidak ditemukan: ${absolutePath}`);
33
- }
34
-
35
- // Baca gambar menggunakan Sharp
36
- const image = sharp(absolutePath);
37
- const metadata = await image.metadata();
38
- const { width, height } = metadata;
39
-
40
- // Konversi ke raw pixel data
41
- const rawData = await image
42
- .raw()
43
- .toBuffer();
44
-
45
- // Format data untuk jsQR
46
- const imageData = new Uint8ClampedArray(width * height * 4);
47
-
48
- for (let i = 0; i < rawData.length; i += 3) { // RGB format
49
- const pixelIndex = (i / 3) * 4; // Convert RGB index to RGBA index
50
- imageData[pixelIndex] = rawData[i]; // R
51
- imageData[pixelIndex + 1] = rawData[i + 1]; // G
52
- imageData[pixelIndex + 2] = rawData[i + 2]; // B
53
- imageData[pixelIndex + 3] = 255; // A (full opacity)
54
- }
55
-
56
- // Dekode QR code menggunakan jsQR
57
- const code = jsQR(imageData, width, height);
58
-
59
- if (code) {
60
- // Simpan baseQrString yang dibaca ke config
61
- this.config.baseQrString = code.data;
62
- return code.data;
63
- } else {
64
- throw new Error('QR code tidak terdeteksi dalam gambar');
65
- }
66
- } catch (error) {
67
- console.error('Error saat membaca QR code:', error);
68
- throw error;
69
- }
70
- }
71
-
72
- /**
73
- * Membaca QR code dari file default (biasanya QRIS.png)
74
- * @returns {Promise<string>} - Promise yang menghasilkan baseQrString
75
- */
76
- async readDefaultQR() {
77
- try {
78
- // Path default ke file QRIS.png
79
- const defaultPath = path.resolve(process.cwd(), this.config.defaultQrPath);
80
- return await this.readQRFromImage(defaultPath);
81
- } catch (error) {
82
- console.error(`Error saat membaca gambar default (${this.config.defaultQrPath}):`, error);
83
- throw error;
84
- }
85
- }
86
-
87
- async generateQRWithLogo(qrString) {
88
- try {
89
- if (!qrString) {
90
- throw new Error('qrString tidak boleh kosong');
91
- }
92
- const canvas = createCanvas(500, 500);
93
- const ctx = canvas.getContext('2d');
94
- await QRCode.toCanvas(canvas, qrString, {
95
- errorCorrectionLevel: 'H',
96
- margin: 2,
97
- width: 500,
98
- color: {
99
- dark: '#000000',
100
- light: '#ffffff'
101
- }
102
- });
103
- if (this.config.logoPath && fs.existsSync(this.config.logoPath)) {
104
- const logo = await loadImage(this.config.logoPath);
105
- const logoSize = canvas.width * 0.2;
106
- const logoPosition = (canvas.width - logoSize) / 2;
107
-
108
- ctx.fillStyle = '#FFFFFF';
109
- ctx.fillRect(logoPosition - 5, logoPosition - 5, logoSize + 10, logoSize + 10);
110
- ctx.drawImage(logo, logoPosition, logoPosition, logoSize, logoSize);
111
- }
112
- return canvas.toBuffer('image/png');
113
- } catch (error) {
114
- throw new Error('Gagal generate QR: ' + error.message);
115
- }
116
- }
117
-
118
- /**
119
- * Membaca QR dari file kemudian generate QR baru dengan nominal
120
- * @param {number} amount - Nominal pembayaran
121
- * @param {string} qrImagePath - Path ke gambar QR (opsional)
122
- * @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
123
- */
124
- async generateQRFromImage(amount, qrImagePath = null) {
125
- try {
126
- // Jika path gambar disediakan, baca dari path tersebut
127
- if (qrImagePath) {
128
- await this.readQRFromImage(qrImagePath);
129
- }
130
- // Jika tidak ada path dan baseQrString kosong, gunakan default
131
- else if (!this.config.baseQrString) {
132
- await this.readDefaultQR();
133
- }
134
-
135
- // Generate QR string dengan nominal
136
- const qrString = this.generateQrString(amount);
137
-
138
- // Generate QR image
139
- const qrBuffer = await this.generateQRWithLogo(qrString);
140
-
141
- return {
142
- qrString,
143
- qrBuffer
144
- };
145
- } catch (error) {
146
- throw new Error('Gagal generate QR dari gambar: ' + error.message);
147
- }
148
- }
149
-
150
- generateQrString(amount) {
151
- try {
152
- if (!amount || amount <= 0) {
153
- throw new Error('Nominal harus lebih besar dari 0');
154
- }
155
-
156
- if (!this.config.baseQrString) {
157
- throw new Error('BaseQrString tidak tersedia. Gunakan readQRFromImage terlebih dahulu atau berikan baseQrString pada config.');
158
- }
159
-
160
- if (!this.config.baseQrString.includes("5802ID")) {
161
- throw new Error("Format QRIS tidak valid");
162
- }
163
-
164
- const finalAmount = Math.floor(amount);
165
- const qrisBase = this.config.baseQrString.slice(0, -4).replace("010211", "010212");
166
- const nominalStr = finalAmount.toString();
167
- const nominalTag = `54${nominalStr.length.toString().padStart(2, '0')}${nominalStr}`;
168
- const insertPosition = qrisBase.indexOf("5802ID");
169
- const qrisWithNominal = qrisBase.slice(0, insertPosition) + nominalTag + qrisBase.slice(insertPosition);
170
- const checksum = this.calculateCRC16(qrisWithNominal);
171
-
172
- return qrisWithNominal + checksum;
173
- } catch (error) {
174
- throw new Error('Gagal generate string QRIS: ' + error.message);
175
- }
176
- }
177
-
178
- calculateCRC16(str) {
179
- try {
180
- if (!str) {
181
- throw new Error('String tidak boleh kosong');
182
- }
183
-
184
- let crc = 0xFFFF;
185
- for (let i = 0; i < str.length; i++) {
186
- crc ^= str.charCodeAt(i) << 8;
187
- for (let j = 0; j < 8; j++) {
188
- crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
189
- }
190
- crc &= 0xFFFF;
191
- }
192
- return crc.toString(16).toUpperCase().padStart(4, '0');
193
- } catch (error) {
194
- throw new Error('Gagal kalkulasi CRC16: ' + error.message);
195
- }
196
- }
197
- }
198
-
1
+ const QRCode = require("qrcode");
2
+ const fs = require("fs");
3
+ const sharp = require("sharp");
4
+ const jsQR = require("jsqr");
5
+ const path = require("path");
6
+ // CJS sudah menyediakan __filename dan __dirname secara otomatis
7
+
8
+ class QRISGenerator {
9
+ constructor(config = {}) {
10
+ this.config = {
11
+ baseQrString: config.baseQrString || '',
12
+ defaultQrPath: config.defaultQrPath || 'QRIS.png'
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Membaca QR code dari gambar dan mengekstrak baseQrString
18
+ * @param {string} imagePath - Path relatif atau absolut ke file gambar QR
19
+ * @returns {Promise<string>} - Promise yang menghasilkan baseQrString
20
+ */
21
+ async readQRFromImage(imagePath) {
22
+ try {
23
+ // Konversi path relatif ke absolut jika diperlukan
24
+ const absolutePath = path.isAbsolute(imagePath)
25
+ ? imagePath
26
+ : path.resolve(process.cwd(), imagePath);
27
+
28
+ // Periksa apakah file ada
29
+ if (!fs.existsSync(absolutePath)) {
30
+ throw new Error(`File tidak ditemukan: ${absolutePath}`);
31
+ }
32
+
33
+ // Baca gambar menggunakan Sharp
34
+ const image = sharp(absolutePath);
35
+ const metadata = await image.metadata();
36
+ const { width, height } = metadata;
37
+
38
+ // Konversi ke raw pixel data
39
+ const rawData = await image
40
+ .raw()
41
+ .toBuffer();
42
+
43
+ // Format data untuk jsQR
44
+ const imageData = new Uint8ClampedArray(width * height * 4);
45
+
46
+ for (let i = 0; i < rawData.length; i += 3) { // RGB format
47
+ const pixelIndex = (i / 3) * 4; // Convert RGB index to RGBA index
48
+ imageData[pixelIndex] = rawData[i]; // R
49
+ imageData[pixelIndex + 1] = rawData[i + 1]; // G
50
+ imageData[pixelIndex + 2] = rawData[i + 2]; // B
51
+ imageData[pixelIndex + 3] = 255; // A (full opacity)
52
+ }
53
+
54
+ // Dekode QR code menggunakan jsQR
55
+ const code = jsQR(imageData, width, height);
56
+
57
+ if (code) {
58
+ // Simpan baseQrString yang dibaca ke config
59
+ this.config.baseQrString = code.data;
60
+ return code.data;
61
+ } else {
62
+ throw new Error('QR code tidak terdeteksi dalam gambar');
63
+ }
64
+ } catch (error) {
65
+ console.error('Error saat membaca QR code:', error);
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Membaca QR code dari file default (biasanya QRIS.png)
72
+ * @returns {Promise<string>} - Promise yang menghasilkan baseQrString
73
+ */
74
+ async readDefaultQR() {
75
+ try {
76
+ // Path default ke file QRIS.png
77
+ const defaultPath = path.resolve(process.cwd(), this.config.defaultQrPath);
78
+ return await this.readQRFromImage(defaultPath);
79
+ } catch (error) {
80
+ console.error(`Error saat membaca gambar default (${this.config.defaultQrPath}):`, error);
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Generate QR code sebagai PNG buffer
87
+ * @param {string} qrString - String QR code yang akan digenerate
88
+ * @returns {Promise<Buffer>} - Buffer PNG dari QR code
89
+ */
90
+ async generateQRImage(qrString) {
91
+ try {
92
+ if (!qrString) {
93
+ throw new Error('qrString tidak boleh kosong');
94
+ }
95
+
96
+ // Gunakan QRCode.toBuffer langsung tanpa canvas
97
+ const qrBuffer = await QRCode.toBuffer(qrString, {
98
+ errorCorrectionLevel: 'H',
99
+ margin: 2,
100
+ width: 500,
101
+ color: {
102
+ dark: '#000000',
103
+ light: '#ffffff'
104
+ }
105
+ });
106
+
107
+ return qrBuffer;
108
+ } catch (error) {
109
+ throw new Error('Gagal generate QR: ' + error.message);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Membaca QR dari file kemudian generate QR baru dengan nominal
115
+ * @param {number} amount - Nominal pembayaran
116
+ * @param {string} qrImagePath - Path ke gambar QR (opsional)
117
+ * @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
118
+ */
119
+ async generateQRFromImage(amount, qrImagePath = null) {
120
+ try {
121
+ // Jika path gambar disediakan, baca dari path tersebut
122
+ if (qrImagePath) {
123
+ await this.readQRFromImage(qrImagePath);
124
+ }
125
+ // Jika tidak ada path dan baseQrString kosong, gunakan default
126
+ else if (!this.config.baseQrString) {
127
+ await this.readDefaultQR();
128
+ }
129
+
130
+ // Generate QR string dengan nominal
131
+ const qrString = this.generateQrString(amount);
132
+
133
+ // Generate QR image
134
+ const qrBuffer = await this.generateQRImage(qrString);
135
+
136
+ return {
137
+ qrString,
138
+ qrBuffer
139
+ };
140
+ } catch (error) {
141
+ throw new Error('Gagal generate QR dari gambar: ' + error.message);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Generate QR dengan nominal tertentu
147
+ * @param {number} amount - Nominal pembayaran
148
+ * @returns {Promise<{qrString: string, qrBuffer: Buffer}>}
149
+ */
150
+ async generateQR(amount) {
151
+ const qrString = this.generateQrString(amount);
152
+ const qrBuffer = await this.generateQRImage(qrString);
153
+ return {
154
+ qrString,
155
+ qrBuffer
156
+ };
157
+ }
158
+
159
+ generateQrString(amount) {
160
+ try {
161
+ if (!amount || amount <= 0) {
162
+ throw new Error('Nominal harus lebih besar dari 0');
163
+ }
164
+
165
+ if (!this.config.baseQrString) {
166
+ throw new Error('BaseQrString tidak tersedia. Gunakan readQRFromImage terlebih dahulu atau berikan baseQrString pada config.');
167
+ }
168
+
169
+ if (!this.config.baseQrString.includes("5802ID")) {
170
+ throw new Error("Format QRIS tidak valid");
171
+ }
172
+
173
+ const finalAmount = Math.floor(amount);
174
+ const qrisBase = this.config.baseQrString.slice(0, -4).replace("010211", "010212");
175
+ const nominalStr = finalAmount.toString();
176
+ const nominalTag = `54${nominalStr.length.toString().padStart(2, '0')}${nominalStr}`;
177
+ const insertPosition = qrisBase.indexOf("5802ID");
178
+ const qrisWithNominal = qrisBase.slice(0, insertPosition) + nominalTag + qrisBase.slice(insertPosition);
179
+ const checksum = this.calculateCRC16(qrisWithNominal);
180
+
181
+ return qrisWithNominal + checksum;
182
+ } catch (error) {
183
+ throw new Error('Gagal generate string QRIS: ' + error.message);
184
+ }
185
+ }
186
+
187
+ calculateCRC16(str) {
188
+ try {
189
+ if (!str) {
190
+ throw new Error('String tidak boleh kosong');
191
+ }
192
+
193
+ let crc = 0xFFFF;
194
+ for (let i = 0; i < str.length; i++) {
195
+ crc ^= str.charCodeAt(i) << 8;
196
+ for (let j = 0; j < 8; j++) {
197
+ crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
198
+ }
199
+ crc &= 0xFFFF;
200
+ }
201
+ return crc.toString(16).toUpperCase().padStart(4, '0');
202
+ } catch (error) {
203
+ throw new Error('Gagal kalkulasi CRC16: ' + error.message);
204
+ }
205
+ }
206
+ }
207
+
199
208
  module.exports = QRISGenerator;