Skip to content

Add support for Rectangular Micro QR Code (rMQR), pure only #681

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 3 commits into from
Dec 6, 2023
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
1 change: 1 addition & 0 deletions core/src/BarcodeFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ static BarcodeFormatName NAMES[] = {
{BarcodeFormat::MicroQRCode, "MicroQRCode"},
{BarcodeFormat::PDF417, "PDF417"},
{BarcodeFormat::QRCode, "QRCode"},
{BarcodeFormat::rMQR, "rMQR"},
{BarcodeFormat::UPCA, "UPC-A"},
{BarcodeFormat::UPCE, "UPC-E"},
{BarcodeFormat::LinearCodes, "Linear-Codes"},
Expand Down
5 changes: 3 additions & 2 deletions core/src/BarcodeFormat.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ enum class BarcodeFormat
UPCA = (1 << 14), ///< UPC-A
UPCE = (1 << 15), ///< UPC-E
MicroQRCode = (1 << 16), ///< Micro QR Code
rMQR = (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 | rMQR,
Any = LinearCodes | MatrixCodes,

_max = MicroQRCode, ///> implementation detail, don't use
_max = rMQR, ///> implementation detail, don't use
};

ZX_DECLARE_FLAGS(BarcodeFormats, BarcodeFormat)
Expand Down
2 changes: 1 addition & 1 deletion core/src/MultiFormatReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ MultiFormatReader::MultiFormatReader(const DecodeHints& hints) : _hints(hints)
if (formats.testFlags(BarcodeFormat::LinearCodes) && !hints.tryHarder())
_readers.emplace_back(new OneD::Reader(hints));

if (formats.testFlags(BarcodeFormat::QRCode | BarcodeFormat::MicroQRCode))
if (formats.testFlags(BarcodeFormat::QRCode | BarcodeFormat::MicroQRCode | BarcodeFormat::rMQR))
_readers.emplace_back(new QRCode::Reader(hints, true));
if (formats.testFlag(BarcodeFormat::DataMatrix))
_readers.emplace_back(new DataMatrix::Reader(hints, true));
Expand Down
15 changes: 9 additions & 6 deletions core/src/Result.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
58 changes: 58 additions & 0 deletions core/src/qrcode/QRBitMatrixParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const Version* ReadVersion(const BitMatrix& bitMatrix, Type type)

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;
}
Expand Down Expand Up @@ -67,6 +68,25 @@ FormatInformation ReadFormatInformation(const BitMatrix& bitMatrix)

return FormatInformation::DecodeMQR(formatInfoBits);
}
if (Version::HasRMQRSize(bitMatrix)) {
// 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;
Expand Down Expand Up @@ -237,10 +257,48 @@ static ByteArray ReadMQRCodewords(const BitMatrix& bitMatrix, const QRCode::Vers
return result;
}

static ByteArray ReadRMQRCodewords(const BitMatrix& bitMatrix, const Version& version, const FormatInformation& formatInfo)
{
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 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);
}
Expand Down
35 changes: 28 additions & 7 deletions core/src/qrcode/QRCodecMode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,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<CodecMode>(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<CodecMode>(bits);
}

throw FormatError("Invalid codec mode");
Expand All @@ -42,6 +49,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)
Expand All @@ -63,12 +84,12 @@ int CharacterCountBits(CodecMode mode, const Version& version)

int CodecModeBitsLength(const Version& version)
{
return version.isMicro() ? version.versionNumber() - 1 : 4;
return version.isMicro() ? version.versionNumber() - 1 : 4 - version.isRMQR();
}

