From d94905fe65416d10abd69f2c32201382107afa6d Mon Sep 17 00:00:00 2001 From: TomZ Date: Mon, 16 Feb 2026 15:42:29 +0100 Subject: [PATCH] Add zxing3 support This backports a post release fix to support the latest ZXing, which came out just when the latest Pay release was made. --- .SRCINFO | 2 + 0001-Port-to-new-ZXing-version-3.patch | 1156 ++++++++++++++++++++++++ PKGBUILD | 3 + 3 files changed, 1161 insertions(+) create mode 100644 0001-Port-to-new-ZXing-version-3.patch diff --git a/.SRCINFO b/.SRCINFO index b35fc84..2f3e21b 100644 --- a/.SRCINFO +++ b/.SRCINFO @@ -22,9 +22,11 @@ pkgbase = flowee-pay options = !lto source = https://codeberg.org/Flowee/pay/archive/2026.02.1.tar.gz source = 0001-Fix-off-by-one-in-unit-test.patch + source = 0001-Port-to-new-ZXing-version-3.patch source = https://flowee.org/products/pay/blockheaders-850000 sha256sums = a3a8443e6236498fa384478366c8b35dea5c7cec3b8b9b06d5b0ba9a835d2b95 sha256sums = f7e4bf13406b1836fb0e80b97f01d8f5098b3c4c9de230ac5463c009a5019316 + sha256sums = 2a31e641e19432c77b467876fce30517378fd1dd646ad05e1d53d1476cf99ebf sha256sums = 4a98c3b655cfd7520b4d4f682d95e3a82e0f03fda4fa687d28f2127205d66047 pkgname = flowee-pay diff --git a/0001-Port-to-new-ZXing-version-3.patch b/0001-Port-to-new-ZXing-version-3.patch new file mode 100644 index 0000000..8aa4be2 --- /dev/null +++ b/0001-Port-to-new-ZXing-version-3.patch @@ -0,0 +1,1156 @@ +From 3d2911eb08a34f5df202ef1d9edee575ad89abaf Mon Sep 17 00:00:00 2001 +From: TomZ +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 ++# Copyright (C) 2020-2026 Tom Zander + # + # 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 +- * Copyright (C) 2020 Axel Waggershauser ++ * Copyright (C) 2022-2026 Tom Zander + * + * 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 + +-#include +- + #include + #include + #include +@@ -34,6 +31,10 @@ + #include + #include + ++#include ++// 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 readBarcodes(const QImage &img) const; + std::vector 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(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 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(gray.bytesPerLine())}, m_decodeHints); +- } +- +- ZXing::ImageView buf(img.bits(), img.width(), img.height(), zxImageFormat, static_cast(img.bytesPerLine())); +- return ZXing::ReadBarcodes(buf, m_decodeHints); +-} +- +-std::vector 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 ++ * Copyright (C) 2020 Axel Waggershauser ++ * ++ * 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 . ++ */ ++#include "CameraController.h" ++#include "QRScanner.h" ++#include "base58.h" ++#include ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++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 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 readBarcodes(const QImage &img) const; ++ std::vector 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(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(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(camera); ++ auto sink = qobject_cast(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(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(bytes.data() + prefixSize), bytes.size() - prefixSize); ++ std::vector 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(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(bytes.data()), bytes.size()); ++ scanType = QRScanner::PaymentDetails; ++ return; ++ } ++ if (bytes.size() > 8 + 40 && memcmp("bchtest:", bytes.data(), 8) == 0) { ++ text = QString::fromUtf8(reinterpret_cast(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(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 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(gray.bytesPerLine())}, m_decodeHints); ++ } ++ ++ ZXing::ImageView buf(img.bits(), img.width(), img.height(), zxImageFormat, static_cast(img.bytesPerLine())); ++ return ZXing::ReadBarcodes(buf, m_decodeHints); ++} ++ ++std::vector 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(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(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(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 + // cmake ensures the presence of the ZXing lib. +-#include +-#include +-#include ++#include ++#include ++#include + + #include + #include +@@ -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>().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 ++ * ++ * 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 . ++ */ ++#include "QRCreator.h" ++ ++#include ++// cmake ensures the presence of the ZXing lib. ++#include ++#include ++#include ++ ++#include ++#include ++ ++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>().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 + diff --git a/PKGBUILD b/PKGBUILD index 4560c6c..8d89b51 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -14,9 +14,11 @@ provides=('flowee-pay') install=flowee-pay.install source=("https://codeberg.org/Flowee/pay/archive/$pkgver.tar.gz" "0001-Fix-off-by-one-in-unit-test.patch" + "0001-Port-to-new-ZXing-version-3.patch" "https://flowee.org/products/pay/blockheaders-850000") sha256sums=('a3a8443e6236498fa384478366c8b35dea5c7cec3b8b9b06d5b0ba9a835d2b95' 'f7e4bf13406b1836fb0e80b97f01d8f5098b3c4c9de230ac5463c009a5019316' + '2a31e641e19432c77b467876fce30517378fd1dd646ad05e1d53d1476cf99ebf' '4a98c3b655cfd7520b4d4f682d95e3a82e0f03fda4fa687d28f2127205d66047') build() { @@ -30,6 +32,7 @@ build() { prepare() { cd "$srcdir/pay" patch -Np1 -i ../0001-Fix-off-by-one-in-unit-test.patch + patch -Np1 -i ../0001-Port-to-new-ZXing-version-3.patch } check() {