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,
pyqt5sip,
pyqt5,
pillow
pillow,
libzbar
# (str) Presplash of the application
#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
Item {
id: scanner
property bool active: false
property string url
property string scanData
property bool _pointsVisible
signal found
VideoOutput {
id: vo
anchors.fill: parent
source: camera
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 {
anchors.fill: parent
onClicked: {
vo.grabToImage(function(result) {
console.log("grab: image=" + (result.image !== undefined) + " url=" + result.url)
if (result.image !== undefined) {
console.log('scanning image for QR')
QR.scanImage(result.image)
}
})
Image {
id: still
anchors.fill: vo
}
SequentialAnimation {
id: foundAnimation
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
viewfinder.resolution: "640x480"
focus {
focusMode: Camera.FocusContinuous
focusPointMode: Camera.FocusPointCustom
customFocusPoint: Qt.point(0.5, 0.5)
}
function dumpstats() {
console.log(camera.viewfinder.resolution)
console.log(camera.viewfinder.minimumFrameRate)
@ -36,6 +108,49 @@ Item {
resolutions.forEach(function(item, i) {
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
Item {
id: scanPage
property string title: qsTr('Scan')
property bool toolbar: false
property string scanData
signal found
QRScan {
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width
onFound: {
scanPage.scanData = scanData
scanPage.found()
app.stack.pop()
}
}
Button {

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

@ -80,7 +80,13 @@ Pane {
Button {
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.QtGui import QImage
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QRect, QPoint
from PyQt5.QtGui import QImage,QColor
from PyQt5.QtQuick import QQuickImageProvider
from electrum.logging import get_logger
from electrum.qrreader import get_qr_reader
from electrum.i18n import _
import qrcode
#from qrcode.image.styledpil import StyledPilImage
#from qrcode.image.styles.moduledrawers import *
from PIL import Image, ImageQt
from ctypes import *
import sys
class QEQR(QObject):
def __init__(self, text=None, parent=None):
super().__init__(parent)
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__)
scanReadyChanged = pyqtSignal()
busyChanged = pyqtSignal()
dataChanged = pyqtSignal()
imageChanged = pyqtSignal()
_scanReady = True
_busy = False
_image = None
@pyqtSlot('QImage')
def scanImage(self, image=None):
if not self._scanReady:
self._logger.warning("Already processing an image. Check 'ready' property before calling scanImage")
if self._busy:
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
self._scanReady = False
self.scanReadyChanged.emit()
pilimage = self.convertToPILImage(image)
self.parseQR(pilimage)
self._busy = True
self.busyChanged.emit()
self._scanReady = True
self.logImageStats(image)
self._parseQR(image)
self._busy = False
self.busyChanged.emit()
def logImageStats(self, image):
self._logger.info('width: ' + str(image.width()))
@ -44,33 +56,63 @@ class QEQR(QObject):
self._logger.info('depth: ' + str(image.depth()))
self._logger.info('format: ' + str(image.format()))
def convertToPILImage(self, image): # -> Image:
self.logImageStats(image)
rawimage = image.constBits()
# assumption: pixels are 32 bits ARGB
numbytes = image.width() * image.height() * 4
self._logger.info(type(rawimage))
buf = bytearray(numbytes)
c_buf = (c_byte * numbytes).from_buffer(buf)
memmove(c_buf, c_void_p(rawimage.__int__()), numbytes)
buf2 = bytes(buf)
return Image.frombytes('RGBA', (image.width(), image.height()), buf2, 'raw')
def parseQR(self, image):
# TODO
pass
@pyqtProperty(bool, notify=scanReadyChanged)
def scanReady(self):
return self._scanReady
def _parseQR(self, image):
self.w = image.width()
self.h = image.height()
img_crop_rect = self._get_crop(image, 360)
frame_cropped = image.copy(img_crop_rect)
# Convert to Y800 / GREY FourCC (single 8-bit channel)
# This creates a copy, so we don't need to keep the frame around anymore
frame_y800 = frame_cropped.convertToFormat(QImage.Format_Grayscale8)
self.frame_id = 0
# Read the QR codes from the frame
self.qrreader_res = self.qrreader.read_qr_code(
frame_y800.constBits().__int__(), frame_y800.byteCount(),
frame_y800.bytesPerLine(),
frame_y800.width(),
frame_y800.height(), self.frame_id
)
if len(self.qrreader_res) > 0:
result = self.qrreader_res[0]
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)
def image(self):
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):
def __init__(self, parent=None):
super().__init__(QQuickImageProvider.Image)

5
electrum/qrreader/zbar.py

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

Loading…
Cancel
Save