Browse Source

implement QR code scanning

patch-4
Sander van Grieken 3 years ago
parent
commit
758a30462e
  1. 3
      contrib/android/buildozer_qml.spec
  2. 137
      electrum/gui/qml/components/QRScan.qml
  3. 12
      electrum/gui/qml/components/Scan.qml
  4. 8
      electrum/gui/qml/components/Send.qml
  5. 110
      electrum/gui/qml/qeqr.py
  6. 5
      electrum/qrreader/zbar.py

3
contrib/android/buildozer_qml.spec

@ -52,7 +52,8 @@ requirements =
cryptography, cryptography,
pyqt5sip, pyqt5sip,
pyqt5, pyqt5,
pillow pillow,
libzbar
# (str) Presplash of the application # (str) Presplash of the application
#presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png #presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png

137
electrum/gui/qml/components/QRScan.qml

@ -1,25 +1,91 @@
import QtQuick 2.6 import QtQuick 2.12
import QtQuick.Controls 2.0
import QtMultimedia 5.6 import QtMultimedia 5.6
Item { Item {
id: scanner
property bool active: false
property string url
property string scanData
property bool _pointsVisible
signal found
VideoOutput { VideoOutput {
id: vo id: vo
anchors.fill: parent anchors.fill: parent
source: camera source: camera
fillMode: VideoOutput.PreserveAspectCrop fillMode: VideoOutput.PreserveAspectCrop
Rectangle {
width: parent.width
height: (parent.height - parent.width) / 2
anchors.top: parent.top
color: Qt.rgba(0,0,0,0.5)
}
Rectangle {
width: parent.width
height: (parent.height - parent.width) / 2
anchors.bottom: parent.bottom
color: Qt.rgba(0,0,0,0.5)
}
} }
MouseArea { Image {
anchors.fill: parent id: still
onClicked: { anchors.fill: vo
vo.grabToImage(function(result) { }
console.log("grab: image=" + (result.image !== undefined) + " url=" + result.url)
if (result.image !== undefined) { SequentialAnimation {
console.log('scanning image for QR') id: foundAnimation
QR.scanImage(result.image) PropertyAction { target: scanner; property: '_pointsVisible'; value: true}
} PauseAnimation { duration: 80 }
}) PropertyAction { target: scanner; property: '_pointsVisible'; value: false}
PauseAnimation { duration: 80 }
PropertyAction { target: scanner; property: '_pointsVisible'; value: true}
PauseAnimation { duration: 80 }
PropertyAction { target: scanner; property: '_pointsVisible'; value: false}
PauseAnimation { duration: 80 }
PropertyAction { target: scanner; property: '_pointsVisible'; value: true}
PauseAnimation { duration: 80 }
PropertyAction { target: scanner; property: '_pointsVisible'; value: false}
PauseAnimation { duration: 80 }
PropertyAction { target: scanner; property: '_pointsVisible'; value: true}
onFinished: found()
}
Component {
id: r
Rectangle {
property int cx
property int cy
width: 15
height: 15
x: cx - width/2
y: cy - height/2
radius: 5
visible: scanner._pointsVisible
}
}
Connections {
target: QR
function onDataChanged() {
console.log(QR.data)
scanner.active = false
scanner.scanData = QR.data
still.source = scanner.url
var sx = still.width/still.sourceSize.width
var sy = still.height/still.sourceSize.height
r.createObject(scanner, {cx: QR.points[0].x * sx, cy: QR.points[0].y * sy, color: 'yellow'})
r.createObject(scanner, {cx: QR.points[1].x * sx, cy: QR.points[1].y * sy, color: 'yellow'})
r.createObject(scanner, {cx: QR.points[2].x * sx, cy: QR.points[2].y * sy, color: 'yellow'})
r.createObject(scanner, {cx: QR.points[3].x * sx, cy: QR.points[3].y * sy, color: 'yellow'})
foundAnimation.start()
} }
} }
@ -28,6 +94,12 @@ Item {
deviceId: QtMultimedia.defaultCamera.deviceId deviceId: QtMultimedia.defaultCamera.deviceId
viewfinder.resolution: "640x480" viewfinder.resolution: "640x480"
focus {
focusMode: Camera.FocusContinuous
focusPointMode: Camera.FocusPointCustom
customFocusPoint: Qt.point(0.5, 0.5)
}
function dumpstats() { function dumpstats() {
console.log(camera.viewfinder.resolution) console.log(camera.viewfinder.resolution)
console.log(camera.viewfinder.minimumFrameRate) console.log(camera.viewfinder.minimumFrameRate)
@ -36,6 +108,49 @@ Item {
resolutions.forEach(function(item, i) { resolutions.forEach(function(item, i) {
console.log('' + item.width + 'x' + item.height) console.log('' + item.width + 'x' + item.height)
}) })
// TODO
// pick a suitable resolution from the available resolutions
// problem: some cameras have no supportedViewfinderResolutions
// but still error out when an invalid resolution is set.
// 640x480 seems to be universally available, but this needs to
// be checked across a range of phone models.
} }
} }
Timer {
id: scanTimer
interval: 200
repeat: true
running: scanner.active
onTriggered: {
if (QR.busy)
return
vo.grabToImage(function(result) {
if (result.image !== undefined) {
scanner.url = result.url
QR.scanImage(result.image)
} else {
console.log('image grab returned null')
}
})
}
}
Component.onCompleted: {
console.log('Scan page initialized')
QtMultimedia.availableCameras.forEach(function(item) {
console.log('cam found')
console.log(item.deviceId)
console.log(item.displayName)
console.log(item.position)
console.log(item.orientation)
if (QtMultimedia.defaultCamera.deviceId == item.deviceId) {
vo.orientation = item.orientation
}
camera.dumpstats()
})
active = true
}
} }

