You've already forked floweepay-aur
1157 lines
42 KiB
Diff
1157 lines
42 KiB
Diff
|
|
From 3d2911eb08a34f5df202ef1d9edee575ad89abaf Mon Sep 17 00:00:00 2001
|
||
|
|
From: TomZ <tom@flowee.org>
|
||
|
|
Date: Mon, 16 Feb 2026 14:34:28 +0100
|
||
|
|
Subject: [PATCH] Port to new ZXing version 3
|
||
|
|
|
||
|
|
The new ZXingCpp release is out, it is a major version (v3) and
|
||
|
|
it has broken source compatibility towards version 2.
|
||
|
|
|
||
|
|
The good news is that we can actually cut out quite a lot of
|
||
|
|
boring code which is now done in the upstream project.
|
||
|
|
|
||
|
|
But to actually benefit from better readability I think the best
|
||
|
|
approach is the "isolate the old" idea. So this copies the v2
|
||
|
|
compatible file to CameraController_zxing2.cpp QRCreator_zxing2.cpp
|
||
|
|
We'll have code duplication that way, but it will never be compiled
|
||
|
|
into the same binary and indeed we'll just be cleanly able to
|
||
|
|
delete the old support when that time comes.
|
||
|
|
---
|
||
|
|
CMakeLists.txt | 4 +-
|
||
|
|
src/CMakeLists.txt | 23 +-
|
||
|
|
src/CameraController.cpp | 129 +-----
|
||
|
|
src/CameraController_zxing2.cpp | 725 ++++++++++++++++++++++++++++++++
|
||
|
|
src/QRCreator.cpp | 35 +-
|
||
|
|
src/QRCreator_zxing2.cpp | 71 ++++
|
||
|
|
6 files changed, 835 insertions(+), 152 deletions(-)
|
||
|
|
create mode 100644 src/CameraController_zxing2.cpp
|
||
|
|
create mode 100644 src/QRCreator_zxing2.cpp
|
||
|
|
|
||
|
|
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||
|
|
index 3ffa53e..a0709dc 100644
|
||
|
|
--- a/CMakeLists.txt
|
||
|
|
+++ b/CMakeLists.txt
|
||
|
|
@@ -60,6 +60,7 @@ if (ANDROID)
|
||
|
|
set_property(TARGET ZXing::ZXing PROPERTY IMPORTED_LINK_INTERFACE_LIBRARIES
|
||
|
|
"/opt/android-zxing/lib/libZXing.a")
|
||
|
|
set (ZXing_FOUND TRUE)
|
||
|
|
+ set (ZXing_VERSION_MAJOR 2)
|
||
|
|
else ()
|
||
|
|
find_package(ZXing REQUIRED)
|
||
|
|
endif()
|
||
|
|
@@ -67,6 +68,7 @@ else ()
|
||
|
|
find_package(Qt6 COMPONENTS DBus LinguistTools)
|
||
|
|
find_package(ZXing REQUIRED)
|
||
|
|
endif()
|
||
|
|
+
|
||
|
|
if (CMAKE_VERSION VERSION_GREATER "3.29.9")
|
||
|
|
# use the upstream boost info instead of the cmake-shipped one for finding
|
||
|
|
# this policy was introduced in cmake 3.30
|
||
|
|
@@ -261,7 +263,7 @@ if (build_desktop_pay)
|
||
|
|
)
|
||
|
|
add_executable(pay ${SOURCES_PAY})
|
||
|
|
set_target_properties(pay PROPERTIES COMPILE_DEFINITIONS "${COMPILE_DEFINITIONS} DESKTOP")
|
||
|
|
- target_link_libraries(pay PRIVATE pay_lib Qt6::Svg ${PAY_MODULES_LIBS})
|
||
|
|
+ target_link_libraries(pay PRIVATE Qt6::Svg ${PAY_MODULES_LIBS})
|
||
|
|
install(TARGETS pay DESTINATION bin)
|
||
|
|
endif()
|
||
|
|
|
||
|
|
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
|
||
|
|
index 78e7db6..822794f 100644
|
||
|
|
--- a/src/CMakeLists.txt
|
||
|
|
+++ b/src/CMakeLists.txt
|
||
|
|
@@ -1,5 +1,5 @@
|
||
|
|
# This file is part of the Flowee project
|
||
|
|
-# Copyright (C) 2020-2025 Tom Zander <tom@flowee.org>
|
||
|
|
+# Copyright (C) 2020-2026 Tom Zander <tom@flowee.org>
|
||
|
|
#
|
||
|
|
# This program is free software: you can redistribute it and/or modify
|
||
|
|
# it under the terms of the GNU General Public License as published by
|
||
|
|
@@ -43,7 +43,6 @@ set (PAY_SOURCES
|
||
|
|
PriceHistoryDataProvider.cpp
|
||
|
|
QMLClipboardHelper.cpp
|
||
|
|
QMLImportHelper.cpp
|
||
|
|
- QRCreator.cpp
|
||
|
|
QRScanner.cpp
|
||
|
|
RepeatPaymentDetails.cpp
|
||
|
|
RepeatPaymentsModel.cpp
|
||
|
|
@@ -91,15 +90,29 @@ else ()
|
||
|
|
)
|
||
|
|
endif ()
|
||
|
|
|
||
|
|
-if (NetworkLogClient)
|
||
|
|
- list(APPEND PAY_SOURCES NetworkLogClient.cpp)
|
||
|
|
+if (ZXing_VERSION_MAJOR EQUAL 2)
|
||
|
|
+ message(STATUS "Using ZXing 2.x")
|
||
|
|
+ list(APPEND PAY_SOURCES QRCreator_zxing2.cpp)
|
||
|
|
+elseif (ZXing_VERSION_MAJOR EQUAL 3)
|
||
|
|
+ message(STATUS "Using ZXing 3.x")
|
||
|
|
+ list(APPEND PAY_SOURCES QRCreator.cpp)
|
||
|
|
+else ()
|
||
|
|
+ message(FATAL_ERROR "Unsupported ZXing version: ${ZXing_VERSION}")
|
||
|
|
endif ()
|
||
|
|
|
||
|
|
if (${Qt6Multimedia_FOUND})
|
||
|
|
- list(APPEND PAY_SOURCES CameraController.cpp)
|
||
|
|
+ if (ZXing_VERSION_MAJOR EQUAL 2)
|
||
|
|
+ list(APPEND PAY_SOURCES CameraController_zxing2.cpp)
|
||
|
|
+ else ()
|
||
|
|
+ list(APPEND PAY_SOURCES CameraController.cpp)
|
||
|
|
+ endif ()
|
||
|
|
list(APPEND PayLib_PRIVATE_LIBS Qt6::Multimedia)
|
||
|
|
endif ()
|
||
|
|
|
||
|
|
+if (NetworkLogClient)
|
||
|
|
+ list(APPEND PAY_SOURCES NetworkLogClient.cpp)
|
||
|
|
+endif ()
|
||
|
|
+
|
||
|
|
add_library(pay_lib STATIC ${PAY_SOURCES})
|
||
|
|
|
||
|
|
target_link_libraries(pay_lib
|
||
|
|
diff --git a/src/CameraController.cpp b/src/CameraController.cpp
|
||
|
|
index 9d69952..7ec27dc 100644
|
||
|
|
--- a/src/CameraController.cpp
|
||
|
|
+++ b/src/CameraController.cpp
|
||
|
|
@@ -1,7 +1,6 @@
|
||
|
|
/*
|
||
|
|
* This file is part of the Flowee project
|
||
|
|
- * Copyright (C) 2022-2025 Tom Zander <tom@flowee.org>
|
||
|
|
- * Copyright (C) 2020 Axel Waggershauser <awagger@gmail.com>
|
||
|
|
+ * Copyright (C) 2022-2026 Tom Zander <tom@flowee.org>
|
||
|
|
*
|
||
|
|
* This program is free software: you can redistribute it and/or modify
|
||
|
|
* it under the terms of the GNU General Public License as published by
|
||
|
|
@@ -21,8 +20,6 @@
|
||
|
|
#include "base58.h"
|
||
|
|
#include <Logger.h>
|
||
|
|
|
||
|
|
-#include <ZXing/ReadBarcode.h>
|
||
|
|
-
|
||
|
|
#include <QImage>
|
||
|
|
#include <QVideoFrame>
|
||
|
|
#include <QVideoSink>
|
||
|
|
@@ -34,6 +31,10 @@
|
||
|
|
#include <QThread>
|
||
|
|
#include <QTimer>
|
||
|
|
|
||
|
|
+#include <ZXing/ZXingQt.h>
|
||
|
|
+// force Qt to moc this external header file.
|
||
|
|
+#include "ZXing/moc_ZXingQt.cpp"
|
||
|
|
+
|
||
|
|
enum AskingState {
|
||
|
|
NotAsked,
|
||
|
|
Denied,
|
||
|
|
@@ -85,14 +86,13 @@ public:
|
||
|
|
protected:
|
||
|
|
void run();
|
||
|
|
private:
|
||
|
|
+#if ZXING_VERSION_MAJOR < 3
|
||
|
|
std::vector<ZXing::Result> readBarcodes(const QImage &img) const;
|
||
|
|
std::vector<ZXing::Result> readBarcodes(QVideoFrame &frame) const;
|
||
|
|
+#endif
|
||
|
|
|
||
|
|
CameraControllerPrivate *m_parent;
|
||
|
|
- // notice that since ZXIng 2.2.0 this is renamed to 'ReaderOptions'.
|
||
|
|
- // this was released in December 2023, so we'll be using the old stuff
|
||
|
|
- // for quite a bit longer to keep stuff compiling on older systems.
|
||
|
|
- ZXing::DecodeHints m_decodeHints;
|
||
|
|
+ ZXing::ReaderOptions m_decodeHints;
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
@@ -278,7 +278,8 @@ void QRScanningThread::run()
|
||
|
|
return;
|
||
|
|
|
||
|
|
lastFrameScanned = time(nullptr);
|
||
|
|
- auto results = readBarcodes(frame);
|
||
|
|
+
|
||
|
|
+ const auto results = ZXingQt::ReadBarcodes(frame);
|
||
|
|
for (const auto &result : results) {
|
||
|
|
const auto &bytes = result.bytes();
|
||
|
|
// logInfo(10005) << "result:" << QString::fromUtf8(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||
|
|
@@ -288,7 +289,7 @@ void QRScanningThread::run()
|
||
|
|
// first, when it starts with 'bch-wif:' this helps, but needs to be cut off.
|
||
|
|
const bool wifPrefix = bytes.size() >= 58 && bytes.size() < 63
|
||
|
|
&& (bytes[8] == 'K' || bytes[8] == 'L')
|
||
|
|
- && 0 == memcmp(&bytes[0], "bch-wif:", 8);
|
||
|
|
+ && 0 == memcmp(bytes.constData(), "bch-wif:", 8);
|
||
|
|
|
||
|
|
if (wifPrefix || (bytes.size() >= 50 && bytes.size() < 55 && (bytes[0] == 'K' || bytes[0] == 'L'))) {
|
||
|
|
// might be one!!
|
||
|
|
@@ -378,114 +379,6 @@ void QRScanningThread::run()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
-std::vector<ZXing::Result> QRScanningThread::readBarcodes(const QImage &img) const
|
||
|
|
-{
|
||
|
|
- auto imageFormat = img.format();
|
||
|
|
- if (imageFormat == QImage::Format_Invalid) // likely a damaged frame in the feed
|
||
|
|
- return {};
|
||
|
|
- auto zxImageFormat = ZXing::ImageFormat::None;
|
||
|
|
- switch (imageFormat) {
|
||
|
|
- case QImage::Format_ARGB32:
|
||
|
|
- case QImage::Format_RGB32:
|
||
|
|
-#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||
|
|
- zxImageFormat = ZXing::ImageFormat::BGRX;
|
||
|
|
-#else
|
||
|
|
- zxImageFormat = ImageFormat::XRGB;
|
||
|
|
-#endif
|
||
|
|
- break;
|
||
|
|
- case QImage::Format_RGB888: zxImageFormat = ZXing::ImageFormat::RGB; break;
|
||
|
|
- case QImage::Format_RGBX8888:
|
||
|
|
- case QImage::Format_RGBA8888: zxImageFormat = ZXing::ImageFormat::RGBX; break;
|
||
|
|
- case QImage::Format_Grayscale8: zxImageFormat = ZXing::ImageFormat::Lum; break;
|
||
|
|
- default: break;
|
||
|
|
- }
|
||
|
|
-
|
||
|
|
- if (zxImageFormat == ZXing::ImageFormat::None) {
|
||
|
|
- QImage gray = img.convertToFormat(QImage::Format_Grayscale8) ;
|
||
|
|
- return ZXing::ReadBarcodes({gray.bits(), gray.width(), gray.height(), ZXing::ImageFormat::Lum, static_cast<int>(gray.bytesPerLine())}, m_decodeHints);
|
||
|
|
- }
|
||
|
|
-
|
||
|
|
- ZXing::ImageView buf(img.bits(), img.width(), img.height(), zxImageFormat, static_cast<int>(img.bytesPerLine()));
|
||
|
|
- return ZXing::ReadBarcodes(buf, m_decodeHints);
|
||
|
|
-}
|
||
|
|
-
|
||
|
|
-std::vector<ZXing::Result> QRScanningThread::readBarcodes(QVideoFrame &frame) const
|
||
|
|
-{
|
||
|
|
- ZXing::ImageFormat fmt = ZXing::ImageFormat::None;
|
||
|
|
- int pixStride = 0;
|
||
|
|
- int pixOffset = 0;
|
||
|
|
-
|
||
|
|
- // note that the comments behind the values are the Qt5 formats.
|
||
|
|
- switch (frame.pixelFormat()) {
|
||
|
|
- case QVideoFrameFormat::Format_ARGB8888: // ARGB32
|
||
|
|
- case QVideoFrameFormat::Format_ARGB8888_Premultiplied: // ARGB32_Premultiplied
|
||
|
|
- case QVideoFrameFormat::Format_RGBX8888: // RGB32
|
||
|
|
- fmt = ZXing::ImageFormat::BGRX;
|
||
|
|
- break;
|
||
|
|
- case QVideoFrameFormat::Format_BGRA8888: // BGRA32
|
||
|
|
- case QVideoFrameFormat::Format_BGRA8888_Premultiplied: // BGRA32_Premultiplied
|
||
|
|
- case QVideoFrameFormat::Format_BGRX8888: // BGR32
|
||
|
|
-#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||
|
|
- fmt = ZXing::ImageFormat::RGBX;
|
||
|
|
-#else
|
||
|
|
- fmt = ImageFormat::XBGR;
|
||
|
|
-#endif
|
||
|
|
- break;
|
||
|
|
- case QVideoFrameFormat::Format_P010:
|
||
|
|
- case QVideoFrameFormat::Format_P016:
|
||
|
|
- fmt = ZXing::ImageFormat::Lum, pixStride = 1; break;
|
||
|
|
- case QVideoFrameFormat::Format_AYUV: // AYUV444
|
||
|
|
- case QVideoFrameFormat::Format_AYUV_Premultiplied: // AYUV444_Premultiplied
|
||
|
|
-#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||
|
|
- fmt = ZXing::ImageFormat::Lum, pixStride = 4, pixOffset = 3;
|
||
|
|
-#else
|
||
|
|
- fmt = ImageFormat::Lum, pixStride = 4, pixOffset = 2;
|
||
|
|
-#endif
|
||
|
|
- break;
|
||
|
|
- case QVideoFrameFormat::Format_YUV420P: // YUV420P
|
||
|
|
- case QVideoFrameFormat::Format_NV12: // NV12
|
||
|
|
- case QVideoFrameFormat::Format_NV21: // NV21
|
||
|
|
- case QVideoFrameFormat::Format_IMC1: // IMC1
|
||
|
|
- case QVideoFrameFormat::Format_IMC2: // IMC2
|
||
|
|
- case QVideoFrameFormat::Format_IMC3: // IMC3
|
||
|
|
- case QVideoFrameFormat::Format_IMC4: // IMC4
|
||
|
|
- case QVideoFrameFormat::Format_YV12: // YV12
|
||
|
|
- case QVideoFrameFormat::Format_Y8: // Y8
|
||
|
|
- case QVideoFrameFormat::Format_YUV422P: // YUV422P
|
||
|
|
- fmt = ZXing::ImageFormat::Lum; break;
|
||
|
|
- case QVideoFrameFormat::Format_UYVY: // UYVY
|
||
|
|
- fmt = ZXing::ImageFormat::Lum, pixStride = 2, pixOffset = 1; break;
|
||
|
|
- case QVideoFrameFormat::Format_YUYV: // YUYV
|
||
|
|
- fmt = ZXing::ImageFormat::Lum, pixStride = 2; break;
|
||
|
|
- case QVideoFrameFormat::Format_Y16: // Y16
|
||
|
|
- fmt = ZXing::ImageFormat::Lum, pixStride = 2, pixOffset = 1; break;
|
||
|
|
-
|
||
|
|
- case QVideoFrameFormat::Format_ABGR8888: // ABGR32
|
||
|
|
-#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||
|
|
- fmt = ZXing::ImageFormat::RGBX;
|
||
|
|
-#else
|
||
|
|
- fmt = ZXing::ImageFormat::XBGR;
|
||
|
|
-#endif
|
||
|
|
- break;
|
||
|
|
- default: break;
|
||
|
|
- }
|
||
|
|
-
|
||
|
|
- if (fmt != ZXing::ImageFormat::None) {
|
||
|
|
- if (!frame.isValid() || !frame.map(QVideoFrame::ReadOnly)){
|
||
|
|
- logDebug(10005) << "invalid QVideoFrame: could not map memory";
|
||
|
|
- return {};
|
||
|
|
- }
|
||
|
|
- QScopeGuard unmap([&] { frame.unmap(); });
|
||
|
|
-
|
||
|
|
- constexpr int FirstPlane = 0;
|
||
|
|
- return ZXing::ReadBarcodes(
|
||
|
|
- {frame.bits(FirstPlane) + pixOffset, frame.width(), frame.height(), fmt, frame.bytesPerLine(FirstPlane), pixStride}, m_decodeHints);
|
||
|
|
- } else {
|
||
|
|
- return readBarcodes(frame.toImage());
|
||
|
|
- }
|
||
|
|
-}
|
||
|
|
-
|
||
|
|
-
|
||
|
|
// --------------------------------------------------------------------
|
||
|
|
|
||
|
|
CameraController::CameraController(QObject *parent)
|
||
|
|
diff --git a/src/CameraController_zxing2.cpp b/src/CameraController_zxing2.cpp
|
||
|
|
new file mode 100644
|
||
|
|
index 0000000..1ee6ad3
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/src/CameraController_zxing2.cpp
|
||
|
|
@@ -0,0 +1,725 @@
|
||
|
|
+/*
|
||
|
|
+ * This file is part of the Flowee project
|
||
|
|
+ * Copyright (C) 2022-2025 Tom Zander <tom@flowee.org>
|
||
|
|
+ * Copyright (C) 2020 Axel Waggershauser <awagger@gmail.com>
|
||
|
|
+ *
|
||
|
|
+ * This program is free software: you can redistribute it and/or modify
|
||
|
|
+ * it under the terms of the GNU General Public License as published by
|
||
|
|
+ * the Free Software Foundation, either version 3 of the License, or
|
||
|
|
+ * (at your option) any later version.
|
||
|
|
+ *
|
||
|
|
+ * This program is distributed in the hope that it will be useful,
|
||
|
|
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
+ * GNU General Public License for more details.
|
||
|
|
+ *
|
||
|
|
+ * You should have received a copy of the GNU General Public License
|
||
|
|
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
+ */
|
||
|
|
+#include "CameraController.h"
|
||
|
|
+#include "QRScanner.h"
|
||
|
|
+#include "base58.h"
|
||
|
|
+#include <Logger.h>
|
||
|
|
+
|
||
|
|
+#include <ZXing/ReadBarcode.h>
|
||
|
|
+
|
||
|
|
+#include <QImage>
|
||
|
|
+#include <QVideoFrame>
|
||
|
|
+#include <QVideoSink>
|
||
|
|
+#include <QCamera>
|
||
|
|
+#include <QGuiApplication>
|
||
|
|
+#include <QClipboard>
|
||
|
|
+#include <QPointer>
|
||
|
|
+#include <QMutex>
|
||
|
|
+#include <QThread>
|
||
|
|
+#include <QTimer>
|
||
|
|
+
|
||
|
|
+enum AskingState {
|
||
|
|
+ NotAsked,
|
||
|
|
+ Denied,
|
||
|
|
+ Authorized
|
||
|
|
+};
|
||
|
|
+
|
||
|
|
+class QRScanningThread;
|
||
|
|
+
|
||
|
|
+class CameraControllerPrivate
|
||
|
|
+{
|
||
|
|
+public:
|
||
|
|
+ explicit CameraControllerPrivate(CameraController *qq);
|
||
|
|
+ // Configure the camera
|
||
|
|
+ void initCamera();
|
||
|
|
+ // check if we need to load the camera.
|
||
|
|
+ void checkState();
|
||
|
|
+
|
||
|
|
+ AskingState state;
|
||
|
|
+ QObject *camera = nullptr;
|
||
|
|
+ QObject *videoSink = nullptr;
|
||
|
|
+ QString helpText;
|
||
|
|
+
|
||
|
|
+ QPointer<QRScanner> scanRequest;
|
||
|
|
+
|
||
|
|
+ mutable QMutex lock;
|
||
|
|
+ QVideoFrame currentFrame;
|
||
|
|
+ QCameraFormat preferredFormat;
|
||
|
|
+
|
||
|
|
+ bool cameraLoaded = false;
|
||
|
|
+ bool cameraStarted = false;
|
||
|
|
+ bool visible = false;
|
||
|
|
+ bool torchEnabled = false;
|
||
|
|
+ int streamWidth = -1;
|
||
|
|
+ int streamHeight = -1;
|
||
|
|
+
|
||
|
|
+ QRScanningThread *scanningThread = nullptr;
|
||
|
|
+
|
||
|
|
+ CameraController *q;
|
||
|
|
+};
|
||
|
|
+
|
||
|
|
+class QRScanningThread : public QThread
|
||
|
|
+{
|
||
|
|
+public:
|
||
|
|
+ explicit QRScanningThread(CameraControllerPrivate *parent);
|
||
|
|
+
|
||
|
|
+ QString text;
|
||
|
|
+ QRScanner::ScanType scanType;
|
||
|
|
+
|
||
|
|
+protected:
|
||
|
|
+ void run();
|
||
|
|
+private:
|
||
|
|
+ std::vector<ZXing::Result> readBarcodes(const QImage &img) const;
|
||
|
|
+ std::vector<ZXing::Result> readBarcodes(QVideoFrame &frame) const;
|
||
|
|
+
|
||
|
|
+ CameraControllerPrivate *m_parent;
|
||
|
|
+ // notice that since ZXIng 2.2.0 this is renamed to 'ReaderOptions'.
|
||
|
|
+ // this was released in December 2023, so we'll be using the old stuff
|
||
|
|
+ // for quite a bit longer to keep stuff compiling on older systems.
|
||
|
|
+ ZXing::DecodeHints m_decodeHints;
|
||
|
|
+};
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+// --------------------------------------------------------------------
|
||
|
|
+
|
||
|
|
+CameraControllerPrivate::CameraControllerPrivate(CameraController *qq)
|
||
|
|
+ : state(NotAsked),
|
||
|
|
+ q(qq)
|
||
|
|
+{
|
||
|
|
+#ifdef TARGET_OS_Android
|
||
|
|
+ auto guiApp = qobject_cast<QGuiApplication*>(QCoreApplication::instance());
|
||
|
|
+ assert(guiApp);
|
||
|
|
+ QObject::connect(guiApp, &QGuiApplication::applicationStateChanged, qq, [=](Qt::ApplicationState appState) {
|
||
|
|
+ // the application state will turn to "Inactive" when the Android dialog
|
||
|
|
+ // asking permissions is put on top, and when the user enters data, we are
|
||
|
|
+ // turned into active again.
|
||
|
|
+ // So check if we are already authorized as that avoid us turning off
|
||
|
|
+ // the camera just after turning it on the first time.
|
||
|
|
+ if (state == Authorized && appState == Qt::ApplicationInactive) {
|
||
|
|
+ logInfo(10005) << "App state inactive, turning off camera";
|
||
|
|
+ // when the user leaves the app screen, the permissions granted to us
|
||
|
|
+ // may have changed, so we need to re-ask.
|
||
|
|
+ state = NotAsked;
|
||
|
|
+ if (cameraStarted) {
|
||
|
|
+ // QtMultimedia doesn't like us not turning off the camera when we get small.
|
||
|
|
+ // so make sure we do, we also cancel the scan request.
|
||
|
|
+ lock.lock();
|
||
|
|
+ cameraStarted = false;
|
||
|
|
+ lock.unlock();
|
||
|
|
+ emit q->cameraActiveChanged();
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ });
|
||
|
|
+#endif
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraControllerPrivate::initCamera()
|
||
|
|
+{
|
||
|
|
+ QCamera *cam = qobject_cast<QCamera *>(camera);
|
||
|
|
+ if (!cam)
|
||
|
|
+ return;
|
||
|
|
+ QCameraFormat preferred;
|
||
|
|
+ bool preferredIsCheap = false;
|
||
|
|
+ for (const auto &format : cam->cameraDevice().videoFormats()) {
|
||
|
|
+ bool formatIsCheap; // if true, we don't need to go via QImage (we avoid double conversion)
|
||
|
|
+ switch (format.pixelFormat()) {
|
||
|
|
+ case QVideoFrameFormat::Format_Invalid:
|
||
|
|
+ case QVideoFrameFormat::Format_XRGB8888:
|
||
|
|
+ case QVideoFrameFormat::Format_XBGR8888:
|
||
|
|
+ case QVideoFrameFormat::Format_RGBA8888:
|
||
|
|
+ case QVideoFrameFormat::Format_SamplerExternalOES:
|
||
|
|
+ case QVideoFrameFormat::Format_Jpeg:
|
||
|
|
+ case QVideoFrameFormat::Format_SamplerRect:
|
||
|
|
+ case QVideoFrameFormat::Format_YUV420P10:
|
||
|
|
+ formatIsCheap = false;
|
||
|
|
+ break;
|
||
|
|
+ default:
|
||
|
|
+ formatIsCheap = true;
|
||
|
|
+ break;
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ logInfo(10005) << " + " << format.resolution().width() << "x" << format.resolution().height()
|
||
|
|
+ << "::" << format.pixelFormat() << formatIsCheap << "framerate:"
|
||
|
|
+ << format.minFrameRate() << "-" << format.maxFrameRate();
|
||
|
|
+
|
||
|
|
+ if (preferred.isNull()) {
|
||
|
|
+ preferred = format;
|
||
|
|
+ preferredIsCheap = formatIsCheap;
|
||
|
|
+ logInfo(10005) << "picked";
|
||
|
|
+ }
|
||
|
|
+ else {
|
||
|
|
+ auto size = format.resolution();
|
||
|
|
+ auto oldSize = preferred.resolution();
|
||
|
|
+ if (preferredIsCheap && !formatIsCheap)
|
||
|
|
+ continue;
|
||
|
|
+ // avoid going for the biggest feed, but not too small either.
|
||
|
|
+ if (oldSize.width() < 800 || (size.width() < oldSize.width() && size.width() >= 800)
|
||
|
|
+ || (size.width() == oldSize.width() && size.height() > oldSize.height() && size.height() < 1000)) {
|
||
|
|
+ preferred = format;
|
||
|
|
+ logInfo(10005) << "picked";
|
||
|
|
+ }
|
||
|
|
+ else if (size == oldSize && format.maxFrameRate() < preferred.maxFrameRate()) {
|
||
|
|
+ preferred = format;
|
||
|
|
+ logInfo(10005) << "picked";
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ logCritical(10005).nospace() << "Selected camera resolution: " << preferred.resolution().width() << "x" << preferred.resolution().height();
|
||
|
|
+ preferredFormat = preferred;
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraControllerPrivate::checkState()
|
||
|
|
+{
|
||
|
|
+ if (state != Authorized)
|
||
|
|
+ return;
|
||
|
|
+ if (!cameraLoaded || !visible) {
|
||
|
|
+ cameraLoaded = true;
|
||
|
|
+ visible = true;
|
||
|
|
+ emit q->visibleChanged();
|
||
|
|
+ emit q->loadCameraChanged();
|
||
|
|
+
|
||
|
|
+ // then wait an event before turning on the actual camera
|
||
|
|
+ QTimer::singleShot(30, q, SLOT(checkState()));
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ if (camera && videoSink && !cameraStarted && scanRequest.get()) {
|
||
|
|
+ QCamera *cam = qobject_cast<QCamera *>(camera);
|
||
|
|
+ auto sink = qobject_cast<QVideoSink*>(videoSink);
|
||
|
|
+ if (!cam || !sink) { // here to detect bug in QML
|
||
|
|
+ logFatal(10005) << "invalid or no camera or sink object set";
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ if (cam->error() != QCamera::NoError)
|
||
|
|
+ logFatal(10005) << "CameraController found cam error:" << cam->errorString();
|
||
|
|
+
|
||
|
|
+ if (!preferredFormat.isNull())
|
||
|
|
+ cam->setCameraFormat(preferredFormat);
|
||
|
|
+ // best too least-acceptable focus mode.
|
||
|
|
+ constexpr QCamera::FocusMode f_modes[] = { QCamera::FocusModeAutoNear, QCamera::FocusModeAuto,
|
||
|
|
+ QCamera::FocusModeHyperfocal, QCamera::FocusModeInfinity };
|
||
|
|
+ for (auto m : f_modes) {
|
||
|
|
+ if (cam->isFocusModeSupported(m)) {
|
||
|
|
+ cam->setFocusMode(m);
|
||
|
|
+ break;
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ constexpr QCamera::WhiteBalanceMode w_modess[] = { QCamera::WhiteBalanceShade, QCamera::WhiteBalanceAuto };
|
||
|
|
+ for (auto m : w_modess) {
|
||
|
|
+ if (cam->isWhiteBalanceModeSupported(m)) {
|
||
|
|
+ cam->setWhiteBalanceMode(QCamera::WhiteBalanceAuto);
|
||
|
|
+ break;
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ constexpr QCamera::ExposureMode e_modes[] = { QCamera::ExposureBarcode,
|
||
|
|
+ QCamera::ExposurePortrait, QCamera::ExposureAuto };
|
||
|
|
+ for (auto m : e_modes) {
|
||
|
|
+ if (cam->isExposureModeSupported(m)) {
|
||
|
|
+ cam->setExposureMode(m);
|
||
|
|
+ break;
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ cameraStarted = true;
|
||
|
|
+ QObject::connect(sink, &QVideoSink::videoFrameChanged, q, [=](const QVideoFrame &frame) {
|
||
|
|
+ currentFrame = frame;
|
||
|
|
+ });
|
||
|
|
+
|
||
|
|
+ assert(scanningThread == nullptr);
|
||
|
|
+ scanningThread = new QRScanningThread(this);
|
||
|
|
+ QObject::connect (scanningThread, SIGNAL(finished()), q, SLOT(qrScanFinished()), Qt::QueuedConnection);
|
||
|
|
+ scanningThread->start();
|
||
|
|
+
|
||
|
|
+ logDebug(10005) << "Camera active is now true";
|
||
|
|
+ emit q->cameraActiveChanged(); // this emit makes QML activate the camera
|
||
|
|
+ }
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+// --------------------------------------------------------------------
|
||
|
|
+
|
||
|
|
+QRScanningThread::QRScanningThread(CameraControllerPrivate *parent)
|
||
|
|
+ : scanType(QRScanner::InvalidType),
|
||
|
|
+ m_parent(parent)
|
||
|
|
+{
|
||
|
|
+ m_decodeHints.setFormats(ZXing::BarcodeFormat::QRCode);
|
||
|
|
+ m_decodeHints.setTryHarder(true);
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void QRScanningThread::run()
|
||
|
|
+{
|
||
|
|
+ auto lastFrameScanned = 0;
|
||
|
|
+ while (true) {
|
||
|
|
+ const auto now = time(nullptr);
|
||
|
|
+ auto sleep = 34 - (now - lastFrameScanned); // assume 30 - FPS
|
||
|
|
+ // Sleep if we are too fast and (assuming 33.3 ms per frame) we would end up
|
||
|
|
+ // parsing the same frame twice.
|
||
|
|
+ if (sleep > 0)
|
||
|
|
+ QThread::msleep(sleep);
|
||
|
|
+
|
||
|
|
+ m_parent->lock.lock();
|
||
|
|
+ bool exit = !m_parent->cameraStarted;
|
||
|
|
+ QVideoFrame frame = m_parent->currentFrame;
|
||
|
|
+ m_parent->lock.unlock();
|
||
|
|
+ if (exit)
|
||
|
|
+ return;
|
||
|
|
+
|
||
|
|
+ lastFrameScanned = time(nullptr);
|
||
|
|
+ auto results = readBarcodes(frame);
|
||
|
|
+ for (const auto &result : results) {
|
||
|
|
+ const auto &bytes = result.bytes();
|
||
|
|
+ // logInfo(10005) << "result:" << QString::fromUtf8(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||
|
|
+
|
||
|
|
+ // Test if the QR held a Private key
|
||
|
|
+ // We support WIF encoded private keys.
|
||
|
|
+ // first, when it starts with 'bch-wif:' this helps, but needs to be cut off.
|
||
|
|
+ const bool wifPrefix = bytes.size() >= 58 && bytes.size() < 63
|
||
|
|
+ && (bytes[8] == 'K' || bytes[8] == 'L')
|
||
|
|
+ && 0 == memcmp(&bytes[0], "bch-wif:", 8);
|
||
|
|
+
|
||
|
|
+ if (wifPrefix || (bytes.size() >= 50 && bytes.size() < 55 && (bytes[0] == 'K' || bytes[0] == 'L'))) {
|
||
|
|
+ // might be one!!
|
||
|
|
+ const size_t prefixSize = wifPrefix ? 8 : 0;
|
||
|
|
+ const std::string str(reinterpret_cast<const char*>(bytes.data() + prefixSize), bytes.size() - prefixSize);
|
||
|
|
+ std::vector<uint8_t> dummy;
|
||
|
|
+ if (Base58::decodeCheck(str, dummy)) {
|
||
|
|
+ // good enough for me. Further checking is done by the app, we just exit scanning now.
|
||
|
|
+ scanType = QRScanner::PrivateKeyWIF;
|
||
|
|
+ text = QString::fromUtf8(str);
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ // Ok, what about a bip21 style url, or a plain address?
|
||
|
|
+ // -> starts with bitcoincash: (which is 12 chars, including that colon)
|
||
|
|
+ if (bytes.size() > 12 + 40 && memcmp("bitcoincash:", bytes.data(), 12) == 0) {
|
||
|
|
+ text = QString::fromUtf8(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||
|
|
+ scanType = QRScanner::PaymentDetails;
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ if (bytes.size() > 40 && bytes.size() < 45 && (bytes[0] == 'q' || bytes[0] == 'p')) {
|
||
|
|
+ // possibly a raw bitcoin cash address.
|
||
|
|
+ text = QString::fromUtf8(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||
|
|
+ scanType = QRScanner::PaymentDetails;
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ if (bytes.size() > 8 + 40 && memcmp("bchtest:", bytes.data(), 8) == 0) {
|
||
|
|
+ text = QString::fromUtf8(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||
|
|
+ scanType = QRScanner::PaymentDetailsTestnet;
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ // not those then, ok. Then check if its a seed :-)
|
||
|
|
+ /*
|
||
|
|
+ * The Seed QR would obviously just use the 12 or 24 words sentence in a QR.
|
||
|
|
+ * Simple.
|
||
|
|
+ *
|
||
|
|
+ * But a pretty big wallet has instead put a bit more in the QR which ends up having
|
||
|
|
+ * the following content:
|
||
|
|
+ *
|
||
|
|
+ * 1|this holds twelve words|livenet|m/44'/0'/0'|false
|
||
|
|
+ *
|
||
|
|
+ * No clue what the leading 1 and the trailing false are about. All wallets I tried
|
||
|
|
+ * have those :shrug:
|
||
|
|
+ *
|
||
|
|
+ * Lets try to recognize those two types of QR.
|
||
|
|
+ * ------
|
||
|
|
+ * While seeds can be verified with their checksum, here we just check if the general
|
||
|
|
+ * pattern fits.
|
||
|
|
+ * Only normal characters and spaces are allowed in the basic seed, and 12 words or 24 words.
|
||
|
|
+ */
|
||
|
|
+ if (bytes.size() > 12 * 3 + 11) { // at least enough chars for a seed.
|
||
|
|
+ QString possibleSeed = QString::fromUtf8(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||
|
|
+ if (possibleSeed.startsWith("1|") && possibleSeed.endsWith("|livenet|m/44'/0'/0'|false"))
|
||
|
|
+ possibleSeed = possibleSeed.mid(2, possibleSeed.length() - 28);
|
||
|
|
+
|
||
|
|
+ int wordCount = 0;
|
||
|
|
+ bool seenSpace = false;
|
||
|
|
+ bool failedChecks = false;
|
||
|
|
+ for (auto i = 0; i < possibleSeed.size(); ++i) {
|
||
|
|
+ auto c = possibleSeed.at(i);
|
||
|
|
+ if (c.isDigit() || c.isSymbol()) {
|
||
|
|
+ failedChecks = true;
|
||
|
|
+ }
|
||
|
|
+ else if (c.isSpace()) {
|
||
|
|
+ if (seenSpace || i == 0)
|
||
|
|
+ failedChecks = true; // double space or leading space
|
||
|
|
+ seenSpace = true;
|
||
|
|
+ ++wordCount;
|
||
|
|
+ }
|
||
|
|
+ else if (c.isLetter()) {
|
||
|
|
+ seenSpace = false;
|
||
|
|
+ }
|
||
|
|
+ if (failedChecks)
|
||
|
|
+ break;
|
||
|
|
+ }
|
||
|
|
+ if (seenSpace == false && wordCount > 0)
|
||
|
|
+ ++wordCount; // one more word not registered due to lack of space after.
|
||
|
|
+ if (!failedChecks && (wordCount == 12 || wordCount == 24)) {
|
||
|
|
+ scanType = QRScanner::Seed;
|
||
|
|
+ text = possibleSeed;
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+std::vector<ZXing::Result> QRScanningThread::readBarcodes(const QImage &img) const
|
||
|
|
+{
|
||
|
|
+ auto imageFormat = img.format();
|
||
|
|
+ if (imageFormat == QImage::Format_Invalid) // likely a damaged frame in the feed
|
||
|
|
+ return {};
|
||
|
|
+ auto zxImageFormat = ZXing::ImageFormat::None;
|
||
|
|
+ switch (imageFormat) {
|
||
|
|
+ case QImage::Format_ARGB32:
|
||
|
|
+ case QImage::Format_RGB32:
|
||
|
|
+#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||
|
|
+ zxImageFormat = ZXing::ImageFormat::BGRX;
|
||
|
|
+#else
|
||
|
|
+ zxImageFormat = ImageFormat::XRGB;
|
||
|
|
+#endif
|
||
|
|
+ break;
|
||
|
|
+ case QImage::Format_RGB888: zxImageFormat = ZXing::ImageFormat::RGB; break;
|
||
|
|
+ case QImage::Format_RGBX8888:
|
||
|
|
+ case QImage::Format_RGBA8888: zxImageFormat = ZXing::ImageFormat::RGBX; break;
|
||
|
|
+ case QImage::Format_Grayscale8: zxImageFormat = ZXing::ImageFormat::Lum; break;
|
||
|
|
+ default: break;
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ if (zxImageFormat == ZXing::ImageFormat::None) {
|
||
|
|
+ QImage gray = img.convertToFormat(QImage::Format_Grayscale8) ;
|
||
|
|
+ return ZXing::ReadBarcodes({gray.bits(), gray.width(), gray.height(), ZXing::ImageFormat::Lum, static_cast<int>(gray.bytesPerLine())}, m_decodeHints);
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ ZXing::ImageView buf(img.bits(), img.width(), img.height(), zxImageFormat, static_cast<int>(img.bytesPerLine()));
|
||
|
|
+ return ZXing::ReadBarcodes(buf, m_decodeHints);
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+std::vector<ZXing::Result> QRScanningThread::readBarcodes(QVideoFrame &frame) const
|
||
|
|
+{
|
||
|
|
+ ZXing::ImageFormat fmt = ZXing::ImageFormat::None;
|
||
|
|
+ int pixStride = 0;
|
||
|
|
+ int pixOffset = 0;
|
||
|
|
+
|
||
|
|
+ // note that the comments behind the values are the Qt5 formats.
|
||
|
|
+ switch (frame.pixelFormat()) {
|
||
|
|
+ case QVideoFrameFormat::Format_ARGB8888: // ARGB32
|
||
|
|
+ case QVideoFrameFormat::Format_ARGB8888_Premultiplied: // ARGB32_Premultiplied
|
||
|
|
+ case QVideoFrameFormat::Format_RGBX8888: // RGB32
|
||
|
|
+ fmt = ZXing::ImageFormat::BGRX;
|
||
|
|
+ break;
|
||
|
|
+ case QVideoFrameFormat::Format_BGRA8888: // BGRA32
|
||
|
|
+ case QVideoFrameFormat::Format_BGRA8888_Premultiplied: // BGRA32_Premultiplied
|
||
|
|
+ case QVideoFrameFormat::Format_BGRX8888: // BGR32
|
||
|
|
+#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||
|
|
+ fmt = ZXing::ImageFormat::RGBX;
|
||
|
|
+#else
|
||
|
|
+ fmt = ImageFormat::XBGR;
|
||
|
|
+#endif
|
||
|
|
+ break;
|
||
|
|
+ case QVideoFrameFormat::Format_P010:
|
||
|
|
+ case QVideoFrameFormat::Format_P016:
|
||
|
|
+ fmt = ZXing::ImageFormat::Lum, pixStride = 1; break;
|
||
|
|
+ case QVideoFrameFormat::Format_AYUV: // AYUV444
|
||
|
|
+ case QVideoFrameFormat::Format_AYUV_Premultiplied: // AYUV444_Premultiplied
|
||
|
|
+#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||
|
|
+ fmt = ZXing::ImageFormat::Lum, pixStride = 4, pixOffset = 3;
|
||
|
|
+#else
|
||
|
|
+ fmt = ImageFormat::Lum, pixStride = 4, pixOffset = 2;
|
||
|
|
+#endif
|
||
|
|
+ break;
|
||
|
|
+ case QVideoFrameFormat::Format_YUV420P: // YUV420P
|
||
|
|
+ case QVideoFrameFormat::Format_NV12: // NV12
|
||
|
|
+ case QVideoFrameFormat::Format_NV21: // NV21
|
||
|
|
+ case QVideoFrameFormat::Format_IMC1: // IMC1
|
||
|
|
+ case QVideoFrameFormat::Format_IMC2: // IMC2
|
||
|
|
+ case QVideoFrameFormat::Format_IMC3: // IMC3
|
||
|
|
+ case QVideoFrameFormat::Format_IMC4: // IMC4
|
||
|
|
+ case QVideoFrameFormat::Format_YV12: // YV12
|
||
|
|
+ case QVideoFrameFormat::Format_Y8: // Y8
|
||
|
|
+ case QVideoFrameFormat::Format_YUV422P: // YUV422P
|
||
|
|
+ fmt = ZXing::ImageFormat::Lum; break;
|
||
|
|
+ case QVideoFrameFormat::Format_UYVY: // UYVY
|
||
|
|
+ fmt = ZXing::ImageFormat::Lum, pixStride = 2, pixOffset = 1; break;
|
||
|
|
+ case QVideoFrameFormat::Format_YUYV: // YUYV
|
||
|
|
+ fmt = ZXing::ImageFormat::Lum, pixStride = 2; break;
|
||
|
|
+ case QVideoFrameFormat::Format_Y16: // Y16
|
||
|
|
+ fmt = ZXing::ImageFormat::Lum, pixStride = 2, pixOffset = 1; break;
|
||
|
|
+
|
||
|
|
+ case QVideoFrameFormat::Format_ABGR8888: // ABGR32
|
||
|
|
+#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||
|
|
+ fmt = ZXing::ImageFormat::RGBX;
|
||
|
|
+#else
|
||
|
|
+ fmt = ZXing::ImageFormat::XBGR;
|
||
|
|
+#endif
|
||
|
|
+ break;
|
||
|
|
+ default: break;
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ if (fmt != ZXing::ImageFormat::None) {
|
||
|
|
+ if (!frame.isValid() || !frame.map(QVideoFrame::ReadOnly)){
|
||
|
|
+ logDebug(10005) << "invalid QVideoFrame: could not map memory";
|
||
|
|
+ return {};
|
||
|
|
+ }
|
||
|
|
+ QScopeGuard unmap([&] { frame.unmap(); });
|
||
|
|
+
|
||
|
|
+ constexpr int FirstPlane = 0;
|
||
|
|
+ return ZXing::ReadBarcodes(
|
||
|
|
+ {frame.bits(FirstPlane) + pixOffset, frame.width(), frame.height(), fmt, frame.bytesPerLine(FirstPlane), pixStride}, m_decodeHints);
|
||
|
|
+ } else {
|
||
|
|
+ return readBarcodes(frame.toImage());
|
||
|
|
+ }
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+// --------------------------------------------------------------------
|
||
|
|
+
|
||
|
|
+CameraController::CameraController(QObject *parent)
|
||
|
|
+ : QObject(parent),
|
||
|
|
+ d(new CameraControllerPrivate(this))
|
||
|
|
+{
|
||
|
|
+ // The Android permissions requesting stuff returns results in a different thread,
|
||
|
|
+ // allow an easy way to move back to the main thread using a connection.
|
||
|
|
+ QObject::connect(this, SIGNAL(startCheckState()), this, SLOT(checkState()), Qt::QueuedConnection);
|
||
|
|
+ QObject::connect(this, SIGNAL(cameraPermissionReceived()), this, SLOT(handleCameraPermission()), Qt::QueuedConnection);
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::startRequest(QRScanner *request)
|
||
|
|
+{
|
||
|
|
+ assert(request);
|
||
|
|
+ d->scanRequest = request;
|
||
|
|
+ setHelpText(request->helpText());
|
||
|
|
+ emit isScanTypeChanged();
|
||
|
|
+
|
||
|
|
+ if (!d->visible) {
|
||
|
|
+ d->visible = true;
|
||
|
|
+ emit visibleChanged();
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ if (d->state == NotAsked || d->state == Denied) {
|
||
|
|
+#if TARGET_OS_Android
|
||
|
|
+ auto me = QJniObject(QNativeInterface::QAndroidApplication::context());
|
||
|
|
+ me.callObjectMethod("canUseCamera", "()V");
|
||
|
|
+#else
|
||
|
|
+ setCameraPermission(true);
|
||
|
|
+#endif
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ // give the overlay screen time to appear before
|
||
|
|
+ // activating the camera.
|
||
|
|
+ // This avoids waiting with the overlay screen until the camera
|
||
|
|
+ // is ready to stream which (depending on drivers) may take half a second.
|
||
|
|
+ QTimer::singleShot(1, this, SLOT(checkState()));
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::abortRequest(QRScanner *request)
|
||
|
|
+{
|
||
|
|
+ if (request && d->scanRequest == request) {
|
||
|
|
+ // The scanning thread will abort and nicely shutdown on change of this variable
|
||
|
|
+ d->lock.lock();
|
||
|
|
+ d->cameraStarted = false;
|
||
|
|
+ d->lock.unlock();
|
||
|
|
+ emit cameraActiveChanged();
|
||
|
|
+
|
||
|
|
+ if (d->scanningThread == nullptr) {
|
||
|
|
+ // then the above would have no effect;
|
||
|
|
+ qrScanFinished();
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::abort()
|
||
|
|
+{
|
||
|
|
+ abortRequest(d->scanRequest);
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+bool CameraController::isPayment() const
|
||
|
|
+{
|
||
|
|
+ if (d->scanRequest == nullptr)
|
||
|
|
+ return false;
|
||
|
|
+ return d->scanRequest->isPayment();
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+bool CameraController::torchEnabled() const
|
||
|
|
+{
|
||
|
|
+ return d->torchEnabled;
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::setTorchEnabled(bool on)
|
||
|
|
+{
|
||
|
|
+ if (d->torchEnabled == on)
|
||
|
|
+ return;
|
||
|
|
+ if (!d->cameraStarted) {
|
||
|
|
+ assert(d->torchEnabled == false);
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ QCamera *cam = qobject_cast<QCamera *>(d->camera);
|
||
|
|
+ if (cam == nullptr)
|
||
|
|
+ return;
|
||
|
|
+ if (cam->isTorchModeSupported(on ? QCamera::TorchOn : QCamera::TorchOff) == false) {
|
||
|
|
+ logWarning(10005) << "Trying to toggle torch, but the camera does not support that";
|
||
|
|
+ return;
|
||
|
|
+ }
|
||
|
|
+ d->torchEnabled = on;
|
||
|
|
+ cam->setTorchMode(on ? QCamera::TorchOn : QCamera::TorchOff);
|
||
|
|
+ logFatal(10005) << "toggling the torch";
|
||
|
|
+
|
||
|
|
+ if (on) {
|
||
|
|
+ if (cam->isWhiteBalanceModeSupported(QCamera::WhiteBalanceFlash))
|
||
|
|
+ cam->setWhiteBalanceMode(QCamera::WhiteBalanceFlash);
|
||
|
|
+ } else if (cam->whiteBalanceMode() == QCamera::WhiteBalanceFlash) {
|
||
|
|
+ // we should not just turn it off but also set it to the most appropriate normal mode.
|
||
|
|
+ constexpr QCamera::WhiteBalanceMode w_modess[] = { QCamera::WhiteBalanceShade, QCamera::WhiteBalanceAuto };
|
||
|
|
+ for (auto m : w_modess) {
|
||
|
|
+ if (cam->isWhiteBalanceModeSupported(m)) {
|
||
|
|
+ cam->setWhiteBalanceMode(QCamera::WhiteBalanceAuto);
|
||
|
|
+ break;
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ }
|
||
|
|
+ emit torchEnabledChanged();
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::setCamera(QObject *object)
|
||
|
|
+{
|
||
|
|
+ if (object == d->camera)
|
||
|
|
+ return;
|
||
|
|
+ d->camera = object;
|
||
|
|
+ emit cameraChanged();
|
||
|
|
+ QTimer::singleShot(10, this, SLOT(initCamera()));
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+QObject *CameraController::camera() const
|
||
|
|
+{
|
||
|
|
+ return d->camera;
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::setVideoSink(QObject *object)
|
||
|
|
+{
|
||
|
|
+ if (d->videoSink == object)
|
||
|
|
+ return;
|
||
|
|
+ auto old = qobject_cast<QVideoSink*>(d->videoSink);
|
||
|
|
+ if (old)
|
||
|
|
+ QObject::disconnect(old, nullptr, this, nullptr);
|
||
|
|
+ d->videoSink = object;
|
||
|
|
+ emit videoSinkChanged();
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+QObject *CameraController::videoSink() const
|
||
|
|
+{
|
||
|
|
+ return d->videoSink;
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+bool CameraController::loadCamera() const
|
||
|
|
+{
|
||
|
|
+ return d->cameraLoaded;
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+bool CameraController::cameraActive() const
|
||
|
|
+{
|
||
|
|
+ return d->cameraStarted;
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+bool CameraController::visible() const
|
||
|
|
+{
|
||
|
|
+ return d->visible;
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::qrScanFinished()
|
||
|
|
+{
|
||
|
|
+ QString resultText;
|
||
|
|
+ QRScanner::ScanType scanType = QRScanner::InvalidType;
|
||
|
|
+ if (d->scanningThread) {
|
||
|
|
+ resultText = d->scanningThread->text;
|
||
|
|
+ scanType = d->scanningThread->scanType;
|
||
|
|
+ d->scanningThread->deleteLater();
|
||
|
|
+ d->scanningThread = nullptr;
|
||
|
|
+ d->currentFrame = QVideoFrame();
|
||
|
|
+ }
|
||
|
|
+ // stop copying video frames
|
||
|
|
+ assert(d->videoSink);
|
||
|
|
+ QObject::disconnect(d->videoSink, nullptr, this, nullptr);
|
||
|
|
+
|
||
|
|
+ d->visible = false;
|
||
|
|
+ emit visibleChanged();
|
||
|
|
+ setHelpText(QString());
|
||
|
|
+ if (d->scanRequest) {
|
||
|
|
+ d->scanRequest->finishedScan(resultText, scanType);
|
||
|
|
+ d->scanRequest = nullptr;
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ QCamera *cam = qobject_cast<QCamera *>(d->camera);
|
||
|
|
+ if (cam)
|
||
|
|
+ cam->setTorchMode(QCamera::TorchOff);
|
||
|
|
+ if (d->torchEnabled) {
|
||
|
|
+ // don't use the simple setter as that one is doing much more.
|
||
|
|
+ d->torchEnabled = false;
|
||
|
|
+ emit torchEnabledChanged();
|
||
|
|
+ }
|
||
|
|
+ // Have a bit of delay with actually turning off the camera.
|
||
|
|
+ QTimer::singleShot(100, this, [=]() {
|
||
|
|
+ d->cameraStarted = false;
|
||
|
|
+ emit cameraActiveChanged(); // makes the QML 'stop()' the camera.
|
||
|
|
+ });
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::checkState()
|
||
|
|
+{
|
||
|
|
+ d->checkState();
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::initCamera()
|
||
|
|
+{
|
||
|
|
+ d->initCamera();
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::handleCameraPermission()
|
||
|
|
+{
|
||
|
|
+ if (d->state == Denied)
|
||
|
|
+ abort();
|
||
|
|
+ else
|
||
|
|
+ QTimer::singleShot(100, this, SLOT(checkState()));
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+QString CameraController::helpText() const
|
||
|
|
+{
|
||
|
|
+ return d->helpText;
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+QRScanner::ScanType CameraController::scanType() const
|
||
|
|
+{
|
||
|
|
+ if (d->scanRequest == nullptr)
|
||
|
|
+ return QRScanner::InvalidType;
|
||
|
|
+ return d->scanRequest->scanType();
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::setCameraPermission(bool allowed)
|
||
|
|
+{
|
||
|
|
+ // lets assume the native OS called this in a thread that is not us.
|
||
|
|
+ d->state = allowed ? Authorized : Denied;
|
||
|
|
+ emit cameraPermissionReceived();
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+void CameraController::setHelpText(const QString &text)
|
||
|
|
+{
|
||
|
|
+ if (d->helpText == text)
|
||
|
|
+ return;
|
||
|
|
+ d->helpText = text;
|
||
|
|
+ emit helpTextChanged();
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+#include "moc_CameraController.cpp"
|
||
|
|
diff --git a/src/QRCreator.cpp b/src/QRCreator.cpp
|
||
|
|
index 361ee18..67eee9c 100644
|
||
|
|
--- a/src/QRCreator.cpp
|
||
|
|
+++ b/src/QRCreator.cpp
|
||
|
|
@@ -19,9 +19,9 @@
|
||
|
|
|
||
|
|
#include <Logger.h>
|
||
|
|
// cmake ensures the presence of the ZXing lib.
|
||
|
|
-#include <ZXing/BarcodeFormat.h>
|
||
|
|
-#include <ZXing/MultiFormatWriter.h>
|
||
|
|
-#include <ZXing/BitMatrix.h>
|
||
|
|
+#include <ZXing/ZXingCpp.h>
|
||
|
|
+#include <ZXing/CreateBarcode.h>
|
||
|
|
+#include <ZXing/WriteBarcode.h>
|
||
|
|
|
||
|
|
#include <codecvt>
|
||
|
|
#include <locale>
|
||
|
|
@@ -43,29 +43,8 @@ QImage QRCreator::requestImage(const QString &id, QSize *size, const QSize &requ
|
||
|
|
} else if (m_type == RawString) {
|
||
|
|
data = id.toUtf8();
|
||
|
|
}
|
||
|
|
-
|
||
|
|
- auto writer = ZXing::MultiFormatWriter(ZXing::BarcodeFormat::QRCode).setMargin(16);
|
||
|
|
- /*
|
||
|
|
- * In newer versions of zxing there is a direct std::string version of the encode()
|
||
|
|
- * call which is nice to avoid the extra coversion.
|
||
|
|
- * For now we leave this extra code here as long as we are still able to compile and
|
||
|
|
- * run on Ubuntu 2022.04 (jammy) which doesn't have this new call.
|
||
|
|
- *
|
||
|
|
- * Ironically, the codecvt below code is deprecated in C++17, so you get warnings now.
|
||
|
|
- * Can't win this one, I guess...
|
||
|
|
- * But the promise is that it will be part of C++ till the 2026 release,
|
||
|
|
- * and I prefer it actually compiling on older zxing. So maybe lets just
|
||
|
|
- * plan to remove the wstring conversion in a year or so (TZ: Feb 2024)
|
||
|
|
- */
|
||
|
|
- std::wstring wdata = std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(data);
|
||
|
|
- ZXing::BitMatrix matrix = writer.encode(wdata, 250, 250);
|
||
|
|
-
|
||
|
|
- QImage result = QImage(matrix.height(), matrix.width(), QImage::Format_RGB32);
|
||
|
|
- constexpr uint black = 0xFF000000;
|
||
|
|
- constexpr uint white = 0xFFFFFFFF;
|
||
|
|
- for (int y = 0; y < matrix.height(); ++y)
|
||
|
|
- for (int x = 0; x < matrix.width(); ++x)
|
||
|
|
- result.setPixel(x, y, matrix.get(x, y) ? black : white);
|
||
|
|
-
|
||
|
|
- return result;
|
||
|
|
+ auto barcode = ZXing::CreateBarcodeFromText(data, ZXing::BarcodeFormat::QRCode);
|
||
|
|
+ auto image = ZXing::WriteBarcodeToImage(barcode);
|
||
|
|
+ QImage result(image.data(), image.width(), image.height(), image.width(), QImage::Format_Grayscale8);
|
||
|
|
+ return result.copy();
|
||
|
|
}
|
||
|
|
diff --git a/src/QRCreator_zxing2.cpp b/src/QRCreator_zxing2.cpp
|
||
|
|
new file mode 100644
|
||
|
|
index 0000000..361ee18
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/src/QRCreator_zxing2.cpp
|
||
|
|
@@ -0,0 +1,71 @@
|
||
|
|
+/*
|
||
|
|
+ * This file is part of the Flowee project
|
||
|
|
+ * Copyright (C) 2018-2024 Tom Zander <tom@flowee.org>
|
||
|
|
+ *
|
||
|
|
+ * This program is free software: you can redistribute it and/or modify
|
||
|
|
+ * it under the terms of the GNU General Public License as published by
|
||
|
|
+ * the Free Software Foundation, either version 3 of the License, or
|
||
|
|
+ * (at your option) any later version.
|
||
|
|
+ *
|
||
|
|
+ * This program is distributed in the hope that it will be useful,
|
||
|
|
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
+ * GNU General Public License for more details.
|
||
|
|
+ *
|
||
|
|
+ * You should have received a copy of the GNU General Public License
|
||
|
|
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
+ */
|
||
|
|
+#include "QRCreator.h"
|
||
|
|
+
|
||
|
|
+#include <Logger.h>
|
||
|
|
+// cmake ensures the presence of the ZXing lib.
|
||
|
|
+#include <ZXing/BarcodeFormat.h>
|
||
|
|
+#include <ZXing/MultiFormatWriter.h>
|
||
|
|
+#include <ZXing/BitMatrix.h>
|
||
|
|
+
|
||
|
|
+#include <codecvt>
|
||
|
|
+#include <locale>
|
||
|
|
+
|
||
|
|
+QRCreator::QRCreator(QRType type)
|
||
|
|
+ : QQuickImageProvider(QQmlImageProviderBase::Image),
|
||
|
|
+ m_type(type)
|
||
|
|
+{
|
||
|
|
+}
|
||
|
|
+
|
||
|
|
+QImage QRCreator::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
|
||
|
|
+{
|
||
|
|
+ Q_UNUSED(size);
|
||
|
|
+ Q_UNUSED(requestedSize);
|
||
|
|
+ std::string data; // assumed utf8
|
||
|
|
+ if (m_type == URLEncoded) {
|
||
|
|
+ QUrl url(id); // go via URL to encode spaces and special chars
|
||
|
|
+ data = url.toEncoded(QUrl::EncodeSpaces);
|
||
|
|
+ } else if (m_type == RawString) {
|
||
|
|
+ data = id.toUtf8();
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ auto writer = ZXing::MultiFormatWriter(ZXing::BarcodeFormat::QRCode).setMargin(16);
|
||
|
|
+ /*
|
||
|
|
+ * In newer versions of zxing there is a direct std::string version of the encode()
|
||
|
|
+ * call which is nice to avoid the extra coversion.
|
||
|
|
+ * For now we leave this extra code here as long as we are still able to compile and
|
||
|
|
+ * run on Ubuntu 2022.04 (jammy) which doesn't have this new call.
|
||
|
|
+ *
|
||
|
|
+ * Ironically, the codecvt below code is deprecated in C++17, so you get warnings now.
|
||
|
|
+ * Can't win this one, I guess...
|
||
|
|
+ * But the promise is that it will be part of C++ till the 2026 release,
|
||
|
|
+ * and I prefer it actually compiling on older zxing. So maybe lets just
|
||
|
|
+ * plan to remove the wstring conversion in a year or so (TZ: Feb 2024)
|
||
|
|
+ */
|
||
|
|
+ std::wstring wdata = std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(data);
|
||
|
|
+ ZXing::BitMatrix matrix = writer.encode(wdata, 250, 250);
|
||
|
|
+
|
||
|
|
+ QImage result = QImage(matrix.height(), matrix.width(), QImage::Format_RGB32);
|
||
|
|
+ constexpr uint black = 0xFF000000;
|
||
|
|
+ constexpr uint white = 0xFFFFFFFF;
|
||
|
|
+ for (int y = 0; y < matrix.height(); ++y)
|
||
|
|
+ for (int x = 0; x < matrix.width(); ++x)
|
||
|
|
+ result.setPixel(x, y, matrix.get(x, y) ? black : white);
|
||
|
|
+
|
||
|
|
+ return result;
|
||
|
|
+}
|
||
|
|
--
|
||
|
|
2.53.0
|
||
|
|
|