From 00d631a0999c36d9bd38e43a92e902e761c4fe63 Mon Sep 17 00:00:00 2001 From: emendir Date: Mon, 25 Mar 2024 19:52:34 +0100 Subject: [PATCH] v1.3.0 migrated to PyQt6 --- ChangeLog.md | 3 + QR-ScanGen.spec | 77 +++++++++-------- ScanGen.py | 142 ++++++++++++++++---------------- __main__.py | 209 ++++++++++++++++++++++++++++------------------- __project__.py | 2 +- build.py | 9 +- requirements.txt | 2 +- 7 files changed, 244 insertions(+), 200 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 28071e1..f3b89e0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,6 @@ +# v1.2.0 (2024-03-25) +- migrate to PyQt6 + # v1.2.0 (2023-10-17) - save QR code to file by clicking it diff --git a/QR-ScanGen.spec b/QR-ScanGen.spec index 64a3df9..82e1bc7 100644 --- a/QR-ScanGen.spec +++ b/QR-ScanGen.spec @@ -1,40 +1,37 @@ -# -*- mode: python ; coding: utf-8 -*- - - -block_cipher = None - - -a = Analysis(['__main__.py'], - pathex=[], - binaries=[('C:\\Users\\weden\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\pyzbar\\libiconv.dll', '.'), ('C:\\Users\\weden\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\pyzbar\\libzbar-64.dll', '.')], - datas=[('Icon.svg', '.')], - hiddenimports=['pywifi', 'pyzbar', 'qrcode'], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) - -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='QR-ScanGen', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=False, - disable_windowed_traceback=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None ) +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['__main__.py'], + pathex=[], + binaries=[], + datas=[('Icon.svg', '.')], + hiddenimports=['pywifi', 'pyzbar', 'qrcode'], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=['PyQt5'], + noarchive=False, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='QR-ScanGen', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/ScanGen.py b/ScanGen.py index b7cc319..a7a829c 100644 --- a/ScanGen.py +++ b/ScanGen.py @@ -1,72 +1,70 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file '.\ScanGen.ui' -# -# Created by: PyQt5 UI code generator 5.15.10 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.resize(800, 600) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName("centralwidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) - self.verticalLayout.setObjectName("verticalLayout") - self.images_frm = QtWidgets.QFrame(self.centralwidget) - self.images_frm.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.images_frm.setFrameShadow(QtWidgets.QFrame.Raised) - self.images_frm.setObjectName("images_frm") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.images_frm) - self.horizontalLayout.setObjectName("horizontalLayout") - self.scanner_video_lbl = QtWidgets.QLabel(self.images_frm) - self.scanner_video_lbl.setText("") - self.scanner_video_lbl.setObjectName("scanner_video_lbl") - self.horizontalLayout.addWidget(self.scanner_video_lbl) - self.right_frame = QtWidgets.QFrame(self.images_frm) - self.right_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.right_frame.setFrameShadow(QtWidgets.QFrame.Raised) - self.right_frame.setObjectName("right_frame") - self.right_frame_lyt = QtWidgets.QVBoxLayout(self.right_frame) - self.right_frame_lyt.setObjectName("right_frame_lyt") - self.qr_code_lbl = QtWidgets.QLabel(self.right_frame) - self.qr_code_lbl.setText("") - self.qr_code_lbl.setObjectName("qr_code_lbl") - self.right_frame_lyt.addWidget(self.qr_code_lbl) - self.label = QtWidgets.QLabel(self.right_frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) - self.label.setSizePolicy(sizePolicy) - self.label.setMaximumSize(QtCore.QSize(16777215, 30)) - self.label.setObjectName("label") - self.right_frame_lyt.addWidget(self.label) - self.horizontalLayout.addWidget(self.right_frame) - self.verticalLayout.addWidget(self.images_frm) - self.text_txbx = QtWidgets.QPlainTextEdit(self.centralwidget) - self.text_txbx.setMaximumSize(QtCore.QSize(16777215, 200)) - self.text_txbx.setObjectName("text_txbx") - self.verticalLayout.addWidget(self.text_txbx) - MainWindow.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 26)) - self.menubar.setObjectName("menubar") - MainWindow.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow) - self.statusbar.setObjectName("statusbar") - MainWindow.setStatusBar(self.statusbar) - - self.retranslateUi(MainWindow) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) - self.label.setText(_translate("MainWindow", "Available Cameras:")) +# Form implementation generated from reading ui file './ScanGen.ui' +# +# Created by: PyQt6 UI code generator 6.5.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(800, 600) + self.centralwidget = QtWidgets.QWidget(parent=MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) + self.verticalLayout.setObjectName("verticalLayout") + self.images_frm = QtWidgets.QFrame(parent=self.centralwidget) + self.images_frm.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.images_frm.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.images_frm.setObjectName("images_frm") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.images_frm) + self.horizontalLayout.setObjectName("horizontalLayout") + self.scanner_video_lbl = QtWidgets.QLabel(parent=self.images_frm) + self.scanner_video_lbl.setText("") + self.scanner_video_lbl.setObjectName("scanner_video_lbl") + self.horizontalLayout.addWidget(self.scanner_video_lbl) + self.right_frame = QtWidgets.QFrame(parent=self.images_frm) + self.right_frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.right_frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.right_frame.setObjectName("right_frame") + self.right_frame_lyt = QtWidgets.QVBoxLayout(self.right_frame) + self.right_frame_lyt.setObjectName("right_frame_lyt") + self.qr_code_lbl = QtWidgets.QLabel(parent=self.right_frame) + self.qr_code_lbl.setText("") + self.qr_code_lbl.setObjectName("qr_code_lbl") + self.right_frame_lyt.addWidget(self.qr_code_lbl) + self.label = QtWidgets.QLabel(parent=self.right_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) + self.label.setSizePolicy(sizePolicy) + self.label.setMaximumSize(QtCore.QSize(16777215, 30)) + self.label.setObjectName("label") + self.right_frame_lyt.addWidget(self.label) + self.horizontalLayout.addWidget(self.right_frame) + self.verticalLayout.addWidget(self.images_frm) + self.text_txbx = QtWidgets.QPlainTextEdit(parent=self.centralwidget) + self.text_txbx.setMaximumSize(QtCore.QSize(16777215, 200)) + self.text_txbx.setObjectName("text_txbx") + self.verticalLayout.addWidget(self.text_txbx) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(parent=MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 26)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(parent=MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.label.setText(_translate("MainWindow", "Available Cameras:")) diff --git a/__main__.py b/__main__.py index 960cb4e..cad1e2d 100644 --- a/__main__.py +++ b/__main__.py @@ -1,15 +1,17 @@ -from PyQt5.QtWidgets import QFileDialog +import subprocess +from PyQt6.QtWidgets import QFileDialog import copy from PIL.ImageQt import ImageQt import qrcode from threading import Thread -from PyQt5 import QtGui, QtCore, QtWidgets -from PyQt5.QtWidgets import QMainWindow, QApplication -from PyQt5.uic import loadUiType +from PyQt6 import QtGui, QtCore, QtWidgets +from PyQt6.QtWidgets import QMainWindow, QApplication +from PyQt6.uic import loadUiType +from PyQt6.QtGui import QPixmap import cv2 import numpy as np from pyzbar.pyzbar import decode -from PyQt5.QtCore import pyqtSignal +from PyQt6.QtCore import pyqtSignal, Qt import time import os import pyperclip @@ -20,7 +22,7 @@ os.chdir(os.path.dirname(__file__)) -def QR_Decoder(image): +def qr_decoder(image): gray_img = cv2.cvtColor(image, 0) qr_code = decode(gray_img) @@ -45,12 +47,12 @@ def QR_Decoder(image): class ScanGen(QMainWindow, Ui_MainWindow): data = "" - update_qr_code = pyqtSignal() - update_text = pyqtSignal() - search_for_cameras = pyqtSignal() - update_camera_list = pyqtSignal(list) + update_qr_code_sig = pyqtSignal() + update_text_sig = pyqtSignal() + search_for_cameras_sig = pyqtSignal() + update_camera_list_sig = pyqtSignal(list) - camera_index = 0 + current_camera_info = None radio_buttons = [] closing = False @@ -62,139 +64,160 @@ def __init__(self, ): self.setWindowIcon(QtGui.QIcon(os.path.join(bundle_dir, 'Icon.svg'))) self.setWindowTitle("QR ScanGen") - self.update_text.connect(self.UpdateText) - self.update_qr_code.connect(self.UpdateQrCode) - self.search_for_cameras.connect(self.SearchForCameras) - self.update_camera_list.connect(self.UpdateCameraList) - self.qr_code_lbl.mousePressEvent = self.SaveQR_File + self.update_text_sig.connect(self.update_text) + self.update_qr_code_sig.connect(self.update_qr_code) + self.search_for_cameras_sig.connect(self.search_for_cameras) + self.update_camera_list_sig.connect(self.udpate_camera_list) + self.qr_code_lbl.mousePressEvent = self.save_qr_file - Thread(target=self.RunScanner, args=()).start() - # self.search_for_cameras.emit() + Thread(target=self.run_scanner, args=()).start() + # self.search_for_cameras_sig.emit() print("ready") - def UpdateQrCode(self): - qr_code = self.GenerateQrCode(self.data) + def update_qr_code(self): + qr_code = self.generate_qr_code(self.data) image = ImageQt(qr_code) self.qr_code_lbl.setPixmap(QtGui.QPixmap.fromImage(image)) - def UpdateText(self): + def update_text(self): self.text_txbx.setPlainText(self.data) - def ListCameraPorts(self): + def list_camera_ports(self): """ Test the ports and returns a tuple with the available ports and the ones that are working. """ non_working_ports = [] - dev_port = 0 - working_ports = [] - available_ports = [] + available_cameras = [] + + # iterate through ports starting at 0. # if there are more than 5 non working ports stop the testing. + dev_port = 0 while len(non_working_ports) < 6: - if dev_port == self.camera_index: - print(f"Skipping port {dev_port} because it is in use.") - working_ports.append(dev_port) + if self.current_camera_info and dev_port == self.current_camera_info['port']: + available_cameras.append(self.current_camera_info) else: camera = cv2.VideoCapture(dev_port) - if not camera.isOpened(): - non_working_ports.append(dev_port) - print("Port %s is not working." % dev_port) - else: + if camera.isOpened(): is_reading, img = camera.read() w = camera.get(3) h = camera.get(4) if is_reading: - print("Port %s is working and reads images (%s x %s)" % (dev_port, h, w)) - working_ports.append(dev_port) - if self.camera_index == "": # if Scanner currently doesn't know which camera to use - self.camera_index = dev_port - else: - print("Port %s for camera ( %s x %s) is present but does not read." % - (dev_port, h, w)) - available_ports.append(dev_port) + width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = camera.get(cv2.CAP_PROP_FPS) + camera_name = get_camera_name(dev_port) + + camera_info = { + "port": dev_port, + "name": camera_name, + "width": width, + "height": height, + "fps": fps + } + available_cameras.append(camera_info) + + else: + non_working_ports.append(dev_port) + camera.release() dev_port += 1 - return available_ports, working_ports, non_working_ports + if not self.current_camera_info: # if Scanner currently doesn't know which camera to use + self.current_camera_info = available_cameras[0] + return available_cameras - def ChangeCamera(self, e): - self.camera_index = self.sender().cv2_index + def change_camera(self, e): + self.current_camera_info = self.sender().camera_info testing_camera_ports = False - def SearchForCameras(self): + def search_for_cameras(self): if self.testing_camera_ports: return self.testing_camera_ports = True def _search_for_cameras(): - working_cameras = self.ListCameraPorts()[1] - self.update_camera_list.emit(working_cameras) + working_cameras = self.list_camera_ports() + self.update_camera_list_sig.emit(working_cameras) self.testing_camera_ports = False Thread(target=_search_for_cameras, args=()).start() - def UpdateCameraList(self, working_cameras: list): + def udpate_camera_list(self, working_cameras: list): for radio_button in self.radio_buttons: try: radio_button.deleteLater() except: pass - for i in working_cameras: - radio_button = QtWidgets.QRadioButton(str(i), self.right_frame) - radio_button.cv2_index = i - if i == self.camera_index: + for cam_info in working_cameras: + label = f"{cam_info['port']} {cam_info['name']}" + radio_button = QtWidgets.QRadioButton(label, self.right_frame) + radio_button.camera_info = cam_info + if cam_info['port'] == self.current_camera_info['port']: radio_button.setChecked(True) - radio_button.clicked.connect(self.ChangeCamera) + radio_button.clicked.connect(self.change_camera) self.right_frame_lyt.addWidget(radio_button) self.radio_buttons.append(radio_button) - def RunScanner(self): - # if self.camera_index == "": - # self.camera_index = 0 + def run_scanner(self): + while True: # camera usage session loop (new iteration only when user changes camera) if self.closing: return - current_camera = copy.deepcopy(self.camera_index) - cap = cv2.VideoCapture(self.camera_index) - self.search_for_cameras.emit() + self.search_for_cameras_sig.emit() + while self.current_camera_info is None: + time.sleep(0.1) + current_camera_port = copy.deepcopy(self.current_camera_info['port']) + cap = cv2.VideoCapture(current_camera_port) try: while True: if self.closing: + cap.release() + return - if current_camera != self.camera_index: + if current_camera_port != self.current_camera_info['port']: + cap.release() + break ret, frame = cap.read() - result = QR_Decoder(frame) + result = qr_decoder(frame) if result is not None: frame = result["image"] data = result["data"].decode() if data != self.data: self.data = data - self.update_text.emit() - self.update_qr_code.emit() - Thread(target=self.AnalyseCode, args=()).start() + self.update_text_sig.emit() + self.update_qr_code_sig.emit() + Thread(target=self.analyse_scanned_code, args=()).start() pyperclip.copy(self.data) else: if self.text_txbx.toPlainText() != self.data: self.data = self.text_txbx.toPlainText() - self.update_qr_code.emit() - self.camera_image = QtGui.QImage( - frame.data, frame.shape[1], frame.shape[0], QtGui.QImage.Format_RGB888).rgbSwapped() - self.scanner_video_lbl.setPixmap(QtGui.QPixmap.fromImage(self.camera_image)) + self.update_qr_code_sig.emit() + self.camera_image = convert_cv_qt( + frame, + self.scanner_video_lbl.width(), + self.scanner_video_lbl.height() + ) + self.scanner_video_lbl.setPixmap(self.camera_image) code = cv2.waitKey(10) if code == ord('q'): + cap.release() + break - except: - print(f"Error working with camera {self.camera_index}") - self.camera_index = "" - self.search_for_cameras.emit() - while self.camera_index == "": - print("waiting for camera") + except Exception as error: + print(error) + print(f"Error working with camera {self.current_camera_info['port']}") + cap.release() + + self.current_camera_info = None + self.search_for_cameras_sig.emit() + while self.current_camera_info is None: time.sleep(0.1) - # self.SearchForCameras() + # self.search_for_cameras() - def AnalyseCode(self): + def analyse_scanned_code(self): # WIFI analysis elements = [e.split(":") for e in self.data.split(";")] if len(elements) > 2 and elements[0][0] == "WIFI" and elements[0][1] == "S" and elements[1][0] == "T" and elements[2][0] == "P": @@ -230,10 +253,10 @@ def AnalyseCode(self): if platform.system() == 'Linux': os.system(f"nmcli dev wifi connect '{ssid}' password '{password}'") - if IsWebsite(self.data): + if is_website(self.data): webbrowser.open_new_tab(self.data) - def GenerateQrCode(self, text): + def generate_qr_code(self, text): qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, @@ -250,11 +273,7 @@ def GenerateQrCode(self, text): qr.box_size = int((desired_height)/(qr_height_squares+2*qr.border)) return qr.make_image() # generate and return QR code image - def closeEvent(self, event): - self.closing = True - event.accept() - - def SaveQR_File(self, eventargs): + def save_qr_file(self, eventargs): """Saves the currently displayed QR-Code to a file, after opening a file dialog asking the user for the file path""" filepath, ok = QFileDialog.getSaveFileName( @@ -265,14 +284,36 @@ def SaveQR_File(self, eventargs): filepath += ".png" self.qr_code_lbl.pixmap().save(filepath) + def closeEvent(self, event): + self.closing = True + event.accept() + + +def convert_cv_qt(cv_img, width, height): + """Convert from an opencv image to QPixmap""" + rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) + h, w, ch = rgb_image.shape + bytes_per_line = ch * w + convert_to_Qt_format = QtGui.QImage( + rgb_image.data, w, h, bytes_per_line, QtGui.QImage.Format.Format_RGB888) + p = convert_to_Qt_format.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatio) + return QPixmap.fromImage(p) -def IsWebsite(text): + +def is_website(text): return text.startswith("https://") or text.startswith("http://") +def get_camera_name(camera_index): + if platform.system() == 'Linux': + return '' + else: + return '' + + if __name__ == '__main__': import sys app = QApplication(sys.argv) main = ScanGen() main.show() - sys.exit(app.exec_()) + sys.exit(app.exec()) diff --git a/__project__.py b/__project__.py index 646a583..8d147c7 100644 --- a/__project__.py +++ b/__project__.py @@ -1,2 +1,2 @@ project_name = "QR-ScanGen" -version = "1.2.0" +version = "1.3.0" diff --git a/build.py b/build.py index bd80cbb..8d5fd35 100644 --- a/build.py +++ b/build.py @@ -7,15 +7,18 @@ "pyzbar", "qrcode", ] +excluded_imports = [ + "PyQt5" +] # converting *.ui files to *.py files for dirname, dirnames, filenames in os.walk("."): if dirname == "./Plugins" or "./.git" in dirname: continue for filename in filenames: path = os.path.join(dirname, filename) - if(filename[-2:] == "ui"): + if (filename[-2:] == "ui"): print(filename) - os.system(f"pyuic5 {path} -o {path[:-2]}py") + os.system(f"pyuic6 {path} -o {path[:-2]}py") if os.path.exists("build"): shutil.rmtree("build") # shutil.rmtree("dist") @@ -35,6 +38,8 @@ cmd = f"pyinstaller --name='{project_name}' --windowed --onefile --add-data='Icon.svg:.' __main__.py" for lib in hidden_imports: cmd += f" --hidden-import={lib}" + for lib in excluded_imports: + cmd += f" --exclude={lib}" cmd = "export QT_DEBUG_PLUGINS=1;"+cmd print(cmd) os.system(cmd) diff --git a/requirements.txt b/requirements.txt index db5c1f1..023388f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy opencv_python Pillow==9.2.0 pyperclip -PyQt5 +PyQt6 pywifi pyzbar qrcode