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, cryptography,
pyqt6sip, pyqt6sip,
pyqt6, pyqt6,
pillow,
libzbar libzbar
# (str) Presplash of the application # (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 { Rectangle {
id: r id: r
width: _qrprops.modules * _qrprops.box_size width: _qrprops.qr_pixelsize
height: width height: width
color: 'white' color: 'white'
} }
@@ -29,8 +29,8 @@ Item {
color: 'white' color: 'white'
x: (parent.width - width) / 2 x: (parent.width - width) / 2
y: (parent.height - height) / 2 y: (parent.height - height) / 2
width: _qrprops.icon_modules * _qrprops.box_size width: _qrprops.icon_pixelsize
height: _qrprops.icon_modules * _qrprops.box_size height: _qrprops.icon_pixelsize
Image { Image {
visible: _qrprops.valid visible: _qrprops.valid

View File

@@ -5,8 +5,6 @@ from qrcode.exceptions import DataOverflowError
import math import math
import urllib import urllib
from PIL import ImageQt
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRect from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRect
from PyQt6.QtGui import QImage, QColor from PyQt6.QtGui import QImage, QColor
from PyQt6.QtQuick import QQuickImageProvider from PyQt6.QtQuick import QQuickImageProvider
@@ -22,6 +20,7 @@ from electrum.logging import get_logger
from electrum.qrreader import get_qr_reader from electrum.qrreader import get_qr_reader
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import profiler, get_asyncio_loop from electrum.util import profiler, get_asyncio_loop
from electrum.gui.common_qt.util import draw_qr
class QEQRParser(QObject): class QEQRParser(QObject):
@@ -125,6 +124,11 @@ class QEQRParser(QObject):
class QEQRImageProvider(QQuickImageProvider): 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): def __init__(self, max_size, parent=None):
super().__init__(QQuickImageProvider.ImageType.Image) super().__init__(QQuickImageProvider.ImageType.Image)
self._max_size = max_size self._max_size = max_size
@@ -147,20 +151,18 @@ class QEQRImageProvider(QQuickImageProvider):
uri = uri._replace(query=query) uri = uri._replace(query=query)
qstr = urllib.parse.urlunparse(uri) qstr = urllib.parse.urlunparse(uri)
qr = qrcode.QRCode(version=1, border=2) qr = qrcode.main.QRCode(border=self.QR_BORDER, error_correction=self.ERROR_CORRECT_LEVEL)
qr.add_data(qstr)
# calculate best box_size # calculate best box_size
pixelsize = min(self._max_size, 400) pixelsize = min(self._max_size, self.MAX_QR_PIXELSIZE)
try: 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.box_size = math.floor(pixelsize/modules)
qr.make(fit=True) qr.make(fit=True)
self.qimg = QImage(modules * qr.box_size, modules * qr.box_size, QImage.Format.Format_RGB32)
pimg = qr.make_image(fill_color='black', back_color='white') draw_qr(qr=qr, paint_device=self.qimg)
self.qimg = ImageQt.ImageQt(pimg) except (ValueError, qrcode.exceptions.DataOverflowError):
except DataOverflowError:
# fake it # fake it
modules = 17 + qr.border * 2 modules = 17 + qr.border * 2
box_size = math.floor(pixelsize/modules) box_size = math.floor(pixelsize/modules)
@@ -179,15 +181,18 @@ class QEQRImageProviderHelper(QObject):
@pyqtSlot(str, result='QVariantMap') @pyqtSlot(str, result='QVariantMap')
def getDimensions(self, qstr): def getDimensions(self, qstr):
qr = qrcode.QRCode(version=1, border=2) qr = qrcode.QRCode(
qr.add_data(qstr) border=QEQRImageProvider.QR_BORDER,
error_correction=QEQRImageProvider.ERROR_CORRECT_LEVEL,
)
# calculate best box_size # calculate best box_size
pixelsize = min(self._max_size, 400) pixelsize = min(self._max_size, QEQRImageProvider.MAX_QR_PIXELSIZE)
try: try:
modules = 17 + 4 * qr.best_fit() + qr.border * 2 qr.add_data(qstr)
modules = len(qr.get_matrix())
valid = True valid = True
except DataOverflowError: except (ValueError, qrcode.exceptions.DataOverflowError):
# fake it # fake it
modules = 17 + qr.border * 2 modules = 17 + qr.border * 2
valid = False valid = False
@@ -198,8 +203,7 @@ class QEQRImageProviderHelper(QObject):
icon_modules += (icon_modules+1) % 2 # force odd icon_modules += (icon_modules+1) % 2 # force odd
return { return {
'modules': modules, 'qr_pixelsize': modules * qr.box_size,
'box_size': qr.box_size, 'icon_pixelsize': icon_modules * qr.box_size,
'icon_modules': icon_modules,
'valid': valid 'valid': valid
} }

View File

@@ -3,13 +3,13 @@ from typing import Optional
import qrcode import qrcode
import qrcode.exceptions import qrcode.exceptions
from PyQt6.QtGui import QColor, QPen
import PyQt6.QtGui as QtGui 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 PyQt6.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QPushButton, QWidget
from electrum.i18n import _ from electrum.i18n import _
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from electrum.gui.common_qt.util import draw_qr
from .util import WindowModalDialog, WWLabel, getSaveFileName from .util import WindowModalDialog, WWLabel, getSaveFileName
@@ -34,8 +34,7 @@ class QRCodeWidget(QWidget):
if data: if data:
qr = qrcode.QRCode( qr = qrcode.QRCode(
error_correction=qrcode.constants.ERROR_CORRECT_L, error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10, border=1,
border=0,
) )
try: try:
qr.add_data(data) qr.add_data(data)
@@ -57,53 +56,12 @@ class QRCodeWidget(QWidget):
def paintEvent(self, e): def paintEvent(self, e):
if not self.data: if not self.data:
return return
draw_qr(
black = QColor(0, 0, 0, 255) qr=self.qr,
grey = QColor(196, 196, 196, 255) paint_device=self,
white = QColor(255, 255, 255, 255) is_enabled=self.isEnabled(),
black_pen = QPen(black) if self.isEnabled() else QPen(grey) min_boxsize=self.MIN_BOXSIZE,
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()
def grab(self) -> QtGui.QPixmap: def grab(self) -> QtGui.QPixmap:
"""Overrides QWidget.grab to only include the QR code itself, """Overrides QWidget.grab to only include the QR code itself,

View File

@@ -45,7 +45,7 @@ extras_require = {
'gui': ['pyqt6'], 'gui': ['pyqt6'],
'crypto': ['cryptography>=2.6'], 'crypto': ['cryptography>=2.6'],
'tests': ['pycryptodomex>=3.7', 'cryptography>=2.6', 'pyaes>=0.1a1'], '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...) # 'full' extra that tries to grab everything an enduser would need (except for libsecp256k1...)
extras_require['full'] = [pkg for sublist in extras_require['full'] = [pkg for sublist in