qml: remove dependency "Pillow" (and its transitive deps)
closes https://github.com/spesmilo/electrum/issues/9572
This commit is contained in:
@@ -76,7 +76,6 @@ requirements =
|
|||||||
cryptography,
|
cryptography,
|
||||||
pyqt6sip,
|
pyqt6sip,
|
||||||
pyqt6,
|
pyqt6,
|
||||||
pillow,
|
|
||||||
libzbar
|
libzbar
|
||||||
|
|
||||||
# (str) Presplash of the application
|
# (str) Presplash of the application
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
75
electrum/gui/common_qt/util.py
Normal file
75
electrum/gui/common_qt/util.py
Normal 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()
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user