""" 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()