Skip to content

Commit a3fc43c

Browse files
committed
qml: remove dependency "Pillow" (and its transitive deps)
closes #9572
1 parent ea4adbb commit a3fc43c

File tree

10 files changed

+111
-147
lines changed

10 files changed

+111
-147
lines changed

contrib/android/buildozer_qml.spec

-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ requirements =
7676
cryptography,
7777
pyqt6sip,
7878
pyqt6,
79-
pillow,
8079
libzbar
8180

8281
# (str) Presplash of the application

contrib/android/p4a_recipes/Pillow/__init__.py

-18
This file was deleted.

contrib/android/p4a_recipes/freetype/__init__.py

-18
This file was deleted.

contrib/android/p4a_recipes/jpeg/__init__.py

-18
This file was deleted.

contrib/android/p4a_recipes/png/__init__.py

-18
This file was deleted.

electrum/gui/common_qt/util.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from typing import Optional
2+
3+
from PyQt6 import QtGui
4+
from PyQt6.QtCore import Qt
5+
from PyQt6.QtGui import QColor, QPen, QPaintDevice
6+
import qrcode
7+
8+
from electrum.i18n import _
9+
10+
11+
def draw_qr(
12+
*,
13+
qr: Optional[qrcode.main.QRCode],
14+
paint_device: QPaintDevice, # target to paint on
15+
is_enabled: bool = True,
16+
min_boxsize: int = 2, # min size in pixels of single black/white unit box of the qr code
17+
) -> None:
18+
"""Draw 'qr' onto 'paint_device'.
19+
- qr.box_size is ignored. We will calculate our own boxsize to fill the whole size of paint_device.
20+
- qr.border is respected.
21+
"""
22+
black = QColor(0, 0, 0, 255)
23+
grey = QColor(196, 196, 196, 255)
24+
white = QColor(255, 255, 255, 255)
25+
black_pen = QPen(black) if is_enabled else QPen(grey)
26+
black_pen.setJoinStyle(Qt.PenJoinStyle.MiterJoin)
27+
28+
if not qr:
29+
qp = QtGui.QPainter()
30+
qp.begin(paint_device)
31+
qp.setBrush(white)
32+
qp.setPen(white)
33+
r = qp.viewport()
34+
qp.drawRect(0, 0, r.width(), r.height())
35+
qp.end()
36+
return
37+
38+
# note: next line can raise qrcode.exceptions.DataOverflowError (or ValueError)
39+
matrix = qr.get_matrix() # includes qr.border
40+
k = len(matrix)
41+
qp = QtGui.QPainter()
42+
qp.begin(paint_device)
43+
r = qp.viewport()
44+
framesize = min(r.width(), r.height())
45+
boxsize = int(framesize / k)
46+
if boxsize < min_boxsize:
47+
# The amount of data is still within what can fit into a QR code,
48+
# however we don't have enough pixels to draw it.
49+
qp.setBrush(white)
50+
qp.setPen(white)
51+
qp.drawRect(0, 0, r.width(), r.height())
52+
qp.setBrush(black)
53+
qp.setPen(black)
54+
qp.drawText(0, 20, _("Cannot draw QR code") + ":")
55+
qp.drawText(0, 40, _("Not enough space available."))
56+
qp.end()
57+
return
58+
size = k * boxsize
59+
left = (framesize - size) / 2
60+
top = (framesize - size) / 2
61+
# Draw white background with margin
62+
qp.setBrush(white)
63+
qp.setPen(white)
64+
qp.drawRect(0, 0, framesize, framesize)
65+
# Draw qr code
66+
qp.setBrush(black if is_enabled else grey)
67+
qp.setPen(black_pen)
68+
for r in range(k):
69+
for c in range(k):
70+
if matrix[r][c]:
71+
qp.drawRect(
72+
int(left + c * boxsize), int(top + r * boxsize),
73+
boxsize - 1, boxsize - 1)
74+
qp.end()
75+

