diff --git a/core/src/aztec/AZDecoder.cpp b/core/src/aztec/AZDecoder.cpp index c33324afcf..f38fbe3fef 100644 --- a/core/src/aztec/AZDecoder.cpp +++ b/core/src/aztec/AZDecoder.cpp @@ -14,6 +14,7 @@ #include "GenericGF.h" #include "ReedSolomonDecoder.h" #include "ZXTestSupport.h" +#include "ZXAlgorithms.h" #include #include @@ -346,9 +347,24 @@ DecoderResult Decode(const BitArray& bits) return DecoderResult(std::move(res)).setStructuredAppend(sai); } +DecoderResult DecodeRune(const DetectorResult& detectorResult) { + Content res; + res.symbology = {'z', 'C', 0}; // Runes cannot have ECI + + // Bizarrely, this is what it says to do in the spec + auto runeString = ToString(detectorResult.runeValue(), 3); + res.append(runeString); + + return DecoderResult(std::move(res)); +} + DecoderResult Decode(const DetectorResult& detectorResult) { try { + if (detectorResult.nbLayers() == 0) { + // This is a rune - just return the rune value + return DecodeRune(detectorResult); + } auto bits = CorrectBits(detectorResult, ExtractBits(detectorResult)); return Decode(bits); } catch (Error e) { diff --git a/core/src/aztec/AZDetector.cpp b/core/src/aztec/AZDetector.cpp index bd528d2b64..51975958d6 100644 --- a/core/src/aztec/AZDetector.cpp +++ b/core/src/aztec/AZDetector.cpp @@ -109,12 +109,17 @@ static std::optional LocateAztecCenter(const BitMatrix& image static std::vector FindPureFinderPattern(const BitMatrix& image) { int left, top, width, height; - if (!image.findBoundingBox(left, top, width, height, 11)) // 11 is the size of an Aztec Rune, see ISO/IEC 24778:2008(E) Annex A - return {}; + if (!image.findBoundingBox(left, top, width, height, 11)) { // 11 is the size of an Aztec Rune, see ISO/IEC 24778:2008(E) Annex A + // Runes 68 and 223 have none of their bits set on the bottom row + if (image.findBoundingBox(left, top, width, height, 10) && (width == 11) && (height == 10)) + height = 11; + else + return {}; + } PointF p(left + width / 2, top + height / 2); constexpr auto PATTERN = FixedPattern<7, 7>{1, 1, 1, 1, 1, 1, 1}; - if (auto pattern = LocateConcentricPattern(image, PATTERN, p, width / 2)) + if (auto pattern = LocateConcentricPattern(image, PATTERN, p, width)) return {*pattern}; else return {}; @@ -261,9 +266,10 @@ static uint32_t SampleOrientationBits(const BitMatrix& image, const PerspectiveT return bits; } -static int ModeMessage(const BitMatrix& image, const PerspectiveTransform& mod2Pix, int radius) +static int ModeMessage(const BitMatrix& image, const PerspectiveTransform& mod2Pix, int radius, bool& isRune) { const bool compact = radius == 5; + isRune = false; // read the bits between the corner bits along the 4 edges uint64_t bits = 0; @@ -291,7 +297,21 @@ static int ModeMessage(const BitMatrix& image, const PerspectiveTransform& mod2P words[i] = narrow_cast(bits & 0xF); bits >>= 4; } - if (!ReedSolomonDecode(GenericGF::AztecParam(), words, numECCodewords)) + + bool decodeResult = ReedSolomonDecode(GenericGF::AztecParam(), words, numECCodewords); + + if ((!decodeResult) && compact) { + // Is this a Rune? + for (auto& word : words) + word ^= 0b1010; + + decodeResult = ReedSolomonDecode(GenericGF::AztecParam(), words, numECCodewords); + + if (decodeResult) + isRune = true; + } + + if (!decodeResult) return -1; int res = 0; @@ -350,6 +370,7 @@ DetectorResults Detect(const BitMatrix& image, bool isPure, bool tryHarder, int int mirror; // 0 or 1 int rotate; // [0..3] int modeMessage = -1; + bool isRune = false; [&]() { // 24778:2008(E) 14.3.3 reads: // In the outer layer of the Core Symbol, the 12 orientation bits at the corners are bitwise compared against the specified @@ -369,7 +390,7 @@ DetectorResults Detect(const BitMatrix& image, bool isPure, bool tryHarder, int rotate = FindRotation(bits, mirror); if (rotate == -1) continue; - modeMessage = ModeMessage(image, PerspectiveTransform(srcQuad, RotatedCorners(*fpQuad, rotate, mirror)), radius); + modeMessage = ModeMessage(image, PerspectiveTransform(srcQuad, RotatedCorners(*fpQuad, rotate, mirror)), radius, isRune); if (modeMessage != -1) return; } @@ -399,7 +420,9 @@ DetectorResults Detect(const BitMatrix& image, bool isPure, bool tryHarder, int int nbLayers = 0; int nbDataBlocks = 0; bool readerInit = false; - ExtractParameters(modeMessage, radius == 5, nbLayers, nbDataBlocks, readerInit); + if (!isRune) { + ExtractParameters(modeMessage, radius == 5, nbLayers, nbDataBlocks, readerInit); + } int dim = radius == 5 ? 4 * nbLayers + 11 : 4 * nbLayers + 2 * ((2 * nbLayers + 6) / 15) + 15; double low = dim / 2.0 + srcQuad[0].x; @@ -409,7 +432,7 @@ DetectorResults Detect(const BitMatrix& image, bool isPure, bool tryHarder, int if (!bits.isValid()) continue; - res.emplace_back(std::move(bits), radius == 5, nbDataBlocks, nbLayers, readerInit, mirror != 0); + res.emplace_back(std::move(bits), radius == 5, nbDataBlocks, nbLayers, readerInit, mirror != 0, isRune ? modeMessage : -1); if (Size(res) == maxSymbols) break; diff --git a/core/src/aztec/AZDetectorResult.h b/core/src/aztec/AZDetectorResult.h index cd65e9daab..37e60a1554 100644 --- a/core/src/aztec/AZDetectorResult.h +++ b/core/src/aztec/AZDetectorResult.h @@ -19,6 +19,7 @@ class DetectorResult : public ZXing::DetectorResult int _nbLayers = 0; bool _readerInit = false; bool _isMirrored = false; + int _runeValue = -1; DetectorResult(const DetectorResult&) = delete; DetectorResult& operator=(const DetectorResult&) = delete; @@ -28,13 +29,14 @@ class DetectorResult : public ZXing::DetectorResult DetectorResult(DetectorResult&&) noexcept = default; DetectorResult& operator=(DetectorResult&&) noexcept = default; - DetectorResult(ZXing::DetectorResult&& result, bool isCompact, int nbDatablocks, int nbLayers, bool readerInit, bool isMirrored) + DetectorResult(ZXing::DetectorResult&& result, bool isCompact, int nbDatablocks, int nbLayers, bool readerInit, bool isMirrored, int runeValue) : ZXing::DetectorResult{std::move(result)}, _compact(isCompact), _nbDatablocks(nbDatablocks), _nbLayers(nbLayers), _readerInit(readerInit), - _isMirrored(isMirrored) + _isMirrored(isMirrored), + _runeValue(runeValue) {} bool isCompact() const { return _compact; } @@ -42,6 +44,9 @@ class DetectorResult : public ZXing::DetectorResult int nbLayers() const { return _nbLayers; } bool readerInit() const { return _readerInit; } bool isMirrored() const { return _isMirrored; } + + // Only meaningful is nbDatablocks == 0 + int runeValue() const { return _runeValue; } }; } // namespace ZXing::Aztec diff --git a/core/src/aztec/AZEncoder.cpp b/core/src/aztec/AZEncoder.cpp index f45eb000db..00b3108463 100644 --- a/core/src/aztec/AZEncoder.cpp +++ b/core/src/aztec/AZEncoder.cpp @@ -82,6 +82,19 @@ void GenerateModeMessage(bool compact, int layers, int messageSizeInWords, BitAr } } +ZXING_EXPORT_TEST_ONLY +void GenerateRuneMessage(uint8_t word, BitArray& runeMessage) +{ + runeMessage = BitArray(); + runeMessage.appendBits(word, 8); + GenerateCheckWords(runeMessage, 28, 4, runeMessage); + // Now flip every other bit + + BitArray xorBits; + xorBits.appendBits(0xAAAAAAAA, 28); + runeMessage.bitwiseXOR(xorBits); +} + static void DrawModeMessage(BitMatrix& matrix, bool compact, int matrixSize, const BitArray& modeMessage) { int center = matrixSize / 2; @@ -176,7 +189,13 @@ Encoder::Encode(const std::string& data, int minECCPercent, int userSpecifiedLay int totalBitsInLayer; int wordSize; BitArray stuffedBits; - if (userSpecifiedLayers != DEFAULT_AZTEC_LAYERS) { + if (userSpecifiedLayers == AZTEC_RUNE_LAYERS) { + compact = true; + layers = 0; + totalBitsInLayer = 0; + wordSize = 0; + stuffedBits = BitArray(0); + } else if (userSpecifiedLayers != DEFAULT_AZTEC_LAYERS) { compact = userSpecifiedLayers < 0; layers = std::abs(userSpecifiedLayers); if (layers > (compact ? MAX_NB_BITS_COMPACT : MAX_NB_BITS)) { @@ -225,13 +244,21 @@ Encoder::Encode(const std::string& data, int minECCPercent, int userSpecifiedLay } } } - BitArray messageBits; - GenerateCheckWords(stuffedBits, totalBitsInLayer, wordSize, messageBits); - // generate mode message - int messageSizeInWords = stuffedBits.size() / wordSize; + BitArray messageBits; BitArray modeMessage; - GenerateModeMessage(compact, layers, messageSizeInWords, modeMessage); + int messageSizeInWords; + + if (layers == 0) { + // This is a rune, and messageBits should be empty + messageBits = BitArray(0); + messageSizeInWords = 0; + GenerateRuneMessage(data[0], modeMessage); + } else { + GenerateCheckWords(stuffedBits, totalBitsInLayer, wordSize, messageBits); + messageSizeInWords = stuffedBits.size() / wordSize; + GenerateModeMessage(compact, layers, messageSizeInWords, modeMessage); + } // allocate symbol int baseMatrixSize = (compact ? 11 : 14) + layers * 4; // not including alignment lines diff --git a/core/src/aztec/AZEncoder.h b/core/src/aztec/AZEncoder.h index 47311c0c90..b5cd7ff465 100644 --- a/core/src/aztec/AZEncoder.h +++ b/core/src/aztec/AZEncoder.h @@ -36,6 +36,7 @@ class Encoder public: static const int DEFAULT_EC_PERCENT = 33; // default minimal percentage of error check words static const int DEFAULT_AZTEC_LAYERS = 0; + static const int AZTEC_RUNE_LAYERS = 0xFF; static EncodeResult Encode(const std::string& data, int minECCPercent, int userSpecifiedLayers); }; diff --git a/test/blackbox/BlackboxTestRunner.cpp b/test/blackbox/BlackboxTestRunner.cpp index 226ea2e7ff..a3fed245b1 100644 --- a/test/blackbox/BlackboxTestRunner.cpp +++ b/test/blackbox/BlackboxTestRunner.cpp @@ -334,12 +334,16 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set auto startTime = std::chrono::steady_clock::now(); // clang-format off - runTests("aztec-1", "Aztec", 27, { - { 26, 27, 0 }, - { 26, 27, 90 }, - { 26, 27, 180 }, - { 26, 27, 270 }, - { 25, 0, pure }, + + // Expected failures: + // abc-inverted.png (fast) - fast does not try inverted + // az-thick.png (pure) + runTests("aztec-1", "Aztec", 31, { + { 30, 31, 0 }, + { 30, 31, 90 }, + { 30, 31, 180 }, + { 30, 31, 270 }, + { 29, 0, pure }, }); runTests("aztec-2", "Aztec", 22, { diff --git a/test/samples/aztec-1/rune-1.png b/test/samples/aztec-1/rune-1.png new file mode 100644 index 0000000000..3fbb06cd88 Binary files /dev/null and b/test/samples/aztec-1/rune-1.png differ diff --git a/test/samples/aztec-1/rune-1.result.txt b/test/samples/aztec-1/rune-1.result.txt new file mode 100644 index 0000000000..2be1501c62 --- /dev/null +++ b/test/samples/aztec-1/rune-1.result.txt @@ -0,0 +1 @@ +symbologyIdentifier=]zC \ No newline at end of file diff --git a/test/samples/aztec-1/rune-1.txt b/test/samples/aztec-1/rune-1.txt new file mode 100644 index 0000000000..0f3016682b --- /dev/null +++ b/test/samples/aztec-1/rune-1.txt @@ -0,0 +1 @@ +001 \ No newline at end of file diff --git a/test/samples/aztec-1/rune-223.png b/test/samples/aztec-1/rune-223.png new file mode 100644 index 0000000000..784a0649c3 Binary files /dev/null and b/test/samples/aztec-1/rune-223.png differ diff --git a/test/samples/aztec-1/rune-223.txt b/test/samples/aztec-1/rune-223.txt new file mode 100644 index 0000000000..fca7fbef0e --- /dev/null +++ b/test/samples/aztec-1/rune-223.txt @@ -0,0 +1 @@ +223 \ No newline at end of file diff --git a/test/samples/aztec-1/rune-64.png b/test/samples/aztec-1/rune-64.png new file mode 100644 index 0000000000..725929e449 Binary files /dev/null and b/test/samples/aztec-1/rune-64.png differ diff --git a/test/samples/aztec-1/rune-64.txt b/test/samples/aztec-1/rune-64.txt new file mode 100644 index 0000000000..06b7423029 --- /dev/null +++ b/test/samples/aztec-1/rune-64.txt @@ -0,0 +1 @@ +064 \ No newline at end of file diff --git a/test/samples/aztec-1/rune-68.png b/test/samples/aztec-1/rune-68.png new file mode 100644 index 0000000000..bb7086941f Binary files /dev/null and b/test/samples/aztec-1/rune-68.png differ diff --git a/test/samples/aztec-1/rune-68.txt b/test/samples/aztec-1/rune-68.txt new file mode 100644 index 0000000000..9c93a7313c --- /dev/null +++ b/test/samples/aztec-1/rune-68.txt @@ -0,0 +1 @@ +068 \ No newline at end of file diff --git a/test/unit/aztec/AZDecoderTest.cpp b/test/unit/aztec/AZDecoderTest.cpp index beae00357c..2d85313e4a 100644 --- a/test/unit/aztec/AZDecoderTest.cpp +++ b/test/unit/aztec/AZDecoderTest.cpp @@ -25,7 +25,7 @@ using namespace ZXing; // Shorthand to call Decode() static DecoderResult parse(BitMatrix&& bits, bool compact, int nbDatablocks, int nbLayers) { - return Aztec::Decode({{std::move(bits), {}}, compact, nbDatablocks, nbLayers, false /*readerInit*/, false /*isMirrored*/}); + return Aztec::Decode({{std::move(bits), {}}, compact, nbDatablocks, nbLayers, false /*readerInit*/, false /*isMirrored*/, 0 /*runeValue*/}); } TEST(AZDecoderTest, AztecResult) diff --git a/test/unit/aztec/AZDetectorTest.cpp b/test/unit/aztec/AZDetectorTest.cpp index 242932f3b3..b58061e34e 100644 --- a/test/unit/aztec/AZDetectorTest.cpp +++ b/test/unit/aztec/AZDetectorTest.cpp @@ -426,3 +426,63 @@ TEST(AZDetectorTest, ReaderInitCompact) EXPECT_EQ(r.nbLayers(), 1); } } + +TEST(AZDetectorTest, Rune) +{ + { + auto r = Aztec::Detect(ParseBitMatrix( + "X X X X X X X \n" + "X X X X X X X X X X X \n" + " X X \n" + "X X X X X X X X X \n" + " X X X X \n" + "X X X X X X X \n" + " X X X X \n" + "X X X X X X X X X \n" + " X X \n" + " X X X X X X X X X X \n" + " X X X X \n" + ), false /*isPure*/, false /*tryHarder*/); + + EXPECT_TRUE(r.isValid()); + EXPECT_EQ(r.nbDatablocks(), 0); + EXPECT_EQ(r.runeValue(), 0); + } + { + auto r = Aztec::Detect(ParseBitMatrix( + "X X X X X X X \n" + "X X X X X X X X X X X \n" + " X X X \n" + " X X X X X X X X \n" + " X X X X \n" + "X X X X X X X \n" + "X X X X X X \n" + "X X X X X X X X \n" + "X X X X \n" + " X X X X X X X X X X \n" + " X X \n" + ), true /*isPure*/, false /*tryHarder*/); + + EXPECT_TRUE(r.isValid()); + EXPECT_EQ(r.nbDatablocks(), 0); + EXPECT_EQ(r.runeValue(), 25); + } + { + auto r = Aztec::Detect(ParseBitMatrix( + "X X X X \n" + "X X X X X X X X X X X \n" + " X X \n" + "X X X X X X X X \n" + " X X X X X \n" + " X X X X X \n" + " X X X X X \n" + " X X X X X X X X \n" + "X X X \n" + " X X X X X X X X X X \n" + " X X \n" + ), true /*isPure*/, false /*tryHarder*/); + + // This is just the core of a regular compact code, and not a valid rune + EXPECT_FALSE(r.isValid()); + } +} \ No newline at end of file diff --git a/test/unit/aztec/AZEncodeDecodeTest.cpp b/test/unit/aztec/AZEncodeDecodeTest.cpp index e07824f97b..8e84fe1a1d 100644 --- a/test/unit/aztec/AZEncodeDecodeTest.cpp +++ b/test/unit/aztec/AZEncodeDecodeTest.cpp @@ -10,6 +10,7 @@ #include "PseudoRandom.h" #include "TextEncoder.h" #include "aztec/AZDecoder.h" +#include "aztec/AZDetector.h" #include "aztec/AZDetectorResult.h" #include "aztec/AZEncoder.h" #include "aztec/AZWriter.h" @@ -35,7 +36,7 @@ namespace { // Shorthand to call Decode() static DecoderResult parse(BitMatrix&& bits, bool compact, int nbDatablocks, int nbLayers) { - return Aztec::Decode({{std::move(bits), {}}, compact, nbDatablocks, nbLayers, false /*readerInit*/, false /*isMirrored*/}); + return Aztec::Decode({{std::move(bits), {}}, compact, nbDatablocks, nbLayers, false /*readerInit*/, false /*isMirrored*/, 0 /*runeValue*/}); } void TestEncodeDecode(const std::string& data, bool compact, int layers) { @@ -228,3 +229,17 @@ TEST(AZEncodeDecodeTest, AztecWriter) Aztec::Encoder::DEFAULT_EC_PERCENT, Aztec::Encoder::DEFAULT_AZTEC_LAYERS); EXPECT_EQ(matrix, aztec.matrix); } + +TEST(AZEncodeDecodeTest, RunePure) +{ + for(uint8_t word = 0; word < 255; word++) { + std::string data(1, word); + Aztec::EncodeResult aztec = + Aztec::Encoder::Encode(data, 0, Aztec::Encoder::AZTEC_RUNE_LAYERS); + + auto result = Aztec::Detect(aztec.matrix, true, false); + EXPECT_TRUE(result.isValid()); + EXPECT_EQ(result.nbDatablocks(), 0); + EXPECT_EQ(result.runeValue(), word); + } +} \ No newline at end of file diff --git a/test/unit/aztec/AZEncoderTest.cpp b/test/unit/aztec/AZEncoderTest.cpp index 045f15e3f3..06acb2e641 100644 --- a/test/unit/aztec/AZEncoderTest.cpp +++ b/test/unit/aztec/AZEncoderTest.cpp @@ -200,3 +200,52 @@ TEST(AZEncoderTest, BorderCompact4Case) EXPECT_TRUE(aztec.compact); EXPECT_EQ(aztec.layers, 4); } + + +TEST(AZEncoderTest, Rune) +{ + { + Aztec::EncodeResult aztec = Aztec::Encoder::Encode("\x19", 0, Aztec::Encoder::AZTEC_RUNE_LAYERS); + + EXPECT_EQ(aztec.layers, 0); + EXPECT_EQ(aztec.matrix, ParseBitMatrix( + "X X X X X X X \n" + "X X X X X X X X X X X \n" + " X X X \n" + " X X X X X X X X \n" + " X X X X \n" + "X X X X X X X \n" + "X X X X X X \n" + "X X X X X X X X \n" + "X X X X \n" + " X X X X X X X X X X \n" + " X X \n" + )); + } + { + Aztec::EncodeResult aztec = Aztec::Encoder::Encode("\xFF", 0, Aztec::Encoder::AZTEC_RUNE_LAYERS); + + EXPECT_EQ(aztec.layers, 0); + EXPECT_EQ(aztec.matrix, ParseBitMatrix( + "X X X X X X \n" + "X X X X X X X X X X X \n" + " X X X \n" + "X X X X X X X X X \n" + "X X X X X X \n" + " X X X X X X \n" + " X X X X \n" + "X X X X X X X X X \n" + "X X X \n" + " X X X X X X X X X X \n" + " X X X X X \n" + )); + } + { + Aztec::EncodeResult aztec = Aztec::Encoder::Encode("\x44", 0, Aztec::Encoder::AZTEC_RUNE_LAYERS); + + std::cout << ToString(aztec.matrix, 'X', ' ', true); + } + + + +} \ No newline at end of file