diff --git a/.editorconfig b/.editorconfig index c643beb33d..50d16bc2ce 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,9 +5,10 @@ insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 -[*.{cpp,c,h,html,py}] +[*.{cpp,c,h,html,py,kt}] indent_style = tab indent_size = 4 +max_line_length = 135 [CMakeLists.txt] indent_style = space diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c26a4243f4..d476f125a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,26 +80,29 @@ jobs: with: ref: ${{github.ref}} - - name: Build the ZXingCpp.xcframework + - name: Build the swift package shell: sh - working-directory: ${{runner.workspace}}/${{github.event.repository.name}}/wrappers/ios - run: ./build-release.sh - - - name: Upload .xcframework - uses: actions/upload-artifact@v3 - with: - name: ios-artifacts - path: ${{runner.workspace}}/${{github.event.repository.name}}/wrappers/ios/ZXingCpp.xcframework + working-directory: ${{runner.workspace}}/${{github.event.repository.name}} + run: swift build - name: Build the demo app shell: sh working-directory: ${{runner.workspace}}/${{github.event.repository.name}}/wrappers/ios/demo run: xcodebuild build -scheme demo -sdk "iphonesimulator" + - name: Validate the Pod + shell: sh + working-directory: ${{runner.workspace}}/${{github.event.repository.name}} + run: pod lib lint --allow-warnings + build-android: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' - name: Build the lib/app working-directory: wrappers/android @@ -136,7 +139,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.12'] os: [ubuntu-latest, macos-latest, windows-latest] steps: @@ -150,7 +153,7 @@ jobs: - name: Install dependencies working-directory: wrappers/python run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip setuptools python -m pip install numpy pillow - name: Build module diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3937ed1f5f..96f35f4209 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,12 +25,12 @@ on: workflow_dispatch jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} strategy: fail-fast: false matrix: - language: [ 'cpp' ] + language: [ 'c-cpp', 'python' ] # , 'java-kotlin', 'swift' currently fail with autobuild # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed @@ -67,3 +67,5 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/msvc-analysis.yml b/.github/workflows/msvc-analysis.yml index 269f231f7e..4c9342c845 100644 --- a/.github/workflows/msvc-analysis.yml +++ b/.github/workflows/msvc-analysis.yml @@ -55,7 +55,8 @@ jobs: buildConfiguration: ${{ env.config }} # Ruleset file that will determine what checks will be run ruleset: NativeRecommendedRules.ruleset - additionalArgs: /wd26451 # Suppress C26451, apparently bogous in VS2019 + # additionalArgs: /wd26451 # Suppress C26451, apparently bogous in VS2019 + # still fails, see https://github.com/microsoft/msvc-code-analysis-action/issues/31 # Upload SARIF file to GitHub Code Scanning Alerts - name: Upload SARIF to GitHub diff --git a/.github/workflows/publish-android.yml b/.github/workflows/publish-android.yml new file mode 100644 index 0000000000..004b41caf9 --- /dev/null +++ b/.github/workflows/publish-android.yml @@ -0,0 +1,31 @@ +name: publish-adroid + +on: +# release: +# types: [published] + + workflow_dispatch: + inputs: + publish: + description: 'Publish package (y/n)' + default: 'n' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' +# working-directory: wrappers/android +# run: ./gradlew versionDisplay + - name: Publish Library + working-directory: wrappers/android + env: + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_GPG_SECRET_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} + ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.OSSRH_USERNAME }} + ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.OSSRH_PASSWORD }} + run: ./gradlew publishRleasePublicationsToSonatypeRepository diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index 607d6b4ccb..2c67d28a69 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -30,15 +30,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.12' - name: Install cibuildwheel - run: python3 -m pip install cibuildwheel==2.12.1 + run: python3 -m pip install cibuildwheel==2.16.2 - name: Build wheels run: python3 -m cibuildwheel --output-dir wheelhouse wrappers/python env: - CIBW_BUILD: cp39-* cp310-* cp311-* + CIBW_BUILD: cp38-* cp39-* cp310-* cp311-* cp312-* CIBW_SKIP: "*musllinux*" CIBW_ARCHS_MACOS: universal2 CIBW_ENVIRONMENT_MACOS: CMAKE_OSX_ARCHITECTURES="arm64;x86_64" @@ -58,7 +58,11 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.12' + + - name: Install dependencies + working-directory: wrappers/python + run: python -m pip install --upgrade pip setuptools - name: Build sdist working-directory: wrappers/python @@ -84,5 +88,5 @@ jobs: - uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ - password: ${{ secrets.PYPI_TOKEN }} - # To test: repository_url: https://test.pypi.org/legacy/ + password: ${{ secrets.TEST_PYPI_TOKEN }} + repository_url: https://test.pypi.org/legacy/ diff --git a/.gitignore b/.gitignore index 9992cb687a..53a44bff88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,19 @@ -CMakeLists.txt.user *.o *.so *.lib *.d *.a compile_commands.json + +# QtCreator +CMakeLists.txt.user + +# Vim +*.swp + +# XCode/Swift +.swiftpm +.build + +# JetBrain (AndroidStudio, clion) +.idea diff --git a/CMakeLists.txt b/CMakeLists.txt index 87785add27..7c6cc67d41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,10 @@ if (BUILD_UNIT_TESTS AND (NOT BUILD_WRITERS OR NOT BUILD_READERS)) set (BUILD_READERS ON) endif() +if (BUILD_EXPERIMENTAL_API) + add_definitions (-DZXING_BUILD_EXPERIMENTAL_API) +endif() + set(BUILD_DEPENDENCIES_LIST AUTO GITHUB LOCAL) set_property(CACHE BUILD_DEPENDENCIES PROPERTY STRINGS ${BUILD_DEPENDENCIES_LIST}) if(NOT BUILD_DEPENDENCIES IN_LIST BUILD_DEPENDENCIES_LIST) diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000000..27e70a9bc2 --- /dev/null +++ b/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version:5.7.1 +import PackageDescription + +let package = Package( + name: "ZXingCpp", + platforms: [ + .iOS(.v11) + ], + products: [ + .library( + name: "ZXingCpp", + targets: ["ZXingCpp"]) + ], + targets: [ + .target( + name: "ZXingCppCore", + path: "core/src", + publicHeadersPath: "." + ), + .target( + name: "ZXingCpp", + dependencies: ["ZXingCppCore"], + path: "wrappers/ios/Sources/Wrapper", + publicHeadersPath: ".", + linkerSettings: [ + .linkedFramework("CoreGraphics"), + .linkedFramework("CoreImage"), + .linkedFramework("CoreVideo") + ] + ) + ], + cxxLanguageStandard: CXXLanguageStandard.gnucxx20 +) diff --git a/README.md b/README.md index f47b910504..1054cb2fae 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ZXing-C++ ("zebra crossing") is an open-source, multi-format linear/matrix barcode image processing library implemented in C++. -It was originally ported from the Java [ZXing Library](https://github.com/zxing/zxing) but has been developed further and now includes many improvements in terms of quality and performance. It can both read and write barcodes in a number of formats. +It was originally ported from the Java [ZXing Library](https://github.com/zxing/zxing) but has been developed further and now includes many improvements in terms of runtime and detection performance. It can both read and write barcodes in a number of formats. ## Sponsors @@ -24,6 +24,7 @@ Thanks a lot for your contribution! * Thread safe * Wrappers/Bindings for: * [Android](wrappers/android/README.md) + * [C](wrappers/c/README.md) * [iOS](wrappers/ios/README.md) * [Python](wrappers/python/README.md) * [WebAssembly](wrappers/wasm/README.md) @@ -36,29 +37,53 @@ Thanks a lot for your contribution! |----------------|-------------------|--------------------| | UPC-A | Code 39 | QR Code | | UPC-E | Code 93 | Micro QR Code | -| EAN-8 | Code 128 | Aztec | -| EAN-13 | Codabar | DataMatrix | -| DataBar | DataBar Expanded | PDF417 | -| | ITF | MaxiCode (partial) | +| EAN-8 | Code 128 | rMQR Code | +| EAN-13 | Codabar | Aztec | +| DataBar | DataBar Expanded | DataMatrix | +| | ITF | PDF417 | +| | | MaxiCode (partial) | [Note:] * DataBar used to be called RSS. - * DataBar, MaxiCode and Micro QR Code are not supported for writing. - * Building with C++20 (see [CMakeLists.txt](https://github.com/zxing-cpp/zxing-cpp/blob/d4b0f502775857f257d13efd25fb840ece1bca3e/CMakeLists.txt#L45)) changes the behaviour of the library: it then supports multi-symbol and position independent detection for DataMatrix. This comes at a noticable performace cost. To enable this in the Android library, one needs to use at least NDK [version 25](https://github.com/zxing-cpp/zxing-cpp/blob/d4b0f502775857f257d13efd25fb840ece1bca3e/wrappers/android/zxingcpp/build.gradle#L9). + * DataBar, MaxiCode, Micro QR Code and rMQR Code are not supported for writing. + * Building with C++20 (see [CMakeLists.txt](https://github.com/zxing-cpp/zxing-cpp/blob/d4b0f502775857f257d13efd25fb840ece1bca3e/CMakeLists.txt#L45)) changes the behaviour of the library: it then supports multi-symbol and position independent detection for DataMatrix. This comes at a noticable performace cost. C++20 is enabled by default for the Android, iOS, Python and WASM wrappers. ## Getting Started ### To read barcodes: -As an example, have a look at [`ZXingReader.cpp`](example/ZXingReader.cpp). 1. Load your image into memory (3rd-party library required). 2. Call `ReadBarcodes()` from [`ReadBarcode.h`](core/src/ReadBarcode.h), the simplest API to get a list of `Result` objects. +A very simple example looks like this: +```c++ +#include "ZXing/ReadBarcode.h" +#include + +int main(int argc, char** argv) +{ + int width, height; + unsigned char* data; + // load your image data from somewhere. ImageFormat::Lum assumes grey scale image data. + + auto image = ZXing::ImageView(data, width, height, ZXing::ImageFormat::Lum); + auto options = ZXing::ReaderOptions().setFormats(ZXing::BarcodeFormat::Any); + auto results = ZXing::ReadBarcodes(image, options); + + for (const auto& r : results) + std::cout << ZXing::ToString(r.format()) << ": " << r.text() << "\n"; + + return 0; +} +``` +To see the full capability of the API, have a look at [`ZXingReader.cpp`](example/ZXingReader.cpp). + ### To write barcodes: -As an example, have a look at [`ZXingWriter.cpp`](example/ZXingWriter.cpp). 1. Create a [`MultiFormatWriter`](core/src/MultiFormatWriter.h) instance with the format you want to generate. Set encoding and margins if needed. 2. Call `encode()` with text content and the image size. This returns a [`BitMatrix`](core/src/BitMatrix.h) which is a binary image of the barcode where `true` == visual black and `false` == visual white. 3. Convert the bit matrix to your native image format. See also the `ToMatrix(BitMatrix&)` helper function. +As an example, have a look at [`ZXingWriter.cpp`](example/ZXingWriter.cpp). + ## Web Demos - [Read barcodes](https://zxing-cpp.github.io/zxing-cpp/demo_reader.html) - [Write barcodes](https://zxing-cpp.github.io/zxing-cpp/demo_writer.html) @@ -76,11 +101,11 @@ These are the generic instructions to build the library on Windows/macOS/Linux. ``` git clone https://github.com/zxing-cpp/zxing-cpp.git --single-branch --depth 1 cmake -S zxing-cpp -B zxing-cpp.release -DCMAKE_BUILD_TYPE=Release -cmake --build zxing-cpp.release -j8 +cmake --build zxing-cpp.release -j8 --config Release ``` [Note: binary packages are available for/as [vcpkg](https://github.com/Microsoft/vcpkg/tree/master/ports/nu-book-zxing-cpp), [conan](https://github.com/conan-io/conan-center-index/tree/master/recipes/zxing-cpp), [mingw](https://github.com/msys2/MINGW-packages/tree/master/mingw-w64-zxing-cpp) and a bunch of -[linux distributions](https://repology.org/project/zxing-cpp-nu-book/versions).] +[linux distributions](https://repology.org/project/zxing-cpp/versions).] diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 24cebd93ab..0fe3bb6abe 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.15) -project (ZXing VERSION "2.1.0") +project (ZXing VERSION "2.2.0") set (ZXING_SONAME 3) # see https://github.com/zxing-cpp/zxing-cpp/issues/333 include(../zxing.cmake) @@ -22,12 +22,16 @@ if (WINRT) -DWINRT ) endif() +if (MSVC) + set (ZXING_CORE_DEFINES ${ZXING_CORE_DEFINES} + /Zc:__cplusplus + ) +endif() set (ZXING_CORE_LOCAL_DEFINES $<$:-DZXING_BUILD_READERS> $<$:-DZXING_BUILD_WRITERS> $<$:-DZXING_BUILD_FOR_TEST> - $<$:-DZXING_BUILD_EXPERIMENTAL_API> ) if (MSVC) set (ZXING_CORE_LOCAL_DEFINES ${ZXING_CORE_LOCAL_DEFINES} @@ -35,7 +39,6 @@ if (MSVC) -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_WARNINGS -DNOMINMAX - /Zc:__cplusplus ) else() set (ZXING_CORE_LOCAL_DEFINES ${ZXING_CORE_LOCAL_DEFINES} @@ -102,7 +105,6 @@ if (BUILD_READERS) src/Content.h src/Content.cpp src/DecodeHints.h - src/DecodeHints.cpp src/DecoderResult.h src/DetectorResult.h src/Error.h @@ -120,6 +122,7 @@ if (BUILD_READERS) src/PerspectiveTransform.h src/PerspectiveTransform.cpp src/Reader.h + src/ReaderOptions.h src/ReadBarcode.h src/ReadBarcode.cpp src/ReedSolomonDecoder.h @@ -169,6 +172,7 @@ if (BUILD_READERS) src/Point.h src/Quadrilateral.h src/ReadBarcode.h + src/ReaderOptions.h src/Result.h src/StructuredAppend.h ) @@ -479,23 +483,6 @@ endif() set_target_properties(ZXing PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADERS}") -if (APPLE AND BUILD_APPLE_FRAMEWORK) - set_target_properties(ZXing PROPERTIES - FRAMEWORK TRUE - FRAMEWORK_VERSION "C" - XCODE_ATTRIBUTE_DEFINES_MODULE YES - XCODE_ATTRIBUTE_BUILD_LIBRARY_FOR_DISTRIBUTION YES - XCODE_ATTRIBUTE_MODULEMAP_FILE "wrappers/ios/Sources/Wrapper/module.modulemap" - XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES - MACOSX_FRAMEWORK_IDENTIFIER "com.zxing-cpp.ios" - CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH NO - #MACOSX_FRAMEWORK_INFO_PLIST Info.plist - PUBLIC_HEADER "${PUBLIC_HEADERS}" - XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer" - XCODE_ATTRIBUTE_ENABLE_BITCODE "NO" - ) -endif() - include (GNUInstallDirs) set(ZX_INSTALL_TARGETS ZXing) diff --git a/core/src/BarcodeFormat.cpp b/core/src/BarcodeFormat.cpp index 58db3decce..01f4d3860e 100644 --- a/core/src/BarcodeFormat.cpp +++ b/core/src/BarcodeFormat.cpp @@ -39,6 +39,7 @@ static BarcodeFormatName NAMES[] = { {BarcodeFormat::MicroQRCode, "MicroQRCode"}, {BarcodeFormat::PDF417, "PDF417"}, {BarcodeFormat::QRCode, "QRCode"}, + {BarcodeFormat::RMQRCode, "rMQRCode"}, {BarcodeFormat::UPCA, "UPC-A"}, {BarcodeFormat::UPCE, "UPC-E"}, {BarcodeFormat::LinearCodes, "Linear-Codes"}, diff --git a/core/src/BarcodeFormat.h b/core/src/BarcodeFormat.h index 3d3a4c326e..72542ed363 100644 --- a/core/src/BarcodeFormat.h +++ b/core/src/BarcodeFormat.h @@ -1,4 +1,4 @@ -/* +/* * Copyright 2016 Nu-book Inc. * Copyright 2016 ZXing authors */ @@ -39,12 +39,13 @@ enum class BarcodeFormat UPCA = (1 << 14), ///< UPC-A UPCE = (1 << 15), ///< UPC-E MicroQRCode = (1 << 16), ///< Micro QR Code + RMQRCode = (1 << 17), ///< Rectangular Micro QR Code LinearCodes = Codabar | Code39 | Code93 | Code128 | EAN8 | EAN13 | ITF | DataBar | DataBarExpanded | UPCA | UPCE, - MatrixCodes = Aztec | DataMatrix | MaxiCode | PDF417 | QRCode | MicroQRCode, + MatrixCodes = Aztec | DataMatrix | MaxiCode | PDF417 | QRCode | MicroQRCode | RMQRCode, Any = LinearCodes | MatrixCodes, - _max = MicroQRCode, ///> implementation detail, don't use + _max = RMQRCode, ///> implementation detail, don't use }; ZX_DECLARE_FLAGS(BarcodeFormats, BarcodeFormat) diff --git a/core/src/BitArray.h b/core/src/BitArray.h index 8e814b5a19..a260b3a3b8 100644 --- a/core/src/BitArray.h +++ b/core/src/BitArray.h @@ -158,7 +158,7 @@ class BitArrayView BitArrayView& skipBits(int n) { - if (n > bits.size()) + if (cur + n > bits.end()) throw std::out_of_range("BitArrayView::skipBits() out of range."); cur += n; return *this; @@ -167,7 +167,7 @@ class BitArrayView int peakBits(int n) const { assert(n <= 32); - if (n > bits.size()) + if (cur + n > bits.end()) throw std::out_of_range("BitArrayView::peakBits() out of range."); int res = 0; for (auto i = cur; n > 0; --n, i++) diff --git a/core/src/BitMatrix.h b/core/src/BitMatrix.h index c063d0e0ac..6d7dd54c9e 100644 --- a/core/src/BitMatrix.h +++ b/core/src/BitMatrix.h @@ -153,7 +153,7 @@ void GetPatternRow(const BitMatrix& matrix, int r, std::vector& pr, bo /** * @brief Inflate scales a BitMatrix up and adds a quiet Zone plus padding - * @param matrix input to be expanded + * @param input matrix to be expanded * @param width new width in bits (pixel) * @param height new height in bits (pixel) * @param quietZone size of quiet zone to add in modules @@ -163,7 +163,7 @@ BitMatrix Inflate(BitMatrix&& input, int width, int height, int quietZone); /** * @brief Deflate (crop + subsample) a bit matrix - * @param matrix + * @param input matrix to be shrinked * @param width new width * @param height new height * @param top cropping starts at top row @@ -171,7 +171,7 @@ BitMatrix Inflate(BitMatrix&& input, int width, int height, int quietZone); * @param subSampling typically the module size * @return deflated input */ -BitMatrix Deflate(const BitMatrix& matrix, int width, int height, float top, float left, float subSampling); +BitMatrix Deflate(const BitMatrix& input, int width, int height, float top, float left, float subSampling); template BitMatrix ToBitMatrix(const Matrix& in, T trueValue = {true}) diff --git a/core/src/BitMatrixCursor.h b/core/src/BitMatrixCursor.h index bdfc523ed6..25cc95cb13 100644 --- a/core/src/BitMatrixCursor.h +++ b/core/src/BitMatrixCursor.h @@ -168,7 +168,7 @@ class BitMatrixCursor template ARRAY readPattern(int range = 0) { - ARRAY res; + ARRAY res = {}; for (auto& i : res) { i = stepToEdge(1, range); if (!i) diff --git a/core/src/BitMatrixIO.cpp b/core/src/BitMatrixIO.cpp index 685e9cf236..08678084af 100644 --- a/core/src/BitMatrixIO.cpp +++ b/core/src/BitMatrixIO.cpp @@ -6,6 +6,7 @@ #include "BitMatrixIO.h" +#include #include #include @@ -30,6 +31,23 @@ std::string ToString(const BitMatrix& matrix, char one, char zero, bool addSpace return result; } +std::string ToString(const BitMatrix& matrix, bool inverted) +{ + constexpr auto map = std::array{" ", "▀", "▄", "█"}; + std::string res; + + for (int y = 0; y < matrix.height(); y += 2) { + for (int x = 0; x < matrix.width(); ++x) { + int tp = matrix.get(x, y) ^ inverted; + int bt = (matrix.height() == 1 && tp) || (y + 1 < matrix.height() && (matrix.get(x, y + 1) ^ inverted)); + res += map[tp | (bt << 1)]; + } + res.push_back('\n'); + } + + return res; +} + std::string ToSVG(const BitMatrix& matrix) { // see https://stackoverflow.com/questions/10789059/create-qr-code-in-vector-image/60638350#60638350 diff --git a/core/src/BitMatrixIO.h b/core/src/BitMatrixIO.h index 022b48ec35..d4b695c7d5 100644 --- a/core/src/BitMatrixIO.h +++ b/core/src/BitMatrixIO.h @@ -12,7 +12,8 @@ namespace ZXing { -std::string ToString(const BitMatrix& matrix, char one = 'X', char zero = ' ', bool addSpace = true, bool printAsCString = false); +std::string ToString(const BitMatrix& matrix, bool inverted = false); +std::string ToString(const BitMatrix& matrix, char one, char zero = ' ', bool addSpace = true, bool printAsCString = false); std::string ToSVG(const BitMatrix& matrix); BitMatrix ParseBitMatrix(const std::string& str, char one = 'X', bool expectSpace = true); void SaveAsPBM(const BitMatrix& matrix, const std::string filename, int quietZone = 0); diff --git a/core/src/Content.h b/core/src/Content.h index 501f89d5b7..99e5a01e71 100644 --- a/core/src/Content.h +++ b/core/src/Content.h @@ -7,7 +7,7 @@ #include "ByteArray.h" #include "CharacterSet.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include #include diff --git a/core/src/DecodeHints.cpp b/core/src/DecodeHints.cpp deleted file mode 100644 index 72ded6073c..0000000000 --- a/core/src/DecodeHints.cpp +++ /dev/null @@ -1,11 +0,0 @@ -/* -* Copyright 2016 Nu-book Inc. -* Copyright 2016 ZXing authors -*/ -// SPDX-License-Identifier: Apache-2.0 - -#include "DecodeHints.h" - -namespace ZXing { - -} // ZXing diff --git a/core/src/DecodeHints.h b/core/src/DecodeHints.h index facee2d7ef..f3276baa0a 100644 --- a/core/src/DecodeHints.h +++ b/core/src/DecodeHints.h @@ -1,175 +1,10 @@ /* -* Copyright 2016 Nu-book Inc. -* Copyright 2016 ZXing authors -* Copyright 2020 Axel Waggershauser +* Copyright 2023 Axel Waggershauser */ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include "BarcodeFormat.h" -#include "CharacterSet.h" +#include "ReaderOptions.h" -#include -#include - -namespace ZXing { - -/** - * @brief The Binarizer enum - * - * Specify which algorithm to use for the grayscale to binary transformation. - * The difference is how to get to a threshold value T which results in a bit - * value R = L <= T. - */ -enum class Binarizer : unsigned char // needs to be unsigned for the bitfield below to work, uint8_t fails as well -{ - LocalAverage, ///< T = average of neighboring pixels for matrix and GlobalHistogram for linear (HybridBinarizer) - GlobalHistogram, ///< T = valley between the 2 largest peaks in the histogram (per line in linear case) - FixedThreshold, ///< T = 127 - BoolCast, ///< T = 0, fastest possible -}; - -enum class EanAddOnSymbol : unsigned char // see above -{ - Ignore, ///< Ignore any Add-On symbol during read/scan - Read, ///< Read EAN-2/EAN-5 Add-On symbol if found - Require, ///< Require EAN-2/EAN-5 Add-On symbol to be present -}; - -enum class TextMode : unsigned char // see above -{ - Plain, ///< bytes() transcoded to unicode based on ECI info or guessed charset (the default mode prior to 2.0) - ECI, ///< standard content following the ECI protocol with every character set ECI segment transcoded to unicode - HRI, ///< Human Readable Interpretation (dependent on the ContentType) - Hex, ///< bytes() transcoded to ASCII string of HEX values - Escaped, ///< Use the EscapeNonGraphical() function (e.g. ASCII 29 will be transcoded to "") -}; - -class DecodeHints -{ - bool _tryHarder : 1; - bool _tryRotate : 1; - bool _tryInvert : 1; - bool _tryDownscale : 1; - bool _isPure : 1; - bool _tryCode39ExtendedMode : 1; - bool _validateCode39CheckSum : 1; - bool _validateITFCheckSum : 1; - bool _returnCodabarStartEnd : 1; - bool _returnErrors : 1; - uint8_t _downscaleFactor : 3; - EanAddOnSymbol _eanAddOnSymbol : 2; - Binarizer _binarizer : 2; - TextMode _textMode : 3; - CharacterSet _characterSet : 6; -#ifdef BUILD_EXPERIMENTAL_API - bool _tryDenoise : 1; -#endif - - uint8_t _minLineCount = 2; - uint8_t _maxNumberOfSymbols = 0xff; - uint16_t _downscaleThreshold = 500; - BarcodeFormats _formats = BarcodeFormat::None; - -public: - // bitfields don't get default initialized to 0 before c++20 - DecodeHints() - : _tryHarder(1), - _tryRotate(1), - _tryInvert(1), - _tryDownscale(1), - _isPure(0), - _tryCode39ExtendedMode(0), - _validateCode39CheckSum(0), - _validateITFCheckSum(0), - _returnCodabarStartEnd(0), - _returnErrors(0), - _downscaleFactor(3), - _eanAddOnSymbol(EanAddOnSymbol::Ignore), - _binarizer(Binarizer::LocalAverage), - _textMode(TextMode::HRI), - _characterSet(CharacterSet::Unknown) -#ifdef BUILD_EXPERIMENTAL_API - , - _tryDenoise(0) -#endif - {} - -#define ZX_PROPERTY(TYPE, GETTER, SETTER) \ - TYPE GETTER() const noexcept { return _##GETTER; } \ - DecodeHints& SETTER(TYPE v)& { return _##GETTER = std::move(v), *this; } \ - DecodeHints&& SETTER(TYPE v)&& { return _##GETTER = std::move(v), std::move(*this); } - - /// Specify a set of BarcodeFormats that should be searched for, the default is all supported formats. - ZX_PROPERTY(BarcodeFormats, formats, setFormats) - - /// Spend more time to try to find a barcode; optimize for accuracy, not speed. - ZX_PROPERTY(bool, tryHarder, setTryHarder) - - /// Also try detecting code in 90, 180 and 270 degree rotated images. - ZX_PROPERTY(bool, tryRotate, setTryRotate) - - /// Also try detecting inverted ("reversed reflectance") codes if the format allows for those. - ZX_PROPERTY(bool, tryInvert, setTryInvert) - - /// Also try detecting code in downscaled images (depending on image size). - ZX_PROPERTY(bool, tryDownscale, setTryDownscale) - -#ifdef BUILD_EXPERIMENTAL_API - /// Also try detecting code after denoising (currently morphological closing filter for 2D symbologies only). - ZX_PROPERTY(bool, tryDenoise, setTryDenoise) -#endif - - /// Binarizer to use internally when using the ReadBarcode function - ZX_PROPERTY(Binarizer, binarizer, setBinarizer) - - /// Set to true if the input contains nothing but a single perfectly aligned barcode (generated image) - ZX_PROPERTY(bool, isPure, setIsPure) - - /// Image size ( min(width, height) ) threshold at which to start downscaled scanning - // WARNING: this API is experimental and may change/disappear - ZX_PROPERTY(uint16_t, downscaleThreshold, setDownscaleThreshold) - - /// Scale factor used during downscaling, meaningful values are 2, 3 and 4 - // WARNING: this API is experimental and may change/disappear - ZX_PROPERTY(uint8_t, downscaleFactor, setDownscaleFactor) - - /// The number of scan lines in a linear barcode that have to be equal to accept the result, default is 2 - ZX_PROPERTY(uint8_t, minLineCount, setMinLineCount) - - /// The maximum number of symbols (barcodes) to detect / look for in the image with ReadBarcodes - ZX_PROPERTY(uint8_t, maxNumberOfSymbols, setMaxNumberOfSymbols) - - /// If true, the Code-39 reader will try to read extended mode. - ZX_PROPERTY(bool, tryCode39ExtendedMode, setTryCode39ExtendedMode) - - /// Assume Code-39 codes employ a check digit and validate it. - ZX_PROPERTY(bool, validateCode39CheckSum, setValidateCode39CheckSum) - - /// Assume ITF codes employ a GS1 check digit and validate it. - ZX_PROPERTY(bool, validateITFCheckSum, setValidateITFCheckSum) - - /// If true, return the start and end chars in a Codabar barcode instead of stripping them. - ZX_PROPERTY(bool, returnCodabarStartEnd, setReturnCodabarStartEnd) - - /// If true, return the barcodes with errors as well (e.g. checksum errors, see @Result::error()) - ZX_PROPERTY(bool, returnErrors, setReturnErrors) - - /// Specify whether to ignore, read or require EAN-2/5 add-on symbols while scanning EAN/UPC codes - ZX_PROPERTY(EanAddOnSymbol, eanAddOnSymbol, setEanAddOnSymbol) - - /// Specifies the TextMode that controls the return of the Result::text() function - ZX_PROPERTY(TextMode, textMode, setTextMode) - - /// Specifies fallback character set to use instead of auto-detecting it (when applicable) - ZX_PROPERTY(CharacterSet, characterSet, setCharacterSet) - DecodeHints& setCharacterSet(std::string_view v)& { return _characterSet = CharacterSetFromString(v), *this; } - DecodeHints&& setCharacterSet(std::string_view v) && { return _characterSet = CharacterSetFromString(v), std::move(*this); } - -#undef ZX_PROPERTY - - bool hasFormat(BarcodeFormats f) const noexcept { return _formats.testFlags(f) || _formats.empty(); } -}; - -} // ZXing +// TODO: remove this backward compatibility header once the deprecated name DecodeHints has been removed (3.0) diff --git a/core/src/Generator.h b/core/src/Generator.h index 7a1fd179f2..a5083e9d15 100644 --- a/core/src/Generator.h +++ b/core/src/Generator.h @@ -5,8 +5,12 @@ #pragma once -#ifdef __cpp_impl_coroutine #ifdef __ANDROID__ +#include +#endif + +#ifdef __cpp_impl_coroutine +#if defined __ANDROID__ && __NDK_MAJOR__ < 26 // NDK 25.1.8937393 can compile this code with c++20 but needs a few tweaks: #include namespace std { @@ -25,7 +29,7 @@ namespace std { // this code is based on https://en.cppreference.com/w/cpp/coroutine/coroutine_handle#Example // but modified trying to prevent accidental copying of generated objects -#ifdef __ANDROID__ +#if defined __ANDROID__ && __NDK_MAJOR__ < 26 template #else template diff --git a/core/src/HRI.cpp b/core/src/HRI.cpp index d3a2687a4a..9a72dc6b31 100644 --- a/core/src/HRI.cpp +++ b/core/src/HRI.cpp @@ -30,9 +30,9 @@ struct AiInfo } }; -// GS1 General Specifications Release 22.0 (Jan 22, 2022) +// https://github.com/gs1/gs1-syntax-dictionary 2023-09-22 static const AiInfo aiInfos[] = { -// TWO_DIGIT_DATA_LENGTH +//TWO_DIGIT_DATA_LENGTH { "00", 18 }, { "01", 14 }, { "02", 14 }, @@ -52,7 +52,6 @@ static const AiInfo aiInfos[] = { { "30", -8 }, { "37", -8 }, - //internal company codes { "90", -30 }, { "91", -90 }, { "92", -90 }, @@ -177,6 +176,7 @@ static const AiInfo aiInfos[] = { { "4306", -70 }, { "4307", 2 }, { "4308", -30 }, + { "4309", 20 }, { "4310", -35 }, { "4311", -35 }, { "4312", -70 }, @@ -194,6 +194,10 @@ static const AiInfo aiInfos[] = { { "4324", 10 }, { "4325", 10 }, { "4326", 6 }, + { "4330", -7 }, + { "4331", -7 }, + { "4332", -7 }, + { "4333", -7 }, { "7001", 13 }, { "7002", -30 }, @@ -205,12 +209,15 @@ static const AiInfo aiInfos[] = { { "7008", -3 }, { "7009", -10 }, { "7010", -2 }, + { "7011", -10 }, { "7020", -20 }, { "7021", -20 }, { "7022", -20 }, { "7023", -30 }, { "7040", 4 }, { "7240", -20 }, + { "7241", 2 }, + { "7242", -25 }, { "8001", 14 }, { "8002", -20 }, @@ -230,6 +237,7 @@ static const AiInfo aiInfos[] = { { "8019", -10 }, { "8020", -25 }, { "8026", 18 }, + { "8030", -90 }, { "8110", -70 }, { "8111", 4 }, { "8112", -70 }, diff --git a/core/src/HybridBinarizer.cpp b/core/src/HybridBinarizer.cpp index 4aaec4ae85..a74fbd2872 100644 --- a/core/src/HybridBinarizer.cpp +++ b/core/src/HybridBinarizer.cpp @@ -18,9 +18,9 @@ namespace ZXing { // This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels. // So this is the smallest dimension in each axis we can accept. -static const int BLOCK_SIZE = 8; -static const int MINIMUM_DIMENSION = BLOCK_SIZE * 5; -static const int MIN_DYNAMIC_RANGE = 24; +static constexpr int BLOCK_SIZE = 8; +static constexpr int MINIMUM_DIMENSION = BLOCK_SIZE * 5; +static constexpr int MIN_DYNAMIC_RANGE = 24; HybridBinarizer::HybridBinarizer(const ImageView& iv) : GlobalHistogramBinarizer(iv) {} diff --git a/core/src/MultiFormatReader.cpp b/core/src/MultiFormatReader.cpp index 58a78cc0ca..27e7f31311 100644 --- a/core/src/MultiFormatReader.cpp +++ b/core/src/MultiFormatReader.cpp @@ -8,7 +8,7 @@ #include "BarcodeFormat.h" #include "BinaryBitmap.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "aztec/AZReader.h" #include "datamatrix/DMReader.h" #include "maxicode/MCReader.h" @@ -20,28 +20,28 @@ namespace ZXing { -MultiFormatReader::MultiFormatReader(const DecodeHints& hints) : _hints(hints) +MultiFormatReader::MultiFormatReader(const ReaderOptions& opts) : _opts(opts) { - auto formats = hints.formats().empty() ? BarcodeFormat::Any : hints.formats(); + auto formats = opts.formats().empty() ? BarcodeFormat::Any : opts.formats(); // Put linear readers upfront in "normal" mode - if (formats.testFlags(BarcodeFormat::LinearCodes) && !hints.tryHarder()) - _readers.emplace_back(new OneD::Reader(hints)); + if (formats.testFlags(BarcodeFormat::LinearCodes) && !opts.tryHarder()) + _readers.emplace_back(new OneD::Reader(opts)); - if (formats.testFlags(BarcodeFormat::QRCode | BarcodeFormat::MicroQRCode)) - _readers.emplace_back(new QRCode::Reader(hints, true)); + if (formats.testFlags(BarcodeFormat::QRCode | BarcodeFormat::MicroQRCode | BarcodeFormat::RMQRCode)) + _readers.emplace_back(new QRCode::Reader(opts, true)); if (formats.testFlag(BarcodeFormat::DataMatrix)) - _readers.emplace_back(new DataMatrix::Reader(hints, true)); + _readers.emplace_back(new DataMatrix::Reader(opts, true)); if (formats.testFlag(BarcodeFormat::Aztec)) - _readers.emplace_back(new Aztec::Reader(hints, true)); + _readers.emplace_back(new Aztec::Reader(opts, true)); if (formats.testFlag(BarcodeFormat::PDF417)) - _readers.emplace_back(new Pdf417::Reader(hints)); + _readers.emplace_back(new Pdf417::Reader(opts)); if (formats.testFlag(BarcodeFormat::MaxiCode)) - _readers.emplace_back(new MaxiCode::Reader(hints)); + _readers.emplace_back(new MaxiCode::Reader(opts)); // At end in "try harder" mode - if (formats.testFlags(BarcodeFormat::LinearCodes) && hints.tryHarder()) - _readers.emplace_back(new OneD::Reader(hints)); + if (formats.testFlags(BarcodeFormat::LinearCodes) && opts.tryHarder()) + _readers.emplace_back(new OneD::Reader(opts)); } MultiFormatReader::~MultiFormatReader() = default; @@ -55,7 +55,7 @@ MultiFormatReader::read(const BinaryBitmap& image) const if (r.isValid()) return r; } - return _hints.returnErrors() ? r : Result(); + return _opts.returnErrors() ? r : Result(); } Results MultiFormatReader::readMultiple(const BinaryBitmap& image, int maxSymbols) const @@ -66,7 +66,7 @@ Results MultiFormatReader::readMultiple(const BinaryBitmap& image, int maxSymbol if (image.inverted() && !reader->supportsInversion) continue; auto r = reader->decode(image, maxSymbols); - if (!_hints.returnErrors()) { + if (!_opts.returnErrors()) { //TODO: C++20 res.erase_if() auto it = std::remove_if(res.begin(), res.end(), [](auto&& r) { return !r.isValid(); }); res.erase(it, res.end()); diff --git a/core/src/MultiFormatReader.h b/core/src/MultiFormatReader.h index 4765b1574a..fe7749b525 100644 --- a/core/src/MultiFormatReader.h +++ b/core/src/MultiFormatReader.h @@ -16,21 +16,13 @@ namespace ZXing { class Result; class Reader; class BinaryBitmap; -class DecodeHints; - -/** -* MultiFormatReader is a convenience class and the main entry point into the library for most uses. -* By default it attempts to decode all barcode formats that the library supports. Optionally, you -* can provide a hints object to request different behavior, for example only decoding QR codes. -* -* @author Sean Owen -* @author dswitkin@google.com (Daniel Switkin) -*/ +class ReaderOptions; + class MultiFormatReader { public: - explicit MultiFormatReader(const DecodeHints& hints); - explicit MultiFormatReader(DecodeHints&& hints) = delete; + explicit MultiFormatReader(const ReaderOptions& opts); + explicit MultiFormatReader(ReaderOptions&& opts) = delete; ~MultiFormatReader(); Result read(const BinaryBitmap& image) const; @@ -40,7 +32,7 @@ class MultiFormatReader private: std::vector> _readers; - const DecodeHints& _hints; + const ReaderOptions& _opts; }; } // ZXing diff --git a/core/src/MultiFormatWriter.cpp b/core/src/MultiFormatWriter.cpp index b278d848dd..e8d48471cc 100644 --- a/core/src/MultiFormatWriter.cpp +++ b/core/src/MultiFormatWriter.cpp @@ -49,9 +49,15 @@ MultiFormatWriter::encode(const std::wstring& contents, int width, int height) c return exec0(std::move(writer)); }; + auto exec2 = [&](auto&& writer) { + if (_encoding != CharacterSet::Unknown) + writer.setEncoding(_encoding); + return exec0(std::move(writer)); + }; + switch (_format) { case BarcodeFormat::Aztec: return exec1(Aztec::Writer(), AztecEccLevel); - case BarcodeFormat::DataMatrix: return exec0(DataMatrix::Writer()); + case BarcodeFormat::DataMatrix: return exec2(DataMatrix::Writer()); case BarcodeFormat::PDF417: return exec1(Pdf417::Writer(), Pdf417EccLevel); case BarcodeFormat::QRCode: return exec1(QRCode::Writer(), QRCodeEccLevel); case BarcodeFormat::Codabar: return exec0(OneD::CodabarWriter()); diff --git a/core/src/Pattern.h b/core/src/Pattern.h index 3d4cbc3ac9..f4d377fc4c 100644 --- a/core/src/Pattern.h +++ b/core/src/Pattern.h @@ -151,9 +151,9 @@ constexpr auto BarAndSpaceSum(const T* view) noexcept /** * @brief FixedPattern describes a compile-time constant (start/stop) pattern. * - * @param N number of bars/spaces - * @param SUM sum over all N elements (size of pattern in modules) - * @param IS_SPARCE whether or not the pattern contains '0's denoting 'wide' bars/spaces + * N = number of bars/spaces + * SUM = sum over all N elements (size of pattern in modules) + * IS_SPARCE = whether or not the pattern contains '0's denoting 'wide' bars/spaces */ template struct FixedPattern diff --git a/core/src/ReadBarcode.cpp b/core/src/ReadBarcode.cpp index f814add08b..43269e0c6d 100644 --- a/core/src/ReadBarcode.cpp +++ b/core/src/ReadBarcode.cpp @@ -5,7 +5,7 @@ #include "ReadBarcode.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "GlobalHistogramBinarizer.h" #include "HybridBinarizer.h" #include "MultiFormatReader.h" @@ -75,7 +75,7 @@ class LumImagePyramid case 2: addLayer<2>(); break; case 3: addLayer<3>(); break; case 4: addLayer<4>(); break; - default: throw std::invalid_argument("Invalid DecodeHints::downscaleFactor"); break; + default: throw std::invalid_argument("Invalid ReaderOptions::downscaleFactor"); break; } } @@ -98,12 +98,12 @@ class LumImagePyramid } }; -ImageView SetupLumImageView(ImageView iv, LumImage& lum, const DecodeHints& hints) +ImageView SetupLumImageView(ImageView iv, LumImage& lum, const ReaderOptions& opts) { if (iv.format() == ImageFormat::None) throw std::invalid_argument("Invalid image format"); - if (hints.binarizer() == Binarizer::GlobalHistogram || hints.binarizer() == Binarizer::LocalAverage) { + if (opts.binarizer() == Binarizer::GlobalHistogram || opts.binarizer() == Binarizer::LocalAverage) { if (iv.format() != ImageFormat::Lum) { lum = ExtractLum(iv, [r = RedIndex(iv.format()), g = GreenIndex(iv.format()), b = BlueIndex(iv.format())]( const uint8_t* src) { return RGBToLum(src[r], src[g], src[b]); }); @@ -128,44 +128,44 @@ std::unique_ptr CreateBitmap(ZXing::Binarizer binarizer, const Ima return {}; // silence gcc warning } -Result ReadBarcode(const ImageView& _iv, const DecodeHints& hints) +Result ReadBarcode(const ImageView& _iv, const ReaderOptions& opts) { - return FirstOrDefault(ReadBarcodes(_iv, DecodeHints(hints).setMaxNumberOfSymbols(1))); + return FirstOrDefault(ReadBarcodes(_iv, ReaderOptions(opts).setMaxNumberOfSymbols(1))); } -Results ReadBarcodes(const ImageView& _iv, const DecodeHints& hints) +Results ReadBarcodes(const ImageView& _iv, const ReaderOptions& opts) { - if (sizeof(PatternType) < 4 && hints.hasFormat(BarcodeFormat::LinearCodes) && (_iv.width() > 0xffff || _iv.height() > 0xffff)) + if (sizeof(PatternType) < 4 && opts.hasFormat(BarcodeFormat::LinearCodes) && (_iv.width() > 0xffff || _iv.height() > 0xffff)) throw std::invalid_argument("maximum image width/height is 65535"); LumImage lum; - ImageView iv = SetupLumImageView(_iv, lum, hints); - MultiFormatReader reader(hints); + ImageView iv = SetupLumImageView(_iv, lum, opts); + MultiFormatReader reader(opts); - if (hints.isPure()) - return {reader.read(*CreateBitmap(hints.binarizer(), iv))}; + if (opts.isPure()) + return {reader.read(*CreateBitmap(opts.binarizer(), iv))}; std::unique_ptr closedReader; -#ifdef BUILD_EXPERIMENTAL_API +#ifdef ZXING_BUILD_EXPERIMENTAL_API auto formatsBenefittingFromClosing = BarcodeFormat::Aztec | BarcodeFormat::DataMatrix | BarcodeFormat::QRCode | BarcodeFormat::MicroQRCode; - if (hints.tryDenoise() && hints.hasFormat(formatsBenefittingFromClosing)) { - DecodeHints closedHints = hints; - closedHints.setFormats((hints.formats().empty() ? BarcodeFormat::Any : hints.formats()) & formatsBenefittingFromClosing); - closedReader = std::make_unique(closedHints); + ReaderOptions closedOptions = opts; + if (opts.tryDenoise() && opts.hasFormat(formatsBenefittingFromClosing)) { + closedOptions.setFormats((opts.formats().empty() ? BarcodeFormat::Any : opts.formats()) & formatsBenefittingFromClosing); + closedReader = std::make_unique(closedOptions); } #endif - LumImagePyramid pyramid(iv, hints.downscaleThreshold() * hints.tryDownscale(), hints.downscaleFactor()); + LumImagePyramid pyramid(iv, opts.downscaleThreshold() * opts.tryDownscale(), opts.downscaleFactor()); Results results; - int maxSymbols = hints.maxNumberOfSymbols() ? hints.maxNumberOfSymbols() : INT_MAX; + int maxSymbols = opts.maxNumberOfSymbols() ? opts.maxNumberOfSymbols() : INT_MAX; for (auto&& iv : pyramid.layers) { - auto bitmap = CreateBitmap(hints.binarizer(), iv); + auto bitmap = CreateBitmap(opts.binarizer(), iv); for (int close = 0; close <= (closedReader ? 1 : 0); ++close) { if (close) bitmap->close(); // TODO: check if closing after invert would be beneficial - for (int invert = 0; invert <= static_cast(hints.tryInvert() && !close); ++invert) { + for (int invert = 0; invert <= static_cast(opts.tryInvert() && !close); ++invert) { if (invert) bitmap->invert(); auto rs = (close ? *closedReader : reader).readMultiple(*bitmap, maxSymbols); @@ -173,7 +173,7 @@ Results ReadBarcodes(const ImageView& _iv, const DecodeHints& hints) if (iv.width() != _iv.width()) r.setPosition(Scale(r.position(), _iv.width() / iv.width())); if (!Contains(results, r)) { - r.setDecodeHints(hints); + r.setReaderOptions(opts); r.setIsInverted(bitmap->inverted()); results.push_back(std::move(r)); --maxSymbols; diff --git a/core/src/ReadBarcode.h b/core/src/ReadBarcode.h index 5b73d16913..343ac8487b 100644 --- a/core/src/ReadBarcode.h +++ b/core/src/ReadBarcode.h @@ -5,7 +5,7 @@ #pragma once -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "ImageView.h" #include "Result.h" @@ -14,20 +14,20 @@ namespace ZXing { /** * Read barcode from an ImageView * - * @param buffer view of the image data including layout and format - * @param hints optional DecodeHints to parameterize / speed up decoding + * @param image view of the image data including layout and format + * @param options optional ReaderOptions to parameterize / speed up detection * @return #Result structure */ -Result ReadBarcode(const ImageView& buffer, const DecodeHints& hints = {}); +Result ReadBarcode(const ImageView& image, const ReaderOptions& options = {}); /** * Read barcodes from an ImageView * - * @param buffer view of the image data including layout and format - * @param hints optional DecodeHints to parameterize / speed up decoding + * @param image view of the image data including layout and format + * @param options optional ReaderOptions to parameterize / speed up detection * @return #Results list of results found, may be empty */ -Results ReadBarcodes(const ImageView& buffer, const DecodeHints& hints = {}); +Results ReadBarcodes(const ImageView& image, const ReaderOptions& options = {}); } // ZXing diff --git a/core/src/Reader.h b/core/src/Reader.h index bc2d69cdc2..7ece21b56d 100644 --- a/core/src/Reader.h +++ b/core/src/Reader.h @@ -6,24 +6,24 @@ #pragma once -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" namespace ZXing { class BinaryBitmap; -class DecodeHints; +class ReaderOptions; class Reader { protected: - const DecodeHints& _hints; + const ReaderOptions& _opts; public: const bool supportsInversion; - explicit Reader(const DecodeHints& hints, bool supportsInversion = false) : _hints(hints), supportsInversion(supportsInversion) {} - explicit Reader(DecodeHints&& hints) = delete; + explicit Reader(const ReaderOptions& opts, bool supportsInversion = false) : _opts(opts), supportsInversion(supportsInversion) {} + explicit Reader(ReaderOptions&& opts) = delete; virtual ~Reader() = default; virtual Result decode(const BinaryBitmap& image) const = 0; @@ -31,7 +31,7 @@ class Reader // WARNING: this API is experimental and may change/disappear virtual Results decode(const BinaryBitmap& image, [[maybe_unused]] int maxSymbols) const { auto res = decode(image); - return res.isValid() || (_hints.returnErrors() && res.format() != BarcodeFormat::None) ? Results{std::move(res)} : Results{}; + return res.isValid() || (_opts.returnErrors() && res.format() != BarcodeFormat::None) ? Results{std::move(res)} : Results{}; } }; diff --git a/core/src/ReaderOptions.h b/core/src/ReaderOptions.h new file mode 100644 index 0000000000..e68d0592d7 --- /dev/null +++ b/core/src/ReaderOptions.h @@ -0,0 +1,177 @@ +/* +* Copyright 2016 Nu-book Inc. +* Copyright 2016 ZXing authors +* Copyright 2020 Axel Waggershauser +*/ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "BarcodeFormat.h" +#include "CharacterSet.h" + +#include +#include + +namespace ZXing { + +/** + * @brief The Binarizer enum + * + * Specify which algorithm to use for the grayscale to binary transformation. + * The difference is how to get to a threshold value T which results in a bit + * value R = L <= T. + */ +enum class Binarizer : unsigned char // needs to be unsigned for the bitfield below to work, uint8_t fails as well +{ + LocalAverage, ///< T = average of neighboring pixels for matrix and GlobalHistogram for linear (HybridBinarizer) + GlobalHistogram, ///< T = valley between the 2 largest peaks in the histogram (per line in linear case) + FixedThreshold, ///< T = 127 + BoolCast, ///< T = 0, fastest possible +}; + +enum class EanAddOnSymbol : unsigned char // see above +{ + Ignore, ///< Ignore any Add-On symbol during read/scan + Read, ///< Read EAN-2/EAN-5 Add-On symbol if found + Require, ///< Require EAN-2/EAN-5 Add-On symbol to be present +}; + +enum class TextMode : unsigned char // see above +{ + Plain, ///< bytes() transcoded to unicode based on ECI info or guessed charset (the default mode prior to 2.0) + ECI, ///< standard content following the ECI protocol with every character set ECI segment transcoded to unicode + HRI, ///< Human Readable Interpretation (dependent on the ContentType) + Hex, ///< bytes() transcoded to ASCII string of HEX values + Escaped, ///< Use the EscapeNonGraphical() function (e.g. ASCII 29 will be transcoded to "") +}; + +class ReaderOptions +{ + bool _tryHarder : 1; + bool _tryRotate : 1; + bool _tryInvert : 1; + bool _tryDownscale : 1; + bool _isPure : 1; + bool _tryCode39ExtendedMode : 1; + bool _validateCode39CheckSum : 1; + bool _validateITFCheckSum : 1; + bool _returnCodabarStartEnd : 1; + bool _returnErrors : 1; + uint8_t _downscaleFactor : 3; + EanAddOnSymbol _eanAddOnSymbol : 2; + Binarizer _binarizer : 2; + TextMode _textMode : 3; + CharacterSet _characterSet : 6; +#ifdef ZXING_BUILD_EXPERIMENTAL_API + bool _tryDenoise : 1; +#endif + + uint8_t _minLineCount = 2; + uint8_t _maxNumberOfSymbols = 0xff; + uint16_t _downscaleThreshold = 500; + BarcodeFormats _formats = BarcodeFormat::None; + +public: + // bitfields don't get default initialized to 0 before c++20 + ReaderOptions() + : _tryHarder(1), + _tryRotate(1), + _tryInvert(1), + _tryDownscale(1), + _isPure(0), + _tryCode39ExtendedMode(0), + _validateCode39CheckSum(0), + _validateITFCheckSum(0), + _returnCodabarStartEnd(0), + _returnErrors(0), + _downscaleFactor(3), + _eanAddOnSymbol(EanAddOnSymbol::Ignore), + _binarizer(Binarizer::LocalAverage), + _textMode(TextMode::HRI), + _characterSet(CharacterSet::Unknown) +#ifdef ZXING_BUILD_EXPERIMENTAL_API + , + _tryDenoise(0) +#endif + {} + +#define ZX_PROPERTY(TYPE, GETTER, SETTER) \ + TYPE GETTER() const noexcept { return _##GETTER; } \ + ReaderOptions& SETTER(TYPE v)& { return (void)(_##GETTER = std::move(v)), *this; } \ + ReaderOptions&& SETTER(TYPE v)&& { return (void)(_##GETTER = std::move(v)), std::move(*this); } + + /// Specify a set of BarcodeFormats that should be searched for, the default is all supported formats. + ZX_PROPERTY(BarcodeFormats, formats, setFormats) + + /// Spend more time to try to find a barcode; optimize for accuracy, not speed. + ZX_PROPERTY(bool, tryHarder, setTryHarder) + + /// Also try detecting code in 90, 180 and 270 degree rotated images. + ZX_PROPERTY(bool, tryRotate, setTryRotate) + + /// Also try detecting inverted ("reversed reflectance") codes if the format allows for those. + ZX_PROPERTY(bool, tryInvert, setTryInvert) + + /// Also try detecting code in downscaled images (depending on image size). + ZX_PROPERTY(bool, tryDownscale, setTryDownscale) + +#ifdef ZXING_BUILD_EXPERIMENTAL_API + /// Also try detecting code after denoising (currently morphological closing filter for 2D symbologies only). + ZX_PROPERTY(bool, tryDenoise, setTryDenoise) +#endif + + /// Binarizer to use internally when using the ReadBarcode function + ZX_PROPERTY(Binarizer, binarizer, setBinarizer) + + /// Set to true if the input contains nothing but a single perfectly aligned barcode (generated image) + ZX_PROPERTY(bool, isPure, setIsPure) + + /// Image size ( min(width, height) ) threshold at which to start downscaled scanning + // WARNING: this API is experimental and may change/disappear + ZX_PROPERTY(uint16_t, downscaleThreshold, setDownscaleThreshold) + + /// Scale factor used during downscaling, meaningful values are 2, 3 and 4 + // WARNING: this API is experimental and may change/disappear + ZX_PROPERTY(uint8_t, downscaleFactor, setDownscaleFactor) + + /// The number of scan lines in a linear barcode that have to be equal to accept the result, default is 2 + ZX_PROPERTY(uint8_t, minLineCount, setMinLineCount) + + /// The maximum number of symbols (barcodes) to detect / look for in the image with ReadBarcodes + ZX_PROPERTY(uint8_t, maxNumberOfSymbols, setMaxNumberOfSymbols) + + /// If true, the Code-39 reader will try to read extended mode. + ZX_PROPERTY(bool, tryCode39ExtendedMode, setTryCode39ExtendedMode) + + /// Assume Code-39 codes employ a check digit and validate it. + ZX_PROPERTY(bool, validateCode39CheckSum, setValidateCode39CheckSum) + + /// Assume ITF codes employ a GS1 check digit and validate it. + ZX_PROPERTY(bool, validateITFCheckSum, setValidateITFCheckSum) + + /// If true, return the start and end chars in a Codabar barcode instead of stripping them. + ZX_PROPERTY(bool, returnCodabarStartEnd, setReturnCodabarStartEnd) + + /// If true, return the barcodes with errors as well (e.g. checksum errors, see @Result::error()) + ZX_PROPERTY(bool, returnErrors, setReturnErrors) + + /// Specify whether to ignore, read or require EAN-2/5 add-on symbols while scanning EAN/UPC codes + ZX_PROPERTY(EanAddOnSymbol, eanAddOnSymbol, setEanAddOnSymbol) + + /// Specifies the TextMode that controls the return of the Result::text() function + ZX_PROPERTY(TextMode, textMode, setTextMode) + + /// Specifies fallback character set to use instead of auto-detecting it (when applicable) + ZX_PROPERTY(CharacterSet, characterSet, setCharacterSet) + ReaderOptions& setCharacterSet(std::string_view v)& { return (void)(_characterSet = CharacterSetFromString(v)), *this; } + ReaderOptions&& setCharacterSet(std::string_view v) && { return (void)(_characterSet = CharacterSetFromString(v)), std::move(*this); } + +#undef ZX_PROPERTY + + bool hasFormat(BarcodeFormats f) const noexcept { return _formats.testFlags(f) || _formats.empty(); } +}; + +using DecodeHints [[deprecated]] = ReaderOptions; + +} // ZXing diff --git a/core/src/RegressionLine.h b/core/src/RegressionLine.h index eb7ecd1d4f..dac602ee9f 100644 --- a/core/src/RegressionLine.h +++ b/core/src/RegressionLine.h @@ -79,6 +79,7 @@ class RegressionLine auto signedDistance(PointF p) const { return dot(normal(), p) - c; } template auto distance(PointT p) const { return std::abs(signedDistance(PointF(p))); } PointF project(PointF p) const { return p - signedDistance(p) * normal(); } + PointF centroid() const { return std::accumulate(_points.begin(), _points.end(), PointF()) / _points.size(); } void reset() { @@ -116,10 +117,13 @@ class RegressionLine return sd > maxSignedDist || sd < -2 * maxSignedDist; }); points.erase(end, points.end()); + // if we threw away too many points, something is off with the line to begin with + if (points.size() < old_points_size / 2 || points.size() < 2) + return false; if (old_points_size == points.size()) break; #ifdef PRINT_DEBUG - printf("removed %zu points\n", old_points_size - points.size()); + printf("removed %zu points -> %zu remaining\n", old_points_size - points.size(), points.size()); #endif ret = evaluate(points); } diff --git a/core/src/Result.cpp b/core/src/Result.cpp index 837a561a43..427ed84e51 100644 --- a/core/src/Result.cpp +++ b/core/src/Result.cpp @@ -64,7 +64,7 @@ std::string Result::text(TextMode mode) const std::string Result::text() const { - return text(_decodeHints.textMode()); + return text(_readerOpts.textMode()); } std::string Result::ecLevel() const @@ -113,11 +113,11 @@ std::string Result::version() const return _version; } -Result& Result::setDecodeHints(DecodeHints hints) +Result& Result::setReaderOptions(const ReaderOptions& opts) { - if (hints.characterSet() != CharacterSet::Unknown) - _content.defaultCharset = hints.characterSet(); - _decodeHints = hints; + if (opts.characterSet() != CharacterSet::Unknown) + _content.defaultCharset = opts.characterSet(); + _readerOpts = opts; return *this; } @@ -141,16 +141,19 @@ bool Result::operator==(const Result& o) const if (lineCount() > 1 && o.lineCount() > 1) return HaveIntersectingBoundingBoxes(o.position(), position()); - // the following code is only meant for this->lineCount == 1 - assert(lineCount() == 1); + // the following code is only meant for this or other lineCount == 1 + assert(lineCount() == 1 || o.lineCount() == 1); + + const auto& r1 = lineCount() == 1 ? *this : o; + const auto& r2 = lineCount() == 1 ? o : *this; // if one line is less than half the length of the other away from the // latter, we consider it to belong to the same symbol. additionally, both need to have // roughly the same length (see #367) - auto dTop = maxAbsComponent(o.position().topLeft() - position().topLeft()); - auto dBot = maxAbsComponent(o.position().bottomLeft() - position().topLeft()); - auto length = maxAbsComponent(position().topLeft() - position().bottomRight()); - auto dLength = std::abs(length - maxAbsComponent(o.position().topLeft() - o.position().bottomRight())); + auto dTop = maxAbsComponent(r2.position().topLeft() - r1.position().topLeft()); + auto dBot = maxAbsComponent(r2.position().bottomLeft() - r1.position().topLeft()); + auto length = maxAbsComponent(r1.position().topLeft() - r1.position().bottomRight()); + auto dLength = std::abs(length - maxAbsComponent(r2.position().topLeft() - r2.position().bottomRight())); return std::min(dTop, dBot) < length / 2 && dLength < length / 5; } diff --git a/core/src/Result.h b/core/src/Result.h index 9defbc5033..30a180f48e 100644 --- a/core/src/Result.h +++ b/core/src/Result.h @@ -10,7 +10,7 @@ #include "BarcodeFormat.h" #include "ByteArray.h" #include "Content.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Error.h" #include "Quadrilateral.h" #include "StructuredAppend.h" @@ -31,10 +31,10 @@ using Position = QuadrilateralI; class Result { void setIsInverted(bool v) { _isInverted = v; } - Result& setDecodeHints(DecodeHints hints); + Result& setReaderOptions(const ReaderOptions& opts); friend Result MergeStructuredAppendSequence(const std::vector& results); - friend std::vector ReadBarcodes(const ImageView&, const DecodeHints&); + friend std::vector ReadBarcodes(const ImageView&, const ReaderOptions&); friend void IncrementLineCount(Result&); public: @@ -68,7 +68,7 @@ class Result std::string text(TextMode mode) const; /** - * @brief text returns the bytes() content rendered to unicode/utf8 text accoring to the TextMode set in the DecodingHints + * @brief text returns the bytes() content rendered to unicode/utf8 text accoring to the TextMode set in the ReaderOptions */ std::string text() const; @@ -101,7 +101,7 @@ class Result bool isMirrored() const { return _isMirrored; } /** - * @brief isInverted is the symbol inverted / has reveresed reflectance (see DecodeHints::tryInvert) + * @brief isInverted is the symbol inverted / has reveresed reflectance (see ReaderOptions::tryInvert) */ bool isInverted() const { return _isInverted; } @@ -157,7 +157,7 @@ class Result Content _content; Error _error; Position _position; - DecodeHints _decodeHints; + ReaderOptions _readerOpts; StructuredAppendInfo _sai; BarcodeFormat _format = BarcodeFormat::None; char _ecLevel[4] = {}; diff --git a/core/src/TextDecoder.cpp b/core/src/TextDecoder.cpp index bca4617616..216c6c9994 100644 --- a/core/src/TextDecoder.cpp +++ b/core/src/TextDecoder.cpp @@ -53,7 +53,6 @@ void TextDecoder::Append(std::wstring& str, const uint8_t* bytes, size_t length, /** * @param bytes bytes encoding a string, whose encoding should be guessed -* @param hints decode hints if applicable * @return name of guessed encoding; at the moment will only guess one of: * {@link #SHIFT_JIS}, {@link #UTF8}, {@link #ISO88591}, or the platform * default encoding if none of these can possibly be correct diff --git a/core/src/Utf.cpp b/core/src/Utf.cpp index ff112e65c8..d15be33765 100644 --- a/core/src/Utf.cpp +++ b/core/src/Utf.cpp @@ -247,8 +247,10 @@ std::wstring EscapeNonGraphical(std::wstring_view str) ws << "<" << ascii_nongraphs[wc == 127 ? 32 : wc] << ">"; else if (wc < 128) // ASCII ws << wc; - else if (IsUtf16SurrogatePair(str)) - ws.write(str.data(), 2), str.remove_prefix(1); + else if (IsUtf16SurrogatePair(str)) { + ws.write(str.data(), 2); + str.remove_prefix(1); + } // Exclude unpaired surrogates and NO-BREAK spaces NBSP and NUMSP else if ((wc < 0xd800 || wc >= 0xe000) && (iswgraph(wc) && wc != 0xA0 && wc != 0x2007 && wc != 0x2000 && wc != 0xfffd)) ws << wc; diff --git a/core/src/WhiteRectDetector.cpp b/core/src/WhiteRectDetector.cpp index 87cfca924e..9644a271a9 100644 --- a/core/src/WhiteRectDetector.cpp +++ b/core/src/WhiteRectDetector.cpp @@ -76,15 +76,16 @@ static bool GetBlackPointOnSegment(const BitMatrix& image, int aX, int aY, int b /** * recenters the points of a constant distance towards the center * +* p0 to p3 describing the corners of the rectangular +* region. The first and last points are opposed on the diagonal, as +* are the second and third. The first point will be the topmost +* point and the last, the bottommost. The second point will be +* leftmost and the third, the rightmost +* * @param y bottom most point * @param z left most point * @param x right most point * @param t top most point -* @return {@link ResultPoint}[] describing the corners of the rectangular -* region. The first and last points are opposed on the diagonal, as -* are the second and third. The first point will be the topmost -* point and the last, the bottommost. The second point will be -* leftmost and the third, the rightmost */ static void CenterEdges(const ResultPoint& y, const ResultPoint& z, const ResultPoint& x, const ResultPoint& t, int width, ResultPoint& p0, ResultPoint& p1, ResultPoint& p2, ResultPoint& p3) { diff --git a/core/src/aztec/AZDecoder.cpp b/core/src/aztec/AZDecoder.cpp index f87e652c3e..7ab6f6a84f 100644 --- a/core/src/aztec/AZDecoder.cpp +++ b/core/src/aztec/AZDecoder.cpp @@ -258,6 +258,8 @@ static void DecodeContent(const BitArray& bits, Content& res) while (remBits.size() >= (shiftTable == Table::DIGIT ? 4 : 5)) { // see ISO/IEC 24778:2008 7.3.1.2 regarding padding bits if (shiftTable == Table::BINARY) { + if (remBits.size() <= 6) // padding bits + break; int length = remBits.readBits(5); if (length == 0) length = remBits.readBits(11) + 31; @@ -321,7 +323,7 @@ DecoderResult Decode(const BitArray& bits) // As converting character set ECIs ourselves and ignoring/skipping non-character ECIs, not using // modifiers that indicate ECI protocol (ISO/IEC 24778:2008 Annex F Table F.1) - if (res.bytes[0] == 29) { + if (res.bytes.size() > 1 && res.bytes[0] == 29) { res.symbology.modifier = '1'; // GS1 res.symbology.aiFlag = AIFlag::GS1; res.erase(0, 1); // Remove FNC1 diff --git a/core/src/aztec/AZDetector.h b/core/src/aztec/AZDetector.h index 364f1f6303..009f3409a6 100644 --- a/core/src/aztec/AZDetector.h +++ b/core/src/aztec/AZDetector.h @@ -16,11 +16,6 @@ namespace Aztec { class DetectorResult; -/** - * Detects an Aztec Code in an image. - * - * @param isMirror if true, image is a mirror-image of original - */ DetectorResult Detect(const BitMatrix& image, bool isPure, bool tryHarder = true); using DetectorResults = std::vector; diff --git a/core/src/aztec/AZReader.cpp b/core/src/aztec/AZReader.cpp index 2464925414..b9b36a7ba6 100644 --- a/core/src/aztec/AZReader.cpp +++ b/core/src/aztec/AZReader.cpp @@ -11,7 +11,7 @@ #include "AZDetector.h" #include "AZDetectorResult.h" #include "BinaryBitmap.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "DecoderResult.h" #include "Result.h" @@ -26,8 +26,8 @@ Reader::decode(const BinaryBitmap& image) const auto binImg = image.getBitMatrix(); if (binImg == nullptr) return {}; - - DetectorResult detectorResult = Detect(*binImg, _hints.isPure(), _hints.tryHarder()); + + DetectorResult detectorResult = Detect(*binImg, _opts.isPure(), _opts.tryHarder()); if (!detectorResult.isValid()) return {}; @@ -44,14 +44,14 @@ Results Reader::decode(const BinaryBitmap& image, int maxSymbols) const auto binImg = image.getBitMatrix(); if (binImg == nullptr) return {}; - - auto detRess = Detect(*binImg, _hints.isPure(), _hints.tryHarder(), maxSymbols); + + auto detRess = Detect(*binImg, _opts.isPure(), _opts.tryHarder(), maxSymbols); Results results; for (auto&& detRes : detRess) { auto decRes = Decode(detRes).setReaderInit(detRes.readerInit()).setIsMirrored(detRes.isMirrored()).setVersionNumber(detRes.nbLayers()); - if (decRes.isValid(_hints.returnErrors())) { + if (decRes.isValid(_opts.returnErrors())) { results.emplace_back(std::move(decRes), std::move(detRes).position(), BarcodeFormat::Aztec); if (maxSymbols > 0 && Size(results) >= maxSymbols) break; diff --git a/core/src/datamatrix/DMDetector.cpp b/core/src/datamatrix/DMDetector.cpp index b263b26adf..ce8d15fde6 100644 --- a/core/src/datamatrix/DMDetector.cpp +++ b/core/src/datamatrix/DMDetector.cpp @@ -213,8 +213,6 @@ static float CrossProductZ(const ResultPoint& a, const ResultPoint& b, const Res /** * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC * and BC is less than AC, and the angle between BC and BA is less than 180 degrees. -* -* @param patterns array of three {@code ResultPoint} to order */ static void OrderByBestPatterns(const ResultPoint*& p0, const ResultPoint*& p1, const ResultPoint*& p2) { @@ -493,6 +491,13 @@ class EdgeTracer : public BitMatrixCursorF { enum class StepResult { FOUND, OPEN_END, CLOSED_END }; + // force this function inline to allow the compiler optimize for the maxStepSize==1 case in traceLine() + // this can result in a 10% speedup of the falsepositive use case when build with c++20 +#if defined(__clang__) || defined(__GNUC__) + inline __attribute__((always_inline)) +#elif defined(_MSC_VER) + __forceinline +#endif StepResult traceStep(PointF dEdge, int maxStepSize, bool goodDirection) { dEdge = mainDirection(dEdge); @@ -552,21 +557,28 @@ class EdgeTracer : public BitMatrixCursorF return true; } + bool updateDirectionFromLine(RegressionLine& line) + { + return line.evaluate(1.5) && updateDirectionFromOrigin(p - line.project(p) + line.points().front()); + } + + bool updateDirectionFromLineCentroid(RegressionLine& line) + { + // Basically a faster, less accurate version of the above without the line evaluation + return updateDirectionFromOrigin(line.centroid()); + } + bool traceLine(PointF dEdge, RegressionLine& line) { line.setDirectionInward(dEdge); do { log(p); line.add(p); - if (line.points().size() % 50 == 10) { - if (!line.evaluate()) - return false; - if (!updateDirectionFromOrigin(p - line.project(p) + line.points().front())) - return false; - } + if (line.points().size() % 50 == 10 && !updateDirectionFromLineCentroid(line)) + return false; auto stepResult = traceStep(dEdge, 1, line.isValid()); if (stepResult != StepResult::FOUND) - return stepResult == StepResult::OPEN_END && line.points().size() > 1; + return stepResult == StepResult::OPEN_END && line.points().size() > 1 && updateDirectionFromLineCentroid(line); } while (true); } @@ -618,9 +630,7 @@ class EdgeTracer : public BitMatrixCursorF if (stepLengthInMainDir > 1 || maxAbsComponent(curStep) >= 2) { ++gaps; if (gaps >= 2 || line.points().size() > 5) { - if (!line.evaluate(1.5)) - return false; - if (!updateDirectionFromOrigin(p - line.project(p) + line.points().front())) + if (!updateDirectionFromLine(line)) return false; // check if the first half of the top-line trace is complete. // the minimum code size is 10x10 -> every code has at least 4 gaps @@ -878,7 +888,7 @@ static DetectorResults DetectNew(const BitMatrix& image, bool tryHarder, bool tr /** * This method detects a code in a "pure" image -- that is, pure monochrome image -* which contains only an unrotated, unskewed, image of a code, with some white border +* which contains only an unrotated, unskewed, image of a code, with some optional white border * around it. This is a specialized method that works exceptionally fast in this special * case. */ @@ -919,29 +929,26 @@ static DetectorResult DetectPure(const BitMatrix& image) DetectorResults Detect(const BitMatrix& image, bool tryHarder, bool tryRotate, bool isPure) { #ifdef __cpp_impl_coroutine - if (isPure) { - co_yield DetectPure(image); - } else { + // First try the very fast DetectPure() path. Also because DetectNew() generally fails with pure module size 1 symbols + // TODO: implement a tryRotate version of DetectPure, see #590. + if (auto r = DetectPure(image); r.isValid()) + co_yield std::move(r); + else if (!isPure) { // If r.isValid() then there is no point in looking for more (no-pure) symbols bool found = false; for (auto&& r : DetectNew(image, tryHarder, tryRotate)) { found = true; co_yield std::move(r); } if (!found && tryHarder) { - if (auto r = DetectPure(image); r.isValid()) - co_yield std::move(r); - else if(auto r = DetectOld(image); r.isValid()) + if (auto r = DetectOld(image); r.isValid()) co_yield std::move(r); } } #else - if (isPure) - return DetectPure(image); - - auto result = DetectNew(image, tryHarder, tryRotate); - if (!result.isValid() && tryHarder) - result = DetectPure(image); - if (!result.isValid() && tryHarder) + auto result = DetectPure(image); + if (!result.isValid() && !isPure) + result = DetectNew(image, tryHarder, tryRotate); + if (!result.isValid() && tryHarder && !isPure) result = DetectOld(image); return result; #endif diff --git a/core/src/datamatrix/DMECEncoder.h b/core/src/datamatrix/DMECEncoder.h index aea7d3bce4..d0cafee2a1 100644 --- a/core/src/datamatrix/DMECEncoder.h +++ b/core/src/datamatrix/DMECEncoder.h @@ -17,9 +17,8 @@ class SymbolInfo; /** * Creates and interleaves the ECC200 error correction for an encoded message. * - * @param codewords the codewords + * @param codewords the codewords (with interleaved error correction after function return) * @param symbolInfo information about the symbol to be encoded - * @return the codewords with interleaved error correction. */ void EncodeECC200(ByteArray& codewords, const SymbolInfo& symbolInfo); diff --git a/core/src/datamatrix/DMHighLevelEncoder.cpp b/core/src/datamatrix/DMHighLevelEncoder.cpp index 9741de5473..db0859347f 100644 --- a/core/src/datamatrix/DMHighLevelEncoder.cpp +++ b/core/src/datamatrix/DMHighLevelEncoder.cpp @@ -857,7 +857,7 @@ static bool EndsWith(const std::wstring& s, const std::wstring& ss) ByteArray Encode(const std::wstring& msg) { - return Encode(msg, SymbolShape::NONE, -1, -1, -1, -1); + return Encode(msg, CharacterSet::ISO8859_1, SymbolShape::NONE, -1, -1, -1, -1); } /** @@ -871,7 +871,7 @@ ByteArray Encode(const std::wstring& msg) * @param maxSize the maximum symbol size constraint or null for no constraint * @return the encoded message (the char values range from 0 to 255) */ -ByteArray Encode(const std::wstring& msg, SymbolShape shape, int minWidth, int minHeight, int maxWidth, int maxHeight) +ByteArray Encode(const std::wstring& msg, CharacterSet charset, SymbolShape shape, int minWidth, int minHeight, int maxWidth, int maxHeight) { //the codewords 0..255 are encoded as Unicode characters //Encoder[] encoders = { @@ -879,7 +879,11 @@ ByteArray Encode(const std::wstring& msg, SymbolShape shape, int minWidth, int m // new X12Encoder(), new EdifactEncoder(), new Base256Encoder() //}; - EncoderContext context(TextEncoder::FromUnicode(msg, CharacterSet::ISO8859_1)); + if (charset == CharacterSet::Unknown) { + charset = CharacterSet::ISO8859_1; + } + + EncoderContext context(TextEncoder::FromUnicode(msg, charset)); context.setSymbolShape(shape); context.setSizeConstraints(minWidth, minHeight, maxWidth, maxHeight); diff --git a/core/src/datamatrix/DMHighLevelEncoder.h b/core/src/datamatrix/DMHighLevelEncoder.h index 1a17587300..8f1e589707 100644 --- a/core/src/datamatrix/DMHighLevelEncoder.h +++ b/core/src/datamatrix/DMHighLevelEncoder.h @@ -6,6 +6,8 @@ #pragma once +#include "CharacterSet.h" + #include namespace ZXing { @@ -21,7 +23,7 @@ enum class SymbolShape; * annex S. */ ByteArray Encode(const std::wstring& msg); -ByteArray Encode(const std::wstring& msg, SymbolShape shape, int minWidth, int minHeight, int maxWidth, int maxHeight); +ByteArray Encode(const std::wstring& msg, CharacterSet encoding, SymbolShape shape, int minWidth, int minHeight, int maxWidth, int maxHeight); } // DataMatrix } // ZXing diff --git a/core/src/datamatrix/DMReader.cpp b/core/src/datamatrix/DMReader.cpp index 0754a8411f..313f39993e 100644 --- a/core/src/datamatrix/DMReader.cpp +++ b/core/src/datamatrix/DMReader.cpp @@ -9,7 +9,7 @@ #include "BinaryBitmap.h" #include "DMDecoder.h" #include "DMDetector.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "DecoderResult.h" #include "DetectorResult.h" #include "Result.h" @@ -26,8 +26,8 @@ Result Reader::decode(const BinaryBitmap& image) const auto binImg = image.getBitMatrix(); if (binImg == nullptr) return {}; - - auto detectorResult = Detect(*binImg, _hints.tryHarder(), _hints.tryRotate(), _hints.isPure()); + + auto detectorResult = Detect(*binImg, _opts.tryHarder(), _opts.tryRotate(), _opts.isPure()); if (!detectorResult.isValid()) return {}; @@ -43,9 +43,9 @@ Results Reader::decode(const BinaryBitmap& image, int maxSymbols) const return {}; Results results; - for (auto&& detRes : Detect(*binImg, _hints.tryHarder(), _hints.tryRotate(), _hints.isPure())) { + for (auto&& detRes : Detect(*binImg, _opts.tryHarder(), _opts.tryRotate(), _opts.isPure())) { auto decRes = Decode(detRes.bits()); - if (decRes.isValid(_hints.returnErrors())) { + if (decRes.isValid(_opts.returnErrors())) { results.emplace_back(std::move(decRes), std::move(detRes).position(), BarcodeFormat::DataMatrix); if (maxSymbols > 0 && Size(results) >= maxSymbols) break; diff --git a/core/src/datamatrix/DMWriter.cpp b/core/src/datamatrix/DMWriter.cpp index e3ab758de3..a6723843c3 100644 --- a/core/src/datamatrix/DMWriter.cpp +++ b/core/src/datamatrix/DMWriter.cpp @@ -8,6 +8,7 @@ #include "BitMatrix.h" #include "ByteArray.h" +#include "CharacterSet.h" #include "DMBitLayout.h" #include "DMECEncoder.h" #include "DMHighLevelEncoder.h" @@ -75,7 +76,8 @@ static BitMatrix EncodeLowLevel(const BitMatrix& placement, const SymbolInfo& sy } Writer::Writer() : - _shapeHint(SymbolShape::NONE) + _shapeHint(SymbolShape::NONE), + _encoding(CharacterSet::Unknown) { } @@ -91,7 +93,7 @@ Writer::encode(const std::wstring& contents, int width, int height) const } //1. step: Data encodation - auto encoded = Encode(contents, _shapeHint, _minWidth, _minHeight, _maxWidth, _maxHeight); + auto encoded = Encode(contents, _encoding, _shapeHint, _minWidth, _minHeight, _maxWidth, _maxHeight); const SymbolInfo* symbolInfo = SymbolInfo::Lookup(Size(encoded), _shapeHint, _minWidth, _minHeight, _maxWidth, _maxHeight); if (symbolInfo == nullptr) { throw std::invalid_argument("Can't find a symbol arrangement that matches the message. Data codewords: " + std::to_string(encoded.size())); diff --git a/core/src/datamatrix/DMWriter.h b/core/src/datamatrix/DMWriter.h index c0e3a43ee4..896f0036e8 100644 --- a/core/src/datamatrix/DMWriter.h +++ b/core/src/datamatrix/DMWriter.h @@ -6,6 +6,7 @@ #pragma once +#include "CharacterSet.h" #include "DMSymbolShape.h" #include @@ -43,12 +44,18 @@ class Writer return *this; } + Writer& setEncoding(CharacterSet encoding) { + _encoding = encoding; + return *this; + } + BitMatrix encode(const std::wstring& contents, int width, int height) const; BitMatrix encode(const std::string& contents, int width, int height) const; private: SymbolShape _shapeHint; int _quietZone = 1, _minWidth = -1, _minHeight = -1, _maxWidth = -1, _maxHeight = -1; + CharacterSet _encoding; }; } // DataMatrix diff --git a/core/src/maxicode/MCReader.cpp b/core/src/maxicode/MCReader.cpp index 6a41fc9b42..66f25aa3f8 100644 --- a/core/src/maxicode/MCReader.cpp +++ b/core/src/maxicode/MCReader.cpp @@ -8,7 +8,7 @@ #include "BinaryBitmap.h" #include "BitMatrix.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "DecoderResult.h" #include "MCBitMatrixParser.h" #include "MCDecoder.h" diff --git a/core/src/oned/ODCodabarReader.cpp b/core/src/oned/ODCodabarReader.cpp index f72b6ad44c..1d6121bc4c 100644 --- a/core/src/oned/ODCodabarReader.cpp +++ b/core/src/oned/ODCodabarReader.cpp @@ -7,7 +7,7 @@ #include "ODCodabarReader.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" #include "ZXAlgorithms.h" @@ -84,7 +84,7 @@ CodabarReader::decodePattern(int rowNumber, PatternView& next, std::unique_ptr&) const { // minimal number of characters that must be present (including start, stop and checksum characters) - int minCharCount = _hints.validateCode39CheckSum() ? 4 : 3; + int minCharCount = _opts.validateCode39CheckSum() ? 4 : 3; auto isStartOrStopSymbol = [](char c) { return c == '*'; }; // provide the indices with the narrow bars/spaces which have to be equally wide @@ -116,7 +116,7 @@ Result Code39Reader::decodePattern(int rowNumber, PatternView& next, std::unique return {}; Error error; - if (_hints.validateCode39CheckSum()) { + if (_opts.validateCode39CheckSum()) { auto checkDigit = txt.back(); txt.pop_back(); int checksum = TransformReduce(txt, 0, [](char c) { return IndexOf(ALPHABET, c); }); @@ -124,12 +124,12 @@ Result Code39Reader::decodePattern(int rowNumber, PatternView& next, std::unique error = ChecksumError(); } - if (!error && _hints.tryCode39ExtendedMode() && !DecodeExtendedCode39AndCode93(txt, "$%/+")) + if (!error && _opts.tryCode39ExtendedMode() && !DecodeExtendedCode39AndCode93(txt, "$%/+")) error = FormatError("Decoding extended Code39/Code93 failed"); // Symbology identifier modifiers ISO/IEC 16388:2007 Annex C Table C.1 constexpr const char symbologyModifiers[4] = { '0', '3' /*checksum*/, '4' /*extended*/, '7' /*checksum,extended*/ }; - SymbologyIdentifier symbologyIdentifier = {'A', symbologyModifiers[(int)_hints.tryCode39ExtendedMode() * 2 + (int)_hints.validateCode39CheckSum()]}; + SymbologyIdentifier symbologyIdentifier = {'A', symbologyModifiers[(int)_opts.tryCode39ExtendedMode() * 2 + (int)_opts.validateCode39CheckSum()]}; int xStop = next.pixelsTillEnd(); return Result(std::move(txt), rowNumber, xStart, xStop, BarcodeFormat::Code39, symbologyIdentifier, error); diff --git a/core/src/oned/ODITFReader.cpp b/core/src/oned/ODITFReader.cpp index eec3cf3799..439a1933dd 100644 --- a/core/src/oned/ODITFReader.cpp +++ b/core/src/oned/ODITFReader.cpp @@ -6,7 +6,7 @@ #include "ODITFReader.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "GTIN.h" #include "Result.h" #include "ZXAlgorithms.h" @@ -66,14 +66,14 @@ Result ITFReader::decodePattern(int rowNumber, PatternView& next, std::unique_pt return {}; Error error; - if (_hints.validateITFCheckSum() && !GTIN::IsCheckDigitValid(txt)) + if (_opts.validateITFCheckSum() && !GTIN::IsCheckDigitValid(txt)) error = ChecksumError(); // Symbology identifier ISO/IEC 16390:2007 Annex C Table C.1 // See also GS1 General Specifications 5.1.3 Figure 5.1.3-2 SymbologyIdentifier symbologyIdentifier = {'I', '0'}; // No check character validation - - if (_hints.validateITFCheckSum() || (txt.size() == 14 && GTIN::IsCheckDigitValid(txt))) // If no hint test if valid ITF-14 + + if (_opts.validateITFCheckSum() || (txt.size() == 14 && GTIN::IsCheckDigitValid(txt))) // If no hint test if valid ITF-14 symbologyIdentifier.modifier = '1'; // Modulo 10 symbol check character validated and transmitted int xStop = next.pixelsTillEnd(); diff --git a/core/src/oned/ODMultiUPCEANReader.cpp b/core/src/oned/ODMultiUPCEANReader.cpp index aa88369c8c..738268e01d 100644 --- a/core/src/oned/ODMultiUPCEANReader.cpp +++ b/core/src/oned/ODMultiUPCEANReader.cpp @@ -9,7 +9,7 @@ #include "BarcodeFormat.h" #include "BitArray.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "GTIN.h" #include "ODUPCEANCommon.h" #include "Result.h" @@ -70,7 +70,7 @@ static bool DecodeDigit(const PatternView& view, std::string& txt, int* lgPatter // clang-format off /* pattern now contains the central 5 bits of the L/G/R code - * L/G-codes always wart with 1 and end with 0, R-codes are simply + * L/G-codes always start with 1 and end with 0, R-codes are simply * inverted L-codes. L-Code G-Code R-Code @@ -272,10 +272,10 @@ Result MultiUPCEANReader::decodePattern(int rowNumber, PatternView& next, std::u PartialResult res; auto begin = next; - - if (!(((_hints.hasFormat(BarcodeFormat::EAN13 | BarcodeFormat::UPCA)) && EAN13(res, begin)) || - (_hints.hasFormat(BarcodeFormat::EAN8) && EAN8(res, begin)) || - (_hints.hasFormat(BarcodeFormat::UPCE) && UPCE(res, begin)))) + + if (!(((_opts.hasFormat(BarcodeFormat::EAN13 | BarcodeFormat::UPCA)) && EAN13(res, begin)) || + (_opts.hasFormat(BarcodeFormat::EAN8) && EAN8(res, begin)) || + (_opts.hasFormat(BarcodeFormat::UPCE) && UPCE(res, begin)))) return {}; Error error; @@ -285,13 +285,13 @@ Result MultiUPCEANReader::decodePattern(int rowNumber, PatternView& next, std::u // If UPC-A was a requested format and we detected a EAN-13 code with a leading '0', then we drop the '0' and call it // a UPC-A code. // TODO: this is questionable - if (_hints.hasFormat(BarcodeFormat::UPCA) && res.format == BarcodeFormat::EAN13 && res.txt.front() == '0') { + if (_opts.hasFormat(BarcodeFormat::UPCA) && res.format == BarcodeFormat::EAN13 && res.txt.front() == '0') { res.txt = res.txt.substr(1); res.format = BarcodeFormat::UPCA; } // if we explicitly requested UPCA but not EAN13, don't return an EAN13 symbol - if (res.format == BarcodeFormat::EAN13 && ! _hints.hasFormat(BarcodeFormat::EAN13)) + if (res.format == BarcodeFormat::EAN13 && ! _opts.hasFormat(BarcodeFormat::EAN13)) return {}; // Symbology identifier modifiers ISO/IEC 15420:2009 Annex B Table B.1 @@ -303,7 +303,7 @@ Result MultiUPCEANReader::decodePattern(int rowNumber, PatternView& next, std::u auto ext = res.end; PartialResult addOnRes; - if (_hints.eanAddOnSymbol() != EanAddOnSymbol::Ignore && ext.skipSymbol() && ext.skipSingle(static_cast(begin.sum() * 3.5)) + if (_opts.eanAddOnSymbol() != EanAddOnSymbol::Ignore && ext.skipSymbol() && ext.skipSingle(static_cast(begin.sum() * 3.5)) && (AddOn(addOnRes, ext, 5) || AddOn(addOnRes, ext, 2))) { // ISO/IEC 15420:2009 states that the content for "]E3" should be 15 or 18 digits, i.e. converted to EAN-13 // and extended with no separator, and that the content for "]E4" should be 8 digits, i.e. no add-on @@ -313,8 +313,8 @@ Result MultiUPCEANReader::decodePattern(int rowNumber, PatternView& next, std::u if (res.format != BarcodeFormat::EAN8) // Keeping EAN-8 with add-on as "]E4" symbologyIdentifier.modifier = '3'; // Combined packet, EAN-13, UPC-A, UPC-E, with add-on } - - if (_hints.eanAddOnSymbol() == EanAddOnSymbol::Require && !addOnRes.isValid()) + + if (_opts.eanAddOnSymbol() == EanAddOnSymbol::Require && !addOnRes.isValid()) return {}; return Result(res.txt, rowNumber, begin.pixelsInFront(), next.pixelsTillEnd(), res.format, symbologyIdentifier, error); diff --git a/core/src/oned/ODReader.cpp b/core/src/oned/ODReader.cpp index e928d04f77..07f6a62533 100644 --- a/core/src/oned/ODReader.cpp +++ b/core/src/oned/ODReader.cpp @@ -8,7 +8,7 @@ #include "ODReader.h" #include "BinaryBitmap.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "ODCodabarReader.h" #include "ODCode128Reader.h" #include "ODCode39Reader.h" @@ -38,29 +38,29 @@ void IncrementLineCount(Result& r) namespace ZXing::OneD { -Reader::Reader(const DecodeHints& hints) : ZXing::Reader(hints) +Reader::Reader(const ReaderOptions& opts) : ZXing::Reader(opts) { _readers.reserve(8); - auto formats = hints.formats().empty() ? BarcodeFormat::Any : hints.formats(); + auto formats = opts.formats().empty() ? BarcodeFormat::Any : opts.formats(); if (formats.testFlags(BarcodeFormat::EAN13 | BarcodeFormat::UPCA | BarcodeFormat::EAN8 | BarcodeFormat::UPCE)) - _readers.emplace_back(new MultiUPCEANReader(hints)); + _readers.emplace_back(new MultiUPCEANReader(opts)); if (formats.testFlag(BarcodeFormat::Code39)) - _readers.emplace_back(new Code39Reader(hints)); + _readers.emplace_back(new Code39Reader(opts)); if (formats.testFlag(BarcodeFormat::Code93)) - _readers.emplace_back(new Code93Reader(hints)); + _readers.emplace_back(new Code93Reader(opts)); if (formats.testFlag(BarcodeFormat::Code128)) - _readers.emplace_back(new Code128Reader(hints)); + _readers.emplace_back(new Code128Reader(opts)); if (formats.testFlag(BarcodeFormat::ITF)) - _readers.emplace_back(new ITFReader(hints)); + _readers.emplace_back(new ITFReader(opts)); if (formats.testFlag(BarcodeFormat::Codabar)) - _readers.emplace_back(new CodabarReader(hints)); + _readers.emplace_back(new CodabarReader(opts)); if (formats.testFlags(BarcodeFormat::DataBar)) - _readers.emplace_back(new DataBarReader(hints)); + _readers.emplace_back(new DataBarReader(opts)); if (formats.testFlags(BarcodeFormat::DataBarExpanded)) - _readers.emplace_back(new DataBarExpandedReader(hints)); + _readers.emplace_back(new DataBarExpandedReader(opts)); } Reader::~Reader() = default; @@ -255,21 +255,21 @@ Result Reader::decode(const BinaryBitmap& image) const { auto result = - DoDecode(_readers, image, _hints.tryHarder(), false, _hints.isPure(), 1, _hints.minLineCount(), _hints.returnErrors()); - - if (result.empty() && _hints.tryRotate()) - result = DoDecode(_readers, image, _hints.tryHarder(), true, _hints.isPure(), 1, _hints.minLineCount(), _hints.returnErrors()); + DoDecode(_readers, image, _opts.tryHarder(), false, _opts.isPure(), 1, _opts.minLineCount(), _opts.returnErrors()); + + if (result.empty() && _opts.tryRotate()) + result = DoDecode(_readers, image, _opts.tryHarder(), true, _opts.isPure(), 1, _opts.minLineCount(), _opts.returnErrors()); return FirstOrDefault(std::move(result)); } Results Reader::decode(const BinaryBitmap& image, int maxSymbols) const { - auto resH = DoDecode(_readers, image, _hints.tryHarder(), false, _hints.isPure(), maxSymbols, _hints.minLineCount(), - _hints.returnErrors()); - if ((!maxSymbols || Size(resH) < maxSymbols) && _hints.tryRotate()) { - auto resV = DoDecode(_readers, image, _hints.tryHarder(), true, _hints.isPure(), maxSymbols - Size(resH), - _hints.minLineCount(), _hints.returnErrors()); + auto resH = DoDecode(_readers, image, _opts.tryHarder(), false, _opts.isPure(), maxSymbols, _opts.minLineCount(), + _opts.returnErrors()); + if ((!maxSymbols || Size(resH) < maxSymbols) && _opts.tryRotate()) { + auto resV = DoDecode(_readers, image, _opts.tryHarder(), true, _opts.isPure(), maxSymbols - Size(resH), + _opts.minLineCount(), _opts.returnErrors()); resH.insert(resH.end(), resV.begin(), resV.end()); } return resH; diff --git a/core/src/oned/ODReader.h b/core/src/oned/ODReader.h index ab2b5866a8..647452ddcf 100644 --- a/core/src/oned/ODReader.h +++ b/core/src/oned/ODReader.h @@ -13,7 +13,7 @@ namespace ZXing { -class DecodeHints; +class ReaderOptions; namespace OneD { @@ -22,7 +22,7 @@ class RowReader; class Reader : public ZXing::Reader { public: - explicit Reader(const DecodeHints& hints); + explicit Reader(const ReaderOptions& opts); ~Reader() override; Result decode(const BinaryBitmap& image) const override; diff --git a/core/src/oned/ODRowReader.h b/core/src/oned/ODRowReader.h index 837037528a..7e3e2e932a 100644 --- a/core/src/oned/ODRowReader.h +++ b/core/src/oned/ODRowReader.h @@ -40,7 +40,7 @@ RSSExp.: v?-74d/?-41c namespace ZXing { -class DecodeHints; +class ReaderOptions; namespace OneD { @@ -51,11 +51,11 @@ namespace OneD { class RowReader { protected: - const DecodeHints& _hints; + const ReaderOptions& _opts; public: - explicit RowReader(const DecodeHints& hints) : _hints(hints) {} - explicit RowReader(DecodeHints&& hints) = delete; + explicit RowReader(const ReaderOptions& opts) : _opts(opts) {} + explicit RowReader(ReaderOptions&&) = delete; struct DecodingState { diff --git a/core/src/pdf417/PDFDecoder.cpp b/core/src/pdf417/PDFDecoder.cpp index 1c6c89633b..ce8d13711a 100644 --- a/core/src/pdf417/PDFDecoder.cpp +++ b/core/src/pdf417/PDFDecoder.cpp @@ -367,7 +367,6 @@ static int ProcessByteECIs(const std::vector& codewords, int codeIndex, Con * This includes all ASCII characters value 0 to 127 inclusive and provides for international * character set support. * -* @param status Set on format error. * @param mode The byte compaction mode i.e. 901 or 924 * @param codewords The array of codewords (data + error) * @param codeIndex The current index into the codeword array. @@ -475,11 +474,9 @@ static std::string DecodeBase900toBase10(const std::vector& codewords, int /** * Numeric Compaction mode (see 5.4.4) permits efficient encoding of numeric data strings. * -* @param status Set on format error. * @param codewords The array of codewords (data + error) * @param codeIndex The current index into the codeword array. * @param result The decoded data is appended to the result. -* @param encoding Currently active character encoding. * @return The next index into the codeword array. */ static int NumericCompaction(const std::vector& codewords, int codeIndex, Content& result) @@ -702,6 +699,8 @@ DecoderResult Decode(const std::vector& codewords) break; } } + } catch (std::exception& e) { + return FormatError(e.what()); } catch (Error e) { return e; } diff --git a/core/src/pdf417/PDFEncoder.cpp b/core/src/pdf417/PDFEncoder.cpp index 2e33035925..a5b884e498 100644 --- a/core/src/pdf417/PDFEncoder.cpp +++ b/core/src/pdf417/PDFEncoder.cpp @@ -305,9 +305,8 @@ static int GetErrorCorrectionCodewordCount(int errorCorrectionLevel) /** * Generates the error correction codewords according to 4.10 in ISO/IEC 15438:2001(E). * -* @param dataCodewords the data codewords +* @param dataCodewords the data codewords (including error correction after return) * @param errorCorrectionLevel the error correction level (0-8) -* @return the String representing the error correction codewords */ static void GenerateErrorCorrection(std::vector& dataCodewords, int errorCorrectionLevel) { @@ -446,7 +445,6 @@ static BarcodeMatrix EncodeLowLevel(const std::vector& fullCodewords, int c * * @param sourceCodeWords number of code words * @param errorCorrectionCodeWords number of error correction code words -* @return dimension object containing cols as width and rows as height */ static void DetermineDimensions(int minCols, int maxCols, int minRows, int maxRows, int sourceCodeWords, int errorCorrectionCodeWords, int& outCols, int& outRows) { diff --git a/core/src/pdf417/PDFEncoder.h b/core/src/pdf417/PDFEncoder.h index 3f086dfd0d..47b53d28fa 100644 --- a/core/src/pdf417/PDFEncoder.h +++ b/core/src/pdf417/PDFEncoder.h @@ -50,7 +50,7 @@ class BarcodeRow * This function scales the row * * @param scale How much you want the image to be scaled, must be greater than or equal to 1. - * @return the scaled row + * @param output the scaled row */ void getScaledRow(int scale, std::vector& output) const { output.resize(_row.size() * scale); diff --git a/core/src/pdf417/PDFHighLevelEncoder.cpp b/core/src/pdf417/PDFHighLevelEncoder.cpp index 07f08b4421..600f5c741a 100644 --- a/core/src/pdf417/PDFHighLevelEncoder.cpp +++ b/core/src/pdf417/PDFHighLevelEncoder.cpp @@ -205,11 +205,11 @@ static bool IsText(int ch) * Encode parts of the message using Text Compaction as described in ISO/IEC 15438:2001(E), * chapter 4.4.2. * -* @param msg the message -* @param startpos the start position within the message -* @param count the number of characters to encode -* @param sb receives the encoded codewords -* @param initialSubmode should normally be SUBMODE_ALPHA +* @param msg the message +* @param startpos the start position within the message +* @param count the number of characters to encode +* @param submode should normally be SUBMODE_ALPHA +* @param output receives the encoded codewords * @return the text submode in which this method ends */ static int EncodeText(const std::wstring& msg, int startpos, int count, int submode, std::vector& output) @@ -318,7 +318,7 @@ static int EncodeText(const std::wstring& msg, int startpos, int count, int subm * @param startpos the start position within the message * @param count the number of bytes to encode * @param startmode the mode from which this method starts -* @param sb receives the encoded codewords +* @param output receives the encoded codewords */ static void EncodeBinary(const std::string& bytes, int startpos, int count, int startmode, std::vector& output) { @@ -456,7 +456,6 @@ static int DetermineConsecutiveTextCount(const std::wstring& msg, int startpos) * * @param msg the message * @param startpos the start position within the message -* @param encoding the charset used to convert the message to a byte array * @return the requested character count */ static int DetermineConsecutiveBinaryCount(const std::wstring& msg, int startpos) diff --git a/core/src/pdf417/PDFReader.cpp b/core/src/pdf417/PDFReader.cpp index dd77c7cc1f..27d943c5e0 100644 --- a/core/src/pdf417/PDFReader.cpp +++ b/core/src/pdf417/PDFReader.cpp @@ -9,7 +9,7 @@ #include "PDFDetector.h" #include "PDFScanningDecoder.h" #include "PDFCodewordDecoder.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "DecoderResult.h" #include "Result.h" @@ -298,20 +298,20 @@ static Result DecodePure(const BinaryBitmap& image_) Result Reader::decode(const BinaryBitmap& image) const { - if (_hints.isPure()) { + if (_opts.isPure()) { auto res = DecodePure(image); if (res.error() != Error::Checksum) return res; // This falls through and tries the non-pure code path if we have a checksum error. This approach is // currently the best option to deal with 'aliased' input like e.g. 03-aliased.png } - - return FirstOrDefault(DoDecode(image, false, _hints.tryRotate(), _hints.returnErrors())); + + return FirstOrDefault(DoDecode(image, false, _opts.tryRotate(), _opts.returnErrors())); } Results Reader::decode(const BinaryBitmap& image, [[maybe_unused]] int maxSymbols) const { - return DoDecode(image, true, _hints.tryRotate(), _hints.returnErrors()); + return DoDecode(image, true, _opts.tryRotate(), _opts.returnErrors()); } } // Pdf417 diff --git a/core/src/qrcode/QRBitMatrixParser.cpp b/core/src/qrcode/QRBitMatrixParser.cpp index b970266c30..1aea0d9dfa 100644 --- a/core/src/qrcode/QRBitMatrixParser.cpp +++ b/core/src/qrcode/QRBitMatrixParser.cpp @@ -1,6 +1,7 @@ /* * Copyright 2016 Nu-book Inc. * Copyright 2016 ZXing authors +* Copyright 2023 gitlost */ // SPDX-License-Identifier: Apache-2.0 @@ -22,24 +23,25 @@ static bool getBit(const BitMatrix& bitMatrix, int x, int y, bool mirrored = fal return mirrored ? bitMatrix.get(y, x) : bitMatrix.get(x, y); } -static bool hasValidDimension(const BitMatrix& bitMatrix, bool isMicro) +const Version* ReadVersion(const BitMatrix& bitMatrix, Type type) { - int dimension = bitMatrix.height(); - if (isMicro) - return dimension >= 11 && dimension <= 17 && (dimension % 2) == 1; - else - return dimension >= 21 && dimension <= 177 && (dimension % 4) == 1; -} + assert(Version::HasValidSize(bitMatrix)); -const Version* ReadVersion(const BitMatrix& bitMatrix) -{ - int dimension = bitMatrix.height(); + int number = Version::Number(bitMatrix); - const Version* version = Version::FromDimension(dimension); + switch (type) { + case Type::Micro: return Version::Micro(number); + case Type::rMQR: return Version::rMQR(number); + case Type::Model1: return Version::Model1(number); + case Type::Model2: break; + } + const Version* version = Version::Model2(number); if (!version || version->versionNumber() < 7) return version; + int dimension = bitMatrix.height(); + for (bool mirror : {false, true}) { // Read top-right/bottom-left version info: 3 wide by 6 tall (depending on mirrored) int versionBits = 0; @@ -55,12 +57,9 @@ const Version* ReadVersion(const BitMatrix& bitMatrix) return nullptr; } -FormatInformation ReadFormatInformation(const BitMatrix& bitMatrix, bool isMicro) +FormatInformation ReadFormatInformation(const BitMatrix& bitMatrix) { - if (!hasValidDimension(bitMatrix, isMicro)) - return {}; - - if (isMicro) { + if (Version::HasValidSize(bitMatrix, Type::Micro)) { // Read top-left format info bits int formatInfoBits = 0; for (int x = 1; x < 9; x++) @@ -70,6 +69,27 @@ FormatInformation ReadFormatInformation(const BitMatrix& bitMatrix, bool isMicro return FormatInformation::DecodeMQR(formatInfoBits); } + if (Version::HasValidSize(bitMatrix, Type::rMQR)) { + // Read top-left format info bits + uint32_t formatInfoBits1 = 0; + for (int y = 3; y >= 1; y--) + AppendBit(formatInfoBits1, getBit(bitMatrix, 11, y)); + for (int x = 10; x >= 8; x--) + for (int y = 5; y >= 1; y--) + AppendBit(formatInfoBits1, getBit(bitMatrix, x, y)); + + // Read bottom-right format info bits + uint32_t formatInfoBits2 = 0; + const int width = bitMatrix.width(); + const int height = bitMatrix.height(); + for (int x = 3; x <= 5; x++) + AppendBit(formatInfoBits2, getBit(bitMatrix, width - x, height - 6)); + for (int x = 6; x <= 8; x++) + for (int y = 2; y <= 6; y++) + AppendBit(formatInfoBits2, getBit(bitMatrix, width - x, height - y)); + + return FormatInformation::DecodeRMQR(formatInfoBits1, formatInfoBits2); + } // Read top-left format info bits int formatInfoBits1 = 0; @@ -135,6 +155,65 @@ static ByteArray ReadQRCodewords(const BitMatrix& bitMatrix, const Version& vers return result; } +static ByteArray ReadQRCodewordsModel1(const BitMatrix& bitMatrix, const Version& version, const FormatInformation& formatInfo) +{ + ByteArray result; + result.reserve(version.totalCodewords()); + int dimension = bitMatrix.height(); + int columns = dimension / 4 + 1 + 2; + for (int j = 0; j < columns; j++) { + if (j <= 1) { // vertical symbols on the right side + int rows = (dimension - 8) / 4; + for (int i = 0; i < rows; i++) { + if (j == 0 && i % 2 == 0 && i > 0 && i < rows - 1) // extension + continue; + int x = (dimension - 1) - (j * 2); + int y = (dimension - 1) - (i * 4); + uint8_t currentByte = 0; + for (int b = 0; b < 8; b++) { + AppendBit(currentByte, GetDataMaskBit(formatInfo.dataMask, x - b % 2, y - (b / 2)) + != getBit(bitMatrix, x - b % 2, y - (b / 2), formatInfo.isMirrored)); + } + result.push_back(currentByte); + } + } else if (columns - j <= 4) { // vertical symbols on the left side + int rows = (dimension - 16) / 4; + for (int i = 0; i < rows; i++) { + int x = (columns - j - 1) * 2 + 1 + (columns - j == 4 ? 1 : 0); // timing + int y = (dimension - 1) - 8 - (i * 4); + uint8_t currentByte = 0; + for (int b = 0; b < 8; b++) { + AppendBit(currentByte, GetDataMaskBit(formatInfo.dataMask, x - b % 2, y - (b / 2)) + != getBit(bitMatrix, x - b % 2, y - (b / 2), formatInfo.isMirrored)); + } + result.push_back(currentByte); + } + } else { // horizontal symbols + int rows = dimension / 2; + for (int i = 0; i < rows; i++) { + if (j == 2 && i >= rows - 4) // alignment & finder + continue; + if (i == 0 && j % 2 == 1 && j + 1 != columns - 4) // extension + continue; + int x = (dimension - 1) - (2 * 2) - (j - 2) * 4; + int y = (dimension - 1) - (i * 2) - (i >= rows - 3 ? 1 : 0); // timing + uint8_t currentByte = 0; + for (int b = 0; b < 8; b++) { + AppendBit(currentByte, GetDataMaskBit(formatInfo.dataMask, x - b % 4, y - (b / 4)) + != getBit(bitMatrix, x - b % 4, y - (b / 4), formatInfo.isMirrored)); + } + result.push_back(currentByte); + } + } + } + + result[0] &= 0xf; // ignore corner + if (Size(result) != version.totalCodewords()) + return {}; + + return result; +} + static ByteArray ReadMQRCodewords(const BitMatrix& bitMatrix, const QRCode::Version& version, const FormatInformation& formatInfo) { BitMatrix functionPattern = version.buildFunctionPattern(); @@ -181,13 +260,53 @@ static ByteArray ReadMQRCodewords(const BitMatrix& bitMatrix, const QRCode::Vers return result; } -ByteArray ReadCodewords(const BitMatrix& bitMatrix, const Version& version, const FormatInformation& formatInfo) +static ByteArray ReadRMQRCodewords(const BitMatrix& bitMatrix, const Version& version, const FormatInformation& formatInfo) { - if (!hasValidDimension(bitMatrix, version.isMicroQRCode())) + BitMatrix functionPattern = version.buildFunctionPattern(); + + ByteArray result; + result.reserve(version.totalCodewords()); + uint8_t currentByte = 0; + bool readingUp = true; + int bitsRead = 0; + const int width = bitMatrix.width(); + const int height = bitMatrix.height(); + // Read columns in pairs, from right to left + for (int x = width - 1 - 1; x > 0; x -= 2) { // Skip right edge alignment + // Read alternatingly from bottom to top then top to bottom + for (int row = 0; row < height; row++) { + int y = readingUp ? height - 1 - row : row; + for (int col = 0; col < 2; col++) { + int xx = x - col; + // Ignore bits covered by the function pattern + if (!functionPattern.get(xx, y)) { + // Read a bit + AppendBit(currentByte, + GetDataMaskBit(formatInfo.dataMask, xx, y) != getBit(bitMatrix, xx, y, formatInfo.isMirrored)); + // If we've made a whole byte, save it off + if (++bitsRead % 8 == 0) + result.push_back(std::exchange(currentByte, 0)); + } + } + } + readingUp = !readingUp; // switch directions + } + if (Size(result) != version.totalCodewords()) return {}; - return version.isMicroQRCode() ? ReadMQRCodewords(bitMatrix, version, formatInfo) - : ReadQRCodewords(bitMatrix, version, formatInfo); + return result; +} + +ByteArray ReadCodewords(const BitMatrix& bitMatrix, const Version& version, const FormatInformation& formatInfo) +{ + switch (version.type()) { + case Type::Micro: return ReadMQRCodewords(bitMatrix, version, formatInfo); + case Type::rMQR: return ReadRMQRCodewords(bitMatrix, version, formatInfo); + case Type::Model1: return ReadQRCodewordsModel1(bitMatrix, version, formatInfo); + case Type::Model2: return ReadQRCodewords(bitMatrix, version, formatInfo); + } + + return {}; } } // namespace ZXing::QRCode diff --git a/core/src/qrcode/QRBitMatrixParser.h b/core/src/qrcode/QRBitMatrixParser.h index 0f070c8403..e695472b84 100644 --- a/core/src/qrcode/QRBitMatrixParser.h +++ b/core/src/qrcode/QRBitMatrixParser.h @@ -6,6 +6,8 @@ #pragma once +#include "QRErrorCorrectionLevel.h" + namespace ZXing { class BitMatrix; @@ -20,14 +22,14 @@ class FormatInformation; * @brief Reads version information from the QR Code. * @return {@link Version} encapsulating the QR Code's version, nullptr if neither location can be parsed */ -const Version* ReadVersion(const BitMatrix& bitMatrix); +const Version* ReadVersion(const BitMatrix& bitMatrix, Type type); /** * @brief Reads format information from one of its two locations within the QR Code. * @return {@link FormatInformation} encapsulating the QR Code's format info, result is invalid if both format * information locations cannot be parsed as the valid encoding of format information */ -FormatInformation ReadFormatInformation(const BitMatrix& bitMatrix, bool isMicro); +FormatInformation ReadFormatInformation(const BitMatrix& bitMatrix); /** * @brief Reads the codewords from the BitMatrix. diff --git a/core/src/qrcode/QRCodecMode.cpp b/core/src/qrcode/QRCodecMode.cpp index 3d9f435996..5020118a87 100644 --- a/core/src/qrcode/QRCodecMode.cpp +++ b/core/src/qrcode/QRCodecMode.cpp @@ -1,6 +1,7 @@ /* * Copyright 2016 Nu-book Inc. * Copyright 2016 ZXing authors +* Copyright 2023 gitlost */ // SPDX-License-Identifier: Apache-2.0 @@ -15,15 +16,22 @@ namespace ZXing::QRCode { -CodecMode CodecModeForBits(int bits, bool isMicro) +CodecMode CodecModeForBits(int bits, Type type) { - if (!isMicro) { - if ((bits >= 0x00 && bits <= 0x05) || (bits >= 0x07 && bits <= 0x09) || bits == 0x0d) - return static_cast(bits); - } else { + if (type == Type::Micro) { constexpr CodecMode Bits2Mode[4] = {CodecMode::NUMERIC, CodecMode::ALPHANUMERIC, CodecMode::BYTE, CodecMode::KANJI}; if (bits < Size(Bits2Mode)) return Bits2Mode[bits]; + } else if (type == Type::rMQR) { + constexpr CodecMode Bits2Mode[8] = { + CodecMode::TERMINATOR, CodecMode::NUMERIC, CodecMode::ALPHANUMERIC, CodecMode::BYTE, + CodecMode::KANJI, CodecMode::FNC1_FIRST_POSITION, CodecMode::FNC1_SECOND_POSITION, CodecMode::ECI + }; + if (bits < Size(Bits2Mode)) + return Bits2Mode[bits]; + } else { + if ((bits >= 0x00 && bits <= 0x05) || (bits >= 0x07 && bits <= 0x09) || bits == 0x0d) + return static_cast(bits); } throw FormatError("Invalid codec mode"); @@ -32,7 +40,7 @@ CodecMode CodecModeForBits(int bits, bool isMicro) int CharacterCountBits(CodecMode mode, const Version& version) { int number = version.versionNumber(); - if (version.isMicroQRCode()) { + if (version.isMicro()) { switch (mode) { case CodecMode::NUMERIC: return std::array{3, 4, 5, 6}[number - 1]; case CodecMode::ALPHANUMERIC: return std::array{3, 4, 5}[number - 2]; @@ -42,6 +50,20 @@ int CharacterCountBits(CodecMode mode, const Version& version) default: return 0; } } + if (version.isRMQR()) { + // See ISO/IEC 23941:2022 7.4.1, Table 3 - Number of bits of character count indicator + constexpr char numeric[32] = {4, 5, 6, 7, 7, 5, 6, 7, 7, 8, 4, 6, 7, 7, 8, 8, 5, 6, 7, 7, 8, 8, 7, 7, 8, 8, 9, 7, 8, 8, 8, 9}; + constexpr char alphanum[32] = {3, 5, 5, 6, 6, 5, 5, 6, 6, 7, 4, 5, 6, 6, 7, 7, 5, 6, 6, 7, 7, 8, 6, 7, 7, 7, 8, 6, 7, 7, 8, 8}; + constexpr char byte[32] = {3, 4, 5, 5, 6, 4, 5, 5, 6, 6, 3, 5, 5, 6, 6, 7, 4, 5, 6, 6, 7, 7, 6, 6, 7, 7, 7, 6, 6, 7, 7, 8}; + constexpr char kanji[32] = {2, 3, 4, 5, 5, 3, 4, 5, 5, 6, 2, 4, 5, 5, 6, 6, 3, 5, 5, 6, 6, 7, 5, 5, 6, 6, 7, 5, 6, 6, 6, 7}; + switch (mode) { + case CodecMode::NUMERIC: return numeric[number - 1]; + case CodecMode::ALPHANUMERIC: return alphanum[number - 1]; + case CodecMode::BYTE: return byte[number - 1]; + case CodecMode::KANJI: return kanji[number - 1]; + default: return 0; + } + } int i; if (number <= 9) @@ -63,12 +85,12 @@ int CharacterCountBits(CodecMode mode, const Version& version) int CodecModeBitsLength(const Version& version) { - return version.isMicroQRCode() ? version.versionNumber() - 1 : 4; + return version.isMicro() ? version.versionNumber() - 1 : 4 - version.isRMQR(); } int TerminatorBitsLength(const Version& version) { - return version.isMicroQRCode() ? version.versionNumber() * 2 + 1 : 4; + return version.isMicro() ? version.versionNumber() * 2 + 1 : 4 - version.isRMQR(); } } // namespace ZXing::QRCode diff --git a/core/src/qrcode/QRCodecMode.h b/core/src/qrcode/QRCodecMode.h index 2c4f9d3551..eff1e18b42 100644 --- a/core/src/qrcode/QRCodecMode.h +++ b/core/src/qrcode/QRCodecMode.h @@ -8,6 +8,7 @@ namespace ZXing::QRCode { +enum class Type; class Version; /** @@ -30,11 +31,11 @@ enum class CodecMode /** * @param bits variable number of bits encoding a QR Code data mode - * @param isMicro is this a MicroQRCode + * @param type type of QR Code * @return Mode encoded by these bits * @throws FormatError if bits do not correspond to a known mode */ -CodecMode CodecModeForBits(int bits, bool isMicro = false); +CodecMode CodecModeForBits(int bits, Type type); /** * @param version version in question diff --git a/core/src/qrcode/QRDecoder.cpp b/core/src/qrcode/QRDecoder.cpp index 905f0a5c8d..44ce4d41b5 100644 --- a/core/src/qrcode/QRDecoder.cpp +++ b/core/src/qrcode/QRDecoder.cpp @@ -147,14 +147,14 @@ static void DecodeAlphanumericSegment(BitSource& bits, int count, Content& resul // See section 6.4.8.1, 6.4.8.2 if (result.symbology.aiFlag != AIFlag::None) { // We need to massage the result a bit if in an FNC1 mode: - for (size_t i = 0; i < buffer.length(); i++) { - if (buffer[i] == '%') { - if (i < buffer.length() - 1 && buffer[i + 1] == '%') { + for (auto i = buffer.begin(); i != buffer.end(); i++) { + if (*i == '%') { + if (i + 1 != buffer.end() && *(i + 1) == '%') { // %% is rendered as % - buffer.erase(i + 1); + i = buffer.erase(i); } else { // In alpha mode, % should be converted to FNC1 separator 0x1D - buffer[i] = static_cast(0x1D); + *i = static_cast(0x1D); } } } @@ -233,10 +233,13 @@ DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCo BitSource bits(bytes); Content result; Error error; - result.symbology = {'Q', '1', 1}; + result.symbology = {'Q', version.isModel1() ? '0' : '1', 1}; StructuredAppendInfo structuredAppend; const int modeBitLength = CodecModeBitsLength(version); + if (version.isModel1()) + bits.readBits(4); // Model 1 is leading with 4 0-bits -> drop them + try { while(!IsEndOfStream(bits, version)) { @@ -244,7 +247,7 @@ DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCo if (modeBitLength == 0) mode = CodecMode::NUMERIC; // MicroQRCode version 1 is always NUMERIC and modeBitLength is 0 else - mode = CodecModeForBits(bits.readBits(modeBitLength), version.isMicroQRCode()); + mode = CodecModeForBits(bits.readBits(modeBitLength), version.type()); switch (mode) { case CodecMode::FNC1_FIRST_POSITION: @@ -274,6 +277,8 @@ DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCo structuredAppend.id = std::to_string(bits.readBits(8)); break; case CodecMode::ECI: + if (version.isModel1()) + throw FormatError("QRCode Model 1 does not support ECI"); // Count doesn't apply to ECI result.switchEncoding(ParseECIValue(bits)); break; @@ -316,15 +321,19 @@ DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCo DecoderResult Decode(const BitMatrix& bits) { - const Version* pversion = ReadVersion(bits); - if (!pversion) - return FormatError("Invalid version"); - const Version& version = *pversion; + if (!Version::HasValidSize(bits)) + return FormatError("Invalid symbol size"); - auto formatInfo = ReadFormatInformation(bits, version.isMicroQRCode()); + auto formatInfo = ReadFormatInformation(bits); if (!formatInfo.isValid()) return FormatError("Invalid format information"); + const Version* pversion = ReadVersion(bits, formatInfo.type()); + if (!pversion) + return FormatError("Invalid version"); + + const Version& version = *pversion; + // Read codewords ByteArray codewords = ReadCodewords(bits, version, formatInfo); if (codewords.empty()) diff --git a/core/src/qrcode/QRDetector.cpp b/core/src/qrcode/QRDetector.cpp index 7b6c16d461..7912962dee 100644 --- a/core/src/qrcode/QRDetector.cpp +++ b/core/src/qrcode/QRDetector.cpp @@ -2,6 +2,7 @@ * Copyright 2016 Nu-book Inc. * Copyright 2016 ZXing authors * Copyright 2020 Axel Waggershauser +* Copyright 2023 gitlost */ // SPDX-License-Identifier: Apache-2.0 @@ -392,7 +393,7 @@ DetectorResult SampleQR(const BitMatrix& image, const FinderPatternSet& fp) log(br, 3); auto mod2Pix = Mod2Pix(dimension, brOffset, {fp.tl, fp.tr, br, fp.bl}); - if( dimension >= Version::DimensionOfVersion(7, false)) { + if( dimension >= Version::SymbolSize(7, Type::Model2).x) { auto version = ReadVersion(image, dimension, mod2Pix); // if the version bits are garbage -> discard the detection @@ -520,8 +521,7 @@ DetectorResult DetectPureQR(const BitMatrix& image) SaveAsPBM(image, "weg.pbm"); #endif - constexpr int MIN_MODULES = Version::DimensionOfVersion(1, false); - constexpr int MAX_MODULES = Version::DimensionOfVersion(40, false); + constexpr int MIN_MODULES = Version::SymbolSize(1, Type::Model2).x; int left, top, width, height; if (!image.findBoundingBox(left, top, width, height, MIN_MODULES) || std::abs(width - height) > 1) @@ -543,7 +543,7 @@ DetectorResult DetectPureQR(const BitMatrix& image) EstimateDimension(image, {tl + fpWidth / 2 * PointF(1, 1), fpWidth}, {tr + fpWidth / 2 * PointF(-1, 1), fpWidth}).dim; float moduleSize = float(width) / dimension; - if (dimension < MIN_MODULES || dimension > MAX_MODULES || + if (!Version::IsValidSize({dimension, dimension}, Type::Model2) || !image.isIn(PointF{left + moduleSize / 2 + (dimension - 1) * moduleSize, top + moduleSize / 2 + (dimension - 1) * moduleSize})) return {}; @@ -565,8 +565,7 @@ DetectorResult DetectPureMQR(const BitMatrix& image) { using Pattern = std::array; - constexpr int MIN_MODULES = Version::DimensionOfVersion(1, true); - constexpr int MAX_MODULES = Version::DimensionOfVersion(4, true); + constexpr int MIN_MODULES = Version::SymbolSize(1, Type::Micro).x; int left, top, width, height; if (!image.findBoundingBox(left, top, width, height, MIN_MODULES) || std::abs(width - height) > 1) @@ -583,7 +582,7 @@ DetectorResult DetectPureMQR(const BitMatrix& image) float moduleSize = float(fpWidth) / 7; int dimension = narrow_cast(std::lround(width / moduleSize)); - if (dimension < MIN_MODULES || dimension > MAX_MODULES || + if (!Version::IsValidSize({dimension, dimension}, Type::Micro) || !image.isIn(PointF{left + moduleSize / 2 + (dimension - 1) * moduleSize, top + moduleSize / 2 + (dimension - 1) * moduleSize})) return {}; @@ -601,6 +600,71 @@ DetectorResult DetectPureMQR(const BitMatrix& image) {{left, top}, {right, top}, {right, bottom}, {left, bottom}}}; } +DetectorResult DetectPureRMQR(const BitMatrix& image) +{ + constexpr auto SUBPATTERN = FixedPattern<4, 4>{1, 1, 1, 1}; + constexpr auto TIMINGPATTERN = FixedPattern<10, 10>{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + + using Pattern = std::array; + using SubPattern = std::array; + using TimingPattern = std::array; + +#ifdef PRINT_DEBUG + SaveAsPBM(image, "weg.pbm"); +#endif + + constexpr int MIN_MODULES = Version::SymbolSize(1, Type::rMQR).y; + + int left, top, width, height; + if (!image.findBoundingBox(left, top, width, height, MIN_MODULES) || height >= width) + return {}; + int right = left + width - 1; + int bottom = top + height - 1; + + PointI tl{left, top}, tr{right, top}, br{right, bottom}, bl{left, bottom}; + + // allow corners be moved one pixel inside to accommodate for possible aliasing artifacts + auto diagonal = BitMatrixCursorI(image, tl, {1, 1}).readPatternFromBlack(1); + if (!IsPattern(diagonal, PATTERN)) + return {}; + + // Finder sub pattern + auto subdiagonal = BitMatrixCursorI(image, br, {-1, -1}).readPatternFromBlack(1); + if (!IsPattern(subdiagonal, SUBPATTERN)) + return {}; + + float moduleSize = Reduce(diagonal) + Reduce(subdiagonal); + + // Horizontal timing patterns + for (auto [p, d] : {std::pair(tr, PointI{-1, 0}), {bl, {1, 0}}, {tl, {1, 0}}, {br, {-1, 0}}}) { + auto cur = BitMatrixCursorI(image, p, d); + // skip corner / finder / sub pattern edge + cur.stepToEdge(2 + cur.isWhite()); + auto timing = cur.readPattern(); + if (!IsPattern(timing, TIMINGPATTERN)) + return {}; + moduleSize += Reduce(timing); + } + + moduleSize /= 7 + 4 + 4 * 10; // fp + sub + 4 x timing + int dimW = narrow_cast(std::lround(width / moduleSize)); + int dimH = narrow_cast(std::lround(height / moduleSize)); + + if (!Version::IsValidSize(PointI{dimW, dimH}, Type::rMQR)) + return {}; + +#ifdef PRINT_DEBUG + LogMatrix log; + LogMatrixWriter lmw(log, image, 5, "grid2.pnm"); + for (int y = 0; y < dimH; y++) + for (int x = 0; x < dimW; x++) + log(PointF(left + (x + .5f) * moduleSize, top + (y + .5f) * moduleSize)); +#endif + + // Now just read off the bits (this is a crop + subsample) + return {Deflate(image, dimW, dimH, top + moduleSize / 2, left + moduleSize / 2, moduleSize), {tl, tr, br, bl}}; +} + DetectorResult SampleMQR(const BitMatrix& image, const ConcentricPattern& fp) { auto fpQuad = FindConcentricPatternCorners(image, fp, fp.size, 2); @@ -646,7 +710,7 @@ DetectorResult SampleMQR(const BitMatrix& image, const ConcentricPattern& fp) if (!bestFI.isValid()) return {}; - const int dim = Version::DimensionOfVersion(bestFI.microVersion, true); + const int dim = Version::SymbolSize(bestFI.microVersion, Type::Micro).x; // check that we are in fact not looking at a corner of a non-micro QRCode symbol // we accept at most 1/3rd black pixels in the quite zone (in a QRCode symbol we expect about 1/2). @@ -662,4 +726,95 @@ DetectorResult SampleMQR(const BitMatrix& image, const ConcentricPattern& fp) return SampleGrid(image, dim, dim, bestPT); } +DetectorResult SampleRMQR(const BitMatrix& image, const ConcentricPattern& fp) +{ + auto fpQuad = FindConcentricPatternCorners(image, fp, fp.size, 2); + if (!fpQuad) + return {}; + + auto srcQuad = Rectangle(7, 7, 0.5); + + static const PointI FORMAT_INFO_EDGE_COORDS[] = {{8, 0}, {9, 0}, {10, 0}, {11, 0}}; + static const PointI FORMAT_INFO_COORDS[] = { + {11, 3}, {11, 2}, {11, 1}, + {10, 5}, {10, 4}, {10, 3}, {10, 2}, {10, 1}, + { 9, 5}, { 9, 4}, { 9, 3}, { 9, 2}, { 9, 1}, + { 8, 5}, { 8, 4}, { 8, 3}, { 8, 2}, { 8, 1}, + }; + + FormatInformation bestFI; + PerspectiveTransform bestPT; + + for (int i = 0; i < 4; ++i) { + auto mod2Pix = PerspectiveTransform(srcQuad, RotatedCorners(*fpQuad, i)); + + auto check = [&](int i, bool on) { + auto p = mod2Pix(centered(FORMAT_INFO_EDGE_COORDS[i])); + return image.isIn(p) && image.get(p) == on; + }; + + // check that we see top edge timing pattern modules + if (!check(0, true) || !check(1, false) || !check(2, true) || !check(3, false)) + continue; + + uint32_t formatInfoBits = 0; + for (int i = 0; i < Size(FORMAT_INFO_COORDS); ++i) + AppendBit(formatInfoBits, image.get(mod2Pix(centered(FORMAT_INFO_COORDS[i])))); + + auto fi = FormatInformation::DecodeRMQR(formatInfoBits, 0 /*formatInfoBits2*/); + if (fi.hammingDistance < bestFI.hammingDistance) { + bestFI = fi; + bestPT = mod2Pix; + } + } + + if (!bestFI.isValid()) + return {}; + + const PointI dim = Version::SymbolSize(bestFI.microVersion, Type::rMQR); + + // TODO: this is a WIP + auto intersectQuads = [](QuadrilateralF& a, QuadrilateralF& b) { + auto tl = Center(a); + auto br = Center(b); + // rotate points such that topLeft of a is furthest away from b and topLeft of b is closest to a + auto dist2B = [c = br](auto a, auto b) { return distance(a, c) < distance(b, c); }; + auto offsetA = std::max_element(a.begin(), a.end(), dist2B) - a.begin(); + auto dist2A = [c = tl](auto a, auto b) { return distance(a, c) < distance(b, c); }; + auto offsetB = std::min_element(b.begin(), b.end(), dist2A) - b.begin(); + + a = RotatedCorners(a, offsetA); + b = RotatedCorners(b, offsetB); + + auto tr = (intersect(RegressionLine(a[0], a[1]), RegressionLine(b[1], b[2])) + + intersect(RegressionLine(a[3], a[2]), RegressionLine(b[0], b[3]))) + / 2; + auto bl = (intersect(RegressionLine(a[0], a[3]), RegressionLine(b[2], b[3])) + + intersect(RegressionLine(a[1], a[2]), RegressionLine(b[0], b[1]))) + / 2; + + log(tr, 2); + log(bl, 2); + + return QuadrilateralF{tl, tr, br, bl}; + }; + + if (auto found = LocateAlignmentPattern(image, fp.size / 7, bestPT(dim - PointF(3, 3)))) { + log(*found, 2); + if (auto spQuad = FindConcentricPatternCorners(image, *found, fp.size / 2, 1)) { + auto dest = intersectQuads(*fpQuad, *spQuad); + if (dim.y <= 9) { + bestPT = PerspectiveTransform({{6.5, 0.5}, {dim.x - 1.5, dim.y - 3.5}, {dim.x - 1.5, dim.y - 1.5}, {6.5, 6.5}}, + {fpQuad->topRight(), spQuad->topRight(), spQuad->bottomRight(), fpQuad->bottomRight()}); + } else { + dest[0] = fp; + dest[2] = *found; + bestPT = PerspectiveTransform({{3.5, 3.5}, {dim.x - 2.5, 3.5}, {dim.x - 2.5, dim.y - 2.5}, {3.5, dim.y - 2.5}}, dest); + } + } + } + + return SampleGrid(image, dim.x, dim.y, bestPT); +} + } // namespace ZXing::QRCode diff --git a/core/src/qrcode/QRDetector.h b/core/src/qrcode/QRDetector.h index 4173d59a7a..be767e6d30 100644 --- a/core/src/qrcode/QRDetector.h +++ b/core/src/qrcode/QRDetector.h @@ -32,9 +32,11 @@ FinderPatternSets GenerateFinderPatternSets(FinderPatterns& patterns); DetectorResult SampleQR(const BitMatrix& image, const FinderPatternSet& fp); DetectorResult SampleMQR(const BitMatrix& image, const ConcentricPattern& fp); +DetectorResult SampleRMQR(const BitMatrix& image, const ConcentricPattern& fp); DetectorResult DetectPureQR(const BitMatrix& image); DetectorResult DetectPureMQR(const BitMatrix& image); +DetectorResult DetectPureRMQR(const BitMatrix& image); } // QRCode } // ZXing diff --git a/core/src/qrcode/QREncoder.cpp b/core/src/qrcode/QREncoder.cpp index cc3f71d8d3..bcf14855fd 100644 --- a/core/src/qrcode/QREncoder.cpp +++ b/core/src/qrcode/QREncoder.cpp @@ -136,7 +136,7 @@ void AppendLengthInfo(int numLetters, const Version& version, CodecMode mode, Bi { int numBits = CharacterCountBits(mode, version); if (numLetters >= (1 << numBits)) { - return throw std::invalid_argument(std::to_string(numLetters) + " is bigger than " + std::to_string((1 << numBits) - 1)); + throw std::invalid_argument(std::to_string(numLetters) + " is bigger than " + std::to_string((1 << numBits) - 1)); } bits.appendBits(numLetters, numBits); } @@ -267,7 +267,7 @@ static bool WillFit(int numInputBits, const Version& version, ErrorCorrectionLev static const Version& ChooseVersion(int numInputBits, ErrorCorrectionLevel ecLevel) { for (int versionNum = 1; versionNum <= 40; versionNum++) { - const Version* version = Version::FromNumber(versionNum); + const Version* version = Version::Model2(versionNum); if (WillFit(numInputBits, *version, ecLevel)) { return *version; } @@ -472,7 +472,7 @@ static const Version& RecommendVersion(ErrorCorrectionLevel ecLevel, CodecMode m // Hard part: need to know version to know how many bits length takes. But need to know how many // bits it takes to know version. First we take a guess at version by assuming version will be // the minimum, 1: - int provisionalBitsNeeded = CalculateBitsNeeded(mode, headerBits, dataBits, *Version::FromNumber(1)); + int provisionalBitsNeeded = CalculateBitsNeeded(mode, headerBits, dataBits, *Version::Model2(1)); const Version& provisionalVersion = ChooseVersion(provisionalBitsNeeded, ecLevel); // Use that guess to calculate the right version. I am still not sure this works in 100% of cases. @@ -517,7 +517,7 @@ EncodeResult Encode(const std::wstring& content, ErrorCorrectionLevel ecLevel, C const Version* version; if (versionNumber > 0) { - version = Version::FromNumber(versionNumber); + version = Version::Model2(versionNumber); if (version != nullptr) { int bitsNeeded = CalculateBitsNeeded(mode, headerBits, dataBits, *version); if (!WillFit(bitsNeeded, *version, ecLevel)) { diff --git a/core/src/qrcode/QRErrorCorrectionLevel.h b/core/src/qrcode/QRErrorCorrectionLevel.h index 38bd9ac307..d87da0978a 100644 --- a/core/src/qrcode/QRErrorCorrectionLevel.h +++ b/core/src/qrcode/QRErrorCorrectionLevel.h @@ -28,4 +28,12 @@ ErrorCorrectionLevel ECLevelFromString(const char* str); ErrorCorrectionLevel ECLevelFromBits(int bits, const bool isMicro = false); int BitsFromECLevel(ErrorCorrectionLevel l); +enum class Type +{ + Model1, + Model2, + Micro, + rMQR, +}; + } // namespace ZXing::QRCode diff --git a/core/src/qrcode/QRFormatInformation.cpp b/core/src/qrcode/QRFormatInformation.cpp index 9195a7fa65..85c88d766b 100644 --- a/core/src/qrcode/QRFormatInformation.cpp +++ b/core/src/qrcode/QRFormatInformation.cpp @@ -1,6 +1,8 @@ /* * Copyright 2016 Nu-book Inc. * Copyright 2016 ZXing authors +* Copyright 2023 Axel Waggershauser +* Copyright 2023 gitlost */ // SPDX-License-Identifier: Apache-2.0 @@ -13,98 +15,31 @@ namespace ZXing::QRCode { -static const int FORMAT_INFO_MASK_QR = 0x5412; - -/** -* See ISO 18004:2006, Annex C, Table C.1 -*/ -static const std::array, 32> FORMAT_INFO_DECODE_LOOKUP = {{ - {0x5412, 0x00}, - {0x5125, 0x01}, - {0x5E7C, 0x02}, - {0x5B4B, 0x03}, - {0x45F9, 0x04}, - {0x40CE, 0x05}, - {0x4F97, 0x06}, - {0x4AA0, 0x07}, - {0x77C4, 0x08}, - {0x72F3, 0x09}, - {0x7DAA, 0x0A}, - {0x789D, 0x0B}, - {0x662F, 0x0C}, - {0x6318, 0x0D}, - {0x6C41, 0x0E}, - {0x6976, 0x0F}, - {0x1689, 0x10}, - {0x13BE, 0x11}, - {0x1CE7, 0x12}, - {0x19D0, 0x13}, - {0x0762, 0x14}, - {0x0255, 0x15}, - {0x0D0C, 0x16}, - {0x083B, 0x17}, - {0x355F, 0x18}, - {0x3068, 0x19}, - {0x3F31, 0x1A}, - {0x3A06, 0x1B}, - {0x24B4, 0x1C}, - {0x2183, 0x1D}, - {0x2EDA, 0x1E}, - {0x2BED, 0x1F}, -}}; - -static const std::array, 32> FORMAT_INFO_DECODE_LOOKUP_MICRO = {{ - {0x4445, 0x00}, - {0x4172, 0x01}, - {0x4E2B, 0x02}, - {0x4B1C, 0x03}, - {0x55AE, 0x04}, - {0x5099, 0x05}, - {0x5FC0, 0x06}, - {0x5AF7, 0x07}, - {0x6793, 0x08}, - {0x62A4, 0x09}, - {0x6DFD, 0x0A}, - {0x68CA, 0x0B}, - {0x7678, 0x0C}, - {0x734F, 0x0D}, - {0x7C16, 0x0E}, - {0x7921, 0x0F}, - {0x06DE, 0x10}, - {0x03E9, 0x11}, - {0x0CB0, 0x12}, - {0x0987, 0x13}, - {0x1735, 0x14}, - {0x1202, 0x15}, - {0x1D5B, 0x16}, - {0x186C, 0x17}, - {0x2508, 0x18}, - {0x203F, 0x19}, - {0x2F66, 0x1A}, - {0x2A51, 0x1B}, - {0x34E3, 0x1C}, - {0x31D4, 0x1D}, - {0x3E8D, 0x1E}, - {0x3BBA, 0x1F}, -}}; - static uint32_t MirrorBits(uint32_t bits) { return BitHacks::Reverse(bits) >> 17; } -static FormatInformation FindBestFormatInfo(int mask, const std::array, 32> lookup, - const std::vector& bits) +static FormatInformation FindBestFormatInfo(const std::vector& masks, const std::vector& bits) { + // See ISO 18004:2015, Annex C, Table C.1 + constexpr uint32_t MODEL2_MASKED_PATTERNS[] = { + 0x5412, 0x5125, 0x5E7C, 0x5B4B, 0x45F9, 0x40CE, 0x4F97, 0x4AA0, 0x77C4, 0x72F3, 0x7DAA, 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976, + 0x1689, 0x13BE, 0x1CE7, 0x19D0, 0x0762, 0x0255, 0x0D0C, 0x083B, 0x355F, 0x3068, 0x3F31, 0x3A06, 0x24B4, 0x2183, 0x2EDA, 0x2BED, + }; + FormatInformation fi; - // Some QR codes apparently do not apply the XOR mask. Try without and with additional masking. - for (auto mask : {0, mask}) + for (auto mask : masks) for (int bitsIndex = 0; bitsIndex < Size(bits); ++bitsIndex) - for (const auto& [pattern, index] : lookup) { - // Find the int in lookup with fewest bits differing - if (int hammingDist = BitHacks::CountBitsSet((bits[bitsIndex] ^ mask) ^ pattern); hammingDist < fi.hammingDistance) { - fi.index = index; + for (uint32_t pattern : MODEL2_MASKED_PATTERNS) { + // 'unmask' the pattern first to get the original 5-data bits + 10-ec bits back + pattern ^= FORMAT_INFO_MASK_MODEL2; + // Find the pattern with fewest bits differing + if (int hammingDist = BitHacks::CountBitsSet((bits[bitsIndex] ^ mask) ^ pattern); + hammingDist < fi.hammingDistance) { + fi.mask = mask; // store the used mask to discriminate between types/models + fi.data = pattern >> 10; // drop the 10 BCH error correction bits fi.hammingDistance = hammingDist; fi.bitsIndex = bitsIndex; } @@ -113,6 +48,56 @@ static FormatInformation FindBestFormatInfo(int mask, const std::array& bits, const std::vector& subbits) +{ + // See ISO/IEC 23941:2022, Annex C, Table C.1 - Valid format information sequences + constexpr uint32_t MASKED_PATTERNS[64] = { // Finder pattern side + 0x1FAB2, 0x1E597, 0x1DBDD, 0x1C4F8, 0x1B86C, 0x1A749, 0x19903, 0x18626, + 0x17F0E, 0x1602B, 0x15E61, 0x14144, 0x13DD0, 0x122F5, 0x11CBF, 0x1039A, + 0x0F1CA, 0x0EEEF, 0x0D0A5, 0x0CF80, 0x0B314, 0x0AC31, 0x0927B, 0x08D5E, + 0x07476, 0x06B53, 0x05519, 0x04A3C, 0x036A8, 0x0298D, 0x017C7, 0x008E2, + 0x3F367, 0x3EC42, 0x3D208, 0x3CD2D, 0x3B1B9, 0x3AE9C, 0x390D6, 0x38FF3, + 0x376DB, 0x369FE, 0x357B4, 0x34891, 0x33405, 0x32B20, 0x3156A, 0x30A4F, + 0x2F81F, 0x2E73A, 0x2D970, 0x2C655, 0x2BAC1, 0x2A5E4, 0x29BAE, 0x2848B, + 0x27DA3, 0x26286, 0x25CCC, 0x243E9, 0x23F7D, 0x22058, 0x21E12, 0x20137, + }; + constexpr uint32_t MASKED_PATTERNS_SUB[64] = { // Finder sub pattern side + 0x20A7B, 0x2155E, 0x22B14, 0x23431, 0x248A5, 0x25780, 0x269CA, 0x276EF, + 0x28FC7, 0x290E2, 0x2AEA8, 0x2B18D, 0x2CD19, 0x2D23C, 0x2EC76, 0x2F353, + 0x30103, 0x31E26, 0x3206C, 0x33F49, 0x343DD, 0x35CF8, 0x362B2, 0x37D97, + 0x384BF, 0x39B9A, 0x3A5D0, 0x3BAF5, 0x3C661, 0x3D944, 0x3E70E, 0x3F82B, + 0x003AE, 0x01C8B, 0x022C1, 0x03DE4, 0x04170, 0x05E55, 0x0601F, 0x07F3A, + 0x08612, 0x09937, 0x0A77D, 0x0B858, 0x0C4CC, 0x0DBE9, 0x0E5A3, 0x0FA86, + 0x108D6, 0x117F3, 0x129B9, 0x1369C, 0x14A08, 0x1552D, 0x16B67, 0x17442, + 0x18D6A, 0x1924F, 0x1AC05, 0x1B320, 0x1CFB4, 0x1D091, 0x1EEDB, 0x1F1FE, + }; + + FormatInformation fi; + + auto best = [&fi](const std::vector& bits, const uint32_t (&patterns)[64], uint32_t mask) + { + for (int bitsIndex = 0; bitsIndex < Size(bits); ++bitsIndex) + for (uint32_t pattern : patterns) { + // 'unmask' the pattern first to get the original 6-data bits + 12-ec bits back + pattern ^= mask; + // Find the pattern with fewest bits differing + if (int hammingDist = BitHacks::CountBitsSet((bits[bitsIndex] ^ mask) ^ pattern); + hammingDist < fi.hammingDistance) { + fi.mask = mask; // store the used mask to discriminate between types/models + fi.data = pattern >> 12; // drop the 12 BCH error correction bits + fi.hammingDistance = hammingDist; + fi.bitsIndex = bitsIndex; + } + } + }; + + best(bits, MASKED_PATTERNS, FORMAT_INFO_MASK_RMQR); + if (Size(subbits)) // TODO probably remove if `sampleRMQR()` done properly + best(subbits, MASKED_PATTERNS_SUB, FORMAT_INFO_MASK_RMQR_SUB); + + return fi; +} + /** * @param formatInfoBits1 format info indicator, with mask still applied * @param formatInfoBits2 second copy of same info; both are checked at the same time to establish best match @@ -122,12 +107,13 @@ FormatInformation FormatInformation::DecodeQR(uint32_t formatInfoBits1, uint32_t // maks out the 'Dark Module' for mirrored and non-mirrored case (see Figure 25 in ISO/IEC 18004:2015) uint32_t mirroredFormatInfoBits2 = MirrorBits(((formatInfoBits2 >> 1) & 0b111111110000000) | (formatInfoBits2 & 0b1111111)); formatInfoBits2 = ((formatInfoBits2 >> 1) & 0b111111100000000) | (formatInfoBits2 & 0b11111111); - auto fi = FindBestFormatInfo(FORMAT_INFO_MASK_QR, FORMAT_INFO_DECODE_LOOKUP, + // Some (Model2) QR codes apparently do not apply the XOR mask. Try with (standard) and without (quirk) masking. + auto fi = FindBestFormatInfo({FORMAT_INFO_MASK_MODEL2, 0, FORMAT_INFO_MASK_MODEL1}, {formatInfoBits1, formatInfoBits2, MirrorBits(formatInfoBits1), mirroredFormatInfoBits2}); // Use bits 3/4 for error correction, and 0-2 for mask. - fi.ecLevel = ECLevelFromBits((fi.index >> 3) & 0x03); - fi.dataMask = static_cast(fi.index & 0x07); + fi.ecLevel = ECLevelFromBits((fi.data >> 3) & 0x03); + fi.dataMask = static_cast(fi.data & 0x07); fi.isMirrored = fi.bitsIndex > 1; return fi; @@ -138,18 +124,38 @@ FormatInformation FormatInformation::DecodeQR(uint32_t formatInfoBits1, uint32_t */ FormatInformation FormatInformation::DecodeMQR(uint32_t formatInfoBits) { - // We don't use the additional masking (with 0x4445) to work around potentially non complying MicroQRCode encoders - auto fi = FindBestFormatInfo(0, FORMAT_INFO_DECODE_LOOKUP_MICRO, {formatInfoBits, MirrorBits(formatInfoBits)}); + auto fi = FindBestFormatInfo({FORMAT_INFO_MASK_MICRO}, {formatInfoBits, MirrorBits(formatInfoBits)}); constexpr uint8_t BITS_TO_VERSION[] = {1, 2, 2, 3, 3, 4, 4, 4}; // Bits 2/3/4 contain both error correction level and version, 0/1 contain mask. - fi.ecLevel = ECLevelFromBits((fi.index >> 2) & 0x07, true); - fi.dataMask = static_cast(fi.index & 0x03); - fi.microVersion = BITS_TO_VERSION[(fi.index >> 2) & 0x07]; + fi.ecLevel = ECLevelFromBits((fi.data >> 2) & 0x07, true); + fi.dataMask = static_cast(fi.data & 0x03); + fi.microVersion = BITS_TO_VERSION[(fi.data >> 2) & 0x07]; fi.isMirrored = fi.bitsIndex == 1; return fi; } +/** +* @param formatInfoBits1 format info indicator, with mask still applied +* @param formatInfoBits2 second copy of same info; both are checked at the same time to establish best match +*/ +FormatInformation FormatInformation::DecodeRMQR(uint32_t formatInfoBits1, uint32_t formatInfoBits2) +{ + FormatInformation fi; + if (formatInfoBits2) + fi = FindBestFormatInfoRMQR({formatInfoBits1}, {formatInfoBits2}); + else // TODO probably remove if `sampleRMQR()` done properly + fi = FindBestFormatInfoRMQR({formatInfoBits1}, {}); + + // Bit 6 is error correction (M/H), and bits 0-5 version. + fi.ecLevel = ECLevelFromBits(((fi.data >> 5) & 1) << 1); // Shift to match QRCode M/H + fi.dataMask = 4; // ((y / 2) + (x / 3)) % 2 == 0 + fi.microVersion = (fi.data & 0x1F) + 1; + fi.isMirrored = false; // TODO: implement mirrored format bit reading + + return fi; +} + } // namespace ZXing::QRCode diff --git a/core/src/qrcode/QRFormatInformation.h b/core/src/qrcode/QRFormatInformation.h index f5ebc096df..c73d75663a 100644 --- a/core/src/qrcode/QRFormatInformation.h +++ b/core/src/qrcode/QRFormatInformation.h @@ -1,6 +1,7 @@ /* * Copyright 2016 Nu-book Inc. * Copyright 2016 ZXing authors +* Copyright 2023 Axel Waggershauser */ // SPDX-License-Identifier: Apache-2.0 @@ -12,28 +13,48 @@ namespace ZXing::QRCode { +static constexpr uint32_t FORMAT_INFO_MASK_MODEL2 = 0x5412; +static constexpr uint32_t FORMAT_INFO_MASK_MODEL1 = 0x2825; +static constexpr uint32_t FORMAT_INFO_MASK_MICRO = 0x4445; +static constexpr uint32_t FORMAT_INFO_MASK_RMQR = 0x1FAB2; // Finder pattern side +static constexpr uint32_t FORMAT_INFO_MASK_RMQR_SUB = 0x20A7B; // Finder sub pattern side + class FormatInformation { public: - uint8_t index = 255; + uint32_t mask = 0; + uint8_t data = 255; uint8_t hammingDistance = 255; + uint8_t bitsIndex = 255; + bool isMirrored = false; uint8_t dataMask = 0; uint8_t microVersion = 0; - uint8_t bitsIndex = 255; ErrorCorrectionLevel ecLevel = ErrorCorrectionLevel::Invalid; FormatInformation() = default; static FormatInformation DecodeQR(uint32_t formatInfoBits1, uint32_t formatInfoBits2); static FormatInformation DecodeMQR(uint32_t formatInfoBits); + static FormatInformation DecodeRMQR(uint32_t formatInfoBits1, uint32_t formatInfoBits2); - // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match + // Hamming distance of the 32 masked codes is 7 (64 and 8 for rMQR), by construction, so <= 3 bits differing means we found a match bool isValid() const { return hammingDistance <= 3; } + Type type() const + { + switch (mask) { + case FORMAT_INFO_MASK_MODEL1: return Type::Model1; + case FORMAT_INFO_MASK_MICRO: return Type::Micro; + case FORMAT_INFO_MASK_RMQR: [[fallthrough]]; + case FORMAT_INFO_MASK_RMQR_SUB: return Type::rMQR; + default: return Type::Model2; + } + } + bool operator==(const FormatInformation& other) const { - return dataMask == other.dataMask && ecLevel == other.ecLevel; + return dataMask == other.dataMask && ecLevel == other.ecLevel && type() == other.type(); } }; diff --git a/core/src/qrcode/QRReader.cpp b/core/src/qrcode/QRReader.cpp index dd597cb2fc..e3bbda0307 100644 --- a/core/src/qrcode/QRReader.cpp +++ b/core/src/qrcode/QRReader.cpp @@ -9,7 +9,7 @@ #include "BinaryBitmap.h" #include "ConcentricFinder.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "DecoderResult.h" #include "DetectorResult.h" #include "LogMatrix.h" @@ -24,7 +24,7 @@ namespace ZXing::QRCode { Result Reader::decode(const BinaryBitmap& image) const { #if 1 - if (!_hints.isPure()) + if (!_opts.isPure()) return FirstOrDefault(decode(image, 1)); #endif @@ -33,10 +33,12 @@ Result Reader::decode(const BinaryBitmap& image) const return {}; DetectorResult detectorResult; - if (_hints.hasFormat(BarcodeFormat::QRCode)) + if (_opts.hasFormat(BarcodeFormat::QRCode)) detectorResult = DetectPureQR(*binImg); - if (_hints.hasFormat(BarcodeFormat::MicroQRCode) && !detectorResult.isValid()) + if (_opts.hasFormat(BarcodeFormat::MicroQRCode) && !detectorResult.isValid()) detectorResult = DetectPureMQR(*binImg); + if (_opts.hasFormat(BarcodeFormat::RMQRCode) && !detectorResult.isValid()) + detectorResult = DetectPureRMQR(*binImg); if (!detectorResult.isValid()) return {}; @@ -45,6 +47,7 @@ Result Reader::decode(const BinaryBitmap& image) const auto position = detectorResult.position(); return Result(std::move(decoderResult), std::move(position), + detectorResult.bits().width() != detectorResult.bits().height() ? BarcodeFormat::RMQRCode : detectorResult.bits().width() < 21 ? BarcodeFormat::MicroQRCode : BarcodeFormat::QRCode); } @@ -73,8 +76,8 @@ Results Reader::decode(const BinaryBitmap& image, int maxSymbols) const #ifdef PRINT_DEBUG LogMatrixWriter lmw(log, *binImg, 5, "qr-log.pnm"); #endif - - auto allFPs = FindFinderPatterns(*binImg, _hints.tryHarder()); + + auto allFPs = FindFinderPatterns(*binImg, _opts.tryHarder()); #ifdef PRINT_DEBUG printf("allFPs: %d\n", Size(allFPs)); @@ -82,8 +85,8 @@ Results Reader::decode(const BinaryBitmap& image, int maxSymbols) const std::vector usedFPs; Results results; - - if (_hints.hasFormat(BarcodeFormat::QRCode)) { + + if (_opts.hasFormat(BarcodeFormat::QRCode)) { auto allFPSets = GenerateFinderPatternSets(allFPs); for (const auto& fpSet : allFPSets) { if (Contains(usedFPs, fpSet.bl) || Contains(usedFPs, fpSet.tl) || Contains(usedFPs, fpSet.tr)) @@ -100,7 +103,7 @@ Results Reader::decode(const BinaryBitmap& image, int maxSymbols) const usedFPs.push_back(fpSet.tl); usedFPs.push_back(fpSet.tr); } - if (decoderResult.isValid(_hints.returnErrors())) { + if (decoderResult.isValid(_opts.returnErrors())) { results.emplace_back(std::move(decoderResult), std::move(position), BarcodeFormat::QRCode); if (maxSymbols && Size(results) == maxSymbols) break; @@ -108,8 +111,8 @@ Results Reader::decode(const BinaryBitmap& image, int maxSymbols) const } } } - - if (_hints.hasFormat(BarcodeFormat::MicroQRCode) && !(maxSymbols && Size(results) == maxSymbols)) { + + if (_opts.hasFormat(BarcodeFormat::MicroQRCode) && !(maxSymbols && Size(results) == maxSymbols)) { for (const auto& fp : allFPs) { if (Contains(usedFPs, fp)) continue; @@ -118,7 +121,7 @@ Results Reader::decode(const BinaryBitmap& image, int maxSymbols) const if (detectorResult.isValid()) { auto decoderResult = Decode(detectorResult.bits()); auto position = detectorResult.position(); - if (decoderResult.isValid(_hints.returnErrors())) { + if (decoderResult.isValid(_opts.returnErrors())) { results.emplace_back(std::move(decoderResult), std::move(position), BarcodeFormat::MicroQRCode); if (maxSymbols && Size(results) == maxSymbols) break; @@ -127,6 +130,26 @@ Results Reader::decode(const BinaryBitmap& image, int maxSymbols) const } } } + + if (_opts.hasFormat(BarcodeFormat::RMQRCode) && !(maxSymbols && Size(results) == maxSymbols)) { + // TODO proper + for (const auto& fp : allFPs) { + if (Contains(usedFPs, fp)) + continue; + + auto detectorResult = SampleRMQR(*binImg, fp); + if (detectorResult.isValid()) { + auto decoderResult = Decode(detectorResult.bits()); + auto position = detectorResult.position(); + if (decoderResult.isValid(_opts.returnErrors())) { + results.emplace_back(std::move(decoderResult), std::move(position), BarcodeFormat::RMQRCode); + if (maxSymbols && Size(results) == maxSymbols) + break; + } + + } + } + } return results; } diff --git a/core/src/qrcode/QRVersion.cpp b/core/src/qrcode/QRVersion.cpp index 2f202b654d..75d89a3bad 100644 --- a/core/src/qrcode/QRVersion.cpp +++ b/core/src/qrcode/QRVersion.cpp @@ -1,6 +1,8 @@ /* * Copyright 2016 Nu-book Inc. * Copyright 2016 ZXing authors +* Copyright 2023 Axel Waggershauser +* Copyright 2023 gitlost */ // SPDX-License-Identifier: Apache-2.0 @@ -28,8 +30,7 @@ namespace ZXing::QRCode { 0x2542E, 0x26A64, 0x27541, 0x28C69 }; -const Version * -Version::AllVersions() +const Version* Version::Model2(int number) { /** * See ISO 18004:2006 6.5.1 Table 9 @@ -276,10 +277,13 @@ Version::AllVersions() 30, 20, 15, 61, 16 }}, }; - return allVersions; + + if (number < 1 || number > 40) + return nullptr; + return allVersions + number - 1; } -const Version* Version::AllMicroVersions() +const Version* Version::Micro(int number) { /** * See ISO 18004:2006 6.5.1 Table 9 @@ -289,38 +293,343 @@ const Version* Version::AllMicroVersions() {2, {5, 1, 5, 0, 0, 6, 1, 4, 0, 0}}, {3, {6, 1, 11, 0, 0, 8, 1, 9, 0, 0}}, {4, {8, 1, 16, 0, 0, 10, 1, 14, 0, 0, 14, 1, 10, 0, 0}}}; - return allVersions; + + if (number < 1 || number > 4) + return nullptr; + return allVersions + number - 1; +} + +const Version* Version::rMQR(int number) +{ + /** + * See ISO/IEC 23941:2022 Annex D, Table D.1 - Column coordinates of centre module of alignment patterns + * See ISO/IEC 23941:2022 7.5.1, Table 8 - Error correction characteristics for rMQR + */ + static const Version allVersions[] = { + // Version number, alignment pattern centres, `ECBlocks` + { 1, {21}, { // R7x43 + // 4 `ECBlocks`, one for each `ecLevel` - rMQR only uses M & H but using 2 dummies to keep `ecLevel` index same as QR Code + // Each begins with no. of error correction codewords divided by no. of error correction blocks, followed by 2 `ECBlock`s + // Each `ECBlock` begins with no. of error correction blocks followed by no. of data codewords per block + 0, 0, 0, 0, 0, // L (dummy) - also used to differentiate rMQR from Model2 in `Version::Version()` + 7, 1, 6, 0, 0, // M + 0, 0, 0, 0, 0, // Q (dummy) + 10, 1, 3, 0, 0, // H + }}, + { 2, {19, 39}, { // R7x59 + 0, 0, 0, 0, 0, + 9, 1, 12, 0, 0, + 0, 0, 0, 0, 0, + 14, 1, 7, 0, 0, + }}, + { 3, {25, 51}, { // R7x77 + 0, 0, 0, 0, 0, + 12, 1, 20, 0, 0, + 0, 0, 0, 0, 0, + 22, 1, 10, 0, 0, + }}, + { 4, {23, 49, 75}, { // R7x99 + 0, 0, 0, 0, 0, + 16, 1, 28, 0, 0, + 0, 0, 0, 0, 0, + 30, 1, 14, 0, 0, + }}, + { 5, {27, 55, 83, 111}, { // R7x139 + 0, 0, 0, 0, 0, + 24, 1, 44, 0, 0, + 0, 0, 0, 0, 0, + 22, 2, 12, 0, 0, + }}, + { 6, {21}, { // R9x43 + 0, 0, 0, 0, 0, + 9, 1, 12, 0, 0, + 0, 0, 0, 0, 0, + 14, 1, 7, 0, 0, + }}, + { 7, {19, 39}, { // R9x59 + 0, 0, 0, 0, 0, + 12, 1, 21, 0, 0, + 0, 0, 0, 0, 0, + 22, 1, 11, 0, 0, + }}, + { 8, {25, 51}, { // R9x77 + 0, 0, 0, 0, 0, + 18, 1, 31, 0, 0, + 0, 0, 0, 0, 0, + 16, 1, 8, 1, 9, + }}, + { 9, {23, 49, 75}, { // R9x99 + 0, 0, 0, 0, 0, + 24, 1, 42, 0, 0, + 0, 0, 0, 0, 0, + 22, 2, 11, 0, 0, + }}, + {10, {27, 55, 83, 111}, { // R9x139 + 0, 0, 0, 0, 0, + 18, 1, 31, 1, 32, + 0, 0, 0, 0, 0, + 22, 3, 11, 0, 0, + }}, + {11, {}, { // R11x27 + 0, 0, 0, 0, 0, + 8, 1, 7, 0, 0, + 0, 0, 0, 0, 0, + 10, 1, 5, 0, 0, + }}, + {12, {21}, { // R11x43 + 0, 0, 0, 0, 0, + 12, 1, 19, 0, 0, + 0, 0, 0, 0, 0, + 20, 1, 11, 0, 0, + }}, + {13, {19, 39}, { // R11x59 + 0, 0, 0, 0, 0, + 16, 1, 31, 0, 0, + 0, 0, 0, 0, 0, + 16, 1, 7, 1, 8, + }}, + {14, {25, 51}, { // R11x77 + 0, 0, 0, 0, 0, + 24, 1, 43, 0, 0, + 0, 0, 0, 0, 0, + 22, 1, 11, 1, 12, + }}, + {15, {23, 49, 75}, { // R11x99 + 0, 0, 0, 0, 0, + 16, 1, 28, 1, 29, + 0, 0, 0, 0, 0, + 30, 1, 14, 1, 15, + }}, + {16, {27, 55, 83, 111}, { // R11x139 + 0, 0, 0, 0, 0, + 24, 2, 42, 0, 0, + 0, 0, 0, 0, 0, + 30, 3, 14, 0, 0, + }}, + {17, {}, { // R13x27 + 0, 0, 0, 0, 0, + 9, 1, 12, 0, 0, + 0, 0, 0, 0, 0, + 14, 1, 7, 0, 0, + }}, + {18, {21}, { // R13x43 + 0, 0, 0, 0, 0, + 14, 1, 27, 0, 0, + 0, 0, 0, 0, 0, + 28, 1, 13, 0, 0, + }}, + {19, {19, 39}, { // R13x59 + 0, 0, 0, 0, 0, + 22, 1, 38, 0, 0, + 0, 0, 0, 0, 0, + 20, 2, 10, 0, 0, + }}, + {20, {25, 51}, { // R13x77 + 0, 0, 0, 0, 0, + 16, 1, 26, 1, 27, + 0, 0, 0, 0, 0, + 28, 1, 14, 1, 15, + }}, + {21, {23, 49, 75}, { // R13x99 + 0, 0, 0, 0, 0, + 20, 1, 36, 1, 37, + 0, 0, 0, 0, 0, + 26, 1, 11, 2, 12, + }}, + {22, {27, 55, 83, 111}, { // R13x139 + 0, 0, 0, 0, 0, + 20, 2, 35, 1, 36, + 0, 0, 0, 0, 0, + 28, 2, 13, 2, 14, + }}, + {23, {21}, { // R15x43 + 0, 0, 0, 0, 0, + 18, 1, 33, 0, 0, + 0, 0, 0, 0, 0, + 18, 1, 7, 1, 8, + }}, + {24, {19, 39}, { // R15x59 + 0, 0, 0, 0, 0, + 26, 1, 48, 0, 0, + 0, 0, 0, 0, 0, + 24, 2, 13, 0, 0, + }}, + {25, {25, 51}, { // R15x77 + 0, 0, 0, 0, 0, + 18, 1, 33, 1, 34, + 0, 0, 0, 0, 0, + 24, 2, 10, 1, 11, + }}, + {26, {23, 49, 75}, { // R15x99 + 0, 0, 0, 0, 0, + 24, 2, 44, 0, 0, + 0, 0, 0, 0, 0, + 22, 4, 12, 0, 0, + }}, + {27, {27, 55, 83, 111}, { // R15x139 + 0, 0, 0, 0, 0, + 24, 2, 42, 1, 43, + 0, 0, 0, 0, 0, + 26, 1, 13, 4, 14, + }}, + {28, {21}, { // R17x43 + 0, 0, 0, 0, 0, + 22, 1, 39, 0, 0, + 0, 0, 0, 0, 0, + 20, 1, 10, 1, 11, + }}, + {29, {19, 39}, { // R17x59 + 0, 0, 0, 0, 0, + 16, 2, 28, 0, 0, + 0, 0, 0, 0, 0, + 30, 2, 14, 0, 0, + }}, + {30, {25, 51}, { // R17x77 + 0, 0, 0, 0, 0, + 22, 2, 39, 0, 0, + 0, 0, 0, 0, 0, + 28, 1, 12, 2, 13, + }}, + {31, {23, 49, 75}, { // R17x99 + 0, 0, 0, 0, 0, + 20, 2, 33, 1, 34, + 0, 0, 0, 0, 0, + 26, 4, 14, 0, 0, + }}, + {32, {27, 55, 83, 111}, { // R17x139 + 0, 0, 0, 0, 0, + 20, 4, 38, 0, 0, + 0, 0, 0, 0, 0, + 26, 2, 12, 4, 13, + }}, + }; + + if (number < 1 || number > Size(allVersions)) + return nullptr; + return allVersions + number - 1; +} + +const Version* Version::Model1(int number) +{ + /** + * See ISO 18004:2000 M.4.2 Table M.2 + * See ISO 18004:2000 M.5 Table M.4 + */ + static const Version allVersions[] = { + {1, { + 7 , 1, 19, 0, 0, + 10, 1, 16, 0, 0, + 13, 1, 13, 0, 0, + 17, 1, 9 , 0, 0 + }}, + {2, { + 10, 1, 36, 0, 0, + 16, 1, 30, 0, 0, + 22, 1, 24, 0, 0, + 30, 1, 16, 0, 0, + }}, + {3, { + 15, 1, 57, 0, 0, + 28, 1, 44, 0, 0, + 36, 1, 36, 0, 0, + 48, 1, 24, 0, 0, + }}, + {4, { + 20, 1, 80, 0, 0, + 40, 1, 60, 0, 0, + 50, 1, 50, 0, 0, + 66, 1, 34, 0, 0, + }}, + {5, { + 26, 1, 108, 0, 0, + 52, 1, 82 , 0, 0, + 66, 1, 68 , 0, 0, + 44, 2, 23 , 0, 0, + }}, + {6, { + 34, 1, 136, 0, 0, + 32, 2, 53 , 0, 0, + 42, 2, 43 , 0, 0, + 56, 2, 29 , 0, 0, + }}, + {7, { + 42, 1, 170, 0, 0, + 40, 2, 66 , 0, 0, + 52, 2, 54 , 0, 0, + 46, 3, 24 , 0, 0, + }}, + {8, { + 24, 2, 104, 0, 0, + 48, 2, 80 , 0, 0, + 64, 2, 64 , 0, 0, + 56, 3, 29 , 0, 0, + }}, + {9, { + 30, 2, 123, 0, 0, + 60, 2, 93 , 0, 0, + 50, 3, 52 , 0, 0, + 68, 3, 34 , 0, 0, + }}, + {10, { + 34, 2, 145, 0, 0, + 68, 2, 111, 0, 0, + 58, 3, 61 , 0, 0, + 58, 4, 31 , 0, 0, + }}, + {11, { + 40, 2, 168, 0, 0, + 40, 4, 64 , 0, 0, + 52, 4, 52 , 0, 0, + 54, 5, 29 , 0, 0, + }}, + {12, { + 46, 2, 192, 0, 0, + 46, 4, 73 , 0, 0, + 58, 4, 61 , 0, 0, + 62, 5, 33 , 0, 0, + }}, + {13, { + 36, 3, 144, 0, 0, + 52, 4, 83 , 0, 0, + 66, 4, 69 , 0, 0, + 58, 6, 32 , 0, 0, + }}, + {14, { + 40, 3, 163, 0, 0, + 60, 4, 92 , 0, 0, + 60, 5, 62 , 0, 0, + 66, 6, 35 , 0, 0, + }}, + }; + + if (number < 1 || number > 14) + return nullptr; + return allVersions + number - 1; } Version::Version(int versionNumber, std::initializer_list alignmentPatternCenters, const std::array& ecBlocks) - : _versionNumber(versionNumber), _alignmentPatternCenters(alignmentPatternCenters), _ecBlocks(ecBlocks), _isMicro(false) + : _versionNumber(versionNumber), _alignmentPatternCenters(alignmentPatternCenters), _ecBlocks(ecBlocks), + _type(ecBlocks[0].codewordsPerBlock == 0 ? Type::rMQR : Type::Model2) { - _totalCodewords = ecBlocks[0].totalDataCodewords(); + _totalCodewords = ecBlocks[1].totalDataCodewords(); // Use 1 (M) as 0 dummy for rMQR } Version::Version(int versionNumber, const std::array& ecBlocks) - : _versionNumber(versionNumber), _ecBlocks(ecBlocks), _isMicro(true) + : _versionNumber(versionNumber), + _ecBlocks(ecBlocks), + _type(ecBlocks[0].codewordsPerBlock < 7 || ecBlocks[0].codewordsPerBlock == 8 ? Type::Micro : Type::Model1) { _totalCodewords = ecBlocks[0].totalDataCodewords(); } -const Version* Version::FromNumber(int versionNumber, bool isMicro) +bool Version::HasValidSize(const BitMatrix& bitMatrix, Type type) { - if (versionNumber < 1 || versionNumber > (isMicro ? 4 : 40)) { - //throw std::invalid_argument("Version should be in range [1-40]."); - return nullptr; - } - return &(isMicro ? AllMicroVersions() : AllVersions())[versionNumber - 1]; + return IsValidSize(PointI{bitMatrix.width(), bitMatrix.height()}, type); } -const Version* Version::FromDimension(int dimension) +int Version::Number(const BitMatrix& bitMatrix) { - bool isMicro = dimension < 21; - if (dimension % DimensionStep(isMicro) != 1) { - //throw std::invalid_argument("Unexpected dimension"); - return nullptr; - } - return FromNumber((dimension - DimensionOffset(isMicro)) / DimensionStep(isMicro), isMicro); + return Number(PointI{bitMatrix.width(), bitMatrix.height()}); } const Version* Version::DecodeVersionInformation(int versionBitsA, int versionBitsB) @@ -329,12 +638,6 @@ const Version* Version::DecodeVersionInformation(int versionBitsA, int versionBi int bestVersion = 0; int i = 0; for (int targetVersion : VERSION_DECODE_INFO) { - // Do the version info bits match exactly? done. - if (targetVersion == versionBitsA || targetVersion == versionBitsB) { - return FromNumber(i + 7); - } - // Otherwise see if this is the closest to a real version info bit string - // we have seen so far for (int bits : {versionBitsA, versionBitsB}) { int bitsDifference = BitHacks::CountBitsSet(bits ^ targetVersion); if (bitsDifference < bestDifference) { @@ -342,13 +645,15 @@ const Version* Version::DecodeVersionInformation(int versionBitsA, int versionBi bestDifference = bitsDifference; } } + if (bestDifference == 0) + break; ++i; } // We can tolerate up to 3 bits of error since no two version info codewords will // differ in less than 8 bits. - if (bestDifference <= 3) { - return FromNumber(bestVersion); - } + if (bestDifference <= 3) + return Model2(bestVersion); + // If we didn't find a close enough match, fail return nullptr; } @@ -358,13 +663,54 @@ const Version* Version::DecodeVersionInformation(int versionBitsA, int versionBi */ BitMatrix Version::buildFunctionPattern() const { + if (isRMQR()) { + PointI size = Version::SymbolSize(versionNumber(), Type::rMQR); + BitMatrix bitMatrix(size.x, size.y); + + // Set edge timing patterns + bitMatrix.setRegion(0, 0, size.x, 1); // Top + bitMatrix.setRegion(0, size.y - 1, size.x, 1); // Bottom + bitMatrix.setRegion(0, 1, 1, size.y - 2); // Left + bitMatrix.setRegion(size.x - 1, 1, 1, size.y - 2); // Right + + // Set vertical timing and alignment patterns + size_t max = _alignmentPatternCenters.size(); // Same as vertical timing column + for (size_t x = 0; x < max; ++x) { + int cx = _alignmentPatternCenters[x]; + bitMatrix.setRegion(cx - 1, 1, 3, 2); // Top alignment pattern + bitMatrix.setRegion(cx - 1, size.y - 3, 3, 2); // Bottom alignment pattern + bitMatrix.setRegion(cx, 3, 1, size.y - 6); // Vertical timing pattern + } + + // Top left finder pattern + separator + bitMatrix.setRegion(1, 1, 8 - 1, 8 - 1 - (size.y == 7)); // R7 finder bottom flush with edge + // Top left format + bitMatrix.setRegion(8, 1, 3, 5); + bitMatrix.setRegion(11, 1, 1, 3); + + // Bottom right finder subpattern + bitMatrix.setRegion(size.x - 5, size.y - 5, 5 - 1, 5 - 1); + // Bottom right format + bitMatrix.setRegion(size.x - 8, size.y - 6, 3, 5); + bitMatrix.setRegion(size.x - 5, size.y - 6, 3, 1); + + // Top right corner finder + bitMatrix.set(size.x - 2, 1); + if (size.y > 9) { + // Bottom left corner finder + bitMatrix.set(1, size.y - 2); + } + + return bitMatrix; + } + int dimension = this->dimension(); BitMatrix bitMatrix(dimension, dimension); // Top left finder pattern + separator + format bitMatrix.setRegion(0, 0, 9, 9); - if (!_isMicro) { + if (!isMicro()) { // Top right finder pattern + separator + format bitMatrix.setRegion(dimension - 8, 0, 8, 9); // Bottom left finder pattern + separator + format diff --git a/core/src/qrcode/QRVersion.h b/core/src/qrcode/QRVersion.h index 0b03270011..f2229465c8 100644 --- a/core/src/qrcode/QRVersion.h +++ b/core/src/qrcode/QRVersion.h @@ -1,13 +1,16 @@ /* * Copyright 2016 Nu-book Inc. * Copyright 2016 ZXing authors +* Copyright 2023 Axel Waggershauser */ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include "Point.h" #include "QRECB.h" #include "QRErrorCorrectionLevel.h" +#include "ZXAlgorithms.h" #include #include @@ -19,58 +22,105 @@ class BitMatrix; namespace QRCode { +// clang-format off +constexpr std::array RMQR_SIZES { + PointI{43, 7}, {59, 7}, {77, 7}, {99, 7}, {139, 7}, + {43, 9}, {59, 9}, {77, 9}, {99, 9}, {139, 9}, + {27, 11}, {43, 11}, {59, 11}, {77, 11}, {99, 11}, {139, 11}, + {27, 13}, {43, 13}, {59, 13}, {77, 13}, {99, 13}, {139, 13}, + {43, 15}, {59, 15}, {77, 15}, {99, 15}, {139, 15}, + {43, 17}, {59, 17}, {77, 17}, {99, 17}, {139, 17}, +}; +// clang-format on + /** * See ISO 18004:2006 Annex D -* -* @author Sean Owen */ class Version { public: + Type type() const { return _type; } + bool isMicro() const { return type() == Type::Micro; } + bool isRMQR() const { return type() == Type::rMQR; } + bool isModel1() const { return type() == Type::Model1; } + bool isModel2() const { return type() == Type::Model2; } + int versionNumber() const { return _versionNumber; } const std::vector& alignmentPatternCenters() const { return _alignmentPatternCenters; } int totalCodewords() const { return _totalCodewords; } - int dimension() const { return DimensionOfVersion(_versionNumber, _isMicro); } + int dimension() const { return SymbolSize(versionNumber(), isMicro() ? Type::Micro : Type::Model2).x; } const ECBlocks& ecBlocksForLevel(ErrorCorrectionLevel ecLevel) const { return _ecBlocks[(int)ecLevel]; } BitMatrix buildFunctionPattern() const; - bool isMicroQRCode() const { return _isMicro; } + static constexpr PointI SymbolSize(int version, Type type) + { + auto square = [](int s) { return PointI(s, s); }; + auto valid = [](int v, int max) { return v >= 1 && v <= max; }; + + switch (type) { + case Type::Model1: return valid(version, 32) ? square(17 + 4 * version) : PointI{}; + case Type::Model2: return valid(version, 40) ? square(17 + 4 * version) : PointI{}; + case Type::Micro: return valid(version, 4) ? square(9 + 2 * version) : PointI{}; + case Type::rMQR: return valid(version, 32) ? RMQR_SIZES[version - 1] : PointI{}; + } + + return {}; // silence warning + } + + static constexpr bool IsValidSize(PointI size, Type type) + { + switch (type) { + case Type::Model1: return size.x == size.y && size.x >= 21 && size.x <= 145 && (size.x % 4 == 1); + case Type::Model2: return size.x == size.y && size.x >= 21 && size.x <= 177 && (size.x % 4 == 1); + case Type::Micro: return size.x == size.y && size.x >= 11 && size.x <= 17 && (size.x % 2 == 1); + case Type::rMQR: + return size.x != size.y && size.x & 1 && size.y & 1 && size.x >= 27 && size.x <= 139 && size.y >= 7 && size.y <= 17 + && IndexOf(RMQR_SIZES, size) != -1; + } + return {}; // silence warning + } + static bool HasValidSize(const BitMatrix& bitMatrix, Type type); - static constexpr int DimensionStep(bool isMicro) { return std::array{4, 2}[isMicro]; } - static constexpr int DimensionOffset(bool isMicro) { return std::array{17, 9}[isMicro]; } - static constexpr int DimensionOfVersion(int version, bool isMicro) + static bool HasValidSize(const BitMatrix& matrix) { - return DimensionOffset(isMicro) + DimensionStep(isMicro) * version; + return HasValidSize(matrix, Type::Model1) || HasValidSize(matrix, Type::Model2) || HasValidSize(matrix, Type::Micro) + || HasValidSize(matrix, Type::rMQR); } - /** - *

Deduces version information purely from micro QR or QR Code dimensions.

- * - * @param dimension dimension in modules - * @return Version for a QR Code of that dimension - */ - static const Version* FromDimension(int dimension); - - static const Version* FromNumber(int versionNumber, bool isMicro = false); + static constexpr int Number(PointI size) + { + if (size.x != size.y) + return IndexOf(RMQR_SIZES, size) + 1; + if (IsValidSize(size, Type::Model2)) + return (size.x - 17) / 4; + if (IsValidSize(size, Type::Micro)) + return (size.x - 9) / 2; + return 0; + } + + static int Number(const BitMatrix& bitMatrix); static const Version* DecodeVersionInformation(int versionBitsA, int versionBitsB = 0); + static const Version* Model1(int number); + static const Version* Model2(int number); + static const Version* Micro(int number); + static const Version* rMQR(int number); + private: int _versionNumber; std::vector _alignmentPatternCenters; std::array _ecBlocks; int _totalCodewords; - bool _isMicro; + Type _type; Version(int versionNumber, std::initializer_list alignmentPatternCenters, const std::array &ecBlocks); Version(int versionNumber, const std::array& ecBlocks); - static const Version* AllVersions(); - static const Version* AllMicroVersions(); }; } // QRCode diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index fbceb837da..9a492ca5eb 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -22,15 +22,15 @@ if (BUILD_READERS) install(TARGETS ZXingReader DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() -if (BUILD_READERS) - find_package(Qt5 COMPONENTS Gui Multimedia Quick QUIET) - if (NOT (Qt5_FOUND OR Qt6_FOUND)) - message("INFO: Qt (Gui/Multimedia/Quick) not found, skipping Qt examples") - endif() +find_package(Qt6 COMPONENTS Gui Multimedia Quick QUIET) +if (NOT (Qt5_FOUND OR Qt6_FOUND)) + message("INFO: Qt (Gui/Multimedia/Quick) not found, skipping Qt examples") +endif() - set(CMAKE_AUTOMOC ON) - set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +if (BUILD_READERS) if (TARGET Qt::Gui) add_executable (ZXingQtReader ZXingQtReader.cpp ZXingQtReader.h) target_link_libraries(ZXingQtReader ZXing::ZXing Qt::Gui) @@ -50,3 +50,10 @@ if (BUILD_READERS) message("INFO: OpenCV not found, skipping ZXingOpenCV example") endif() endif() + +if (BUILD_WRITERS) + if (TARGET Qt::Gui) + add_executable (ZXingQtWriter ZXingQtWriter.cpp) + target_link_libraries(ZXingQtWriter ZXing::ZXing Qt::Gui) + endif() +endif() diff --git a/example/ZXingOpenCV.h b/example/ZXingOpenCV.h index c81d0e598c..bd125c2aa2 100644 --- a/example/ZXingOpenCV.h +++ b/example/ZXingOpenCV.h @@ -26,9 +26,9 @@ inline ZXing::ImageView ImageViewFromMat(const cv::Mat& image) return {image.data, image.cols, image.rows, fmt}; } -inline ZXing::Results ReadBarcodes(const cv::Mat& image, const ZXing::DecodeHints& hints = {}) +inline ZXing::Results ReadBarcodes(const cv::Mat& image, const ZXing::ReaderOptions& options = {}) { - return ZXing::ReadBarcodes(ImageViewFromMat(image), hints); + return ZXing::ReadBarcodes(ImageViewFromMat(image), options); } inline void DrawResult(cv::Mat& img, ZXing::Result res) diff --git a/example/ZXingQt6CamReader.qml b/example/ZXingQt6CamReader.qml index ceaf431617..ecb348e410 100644 --- a/example/ZXingQt6CamReader.qml +++ b/example/ZXingQt6CamReader.qml @@ -86,7 +86,7 @@ Window { id: camerasComboBox Layout.fillWidth: true model: devices.videoInputs - textRole: "displayName" + textRole: "description" currentIndex: 0 } } @@ -96,36 +96,71 @@ Window { Layout.fillHeight: true Layout.fillWidth: true -// Shape { -// id: polygon -// anchors.fill: parent -// visible: points.length == 4 -// ShapePath { -// strokeWidth: 3 -// strokeColor: "red" -// strokeStyle: ShapePath.SolidLine -// fillColor: "transparent" -// //TODO: really? I don't know qml... -// startX: videoOutput.mapPointToItem(points[3]).x -// startY: videoOutput.mapPointToItem(points[3]).y -// PathLine { -// x: videoOutput.mapPointToItem(points[0]).x -// y: videoOutput.mapPointToItem(points[0]).y -// } -// PathLine { -// x: videoOutput.mapPointToItem(points[1]).x -// y: videoOutput.mapPointToItem(points[1]).y -// } -// PathLine { -// x: videoOutput.mapPointToItem(points[2]).x -// y: videoOutput.mapPointToItem(points[2]).y -// } -// PathLine { -// x: videoOutput.mapPointToItem(points[3]).x -// y: videoOutput.mapPointToItem(points[3]).y -// } -// } -// } + function mapPointToItem(point) + { + if (videoOutput.sourceRect.width === 0 || videoOutput.sourceRect.height === 0) + return Qt.point(0, 0); + + let dx = point.x; + let dy = point.y; + + if ((videoOutput.orientation % 180) == 0) + { + dx = dx * videoOutput.contentRect.width / videoOutput.sourceRect.width; + dy = dy * videoOutput.contentRect.height / videoOutput.sourceRect.height; + } + else + { + dx = dx * videoOutput.contentRect.height / videoOutput.sourceRect.height; + dy = dx * videoOutput.contentRect.width / videoOutput.sourceRect.width; + } + + switch ((videoOutput.orientation + 360) % 360) + { + case 0: + default: + return Qt.point(videoOutput.contentRect.x + dx, videoOutput.contentRect.y + dy); + case 90: + return Qt.point(videoOutput.contentRect.x + dy, videoOutput.contentRect.y + videoOutput.contentRect.height - dx); + case 180: + return Qt.point(videoOutput.contentRect.x + videoOutput.contentRect.width - dx, videoOutput.contentRect.y + videoOutput.contentRect.height -dy); + case 270: + return Qt.point(videoOutput.contentRect.x + videoOutput.contentRect.width - dy, videoOutput.contentRect.y + dx); + } + } + + Shape { + id: polygon + anchors.fill: parent + visible: control.points.length === 4 + + ShapePath { + strokeWidth: 3 + strokeColor: "red" + strokeStyle: ShapePath.SolidLine + fillColor: "transparent" + //TODO: really? I don't know qml... + startX: videoOutput.mapPointToItem(points[3]).x + startY: videoOutput.mapPointToItem(points[3]).y + + PathLine { + x: videoOutput.mapPointToItem(points[0]).x + y: videoOutput.mapPointToItem(points[0]).y + } + PathLine { + x: videoOutput.mapPointToItem(points[1]).x + y: videoOutput.mapPointToItem(points[1]).y + } + PathLine { + x: videoOutput.mapPointToItem(points[2]).x + y: videoOutput.mapPointToItem(points[2]).y + } + PathLine { + x: videoOutput.mapPointToItem(points[3]).x + y: videoOutput.mapPointToItem(points[3]).y + } + } + } Label { id: info diff --git a/example/ZXingQtReader.cpp b/example/ZXingQtReader.cpp index 757cabd87c..fe04192160 100644 --- a/example/ZXingQtReader.cpp +++ b/example/ZXingQtReader.cpp @@ -25,12 +25,12 @@ int main(int argc, char* argv[]) return 1; } - auto hints = DecodeHints() + auto options = ReaderOptions() .setFormats(BarcodeFormat::Any) .setTryRotate(false) .setMaxNumberOfSymbols(10); - auto results = ReadBarcodes(fileImage, hints); + auto results = ReadBarcodes(fileImage, options); for (auto& result : results) { qDebug() << "Text: " << result.text(); diff --git a/example/ZXingQtReader.h b/example/ZXingQtReader.h index 9fec57c900..1c11c0f50c 100644 --- a/example/ZXingQtReader.h +++ b/example/ZXingQtReader.h @@ -51,9 +51,10 @@ enum class BarcodeFormat UPCA = (1 << 14), ///< UPC-A UPCE = (1 << 15), ///< UPC-E MicroQRCode = (1 << 16), ///< Micro QR Code + RMQRCode = (1 << 17), ///< Rectangular Micro QR Code LinearCodes = Codabar | Code39 | Code93 | Code128 | EAN8 | EAN13 | ITF | DataBar | DataBarExpanded | UPCA | UPCE, - MatrixCodes = Aztec | DataMatrix | MaxiCode | PDF417 | QRCode | MicroQRCode, + MatrixCodes = Aztec | DataMatrix | MaxiCode | PDF417 | QRCode | MicroQRCode | RMQRCode, }; enum class ContentType { Text, Binary, Mixed, GS1, ISO15434, UnknownECI }; @@ -63,7 +64,7 @@ using ZXing::BarcodeFormat; using ZXing::ContentType; #endif -using ZXing::DecodeHints; +using ZXing::ReaderOptions; using ZXing::Binarizer; using ZXing::BarcodeFormats; @@ -140,7 +141,7 @@ inline QList QListResults(ZXing::Results&& zxres) return res; } -inline QList ReadBarcodes(const QImage& img, const DecodeHints& hints = {}) +inline QList ReadBarcodes(const QImage& img, const ReaderOptions& opts = {}) { using namespace ZXing; @@ -163,20 +164,20 @@ inline QList ReadBarcodes(const QImage& img, const DecodeHints& hints = auto exec = [&](const QImage& img) { return QListResults(ZXing::ReadBarcodes( - {img.bits(), img.width(), img.height(), ImgFmtFromQImg(img), static_cast(img.bytesPerLine())}, hints)); + {img.bits(), img.width(), img.height(), ImgFmtFromQImg(img), static_cast(img.bytesPerLine())}, opts)); }; return ImgFmtFromQImg(img) == ImageFormat::None ? exec(img.convertToFormat(QImage::Format_Grayscale8)) : exec(img); } -inline Result ReadBarcode(const QImage& img, const DecodeHints& hints = {}) +inline Result ReadBarcode(const QImage& img, const ReaderOptions& opts = {}) { - auto res = ReadBarcodes(img, DecodeHints(hints).setMaxNumberOfSymbols(1)); + auto res = ReadBarcodes(img, ReaderOptions(opts).setMaxNumberOfSymbols(1)); return !res.isEmpty() ? res.takeFirst() : Result(); } #ifdef QT_MULTIMEDIA_LIB -inline QList ReadBarcodes(const QVideoFrame& frame, const DecodeHints& hints = {}) +inline QList ReadBarcodes(const QVideoFrame& frame, const ReaderOptions& opts = {}) { using namespace ZXing; @@ -273,7 +274,7 @@ inline QList ReadBarcodes(const QVideoFrame& frame, const DecodeHints& h QScopeGuard unmap([&] { img.unmap(); }); return QListResults(ZXing::ReadBarcodes( - {img.bits(FIRST_PLANE) + pixOffset, img.width(), img.height(), fmt, img.bytesPerLine(FIRST_PLANE), pixStride}, hints)); + {img.bits(FIRST_PLANE) + pixOffset, img.width(), img.height(), fmt, img.bytesPerLine(FIRST_PLANE), pixStride}, opts)); } else { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) @@ -286,26 +287,26 @@ inline QList ReadBarcodes(const QVideoFrame& frame, const DecodeHints& h auto qimg = frame.toImage(); #endif if (qimg.format() != QImage::Format_Invalid) - return ReadBarcodes(qimg, hints); + return ReadBarcodes(qimg, opts); qWarning() << "failed to convert QVideoFrame to QImage"; return {}; } } -inline Result ReadBarcode(const QVideoFrame& frame, const DecodeHints& hints = {}) +inline Result ReadBarcode(const QVideoFrame& frame, const ReaderOptions& opts = {}) { - auto res = ReadBarcodes(frame, DecodeHints(hints).setMaxNumberOfSymbols(1)); + auto res = ReadBarcodes(frame, ReaderOptions(opts).setMaxNumberOfSymbols(1)); return !res.isEmpty() ? res.takeFirst() : Result(); } #define ZQ_PROPERTY(Type, name, setter) \ public: \ Q_PROPERTY(Type name READ name WRITE setter NOTIFY name##Changed) \ - Type name() const noexcept { return DecodeHints::name(); } \ + Type name() const noexcept { return ReaderOptions::name(); } \ Q_SLOT void setter(const Type& newVal) \ { \ if (name() != newVal) { \ - DecodeHints::setter(newVal); \ + ReaderOptions::setter(newVal); \ emit name##Changed(); \ } \ } \ @@ -313,9 +314,9 @@ public: \ #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -class BarcodeReader : public QAbstractVideoFilter, private DecodeHints +class BarcodeReader : public QAbstractVideoFilter, private ReaderOptions #else -class BarcodeReader : public QObject, private DecodeHints +class BarcodeReader : public QObject, private ReaderOptions #endif { Q_OBJECT @@ -333,15 +334,15 @@ class BarcodeReader : public QObject, private DecodeHints Q_PROPERTY(int formats READ formats WRITE setFormats NOTIFY formatsChanged) int formats() const noexcept { - auto fmts = DecodeHints::formats(); + auto fmts = ReaderOptions::formats(); return *reinterpret_cast(&fmts); } Q_SLOT void setFormats(int newVal) { if (formats() != newVal) { - DecodeHints::setFormats(static_cast(newVal)); + ReaderOptions::setFormats(static_cast(newVal)); emit formatsChanged(); - qDebug() << DecodeHints::formats(); + qDebug() << ReaderOptions::formats(); } } Q_SIGNAL void formatsChanged(); diff --git a/example/ZXingQtWriter.cpp b/example/ZXingQtWriter.cpp new file mode 100644 index 0000000000..047f182a7d --- /dev/null +++ b/example/ZXingQtWriter.cpp @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Axel Waggershauser + */ +// SPDX-License-Identifier: Apache-2.0 + +#include "BarcodeFormat.h" +#include "BitMatrix.h" +#include "MultiFormatWriter.h" + +#include +#include + +namespace ZXingQt { + +QImage WriteBarcode(QStringView text, ZXing::BarcodeFormat format) +{ + using namespace ZXing; + + auto writer = MultiFormatWriter(format); + auto matrix = writer.encode(text.toString().toStdString(), 0, 0); + auto bitmap = ToMatrix(matrix); + + return QImage(bitmap.data(), bitmap.width(), bitmap.height(), bitmap.width(), QImage::Format::Format_Grayscale8).copy(); +} + +} // namespace ZXingQt + +int main(int argc, char* argv[]) +{ + if (argc != 4) { + qDebug() << "usage: ZXingQtWriter "; + return 1; + } + + auto format = ZXing::BarcodeFormatFromString(argv[1]); + auto text = QString(argv[2]); + auto filename = QString(argv[3]); + + auto result = ZXingQt::WriteBarcode(text, format); + + result.save(filename); + + return 0; +} diff --git a/example/ZXingReader.cpp b/example/ZXingReader.cpp index b857b2505d..74682a9d2a 100644 --- a/example/ZXingReader.cpp +++ b/example/ZXingReader.cpp @@ -51,36 +51,36 @@ static void PrintUsage(const char* exePath) std::cout << "Formats can be lowercase, with or without '-', separated by ',' and/or '|'\n"; } -static bool ParseOptions(int argc, char* argv[], DecodeHints& hints, bool& oneLine, bool& bytesOnly, +static bool ParseOptions(int argc, char* argv[], ReaderOptions& options, bool& oneLine, bool& bytesOnly, std::vector& filePaths, std::string& outPath) { -#ifdef BUILD_EXPERIMENTAL_API - hints.setTryDenoise(true); +#ifdef ZXING_BUILD_EXPERIMENTAL_API + options.setTryDenoise(true); #endif for (int i = 1; i < argc; ++i) { auto is = [&](const char* str) { return strncmp(argv[i], str, strlen(argv[i])) == 0; }; if (is("-fast")) { - hints.setTryHarder(false); -#ifdef BUILD_EXPERIMENTAL_API - hints.setTryDenoise(false); + options.setTryHarder(false); +#ifdef ZXING_BUILD_EXPERIMENTAL_API + options.setTryDenoise(false); #endif } else if (is("-norotate")) { - hints.setTryRotate(false); + options.setTryRotate(false); } else if (is("-noinvert")) { - hints.setTryInvert(false); + options.setTryInvert(false); } else if (is("-noscale")) { - hints.setTryDownscale(false); + options.setTryDownscale(false); } else if (is("-ispure")) { - hints.setIsPure(true); - hints.setBinarizer(Binarizer::FixedThreshold); + options.setIsPure(true); + options.setBinarizer(Binarizer::FixedThreshold); } else if (is("-errors")) { - hints.setReturnErrors(true); + options.setReturnErrors(true); } else if (is("-format")) { if (++i == argc) return false; try { - hints.setFormats(BarcodeFormatsFromString(argv[i])); + options.setFormats(BarcodeFormatsFromString(argv[i])); } catch (const std::exception& e) { std::cerr << e.what() << "\n"; return false; @@ -89,13 +89,13 @@ static bool ParseOptions(int argc, char* argv[], DecodeHints& hints, bool& oneLi if (++i == argc) return false; else if (is("plain")) - hints.setTextMode(TextMode::Plain); + options.setTextMode(TextMode::Plain); else if (is("eci")) - hints.setTextMode(TextMode::ECI); + options.setTextMode(TextMode::ECI); else if (is("hri")) - hints.setTextMode(TextMode::HRI); + options.setTextMode(TextMode::HRI); else if (is("escaped")) - hints.setTextMode(TextMode::Escaped); + options.setTextMode(TextMode::Escaped); else return false; } else if (is("-1")) { @@ -151,7 +151,7 @@ void drawRect(const ImageView& image, const Position& pos, bool error) int main(int argc, char* argv[]) { - DecodeHints hints; + ReaderOptions options; std::vector filePaths; Results allResults; std::string outPath; @@ -159,10 +159,10 @@ int main(int argc, char* argv[]) bool bytesOnly = false; int ret = 0; - hints.setTextMode(TextMode::HRI); - hints.setEanAddOnSymbol(EanAddOnSymbol::Read); + options.setTextMode(TextMode::HRI); + options.setEanAddOnSymbol(EanAddOnSymbol::Read); - if (!ParseOptions(argc, argv, hints, oneLine, bytesOnly, filePaths, outPath)) { + if (!ParseOptions(argc, argv, options, oneLine, bytesOnly, filePaths, outPath)) { PrintUsage(argv[0]); return -1; } @@ -174,12 +174,12 @@ int main(int argc, char* argv[]) int width, height, channels; std::unique_ptr buffer(stbi_load(filePath.c_str(), &width, &height, &channels, 3), stbi_image_free); if (buffer == nullptr) { - std::cerr << "Failed to read image: " << filePath << "\n"; + std::cerr << "Failed to read image: " << filePath << " (" << stbi_failure_reason() << ")" << "\n"; return -1; } ImageView image{buffer.get(), width, height, ImageFormat::RGB}; - auto results = ReadBarcodes(image, hints); + auto results = ReadBarcodes(image, options); // if we did not find anything, insert a dummy to produce some output for each file if (results.empty()) @@ -229,7 +229,7 @@ int main(int argc, char* argv[]) } std::cout << "Text: \"" << result.text() << "\"\n" - << "Bytes: " << ToHex(hints.textMode() == TextMode::ECI ? result.bytesECI() : result.bytes()) << "\n" + << "Bytes: " << ToHex(options.textMode() == TextMode::ECI ? result.bytesECI() : result.bytes()) << "\n" << "Format: " << ToString(result.format()) << "\n" << "Identifier: " << result.symbologyIdentifier() << "\n" << "Content: " << ToString(result.contentType()) << "\n" @@ -283,7 +283,7 @@ int main(int argc, char* argv[]) int blockSize = 1; do { for (int i = 0; i < blockSize; ++i) - ReadBarcodes(image, hints); + ReadBarcodes(image, options); N += blockSize; duration = std::chrono::high_resolution_clock::now() - startTime; if (blockSize < 1000 && duration < std::chrono::milliseconds(100)) diff --git a/test/blackbox/BlackboxTestRunner.cpp b/test/blackbox/BlackboxTestRunner.cpp index af2e247d80..7dd5234333 100644 --- a/test/blackbox/BlackboxTestRunner.cpp +++ b/test/blackbox/BlackboxTestRunner.cpp @@ -212,7 +212,7 @@ static std::vector getImagesInDirectory(const fs::path& directory) } static void doRunTests(const fs::path& directory, std::string_view format, int totalTests, const std::vector& tests, - DecodeHints hints) + ReaderOptions opts) { auto imgPaths = getImagesInDirectory(directory); auto folderName = directory.stem(); @@ -228,15 +228,17 @@ static void doRunTests(const fs::path& directory, std::string_view format, int t if (tc.name.empty()) break; auto startTime = std::chrono::steady_clock::now(); - hints.setTryDownscale(false); - hints.setTryHarder(tc.name == "slow"); - hints.setTryRotate(tc.name == "slow"); - hints.setTryInvert(tc.name == "slow"); - hints.setIsPure(tc.name == "pure"); - if (hints.isPure()) - hints.setBinarizer(Binarizer::FixedThreshold); + opts.setTryDownscale(tc.name == "slow_"); + opts.setDownscaleFactor(2); + opts.setDownscaleThreshold(180); + opts.setTryHarder(tc.name == "slow"); + opts.setTryRotate(tc.name == "slow"); + opts.setTryInvert(tc.name == "slow"); + opts.setIsPure(tc.name == "pure"); + if (opts.isPure()) + opts.setBinarizer(Binarizer::FixedThreshold); for (const auto& imgPath : imgPaths) { - auto result = ReadBarcode(ImageLoader::load(imgPath).rotated(test.rotation), hints); + auto result = ReadBarcode(ImageLoader::load(imgPath).rotated(test.rotation), opts); if (result.isValid()) { auto error = checkResult(imgPath, format, result); if (!error.empty()) @@ -260,7 +262,7 @@ static Result readMultiple(const std::vector& imgPaths, std::string_vi Results allResults; for (const auto& imgPath : imgPaths) { auto results = ReadBarcodes(ImageLoader::load(imgPath), - DecodeHints().setFormats(BarcodeFormatFromString(format)).setTryDownscale(false)); + ReaderOptions().setFormats(BarcodeFormatFromString(format)).setTryDownscale(false)); allResults.insert(allResults.end(), results.begin(), results.end()); } @@ -316,9 +318,9 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set }; auto runTests = [&](std::string_view directory, std::string_view format, int total, - const std::vector& tests, const DecodeHints& hints = DecodeHints()) { + const std::vector& tests, const ReaderOptions& opts = ReaderOptions()) { if (hasTest(directory)) - doRunTests(testPathPrefix / directory, format, total, tests, hints); + doRunTests(testPathPrefix / directory, format, total, tests, opts); }; auto runStructuredAppendTest = [&](std::string_view directory, std::string_view format, int total, @@ -332,12 +334,12 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set auto startTime = std::chrono::steady_clock::now(); // clang-format off - runTests("aztec-1", "Aztec", 26, { - { 25, 26, 0 }, - { 25, 26, 90 }, - { 25, 26, 180 }, - { 25, 26, 270 }, - { 24, 0, pure }, + runTests("aztec-1", "Aztec", 27, { + { 26, 27, 0 }, + { 26, 27, 90 }, + { 26, 27, 180 }, + { 26, 27, 270 }, + { 25, 0, pure }, }); runTests("aztec-2", "Aztec", 22, { @@ -348,7 +350,7 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set }); runTests("datamatrix-1", "DataMatrix", 29, { - { 27, 29, 0 }, + { 29, 29, 0 }, { 0, 27, 90 }, { 0, 27, 180 }, { 0, 27, 270 }, @@ -395,7 +397,7 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set runTests("code39-2", "Code39", 2, { { 2, 2, 0 }, { 2, 2, 180 }, - }, DecodeHints().setTryCode39ExtendedMode(true)); + }, ReaderOptions().setTryCode39ExtendedMode(true)); runTests("code39-3", "Code39", 12, { { 12, 12, 0 }, @@ -451,7 +453,7 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set runTests("ean13-extension-1", "EAN-13", 5, { { 3, 5, 0 }, { 3, 5, 180 }, - }, DecodeHints().setEanAddOnSymbol(EanAddOnSymbol::Require)); + }, ReaderOptions().setEanAddOnSymbol(EanAddOnSymbol::Require)); runTests("itf-1", "ITF", 11, { { 10, 11, 0 }, @@ -499,7 +501,7 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set runTests("upca-extension-1", "UPC-A", 6, { { 4, 4, 0 }, { 3, 4, 180 }, - }, DecodeHints().setEanAddOnSymbol(EanAddOnSymbol::Require)); + }, ReaderOptions().setEanAddOnSymbol(EanAddOnSymbol::Require)); runTests("upce-1", "UPC-E", 3, { { 3, 3, 0 }, @@ -562,12 +564,12 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set { 16, 16, 270 }, }); - runTests("qrcode-2", "QRCode", 49, { - { 45, 47, 0 }, - { 45, 47, 90 }, - { 45, 47, 180 }, - { 45, 47, 270 }, - { 21, 1, pure }, // the misread is the 'outer' symbol in 16.png + runTests("qrcode-2", "QRCode", 50, { + { 46, 48, 0 }, + { 46, 48, 90 }, + { 46, 48, 180 }, + { 46, 48, 270 }, + { 22, 1, pure }, // the misread is the 'outer' symbol in 16.png }); runTests("qrcode-3", "QRCode", 28, { @@ -611,6 +613,14 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set { 9, 0, pure }, }); + runTests("rmqrcode-1", "rMQRCode", 3, { + { 2, 3, 0 }, + { 2, 3, 90 }, + { 2, 3, 180 }, + { 2, 3, 270 }, + { 2, 2, pure }, + }); + runTests("pdf417-1", "PDF417", 17, { { 16, 17, 0 }, { 1, 17, 90 }, diff --git a/test/blackbox/TestReaderMain.cpp b/test/blackbox/TestReaderMain.cpp index 31f9063092..5a69c6143c 100644 --- a/test/blackbox/TestReaderMain.cpp +++ b/test/blackbox/TestReaderMain.cpp @@ -35,13 +35,13 @@ int main(int argc, char** argv) fs::path pathPrefix = argv[1]; if (Contains({".png", ".jpg", ".pgm", ".gif"}, pathPrefix.extension())) { - auto hints = DecodeHints().setTryHarder(!getEnv("FAST", false)).setTryRotate(true).setIsPure(getEnv("IS_PURE")); + auto opts = ReaderOptions().setTryHarder(!getEnv("FAST", false)).setTryRotate(true).setIsPure(getEnv("IS_PURE")); if (getenv("FORMATS")) - hints.setFormats(BarcodeFormatsFromString(getenv("FORMATS"))); + opts.setFormats(BarcodeFormatsFromString(getenv("FORMATS"))); int rotation = getEnv("ROTATION"); for (int i = 1; i < argc; ++i) { - Result result = ReadBarcode(ImageLoader::load(argv[i]).rotated(rotation), hints); + Result result = ReadBarcode(ImageLoader::load(argv[i]).rotated(rotation), opts); std::cout << argv[i] << ": "; if (result.isValid()) std::cout << ToString(result.format()) << ": " << result.text() << "\n"; diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt index 8d4e7c3af1..a5202f19a5 100644 --- a/test/fuzz/CMakeLists.txt +++ b/test/fuzz/CMakeLists.txt @@ -5,7 +5,7 @@ set (CMAKE_CXX_COMPILER /usr/bin/clang++) project (ZXingFuzz) -set (CMAKE_CXX_STANDARD 17) +set (CMAKE_CXX_STANDARD 20) set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -march=native -fsanitize=address,fuzzer") set (BUILD_WRITERS ON) @@ -16,9 +16,10 @@ add_subdirectory (${CMAKE_CURRENT_SOURCE_DIR}/../../core ${CMAKE_BINARY_DIR}/ZXi set (TESTS DBEDecoder - DMDecoder DMEncoder - ODDecoders + ReadLinear + ReadMatrix + DecodeMatrix ) foreach (test ${TESTS}) @@ -26,3 +27,4 @@ foreach (test ${TESTS}) add_executable (${name} "${name}.cpp") target_link_libraries (${name} ZXing::ZXing) endforeach() + diff --git a/test/fuzz/fuzzDBEDecoder.cpp b/test/fuzz/fuzzDBEDecoder.cpp index 6270fff552..239aab2484 100644 --- a/test/fuzz/fuzzDBEDecoder.cpp +++ b/test/fuzz/fuzzDBEDecoder.cpp @@ -7,6 +7,7 @@ #include #include "BitArray.h" +#include "Error.h" #include "oned/ODDataBarExpandedBitDecoder.h" using namespace ZXing; @@ -20,7 +21,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) for (size_t i = 0; i < size; ++i) bits.appendBits(data[i], 8); - OneD::DataBar::DecodeExpandedBits(bits); + try { + OneD::DataBar::DecodeExpandedBits(bits); + } catch (std::out_of_range) { + } catch (Error) { + } return 0; } diff --git a/test/fuzz/fuzzDMDecoder.cpp b/test/fuzz/fuzzDMDecoder.cpp deleted file mode 100644 index 6b4500d8c7..0000000000 --- a/test/fuzz/fuzzDMDecoder.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2021 Axel Waggershauser - */ -// SPDX-License-Identifier: Apache-2.0 - -#include -#include - -#include "ByteArray.h" -#include "DecoderResult.h" - -using namespace ZXing; - -namespace ZXing::DataMatrix::DecodedBitStreamParser { -DecoderResult Decode(ByteArray&& bytes, const bool isDMRE); -} - -extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) -{ - if (size < 2) - return 0; - - ByteArray ba; - ba.insert(ba.begin(), data, data + size); - try { - DataMatrix::DecodedBitStreamParser::Decode(std::move(ba), false); - } catch (...) { - } - - return 0; -} diff --git a/test/fuzz/fuzzDecodeMatrix.cpp b/test/fuzz/fuzzDecodeMatrix.cpp new file mode 100644 index 0000000000..88ec435eae --- /dev/null +++ b/test/fuzz/fuzzDecodeMatrix.cpp @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Axel Waggershauser + */ +// SPDX-License-Identifier: Apache-2.0 + +#include "BitArray.h" +#include "ByteArray.h" +#include "DecoderResult.h" +#include "qrcode/QRErrorCorrectionLevel.h" +#include "qrcode/QRVersion.h" +#include "pdf417/PDFDecoder.h" + +#include +#include +#include + +using namespace ZXing; + +namespace ZXing::Aztec { +DecoderResult Decode(const BitArray& bits); +} + +namespace ZXing::DataMatrix::DecodedBitStreamParser { +DecoderResult Decode(ByteArray&& bytes, const bool isDMRE); +} + +namespace ZXing::QRCode { +DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCorrectionLevel ecLevel); +} + +void az(const uint8_t* data, size_t size) +{ + BitArray bits; + for (size_t i = 1; i < size - 1; ++i) + bits.appendBits(data[i], 8); + + bits.appendBits(data[size - 1], (data[0] & 0x7) + 1); + + Aztec::Decode(bits); +} + +void dm(const uint8_t* data, size_t size) +{ + ByteArray ba; + ba.insert(ba.begin(), data, data + size); + try { + DataMatrix::DecodedBitStreamParser::Decode(std::move(ba), false); + } catch (...) { + } +} + +void qr(const uint8_t* data, size_t size) +{ + auto version = QRCode::Version::Model2(std::clamp(data[0] & 0x3F, 1, 40)); + auto ecLevel = QRCode::ECLevelFromBits(data[0] >> 6); + + ByteArray ba; + ba.insert(ba.begin(), data, data + size); + + QRCode::DecodeBitStream(std::move(ba), *version, ecLevel); +} + +void pd(const uint8_t* data, size_t size) +{ + auto codewords = std::vector(size / 2); + auto u16 = reinterpret_cast(data); + + for (int i = 0; i < Size(codewords); ++i) + codewords[i] = u16[i] % 929; + + codewords[0] = Size(codewords); + + Pdf417::Decode(codewords); +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (size < 3) + return 0; + + az(data, size); + dm(data, size); + qr(data, size); + pd(data, size); + + return 0; +} diff --git a/test/fuzz/fuzzODDecoders.cpp b/test/fuzz/fuzzReadLinear.cpp similarity index 72% rename from test/fuzz/fuzzODDecoders.cpp rename to test/fuzz/fuzzReadLinear.cpp index a3d44ec84d..6f05c9d1c8 100644 --- a/test/fuzz/fuzzODDecoders.cpp +++ b/test/fuzz/fuzzReadLinear.cpp @@ -14,7 +14,7 @@ #include "oned/ODDataBarExpandedReader.h" #include "oned/ODITFReader.h" #include "oned/ODCodabarReader.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" using namespace ZXing; @@ -24,15 +24,16 @@ static std::vector> readers; bool init() { - DecodeHints hints; - readers.emplace_back(new MultiUPCEANReader(hints)); - readers.emplace_back(new Code39Reader(hints)); - readers.emplace_back(new Code93Reader(hints)); - readers.emplace_back(new Code128Reader(hints)); - readers.emplace_back(new ITFReader(hints)); - readers.emplace_back(new CodabarReader(hints)); - readers.emplace_back(new DataBarReader(hints)); - readers.emplace_back(new DataBarExpandedReader(hints)); + static ReaderOptions opts; + opts.setReturnErrors(true); + readers.emplace_back(new MultiUPCEANReader(opts)); + readers.emplace_back(new Code39Reader(opts)); + readers.emplace_back(new Code93Reader(opts)); + readers.emplace_back(new Code128Reader(opts)); + readers.emplace_back(new ITFReader(opts)); + readers.emplace_back(new CodabarReader(opts)); + readers.emplace_back(new DataBarReader(opts)); + readers.emplace_back(new DataBarExpandedReader(opts)); return true; } diff --git a/test/fuzz/fuzzReadMatrix.cpp b/test/fuzz/fuzzReadMatrix.cpp new file mode 100644 index 0000000000..dd01b40171 --- /dev/null +++ b/test/fuzz/fuzzReadMatrix.cpp @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Axel Waggershauser + */ +// SPDX-License-Identifier: Apache-2.0 + +#include "ReadBarcode.h" + +#include +#include +#include +#include + +using namespace ZXing; + +uint64_t Expand(uint8_t b) +{ + uint64_t shift = 0x0000040810204081ul; // bits set: 0, 7, 14, 21, 28, 35, 42 + uint64_t mask = 0x0001010101010101ul; // bits set: 0, 8, 16, 24, 32, 40, 48 + return ((uint64_t)(b & 127) * shift & mask) | (uint64_t)(b & 128) << 49; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (size < 3) + return 0; + + static auto opts = ReaderOptions() + .setFormats(BarcodeFormat::MatrixCodes) + .setBinarizer(Binarizer::BoolCast) + .setReturnErrors(true) + .setTryInvert(false) + .setTryRotate(false); + + int ratio = data[0] + 1; + int nBits = (size - 1) * 8; + int width = std::clamp(nBits * ratio / 256, 1, nBits); + int height = std::clamp(nBits / width, 1, nBits); + + assert(width * height <= nBits); + + ByteArray buffer(nBits); + for (size_t i = 1; i < size; ++i) + *reinterpret_cast(&buffer[(i - 1) * 8]) = Expand(data[i]); + +#ifdef PRINT_DEBUG + printf("s: %zu, r: %d, n: %d -> %d x %d\n", size, ratio, nBits, width, height); +#endif + + auto image = ImageView(buffer.data(), width, height, ImageFormat::Lum); + auto res = ReadBarcodes(image, opts); + +#ifdef PRINT_DEBUG + for (const auto& r : res) + printf("%s: %s / %s\n", ToString(r.format()).c_str(), r.text().c_str(), ToString(r.error()).c_str()); +#endif + + static int detectedSybols = 0; + detectedSybols += Size(res); + if (!res.empty() && detectedSybols % 100 == 0) + printf("detected barcode symbols: %d\n", detectedSybols); + + return 0; +} diff --git a/test/samples/aztec-1/padding_bs.png b/test/samples/aztec-1/padding_bs.png new file mode 100644 index 0000000000..925ad93df1 Binary files /dev/null and b/test/samples/aztec-1/padding_bs.png differ diff --git a/test/samples/aztec-1/padding_bs.txt b/test/samples/aztec-1/padding_bs.txt new file mode 100644 index 0000000000..d5749737a0 Binary files /dev/null and b/test/samples/aztec-1/padding_bs.txt differ diff --git a/test/samples/qrcode-2/qr-model-1.png b/test/samples/qrcode-2/qr-model-1.png new file mode 100644 index 0000000000..4b199902d0 Binary files /dev/null and b/test/samples/qrcode-2/qr-model-1.png differ diff --git a/test/samples/qrcode-2/qr-model-1.result.txt b/test/samples/qrcode-2/qr-model-1.result.txt new file mode 100644 index 0000000000..7f3634787a --- /dev/null +++ b/test/samples/qrcode-2/qr-model-1.result.txt @@ -0,0 +1,2 @@ +symbologyIdentifier=]Q0 +ecLevel=M diff --git a/test/samples/qrcode-2/qr-model-1.txt b/test/samples/qrcode-2/qr-model-1.txt new file mode 100644 index 0000000000..a6bce8d223 --- /dev/null +++ b/test/samples/qrcode-2/qr-model-1.txt @@ -0,0 +1 @@ +QR Code Model 1 \ No newline at end of file diff --git a/test/samples/rmqrcode-1/R17x139.png b/test/samples/rmqrcode-1/R17x139.png new file mode 100644 index 0000000000..19d17e352d Binary files /dev/null and b/test/samples/rmqrcode-1/R17x139.png differ diff --git a/test/samples/rmqrcode-1/R17x139.txt b/test/samples/rmqrcode-1/R17x139.txt new file mode 100644 index 0000000000..ea78e300a4 --- /dev/null +++ b/test/samples/rmqrcode-1/R17x139.txt @@ -0,0 +1 @@ +3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081 \ No newline at end of file diff --git a/test/samples/rmqrcode-1/R7x43-H.png b/test/samples/rmqrcode-1/R7x43-H.png new file mode 100644 index 0000000000..d62eb47c6e Binary files /dev/null and b/test/samples/rmqrcode-1/R7x43-H.png differ diff --git a/test/samples/rmqrcode-1/R7x43-H.result.txt b/test/samples/rmqrcode-1/R7x43-H.result.txt new file mode 100644 index 0000000000..f29c889326 --- /dev/null +++ b/test/samples/rmqrcode-1/R7x43-H.result.txt @@ -0,0 +1 @@ +symbologyIdentifier=]Q1 diff --git a/test/samples/rmqrcode-1/R7x43-H.txt b/test/samples/rmqrcode-1/R7x43-H.txt new file mode 100644 index 0000000000..8b22b22d43 --- /dev/null +++ b/test/samples/rmqrcode-1/R7x43-H.txt @@ -0,0 +1 @@ +,, \ No newline at end of file diff --git a/test/samples/rmqrcode-1/R7x43-H_inverted.png b/test/samples/rmqrcode-1/R7x43-H_inverted.png new file mode 100644 index 0000000000..138a06562d Binary files /dev/null and b/test/samples/rmqrcode-1/R7x43-H_inverted.png differ diff --git a/test/samples/rmqrcode-1/R7x43-H_inverted.result.txt b/test/samples/rmqrcode-1/R7x43-H_inverted.result.txt new file mode 100644 index 0000000000..da82284f3e --- /dev/null +++ b/test/samples/rmqrcode-1/R7x43-H_inverted.result.txt @@ -0,0 +1 @@ +isInverted=true diff --git a/test/samples/rmqrcode-1/R7x43-H_inverted.txt b/test/samples/rmqrcode-1/R7x43-H_inverted.txt new file mode 100644 index 0000000000..8b22b22d43 --- /dev/null +++ b/test/samples/rmqrcode-1/R7x43-H_inverted.txt @@ -0,0 +1 @@ +,, \ No newline at end of file diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index cf1dcd4ad6..02d78d0f2c 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -5,6 +5,11 @@ if (GTest_POPULATED) set (INSTALL_GTEST OFF CACHE BOOL "" FORCE) endif() +if (MSVC) + # default to UTF-8 + add_compile_options("$<$:/utf-8>") +endif() + # Our executable add_executable (UnitTest BarcodeFormatTest.cpp @@ -62,6 +67,7 @@ add_executable (UnitTest qrcode/QRModeTest.cpp qrcode/QRVersionTest.cpp qrcode/QRWriterTest.cpp + qrcode/RMQRDecoderTest.cpp pdf417/PDF417DecoderTest.cpp pdf417/PDF417ErrorCorrectionTest.cpp pdf417/PDF417HighLevelEncoderTest.cpp diff --git a/test/unit/ThresholdBinarizerTest.cpp b/test/unit/ThresholdBinarizerTest.cpp index d49e51d9cf..0755696cf2 100644 --- a/test/unit/ThresholdBinarizerTest.cpp +++ b/test/unit/ThresholdBinarizerTest.cpp @@ -44,7 +44,7 @@ TEST(ThresholdBinarizerTest, PatternRowClear) { std::string bitstream; BitMatrix bits; - DecodeHints hints; + ReaderOptions opts; std::vector buf; // Test that ThresholdBinarizer::getPatternRow() clears row first (same as GlobalHistogramBinarizer) @@ -94,9 +94,9 @@ TEST(ThresholdBinarizerTest, PatternRowClear) "01000111000101111010011000000000101011110100111000010"; bits = ParseBitMatrix(bitstream, 53 /*width*/); - hints.setFormats(BarcodeFormat::DataBarExpanded); - hints.setMinLineCount(1); - OneD::Reader reader(hints); + opts.setFormats(BarcodeFormat::DataBarExpanded); + opts.setMinLineCount(1); + OneD::Reader reader(opts); Result result = reader.decode(ThresholdBinarizer(getImageView(buf, bits), 0x7F)); EXPECT_TRUE(result.isValid()); diff --git a/test/unit/ZXAlgorithmsTest.cpp b/test/unit/ZXAlgorithmsTest.cpp index 7f73d7d7f5..968419d9f1 100644 --- a/test/unit/ZXAlgorithmsTest.cpp +++ b/test/unit/ZXAlgorithmsTest.cpp @@ -40,7 +40,7 @@ TEST(ZXAlgorithmsTest, UpdateMinMax) EXPECT_EQ(m, 2); EXPECT_EQ(M, 5); - m = 1, M = 1; + m = M = 1; UpdateMinMax(m, M, 0); EXPECT_EQ(m, 0); EXPECT_EQ(M, 1); diff --git a/test/unit/datamatrix/DMHighLevelEncodeTest.cpp b/test/unit/datamatrix/DMHighLevelEncodeTest.cpp index cf03ef4e62..4a77810ba7 100644 --- a/test/unit/datamatrix/DMHighLevelEncodeTest.cpp +++ b/test/unit/datamatrix/DMHighLevelEncodeTest.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "ByteArray.h" +#include "CharacterSet.h" #include "ZXAlgorithms.h" #include "datamatrix/DMHighLevelEncoder.h" #include "datamatrix/DMSymbolInfo.h" @@ -235,7 +236,7 @@ TEST(DMHighLevelEncodeTest, EDIFACTEncodation) // Checking temporary unlatch from EDIFACT visualized = Encode(L".XXX.XXX.XXX.XXX.XXX.XXX.\xFCXX.XXX.XXX.XXX.XXX.XXX.XXX"); EXPECT_EQ(visualized, "240 185 134 24 185 134 24 185 134 24 185 134 24 185 134 24 185 134 24" - " 124 47 235 125 240" //<-- this is the temporary unlatch + " 124 47 235 125 240" // <- this is the temporary unlatch " 97 139 152 97 139 152 97 139 152 97 139 152 97 139 152 97 139 152 89 89"); } @@ -346,7 +347,7 @@ TEST(DMHighLevelEncodeTest, EncodingWithStartAsX12AndLatchToEDIFACTInTheMiddle) TEST(DMHighLevelEncodeTest, EDIFACTWithEODBug) { std::string visualized = Visualize( - DataMatrix::Encode(L"abc<->ABCDE", DataMatrix::SymbolShape::SQUARE, -1, -1, -1, -1)); + DataMatrix::Encode(L"abc<->ABCDE", CharacterSet::ISO8859_1, DataMatrix::SymbolShape::SQUARE, -1, -1, -1, -1)); // switch to EDIFACT on '<', uses 10 code words + 2 padding. Buggy code introduced invalid 254 after the 5 EXPECT_EQ(visualized, "98 99 100 240 242 223 129 8 49 5 129 147"); } diff --git a/test/unit/oned/ODCodaBarWriterTest.cpp b/test/unit/oned/ODCodaBarWriterTest.cpp index 471ac52473..c49ca3d880 100644 --- a/test/unit/oned/ODCodaBarWriterTest.cpp +++ b/test/unit/oned/ODCodaBarWriterTest.cpp @@ -6,7 +6,7 @@ #include "oned/ODCodabarWriter.h" #include "BitMatrixIO.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" #include "oned/ODCodabarReader.h" @@ -50,9 +50,9 @@ TEST(ODCodaBarWriterTest, FullCircle) { std::string text = "A0123456789-$:/.+A"; auto matrix = CodabarWriter().encode(text, 0, 0); - auto hints = DecodeHints().setReturnCodabarStartEnd(true); + auto opts = ReaderOptions().setReturnCodabarStartEnd(true); - Result res = OneD::DecodeSingleRow(CodabarReader(hints), matrix.row(0)); + Result res = OneD::DecodeSingleRow(CodabarReader(opts), matrix.row(0)); EXPECT_EQ(text, res.text()); } diff --git a/test/unit/oned/ODCode128ReaderTest.cpp b/test/unit/oned/ODCode128ReaderTest.cpp index b1b122eaab..1c68d94357 100644 --- a/test/unit/oned/ODCode128ReaderTest.cpp +++ b/test/unit/oned/ODCode128ReaderTest.cpp @@ -5,7 +5,7 @@ #include "oned/ODCode128Reader.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" #include "gtest/gtest.h" @@ -26,8 +26,8 @@ static Result parse(const int startPattern, PatternRow row) row.insert(row.end(), { 2, 3, 3, 1, 1, 1, 2, 0 }); // Stop pattern std::unique_ptr state; - DecodeHints hints; - Code128Reader reader(hints); + ReaderOptions opts; + Code128Reader reader(opts); PatternView next(row); return reader.decodePattern(0, next, state); } diff --git a/test/unit/oned/ODCode128WriterTest.cpp b/test/unit/oned/ODCode128WriterTest.cpp index de4c0fe912..7c7291b3bc 100644 --- a/test/unit/oned/ODCode128WriterTest.cpp +++ b/test/unit/oned/ODCode128WriterTest.cpp @@ -6,7 +6,7 @@ #include "oned/ODCode128Writer.h" #include "BitMatrixIO.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" #include "oned/ODCode128Reader.h" @@ -37,13 +37,13 @@ static std::string LineMatrixToString(const BitMatrix& matrix) static ZXing::Result Decode(const BitMatrix &matrix) { - DecodeHints hints; - return DecodeSingleRow(Code128Reader(hints), matrix.row(0)); + ReaderOptions opts; + return DecodeSingleRow(Code128Reader(opts), matrix.row(0)); } TEST(ODCode128Writer, EncodeWithFunc1) { - auto toEncode = L"\xf1""123"; + auto toEncode = L"\u00f1123"; // "12" "3" check digit 92 auto expected = QUIET_SPACE + START_CODE_C + FNC1 + "10110011100" + SWITCH_CODE_B + "11001011100" + "10101111000" + STOP + QUIET_SPACE; @@ -53,7 +53,7 @@ TEST(ODCode128Writer, EncodeWithFunc1) TEST(ODCode128Writer, EncodeWithFunc2) { - auto toEncode = L"\xf2""123"; + auto toEncode = L"\u00f2123"; // "1" "2" "3" check digit 56 auto expected = QUIET_SPACE + START_CODE_B + FNC2 + "10011100110" + "11001110010" + "11001011100" + "11100010110" + STOP + QUIET_SPACE; @@ -63,7 +63,7 @@ TEST(ODCode128Writer, EncodeWithFunc2) TEST(ODCode128Writer, EncodeWithFunc3) { - auto toEncode = L"\xf3""123"; + auto toEncode = L"\u00f3123"; // "1" "2" "3" check digit 51 auto expected = QUIET_SPACE + START_CODE_B + FNC3 + "10011100110" + "11001110010" + "11001011100" + "11101000110" + STOP + QUIET_SPACE; @@ -73,7 +73,7 @@ TEST(ODCode128Writer, EncodeWithFunc3) TEST(ODCode128Writer, EncodeWithFunc4) { - auto toEncode = L"\xf4""123"; + auto toEncode = L"\u00f4123"; // "1" "2" "3" check digit 59 auto expected = QUIET_SPACE + START_CODE_B + FNC4B + "10011100110" + "11001110010" + "11001011100" + "11100011010" + STOP + QUIET_SPACE; @@ -83,7 +83,7 @@ TEST(ODCode128Writer, EncodeWithFunc4) TEST(ODCode128Writer, EncodeWithFncsAndNumberInCodesetA) { - auto toEncode = L"\n" "\xf1" "\xf4" "1" "\n"; + auto toEncode = L"\n\u00f1\u00f41\n"; auto expected = QUIET_SPACE + START_CODE_A + LF + FNC1 + FNC4A + "10011100110" + LF + "10101111000" + STOP + QUIET_SPACE; auto actual = LineMatrixToString(Code128Writer().encode(toEncode, 0, 0)); EXPECT_EQ(actual, expected); @@ -91,7 +91,7 @@ TEST(ODCode128Writer, EncodeWithFncsAndNumberInCodesetA) TEST(ODCode128Writer, RoundtripGS1) { - auto toEncode = L"\xf1" "10958" "\xf1" "17160526"; + auto toEncode = L"\u00f110958\u00f117160526"; auto decResult = Decode(Code128Writer().encode(toEncode, 0, 0)); EXPECT_EQ(decResult.text(TextMode::HRI), "(10)958(17)160526"); @@ -100,7 +100,7 @@ TEST(ODCode128Writer, RoundtripGS1) TEST(ODCode128Writer, RoundtripFNC1) { - auto toEncode = L"1\xf1" "0958" "\xf1" "17160526"; + auto toEncode = L"1\u00f10958\u00f117160526"; auto encResult = Code128Writer().encode(toEncode, 0, 0); auto decResult = Decode(encResult); diff --git a/test/unit/oned/ODCode39ExtendedModeTest.cpp b/test/unit/oned/ODCode39ExtendedModeTest.cpp index b7af0c6918..589880a617 100644 --- a/test/unit/oned/ODCode39ExtendedModeTest.cpp +++ b/test/unit/oned/ODCode39ExtendedModeTest.cpp @@ -6,7 +6,7 @@ #include "BitArray.h" #include "BitArrayUtility.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" #include "oned/ODCode39Reader.h" @@ -17,9 +17,9 @@ using namespace ZXing::OneD; static std::string Decode(std::string_view encoded) { - auto hints = DecodeHints().setTryCode39ExtendedMode(true); + auto opts = ReaderOptions().setTryCode39ExtendedMode(true); BitArray row = Utility::ParseBitArray(encoded, '1'); - Result result = DecodeSingleRow(Code39Reader(hints), row.range()); + Result result = DecodeSingleRow(Code39Reader(opts), row.range()); return result.text(TextMode::Plain); } diff --git a/test/unit/oned/ODCode39ReaderTest.cpp b/test/unit/oned/ODCode39ReaderTest.cpp index e33f587a0c..a8684a7682 100644 --- a/test/unit/oned/ODCode39ReaderTest.cpp +++ b/test/unit/oned/ODCode39ReaderTest.cpp @@ -5,7 +5,7 @@ #include "oned/ODCode39Reader.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" #include "gtest/gtest.h" @@ -14,9 +14,9 @@ using namespace ZXing; using namespace ZXing::OneD; // Helper to call decodePattern() -static Result parse(PatternRow row, DecodeHints hints = {}) +static Result parse(PatternRow row, ReaderOptions opts = {}) { - Code39Reader reader(hints); + Code39Reader reader(opts); row.insert(row.begin(), { 0, 1, 2, 1, 1, 2, 1, 2, 1, 1, 0 }); row.insert(row.end(), { 0, 1, 2, 1, 1, 2, 1, 2, 1, 1, 0 }); @@ -38,7 +38,7 @@ TEST(ODCode39ReaderTest, SymbologyIdentifier) { // "A" with checksum PatternRow row({ 2, 1, 1, 1, 1, 2, 1, 1, 2, 0, 2, 1, 1, 1, 1, 2, 1, 1, 2 }); - auto result = parse(row, DecodeHints().setValidateCode39CheckSum(true)); + auto result = parse(row, ReaderOptions().setValidateCode39CheckSum(true)); EXPECT_EQ(result.symbologyIdentifier(), "]A3"); EXPECT_EQ(result.text(), "A"); @@ -49,7 +49,7 @@ TEST(ODCode39ReaderTest, SymbologyIdentifier) { // Extended "a" PatternRow row({ 1, 2, 1, 1, 1, 2, 1, 2, 1, 0, 2, 1, 1, 1, 1, 2, 1, 1, 2 }); - auto result = parse(row, DecodeHints().setTryCode39ExtendedMode(true)); + auto result = parse(row, ReaderOptions().setTryCode39ExtendedMode(true)); EXPECT_EQ(result.symbologyIdentifier(), "]A4"); EXPECT_EQ(result.text(), "a"); @@ -60,7 +60,7 @@ TEST(ODCode39ReaderTest, SymbologyIdentifier) { // Extended "a" with checksum PatternRow row({ 1, 2, 1, 1, 1, 2, 1, 2, 1, 0, 2, 1, 1, 1, 1, 2, 1, 1, 2, 0, 2, 1, 1, 2, 1, 1, 2, 1, 1 }); - auto result = parse(row, DecodeHints().setTryCode39ExtendedMode(true).setValidateCode39CheckSum(true)); + auto result = parse(row, ReaderOptions().setTryCode39ExtendedMode(true).setValidateCode39CheckSum(true)); EXPECT_EQ(result.symbologyIdentifier(), "]A7"); EXPECT_EQ(result.text(), "a"); diff --git a/test/unit/oned/ODCode93ReaderTest.cpp b/test/unit/oned/ODCode93ReaderTest.cpp index 98b49de46e..58c09ea035 100644 --- a/test/unit/oned/ODCode93ReaderTest.cpp +++ b/test/unit/oned/ODCode93ReaderTest.cpp @@ -7,7 +7,7 @@ #include "oned/ODCode93Reader.h" #include "BitArray.h" #include "BitArrayUtility.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" #include "gtest/gtest.h" @@ -17,9 +17,9 @@ using namespace ZXing::OneD; static std::string Decode(std::string_view input) { - DecodeHints hints; + ReaderOptions opts; auto row = Utility::ParseBitArray(input, '1'); - auto result = DecodeSingleRow(Code93Reader(hints), row.range()); + auto result = DecodeSingleRow(Code93Reader(opts), row.range()); return result.text(TextMode::Plain); } diff --git a/test/unit/oned/ODDataBarReaderTest.cpp b/test/unit/oned/ODDataBarReaderTest.cpp index 70cbfacbb8..cab6b36876 100644 --- a/test/unit/oned/ODDataBarReaderTest.cpp +++ b/test/unit/oned/ODDataBarReaderTest.cpp @@ -5,7 +5,7 @@ #include "oned/ODDataBarReader.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "Result.h" #include "gtest/gtest.h" @@ -14,9 +14,9 @@ using namespace ZXing; using namespace ZXing::OneD; // Helper to call decodePattern() -static Result parse(PatternRow row, DecodeHints hints = {}) +static Result parse(PatternRow row, ReaderOptions opts = {}) { - DataBarReader reader(hints); + DataBarReader reader(opts); row.insert(row.begin(), { 1, 1 }); // Left guard row.insert(row.end(), { 1, 1 }); // Right guard diff --git a/test/unit/qrcode/QRBitMatrixParserTest.cpp b/test/unit/qrcode/QRBitMatrixParserTest.cpp index f8c5a93a0d..4b438a304d 100644 --- a/test/unit/qrcode/QRBitMatrixParserTest.cpp +++ b/test/unit/qrcode/QRBitMatrixParserTest.cpp @@ -35,9 +35,9 @@ TEST(QRBitMatrixParserTest, MQRCodeM3L) "XXX XX X X XXXX\n", 88, false); - const auto version = ReadVersion(bitMatrix); + const auto format = ReadFormatInformation(bitMatrix); + const auto version = ReadVersion(bitMatrix, format.type()); EXPECT_EQ(3, version->versionNumber()); - const auto format = ReadFormatInformation(bitMatrix, true); const auto codewords = ReadCodewords(bitMatrix, *version, format); EXPECT_EQ(17, codewords.size()); EXPECT_EQ(0x0, codewords[10]); @@ -63,9 +63,9 @@ TEST(QRBitMatrixParserTest, MQRCodeM3M) "X X XXXX XXX\n", 88, false); - const auto version = ReadVersion(bitMatrix); + const auto format = ReadFormatInformation(bitMatrix); + const auto version = ReadVersion(bitMatrix, format.type()); EXPECT_EQ(3, version->versionNumber()); - const auto format = ReadFormatInformation(bitMatrix, true); const auto codewords = ReadCodewords(bitMatrix, *version, format); EXPECT_EQ(17, codewords.size()); EXPECT_EQ(0x0, codewords[8]); diff --git a/test/unit/qrcode/QRDecodedBitStreamParserTest.cpp b/test/unit/qrcode/QRDecodedBitStreamParserTest.cpp index b979e73855..3521c91bc2 100644 --- a/test/unit/qrcode/QRDecodedBitStreamParserTest.cpp +++ b/test/unit/qrcode/QRDecodedBitStreamParserTest.cpp @@ -7,8 +7,6 @@ #include "BitArray.h" #include "ByteArray.h" #include "DecoderResult.h" -#include "qrcode/QRDataMask.h" -#include "qrcode/QRDecoder.h" #include "qrcode/QRErrorCorrectionLevel.h" #include "qrcode/QRVersion.h" @@ -31,7 +29,7 @@ TEST(QRDecodedBitStreamParserTest, SimpleByteMode) ba.appendBits(0xF1, 8); ba.appendBits(0xF2, 8); ba.appendBits(0xF3, 8); - auto result = DecodeBitStream(ba.toBytes(), *Version::FromNumber(1), ErrorCorrectionLevel::Medium).text(); + auto result = DecodeBitStream(ba.toBytes(), *Version::Model2(1), ErrorCorrectionLevel::Medium).text(); EXPECT_EQ(L"\xF1\xF2\xF3", result); } @@ -44,7 +42,7 @@ TEST(QRDecodedBitStreamParserTest, SimpleSJIS) ba.appendBits(0xA2, 8); ba.appendBits(0xA3, 8); ba.appendBits(0xD0, 8); - auto result = DecodeBitStream(ba.toBytes(), *Version::FromNumber(1), ErrorCorrectionLevel::Medium).text(); + auto result = DecodeBitStream(ba.toBytes(), *Version::Model2(1), ErrorCorrectionLevel::Medium).text(); EXPECT_EQ(L"\uff61\uff62\uff63\uff90", result); } @@ -58,7 +56,7 @@ TEST(QRDecodedBitStreamParserTest, ECI) ba.appendBits(0xA1, 8); ba.appendBits(0xA2, 8); ba.appendBits(0xA3, 8); - auto result = DecodeBitStream(ba.toBytes(), *Version::FromNumber(1), ErrorCorrectionLevel::Medium).text(); + auto result = DecodeBitStream(ba.toBytes(), *Version::Model2(1), ErrorCorrectionLevel::Medium).text(); EXPECT_EQ(L"\xED\xF3\xFA", result); } @@ -69,7 +67,7 @@ TEST(QRDecodedBitStreamParserTest, Hanzi) ba.appendBits(0x01, 4); // Subset 1 = GB2312 encoding ba.appendBits(0x01, 8); // 1 characters ba.appendBits(0x03C1, 13); - auto result = DecodeBitStream(ba.toBytes(), *Version::FromNumber(1), ErrorCorrectionLevel::Medium).text(); + auto result = DecodeBitStream(ba.toBytes(), *Version::Model2(1), ErrorCorrectionLevel::Medium).text(); EXPECT_EQ(L"\u963f", result); } @@ -82,13 +80,13 @@ TEST(QRDecodedBitStreamParserTest, HanziLevel1) // A5A2 (U+30A2) => A5A2 - A1A1 = 401, 4*60 + 01 = 0181 ba.appendBits(0x0181, 13); - auto result = DecodeBitStream(ba.toBytes(), *Version::FromNumber(1), ErrorCorrectionLevel::Medium).text(); + auto result = DecodeBitStream(ba.toBytes(), *Version::Model2(1), ErrorCorrectionLevel::Medium).text(); EXPECT_EQ(L"\u30a2", result); } TEST(QRDecodedBitStreamParserTest, SymbologyIdentifier) { - const Version& version = *Version::FromNumber(1); + const Version& version = *Version::Model2(1); const ErrorCorrectionLevel ecLevel = ErrorCorrectionLevel::Medium; DecoderResult result; @@ -132,3 +130,15 @@ TEST(QRDecodedBitStreamParserTest, SymbologyIdentifier) result = DecodeBitStream({0x9A, 0x42, 0x00, 0x96, 0x00}, version, ecLevel); EXPECT_FALSE(result.isValid()); } + +TEST(QRDecodedBitStreamParserTest, GS1PercentGS) +{ + const Version& version = *Version::Model2(1); + const ErrorCorrectionLevel ecLevel = ErrorCorrectionLevel::Quality; + DecoderResult result; + + // GS1 "FNC1(1st) A(11) 9112%%%2012 (9112%2012)" + result = DecodeBitStream({0x52, 0x05, 0x99, 0x60, 0x5F, 0xB5, 0x35, 0x80, 0x01, 0x08, 0x00, 0xEC, 0x11}, version, ecLevel); + EXPECT_EQ(result.content().text(TextMode::Plain), "9112%\x1D" "2012"); + EXPECT_EQ(result.content().text(TextMode::HRI), "(91)12%(20)12"); +} diff --git a/test/unit/qrcode/QREncoderTest.cpp b/test/unit/qrcode/QREncoderTest.cpp index 8e28abc515..2f640d4674 100644 --- a/test/unit/qrcode/QREncoderTest.cpp +++ b/test/unit/qrcode/QREncoderTest.cpp @@ -118,7 +118,7 @@ TEST(QREncoderTest, Encode) ASSERT_NE(qrCode.version, nullptr); EXPECT_EQ(qrCode.version->versionNumber(), 1); EXPECT_EQ(qrCode.maskPattern, 4); - EXPECT_EQ(ToString(qrCode.matrix), + EXPECT_EQ(ToString(qrCode.matrix, 'X', ' ', true), "X X X X X X X X X X X X X X X X \n" "X X X X X X X \n" "X X X X X X X X X X \n" @@ -164,7 +164,7 @@ TEST(QREncoderTest, SimpleUTF8ECI) ASSERT_NE(qrCode.version, nullptr); EXPECT_EQ(qrCode.version->versionNumber(), 1); EXPECT_EQ(qrCode.maskPattern, 6); - EXPECT_EQ(ToString(qrCode.matrix), // break the line comment + EXPECT_EQ(ToString(qrCode.matrix, 'X', ' ', true), "X X X X X X X X X X X X X X X X \n" "X X X X X X \n" "X X X X X X X X X X X X X \n" @@ -196,7 +196,7 @@ TEST(QREncoderTest, SimpleBINARYECI) ASSERT_NE(qrCode.version, nullptr); EXPECT_EQ(qrCode.version->versionNumber(), 1); EXPECT_EQ(qrCode.maskPattern, 6); - EXPECT_EQ(ToString(qrCode.matrix), + EXPECT_EQ(ToString(qrCode.matrix, 'X', ' ', true), "X X X X X X X X X X X X X X X X X X \n" "X X X X X \n" "X X X X X X X X X X X X X \n" @@ -228,7 +228,7 @@ TEST(QREncoderTest, EncodeKanjiMode) ASSERT_NE(qrCode.version, nullptr); EXPECT_EQ(qrCode.version->versionNumber(), 1); EXPECT_EQ(qrCode.maskPattern, 0); - EXPECT_EQ(ToString(qrCode.matrix), + EXPECT_EQ(ToString(qrCode.matrix, 'X', ' ', true), "X X X X X X X X X X X X X X X X \n" "X X X X X X \n" "X X X X X X X X X X X X X X \n" @@ -260,7 +260,7 @@ TEST(QREncoderTest, EncodeShiftjisNumeric) ASSERT_NE(qrCode.version, nullptr); EXPECT_EQ(qrCode.version->versionNumber(), 1); EXPECT_EQ(qrCode.maskPattern, 2); - EXPECT_EQ(ToString(qrCode.matrix), + EXPECT_EQ(ToString(qrCode.matrix, 'X', ' ', true), "X X X X X X X X X X X X X X X X X \n" "X X X X X X \n" "X X X X X X X X X X X \n" @@ -292,7 +292,7 @@ TEST(QREncoderTest, EncodeGS1) ASSERT_NE(qrCode.version, nullptr); EXPECT_EQ(qrCode.version->versionNumber(), 2); EXPECT_EQ(qrCode.maskPattern, 4); - EXPECT_EQ(ToString(qrCode.matrix), + EXPECT_EQ(ToString(qrCode.matrix, 'X', ' ', true), "X X X X X X X X X X X X X X X X X X X X \n" "X X X X X X X X \n" "X X X X X X X X X X X X X X \n" @@ -328,7 +328,7 @@ TEST(QREncoderTest, EncodeGS1ModeHeaderWithECI) ASSERT_NE(qrCode.version, nullptr); EXPECT_EQ(qrCode.version->versionNumber(), 1); EXPECT_EQ(qrCode.maskPattern, 5); - EXPECT_EQ(ToString(qrCode.matrix), + EXPECT_EQ(ToString(qrCode.matrix, 'X', ' ', true), "X X X X X X X X X X X X X X X X X \n" "X X X X X X \n" "X X X X X X X X X X X X X \n" @@ -363,22 +363,22 @@ TEST(QREncoderTest, AppendLengthInfo) { BitArray bits; AppendLengthInfo(1, // 1 letter (1/1). - *Version::FromNumber(1), CodecMode::NUMERIC, bits); + *Version::Model2(1), CodecMode::NUMERIC, bits); EXPECT_EQ(ToString(bits), RemoveSpace("........ .X")); // 10 bits. bits = BitArray(); AppendLengthInfo(2, // 2 letters (2/1). - *Version::FromNumber(10), CodecMode::ALPHANUMERIC, bits); + *Version::Model2(10), CodecMode::ALPHANUMERIC, bits); EXPECT_EQ(ToString(bits), RemoveSpace("........ .X.")); // 11 bits. bits = BitArray(); AppendLengthInfo(255, // 255 letter (255/1). - *Version::FromNumber(27), CodecMode::BYTE, bits); + *Version::Model2(27), CodecMode::BYTE, bits); EXPECT_EQ(ToString(bits), RemoveSpace("........ XXXXXXXX")); // 16 bits. bits = BitArray(); AppendLengthInfo(512, // 512 letters (1024/2). - *Version::FromNumber(40), CodecMode::KANJI, bits); + *Version::Model2(40), CodecMode::KANJI, bits); EXPECT_EQ(ToString(bits), RemoveSpace("..X..... ....")); // 12 bits. } diff --git a/test/unit/qrcode/QRFormatInformationTest.cpp b/test/unit/qrcode/QRFormatInformationTest.cpp index bbbef8a0ed..0e8488bd9c 100644 --- a/test/unit/qrcode/QRFormatInformationTest.cpp +++ b/test/unit/qrcode/QRFormatInformationTest.cpp @@ -15,7 +15,8 @@ static const int MASKED_TEST_FORMAT_INFO = 0x2BED; static const int MASKED_TEST_FORMAT_INFO2 = ((0x2BED << 1) & 0b1111111000000000) | 0b100000000 | (0x2BED & 0b11111111); // insert the 'Dark Module' static const int UNMASKED_TEST_FORMAT_INFO = MASKED_TEST_FORMAT_INFO ^ 0x5412; static const int MICRO_MASKED_TEST_FORMAT_INFO = 0x3BBA; -static const int MICRO_UNMASKED_TEST_FORMAT_INFO = MICRO_MASKED_TEST_FORMAT_INFO ^ 0x4445; +static const int RMQR_MASKED_TEST_FORMAT_INFO = 0x20137; +static const int RMQR_MASKED_TEST_FORMAT_INFO_SUB = 0x1F1FE; static void DoFormatInformationTest(const int formatInfo, const uint8_t expectedMask, const ErrorCorrectionLevel& expectedECL) { @@ -25,6 +26,18 @@ static void DoFormatInformationTest(const int formatInfo, const uint8_t expected EXPECT_EQ(expectedECL, parsedFormat.ecLevel); } +// Helper for rMQR to unset `numBits` number of bits +static uint32_t RMQRUnsetBits(uint32_t formatInfoBits, int numBits) +{ + for (int i = 0; i < 18 && numBits; i++) { + if (formatInfoBits & (1 << i)) { + formatInfoBits ^= 1 << i; + numBits--; + } + } + return formatInfoBits; +} + TEST(QRFormatInformationTest, Decode) { // Normal case @@ -43,7 +56,9 @@ TEST(QRFormatInformationTest, DecodeWithBitDifference) EXPECT_EQ(expected, FormatInformation::DecodeQR(MASKED_TEST_FORMAT_INFO ^ 0x01, MASKED_TEST_FORMAT_INFO2 ^ 0x01)); EXPECT_EQ(expected, FormatInformation::DecodeQR(MASKED_TEST_FORMAT_INFO ^ 0x03, MASKED_TEST_FORMAT_INFO2 ^ 0x03)); EXPECT_EQ(expected, FormatInformation::DecodeQR(MASKED_TEST_FORMAT_INFO ^ 0x07, MASKED_TEST_FORMAT_INFO2 ^ 0x07)); - EXPECT_TRUE(!FormatInformation::DecodeQR(MASKED_TEST_FORMAT_INFO ^ 0x0F, MASKED_TEST_FORMAT_INFO2 ^ 0x0F).isValid()); + auto unexpected = FormatInformation::DecodeQR(MASKED_TEST_FORMAT_INFO ^ 0x0F, MASKED_TEST_FORMAT_INFO2 ^ 0x0F); + EXPECT_FALSE(expected == unexpected); + EXPECT_FALSE(unexpected.isValid() && unexpected.type() == Type::Model2); } TEST(QRFormatInformationTest, DecodeWithMisread) @@ -65,6 +80,7 @@ TEST(QRFormatInformationTest, DecodeMicro) DoFormatInformationTest(MICRO_MASKED_TEST_FORMAT_INFO, 0x3, ErrorCorrectionLevel::Quality); // where the code forgot the mask! +// static const int MICRO_UNMASKED_TEST_FORMAT_INFO = MICRO_MASKED_TEST_FORMAT_INFO ^ 0x4445; // DoFormatInformationTest(MICRO_UNMASKED_TEST_FORMAT_INFO, 0x3, ErrorCorrectionLevel::Quality); } @@ -87,3 +103,44 @@ TEST(QRFormatInformationTest, DecodeMicroWithBitDifference) // EXPECT_NE(expected.errorCorrectionLevel(), // FormatInformation::DecodeFormatInformation(MICRO_MASKED_TEST_FORMAT_INFO ^ 0x3f).errorCorrectionLevel()); } + +TEST(QRFormatInformationTest, DecodeRMQR) +{ + // Normal case + FormatInformation expected = FormatInformation::DecodeRMQR(RMQR_MASKED_TEST_FORMAT_INFO, RMQR_MASKED_TEST_FORMAT_INFO_SUB); + EXPECT_TRUE(expected.isValid()); + EXPECT_EQ(4, expected.dataMask); + EXPECT_EQ(ErrorCorrectionLevel::High, expected.ecLevel); + EXPECT_EQ(FORMAT_INFO_MASK_RMQR, expected.mask); + // Not catered for: where the code forgot the mask! +} + +TEST(QRFormatInformationTest, DecodeRMQRWithBitDifference) +{ + FormatInformation expected = FormatInformation::DecodeRMQR(RMQR_MASKED_TEST_FORMAT_INFO, RMQR_MASKED_TEST_FORMAT_INFO_SUB); + EXPECT_EQ(expected.ecLevel, ErrorCorrectionLevel::High); + // 1,2,3,4,5 bits difference + EXPECT_EQ(expected, FormatInformation::DecodeRMQR(RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO, 1), RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO_SUB, 1))); + EXPECT_EQ(expected, FormatInformation::DecodeRMQR(RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO, 2), RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO_SUB, 2))); + EXPECT_EQ(expected, FormatInformation::DecodeRMQR(RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO, 3), RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO_SUB, 3))); + EXPECT_EQ(expected, FormatInformation::DecodeRMQR(RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO, 4), RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO_SUB, 4))); + auto unexpected = FormatInformation::DecodeRMQR(RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO, 5), RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO_SUB, 5)); + EXPECT_FALSE(expected == unexpected); + EXPECT_FALSE(unexpected.isValid()); + EXPECT_TRUE(unexpected.type() == Type::rMQR); // Note `mask` (used to determine type) set regardless +} + +TEST(QRFormatInformationTest, DecodeRMQRWithMisread) +{ + FormatInformation expected = FormatInformation::DecodeRMQR(RMQR_MASKED_TEST_FORMAT_INFO, RMQR_MASKED_TEST_FORMAT_INFO_SUB); + { + auto actual = FormatInformation::DecodeRMQR(RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO, 2), RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO_SUB, 4)); + EXPECT_EQ(expected, actual); + EXPECT_EQ(actual.mask, FORMAT_INFO_MASK_RMQR); + } + { + auto actual = FormatInformation::DecodeRMQR(RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO, 5), RMQRUnsetBits(RMQR_MASKED_TEST_FORMAT_INFO_SUB, 4)); + EXPECT_EQ(expected, actual); + EXPECT_EQ(actual.mask, FORMAT_INFO_MASK_RMQR_SUB); + } +} diff --git a/test/unit/qrcode/QRModeTest.cpp b/test/unit/qrcode/QRModeTest.cpp index 28411f1cf5..9b83b15e68 100644 --- a/test/unit/qrcode/QRModeTest.cpp +++ b/test/unit/qrcode/QRModeTest.cpp @@ -16,53 +16,77 @@ using namespace ZXing::QRCode; TEST(QRModeTest, ForBits) { - ASSERT_EQ(CodecMode::TERMINATOR, CodecModeForBits(0x00)); - ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x01)); - ASSERT_EQ(CodecMode::ALPHANUMERIC, CodecModeForBits(0x02)); - ASSERT_EQ(CodecMode::BYTE, CodecModeForBits(0x04)); - ASSERT_EQ(CodecMode::KANJI, CodecModeForBits(0x08)); - ASSERT_THROW(CodecModeForBits(0x10), Error); + ASSERT_EQ(CodecMode::TERMINATOR, CodecModeForBits(0x00, Type::Model2)); + ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x01, Type::Model2)); + ASSERT_EQ(CodecMode::ALPHANUMERIC, CodecModeForBits(0x02, Type::Model2)); + ASSERT_EQ(CodecMode::BYTE, CodecModeForBits(0x04, Type::Model2)); + ASSERT_EQ(CodecMode::KANJI, CodecModeForBits(0x08, Type::Model2)); + ASSERT_THROW(CodecModeForBits(0x10, Type::Model2), Error); } TEST(QRModeTest, CharacterCount) { // Spot check a few values - ASSERT_EQ(10, CharacterCountBits(CodecMode::NUMERIC, *Version::FromNumber(5))); - ASSERT_EQ(12, CharacterCountBits(CodecMode::NUMERIC, *Version::FromNumber(26))); - ASSERT_EQ(14, CharacterCountBits(CodecMode::NUMERIC, *Version::FromNumber(40))); - ASSERT_EQ(9, CharacterCountBits(CodecMode::ALPHANUMERIC, *Version::FromNumber(6))); - ASSERT_EQ(8, CharacterCountBits(CodecMode::BYTE, *Version::FromNumber(7))); - ASSERT_EQ(8, CharacterCountBits(CodecMode::KANJI, *Version::FromNumber(8))); + ASSERT_EQ(10, CharacterCountBits(CodecMode::NUMERIC, *Version::Model2(5))); + ASSERT_EQ(12, CharacterCountBits(CodecMode::NUMERIC, *Version::Model2(26))); + ASSERT_EQ(14, CharacterCountBits(CodecMode::NUMERIC, *Version::Model2(40))); + ASSERT_EQ(9, CharacterCountBits(CodecMode::ALPHANUMERIC, *Version::Model2(6))); + ASSERT_EQ(8, CharacterCountBits(CodecMode::BYTE, *Version::Model2(7))); + ASSERT_EQ(8, CharacterCountBits(CodecMode::KANJI, *Version::Model2(8))); } TEST(QRModeTest, MicroForBits) { // M1 - ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x00, true)); + ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x00, Type::Micro)); // M2 - ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x00, true)); - ASSERT_EQ(CodecMode::ALPHANUMERIC, CodecModeForBits(0x01, true)); + ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x00, Type::Micro)); + ASSERT_EQ(CodecMode::ALPHANUMERIC, CodecModeForBits(0x01, Type::Micro)); // M3 - ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x00, true)); - ASSERT_EQ(CodecMode::ALPHANUMERIC, CodecModeForBits(0x01, true)); - ASSERT_EQ(CodecMode::BYTE, CodecModeForBits(0x02, true)); - ASSERT_EQ(CodecMode::KANJI, CodecModeForBits(0x03, true)); + ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x00, Type::Micro)); + ASSERT_EQ(CodecMode::ALPHANUMERIC, CodecModeForBits(0x01, Type::Micro)); + ASSERT_EQ(CodecMode::BYTE, CodecModeForBits(0x02, Type::Micro)); + ASSERT_EQ(CodecMode::KANJI, CodecModeForBits(0x03, Type::Micro)); // M4 - ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x00, true)); - ASSERT_EQ(CodecMode::ALPHANUMERIC, CodecModeForBits(0x01, true)); - ASSERT_EQ(CodecMode::BYTE, CodecModeForBits(0x02, true)); - ASSERT_EQ(CodecMode::KANJI, CodecModeForBits(0x03, true)); + ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x00, Type::Micro)); + ASSERT_EQ(CodecMode::ALPHANUMERIC, CodecModeForBits(0x01, Type::Micro)); + ASSERT_EQ(CodecMode::BYTE, CodecModeForBits(0x02, Type::Micro)); + ASSERT_EQ(CodecMode::KANJI, CodecModeForBits(0x03, Type::Micro)); - ASSERT_THROW(CodecModeForBits(0x04, true), Error); + ASSERT_THROW(CodecModeForBits(0x04, Type::Micro), Error); } TEST(QRModeTest, MicroCharacterCount) { // Spot check a few values - ASSERT_EQ(3, CharacterCountBits(CodecMode::NUMERIC, *Version::FromNumber(1, true))); - ASSERT_EQ(4, CharacterCountBits(CodecMode::NUMERIC, *Version::FromNumber(2, true))); - ASSERT_EQ(6, CharacterCountBits(CodecMode::NUMERIC, *Version::FromNumber(4, true))); - ASSERT_EQ(3, CharacterCountBits(CodecMode::ALPHANUMERIC, *Version::FromNumber(2, true))); - ASSERT_EQ(4, CharacterCountBits(CodecMode::BYTE, *Version::FromNumber(3, true))); - ASSERT_EQ(4, CharacterCountBits(CodecMode::KANJI, *Version::FromNumber(4, true))); + ASSERT_EQ(3, CharacterCountBits(CodecMode::NUMERIC, *Version::Micro(1))); + ASSERT_EQ(4, CharacterCountBits(CodecMode::NUMERIC, *Version::Micro(2))); + ASSERT_EQ(6, CharacterCountBits(CodecMode::NUMERIC, *Version::Micro(4))); + ASSERT_EQ(3, CharacterCountBits(CodecMode::ALPHANUMERIC, *Version::Micro(2))); + ASSERT_EQ(4, CharacterCountBits(CodecMode::BYTE, *Version::Micro(3))); + ASSERT_EQ(4, CharacterCountBits(CodecMode::KANJI, *Version::Micro(4))); +} + +TEST(QRModeTest, RMQRForBits) +{ + ASSERT_EQ(CodecMode::TERMINATOR, CodecModeForBits(0x00, Type::rMQR)); + ASSERT_EQ(CodecMode::NUMERIC, CodecModeForBits(0x01, Type::rMQR)); + ASSERT_EQ(CodecMode::ALPHANUMERIC, CodecModeForBits(0x02, Type::rMQR)); + ASSERT_EQ(CodecMode::BYTE, CodecModeForBits(0x03, Type::rMQR)); + ASSERT_EQ(CodecMode::KANJI, CodecModeForBits(0x04, Type::rMQR)); + ASSERT_EQ(CodecMode::FNC1_FIRST_POSITION, CodecModeForBits(0x05, Type::rMQR)); + ASSERT_EQ(CodecMode::FNC1_SECOND_POSITION, CodecModeForBits(0x06, Type::rMQR)); + ASSERT_EQ(CodecMode::ECI, CodecModeForBits(0x07, Type::rMQR)); + ASSERT_THROW(CodecModeForBits(0x08, Type::rMQR), Error); +} + +TEST(QRModeTest, RMQRCharacterCount) +{ + // Spot check a few values + ASSERT_EQ(7, CharacterCountBits(CodecMode::NUMERIC, *Version::rMQR(5))); + ASSERT_EQ(8, CharacterCountBits(CodecMode::NUMERIC, *Version::rMQR(26))); + ASSERT_EQ(9, CharacterCountBits(CodecMode::NUMERIC, *Version::rMQR(32))); + ASSERT_EQ(5, CharacterCountBits(CodecMode::ALPHANUMERIC, *Version::rMQR(6))); + ASSERT_EQ(5, CharacterCountBits(CodecMode::BYTE, *Version::rMQR(7))); + ASSERT_EQ(5, CharacterCountBits(CodecMode::KANJI, *Version::rMQR(8))); } diff --git a/test/unit/qrcode/QRVersionTest.cpp b/test/unit/qrcode/QRVersionTest.cpp index ff2bea8b4b..8b7851ec0d 100644 --- a/test/unit/qrcode/QRVersionTest.cpp +++ b/test/unit/qrcode/QRVersionTest.cpp @@ -7,6 +7,7 @@ #include "qrcode/QRVersion.h" #include "BitMatrix.h" +#include "BitMatrixIO.h" #include "gtest/gtest.h" @@ -18,7 +19,7 @@ namespace { void CheckVersion(const Version* version, int number, int dimension) { ASSERT_NE(version, nullptr); EXPECT_EQ(number, version->versionNumber()); - if (number > 1 && !version->isMicroQRCode()) { + if (number > 1 && version->isModel2()) { EXPECT_FALSE(version->alignmentPatternCenters().empty()); } EXPECT_EQ(dimension, version->dimension()); @@ -34,11 +35,11 @@ namespace { TEST(QRVersionTest, VersionForNumber) { - auto version = Version::FromNumber(0); + auto version = Version::Model2(0); EXPECT_EQ(version, nullptr) << "There is version with number 0"; for (int i = 1; i <= 40; i++) { - CheckVersion(Version::FromNumber(i), i, 4*i + 17); + CheckVersion(Version::Model2(i), i, 4*i + 17); } } @@ -46,9 +47,7 @@ TEST(QRVersionTest, VersionForNumber) TEST(QRVersionTest, GetProvisionalVersionForDimension) { for (int i = 1; i <= 40; i++) { - auto prov = Version::FromDimension(4 * i + 17); - ASSERT_NE(prov, nullptr); - EXPECT_EQ(i, prov->versionNumber()); + EXPECT_EQ(i, Version::Number(BitMatrix(4 * i + 17))); } } @@ -65,20 +64,18 @@ TEST(QRVersionTest, DecodeVersionInformation) TEST(QRVersionTest, MicroVersionForNumber) { - auto version = Version::FromNumber(0, true); + auto version = Version::Micro(0); EXPECT_EQ(version, nullptr) << "There is version with number 0"; for (int i = 1; i <= 4; i++) { - CheckVersion(Version::FromNumber(i, true), i, 2 * i + 9); + CheckVersion(Version::Micro(i), i, 2 * i + 9); } } TEST(QRVersionTest, GetProvisionalMicroVersionForDimension) { for (int i = 1; i <= 4; i++) { - auto prov = Version::FromDimension(2 * i + 9); - ASSERT_NE(prov, nullptr); - EXPECT_EQ(i, prov->versionNumber()); + EXPECT_EQ(i, Version::Number(BitMatrix(2 * i + 9))); } } @@ -90,7 +87,7 @@ TEST(QRVersionTest, FunctionPattern) EXPECT_TRUE(bitMatrix.get(col, row)); }; for (int i = 1; i <= 4; i++) { - const auto version = Version::FromNumber(i, true); + const auto version = Version::Micro(i); const auto functionPattern = version->buildFunctionPattern(); testFinderPatternRegion(functionPattern); @@ -102,3 +99,111 @@ TEST(QRVersionTest, FunctionPattern) EXPECT_TRUE(functionPattern.get(col, 0)); } } + +namespace { + + void CheckRMQRVersion(const Version* version, int number) { + ASSERT_NE(version, nullptr); + EXPECT_EQ(number, version->versionNumber()); + EXPECT_EQ(Version::SymbolSize(number, Type::rMQR).x == 27, version->alignmentPatternCenters().empty()); + } + +} + +TEST(QRVersionTest, RMQRVersionForNumber) +{ + auto version = Version::rMQR(0); + EXPECT_EQ(version, nullptr) << "There is version with number 0"; + + for (int i = 1; i <= 32; i++) { + CheckRMQRVersion(Version::rMQR(i), i); + } +} + +TEST(QRVersionTest, RMQRFunctionPattern) +{ + { + const auto expected = ParseBitMatrix( + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n" + "XXXXXXXXXXXX XXX XXXXXXXX\n" + "XXXXXXXXXXXX XXX XXXXXXXX\n" + "XXXXXXXXXXXX X XXXXXXXX\n" + "XXXXXXXXXXX XXX XXXXXXXX\n" + "XXXXXXXXXXX XXX XXXXXXXX\n" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n", + 'X', false); + const auto version = Version::rMQR(1); // R7x43 + const auto functionPattern = version->buildFunctionPattern(); + EXPECT_EQ(expected, functionPattern); + } + { + const auto expected = ParseBitMatrix( + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n" + "XXXXXXXXXXXX XXX XX\n" + "XXXXXXXXXXXX XXX X\n" + "XXXXXXXXXXXX X XXXXXX X\n" + "XXXXXXXXXXX X XXXXXXXX\n" + "XXXXXXXXXXX X XXXXXXXX\n" + "XXXXXXXX XXX XXXXXXXX\n" + "XXXXXXXX XXX XXXXXXXX\n" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n", + 'X', false); + const auto version = Version::rMQR(6); // R9x43 + const auto functionPattern = version->buildFunctionPattern(); + EXPECT_EQ(expected, functionPattern); + } + { + const auto expected = ParseBitMatrix( + "XXXXXXXXXXXXXXXXXXXXXXXXXXX\n" + "XXXXXXXXXXXX XX\n" + "XXXXXXXXXXXX X\n" + "XXXXXXXXXXXX X\n" + "XXXXXXXXXXX X\n" + "XXXXXXXXXXX XXXXXX X\n" + "XXXXXXXX XXXXXXXX\n" + "XXXXXXXX XXXXXXXX\n" + "X XXXXXXXX\n" + "XX XXXXXXXX\n" + "XXXXXXXXXXXXXXXXXXXXXXXXXXX\n", + 'X', false); + const auto version = Version::rMQR(11); // R11x27 + const auto functionPattern = version->buildFunctionPattern(); + EXPECT_EQ(expected, functionPattern); + } + { + const auto expected = ParseBitMatrix( + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n" + "XXXXXXXXXXXX XXX XX\n" + "XXXXXXXXXXXX XXX X\n" + "XXXXXXXXXXXX X X\n" + "XXXXXXXXXXX X X\n" + "XXXXXXXXXXX X XXXXXX X\n" + "XXXXXXXX X XXXXXXXX\n" + "XXXXXXXX X XXXXXXXX\n" + "X XXX XXXXXXXX\n" + "XX XXX XXXXXXXX\n" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n", + 'X', false); + const auto version = Version::rMQR(12); // R11x43 + const auto functionPattern = version->buildFunctionPattern(); + EXPECT_EQ(expected, functionPattern); + } + { + const auto expected = ParseBitMatrix( + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n" + "XXXXXXXXXXXX XXX XXX XX\n" + "XXXXXXXXXXXX XXX XXX X\n" + "XXXXXXXXXXXX X X X\n" + "XXXXXXXXXXX X X X\n" + "XXXXXXXXXXX X X XXXXXX X\n" + "XXXXXXXX X X XXXXXXXX\n" + "XXXXXXXX X X XXXXXXXX\n" + "X XXX XXX XXXXXXXX\n" + "XX XXX XXX XXXXXXXX\n" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n", + 'X', false); + const auto version = Version::rMQR(13); // R11x59 + const auto functionPattern = version->buildFunctionPattern(); + EXPECT_EQ(expected, functionPattern); + } +} diff --git a/test/unit/qrcode/RMQRDecoderTest.cpp b/test/unit/qrcode/RMQRDecoderTest.cpp new file mode 100644 index 0000000000..46c20664f0 --- /dev/null +++ b/test/unit/qrcode/RMQRDecoderTest.cpp @@ -0,0 +1,206 @@ +/* + * Copyright 2023 gitlost +*/ +// SPDX-License-Identifier: Apache-2.0 + +#include "qrcode/QRDecoder.h" + +#include "BitMatrix.h" +#include "BitMatrixIO.h" +#include "DecoderResult.h" +#include "ECI.h" + +#include "gtest/gtest.h" + +using namespace ZXing; +using namespace ZXing::QRCode; + +TEST(RMQRDecoderTest, RMQRCodeR7x43M) +{ + const auto bitMatrix = ParseBitMatrix( + "XXXXXXX X X X X X X XXX X X X X X X X X XXX\n" + "X X X XXX XXXXX XXX X X XX X X\n" + "X XXX X X XXX X X X XXXX XXXX X X XXXXXXXX\n" + "X XXX X XX XXXXX XXXXXX X X X X\n" + "X XXX X XX XXX XXXXXXX X X XX X X X\n" + "X X XXXXX XXX XXX XXXXX XXXXXX X X\n" + "XXXXXXX X X X X X X XXX X X X X X X X XXXXX\n", + 'X', false); + + const auto result = Decode(bitMatrix); + EXPECT_TRUE(result.isValid()); + EXPECT_EQ(result.content().text(TextMode::Plain), "ABCDEFG"); +} + +TEST(RMQRDecoderTest, RMQRCodeR7x43MError6Bits) +{ + const auto bitMatrix = ParseBitMatrix( + "XXXXXXX X X X X X X XXX X X X X X X X X XXX\n" + "X X X XXX XXXXX XXX X X XX X X\n" + "X XXX X X XXX X X XXXX XXXX XX X XXXXXXXX\n" // 2 + "X XXX X XX XXXXX X XXXXXX X X X X\n" // 3 + "X XXX X XX XXX XXXXXXX X X XXX X X X\n" // 5 + "X X XXXXX XXX XXX XXXX X XXXXXX X X\n" // 6 + "XXXXXXX X X X X X X XXX X X X X X X X XXXXX\n", + 'X', false); + + const auto result = Decode(bitMatrix); + EXPECT_EQ(Error::Checksum, result.error()); + EXPECT_TRUE(result.text().empty()); + EXPECT_TRUE(result.content().text(TextMode::Plain).empty()); +} + +TEST(RMQRDecoderTest, RMQRCodeR7x139H) +{ + const auto bitMatrix = ParseBitMatrix( + "XXXXXXX X X X X X X X X X XXX X X X X X X X X X X X X XXX X X X X X X X X X X X X XXX X X X X X X X X X X X X XXX X X X X X X X X X X X XXX\n" + "X X XX XXX X X X X X XX XX X X X XXX XX XXXX XXX XX XX XX X XX X X X XXX X XX XX XX X X XX X XX XXXX X X X\n" + "X XXX X X XXXXX X XXXXX X X XXX XX X XXX X XX XXX XX X XXX X X XXXX X XXXXXXX X XX XXX X X X XXX X XXXXX\n" + "X XXX X XXXX X XX X X XX XX X XX XX X XXX XX X XX X XX X X XX X X XXX X X X X X X X XX X XX XX X X X\n" + "X XXX X XXXX XXXXX X X XXXXXX XX X XXXX X XXXX X XXX XXXX X XXXXXXX XXX XXXXXX X X XX X XXX X XXXXXXXXX X XXXX X X X X X\n" + "X X X XX XX X X XX X X X XXXX X X X XX X XXX X X X X X XXX XX XXX X X XX XXXX XX X X X X XXXXX XXX XX X XX X\n" + "XXXXXXX X X X X X X X X X XXX X X X X X X X X X X X X XXX X X X X X X X X X X X X XXX X X X X X X X X X X X X XXX X X X X X X X X X X XXXXX\n", + 'X', false); + + const auto result = Decode(bitMatrix); + EXPECT_TRUE(result.isValid()); + EXPECT_EQ(result.content().text(TextMode::Plain), "1234567890,ABCDEFGHIJKLMOPQRSTUVW"); +} + +TEST(RMQRDecoderTest, RMQRCodeR9x59H) +{ + const auto bitMatrix = ParseBitMatrix( + "XXXXXXX X X X X X XXX X X X X X X X X XXX X X X X X X X XXX\n" + "X X X XXXXX XXX X X XXXXXXXX X X X X XXXX X X\n" + "X XXX X XX XXX X XXX XXXX X XXXXXXX X XXXXX X X\n" + "X XXX X XXXX X XX X XX XXXX XX XX X X X XXX X \n" + "X XXX X X X XX XXXXXX X X XX X XX X X XXXX XXXXX\n" + "X X X X X X XXX X X X XX X XXXX XX X X X X\n" + "XXXXXXX XXXXX XXXXXX X XX XXX X XXXX X X X XX X X\n" + " XXX XXXX XX XXX X XXXXXXX X XX XXX XX XX X\n" + "XXX X X X X X X X XXX X X X X X X X X XXX X X X X X X XXXXX\n", + 'X', false); + + const auto result = Decode(bitMatrix); + EXPECT_TRUE(result.isValid()); + EXPECT_EQ(result.content().text(TextMode::Plain), "ABCDEFGHIJKLMN"); +} + +TEST(RMQRDecoderTest, RMQRCodeR9x77M) +{ + const auto bitMatrix = ParseBitMatrix( + "XXXXXXX X X X X X X X X XXX X X X X X X X X X X X XXX X X X X X X X X X X XXX\n" + "X X XXX XX XXX XXX XXXX XXX XX X XXXXXXXXX X XXX XXXX X XXXX XX XXX X\n" + "X XXX X X X X XXX X XXXX XX XX X XX XX XXX XXXX X X XX X X XX X\n" + "X XXX X X X XXXXXX X XX XXXX X XXX X XX X XX XX XX X XXX X X XXX XX \n" + "X XXX X XXXX X X XXXX XXXX XX XXX X XX XXXXXX X X XXX XX XXXXX\n" + "X X X X XX XXX X X XX X X XX XXX X X X X X XX XXXXX X\n" + "XXXXXXX X XX XX X XXXX X X X X X XX XXX X XX X XXX XX X X\n" + " X XXXXX XX X XXXXXX XX XXXXX X XX XX XXXXX XXX X\n" + "XXX X X X X X X X X X X XXX X X X X X X X X X X X XXX X X X X X X X X X XXXXX\n", + 'X', false); + + const auto result = Decode(bitMatrix); + EXPECT_TRUE(result.isValid()); + EXPECT_EQ(result.content().text(TextMode::Plain), "__ABCDEFGH__1234567890___ABCDEFGHIJK"); +} + +TEST(RMQRDecoderTest, RMQRCodeR11x27H) +{ + const auto bitMatrix = ParseBitMatrix( + "XXXXXXX X X X X X X X X XXX\n" + "X X XX X X X X\n" + "X XXX X X XX X X XX\n" + "X XXX X XXXX XX X XXXXXX \n" + "X XXX X X X XX XX XXX X\n" + "X X XXX X XX XXXX X \n" + "XXXXXXX X XX X XXXXX\n" + " X X X X X\n" + "XXXX X X X XX XXXXXX X X\n" + "X XX XXXXXX XXX XXXX X X\n" + "XXX X X X X X X X X X XXXXX\n", + 'X', false); + + const auto result = Decode(bitMatrix); + EXPECT_TRUE(result.isValid()); + EXPECT_EQ(result.content().text(TextMode::Plain), "ABCDEF"); +} + +TEST(RMQRDecoderTest, RMQRCodeR13x27M_ECI) +{ + const auto bitMatrix = ParseBitMatrix( + "XXXXXXX X X X X X X X X XXX\n" + "X X XX XX XXX XX X\n" + "X XXX X XX X XX XX XXX X\n" + "X XXX X XX X XX X X XX \n" + "X XXX X XXXXXXX X X XX\n" + "X X XX X XXX XX XX \n" + "XXXXXXX X X X X XXX\n" + " XXX XX X XX XXX \n" + "XXX XX XX X X XX XX XXXXX\n" + " XXX X X X X X X\n" + "X XX X X XX X XX X X X X\n" + "X X X X X X X X X\n" + "XXX X X X X X X X X X XXXXX\n", + 'X', false); + + const auto result = Decode(bitMatrix); + EXPECT_TRUE(result.isValid()); + EXPECT_EQ(result.content().text(TextMode::Plain), "AB貫12345AB"); + EXPECT_TRUE(result.content().hasECI); + EXPECT_EQ(result.content().encodings[0].eci, ECI::Shift_JIS); + EXPECT_EQ(result.content().symbology.toString(), "]Q1"); +} + +TEST(RMQRDecoderTest, RMQRCodeR15x59H_GS1) +{ + const auto bitMatrix = ParseBitMatrix( + "XXXXXXX X X X X X XXX X X X X X X X X XXX X X X X X X X XXX\n" + "X X XXX XXX X XXXXX XX XXX X X X X X X XXX X\n" + "X XXX X XXX XX X XXX XXX X X XXX XXXXX XX XXX XX\n" + "X XXX X X X XX X X XXX X X X XXXXX XX XXX \n" + "X XXX X XX XXX XX X X X XX XX XX XXX XXXX X XXXX\n" + "X X X X X X X XXX XXX XXXX X XXX XX X X \n" + "XXXXXXX X XXX XXXX X XX XXXX X X XX XXX XXXXX X\n" + " X XXX X XXXXX X XX XXXX XX X \n" + "XX XX X X X XXXXX XX X X XX XX X XX X X XX X\n" + " XX XX X XXXXXX XXX XX X X XX XXX X X XXX \n" + "X X XX XXXXXXXXXX XX X X XX XX XX X XXXX XX XXXXXX\n" + " XX X XX X XXX X X X XXX X XXX X X XXX XXXX X\n" + "XXXX X X XX XXX X X X XX XXXXX XX X XX XXX X X\n" + "X X X XX XXX XXXXXXX XXX X XXX XX X X X XX X\n" + "XXX X X X X X X X XXX X X X X X X X X XXX X X X X X X XXXXX\n", + 'X', false); + + const auto result = Decode(bitMatrix); + EXPECT_TRUE(result.isValid()); + EXPECT_TRUE(result.content().type() == ContentType::GS1); + EXPECT_EQ(result.content().text(TextMode::HRI), "(01)09524000059109(21)12345678p901(10)1234567p(17)231120"); +} + +TEST(RMQRDecoderTest, RMQRCodeR17x99H) +{ + const auto bitMatrix = ParseBitMatrix( + "XXXXXXX X X X X X X X XXX X X X X X X X X X X X XXX X X X X X X X X X X X XXX X X X X X X X X X XXX\n" + "X X X XXXXX XXX X X X XX X X XX XXXXX X XX X XX XXX X X XX X X XXX X X XX X X X\n" + "X XXX X X X XXX XXX X XXX XXX X X XX XXXX X X X X XXX XXXXX X X XX X XX X X\n" + "X XXX X XX X XX X X XX X XXXX X XXXXX X X XX X XXX XX X X X X XXXXXX X \n" + "X XXX X X XX X X X X X X X X XXX XX XXXXXX X X XXX X XXXXXX X X X X X X X XX X\n" + "X X XX X X XXXXX XX X XXX X XX X X XXX X XXX XXX X XXXX XX X X X XX XXXX \n" + "XXXXXXX X XX X XX X X XXX XX X XXXX X X XXX X X XX X XXXX XX X X X XX X XXXX\n" + " XX XX XX XX X XX X X X XXX XX X X XXX XXXX XX X X X X XX XX XXX \n" + "XX X XXX X X XXXX XXX XXXXX XXX XXX X X X X X XXX X XX XX X X X X XX X XXX\n" + " X XXXXX X X XXXXX X XX X XX XXXX X X XXXXX X XX X XX X XX X XX XX \n" + "X XX XX X XX XXX XX XXXXXX X XXXXX XX XXXX X X X X XXXX XX X X XXXXXX XX X X\n" + " XXX XX XXX XX XX X X X XX X X X X XX XXX XXXX X XX XXX X X X XXXX XXXXX X XXX \n" + "X X XX X XX XX XX X X XX X X X XX XXXXXXXX X XX XX X X X X X XX X X XXXXXXXXXXX\n" + " X X X XX X X X XX XXXX X XXX X XX X X X X X XXX XXXXX XX X X X XXXXX X X X\n" + "XXXX XX XX X XXXX XXXX X XX X XX XX XX XXXX XXX X X XX XX X XXXX X XXX XX X XX X X\n" + "X XXX XX XXX X X X XXX X XXX X XXXX XX X X XXXXX X XX X X X X X X X X XXXX XXXX X\n" + "XXX X X X X X X X X X XXX X X X X X X X X X X X XXX X X X X X X X X X X X XXX X X X X X X X X XXXXX\n", + 'X', false); + + const auto result = Decode(bitMatrix); + EXPECT_TRUE(result.isValid()); + EXPECT_EQ(result.content().text(TextMode::Plain), "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890________________________"); +} diff --git a/wrappers/android/README.md b/wrappers/android/README.md index 448f53e871..c324580e1d 100644 --- a/wrappers/android/README.md +++ b/wrappers/android/README.md @@ -1,10 +1,32 @@ # ZXing-C++ Android Library -To use the Android (wrapper) library in other apps, it is easiest -to build the library project and include the resulting AAR (Android -Archive) file in your app. +## Install -## Build +The easiest way to use the library is to fetch if from _mavenCentral_. Simply add +```gradle +implementation("io.github.zxing-cpp:android:2.2.0") +``` +to your `build.gradle.kts` file in the `dependencies` section. + +## Use + +A trivial use case looks like this (in Kotlin): + +```kotlin +import zxingcpp.BarcodeReader + +var barcodeReader = BarcodeReader() + +fun process(image: ImageProxy) { + image.use { + barcodeReader.read(it) + }.joinToString("\n") { result -> + "${result.format} (${result.contentType}): ${result.text}" + } +} +``` + +## Build locally 1. Install AndroidStudio including NDK and CMake (see 'SDK Tools'). 2. Open the project in folder containing this README. @@ -16,3 +38,4 @@ To build the AAR (Android Archive) from the command line: Then copy `zxingcpp/build/outputs/aar/zxingcpp-release.aar` into `app/libs` of your app. + diff --git a/wrappers/android/app/.gitignore b/wrappers/android/app/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/wrappers/android/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/wrappers/android/app/build.gradle b/wrappers/android/app/build.gradle deleted file mode 100644 index 14437f1648..0000000000 --- a/wrappers/android/app/build.gradle +++ /dev/null @@ -1,59 +0,0 @@ -plugins { - id 'com.android.application' - id 'kotlin-android' -} - -android { - compileSdkVersion 33 - buildToolsVersion "32.0.0" - - defaultConfig { - applicationId 'com.example.zxingdemo' - minSdkVersion 26 - targetSdkVersion 33 - versionCode 1 - versionName "1.0" - } - - buildFeatures { - viewBinding true - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - lint { - disable 'UnsafeExperimentalUsageError' - } - namespace 'com.example.zxingcppdemo' -} - -dependencies { - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.8.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - - // CameraX - def camerax_version = "1.2.1" - implementation "androidx.camera:camera-core:${camerax_version}" - implementation "androidx.camera:camera-camera2:${camerax_version}" - implementation "androidx.camera:camera-lifecycle:${camerax_version}" - implementation 'androidx.camera:camera-view:1.0.0-alpha32' - - // Java 'upstream' version of zxing (to compare performance) - implementation 'com.google.zxing:core:3.5.0' - - implementation project(':zxingcpp') -} diff --git a/wrappers/android/app/build.gradle.kts b/wrappers/android/app/build.gradle.kts new file mode 100644 index 0000000000..496145bf74 --- /dev/null +++ b/wrappers/android/app/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "zxingcpp.app" + defaultConfig { + applicationId = "io.github.zxingcpp.app" + compileSdk = libs.versions.androidCompileSdk.get().toInt() + minSdk = 26 // for the adaptive icons. TODO: remove adaptive icons and lower to API 21 + targetSdk = libs.versions.androidTargetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + } + buildFeatures { + viewBinding = true + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + lint { + disable.add("UnsafeExperimentalUsageError") + } +} + +dependencies { + implementation(project(":zxingcpp")) + + implementation(libs.androidx.appcompat) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.core) + implementation(libs.androidx.camera.camera2) + implementation(libs.androidx.camera.lifecycle) + implementation(libs.androidx.camera.view) + implementation(libs.android.material) + + // Java "upstream" version of zxing (to compare performance) + implementation(libs.zxing.core) +} diff --git a/wrappers/android/app/proguard-rules.pro b/wrappers/android/app/proguard-rules.pro index 64b4a059a2..49dab8dee1 100644 --- a/wrappers/android/app/proguard-rules.pro +++ b/wrappers/android/app/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile diff --git a/wrappers/android/app/src/main/AndroidManifest.xml b/wrappers/android/app/src/main/AndroidManifest.xml index 4127b366bf..1317424c9f 100644 --- a/wrappers/android/app/src/main/AndroidManifest.xml +++ b/wrappers/android/app/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> @@ -25,4 +25,4 @@ - \ No newline at end of file + diff --git a/wrappers/android/app/src/main/java/com/example/zxingcppdemo/MainActivity.kt b/wrappers/android/app/src/main/java/zxingcpp/app/MainActivity.kt similarity index 70% rename from wrappers/android/app/src/main/java/com/example/zxingcppdemo/MainActivity.kt rename to wrappers/android/app/src/main/java/zxingcpp/app/MainActivity.kt index dd0184ad63..2184b1be0d 100644 --- a/wrappers/android/app/src/main/java/com/example/zxingcppdemo/MainActivity.kt +++ b/wrappers/android/app/src/main/java/zxingcpp/app/MainActivity.kt @@ -1,313 +1,352 @@ -/* -* Copyright 2021 Axel Waggershauser -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - -package com.example.zxingcppdemo - -import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import android.graphics.* -import android.hardware.camera2.CaptureRequest -import android.media.AudioManager -import android.media.MediaActionSound -import android.media.ToneGenerator -import android.os.Bundle -import android.os.Environment -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.camera.camera2.interop.Camera2CameraControl -import androidx.camera.camera2.interop.CaptureRequestOptions -import androidx.camera.core.* -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.core.graphics.toPointF -import androidx.lifecycle.LifecycleOwner -import com.example.zxingcppdemo.databinding.ActivityCameraBinding -import com.google.zxing.* -import com.google.zxing.common.HybridBinarizer -import com.zxingcpp.BarcodeReader -import com.zxingcpp.BarcodeReader.Format -import java.io.ByteArrayOutputStream -import java.io.File -import java.util.concurrent.Executors -import kotlin.random.Random - - -class MainActivity : AppCompatActivity() { - private lateinit var binding: ActivityCameraBinding - - private val executor = Executors.newSingleThreadExecutor() - private val permissions = listOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE) - private val permissionsRequestCode = Random.nextInt(0, 10000) - - private val beeper = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 50) - private var lastText = String() - private var doSaveImage: Boolean = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityCameraBinding.inflate(layoutInflater) - setContentView(binding.root) - - binding.capture.setOnClickListener { - // Disable all camera controls - it.isEnabled = false - doSaveImage = true - // Re-enable camera controls - it.isEnabled = true - } - } - - private fun ImageProxy.toJpeg(): ByteArray { - //This converts the ImageProxy (from the imageAnalysis Use Case) - //to a ByteArray (compressed as JPEG) for then to be saved for debugging purposes - //This is the closest representation of the image that is passed to the - //decoding algorithm. - - val yBuffer = planes[0].buffer // Y - val vuBuffer = planes[2].buffer // VU - - val ySize = yBuffer.remaining() - val vuSize = vuBuffer.remaining() - - val nv21 = ByteArray(ySize + vuSize) - - yBuffer.get(nv21, 0, ySize) - vuBuffer.get(nv21, ySize, vuSize) - - val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null) - val out = ByteArrayOutputStream() - yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 90, out) - return out.toByteArray() - } - - private fun saveImage(image: ImageProxy) { - try { - val currentMillis = System.currentTimeMillis().toString() - val filename = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) - .toString() + "/" + currentMillis + "_ZXingCpp.jpg" - - File(filename).outputStream().use { out -> - out.write(image.toJpeg()) - } - MediaActionSound().play(MediaActionSound.SHUTTER_CLICK) - } catch (e: Exception) { - beeper.startTone(ToneGenerator.TONE_CDMA_SOFT_ERROR_LITE) //Fail Tone - } - } - - private fun bindCameraUseCases() = binding.viewFinder.post { - - val cameraProviderFuture = ProcessCameraProvider.getInstance(this) - cameraProviderFuture.addListener({ - -// val size = Size(1600, 1200) - - // Set up the view finder use case to display camera preview - val preview = Preview.Builder() - .setTargetAspectRatio(AspectRatio.RATIO_16_9) -// .setTargetResolution(size) - .build() - - // Set up the image analysis use case which will process frames in real time - val imageAnalysis = ImageAnalysis.Builder() - .setTargetAspectRatio(AspectRatio.RATIO_16_9) // -> 1280x720 -// .setTargetResolution(size) - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() - - var frameCounter = 0 - var lastFpsTimestamp = System.currentTimeMillis() - var runtimes: Long = 0 - var runtime2: Long = 0 - val readerJava = MultiFormatReader() - val readerCpp = BarcodeReader() - - // Create a new camera selector each time, enforcing lens facing - val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() - - // Camera provider is now guaranteed to be available - val cameraProvider = cameraProviderFuture.get() - - // Apply declared configs to CameraX using the same lifecycle owner - cameraProvider.unbindAll() - val camera = cameraProvider.bindToLifecycle( - this as LifecycleOwner, cameraSelector, preview, imageAnalysis - ) - - // Reduce exposure time to decrease effect of motion blur - val camera2 = Camera2CameraControl.from(camera.cameraControl) - camera2.captureRequestOptions = CaptureRequestOptions.Builder() - .setCaptureRequestOption(CaptureRequest.SENSOR_SENSITIVITY, 1600) - .setCaptureRequestOption(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, -8) - .build() - - // Use the camera object to link our preview use case with the view - preview.setSurfaceProvider(binding.viewFinder.surfaceProvider) - - imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { image -> - // Early exit: image analysis is in paused state - if (binding.pause.isChecked) { - image.close() - return@Analyzer - } - - if (doSaveImage) { - doSaveImage = false - saveImage(image) - } - - val cropSize = image.height / 3 * 2 - val cropRect = if (binding.crop.isChecked) - Rect( - (image.width - cropSize) / 2, (image.height - cropSize) / 2, - (image.width - cropSize) / 2 + cropSize, (image.height - cropSize) / 2 + cropSize - ) - else - Rect(0, 0, image.width, image.height) - image.setCropRect(cropRect) - - val startTime = System.currentTimeMillis() - var resultText: String - var resultPoints: List? = null - - if (binding.java.isChecked) { - val yPlane = image.planes[0] - val yBuffer = yPlane.buffer - val yStride = yPlane.rowStride - val data = ByteArray(yBuffer.remaining()) - yBuffer.get(data, 0, data.size) - image.close() - val hints = mutableMapOf() - if (binding.qrcode.isChecked) - hints[DecodeHintType.POSSIBLE_FORMATS] = arrayListOf(BarcodeFormat.QR_CODE) - if (binding.tryHarder.isChecked) - hints[DecodeHintType.TRY_HARDER] = true - - resultText = try { - val bitmap = BinaryBitmap( - HybridBinarizer( - PlanarYUVLuminanceSource( - data, yStride, image.height, - cropRect.left, cropRect.top, cropRect.width(), cropRect.height(), - false - ) - ) - ) - val result = readerJava.decode(bitmap, hints) - result?.let { "${it.barcodeFormat}: ${it.text}" } ?: "" - } catch (e: Throwable) { - if (e.toString() != "com.google.zxing.NotFoundException") e.toString() else "" - } - } else { - readerCpp.options = BarcodeReader.Options( - formats = if (binding.qrcode.isChecked) setOf(Format.QR_CODE) else setOf(), - tryHarder = binding.tryHarder.isChecked, - tryRotate = binding.tryRotate.isChecked, - tryInvert = binding.tryInvert.isChecked, - tryDownscale = binding.tryDownscale.isChecked - ) - - resultText = try { - val result = readerCpp.read(image) - runtime2 += result?.time?.toInt() ?: 0 - resultPoints = result?.position?.let { - listOf( - it.topLeft, - it.topRight, - it.bottomRight, - it.bottomLeft - ).map { p -> - p.toPointF() - } - } - (result?.let { "${it.format} (${it.contentType}): " + - "${if (it.contentType != BarcodeReader.ContentType.BINARY) it.text else it.bytes!!.joinToString(separator = "") { v -> "%02x".format(v) }}" } - ?: "") - } catch (e: Throwable) { - e.message ?: "Error" - } - } - - runtimes += System.currentTimeMillis() - startTime - - var infoText: String? = null - if (++frameCounter == 15) { - val now = System.currentTimeMillis() - val fps = 1000 * frameCounter.toDouble() / (now - lastFpsTimestamp) - - infoText = "Time: %2d/%2d ms, FPS: %.02f, (%dx%d)" - .format(runtimes / frameCounter, runtime2 / frameCounter, fps, image.width, image.height) - lastFpsTimestamp = now - frameCounter = 0 - runtimes = 0 - runtime2 = 0 - } - - camera.cameraControl.enableTorch(binding.torch.isChecked) - - showResult(resultText, infoText, resultPoints, image) - }) - - }, ContextCompat.getMainExecutor(this)) - } - - private fun showResult(resultText: String, fpsText: String?, points: List?, image: ImageProxy) = - binding.viewFinder.post { - // Update the text and UI - binding.result.text = resultText - binding.result.visibility = View.VISIBLE - - binding.overlay.update(binding.viewFinder, image, points) - - if (fpsText != null) - binding.fps.text = fpsText - - if (resultText.isNotEmpty() && lastText != resultText) { - lastText = resultText - beeper.startTone(ToneGenerator.TONE_PROP_BEEP) - } - } - - override fun onResume() { - super.onResume() - - // Request permissions each time the app resumes, since they can be revoked at any time - if (!hasPermissions(this)) { - ActivityCompat.requestPermissions(this, permissions.toTypedArray(), permissionsRequestCode) - } else { - bindCameraUseCases() - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray, - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == permissionsRequestCode && hasPermissions(this)) { - bindCameraUseCases() - } else { - finish() // If we don't have the required permissions, we can't run - } - } - - private fun hasPermissions(context: Context) = permissions.all { - ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED - } -} \ No newline at end of file +/* +* Copyright 2021 Axel Waggershauser +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package zxingcpp.app + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.ImageFormat +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.YuvImage +import android.hardware.camera2.CaptureRequest +import android.media.AudioManager +import android.media.MediaActionSound +import android.media.ToneGenerator +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.camera.camera2.interop.Camera2CameraControl +import androidx.camera.camera2.interop.CaptureRequestOptions +import androidx.camera.core.AspectRatio +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.graphics.toPointF +import androidx.lifecycle.LifecycleOwner +import com.google.zxing.BarcodeFormat +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.MultiFormatReader +import com.google.zxing.PlanarYUVLuminanceSource +import com.google.zxing.common.HybridBinarizer +import java.io.ByteArrayOutputStream +import java.io.File +import java.util.concurrent.Executors +import zxingcpp.app.databinding.ActivityCameraBinding +import zxingcpp.BarcodeReader +import zxingcpp.BarcodeReader.Format.* + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityCameraBinding + + private val executor = Executors.newSingleThreadExecutor() + private val permissions = mutableListOf(Manifest.permission.CAMERA) + private val permissionsRequestCode = 1 + private val beeper = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 50) + + private var lastText = String() + private var doSaveImage: Boolean = false + + init { + // On R or higher, this permission has no effect. See: + // https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityCameraBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.capture.setOnClickListener { + // Disable all camera controls + it.isEnabled = false + doSaveImage = true + // Re-enable camera controls + it.isEnabled = true + } + } + + private fun ImageProxy.toJpeg(): ByteArray { + // This converts the ImageProxy (from the imageAnalysis Use Case) + // to a ByteArray (compressed as JPEG) for then to be saved for debugging purposes + // This is the closest representation of the image that is passed to the + // decoding algorithm. + + val yBuffer = planes[0].buffer // Y + val vuBuffer = planes[2].buffer // VU + + val ySize = yBuffer.remaining() + val vuSize = vuBuffer.remaining() + + val nv21 = ByteArray(ySize + vuSize) + + yBuffer.get(nv21, 0, ySize) + vuBuffer.get(nv21, ySize, vuSize) + + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null) + val out = ByteArrayOutputStream() + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 90, out) + return out.toByteArray() + } + + private fun saveImage(image: ImageProxy) { + try { + val currentMillis = System.currentTimeMillis().toString() + val filename = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + .toString() + "/" + currentMillis + "_ZXingCpp.jpg" + + File(filename).outputStream().use { out -> + out.write(image.toJpeg()) + } + MediaActionSound().play(MediaActionSound.SHUTTER_CLICK) + } catch (e: Exception) { + beeper.startTone(ToneGenerator.TONE_CDMA_SOFT_ERROR_LITE) //Fail Tone + } + } + + private fun bindCameraUseCases() = binding.viewFinder.post { + + val cameraProviderFuture = ProcessCameraProvider.getInstance(this) + cameraProviderFuture.addListener({ + + // Set up the view finder use case to display camera preview + val preview = Preview.Builder() + .setTargetAspectRatio(AspectRatio.RATIO_16_9) + .build() + + // Set up the image analysis use case which will process frames in real time + val imageAnalysis = ImageAnalysis.Builder() + .setTargetAspectRatio(AspectRatio.RATIO_16_9) // -> 1280x720 + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + + var frameCounter = 0 + var lastFpsTimestamp = System.currentTimeMillis() + var runtimes: Long = 0 + var runtime2: Long = 0 + val readerJava = MultiFormatReader() + val readerCpp = BarcodeReader() + + + // Create a new camera selector each time, enforcing lens facing + val cameraSelector = + CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() + + // Camera provider is now guaranteed to be available + val cameraProvider = cameraProviderFuture.get() + + // Apply declared configs to CameraX using the same lifecycle owner + cameraProvider.unbindAll() + val camera = cameraProvider.bindToLifecycle( + this as LifecycleOwner, cameraSelector, preview, imageAnalysis + ) + + // Reduce exposure time to decrease effect of motion blur + val camera2 = Camera2CameraControl.from(camera.cameraControl) + camera2.captureRequestOptions = CaptureRequestOptions.Builder() + .setCaptureRequestOption(CaptureRequest.SENSOR_SENSITIVITY, 1600) + .setCaptureRequestOption(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, -8) + .build() + + // Use the camera object to link our preview use case with the view + preview.setSurfaceProvider(binding.viewFinder.surfaceProvider) + + imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { image -> + // Early exit: image analysis is in paused state + if (binding.pause.isChecked) { + image.close() + return@Analyzer + } + + if (doSaveImage) { + doSaveImage = false + saveImage(image) + } + + val cropSize = image.height / 3 * 2 + val cropRect = if (binding.crop.isChecked) + Rect( + (image.width - cropSize) / 2, + (image.height - cropSize) / 2, + (image.width - cropSize) / 2 + cropSize, + (image.height - cropSize) / 2 + cropSize + ) + else + Rect(0, 0, image.width, image.height) + image.setCropRect(cropRect) + + val startTime = System.currentTimeMillis() + var resultText: String + val resultPoints = mutableListOf>() + + if (binding.java.isChecked) { + val yPlane = image.planes[0] + val yBuffer = yPlane.buffer + val yStride = yPlane.rowStride + val data = ByteArray(yBuffer.remaining()) + yBuffer.get(data, 0, data.size) + image.close() + val hints = mutableMapOf() + if (binding.qrcode.isChecked) + hints[DecodeHintType.POSSIBLE_FORMATS] = arrayListOf(BarcodeFormat.QR_CODE) + if (binding.tryHarder.isChecked) + hints[DecodeHintType.TRY_HARDER] = true + + resultText = try { + val bitmap = BinaryBitmap( + HybridBinarizer( + PlanarYUVLuminanceSource( + data, + yStride, + image.height, + cropRect.left, + cropRect.top, + cropRect.width(), + cropRect.height(), + false + ) + ) + ) + val result = readerJava.decode(bitmap, hints) + result?.let { "${it.barcodeFormat}: ${it.text}" } ?: "" + } catch (e: Throwable) { + if (e.toString() != "com.google.zxing.NotFoundException") e.toString() else "" + } + } else { + readerCpp.options.apply { + formats = if (binding.qrcode.isChecked) setOf(QR_CODE, MICRO_QR_CODE, RMQR_CODE) else setOf() + tryHarder = binding.tryHarder.isChecked + tryRotate = binding.tryRotate.isChecked + tryInvert = binding.tryInvert.isChecked + tryDownscale = binding.tryDownscale.isChecked + maxNumberOfSymbols = if (binding.multiSymbol.isChecked) 255 else 1 + } + + resultText = try { + image.use { + readerCpp.read(it) + }.joinToString("\n") { result -> + result.position.let { + resultPoints.add(listOf( + it.topLeft, it.topRight, it.bottomRight, it.bottomLeft + ).map { p -> + p.toPointF() + }) + } + "${result.format} (${result.contentType}): ${ + if (result.contentType != BarcodeReader.ContentType.BINARY) { + result.text + } else { + result.bytes!!.joinToString(separator = "") { v -> "%02x".format(v) } + } + }" + } + } catch (e: Throwable) { + e.message ?: "Error" + } + } + + runtimes += System.currentTimeMillis() - startTime + runtime2 += readerCpp.lastReadTime + + var infoText: String? = null + if (++frameCounter == 15) { + val now = System.currentTimeMillis() + val fps = 1000 * frameCounter.toDouble() / (now - lastFpsTimestamp) + + infoText = "Time: %2d/%2d ms, FPS: %.02f, (%dx%d)".format( + runtimes / frameCounter, runtime2 / frameCounter, fps, image.width, image.height + ) + lastFpsTimestamp = now + frameCounter = 0 + runtimes = 0 + runtime2 = 0 + } + + camera.cameraControl.enableTorch(binding.torch.isChecked) + + showResult(resultText, infoText, resultPoints, image) + }) + + }, ContextCompat.getMainExecutor(this)) + } + + private fun showResult( + resultText: String, + fpsText: String?, + points: List>, + image: ImageProxy + ) = + binding.viewFinder.post { + // Update the text and UI + binding.result.text = resultText + binding.result.visibility = View.VISIBLE + + binding.overlay.update(binding.viewFinder, image, points) + + if (fpsText != null) + binding.fps.text = fpsText + + if (resultText.isNotEmpty() && lastText != resultText) { + lastText = resultText + beeper.startTone(ToneGenerator.TONE_PROP_BEEP) + } + } + + override fun onResume() { + super.onResume() + + // Request permissions each time the app resumes, since they can be revoked at any time + if (!hasPermissions(this)) { + ActivityCompat.requestPermissions( + this, + permissions.toTypedArray(), + permissionsRequestCode + ) + } else { + bindCameraUseCases() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == permissionsRequestCode && hasPermissions(this)) { + bindCameraUseCases() + } else { + finish() // If we don't have the required permissions, we can't run + } + } + + private fun hasPermissions(context: Context) = permissions.all { + ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED + } +} diff --git a/wrappers/android/app/src/main/java/com/example/zxingcppdemo/PreviewOverlay.kt b/wrappers/android/app/src/main/java/zxingcpp/app/PreviewOverlay.kt similarity index 82% rename from wrappers/android/app/src/main/java/com/example/zxingcppdemo/PreviewOverlay.kt rename to wrappers/android/app/src/main/java/zxingcpp/app/PreviewOverlay.kt index 7c2db15c12..8a79cb117f 100644 --- a/wrappers/android/app/src/main/java/com/example/zxingcppdemo/PreviewOverlay.kt +++ b/wrappers/android/app/src/main/java/zxingcpp/app/PreviewOverlay.kt @@ -1,86 +1,94 @@ -/* -* Copyright 2022 Axel Waggershauser -* Copyright 2022 Markus Fisch -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - -package com.example.zxingcppdemo - -import android.content.Context -import android.graphics.* -import android.util.AttributeSet -import android.view.View -import androidx.camera.core.ImageProxy -import kotlin.math.max -import kotlin.math.min - -class PreviewOverlay constructor(context: Context, attributeSet: AttributeSet?) : - View(context, attributeSet) { - - private val paintPath = Paint(Paint.ANTI_ALIAS_FLAG).apply { - style = Paint.Style.STROKE - color = 0xff00ff00.toInt() - strokeWidth = 2 * context.resources.displayMetrics.density - } - private val paintRect = Paint().apply { - style = Paint.Style.STROKE - color = 0x80ffffff.toInt() - strokeWidth = 3 * context.resources.displayMetrics.density - } - private val path = Path() - private var cropRect = Rect() - private var rotation = 0 - private var s: Float = 0f - private var o: Float = 0f - - fun update(viewFinder: View, image: ImageProxy, points: List?) { - cropRect = image.cropRect - rotation = image.imageInfo.rotationDegrees - s = min(viewFinder.width, viewFinder.height).toFloat() / image.height - o = (max(viewFinder.width, viewFinder.height) - (image.width * s).toInt()).toFloat() / 2 - - path.apply { - rewind() - if (!points.isNullOrEmpty()) { - moveTo(points.last().x, points.last().y) - for (p in points) - lineTo(p.x, p.y) - } - } - invalidate() - } - - override fun onDraw(canvas: Canvas) { - canvas.apply { - // draw the cropRect, which is relative to the original image orientation - save() - if (rotation == 90) { - translate(width.toFloat(), 0f) - rotate(rotation.toFloat()) - } - translate(o, 0f) - scale(s, s) - drawRect(cropRect, paintRect) - restore() - - // draw the path, which is relative to the (centered) rotated cropRect - when (rotation) { - 0, 180 -> translate(o + cropRect.left * s, cropRect.top * s) - 90 -> translate(cropRect.top * s, o + cropRect.left * s) - } - scale(s, s) - drawPath(path, paintPath) - } - } -} +/* +* Copyright 2022 Axel Waggershauser +* Copyright 2022 Markus Fisch +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.zxingcppdemo + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PointF +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import androidx.camera.core.ImageProxy +import kotlin.math.max +import kotlin.math.min + +class PreviewOverlay constructor(context: Context, attributeSet: AttributeSet?) : + View(context, attributeSet) { + + private val paintPath = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + color = 0xff00ff00.toInt() + strokeWidth = 2 * context.resources.displayMetrics.density + } + private val paintRect = Paint().apply { + style = Paint.Style.STROKE + color = 0x80ffffff.toInt() + strokeWidth = 3 * context.resources.displayMetrics.density + } + private val path = Path() + private var cropRect = Rect() + private var rotation = 0 + private var s: Float = 0f + private var o: Float = 0f + + fun update(viewFinder: View, image: ImageProxy, points: List>) { + cropRect = image.cropRect + rotation = image.imageInfo.rotationDegrees + s = min(viewFinder.width, viewFinder.height).toFloat() / image.height + o = (max(viewFinder.width, viewFinder.height) - (image.width * s).toInt()).toFloat() / 2 + + path.apply { + rewind() + points.forEach { + if (!it.isEmpty()) { + val last = it.last() + moveTo(last.x, last.y) + for (p in it) { + lineTo(p.x, p.y) + } + } + } + } + invalidate() + } + + override fun onDraw(canvas: Canvas) { + canvas.apply { + // draw the cropRect, which is relative to the original image orientation + save() + if (rotation == 90) { + translate(width.toFloat(), 0f) + rotate(rotation.toFloat()) + } + translate(o, 0f) + scale(s, s) + drawRect(cropRect, paintRect) + restore() + + // draw the path, which is relative to the (centered) rotated cropRect + when (rotation) { + 0, 180 -> translate(o + cropRect.left * s, cropRect.top * s) + 90 -> translate(cropRect.top * s, o + cropRect.left * s) + } + scale(s, s) + drawPath(path, paintPath) + } + } +} diff --git a/wrappers/android/app/src/main/res/layout-land/activity_camera.xml b/wrappers/android/app/src/main/res/layout-land/activity_camera.xml index cf3dca0bde..50f1cb7c31 100644 --- a/wrappers/android/app/src/main/res/layout-land/activity_camera.xml +++ b/wrappers/android/app/src/main/res/layout-land/activity_camera.xml @@ -99,6 +99,11 @@ style="@style/Chip" android:text="tryDownscale" /> + + - \ No newline at end of file + diff --git a/wrappers/android/app/src/main/res/layout/activity_camera.xml b/wrappers/android/app/src/main/res/layout/activity_camera.xml index 4b5d3f4eb4..41cb9b8fd1 100644 --- a/wrappers/android/app/src/main/res/layout/activity_camera.xml +++ b/wrappers/android/app/src/main/res/layout/activity_camera.xml @@ -102,6 +102,11 @@ style="@style/ChipR" android:text="tryDownscale" /> + + - \ No newline at end of file + diff --git a/wrappers/android/app/src/main/res/values/values.xml b/wrappers/android/app/src/main/res/values/values.xml index 027c699408..8d7301984f 100644 --- a/wrappers/android/app/src/main/res/values/values.xml +++ b/wrappers/android/app/src/main/res/values/values.xml @@ -22,7 +22,7 @@ - ZXingCpp-Demo + zxing-cpp Capture UNKNOWN diff --git a/wrappers/android/build.gradle b/wrappers/android/build.gradle deleted file mode 100644 index b46a4947a6..0000000000 --- a/wrappers/android/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - ext.kotlin_version = "1.7.20" - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} \ No newline at end of file diff --git a/wrappers/android/build.gradle.kts b/wrappers/android/build.gradle.kts new file mode 100644 index 0000000000..140c5d81f9 --- /dev/null +++ b/wrappers/android/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + base + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.android) apply false +} + +tasks.named("clean") { + val buildDirs = allprojects.map { it.layout.buildDirectory.asFile } + delete(buildDirs) +} diff --git a/wrappers/android/gradle.properties b/wrappers/android/gradle.properties index 95845120f6..cb35e9e670 100644 --- a/wrappers/android/gradle.properties +++ b/wrappers/android/gradle.properties @@ -4,19 +4,20 @@ # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html + # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=false + # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -# org.gradle.warning.mode=all \ No newline at end of file diff --git a/wrappers/android/gradle/libs.versions.toml b/wrappers/android/gradle/libs.versions.toml new file mode 100644 index 0000000000..2f72e440dc --- /dev/null +++ b/wrappers/android/gradle/libs.versions.toml @@ -0,0 +1,32 @@ +[versions] +androidCoreDesugaring = "2.0.3" +androidGradlePlugin = "8.1.4" +androidCompileSdk = "34" +androidMinSdk = "21" +androidTargetSdk = "33" +androidx-activity = "1.8.1" +androidx-appcompat = "1.6.1" +androidx-camera = "1.3.0" +androidx-core = "1.12.0" +androidx-constraintLayout = "2.1.4" +android-material = "1.11.0-rc01" +kotlin = "1.9.10" +zxing-core = "3.5.2" + +[libraries] +androidx-activityktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" } +androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" } +androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" } +androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintLayout" } +androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +android-material = { module = "com.google.android.material:material", version.ref = "android-material" } +zxing-core = { module = "com.google.zxing:core", version.ref = "zxing-core" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + diff --git a/wrappers/android/gradle/wrapper/gradle-wrapper.jar b/wrappers/android/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a..7f93135c49 100644 Binary files a/wrappers/android/gradle/wrapper/gradle-wrapper.jar and b/wrappers/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/wrappers/android/gradle/wrapper/gradle-wrapper.properties b/wrappers/android/gradle/wrapper/gradle-wrapper.properties index cfb22fcb3f..ac72c34e8a 100644 --- a/wrappers/android/gradle/wrapper/gradle-wrapper.properties +++ b/wrappers/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Tue Apr 13 21:15:17 CEST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip diff --git a/wrappers/android/gradlew b/wrappers/android/gradlew index cccdd3d517..0adc8e1a53 100755 --- a/wrappers/android/gradlew +++ b/wrappers/android/gradlew @@ -1,78 +1,127 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/wrappers/android/gradlew.bat b/wrappers/android/gradlew.bat index e95643d6a2..6689b85bee 100644 --- a/wrappers/android/gradlew.bat +++ b/wrappers/android/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,19 +25,23 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,38 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/wrappers/android/settings.gradle b/wrappers/android/settings.gradle deleted file mode 100644 index c068e8ec94..0000000000 --- a/wrappers/android/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -include ':zxingcpp' -include ':app' -rootProject.name = "ZXingCpp" diff --git a/wrappers/android/settings.gradle.kts b/wrappers/android/settings.gradle.kts new file mode 100644 index 0000000000..7cd059c9b2 --- /dev/null +++ b/wrappers/android/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + google() + } +} + +include(":app") +include(":zxingcpp") + +rootProject.name = "zxing-cpp" diff --git a/wrappers/android/zxingcpp/.gitignore b/wrappers/android/zxingcpp/.gitignore deleted file mode 100644 index 796b96d1c4..0000000000 --- a/wrappers/android/zxingcpp/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/wrappers/android/zxingcpp/build.gradle b/wrappers/android/zxingcpp/build.gradle deleted file mode 100644 index 2b3a4ed505..0000000000 --- a/wrappers/android/zxingcpp/build.gradle +++ /dev/null @@ -1,53 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' -} - -android { - compileSdkVersion 33 - buildToolsVersion "32.0.0" - // ndk version 25 is known to support c++20 (see #386) - // ndkVersion '25.1.8937393' - - defaultConfig { - minSdkVersion 21 - targetSdkVersion 33 - - ndk { - // speed up build: compile only arm versions - abiFilters 'armeabi-v7a', 'arm64-v8a' - } - externalNativeBuild { - cmake { - arguments "-DCMAKE_BUILD_TYPE=RelWithDebInfo" - } - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - externalNativeBuild { - cmake { - path file('src/main/cpp/CMakeLists.txt') - } - } - lint { - disable 'UnsafeExperimentalUsageError' - } - namespace 'com.zxingcpp' -} - -dependencies { - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.core:core-ktx:1.9.0' - - def camerax_version = "1.2.1" - implementation "androidx.camera:camera-core:${camerax_version}" - -} diff --git a/wrappers/android/zxingcpp/build.gradle.kts b/wrappers/android/zxingcpp/build.gradle.kts new file mode 100644 index 0000000000..7f1e43c99c --- /dev/null +++ b/wrappers/android/zxingcpp/build.gradle.kts @@ -0,0 +1,127 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + id("maven-publish") + id("signing") +} + +android { + namespace = "zxingcpp" + // ndk version 25 is known to support c++20 (see #386) + // ndkVersion = "25.1.8937393" + + defaultConfig { + compileSdk = libs.versions.androidCompileSdk.get().toInt() + minSdk = libs.versions.androidMinSdk.get().toInt() + + ndk { + // speed up build: compile only arm versions + + //noinspection ChromeOsAbiSupport + abiFilters += "armeabi-v7a" + + //noinspection ChromeOsAbiSupport + abiFilters += "arm64-v8a" + } + externalNativeBuild { + cmake { + arguments("-DCMAKE_BUILD_TYPE=RelWithDebInfo") + } + } + + consumerProguardFiles("consumer-rules.pro") + } + compileOptions { + sourceCompatibility(JavaVersion.VERSION_1_8) + targetCompatibility(JavaVersion.VERSION_1_8) + } + kotlinOptions { + jvmTarget = "1.8" + } + externalNativeBuild { + cmake { + path(file("src/main/cpp/CMakeLists.txt")) + } + } + lint { + disable.add("UnsafeExperimentalUsageError") + } +} + +kotlin { + explicitApi() +} + +dependencies { + implementation(libs.androidx.camera.core) +} + +group = "io.github.zxing-cpp" +version = "2.2.0" + +val javadocJar by tasks.registering(Jar::class) { + archiveClassifier.set("javadoc") +} + +publishing { + publications { + register("release") { + artifactId = "android" + groupId = project.group.toString() + version = project.version.toString() + + afterEvaluate { + from(components["release"]) + } + + artifact(javadocJar.get()) + + pom { + name = "zxing-cpp" + description = "Wrapper for zxing-cpp barcode image processing library" + url = "https://github.com/zxing-cpp/zxing-cpp" + licenses { + license { + name = "The Apache License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + id = "zxing-cpp" + name = "zxing-cpp community" + email = "zxingcpp@gmail.com" + } + } + scm { + connection = "scm:git:git://github.com/zxing-cpp/zxing-cpp.git" + developerConnection = "scm:git:git://github.com/zxing-cpp/zxing-cpp.git" + url = "https://github.com/zxing-cpp/zxing-cpp" + } + } + } + } + repositories { + maven { + name = "sonatype" + + val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" + setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) + + credentials { + val ossrhUsername: String? by project + val ossrhPassword: String? by project + username = ossrhUsername + password = ossrhPassword + } + } + } +} + +signing { + val signingKey: String? by project + val signingPassword: String? by project + useInMemoryPgpKeys(signingKey, signingPassword) + sign(publishing.publications) +} diff --git a/wrappers/android/zxingcpp/consumer-rules.pro b/wrappers/android/zxingcpp/consumer-rules.pro new file mode 100644 index 0000000000..0f792e00c7 --- /dev/null +++ b/wrappers/android/zxingcpp/consumer-rules.pro @@ -0,0 +1 @@ +-keep class zxingcpp.** { *; } diff --git a/wrappers/android/zxingcpp/src/main/AndroidManifest.xml b/wrappers/android/zxingcpp/src/main/AndroidManifest.xml index 8bdb7e14b3..8072ee00db 100644 --- a/wrappers/android/zxingcpp/src/main/AndroidManifest.xml +++ b/wrappers/android/zxingcpp/src/main/AndroidManifest.xml @@ -1,4 +1,2 @@ - - - + diff --git a/wrappers/android/zxingcpp/src/main/cpp/BarcodeReader.cpp b/wrappers/android/zxingcpp/src/main/cpp/BarcodeReader.cpp deleted file mode 100644 index c7b116dea3..0000000000 --- a/wrappers/android/zxingcpp/src/main/cpp/BarcodeReader.cpp +++ /dev/null @@ -1,218 +0,0 @@ -/* -* Copyright 2021 Axel Waggershauser -*/ -// SPDX-License-Identifier: Apache-2.0 - -#include "JNIUtils.h" -#include "ReadBarcode.h" - -#include -#include -#include -#include - -using namespace ZXing; - -static const char* JavaBarcodeFormatName(BarcodeFormat format) -{ - // These have to be the names of the enum constants in the kotlin code. - switch (format) { - case BarcodeFormat::None: return "NONE"; - case BarcodeFormat::Aztec: return "AZTEC"; - case BarcodeFormat::Codabar: return "CODABAR"; - case BarcodeFormat::Code39: return "CODE_39"; - case BarcodeFormat::Code93: return "CODE_93"; - case BarcodeFormat::Code128: return "CODE_128"; - case BarcodeFormat::DataMatrix: return "DATA_MATRIX"; - case BarcodeFormat::EAN8: return "EAN_8"; - case BarcodeFormat::EAN13: return "EAN_13"; - case BarcodeFormat::ITF: return "ITF"; - case BarcodeFormat::MaxiCode: return "MAXICODE"; - case BarcodeFormat::PDF417: return "PDF_417"; - case BarcodeFormat::QRCode: return "QR_CODE"; - case BarcodeFormat::MicroQRCode: return "MICRO_QR_CODE"; - case BarcodeFormat::DataBar: return "DATA_BAR"; - case BarcodeFormat::DataBarExpanded: return "DATA_BAR_EXPANDED"; - case BarcodeFormat::UPCA: return "UPC_A"; - case BarcodeFormat::UPCE: return "UPC_E"; - default: throw std::invalid_argument("Invalid format"); - } -} - -static const char* JavaContentTypeName(ContentType contentType) -{ - // These have to be the names of the enum constants in the kotlin code. - switch (contentType) { - case ContentType::Text: return "TEXT"; - case ContentType::Binary: return "BINARY"; - case ContentType::Mixed: return "MIXED"; - case ContentType::GS1: return "GS1"; - case ContentType::ISO15434: return "ISO15434"; - case ContentType::UnknownECI: return "UNKNOWN_ECI"; - default: throw std::invalid_argument("Invalid contentType"); - } -} - -static jstring ThrowJavaException(JNIEnv* env, const char* message) -{ - // if (env->ExceptionCheck()) - // return 0; - jclass jcls = env->FindClass("java/lang/RuntimeException"); - env->ThrowNew(jcls, message); - return nullptr; -} - -static jobject CreateContentType(JNIEnv* env, ContentType contentType) -{ - jclass cls = env->FindClass("com/zxingcpp/BarcodeReader$ContentType"); - jfieldID fidCT = env->GetStaticFieldID(cls , JavaContentTypeName(contentType), "Lcom/zxingcpp/BarcodeReader$ContentType;"); - return env->GetStaticObjectField(cls, fidCT); -} - -static jobject CreateAndroidPoint(JNIEnv* env, const PointT& point) -{ - jclass cls = env->FindClass("android/graphics/Point"); - auto constructor = env->GetMethodID(cls, "", "(II)V"); - return env->NewObject(cls, constructor, point.x, point.y); -} - -static jobject CreatePosition(JNIEnv* env, const Position& position) -{ - jclass cls = env->FindClass("com/zxingcpp/BarcodeReader$Position"); - auto constructor = env->GetMethodID( - cls, "", - "(Landroid/graphics/Point;" - "Landroid/graphics/Point;" - "Landroid/graphics/Point;" - "Landroid/graphics/Point;" - "D)V"); - return env->NewObject( - cls, constructor, - CreateAndroidPoint(env, position.topLeft()), - CreateAndroidPoint(env, position.topRight()), - CreateAndroidPoint(env, position.bottomLeft()), - CreateAndroidPoint(env, position.bottomRight()), - position.orientation()); -} - -jstring Read(JNIEnv *env, ImageView image, jstring formats, jboolean tryHarder, jboolean tryRotate, - jboolean tryInvert, jboolean tryDownscale, jobject result) -{ - try { - auto hints = DecodeHints() - .setFormats(BarcodeFormatsFromString(J2CString(env, formats))) - .setTryHarder(tryHarder) - .setTryRotate(tryRotate) - .setTryInvert(tryInvert) - .setTryDownscale(tryDownscale) - .setMaxNumberOfSymbols(1); - - auto startTime = std::chrono::high_resolution_clock::now(); - auto results = ReadBarcodes(image, hints); - auto duration = std::chrono::high_resolution_clock::now() - startTime; -// LOGD("time: %4d ms\n", (int)std::chrono::duration_cast(duration).count()); - - jclass clResult = env->GetObjectClass(result); - - jfieldID fidTime = env->GetFieldID(clResult, "time", "Ljava/lang/String;"); - auto time = std::to_wstring(std::chrono::duration_cast(duration).count()); - env->SetObjectField(result, fidTime, C2JString(env, time)); - - if (!results.empty()) { - auto& res = results.front(); - jbyteArray jByteArray = env->NewByteArray(res.bytes().size()); - env->SetByteArrayRegion(jByteArray, 0, res.bytes().size(), (jbyte*)res.bytes().data()); - jfieldID fidBytes = env->GetFieldID(clResult, "bytes", "[B"); - env->SetObjectField(result, fidBytes, jByteArray); - - jfieldID fidText = env->GetFieldID(clResult, "text", "Ljava/lang/String;"); - env->SetObjectField(result, fidText, C2JString(env, res.text())); - - jfieldID fidContentType = env->GetFieldID(clResult , "contentType", "Lcom/zxingcpp/BarcodeReader$ContentType;"); - env->SetObjectField(result, fidContentType, CreateContentType(env, res.contentType())); - - jfieldID fidPosition = env->GetFieldID(clResult, "position", "Lcom/zxingcpp/BarcodeReader$Position;"); - env->SetObjectField(result, fidPosition, CreatePosition(env, res.position())); - - jfieldID fidOrientation = env->GetFieldID(clResult, "orientation", "I"); - env->SetIntField(result, fidOrientation, res.orientation()); - - jfieldID fidEcLevel = env->GetFieldID(clResult, "ecLevel", "Ljava/lang/String;"); - env->SetObjectField(result, fidEcLevel, C2JString(env, res.ecLevel())); - - jfieldID fidSymbologyIdentifier = env->GetFieldID(clResult, "symbologyIdentifier", "Ljava/lang/String;"); - env->SetObjectField(result, fidSymbologyIdentifier, C2JString(env, res.symbologyIdentifier())); - - return C2JString(env, JavaBarcodeFormatName(res.format())); - } else - return C2JString(env, "NotFound"); - } catch (const std::exception& e) { - return ThrowJavaException(env, e.what()); - } catch (...) { - return ThrowJavaException(env, "Unknown exception"); - } -} - -extern "C" JNIEXPORT jstring JNICALL -Java_com_zxingcpp_BarcodeReader_readYBuffer( - JNIEnv *env, jobject thiz, jobject yBuffer, jint rowStride, - jint left, jint top, jint width, jint height, jint rotation, - jstring formats, jboolean tryHarder, jboolean tryRotate, jboolean tryInvert, jboolean tryDownscale, - jobject result) -{ - const uint8_t* pixels = static_cast(env->GetDirectBufferAddress(yBuffer)); - - auto image = - ImageView{pixels + top * rowStride + left, width, height, ImageFormat::Lum, rowStride} - .rotated(rotation); - - return Read(env, image, formats, tryHarder, tryRotate, tryInvert, tryDownscale, result); -} - -struct LockedPixels -{ - JNIEnv* env; - jobject bitmap; - void *pixels = nullptr; - - LockedPixels(JNIEnv* env, jobject bitmap) : env(env), bitmap(bitmap) { - if (AndroidBitmap_lockPixels(env, bitmap, &pixels) != ANDROID_BITMAP_RESUT_SUCCESS) - pixels = nullptr; - } - - operator const uint8_t*() const { return static_cast(pixels); } - - ~LockedPixels() { - if (pixels) - AndroidBitmap_unlockPixels(env, bitmap); - } -}; - -extern "C" JNIEXPORT jstring JNICALL -Java_com_zxingcpp_BarcodeReader_readBitmap( - JNIEnv* env, jobject thiz, jobject bitmap, - jint left, jint top, jint width, jint height, jint rotation, - jstring formats, jboolean tryHarder, jboolean tryRotate, jboolean tryInvert, jboolean tryDownscale, - jobject result) -{ - AndroidBitmapInfo bmInfo; - AndroidBitmap_getInfo(env, bitmap, &bmInfo); - - ImageFormat fmt = ImageFormat::None; - switch (bmInfo.format) { - case ANDROID_BITMAP_FORMAT_A_8: fmt = ImageFormat::Lum; break; - case ANDROID_BITMAP_FORMAT_RGBA_8888: fmt = ImageFormat::RGBX; break; - default: return ThrowJavaException(env, "Unsupported format"); - } - - auto pixels = LockedPixels(env, bitmap); - - if (!pixels) - return ThrowJavaException(env, "Failed to lock/Read AndroidBitmap data"); - - auto image = ImageView{pixels, (int)bmInfo.width, (int)bmInfo.height, fmt, (int)bmInfo.stride} - .cropped(left, top, width, height) - .rotated(rotation); - - return Read(env, image, formats, tryHarder, tryRotate, tryInvert, tryDownscale, result); -} diff --git a/wrappers/android/zxingcpp/src/main/cpp/CMakeLists.txt b/wrappers/android/zxingcpp/src/main/cpp/CMakeLists.txt index d078a4eaef..f8b65ae02b 100644 --- a/wrappers/android/zxingcpp/src/main/cpp/CMakeLists.txt +++ b/wrappers/android/zxingcpp/src/main/cpp/CMakeLists.txt @@ -1,8 +1,8 @@ cmake_minimum_required(VERSION 3.14) -project(ZXingAndroid) +project(ZXingCppAndroid) -# Force to build by C++17. The zxing-cpp require C++17 to build -set(CMAKE_CXX_STANDARD 17) +# The zxing-cpp requires at least C++17 to build +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(BUILD_READERS ON) @@ -10,7 +10,7 @@ set(BUILD_WRITERS OFF) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../core ZXing EXCLUDE_FROM_ALL) -add_library(zxing_android SHARED BarcodeReader.cpp JNIUtils.cpp) +add_library(zxingcpp_android SHARED ZXingCpp.cpp) -target_link_libraries(zxing_android PRIVATE ZXing::ZXing log jnigraphics) +target_link_libraries(zxingcpp_android PRIVATE ZXing::ZXing log jnigraphics) diff --git a/wrappers/android/zxingcpp/src/main/cpp/JNIUtils.cpp b/wrappers/android/zxingcpp/src/main/cpp/JNIUtils.cpp deleted file mode 100644 index f797bda380..0000000000 --- a/wrappers/android/zxingcpp/src/main/cpp/JNIUtils.cpp +++ /dev/null @@ -1,73 +0,0 @@ -/* -* Copyright 2016 Nu-book Inc. -*/ -// SPDX-License-Identifier: Apache-2.0 -#include "JNIUtils.h" - -#include "Utf.h" - -#include -#include - -static bool RequiresSurrogates(uint32_t ucs4) -{ - return ucs4 >= 0x10000; -} - -static uint16_t HighSurrogate(uint32_t ucs4) -{ - return uint16_t((ucs4 >> 10) + 0xd7c0); -} - -static uint16_t LowSurrogate(uint32_t ucs4) -{ - return uint16_t(ucs4 % 0x400 + 0xdc00); -} - -static void Utf32toUtf16(const uint32_t* utf32, size_t length, std::vector& result) -{ - result.clear(); - result.reserve(length); - for (size_t i = 0; i < length; ++i) { - uint32_t c = utf32[i]; - if (RequiresSurrogates(c)) { - result.push_back(HighSurrogate(c)); - result.push_back(LowSurrogate(c)); - } else { - result.push_back(c); - } - } -} - -jstring C2JString(JNIEnv* env, const std::wstring& str) -{ - if (env->ExceptionCheck()) - return 0; - - if constexpr (sizeof(wchar_t) == 2) { - return env->NewString((const jchar*)str.data(), str.size()); - } else { - std::vector buffer; - Utf32toUtf16((const uint32_t*)str.data(), str.size(), buffer); - return env->NewString((const jchar*)buffer.data(), buffer.size()); - } -} - -jstring C2JString(JNIEnv* env, const std::string& str) -{ - return C2JString(env, ZXing::FromUtf8(str)); -} - -std::string J2CString(JNIEnv* env, jstring str) -{ - // Buffer size must be in bytes. - const jsize size = env->GetStringUTFLength(str); - std::string res(size, 0); - - // Translates 'len' number of Unicode characters into modified - // UTF-8 encoding and place the result in the given buffer. - const jsize len = env->GetStringLength(str); - env->GetStringUTFRegion(str, 0, len, res.data()); - - return res; -} diff --git a/wrappers/android/zxingcpp/src/main/cpp/JNIUtils.h b/wrappers/android/zxingcpp/src/main/cpp/JNIUtils.h deleted file mode 100644 index ed1391e022..0000000000 --- a/wrappers/android/zxingcpp/src/main/cpp/JNIUtils.h +++ /dev/null @@ -1,22 +0,0 @@ -/* -* Copyright 2016 Nu-book Inc. -*/ -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include -#include - -#include - -#define ZX_LOG_TAG "ZXing" - -#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, ZX_LOG_TAG, __VA_ARGS__) -#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, ZX_LOG_TAG, __VA_ARGS__) -#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, ZX_LOG_TAG, __VA_ARGS__) -#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, ZX_LOG_TAG, __VA_ARGS__) - -jstring C2JString(JNIEnv* env, const std::wstring& str); -jstring C2JString(JNIEnv* env, const std::string& str); -std::string J2CString(JNIEnv* env, jstring str); diff --git a/wrappers/android/zxingcpp/src/main/cpp/ZXingCpp.cpp b/wrappers/android/zxingcpp/src/main/cpp/ZXingCpp.cpp new file mode 100644 index 0000000000..e8154aaa3d --- /dev/null +++ b/wrappers/android/zxingcpp/src/main/cpp/ZXingCpp.cpp @@ -0,0 +1,381 @@ +/* +* Copyright 2021 Axel Waggershauser +*/ +// SPDX-License-Identifier: Apache-2.0 + +#include "ReadBarcode.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace ZXing; +using namespace std::string_literals; + +#define PACKAGE "zxingcpp/BarcodeReader$" + +#define ZX_LOG_TAG "zxingcpp" +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, ZX_LOG_TAG, __VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, ZX_LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, ZX_LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, ZX_LOG_TAG, __VA_ARGS__) + +static const char* JavaBarcodeFormatName(BarcodeFormat format) +{ + // These have to be the names of the enum constants in the kotlin code. + switch (format) { + case BarcodeFormat::None: return "NONE"; + case BarcodeFormat::Aztec: return "AZTEC"; + case BarcodeFormat::Codabar: return "CODABAR"; + case BarcodeFormat::Code39: return "CODE_39"; + case BarcodeFormat::Code93: return "CODE_93"; + case BarcodeFormat::Code128: return "CODE_128"; + case BarcodeFormat::DataMatrix: return "DATA_MATRIX"; + case BarcodeFormat::EAN8: return "EAN_8"; + case BarcodeFormat::EAN13: return "EAN_13"; + case BarcodeFormat::ITF: return "ITF"; + case BarcodeFormat::MaxiCode: return "MAXICODE"; + case BarcodeFormat::PDF417: return "PDF_417"; + case BarcodeFormat::QRCode: return "QR_CODE"; + case BarcodeFormat::MicroQRCode: return "MICRO_QR_CODE"; + case BarcodeFormat::RMQRCode: return "RMQR_CODE"; + case BarcodeFormat::DataBar: return "DATA_BAR"; + case BarcodeFormat::DataBarExpanded: return "DATA_BAR_EXPANDED"; + case BarcodeFormat::UPCA: return "UPC_A"; + case BarcodeFormat::UPCE: return "UPC_E"; + default: throw std::invalid_argument("Invalid BarcodeFormat"); + } +} + +static const char* JavaContentTypeName(ContentType contentType) +{ + // These have to be the names of the enum constants in the kotlin code. + switch (contentType) { + case ContentType::Text: return "TEXT"; + case ContentType::Binary: return "BINARY"; + case ContentType::Mixed: return "MIXED"; + case ContentType::GS1: return "GS1"; + case ContentType::ISO15434: return "ISO15434"; + case ContentType::UnknownECI: return "UNKNOWN_ECI"; + default: throw std::invalid_argument("Invalid contentType"); + } +} + +static const char* JavaErrorTypeName(Error::Type errorType) +{ + // These have to be the names of the enum constants in the kotlin code. + switch (errorType) { + case Error::Type::Format: return "FORMAT"; + case Error::Type::Checksum: return "CHECKSUM"; + case Error::Type::Unsupported: return "UNSUPPORTED"; + default: throw std::invalid_argument("Invalid errorType"); + } +} + +inline constexpr auto hash(std::string_view sv) +{ + unsigned int hash = 5381; + for (unsigned char c : sv) + hash = ((hash << 5) + hash) ^ c; + return hash; +} + +inline constexpr auto operator "" _h(const char* str, size_t len){ return hash({str, len}); } + +static EanAddOnSymbol EanAddOnSymbolFromString(std::string_view name) +{ + switch (hash(name)) { + case "IGNORE"_h : return EanAddOnSymbol::Ignore; + case "READ"_h : return EanAddOnSymbol::Read; + case "REQUIRE"_h : return EanAddOnSymbol::Require; + default: throw std::invalid_argument("Invalid eanAddOnSymbol name"); + } +} + +static Binarizer BinarizerFromString(std::string_view name) +{ + switch (hash(name)) { + case "LOCAL_AVERAGE"_h : return Binarizer::LocalAverage; + case "GLOBAL_HISTOGRAM"_h : return Binarizer::GlobalHistogram; + case "FIXED_THRESHOLD"_h : return Binarizer::FixedThreshold; + case "BOOL_CAST"_h : return Binarizer::BoolCast; + default: throw std::invalid_argument("Invalid binarizer name"); + } +} + +static TextMode TextModeFromString(std::string_view name) +{ + switch (hash(name)) { + case "PLAIN"_h : return TextMode::Plain; + case "ECI"_h : return TextMode::ECI; + case "HRI"_h : return TextMode::HRI; + case "HEX"_h : return TextMode::Hex; + case "ESCAPED"_h : return TextMode::Escaped; + default: throw std::invalid_argument("Invalid textMode name"); + } +} + +static jstring ThrowJavaException(JNIEnv* env, const char* message) +{ + // if (env->ExceptionCheck()) + // return 0; + jclass cls = env->FindClass("java/lang/RuntimeException"); + env->ThrowNew(cls, message); + return nullptr; +} + +jstring C2JString(JNIEnv* env, const std::string& str) +{ + return env->NewStringUTF(str.c_str()); +} + +std::string J2CString(JNIEnv* env, jstring str) +{ + // Buffer size must be in bytes. + const jsize size = env->GetStringUTFLength(str); + std::string res(size, 0); + + // Translates 'len' number of Unicode characters into modified + // UTF-8 encoding and place the result in the given buffer. + const jsize len = env->GetStringLength(str); + env->GetStringUTFRegion(str, 0, len, res.data()); + + return res; +} + +static jobject NewPosition(JNIEnv* env, const Position& position) +{ + jclass clsPosition = env->FindClass(PACKAGE "Position"); + jclass clsPoint = env->FindClass("android/graphics/Point"); + jmethodID midPointInit= env->GetMethodID(clsPoint, "", "(II)V"); + auto NewPoint = [&](const PointI& point) { + return env->NewObject(clsPoint, midPointInit, point.x, point.y); + }; + jmethodID midPositionInit= env->GetMethodID( + clsPosition, "", + "(Landroid/graphics/Point;" + "Landroid/graphics/Point;" + "Landroid/graphics/Point;" + "Landroid/graphics/Point;" + "D)V"); + return env->NewObject( + clsPosition, midPositionInit, + NewPoint(position[0]), + NewPoint(position[1]), + NewPoint(position[2]), + NewPoint(position[3]), + position.orientation()); +} + +static jbyteArray NewByteArray(JNIEnv* env, const std::vector& byteArray) +{ + auto size = static_cast(byteArray.size()); + jbyteArray res = env->NewByteArray(size); + env->SetByteArrayRegion(res, 0, size, reinterpret_cast(byteArray.data())); + return res; +} + +static jobject NewEnum(JNIEnv* env, const char* value, const char* type) +{ + auto className = PACKAGE ""s + type; + jclass cls = env->FindClass(className.c_str()); + jfieldID fidCT = env->GetStaticFieldID(cls, value, ("L" + className + ";").c_str()); + return env->GetStaticObjectField(cls, fidCT); +} + +static jobject NewError(JNIEnv* env, const Error& error) +{ + jclass cls = env->FindClass(PACKAGE "Error"); + jmethodID midInit = env->GetMethodID(cls, "", "(L" PACKAGE "ErrorType;" "Ljava/lang/String;)V"); + return env->NewObject(cls, midInit, NewEnum(env, JavaErrorTypeName(error.type()), "ErrorType"), C2JString(env, error.msg())); +} + +static jobject NewResult(JNIEnv* env, const Result& result) +{ + jclass cls = env->FindClass(PACKAGE "Result"); + jmethodID midInit = env->GetMethodID( + cls, "", + "(L" PACKAGE "Format;" + "[B" + "Ljava/lang/String;" + "L" PACKAGE "ContentType;" + "L" PACKAGE "Position;" + "I" + "Ljava/lang/String;" + "Ljava/lang/String;" + "I" + "I" + "Ljava/lang/String;" + "Z" + "I" + "L" PACKAGE "Error;" + ")V"); + bool valid = result.isValid(); + return env->NewObject(cls, midInit, + NewEnum(env, JavaBarcodeFormatName(result.format()), "Format"), + valid ? NewByteArray(env, result.bytes()) : nullptr, + valid ? C2JString(env, result.text()) : nullptr, + NewEnum(env, JavaContentTypeName(result.contentType()), "ContentType"), + NewPosition(env, result.position()), + result.orientation(), + valid ? C2JString(env, result.ecLevel()) : nullptr, + valid ? C2JString(env, result.symbologyIdentifier()) : nullptr, + result.sequenceSize(), + result.sequenceIndex(), + valid ? C2JString(env, result.sequenceId()) : nullptr, + result.readerInit(), + result.lineCount(), + result.error() ? NewError(env, result.error()) : nullptr + ); +} + +static jobject Read(JNIEnv *env, jobject thiz, ImageView image, const ReaderOptions& opts) +{ + try { + auto startTime = std::chrono::high_resolution_clock::now(); + auto results = ReadBarcodes(image, opts); + auto duration = std::chrono::high_resolution_clock::now() - startTime; +// LOGD("time: %4d ms\n", (int)std::chrono::duration_cast(duration).count()); + auto time = std::chrono::duration_cast(duration).count(); + + env->SetIntField(thiz, env->GetFieldID(env->GetObjectClass(thiz), "lastReadTime", "I"), time); + + jclass clsList = env->FindClass("java/util/ArrayList"); + jobject objList = env->NewObject(clsList, env->GetMethodID(clsList, "", "()V")); + if (!results.empty()) { + jmethodID midAdd = env->GetMethodID(clsList, "add", "(Ljava/lang/Object;)Z"); + for (const auto& result: results) + env->CallBooleanMethod(objList, midAdd, NewResult(env, result)); + } + return objList; + } catch (const std::exception& e) { + return ThrowJavaException(env, e.what()); + } catch (...) { + return ThrowJavaException(env, "Unknown exception"); + } +} + +static bool GetBooleanField(JNIEnv* env, jclass cls, jobject opts, const char* name) +{ + return env->GetBooleanField(opts, env->GetFieldID(cls, name, "Z")); +} + +static int GetIntField(JNIEnv* env, jclass cls, jobject opts, const char* name) +{ + return env->GetIntField(opts, env->GetFieldID(cls, name, "I")); +} + +static std::string GetEnumField(JNIEnv* env, jclass cls, jobject opts, const char* name, const char* type) +{ + auto className = PACKAGE ""s + type; + jmethodID midName = env->GetMethodID(env->FindClass(className.c_str()), "name", "()Ljava/lang/String;"); + jobject objField = env->GetObjectField(opts, env->GetFieldID(cls, name, ("L"s + className + ";").c_str())); + return J2CString(env, static_cast(env->CallObjectMethod(objField, midName))); +} + +static BarcodeFormats GetFormats(JNIEnv* env, jclass clsOptions, jobject opts) +{ + jobject objField = env->GetObjectField(opts, env->GetFieldID(clsOptions, "formats", "Ljava/util/Set;")); + jmethodID midToArray = env->GetMethodID(env->FindClass("java/util/Set"), "toArray", "()[Ljava/lang/Object;"); + auto objArray = static_cast(env->CallObjectMethod(objField, midToArray)); + if (!objArray) + return {}; + + jmethodID midName = env->GetMethodID(env->FindClass(PACKAGE "Format"), "name", "()Ljava/lang/String;"); + BarcodeFormats ret; + for (int i = 0, size = env->GetArrayLength(objArray); i < size; ++i) { + auto objName = static_cast(env->CallObjectMethod(env->GetObjectArrayElement(objArray, i), midName)); + ret |= BarcodeFormatFromString(J2CString(env, objName)); + } + return ret; +} + +static ReaderOptions CreateReaderOptions(JNIEnv* env, jobject opts) +{ + jclass cls = env->GetObjectClass(opts); + return ReaderOptions() + .setFormats(GetFormats(env, cls, opts)) + .setTryHarder(GetBooleanField(env, cls, opts, "tryHarder")) + .setTryRotate(GetBooleanField(env, cls, opts, "tryRotate")) + .setTryInvert(GetBooleanField(env, cls, opts, "tryInvert")) + .setTryDownscale(GetBooleanField(env, cls, opts, "tryDownscale")) + .setIsPure(GetBooleanField(env, cls, opts, "isPure")) + .setBinarizer(BinarizerFromString(GetEnumField(env, cls, opts, "binarizer", "Binarizer"))) + .setDownscaleThreshold(GetIntField(env, cls, opts, "downscaleThreshold")) + .setDownscaleFactor(GetIntField(env, cls, opts, "downscaleFactor")) + .setMinLineCount(GetIntField(env, cls, opts, "minLineCount")) + .setMaxNumberOfSymbols(GetIntField(env, cls, opts, "maxNumberOfSymbols")) + .setTryCode39ExtendedMode(GetBooleanField(env, cls, opts, "tryCode39ExtendedMode")) + .setValidateCode39CheckSum(GetBooleanField(env, cls, opts, "validateCode39CheckSum")) + .setValidateITFCheckSum(GetBooleanField(env, cls, opts, "validateITFCheckSum")) + .setReturnCodabarStartEnd(GetBooleanField(env, cls, opts, "returnCodabarStartEnd")) + .setReturnErrors(GetBooleanField(env, cls, opts, "returnErrors")) + .setEanAddOnSymbol(EanAddOnSymbolFromString(GetEnumField(env, cls, opts, "eanAddOnSymbol", "EanAddOnSymbol"))) + .setTextMode(TextModeFromString(GetEnumField(env, cls, opts, "textMode", "TextMode"))) + ; +} + +extern "C" JNIEXPORT jobject JNICALL +Java_zxingcpp_BarcodeReader_readYBuffer( + JNIEnv *env, jobject thiz, jobject yBuffer, jint rowStride, + jint left, jint top, jint width, jint height, jint rotation, jobject options) +{ + const uint8_t* pixels = static_cast(env->GetDirectBufferAddress(yBuffer)); + + auto image = + ImageView{pixels + top * rowStride + left, width, height, ImageFormat::Lum, rowStride} + .rotated(rotation); + + return Read(env, thiz, image, CreateReaderOptions(env, options)); +} + +struct LockedPixels +{ + JNIEnv* env; + jobject bitmap; + void *pixels = nullptr; + + LockedPixels(JNIEnv* env, jobject bitmap) : env(env), bitmap(bitmap) { + if (AndroidBitmap_lockPixels(env, bitmap, &pixels) != ANDROID_BITMAP_RESUT_SUCCESS) + pixels = nullptr; + } + + operator const uint8_t*() const { return static_cast(pixels); } + + ~LockedPixels() { + if (pixels) + AndroidBitmap_unlockPixels(env, bitmap); + } +}; + +extern "C" JNIEXPORT jobject JNICALL +Java_zxingcpp_BarcodeReader_readBitmap( + JNIEnv* env, jobject thiz, jobject bitmap, + jint left, jint top, jint width, jint height, jint rotation, jobject options) +{ + AndroidBitmapInfo bmInfo; + AndroidBitmap_getInfo(env, bitmap, &bmInfo); + + ImageFormat fmt = ImageFormat::None; + switch (bmInfo.format) { + case ANDROID_BITMAP_FORMAT_A_8: fmt = ImageFormat::Lum; break; + case ANDROID_BITMAP_FORMAT_RGBA_8888: fmt = ImageFormat::RGBX; break; + default: return ThrowJavaException(env, "Unsupported image format in AndroidBitmap"); + } + + auto pixels = LockedPixels(env, bitmap); + + if (!pixels) + return ThrowJavaException(env, "Failed to lock/read AndroidBitmap data"); + + auto image = + ImageView{pixels, (int)bmInfo.width, (int)bmInfo.height, fmt, (int)bmInfo.stride} + .cropped(left, top, width, height) + .rotated(rotation); + + return Read(env, thiz, image, CreateReaderOptions(env, options)); +} diff --git a/wrappers/android/zxingcpp/src/main/java/com/zxingcpp/BarcodeReader.kt b/wrappers/android/zxingcpp/src/main/java/com/zxingcpp/BarcodeReader.kt deleted file mode 100644 index b241cef0f2..0000000000 --- a/wrappers/android/zxingcpp/src/main/java/com/zxingcpp/BarcodeReader.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* -* Copyright 2021 Axel Waggershauser -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - -package com.zxingcpp - -import android.graphics.Bitmap -import android.graphics.ImageFormat -import android.graphics.Point -import android.graphics.Rect -import androidx.camera.core.ImageProxy -import java.lang.RuntimeException -import java.nio.ByteBuffer - -class BarcodeReader { - - // Enumerates barcode formats known to this package. - // Note that this has to be kept synchronized with native (C++/JNI) side. - enum class Format { - NONE, AZTEC, CODABAR, CODE_39, CODE_93, CODE_128, DATA_BAR, DATA_BAR_EXPANDED, - DATA_MATRIX, EAN_8, EAN_13, ITF, MAXICODE, PDF_417, QR_CODE, MICRO_QR_CODE, UPC_A, UPC_E - } - enum class ContentType { - TEXT, BINARY, MIXED, GS1, ISO15434, UNKNOWN_ECI - } - - data class Options( - val formats: Set = setOf(), - val tryHarder: Boolean = false, - val tryRotate: Boolean = false, - val tryInvert: Boolean = false, - val tryDownscale: Boolean = false - ) - - data class Position( - val topLeft: Point, - val topRight: Point, - val bottomLeft: Point, - val bottomRight: Point, - val orientation: Double - ) - - data class Result( - val format: Format = Format.NONE, - val bytes: ByteArray? = null, - val text: String? = null, - val time: String? = null, // for development/debug purposes only - val contentType: ContentType = ContentType.TEXT, - val position: Position? = null, - val orientation: Int = 0, - val ecLevel: String? = null, - val symbologyIdentifier: String? = null - ) - - var options : Options = Options() - - fun read(image: ImageProxy): Result? { - val supportedYUVFormats = arrayOf(ImageFormat.YUV_420_888, ImageFormat.YUV_422_888, ImageFormat.YUV_444_888) - if (image.format !in supportedYUVFormats) { - error("invalid image format") - } - - var result = Result() - val status = image.use { - readYBuffer( - it.planes[0].buffer, - it.planes[0].rowStride, - it.cropRect.left, - it.cropRect.top, - it.cropRect.width(), - it.cropRect.height(), - it.imageInfo.rotationDegrees, - options.formats.joinToString(), - options.tryHarder, - options.tryRotate, - options.tryInvert, - options.tryDownscale, - result - ) - } - return try { - result.copy(format = Format.valueOf(status!!)) - } catch (e: Throwable) { - if (status == "NotFound") null else throw RuntimeException(status!!) - } - } - - fun read(bitmap: Bitmap, cropRect: Rect = Rect(), rotation: Int = 0): Result? { - return read(bitmap, options, cropRect, rotation) - } - - fun read(bitmap: Bitmap, options: Options, cropRect: Rect = Rect(), rotation: Int = 0): Result? { - var result = Result() - val status = with(options) { - readBitmap( - bitmap, cropRect.left, cropRect.top, cropRect.width(), cropRect.height(), rotation, - formats.joinToString(), tryHarder, tryRotate, tryInvert, tryDownscale, result - ) - } - return try { - result.copy(format = Format.valueOf(status!!)) - } catch (e: Throwable) { - if (status == "NotFound") null else throw RuntimeException(status!!) - } - } - - // setting the format enum from inside the JNI code is a hassle -> use returned String instead - private external fun readYBuffer( - yBuffer: ByteBuffer, rowStride: Int, left: Int, top: Int, width: Int, height: Int, rotation: Int, - formats: String, tryHarder: Boolean, tryRotate: Boolean, tryInvert: Boolean, tryDownscale: Boolean, - result: Result, - ): String? - - private external fun readBitmap( - bitmap: Bitmap, left: Int, top: Int, width: Int, height: Int, rotation: Int, - formats: String, tryHarder: Boolean, tryRotate: Boolean, tryInvert: Boolean, tryDownscale: Boolean, - result: Result, - ): String? - - init { - System.loadLibrary("zxing_android") - } -} diff --git a/wrappers/android/zxingcpp/src/main/java/zxingcpp/BarcodeReader.kt b/wrappers/android/zxingcpp/src/main/java/zxingcpp/BarcodeReader.kt new file mode 100644 index 0000000000..2d330baa7b --- /dev/null +++ b/wrappers/android/zxingcpp/src/main/java/zxingcpp/BarcodeReader.kt @@ -0,0 +1,151 @@ +/* +* Copyright 2021 Axel Waggershauser +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package zxingcpp + +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Point +import android.graphics.Rect +import android.os.Build +import androidx.camera.core.ImageProxy +import java.nio.ByteBuffer + +public class BarcodeReader(public var options: Options = Options()) { + private val supportedYUVFormats: List = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + listOf(ImageFormat.YUV_420_888, ImageFormat.YUV_422_888, ImageFormat.YUV_444_888) + } else { + listOf(ImageFormat.YUV_420_888) + } + + init { + System.loadLibrary("zxingcpp_android") + } + + // Enumerates barcode formats known to this package. + // Note that this has to be kept synchronized with native (C++/JNI) side. + public enum class Format { + NONE, AZTEC, CODABAR, CODE_39, CODE_93, CODE_128, DATA_BAR, DATA_BAR_EXPANDED, + DATA_MATRIX, EAN_8, EAN_13, ITF, MAXICODE, PDF_417, QR_CODE, MICRO_QR_CODE, RMQR_CODE, UPC_A, UPC_E + } + + public enum class ContentType { + TEXT, BINARY, MIXED, GS1, ISO15434, UNKNOWN_ECI + } + + public enum class Binarizer { + LOCAL_AVERAGE, GLOBAL_HISTOGRAM, FIXED_THRESHOLD, BOOL_CAST + } + + public enum class EanAddOnSymbol { + IGNORE, READ, REQUIRE + } + + public enum class TextMode { + PLAIN, ECI, HRI, HEX, ESCAPED + } + + public enum class ErrorType { + FORMAT, CHECKSUM, UNSUPPORTED + } + + public data class Options( + var formats: Set = setOf(), + var tryHarder: Boolean = false, + var tryRotate: Boolean = false, + var tryInvert: Boolean = false, + var tryDownscale: Boolean = false, + var isPure: Boolean = false, + var binarizer: Binarizer = Binarizer.LOCAL_AVERAGE, + var downscaleFactor: Int = 3, + var downscaleThreshold: Int = 500, + var minLineCount: Int = 2, + var maxNumberOfSymbols: Int = 0xff, + var tryCode39ExtendedMode: Boolean = false, + var validateCode39CheckSum: Boolean = false, + var validateITFCheckSum: Boolean = false, + var returnCodabarStartEnd: Boolean = false, + var returnErrors: Boolean = false, + var eanAddOnSymbol: EanAddOnSymbol = EanAddOnSymbol.IGNORE, + var textMode: TextMode = TextMode.HRI, + ) + + public data class Error( + val type: ErrorType, + val message: String + ) + + public data class Position( + val topLeft: Point, + val topRight: Point, + val bottomRight: Point, + val bottomLeft: Point, + val orientation: Double + ) + + public data class Result( + val format: Format, + val bytes: ByteArray?, + val text: String?, + val contentType: ContentType, + val position: Position, + val orientation: Int, + val ecLevel: String?, + val symbologyIdentifier: String?, + val sequenceSize: Int, + val sequenceIndex: Int, + val sequenceId: String?, + val readerInit: Boolean, + val lineCount: Int, + val error: Error?, + ) + + public val lastReadTime : Int = 0 // runtime of last read call in ms (for debugging purposes only) + + public fun read(image: ImageProxy): List { + check(image.format in supportedYUVFormats) { + "Invalid image format: ${image.format}. Must be one of: $supportedYUVFormats" + } + + return readYBuffer( + image.planes[0].buffer, + image.planes[0].rowStride, + image.cropRect.left, + image.cropRect.top, + image.cropRect.width(), + image.cropRect.height(), + image.imageInfo.rotationDegrees, + options + ) + } + + public fun read( + bitmap: Bitmap, cropRect: Rect = Rect(), rotation: Int = 0 + ): List { + return readBitmap( + bitmap, cropRect.left, cropRect.top, cropRect.width(), cropRect.height(), rotation, options + ) + } + + private external fun readYBuffer( + yBuffer: ByteBuffer, rowStride: Int, left: Int, top: Int, width: Int, height: Int, rotation: Int, options: Options + ): List + + private external fun readBitmap( + bitmap: Bitmap, left: Int, top: Int, width: Int, height: Int, rotation: Int, options: Options + ): List +} diff --git a/wrappers/c/README.md b/wrappers/c/README.md new file mode 100644 index 0000000000..550565ae71 --- /dev/null +++ b/wrappers/c/README.md @@ -0,0 +1,47 @@ +# C bindings for zxing-cpp + +This is a preview/proposal for a C-API to zxing-cpp. If this turns out to be useful and practical, it will most likely be merged into the library itself so that it will be trivially accessible for everyone. If you have any comments or feedback, please have a look at https://github.com/zxing-cpp/zxing-cpp/discussions/583. + +## Installation + +Probably the easiest way to play with the C-API is to either just modify the [zxing-c-test.c](https://github.com/zxing-cpp/zxing-cpp/blob/master/wrappers/c/zxing-c-test.c) file or copy the files [zxing-c.h](https://github.com/zxing-cpp/zxing-cpp/blob/master/wrappers/c/zxing-c.h) and [zxing-c.cpp](https://github.com/zxing-cpp/zxing-cpp/blob/master/wrappers/c/zxing-c.cpp) into your own project and link it to the standard zxing-cpp library. + +## Usage + +The following is close to the most trivial use case scenario that is supported. + +```c +#include "zxing-c.h" + +int main(int argc, char** argv) +{ + int width, height; + unsigned char* data; + /* load your image data from somewhere. ImageFormat_Lum assumes grey scale image data. */ + + zxing_ImageView* iv = zxing_ImageView_new(data, width, height, zxing_ImageFormat_Lum, 0, 0); + + zxing_ReaderOptions* opts = zxing_ReaderOptions_new(); + /* set ReaderOptions properties, if requried */ + + zxing_Result* result = zxing_ReadBarcode(iv, opts); + + if (result) { + printf("Format : %s\n", zxing_BarcodeFormatToString(zxing_Result_format(result))); + + const char* text = zxing_Result_text(result); + printf("Text : %s\n", text); + free(text); + + zxing_Result_delete(result); + } else { + printf("No barcode found\n"); + } + + zxing_ImageView_delete(iv); + zxing_ReaderOptions_delete(opts); + + return 0; +} +``` + diff --git a/wrappers/c/zxing-c-test.c b/wrappers/c/zxing-c-test.c index 24d4457d66..0497a1cadd 100644 --- a/wrappers/c/zxing-c-test.c +++ b/wrappers/c/zxing-c-test.c @@ -54,15 +54,15 @@ int main(int argc, char** argv) if (!data) return 2; - zxing_DecodeHints* hints = zxing_DecodeHints_new(); - zxing_DecodeHints_setTextMode(hints, zxing_TextMode_HRI); - zxing_DecodeHints_setEanAddOnSymbol(hints, zxing_EanAddOnSymbol_Ignore); - zxing_DecodeHints_setFormats(hints, formats); - zxing_DecodeHints_setReturnErrors(hints, true); + zxing_ReaderOptions* opts = zxing_ReaderOptions_new(); + zxing_ReaderOptions_setTextMode(opts, zxing_TextMode_HRI); + zxing_ReaderOptions_setEanAddOnSymbol(opts, zxing_EanAddOnSymbol_Ignore); + zxing_ReaderOptions_setFormats(opts, formats); + zxing_ReaderOptions_setReturnErrors(opts, true); zxing_ImageView* iv = zxing_ImageView_new(data, width, height, zxing_ImageFormat_Lum, 0, 0); - zxing_Results* results = zxing_ReadBarcodes(iv, hints); + zxing_Results* results = zxing_ReadBarcodes(iv, opts); if (results) { for (int i = 0, n = zxing_Results_size(results); i < n; ++i) { @@ -86,7 +86,7 @@ int main(int argc, char** argv) } zxing_ImageView_delete(iv); - zxing_DecodeHints_delete(hints); + zxing_ReaderOptions_delete(opts); stbi_image_free(data); return 0; diff --git a/wrappers/c/zxing-c.cpp b/wrappers/c/zxing-c.cpp index f40916027a..00c09963fe 100644 --- a/wrappers/c/zxing-c.cpp +++ b/wrappers/c/zxing-c.cpp @@ -20,222 +20,227 @@ char* copy(std::string_view sv) return ret; } -extern "C" +extern "C" { +/* + * ZXing/ImageView.h + */ + +zxing_ImageView* zxing_ImageView_new(const uint8_t* data, int width, int height, zxing_ImageFormat format, int rowStride, + int pixStride) { - /* - * ZXing/ImageView.h - */ + ImageFormat cppformat = static_cast(format); + return new ImageView(data, width, height, cppformat, rowStride, pixStride); +} - zxing_ImageView* zxing_ImageView_new(const uint8_t* data, int width, int height, zxing_ImageFormat format, int rowStride, - int pixStride) - { - ImageFormat cppformat = static_cast(format); - return new ImageView(data, width, height, cppformat, rowStride, pixStride); - } +void zxing_ImageView_delete(zxing_ImageView* iv) +{ + delete iv; +} - void zxing_ImageView_delete(zxing_ImageView* iv) - { - delete iv; - } +/* + * ZXing/BarcodeFormat.h + */ - /* - * ZXing/BarcodeFormat.h - */ - - zxing_BarcodeFormats zxing_BarcodeFormatsFromString(const char* str) - { - if (!str) - return {}; - try { - auto format = BarcodeFormatsFromString(str); - return static_cast(*reinterpret_cast(&format)); - } catch (...) { - return zxing_BarcodeFormat_Invalid; - } +zxing_BarcodeFormats zxing_BarcodeFormatsFromString(const char* str) +{ + if (!str) + return {}; + try { + auto format = BarcodeFormatsFromString(str); + return static_cast(*reinterpret_cast(&format)); + } catch (...) { + return zxing_BarcodeFormat_Invalid; } +} - zxing_BarcodeFormat zxing_BarcodeFormatFromString(const char* str) - { - zxing_BarcodeFormat res = zxing_BarcodeFormatsFromString(str); - return BitHacks::CountBitsSet(res) == 1 ? res : zxing_BarcodeFormat_Invalid; - } +zxing_BarcodeFormat zxing_BarcodeFormatFromString(const char* str) +{ + zxing_BarcodeFormat res = zxing_BarcodeFormatsFromString(str); + return BitHacks::CountBitsSet(res) == 1 ? res : zxing_BarcodeFormat_Invalid; +} - char* zxing_BarcodeFormatToString(zxing_BarcodeFormat format) - { - return copy(ToString(static_cast(format))); - } +char* zxing_BarcodeFormatToString(zxing_BarcodeFormat format) +{ + return copy(ToString(static_cast(format))); +} - /* - * ZXing/DecodeHints.h - */ +/* + * ZXing/ReaderOptions.h + */ - zxing_DecodeHints* zxing_DecodeHints_new() - { - return new DecodeHints(); - } +zxing_ReaderOptions* zxing_ReaderOptions_new() +{ + return new ReaderOptions(); +} - void zxing_DecodeHints_delete(zxing_DecodeHints* hints) - { - delete hints; - } +void zxing_ReaderOptions_delete(zxing_ReaderOptions* opts) +{ + delete opts; +} - void zxing_DecodeHints_setTryHarder(zxing_DecodeHints* hints, bool tryHarder) - { - hints->setTryHarder(tryHarder); - } +void zxing_ReaderOptions_setTryHarder(zxing_ReaderOptions* opts, bool tryHarder) +{ + opts->setTryHarder(tryHarder); +} - void zxing_DecodeHints_setTryRotate(zxing_DecodeHints* hints, bool tryRotate) - { - hints->setTryRotate(tryRotate); - } +void zxing_ReaderOptions_setTryRotate(zxing_ReaderOptions* opts, bool tryRotate) +{ + opts->setTryRotate(tryRotate); +} - void zxing_DecodeHints_setTryInvert(zxing_DecodeHints* hints, bool tryInvert) - { - hints->setTryInvert(tryInvert); - } +void zxing_ReaderOptions_setTryInvert(zxing_ReaderOptions* opts, bool tryInvert) +{ + opts->setTryInvert(tryInvert); +} - void zxing_DecodeHints_setTryDownscale(zxing_DecodeHints* hints, bool tryDownscale) - { - hints->setTryDownscale(tryDownscale); - } +void zxing_ReaderOptions_setTryDownscale(zxing_ReaderOptions* opts, bool tryDownscale) +{ + opts->setTryDownscale(tryDownscale); +} - void zxing_DecodeHints_setIsPure(zxing_DecodeHints* hints, bool isPure) - { - hints->setIsPure(isPure); - } +void zxing_ReaderOptions_setIsPure(zxing_ReaderOptions* opts, bool isPure) +{ + opts->setIsPure(isPure); +} - void zxing_DecodeHints_setReturnErrors(zxing_DecodeHints* hints, bool returnErrors) - { - hints->setReturnErrors(returnErrors); - } +void zxing_ReaderOptions_setReturnErrors(zxing_ReaderOptions* opts, bool returnErrors) +{ + opts->setReturnErrors(returnErrors); +} - void zxing_DecodeHints_setFormats(zxing_DecodeHints* hints, zxing_BarcodeFormats formats) - { - hints->setFormats(static_cast(formats)); - } +void zxing_ReaderOptions_setFormats(zxing_ReaderOptions* opts, zxing_BarcodeFormats formats) +{ + opts->setFormats(static_cast(formats)); +} - void zxing_DecodeHints_setBinarizer(zxing_DecodeHints* hints, zxing_Binarizer binarizer) - { - hints->setBinarizer(static_cast(binarizer)); - } +void zxing_ReaderOptions_setBinarizer(zxing_ReaderOptions* opts, zxing_Binarizer binarizer) +{ + opts->setBinarizer(static_cast(binarizer)); +} - void zxing_DecodeHints_setEanAddOnSymbol(zxing_DecodeHints* hints, zxing_EanAddOnSymbol eanAddOnSymbol) - { - hints->setEanAddOnSymbol(static_cast(eanAddOnSymbol)); - } +void zxing_ReaderOptions_setEanAddOnSymbol(zxing_ReaderOptions* opts, zxing_EanAddOnSymbol eanAddOnSymbol) +{ + opts->setEanAddOnSymbol(static_cast(eanAddOnSymbol)); +} - void zxing_DecodeHints_setTextMode(zxing_DecodeHints* hints, zxing_TextMode textMode) - { - hints->setTextMode(static_cast(textMode)); - } +void zxing_ReaderOptions_setTextMode(zxing_ReaderOptions* opts, zxing_TextMode textMode) +{ + opts->setTextMode(static_cast(textMode)); +} - /* - * ZXing/Result.h - */ +void zxing_ReaderOptions_setMaxNumberOfSymbols(zxing_ReaderOptions* opts, int n) +{ + opts->setMaxNumberOfSymbols(n); +} - char* zxing_ContentTypeToString(zxing_ContentType type) - { - return copy(ToString(static_cast(type))); - } +/* + * ZXing/Result.h + */ - bool zxing_Result_isValid(const zxing_Result* result) - { - return result != NULL && result->isValid(); - } +char* zxing_ContentTypeToString(zxing_ContentType type) +{ + return copy(ToString(static_cast(type))); +} - char* zxing_Result_errorMsg(const zxing_Result* result) - { - return copy(ToString(result->error())); - } +bool zxing_Result_isValid(const zxing_Result* result) +{ + return result != NULL && result->isValid(); +} - zxing_BarcodeFormat zxing_Result_format(const zxing_Result* result) - { - return static_cast(result->format()); - } +char* zxing_Result_errorMsg(const zxing_Result* result) +{ + return copy(ToString(result->error())); +} - zxing_ContentType zxing_Result_contentType(const zxing_Result* result) - { - return static_cast(result->contentType()); - } +zxing_BarcodeFormat zxing_Result_format(const zxing_Result* result) +{ + return static_cast(result->format()); +} - uint8_t* zxing_Result_bytes(const zxing_Result* result, int* len) - { - *len = Size(result->bytes()); +zxing_ContentType zxing_Result_contentType(const zxing_Result* result) +{ + return static_cast(result->contentType()); +} - auto ret = (uint8_t*)malloc(*len + 1); - if (ret) - memcpy(ret, result->bytes().data(), *len); - else - *len = 0; +uint8_t* zxing_Result_bytes(const zxing_Result* result, int* len) +{ + *len = Size(result->bytes()); - return ret; - } + auto ret = (uint8_t*)malloc(*len + 1); + if (ret) + memcpy(ret, result->bytes().data(), *len); + else + *len = 0; - char* zxing_Result_text(const zxing_Result* result) - { - return copy(result->text()); - } + return ret; +} - char* zxing_Result_ecLevel(const zxing_Result* result) - { - return copy(result->ecLevel()); - } +char* zxing_Result_text(const zxing_Result* result) +{ + return copy(result->text()); +} - char* zxing_Result_symbologyIdentifier(const zxing_Result* result) - { - return copy(result->symbologyIdentifier()); - } +char* zxing_Result_ecLevel(const zxing_Result* result) +{ + return copy(result->ecLevel()); +} - int zxing_Result_orientation(const zxing_Result* result) - { - return result->orientation(); - } +char* zxing_Result_symbologyIdentifier(const zxing_Result* result) +{ + return copy(result->symbologyIdentifier()); +} - bool zxing_Result_isInverted(const zxing_Result* result) - { - return result->isInverted(); - } +int zxing_Result_orientation(const zxing_Result* result) +{ + return result->orientation(); +} - bool zxing_Result_isMirrored(const zxing_Result* result) - { - return result->isMirrored(); - } +bool zxing_Result_isInverted(const zxing_Result* result) +{ + return result->isInverted(); +} - /* - * ZXing/ReadBarcode.h - */ +bool zxing_Result_isMirrored(const zxing_Result* result) +{ + return result->isMirrored(); +} - zxing_Result* zxing_ReadBarcode(const zxing_ImageView* iv, const zxing_DecodeHints* hints) - { - auto res = ReadBarcode(*iv, *hints); - return res.format() != BarcodeFormat::None ? new Result(std::move(res)) : NULL; - } +/* + * ZXing/ReadBarcode.h + */ - zxing_Results* zxing_ReadBarcodes(const zxing_ImageView* iv, const zxing_DecodeHints* hints) - { - auto res = ReadBarcodes(*iv, *hints); - return !res.empty() ? new Results(std::move(res)) : NULL; - } +zxing_Result* zxing_ReadBarcode(const zxing_ImageView* iv, const zxing_ReaderOptions* opts) +{ + auto res = ReadBarcode(*iv, *opts); + return res.format() != BarcodeFormat::None ? new Result(std::move(res)) : NULL; +} - void zxing_Result_delete(zxing_Result* result) - { - delete result; - } +zxing_Results* zxing_ReadBarcodes(const zxing_ImageView* iv, const zxing_ReaderOptions* opts) +{ + auto res = ReadBarcodes(*iv, *opts); + return !res.empty() ? new Results(std::move(res)) : NULL; +} - void zxing_Results_delete(zxing_Results* results) - { - delete results; - } +void zxing_Result_delete(zxing_Result* result) +{ + delete result; +} - int zxing_Results_size(const zxing_Results* results) - { - return results ? Size(*results) : 0; - } +void zxing_Results_delete(zxing_Results* results) +{ + delete results; +} - const zxing_Result* zxing_Results_at(const zxing_Results* results, int i) - { - if (!results || i < 0 || i >= Size(*results)) - return NULL; - return &(*results)[i]; - } +int zxing_Results_size(const zxing_Results* results) +{ + return results ? Size(*results) : 0; } + +const zxing_Result* zxing_Results_at(const zxing_Results* results, int i) +{ + if (!results || i < 0 || i >= Size(*results)) + return NULL; + return &(*results)[i]; +} + +} // extern "C" diff --git a/wrappers/c/zxing-c.h b/wrappers/c/zxing-c.h index daded89240..636dd433b4 100644 --- a/wrappers/c/zxing-c.h +++ b/wrappers/c/zxing-c.h @@ -12,12 +12,12 @@ #ifdef __cplusplus -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "ImageView.h" #include "Result.h" typedef ZXing::ImageView zxing_ImageView; -typedef ZXing::DecodeHints zxing_DecodeHints; +typedef ZXing::ReaderOptions zxing_ReaderOptions; typedef ZXing::Result zxing_Result; typedef ZXing::Results zxing_Results; @@ -26,156 +26,159 @@ extern "C" #else typedef struct zxing_ImageView zxing_ImageView; -typedef struct zxing_DecodeHints zxing_DecodeHints; +typedef struct zxing_ReaderOptions zxing_ReaderOptions; typedef struct zxing_Result zxing_Result; typedef struct zxing_Results zxing_Results; #endif - /* - * ZXing/ImageView.h - */ - - typedef enum - { - zxing_ImageFormat_None = 0, - zxing_ImageFormat_Lum = 0x01000000, - zxing_ImageFormat_RGB = 0x03000102, - zxing_ImageFormat_BGR = 0x03020100, - zxing_ImageFormat_RGBX = 0x04000102, - zxing_ImageFormat_XRGB = 0x04010203, - zxing_ImageFormat_BGRX = 0x04020100, - zxing_ImageFormat_XBGR = 0x04030201, - } zxing_ImageFormat; - - zxing_ImageView* zxing_ImageView_new(const uint8_t* data, int width, int height, zxing_ImageFormat format, int rowStride, - int pixStride); - void zxing_ImageView_delete(zxing_ImageView* iv); - - /* - * ZXing/BarcodeFormat.h - */ - - typedef enum - { - zxing_BarcodeFormat_None = 0, - zxing_BarcodeFormat_Aztec = (1 << 0), - zxing_BarcodeFormat_Codabar = (1 << 1), - zxing_BarcodeFormat_Code39 = (1 << 2), - zxing_BarcodeFormat_Code93 = (1 << 3), - zxing_BarcodeFormat_Code128 = (1 << 4), - zxing_BarcodeFormat_DataBar = (1 << 5), - zxing_BarcodeFormat_DataBarExpanded = (1 << 6), - zxing_BarcodeFormat_DataMatrix = (1 << 7), - zxing_BarcodeFormat_EAN8 = (1 << 8), - zxing_BarcodeFormat_EAN13 = (1 << 9), - zxing_BarcodeFormat_ITF = (1 << 10), - zxing_BarcodeFormat_MaxiCode = (1 << 11), - zxing_BarcodeFormat_PDF417 = (1 << 12), - zxing_BarcodeFormat_QRCode = (1 << 13), - zxing_BarcodeFormat_UPCA = (1 << 14), - zxing_BarcodeFormat_UPCE = (1 << 15), - zxing_BarcodeFormat_MicroQRCode = (1 << 16), - - zxing_BarcodeFormat_LinearCodes = zxing_BarcodeFormat_Codabar | zxing_BarcodeFormat_Code39 | zxing_BarcodeFormat_Code93 - | zxing_BarcodeFormat_Code128 | zxing_BarcodeFormat_EAN8 | zxing_BarcodeFormat_EAN13 - | zxing_BarcodeFormat_ITF | zxing_BarcodeFormat_DataBar | zxing_BarcodeFormat_DataBarExpanded - | zxing_BarcodeFormat_UPCA | zxing_BarcodeFormat_UPCE, - zxing_BarcodeFormat_MatrixCodes = zxing_BarcodeFormat_Aztec | zxing_BarcodeFormat_DataMatrix | zxing_BarcodeFormat_MaxiCode - | zxing_BarcodeFormat_PDF417 | zxing_BarcodeFormat_QRCode | zxing_BarcodeFormat_MicroQRCode, - zxing_BarcodeFormat_Any = zxing_BarcodeFormat_LinearCodes | zxing_BarcodeFormat_MatrixCodes, - - zxing_BarcodeFormat_Invalid = -1 /* return value when BarcodeFormatsFromString() throws */ - } zxing_BarcodeFormat; - - typedef zxing_BarcodeFormat zxing_BarcodeFormats; - - zxing_BarcodeFormats zxing_BarcodeFormatsFromString(const char* str); - zxing_BarcodeFormat zxing_BarcodeFormatFromString(const char* str); - char* zxing_BarcodeFormatToString(zxing_BarcodeFormat format); - - /* - * ZXing/DecodeHints.h - */ - - typedef enum - { - zxing_Binarizer_LocalAverage, - zxing_Binarizer_GlobalHistogram, - zxing_Binarizer_FixedThreshold, - zxing_Binarizer_BoolCast, - } zxing_Binarizer; - - typedef enum - { - zxing_EanAddOnSymbol_Ignore, - zxing_EanAddOnSymbol_Read, - zxing_EanAddOnSymbol_Require, - } zxing_EanAddOnSymbol; - - typedef enum - { - zxing_TextMode_Plain, - zxing_TextMode_ECI, - zxing_TextMode_HRI, - zxing_TextMode_Hex, - zxing_TextMode_Escaped, - } zxing_TextMode; - - zxing_DecodeHints* zxing_DecodeHints_new(); - void zxing_DecodeHints_delete(zxing_DecodeHints* hints); - - void zxing_DecodeHints_setTryHarder(zxing_DecodeHints* hints, bool tryHarder); - void zxing_DecodeHints_setTryRotate(zxing_DecodeHints* hints, bool tryRotate); - void zxing_DecodeHints_setTryInvert(zxing_DecodeHints* hints, bool tryInvert); - void zxing_DecodeHints_setTryDownscale(zxing_DecodeHints* hints, bool tryDownscale); - void zxing_DecodeHints_setIsPure(zxing_DecodeHints* hints, bool isPure); - void zxing_DecodeHints_setReturnErrors(zxing_DecodeHints* hints, bool returnErrors); - void zxing_DecodeHints_setFormats(zxing_DecodeHints* hints, zxing_BarcodeFormats formats); - void zxing_DecodeHints_setBinarizer(zxing_DecodeHints* hints, zxing_Binarizer binarizer); - void zxing_DecodeHints_setEanAddOnSymbol(zxing_DecodeHints* hints, zxing_EanAddOnSymbol eanAddOnSymbol); - void zxing_DecodeHints_setTextMode(zxing_DecodeHints* hints, zxing_TextMode textMode); - - /* - * ZXing/Result.h - */ - - typedef enum - { - zxing_ContentType_Text, - zxing_ContentType_Binary, - zxing_ContentType_Mixed, - zxing_ContentType_GS1, - zxing_ContentType_ISO15434, - zxing_ContentType_UnknownECI - } zxing_ContentType; - - char* zxing_ContentTypeToString(zxing_ContentType type); - - bool zxing_Result_isValid(const zxing_Result* result); - char* zxing_Result_errorMsg(const zxing_Result* result); - zxing_BarcodeFormat zxing_Result_format(const zxing_Result* result); - zxing_ContentType zxing_Result_contentType(const zxing_Result* result); - uint8_t* zxing_Result_bytes(const zxing_Result* result, int* len); - char* zxing_Result_text(const zxing_Result* result); - char* zxing_Result_ecLevel(const zxing_Result* result); - char* zxing_Result_symbologyIdentifier(const zxing_Result* result); - int zxing_Result_orientation(const zxing_Result* result); - bool zxing_Result_isInverted(const zxing_Result* result); - bool zxing_Result_isMirrored(const zxing_Result* result); - - /* - * ZXing/ReadBarcode.h - */ - - zxing_Result* zxing_ReadBarcode(const zxing_ImageView* iv, const zxing_DecodeHints* hints); - zxing_Results* zxing_ReadBarcodes(const zxing_ImageView* iv, const zxing_DecodeHints* hints); - - void zxing_Result_delete(zxing_Result* result); - void zxing_Results_delete(zxing_Results* results); - - int zxing_Results_size(const zxing_Results* results); - const zxing_Result* zxing_Results_at(const zxing_Results* results, int i); +/* + * ZXing/ImageView.h + */ + +typedef enum +{ + zxing_ImageFormat_None = 0, + zxing_ImageFormat_Lum = 0x01000000, + zxing_ImageFormat_RGB = 0x03000102, + zxing_ImageFormat_BGR = 0x03020100, + zxing_ImageFormat_RGBX = 0x04000102, + zxing_ImageFormat_XRGB = 0x04010203, + zxing_ImageFormat_BGRX = 0x04020100, + zxing_ImageFormat_XBGR = 0x04030201, +} zxing_ImageFormat; + +zxing_ImageView* zxing_ImageView_new(const uint8_t* data, int width, int height, zxing_ImageFormat format, int rowStride, + int pixStride); +void zxing_ImageView_delete(zxing_ImageView* iv); + +/* + * ZXing/BarcodeFormat.h + */ + +typedef enum +{ + zxing_BarcodeFormat_None = 0, + zxing_BarcodeFormat_Aztec = (1 << 0), + zxing_BarcodeFormat_Codabar = (1 << 1), + zxing_BarcodeFormat_Code39 = (1 << 2), + zxing_BarcodeFormat_Code93 = (1 << 3), + zxing_BarcodeFormat_Code128 = (1 << 4), + zxing_BarcodeFormat_DataBar = (1 << 5), + zxing_BarcodeFormat_DataBarExpanded = (1 << 6), + zxing_BarcodeFormat_DataMatrix = (1 << 7), + zxing_BarcodeFormat_EAN8 = (1 << 8), + zxing_BarcodeFormat_EAN13 = (1 << 9), + zxing_BarcodeFormat_ITF = (1 << 10), + zxing_BarcodeFormat_MaxiCode = (1 << 11), + zxing_BarcodeFormat_PDF417 = (1 << 12), + zxing_BarcodeFormat_QRCode = (1 << 13), + zxing_BarcodeFormat_UPCA = (1 << 14), + zxing_BarcodeFormat_UPCE = (1 << 15), + zxing_BarcodeFormat_MicroQRCode = (1 << 16), + zxing_BarcodeFormat_RMQRCode = (1 << 17), + + zxing_BarcodeFormat_LinearCodes = zxing_BarcodeFormat_Codabar | zxing_BarcodeFormat_Code39 | zxing_BarcodeFormat_Code93 + | zxing_BarcodeFormat_Code128 | zxing_BarcodeFormat_EAN8 | zxing_BarcodeFormat_EAN13 + | zxing_BarcodeFormat_ITF | zxing_BarcodeFormat_DataBar | zxing_BarcodeFormat_DataBarExpanded + | zxing_BarcodeFormat_UPCA | zxing_BarcodeFormat_UPCE, + zxing_BarcodeFormat_MatrixCodes = zxing_BarcodeFormat_Aztec | zxing_BarcodeFormat_DataMatrix | zxing_BarcodeFormat_MaxiCode + | zxing_BarcodeFormat_PDF417 | zxing_BarcodeFormat_QRCode | zxing_BarcodeFormat_MicroQRCode + | zxing_BarcodeFormat_RMQRCode, + zxing_BarcodeFormat_Any = zxing_BarcodeFormat_LinearCodes | zxing_BarcodeFormat_MatrixCodes, + + zxing_BarcodeFormat_Invalid = -1 /* return value when BarcodeFormatsFromString() throws */ +} zxing_BarcodeFormat; + +typedef zxing_BarcodeFormat zxing_BarcodeFormats; + +zxing_BarcodeFormats zxing_BarcodeFormatsFromString(const char* str); +zxing_BarcodeFormat zxing_BarcodeFormatFromString(const char* str); +char* zxing_BarcodeFormatToString(zxing_BarcodeFormat format); + +/* + * ZXing/ReaderOptions.h + */ + +typedef enum +{ + zxing_Binarizer_LocalAverage, + zxing_Binarizer_GlobalHistogram, + zxing_Binarizer_FixedThreshold, + zxing_Binarizer_BoolCast, +} zxing_Binarizer; + +typedef enum +{ + zxing_EanAddOnSymbol_Ignore, + zxing_EanAddOnSymbol_Read, + zxing_EanAddOnSymbol_Require, +} zxing_EanAddOnSymbol; + +typedef enum +{ + zxing_TextMode_Plain, + zxing_TextMode_ECI, + zxing_TextMode_HRI, + zxing_TextMode_Hex, + zxing_TextMode_Escaped, +} zxing_TextMode; + +zxing_ReaderOptions* zxing_ReaderOptions_new(); +void zxing_ReaderOptions_delete(zxing_ReaderOptions* opts); + +void zxing_ReaderOptions_setTryHarder(zxing_ReaderOptions* opts, bool tryHarder); +void zxing_ReaderOptions_setTryRotate(zxing_ReaderOptions* opts, bool tryRotate); +void zxing_ReaderOptions_setTryInvert(zxing_ReaderOptions* opts, bool tryInvert); +void zxing_ReaderOptions_setTryDownscale(zxing_ReaderOptions* opts, bool tryDownscale); +void zxing_ReaderOptions_setIsPure(zxing_ReaderOptions* opts, bool isPure); +void zxing_ReaderOptions_setReturnErrors(zxing_ReaderOptions* opts, bool returnErrors); +void zxing_ReaderOptions_setFormats(zxing_ReaderOptions* opts, zxing_BarcodeFormats formats); +void zxing_ReaderOptions_setBinarizer(zxing_ReaderOptions* opts, zxing_Binarizer binarizer); +void zxing_ReaderOptions_setEanAddOnSymbol(zxing_ReaderOptions* opts, zxing_EanAddOnSymbol eanAddOnSymbol); +void zxing_ReaderOptions_setTextMode(zxing_ReaderOptions* opts, zxing_TextMode textMode); +void zxing_ReaderOptions_setMaxNumberOfSymbols(zxing_ReaderOptions* opts, int n); + +/* + * ZXing/Result.h + */ + +typedef enum +{ + zxing_ContentType_Text, + zxing_ContentType_Binary, + zxing_ContentType_Mixed, + zxing_ContentType_GS1, + zxing_ContentType_ISO15434, + zxing_ContentType_UnknownECI +} zxing_ContentType; + +char* zxing_ContentTypeToString(zxing_ContentType type); + +bool zxing_Result_isValid(const zxing_Result* result); +char* zxing_Result_errorMsg(const zxing_Result* result); +zxing_BarcodeFormat zxing_Result_format(const zxing_Result* result); +zxing_ContentType zxing_Result_contentType(const zxing_Result* result); +uint8_t* zxing_Result_bytes(const zxing_Result* result, int* len); +char* zxing_Result_text(const zxing_Result* result); +char* zxing_Result_ecLevel(const zxing_Result* result); +char* zxing_Result_symbologyIdentifier(const zxing_Result* result); +int zxing_Result_orientation(const zxing_Result* result); +bool zxing_Result_isInverted(const zxing_Result* result); +bool zxing_Result_isMirrored(const zxing_Result* result); + +/* + * ZXing/ReadBarcode.h + */ + +zxing_Result* zxing_ReadBarcode(const zxing_ImageView* iv, const zxing_ReaderOptions* opts); +zxing_Results* zxing_ReadBarcodes(const zxing_ImageView* iv, const zxing_ReaderOptions* opts); + +void zxing_Result_delete(zxing_Result* result); +void zxing_Results_delete(zxing_Results* results); + +int zxing_Results_size(const zxing_Results* results); +const zxing_Result* zxing_Results_at(const zxing_Results* results, int i); #ifdef __cplusplus } diff --git a/wrappers/ios/.gitignore b/wrappers/ios/.gitignore index be5558caa1..9bce6af399 100644 --- a/wrappers/ios/.gitignore +++ b/wrappers/ios/.gitignore @@ -1,5 +1 @@ -.swiftpm -_builds -.build xcuserdata -ZXingCpp.xcframework diff --git a/wrappers/ios/Info.plist b/wrappers/ios/Info.plist deleted file mode 100644 index 0433f478b4..0000000000 Binary files a/wrappers/ios/Info.plist and /dev/null differ diff --git a/wrappers/ios/Package.swift b/wrappers/ios/Package.swift deleted file mode 100644 index 7e3352acd7..0000000000 --- a/wrappers/ios/Package.swift +++ /dev/null @@ -1,31 +0,0 @@ -// swift-tools-version:5.3 -import PackageDescription - -let package = Package( - name: "ZXingCppWrapper", - platforms: [ - .iOS(.v11) - ], - products: [ - .library( - name: "ZXingCppWrapper", - type: .static, - targets: ["ZXingCppWrapper"]) - ], - targets: [ - .binaryTarget( - name: "ZXingCpp", - path: "ZXingCpp.xcframework" - ), - .target( - name: "ZXingCppWrapper", - dependencies: ["ZXingCpp"], - path: "Sources/Wrapper", - publicHeadersPath: ".", - cxxSettings: [ - .unsafeFlags(["-stdlib=libc++"]), - .unsafeFlags(["-std=gnu++17"]) - ] - ) - ] -) diff --git a/wrappers/ios/README.md b/wrappers/ios/README.md index 44aae4bf41..c6b6bfd1ba 100644 --- a/wrappers/ios/README.md +++ b/wrappers/ios/README.md @@ -1,14 +1,25 @@ # ZXingCpp iOS Framework -To use the iOS (wrapper) framework in other apps, it is easiest -to build the library project and include the resulting xcframework -file in your app. +## Installation -## How to build and use +### SwiftPM -To build the xcframework: +As this repository provides a `Package.swift` on the root level, so you can add `zxing-cpp` including wrapper code by adding a Package Dependency in Xcode. - $ ./build-release.sh - -Then you can add the iOS Wrapper as a local Swift Package by adding it as a dependency to your app. -Don't forget to add the wrapper to the `Frameworks, Libraries, and Embedded Content` section within the `General` tab. +An alternative way is to check this repository out and add it as a local Swift Package by adding it as a dependency to your app. + +### CocoaPods + +You can also use [CocoaPods](https://cocoapods.org/pods/zxing-cpp). Just add the Pod as a dependency: + +``` +pod 'zxing-cpp' +``` +The module to be imported is named `ZXingCpp`. If you just need the core without the wrapper code, you can use: +``` +pod 'zxing-cpp/Core' +``` + +## Usage + +For general usage of the ObjectiveC/Swift wrapper, please have a look at the source code provided in the [demo project](demo). diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIBarcodeReader.h b/wrappers/ios/Sources/Wrapper/Reader/ZXIBarcodeReader.h index f2cca0ba21..153abcdebb 100644 --- a/wrappers/ios/Sources/Wrapper/Reader/ZXIBarcodeReader.h +++ b/wrappers/ios/Sources/Wrapper/Reader/ZXIBarcodeReader.h @@ -2,20 +2,28 @@ // // SPDX-License-Identifier: Apache-2.0 +#import #import #import #import "ZXIResult.h" -#import "ZXIDecodeHints.h" +#import "ZXIReaderOptions.h" NS_ASSUME_NONNULL_BEGIN @interface ZXIBarcodeReader : NSObject -@property(nonatomic, strong) ZXIDecodeHints *hints; +@property(nonatomic, strong) ZXIReaderOptions *options; + +-(instancetype)initWithOptions:(ZXIReaderOptions*)options; + +-(nullable NSArray *)readCIImage:(nonnull CIImage *)image + error:(NSError *__autoreleasing _Nullable *)error; + +-(nullable NSArray *)readCGImage:(nonnull CGImageRef)image + error:(NSError *__autoreleasing _Nullable *)error; + +-(nullable NSArray *)readCVPixelBuffer:(nonnull CVPixelBufferRef)pixelBuffer + error:(NSError *__autoreleasing _Nullable *)error; -- (instancetype)initWithHints:(ZXIDecodeHints*)options; -- (NSArray *)readCIImage:(nonnull CIImage *)image; -- (NSArray *)readCGImage:(nonnull CGImageRef)image; -- (NSArray *)readCVPixelBuffer:(nonnull CVPixelBufferRef)pixelBuffer; @end NS_ASSUME_NONNULL_END diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIBarcodeReader.mm b/wrappers/ios/Sources/Wrapper/Reader/ZXIBarcodeReader.mm index fbf1eaeb7f..4b21c81bf3 100644 --- a/wrappers/ios/Sources/Wrapper/Reader/ZXIBarcodeReader.mm +++ b/wrappers/ios/Sources/Wrapper/Reader/ZXIBarcodeReader.mm @@ -3,14 +3,37 @@ // SPDX-License-Identifier: Apache-2.0 #import "ZXIBarcodeReader.h" -#import "ZXing/ReadBarcode.h" -#import "ZXing/ImageView.h" -#import "ZXing/Result.h" +#import "ReadBarcode.h" +#import "ImageView.h" +#import "Result.h" +#import "GTIN.h" #import "ZXIFormatHelper.h" #import "ZXIPosition+Helper.h" +#import "ZXIErrors.h" using namespace ZXing; +NSString *stringToNSString(const std::string &text) { + return [[NSString alloc]initWithBytes:text.data() length:text.size() encoding:NSUTF8StringEncoding]; +} + +ZXIGTIN *getGTIN(const Result &result) { + try { + auto country = GTIN::LookupCountryIdentifier(result.text(TextMode::Plain), result.format()); + auto addOn = GTIN::EanAddOn(result); + return country.empty() + ? nullptr + : [[ZXIGTIN alloc]initWithCountry:stringToNSString(country) + addOn:stringToNSString(addOn) + price:stringToNSString(GTIN::Price(addOn)) + issueNumber:stringToNSString(GTIN::IssueNr(addOn))]; + } catch (std::exception e) { + // Because invalid GTIN data can lead to exceptions, in which case + // we don't want to discard the whole result. + return nullptr; + } +} + @interface ZXIBarcodeReader() @property (nonatomic, strong) CIContext* ciContext; @end @@ -18,17 +41,18 @@ @interface ZXIBarcodeReader() @implementation ZXIBarcodeReader - (instancetype)init { - return [self initWithHints: [[ZXIDecodeHints alloc] init]]; + return [self initWithOptions: [[ZXIReaderOptions alloc] init]]; } -- (instancetype)initWithHints:(ZXIDecodeHints*)hints{ +- (instancetype)initWithOptions:(ZXIReaderOptions*)options{ self = [super init]; self.ciContext = [[CIContext alloc] initWithOptions:@{kCIContextWorkingColorSpace: [NSNull new]}]; - self.hints = hints; + self.options = options; return self; } -- (NSArray *)readCVPixelBuffer:(nonnull CVPixelBufferRef)pixelBuffer { +- (NSArray *)readCVPixelBuffer:(nonnull CVPixelBufferRef)pixelBuffer + error:(NSError *__autoreleasing _Nullable *)error { OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer); // We tried to work with all luminance based formats listed in kCVPixelFormatType @@ -48,24 +72,26 @@ - (instancetype)initWithHints:(ZXIDecodeHints*)hints{ ImageFormat::Lum, static_cast(bytesPerRow), 0); - NSArray* results = [self readImageView:imageView]; + NSArray* results = [self readImageView:imageView error:error]; CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); return results; } // If given pixel format is not a supported type with a luminance channel we just use the // default method - return [self readCIImage:[[CIImage alloc] initWithCVImageBuffer:pixelBuffer]]; + return [self readCIImage:[[CIImage alloc] initWithCVImageBuffer:pixelBuffer] error:error]; } -- (NSArray *)readCIImage:(nonnull CIImage *)image { +- (NSArray *)readCIImage:(nonnull CIImage *)image + error:(NSError *__autoreleasing _Nullable *)error { CGImageRef cgImage = [self.ciContext createCGImage:image fromRect:image.extent]; - auto results = [self readCGImage:cgImage]; + auto results = [self readCGImage:cgImage error:error]; CGImageRelease(cgImage); return results; } -- (NSArray *)readCGImage: (nonnull CGImageRef)image { +- (NSArray *)readCGImage:(nonnull CGImageRef)image + error:(NSError *__autoreleasing _Nullable *)error { CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericGray); CGFloat cols = CGImageGetWidth(image); CGFloat rows = CGImageGetHeight(image); @@ -88,45 +114,54 @@ - (instancetype)initWithHints:(ZXIDecodeHints*)hints{ static_cast(cols), static_cast(rows), ImageFormat::Lum); - return [self readImageView:imageView]; + return [self readImageView:imageView error:error]; } -+ (DecodeHints)DecodeHintsFromZXIOptions:(ZXIDecodeHints*)hints { ++ (ReaderOptions)ReaderOptionsFromZXIReaderOptions:(ZXIReaderOptions*)options { BarcodeFormats formats; - for(NSNumber* flag in hints.formats) { + for(NSNumber* flag in options.formats) { formats.setFlag(BarcodeFormatFromZXIFormat((ZXIFormat)flag.integerValue)); } - DecodeHints resultingHints = DecodeHints() - .setTryRotate(hints.tryRotate) - .setTryHarder(hints.tryHarder) - .setTryInvert(hints.tryInvert) - .setTryDownscale(hints.tryDownscale) - .setTryCode39ExtendedMode(hints.tryCode39ExtendedMode) - .setValidateCode39CheckSum(hints.validateCode39CheckSum) - .setValidateITFCheckSum(hints.validateITFCheckSum) - + ReaderOptions cppOpts = ReaderOptions() .setFormats(formats) - .setMaxNumberOfSymbols(hints.maxNumberOfSymbols); - return resultingHints; + .setTryRotate(options.tryRotate) + .setTryHarder(options.tryHarder) + .setTryInvert(options.tryInvert) + .setTryDownscale(options.tryDownscale) + .setTryCode39ExtendedMode(options.tryCode39ExtendedMode) + .setValidateCode39CheckSum(options.validateCode39CheckSum) + .setValidateITFCheckSum(options.validateITFCheckSum) + .setMaxNumberOfSymbols(options.maxNumberOfSymbols); + return cppOpts; } -- (NSArray *)readImageView: (ImageView)imageView { - Results results = ReadBarcodes(imageView, [ZXIBarcodeReader DecodeHintsFromZXIOptions:self.hints]); - - NSMutableArray* zxiResults = [NSMutableArray array]; - for (auto result: results) { - auto resultText = result.text(); - NSString *text = [[NSString alloc]initWithBytes:resultText.data() length:resultText.size() encoding:NSUTF8StringEncoding]; - - NSData *bytes = [[NSData alloc] initWithBytes:result.bytes().data() length:result.bytes().size()]; - [zxiResults addObject: - [[ZXIResult alloc] init:text - format:ZXIFormatFromBarcodeFormat(result.format()) - bytes:bytes - position:[[ZXIPosition alloc]initWithPosition: result.position()] - ]]; +- (NSArray *)readImageView:(ImageView)imageView + error:(NSError *__autoreleasing _Nullable *)error { + try { + Results results = ReadBarcodes(imageView, [ZXIBarcodeReader ReaderOptionsFromZXIReaderOptions:self.options]); + NSMutableArray* zxiResults = [NSMutableArray array]; + for (auto result: results) { + [zxiResults addObject: + [[ZXIResult alloc] init:stringToNSString(result.text()) + format:ZXIFormatFromBarcodeFormat(result.format()) + bytes:[[NSData alloc] initWithBytes:result.bytes().data() length:result.bytes().size()] + position:[[ZXIPosition alloc]initWithPosition: result.position()] + orientation:result.orientation() + ecLevel:stringToNSString(result.ecLevel()) + symbologyIdentifier:stringToNSString(result.symbologyIdentifier()) + sequenceSize:result.sequenceSize() + sequenceIndex:result.sequenceIndex() + sequenceId:stringToNSString(result.sequenceId()) + readerInit:result.readerInit() + lineCount:result.lineCount() + gtin:getGTIN(result)] + ]; + } + return zxiResults; + } catch(std::exception &e) { + SetNSError(error, ZXIReaderError, e.what()); + return nil; } - return zxiResults; } @end diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIDecodeHints.mm b/wrappers/ios/Sources/Wrapper/Reader/ZXIDecodeHints.mm deleted file mode 100644 index 9a50235c0d..0000000000 --- a/wrappers/ios/Sources/Wrapper/Reader/ZXIDecodeHints.mm +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2022 KURZ Digital Solutions GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -#import "ZXIDecodeHints.h" -#import "ZXing/DecodeHints.h" - -@interface ZXIDecodeHints() -@property(nonatomic) ZXing::DecodeHints zxingHints; -@end - -@implementation ZXIDecodeHints - --(instancetype)init { - self = [super init]; - self.zxingHints = ZXing::DecodeHints(); - return self; -} - -- (instancetype)initWithTryHarder:(BOOL)tryHarder - tryRotate:(BOOL)tryRotate - tryDownscale:(BOOL)tryDownscale - maxNumberOfSymbols:(NSInteger)maxNumberOfSymbols - tryInvert:(BOOL)tryInvert - tryCode39ExtendedMode:(BOOL)tryCode39ExtendedMode - validateCode39CheckSum:(BOOL)validateCode39CheckSum - validateITFCheckSum:(BOOL)validateITFCheckSum - downscaleFactor:(uint8_t)downscaleFactor - downscaleThreshold:(uint16_t)downscaleThreshold - formats:(NSArray*)formats { - self = [super init]; - self.zxingHints = ZXing::DecodeHints(); - self.tryHarder = tryHarder; - self.tryRotate = tryRotate; - self.tryDownscale = tryDownscale; - self.maxNumberOfSymbols = maxNumberOfSymbols; - self.tryInvert = tryInvert; - self.tryCode39ExtendedMode = tryCode39ExtendedMode; - self.validateCode39CheckSum = validateCode39CheckSum; - self.validateITFCheckSum = validateITFCheckSum; - self.downscaleFactor = downscaleFactor; - self.downscaleThreshold = downscaleThreshold; - self.formats = formats; - return self; -} - --(void)setMaxNumberOfSymbols:(NSInteger)maxNumberOfSymbols { - self.zxingHints.setMaxNumberOfSymbols(maxNumberOfSymbols); -} - --(void)setTryHarder:(BOOL)tryHarder { - self.zxingHints.setTryHarder(tryHarder); -} - --(void)setTryrotate:(BOOL)tryRotate { - self.zxingHints.setTryRotate(tryRotate); -} - --(void)setTrydownscale:(BOOL)tryDownscale { - self.zxingHints.setTryDownscale(tryDownscale); -} - --(void)setTryinvert:(BOOL)tryInvert { - self.zxingHints.setTryInvert(tryInvert); -} - --(void)setTrycode39Extendedmode:(BOOL)tryCode39ExtendedMode { - self.zxingHints.setTryCode39ExtendedMode(tryCode39ExtendedMode); -} - --(void)setValidatecode39Checksum:(BOOL)validateCode39CheckSum { - self.zxingHints.setValidateCode39CheckSum(validateCode39CheckSum); -} - --(void)setValidateitfchecksum:(BOOL)validateITFCheckSum { - self.zxingHints.setValidateITFCheckSum(validateITFCheckSum); -} - --(void)setDownscalefactor:(uint8_t)downscaleFactor { - self.zxingHints.setDownscaleFactor(downscaleFactor); -} - --(void)setDownscalethreshold:(uint16_t)downscaleThreshold { - self.zxingHints.setDownscaleThreshold(downscaleThreshold); -} - -- (NSInteger)maxNumberOfSymbols { - return self.zxingHints.maxNumberOfSymbols(); -} - --(BOOL)tryHarder { - return self.zxingHints.tryHarder(); -} - --(BOOL)tryRotate { - return self.zxingHints.tryRotate(); -} - --(BOOL)tryDownscale { - return self.zxingHints.tryDownscale(); -} - --(BOOL)tryInvert { - return self.zxingHints.tryInvert(); -} - --(BOOL)tryCode39ExtendedMode { - return self.zxingHints.tryCode39ExtendedMode(); -} - --(BOOL)validateCode39CheckSum { - return self.zxingHints.validateCode39CheckSum(); -} - --(BOOL)validateITFCheckSum { - return self.zxingHints.validateITFCheckSum(); -} - --(uint8_t)downscaleFactor { - return self.zxingHints.downscaleFactor(); -} - --(uint16_t)downscaleThreshold { - return self.zxingHints.downscaleThreshold(); -} - -@end diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIGTIN.h b/wrappers/ios/Sources/Wrapper/Reader/ZXIGTIN.h new file mode 100644 index 0000000000..0992d845ff --- /dev/null +++ b/wrappers/ios/Sources/Wrapper/Reader/ZXIGTIN.h @@ -0,0 +1,22 @@ +// Copyright 2022 KURZ Digital Solutions GmbH +// +// SPDX-License-Identifier: Apache-2.0 + + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ZXIGTIN : NSObject +@property(nonatomic, nonnull)NSString *country; +@property(nonatomic, nonnull)NSString *addOn; +@property(nonatomic, nonnull)NSString *price; +@property(nonatomic, nonnull)NSString *issueNumber; + +- (instancetype)initWithCountry:(NSString *)country + addOn:(NSString *)addOn + price:(NSString *)price + issueNumber:(NSString *)issueNumber; +@end + +NS_ASSUME_NONNULL_END diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIGTIN.mm b/wrappers/ios/Sources/Wrapper/Reader/ZXIGTIN.mm new file mode 100644 index 0000000000..c0d801d0b6 --- /dev/null +++ b/wrappers/ios/Sources/Wrapper/Reader/ZXIGTIN.mm @@ -0,0 +1,20 @@ +// Copyright 2022 KURZ Digital Solutions GmbH +// +// SPDX-License-Identifier: Apache-2.0 + + +#import "ZXIGTIN.h" + +@implementation ZXIGTIN +- (instancetype)initWithCountry:(NSString *)country + addOn:(NSString *)addOn + price:(NSString *)price + issueNumber:(NSString *)issueNumber { + self = [super init]; + self.country = country; + self.addOn = addOn; + self.price = price; + self.issueNumber = issueNumber; + return self; +} +@end diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIPosition+Helper.h b/wrappers/ios/Sources/Wrapper/Reader/ZXIPosition+Helper.h index 5224ea8d9c..93ec83b8d8 100644 --- a/wrappers/ios/Sources/Wrapper/Reader/ZXIPosition+Helper.h +++ b/wrappers/ios/Sources/Wrapper/Reader/ZXIPosition+Helper.h @@ -4,7 +4,7 @@ #import #import "ZXIPosition.h" -#import "ZXing/Result.h" +#import "Result.h" NS_ASSUME_NONNULL_BEGIN diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIDecodeHints.h b/wrappers/ios/Sources/Wrapper/Reader/ZXIReaderOptions.h similarity index 84% rename from wrappers/ios/Sources/Wrapper/Reader/ZXIDecodeHints.h rename to wrappers/ios/Sources/Wrapper/Reader/ZXIReaderOptions.h index 5edd6625e5..cae964c2ff 100644 --- a/wrappers/ios/Sources/Wrapper/Reader/ZXIDecodeHints.h +++ b/wrappers/ios/Sources/Wrapper/Reader/ZXIReaderOptions.h @@ -6,32 +6,31 @@ NS_ASSUME_NONNULL_BEGIN -@interface ZXIDecodeHints : NSObject +@interface ZXIReaderOptions : NSObject +/// An array of ZXIFormat +@property(nonatomic, strong) NSArray *formats; @property(nonatomic) BOOL tryHarder; @property(nonatomic) BOOL tryRotate; -@property(nonatomic) BOOL tryDownscale; @property(nonatomic) BOOL tryInvert; +@property(nonatomic) BOOL tryDownscale; @property(nonatomic) BOOL tryCode39ExtendedMode; @property(nonatomic) BOOL validateCode39CheckSum; @property(nonatomic) BOOL validateITFCheckSum; @property(nonatomic) uint8_t downscaleFactor; @property(nonatomic) uint16_t downscaleThreshold; - @property(nonatomic) NSInteger maxNumberOfSymbols; -/// An array of ZXIFormat -@property(nonatomic, strong) NSArray *formats; -- (instancetype)initWithTryHarder:(BOOL)tryHarder +- (instancetype)initWithFormats:(NSArray*)formats + tryHarder:(BOOL)tryHarder tryRotate:(BOOL)tryRotate - tryDownscale:(BOOL)tryDownscale - maxNumberOfSymbols:(NSInteger)maxNumberOfSymbols tryInvert:(BOOL)tryInvert + tryDownscale:(BOOL)tryDownscale tryCode39ExtendedMode:(BOOL)tryCode39ExtendedMode validateCode39CheckSum:(BOOL)validateCode39CheckSum validateITFCheckSum:(BOOL)validateITFCheckSum downscaleFactor:(uint8_t)downscaleFactor downscaleThreshold:(uint16_t)downscaleThreshold - formats:(NSArray*)formats; + maxNumberOfSymbols:(NSInteger)maxNumberOfSymbols; @end NS_ASSUME_NONNULL_END diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIReaderOptions.mm b/wrappers/ios/Sources/Wrapper/Reader/ZXIReaderOptions.mm new file mode 100644 index 0000000000..7cfc8f7901 --- /dev/null +++ b/wrappers/ios/Sources/Wrapper/Reader/ZXIReaderOptions.mm @@ -0,0 +1,127 @@ +// Copyright 2022 KURZ Digital Solutions GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#import "ZXIReaderOptions.h" +#import "ReaderOptions.h" + +@interface ZXIReaderOptions() +@property(nonatomic) ZXing::ReaderOptions cppOpts; +@end + +@implementation ZXIReaderOptions + +-(instancetype)init { + self = [super init]; + self.cppOpts = ZXing::ReaderOptions(); + return self; +} + +- (instancetype)initWithFormats:(NSArray*)formats + tryHarder:(BOOL)tryHarder + tryRotate:(BOOL)tryRotate + tryInvert:(BOOL)tryInvert + tryDownscale:(BOOL)tryDownscale + tryCode39ExtendedMode:(BOOL)tryCode39ExtendedMode + validateCode39CheckSum:(BOOL)validateCode39CheckSum + validateITFCheckSum:(BOOL)validateITFCheckSum + downscaleFactor:(uint8_t)downscaleFactor + downscaleThreshold:(uint16_t)downscaleThreshold + maxNumberOfSymbols:(NSInteger)maxNumberOfSymbols { + self = [super init]; + self.cppOpts = ZXing::ReaderOptions(); + self.tryHarder = tryHarder; + self.tryRotate = tryRotate; + self.tryInvert = tryInvert; + self.tryDownscale = tryDownscale; + self.tryCode39ExtendedMode = tryCode39ExtendedMode; + self.validateCode39CheckSum = validateCode39CheckSum; + self.validateITFCheckSum = validateITFCheckSum; + self.downscaleFactor = downscaleFactor; + self.downscaleThreshold = downscaleThreshold; + self.maxNumberOfSymbols = maxNumberOfSymbols; + self.formats = formats; + return self; +} + +-(void)setTryHarder:(BOOL)tryHarder { + self.cppOpts.setTryHarder(tryHarder); +} + +-(void)setTryRotate:(BOOL)tryRotate { + self.cppOpts.setTryRotate(tryRotate); +} + +-(void)setTryInvert:(BOOL)tryInvert { + self.cppOpts.setTryInvert(tryInvert); +} + +-(void)setTryDownscale:(BOOL)tryDownscale { + self.cppOpts.setTryDownscale(tryDownscale); +} + +-(void)setTryCode39ExtendedMode:(BOOL)tryCode39ExtendedMode { + self.cppOpts.setTryCode39ExtendedMode(tryCode39ExtendedMode); +} + +-(void)setValidateCode39CheckSum:(BOOL)validateCode39CheckSum { + self.cppOpts.setValidateCode39CheckSum(validateCode39CheckSum); +} + +-(void)setValidateITFCheckSum:(BOOL)validateITFCheckSum { + self.cppOpts.setValidateITFCheckSum(validateITFCheckSum); +} + +-(void)setDownscaleFactor:(uint8_t)downscaleFactor { + self.cppOpts.setDownscaleFactor(downscaleFactor); +} + +-(void)setDownscaleThreshold:(uint16_t)downscaleThreshold { + self.cppOpts.setDownscaleThreshold(downscaleThreshold); +} + +-(void)setMaxNumberOfSymbols:(NSInteger)maxNumberOfSymbols { + self.cppOpts.setMaxNumberOfSymbols(maxNumberOfSymbols); +} + +-(BOOL)tryHarder { + return self.cppOpts.tryHarder(); +} + +-(BOOL)tryRotate { + return self.cppOpts.tryRotate(); +} + +-(BOOL)tryInvert { + return self.cppOpts.tryInvert(); +} + +-(BOOL)tryDownscale { + return self.cppOpts.tryDownscale(); +} + +-(BOOL)tryCode39ExtendedMode { + return self.cppOpts.tryCode39ExtendedMode(); +} + +-(BOOL)validateCode39CheckSum { + return self.cppOpts.validateCode39CheckSum(); +} + +-(BOOL)validateITFCheckSum { + return self.cppOpts.validateITFCheckSum(); +} + +-(uint8_t)downscaleFactor { + return self.cppOpts.downscaleFactor(); +} + +-(uint16_t)downscaleThreshold { + return self.cppOpts.downscaleThreshold(); +} + +- (NSInteger)maxNumberOfSymbols { + return self.cppOpts.maxNumberOfSymbols(); +} + +@end diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIResult.h b/wrappers/ios/Sources/Wrapper/Reader/ZXIResult.h index 83d8a4cffc..9c4c4e8a36 100644 --- a/wrappers/ios/Sources/Wrapper/Reader/ZXIResult.h +++ b/wrappers/ios/Sources/Wrapper/Reader/ZXIResult.h @@ -5,6 +5,7 @@ #import #import "ZXIFormat.h" #import "ZXIPosition.h" +#import "ZXIGTIN.h" NS_ASSUME_NONNULL_BEGIN @@ -13,11 +14,29 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong) NSData *bytes; @property(nonatomic, strong) ZXIPosition *position; @property(nonatomic) ZXIFormat format; +@property(nonatomic) NSInteger orientation; +@property(nonatomic, strong) NSString *ecLevel; +@property(nonatomic, strong) NSString *symbologyIdentifier; +@property(nonatomic) NSInteger sequenceSize; +@property(nonatomic) NSInteger sequenceIndex; +@property(nonatomic, strong) NSString *sequenceId; +@property(nonatomic) BOOL readerInit; +@property(nonatomic) NSInteger lineCount; +@property(nonatomic, strong) ZXIGTIN *gtin; - (instancetype)init:(NSString *)text format:(ZXIFormat)format bytes:(NSData *)bytes - position:(ZXIPosition *)position; + position:(ZXIPosition *)position + orientation:(NSInteger)orientation + ecLevel:(NSString *)ecLevel + symbologyIdentifier:(NSString *)symbologyIdentifier + sequenceSize:(NSInteger)sequenceSize + sequenceIndex:(NSInteger)sequenceIndex + sequenceId:(NSString *)sequenceId + readerInit:(BOOL)readerInit + lineCount:(NSInteger)lineCount + gtin:(ZXIGTIN *)gtin; @end NS_ASSUME_NONNULL_END diff --git a/wrappers/ios/Sources/Wrapper/Reader/ZXIResult.mm b/wrappers/ios/Sources/Wrapper/Reader/ZXIResult.mm index b84267d2a0..6a80a77d28 100644 --- a/wrappers/ios/Sources/Wrapper/Reader/ZXIResult.mm +++ b/wrappers/ios/Sources/Wrapper/Reader/ZXIResult.mm @@ -8,12 +8,30 @@ @implementation ZXIResult - (instancetype)init:(NSString *)text format:(ZXIFormat)format bytes:(NSData *)bytes - position:(ZXIPosition *)position { + position:(ZXIPosition *)position + orientation:(NSInteger)orientation + ecLevel:(NSString *)ecLevel + symbologyIdentifier:(NSString *)symbologyIdentifier + sequenceSize:(NSInteger)sequenceSize + sequenceIndex:(NSInteger)sequenceIndex + sequenceId:(NSString *)sequenceId + readerInit:(BOOL)readerInit + lineCount:(NSInteger)lineCount + gtin:(ZXIGTIN *)gtin { self = [super init]; self.text = text; self.format = format; self.bytes = bytes; self.position = position; + self.orientation = orientation; + self.ecLevel = ecLevel; + self.symbologyIdentifier = symbologyIdentifier; + self.sequenceSize = sequenceSize; + self.sequenceIndex = sequenceIndex; + self.sequenceId = sequenceId; + self.readerInit = readerInit; + self.lineCount = lineCount; + self.gtin = gtin; return self; } @end diff --git a/wrappers/ios/Sources/Wrapper/UmbrellaHeader.h b/wrappers/ios/Sources/Wrapper/UmbrellaHeader.h index 951357f673..7306d381dc 100644 --- a/wrappers/ios/Sources/Wrapper/UmbrellaHeader.h +++ b/wrappers/ios/Sources/Wrapper/UmbrellaHeader.h @@ -9,7 +9,9 @@ #import "Reader/ZXIResult.h" #import "Reader/ZXIPosition.h" #import "Reader/ZXIPoint.h" -#import "Reader/ZXIDecodeHints.h" +#import "Reader/ZXIGTIN.h" +#import "Reader/ZXIReaderOptions.h" +#import "Writer/ZXIWriterOptions.h" #import "Writer/ZXIBarcodeWriter.h" #import "ZXIErrors.h" #import "ZXIFormat.h" diff --git a/wrappers/ios/Sources/Wrapper/Writer/ZXIBarcodeWriter.h b/wrappers/ios/Sources/Wrapper/Writer/ZXIBarcodeWriter.h index 664ff3dd84..41b6515438 100644 --- a/wrappers/ios/Sources/Wrapper/Writer/ZXIBarcodeWriter.h +++ b/wrappers/ios/Sources/Wrapper/Writer/ZXIBarcodeWriter.h @@ -3,17 +3,21 @@ // SPDX-License-Identifier: Apache-2.0 #import -#import "ZXIFormat.h" +#import "ZXIWriterOptions.h" NS_ASSUME_NONNULL_BEGIN @interface ZXIBarcodeWriter : NSObject +@property(nonatomic, strong) ZXIWriterOptions *options; + +-(instancetype)initWithOptions:(ZXIWriterOptions*)options; + +-(nullable CGImageRef)writeString:(NSString *)contents + error:(NSError *__autoreleasing _Nullable *)error; + +-(nullable CGImageRef)writeData:(NSData *)data + error:(NSError *__autoreleasing _Nullable *)error; --(nullable CGImageRef)write:(NSString *)contents - width:(int)width - height:(int)height - format:(ZXIFormat)format - error:(NSError **)error; @end NS_ASSUME_NONNULL_END diff --git a/wrappers/ios/Sources/Wrapper/Writer/ZXIBarcodeWriter.mm b/wrappers/ios/Sources/Wrapper/Writer/ZXIBarcodeWriter.mm index 430fed9995..eafd885310 100644 --- a/wrappers/ios/Sources/Wrapper/Writer/ZXIBarcodeWriter.mm +++ b/wrappers/ios/Sources/Wrapper/Writer/ZXIBarcodeWriter.mm @@ -4,8 +4,10 @@ #import #import "ZXIBarcodeWriter.h" -#import "ZXing/MultiFormatWriter.h" -#import "ZXing/BitMatrix.h" +#import "ZXIWriterOptions.h" +#import "MultiFormatWriter.h" +#import "BitMatrix.h" +#import "BitMatrixIO.h" #import "ZXIFormatHelper.h" #import "ZXIErrors.h" #import @@ -18,76 +20,105 @@ sizeof(wchar_t)); } -#ifdef DEBUG -std::string ToString(const BitMatrix& matrix, char one, char zero, bool addSpace, bool printAsCString) -{ - std::string result; - result.reserve((addSpace ? 2 : 1) * (matrix.width() * matrix.height()) + matrix.height()); - for (int y = 0; y < matrix.height(); ++y) { - for (int x = 0; x < matrix.width(); ++x) { - result += matrix.get(x, y) ? one : zero; - if (addSpace) - result += ' '; - } - if (printAsCString) - result += "\\n\""; - result += '\n'; +std::wstring NSDataToStringW(NSData *data) { + std::wstring s; + const unsigned char *bytes = (const unsigned char *) [data bytes]; + size_t len = [data length]; + for (int i = 0; i < len; ++i) { + s.push_back(bytes[i]); } - return result; + return s; } -#endif @implementation ZXIBarcodeWriter --(CGImageRef)write:(NSString *)contents - width:(int)width - height:(int)height - format:(ZXIFormat)format - error:(NSError *__autoreleasing _Nullable *)error { +- (instancetype)init { + return [self initWithOptions: [[ZXIWriterOptions alloc] init]]; +} + +- (instancetype)initWithOptions:(ZXIWriterOptions*)options{ + self = [super init]; + self.options = options; + return self; +} + +-(CGImageRef)writeData:(NSData *)data + error:(NSError *__autoreleasing _Nullable *)error { + return [self encode: NSDataToStringW(data) + encoding: CharacterSet::BINARY + format: self.options.format + width: self.options.width + height: self.options.height + margin: self.options.margin + ecLevel: self.options.ecLevel + error: error]; +} + +-(CGImageRef)writeString:(NSString *)contents + error:(NSError *__autoreleasing _Nullable *)error { + return [self encode: NSStringToStringW(contents) + encoding: CharacterSet::UTF8 + format: self.options.format + width: self.options.width + height: self.options.height + margin: self.options.margin + ecLevel: self.options.ecLevel + error: error]; +} + +-(CGImageRef)encode:(std::wstring)content + encoding:(CharacterSet)encoding + format:(ZXIFormat)format + width:(int)width + height:(int)height + margin:(int)margin + ecLevel:(int)ecLevel + error:(NSError *__autoreleasing _Nullable *)error { MultiFormatWriter writer { BarcodeFormatFromZXIFormat(format) }; + writer.setEncoding(encoding); + writer.setMargin(margin); + writer.setEccLevel(ecLevel); // Catch exception for invalid formats try { - BitMatrix result = writer.encode(NSStringToStringW(contents), width, height); - int realWidth = result.width(); - int realHeight = result.height(); + BitMatrix bitMatrix = writer.encode(content, width, height); + return [self inflate:&bitMatrix]; + } catch(std::exception &e) { + SetNSError(error, ZXIWriterError, e.what()); + return nil; + } +} + +-(CGImageRef)inflate:(BitMatrix *)bitMatrix { + int realWidth = bitMatrix->width(); + int realHeight = bitMatrix->height(); #ifdef DEBUG -// std::cout << ToString(result, 'X', ' ', false, false); + std::cout << ToString(*bitMatrix, 'X', ' ', false, false); #endif - NSMutableData *resultAsNSData = [[NSMutableData alloc] initWithLength:realWidth * realHeight]; - size_t index = 0; - uint8_t *bytes = (uint8_t*)resultAsNSData.mutableBytes; - for (int y = 0; y < realHeight; ++y) { - for (int x = 0; x < realWidth; ++x) { - bytes[index] = result.get(x, y) ? 0 : 255; - ++index; - } - } - - CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericGray); - - CGImageRef cgimage = CGImageCreate(realWidth, - realHeight, - 8, - 8, - realWidth, - colorSpace, - kCGBitmapByteOrderDefault, - CGDataProviderCreateWithCFData((CFDataRef)resultAsNSData), - NULL, - YES, - kCGRenderingIntentDefault); - return cgimage; - } catch(std::exception &e) { - if(error != nil) { - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: [[NSString alloc] initWithUTF8String:e.what()] - }; - *error = [[NSError alloc] initWithDomain:ZXIErrorDomain code:ZXIWriterError userInfo:userInfo]; + NSMutableData *resultAsNSData = [[NSMutableData alloc] initWithLength:realWidth * realHeight]; + size_t index = 0; + uint8_t *bytes = (uint8_t*)resultAsNSData.mutableBytes; + for (int y = 0; y < realHeight; ++y) { + for (int x = 0; x < realWidth; ++x) { + bytes[index] = bitMatrix->get(x, y) ? 0 : 255; + ++index; } - return nil; } + + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericGray); + + return CGImageCreate(realWidth, + realHeight, + 8, + 8, + realWidth, + colorSpace, + kCGBitmapByteOrderDefault, + CGDataProviderCreateWithCFData((CFDataRef)resultAsNSData), + NULL, + YES, + kCGRenderingIntentDefault); } @end diff --git a/wrappers/ios/Sources/Wrapper/Writer/ZXIWriterOptions.h b/wrappers/ios/Sources/Wrapper/Writer/ZXIWriterOptions.h new file mode 100644 index 0000000000..a68386d919 --- /dev/null +++ b/wrappers/ios/Sources/Wrapper/Writer/ZXIWriterOptions.h @@ -0,0 +1,49 @@ +// Copyright 2023 KURZ Digital Solutions GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#import +#import "ZXIFormat.h" + +NS_ASSUME_NONNULL_BEGIN + +extern const int AZTEC_ERROR_CORRECTION_0; +extern const int AZTEC_ERROR_CORRECTION_12; +extern const int AZTEC_ERROR_CORRECTION_25; +extern const int AZTEC_ERROR_CORRECTION_37; +extern const int AZTEC_ERROR_CORRECTION_50; +extern const int AZTEC_ERROR_CORRECTION_62; +extern const int AZTEC_ERROR_CORRECTION_75; +extern const int AZTEC_ERROR_CORRECTION_87; +extern const int AZTEC_ERROR_CORRECTION_100; +extern const int QR_ERROR_CORRECTION_LOW; +extern const int QR_ERROR_CORRECTION_MEDIUM; +extern const int QR_ERROR_CORRECTION_QUARTILE; +extern const int QR_ERROR_CORRECTION_HIGH; +extern const int PDF417_ERROR_CORRECTION_0; +extern const int PDF417_ERROR_CORRECTION_1; +extern const int PDF417_ERROR_CORRECTION_2; +extern const int PDF417_ERROR_CORRECTION_3; +extern const int PDF417_ERROR_CORRECTION_4; +extern const int PDF417_ERROR_CORRECTION_5; +extern const int PDF417_ERROR_CORRECTION_6; +extern const int PDF417_ERROR_CORRECTION_7; +extern const int PDF417_ERROR_CORRECTION_8; + +@interface ZXIWriterOptions : NSObject +@property(nonatomic) ZXIFormat format; +@property(nonatomic) int width; +@property(nonatomic) int height; +@property(nonatomic) int ecLevel; +@property(nonatomic) int margin; + +- (instancetype)initWithFormat:(ZXIFormat)format; + +- (instancetype)initWithFormat:(ZXIFormat)format + width:(int)width + height:(int)height + ecLevel:(int)ecLevel + margin:(int)margin; +@end + +NS_ASSUME_NONNULL_END diff --git a/wrappers/ios/Sources/Wrapper/Writer/ZXIWriterOptions.mm b/wrappers/ios/Sources/Wrapper/Writer/ZXIWriterOptions.mm new file mode 100644 index 0000000000..651bd135e6 --- /dev/null +++ b/wrappers/ios/Sources/Wrapper/Writer/ZXIWriterOptions.mm @@ -0,0 +1,56 @@ +// Copyright 2023 KURZ Digital Solutions GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#import "ZXIWriterOptions.h" + +const int AZTEC_ERROR_CORRECTION_0 = 0; +const int AZTEC_ERROR_CORRECTION_12 = 1; +const int AZTEC_ERROR_CORRECTION_25 = 2; +const int AZTEC_ERROR_CORRECTION_37 = 3; +const int AZTEC_ERROR_CORRECTION_50 = 4; +const int AZTEC_ERROR_CORRECTION_62 = 5; +const int AZTEC_ERROR_CORRECTION_75 = 6; +const int AZTEC_ERROR_CORRECTION_87 = 7; +const int AZTEC_ERROR_CORRECTION_100 = 8; +const int QR_ERROR_CORRECTION_LOW = 2; +const int QR_ERROR_CORRECTION_MEDIUM = 4; +const int QR_ERROR_CORRECTION_QUARTILE = 6; +const int QR_ERROR_CORRECTION_HIGH = 8; +const int PDF417_ERROR_CORRECTION_0 = 0; +const int PDF417_ERROR_CORRECTION_1 = 1; +const int PDF417_ERROR_CORRECTION_2 = 2; +const int PDF417_ERROR_CORRECTION_3 = 3; +const int PDF417_ERROR_CORRECTION_4 = 4; +const int PDF417_ERROR_CORRECTION_5 = 5; +const int PDF417_ERROR_CORRECTION_6 = 6; +const int PDF417_ERROR_CORRECTION_7 = 7; +const int PDF417_ERROR_CORRECTION_8 = 8; + +@implementation ZXIWriterOptions + +- (instancetype)initWithFormat:(ZXIFormat)format { + self = [super init]; + self.format = format; + self.width = 0; + self.height = 0; + self.ecLevel = -1; + self.margin = -1; + return self; +} + +- (instancetype)initWithFormat:(ZXIFormat)format + width:(int)width + height:(int)height + ecLevel:(int)ecLevel + margin:(int)margin { + self = [super init]; + self.format = format; + self.width = width; + self.height = height; + self.ecLevel = ecLevel; + self.margin = margin; + return self; +} + +@end diff --git a/wrappers/ios/Sources/Wrapper/ZXIErrors.h b/wrappers/ios/Sources/Wrapper/ZXIErrors.h index 7f1ebb6e26..583df50771 100644 --- a/wrappers/ios/Sources/Wrapper/ZXIErrors.h +++ b/wrappers/ios/Sources/Wrapper/ZXIErrors.h @@ -8,8 +8,14 @@ NS_ASSUME_NONNULL_BEGIN #define ZXIErrorDomain @"ZXIErrorDomain" +typedef NS_ENUM(NSInteger, ZXIBarcodeReaderError) { + ZXIReaderError, +}; + typedef NS_ENUM(NSInteger, ZXIBarcodeWriterError) { ZXIWriterError, }; +void SetNSError(NSError *__autoreleasing _Nullable* error, NSInteger code, const char* message); + NS_ASSUME_NONNULL_END diff --git a/wrappers/ios/Sources/Wrapper/ZXIErrors.mm b/wrappers/ios/Sources/Wrapper/ZXIErrors.mm new file mode 100644 index 0000000000..6b88bcb1de --- /dev/null +++ b/wrappers/ios/Sources/Wrapper/ZXIErrors.mm @@ -0,0 +1,25 @@ +// Copyright 2023 KURZ Digital Solutions GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#import "ZXIErrors.h" + +void SetNSError(NSError *__autoreleasing _Nullable* error, + NSInteger code, + const char* message) { + if (error == nil) { + return; + } + NSString *errorDescription = @"Unknown C++ error"; + if (message && strlen(message) > 0) { + try { + errorDescription = [NSString stringWithUTF8String: message]; + } catch (NSException *exception) { + errorDescription = @"Unknown ObjC error"; + } + } + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: errorDescription }; + *error = [NSError errorWithDomain:ZXIErrorDomain + code:code + userInfo:userInfo]; +} diff --git a/wrappers/ios/Sources/Wrapper/ZXIFormat.h b/wrappers/ios/Sources/Wrapper/ZXIFormat.h index 157ac227cf..0ba9bd8f6d 100644 --- a/wrappers/ios/Sources/Wrapper/ZXIFormat.h +++ b/wrappers/ios/Sources/Wrapper/ZXIFormat.h @@ -8,7 +8,7 @@ typedef NS_ENUM(NSInteger, ZXIFormat) { NONE, AZTEC, CODABAR, CODE_39, CODE_93, CODE_128, DATA_BAR, DATA_BAR_EXPANDED, - DATA_MATRIX, EAN_8, EAN_13, ITF, MAXICODE, PDF_417, QR_CODE, MICRO_QR_CODE, UPC_A, UPC_E, + DATA_MATRIX, EAN_8, EAN_13, ITF, MAXICODE, PDF_417, QR_CODE, MICRO_QR_CODE, RMQR_CODE, UPC_A, UPC_E, LINEAR_CODES, MATRIX_CODES, ANY }; diff --git a/wrappers/ios/Sources/Wrapper/ZXIFormatHelper.h b/wrappers/ios/Sources/Wrapper/ZXIFormatHelper.h index 29f90a03bf..257314506c 100644 --- a/wrappers/ios/Sources/Wrapper/ZXIFormatHelper.h +++ b/wrappers/ios/Sources/Wrapper/ZXIFormatHelper.h @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 #import -#import "ZXing/BarcodeFormat.h" +#import "BarcodeFormat.h" #import "ZXIFormat.h" NS_ASSUME_NONNULL_BEGIN diff --git a/wrappers/ios/Sources/Wrapper/ZXIFormatHelper.mm b/wrappers/ios/Sources/Wrapper/ZXIFormatHelper.mm index 457c6e551f..487933f4e4 100644 --- a/wrappers/ios/Sources/Wrapper/ZXIFormatHelper.mm +++ b/wrappers/ios/Sources/Wrapper/ZXIFormatHelper.mm @@ -46,6 +46,8 @@ return ZXing::BarcodeFormat::Aztec; case ZXIFormat::MICRO_QR_CODE: return ZXing::BarcodeFormat::MicroQRCode; + case ZXIFormat::RMQR_CODE: + return ZXing::BarcodeFormat::RMQRCode; case ZXIFormat::NONE: return ZXing::BarcodeFormat::None; } @@ -95,6 +97,8 @@ ZXIFormat ZXIFormatFromBarcodeFormat(ZXing::BarcodeFormat format) { return ZXIFormat::MATRIX_CODES; case ZXing::BarcodeFormat::MicroQRCode: return ZXIFormat::MICRO_QR_CODE; + case ZXing::BarcodeFormat::RMQRCode: + return ZXIFormat::RMQR_CODE; case ZXing::BarcodeFormat::Any: return ZXIFormat::ANY; } diff --git a/wrappers/ios/Sources/Wrapper/module.modulemap b/wrappers/ios/Sources/Wrapper/module.modulemap index f613294fc1..0d334fc7d8 100644 --- a/wrappers/ios/Sources/Wrapper/module.modulemap +++ b/wrappers/ios/Sources/Wrapper/module.modulemap @@ -1,4 +1,4 @@ -module ZXingCppWrapper { +module ZXingCpp { umbrella header "UmbrellaHeader.h" export * module * { export * } diff --git a/wrappers/ios/build-release.sh b/wrappers/ios/build-release.sh deleted file mode 100755 index 7c1f4b4d5e..0000000000 --- a/wrappers/ios/build-release.sh +++ /dev/null @@ -1,37 +0,0 @@ -echo ========= Remove previous builds -rm -rf _builds -rm -rf ZXingCpp.xcframework - -echo ========= Create project structure -cmake -S../../ -B_builds -GXcode \ - -DCMAKE_SYSTEM_NAME=iOS \ - "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64" \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \ - -DCMAKE_INSTALL_PREFIX=`pwd`/_install \ - -DCMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH=NO \ - -DBUILD_UNIT_TESTS=NO \ - -DBUILD_BLACKBOX_TESTS=NO \ - -DBUILD_EXAMPLES=NO \ - -DBUILD_APPLE_FRAMEWORK=YES - -echo ========= Build the sdk for simulators -xcodebuild -project _builds/ZXing.xcodeproj build \ - -target ZXing \ - -parallelizeTargets \ - -configuration Release \ - -hideShellScriptEnvironment \ - -sdk iphonesimulator - -echo ========= Build the sdk for iOS -xcodebuild -project _builds/ZXing.xcodeproj build \ - -target ZXing \ - -parallelizeTargets \ - -configuration Release \ - -hideShellScriptEnvironment \ - -sdk iphoneos - -echo ========= Create the xcframework -xcodebuild -create-xcframework \ - -framework ./_builds/core/Release-iphonesimulator/ZXing.framework \ - -framework ./_builds/core/Release-iphoneos/ZXing.framework \ - -output ZXingCpp.xcframework diff --git a/wrappers/ios/demo/README.md b/wrappers/ios/demo/README.md index 6d8a8ed41e..61f28ef695 100644 --- a/wrappers/ios/demo/README.md +++ b/wrappers/ios/demo/README.md @@ -1,5 +1,3 @@ -# ZXingWrapper Demo Project +# ZXingCpp Demo Project This demo-project sets up a basic `AVCaptureSession` and uses ZXing on the incoming frames. -You have to build the `ZXing.xcframework` before, by performing the `build-release.sh` script -in the parent-directory. diff --git a/wrappers/ios/demo/demo.xcodeproj/project.pbxproj b/wrappers/ios/demo/demo.xcodeproj/project.pbxproj index 204cd12fe1..e7cbad577b 100644 --- a/wrappers/ios/demo/demo.xcodeproj/project.pbxproj +++ b/wrappers/ios/demo/demo.xcodeproj/project.pbxproj @@ -13,10 +13,23 @@ 388BF030283CC49D005CE271 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 388BF02E283CC49D005CE271 /* Main.storyboard */; }; 388BF032283CC49E005CE271 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 388BF031283CC49E005CE271 /* Assets.xcassets */; }; 388BF035283CC49E005CE271 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 388BF033283CC49E005CE271 /* LaunchScreen.storyboard */; }; - 9507445028609C0500E02D06 /* ZXingCppWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = 9507444F28609C0500E02D06 /* ZXingCppWrapper */; }; + 5EFA4B742ADF0F35000132A0 /* ZXingCpp in Frameworks */ = {isa = PBXBuildFile; productRef = 5EFA4B732ADF0F35000132A0 /* ZXingCpp */; }; 950744522860A3A300E02D06 /* WriteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950744512860A3A300E02D06 /* WriteViewController.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + 52A975482ADAD7DE002D6BD8 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 388BF025283CC49D005CE271 /* demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 388BF028283CC49D005CE271 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -27,8 +40,8 @@ 388BF034283CC49E005CE271 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 388BF036283CC49E005CE271 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 388BF043283CE0AC005CE271 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 5EFA4B712ADF0F16000132A0 /* zxing-cpp */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "zxing-cpp"; path = ../../..; sourceTree = ""; }; 950744512860A3A300E02D06 /* WriteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteViewController.swift; sourceTree = ""; }; - 9550105328609B7900ED103F /* ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ios; path = ..; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -36,7 +49,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9507445028609C0500E02D06 /* ZXingCppWrapper in Frameworks */, + 5EFA4B742ADF0F35000132A0 /* ZXingCpp in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -46,11 +59,11 @@ 388BF01C283CC49D005CE271 = { isa = PBXGroup; children = ( + 5EFA4B702ADF0F16000132A0 /* Packages */, 388BF043283CE0AC005CE271 /* README.md */, - 388BF03E283CD6C5005CE271 /* Packages */, 388BF027283CC49D005CE271 /* demo */, 388BF026283CC49D005CE271 /* Products */, - 388BF040283CD908005CE271 /* Frameworks */, + 5EFA4B722ADF0F35000132A0 /* Frameworks */, ); sourceTree = ""; }; @@ -77,15 +90,15 @@ path = demo; sourceTree = ""; }; - 388BF03E283CD6C5005CE271 /* Packages */ = { + 5EFA4B702ADF0F16000132A0 /* Packages */ = { isa = PBXGroup; children = ( - 9550105328609B7900ED103F /* ios */, + 5EFA4B712ADF0F16000132A0 /* zxing-cpp */, ); name = Packages; sourceTree = ""; }; - 388BF040283CD908005CE271 /* Frameworks */ = { + 5EFA4B722ADF0F35000132A0 /* Frameworks */ = { isa = PBXGroup; children = ( ); @@ -102,6 +115,7 @@ 388BF021283CC49D005CE271 /* Sources */, 388BF022283CC49D005CE271 /* Frameworks */, 388BF023283CC49D005CE271 /* Resources */, + 52A975482ADAD7DE002D6BD8 /* Embed Frameworks */, ); buildRules = ( ); @@ -109,7 +123,7 @@ ); name = demo; packageProductDependencies = ( - 9507444F28609C0500E02D06 /* ZXingCppWrapper */, + 5EFA4B732ADF0F35000132A0 /* ZXingCpp */, ); productName = demo; productReference = 388BF025283CC49D005CE271 /* demo.app */; @@ -139,6 +153,8 @@ Base, ); mainGroup = 388BF01C283CC49D005CE271; + packageReferences = ( + ); productRefGroup = 388BF026283CC49D005CE271 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -314,6 +330,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; @@ -326,6 +343,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -333,6 +351,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.zxing-cpp.ios.demo-${SAMPLE_CODE_DISAMBIGUATOR}"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SAMPLE_CODE_DISAMBIGUATOR = "${DEVELOPMENT_TEAM}"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -345,6 +364,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; @@ -357,6 +377,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -364,6 +385,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.zxing-cpp.ios.demo-${SAMPLE_CODE_DISAMBIGUATOR}"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SAMPLE_CODE_DISAMBIGUATOR = "${DEVELOPMENT_TEAM}"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -395,9 +417,9 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 9507444F28609C0500E02D06 /* ZXingCppWrapper */ = { + 5EFA4B732ADF0F35000132A0 /* ZXingCpp */ = { isa = XCSwiftPackageProductDependency; - productName = ZXingCppWrapper; + productName = ZXingCpp; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/wrappers/ios/demo/demo/ViewController.swift b/wrappers/ios/demo/demo/ViewController.swift index 4b4dd1e245..30d437eb31 100644 --- a/wrappers/ios/demo/demo/ViewController.swift +++ b/wrappers/ios/demo/demo/ViewController.swift @@ -6,7 +6,7 @@ import UIKit import AVFoundation -import ZXingCppWrapper +import ZXingCpp class ViewController: UIViewController { let captureSession = AVCaptureSession() @@ -25,8 +25,13 @@ class ViewController: UIViewController { // setup camera session self.requestAccess { let discoverySession = AVCaptureDevice.DiscoverySession( - deviceTypes: [.builtInWideAngleCamera], - mediaType: AVMediaType.video, + deviceTypes: [ + .builtInTripleCamera, + .builtInDualWideCamera, + .builtInDualCamera, + .builtInWideAngleCamera + ], + mediaType: .video, position: .back) let device = discoverySession.devices.first! @@ -35,11 +40,13 @@ class ViewController: UIViewController { self.captureSession.addInput(cameraInput) let videoDataOutput = AVCaptureVideoDataOutput() videoDataOutput.setSampleBufferDelegate(self, queue: self.queue) - videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)] + videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] videoDataOutput.alwaysDiscardsLateVideoFrames = true self.captureSession.addOutput(videoDataOutput) self.captureSession.commitConfiguration() - self.captureSession.startRunning() + DispatchQueue.global(qos: .background).async { + self.captureSession.startRunning() + } } } } @@ -63,7 +70,7 @@ extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate { return } let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)! - if let result = reader.read(imageBuffer).first { + if let result = try? reader.read(imageBuffer).first { print("Found barcode of format", result.format.rawValue, "with text", result.text) } self.zxingLock.signal() diff --git a/wrappers/ios/demo/demo/WriteViewController.swift b/wrappers/ios/demo/demo/WriteViewController.swift index 8f1e1f28e8..9a4e21df7c 100644 --- a/wrappers/ios/demo/demo/WriteViewController.swift +++ b/wrappers/ios/demo/demo/WriteViewController.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: Apache-2.0 import UIKit -import ZXingCppWrapper +import ZXingCpp class WriteViewController: UIViewController { @IBOutlet fileprivate var imageView: UIImageView! @@ -14,8 +14,9 @@ class WriteViewController: UIViewController { // MARK: - Actions @IBAction func textFieldChanged(_ sender: UITextField) { + let options = ZXIWriterOptions(format: .QR_CODE, width: 200, height: 200, ecLevel: QR_ERROR_CORRECTION_LOW, margin: -1) guard let text = sender.text, - let image = try? ZXIBarcodeWriter().write(text, width: 200, height: 200, format: .QR_CODE) + let image = try? ZXIBarcodeWriter(options: options).write(text) else { return } diff --git a/wrappers/python/README.md b/wrappers/python/README.md index 52b23f9b14..5366af17da 100644 --- a/wrappers/python/README.md +++ b/wrappers/python/README.md @@ -19,14 +19,16 @@ python setup.py install ## Usage ```python -import cv2 -import zxingcpp +import cv2, zxingcpp -img = cv2.imread('myimage.png') +img = cv2.imread('test.png') results = zxingcpp.read_barcodes(img) for result in results: - print("Found barcode:\n Text: '{}'\n Format: {}\n Position: {}" - .format(result.text, result.format, result.position)) + print('Found barcode:' + f'\n Text: "{result.text}"' + f'\n Format: {result.format}' + f'\n Content: {result.content_type}' + f'\n Position: {result.position}') if len(results) == 0: print("Could not find any barcode.") ``` diff --git a/wrappers/python/demo_reader.py b/wrappers/python/demo_reader.py index c2a3ad9468..60a0a11fbb 100644 --- a/wrappers/python/demo_reader.py +++ b/wrappers/python/demo_reader.py @@ -4,7 +4,10 @@ img = Image.open(sys.argv[1]) results = zxingcpp.read_barcodes(img) for result in results: - print("Found barcode:\n Text: '{}'\n Format: {}\n Content: {}\n Position: {}" - .format(result.text, result.format, result.content_type, result.position)) + print('Found barcode:' + f'\n Text: "{result.text}"' + f'\n Format: {result.format}' + f'\n Content: {result.content_type}' + f'\n Position: {result.position}') if len(results) == 0: print("Could not find any barcode.") diff --git a/wrappers/python/setup.py b/wrappers/python/setup.py index 232b5a731f..f3c3b38c7d 100644 --- a/wrappers/python/setup.py +++ b/wrappers/python/setup.py @@ -59,7 +59,7 @@ def build_extension(self, ext): # "local_scheme": "no-local-version", # "tag_regex": "v?([0-9]+.[0-9]+.[0-9]+)", # }, - version='2.1.0', + version='2.2.0', description='Python bindings for the zxing-cpp barcode library', long_description=long_description, long_description_content_type="text/markdown", @@ -76,7 +76,6 @@ def build_extension(self, ext): "Topic :: Multimedia :: Graphics", ], python_requires=">=3.6", - install_requires=["numpy"], ext_modules=[CMakeExtension('zxingcpp')], cmdclass=dict(build_ext=CMakeBuild), zip_safe=False, diff --git a/wrappers/python/test.py b/wrappers/python/test.py index 5d7f8cf30e..a086b24413 100644 --- a/wrappers/python/test.py +++ b/wrappers/python/test.py @@ -1,10 +1,12 @@ import importlib.util import unittest +import math import zxingcpp has_numpy = importlib.util.find_spec('numpy') is not None has_pil = importlib.util.find_spec('PIL') is not None +has_cv2 = importlib.util.find_spec('cv2') is not None BF = zxingcpp.BarcodeFormat @@ -16,7 +18,6 @@ def test_format(self): self.assertEqual(zxingcpp.barcode_formats_from_str('ITF, qrcode'), BF.ITF | BF.QRCode) -@unittest.skipIf(not has_numpy, "need numpy for read/write tests") class TestReadWrite(unittest.TestCase): def check_res(self, res, format, text): @@ -60,7 +61,19 @@ def test_write_read_multi_cycle(self): res = zxingcpp.read_barcodes(img)[0] self.check_res(res, format, text) - def test_failed_read(self): + @staticmethod + def zeroes(shape): + return memoryview(b"0" * math.prod(shape)).cast("B", shape=shape) + + def test_failed_read_buffer(self): + res = zxingcpp.read_barcode( + self.zeroes((100, 100)), formats=BF.EAN8 | BF.Aztec, binarizer=zxingcpp.Binarizer.BoolCast + ) + + self.assertEqual(res, None) + + @unittest.skipIf(not has_numpy, "need numpy for read/write tests") + def test_failed_read_numpy(self): import numpy as np res = zxingcpp.read_barcode( np.zeros((100, 100), np.uint8), formats=BF.EAN8 | BF.Aztec, binarizer=zxingcpp.Binarizer.BoolCast @@ -68,6 +81,24 @@ def test_failed_read(self): self.assertEqual(res, None) + def test_write_read_cycle_buffer(self): + format = BF.QRCode + text = "I have the best words." + img = zxingcpp.write_barcode(format, text) + + self.check_res(zxingcpp.read_barcode(img), format, text) + + @unittest.skipIf(not has_numpy, "need numpy for read/write tests") + def test_write_read_cycle_numpy(self): + import numpy as np + format = BF.QRCode + text = "I have the best words." + img = zxingcpp.write_barcode(format, text, quiet_zone=10) + img = np.array(img) + + self.check_res(zxingcpp.read_barcode(img), format, text) + self.check_res(zxingcpp.read_barcode(img[5:40,5:40]), format, text) + @unittest.skipIf(not has_pil, "need PIL for read/write tests") def test_write_read_cycle_pil(self): from PIL import Image @@ -82,15 +113,33 @@ def test_write_read_cycle_pil(self): self.check_res(zxingcpp.read_barcode(img.convert("1")), format, text) self.check_res(zxingcpp.read_barcode(img.convert("CMYK")), format, text) + @unittest.skipIf(not has_cv2, "need cv2 for read/write tests") + def test_write_read_cycle_cv2(self): + import cv2, numpy + format = BF.QRCode + text = "I have the best words." + img = zxingcpp.write_barcode(format, text, quiet_zone=10) + img = cv2.cvtColor(numpy.array(img), cv2.COLOR_GRAY2BGR ) + + self.check_res(zxingcpp.read_barcode(img), format, text) + self.check_res(zxingcpp.read_barcode(img[5:40,5:40,:]), format, text) + def test_read_invalid_type(self): self.assertRaisesRegex( - TypeError, "Could not convert to numpy array.", zxingcpp.read_barcode, "foo" + TypeError, "Invalid input: does not support the buffer protocol.", zxingcpp.read_barcode, "foo" + ) + + def test_read_invalid_numpy_array_channels_buffer(self): + self.assertRaisesRegex( + ValueError, "Unsupported number of channels for buffer: 4", zxingcpp.read_barcode, + self.zeroes((100, 100, 4)) ) - def test_read_invalid_numpy_array_channels(self): + @unittest.skipIf(not has_numpy, "need numpy for read/write tests") + def test_read_invalid_numpy_array_channels_numpy(self): import numpy as np self.assertRaisesRegex( - ValueError, "Unsupported number of channels for numpy array: 4", zxingcpp.read_barcode, + ValueError, "Unsupported number of channels for buffer: 4", zxingcpp.read_barcode, np.zeros((100, 100, 4), np.uint8) ) diff --git a/wrappers/python/zxing.cpp b/wrappers/python/zxing.cpp index 79db3c480d..a83e1b36ea 100644 --- a/wrappers/python/zxing.cpp +++ b/wrappers/python/zxing.cpp @@ -13,20 +13,20 @@ // Writer #include "BitMatrix.h" +#include "Matrix.h" #include "MultiFormatWriter.h" -#include #include #include #include +#include #include +#include #include using namespace ZXing; namespace py = pybind11; - -// Numpy array wrapper class for images (either BGR or GRAYSCALE) -using Image = py::array_t; +using namespace pybind11::literals; // to bring in the `_a` literal std::ostream& operator<<(std::ostream& os, const Position& points) { for (const auto& p : points) @@ -37,9 +37,10 @@ std::ostream& operator<<(std::ostream& os, const Position& points) { } auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool try_rotate, bool try_downscale, TextMode text_mode, - Binarizer binarizer, bool is_pure, EanAddOnSymbol ean_add_on_symbol, uint8_t max_number_of_symbols = 0xff) + Binarizer binarizer, bool is_pure, EanAddOnSymbol ean_add_on_symbol, bool return_errors, + uint8_t max_number_of_symbols = 0xff) { - const auto hints = DecodeHints() + const auto opts = ReaderOptions() .setFormats(formats) .setTryRotate(try_rotate) .setTryDownscale(try_downscale) @@ -47,38 +48,84 @@ auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool t .setBinarizer(binarizer) .setIsPure(is_pure) .setMaxNumberOfSymbols(max_number_of_symbols) - .setEanAddOnSymbol(ean_add_on_symbol); + .setEanAddOnSymbol(ean_add_on_symbol) + .setReturnErrors(return_errors); const auto _type = std::string(py::str(py::type::of(_image))); - Image image; + py::buffer_info info; ImageFormat imgfmt = ImageFormat::None; try { - if (_type.find("PIL.") != std::string::npos) { - _image.attr("load")(); - const auto mode = _image.attr("mode").cast(); - if (mode == "L") - imgfmt = ImageFormat::Lum; - else if (mode == "RGB") - imgfmt = ImageFormat::RGB; - else if (mode == "RGBA") - imgfmt = ImageFormat::RGBX; - else { - // Unsupported mode in ImageFormat. Let's do conversion to L mode with PIL. - _image = _image.attr("convert")("L"); - imgfmt = ImageFormat::Lum; + if (py::hasattr(_image, "__array_interface__")) { + if (_type.find("PIL.") != std::string::npos) { + _image.attr("load")(); + const auto mode = _image.attr("mode").cast(); + if (mode == "L") + imgfmt = ImageFormat::Lum; + else if (mode == "RGB") + imgfmt = ImageFormat::RGB; + else if (mode == "RGBA") + imgfmt = ImageFormat::RGBX; + else { + // Unsupported mode in ImageFormat. Let's do conversion to L mode with PIL. + _image = _image.attr("convert")("L"); + imgfmt = ImageFormat::Lum; + } } + + auto ai = _image.attr("__array_interface__").cast(); + auto shape = ai["shape"].cast>(); + auto typestr = ai["typestr"].cast(); + + if (typestr != "|u1") + throw py::type_error("Incompatible __array_interface__ data type (" + typestr + "): expected a uint8_t array (|u1)."); + + if (ai.contains("data")) { + auto adata = ai["data"]; + + if (py::isinstance(adata)) { + // PIL and our own __array_interface__ passes data as a buffer/bytes object + info = adata.cast().request(); + // PIL's bytes object has wrong dim/shape/strides info + if (info.ndim != Size(shape)) { + info.ndim = Size(shape); + info.shape = shape; + info.strides = py::detail::c_strides(shape, 1); + } + } else if (py::isinstance(adata)) { + // numpy data is passed as a tuple + auto strides = py::detail::c_strides(shape, 1); + if (ai.contains("strides") && !ai["strides"].is_none()) + strides = ai["strides"].cast>(); + auto data_ptr = reinterpret_cast(adata.cast()[0].cast()); + info = py::buffer_info(data_ptr, 1, "B", Size(shape), shape, strides); + } else { + throw py::type_error("No way to get data from __array_interface__"); + } + } else { + info = _image.cast().request(); + } + } else { + info = _image.cast().request(); } - image = _image.cast(); #if PYBIND11_VERSION_HEX > 0x02080000 // py::raise_from is available starting from 2.8.0 } catch (py::error_already_set &e) { - py::raise_from(e, PyExc_TypeError, ("Could not convert " + _type + " to numpy array of dtype 'uint8'.").c_str()); + py::raise_from(e, PyExc_TypeError, ("Invalid input: " + _type + " does not support the buffer protocol.").c_str()); throw py::error_already_set(); #endif } catch (...) { - throw py::type_error("Could not convert " + _type + " to numpy array. Expecting a PIL Image or numpy array."); + throw py::type_error("Invalid input: " + _type + " does not support the buffer protocol."); } - const auto height = narrow_cast(image.shape(0)); - const auto width = narrow_cast(image.shape(1)); - const auto channels = image.ndim() == 2 ? 1 : narrow_cast(image.shape(2)); + + if (info.format != py::format_descriptor::format()) + throw py::type_error("Incompatible buffer format '" + info.format + "': expected a uint8_t array."); + + if (info.ndim != 2 && info.ndim != 3) + throw py::type_error("Incompatible buffer dimension " + std::to_string(info.ndim) + " (needs to be 2 or 3)."); + + const auto height = narrow_cast(info.shape[0]); + const auto width = narrow_cast(info.shape[1]); + const auto channels = info.ndim == 2 ? 1 : narrow_cast(info.shape[2]); + const auto rowStride = narrow_cast(info.strides[0]); + const auto pixStride = narrow_cast(info.strides[1]); if (imgfmt == ImageFormat::None) { // Assume grayscale or BGR image depending on channels number if (channels == 1) @@ -86,37 +133,36 @@ auto read_barcodes_impl(py::object _image, const BarcodeFormats& formats, bool t else if (channels == 3) imgfmt = ImageFormat::BGR; else - throw py::value_error("Unsupported number of channels for numpy array: " + std::to_string(channels)); + throw py::value_error("Unsupported number of channels for buffer: " + std::to_string(channels)); } - const auto bytes = image.data(); - return ReadBarcodes({bytes, width, height, imgfmt, width * channels, channels}, hints); + const auto bytes = static_cast(info.ptr); + // Disables the GIL during zxing processing (restored automatically upon completion) + py::gil_scoped_release release; + return ReadBarcodes({bytes, width, height, imgfmt, rowStride, pixStride}, opts); } std::optional read_barcode(py::object _image, const BarcodeFormats& formats, bool try_rotate, bool try_downscale, - TextMode text_mode, Binarizer binarizer, bool is_pure, EanAddOnSymbol ean_add_on_symbol) + TextMode text_mode, Binarizer binarizer, bool is_pure, EanAddOnSymbol ean_add_on_symbol, + bool return_errors) { - auto res = read_barcodes_impl(_image, formats, try_rotate, try_downscale, text_mode, binarizer, is_pure, ean_add_on_symbol, 1); + auto res = read_barcodes_impl(_image, formats, try_rotate, try_downscale, text_mode, binarizer, is_pure, ean_add_on_symbol, + return_errors, 1); return res.empty() ? std::nullopt : std::optional(res.front()); } Results read_barcodes(py::object _image, const BarcodeFormats& formats, bool try_rotate, bool try_downscale, - TextMode text_mode, Binarizer binarizer, bool is_pure, EanAddOnSymbol ean_add_on_symbol) + TextMode text_mode, Binarizer binarizer, bool is_pure, EanAddOnSymbol ean_add_on_symbol, bool return_errors) { - return read_barcodes_impl(_image, formats, try_rotate, try_downscale, text_mode, binarizer, is_pure, ean_add_on_symbol); + return read_barcodes_impl(_image, formats, try_rotate, try_downscale, text_mode, binarizer, is_pure, ean_add_on_symbol, + return_errors); } -Image write_barcode(BarcodeFormat format, std::string text, int width, int height, int quiet_zone, int ec_level) +Matrix write_barcode(BarcodeFormat format, std::string text, int width, int height, int quiet_zone, int ec_level) { auto writer = MultiFormatWriter(format).setEncoding(CharacterSet::UTF8).setMargin(quiet_zone).setEccLevel(ec_level); auto bitmap = writer.encode(text, width, height); - - auto result = Image({bitmap.height(), bitmap.width()}); - auto r = result.mutable_unchecked<2>(); - for (py::ssize_t y = 0; y < r.shape(0); y++) - for (py::ssize_t x = 0; x < r.shape(1); x++) - r(y, x) = bitmap.get(narrow_cast(x), narrow_cast(y)) ? 0 : 255; - return result; + return ToMatrix(bitmap); } @@ -142,6 +188,7 @@ PYBIND11_MODULE(zxingcpp, m) .value("PDF417", BarcodeFormat::PDF417) .value("QRCode", BarcodeFormat::QRCode) .value("MicroQRCode", BarcodeFormat::MicroQRCode) + .value("RMQRCode", BarcodeFormat::RMQRCode) .value("DataBar", BarcodeFormat::DataBar) .value("DataBarExpanded", BarcodeFormat::DataBarExpanded) .value("UPCA", BarcodeFormat::UPCA) @@ -211,6 +258,20 @@ PYBIND11_MODULE(zxingcpp, m) oss << pos; return oss.str(); }); + py::enum_(m, "ErrorType", "") + .value("None", Error::Type::None, "No error") + .value("Format", Error::Type::Format, "Data format error") + .value("Checksum", Error::Type::Checksum, "Checksum error") + .value("Unsupported", Error::Type::Unsupported, "Unsupported content error") + .export_values(); + py::class_(m, "Error", "Barcode reading error") + .def_property_readonly("type", &Error::type, + ":return: Error type\n" + ":rtype: zxing.ErrorType") + .def_property_readonly("message", &Error::msg, + ":return: Error message\n" + ":rtype: str") + .def("__str__", [](Error e) { return ToString(e); }); py::class_(m, "Result", "Result of barcode reading") .def_property_readonly("valid", &Result::isValid, ":return: whether or not result is valid (i.e. a symbol was found)\n" @@ -227,6 +288,9 @@ PYBIND11_MODULE(zxingcpp, m) .def_property_readonly("symbology_identifier", &Result::symbologyIdentifier, ":return: decoded symbology idendifier\n" ":rtype: str") + .def_property_readonly("ec_level", &Result::ecLevel, + ":return: error correction level of the symbol (empty string if not applicable)\n" + ":rtype: str") .def_property_readonly("content_type", &Result::contentType, ":return: content type of symbol\n" ":rtype: zxing.ContentType") @@ -235,7 +299,11 @@ PYBIND11_MODULE(zxingcpp, m) ":rtype: zxing.Position") .def_property_readonly("orientation", &Result::orientation, ":return: orientation (in degree) of the decoded symbol\n" - ":rtype: int"); + ":rtype: int") + .def_property_readonly( + "error", [](const Result& res) { return res.error() ? std::optional(res.error()) : std::nullopt; }, + ":return: Error code or None\n" + ":rtype: zxing.Error"); m.def("barcode_format_from_str", &BarcodeFormatFromString, py::arg("str"), "Convert string to BarcodeFormat\n\n" @@ -259,9 +327,11 @@ PYBIND11_MODULE(zxingcpp, m) py::arg("binarizer") = Binarizer::LocalAverage, py::arg("is_pure") = false, py::arg("ean_add_on_symbol") = EanAddOnSymbol::Ignore, + py::arg("return_errors") = false, "Read (decode) a barcode from a numpy BGR or grayscale image array or from a PIL image.\n\n" - ":type image: numpy.ndarray|PIL.Image.Image\n" + ":type image: buffer|numpy.ndarray|PIL.Image.Image\n" ":param image: The image object to decode. The image can be either:\n" + " - a buffer with the correct shape, use .cast on memory view to convert\n" " - a numpy array containing image either in grayscale (1 byte per pixel) or BGR mode (3 bytes per pixel)\n" " - a PIL Image\n" ":type formats: zxing.BarcodeFormat|zxing.BarcodeFormats\n" @@ -284,6 +354,9 @@ PYBIND11_MODULE(zxingcpp, m) ":type ean_add_on_symbol: zxing.EanAddOnSymbol\n" ":param ean_add_on_symbol: Specify whether to Ignore, Read or Require EAN-2/5 add-on symbols while scanning \n" " EAN/UPC codes. Default is ``Ignore``.\n" + ":type return_errors: bool\n" + ":param return_errors: Set to True to return the barcodes with errors as well (e.g. checksum errors); see ``Result.error``.\n" + " Default is False." ":rtype: zxing.Result\n" ":return: a zxing result containing decoded symbol if found, None otherwise" ); @@ -296,9 +369,11 @@ PYBIND11_MODULE(zxingcpp, m) py::arg("binarizer") = Binarizer::LocalAverage, py::arg("is_pure") = false, py::arg("ean_add_on_symbol") = EanAddOnSymbol::Ignore, + py::arg("return_errors") = false, "Read (decode) multiple barcodes from a numpy BGR or grayscale image array or from a PIL image.\n\n" - ":type image: numpy.ndarray|PIL.Image.Image\n" + ":type image: buffer|numpy.ndarray|PIL.Image.Image\n" ":param image: The image object to decode. The image can be either:\n" + " - a buffer with the correct shape, use .cast on memory view to convert\n" " - a numpy array containing image either in grayscale (1 byte per pixel) or BGR mode (3 bytes per pixel)\n" " - a PIL Image\n" ":type formats: zxing.BarcodeFormat|zxing.BarcodeFormats\n" @@ -321,9 +396,31 @@ PYBIND11_MODULE(zxingcpp, m) ":type ean_add_on_symbol: zxing.EanAddOnSymbol\n" ":param ean_add_on_symbol: Specify whether to Ignore, Read or Require EAN-2/5 add-on symbols while scanning \n" " EAN/UPC codes. Default is ``Ignore``.\n" + ":type return_errors: bool\n" + ":param return_errors: Set to True to return the barcodes with errors as well (e.g. checksum errors); see ``Result.error``.\n" + " Default is False.\n" ":rtype: zxing.Result\n" ":return: a list of zxing results containing decoded symbols, the list is empty if none is found" - ); + ); + py::class_>(m, "Bitmap", py::buffer_protocol()) + .def_property_readonly( + "__array_interface__", + [](const Matrix& m) { + return py::dict("version"_a = 3, "data"_a = m, "shape"_a = py::make_tuple(m.height(), m.width()), "typestr"_a = "|u1"); + }) + .def_property_readonly("shape", [](const Matrix& m) { return py::make_tuple(m.height(), m.width()); }) + .def_buffer([](const Matrix& m) -> py::buffer_info { + return { + const_cast(m.data()), // Pointer to buffer + sizeof(uint8_t), // Size of one scalar + py::format_descriptor::format(), // Python struct-style format descriptor + 2, // Number of dimensions + {m.height(), m.width()}, // Buffer dimensions + {sizeof(uint8_t) * m.width(), sizeof(uint8_t)}, // Strides (in bytes) for each index + true // read-only + }; + }); + m.def("write_barcode", &write_barcode, py::arg("format"), py::arg("text"), @@ -331,7 +428,7 @@ PYBIND11_MODULE(zxingcpp, m) py::arg("height") = 0, py::arg("quiet_zone") = -1, py::arg("ec_level") = -1, - "Write (encode) a text into a barcode and return numpy (grayscale) image array\n\n" + "Write (encode) a text into a barcode and return 8-bit grayscale bitmap buffer\n\n" ":type format: zxing.BarcodeFormat\n" ":param format: format of the barcode to create\n" ":type text: str\n" @@ -346,7 +443,7 @@ PYBIND11_MODULE(zxingcpp, m) ":param quiet_zone: minimum size (in pixels) of the quiet zone around barcode. If undefined (or set to -1), \n" " the minimum quiet zone of respective barcode is used." ":type ec_level: int\n" - ":param ec_level: error correction level of the barcode\n" - " (Used for Aztec, PDF417, and QRCode only)." + ":param ec_level: error correction level of the barcode (Used for Aztec, PDF417, and QRCode only).\n" + ":rtype: zxing.Bitmap\n" ); } diff --git a/wrappers/wasm/BarcodeReader.cpp b/wrappers/wasm/BarcodeReader.cpp index e56762e490..a023368850 100644 --- a/wrappers/wasm/BarcodeReader.cpp +++ b/wrappers/wasm/BarcodeReader.cpp @@ -7,6 +7,7 @@ #include "ReadBarcode.h" #include +#include #include #include #include @@ -20,6 +21,7 @@ struct ReadResult { std::string format{}; std::string text{}; + emscripten::val bytes; std::string error{}; Position position{}; std::string symbologyIdentifier{}; @@ -28,29 +30,39 @@ struct ReadResult std::vector readBarcodes(ImageView iv, bool tryHarder, const std::string& format, int maxSymbols) { try { - DecodeHints hints; - hints.setTryHarder(tryHarder); - hints.setTryRotate(tryHarder); - hints.setTryInvert(tryHarder); - hints.setTryDownscale(tryHarder); - hints.setFormats(BarcodeFormatsFromString(format)); - hints.setMaxNumberOfSymbols(maxSymbols); -// hints.setReturnErrors(maxSymbols > 1); + ReaderOptions opts; + opts.setTryHarder(tryHarder); + opts.setTryRotate(tryHarder); + opts.setTryInvert(tryHarder); + opts.setTryDownscale(tryHarder); + opts.setFormats(BarcodeFormatsFromString(format)); + opts.setMaxNumberOfSymbols(maxSymbols); +// opts.setReturnErrors(maxSymbols > 1); - auto results = ReadBarcodes(iv, hints); + auto results = ReadBarcodes(iv, opts); std::vector readResults{}; readResults.reserve(results.size()); - for (auto& result : results) { - readResults.push_back({ToString(result.format()), result.text(), ToString(result.error()), result.position(), result.symbologyIdentifier()}); + thread_local const emscripten::val Uint8Array = emscripten::val::global("Uint8Array"); + + for (auto&& result : results) { + const ByteArray& bytes = result.bytes(); + readResults.push_back({ + ToString(result.format()), + result.text(), + Uint8Array.new_(emscripten::typed_memory_view(bytes.size(), bytes.data())), + ToString(result.error()), + result.position(), + result.symbologyIdentifier() + }); } return readResults; } catch (const std::exception& e) { - return {{"", "", e.what()}}; + return {{"", "", {}, e.what()}}; } catch (...) { - return {{"", "", "Unknown error"}}; + return {{"", "", {}, "Unknown error"}}; } return {}; } @@ -62,7 +74,7 @@ std::vector readBarcodesFromImage(int bufferPtr, int bufferLength, b stbi_load_from_memory(reinterpret_cast(bufferPtr), bufferLength, &width, &height, &channels, 1), stbi_image_free); if (buffer == nullptr) - return {{"", "", "Error loading image"}}; + return {{"", "", {}, "Error loading image"}}; return readBarcodes({buffer.get(), width, height, ImageFormat::Lum}, tryHarder, format, maxSymbols); } @@ -89,6 +101,7 @@ EMSCRIPTEN_BINDINGS(BarcodeReader) value_object("ReadResult") .field("format", &ReadResult::format) .field("text", &ReadResult::text) + .field("bytes", &ReadResult::bytes) .field("error", &ReadResult::error) .field("position", &ReadResult::position) .field("symbologyIdentifier", &ReadResult::symbologyIdentifier); diff --git a/wrappers/wasm/README.md b/wrappers/wasm/README.md index b5dd82d3a8..c0e7c3794f 100644 --- a/wrappers/wasm/README.md +++ b/wrappers/wasm/README.md @@ -1,4 +1,4 @@ -# WebAssembly/WASM Wrapper/Library +# WebAssembly/WASM Wrapper ## Build @@ -10,6 +10,10 @@ You can also download the latest build output from the continuous integration system from the [Actions](https://github.com/zxing-cpp/zxing-cpp/actions) tab. Look for 'wasm-artifacts'. Also check out the [live demos](https://github.com/zxing-cpp/zxing-cpp#web-demos). +## Alternative Wrapper Project + +There is an alternative (external) wrapper project called [zxing-wasm](https://github.com/Sec-ant/zxing-wasm). It is written in TypeScript, has a more feature complete interface closer to the C++ API, spares you from dealing with WASM intricacies and is provided as a fully fledged ES module on [npmjs](https://www.npmjs.com/package/zxing-wasm). + ## Performance It turns out that compiling the library with the `-Os` (`MinSizeRel`) flag causes a noticible performance penalty. Here are some measurements from the demo_cam_reader (performed on Chromium 109 running on a Core i9-9980HK): diff --git a/wrappers/wasm/demo_cam_reader.html b/wrappers/wasm/demo_cam_reader.html index 1791d8cf66..1040a142df 100644 --- a/wrappers/wasm/demo_cam_reader.html +++ b/wrappers/wasm/demo_cam_reader.html @@ -38,6 +38,7 @@

zxing-cpp/wasm live demo

+ @@ -111,12 +112,16 @@

zxing-cpp/wasm live demo

} } + function escapeTags(htmlStr) { + return htmlStr.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); + } + const processFrame = function () { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const code = readBarcodeFromCanvas(canvas, format.value, mode.value === 'true'); if (code.format) { - resultElement.innerText = code.format + ": " + code.text; + resultElement.innerText = code.format + ": " + escapeTags(code.text); drawResult(code) } else { resultElement.innerText = "No barcode found"; diff --git a/wrappers/wasm/demo_reader.html b/wrappers/wasm/demo_reader.html index cbcb7cfcca..fadc566a3c 100644 --- a/wrappers/wasm/demo_reader.html +++ b/wrappers/wasm/demo_reader.html @@ -70,6 +70,14 @@ img.src = URL.createObjectURL(file) } +function escapeTags(htmlStr) { + return htmlStr.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); +} + +function u8a2hex(bytes) { + return bytes.reduce((a, b) => a + b.toString(16).padStart(2, '0') + ' ', ''); +} + function showResults(results) { const resultsDiv = document.getElementById("results"); resultsDiv.innerHTML = ""; @@ -78,8 +86,11 @@ } else { for (let i = 0; i < results.size(); i += 1) { - const { error, format, text } = results.get(i); - resultsDiv.innerHTML += "
  • Format: " + format + "
    " + (text || 'Error: ' + error + '') + "
  • "; + const { format, text, bytes, error } = results.get(i); + resultsDiv.innerHTML += "
  • Format: " + format + "" + + "
    " + (escapeTags(text) || 'Error: ' + error + '') + "
    " + + "
    " + u8a2hex(bytes) + "
    " + + "
  • "; } } } @@ -111,6 +122,7 @@

    zxing-cpp/wasm reader demo

    + diff --git a/wrappers/winrt/BarcodeReader.cpp b/wrappers/winrt/BarcodeReader.cpp index 1fcd7a751c..b3186024c1 100644 --- a/wrappers/winrt/BarcodeReader.cpp +++ b/wrappers/winrt/BarcodeReader.cpp @@ -11,7 +11,7 @@ #include "BarcodeReader.h" #include "BarcodeFormat.h" -#include "DecodeHints.h" +#include "ReaderOptions.h" #include "ReadBarcode.h" #include "ReadResult.h" #include "Utf.h" @@ -45,17 +45,17 @@ BarcodeReader::BarcodeReader(bool tryHarder) void BarcodeReader::init(bool tryHarder, bool tryRotate, const Platform::Array^ types) { - m_hints.reset(new DecodeHints()); - m_hints->setTryHarder(tryHarder); - m_hints->setTryRotate(tryRotate); - m_hints->setTryInvert(tryHarder); + m_opts.reset(new ReaderOptions()); + m_opts->setTryHarder(tryHarder); + m_opts->setTryRotate(tryRotate); + m_opts->setTryInvert(tryHarder); if (types != nullptr && types->Length > 0) { BarcodeFormats barcodeFormats; for (BarcodeType type : types) { barcodeFormats |= BarcodeReader::ConvertRuntimeToNative(type); } - m_hints->setFormats(barcodeFormats); + m_opts->setFormats(barcodeFormats); } } @@ -92,6 +92,8 @@ BarcodeFormat BarcodeReader::ConvertRuntimeToNative(BarcodeType type) return BarcodeFormat::QRCode; case BarcodeType::MICRO_QR_CODE: return BarcodeFormat::MicroQRCode; + case BarcodeType::RMQR_CODE: + return BarcodeFormat::RMQRCode; case BarcodeType::RSS_14: return BarcodeFormat::DataBar; case BarcodeType::RSS_EXPANDED: @@ -135,6 +137,8 @@ BarcodeType BarcodeReader::ConvertNativeToRuntime(BarcodeFormat format) return BarcodeType::QR_CODE; case BarcodeFormat::MicroQRCode: return BarcodeType::MICRO_QR_CODE; + case BarcodeFormat::RMQRCode: + return BarcodeType::RMQR_CODE; case BarcodeFormat::DataBar: return BarcodeType::RSS_14; case BarcodeFormat::DataBarExpanded: @@ -185,7 +189,7 @@ BarcodeReader::Read(SoftwareBitmap^ bitmap, int cropWidth, int cropHeight) auto img = ImageView(inBytes, bitmap->PixelWidth, bitmap->PixelHeight, fmt, inBuffer->GetPlaneDescription(0).Stride) .cropped(cropLeft, cropTop, cropWidth, cropHeight); - auto result = ReadBarcode(img, *m_hints); + auto result = ReadBarcode(img, *m_opts); if (result.isValid()) { return ref new ReadResult(ToPlatformString(ZXing::ToString(result.format())), ToPlatformString(result.text()), ConvertNativeToRuntime(result.format())); } diff --git a/wrappers/winrt/BarcodeReader.h b/wrappers/winrt/BarcodeReader.h index e68383e0e7..16e8352130 100644 --- a/wrappers/winrt/BarcodeReader.h +++ b/wrappers/winrt/BarcodeReader.h @@ -6,6 +6,7 @@ #pragma once #include "BarcodeFormat.h" +#include "ReaderOptions.h" #include @@ -25,13 +26,13 @@ public enum class BarcodeType : int { PDF_417, QR_CODE, MICRO_QR_CODE, + RMQR_CODE, RSS_14, RSS_EXPANDED, UPC_A, UPC_E }; -class DecodeHints; ref class ReadResult; public ref class BarcodeReader sealed @@ -51,7 +52,7 @@ public ref class BarcodeReader sealed static BarcodeFormat ConvertRuntimeToNative(BarcodeType type); static BarcodeType ConvertNativeToRuntime(BarcodeFormat format); - std::unique_ptr m_hints; + std::unique_ptr m_opts; }; } // ZXing diff --git a/zxing-cpp.podspec b/zxing-cpp.podspec new file mode 100644 index 0000000000..4db8ff0c6e --- /dev/null +++ b/zxing-cpp.podspec @@ -0,0 +1,39 @@ +Pod::Spec.new do |s| + s.name = 'zxing-cpp' + s.version = '2.2.0' + s.summary = 'C++ port of ZXing' + s.homepage = 'https://github.com/zxing-cpp/zxing-cpp' + s.author = 'axxel' + s.readme = 'https://raw.githubusercontent.com/zxing-cpp/zxing-cpp/master/wrappers/ios/README.md' + s.license = { + :type => 'Apache License 2.0', + :file => 'LICENSE' + } + s.source = { + :git => 'https://github.com/zxing-cpp/zxing-cpp.git', + :tag => "v#{s.version}" + } + s.module_name = 'ZXingCpp' + s.platform = :ios, '11.0' + s.library = ['c++'] + s.pod_target_xcconfig = { + 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++20' + } + + s.default_subspec = 'Wrapper' + + s.subspec 'Core' do |ss| + ss.source_files = 'core/src/**/*.{h,c,cpp}' + ss.private_header_files = 'core/src/**/*.h' + end + + s.subspec 'Wrapper' do |ss| + ss.dependency 'zxing-cpp/Core' + ss.frameworks = 'CoreGraphics', 'CoreImage', 'CoreVideo' + ss.source_files = 'wrappers/ios/Sources/Wrapper/**/*.{h,m,mm}' + ss.public_header_files = 'wrappers/ios/Sources/Wrapper/Reader/{ZXIBarcodeReader,ZXIResult,ZXIPosition,ZXIPoint,ZXIGTIN,ZXIReaderOptions}.h', + 'wrappers/ios/Sources/Wrapper/Writer/{ZXIBarcodeWriter,ZXIWriterOptions}.h', + 'wrappers/ios/Sources/Wrapper/{ZXIErrors,ZXIFormat}.h' + ss.exclude_files = 'wrappers/ios/Sources/Wrapper/UmbrellaHeader.h' + end +end pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy