first init
This commit is contained in:
+337
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user