12
electrum/gui/qml/components/Scan.qml

@ -2,13 +2,25 @@ import QtQuick 2.6
import QtQuick.Controls 2.0 import QtQuick.Controls 2.0
Item { Item {
id: scanPage
property string title: qsTr('Scan')
property bool toolbar: false property bool toolbar: false
property string scanData
signal found
QRScan { QRScan {
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
width: parent.width width: parent.width
onFound: {
scanPage.scanData = scanData
scanPage.found()
app.stack.pop()
}
} }
Button { Button {

8
electrum/gui/qml/components/Send.qml

@ -80,7 +80,13 @@ Pane {
Button { Button {
text: qsTr('Scan QR Code') text: qsTr('Scan QR Code')
onClicked: app.stack.push(Qt.resolvedUrl('Scan.qml')) onClicked: {
var page = app.stack.push(Qt.resolvedUrl('Scan.qml'))
page.onFound.connect(function() {
console.log('got ' + page.scanData)
address.text = page.scanData
})
}
} }
} }
} }

110
electrum/gui/qml/qeqr.py

@ -1,42 +1,54 @@
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QRect, QPoint
from PyQt5.QtGui import QImage from PyQt5.QtGui import QImage,QColor
from PyQt5.QtQuick import QQuickImageProvider from PyQt5.QtQuick import QQuickImageProvider
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.qrreader import get_qr_reader
from electrum.i18n import _
import qrcode import qrcode
#from qrcode.image.styledpil import StyledPilImage #from qrcode.image.styledpil import StyledPilImage
#from qrcode.image.styles.moduledrawers import * #from qrcode.image.styles.moduledrawers import *
from PIL import Image, ImageQt from PIL import Image, ImageQt
from ctypes import * from ctypes import *
import sys
class QEQR(QObject): class QEQR(QObject):
def __init__(self, text=None, parent=None): def __init__(self, text=None, parent=None):
super().__init__(parent) super().__init__(parent)
self._text = text self._text = text
self.qrreader = get_qr_reader()
if not self.qrreader:
raise Exception(_("The platform QR detection library is not available."))
_logger = get_logger(__name__) _logger = get_logger(__name__)
scanReadyChanged = pyqtSignal() busyChanged = pyqtSignal()
dataChanged = pyqtSignal()
imageChanged = pyqtSignal() imageChanged = pyqtSignal()
_scanReady = True _busy = False
_image = None _image = None
@pyqtSlot('QImage') @pyqtSlot('QImage')
def scanImage(self, image=None): def scanImage(self, image=None):
if not self._scanReady: if self._busy:
self._logger.warning("Already processing an image. Check 'ready' property before calling scanImage") self._logger.warning("Already processing an image. Check 'busy' property before calling scanImage")
return
if image == None:
self._logger.warning("No image to decode")
return return
self._scanReady = False
self.scanReadyChanged.emit()
pilimage = self.convertToPILImage(image) self._busy = True
self.parseQR(pilimage) self.busyChanged.emit()
self._scanReady = True self.logImageStats(image)
self._parseQR(image)
self._busy = False
self.busyChanged.emit()
def logImageStats(self, image): def logImageStats(self, image):
self._logger.info('width: ' + str(image.width())) self._logger.info('width: ' + str(image.width()))
@ -44,33 +56,63 @@ class QEQR(QObject):
self._logger.info('depth: ' + str(image.depth())) self._logger.info('depth: ' + str(image.depth()))
self._logger.info('format: ' + str(image.format())) self._logger.info('format: ' + str(image.format()))
def convertToPILImage(self, image): # -> Image: def _parseQR(self, image):
self.logImageStats(image) self.w = image.width()
self.h = image.height()
rawimage = image.constBits() img_crop_rect = self._get_crop(image, 360)
# assumption: pixels are 32 bits ARGB frame_cropped = image.copy(img_crop_rect)
numbytes = image.width() * image.height() * 4
# Convert to Y800 / GREY FourCC (single 8-bit channel)
self._logger.info(type(rawimage)) # This creates a copy, so we don't need to keep the frame around anymore
buf = bytearray(numbytes) frame_y800 = frame_cropped.convertToFormat(QImage.Format_Grayscale8)
c_buf = (c_byte * numbytes).from_buffer(buf)
memmove(c_buf, c_void_p(rawimage.__int__()), numbytes) self.frame_id = 0
buf2 = bytes(buf) # Read the QR codes from the frame
self.qrreader_res = self.qrreader.read_qr_code(
return Image.frombytes('RGBA', (image.width(), image.height()), buf2, 'raw') frame_y800.constBits().__int__(), frame_y800.byteCount(),
frame_y800.bytesPerLine(),
def parseQR(self, image): frame_y800.width(),
# TODO frame_y800.height(), self.frame_id
pass )
@pyqtProperty(bool, notify=scanReadyChanged) if len(self.qrreader_res) > 0:
def scanReady(self): result = self.qrreader_res[0]
return self._scanReady self._data = result
self.dataChanged.emit()
def _get_crop(self, image: QImage, scan_size: int) -> QRect:
"""
Returns a QRect that is scan_size x scan_size in the middle of the resolution
"""
self.scan_pos_x = (image.width() - scan_size) // 2
self.scan_pos_y = (image.height() - scan_size) // 2
return QRect(self.scan_pos_x, self.scan_pos_y, scan_size, scan_size)
@pyqtProperty(bool, notify=busyChanged)
def busy(self):
return self._busy
@pyqtProperty('QImage', notify=imageChanged) @pyqtProperty('QImage', notify=imageChanged)
def image(self): def image(self):
return self._image return self._image
@pyqtProperty(str, notify=dataChanged)
def data(self):
return self._data.data
@pyqtProperty('QPoint', notify=dataChanged)
def center(self):
(x,y) = self._data.center
return QPoint(x+self.scan_pos_x, y+self.scan_pos_y)
@pyqtProperty('QVariant', notify=dataChanged)
def points(self):
result = []
for item in self._data.points:
(x,y) = item
result.append(QPoint(x+self.scan_pos_x, y+self.scan_pos_y))
return result
class QEQRImageProvider(QQuickImageProvider): class QEQRImageProvider(QQuickImageProvider):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(QQuickImageProvider.Image) super().__init__(QQuickImageProvider.Image)

5
electrum/qrreader/zbar.py

@ -37,8 +37,9 @@ from .abstract_base import AbstractQrCodeReader, QrCodeResult
_logger = get_logger(__name__) _logger = get_logger(__name__)
if 'ANDROID_DATA' in os.environ:
if sys.platform == 'darwin': LIBNAME = 'libzbar.so'
elif sys.platform == 'darwin':
LIBNAME = 'libzbar.0.dylib' LIBNAME = 'libzbar.0.dylib'
elif sys.platform in ('windows', 'win32'): elif sys.platform in ('windows', 'win32'):
LIBNAME = 'libzbar-0.dll' LIBNAME = 'libzbar-0.dll'

Loading…
Cancel
Save