implement QR code scanning
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
self._scanReady = False
|
||||
self.scanReadyChanged.emit()
|
||||
|
||||
pilimage = self.convertToPILImage(image)
|
||||
self.parseQR(pilimage)
|
||||
if image == None:
|
||||
self._logger.warning("No image to decode")
|
||||
return
|
||||
|
||||
self._scanReady = True
|
||||
self._busy = True
|
||||
self.busyChanged.emit()
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
rawimage = image.constBits()
|
||||
# assumption: pixels are 32 bits ARGB
|
||||
numbytes = image.width() * image.height() * 4
|
||||
# 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._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)
|
||||
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
|
||||
)
|
||||
|
||||
return Image.frombytes('RGBA', (image.width(), image.height()), buf2, 'raw')
|
||||
if len(self.qrreader_res) > 0:
|
||||
result = self.qrreader_res[0]
|
||||
self._data = result
|
||||
self.dataChanged.emit()
|
||||
|
||||
def parseQR(self, image):
|
||||
# TODO
|
||||
pass
|
||||
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=scanReadyChanged)
|
||||
def scanReady(self):
|
||||
return self._scanReady
|
||||
@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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user