1
0

add qr reading from file to ScanQRTextEdit and SendTab

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.
This commit is contained in:
f321x
2025-05-02 12:49:47 +02:00
parent dfb7a8ab5a
commit 63c224cb53
6 changed files with 95 additions and 11 deletions

View File

@@ -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,

View File

@@ -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."""

View File

@@ -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())

View File

@@ -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()

View File

@@ -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,
*,