There was no ability to read qr codes contained in image files. This could lead to confusion in some contexts, as `on_file_input()` of ScanQRTextEdit will read the whole content of the file (instead of looking for qr codes). The revealer plugin for example generates png files containing qr codes and uses the `ScanQRTextEdit` to get user input, for the user it would seem logical to click on 'Read from file' to load the generated file, however this will result in the wrong data being loaded. Having the option to explicitly load a QR from file makes this clear. Also it seems useful, especially considering reading QR from screenshots doesn't work on wayland.
183 lines
6.3 KiB
Python
183 lines
6.3 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 as e:
|
|
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 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)
|
|
|