@osca16/slnic 0.1.0

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 ADDED
@@ -0,0 +1,16 @@
1
+ # Welcome to the my Package
2
+
3
+ This Package Contains Sri Lankan NIC Validiator..If you wish to add Sri Lankan NIC validation to your web site or node.js base development this is for you.
4
+
5
+ Prerequisites
6
+ #Ensure Your minimum node enviroment upto 18+
7
+ #Ensure your development language is javascript (typescript available soon).
8
+ #if you have any error or additiones please let me know from my git or following my social accounts.
9
+ #enjoy the package
10
+
11
+ installation and usage
12
+
13
+ #npm install or npm init -y
14
+ #npm install @srilanka-nic-validator/slnic-js (for install package)
15
+
16
+ Thankyou!
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@osca16/slnic",
3
+ "version": "0.1.0",
4
+ "description": "Sri Lankan NIC parser + optional OCR & PDF417 barcode verification (Sinhala/Tamil/English) – pure JavaScript",
5
+ "keywords": ["sri lanka", "nic", "ocr", "pdf417", "barcode", "sinhala", "tamil"],
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js",
11
+ "require": "./src/index.js"
12
+ }
13
+ },
14
+ "main": "src/index.js",
15
+ "module": "src/index.js",
16
+ "files": ["src", "README.md", "LICENSE"],
17
+ "sideEffects": false,
18
+ "scripts": {
19
+ "test": "node -e \"import('./src/index.js').then(m=>console.log('OK', Object.keys(m)))\"",
20
+ "prepublishOnly": "node -e \"console.log('building… (no build step for JS)')\""
21
+ }
22
+ }
package/src/barcode.js ADDED
@@ -0,0 +1,73 @@
1
+ import { BrowserPDF417Reader, HTMLCanvasElementLuminanceSource, HybridBinarizer, BinaryBitmap } from "@zxing/library";
2
+
3
+ async function toCanvas(input) {
4
+ if (input instanceof HTMLCanvasElement) return input;
5
+ const canvas = document.createElement("canvas");
6
+ const ctx = canvas.getContext("2d");
7
+
8
+ let bitmap;
9
+ if (input instanceof Blob) {
10
+ bitmap = await createImageBitmap(input);
11
+ } else {
12
+ bitmap = await createImageBitmap(input);
13
+ }
14
+ canvas.width = bitmap.width;
15
+ canvas.height = bitmap.height;
16
+ ctx.drawImage(bitmap, 0, 0);
17
+ return canvas;
18
+ }
19
+
20
+ export async function decodeBackBarcode(image) {
21
+ const reader = new BrowserPDF417Reader();
22
+ const canvas = await toCanvas(image);
23
+
24
+ try {
25
+ const source = new HTMLCanvasElementLuminanceSource(canvas);
26
+ const bitmap = new BinaryBitmap(new HybridBinarizer(source));
27
+ const result = reader.decodeBitmap(bitmap);
28
+
29
+ const raw = result.getText();
30
+ return { decoded: true, raw, fields: parseKeyValues(raw) };
31
+ } catch {
32
+ // Try fallback decodeFromImage if available
33
+ try {
34
+ const result = await reader.decodeFromImage(undefined, canvas);
35
+ const raw = result.getText();
36
+ return { decoded: true, raw, fields: parseKeyValues(raw) };
37
+ } catch {
38
+ return { decoded: false };
39
+ }
40
+ }
41
+ }
42
+
43
+ function parseKeyValues(raw) {
44
+ const fields = {};
45
+ (raw || "")
46
+ .split(/\r?\n|;|\|/)
47
+ .map((s) => s.trim())
48
+ .forEach((line) => {
49
+ const m = line.match(/^([A-Za-z_]+)\s*[:=]\s*(.+)$/);
50
+ if (m) fields[m[1].toLowerCase()] = m[2];
51
+ });
52
+ return fields;
53
+ }
54
+
55
+ function normalizeNic(s) {
56
+ return (s || "").toUpperCase().replace(/\s+/g, "");
57
+ }
58
+
59
+ /** Compare decoded barcode fields vs parsed NIC object { nic, birthDateISO } */
60
+ export function compareBarcodeAndParsed(barcode, parsed) {
61
+ if (!barcode || !barcode.decoded) return false;
62
+ const fields = barcode.fields || {};
63
+
64
+ const nicFromBarcode = normalizeNic(fields.nic || fields.id || fields.nid);
65
+ const nicMatch = nicFromBarcode ? nicFromBarcode === normalizeNic(parsed.nic) : true;
66
+
67
+ const dobFromBarcode = fields.dob || fields.dateofbirth || fields.birthdate;
68
+ const dobMatch = dobFromBarcode
69
+ ? [dobFromBarcode, dobFromBarcode.replaceAll("/", "-")].some((v) => (parsed.birthDateISO || "").startsWith(v))
70
+ : true;
71
+
72
+ return nicMatch && dobMatch;
73
+ }
package/src/face.js ADDED
@@ -0,0 +1,5 @@
1
+ // Later: plug in onnxruntime-web + ArcFace embeddings.
2
+ // For now we return undefined similarity so it doesn't block verification.
3
+ export async function compareFaces(_frontImage, _selfieImage) {
4
+ return { similarity: undefined, livenessPassed: false };
5
+ }
package/src/index.js ADDED
@@ -0,0 +1,73 @@
1
+ export { parseSriLankaNIC, formatNicDetails } from "./validators.js";
2
+ export { ocrNicFromImage } from "./ocr.js";
3
+ export { decodeBackBarcode, compareBarcodeAndParsed } from "./barcode.js";
4
+ export { compareFaces } from "./face.js";
5
+
6
+ /**
7
+ * High-level helper (front/back/selfie are optional; returns best-effort).
8
+ * frontImage/backImage/selfieImage: Blob | HTMLImageElement | HTMLCanvasElement
9
+ * languages: ["en-LK","si-LK","ta-LK"]
10
+ */
11
+ export async function verifyNicFromImages(opts = {}) {
12
+ const { frontImage, backImage, selfieImage, languages = ["en-LK", "si-LK", "ta-LK"] } = opts;
13
+
14
+ const reasons = [];
15
+ const result = { decision: "insufficient", reasons };
16
+
17
+ // 1) OCR + parse
18
+ if (frontImage) {
19
+ try {
20
+ const ocr = await (await import("./ocr.js")).ocrNicFromImage(frontImage, languages);
21
+ result.ocr = ocr;
22
+ if (ocr.nicText) {
23
+ const parsed = (await import("./validators.js")).parseSriLankaNIC(ocr.nicText);
24
+ result.nic = parsed;
25
+ if (!parsed.valid) reasons.push("NIC pattern/DOB invalid");
26
+ } else {
27
+ reasons.push("Could not read NIC number via OCR");
28
+ }
29
+ } catch (e) {
30
+ reasons.push("OCR failed");
31
+ }
32
+ }
33
+
34
+ // 2) Barcode decode + compare
35
+ if (backImage) {
36
+ try {
37
+ const barcode = await (await import("./barcode.js")).decodeBackBarcode(backImage);
38
+ result.barcode = barcode;
39
+ if (barcode.decoded && result.nic) {
40
+ const ok = (await import("./barcode.js")).compareBarcodeAndParsed(barcode, result.nic);
41
+ if (!ok) reasons.push("Barcode data does not match OCR/parsed NIC");
42
+ } else if (!barcode.decoded) {
43
+ reasons.push("Could not decode NIC barcode");
44
+ }
45
+ } catch {
46
+ reasons.push("Barcode decode failed");
47
+ }
48
+ }
49
+
50
+ // 3) Face compare (placeholder)
51
+ if (frontImage && selfieImage) {
52
+ try {
53
+ const face = await (await import("./face.js")).compareFaces(frontImage, selfieImage);
54
+ result.face = face;
55
+ if (typeof face.similarity === "number" && face.similarity < 0.4) {
56
+ reasons.push("Face similarity below threshold");
57
+ }
58
+ } catch {
59
+ reasons.push("Face compare failed");
60
+ }
61
+ }
62
+
63
+ // Decision
64
+ const nicValid = result.nic && result.nic.valid;
65
+ const barcodeOK = result.barcode?.decoded ? !reasons.includes("Barcode data does not match OCR/parsed NIC") : true;
66
+ const faceOK = result.face?.similarity === undefined || result.face.similarity >= 0.4;
67
+
68
+ if (nicValid && barcodeOK && faceOK) result.decision = "verified";
69
+ else if (reasons.length > 0) result.decision = "mismatch";
70
+ else result.decision = "insufficient";
71
+
72
+ return result;
73
+ }
package/src/ocr.js ADDED
@@ -0,0 +1,48 @@
1
+ import Tesseract from "tesseract.js";
2
+
3
+ async function toCanvas(input) {
4
+ if (input instanceof HTMLCanvasElement) return input;
5
+ const canvas = document.createElement("canvas");
6
+ const ctx = canvas.getContext("2d");
7
+
8
+ let bitmap;
9
+ if (input instanceof Blob) {
10
+ bitmap = await createImageBitmap(input);
11
+ } else {
12
+ // HTMLImageElement
13
+ bitmap = await createImageBitmap(input);
14
+ }
15
+ canvas.width = bitmap.width;
16
+ canvas.height = bitmap.height;
17
+ ctx.drawImage(bitmap, 0, 0);
18
+ return canvas;
19
+ }
20
+
21
+ /**
22
+ * languages: array of locales ["en-LK","si-LK","ta-LK"]
23
+ */
24
+ export async function ocrNicFromImage(image, languages = ["en-LK", "si-LK", "ta-LK"]) {
25
+ const canvas = await toCanvas(image);
26
+ const langMap = { "en-LK": "eng", "si-LK": "sin", "ta-LK": "tam" };
27
+ const langs = languages.map((l) => langMap[l]).join("+");
28
+
29
+ const { data } = await Tesseract.recognize(canvas, langs, {
30
+ tessedit_char_whitelist: "0123456789VvXxABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/-. :"
31
+ });
32
+
33
+ const text = (data.text || "").replace(/\s+/g, " ").trim();
34
+
35
+ const nicMatch =
36
+ text.match(/\b\d{12}\b/) || // new
37
+ text.match(/\b\d{9}[VvXx]\b/); // old
38
+
39
+ const nameMatch = text.match(/Name[:\s]+([A-Za-z .]+)/i);
40
+ const dobMatch = text.match(/(DOB|Date of Birth)[:\s]+([0-9]{4}-[0-9]{2}-[0-9]{2}|[0-9]{2}[-/][0-9]{2}[-/][0-9]{4})/i);
41
+
42
+ return {
43
+ nicText: nicMatch ? nicMatch[0] : undefined,
44
+ name: nameMatch ? nameMatch[1].trim() : undefined,
45
+ dobText: dobMatch ? dobMatch[2] : undefined,
46
+ confidence: data.confidence
47
+ };
48
+ }
@@ -0,0 +1,53 @@
1
+ const OLD_RE = /^(\d{2})(\d{3})(\d{3})(\d)([VvXx])$/; // YY DDD SSS C L
2
+ const NEW_RE = /^(\d{4})(\d{3})(\d{4})(\d)$/; // YYYY DDD SSSS C
3
+
4
+ function isLeapYear(y) {
5
+ return (y % 4 === 0 && y % 100 !== 0) || (y % 400 === 0);
6
+ }
7
+ function dayOfYearToDate(year, doy) {
8
+ if (doy < 1 || doy > (isLeapYear(year) ? 366 : 365)) return null;
9
+ const d = new Date(Date.UTC(year, 0, 1));
10
+ d.setUTCDate(d.getUTCDate() + (doy - 1));
11
+ return d.toISOString().slice(0, 10);
12
+ }
13
+
14
+ export function parseSriLankaNIC(input) {
15
+ const nic = (input || "").toString().replace(/\s+/g, "");
16
+ let m = nic.match(NEW_RE);
17
+ if (m) {
18
+ const year = parseInt(m[1], 10);
19
+ let ddd = parseInt(m[2], 10);
20
+ const female = ddd >= 500;
21
+ if (female) ddd -= 500;
22
+ const birthISO = dayOfYearToDate(year, ddd);
23
+ if (!birthISO) return { valid: false, format: "new", nic, message: "Invalid day-of-year." };
24
+ return { valid: true, format: "new", nic, gender: female ? "female" : "male", birthDateISO: birthISO, birthYear: year, dayOfYear: ddd };
25
+ }
26
+ m = nic.match(OLD_RE);
27
+ if (m) {
28
+ const year = 1900 + parseInt(m[1], 10);
29
+ let ddd = parseInt(m[2], 10);
30
+ const female = ddd >= 500;
31
+ if (female) ddd -= 500;
32
+ const birthISO = dayOfYearToDate(year, ddd);
33
+ if (!birthISO) return { valid: false, format: "old", nic, message: "Invalid day-of-year." };
34
+ return { valid: true, format: "old", nic, gender: female ? "female" : "male", birthDateISO: birthISO, birthYear: year, dayOfYear: ddd };
35
+ }
36
+ return { valid: false, format: null, nic, message: "Does not match old or new NIC patterns." };
37
+ }
38
+
39
+ export function formatNicDetails(details, locale = "en-LK") {
40
+ if (!details || !details.valid || !details.birthDateISO) return details;
41
+ const dt = new Date(details.birthDateISO + "T00:00:00Z");
42
+ const df = new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "numeric" });
43
+ const genderLabels = {
44
+ "en-LK": { male: "Male", female: "Female" },
45
+ "si-LK": { male: "පුරුෂ", female: "ස්ත්‍රී" },
46
+ "ta-LK": { male: "ஆண்", female: "பெண்" }
47
+ };
48
+ return {
49
+ ...details,
50
+ birthDateLocalized: df.format(dt),
51
+ genderLocalized: details.gender ? genderLabels[locale][details.gender] : undefined
52
+ };
53
+ }