# -*- coding: utf8 -*- """ ******************************************************************************** * CNIRevelator * * * * Desc: Application IHM & work main class * * * * Copyright © 2018-2019 Adrien Bourmault (neox95) * * * * This file is part of CNIRevelator. * * * * CNIRevelator is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * any later version. * * * * CNIRevelator is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY*without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with CNIRevelator. If not, see . * ******************************************************************************** """ from PIL import Image, ImageFont, ImageDraw, ImageTk, ImageEnhance, ImageFilter import math, warnings, string from tkinter import * from tkinter.messagebox import * from tkinter import filedialog from tkinter import ttk import threading from datetime import datetime from importlib import reload import unicodedata import re import cv2 import PIL.Image, PIL.ImageTk import os, shutil import sys, os import numpy import webbrowser import critical # critical.py import ihm # ihm.py import logger # logger.py import mrz # mrz.py import globs # globs.py import pytesseract # pytesseract.py import lang # lang.py import updater # updater.py # Global handler logfile = logger.logCur class mainWindow(Tk): ## App Pattern def __init__(self): Tk.__init__(self) self.initialize() def initialize(self): """ Initializes the main window """ self.mrzChar = "" self.mrzDecided = False self.Tags = [] self.compliance = True self.corners = [] self.indicators = [] self.validatedtext = "" # The icon if getattr(sys, 'frozen', False): self.iconbitmap(sys._MEIPASS + '\\id-card.ico\\id-card.ico') else: self.iconbitmap('id-card.ico') # Hide during construction self.withdraw() # Get the screen size and center ws = self.winfo_screenwidth() hs = self.winfo_screenheight() logfile.printdbg('Launching main window with resolution' + str(ws) + 'x' + str(hs)) # Configuring the size of each part of the window self.grid_columnconfigure(0, minsize=(ws / 2 * 0.3333333333333333)) self.grid_columnconfigure(1, minsize=(ws / 2 * 0.3333333333333333)) self.grid_columnconfigure(2, weight=1, minsize=(ws / 2 * 0.3333333333333333)) self.grid_rowconfigure(0, minsize=(hs / 2 * 0.5)) self.grid_rowconfigure(1, minsize=(hs / 2 * 0.10)) self.grid_rowconfigure(2, minsize=(hs / 2 * 0.35)) self.grid_rowconfigure(3, minsize=10) # Prepare the data sections self.lecteur_ci = ttk.Labelframe(self, text=lang.all[globs.CNIRlang]["Informations about the current document"]) self.lecteur_ci.grid_columnconfigure(0, weight=1) self.lecteur_ci.grid_columnconfigure(1, weight=1) self.lecteur_ci.grid_columnconfigure(2, weight=1) self.lecteur_ci.grid_columnconfigure(3, weight=1) self.lecteur_ci.grid_columnconfigure(4, weight=1) self.lecteur_ci.grid_columnconfigure(5, weight=1) self.lecteur_ci.grid_rowconfigure(1, weight=1) self.lecteur_ci.grid_rowconfigure(2, weight=1) self.lecteur_ci.grid_rowconfigure(3, weight=1) self.lecteur_ci.grid_rowconfigure(4, weight=1) self.lecteur_ci.grid_rowconfigure(5, weight=1) self.lecteur_ci.grid_rowconfigure(6, weight=1) # And what about the status bar ? self.statusbar = ihm.StatusBar(self) self.statusbar.grid(row=3, columnspan=3, sticky="NSEW") # Fill the data sections ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Status"])).grid(column=0, row=0, padx=5, pady=5) self.statusbar.set(lang.all[globs.CNIRlang]["IDLE"]) self.STATUStxt = ttk.Label((self.lecteur_ci), text=lang.all[globs.CNIRlang]["IDLE"], font=("TkDefaultFont", 13, "bold"), foreground="orange", anchor=CENTER) self.STATUStxt.grid(column=1, row=0, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Name"])).grid(column=0, row=1, padx=5, pady=5) self.nom = ttk.Label((self.lecteur_ci), text=' ') self.nom.grid(column=1, row=1, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} (2) : '.format(lang.all[globs.CNIRlang]["Name"])).grid(column=0, row=2, padx=5, pady=5) self.prenom = ttk.Label((self.lecteur_ci), text=' ') self.prenom.grid(column=1, row=2, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Birth date"])).grid(column=0, row=3, padx=5, pady=5) self.bdate = ttk.Label((self.lecteur_ci), text=' ') self.bdate.grid(column=1, row=3, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Issue date"])).grid(column=0, row=4, padx=5, pady=5) self.ddate = ttk.Label((self.lecteur_ci), text=' ') self.ddate.grid(column=1, row=4, padx=5, pady=5) ttk.Label((self.lecteur_ci), text="{} : ".format(lang.all[globs.CNIRlang]["Expiration date"])).grid(column=0, row=5, padx=5, pady=5) self.edate = ttk.Label((self.lecteur_ci), text=' ') self.edate.grid(column=1, row=5, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Sex"])).grid(column=4, row=1, padx=5, pady=5) self.sex = ttk.Label((self.lecteur_ci), text=' ') self.sex.grid(column=5, row=1, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Issuing country"])).grid(column=4, row=2, padx=5, pady=5) self.pays = ttk.Label((self.lecteur_ci), text=' ') self.pays.grid(column=5, row=2, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Nationality"])).grid(column=4, row=3, padx=5, pady=5) self.nat = ttk.Label((self.lecteur_ci), text=' ') self.nat.grid(column=5, row=3, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Registration"])).grid(column=4, row=4, padx=5, pady=5) self.indic = ttk.Label((self.lecteur_ci), text=' ') self.indic.grid(column=5, row=4, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Document number"])).grid(column=4, row=5, padx=5, pady=5) self.no = ttk.Label((self.lecteur_ci), text=' ') self.no.grid(column=5, row=5, padx=5, pady=5) ttk.Label((self.lecteur_ci), text='{} : '.format(lang.all[globs.CNIRlang]["Length"])).grid(column=0, row=6, padx=5, pady=5) self.len = ttk.Label((self.lecteur_ci), text=' ') self.len.grid(column=1, row=6, padx=5, pady=5) self.nom['text'] = lang.all[globs.CNIRlang]["Unknown"] self.prenom['text'] = lang.all[globs.CNIRlang]["Unknown"] self.bdate['text'] = lang.all[globs.CNIRlang]["Unknown"] self.ddate['text'] = lang.all[globs.CNIRlang]["Unknown"] self.edate['text'] = lang.all[globs.CNIRlang]["Unknown"] self.no['text'] = lang.all[globs.CNIRlang]["Unknown"] self.sex['text'] = lang.all[globs.CNIRlang]["Unknown"] self.nat['text'] = lang.all[globs.CNIRlang]["Unknown"] self.pays['text'] = lang.all[globs.CNIRlang]["Unknown"] self.indic['text'] = lang.all[globs.CNIRlang]["Unknown"] self.len['text'] = lang.all[globs.CNIRlang]["Unknown"] self.infoList = \ { "NOM" : self.nom, "PRENOM" : self.prenom, "BDATE" : self.bdate, "DDATE" : self.ddate, "EDATE" : self.edate, "NO" : self.no, "SEX" : self.sex, "NAT" : self.nat, "PAYS" : self.pays, "INDIC" : self.indic, "LEN" : self.len, } # The the image viewer self.imageViewer = ttk.Labelframe(self, text=lang.all[globs.CNIRlang]["Display and processing of documents"]) self.imageViewer.grid_columnconfigure(0, weight=1) self.imageViewer.grid_columnconfigure(1, weight=0) self.imageViewer.grid_rowconfigure(0, weight=1) self.imageViewer.grid_rowconfigure(1, weight=1) self.imageViewer.grid_rowconfigure(2, weight=1) self.imageViewer.frame = Frame(self.imageViewer) self.imageViewer.frame.grid(column=0, row=0, sticky='NSEW') self.imageViewer.frame.grid_columnconfigure(0, weight=1) self.imageViewer.frame.grid_rowconfigure(0, weight=1) # + toolbar self.toolbar = ttk.Frame(self.imageViewer) self.toolbar.grid_columnconfigure(0, weight=1) self.toolbar.grid_columnconfigure(1, weight=1) self.toolbar.grid_columnconfigure(2, weight=1) self.toolbar.grid_columnconfigure(3, weight=1) self.toolbar.grid_columnconfigure(4, weight=1) self.toolbar.grid_columnconfigure(5, weight=1) self.toolbar.grid_columnconfigure(6, weight=1, minsize=10) self.toolbar.grid_columnconfigure(7, weight=1) self.toolbar.grid_columnconfigure(8, weight=1, minsize=10) self.toolbar.grid_columnconfigure(9, weight=1) self.toolbar.grid_columnconfigure(10, weight=1) self.toolbar.grid_columnconfigure(11, weight=1) self.toolbar.grid_columnconfigure(12, weight=1) self.toolbar.grid_columnconfigure(13, weight=1, minsize=10) self.toolbar.grid_columnconfigure(14, weight=1) self.toolbar.grid_columnconfigure(15, weight=1, minsize=10) self.toolbar.grid_columnconfigure(16, weight=1) self.toolbar.grid_rowconfigure(0, weight=1) self.toolbar.zoomIn50Img = ImageTk.PhotoImage(PIL.Image.open("zoomIn50.png")) self.toolbar.zoomIn50 = ttk.Button(self.toolbar, image=self.toolbar.zoomIn50Img, command=self.zoomInScan50) self.toolbar.zoomIn50.grid(column=0, row=0) self.toolbar.zoomIn20Img = ImageTk.PhotoImage(PIL.Image.open("zoomIn20.png")) self.toolbar.zoomIn20 = ttk.Button(self.toolbar, image=self.toolbar.zoomIn20Img, command=self.zoomInScan20) self.toolbar.zoomIn20.grid(column=1, row=0) self.toolbar.zoomInImg = ImageTk.PhotoImage(PIL.Image.open("zoomIn.png")) self.toolbar.zoomIn = ttk.Button(self.toolbar, image=self.toolbar.zoomInImg, command=self.zoomInScan) self.toolbar.zoomIn.grid(column=2, row=0) self.toolbar.zoomOutImg = ImageTk.PhotoImage(PIL.Image.open("zoomOut.png")) self.toolbar.zoomOut = ttk.Button(self.toolbar, image=self.toolbar.zoomOutImg, command=self.zoomOutScan) self.toolbar.zoomOut.grid(column=3, row=0) self.toolbar.zoomOut20Img = ImageTk.PhotoImage(PIL.Image.open("zoomOut20.png")) self.toolbar.zoomOut20 = ttk.Button(self.toolbar, image=self.toolbar.zoomOut20Img, command=self.zoomOutScan20) self.toolbar.zoomOut20.grid(column=4, row=0) self.toolbar.zoomOut50Img = ImageTk.PhotoImage(PIL.Image.open("zoomOut50.png")) self.toolbar.zoomOut50 = ttk.Button(self.toolbar, image=self.toolbar.zoomOut50Img, command=self.zoomOutScan50) self.toolbar.zoomOut50.grid(column=5, row=0) self.toolbar.invertImg = ImageTk.PhotoImage(PIL.Image.open("invert.png")) self.toolbar.invert = ttk.Button(self.toolbar, image=self.toolbar.invertImg, command=self.threshScan) self.toolbar.invert.grid(column=7, row=0) self.toolbar.rotateLeftImg = ImageTk.PhotoImage(PIL.Image.open("rotateLeft.png")) self.toolbar.rotateLeft = ttk.Button(self.toolbar, image=self.toolbar.rotateLeftImg, command=self.rotateLeft) self.toolbar.rotateLeft.grid(column=9, row=0) self.toolbar.rotateLeft1Img = ImageTk.PhotoImage(PIL.Image.open("rotateLeft1.png")) self.toolbar.rotateLeft1 = ttk.Button(self.toolbar, image=self.toolbar.rotateLeft1Img, command=self.rotateLeft1) self.toolbar.rotateLeft1.grid(column=10, row=0) self.toolbar.rotateRight1Img = ImageTk.PhotoImage(PIL.Image.open("rotateRight1.png")) self.toolbar.rotateRight1 = ttk.Button(self.toolbar, image=self.toolbar.rotateRight1Img, command=self.rotateRight1) self.toolbar.rotateRight1.grid(column=11, row=0) self.toolbar.rotateRightImg = ImageTk.PhotoImage(PIL.Image.open("rotateRight.png")) self.toolbar.rotateRight = ttk.Button(self.toolbar, image=self.toolbar.rotateRightImg, command=self.rotateRight) self.toolbar.rotateRight.grid(column=12, row=0) self.toolbar.goOCRImg = ImageTk.PhotoImage(PIL.Image.open("OCR.png")) self.toolbar.goOCR = ttk.Button(self.toolbar, image=self.toolbar.goOCRImg, command=self.goOCRDetection) self.toolbar.goOCR.grid(column=14, row=0) self.toolbar.pagenumber = StringVar() self.toolbar.pageChooser = ttk.Combobox(self.toolbar, textvariable=self.toolbar.pagenumber) self.toolbar.pageChooser.bind("<>", self.goPageChoice) self.toolbar.pageChooser['values'] = ('1') self.toolbar.pageChooser.current(0) self.toolbar.pageChooser.grid(column=16, row=0) self.toolbar.grid(column=0, row=2, padx=0, pady=0) # + image with scrollbars self.imageViewer.hbar = ttk.Scrollbar(self.imageViewer, orient='horizontal') self.imageViewer.vbar = ttk.Scrollbar(self.imageViewer, orient='vertical') self.imageViewer.hbar.grid(row=1, column=0, sticky="NSEW") self.imageViewer.vbar.grid(row=0, column=1, sticky="NSEW") self.imageViewer.ZONE = ihm.ResizeableCanvas(self.imageViewer.frame, bg=self["background"], xscrollcommand=(self.imageViewer.hbar.set), yscrollcommand=(self.imageViewer.vbar.set)) self.imageViewer.ZONE.grid(sticky="NSEW") self.imageViewer.hbar.config(command=self.imageViewer.ZONE.xview) self.imageViewer.vbar.config(command=self.imageViewer.ZONE.yview) self.STATUSimg = self.imageViewer.ZONE.create_image(0,0, image=None, anchor="nw") # The terminal to enter the MRZ self.terminal = ttk.Labelframe(self, text=lang.all[globs.CNIRlang]["Complete MRZ capture terminal"]) self.terminal.grid_columnconfigure(0, weight=1) self.terminal.grid_rowconfigure(0, weight=1) self.termframe = Frame(self.terminal) self.termframe.grid(column=0, row=0, sticky='EW') self.termframe.grid_columnconfigure(0, weight=1) self.termframe.grid_rowconfigure(0, weight=1) self.termguide = Label((self.termframe), text='', font='Terminal 17', fg='#006699') self.termguide.grid(column=0, row=0, padx=5, pady=0, sticky='NW') self.termguide['text'] = '0 |5 |10 |15 |20 |25 |30 |35 |40 |45' self.termtext = Text((self.termframe), state='normal', width=60, height=4, wrap='none', font='Terminal 17', fg='#121f38') self.termtext.grid(column=0, row=0, sticky='SW', padx=5, pady=25) # Speed Entry Zone for 731 self.terminal2 = ttk.Labelframe(self, text=lang.all[globs.CNIRlang]["Quick entry terminal (731)"]) self.terminal2.grid_columnconfigure(0, weight=1) self.terminal2.grid_rowconfigure(0, weight=1) self.speed731 = Frame(self.terminal2) self.speed731.grid(column=0, row=0, sticky='EW') self.speed731.grid_columnconfigure(0, weight=1) self.speed731.grid_columnconfigure(1, weight=1) self.speed731.grid_columnconfigure(2, weight=1) self.speed731.grid_columnconfigure(3, weight=1) self.speed731.grid_columnconfigure(4, weight=1) self.speed731.grid_columnconfigure(5, weight=1) self.speed731.grid_columnconfigure(6, weight=1) self.speed731.grid_columnconfigure(7, weight=1) self.speed731.grid_columnconfigure(8, weight=1) self.speed731.grid_columnconfigure(9, weight=1) self.speed731.grid_rowconfigure(0, weight=1) self.speed731text = Entry(self.speed731, font='Terminal 14') self.speed731text.grid(column=0, row=0, columnspan=7, sticky='NEW', padx=5, pady=5) self.speedResult = Text((self.speed731), state='disabled', width=1, height=1, wrap='none', font='Terminal 14') self.speedResult.grid(column=7, row=0, sticky='NEW', padx=5, pady=5) # The monitor that indicates some useful infos self.monitor = ttk.Labelframe(self, text=lang.all[globs.CNIRlang]["Monitor"]) self.monlog = Text((self.monitor), state='disabled', width=60, height=10, wrap='word') self.monlog.grid(column=0, row=0, sticky='EWNS', padx=5, pady=5) self.scrollb = ttk.Scrollbar((self.monitor), command=(self.monlog.yview)) self.scrollb.grid(column=1, row=0, sticky='EWNS', padx=5, pady=5) self.monlog['yscrollcommand'] = self.scrollb.set self.monitor.grid_columnconfigure(0, weight=1) self.monitor.grid_rowconfigure(0, weight=1) # All the items griding self.lecteur_ci.grid(column=2, row=0, sticky='EWNS', columnspan=1, padx=5, pady=5) self.imageViewer.grid(column=0, row=0, sticky='EWNS', columnspan=2, padx=5, pady=5) self.terminal.grid(column=0, row=2, sticky='EWNS', columnspan=2, padx=5, pady=5) self.terminal2.grid(column=0, row=1, sticky='EWNS', columnspan=2, padx=5, pady=5) self.monitor.grid(column=2, row=1, sticky='EWNS', columnspan=1, rowspan=2, padx=5, pady=5) # What is a window without a menu bar ? menubar = Menu(self) menu1 = Menu(menubar, tearoff=0) menu1.add_command(label=lang.all[globs.CNIRlang]["New"], command=(self.newEntry)) menu1.add_command(label=lang.all[globs.CNIRlang]["Open scan..."], command=(self.openingScan)) menu1.add_separator() menu1.add_command(label=lang.all[globs.CNIRlang]["Quit"], command=(self.destroy)) menubar.add_cascade(label=lang.all[globs.CNIRlang]["File"], menu=menu1) menu2 = Menu(menubar, tearoff=0) menu2.add_command(label=lang.all[globs.CNIRlang]["Update options"], command=(self.updateSet)) menu2.add_command(label=lang.all[globs.CNIRlang]["Show Changelog"], command=(self.showChangeLog)) menu2.add_separator() menu2.add_command(label=lang.all[globs.CNIRlang]["Language"], command=(self.languageSet)) menubar.add_cascade(label=lang.all[globs.CNIRlang]["Settings"], menu=menu2) menu3 = Menu(menubar, tearoff=0) menu3.add_command(label=lang.all[globs.CNIRlang]["Keyboard commands"], command=(self.helpbox)) menu3.add_command(label=lang.all[globs.CNIRlang]["Report a bug"], command=(self.openIssuePage)) menu3.add_separator() menu3.add_command(label=lang.all[globs.CNIRlang]["Project Webpage"], command=(self.openProjectPage)) menu3.add_command(label=lang.all[globs.CNIRlang]["About CNIRevelator"], command=(self.infobox)) menubar.add_cascade(label=lang.all[globs.CNIRlang]["Help"], menu=menu3) menubar.add_command(label="<|>", command=(self.panelResize)) self.config(menu=menubar) # The title self.wm_title(globs.CNIRName) # Make this window resizable and set her size self.resizable(0, 0) self.update() self.w = int(self.winfo_reqwidth()) self.h = int(self.winfo_reqheight()) self.ws = self.winfo_screenwidth() self.hs = self.winfo_screenheight() self.x = (self.ws - self.w)/2 self.y = (self.hs - self.h)/2 self.geometry('%dx%d+%d+%d' % (self.w, self.h, self.x, self.y)) self.update() self.deiconify() #self.attributes("-topmost", 1) self.maxsize(self.w, self.h) self.minsize(int(2.15 * (self.ws / 2 * 0.3333333333333333)), self.h) self.currentw = self.w # Set image self.imageViewer.image = None self.imageViewer.imagePath = None self.imageViewer.imgZoom = 1 self.imageViewer.rotateCount = 0 self.imageViewer.blackhat = 0 self.imageViewer.pagenumber = 0 # Some bindings self.bind('', self.entryValidation) self.termtext.bind('', self.entryValidation) self.termtext.bind('<>', self.pasteValidation) self.speed731text.bind('', self.speedValidation) self.imageViewer.ZONE.bind("", self.rectangleSelectScan) logfile.printdbg('Initialization successful') if globs.CNIROpenFile: self.after_idle(lambda : self.openScanFile(sys.argv[1])) ## OCR related functions def rectangleSelectScan(self, event): """ Realises the selection of the MRZ to make OCR possible """ if self.imageViewer.image: canvas = event.widget #print("Get coordinates : [{}, {}], for [{}, {}]".format(canvas.canvasx(event.x), canvas.canvasy(event.y), event.x, event.y)) self.corners.append([canvas.canvasx(event.x), canvas.canvasy(event.y)]) if len(self.corners) == 2: self.select = self.imageViewer.ZONE.create_rectangle(self.corners[0][0], self.corners[0][1], self.corners[1][0], self.corners[1][1], outline ='red', width = 2) #print("Get rectangle : [{}, {}], for [{}, {}]".format(self.corners[0][0], self.corners[0][1], self.corners[1][0], self.corners[1][1])) # Reinit if len(self.corners) > 2: self.corners = [] self.imageViewer.ZONE.delete(self.select) for k in self.indicators: self.imageViewer.ZONE.delete(k) # Draw indicator else: self.indicators.append(self.imageViewer.ZONE.create_oval(canvas.canvasx(event.x)-4, canvas.canvasy(event.y)-4,canvas.canvasx(event.x)+4, canvas.canvasy(event.y)+4, fill='red', outline='red', width = 2)) def goOCRDetection(self): """ Realises the OCR detection and get the text in self.mrzChar (and prints it) """ if self.imageViewer.image: cv_img = cv2.imreadmulti(self.imageViewer.imagePath)[1][self.imageViewer.pagenumber] cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) if self.imageViewer.blackhat == 1: cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) cv_img = cv2.threshold(cv_img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] elif self.imageViewer.blackhat == 2: cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) cv_img = cv2.medianBlur(cv_img, 3) try: # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width, channels_no = cv_img.shape # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width, channels_no = cv_img.shape except ValueError: # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width = cv_img.shape # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width = cv_img.shape # Rotate cv_img, width, height = self.rotateBound(cv_img, int(self.imageViewer.rotateCount*90)) # Resize dim = (int(width * (self.imageViewer.imgZoom + 100) / 100), int(height * (self.imageViewer.imgZoom + 100) / 100)) cv_img = cv2.resize(cv_img, dim, interpolation = cv2.INTER_AREA) # Crop coords = self.imageViewer.ZONE.coords(self.select) x0 = int(coords[0]) y0 = int(coords[1]) x1 = int(coords[2]) y1 = int(coords[3]) crop_img = cv_img[y0:y1, x0:x1] # Get the text by OCR try: os.environ['PATH'] = globs.CNIRTesser os.environ['TESSDATA_PREFIX'] = globs.CNIRTesser + '\\tessdata' text = pytesseract.image_to_string(crop_img, lang='ocrb', config='--psm 6 --oem 1 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890<') # manual validation # the regex regex = re.compile("[^A-Z0-9<\n]") text = re.sub(regex, '', text) self.validatedtext = '' invite = ihm.OpenScanDialog(self, text) invite.transient(self) invite.grab_set() invite.focus_force() self.wait_window(invite) #print("text : {}".format(self.validatedtext)) self.mrzChar = "" # Get that for char in self.validatedtext: self.termtext.delete("1.0","end") self.termtext.insert("1.0", self.mrzChar) self.mrzChar = self.mrzChar + char self.stringValidation(isFull=True) #print(self.mrzChar) # Reinstall tesseract except pytesseract.TesseractNotFoundError as e: try: shutil.rmtree(globs.CNIRTesser) except Exception: pass showerror(lang.all[globs.CNIRlang]["OCR module error"], (lang.all[globs.CNIRlang]["The OCR module located at {} can not be found or corrupted. It will be reinstalled at the next run"].format(os.environ['PATH'])), parent=self) logfile.printerr(lang.all[globs.CNIRlang]["Tesseract error : {}. Will be reinstallated"].format(e)) # Tesseract error except pytesseract.TesseractError as e: logfile.printerr("Tesseract error : {}".format(e)) showerror(lang.all[globs.CNIRlang]["OCR module error"], (lang.all[globs.CNIRlang]["The Tesseract module encountered a problem: {}"].format(e)), parent=self) ## Regex and document detection + control related functions def stringValidation(self, keysym="", isFull=False): """ Analysis of the already typed document """ # analysis # If we must decide the type of the document if not self.mrzDecided: # Get the candidates candidates = mrz.allDocMatch(self.mrzChar.split("\n"), final=isFull) if len(candidates) >= 2 and len(candidates) < 4 and len(self.mrzChar) >= 8: # Parameters for the choice invite invite = ihm.DocumentAsk(self, candidates) invite.transient(self) invite.grab_set() invite.focus_force() self.wait_window(invite) self.logOnTerm(lang.all[globs.CNIRlang]["Document detected: {}\n"].format(candidates[invite.choice][2])) self.statusbar.set(candidates[invite.choice][2]) self.mrzDecided = candidates[invite.choice] elif len(candidates) == 1: self.logOnTerm(lang.all[globs.CNIRlang]["Document detected: {}\n"].format(candidates[0][2])) self.statusbar.set(candidates[0][2]) self.mrzDecided = candidates[0] else: # corrects some problems if keysym in ["BackSpace", "Delete"]: return # get the cursor position curPos = self.termtext.index(INSERT) # break the line if (len(self.mrzChar) - 2 >= len(self.mrzDecided[0][0])) and ("\n" not in self.mrzChar[:-1]): # In case of there is no second line if len(self.mrzDecided[0][1]) == 0: self.mrzChar = self.termtext.get("1.0", "end")[:-1] self.termtext.delete("1.0","end") self.termtext.insert("1.0", self.mrzChar[:-1]) self.termtext.mark_set(INSERT, curPos) else: # In case of there is a second line self.mrzChar = self.termtext.get("1.0", "end")[:-1] + '\n' self.termtext.delete("1.0","end") self.termtext.insert("1.0", self.mrzChar) # stop when limit reached elif (len(self.mrzChar) - 3 >= 2 * len(self.mrzDecided[0][0])): i = len(self.mrzChar) - 3 while i >= 2 * len(self.mrzDecided[0][0]): i-=1 self.mrzChar = self.termtext.get("1.0", "end")[:-1] self.termtext.delete("1.0","end") self.termtext.insert("1.0", self.mrzChar[:-1]) self.termtext.mark_set(INSERT, curPos) # compute the control sum if needed self.computeSigma() def entryValidation(self, event): """ On the fly validation with regex """ controlled = False # get the cursor if self.mrzDecided: curPosition = self.termtext.index(INSERT) position = curPosition.split(".") pos = (int(position[0]) - 1) * len(self.mrzDecided[0][0]) + (int(position[1]) - 1) else: curPosition = self.termtext.index(INSERT) position = curPosition.split(".") pos = (int(position[1]) - 1) # verifying that there is no Ctrl-C/Ctrl-V and others if event.state & 0x0004 and ( event.keysym == "c" or event.keysym == "v" or event.keysym == "a" or event.keysym == "z" or event.keysym == "y" ): controlled = True if event.keysym == "Tab": if self.mrzDecided: controlled = True self.mrzChar = self.termtext.get("1.0", "end")[:-1] # the regex regex = re.compile("[^A-Z0-9<]") code = re.sub(regex, '', self.mrzChar) number = mrz.completeDocField(self.mrzDecided, code, pos) - 1 if number == 0: return "break" mrzChar = self.termtext.get(curPosition, "end")[:-1] self.termtext.delete(curPosition,"end") self.termtext.insert(curPosition, "<"*number + mrzChar) self.termtext.mark_set("insert", "%d.%d" % (int(position[0]), int(position[1]) + number)) return "break" if event.keysym == "Escape": if self.mrzDecided: # Get the candidates candidates = mrz.allDocMatch(self.mrzChar.split("\n")) if len(candidates) >= 2 and len(candidates) < 4 and len(self.mrzChar) >= 8: # Parameters for the choice invite invite = ihm.DocumentAsk(self, candidates) invite.transient(self) invite.grab_set() invite.focus_force() self.wait_window(invite) self.logOnTerm(lang.all[globs.CNIRlang]["Document detected again: {}\n"].format(candidates[invite.choice][2])) self.statusbar.set(candidates[invite.choice][2]) self.mrzDecided = candidates[invite.choice] elif len(candidates) == 1: self.logOnTerm(lang.all[globs.CNIRlang]["Document detected again: {}\n"].format(candidates[0][2])) self.statusbar.set(candidates[0][2]) self.mrzDecided = candidates[0] return "break" # If not a control char if not controlled and not event.keysym in ihm.controlKeys: # the regex regex = re.compile("[A-Z]|<|[0-9]") # match ! if not regex.fullmatch(event.char): self.logOnTerm(lang.all[globs.CNIRlang]["Character not accepted !\n"]) return "break" # Adds the entry tempChar = self.termtext.get("1.0", "end")[:-1] self.mrzChar = tempChar[:pos+1] + event.char + tempChar[pos+1:] + '\n' # validation of the mrz string self.stringValidation(event.keysym) def pasteValidation(self, event): """ On the fly validation of pasted text """ # cleanup self.termtext.delete("1.0","end") # get the clipboard content lines = self.clipboard_get() self.mrzChar = "" # the regex regex = re.compile("[^A-Z0-9<]") lines = re.sub(regex, '', lines) # Get that for char in lines: self.termtext.delete("1.0","end") self.termtext.insert("1.0", self.mrzChar) self.mrzChar = self.mrzChar + char self.stringValidation("") self.termtext.insert("1.0", self.mrzChar) return "break" def speedValidation(self, event): """ Computation of the speed entry """ char = self.speed731text.get() self.speedResultPrint(str(mrz.computeControlSum(char))) return "break" def computeSigma(self): """ Launch the checksum computation, infos validation and print/display the results """ # the regex regex = re.compile("[^A-Z0-9<]") code = re.sub(regex, '', self.mrzChar) self.compliance = True allSums = mrz.computeAllControlSum(self.mrzDecided, code)["ctrlSumList"] #print("Code : _{}_ | Sums : {}".format(code, allSums)) self.termtext.tag_remove("conforme", "1.0", "end") self.termtext.tag_remove("nonconforme", "1.0", "end") self.clearTerm() self.logOnTerm(lang.all[globs.CNIRlang]["Document Review: {}\n\n"].format(self.mrzDecided[2])) for sum in allSums: x = sum[1] // len(self.mrzDecided[0][0]) +1 y = sum[1] % len(self.mrzDecided[0][0]) #print("index : {}.{}".format(x,y)) #print("{} == {}".format(code[sum[1]], sum[2])) if sum[3]: self.logOnTerm(lang.all[globs.CNIRlang]["Checksum position {}: Lu {} VS Calculated {} [facultative]\n"].format(sum[1], code[sum[1]], sum[2])) else: self.logOnTerm(lang.all[globs.CNIRlang]["Checksum position {}: Lu {} VS Calculated {}\n"].format(sum[1], code[sum[1]], sum[2])) # if sum is facultative or if sum is ok try: if sum[3] or int(code[sum[1]]) == int(sum[2]): self.termtext.tag_add("conforme", "{}.{}".format(x,y), "{}.{}".format(x,y+1)) self.termtext.tag_configure("conforme", background="green", foreground="white") else: self.termtext.tag_add("nonconforme", "{}.{}".format(x,y), "{}.{}".format(x,y+1)) self.termtext.tag_configure("nonconforme", background="red", relief='raised', foreground="white") self.compliance = False except ValueError: self.termtext.tag_add("nonconforme", "{}.{}".format(x,y), "{}.{}".format(x,y+1)) self.termtext.tag_configure("nonconforme", background="red", relief='raised', foreground="white") self.compliance = False # get the infos docInfos = mrz.getDocInfos(self.mrzDecided, code) #print(docInfos) # display the infos for key in [ e for e in docInfos ]: #printkey) if key in ["CODE", "CTRL", "CTRLF", "FACULT", "NOINT"]: continue if not docInfos[key][1] == False: if not docInfos[key][0] == "": self.infoList[key]['text'] = docInfos[key][0] self.infoList[key]['background'] = self['background'] self.infoList[key]['foreground'] = "black" else: self.infoList[key]['text'] = lang.all[globs.CNIRlang]["Unknown"] self.infoList[key]['background'] = self['background'] self.infoList[key]['foreground'] = "black" else: self.infoList[key]['background'] = "red" self.infoList[key]['foreground'] = "white" self.infoList[key]['text'] = docInfos[key][0] self.compliance = False if self.compliance == True: self.STATUStxt["text"] = lang.all[globs.CNIRlang]["COMPLIANT"] self.STATUStxt["foreground"] = "green" self.statusbar.set(lang.all[globs.CNIRlang]["COMPLIANT"]) else: self.STATUStxt["text"] = lang.all[globs.CNIRlang]["IMPROPER"] self.STATUStxt["foreground"] = "red" self.statusbar.set(lang.all[globs.CNIRlang]["IMPROPER"]) ## Print functions def logOnTerm(self, text): """ Writes on the monitor """ self.monlog['state'] = 'normal' self.monlog.insert('end', text) self.monlog['state'] = 'disabled' self.monlog.yview(END) def clearTerm(self): """ Clears the monitor """ self.monlog['state'] = 'normal' self.monlog.delete('1.0', 'end') self.monlog['state'] = 'disabled' self.monlog.yview(END) def speedResultPrint(self, text): """ Prints a result in the quick entry terminal """ self.speedResult['state'] = 'normal' self.speedResult.delete("1.0", 'end') self.speedResult.insert('end', text) self.speedResult['state'] = 'disabled' ## Document display related functions def DisplayUpdate(self, image=None, setplace=False): """ Reload the displayer to display the image or not """ if image: self.imageViewer.image = image self.imageViewer.ZONE.itemconfigure(self.STATUSimg, image=(self.imageViewer.image)) self.imageViewer.ZONE.configure(scrollregion=self.imageViewer.ZONE.bbox("all")) def goPageChoice(self, event): """ Change the current viewed page of the multipage tiff if needed """ self.imageViewer.pagenumber = int(self.toolbar.pageChooser.get()) - 1 self.resizeScan() def openingScan(self): """ Open the scan, ask its path and displays it """ self.initialize() path = '' path = filedialog.askopenfilename(parent=self, title=lang.all[globs.CNIRlang]["Open a scan of document..."], filetypes=( ('TIF files', '*.tif'), ('TIF files', '*.tiff'), ('JPEG files', '*.jpg'), ('JPEG files', '*.jpeg'), ('PNG files', '*.png') )) self.openScanFile(path) def openScanFile(self, path): """ Open an image file at path to display it on the displayer """ # Check if the file is valid if ( path[-3:] != 'jpg' and path[-3:] != 'tif' and path[-4:] != 'jpeg' and path[-4:] != 'tiff' and path[-3:] != 'png' ) or not os.path.isfile(path): showerror(lang.all[globs.CNIRlang]["Open a scan of document..."], lang.all[globs.CNIRlang]["The file you provided is not valid : {}"].format(path)) return # Load an image using OpenCV self.imageViewer.imagePath = path self.imageViewer.imgZoom = 1 self.imageViewer.blackhat = 0 self.imageViewer.rotateCount = 0 self.imageViewer.pagenumber = 0 # Determine how many pages self.toolbar.pageChooser['values'] = ('1') total = len(cv2.imreadmulti(self.imageViewer.imagePath)[1]) for i in range(2, total + 1): self.toolbar.pageChooser['values'] += tuple(str(i)) # Open the first page try: cv_img = cv2.imreadmulti(self.imageViewer.imagePath)[1][self.imageViewer.pagenumber] cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) except: logfile.printerr("Error with : {} in {} with total of {} pages".format(cv2.imreadmulti(self.imageViewer.imagePath), self.imageViewer.imagePath, total)) critical.crashCNIR(False, "OpenCV openScanFile() error") try: # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width, channels_no = cv_img.shape # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width, channels_no = cv_img.shape except ValueError: # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width = cv_img.shape # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width = cv_img.shape # Update shape self.imageViewer.height, self.imageViewer.width = cv_img.shape[:2] # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) self.DisplayUpdate(photo) def zoomInScan50(self, quantity = 50): if self.imageViewer.image: self.imageViewer.imgZoom += quantity self.resizeScan() def zoomOutScan50(self, quantity = 50): if self.imageViewer.image and int(self.imageViewer.width * (self.imageViewer.imgZoom - quantity + 100) / 100) > 100: self.imageViewer.imgZoom -= quantity self.resizeScan() def zoomInScan20(self, quantity = 20): if self.imageViewer.image: self.imageViewer.imgZoom += quantity self.resizeScan() def zoomOutScan20(self, quantity = 20): if self.imageViewer.image and int(self.imageViewer.width * (self.imageViewer.imgZoom - quantity + 100) / 100) > 100: self.imageViewer.imgZoom -= quantity self.resizeScan() def zoomInScan(self, quantity = 1): if self.imageViewer.image: self.imageViewer.imgZoom += quantity self.resizeScan() def zoomOutScan(self, quantity = 1): if self.imageViewer.image and int(self.imageViewer.width * (self.imageViewer.imgZoom - quantity + 100) / 100) > 100: self.imageViewer.imgZoom -= quantity self.resizeScan() def rotateRight(self): if self.imageViewer.image: self.imageViewer.rotateCount -= 1 if self.imageViewer.rotateCount < 0: self.imageViewer.rotateCount = 4 self.resizeScan() def rotateLeft(self): if self.imageViewer.image: self.imageViewer.rotateCount += 1 if self.imageViewer.rotateCount > 4: self.imageViewer.rotateCount = 0 self.resizeScan() def rotateLeft1(self): if self.imageViewer.image: self.imageViewer.rotateCount += 0.01 if self.imageViewer.rotateCount > 4: self.imageViewer.rotateCount = 0 self.resizeScan() def rotateRight1(self): if self.imageViewer.image: self.imageViewer.rotateCount -= 0.01 if self.imageViewer.rotateCount < 0: self.imageViewer.rotateCount = 4 self.resizeScan() def threshScan(self): """ Invert the bits to make a negative of the scan (and highlight the contrasts) """ if self.imageViewer.image: # Load an image using OpenCV cv_img = cv2.imreadmulti(self.imageViewer.imagePath)[1][self.imageViewer.pagenumber] cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) if self.imageViewer.blackhat == 0: self.imageViewer.blackhat = 1 cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) cv_img = cv2.threshold(cv_img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] elif self.imageViewer.blackhat == 1: self.imageViewer.blackhat = 2 cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) cv_img = cv2.medianBlur(cv_img, 3) else: self.imageViewer.blackhat = 0 self.resizeScan(cv_img) def rotateBound(self, image, angle): """ Computes the rotation matrix and the new shapes in order to rotate an image """ # grab the dimensions of the image and then determine the # center (h, w) = image.shape[:2] (cX, cY) = (w // 2, h // 2) # grab the rotation matrix , then grab the sine and cosine # (i.e., the rotation components of the matrix) M = cv2.getRotationMatrix2D((cX, cY), angle, 1.0) cos = numpy.abs(M[0, 0]) sin = numpy.abs(M[0, 1]) # compute the new bounding dimensions of the image nW = int((h * sin) + (w * cos)) nH = int((h * cos) + (w * sin)) # adjust the rotation matrix to take into account translation M[0, 2] += (nW / 2) - cX M[1, 2] += (nH / 2) - cY # perform the actual rotation and return the image return cv2.warpAffine(image, M, (nW, nH)), nW, nH def resizeScan(self, cv_img = None): """ Reloads the image according to settings """ if self.imageViewer.image: try: if not hasattr(cv_img, 'shape'): # Load an image using OpenCV cv_img = cv2.imreadmulti(self.imageViewer.imagePath)[1][self.imageViewer.pagenumber] cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) if self.imageViewer.blackhat == 1: cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) cv_img = cv2.threshold(cv_img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] elif self.imageViewer.blackhat == 2: cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) cv_img = cv2.medianBlur(cv_img, 3) try: # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width, channels_no = cv_img.shape # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width, channels_no = cv_img.shape except ValueError: # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width = cv_img.shape # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width = cv_img.shape # Rotate cv_img, width, height = self.rotateBound(cv_img, int(self.imageViewer.rotateCount*90)) # Resize dim = (int(width * (self.imageViewer.imgZoom + 100) / 100), int(height * (self.imageViewer.imgZoom + 100) / 100)) cv_img = cv2.resize(cv_img, dim, interpolation = cv2.INTER_AREA) # Update shape self.imageViewer.height, self.imageViewer.width = cv_img.shape[:2] # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) self.DisplayUpdate( photo) except Exception as e: logfile.printerr("Error with opencv : {}".format(e)) try: # Reload an image using OpenCV path = self.imageViewer.imagePath self.imageViewer.imgZoom = 1 self.imageViewer.blackhat = 0 self.imageViewer.rotateCount = 0 cv_img = cv2.imreadmulti(path) cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width, channels_no = cv_img.shape # Get the image dimensions (OpenCV stores image data as NumPy ndarray) height, width, channels_no = cv_img.shape # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) self.DisplayUpdate(photo) except Exception as e: logfile.printerr("Critical error with opencv : ".format(e)) critical.crashCNIR(False, "OpenCV resizeScan() error") self.initialize() ## IHM and user interface related functions def newEntry(self): """ Reinits the IHM and invite """ self.initialize() self.logOnTerm('\n\n{}\n'.format(lang.all[globs.CNIRlang]["Please type a MRZ or open a scan"])) def infobox(self): """ Shows the About dialog """ Tk().withdraw() showinfo( lang.all[globs.CNIRlang]["About CNIRevelator"], ( lang.all[globs.CNIRlang]["ABOUT"] ), parent=self) def helpbox(self): """ Shows the keyboard help summary """ Tk().withdraw() showinfo( lang.all[globs.CNIRlang]["Keyboard commands"], ( lang.all[globs.CNIRlang]["KEYBHELP"] ), parent=self) def openIssuePage(self): """ Sends a bug report """ critical.crashCNIR(shutdown=False, option="Voluntary Bug Report", isVoluntary=True) def openProjectPage(self): """ Opens the Project Repository webpage """ webbrowser.open_new("https://github.com/neox95/CNIRevelator") def showChangeLog(self): changelogWin = ihm.ChangelogDialog(self, ('{} : CNIRevelator {}\n\n{}'.format(lang.all[globs.CNIRlang]["Program version"], globs.verstring_full, lang.all[globs.CNIRlang]["CHANGELOG"]))) changelogWin.transient(self) changelogWin.grab_set() changelogWin.focus_force() self.wait_window(changelogWin) def updateSet(self): """ Update Settings """ changeupdateWin = ihm.updateSetDialog(self, updater.getUpdateChannel()) changeupdateWin.transient(self) changeupdateWin.grab_set() changeupdateWin.focus_force() self.wait_window(changeupdateWin) def languageSet(self): """ Lang Settings """ changelangWin = ihm.langDialog(self, globs.CNIRlang) changelangWin.transient(self) changelangWin.grab_set() changelangWin.focus_force() self.wait_window(changelangWin) global mrz mrz = reload(mrz) self.initialize() def panelResize(self): """ Shows or hides the panel """ if self.currentw > int(2.15 * (self.ws / 2 * 0.3333333333333333)): self.currentw = int(2.15 * (self.ws / 2 * 0.3333333333333333)) self.geometry('%dx%d+%d+%d' % (self.currentw, self.h, self.x, self.y)) self.update() else: self.currentw = self.w self.geometry('%dx%d+%d+%d' % (self.currentw, self.h, self.x, self.y)) self.update()