The user should still be able to open the Preferences dialog, despite the camera functionality being broken. see https://github.com/spesmilo/electrum/issues/9949 ``` 6.36 | E | gui.qt.exception_window.Exception_Hook | exception caught by crash reporter Traceback (most recent call last): File "electrum\gui\qt\main_window.py", line 2672, in settings_dialog File "electrum\gui\qt\settings_dialog.py", line 229, in __init__ File "electrum\gui\qt\qrreader\__init__.py", line 96, in find_system_cameras File "<frozen importlib._bootstrap>", line 1360, in _find_and_load File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 935, in _load_unlocked File "PyInstaller\loader\pyimod02_importers.py", line 450, in exec_module File "electrum\gui\qt\qrreader\qtmultimedia\__init__.py", line 28, in <module> File "<frozen importlib._bootstrap>", line 1360, in _find_and_load File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 935, in _load_unlocked File "PyInstaller\loader\pyimod02_importers.py", line 450, in exec_module File "electrum\gui\qt\qrreader\qtmultimedia\camera_dialog.py", line 32, in <module> RuntimeError: PyQt6.QtMultimedia cannot import type '���d�⌂' from PyQt6.QtCore ```
184 lines
6.4 KiB
Python
184 lines
6.4 KiB
Python
# Copyright (C) 2021 The Electrum developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
|
|
#
|
|
# We have two toolchains to scan qr codes:
|
|
# 1. access camera via QtMultimedia, take picture, feed picture to zbar
|
|
# 2. let zbar handle whole flow (including accessing the camera)
|
|
#
|
|
# notes:
|
|
# - zbar needs to be compiled with platform-dependent extra config options to be able
|
|
# to access the camera
|
|
# - zbar fails to access the camera on macOS
|
|
# - qtmultimedia seems to support more cameras on Windows than zbar
|
|
# - qtmultimedia is often not packaged with PyQt
|
|
# in particular, on debian, you need both "python3-pyqt6" and "python3-pyqt6.qtmultimedia"
|
|
# - older versions of qtmultimedia don't seem to work reliably
|
|
#
|
|
# Considering the above, we use QtMultimedia for Windows and macOS, as there
|
|
# most users run our binaries where we can make sure the packaged versions work well.
|
|
# On Linux where many people run from source, we use zbar.
|
|
#
|
|
# Note: this module is safe to import on all platforms.
|
|
|
|
import sys
|
|
from typing import Callable, Optional, TYPE_CHECKING, Mapping, Sequence
|
|
|
|
from PyQt6.QtWidgets import QMessageBox, QWidget
|
|
from PyQt6.QtGui import QImage, QPainter, QColor
|
|
from PyQt6.QtCore import QRect
|
|
|
|
from electrum.i18n import _
|
|
from electrum.util import UserFacingException
|
|
from electrum.logging import get_logger
|
|
from electrum.qrreader import get_qr_reader, QrCodeResult, MissingQrDetectionLib
|
|
|
|
from electrum.gui.qt.util import MessageBoxMixin, custom_message_box
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from electrum.simple_config import SimpleConfig
|
|
|
|
|
|
_logger = get_logger(__name__)
|
|
|
|
|
|
def scan_qrcode(
|
|
*,
|
|
parent: Optional[QWidget],
|
|
config: 'SimpleConfig',
|
|
callback: Callable[[bool, str, Optional[str]], None],
|
|
) -> None:
|
|
"""Scans QR code using camera."""
|
|
assert parent is None or isinstance(parent, QWidget), f"parent should be a QWidget, not {parent!r}"
|
|
if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'):
|
|
_scan_qrcode_using_qtmultimedia(parent=parent, config=config, callback=callback)
|
|
else: # desktop Linux and similar
|
|
_scan_qrcode_using_zbar(parent=parent, config=config, callback=callback)
|
|
|
|
|
|
def scan_qr_from_image(image: QImage) -> Sequence[QrCodeResult]:
|
|
"""Might raise exception: MissingQrDetectionLib."""
|
|
qr_reader = get_qr_reader()
|
|
|
|
for attempt in range(4):
|
|
image_y800 = image.convertToFormat(QImage.Format.Format_Grayscale8)
|
|
res = qr_reader.read_qr_code(
|
|
image_y800.constBits().__int__(),
|
|
image_y800.sizeInBytes(),
|
|
image_y800.bytesPerLine(),
|
|
image_y800.width(),
|
|
image_y800.height(),
|
|
)
|
|
if res:
|
|
break
|
|
# zbar doesn't like qr codes that are too large in relation to the whole image
|
|
image = _reduce_qr_code_density(image)
|
|
return res
|
|
|
|
def _reduce_qr_code_density(image: QImage) -> QImage:
|
|
""" Reduces the size of the qr code relative to the whole image. """
|
|
new_image = QImage(image.width(), image.height(), QImage.Format.Format_RGB32)
|
|
new_image.fill(QColor(255, 255, 255)) # Fill white
|
|
|
|
painter = QPainter(new_image)
|
|
source_rect = QRect(0, 0, image.width(), image.height())
|
|
target_rect = QRect(0, 0, int(image.width() * 0.75), int(image.height() * 0.75))
|
|
painter.drawImage(target_rect, image, source_rect)
|
|
painter.end()
|
|
|
|
return new_image
|
|
|
|
def find_system_cameras() -> Mapping[str, str]:
|
|
"""Returns a camera_description -> camera_path map."""
|
|
if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'):
|
|
try:
|
|
from .qtmultimedia import find_system_cameras
|
|
except (ImportError, RuntimeError) as e:
|
|
_logger.exception('error importing .qtmultimedia')
|
|
return {}
|
|
else:
|
|
return find_system_cameras()
|
|
else: # desktop Linux and similar
|
|
from electrum import qrscanner
|
|
return qrscanner.find_system_cameras()
|
|
|
|
|
|
# --- Internals below (not part of external API)
|
|
|
|
def _scan_qrcode_using_zbar(
|
|
*,
|
|
parent: Optional[QWidget],
|
|
config: 'SimpleConfig',
|
|
callback: Callable[[bool, str, Optional[str]], None],
|
|
) -> None:
|
|
from electrum import qrscanner
|
|
data = None
|
|
try:
|
|
data = qrscanner.scan_barcode(config.get_video_device())
|
|
except UserFacingException as e:
|
|
success = False
|
|
error = str(e)
|
|
except BaseException as e:
|
|
_logger.exception('camera error')
|
|
success = False
|
|
error = repr(e)
|
|
else:
|
|
success = True
|
|
error = ""
|
|
if data is None:
|
|
# probably user cancelled
|
|
success = False
|
|
callback(success, error, data)
|
|
|
|
|
|
# Use a global to prevent multiple QR dialogs created simultaneously
|
|
_qr_dialog = None
|
|
|
|
|
|
def _scan_qrcode_using_qtmultimedia(
|
|
*,
|
|
parent: Optional[QWidget],
|
|
config: 'SimpleConfig',
|
|
callback: Callable[[bool, str, Optional[str]], None],
|
|
) -> None:
|
|
try:
|
|
from .qtmultimedia import QrReaderCameraDialog, CameraError
|
|
except (ImportError, RuntimeError) as e:
|
|
icon = QMessageBox.Icon.Warning
|
|
title = _("QR Reader Error")
|
|
message = _("QR reader failed to load. This may happen if "
|
|
"you are using an older version of PyQt.") + "\n\n" + str(e)
|
|
_logger.exception(message)
|
|
if isinstance(parent, MessageBoxMixin):
|
|
parent.msg_box(title=title, text=message, icon=icon, parent=None)
|
|
else:
|
|
custom_message_box(title=title, text=message, icon=icon, parent=parent)
|
|
return
|
|
|
|
global _qr_dialog
|
|
if _qr_dialog:
|
|
_logger.warning("QR dialog is already presented, ignoring.")
|
|
return
|
|
_qr_dialog = None
|
|
try:
|
|
_qr_dialog = QrReaderCameraDialog(parent=parent, config=config)
|
|
|
|
def _on_qr_reader_finished(success: bool, error: str, data):
|
|
global _qr_dialog
|
|
if _qr_dialog:
|
|
_qr_dialog.deleteLater()
|
|
_qr_dialog = None
|
|
callback(success, error, data)
|
|
|
|
_qr_dialog.qr_finished.connect(_on_qr_reader_finished)
|
|
_qr_dialog.start_scan(config.get_video_device())
|
|
except (MissingQrDetectionLib, CameraError) as e:
|
|
_qr_dialog = None
|
|
callback(False, str(e), None)
|
|
except Exception as e:
|
|
_logger.exception('camera error')
|
|
_qr_dialog = None
|
|
callback(False, repr(e), None)
|
|
|