diff --git a/.gitignore b/.gitignore index d0596f3..93d25c3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ build/* dist/* +src/Tesseract-OCR4/* +src/downloads/* +src/config/* +src/logs/* +signtool_8.1/* diff --git a/VERSIONS.LST b/VERSIONS.LST index 856ab09..64b66ab 100644 --- a/VERSIONS.LST +++ b/VERSIONS.LST @@ -1,2 +1,2 @@ # ver|url|checksum, and | as separator, one version per || -3.0.4|https://github.com/neox95/CNIRevelator/releases/download/3.0.4/CNIRevelator.zip|d03a18b35dfbb20d90664dc2c0f990adc5522e46||3.0.5|https://github.com/neox95/CNIRevelator/releases/download/3.0.5/CNIRevelator.zip|8b52290fb0910d8b9c4ec43293b08017e0031ca2||3.0.6|https://github.com/neox95/CNIRevelator/releases/download/3.0.6/CNIRevelator.zip|4bb4606dc9310d7b34b1fb38f9a0c2daf9518dc5|| +3.0.4|https://github.com/neox95/CNIRevelator/releases/download/3.0.4/CNIRevelator.zip|d03a18b35dfbb20d90664dc2c0f990adc5522e46||3.0.5|https://github.com/neox95/CNIRevelator/releases/download/3.0.5/CNIRevelator.zip|8b52290fb0910d8b9c4ec43293b08017e0031ca2||3.0.6|https://github.com/neox95/CNIRevelator/releases/download/3.0.6/CNIRevelator.zip|4bb4606dc9310d7b34b1fb38f9a0c2daf9518dc5||3.0.7|https://github.com/neox95/CNIRevelator/releases/download/3.0.7/CNIRevelator.zip|9aea1627c0b75610225a02458d5705563ca0d6af||3.0.8|https://github.com/neox95/CNIRevelator/releases/download/3.0.8/CNIRevelator.zip|8e849f8fcb5c952c09bdd4eb3392456ec9c6cf8f|| diff --git a/id-card.ico b/id-card.ico deleted file mode 100644 index efa86e4..0000000 Binary files a/id-card.ico and /dev/null differ diff --git a/make.bat b/make.bat index eefda6d..5d6414b 100644 --- a/make.bat +++ b/make.bat @@ -4,15 +4,15 @@ title Compilation de CNIRevelator -call pyinstaller -w -D --exclude-module PyQt5 --bootloader-ignore-signals --add-data "C:\Users\pf04950\AppData\Local\Continuum\anaconda3\Lib\site-packages\tld\res\effective_tld_names.dat.txt";"tld\res" --add-data "id-card.ico";"id-card.ico" -i "id-card.ico" -n CNIRevelator src\CNIRevelator.py +call pyinstaller -w -D --exclude-module PyQt5 --bootloader-ignore-signals --add-data "C:\Users\adrie\Anaconda3\Lib\site-packages\tld\res\effective_tld_names.dat.txt";"tld\res" --add-data "src\id-card.ico";"id-card.ico" -i "src\id-card.ico" -n CNIRevelator src\CNIRevelator.py copy LICENSE dist\CNIRevelator\LICENSE copy src\id-card.ico dist\CNIRevelator\id-card.ico -copy src\background.png dist\CNIRevelator\background.png +copy src\*.png dist\CNIRevelator\*.png -D:\Public\CNIRevelator-master\CNIRevelator-master\signtool_8.1\signtool\signtool.exe sign /n "CNIRevelator by Adrien Bourmault (neox95)" dist\CNIRevelator\CNIRevelator.exe +signtool_8.1\signtool\signtool.exe sign /n "CNIRevelator by Adrien Bourmault (neox95)" dist\CNIRevelator\CNIRevelator.exe pause diff --git a/src/CNIRevelator.py b/src/CNIRevelator.py index 9e9e088..3fe7fb0 100644 --- a/src/CNIRevelator.py +++ b/src/CNIRevelator.py @@ -31,7 +31,7 @@ import threading import traceback import psutil -import launcher # launcher.py +import launcher # launcher.py" import updater # updater.py import globs # globs.py import pytesseract # pytesseract.py diff --git a/src/Invert.png b/src/Invert.png new file mode 100644 index 0000000..f9b3fb3 Binary files /dev/null and b/src/Invert.png differ diff --git a/src/OCR.png b/src/OCR.png new file mode 100644 index 0000000..6fc81ea Binary files /dev/null and b/src/OCR.png differ diff --git a/src/globs.py b/src/globs.py index 674104c..42b1e6c 100644 --- a/src/globs.py +++ b/src/globs.py @@ -25,18 +25,13 @@ import os # CNIRevelator version -verType = "alpha" -version = [3, 0, 6] +verType = "final release" +version = [3, 1, 0] verstring_full = "{}.{}.{} {}".format(version[0], version[1], version[2], verType) verstring = "{}.{}".format(version[0], version[1]) debug = True -changelog = "Version 3.0.6 \nMise-à-jour mineure avec les corrections suivantes :\n- Changement de l'apparence du launcher de l'application\n- Améliorations de l'interface, notamment de la stabilité\n- Ajout de la signature numérique de l'exécutable\n\n" + \ -"Version 3.0.5 \nMise-à-jour mineure avec les corrections suivantes :\n- Changement de l'icône de l'exécutable afin de refléter le changement de version majeur accompli en 3.0\n\n" + \ -"Version 3.0.4 \nMise-à-jour mineure avec les corrections suivantes :\n- Correction d'un bug affectant le système de mise-à-jour\n\n" + \ -"Version 3.0.3 \nMise-à-jour mineure avec les corrections suivantes :\n- Correction d'un bug affectant le changelog\n- Correction d'une erreur avec la touche Suppr Arrière et Suppr causant une perte de données\n\n" + \ -"Version 3.0.2 \nMise-à-jour mineure avec les corrections suivantes :\n- Changement d'icône de l'exécutable\n- Correction d'un bug affectant le logging\n- Correction d'un bug affectant la détection de documents\n- Et autres modifications mineures\n\n" + \ -"Version 3.0.1 \nMise-à-jour majeure avec les corrections suivantes :\n- Renouvellement de la signature numérique de l'exécutable\n- Amélioration de présentation du log en cas d'erreur\n- Refonte totale du code source et désobfuscation\n- Téléchargements en HTTPS fiables avec somme de contrôle\n- Nouveaux terminaux d'entrées : un rapide (731) et un complet\n- Détection des documents améliorée, possibilité de choix plus fin\nEt les regressions suivantes :\n- Suppression temporaire de la fonction de lecture OCR. Retour planifié pour une prochaine version" +changelog = "Version 3.1.0 \nMise-à-jour majeure avec les progressions suivantes :\n- Modifications cosmétiques de l'interface utilisateur\n- Stabilisation des changements effectués sur la version mineure 3.0 : interface utilisateur, OCR, VISA A et B, logging" CNIRTesserHash = '5b58db27f7bc08c58a2cb33d01533b034b067cf8' CNIRFolder = os.getcwd() @@ -46,6 +41,7 @@ CNIRCryptoKey = '82Xh!efX3#@P~2eG' CNIRNewVersion = False CNIRConfig = CNIRFolder + '\\config\\conf.ig' +CNIRTesser = CNIRFolder + '\\Tesseract-OCR4\\' CNIRErrLog = CNIRFolder + '\\logs\\error.log' CNIRMainLog = CNIRFolder + '\\logs\\main.log' CNIRUrlConfig = CNIRFolder + '\\config\\urlconf.ig' diff --git a/src/ihm.py b/src/ihm.py index e254cfe..9c7622c 100644 --- a/src/ihm.py +++ b/src/ihm.py @@ -32,7 +32,6 @@ import PIL.Image, PIL.ImageTk import logger # logger.py import globs # globs.py -import image # image.py controlKeys = ["Escape", "Right", "Left", "Up", "Down", "Home", "End", "BackSpace", "Delete", "Inser", "Shift_L", "Shift_R", "Control_R", "Control_L"] @@ -69,7 +68,45 @@ class DocumentAsk(Toplevel): self.choice = 1 def ok(self): self.destroy() + + +class OpenScanDialog(Toplevel): + def __init__(self, parent, text): + super().__init__(parent) + self.parent = parent + self.title('Validation de la MRZ détectée par OCR') + self.resizable(width=False, height=False) + self.termtext = Text(self, state='normal', width=45, height=2, wrap='none', font='Terminal 17', fg='#121f38') + self.termtext.grid(column=0, row=0, sticky='NEW', padx=5, pady=5) + self.termtext.insert('end', text + '\n') + self.button = Button(self, text='Valider', command=(self.valid)) + self.button.grid(column=0, row=1, sticky='S', padx=5, pady=5) + self.update() + hs = self.winfo_screenheight() + w = int(self.winfo_width()) + h = int(self.winfo_height()) + ws = self.winfo_screenwidth() + hs = self.winfo_screenheight() + x = ws / 2 - w / 2 + y = hs / 2 - h / 2 + self.geometry('%dx%d+%d+%d' % (w, h, x, y)) + if getattr(sys, 'frozen', False): + self.iconbitmap(sys._MEIPASS + '\\id-card.ico\\id-card.ico') + else: + self.iconbitmap('id-card.ico') + + def valid(self): + self.parent.validatedtext = self.termtext.get('1.0', 'end') + texting = self.parent.validatedtext.replace(' ', '').replace('\r', '').split('\n') + for i in range(len(texting)): + for char in texting[i]: + if char not in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789<': + showerror('Erreur de validation', 'La MRZ soumise contient des caractères invalides', parent=self) + self.parent.validatedtext = '' + + self.destroy() + class LoginDialog(Toplevel): def __init__(self, parent): @@ -178,21 +215,6 @@ class LauncherWindow(Tk): def exit(self): self.after(1000, self.destroy) -class AutoScrollbar(ttk.Scrollbar): - - def set(self, lo, hi): - if float(lo) <= 0.0: - if float(hi) >= 1.0: - self.grid_remove() - self.grid() - ttk.Scrollbar.set(self, lo, hi) - - def pack(self, **kw): - raise TclError('Cannot use pack with the widget ' + self.__class__.__name__) - - def place(self, **kw): - raise TclError('Cannot use place with the widget ' + self.__class__.__name__) - class ResizeableCanvas(Canvas): def __init__(self,parent,**kwargs): Canvas.__init__(self,parent,**kwargs) @@ -208,7 +230,7 @@ class ResizeableCanvas(Canvas): self.height = event.height # rescale all the objects tagged with the "all" tag self.scale("all",0,0,wscale,hscale) - + ## Global Handler launcherWindowCur = LauncherWindow() diff --git a/src/image.py b/src/image.py deleted file mode 100644 index 3afca8e..0000000 --- a/src/image.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -******************************************************************************** -* CNIRevelator * -* * -* Desc: Image calculation for CNI printing * -* * -* 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 . * -******************************************************************************** -""" - diff --git a/src/main.py b/src/main.py index fd53870..b921838 100644 --- a/src/main.py +++ b/src/main.py @@ -35,13 +35,14 @@ import re import traceback import cv2 import PIL.Image, PIL.ImageTk +import os, shutil +import webbrowser import ihm # ihm.py import logger # logger.py import mrz # mrz.py import globs # globs.py import pytesseract # pytesseract.py -from image import * # image.py # Global handler logfile = logger.logCur @@ -53,10 +54,12 @@ class mainWindow(Tk): self.initialize() def initialize(self): - self.mrzChar = '' + self.mrzChar = "" self.mrzDecided = False self.Tags = [] self.compliance = True + self.corners = [] + self.validatedtext = "" # Hide during construction self.withdraw() @@ -89,6 +92,9 @@ class mainWindow(Tk): self.lecteur_ci.grid_rowconfigure(5, weight=1) # Fill the data sections + ttk.Label((self.lecteur_ci), text='Statut : ').grid(column=0, row=0, padx=5, pady=5) + self.STATUStxt = ttk.Label((self.lecteur_ci), text='EN ATTENTE', 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='Nom : ').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) @@ -146,18 +152,110 @@ class mainWindow(Tk): "INDIC" : self.indic, } - # The STATUS indicator + image display - self.STATUT = ttk.Labelframe(self, text='Affichage de documents et statut') - self.STATUT.grid_columnconfigure(0, weight=1) - self.STATUT.grid_rowconfigure(0, weight=1) - self.STATUT.frame = Frame(self.STATUT) - self.STATUT.frame.grid(column=0, row=0, sticky='NSEW') - self.STATUT.frame.grid_columnconfigure(0, weight=1) - self.STATUT.frame.grid_rowconfigure(0, weight=1) - self.STATUT.ZONE = ihm.ResizeableCanvas(self.STATUT.frame, bg=self["background"]) - self.STATUT.ZONE.pack(fill="both", expand=True) - self.STATUSimg = self.STATUT.ZONE.create_image(0,0, image=None) - self.STATUStxt = self.STATUT.ZONE.create_text(0,0, text='', font='Times 24', fill='#FFBF00') + # The the image viewer + self.imageViewer = ttk.Labelframe(self, text='Affichage et traitement de 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.negativeScan) + 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='Terminal de saisie de MRZ complète') @@ -191,9 +289,9 @@ class mainWindow(Tk): 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, sticky='NEW', padx=5) + self.speed731text.grid(column=0, row=0, 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=2, row=0, sticky='NEW') + self.speedResult.grid(column=2, row=0, sticky='NEW', padx=5, pady=5) # The monitor that indicates some useful infos self.monitor = ttk.Labelframe(self, text='Moniteur') @@ -206,8 +304,8 @@ class mainWindow(Tk): self.monitor.grid_rowconfigure(0, weight=1) # All the items griding - self.lecteur_ci.grid(column=0, row=0, sticky='EWNS', columnspan=2, padx=5, pady=5) - self.STATUT.grid(column=2, row=0, sticky='EWNS', columnspan=1, padx=5, pady=5) + 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) @@ -222,6 +320,8 @@ class mainWindow(Tk): menubar.add_cascade(label='Fichier', menu=menu1) menu3 = Menu(menubar, tearoff=0) menu3.add_command(label='Commandes au clavier', command=(self.helpbox)) + menu3.add_command(label='Signaler un problème', command=(self.openIssuePage)) + menu3.add_separator() menu3.add_command(label='A propos de CNIRevelator', command=(self.infobox)) menubar.add_cascade(label='Aide', menu=menu3) self.config(menu=menubar) @@ -248,36 +348,119 @@ class mainWindow(Tk): self.update() self.deiconify() self.minsize(self.winfo_width(), self.winfo_height()) - - # Load an image using OpenCV - cv_img = cv2.imread("background.png") - cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) - cv_img = cv2.blur(cv_img, (15, 15)) - # 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 - # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage - photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) - self.statusUpdate("EN ATTENTE", "#FFBF00", photo, setplace=True) - + + # Set image + self.imageViewer.image = None + self.imageViewer.imagePath = None + self.imageViewer.imgZoom = 1 + self.imageViewer.rotateCount = 0 + self.imageViewer.blackhat = False + self.imageViewer.pagenumber = 0 # Some bindings 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') - def statusUpdate(self, msg, color, image=None, setplace=False): + def statusUpdate(self, image=None, setplace=False): if image: - self.STATUT.image = 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 rectangleSelectScan(self, event): + 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 ='cyan', width = 2) + print("Get rectangle : [{}, {}], for [{}, {}]".format(self.corners[0][0], self.corners[0][1], self.corners[1][0], self.corners[1][1])) + if len(self.corners) > 2: + self.corners = [] + self.imageViewer.ZONE.delete(self.select) - self.STATUT.ZONE.itemconfigure(self.STATUSimg, image=(self.STATUT.image)) - self.STATUT.ZONE.itemconfigure(self.STATUStxt, text=(msg), fill=color) + def goOCRDetection(self): + 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: + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) + cv_img = cv2.GaussianBlur(cv_img, (3, 3), 0) + cv_img = cv2.bitwise_not(cv_img) + 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 + rotationMatrix=cv2.getRotationMatrix2D((width/2, height/2),int(self.imageViewer.rotateCount*90),1) + cv_img=cv2.warpAffine(cv_img,rotationMatrix,(width,height)) + # 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) + + x0 = int(self.corners[0][0]) + y0 = int(self.corners[0][1]) + x1 = int(self.corners[1][0]) + y1 = int(self.corners[1][1]) + + 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', boxes=False, config='--psm 6 --oem 0 -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("") + print(self.mrzChar) + + # Reinstall tesseract + except pytesseract.TesseractNotFoundError as e: + try: + shutil.rmtree(globs.CNIRTesser) + except Exception: + pass + showerror('Erreur de module OCR', ('Le module OCR localisé en ' + str(os.environ['PATH']) + 'est introuvable ou corrompu. Il sera réinstallé à la prochaine exécution'), parent=self) + logfile.printerr("Tesseract error : {}. Will be reinstallated".format(e)) + + # Tesseract error + except pytesseract.TesseractError as e: + logfile.printerr("Tesseract error : {}".format(e)) + showerror('Erreur de module OCR', ("Le module Tesseract a rencontré un problème : {}".format(e)), parent=self) - if setplace: - self.STATUT.ZONE.move(self.STATUSimg, self.STATUT.ZONE.winfo_reqwidth() / 2, self.STATUT.ZONE.winfo_reqheight() / 2) - self.STATUT.ZONE.move(self.STATUStxt, self.STATUT.ZONE.winfo_reqwidth() / 2, self.STATUT.ZONE.winfo_reqheight() / 2) def stringValidation(self, keysym): # analysis @@ -427,9 +610,11 @@ class mainWindow(Tk): # 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" @@ -459,15 +644,175 @@ class mainWindow(Tk): self.speedResult.insert('end', text) self.speedResult['state'] = 'disabled' - + def goPageChoice(self, event): + self.imageViewer.pagenumber = int(self.toolbar.pageChooser.get()) - 1 + self.resizeScan() + def openingScan(self): path = '' path = filedialog.askopenfilename(parent=self, title='Ouvrir un scan de CNI...', filetypes=(('TIF files', '*.tif'), ('TIF files', '*.tiff'), ('JPEG files', '*.jpg'), ('JPEG files', '*.jpeg'))) - self.mrzdetected = '' - self.mrzdict = {} + # Load an image using OpenCV + self.imageViewer.imagePath = path + self.imageViewer.imgZoom = 1 + self.imageViewer.blackhat = False + 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 + cv_img = cv2.imreadmulti(self.imageViewer.imagePath)[1][self.imageViewer.pagenumber] + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) + + 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 + + # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage + photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) + self.statusUpdate(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: + 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: + 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: + 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 negativeScan(self): + 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 not self.imageViewer.blackhat: + self.imageViewer.blackhat = True + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) + cv_img = cv2.GaussianBlur(cv_img, (3, 3), 0) + cv_img = cv2.bitwise_not(cv_img) + else: + self.imageViewer.blackhat = False + self.resizeScan(cv_img) + + def resizeScan(self, cv_img = None): + 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: + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) + cv_img = cv2.GaussianBlur(cv_img, (3, 3), 0) + cv_img = cv2.bitwise_not(cv_img) + + 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 + rotationMatrix=cv2.getRotationMatrix2D((width/2, height/2),int(self.imageViewer.rotateCount*90),1) + cv_img=cv2.warpAffine(cv_img,rotationMatrix,(width,height)) + # 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) + # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage + photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) + self.statusUpdate( photo) + except Exception as e: + logfile.printerr("Error with opencv : {}".format(e)) + traceback.print_exc(file=sys.stdout) + try: + # Reload an image using OpenCV + path = self.imageViewer.imagePath + self.imageViewer.imgZoom = 1 + self.imageViewer.blackhat = False + 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.statusUpdate(photo) + except Exception as e: + logfile.printerr("Critical error with opencv : ".format(e)) + traceback.print_exc(file=sys.stdout) + showerror("Erreur OpenCV (traitement d'images)", "Une erreur critique s'est produite dans le gestionnaire de traitement d'images OpenCV utilisé par CNIRevelator. L'application va se réinitialiser") + self.initialize() def newEntry(self): self.initialize() @@ -477,25 +822,27 @@ class mainWindow(Tk): Tk().withdraw() showinfo('A propos de CNIRevelator', - ( 'Version du logiciel : CNIRevelator ' + globs.verstring_full + '\n\n' + - "CNIRevelator est un logiciel libre : vous avez le droit de le modifier et/ou le distribuer " + - "dans les termes de la GNU General Public License telle que publiée par " + - "la Free Software Foundation, dans sa version 3 ou " + - "ultérieure. " + "\n\n" + - "CNIRevelator est distribué dans l'espoir d'être utile, sans toutefois " + - "impliquer une quelconque garantie de " + - "QUALITÉ MARCHANDE ou APTITUDE À UN USAGE PARTICULIER. Référez vous à la " + - "GNU General Public License pour plus de détails à ce sujet. " + - "\n\n" + - "Vous devriez avoir reçu une copie de la GNU General Public License " + - "avec CNIRevelator. Si cela n'est pas le cas, jetez un oeil à '. " + - "\n\n" + - "Le module d'OCR Tesseract 4.0 est soumis à l'Apache License 2004" + - "\n\n" + - "Les bibliothèques python et l'environnement Anaconda 3 sont soumis à la licence BSD 2018-2019" + - "\n\n" + - "Le code source de ce programme est disponible sur Github à l'adresse .\n" + - " En cas de problèmes ou demande particulière, ouvrez-y une issue ou bien envoyez un mail à neox@os-k.eu !" + ( 'Version du logiciel : CNIRevelator ' + globs.verstring_full + '\n\n' + "Copyright © 2018-2019 Adrien Bourmault (neox95)" + "\n\n" + "CNIRevelator est un logiciel libre : vous avez le droit de le modifier et/ou le distribuer " + "dans les termes de la GNU General Public License telle que publiée par " + "la Free Software Foundation, dans sa version 3 ou " + "ultérieure. " + "\n\n" + "CNIRevelator est distribué dans l'espoir d'être utile, sans toutefois " + "impliquer une quelconque garantie de " + "QUALITÉ MARCHANDE ou APTITUDE À UN USAGE PARTICULIER. Référez vous à la " + "GNU General Public License pour plus de détails à ce sujet. " + "\n\n" + "Vous devriez avoir reçu une copie de la GNU General Public License " + "avec CNIRevelator. Si cela n'est pas le cas, jetez un oeil à . " + "\n\n" + "Le module d'OCR Tesseract 4.0 est soumis à l'Apache License 2004." + "\n\n" + "Les bibliothèques python et l'environnement Anaconda 3 sont soumis à la licence BSD 2018-2019." + "\n\n" + "Le code source de ce programme est disponible sur Github à l'adresse .\n" + "Son fonctionnement est conforme aux normes et directives du document 9303 de l'OACI régissant les documents de voyages et d'identité." + '\n\n' + " En cas de problèmes ou demande particulière, ouvrez-y une issue ou bien envoyez un mail à neox@os-k.eu !\n\n" ), parent=self) @@ -520,6 +867,12 @@ class mainWindow(Tk): ), parent=self) + + def openIssuePage(self): + self.openBrowser("https://github.com/neox95/CNIRevelator/issues") + + def openBrowser(self, url): + webbrowser.open_new(url) def computeSigma(self): """ @@ -580,9 +933,11 @@ class mainWindow(Tk): self.compliance = False if self.compliance == True: - self.statusUpdate("CONFORME", "chartreuse2") + self.STATUStxt["text"] = "CONFORME" + self.STATUStxt["foreground"] = "green" else: - self.statusUpdate("NON-CONFORME","red") + self.STATUStxt["text"] = "NON CONFORME" + self.STATUStxt["foreground"] = "red" return diff --git a/src/mrz.py b/src/mrz.py index 4619d8a..e51fad6 100644 --- a/src/mrz.py +++ b/src/mrz.py @@ -640,27 +640,26 @@ AC = [ "Certificat de membre d'équipage" ] -## XXXXXXXXXXX -# VB = [ -# ["11222333333333333333333333333333333333333333", "444444444566677777789AAAAAABCCCCCCCCCCCCCCCDE"], -# { -# "1": ["2", "CODE", "V."], -# "2": ["3", "PAYS", "[A-Z]+"], -# "3": ["39", "NOM", "[A-Z]+"], -# "4": ["9", "NO", ".+"], -# "5": ["1", "CTRL", "[0-9]","4"], -# "6": ["3", "NAT", "[A-Z]+"], -# "7": ["6", "BDATE", "[0-9]+"], -# "8": ["1", "CTRL", "[0-9]", "7"], -# "9": ["1", "SEX", "[A-Z]"], -# "A": ["6", "EDATE", "[0-9]+"], -# "B": ["1", "CTRL", "[0-9]", "A"], -# "C": ["14", "FACULT", ".+"] -# }, -# "Visa de type B" -# ] - VA = [ + ["11222333333333333333333333333333333333333333", "444444444566677777789AAAAAABCCCCCCCCCCCCCCCCC"], + { + "1": ["2", "CODE", "V."], + "2": ["3", "PAYS", "[A-Z]+"], + "3": ["39", "NOM", "[A-Z]+"], + "4": ["9", "NO", ".+"], + "5": ["1", "CTRL", "[0-9]","4"], + "6": ["3", "NAT", "[A-Z]+"], + "7": ["6", "BDATE", "[0-9]+"], + "8": ["1", "CTRL", "[0-9]", "7"], + "9": ["1", "SEX", "[A-Z]"], + "A": ["6", "EDATE", "[0-9]+"], + "B": ["1", "CTRL", "[0-9]", "A"], + "C": ["16", "FACULT", ".+"] + }, + "Visa de type A" +] + +VB = [ ["112223333333333333333333333333333333", "444444444566677777789AAAAAABCCCCCC"], { "1": ["2", "CODE", "V."], @@ -676,14 +675,14 @@ VA = [ "B": ["1", "CTRL", "[0-9]", "A"], "C": ["8", "FACULT", ".+"] }, - "Visa de type A" + "Visa de type B" ] TSF = [ ["112223333333333333333333333333333333", "444444444566677777789AAAAAABCCCCCC"], { "1": ["2", "CODE", "TS"], - "2": ["3", "PAYS", "[A-Z]+"], + "2": ["3", "PAYS", "FRA"], "3": ["31", "NOM", "([A-Z]|<)+"], "4": ["9", "NO", ".+"], "5": ["1", "CTRL", "[0-9]","4"], @@ -695,7 +694,7 @@ TSF = [ "B": ["1", "CTRL", "[0-9]", "A"], "C": ["8", "FACULT", ".+"] }, - "Titre de séjour" + "Carte de séjour" ] I__ = [ @@ -752,8 +751,7 @@ DL = [ "Permis de conduire" ] -#TYPES = [ID, I__, VB, VA, AC, I_, IP, P, DL] -TYPES = [IDFR, I__, VA, AC, I_, IP, P, DL, TSF] +TYPES = [IDFR, I__, VB, VA, AC, I_, IP, P, DL, TSF] # longest document MRZ line longest = max([len(x[0][0]) for x in TYPES]) diff --git a/src/rotateLeft.png b/src/rotateLeft.png new file mode 100644 index 0000000..7653faa Binary files /dev/null and b/src/rotateLeft.png differ diff --git a/src/rotateLeft1.png b/src/rotateLeft1.png new file mode 100644 index 0000000..9224963 Binary files /dev/null and b/src/rotateLeft1.png differ diff --git a/src/rotateRight.png b/src/rotateRight.png new file mode 100644 index 0000000..3e008dc Binary files /dev/null and b/src/rotateRight.png differ diff --git a/src/rotateRight1.png b/src/rotateRight1.png new file mode 100644 index 0000000..a7de862 Binary files /dev/null and b/src/rotateRight1.png differ diff --git a/src/updater.py b/src/updater.py index b02ca37..b41c16b 100644 --- a/src/updater.py +++ b/src/updater.py @@ -56,7 +56,7 @@ def createShortcut(path, target='', wDir='', icon=''): shortcut.close() else: shell = Dispatch('WScript.Shell') - shortcut = shell.CreateShortCut(path) + shortcut = shell.CreateShortCut(shell.SpecialFolders("Desktop") + r"\{}".format(path)) shortcut.Targetpath = target shortcut.WorkingDirectory = wDir if icon == '': @@ -192,7 +192,6 @@ def getLatestVersion(credentials): return (finalver, finalurl, finalchecksum) -# XXX Warning : when tesseracturl is not found, it seems to hang and freeze def tessInstall(PATH, credentials): # Global Handlers logfile = logger.logCur @@ -207,18 +206,43 @@ def tessInstall(PATH, credentials): logfile.printdbg('Preparing download of Tesseract OCR 4...') getTesseract = downloader.newdownload(credentials, tesseracturl, PATH + '\\downloads\\TsrtPackage.zip', "Tesseract 4 OCR Module").download() - # Unzip Tesseract - logfile.printdbg("Unzipping the package") - launcherWindow.printmsg('Installing the updates') - zip_ref = zipfile.ZipFile(PATH + '\\downloads\\TsrtPackage.zip', 'r') - zip_ref.extractall(PATH) - zip_ref.close() - - # Cleanup try: - os.remove(UPATH + '\\downloads\\TsrtPackage.zip') + # CHECKSUM + BUF_SIZE = 65536 # lets read stuff in 64kb chunks! + + sha1 = hashlib.sha1() + + with open(globs.CNIRFolder + '\\downloads\\TsrtPackage.zip', 'rb') as f: + while True: + data = f.read(BUF_SIZE) + if not data: + break + sha1.update(data) + + check = sha1.hexdigest() + logfile.printdbg("SHA1: {0}".format(check)) + + if not check == globs.CNIRTesserHash: + logfile.printerr("Checksum error") + return False + + # Unzip Tesseract + logfile.printdbg("Unzipping the package") + launcherWindow.printmsg('Installing the updates') + zip_ref = zipfile.ZipFile(PATH + '\\downloads\\TsrtPackage.zip', 'r') + zip_ref.extractall(PATH) + zip_ref.close() + # Cleanup + try: + os.remove(UPATH + '\\downloads\\TsrtPackage.zip') + except: + pass + return True + except: - pass + return False + else: + return True ## Main Batch Function def batch(credentials): @@ -284,6 +308,9 @@ def batch(credentials): shutil.rmtree(UPATH + 'temp') logfile.printdbg('Extracted :' + UPATH + '\\CNIRevelator.exe') + # Make a shortcut + createShortcut("CNIRevelator.lnk", UPATH + '\\CNIRevelator.exe', UPATH) + launcherWindow.printmsg('Success !') # Cleanup @@ -366,8 +393,6 @@ def umain(): logfile.printerr(str(e)) launcherWindow.printmsg('Fail :{}'.format(e)) launcherWindow.printmsg('Starting...') - else: - tessInstall(globs.CNIRFolder, credentials) try: try: @@ -386,9 +411,39 @@ def umain(): else: logfile.printerr("An error occured. No effective update !") launcherWindow.printmsg('An error occured. No effective update !') - time.sleep(2) + time.sleep(2) + launcherWindow.exit() + return 0 + + if UPDATE_IS_MADE: + launcherWindow.exit() + return 0 + except: + logfile.printerr("A FATAL ERROR OCCURED : " + str(traceback.format_exc())) launcherWindow.exit() - return 0 + sys.exit(2) + return 2 + + try: + try: + # INSTALLING TESSERACT OCR + success = tessInstall(globs.CNIRFolder, credentials) + except Exception as e: + logfile.printerr("An error occured on the thread : " + str(traceback.format_exc())) + launcherWindow.printmsg('ERROR : ' + str(e)) + time.sleep(3) + launcherWindow.exit() + return 1 + + if success: + logfile.printdbg("Software is up-to-date !") + launcherWindow.printmsg('Software is up-to-date !') + else: + logfile.printerr("An error occured. No effective update !") + launcherWindow.printmsg('An error occured. No effective update !') + time.sleep(2) + launcherWindow.exit() + return 0 except: logfile.printerr("A FATAL ERROR OCCURED : " + str(traceback.format_exc())) @@ -396,5 +451,6 @@ def umain(): sys.exit(2) return 2 - return - + time.sleep(2) + launcherWindow.exit() + return 0 diff --git a/src/zoomIn.png b/src/zoomIn.png new file mode 100644 index 0000000..959aa41 Binary files /dev/null and b/src/zoomIn.png differ diff --git a/src/zoomIn20.png b/src/zoomIn20.png new file mode 100644 index 0000000..d341227 Binary files /dev/null and b/src/zoomIn20.png differ diff --git a/src/zoomIn50.png b/src/zoomIn50.png new file mode 100644 index 0000000..21acbdc Binary files /dev/null and b/src/zoomIn50.png differ diff --git a/src/zoomOut.png b/src/zoomOut.png new file mode 100644 index 0000000..0eca4be Binary files /dev/null and b/src/zoomOut.png differ diff --git a/src/zoomOut20.png b/src/zoomOut20.png new file mode 100644 index 0000000..fb97321 Binary files /dev/null and b/src/zoomOut20.png differ diff --git a/src/zoomOut50.png b/src/zoomOut50.png new file mode 100644 index 0000000..f753234 Binary files /dev/null and b/src/zoomOut50.png differ