Skip to content

Add support for detecting and reading Aztec Runes #763

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions core/src/aztec/AZDecoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "GenericGF.h"
#include "ReedSolomonDecoder.h"
#include "ZXTestSupport.h"
#include "ZXAlgorithms.h"

#include <cctype>
#include <cstring>
Expand Down Expand Up @@ -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) {
Expand Down
39 changes: 31 additions & 8 deletions core/src/aztec/AZDetector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,17 @@ static std::optional<ConcentricPattern> LocateAztecCenter(const BitMatrix& image
static std::vector<ConcentricPattern> 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 {};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -291,7 +297,21 @@ static int ModeMessage(const BitMatrix& image, const PerspectiveTransform& mod2P
words[i] = narrow_cast<int>(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;
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
9 changes: 7 additions & 2 deletions core/src/aztec/AZDetectorResult.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,20 +29,24 @@ 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; }
int nbDatablocks() const { return _nbDatablocks; }
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
39 changes: 33 additions & 6 deletions core/src/aztec/AZEncoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions core/src/aztec/AZEncoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
16 changes: 10 additions & 6 deletions test/blackbox/BlackboxTestRunner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,16 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set<std::string>
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, {
Expand Down
Binary file added test/samples/aztec-1/rune-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/samples/aztec-1/rune-1.result.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
symbologyIdentifier=]zC
1 change: 1 addition & 0 deletions test/samples/aztec-1/rune-1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
001
Binary file added test/samples/aztec-1/rune-223.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/samples/aztec-1/rune-223.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
223
Binary file added test/samples/aztec-1/rune-64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/samples/aztec-1/rune-64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
064
Binary file added test/samples/aztec-1/rune-68.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/samples/aztec-1/rune-68.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
068
2 changes: 1 addition & 1 deletion test/unit/aztec/AZDecoderTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions test/unit/aztec/AZDetectorTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
17 changes: 16 additions & 1 deletion test/unit/aztec/AZEncodeDecodeTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
}
Loading
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