ghost43
4 years ago
committed by
GitHub
26 changed files with 1744 additions and 171 deletions
@ -1 +0,0 @@ |
|||||
Subproject commit 59dfc03272751cd29ee311456fa34c40f7ebb7c0 |
|
@ -0,0 +1,141 @@ |
|||||
|
# Copyright (C) 2021 The Electrum developers |
||||
|
# Distributed under the MIT software license, see the accompanying |
||||
|
# file LICENCE or http://www.opensource.org/licenses/mit-license.php |
||||
|
# |
||||
|
# We have two toolchains to scan qr codes: |
||||
|
# 1. access camera via QtMultimedia, take picture, feed picture to zbar |
||||
|
# 2. let zbar handle whole flow (including accessing the camera) |
||||
|
# |
||||
|
# notes: |
||||
|
# - zbar needs to be compiled with platform-dependent extra config options to be able |
||||
|
# to access the camera |
||||
|
# - zbar fails to access the camera on macOS |
||||
|
# - qtmultimedia seems to support more cameras on Windows than zbar |
||||
|
# - qtmultimedia is often not packaged with PyQt5 |
||||
|
# in particular, on debian, you need both "python3-pyqt5" and "python3-pyqt5.qtmultimedia" |
||||
|
# - older versions of qtmultimedia don't seem to work reliably |
||||
|
# |
||||
|
# Considering the above, we use QtMultimedia for Windows and macOS, as there |
||||
|
# most users run our binaries where we can make sure the packaged versions work well. |
||||
|
# On Linux where many people run from source, we use zbar. |
||||
|
# |
||||
|
# Note: this module is safe to import on all platforms. |
||||
|
|
||||
|
import sys |
||||
|
from typing import Callable, Optional, TYPE_CHECKING, Mapping |
||||
|
|
||||
|
from PyQt5.QtWidgets import QMessageBox, QWidget |
||||
|
|
||||
|
from electrum.i18n import _ |
||||
|
from electrum.util import UserFacingException |
||||
|
from electrum.logging import get_logger |
||||
|
|
||||
|
from electrum.gui.qt.util import MessageBoxMixin, custom_message_box |
||||
|
|
||||
|
|
||||
|
if TYPE_CHECKING: |
||||
|
from electrum.simple_config import SimpleConfig |
||||
|
|
||||
|
|
||||
|
_logger = get_logger(__name__) |
||||
|
|
||||
|
|
||||
|
def scan_qrcode( |
||||
|
*, |
||||
|
parent: Optional[QWidget], |
||||
|
config: 'SimpleConfig', |
||||
|
callback: Callable[[bool, str, Optional[str]], None], |
||||
|
) -> None: |
||||
|
if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'): |
||||
|
_scan_qrcode_using_qtmultimedia(parent=parent, config=config, callback=callback) |
||||
|
else: # desktop Linux and similar |
||||
|
_scan_qrcode_using_zbar(parent=parent, config=config, callback=callback) |
||||
|
|
||||
|
|
||||
|
def find_system_cameras() -> Mapping[str, str]: |
||||
|
"""Returns a camera_description -> camera_path map.""" |
||||
|
if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'): |
||||
|
try: |
||||
|
from .qtmultimedia import find_system_cameras |
||||
|
except ImportError as e: |
||||
|
return {} |
||||
|
else: |
||||
|
return find_system_cameras() |
||||
|
else: # desktop Linux and similar |
||||
|
from electrum import qrscanner |
||||
|
return qrscanner.find_system_cameras() |
||||
|
|
||||
|
|
||||
|
# --- Internals below (not part of external API) |
||||
|
|
||||
|
def _scan_qrcode_using_zbar( |
||||
|
*, |
||||
|
parent: Optional[QWidget], |
||||
|
config: 'SimpleConfig', |
||||
|
callback: Callable[[bool, str, Optional[str]], None], |
||||
|
) -> None: |
||||
|
from electrum import qrscanner |
||||
|
data = None |
||||
|
try: |
||||
|
data = qrscanner.scan_barcode(config.get_video_device()) |
||||
|
except UserFacingException as e: |
||||
|
success = False |
||||
|
error = str(e) |
||||
|
except BaseException as e: |
||||
|
_logger.exception('camera error') |
||||
|
success = False |
||||
|
error = repr(e) |
||||
|
else: |
||||
|
success = True |
||||
|
error = "" |
||||
|
callback(success, error, data) |
||||
|
|
||||
|
|
||||
|
# Use a global to prevent multiple QR dialogs created simultaneously |
||||
|
_qr_dialog = None |
||||
|
|
||||
|
|
||||
|
def _scan_qrcode_using_qtmultimedia( |
||||
|
*, |
||||
|
parent: Optional[QWidget], |
||||
|
config: 'SimpleConfig', |
||||
|
callback: Callable[[bool, str, Optional[str]], None], |
||||
|
) -> None: |
||||
|
try: |
||||
|
from .qtmultimedia import QrReaderCameraDialog, CameraError, MissingQrDetectionLib |
||||
|
except ImportError as e: |
||||
|
icon = QMessageBox.Warning |
||||
|
title = _("QR Reader Error") |
||||
|
message = _("QR reader failed to load. This may happen if " |
||||
|
"you are using an older version of PyQt5.") + "\n\n" + str(e) |
||||
|
if isinstance(parent, MessageBoxMixin): |
||||
|
parent.msg_box(title=title, text=message, icon=icon, parent=None) |
||||
|
else: |
||||
|
custom_message_box(title=title, text=message, icon=icon, parent=parent) |
||||
|
return |
||||
|
|
||||
|
global _qr_dialog |
||||
|
if _qr_dialog: |
||||
|
_logger.warning("QR dialog is already presented, ignoring.") |
||||
|
return |
||||
|
_qr_dialog = None |
||||
|
try: |
||||
|
_qr_dialog = QrReaderCameraDialog(parent=parent, config=config) |
||||
|
|
||||
|
def _on_qr_reader_finished(success: bool, error: str, data): |
||||
|
global _qr_dialog |
||||
|
if _qr_dialog: |
||||
|
_qr_dialog.deleteLater() |
||||
|
_qr_dialog = None |
||||
|
callback(success, error, data) |
||||
|
|
||||
|
_qr_dialog.qr_finished.connect(_on_qr_reader_finished) |
||||
|
_qr_dialog.start_scan(config.get_video_device()) |
||||
|
except (MissingQrDetectionLib, CameraError) as e: |
||||
|
_qr_dialog = None |
||||
|
callback(False, str(e), None) |
||||
|
except Exception as e: |
||||
|
_logger.exception('camera error') |
||||
|
_qr_dialog = None |
||||
|
callback(False, repr(e), None) |
||||
|
|
@ -0,0 +1,39 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
|
||||
|
from typing import Mapping |
||||
|
|
||||
|
from .camera_dialog import (QrReaderCameraDialog, CameraError, NoCamerasFound, |
||||
|
NoCameraResolutionsFound, MissingQrDetectionLib) |
||||
|
from .validator import (QrReaderValidatorResult, AbstractQrReaderValidator, |
||||
|
QrReaderValidatorCounting, QrReaderValidatorColorizing, |
||||
|
QrReaderValidatorStrong, QrReaderValidatorCounted) |
||||
|
|
||||
|
|
||||
|
def find_system_cameras() -> Mapping[str, str]: |
||||
|
"""Returns a camera_description -> camera_path map.""" |
||||
|
from PyQt5.QtMultimedia import QCameraInfo |
||||
|
system_cameras = QCameraInfo.availableCameras() |
||||
|
return {cam.description(): cam.deviceName() for cam in system_cameras} |
@ -0,0 +1,463 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
|
||||
|
import time |
||||
|
import math |
||||
|
import sys |
||||
|
import os |
||||
|
from typing import List |
||||
|
|
||||
|
from PyQt5.QtMultimedia import QCameraInfo, QCamera, QCameraViewfinderSettings |
||||
|
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QCheckBox, QPushButton, QLabel |
||||
|
from PyQt5.QtGui import QImage, QPixmap |
||||
|
from PyQt5.QtCore import QSize, QRect, Qt, pyqtSignal, PYQT_VERSION |
||||
|
|
||||
|
from electrum.simple_config import SimpleConfig |
||||
|
from electrum.i18n import _ |
||||
|
from electrum.qrreader import get_qr_reader, QrCodeResult |
||||
|
from electrum.logging import Logger |
||||
|
|
||||
|
from electrum.gui.qt.util import MessageBoxMixin, FixedAspectRatioLayout, ImageGraphicsEffect |
||||
|
|
||||
|
from .video_widget import QrReaderVideoWidget |
||||
|
from .video_overlay import QrReaderVideoOverlay |
||||
|
from .video_surface import QrReaderVideoSurface |
||||
|
from .crop_blur_effect import QrReaderCropBlurEffect |
||||
|
from .validator import AbstractQrReaderValidator, QrReaderValidatorCounted, QrReaderValidatorResult |
||||
|
|
||||
|
|
||||
|
class CameraError(RuntimeError): |
||||
|
''' Base class of the camera-related error conditions. ''' |
||||
|
|
||||
|
class NoCamerasFound(CameraError): |
||||
|
''' Raised by start_scan if no usable cameras were found. Interested |
||||
|
code can catch this specific exception.''' |
||||
|
|
||||
|
class NoCameraResolutionsFound(CameraError): |
||||
|
''' Raised internally if no usable camera resolutions were found. ''' |
||||
|
|
||||
|
class MissingQrDetectionLib(RuntimeError): |
||||
|
''' Raised if we can't find zbar or whatever other platform lib |
||||
|
we require to detect QR in image frames. ''' |
||||
|
|
||||
|
class QrReaderCameraDialog(Logger, MessageBoxMixin, QDialog): |
||||
|
""" |
||||
|
Dialog for reading QR codes from a camera |
||||
|
""" |
||||
|
|
||||
|
# Try to crop so we have minimum 512 dimensions |
||||
|
SCAN_SIZE: int = 512 |
||||
|
|
||||
|
qr_finished = pyqtSignal(bool, str, object) |
||||
|
|
||||
|
def __init__(self, parent, *, config: SimpleConfig): |
||||
|
''' Note: make sure parent is a "top_level_window()" as per |
||||
|
MessageBoxMixin API else bad things can happen on macOS. ''' |
||||
|
QDialog.__init__(self, parent=parent) |
||||
|
Logger.__init__(self) |
||||
|
|
||||
|
self.validator: AbstractQrReaderValidator = None |
||||
|
self.frame_id: int = 0 |
||||
|
self.qr_crop: QRect = None |
||||
|
self.qrreader_res: List[QrCodeResult] = [] |
||||
|
self.validator_res: QrReaderValidatorResult = None |
||||
|
self.last_stats_time: float = 0.0 |
||||
|
self.frame_counter: int = 0 |
||||
|
self.qr_frame_counter: int = 0 |
||||
|
self.last_qr_scan_ts: float = 0.0 |
||||
|
self.camera: QCamera = None |
||||
|
self._error_message: str = None |
||||
|
self._ok_done: bool = False |
||||
|
self.camera_sc_conn = None |
||||
|
self.resolution: QSize = None |
||||
|
|
||||
|
self.config = config |
||||
|
|
||||
|
# Try to get the QR reader for this system |
||||
|
self.qrreader = get_qr_reader() |
||||
|
if not self.qrreader: |
||||
|
raise MissingQrDetectionLib(_("The platform QR detection library is not available.")) |
||||
|
|
||||
|
# Set up the window, add the maximize button |
||||
|
flags = self.windowFlags() |
||||
|
flags = flags | Qt.WindowMaximizeButtonHint |
||||
|
self.setWindowFlags(flags) |
||||
|
self.setWindowTitle(_("Scan QR Code")) |
||||
|
self.setWindowModality(Qt.WindowModal if parent else Qt.ApplicationModal) |
||||
|
|
||||
|
# Create video widget and fixed aspect ratio layout to contain it |
||||
|
self.video_widget = QrReaderVideoWidget() |
||||
|
self.video_overlay = QrReaderVideoOverlay() |
||||
|
self.video_layout = FixedAspectRatioLayout() |
||||
|
self.video_layout.addWidget(self.video_widget) |
||||
|
self.video_layout.addWidget(self.video_overlay) |
||||
|
|
||||
|
# Create root layout and add the video widget layout to it |
||||
|
vbox = QVBoxLayout() |
||||
|
self.setLayout(vbox) |
||||
|
vbox.setContentsMargins(0, 0, 0, 0) |
||||
|
vbox.addLayout(self.video_layout) |
||||
|
|
||||
|
self.lowres_label = QLabel(_("Note: This camera generates frames of relatively low resolution; QR scanning accuracy may be affected")) |
||||
|
self.lowres_label.setWordWrap(True) |
||||
|
self.lowres_label.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) |
||||
|
vbox.addWidget(self.lowres_label) |
||||
|
self.lowres_label.setHidden(True) |
||||
|
|
||||
|
# Create a layout for the controls |
||||
|
controls_layout = QHBoxLayout() |
||||
|
controls_layout.addStretch(2) |
||||
|
controls_layout.setContentsMargins(10, 10, 10, 10) |
||||
|
controls_layout.setSpacing(10) |
||||
|
vbox.addLayout(controls_layout) |
||||
|
|
||||
|
# Flip horizontally checkbox with default coming from global config |
||||
|
self.flip_x = QCheckBox() |
||||
|
self.flip_x.setText(_("&Flip horizontally")) |
||||
|
self.flip_x.setChecked(bool(self.config.get('qrreader_flip_x', True))) |
||||
|
self.flip_x.stateChanged.connect(self._on_flip_x_changed) |
||||
|
controls_layout.addWidget(self.flip_x) |
||||
|
|
||||
|
close_but = QPushButton(_("&Close")) |
||||
|
close_but.clicked.connect(self.reject) |
||||
|
controls_layout.addWidget(close_but) |
||||
|
|
||||
|
# Create the video surface and receive events when new frames arrive |
||||
|
self.video_surface = QrReaderVideoSurface(self) |
||||
|
self.video_surface.frame_available.connect(self._on_frame_available) |
||||
|
|
||||
|
# Create the crop blur effect |
||||
|
self.crop_blur_effect = QrReaderCropBlurEffect(self) |
||||
|
self.image_effect = ImageGraphicsEffect(self, self.crop_blur_effect) |
||||
|
|
||||
|
|
||||
|
# Note these should stay as queued connections becasue we use the idiom |
||||
|
# self.reject() and self.accept() in this class to kill the scan -- |
||||
|
# and we do it from within callback functions. If you don't use |
||||
|
# queued connections here, bad things can happen. |
||||
|
self.finished.connect(self._boilerplate_cleanup, Qt.QueuedConnection) |
||||
|
self.finished.connect(self._on_finished, Qt.QueuedConnection) |
||||
|
|
||||
|
def _on_flip_x_changed(self, _state: int): |
||||
|
self.config.set_key('qrreader_flip_x', self.flip_x.isChecked()) |
||||
|
|
||||
|
def _get_resolution(self, resolutions: List[QSize], min_size: int) -> QSize: |
||||
|
""" |
||||
|
Given a list of resolutions that the camera supports this function picks the |
||||
|
lowest resolution that is at least min_size in both width and height. |
||||
|
If no resolution is found, NoCameraResolutionsFound is raised. |
||||
|
""" |
||||
|
def res_list_to_str(res_list: List[QSize]) -> str: |
||||
|
return ', '.join(['{}x{}'.format(r.width(), r.height()) for r in res_list]) |
||||
|
|
||||
|
def check_res(res: QSize): |
||||
|
return res.width() >= min_size and res.height() >= min_size |
||||
|
|
||||
|
self.logger.info('searching for at least {0}x{0}'.format(min_size)) |
||||
|
|
||||
|
# Query and display all resolutions the camera supports |
||||
|
format_str = 'camera resolutions: {}' |
||||
|
self.logger.info(format_str.format(res_list_to_str(resolutions))) |
||||
|
|
||||
|
# Filter to those that are at least min_size in both width and height |
||||
|
candidate_resolutions = [] |
||||
|
ideal_resolutions = [r for r in resolutions if check_res(r)] |
||||
|
less_than_ideal_resolutions = [r for r in resolutions if r not in ideal_resolutions] |
||||
|
format_str = 'ideal resolutions: {}, less-than-ideal resolutions: {}' |
||||
|
self.logger.info(format_str.format(res_list_to_str(ideal_resolutions), res_list_to_str(less_than_ideal_resolutions))) |
||||
|
|
||||
|
# Raise an error if we have no usable resolutions |
||||
|
if not ideal_resolutions and not less_than_ideal_resolutions: |
||||
|
raise NoCameraResolutionsFound(_("Cannot start QR scanner, no usable camera resolution found.") + self._linux_pyqt5bug_msg()) |
||||
|
|
||||
|
if not ideal_resolutions: |
||||
|
self.logger.warning('No ideal resolutions found, falling back to less-than-ideal resolutions -- QR recognition may fail!') |
||||
|
candidate_resolutions = less_than_ideal_resolutions |
||||
|
is_ideal = False |
||||
|
else: |
||||
|
candidate_resolutions = ideal_resolutions |
||||
|
is_ideal = True |
||||
|
|
||||
|
|
||||
|
# Sort the usable resolutions, least number of pixels first, get the first element |
||||
|
resolution = sorted(candidate_resolutions, key=lambda r: r.width() * r.height(), reverse=not is_ideal)[0] |
||||
|
format_str = 'chosen resolution is {}x{}' |
||||
|
self.logger.info(format_str.format(resolution.width(), resolution.height())) |
||||
|
|
||||
|
return resolution, is_ideal |
||||
|
|
||||
|
@staticmethod |
||||
|
def _get_crop(resolution: QSize, scan_size: int) -> QRect: |
||||
|
""" |
||||
|
Returns a QRect that is scan_size x scan_size in the middle of the resolution |
||||
|
""" |
||||
|
scan_pos_x = (resolution.width() - scan_size) / 2 |
||||
|
scan_pos_y = (resolution.height() - scan_size) / 2 |
||||
|
return QRect(scan_pos_x, scan_pos_y, scan_size, scan_size) |
||||
|
|
||||
|
@staticmethod |
||||
|
def _linux_pyqt5bug_msg(): |
||||
|
''' Returns a string that may be appended to an exception error message |
||||
|
only if on Linux and PyQt5 < 5.12.2, otherwise returns an empty string. ''' |
||||
|
if (sys.platform == 'linux' and PYQT_VERSION < 0x050c02 # Check if PyQt5 < 5.12.2 on linux |
||||
|
# Also: this warning is not relevant to APPIMAGE; so make sure |
||||
|
# we are not running from APPIMAGE. |
||||
|
and not os.environ.get('APPIMAGE')): |
||||
|
# In this case it's possible we couldn't detect a camera because |
||||
|
# of that missing libQt5MultimediaGstTools.so problem. |
||||
|
return ("\n\n" + _('If you indeed do have a usable camera connected, then this error may be caused by bugs in previous PyQt5 versions on Linux. Try installing the latest PyQt5:') |
||||
|
+ "\n\n" + "python3 -m pip install --user -I pyqt5") |
||||
|
return '' |
||||
|
|
||||
|
def start_scan(self, device: str = ''): |
||||
|
""" |
||||
|
Scans a QR code from the given camera device. |
||||
|
If no QR code is found the returned string will be empty. |
||||
|
If the camera is not found or can't be opened NoCamerasFound will be raised. |
||||
|
""" |
||||
|
|
||||
|
self.validator = QrReaderValidatorCounted() |
||||
|
self.validator.strong_count = 5 # FIXME: make this time based rather than framect based |
||||
|
|
||||
|
device_info = None |
||||
|
|
||||
|
for camera in QCameraInfo.availableCameras(): |
||||
|
if camera.deviceName() == device: |
||||
|
device_info = camera |
||||
|
break |
||||
|
|
||||
|
if not device_info: |
||||
|
self.logger.info('Failed to open selected camera, trying to use default camera') |
||||
|
device_info = QCameraInfo.defaultCamera() |
||||
|
|
||||
|
if not device_info or device_info.isNull(): |
||||
|
raise NoCamerasFound(_("Cannot start QR scanner, no usable camera found.") + self._linux_pyqt5bug_msg()) |
||||
|
|
||||
|
self._init_stats() |
||||
|
self.qrreader_res = [] |
||||
|
self.validator_res = None |
||||
|
self._ok_done = False |
||||
|
self._error_message = None |
||||
|
|
||||
|
if self.camera: |
||||
|
self.logger.info("Warning: start_scan already called for this instance.") |
||||
|
|
||||
|
self.camera = QCamera(device_info) |
||||
|
self.camera.setViewfinder(self.video_surface) |
||||
|
self.camera.setCaptureMode(QCamera.CaptureViewfinder) |
||||
|
|
||||
|
# this operates on camera from within the signal handler, so should be a queued connection |
||||
|
self.camera_sc_conn = self.camera.statusChanged.connect(self._on_camera_status_changed, Qt.QueuedConnection) |
||||
|
self.camera.error.connect(self._on_camera_error) # log the errors we get, if any, for debugging |
||||
|
# Camera needs to be loaded to query resolutions, this tries to open the camera |
||||
|
self.camera.load() |
||||
|
|
||||
|
_camera_status_names = { |
||||
|
QCamera.UnavailableStatus: _('unavailable'), |
||||
|
QCamera.UnloadedStatus: _('unloaded'), |
||||
|
QCamera.UnloadingStatus: _('unloading'), |
||||
|
QCamera.LoadingStatus: _('loading'), |
||||
|
QCamera.LoadedStatus: _('loaded'), |
||||
|
QCamera.StandbyStatus: _('standby'), |
||||
|
QCamera.StartingStatus: _('starting'), |
||||
|
QCamera.StoppingStatus: _('stopping'), |
||||
|
QCamera.ActiveStatus: _('active') |
||||
|
} |
||||
|
|
||||
|
def _get_camera_status_name(self, status: QCamera.Status): |
||||
|
return self._camera_status_names.get(status, _('unknown')) |
||||
|
|
||||
|
def _set_resolution(self, resolution: QSize): |
||||
|
self.resolution = resolution |
||||
|
self.qr_crop = self._get_crop(resolution, self.SCAN_SIZE) |
||||
|
|
||||
|
# Initialize the video widget |
||||
|
#self.video_widget.setMinimumSize(resolution) # <-- on macOS this makes it fixed size for some reason. |
||||
|
self.resize(720, 540) |
||||
|
self.video_overlay.set_crop(self.qr_crop) |
||||
|
self.video_overlay.set_resolution(resolution) |
||||
|
self.video_layout.set_aspect_ratio(resolution.width() / resolution.height()) |
||||
|
|
||||
|
# Set up the crop blur effect |
||||
|
self.crop_blur_effect.setCrop(self.qr_crop) |
||||
|
|
||||
|
def _on_camera_status_changed(self, status: QCamera.Status): |
||||
|
if self._ok_done: |
||||
|
# camera/scan is quitting, abort. |
||||
|
return |
||||
|
|
||||
|
self.logger.info('camera status changed to {}'.format(self._get_camera_status_name(status))) |
||||
|
|
||||
|
if status == QCamera.LoadedStatus: |
||||
|
# Determine the optimal resolution and compute the crop rect |
||||
|
camera_resolutions = self.camera.supportedViewfinderResolutions() |
||||
|
try: |
||||
|
resolution, was_ideal = self._get_resolution(camera_resolutions, self.SCAN_SIZE) |
||||
|
except RuntimeError as e: |
||||
|
self._error_message = str(e) |
||||
|
self.reject() |
||||
|
return |
||||
|
self._set_resolution(resolution) |
||||
|
|
||||
|
# Set the camera resolution |
||||
|
viewfinder_settings = QCameraViewfinderSettings() |
||||
|
viewfinder_settings.setResolution(resolution) |
||||
|
self.camera.setViewfinderSettings(viewfinder_settings) |
||||
|
|
||||
|
# Counter for the QR scanner frame number |
||||
|
self.frame_id = 0 |
||||
|
|
||||
|
self.camera.start() |
||||
|
self.lowres_label.setVisible(not was_ideal) # if they have a low res camera, show the warning label. |
||||
|
elif status == QCamera.UnloadedStatus or status == QCamera.UnavailableStatus: |
||||
|
self._error_message = _("Cannot start QR scanner, camera is unavailable.") |
||||
|
self.reject() |
||||
|
elif status == QCamera.ActiveStatus: |
||||
|
self.open() |
||||
|
|
||||
|
CameraErrorStrings = { |
||||
|
QCamera.NoError : "No Error", |
||||
|
QCamera.CameraError : "Camera Error", |
||||
|
QCamera.InvalidRequestError : "Invalid Request Error", |
||||
|
QCamera.ServiceMissingError : "Service Missing Error", |
||||
|
QCamera.NotSupportedFeatureError : "Unsupported Feature Error" |
||||
|
} |
||||
|
def _on_camera_error(self, errorCode): |
||||
|
errStr = self.CameraErrorStrings.get(errorCode, "Unknown Error") |
||||
|
self.logger.info(f"QCamera error: {errStr}") |
||||
|
|
||||
|
def accept(self): |
||||
|
self._ok_done = True # immediately blocks further processing |
||||
|
super().accept() |
||||
|
|
||||
|
def reject(self): |
||||
|
self._ok_done = True # immediately blocks further processing |
||||
|
super().reject() |
||||
|
|
||||
|
def _boilerplate_cleanup(self): |
||||
|
self._close_camera() |
||||
|
if self.isVisible(): |
||||
|
self.close() |
||||
|
|
||||
|
def _close_camera(self): |
||||
|
if self.camera: |
||||
|
self.camera.setViewfinder(None) |
||||
|
if self.camera_sc_conn: |
||||
|
self.camera.statusChanged.disconnect(self.camera_sc_conn) |
||||
|
self.camera_sc_conn = None |
||||
|
self.camera.unload() |
||||
|
self.camera = None |
||||
|
|
||||
|
def _on_finished(self, code): |
||||
|
res = ( (code == QDialog.Accepted |
||||
|
and self.validator_res and self.validator_res.accepted |
||||
|
and self.validator_res.simple_result) |
||||
|
or '' ) |
||||
|
|
||||
|
self.validator = None |
||||
|
|
||||
|
self.logger.info(f'closed {res}') |
||||
|
|
||||
|
self.qr_finished.emit(code == QDialog.Accepted, self._error_message, res) |
||||
|
|
||||
|
def _on_frame_available(self, frame: QImage): |
||||
|
if self._ok_done: |
||||
|
return |
||||
|
|
||||
|
self.frame_id += 1 |
||||
|
|
||||
|
if frame.size() != self.resolution: |
||||
|
self.logger.info('Getting video data at {}x{} instead of the requested {}x{}, switching resolution.'.format( |
||||
|
frame.size().width(), frame.size().height(), |
||||
|
self.resolution.width(), self.resolution.height() |
||||
|
)) |
||||
|
self._set_resolution(frame.size()) |
||||
|
|
||||
|
flip_x = self.flip_x.isChecked() |
||||
|
|
||||
|
# Only QR scan every QR_SCAN_PERIOD secs |
||||
|
qr_scanned = time.time() - self.last_qr_scan_ts >= self.qrreader.interval() |
||||
|
if qr_scanned: |
||||
|
self.last_qr_scan_ts = time.time() |
||||
|
# Crop the frame so we only scan a SCAN_SIZE rect |
||||
|
frame_cropped = frame.copy(self.qr_crop) |
||||
|
|
||||
|
# 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) |
||||
|
|
||||
|
# 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 |
||||
|
) |
||||
|
|
||||
|
# Call the validator to see if the scanned results are acceptable |
||||
|
self.validator_res = self.validator.validate_results(self.qrreader_res) |
||||
|
|
||||
|
# Update the video overlay with the results |
||||
|
self.video_overlay.set_results(self.qrreader_res, flip_x, self.validator_res) |
||||
|
|
||||
|
# Close the dialog if the validator accepted the result |
||||
|
if self.validator_res.accepted: |
||||
|
self.accept() |
||||
|
return |
||||
|
|
||||
|
# Apply the crop blur effect |
||||
|
if self.image_effect: |
||||
|
frame = self.image_effect.apply(frame) |
||||
|
|
||||
|
# If horizontal flipping is enabled, only flip the display |
||||
|
if flip_x: |
||||
|
frame = frame.mirrored(True, False) |
||||
|
|
||||
|
# Display the frame in the widget |
||||
|
self.video_widget.setPixmap(QPixmap.fromImage(frame)) |
||||
|
|
||||
|
self._update_stats(qr_scanned) |
||||
|
|
||||
|
def _init_stats(self): |
||||
|
self.last_stats_time = time.perf_counter() |
||||
|
self.frame_counter = 0 |
||||
|
self.qr_frame_counter = 0 |
||||
|
|
||||
|
def _update_stats(self, qr_scanned): |
||||
|
self.frame_counter += 1 |
||||
|
if qr_scanned: |
||||
|
self.qr_frame_counter += 1 |
||||
|
now = time.perf_counter() |
||||
|
last_stats_delta = now - self.last_stats_time |
||||
|
if last_stats_delta > 1.0: # stats every 1.0 seconds |
||||
|
fps = self.frame_counter / last_stats_delta |
||||
|
qr_fps = self.qr_frame_counter / last_stats_delta |
||||
|
if self.validator is not None: |
||||
|
self.validator.strong_count = math.ceil(qr_fps / 3) # 1/3 of a second's worth of qr frames determines strong_count |
||||
|
stats_format = 'running at {} FPS, scanner at {} FPS' |
||||
|
self.logger.info(stats_format.format(fps, qr_fps)) |
||||
|
self.frame_counter = 0 |
||||
|
self.qr_frame_counter = 0 |
||||
|
self.last_stats_time = now |
@ -0,0 +1,77 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
|
||||
|
from PyQt5.QtWidgets import QGraphicsBlurEffect, QGraphicsEffect |
||||
|
from PyQt5.QtGui import QPainter, QTransform, QRegion |
||||
|
from PyQt5.QtCore import QObject, QRect, QPoint, Qt |
||||
|
|
||||
|
|
||||
|
class QrReaderCropBlurEffect(QGraphicsBlurEffect): |
||||
|
CROP_OFFSET_ENABLED = False |
||||
|
CROP_OFFSET = QPoint(5, 5) |
||||
|
|
||||
|
BLUR_DARKEN = 0.25 |
||||
|
BLUR_RADIUS = 8 |
||||
|
|
||||
|
def __init__(self, parent: QObject, crop: QRect = None): |
||||
|
super().__init__(parent) |
||||
|
self.crop = crop |
||||
|
self.setBlurRadius(self.BLUR_RADIUS) |
||||
|
|
||||
|
def setCrop(self, crop: QRect = None): |
||||
|
self.crop = crop |
||||
|
|
||||
|
def draw(self, painter: QPainter): |
||||
|
assert self.crop, 'crop must be set' |
||||
|
|
||||
|
# Compute painter regions for the crop and the blur |
||||
|
all_region = QRegion(painter.viewport()) |
||||
|
crop_region = QRegion(self.crop) |
||||
|
blur_region = all_region.subtracted(crop_region) |
||||
|
|
||||
|
# Let the QGraphicsBlurEffect only paint in blur_region |
||||
|
painter.setClipRegion(blur_region) |
||||
|
|
||||
|
# Fill with black and set opacity so that the blurred region is drawn darker |
||||
|
if self.BLUR_DARKEN > 0.0: |
||||
|
painter.fillRect(painter.viewport(), Qt.black) |
||||
|
painter.setOpacity(1 - self.BLUR_DARKEN) |
||||
|
|
||||
|
# Draw the blur effect |
||||
|
super().draw(painter) |
||||
|
|
||||
|
# Restore clipping and opacity |
||||
|
painter.setClipping(False) |
||||
|
painter.setOpacity(1.0) |
||||
|
|
||||
|
# Get the source pixmap |
||||
|
pixmap, offset = self.sourcePixmap(Qt.DeviceCoordinates, QGraphicsEffect.NoPad) |
||||
|
painter.setWorldTransform(QTransform()) |
||||
|
|
||||
|
# Get the source by adding the offset to the crop location |
||||
|
source = self.crop |
||||
|
if self.CROP_OFFSET_ENABLED: |
||||
|
source = source.translated(self.CROP_OFFSET) |
||||
|
painter.drawPixmap(self.crop.topLeft() + offset, pixmap, source) |
@ -0,0 +1,166 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
|
||||
|
from typing import List, Dict, Callable, Any |
||||
|
from abc import ABC, abstractmethod |
||||
|
|
||||
|
from PyQt5.QtGui import QColor |
||||
|
from PyQt5.QtCore import Qt |
||||
|
|
||||
|
from electrum.i18n import _ |
||||
|
from electrum.qrreader import QrCodeResult |
||||
|
|
||||
|
from electrum.gui.qt.util import ColorScheme, QColorLerp |
||||
|
|
||||
|
|
||||
|
class QrReaderValidatorResult(): |
||||
|
""" |
||||
|
Result of a QR code validator |
||||
|
""" |
||||
|
|
||||
|
def __init__(self): |
||||
|
self.accepted: bool = False |
||||
|
|
||||
|
self.message: str = None |
||||
|
self.message_color: QColor = None |
||||
|
|
||||
|
self.simple_result : str = None |
||||
|
|
||||
|
self.result_usable: Dict[QrCodeResult, bool] = {} |
||||
|
self.result_colors: Dict[QrCodeResult, QColor] = {} |
||||
|
self.result_messages: Dict[QrCodeResult, str] = {} |
||||
|
|
||||
|
self.selected_results: List[QrCodeResult] = [] |
||||
|
|
||||
|
|
||||
|
class AbstractQrReaderValidator(ABC): |
||||
|
""" |
||||
|
Abstract base class for QR code result validators. |
||||
|
""" |
||||
|
|
||||
|
@abstractmethod |
||||
|
def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult: |
||||
|
""" |
||||
|
Checks a list of QR code results for usable codes. |
||||
|
""" |
||||
|
|
||||
|
class QrReaderValidatorCounting(AbstractQrReaderValidator): |
||||
|
""" |
||||
|
This QR code result validator doesn't directly accept any results but maintains a dictionary |
||||
|
of detection counts in `result_counts`. |
||||
|
""" |
||||
|
|
||||
|
result_counts: Dict[QrCodeResult, int] = {} |
||||
|
|
||||
|
def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult: |
||||
|
res = QrReaderValidatorResult() |
||||
|
|
||||
|
for result in results: |
||||
|
# Increment the detection count |
||||
|
if not result in self.result_counts: |
||||
|
self.result_counts[result] = 0 |
||||
|
self.result_counts[result] += 1 |
||||
|
|
||||
|
# Search for missing results, iterate over a copy because the loop might modify the dict |
||||
|
for result in self.result_counts.copy(): |
||||
|
# Count down missing results |
||||
|
if result in results: |
||||
|
continue |
||||
|
self.result_counts[result] -= 2 |
||||
|
# When the count goes to zero, remove |
||||
|
if self.result_counts[result] < 1: |
||||
|
del self.result_counts[result] |
||||
|
|
||||
|
return res |
||||
|
|
||||
|
class QrReaderValidatorColorizing(QrReaderValidatorCounting): |
||||
|
""" |
||||
|
This QR code result validator doesn't directly accept any results but colorizes the results |
||||
|
based on the counts maintained by `QrReaderValidatorCounting`. |
||||
|
""" |
||||
|
|
||||
|
WEAK_COLOR: QColor = QColor(Qt.red) |
||||
|
STRONG_COLOR: QColor = QColor(Qt.green) |
||||
|
|
||||
|
strong_count: int = 10 |
||||
|
|
||||
|
def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult: |
||||
|
res = super().validate_results(results) |
||||
|
|
||||
|
# Colorize the QR code results by their detection counts |
||||
|
for result in results: |
||||
|
# Enforce strong_count as upper limit |
||||
|
self.result_counts[result] = min(self.result_counts[result], self.strong_count) |
||||
|
|
||||
|
# Interpolate between WEAK_COLOR and STRONG_COLOR based on count / strong_count |
||||
|
lerp_factor = (self.result_counts[result] - 1) / self.strong_count |
||||
|
lerped_color = QColorLerp(self.WEAK_COLOR, self.STRONG_COLOR, lerp_factor) |
||||
|
res.result_colors[result] = lerped_color |
||||
|
|
||||
|
return res |
||||
|
|
||||
|
class QrReaderValidatorStrong(QrReaderValidatorColorizing): |
||||
|
""" |
||||
|
This QR code result validator doesn't directly accept any results but passes every strong |
||||
|
detection in the return values `selected_results`. |
||||
|
""" |
||||
|
|
||||
|
def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult: |
||||
|
res = super().validate_results(results) |
||||
|
|
||||
|
for result in results: |
||||
|
if self.result_counts[result] >= self.strong_count: |
||||
|
res.selected_results.append(result) |
||||
|
break |
||||
|
|
||||
|
return res |
||||
|
|
||||
|
class QrReaderValidatorCounted(QrReaderValidatorStrong): |
||||
|
""" |
||||
|
This QR code result validator accepts a result as soon as there is at least `minimum` and at |
||||
|
most `maximum` QR code(s) with strong detection. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, minimum: int = 1, maximum: int = 1): |
||||
|
super().__init__() |
||||
|
self.minimum = minimum |
||||
|
self.maximum = maximum |
||||
|
|
||||
|
def validate_results(self, results: List[QrCodeResult]) -> QrReaderValidatorResult: |
||||
|
res = super().validate_results(results) |
||||
|
|
||||
|
num_results = len(res.selected_results) |
||||
|
if num_results < self.minimum: |
||||
|
if num_results > 0: |
||||
|
res.message = _('Too few QR codes detected.') |
||||
|
res.message_color = ColorScheme.RED.as_color() |
||||
|
elif num_results > self.maximum: |
||||
|
res.message = _('Too many QR codes detected.') |
||||
|
res.message_color = ColorScheme.RED.as_color() |
||||
|
else: |
||||
|
res.accepted = True |
||||
|
res.simple_result = (results and results[0].data) or '' # hack added by calin just to take the first one |
||||
|
|
||||
|
return res |
@ -0,0 +1,157 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
|
||||
|
from typing import List |
||||
|
|
||||
|
from PyQt5.QtWidgets import QWidget |
||||
|
from PyQt5.QtGui import QPainter, QPaintEvent, QPen, QPainterPath, QColor, QTransform |
||||
|
from PyQt5.QtCore import QPoint, QSize, QRect, QRectF, Qt |
||||
|
|
||||
|
from electrum.qrreader import QrCodeResult |
||||
|
|
||||
|
from .validator import QrReaderValidatorResult |
||||
|
|
||||
|
|
||||
|
class QrReaderVideoOverlay(QWidget): |
||||
|
""" |
||||
|
Overlays the QR scanner results over the video |
||||
|
""" |
||||
|
|
||||
|
BG_RECT_PADDING = 10 |
||||
|
BG_RECT_CORNER_RADIUS = 10.0 |
||||
|
BG_RECT_OPACITY = 0.75 |
||||
|
|
||||
|
def __init__(self, parent: QWidget = None): |
||||
|
super().__init__(parent) |
||||
|
|
||||
|
self.results = [] |
||||
|
self.flip_x = False |
||||
|
self.validator_results = None |
||||
|
self.crop = None |
||||
|
self.resolution = None |
||||
|
|
||||
|
self.qr_outline_pen = QPen() |
||||
|
self.qr_outline_pen.setColor(Qt.red) |
||||
|
self.qr_outline_pen.setWidth(3) |
||||
|
self.qr_outline_pen.setStyle(Qt.DotLine) |
||||
|
|
||||
|
self.text_pen = QPen() |
||||
|
self.text_pen.setColor(Qt.black) |
||||
|
|
||||
|
self.bg_rect_pen = QPen() |
||||
|
self.bg_rect_pen.setColor(Qt.black) |
||||
|
self.bg_rect_pen.setStyle(Qt.DotLine) |
||||
|
self.bg_rect_fill = QColor(255, 255, 255, 255 * self.BG_RECT_OPACITY) |
||||
|
|
||||
|
def set_results(self, results: List[QrCodeResult], flip_x: bool, |
||||
|
validator_results: QrReaderValidatorResult): |
||||
|
self.results = results |
||||
|
self.flip_x = flip_x |
||||
|
self.validator_results = validator_results |
||||
|
self.update() |
||||
|
|
||||
|
def set_crop(self, crop: QRect): |
||||
|
self.crop = crop |
||||
|
|
||||
|
def set_resolution(self, resolution: QSize): |
||||
|
self.resolution = resolution |
||||
|
|
||||
|
def paintEvent(self, _event: QPaintEvent): |
||||
|
if not self.crop or not self.resolution: |
||||
|
return |
||||
|
|
||||
|
painter = QPainter(self) |
||||
|
|
||||
|
# Keep a backup of the transform and create a new one |
||||
|
transform = painter.worldTransform() |
||||
|
|
||||
|
# Set scaling transform |
||||
|
transform = transform.scale(self.width() / self.resolution.width(), |
||||
|
self.height() / self.resolution.height()) |
||||
|
|
||||
|
# Compute the transform to flip the coordinate system on the x axis |
||||
|
transform_flip = QTransform() |
||||
|
if self.flip_x: |
||||
|
transform_flip = transform_flip.translate(self.resolution.width(), 0.0) |
||||
|
transform_flip = transform_flip.scale(-1.0, 1.0) |
||||
|
|
||||
|
# Small helper for tuple to QPoint |
||||
|
def toqp(point): |
||||
|
return QPoint(point[0], point[1]) |
||||
|
|
||||
|
# Starting from here we care about AA |
||||
|
painter.setRenderHint(QPainter.Antialiasing) |
||||
|
|
||||
|
# Draw all the QR code results |
||||
|
for res in self.results: |
||||
|
painter.setWorldTransform(transform_flip * transform, False) |
||||
|
|
||||
|
# Draw lines between all of the QR code points |
||||
|
pen = QPen(self.qr_outline_pen) |
||||
|
if res in self.validator_results.result_colors: |
||||
|
pen.setColor(self.validator_results.result_colors[res]) |
||||
|
painter.setPen(pen) |
||||
|
num_points = len(res.points) |
||||
|
for i in range(0, num_points): |
||||
|
i_n = i + 1 |
||||
|
|
||||
|
line_from = toqp(res.points[i]) |
||||
|
line_from += self.crop.topLeft() |
||||
|
|
||||
|
line_to = toqp(res.points[i_n] if i_n < num_points else res.points[0]) |
||||
|
line_to += self.crop.topLeft() |
||||
|
|
||||
|
painter.drawLine(line_from, line_to) |
||||
|
|
||||
|
# Draw the QR code data |
||||
|
# Note that we reset the world transform to only the scaled transform |
||||
|
# because otherwise the text could be flipped. We only use transform_flip |
||||
|
# to map the center point of the result. |
||||
|
painter.setWorldTransform(transform, False) |
||||
|
font_metrics = painter.fontMetrics() |
||||
|
data_metrics = QSize(font_metrics.horizontalAdvance(res.data), font_metrics.capHeight()) |
||||
|
|
||||
|
center_pos = toqp(res.center) |
||||
|
center_pos += self.crop.topLeft() |
||||
|
center_pos = transform_flip.map(center_pos) |
||||
|
|
||||
|
text_offset = QPoint(data_metrics.width(), data_metrics.height()) |
||||
|
text_offset = text_offset / 2 |
||||
|
text_offset.setX(-text_offset.x()) |
||||
|
center_pos += text_offset |
||||
|
|
||||
|
padding = self.BG_RECT_PADDING |
||||
|
bg_rect_pos = center_pos - QPoint(padding, data_metrics.height() + padding) |
||||
|
bg_rect_size = data_metrics + (QSize(padding, padding) * 2) |
||||
|
bg_rect = QRect(bg_rect_pos, bg_rect_size) |
||||
|
bg_rect_path = QPainterPath() |
||||
|
radius = self.BG_RECT_CORNER_RADIUS |
||||
|
bg_rect_path.addRoundedRect(QRectF(bg_rect), radius, radius, Qt.AbsoluteSize) |
||||
|
painter.setPen(self.bg_rect_pen) |
||||
|
painter.fillPath(bg_rect_path, self.bg_rect_fill) |
||||
|
painter.drawPath(bg_rect_path) |
||||
|
|
||||
|
painter.setPen(self.text_pen) |
||||
|
painter.drawText(center_pos, res.data) |
@ -0,0 +1,91 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
|
||||
|
from typing import List |
||||
|
|
||||
|
from PyQt5.QtMultimedia import (QVideoFrame, QAbstractVideoBuffer, QAbstractVideoSurface, |
||||
|
QVideoSurfaceFormat) |
||||
|
from PyQt5.QtGui import QImage |
||||
|
from PyQt5.QtCore import QObject, pyqtSignal |
||||
|
|
||||
|
from electrum.i18n import _ |
||||
|
from electrum.logging import get_logger |
||||
|
|
||||
|
|
||||
|
_logger = get_logger(__name__) |
||||
|
|
||||
|
|
||||
|
class QrReaderVideoSurface(QAbstractVideoSurface): |
||||
|
""" |
||||
|
Receives QVideoFrames from QCamera, converts them into a QImage, flips the X and Y axis if |
||||
|
necessary and sends them to listeners via the frame_available event. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, parent: QObject = None): |
||||
|
super().__init__(parent) |
||||
|
|
||||
|
def present(self, frame: QVideoFrame) -> bool: |
||||
|
if not frame.isValid(): |
||||
|
return False |
||||
|
|
||||
|
image_format = QVideoFrame.imageFormatFromPixelFormat(frame.pixelFormat()) |
||||
|
if image_format == QVideoFrame.Format_Invalid: |
||||
|
_logger.info(_('QR code scanner for video frame with invalid pixel format')) |
||||
|
return False |
||||
|
|
||||
|
if not frame.map(QAbstractVideoBuffer.ReadOnly): |
||||
|
_logger.info(_('QR code scanner failed to map video frame')) |
||||
|
return False |
||||
|
|
||||
|
try: |
||||
|
img = QImage(frame.bits(), frame.width(), frame.height(), image_format) |
||||
|
|
||||
|
# Check whether we need to flip the image on any axis |
||||
|
surface_format = self.surfaceFormat() |
||||
|
flip_x = surface_format.isMirrored() |
||||
|
flip_y = surface_format.scanLineDirection() == QVideoSurfaceFormat.BottomToTop |
||||
|
|
||||
|
# Mirror the image if needed |
||||
|
if flip_x or flip_y: |
||||
|
img = img.mirrored(flip_x, flip_y) |
||||
|
|
||||
|
# Create a copy of the image so the original frame data can be freed |
||||
|
img = img.copy() |
||||
|
finally: |
||||
|
frame.unmap() |
||||
|
|
||||
|
self.frame_available.emit(img) |
||||
|
|
||||
|
return True |
||||
|
|
||||
|
def supportedPixelFormats(self, handle_type: QAbstractVideoBuffer.HandleType) -> List[QVideoFrame.PixelFormat]: |
||||
|
if handle_type == QAbstractVideoBuffer.NoHandle: |
||||
|
# We support all pixel formats that can be understood by QImage directly |
||||
|
return [QVideoFrame.Format_ARGB32, QVideoFrame.Format_ARGB32_Premultiplied, |
||||
|
QVideoFrame.Format_RGB32, QVideoFrame.Format_RGB24, QVideoFrame.Format_RGB565, |
||||
|
QVideoFrame.Format_RGB555, QVideoFrame.Format_ARGB8565_Premultiplied] |
||||
|
return [] |
||||
|
|
||||
|
frame_available = pyqtSignal(QImage) |
@ -0,0 +1,52 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
|
||||
|
from PyQt5.QtWidgets import QWidget |
||||
|
from PyQt5.QtGui import QPixmap, QPainter, QPaintEvent |
||||
|
|
||||
|
|
||||
|
class QrReaderVideoWidget(QWidget): |
||||
|
""" |
||||
|
Simple widget for drawing a pixmap |
||||
|
""" |
||||
|
|
||||
|
USE_BILINEAR_FILTER = True |
||||
|
|
||||
|
def __init__(self, parent: QWidget = None): |
||||
|
super().__init__(parent) |
||||
|
|
||||
|
self.pixmap = None |
||||
|
|
||||
|
def paintEvent(self, _event: QPaintEvent): |
||||
|
if not self.pixmap: |
||||
|
return |
||||
|
painter = QPainter(self) |
||||
|
if self.USE_BILINEAR_FILTER: |
||||
|
painter.setRenderHint(QPainter.SmoothPixmapTransform) |
||||
|
painter.drawPixmap(self.rect(), self.pixmap, self.pixmap.rect()) |
||||
|
|
||||
|
def setPixmap(self, pixmap: QPixmap): |
||||
|
self.pixmap = pixmap |
||||
|
self.update() |
@ -0,0 +1,63 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
# |
||||
|
# A module, that, given an image (buffer), finds and decodes a QR code in it. |
||||
|
|
||||
|
from typing import Optional |
||||
|
|
||||
|
from ..logging import get_logger |
||||
|
|
||||
|
from .abstract_base import AbstractQrCodeReader, QrCodeResult |
||||
|
|
||||
|
|
||||
|
_logger = get_logger(__name__) |
||||
|
|
||||
|
|
||||
|
class MissingLib(RuntimeError): |
||||
|
''' Raised by underlying implementation if missing libs ''' |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
def get_qr_reader() -> Optional[AbstractQrCodeReader]: |
||||
|
""" |
||||
|
Get the Qr code reader for the current platform |
||||
|
""" |
||||
|
try: |
||||
|
from .zbar import ZbarQrCodeReader |
||||
|
return ZbarQrCodeReader() |
||||
|
""" |
||||
|
# DEBUG CODE BELOW |
||||
|
# If you want to test this code on a platform that doesn't yet work or have |
||||
|
# zbar, use the below... |
||||
|
class Fake(AbstractQrCodeReader): |
||||
|
def read_qr_code(self, buffer, buffer_size, dummy, width, height, frame_id = -1): |
||||
|
''' fake noop to test ''' |
||||
|
return [] |
||||
|
return Fake() |
||||
|
""" |
||||
|
except MissingLib as e: |
||||
|
_logger.exception("") |
||||
|
|
||||
|
return None |
@ -0,0 +1,77 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
|
||||
|
import ctypes |
||||
|
from typing import List, Tuple |
||||
|
from abc import ABC, abstractmethod |
||||
|
|
||||
|
QrCodePoint = Tuple[int, int] |
||||
|
QrCodePointList = List[QrCodePoint] |
||||
|
|
||||
|
|
||||
|
class QrCodeResult(): |
||||
|
""" |
||||
|
A detected QR code. |
||||
|
""" |
||||
|
def __init__(self, data: str, center: QrCodePoint, points: QrCodePointList): |
||||
|
self.data: str = data |
||||
|
self.center: QrCodePoint = center |
||||
|
self.points: QrCodePointList = points |
||||
|
|
||||
|
def __str__(self) -> str: |
||||
|
return 'data: {} center: {} points: {}'.format(self.data, self.center, self.points) |
||||
|
|
||||
|
def __hash__(self): |
||||
|
return hash(self.data) |
||||
|
|
||||
|
def __eq__(self, other): |
||||
|
return self.data == other.data |
||||
|
|
||||
|
def __ne__(self, other): |
||||
|
return not self == other |
||||
|
|
||||
|
class AbstractQrCodeReader(ABC): |
||||
|
""" |
||||
|
Abstract base class for QR code readers. |
||||
|
""" |
||||
|
|
||||
|
def interval(self) -> float: |
||||
|
''' Reimplement to specify a time (in seconds) that the implementation |
||||
|
recommends elapse between subsequent calls to read_qr_code. |
||||
|
Implementations that have very expensive and/or slow detection code |
||||
|
may want to rate-limit read_qr_code calls by overriding this function. |
||||
|
e.g.: to make detection happen every 200ms, you would return 0.2 here. |
||||
|
Defaults to 0.0''' |
||||
|
return 0.0 |
||||
|
|
||||
|
@abstractmethod |
||||
|
def read_qr_code(self, buffer: ctypes.c_void_p, |
||||
|
buffer_size: int, # overall image size in bytes |
||||
|
rowlen_bytes: int, # the scan line length in bytes. (many libs, such as OSX, expect this value to properly grok image data) |
||||
|
width: int, height: int, frame_id: int = -1) -> List[QrCodeResult]: |
||||
|
""" |
||||
|
Reads a QR code from an image buffer in Y800 / GREY format. |
||||
|
Returns a list of detected QR codes which includes their data and positions. |
||||
|
""" |
@ -0,0 +1,183 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
# |
||||
|
# Electron Cash - lightweight Bitcoin client |
||||
|
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> |
||||
|
# |
||||
|
# Permission is hereby granted, free of charge, to any person |
||||
|
# obtaining a copy of this software and associated documentation files |
||||
|
# (the "Software"), to deal in the Software without restriction, |
||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||
|
# subject to the following conditions: |
||||
|
# |
||||
|
# The above copyright notice and this permission notice shall be |
||||
|
# included in all copies or substantial portions of the Software. |
||||
|
# |
||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
# SOFTWARE. |
||||
|
|
||||
|
import sys |
||||
|
import ctypes |
||||
|
import os |
||||
|
from typing import List |
||||
|
from enum import IntEnum |
||||
|
|
||||
|
from ..logging import get_logger |
||||
|
|
||||
|
from . import MissingLib |
||||
|
from .abstract_base import AbstractQrCodeReader, QrCodeResult |
||||
|
|
||||
|
|
||||
|
_logger = get_logger(__name__) |
||||
|
|
||||
|
|
||||
|
if sys.platform == 'darwin': |
||||
|
LIBNAME = 'libzbar.0.dylib' |
||||
|
elif sys.platform in ('windows', 'win32'): |
||||
|
LIBNAME = 'libzbar-0.dll' |
||||
|
else: |
||||
|
LIBNAME = 'libzbar.so.0' |
||||
|
|
||||
|
try: |
||||
|
try: |
||||
|
LIBZBAR = ctypes.cdll.LoadLibrary(os.path.join(os.path.dirname(__file__), '..', LIBNAME)) |
||||
|
except OSError as e: |
||||
|
LIBZBAR = ctypes.cdll.LoadLibrary(LIBNAME) |
||||
|
|
||||
|
LIBZBAR.zbar_image_create.restype = ctypes.c_void_p |
||||
|
LIBZBAR.zbar_image_scanner_create.restype = ctypes.c_void_p |
||||
|
LIBZBAR.zbar_image_scanner_get_results.restype = ctypes.c_void_p |
||||
|
LIBZBAR.zbar_symbol_set_first_symbol.restype = ctypes.c_void_p |
||||
|
LIBZBAR.zbar_symbol_get_data.restype = ctypes.POINTER(ctypes.c_char_p) |
||||
|
LIBZBAR.zbar_image_scanner_set_config.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int] |
||||
|
LIBZBAR.zbar_image_set_sequence.argtypes = [ctypes.c_void_p, ctypes.c_int] |
||||
|
LIBZBAR.zbar_image_set_size.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int] |
||||
|
LIBZBAR.zbar_image_set_format.argtypes = [ctypes.c_void_p, ctypes.c_int] |
||||
|
LIBZBAR.zbar_image_set_data.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_image_scanner_recycle_image.argtypes = [ctypes.c_void_p, ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_scan_image.argtypes = [ctypes.c_void_p, ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_image_scanner_get_results.argtypes = [ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_symbol_set_first_symbol.argtypes = [ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_symbol_get_data_length.argtypes = [ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_symbol_get_data.argtypes = [ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_symbol_get_loc_size.argtypes = [ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_symbol_get_loc_x.argtypes = [ctypes.c_void_p, ctypes.c_int] |
||||
|
LIBZBAR.zbar_symbol_get_loc_y.argtypes = [ctypes.c_void_p, ctypes.c_int] |
||||
|
LIBZBAR.zbar_symbol_next.argtypes = [ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_image_scanner_destroy.argtypes = [ctypes.c_void_p] |
||||
|
LIBZBAR.zbar_image_destroy.argtypes = [ctypes.c_void_p] |
||||
|
|
||||
|
#if is_verbose: |
||||
|
#LIBZBAR.zbar_set_verbosity(100) |
||||
|
except OSError: |
||||
|
_logger.exception("Failed to load zbar") |
||||
|
LIBZBAR = None |
||||
|
|
||||
|
FOURCC_Y800 = 0x30303859 |
||||
|
|
||||
|
@ctypes.CFUNCTYPE(None, ctypes.c_void_p) |
||||
|
def zbar_cleanup(image): |
||||
|
""" |
||||
|
Do nothing, this is just so zbar doesn't try to manage our QImage buffers |
||||
|
""" |
||||
|
|
||||
|
class ZbarSymbolType(IntEnum): |
||||
|
""" |
||||
|
Supported symbol types, see zbar_symbol_type_e in zbar.h |
||||
|
""" |
||||
|
EAN2 = 2 |
||||
|
EAN5 = 5 |
||||
|
EAN8 = 8 |
||||
|
UPCE = 9 |
||||
|
ISBN10 = 10 |
||||
|
UPCA = 12 |
||||
|
EAN13 = 13 |
||||
|
ISBN13 = 14 |
||||
|
COMPOSITE = 15 |
||||
|
I25 = 25 |
||||
|
DATABAR = 34 |
||||
|
DATABAR_EXP = 35 |
||||
|
CODABAR = 38 |
||||
|
CODE39 = 39 |
||||
|
PDF417 = 57 |
||||
|
QRCODE = 64 |
||||
|
SQCODE = 80 |
||||
|
CODE93 = 93 |
||||
|
CODE128 = 128 |
||||
|
|
||||
|
class ZbarConfig(IntEnum): |
||||
|
""" |
||||
|
Supported configuration options, see zbar_config_e in zbar.h |
||||
|
""" |
||||
|
ENABLE = 0 |
||||
|
|
||||
|
class ZbarQrCodeReader(AbstractQrCodeReader): |
||||
|
""" |
||||
|
Reader that uses libzbar |
||||
|
""" |
||||
|
|
||||
|
def __init__(self): |
||||
|
if not LIBZBAR: |
||||
|
raise MissingLib('Zbar library not found') |
||||
|
# Set up zbar |
||||
|
self.zbar_scanner = LIBZBAR.zbar_image_scanner_create() |
||||
|
self.zbar_image = LIBZBAR.zbar_image_create() |
||||
|
|
||||
|
# Disable all symbols |
||||
|
for sym_type in ZbarSymbolType: |
||||
|
LIBZBAR.zbar_image_scanner_set_config(self.zbar_scanner, sym_type, ZbarConfig.ENABLE, 0) |
||||
|
|
||||
|
# Enable only QR codes |
||||
|
LIBZBAR.zbar_image_scanner_set_config(self.zbar_scanner, ZbarSymbolType.QRCODE, |
||||
|
ZbarConfig.ENABLE, 1) |
||||
|
|
||||
|
def __del__(self): |
||||
|
if LIBZBAR: |
||||
|
LIBZBAR.zbar_image_scanner_destroy(self.zbar_scanner) |
||||
|
LIBZBAR.zbar_image_destroy(self.zbar_image) |
||||
|
|
||||
|
def read_qr_code(self, buffer: ctypes.c_void_p, buffer_size: int, |
||||
|
rowlen_bytes: int, # this param is ignored in this implementation |
||||
|
width: int, height: int, frame_id: int = -1) -> List[QrCodeResult]: |
||||
|
LIBZBAR.zbar_image_set_sequence(self.zbar_image, frame_id) |
||||
|
LIBZBAR.zbar_image_set_size(self.zbar_image, width, height) |
||||
|
LIBZBAR.zbar_image_set_format(self.zbar_image, FOURCC_Y800) |
||||
|
LIBZBAR.zbar_image_set_data(self.zbar_image, buffer, buffer_size, zbar_cleanup) |
||||
|
LIBZBAR.zbar_image_scanner_recycle_image(self.zbar_scanner, self.zbar_image) |
||||
|
LIBZBAR.zbar_scan_image(self.zbar_scanner, self.zbar_image) |
||||
|
|
||||
|
result_set = LIBZBAR.zbar_image_scanner_get_results(self.zbar_scanner) |
||||
|
|
||||
|
res = [] |
||||
|
symbol = LIBZBAR.zbar_symbol_set_first_symbol(result_set) |
||||
|
while symbol: |
||||
|
symbol_data_len = LIBZBAR.zbar_symbol_get_data_length(symbol) |
||||
|
symbol_data_ptr = LIBZBAR.zbar_symbol_get_data(symbol) |
||||
|
symbol_data_bytes = ctypes.string_at(symbol_data_ptr, symbol_data_len) |
||||
|
symbol_data = symbol_data_bytes.decode('utf-8') |
||||
|
|
||||
|
symbol_loc = [] |
||||
|
symbol_loc_len = LIBZBAR.zbar_symbol_get_loc_size(symbol) |
||||
|
for i in range(0, symbol_loc_len): |
||||
|
# Normalize the coordinates into 0..1 range by dividing by width / height |
||||
|
symbol_loc_x = LIBZBAR.zbar_symbol_get_loc_x(symbol, i) |
||||
|
symbol_loc_y = LIBZBAR.zbar_symbol_get_loc_y(symbol, i) |
||||
|
symbol_loc.append((symbol_loc_x, symbol_loc_y)) |
||||
|
|
||||
|
# Find the center by getting the average values of the corners x and y coordinates |
||||
|
symbol_loc_sum_x = sum([l[0] for l in symbol_loc]) |
||||
|
symbol_loc_sum_y = sum([l[1] for l in symbol_loc]) |
||||
|
symbol_loc_center = (int(symbol_loc_sum_x / symbol_loc_len), int(symbol_loc_sum_y / symbol_loc_len)) |
||||
|
|
||||
|
res.append(QrCodeResult(symbol_data, symbol_loc_center, symbol_loc)) |
||||
|
|
||||
|
symbol = LIBZBAR.zbar_symbol_next(symbol) |
||||
|
|
||||
|
return res |
Loading…
Reference in new issue