1
0

qml: remove dependency "Pillow" (and its transitive deps)

closes https://github.com/spesmilo/electrum/issues/9572
This commit is contained in:
SomberNight
2025-02-20 17:58:42 +00:00
parent ea4adbb4d6
commit a3fc43cc2d
10 changed files with 111 additions and 147 deletions

View File

@@ -76,7 +76,6 @@ requirements =
cryptography,
pyqt6sip,
pyqt6,
pillow,
libzbar
# (str) Presplash of the application

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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