diff --git a/contrib/android/buildozer_qml.spec b/contrib/android/buildozer_qml.spec index 2437be2b1..926bf5af6 100644 --- a/contrib/android/buildozer_qml.spec +++ b/contrib/android/buildozer_qml.spec @@ -76,7 +76,6 @@ requirements = cryptography, pyqt6sip, pyqt6, - pillow, libzbar # (str) Presplash of the application diff --git a/contrib/android/p4a_recipes/Pillow/__init__.py b/contrib/android/p4a_recipes/Pillow/__init__.py deleted file mode 100644 index fd9a91ea7..000000000 --- a/contrib/android/p4a_recipes/Pillow/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -from pythonforandroid.recipes.Pillow import PillowRecipe -from pythonforandroid.util import load_source - -util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) - - -assert PillowRecipe._version == "8.4.0" -assert PillowRecipe.depends == ['png', 'jpeg', 'freetype', 'setuptools', 'python3'] -assert PillowRecipe.python_depends == [] - - -class PillowRecipePinned(util.InheritedRecipeMixin, PillowRecipe): - sha512sum = "d395f69ccb37c52a3b6f45836700ffbc3173afae31848cc61d7b47db88ca1594541023beb9a14fd9067aca664e182c7d6e3300ab3e3095c31afe8dcbc6e08233" - - -recipe = PillowRecipePinned() diff --git a/contrib/android/p4a_recipes/freetype/__init__.py b/contrib/android/p4a_recipes/freetype/__init__.py deleted file mode 100644 index 0d8920655..000000000 --- a/contrib/android/p4a_recipes/freetype/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -from pythonforandroid.recipes.freetype import FreetypeRecipe -from pythonforandroid.util import load_source - -util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) - - -assert FreetypeRecipe._version == "2.10.1" -assert FreetypeRecipe.depends == [] -assert FreetypeRecipe.python_depends == [] - - -class FreetypeRecipePinned(util.InheritedRecipeMixin, FreetypeRecipe): - sha512sum = "346c682744bcf06ca9d71265c108a242ad7d78443eff20142454b72eef47ba6d76671a6e931ed4c4c9091dd8f8515ebdd71202d94b073d77931345ff93cfeaa7" - - -recipe = FreetypeRecipePinned() diff --git a/contrib/android/p4a_recipes/jpeg/__init__.py b/contrib/android/p4a_recipes/jpeg/__init__.py deleted file mode 100644 index dc3e2998e..000000000 --- a/contrib/android/p4a_recipes/jpeg/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -from pythonforandroid.recipes.jpeg import JpegRecipe -from pythonforandroid.util import load_source - -util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) - - -assert JpegRecipe._version == "2.0.1" -assert JpegRecipe.depends == [] -assert JpegRecipe.python_depends == [] - - -class JpegRecipePinned(util.InheritedRecipeMixin, JpegRecipe): - sha512sum = "d456515dcda7c5e2e257c9fd1441f3a5cff0d33281237fb9e3584bbec08a181c4b037947a6f87d805977ec7528df39b12a5d32f6e8db878a62bcc90482f86e0e" - - -recipe = JpegRecipePinned() diff --git a/contrib/android/p4a_recipes/png/__init__.py b/contrib/android/p4a_recipes/png/__init__.py deleted file mode 100644 index b4f500a2e..000000000 --- a/contrib/android/p4a_recipes/png/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -from pythonforandroid.recipes.png import PngRecipe -from pythonforandroid.util import load_source - -util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) - - -assert PngRecipe._version == "1.6.37" -assert PngRecipe.depends == [] -assert PngRecipe.python_depends == [] - - -class PngRecipePinned(util.InheritedRecipeMixin, PngRecipe): - sha512sum = "f304f8aaaee929dbeff4ee5260c1ab46d231dcb0261f40f5824b5922804b6b4ed64c91cbf6cc1e08554c26f50ac017899a5971190ca557bc3c11c123379a706f" - - -recipe = PngRecipePinned() diff --git a/electrum/gui/common_qt/util.py b/electrum/gui/common_qt/util.py new file mode 100644 index 000000000..ef3d07e46 --- /dev/null +++ b/electrum/gui/common_qt/util.py @@ -0,0 +1,75 @@ +from typing import Optional + +from PyQt6 import QtGui +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QColor, QPen, QPaintDevice +import qrcode + +from electrum.i18n import _ + + +def draw_qr( + *, + qr: Optional[qrcode.main.QRCode], + paint_device: QPaintDevice, # target to paint on + is_enabled: bool = True, + min_boxsize: int = 2, # min size in pixels of single black/white unit box of the qr code +) -> None: + """Draw 'qr' onto 'paint_device'. + - qr.box_size is ignored. We will calculate our own boxsize to fill the whole size of paint_device. + - qr.border is respected. + """ + black = QColor(0, 0, 0, 255) + grey = QColor(196, 196, 196, 255) + white = QColor(255, 255, 255, 255) + black_pen = QPen(black) if is_enabled else QPen(grey) + black_pen.setJoinStyle(Qt.PenJoinStyle.MiterJoin) + + if not qr: + qp = QtGui.QPainter() + qp.begin(paint_device) + qp.setBrush(white) + qp.setPen(white) + r = qp.viewport() + qp.drawRect(0, 0, r.width(), r.height()) + qp.end() + return + + # note: next line can raise qrcode.exceptions.DataOverflowError (or ValueError) + matrix = qr.get_matrix() # includes qr.border + k = len(matrix) + qp = QtGui.QPainter() + qp.begin(paint_device) + r = qp.viewport() + framesize = min(r.width(), r.height()) + boxsize = int(framesize / k) + if boxsize < min_boxsize: + # The amount of data is still within what can fit into a QR code, + # however we don't have enough pixels to draw it. + qp.setBrush(white) + qp.setPen(white) + qp.drawRect(0, 0, r.width(), r.height()) + qp.setBrush(black) + qp.setPen(black) + qp.drawText(0, 20, _("Cannot draw QR code") + ":") + qp.drawText(0, 40, _("Not enough space available.")) + qp.end() + return + size = k * boxsize + left = (framesize - size) / 2 + top = (framesize - size) / 2 + # Draw white background with margin + qp.setBrush(white) + qp.setPen(white) + qp.drawRect(0, 0, framesize, framesize) + # Draw qr code + qp.setBrush(black if is_enabled else grey) + qp.setPen(black_pen) + for r in range(k): + for c in range(k): + if matrix[r][c]: + qp.drawRect( + int(left + c * boxsize), int(top + r * boxsize), + boxsize - 1, boxsize - 1) + qp.end() + diff --git a/electrum/gui/qml/components/controls/QRImage.qml b/electrum/gui/qml/components/controls/QRImage.qml index 67190a138..46929ea7b 100644 --- a/electrum/gui/qml/components/controls/QRImage.qml +++ b/electrum/gui/qml/components/controls/QRImage.qml @@ -15,7 +15,7 @@ Item { Rectangle { id: r - width: _qrprops.modules * _qrprops.box_size + width: _qrprops.qr_pixelsize height: width color: 'white' } @@ -29,8 +29,8 @@ Item { color: 'white' x: (parent.width - width) / 2 y: (parent.height - height) / 2 - width: _qrprops.icon_modules * _qrprops.box_size - height: _qrprops.icon_modules * _qrprops.box_size + width: _qrprops.icon_pixelsize + height: _qrprops.icon_pixelsize Image { visible: _qrprops.valid diff --git a/electrum/gui/qml/qeqr.py b/electrum/gui/qml/qeqr.py index d1ec0c9cb..f7c20af59 100644 --- a/electrum/gui/qml/qeqr.py +++ b/electrum/gui/qml/qeqr.py @@ -5,8 +5,6 @@ from qrcode.exceptions import DataOverflowError import math import urllib -from PIL import ImageQt - from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRect from PyQt6.QtGui import QImage, QColor from PyQt6.QtQuick import QQuickImageProvider @@ -22,6 +20,7 @@ from electrum.logging import get_logger from electrum.qrreader import get_qr_reader from electrum.i18n import _ from electrum.util import profiler, get_asyncio_loop +from electrum.gui.common_qt.util import draw_qr class QEQRParser(QObject): @@ -125,6 +124,11 @@ class QEQRParser(QObject): class QEQRImageProvider(QQuickImageProvider): + MAX_QR_PIXELSIZE = 400 + ERROR_CORRECT_LEVEL = qrcode.constants.ERROR_CORRECT_M + # ^ note: this is higher than for desktop. but on desktop we don't put a logo in the middle. + QR_BORDER = 2 + def __init__(self, max_size, parent=None): super().__init__(QQuickImageProvider.ImageType.Image) self._max_size = max_size @@ -147,20 +151,18 @@ class QEQRImageProvider(QQuickImageProvider): uri = uri._replace(query=query) qstr = urllib.parse.urlunparse(uri) - qr = qrcode.QRCode(version=1, border=2) - qr.add_data(qstr) + qr = qrcode.main.QRCode(border=self.QR_BORDER, error_correction=self.ERROR_CORRECT_LEVEL) # calculate best box_size - pixelsize = min(self._max_size, 400) + pixelsize = min(self._max_size, self.MAX_QR_PIXELSIZE) try: - modules = 17 + 4 * qr.best_fit() + qr.border * 2 + qr.add_data(qstr) + modules = len(qr.get_matrix()) qr.box_size = math.floor(pixelsize/modules) - qr.make(fit=True) - - pimg = qr.make_image(fill_color='black', back_color='white') - self.qimg = ImageQt.ImageQt(pimg) - except DataOverflowError: + self.qimg = QImage(modules * qr.box_size, modules * qr.box_size, QImage.Format.Format_RGB32) + draw_qr(qr=qr, paint_device=self.qimg) + except (ValueError, qrcode.exceptions.DataOverflowError): # fake it modules = 17 + qr.border * 2 box_size = math.floor(pixelsize/modules) @@ -179,15 +181,18 @@ class QEQRImageProviderHelper(QObject): @pyqtSlot(str, result='QVariantMap') def getDimensions(self, qstr): - qr = qrcode.QRCode(version=1, border=2) - qr.add_data(qstr) + qr = qrcode.QRCode( + border=QEQRImageProvider.QR_BORDER, + error_correction=QEQRImageProvider.ERROR_CORRECT_LEVEL, + ) # calculate best box_size - pixelsize = min(self._max_size, 400) + pixelsize = min(self._max_size, QEQRImageProvider.MAX_QR_PIXELSIZE) try: - modules = 17 + 4 * qr.best_fit() + qr.border * 2 + qr.add_data(qstr) + modules = len(qr.get_matrix()) valid = True - except DataOverflowError: + except (ValueError, qrcode.exceptions.DataOverflowError): # fake it modules = 17 + qr.border * 2 valid = False @@ -198,8 +203,7 @@ class QEQRImageProviderHelper(QObject): icon_modules += (icon_modules+1) % 2 # force odd return { - 'modules': modules, - 'box_size': qr.box_size, - 'icon_modules': icon_modules, + 'qr_pixelsize': modules * qr.box_size, + 'icon_pixelsize': icon_modules * qr.box_size, 'valid': valid } diff --git a/electrum/gui/qt/qrcodewidget.py b/electrum/gui/qt/qrcodewidget.py index d1fa4bb07..823a9d44b 100644 --- a/electrum/gui/qt/qrcodewidget.py +++ b/electrum/gui/qt/qrcodewidget.py @@ -3,13 +3,13 @@ from typing import Optional import qrcode import qrcode.exceptions -from PyQt6.QtGui import QColor, QPen import PyQt6.QtGui as QtGui -from PyQt6.QtCore import Qt, QRect +from PyQt6.QtCore import QRect from PyQt6.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QPushButton, QWidget from electrum.i18n import _ from electrum.simple_config import SimpleConfig +from electrum.gui.common_qt.util import draw_qr from .util import WindowModalDialog, WWLabel, getSaveFileName @@ -34,8 +34,7 @@ class QRCodeWidget(QWidget): if data: qr = qrcode.QRCode( error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=0, + border=1, ) try: qr.add_data(data) @@ -57,53 +56,12 @@ class QRCodeWidget(QWidget): def paintEvent(self, e): if not self.data: return - - black = QColor(0, 0, 0, 255) - grey = QColor(196, 196, 196, 255) - white = QColor(255, 255, 255, 255) - black_pen = QPen(black) if self.isEnabled() else QPen(grey) - black_pen.setJoinStyle(Qt.PenJoinStyle.MiterJoin) - - if not self.qr: - qp = QtGui.QPainter() - qp.begin(self) - qp.setBrush(white) - qp.setPen(white) - r = qp.viewport() - qp.drawRect(0, 0, r.width(), r.height()) - qp.end() - return - - matrix = self.qr.get_matrix() - k = len(matrix) - qp = QtGui.QPainter() - qp.begin(self) - r = qp.viewport() - framesize = min(r.width(), r.height()) - self._framesize = framesize - boxsize = int(framesize/(k + 2)) - if boxsize < self.MIN_BOXSIZE: - qp.drawText(0, 20, _("Cannot draw QR code")+":") - qp.drawText(0, 40, _("Not enough space available. Try increasing the window size.")) - qp.end() - return - size = k*boxsize - left = (framesize - size)/2 - top = (framesize - size)/2 - # Draw white background with margin - qp.setBrush(white) - qp.setPen(white) - qp.drawRect(0, 0, framesize, framesize) - # Draw qr code - qp.setBrush(black if self.isEnabled() else grey) - qp.setPen(black_pen) - for r in range(k): - for c in range(k): - if matrix[r][c]: - qp.drawRect( - int(left+c*boxsize), int(top+r*boxsize), - boxsize - 1, boxsize - 1) - qp.end() + draw_qr( + qr=self.qr, + paint_device=self, + is_enabled=self.isEnabled(), + min_boxsize=self.MIN_BOXSIZE, + ) def grab(self) -> QtGui.QPixmap: """Overrides QWidget.grab to only include the QR code itself, diff --git a/setup.py b/setup.py index a004490f6..ba15de1ba 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ extras_require = { 'gui': ['pyqt6'], 'crypto': ['cryptography>=2.6'], 'tests': ['pycryptodomex>=3.7', 'cryptography>=2.6', 'pyaes>=0.1a1'], - 'qml_gui': ['pyqt6', 'Pillow>=8.4.0'] + 'qml_gui': ['pyqt6<6.6', 'pyqt6-qt6<6.6'] } # 'full' extra that tries to grab everything an enduser would need (except for libsecp256k1...) extras_require['full'] = [pkg for sublist in