Files
mykad-reader-api/MyKad_Reader.py
T
ISMAIL MASSERAN 1e6cebc163 first init
2026-09-01 12:22:43 +08:00

338 lines
11 KiB
Python

"""
MyKad Reader GUI
Cross-platform (macOS / Windows) interface for reading a Malaysian MyKad
through any PC/SC reader, including the ACS ACR39U.
REQUIRES: pyscard, tkinter
OPTIONAL: Pillow (pip install pillow) — in-app photo preview
"""
from __future__ import annotations
import io
import os
import platform
import sys
import threading
import tkinter as tk
from tkinter import filedialog, messagebox
from API import list_readers
from mykad import MyKadData, MyKadError, read_mykad_from_reader
os.environ.setdefault("TK_SILENCE_DEPRECATION", "1")
BG = "#e8e8e8"
FG = "#111111"
ENTRY_BG = "#ffffff"
BUTTON_BG = "#dcdcdc"
FIELDS = [
("Name", "name"),
("GMPC name", "gmpc_name"),
("KPT name", "kpt_name"),
("NRIC", "nric"),
("Gender", "gender"),
("Old IC", "old_ic"),
("Date of birth", "dob"),
("Age", "age"),
("Birth place", "birth_place"),
("Date issued", "date_issued"),
("Citizenship", "citizenship"),
("Race", "race"),
("Religion", "religion"),
("Address 1", "address1"),
("Address 2", "address2"),
("Address 3", "address3"),
("Postcode", "postcode"),
("City", "city"),
("State", "state"),
("SOCSO", "socso"),
("Locality", "locality"),
]
def _load_pillow():
try:
from PIL import Image, ImageTk
return Image, ImageTk
except ImportError:
return None, None
class MyKadReaderUI:
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("MyKad Reader")
self.root.configure(bg=BG)
try:
self.root.iconbitmap("ACS_Small.ico")
except Exception:
pass
self.root.geometry("980x720")
self.root.minsize(820, 600)
self.reader_var = tk.StringVar(master=self.root)
self.status_var = tk.StringVar(
master=self.root, value="Insert a MyKad and click Read MyKad."
)
self.field_vars = {
key: tk.StringVar(master=self.root) for _, key in FIELDS
}
self.current_data = None
self._photo_image = None
self._reading = False
self._pil_image = None
self._pil_imagetk = None
self._reader_names = []
self._build()
self.root.after_idle(lambda: self.refresh_readers(warn=False))
def _frame(self, parent, **kwargs):
options = {"bg": BG, "bd": 1, "relief": "groove", "padx": 8, "pady": 8}
options.update(kwargs)
return tk.Frame(parent, **options)
def _label(self, parent, **kwargs):
options = {"bg": BG, "fg": FG, "anchor": "w"}
options.update(kwargs)
return tk.Label(parent, **options)
def _button(self, parent, **kwargs):
options = {
"bg": BUTTON_BG,
"fg": FG,
"activebackground": "#c8c8c8",
"activeforeground": FG,
"highlightbackground": BG,
"padx": 8,
}
options.update(kwargs)
return tk.Button(parent, **options)
def _entry(self, parent, textvariable):
return tk.Entry(
parent,
textvariable=textvariable,
bg=ENTRY_BG,
fg=FG,
insertbackground=FG,
highlightbackground=BG,
relief="sunken",
readonlybackground=ENTRY_BG,
state="readonly",
)
def _build(self):
status = self._label(
self.root, textvariable=self.status_var, anchor="w", padx=12, pady=8
)
status.pack(fill="x", side="bottom")
reader_box = self._frame(self.root)
reader_box.pack(fill="x", padx=10, pady=10, side="top")
self._label(reader_box, text="Smart Card Reader").pack(anchor="w")
row = tk.Frame(reader_box, bg=BG)
row.pack(fill="x", pady=(6, 0))
self._label(row, text="Select reader:").pack(side="left", padx=(0, 6))
self.reader_option = tk.OptionMenu(row, self.reader_var, "")
self.reader_option.configure(
bg=BUTTON_BG,
fg=FG,
activebackground="#c8c8c8",
activeforeground=FG,
highlightbackground=BG,
width=40,
)
self.reader_option["menu"].configure(bg=ENTRY_BG, fg=FG)
self.reader_option.pack(side="left", fill="x", expand=True, padx=4)
self._button(row, text="Refresh", command=lambda: self.refresh_readers(True)).pack(
side="left", padx=4
)
self._button(row, text="Read MyKad", command=self.read_card).pack(side="left", padx=4)
self._button(row, text="Save Photo...", command=self.save_photo).pack(
side="left", padx=4
)
body = tk.Frame(self.root, bg=BG)
body.pack(fill="both", expand=True, padx=10, pady=(0, 8))
photo_box = self._frame(body)
photo_box.pack(side="left", fill="y", padx=(0, 10))
self._label(photo_box, text="Photo").pack(anchor="w")
self.photo_label = tk.Label(
photo_box,
text="No photo",
width=24,
height=16,
bg=ENTRY_BG,
fg=FG,
relief="sunken",
bd=1,
)
self.photo_label.pack(pady=(6, 0))
form_box = self._frame(body)
form_box.pack(side="left", fill="both", expand=True)
self._label(form_box, text="JPN data").pack(anchor="w")
grid = tk.Frame(form_box, bg=BG)
grid.pack(fill="both", expand=True, pady=(6, 0))
grid.columnconfigure(1, weight=1)
grid.columnconfigure(3, weight=1)
midpoint = (len(FIELDS) + 1) // 2
for index, (label, key) in enumerate(FIELDS):
if index < midpoint:
row_i, col = index, 0
else:
row_i, col = index - midpoint, 2
self._label(grid, text=f"{label}:").grid(
row=row_i, column=col, sticky="w", padx=(4, 6), pady=3
)
self._entry(grid, self.field_vars[key]).grid(
row=row_i, column=col + 1, sticky="ew", padx=(0, 10), pady=3
)
def _set_reader_options(self, names):
menu = self.reader_option["menu"]
menu.delete(0, "end")
self._reader_names = list(names)
if not names:
self.reader_var.set("")
return
for name in names:
menu.add_command(label=name, command=lambda n=name: self.reader_var.set(n))
if self.reader_var.get() not in names:
self.reader_var.set(names[0])
def refresh_readers(self, warn=True):
readers = list_readers()
names = [str(reader) for reader in readers]
self._set_reader_options(names)
if names:
self.status_var.set(
f"{len(names)} reader(s) found. Insert a MyKad and click Read MyKad."
)
else:
self.status_var.set("No readers found. Check the ACR39U USB connection.")
if warn:
messagebox.showwarning(
"No Readers Found",
"No smart card readers found.\n\nConnect the ACR39U and click Refresh.",
parent=self.root,
)
def read_card(self):
if self._reading:
return
reader_name = self.reader_var.get().strip()
if not reader_name:
messagebox.showerror("Error", "Please select a reader first.", parent=self.root)
return
self._reading = True
self.status_var.set("Reading MyKad... keep the card still.")
self.root.update_idletasks()
def worker():
try:
data = read_mykad_from_reader(reader_name=reader_name)
self.root.after(0, lambda result=data: self._apply_result(result))
except MyKadError as exc:
message = str(exc)
self.root.after(0, lambda msg=message: self._apply_error(msg))
except Exception as exc:
message = str(exc)
self.root.after(0, lambda msg=message: self._apply_error(msg))
threading.Thread(target=worker, daemon=True).start()
def _apply_result(self, data: MyKadData):
self._reading = False
self.current_data = data
values = data.as_dict()
for _, key in FIELDS:
value = values.get(key)
self.field_vars[key].set("" if value is None else str(value))
self._show_photo(data.photo_jpeg)
extra = ""
if data.photo_jpeg and self._pil_imagetk is None:
extra = " Photo is in memory; install Pillow to preview."
self.status_var.set(f"Read OK — {data.nric or 'no NRIC'}." + extra)
def _apply_error(self, message: str):
self._reading = False
self.status_var.set(message)
messagebox.showerror("Read failed", message, parent=self.root)
def _show_photo(self, jpeg_bytes: bytes):
if not jpeg_bytes:
self._photo_image = None
self.photo_label.configure(image="", text="No photo")
return
if self._pil_image is None or self._pil_imagetk is None:
self._pil_image, self._pil_imagetk = _load_pillow()
if self._pil_image is None or self._pil_imagetk is None:
self._photo_image = None
self.photo_label.configure(image="", text="Photo read\n(install Pillow\nto preview)")
return
image = self._pil_image.open(io.BytesIO(jpeg_bytes))
image.thumbnail((220, 280))
self._photo_image = self._pil_imagetk.PhotoImage(image)
self.photo_label.configure(image=self._photo_image, text="")
def save_photo(self):
if not self.current_data or not self.current_data.photo_jpeg:
messagebox.showinfo(
"Save Photo", "Read a MyKad with a photo first.", parent=self.root
)
return
path = filedialog.asksaveasfilename(
parent=self.root,
title="Save MyKad photo",
defaultextension=".jpg",
filetypes=[("JPEG", "*.jpg"), ("All files", "*.*")],
initialfile="photo.jpg",
)
if not path:
return
try:
saved = self.current_data.save_photo(path)
except MyKadError as exc:
messagebox.showerror("Save Photo", str(exc), parent=self.root)
return
self.status_var.set(f"Photo saved: {saved}")
def main():
root = tk.Tk()
MyKadReaderUI(root)
root.mainloop()
if __name__ == "__main__":
try:
import tkinter as _tk # noqa: F401
except ImportError:
system = platform.system()
if system == "Darwin":
print("tkinter is missing. Install Python from python.org or: brew install python-tk")
elif system == "Linux":
print("tkinter is missing. Install it with: sudo apt-get install python3-tk")
else:
print("tkinter is missing. Reinstall Python and enable tcl/tk.")
if sys.stdout.isatty():
input("Press Enter to exit...")
sys.exit(1)
main()