electrum/gui/qml/components/controls/QRImage.qml

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Item {
1515

1616
Rectangle {
1717
id: r
18-
width: _qrprops.modules * _qrprops.box_size
18+
width: _qrprops.qr_pixelsize
1919
height: width
2020
color: 'white'
2121
}
@@ -29,8 +29,8 @@ Item {
2929
color: 'white'
3030
x: (parent.width - width) / 2
3131
y: (parent.height - height) / 2
32-
width: _qrprops.icon_modules * _qrprops.box_size
33-
height: _qrprops.icon_modules * _qrprops.box_size
32+
width: _qrprops.icon_pixelsize
33+
height: _qrprops.icon_pixelsize
3434

3535
Image {
3636
visible: _qrprops.valid

electrum/gui/qml/qeqr.py

+23-19
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
import math
66
import urllib
77

8-
from PIL import ImageQt
9-
108
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRect
119
from PyQt6.QtGui import QImage, QColor
1210
from PyQt6.QtQuick import QQuickImageProvider
@@ -22,6 +20,7 @@
2220
from electrum.qrreader import get_qr_reader
2321
from electrum.i18n import _
2422
from electrum.util import profiler, get_asyncio_loop
23+
from electrum.gui.common_qt.util import draw_qr
2524

2625

2726
class QEQRParser(QObject):
@@ -125,6 +124,11 @@ def reset(self):
125124

126125

127126
class QEQRImageProvider(QQuickImageProvider):
127+
MAX_QR_PIXELSIZE = 400
128+
ERROR_CORRECT_LEVEL = qrcode.constants.ERROR_CORRECT_M
129+
# ^ note: this is higher than for desktop. but on desktop we don't put a logo in the middle.
130+
QR_BORDER = 2
131+
128132
def __init__(self, max_size, parent=None):
129133
super().__init__(QQuickImageProvider.ImageType.Image)
130134
self._max_size = max_size
@@ -147,20 +151,18 @@ def requestImage(self, qstr, size):
147151
uri = uri._replace(query=query)
148152
qstr = urllib.parse.urlunparse(uri)
149153

150-
qr = qrcode.QRCode(version=1, border=2)
151-
qr.add_data(qstr)
154+
qr = qrcode.main.QRCode(border=self.QR_BORDER, error_correction=self.ERROR_CORRECT_LEVEL)
152155

153156
# calculate best box_size
154-
pixelsize = min(self._max_size, 400)
157+
pixelsize = min(self._max_size, self.MAX_QR_PIXELSIZE)
155158
try:
156-
modules = 17 + 4 * qr.best_fit() + qr.border * 2
159+
qr.add_data(qstr)
160+
modules = len(qr.get_matrix())
157161
qr.box_size = math.floor(pixelsize/modules)
158-
159162
qr.make(fit=True)
160-
161-
pimg = qr.make_image(fill_color='black', back_color='white')
162-
self.qimg = ImageQt.ImageQt(pimg)
163-
except DataOverflowError:
163+
self.qimg = QImage(modules * qr.box_size, modules * qr.box_size, QImage.Format.Format_RGB32)
164+
draw_qr(qr=qr, paint_device=self.qimg)
165+
except (ValueError, qrcode.exceptions.DataOverflowError):
164166
# fake it
165167
modules = 17 + qr.border * 2
166168
box_size = math.floor(pixelsize/modules)
@@ -179,15 +181,18 @@ def __init__(self, max_size, parent=None):
179181

180182
@pyqtSlot(str, result='QVariantMap')
181183
def getDimensions(self, qstr):
182-
qr = qrcode.QRCode(version=1, border=2)
183-
qr.add_data(qstr)
184+
qr = qrcode.QRCode(
185+
border=QEQRImageProvider.QR_BORDER,
186+
error_correction=QEQRImageProvider.ERROR_CORRECT_LEVEL,
187+
)
184188

185189
# calculate best box_size
186-
pixelsize = min(self._max_size, 400)
190+
pixelsize = min(self._max_size, QEQRImageProvider.MAX_QR_PIXELSIZE)
187191
try:
188-
modules = 17 + 4 * qr.best_fit() + qr.border * 2
192+
qr.add_data(qstr)
193+
modules = len(qr.get_matrix())
189194
valid = True
190-
except DataOverflowError:
195+
except (ValueError, qrcode.exceptions.DataOverflowError):
191196
# fake it
192197
modules = 17 + qr.border * 2
193198
valid = False
@@ -198,8 +203,7 @@ def getDimensions(self, qstr):
198203
icon_modules += (icon_modules+1) % 2 # force odd
199204

200205
return {
201-
'modules': modules,
202-
'box_size': qr.box_size,
203-
'icon_modules': icon_modules,
206+
'qr_pixelsize': modules * qr.box_size,
207+
'icon_pixelsize': icon_modules * qr.box_size,
204208
'valid': valid
205209
}

electrum/gui/qt/qrcodewidget.py

+9-51
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
import qrcode
44
import qrcode.exceptions
55

6-
from PyQt6.QtGui import QColor, QPen
76
import PyQt6.QtGui as QtGui
8-
from PyQt6.QtCore import Qt, QRect
7+
from PyQt6.QtCore import QRect
98
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QPushButton, QWidget
109

1110
from electrum.i18n import _
1211
from electrum.simple_config import SimpleConfig
12+
from electrum.gui.common_qt.util import draw_qr
1313

1414
from .util import WindowModalDialog, WWLabel, getSaveFileName
1515

@@ -34,8 +34,7 @@ def setData(self, data):
3434
if data:
3535
qr = qrcode.QRCode(
3636
error_correction=qrcode.constants.ERROR_CORRECT_L,
37-
box_size=10,
38-
border=0,
37+
border=1,
3938
)
4039
try:
4140
qr.add_data(data)
@@ -57,53 +56,12 @@ def setData(self, data):
5756
def paintEvent(self, e):
5857
if not self.data:
5958
return
60-
61-
black = QColor(0, 0, 0, 255)
62-
grey = QColor(196, 196, 196, 255)
63-
white = QColor(255, 255, 255, 255)
64-
black_pen = QPen(black) if self.isEnabled() else QPen(grey)
65-
black_pen.setJoinStyle(Qt.PenJoinStyle.MiterJoin)
66-
67-
if not self.qr:
68-
qp = QtGui.QPainter()
69-
qp.begin(self)
70-
qp.setBrush(white)
71-
qp.setPen(white)
72-
r = qp.viewport()
73-
qp.drawRect(0, 0, r.width(), r.height())
74-
qp.end()
75-
return
76-
77-
matrix = self.qr.get_matrix()
78-
k = len(matrix)
79-
qp = QtGui.QPainter()
80-
qp.begin(self)
81-
r = qp.viewport()
82-
framesize = min(r.width(), r.height())
83-
self._framesize = framesize
84-
boxsize = int(framesize/(k + 2))
85-
if boxsize < self.MIN_BOXSIZE:
86-
qp.drawText(0, 20, _("Cannot draw QR code")+":")
87-
qp.drawText(0, 40, _("Not enough space available. Try increasing the window size."))
88-
qp.end()
89-
return
90-
size = k*boxsize
91-
left = (framesize - size)/2
92-
top = (framesize - size)/2
93-
# Draw white background with margin
94-
qp.setBrush(white)
95-
qp.setPen(white)
96-
qp.drawRect(0, 0, framesize, framesize)
97-
# Draw qr code
98-
qp.setBrush(black if self.isEnabled() else grey)
99-
qp.setPen(black_pen)
100-
for r in range(k):
101-
for c in range(k):
102-
if matrix[r][c]:
103-
qp.drawRect(
104-
int(left+c*boxsize), int(top+r*boxsize),
105-
boxsize - 1, boxsize - 1)
106-
qp.end()
59+
draw_qr(
60+
qr=self.qr,
61+
paint_device=self,
62+
is_enabled=self.isEnabled(),
63+
min_boxsize=self.MIN_BOXSIZE,
64+
)
10765

10866
def grab(self) -> QtGui.QPixmap:
10967
"""Overrides QWidget.grab to only include the QR code itself,

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
'gui': ['pyqt6'],
4646
'crypto': ['cryptography>=2.6'],
4747
'tests': ['pycryptodomex>=3.7', 'cryptography>=2.6', 'pyaes>=0.1a1'],
48-
'qml_gui': ['pyqt6', 'Pillow>=8.4.0']
48+
'qml_gui': ['pyqt6<6.6', 'pyqt6-qt6<6.6']
4949
}
5050
# 'full' extra that tries to grab everything an enduser would need (except for libsecp256k1...)
5151
extras_require['full'] = [pkg for sublist in

0 commit comments

Comments
 (0)