option(PLATFORM_INSTALLER "Built-in updater fetches installer (windows-only)" OFF)
option(USE_DEVICE_TREZOR "Trezor support compilation" ON)
option(DONATE_BEG "Prompt donation window every once in a while" ON)
-
-
-option(WITH_SCANNER "Enable webcam QR scanner" OFF)
+option(WITH_SCANNER "Enable webcam QR scanner" ON)
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_SOURCE_DIR}/cmake")
include(CheckCCompilerFlag)
"polyseed/*.h"
"polyseed/*.cpp"
"polyseed/*.c"
- "qrcode_scanner/QrCodeUtils.cpp"
- "qrcode_scanner/QrCodeUtils.h"
+ "qrcode_utils/QrCodeUtils.cpp"
+ "qrcode_utils/QrCodeUtils.h"
"monero_seed/argon2/blake2/*.c"
"monero_seed/argon2/*.c"
"monero_seed/*.cpp"
target_compile_definitions(feather PRIVATE HAS_XMRIG=1)
endif()
-if(WITH_SCANNER AND NOT Qt6_FOUND)
+if(WITH_SCANNER)
target_compile_definitions(feather PRIVATE WITH_SCANNER=1)
endif()
#if defined(WITH_SCANNER) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include "qrcode_scanner/QrCodeScanDialog.h"
#include <QtMultimedia/QCameraInfo>
+#elif defined(WITH_SCANNER)
+#include "qrcode_scanner_qt6/QrCodeScanDialog.h"
+#include <QMediaDevices>
#endif
SendWidget::SendWidget(QSharedPointer<AppContext> ctx, QWidget *parent)
dialog->exec();
ui->lineAddress->setText(dialog->decodedString);
dialog->deleteLater();
+#elif defined(WITH_SCANNER)
+ auto cameras = QMediaDevices::videoInputs();
+ if (cameras.empty()) {
+ QMessageBox::warning(this, "QR code scanner", "No available cameras found.");
+ return;
+ }
+
+ 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
#include <QCamera>
#include <QMediaDevices>
#include <QCameraDevice>
+#include <QMessageBox>
+#include <QImageCapture>
+#include <QVideoFrame>
QrCodeScanDialog::QrCodeScanDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::QrCodeScanDialog)
{
ui->setupUi(this);
+ this->setWindowTitle("Scan QR code");
- m_camera.reset(new QCamera(QMediaDevices::defaultVideoInput()));
- m_captureSession.setCamera(m_camera.data());
+ QPixmap pixmap = QPixmap(":/assets/images/warning.png");
+ ui->icon_warning->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation));
+
+ const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();
+ for (const auto &camera : cameras) {
+ ui->combo_camera->addItem(camera.description());
+ }
+
+ 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) {
+ const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();
+
+ if (index >= cameras.size()) {
+ return;
+ }
+
+ m_camera.reset(new QCamera(cameras.at(index)));
+ m_captureSession.setCamera(m_camera.data());
m_captureSession.setVideoOutput(ui->viewfinder);
+ m_imageCapture = new QImageCapture;
+ m_captureSession.setImageCapture(m_imageCapture);
+
+ connect(m_imageCapture, &QImageCapture::imageCaptured, this, &QrCodeScanDialog::processCapturedImage);
+ connect(m_camera.data(), &QCamera::errorOccurred, this, &QrCodeScanDialog::displayCameraError);
+ connect(m_camera.data(), &QCamera::activeChanged, [this](bool active){
+ ui->frame_unavailable->setVisible(!active);
+ });
+
m_camera->start();
+}
+
+void QrCodeScanDialog::processCapturedImage(int requestId, const QImage& img) {
+ Q_UNUSED(requestId);
+ QImage image{img};
+ image.convertTo(QImage::Format_RGB32);
+ m_thread->addImage(image);
+}
+
+void QrCodeScanDialog::takeImage()
+{
+ if (m_imageCapture->isReadyForCapture()) {
+ m_imageCapture->capture();
+ }
+}
+
+void QrCodeScanDialog::onDecoded(int type, const QString &data) {
+ decodedString = data;
+ this->accept();
+}
+
+void QrCodeScanDialog::displayCameraError()
+{
+ if (m_camera->error() != QCamera::NoError) {
+ QMessageBox::warning(this, tr("Camera Error"), m_camera->errorString());
+ }
+}
- const QList<QCameraDevice> availableCameras = QMediaDevices::videoInputs();
+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();
+ }
}
\ No newline at end of file
#include <QCamera>
#include <QScopedPointer>
#include <QMediaCaptureSession>
+#include <QTimer>
+
+#include "QrScanThread.h"
namespace Ui {
class QrCodeScanDialog;
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 processCapturedImage(int requestId, const QImage& img);
+ void displayCameraError();
+ void takeImage();
+
QScopedPointer<Ui::QrCodeScanDialog> ui;
+ QrScanThread *m_thread;
+ QImageCapture *m_imageCapture;
+ QTimer m_imageTimer;
QScopedPointer<QCamera> m_camera;
QMediaCaptureSession m_captureSession;
};
<rect>
<x>0</x>
<y>0</y>
- <width>816</width>
- <height>688</height>
+ <width>490</width>
+ <height>422</height>
</rect>
</property>
<property name="windowTitle">
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
- <widget class="QVideoWidget" name="viewfinder" native="true"/>
+ <widget class="QVideoWidget" 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="QDialogButtonBox" name="buttonBox">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
+ <widget class="QFrame" name="frame_unavailable">
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
</property>
- <property name="standardButtons">
- <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
</property>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <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>55</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <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>
</customwidgets>
<resources/>
- <connections>
- <connection>
- <sender>buttonBox</sender>
- <signal>accepted()</signal>
- <receiver>QrCodeScanDialog</receiver>
- <slot>accept()</slot>
- <hints>
- <hint type="sourcelabel">
- <x>248</x>
- <y>254</y>
- </hint>
- <hint type="destinationlabel">
- <x>157</x>
- <y>274</y>
- </hint>
- </hints>
- </connection>
- <connection>
- <sender>buttonBox</sender>
- <signal>rejected()</signal>
- <receiver>QrCodeScanDialog</receiver>
- <slot>reject()</slot>
- <hints>
- <hint type="sourcelabel">
- <x>316</x>
- <y>260</y>
- </hint>
- <hint type="destinationlabel">
- <x>286</x>
- <y>274</y>
- </hint>
- </hints>
- </connection>
- </connections>
+ <connections/>
</ui>
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2022 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(zbar_fourcc('Y', '8', '0', '0'));
+ 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());
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2022 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
\ No newline at end of file
#include "libwalletqt/WalletManager.h"
#include "model/ModelUtils.h"
-#include "qrcode_scanner/QrCodeUtils.h"
+#include "qrcode_utils/QrCodeUtils.h"
PayToEdit::PayToEdit(QWidget *parent) : QPlainTextEdit(parent)
{