diff --git a/electrum/gui/icons/qr_file.png b/electrum/gui/icons/qr_file.png new file mode 100644 index 000000000..efacb739a Binary files /dev/null and b/electrum/gui/icons/qr_file.png differ diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 4de20d8c9..136a97b48 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -161,6 +161,13 @@ class PayToEdit(QWidget, Logger, GenericInputHandler): show_error=self.send_tab.show_error, setText=self.try_payment_identifier, ) + self.on_qr_from_file_input_btn = partial( + self.input_qr_from_file, + allow_multi=False, + config=self.config, + show_error=self.send_tab.show_error, + setText=self.try_payment_identifier, + ) self.on_input_file = partial( self.input_file, config=self.config, diff --git a/electrum/gui/qt/qrreader/__init__.py b/electrum/gui/qt/qrreader/__init__.py index 38c54e214..36191b299 100644 --- a/electrum/gui/qt/qrreader/__init__.py +++ b/electrum/gui/qt/qrreader/__init__.py @@ -25,7 +25,8 @@ import sys from typing import Callable, Optional, TYPE_CHECKING, Mapping, Sequence from PyQt6.QtWidgets import QMessageBox, QWidget -from PyQt6.QtGui import QImage +from PyQt6.QtGui import QImage, QPainter, QColor +from PyQt6.QtCore import QRect from electrum.i18n import _ from electrum.util import UserFacingException @@ -59,16 +60,34 @@ def scan_qrcode( def scan_qr_from_image(image: QImage) -> Sequence[QrCodeResult]: """Might raise exception: MissingQrDetectionLib.""" qr_reader = get_qr_reader() - 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(), - ) + + 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.""" diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py index 54514d548..80bc6bbac 100644 --- a/electrum/gui/qt/qrtextedit.py +++ b/electrum/gui/qt/qrtextedit.py @@ -47,6 +47,13 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin): show_error=self.show_error, setText=setText, ) + self.on_qr_from_file_input_btn = partial( + self.input_qr_from_file, + allow_multi=allow_multi, + config=config, + show_error=self.show_error, + setText=setText, + ) self.on_input_file = partial( self.input_file, config=config, @@ -62,7 +69,8 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin): self.add_menu_button( options=[ ("picture_in_picture.png", _("Read QR code from screen"), self.on_qr_from_screenshot_input_btn), - ("file.png", _("Read file"), self.on_input_file), + ("qr_file.png", _("Read QR code from file"), self.on_qr_from_file_input_btn), + ("file.png", _("Read text from file"), self.on_input_file), ], ) self.add_qr_input_from_camera_button(config=config, show_error=self.show_error, allow_multi=allow_multi, setText=setText) @@ -72,7 +80,8 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin): m.addSeparator() m.addAction(get_icon_camera(), _("Read QR code with camera"), self.on_qr_from_camera_input_btn) m.addAction(read_QIcon("picture_in_picture.png"), _("Read QR code from screen"), self.on_qr_from_screenshot_input_btn) - m.addAction(read_QIcon("file.png"), _("Read file"), self.on_input_file) + m.addAction(read_QIcon("qr_file.png"), _("Read QR code from file"), self.on_qr_from_file_input_btn) + m.addAction(read_QIcon("file.png"), _("Read text from file"), self.on_input_file) m.exec(e.globalPos()) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 81c9ca3fd..2577dc8d7 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -180,6 +180,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): menu.addAction(get_icon_camera(), _("Read QR code with camera"), self.payto_e.on_qr_from_camera_input_btn) menu.addAction(read_QIcon("picture_in_picture.png"), _("Read QR code from screen"), self.payto_e.on_qr_from_screenshot_input_btn) + menu.addAction(read_QIcon("qr_file.png"), _("Read QR code from file"), self.payto_e.on_qr_from_file_input_btn) menu.addAction(read_QIcon("file.png"), _("Read invoice from file"), self.payto_e.on_input_file) self.paytomany_menu = menu.addToggle(_("&Pay to many"), self.toggle_paytomany) menu.addSeparator() diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 0b11435e9..5f33d4f32 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -832,6 +832,8 @@ class GenericInputHandler: fileName = getOpenFileName( parent=None, title='select file', + # trying to open non-text things like pdfs makes electrum freeze + filter="Text files (*.txt *.csv);;All files (*)", config=config, ) if not fileName: @@ -852,6 +854,52 @@ class GenericInputHandler: except Exception as e: show_error(_('Invalid payment identifier in file') + ':\n' + repr(e)) + def input_qr_from_file( + self, + *, + allow_multi: bool = False, + config: 'SimpleConfig', + show_error: Callable[[str], None], + setText: Callable[[str], None] = None, + ): + from .qrreader import scan_qr_from_image + if setText is None: + setText = self.setText + + file_name = getOpenFileName( + parent=None, + title=_("Select image file"), + config=config, + filter="Image files (*.png *.jpg *.jpeg *.bmp);;", + ) + if not file_name: + return + image = QImage(file_name) + if image.isNull(): + show_error(_("Failed to open image file.")) + return + try: + scan_result: Sequence[QrCodeResult] = scan_qr_from_image(image) + except MissingQrDetectionLib as e: + show_error(_("Unable to scan image.") + "\n" + repr(e)) + return + if len(scan_result) < 1: + show_error(_("No QR code was found in the image.")) + return + if len(scan_result) > 1 and not allow_multi: + show_error(_("More than one QR code was found in the image.")) + return + + if len(scan_result) > 1: + result_text = "\n".join([r.data for r in scan_result]) + else: + result_text = scan_result[0].data + + try: + setText(result_text) + except Exception as e: + show_error(_("Couldn't set result") + ':\n' + repr(e)) + def input_paste_from_clipboard( self, *,