diff --git a/core/src/qrcode/QRBitMatrixParser.cpp b/core/src/qrcode/QRBitMatrixParser.cpp index b970266c30..c554242226 100644 --- a/core/src/qrcode/QRBitMatrixParser.cpp +++ b/core/src/qrcode/QRBitMatrixParser.cpp @@ -135,6 +135,66 @@ 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(); @@ -185,9 +245,12 @@ ByteArray ReadCodewords(const BitMatrix& bitMatrix, const Version& version, cons { if (!hasValidDimension(bitMatrix, version.isMicroQRCode())) return {}; - - return version.isMicroQRCode() ? ReadMQRCodewords(bitMatrix, version, formatInfo) - : ReadQRCodewords(bitMatrix, version, formatInfo); + if (version.isMicroQRCode()) + return ReadMQRCodewords(bitMatrix, version, formatInfo); + else if (formatInfo.isModel1) + return ReadQRCodewordsModel1(bitMatrix, version, formatInfo); + else + return ReadQRCodewords(bitMatrix, version, formatInfo); } } // namespace ZXing::QRCode diff --git a/core/src/qrcode/QRDecoder.cpp b/core/src/qrcode/QRDecoder.cpp index 905f0a5c8d..2190618ca5 100644 --- a/core/src/qrcode/QRDecoder.cpp +++ b/core/src/qrcode/QRDecoder.cpp @@ -237,6 +237,9 @@ DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCo StructuredAppendInfo structuredAppend; const int modeBitLength = CodecModeBitsLength(version); + if (version.isQRCodeModel1()) + bits.readBits(4); // Model 1 is leading with 4 0-bits -> drop them + try { while(!IsEndOfStream(bits, version)) { @@ -316,14 +319,16 @@ DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCo DecoderResult Decode(const BitMatrix& bits) { - const Version* pversion = ReadVersion(bits); + bool isMicroQRCode = bits.height() < 21; + auto formatInfo = ReadFormatInformation(bits, isMicroQRCode); + if (!formatInfo.isValid()) + return FormatError("Invalid format information"); + + const Version* pversion = formatInfo.isModel1 ? Version::FromDimension(bits.height(), true) : ReadVersion(bits); if (!pversion) return FormatError("Invalid version"); - const Version& version = *pversion; - auto formatInfo = ReadFormatInformation(bits, version.isMicroQRCode()); - if (!formatInfo.isValid()) - return FormatError("Invalid format information"); + const Version& version = *pversion; // Read codewords ByteArray codewords = ReadCodewords(bits, version, formatInfo); diff --git a/core/src/qrcode/QRFormatInformation.cpp b/core/src/qrcode/QRFormatInformation.cpp index 9195a7fa65..c5502d1553 100644 --- a/core/src/qrcode/QRFormatInformation.cpp +++ b/core/src/qrcode/QRFormatInformation.cpp @@ -15,6 +15,8 @@ namespace ZXing::QRCode { static const int FORMAT_INFO_MASK_QR = 0x5412; +static const int FORMAT_INFO_MASK_QR_MODEL1 = 0x2825; + /** * See ISO 18004:2006, Annex C, Table C.1 */ @@ -93,13 +95,12 @@ static uint32_t MirrorBits(uint32_t bits) return BitHacks::Reverse(bits) >> 17; } -static FormatInformation FindBestFormatInfo(int mask, const std::array, 32> lookup, +static FormatInformation FindBestFormatInfo(const std::vector& masks, const std::array, 32> lookup, const std::vector& bits) { 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 @@ -122,8 +123,16 @@ 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 QR codes apparently do not apply the XOR mask. Try without and with additional masking. + auto fi = FindBestFormatInfo({0, FORMAT_INFO_MASK_QR}, FORMAT_INFO_DECODE_LOOKUP, {formatInfoBits1, formatInfoBits2, MirrorBits(formatInfoBits1), mirroredFormatInfoBits2}); + auto fi_model1 = FindBestFormatInfo({FORMAT_INFO_MASK_QR ^ FORMAT_INFO_MASK_QR_MODEL1}, FORMAT_INFO_DECODE_LOOKUP, + {formatInfoBits1, formatInfoBits2, MirrorBits(formatInfoBits1), mirroredFormatInfoBits2}); + + if (fi_model1.hammingDistance < fi.hammingDistance) { + fi_model1.isModel1 = true; + fi = fi_model1; + } // Use bits 3/4 for error correction, and 0-2 for mask. fi.ecLevel = ECLevelFromBits((fi.index >> 3) & 0x03); @@ -139,7 +148,7 @@ 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({0}, FORMAT_INFO_DECODE_LOOKUP_MICRO, {formatInfoBits, MirrorBits(formatInfoBits)}); constexpr uint8_t BITS_TO_VERSION[] = {1, 2, 2, 3, 3, 4, 4, 4}; diff --git a/core/src/qrcode/QRFormatInformation.h b/core/src/qrcode/QRFormatInformation.h index f5ebc096df..ec2d1ce322 100644 --- a/core/src/qrcode/QRFormatInformation.h +++ b/core/src/qrcode/QRFormatInformation.h @@ -18,6 +18,7 @@ class FormatInformation uint8_t index = 255; uint8_t hammingDistance = 255; bool isMirrored = false; + bool isModel1 = false; uint8_t dataMask = 0; uint8_t microVersion = 0; uint8_t bitsIndex = 255; diff --git a/core/src/qrcode/QRVersion.cpp b/core/src/qrcode/QRVersion.cpp index 2f202b654d..83971c85ac 100644 --- a/core/src/qrcode/QRVersion.cpp +++ b/core/src/qrcode/QRVersion.cpp @@ -292,35 +292,136 @@ const Version* Version::AllMicroVersions() return allVersions; } +const Version* Version::AllModel1Versions() +{ + /** + * 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, + 88, 2, 46 , 0, 0, + }}, + {6, { + 34 , 1, 136, 0, 0, + 63 , 2, 106, 0, 0, + 84 , 2, 86 , 0, 0, + 112, 2, 58 , 0, 0, + }}, + {7, { + 42 , 1, 170, 0, 0, + 80 , 2, 132, 0, 0, + 104, 2, 108, 0, 0, + 138, 3, 72 , 0, 0, + }}, + {8, { + 48 , 2, 208, 0, 0, + 96 , 2, 160, 0, 0, + 128, 2, 128, 0, 0, + 168, 3, 87 , 0, 0, + }}, + {9, { + 60 , 2, 246, 0, 0, + 120, 2, 186, 0, 0, + 150, 3, 156, 0, 0, + 204, 3, 102, 0, 0, + }}, + {10, { + 68 , 2, 290, 0, 0, + 136, 2, 222, 0, 0, + 174, 3, 183, 0, 0, + 232, 4, 124, 0, 0, + }}, + {11, { + 80 , 2, 336, 0, 0, + 160, 4, 256, 0, 0, + 208, 4, 208, 0, 0, + 270, 5, 145, 0, 0, + }}, + {12, { + 92 , 2, 384, 0, 0, + 184, 4, 292, 0, 0, + 232, 4, 244, 0, 0, + 310, 5, 165, 0, 0, + }}, + {13, { + 108, 3, 432, 0, 0, + 208, 4, 332, 0, 0, + 264, 4, 276, 0, 0, + 348, 6, 192, 0, 0, + }}, + {14, { + 120, 3, 489, 0, 0, + 240, 4, 368, 0, 0, + 300, 5, 310, 0, 0, + 396, 6, 210, 0, 0, + }}, + }; + return allVersions; +} + +static inline bool isMicro(const std::array& ecBlocks) +{ + return ecBlocks[0].codewordsPerBlock < 7 || ecBlocks[0].codewordsPerBlock == 8; +} + 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), _isMicro(false), _isModel1(false) { _totalCodewords = ecBlocks[0].totalDataCodewords(); } Version::Version(int versionNumber, const std::array& ecBlocks) - : _versionNumber(versionNumber), _ecBlocks(ecBlocks), _isMicro(true) + : _versionNumber(versionNumber), _ecBlocks(ecBlocks), _isMicro(isMicro(ecBlocks)), _isModel1(!isMicro(ecBlocks)) { _totalCodewords = ecBlocks[0].totalDataCodewords(); } -const Version* Version::FromNumber(int versionNumber, bool isMicro) +const Version* Version::FromNumber(int versionNumber, bool isMicro, bool isModel1) { - if (versionNumber < 1 || versionNumber > (isMicro ? 4 : 40)) { + if (versionNumber < 1 || versionNumber > (isMicro ? 4 : (isModel1 ? 14 : 40))) { //throw std::invalid_argument("Version should be in range [1-40]."); return nullptr; } - return &(isMicro ? AllMicroVersions() : AllVersions())[versionNumber - 1]; + + return &(isMicro ? AllMicroVersions() : (isModel1 ? AllModel1Versions() : AllVersions()))[versionNumber - 1]; } -const Version* Version::FromDimension(int dimension) +const Version* Version::FromDimension(int dimension, bool isModel1) { 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 FromNumber((dimension - DimensionOffset(isMicro)) / DimensionStep(isMicro), isMicro, isModel1); } const Version* Version::DecodeVersionInformation(int versionBitsA, int versionBitsB) diff --git a/core/src/qrcode/QRVersion.h b/core/src/qrcode/QRVersion.h index 0b03270011..eca41d87e3 100644 --- a/core/src/qrcode/QRVersion.h +++ b/core/src/qrcode/QRVersion.h @@ -40,6 +40,7 @@ class Version BitMatrix buildFunctionPattern() const; bool isMicroQRCode() const { return _isMicro; } + bool isQRCodeModel1() const { return _isModel1; } static constexpr int DimensionStep(bool isMicro) { return std::array{4, 2}[isMicro]; } static constexpr int DimensionOffset(bool isMicro) { return std::array{17, 9}[isMicro]; } @@ -54,9 +55,9 @@ class Version * @param dimension dimension in modules * @return Version for a QR Code of that dimension */ - static const Version* FromDimension(int dimension); + static const Version* FromDimension(int dimension, bool isModel1 = false); - static const Version* FromNumber(int versionNumber, bool isMicro = false); + static const Version* FromNumber(int versionNumber, bool isMicro = false, bool isModel1 = false); static const Version* DecodeVersionInformation(int versionBitsA, int versionBitsB = 0); @@ -66,11 +67,13 @@ class Version std::array _ecBlocks; int _totalCodewords; bool _isMicro; + bool _isModel1; 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(); + static const Version* AllModel1Versions(); }; } // QRCode diff --git a/test/blackbox/BlackboxTestRunner.cpp b/test/blackbox/BlackboxTestRunner.cpp index af2e247d80..e98f22a31b 100644 --- a/test/blackbox/BlackboxTestRunner.cpp +++ b/test/blackbox/BlackboxTestRunner.cpp @@ -562,12 +562,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, { 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.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/unit/qrcode/QRFormatInformationTest.cpp b/test/unit/qrcode/QRFormatInformationTest.cpp index bbbef8a0ed..fb933e0ab5 100644 --- a/test/unit/qrcode/QRFormatInformationTest.cpp +++ b/test/unit/qrcode/QRFormatInformationTest.cpp @@ -15,7 +15,6 @@ 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 void DoFormatInformationTest(const int formatInfo, const uint8_t expectedMask, const ErrorCorrectionLevel& expectedECL) { @@ -43,7 +42,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.isModel1); } TEST(QRFormatInformationTest, DecodeWithMisread) @@ -65,6 +66,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); }