int TerminatorBitsLength(const Version& version)
{
return version.isMicro() ? version.versionNumber() * 2 + 1 : 4;
return version.isMicro() ? version.versionNumber() * 2 + 1 : 4 - version.isRMQR();
}

} // namespace ZXing::QRCode
5 changes: 3 additions & 2 deletions core/src/qrcode/QRCodecMode.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace ZXing::QRCode {

enum class Type;
class Version;

/**
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion core/src/qrcode/QRDecoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,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.isMicro());
mode = CodecModeForBits(bits.readBits(modeBitLength), version.type());

switch (mode) {
case CodecMode::FNC1_FIRST_POSITION:
Expand Down
131 changes: 131 additions & 0 deletions core/src/qrcode/QRDetector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
namespace ZXing::QRCode {

constexpr auto PATTERN = FixedPattern<5, 7>{1, 1, 3, 1, 1};
constexpr auto SUBPATTERN_RMQR = FixedPattern<5, 5>{1, 1, 1, 1, 1};
constexpr auto CORNER_EDGE_RMQR = FixedPattern<2, 4>{3, 1};
constexpr bool E2E = true;

PatternView FindPattern(const PatternView& view)
Expand Down Expand Up @@ -601,6 +603,84 @@ DetectorResult DetectPureMQR(const BitMatrix& image)
{{left, top}, {right, top}, {right, bottom}, {left, bottom}}};
}

DetectorResult DetectPureRMQR(const BitMatrix& image)
{
using Pattern = std::array<PatternView::value_type, PATTERN.size()>;
using SubPattern = std::array<PatternView::value_type, SUBPATTERN_RMQR.size()>;
using CornerEdgePattern = std::array<PatternView::value_type, CORNER_EDGE_RMQR.size()>;

#ifdef PRINT_DEBUG
SaveAsPBM(image, "weg.pbm");
#endif

constexpr int MIN_MODULES = 7;
constexpr int MIN_MODULES_W = 27;
constexpr int MIN_MODULES_H = 7;
constexpr int MAX_MODULES_W = 139;
constexpr int MAX_MODULES_H = 17;

int left, top, width, height;
if (!image.findBoundingBox(left, top, width, height, MIN_MODULES))
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<Pattern>(1);
if (!IsPattern(diagonal, PATTERN))
return {};

// Finder sub pattern
auto subdiagonal = BitMatrixCursorI(image, br, {-1, -1}).readPatternFromBlack<SubPattern>(1);
if (Size(subdiagonal) == 5 && subdiagonal[4] > subdiagonal[3]) // Sub pattern has no separator so can run off along the diagonal
subdiagonal[4] = subdiagonal[3]; // Hack it back to previous
if (!IsPattern(subdiagonal, SUBPATTERN_RMQR))
return {};

// Horizontal corner finder patterns (for vertical ones see below)
for (auto [p, d] : {std::pair(tr, PointI{-1, 0}), {bl, {1, 0}}}) {
auto corner = BitMatrixCursorI(image, p, d).readPatternFromBlack<CornerEdgePattern>(1);
if (!IsPattern(corner, CORNER_EDGE_RMQR))
return {};
}

auto fpWidth = Reduce(diagonal);
float moduleSize = float(fpWidth) / 7;
int dimW = narrow_cast<int>(std::lround(width / moduleSize));
int dimH = narrow_cast<int>(std::lround(height / moduleSize));

if (dimW == dimH || !(dimW & 1) || !(dimH & 1) ||
dimW < MIN_MODULES_W || dimW > MAX_MODULES_W || dimH < MIN_MODULES_H || dimH > MAX_MODULES_H ||
!image.isIn(PointF{left + moduleSize / 2 + (dimW - 1) * moduleSize,
top + moduleSize / 2 + (dimH - 1) * moduleSize}))
return {};

// Vertical corner finder patterns
if (dimH > 7) { // None for R7
auto corner = BitMatrixCursorI(image, tr, {0, 1}).readPatternFromBlack<CornerEdgePattern>(1);
if (!IsPattern(corner, CORNER_EDGE_RMQR))
return {};
if (dimH > 9) { // No bottom left for R9
corner = BitMatrixCursorI(image, bl, {0, -1}).readPatternFromBlack<CornerEdgePattern>(1);
if (!IsPattern(corner, CORNER_EDGE_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);
Expand Down Expand Up @@ -662,4 +742,55 @@ DetectorResult SampleMQR(const BitMatrix& image, const ConcentricPattern& fp)
return SampleGrid(image, dim, dim, bestPT);
}

DetectorResult SampleRMQR(const BitMatrix& image, const ConcentricPattern& fp)
{
// TODO proper
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[] = {
{ 8, 1}, { 8, 2}, { 8, 3}, { 8, 4}, { 8, 5},
{ 9, 1}, { 9, 2}, { 9, 3}, { 9, 4}, { 9, 5},
{10, 1}, {10, 2}, {10, 3}, {10, 4}, {10, 5},
{11, 1}, {11, 2}, {11, 3},
};

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::DimensionOfVersionRMQR(bestFI.rMQRVersion + 1);

return SampleGrid(image, dim.x, dim.y, bestPT);
}

} // namespace ZXing::QRCode
2 changes: 2 additions & 0 deletions core/src/qrcode/QRDetector.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading