1
0

implement QR code scanning

This commit is contained in:
Sander van Grieken
2022-03-29 16:36:20 +02:00
parent 62009c647e
commit 758a30462e
6 changed files with 221 additions and 44 deletions

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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
})
}
}
}
}

View File

@@ -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)

View File

@@ -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'