Merge pull request 'Webcam QrCode Scanner' (#367) from tobtoht/feather:qrscanner into master

Reviewed-on: https://git.featherwallet.org/feather/feather/pulls/367
This commit is contained in:
tobtoht
2021-06-27 00:45:13 +00:00
47 changed files with 1149 additions and 2927 deletions
+56 -10
View File
@@ -5,11 +5,24 @@ set(CMAKE_AUTOUIC ON)
# pthread
find_package(Threads REQUIRED)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Network Svg Xml WebSockets)
set(QT5_COMPONENTS
Core
Widgets
Gui
Network
Svg
Xml
WebSockets
)
if (WITH_SCANNER)
list(APPEND QT5_COMPONENTS
Multimedia
MultimediaWidgets)
endif()
find_package(Qt5 REQUIRED COMPONENTS ${QT5_COMPONENTS})
add_subdirectory(libwalletqt)
add_subdirectory(model)
add_subdirectory(utils)
add_subdirectory(openpgp)
qt5_add_resources(RESOURCES assets.qrc)
@@ -46,6 +59,15 @@ file(GLOB SOURCE_FILES
"dialog/*.cpp"
)
if (WITH_SCANNER)
file(GLOB SCANNER_FILES
"qrcode_scanner/*.h"
"qrcode_scanner/*.cpp")
list(APPEND SOURCE_FILES
${SCANNER_FILES})
endif()
if(TOR_BIN)
if(APPLE)
set(ASSETS_TOR "assets_tor_macos.qrc")
@@ -90,12 +112,9 @@ set_target_properties(feather PROPERTIES
set_property(TARGET feather PROPERTY RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
target_include_directories(feather PUBLIC ${OPENGL_INCLUDE_DIR})
#target_include_directories(feather PUBLIC ${OPENGL_INCLUDE_DIR})
target_include_directories(feather PUBLIC ${Qt5Gui_PRIVATE_INCLUDE_DIRS})
file(GLOB_RECURSE SRC_SOURCES *.cpp)
file(GLOB_RECURSE SRC_HEADERS *.h)
target_include_directories(feather PUBLIC
${CMAKE_BINARY_DIR}/src/feather_autogen/include
${CMAKE_SOURCE_DIR}/monero/include
@@ -124,6 +143,14 @@ target_include_directories(feather PUBLIC
${LIBZIP_INCLUDE_DIRS}
)
if(WITH_SCANNER)
target_include_directories(feather PUBLIC
${ZBAR_INCLUDE_DIR}
${Qt5Multimedia_INCLUDE_DIRS}
${Qt5MultimediaWidgets_INCLUDE_DIRS}
)
endif()
if(DONATE_BEG)
target_compile_definitions(feather PRIVATE DONATE_BEG=1)
endif()
@@ -144,6 +171,10 @@ if(XMRIG)
target_compile_definitions(feather PRIVATE HAS_XMRIG=1)
endif()
if(WITH_SCANNER)
target_compile_definitions(feather PRIVATE WITH_SCANNER=1)
endif()
# TODO: PLACEHOLDER
target_compile_definitions(feather PRIVATE HAS_WEBSOCKET=1)
@@ -172,7 +203,14 @@ target_compile_definitions(feather
${Qt5Svg_DEFINITIONS}
${Qt5Xml_DEFINITIONS}
${Qt5WebSockets_DEFINITIONS}
)
)
if (WITH_SCANNER)
target_compile_definitions(feather PUBLIC
${Qt5Multimedia_DEFINITIONS}
${Qt5MultimediaWidgets_DEFINITIONS}
)
endif()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}")
@@ -212,6 +250,14 @@ target_link_libraries(feather
${LIBZIP_LIBRARIES}
)
if (WITH_SCANNER)
target_link_libraries(feather
Qt5::Multimedia
Qt5::MultimediaWidgets
${ZBAR_LIBRARIES}
)
endif()
if(APPLE)
target_link_libraries(feather
KDMacTouchBar
@@ -247,4 +293,4 @@ endif()
install(TARGETS feather
DESTINATION ${CMAKE_INSTALL_PREFIX}
)
)
-2
View File
@@ -1,2 +0,0 @@
file(GLOB_RECURSE SRC_SOURCES *.cpp)
file(GLOB_RECURSE SRC_HEADERS *.h)
+1
View File
@@ -14,6 +14,7 @@
#if defined(Q_OS_WIN)
#include <windows.h>
#include <vfw.h>
#endif
#if defined(Q_OS_LINUX) && defined(STATIC)
+5 -1
View File
@@ -74,6 +74,7 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa
connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, m_sendWidget, QOverload<>::of(&SendWidget::onPreferredFiatCurrencyChanged));
connect(m_windowSettings, &Settings::amountPrecisionChanged, m_ctx.get(), &AppContext::onAmountPrecisionChanged);
connect(m_windowSettings, &Settings::skinChanged, this, &MainWindow::skinChanged);
QTimer::singleShot(1, [this]{this->updateWidgetIcons();});
connect(m_windowManager, &WindowManager::torSettingsChanged, m_ctx.get(), &AppContext::onTorSettingsChanged);
connect(torManager(), &TorManager::connectionStateChanged, this, &MainWindow::onTorConnectionStateChanged);
@@ -791,11 +792,14 @@ void MainWindow::menuVerifyTxProof() {
void MainWindow::skinChanged(const QString &skinName) {
m_windowManager->changeSkin(skinName);
ColorScheme::updateFromWidget(this);
this->updateWidgetIcons();
}
void MainWindow::updateWidgetIcons() {
m_sendWidget->skinChanged();
#ifdef HAS_LOCALMONERO
m_localMoneroWidget->skinChanged();
#endif
ui->conversionWidget->skinChanged();
}
+1
View File
@@ -204,6 +204,7 @@ private:
void updateTitle();
void donationNag();
void updateRecentlyOpened(const QString &filename);
void updateWidgetIcons();
Ui::MainWindow *ui;
WindowManager *m_windowManager;
-4
View File
@@ -1,4 +0,0 @@
file(GLOB_RECURSE SRC_SOURCES *.cpp)
file(GLOB_RECURSE SRC_HEADERS *.h)
+3
View File
@@ -0,0 +1,3 @@
file(GLOB_RECURSE SRC_SOURCES *.cpp)
file(GLOB_RECURSE SRC_HEADERS *.h)
+123
View File
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include "QrCodeScanDialog.h"
#include "ui_QrCodeScanDialog.h"
#include <QMessageBox>
#include <QtMultimedia/QCamera>
#include <QtMultimedia/QCameraInfo>
QrCodeScanDialog::QrCodeScanDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::QrCodeScanDialog)
{
ui->setupUi(this);
this->setWindowTitle("Scan QR Code");
QPixmap pixmap = QPixmap(":/assets/images/warning.png");
ui->icon_warning->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation));
m_cameras = QCameraInfo::availableCameras();
if (m_cameras.count() < 1) {
QMessageBox::warning(parent, "QR code scanner", "No available cameras found.");
this->close();
}
for (const auto &camera : m_cameras) {
ui->combo_camera->addItem(camera.deviceName());
}
connect(ui->combo_camera, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &QrCodeScanDialog::onCameraSwitched);
this->onCameraSwitched(0);
m_thread = new QrScanThread(this);
m_thread->start();
connect(m_thread, &QrScanThread::decoded, this, &QrCodeScanDialog::onDecoded);
connect(m_thread, &QrScanThread::notifyError, this, &QrCodeScanDialog::notifyError);
connect(&m_imageTimer, &QTimer::timeout, this, &QrCodeScanDialog::takeImage);
m_imageTimer.start(500);
}
void QrCodeScanDialog::onCameraSwitched(int index) {
if (index >= m_cameras.size()) {
return;
}
m_camera.reset(new QCamera(m_cameras.at(index)));
auto captureMode = QCamera::CaptureStillImage;
if (m_camera->isCaptureModeSupported(captureMode)) {
m_camera->setCaptureMode(captureMode);
}
connect(m_camera.data(), QOverload<QCamera::Error>::of(&QCamera::error), this, &QrCodeScanDialog::displayCameraError);
connect(m_camera.data(), &QCamera::statusChanged, [this](QCamera::Status status){
bool unloaded = (status == QCamera::Status::UnloadedStatus);
ui->frame_unavailable->setVisible(unloaded);
});
m_imageCapture.reset(new QCameraImageCapture(m_camera.data()));
if (!m_imageCapture->isCaptureDestinationSupported(QCameraImageCapture::CaptureToBuffer)) {
qDebug() << "Capture to buffer is NOT supported";
}
m_imageCapture->setCaptureDestination(QCameraImageCapture::CaptureToBuffer);
connect(m_imageCapture.data(), &QCameraImageCapture::imageAvailable, this, &QrCodeScanDialog::processAvailableImage);
connect(m_imageCapture.data(), QOverload<int, QCameraImageCapture::Error, const QString &>::of(&QCameraImageCapture::error),
this, &QrCodeScanDialog::displayCaptureError);
m_camera->setViewfinder(ui->viewfinder);
m_camera->start();
}
void QrCodeScanDialog::displayCaptureError(int id, const QCameraImageCapture::Error error, const QString &errorString)
{
Q_UNUSED(id);
Q_UNUSED(error);
QMessageBox::warning(this, "Image Capture Error", errorString);
}
void QrCodeScanDialog::displayCameraError()
{
QMessageBox::warning(this, "Camera Error", m_camera->errorString());
}
void QrCodeScanDialog::processAvailableImage(int id, const QVideoFrame &frame) {
Q_UNUSED(id);
QImage img = frame.image();
img.convertTo(QImage::Format_RGB32);
m_thread->addImage(img);
}
void QrCodeScanDialog::takeImage()
{
if (m_imageCapture->isReadyForCapture()) {
m_imageCapture->capture();
}
}
void QrCodeScanDialog::onDecoded(int type, const QString &data) {
decodedString = data;
this->accept();
}
void QrCodeScanDialog::notifyError(const QString &msg) {
qDebug() << "QrScanner error: " << msg;
}
QrCodeScanDialog::~QrCodeScanDialog()
{
m_thread->stop();
m_thread->quit();
if (!m_thread->wait(5000))
{
m_thread->terminate();
m_thread->wait();
}
delete ui;
}
+50
View File
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#ifndef FEATHER_QRCODESCANDIALOG_H
#define FEATHER_QRCODESCANDIALOG_H
#include <QDialog>
#include <QCamera>
#include <QCameraImageCapture>
#include <QTimer>
#include <QVideoFrame>
#include "QrScanThread.h"
namespace Ui {
class QrCodeScanDialog;
}
class QrCodeScanDialog : public QDialog
{
Q_OBJECT
public:
explicit QrCodeScanDialog(QWidget *parent);
~QrCodeScanDialog() override;
QString decodedString = "";
private slots:
void onCameraSwitched(int index);
void onDecoded(int type, const QString &data);
void notifyError(const QString &msg);
private:
void processAvailableImage(int id, const QVideoFrame &frame);
void displayCaptureError(int, QCameraImageCapture::Error, const QString &errorString);
void displayCameraError();
void takeImage();
Ui::QrCodeScanDialog *ui;
QScopedPointer<QCamera> m_camera;
QScopedPointer<QCameraImageCapture> m_imageCapture;
QrScanThread *m_thread;
QTimer m_imageTimer;
QList<QCameraInfo> m_cameras;
};
#endif //FEATHER_QRCODESCANDIALOG_H
+110
View File
@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QrCodeScanDialog</class>
<widget class="QDialog" name="QrCodeScanDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCameraViewfinder" name="viewfinder" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_unavailable">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="icon_warning">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>icon</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Lost connection to camera. Please restart scan dialog.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Camera:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="combo_camera"/>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QCameraViewfinder</class>
<extends>QWidget</extends>
<header>qcameraviewfinder.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+96
View File
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2014-2020, The Monero Project.
#include "QrScanThread.h"
#include <QtGlobal>
#include <QDebug>
QrScanThread::QrScanThread(QObject *parent)
: QThread(parent)
, m_running(true)
{
m_scanner.set_handler(*this);
}
void QrScanThread::image_callback(zbar::Image &image)
{
qDebug() << "image_callback : Found Code ! " ;
for (zbar::Image::SymbolIterator sym = image.symbol_begin(); sym != image.symbol_end(); ++sym) {
if (!sym->get_count()) {
QString data = QString::fromStdString(sym->get_data());
emit decoded(sym->get_type(), data);
}
}
}
void QrScanThread::processZImage(zbar::Image &image)
{
m_scanner.recycle_image(image);
zbar::Image tmp = image.convert(*(long*)"Y800");
m_scanner.scan(tmp);
image.set_symbols(tmp.get_symbols());
}
bool QrScanThread::zimageFromQImage(const QImage &qimg, zbar::Image &dst)
{
switch (qimg.format()) {
case QImage::Format_RGB32 :
case QImage::Format_ARGB32 :
case QImage::Format_ARGB32_Premultiplied :
break;
default :
qDebug() << "Format: " << qimg.format();
emit notifyError(QString("Invalid QImage Format !"));
return false;
}
unsigned int bpl( qimg.bytesPerLine() ), width( bpl / 4), height( qimg.height());
dst.set_size(width, height);
dst.set_format("BGR4");
unsigned long datalen = qimg.sizeInBytes();
dst.set_data(qimg.bits(), datalen);
if((width * 4 != bpl) || (width * height * 4 > datalen)){
emit notifyError(QString("QImage to Zbar::Image failed !"));
return false;
}
return true;
}
void QrScanThread::processQImage(const QImage &qimg)
{
try {
m_image = QSharedPointer<zbar::Image>(new zbar::Image());
if (!zimageFromQImage(qimg, *m_image))
return;
processZImage(*m_image);
}
catch(std::exception &e) {
qDebug() << "ERROR: " << e.what();
emit notifyError(e.what());
}
}
void QrScanThread::stop()
{
m_running = false;
m_waitCondition.wakeOne();
}
void QrScanThread::addImage(const QImage &img)
{
QMutexLocker locker(&m_mutex);
m_queue.append(img);
m_waitCondition.wakeOne();
}
void QrScanThread::run()
{
while (m_running) {
QMutexLocker locker(&m_mutex);
while (m_queue.isEmpty() && m_running) {
m_waitCondition.wait(&m_mutex);
}
if (!m_queue.isEmpty()) {
processQImage(m_queue.takeFirst());
}
}
}
+42
View File
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020, The Monero Project.
#ifndef _QRSCANTHREAD_H_
#define _QRSCANTHREAD_H_
#include <QThread>
#include <QMutex>
#include <QWaitCondition>
#include <QEvent>
#include <QCamera>
#include <zbar.h>
class QrScanThread : public QThread, public zbar::Image::Handler
{
Q_OBJECT
public:
QrScanThread(QObject *parent = nullptr);
void addImage(const QImage &img);
virtual void stop();
signals:
void decoded(int type, const QString &data);
void notifyError(const QString &error, bool warning = false);
protected:
virtual void run();
void processQImage(const QImage &);
void processZImage(zbar::Image &image);
virtual void image_callback(zbar::Image &image);
bool zimageFromQImage(const QImage&, zbar::Image &);
private:
zbar::ImageScanner m_scanner;
QSharedPointer<zbar::Image> m_image;
bool m_running;
QMutex m_mutex;
QWaitCondition m_waitCondition;
QList<QImage> m_queue;
};
#endif
+26
View File
@@ -7,6 +7,12 @@
#include "ui_sendwidget.h"
#include "constants.h"
#include "utils/AppData.h"
#include "Icons.h"
#include "ColorScheme.h"
#ifdef WITH_SCANNER
#include "qrcode_scanner/QrCodeScanDialog.h"
#endif
SendWidget::SendWidget(QSharedPointer<AppContext> ctx, QWidget *parent)
: QWidget(parent)
@@ -26,6 +32,7 @@ SendWidget::SendWidget(QSharedPointer<AppContext> ctx, QWidget *parent)
connect(m_ctx.get(), &AppContext::openAliasResolved, this, &SendWidget::onOpenAliasResolved);
connect(m_ctx.get(), &AppContext::openAliasResolveError, this, &SendWidget::onOpenAliasResolveError);
connect(ui->btnScan, &QPushButton::clicked, this, &SendWidget::scanClicked);
connect(ui->btnSend, &QPushButton::clicked, this, &SendWidget::sendClicked);
connect(ui->btnClear, &QPushButton::clicked, this, &SendWidget::clearClicked);
connect(ui->btnMax, &QPushButton::clicked, this, &SendWidget::btnMaxClicked);
@@ -103,6 +110,17 @@ void SendWidget::fillAddress(const QString &address) {
ui->lineAddress->moveCursor(QTextCursor::Start);
}
void SendWidget::scanClicked() {
#ifdef WITH_SCANNER
auto *dialog = new QrCodeScanDialog(this);
dialog->exec();
ui->lineAddress->setText(dialog->decodedString);
dialog->deleteLater();
#else
QMessageBox::warning(this, "QR scanner", "Feather was built without webcam QR scanner support.");
#endif
}
void SendWidget::sendClicked() {
if (!m_ctx->wallet->isConnected()) {
QMessageBox::warning(this, "Error", "Unable to create transaction:\n\n"
@@ -276,6 +294,14 @@ void SendWidget::onPreferredFiatCurrencyChanged() {
this->setupComboBox();
}
void SendWidget::skinChanged() {
if (ColorScheme::hasDarkBackground(this)) {
ui->btnScan->setIcon(icons()->icon("camera_white.png"));
} else {
ui->btnScan->setIcon(icons()->icon("camera_dark.png"));
}
}
SendWidget::~SendWidget() {
delete ui;
}
+2
View File
@@ -26,6 +26,8 @@ public:
~SendWidget() override;
public slots:
void skinChanged();
void scanClicked();
void sendClicked();
void clearClicked();
void aliasClicked();
+55 -10
View File
@@ -52,17 +52,51 @@
</widget>
</item>
<item row="0" column="1">
<widget class="PayToEdit" name="lineAddress">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>10</number>
</property>
<property name="tabChangesFocus">
<bool>true</bool>
</property>
<property name="lineWrapMode">
<enum>QPlainTextEdit::NoWrap</enum>
</property>
</widget>
<item>
<widget class="PayToEdit" name="lineAddress">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="tabChangesFocus">
<bool>true</bool>
</property>
<property name="lineWrapMode">
<enum>QPlainTextEdit::NoWrap</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnScan">
<property name="text">
<string>Scan</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="HelpLabel" name="label_Description">
@@ -208,6 +242,17 @@
<header>widgets/PayToEdit.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>lineAddress</tabstop>
<tabstop>lineDescription</tabstop>
<tabstop>lineAmount</tabstop>
<tabstop>comboCurrencySelection</tabstop>
<tabstop>btnMax</tabstop>
<tabstop>btn_openAlias</tabstop>
<tabstop>btnClear</tabstop>
<tabstop>btnSend</tabstop>
<tabstop>btnScan</tabstop>
</tabstops>
<resources/>
<connections/>
<slots>
-2
View File
@@ -1,2 +0,0 @@
file(GLOB_RECURSE SRC_SOURCES *.cpp)
file(GLOB_RECURSE SRC_HEADERS *.h)
+1 -1
View File
@@ -15,7 +15,7 @@ class ChildProcess : public QProcess {
Q_OBJECT
public:
explicit ChildProcess(QObject* parent = nullptr);
~ChildProcess();
~ChildProcess() override;
protected:
void setupChildProcess() override;
};