Skip to content

Commit ebd7db8

Browse files
sayzzy-ntaxxel
andauthored
Support QR Code Model1 (zxing-cpp#633)
This adds basic support to read/decode QR Code Model 1 symbols. It can be considered to fix zxing-cpp#614, although axxel believes that it only works for version 1-4. For higher versions there is still work to be done regarding the ecBlock interlacing (or the lack thereof). --------- Co-authored-by: axxel <[email protected]>
1 parent 6624175 commit ebd7db8

File tree

10 files changed

+215
-30
lines changed

10 files changed

+215
-30
lines changed

core/src/qrcode/QRBitMatrixParser.cpp

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,66 @@ static ByteArray ReadQRCodewords(const BitMatrix& bitMatrix, const Version& vers
135135
return result;
136136
}
137137

138+
static ByteArray ReadQRCodewordsModel1(const BitMatrix& bitMatrix, const Version& version, const FormatInformation& formatInfo)
139+
{
140+
141+
ByteArray result;
142+
result.reserve(version.totalCodewords());
143+
int dimension = bitMatrix.height();
144+
int columns = dimension / 4 + 1 + 2;
145+
for (int j = 0; j < columns; j++) {
146+
if (j <= 1) { // vertical symbols on the right side
147+
int rows = (dimension - 8) / 4;
148+
for (int i = 0; i < rows; i++) {
149+
if (j == 0 && i % 2 == 0 && i > 0 && i < rows - 1) // extension
150+
continue;
151+
int x = (dimension - 1) - (j * 2);
152+
int y = (dimension - 1) - (i * 4);
153+
uint8_t currentByte = 0;
154+
for (int b = 0; b < 8; b++) {
155+
AppendBit(currentByte, GetDataMaskBit(formatInfo.dataMask, x - b % 2, y - (b / 2))
156+
!= getBit(bitMatrix, x - b % 2, y - (b / 2), formatInfo.isMirrored));
157+
}
158+
result.push_back(currentByte);
159+
}
160+
} else if (columns - j <= 4) { // vertical symbols on the left side
161+
int rows = (dimension - 16) / 4;
162+
for (int i = 0; i < rows; i++) {
163+
int x = (columns - j - 1) * 2 + 1 + (columns - j == 4 ? 1 : 0); // timing
164+
int y = (dimension - 1) - 8 - (i * 4);
165+
uint8_t currentByte = 0;
166+
for (int b = 0; b < 8; b++) {
167+
AppendBit(currentByte, GetDataMaskBit(formatInfo.dataMask, x - b % 2, y - (b / 2))
168+
!= getBit(bitMatrix, x - b % 2, y - (b / 2), formatInfo.isMirrored));
169+
}
170+
result.push_back(currentByte);
171+
}
172+
} else { // horizontal symbols
173+
int rows = dimension / 2;
174+
for (int i = 0; i < rows; i++) {
175+
if (j == 2 && i >= rows - 4) // alignment & finder
176+
continue;
177+
if (i == 0 && j % 2 == 1 && j + 1 != columns - 4) // extension
178+
continue;
179+
int x = (dimension - 1) - (2 * 2) - (j - 2) * 4;
180+
int y = (dimension - 1) - (i * 2) - (i >= rows - 3 ? 1 : 0); // timing
181+
uint8_t currentByte = 0;
182+
for (int b = 0; b < 8; b++) {
183+
AppendBit(currentByte, GetDataMaskBit(formatInfo.dataMask, x - b % 4, y - (b / 4))
184+
!= getBit(bitMatrix, x - b % 4, y - (b / 4), formatInfo.isMirrored));
185+
}
186+
result.push_back(currentByte);
187+
}
188+
}
189+
}
190+
191+
result[0] &= 0xf; // ignore corner
192+
if (Size(result) != version.totalCodewords())
193+
return {};
194+
195+
return result;
196+
}
197+
138198
static ByteArray ReadMQRCodewords(const BitMatrix& bitMatrix, const QRCode::Version& version, const FormatInformation& formatInfo)
139199
{
140200
BitMatrix functionPattern = version.buildFunctionPattern();
@@ -185,9 +245,12 @@ ByteArray ReadCodewords(const BitMatrix& bitMatrix, const Version& version, cons
185245
{
186246
if (!hasValidDimension(bitMatrix, version.isMicroQRCode()))
187247
return {};
188-
189-
return version.isMicroQRCode() ? ReadMQRCodewords(bitMatrix, version, formatInfo)
190-
: ReadQRCodewords(bitMatrix, version, formatInfo);
248+
if (version.isMicroQRCode())
249+
return ReadMQRCodewords(bitMatrix, version, formatInfo);
250+
else if (formatInfo.isModel1)
251+
return ReadQRCodewordsModel1(bitMatrix, version, formatInfo);
252+
else
253+
return ReadQRCodewords(bitMatrix, version, formatInfo);
191254
}
192255

193256
} // namespace ZXing::QRCode

core/src/qrcode/QRDecoder.cpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCo
237237
StructuredAppendInfo structuredAppend;
238238
const int modeBitLength = CodecModeBitsLength(version);
239239

240+
if (version.isQRCodeModel1())
241+
bits.readBits(4); // Model 1 is leading with 4 0-bits -> drop them
242+
240243
try
241244
{
242245
while(!IsEndOfStream(bits, version)) {
@@ -316,14 +319,16 @@ DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCo
316319

317320
DecoderResult Decode(const BitMatrix& bits)
318321
{
319-
const Version* pversion = ReadVersion(bits);
322+
bool isMicroQRCode = bits.height() < 21;
323+
auto formatInfo = ReadFormatInformation(bits, isMicroQRCode);
324+
if (!formatInfo.isValid())
325+
return FormatError("Invalid format information");
326+
327+
const Version* pversion = formatInfo.isModel1 ? Version::FromDimension(bits.height(), true) : ReadVersion(bits);
320328
if (!pversion)
321329
return FormatError("Invalid version");
322-
const Version& version = *pversion;
323330

324-
auto formatInfo = ReadFormatInformation(bits, version.isMicroQRCode());
325-
if (!formatInfo.isValid())
326-
return FormatError("Invalid format information");
331+
const Version& version = *pversion;
327332

328333
// Read codewords
329334
ByteArray codewords = ReadCodewords(bits, version, formatInfo);

core/src/qrcode/QRFormatInformation.cpp

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ namespace ZXing::QRCode {
1515

1616
static const int FORMAT_INFO_MASK_QR = 0x5412;
1717

18+
static const int FORMAT_INFO_MASK_QR_MODEL1 = 0x2825;
19+
1820
/**
1921
* See ISO 18004:2006, Annex C, Table C.1
2022
*/
@@ -93,13 +95,12 @@ static uint32_t MirrorBits(uint32_t bits)
9395
return BitHacks::Reverse(bits) >> 17;
9496
}
9597

96-
static FormatInformation FindBestFormatInfo(int mask, const std::array<std::pair<int, int>, 32> lookup,
98+
static FormatInformation FindBestFormatInfo(const std::vector<uint32_t>& masks, const std::array<std::pair<int, int>, 32> lookup,
9799
const std::vector<uint32_t>& bits)
98100
{
99101
FormatInformation fi;
100102

101-
// Some QR codes apparently do not apply the XOR mask. Try without and with additional masking.
102-
for (auto mask : {0, mask})
103+
for (auto mask : masks)
103104
for (int bitsIndex = 0; bitsIndex < Size(bits); ++bitsIndex)
104105
for (const auto& [pattern, index] : lookup) {
105106
// Find the int in lookup with fewest bits differing
@@ -122,8 +123,16 @@ FormatInformation FormatInformation::DecodeQR(uint32_t formatInfoBits1, uint32_t
122123
// maks out the 'Dark Module' for mirrored and non-mirrored case (see Figure 25 in ISO/IEC 18004:2015)
123124
uint32_t mirroredFormatInfoBits2 = MirrorBits(((formatInfoBits2 >> 1) & 0b111111110000000) | (formatInfoBits2 & 0b1111111));
124125
formatInfoBits2 = ((formatInfoBits2 >> 1) & 0b111111100000000) | (formatInfoBits2 & 0b11111111);
125-
auto fi = FindBestFormatInfo(FORMAT_INFO_MASK_QR, FORMAT_INFO_DECODE_LOOKUP,
126+
// Some QR codes apparently do not apply the XOR mask. Try without and with additional masking.
127+
auto fi = FindBestFormatInfo({0, FORMAT_INFO_MASK_QR}, FORMAT_INFO_DECODE_LOOKUP,
126128
{formatInfoBits1, formatInfoBits2, MirrorBits(formatInfoBits1), mirroredFormatInfoBits2});
129+
auto fi_model1 = FindBestFormatInfo({FORMAT_INFO_MASK_QR ^ FORMAT_INFO_MASK_QR_MODEL1}, FORMAT_INFO_DECODE_LOOKUP,
130+
{formatInfoBits1, formatInfoBits2, MirrorBits(formatInfoBits1), mirroredFormatInfoBits2});
131+
132+
if (fi_model1.hammingDistance < fi.hammingDistance) {
133+
fi_model1.isModel1 = true;
134+
fi = fi_model1;
135+
}
127136

128137
// Use bits 3/4 for error correction, and 0-2 for mask.
129138
fi.ecLevel = ECLevelFromBits((fi.index >> 3) & 0x03);
@@ -139,7 +148,7 @@ FormatInformation FormatInformation::DecodeQR(uint32_t formatInfoBits1, uint32_t
139148
FormatInformation FormatInformation::DecodeMQR(uint32_t formatInfoBits)
140149
{
141150
// We don't use the additional masking (with 0x4445) to work around potentially non complying MicroQRCode encoders
142-
auto fi = FindBestFormatInfo(0, FORMAT_INFO_DECODE_LOOKUP_MICRO, {formatInfoBits, MirrorBits(formatInfoBits)});
151+
auto fi = FindBestFormatInfo({0}, FORMAT_INFO_DECODE_LOOKUP_MICRO, {formatInfoBits, MirrorBits(formatInfoBits)});
143152

144153
constexpr uint8_t BITS_TO_VERSION[] = {1, 2, 2, 3, 3, 4, 4, 4};
145154

core/src/qrcode/QRFormatInformation.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class FormatInformation
1818
uint8_t index = 255;
1919
uint8_t hammingDistance = 255;
2020
bool isMirrored = false;
21+
bool isModel1 = false;
2122
uint8_t dataMask = 0;
2223
uint8_t microVersion = 0;
2324
uint8_t bitsIndex = 255;

core/src/qrcode/QRVersion.cpp

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -292,35 +292,136 @@ const Version* Version::AllMicroVersions()
292292
return allVersions;
293293
}
294294

295+
const Version* Version::AllModel1Versions()
296+
{
297+
/**
298+
* See ISO 18004:2000 M.4.2 Table M.2
299+
* See ISO 18004:2000 M.5 Table M.4
300+
*/
301+
static const Version allVersions[] = {
302+
{1, {
303+
7 , 1, 19, 0, 0,
304+
10, 1, 16, 0, 0,
305+
13, 1, 13, 0, 0,
306+
17, 1, 9 , 0, 0
307+
}},
308+
{2, {
309+
10, 1, 36, 0, 0,
310+
16, 1, 30, 0, 0,
311+
22, 1, 24, 0, 0,
312+
30, 1, 16, 0, 0,
313+
}},
314+
{3, {
315+
15, 1, 57, 0, 0,
316+
28, 1, 44, 0, 0,
317+
36, 1, 36, 0, 0,
318+
48, 1, 24, 0, 0,
319+
}},
320+
{4, {
321+
20, 1, 80, 0, 0,
322+
40, 1, 60, 0, 0,
323+
50, 1, 50, 0, 0,
324+
66, 1, 34, 0, 0,
325+
}},
326+
{5, {
327+
26, 1, 108, 0, 0,
328+
52, 1, 82 , 0, 0,
329+
66, 1, 68 , 0, 0,
330+
88, 2, 46 , 0, 0,
331+
}},
332+
{6, {
333+
34 , 1, 136, 0, 0,
334+
63 , 2, 106, 0, 0,
335+
84 , 2, 86 , 0, 0,
336+
112, 2, 58 , 0, 0,
337+
}},
338+
{7, {
339+
42 , 1, 170, 0, 0,
340+
80 , 2, 132, 0, 0,
341+
104, 2, 108, 0, 0,
342+
138, 3, 72 , 0, 0,
343+
}},
344+
{8, {
345+
48 , 2, 208, 0, 0,
346+
96 , 2, 160, 0, 0,
347+
128, 2, 128, 0, 0,
348+
168, 3, 87 , 0, 0,
349+
}},
350+
{9, {
351+
60 , 2, 246, 0, 0,
352+
120, 2, 186, 0, 0,
353+
150, 3, 156, 0, 0,
354+
204, 3, 102, 0, 0,
355+
}},
356+
{10, {
357+
68 , 2, 290, 0, 0,
358+
136, 2, 222, 0, 0,
359+
174, 3, 183, 0, 0,
360+
232, 4, 124, 0, 0,
361+
}},
362+
{11, {
363+
80 , 2, 336, 0, 0,
364+
160, 4, 256, 0, 0,
365+
208, 4, 208, 0, 0,
366+
270, 5, 145, 0, 0,
367+
}},
368+
{12, {
369+
92 , 2, 384, 0, 0,
370+
184, 4, 292, 0, 0,
371+
232, 4, 244, 0, 0,
372+
310, 5, 165, 0, 0,
373+
}},
374+
{13, {
375+
108, 3, 432, 0, 0,
376+
208, 4, 332, 0, 0,
377+
264, 4, 276, 0, 0,
378+
348, 6, 192, 0, 0,
379+
}},
380+
{14, {
381+
120, 3, 489, 0, 0,
382+
240, 4, 368, 0, 0,
383+
300, 5, 310, 0, 0,
384+
396, 6, 210, 0, 0,
385+
}},
386+
};
387+
return allVersions;
388+
}
389+
390+
static inline bool isMicro(const std::array<ECBlocks, 4>& ecBlocks)
391+
{
392+
return ecBlocks[0].codewordsPerBlock < 7 || ecBlocks[0].codewordsPerBlock == 8;
393+
}
394+
295395
Version::Version(int versionNumber, std::initializer_list<int> alignmentPatternCenters, const std::array<ECBlocks, 4>& ecBlocks)
296-
: _versionNumber(versionNumber), _alignmentPatternCenters(alignmentPatternCenters), _ecBlocks(ecBlocks), _isMicro(false)
396+
: _versionNumber(versionNumber), _alignmentPatternCenters(alignmentPatternCenters), _ecBlocks(ecBlocks), _isMicro(false), _isModel1(false)
297397
{
298398
_totalCodewords = ecBlocks[0].totalDataCodewords();
299399
}
300400

301401
Version::Version(int versionNumber, const std::array<ECBlocks, 4>& ecBlocks)
302-
: _versionNumber(versionNumber), _ecBlocks(ecBlocks), _isMicro(true)
402+
: _versionNumber(versionNumber), _ecBlocks(ecBlocks), _isMicro(isMicro(ecBlocks)), _isModel1(!isMicro(ecBlocks))
303403
{
304404
_totalCodewords = ecBlocks[0].totalDataCodewords();
305405
}
306406

307-
const Version* Version::FromNumber(int versionNumber, bool isMicro)
407+
const Version* Version::FromNumber(int versionNumber, bool isMicro, bool isModel1)
308408
{
309-
if (versionNumber < 1 || versionNumber > (isMicro ? 4 : 40)) {
409+
if (versionNumber < 1 || versionNumber > (isMicro ? 4 : (isModel1 ? 14 : 40))) {
310410
//throw std::invalid_argument("Version should be in range [1-40].");
311411
return nullptr;
312412
}
313-
return &(isMicro ? AllMicroVersions() : AllVersions())[versionNumber - 1];
413+
414+
return &(isMicro ? AllMicroVersions() : (isModel1 ? AllModel1Versions() : AllVersions()))[versionNumber - 1];
314415
}
315416

316-
const Version* Version::FromDimension(int dimension)
417+
const Version* Version::FromDimension(int dimension, bool isModel1)
317418
{
318419
bool isMicro = dimension < 21;
319420
if (dimension % DimensionStep(isMicro) != 1) {
320421
//throw std::invalid_argument("Unexpected dimension");
321422
return nullptr;
322423
}
323-
return FromNumber((dimension - DimensionOffset(isMicro)) / DimensionStep(isMicro), isMicro);
424+
return FromNumber((dimension - DimensionOffset(isMicro)) / DimensionStep(isMicro), isMicro, isModel1);
324425
}
325426

326427
const Version* Version::DecodeVersionInformation(int versionBitsA, int versionBitsB)

core/src/qrcode/QRVersion.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class Version
4040
BitMatrix buildFunctionPattern() const;
4141

4242
bool isMicroQRCode() const { return _isMicro; }
43+
bool isQRCodeModel1() const { return _isModel1; }
4344

4445
static constexpr int DimensionStep(bool isMicro) { return std::array{4, 2}[isMicro]; }
4546
static constexpr int DimensionOffset(bool isMicro) { return std::array{17, 9}[isMicro]; }
@@ -54,9 +55,9 @@ class Version
5455
* @param dimension dimension in modules
5556
* @return Version for a QR Code of that dimension
5657
*/
57-
static const Version* FromDimension(int dimension);
58+
static const Version* FromDimension(int dimension, bool isModel1 = false);
5859

59-
static const Version* FromNumber(int versionNumber, bool isMicro = false);
60+
static const Version* FromNumber(int versionNumber, bool isMicro = false, bool isModel1 = false);
6061

6162
static const Version* DecodeVersionInformation(int versionBitsA, int versionBitsB = 0);
6263

@@ -66,11 +67,13 @@ class Version
6667
std::array<ECBlocks, 4> _ecBlocks;
6768
int _totalCodewords;
6869
bool _isMicro;
70+
bool _isModel1;
6971

7072
Version(int versionNumber, std::initializer_list<int> alignmentPatternCenters, const std::array<ECBlocks, 4> &ecBlocks);
7173
Version(int versionNumber, const std::array<ECBlocks, 4>& ecBlocks);
7274
static const Version* AllVersions();
7375
static const Version* AllMicroVersions();
76+
static const Version* AllModel1Versions();
7477
};
7578

7679
} // QRCode

test/blackbox/BlackboxTestRunner.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,12 @@ int runBlackBoxTests(const fs::path& testPathPrefix, const std::set<std::string>
562562
{ 16, 16, 270 },
563563
});
564564

565-
runTests("qrcode-2", "QRCode", 49, {
566-
{ 45, 47, 0 },
567-
{ 45, 47, 90 },
568-
{ 45, 47, 180 },
569-
{ 45, 47, 270 },
570-
{ 21, 1, pure }, // the misread is the 'outer' symbol in 16.png
565+
runTests("qrcode-2", "QRCode", 50, {
566+
{ 46, 48, 0 },
567+
{ 46, 48, 90 },
568+
{ 46, 48, 180 },
569+
{ 46, 48, 270 },
570+
{ 22, 1, pure }, // the misread is the 'outer' symbol in 16.png
571571
});
572572

573573
runTests("qrcode-3", "QRCode", 28, {

test/samples/qrcode-2/qr-model-1.png

230 Bytes
Loading

test/samples/qrcode-2/qr-model-1.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
QR Code Model 1

0 commit comments

Comments
 (0)