diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,8 +13,10 @@ include(ECMGenerateHeaders) include(ECMGeneratePriFile) include(ECMAddQch) +include(ECMAddTests) include(CMakePackageConfigHelpers) include(ECMSetupVersion) +include(ECMQtDeclareLoggingCategory) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) @@ -42,6 +44,7 @@ add_subdirectory(src) if(BUILD_TESTING) find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test Widgets) + add_subdirectory(autotests) add_subdirectory(tests) endif() diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,18 @@ +set(aztecbarcodetest_srcs + aztecbarcodetest.cpp + ../src/lib/aztecbarcode.cpp + ../src/lib/bitvector.cpp + ../src/lib/reedsolomon.cpp + ${CMAKE_CURRENT_BINARY_DIR}/../src/lib/prison_debug.cpp +) +qt5_add_resources(aztecbarcodetest_srcs aztec/aztec.qrc) + +ecm_add_test(${aztecbarcodetest_srcs} TEST_NAME prison-aztecbarcodetest LINK_LIBRARIES Qt5::Test KF5::Prison) + +ecm_add_test( + reedsolomontest.cpp + ../src/lib/bitvector.cpp + ../src/lib/reedsolomon.cpp + TEST_NAME prison-reedsolomontest + LINK_LIBRARIES Qt5::Test KF5::Prison +) diff --git a/autotests/aztec/aztec.qrc b/autotests/aztec/aztec.qrc new file mode 100644 --- /dev/null +++ b/autotests/aztec/aztec.qrc @@ -0,0 +1,29 @@ + + + rendering/aztec-full-grid.png + rendering/aztec-full-data-0011.png + rendering/aztec-full-data-0101.png + rendering/aztec-full-data-1001.png + rendering/aztec-full-data-1010.png + rendering/aztec-full-data-1111.png + rendering/aztec-full-mode-1111.png + rendering/aztec-full-mode-1234.png + rendering/aztec-full-mode-1234-rev.png + + rendering/aztec-compact-grid.png + rendering/aztec-compact-data-0011.png + rendering/aztec-compact-data-0101.png + rendering/aztec-compact-data-1001.png + rendering/aztec-compact-data-1010.png + rendering/aztec-compact-data-1111.png + rendering/aztec-compact-mode-1111.png + rendering/aztec-compact-mode-1234.png + rendering/aztec-compact-mode-1234-rev.png + + encoding/aztec-complete-compact1.png + encoding/aztec-complete-compact3.png + encoding/aztec-complete-compact4.png + encoding/aztec-complete-full5.png + encoding/aztec-complete-big.png + + diff --git a/autotests/aztec/encoding/aztec-complete-big.png b/autotests/aztec/encoding/aztec-complete-big.png new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include "../src/lib/aztecbarcode.h" +#include "../src/lib/bitvector_p.h" + +#include +#include +#include + +Q_DECLARE_METATYPE(Prison::BitVector) + +using namespace Prison; + +class AztecBarcodeTest : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void testAztecEncode_data() + { + QTest::addColumn("input"); + QTest::addColumn("output"); + + QTest::newRow("empty") << QByteArray() << BitVector(); + BitVector v; + v.appendMSB(12, 5); v.appendMSB(5, 5); v.appendMSB(6, 5); + QTest::newRow("all uppper") << QByteArray("KDE") << v; + v.clear(); + v.appendMSB(28, 5); v.appendMSB(12, 5); v.appendMSB(5, 5); v.appendMSB(6, 5); + QTest::newRow("all lower") << QByteArray("kde") << v; + v.clear(); + v.appendMSB(12, 5); v.appendMSB(28, 5); v.appendMSB(5, 5); v.appendMSB(6, 5); + QTest::newRow("upper -> lower latch") << QByteArray("Kde") << v; + v.clear(); + v.appendMSB(28, 5); v.appendMSB(2, 5); v.appendMSB(29, 5); v.appendMSB(30, 5); v.appendMSB(16, 5); v.appendMSB(16, 5); + QTest::newRow("lower -> punct latch") << QByteArray("a++") << v; + v.clear(); + v.appendMSB(30, 5); v.appendMSB(6, 4); v.appendMSB(4, 4); + QTest::newRow("digit") << QByteArray("42") << v; + v.clear(); + v.appendMSB(29, 5); v.appendMSB(30, 5); v.appendMSB(25, 5); v.appendMSB(24, 5); v.appendMSB(31, 5); v.appendMSB(30, 5); v.appendMSB(11, 4); v.appendMSB(2, 4); + QTest::newRow("punct -> digit latch") << QByteArray(">=90") << v; + v.clear(); + v.appendMSB(30, 5); v.appendMSB(10, 4); v.appendMSB(3, 4); v.appendMSB(14, 4); v.appendMSB(29, 5); v.appendMSB(30, 5); v.appendMSB(13, 5); v.appendMSB(14, 5); + QTest::newRow("digit -> punct latch") << QByteArray("81()") << v; + v.clear(); + v.appendMSB(29, 5); v.appendMSB(11, 5); v.appendMSB(8, 5); + QTest::newRow("mixed") << QByteArray("\n\a") << v; + v.clear(); + v.appendMSB(29, 5); v.appendMSB(30, 5); v.appendMSB(2, 5); v.appendMSB(2, 5); + QTest::newRow("CR LF") << QByteArray("\r\n\r\n") << v; + v.clear(); + v.appendMSB(31, 5); v.appendMSB(2, 5); v.appendMSB(128, 8); v.appendMSB(129, 8); + QTest::newRow("binary") << QByteArray("\x80\x81") << v; + v.clear(); + v.appendMSB(31, 5); v.appendMSB(2, 5); v.appendMSB(255, 8); v.appendMSB(254, 8); v.appendMSB(28, 5); v.appendMSB(3, 5); + QTest::newRow("binary/lower") << QByteArray("\xff\xfe" "b") << v; + v.clear(); + v.appendMSB(12, 5); v.appendMSB(0, 5); v.appendMSB(6, 5); + QTest::newRow("upper -> punct shift") << QByteArray("K!") << v; + v.clear(); + v.appendMSB(30, 5); v.appendMSB(9, 4); v.appendMSB(4, 4); v.appendMSB(15, 4); v.appendMSB(6, 5); v.appendMSB(5, 4); + QTest::newRow("digit -> upper shift") << QByteArray("72E3") << v; + v.clear(); + v.appendMSB(25, 5); v.appendMSB(1, 5); v.appendMSB(26, 5); + QTest::newRow("upper space") << QByteArray("X Y") << v; + v.clear(); + v.appendMSB(20, 5); v.appendMSB(0, 5); v.appendMSB(4, 5); v.appendMSB(23, 5); + QTest::newRow("upper punct double char shift") << QByteArray("S, V") << v; + v.clear(); + v.appendMSB(17, 5); v.appendMSB(0, 5); v.appendMSB(17, 5); v.appendMSB(18, 5); + QTest::newRow("upper ambigious punct shift") << QByteArray("P,Q") << v; + v.clear(); + v.appendMSB(30, 5); v.appendMSB(13, 4); v.appendMSB(7, 4); + QTest::newRow("digit ambigious punct latch") << QByteArray(".5") << v; + } + + void testAztecEncode() + { + QFETCH(QByteArray, input); + QFETCH(BitVector, output); + + AztecBarcode code; + const auto v = code.aztecEncode(input); + if (v != output) { + qDebug() << "Actual :" << v; + qDebug() << "Expected:" << output; + } + QCOMPARE(v, output); + } + + void testStuffAndPad_data() + { + QTest::addColumn("input"); + QTest::addColumn("output"); + QTest::addColumn("codeWordSize"); + + BitVector in, out; + QTest::newRow("emtpy") << in << out << 4; + in.appendMSB(0x3, 2); out.appendMSB(0xf, 4); + QTest::newRow("pad only") << in << out << 4; + in.clear(); out.clear(); + in.appendMSB(0xe0, 8); out.appendMSB(0xe13, 12); + QTest::newRow("stuff and pad") << in << out << 4; + in.clear(); out.clear(); + in.appendMSB(0, 6); out.appendMSB(0x11, 8); + QTest::newRow("stuff only") << in << out << 4; + } + + void testStuffAndPad() + { + QFETCH(BitVector, input); + QFETCH(BitVector, output); + QFETCH(int, codeWordSize); + AztecBarcode code; + const auto res= code.bitStuffAndPad(input, codeWordSize); + QCOMPARE(res.size(), output.size()); + if (res != output) { + qDebug() << "Actual :" << res; + qDebug() << "Expected:" << output; + } + QCOMPARE(res, output); + } + + void testFullGrid() + { + AztecBarcode code; + QImage img(151, 151, QImage::Format_ARGB32_Premultiplied); + img.fill(code.backgroundColor()); + code.paintFullGrid(&img); + + QImage ref(QStringLiteral(":/rendering/aztec-full-grid.png")); + ref = ref.convertToFormat(img.format()); + QCOMPARE(img, ref); + } + + void testCompactGrid() + { + AztecBarcode code; + QImage img(27, 27, QImage::Format_ARGB32_Premultiplied); + img.fill(code.backgroundColor()); + code.paintCompactGrid(&img); + img.save(QStringLiteral("aztec-compact-grid.png")); + + QImage ref(QStringLiteral(":/rendering/aztec-compact-grid.png")); + ref = ref.convertToFormat(img.format()); + QCOMPARE(img, ref); + } + + void testFullData_data() + { + QTest::addColumn("data"); + QTest::addColumn("refName"); + QTest::addColumn("layer"); + + BitVector v; + for (int i = 0; i < 1248; ++i) + v.appendLSB(0x9249, 16); + QTest::newRow("1001-31") << v << QStringLiteral("aztec-full-data-1001.png") << 32; + + v.clear(); + for (int i = 0; i < 1248 * 8; ++i) + v.appendLSB(0x2, 2); + QTest::newRow("0101-31") << v << QStringLiteral("aztec-full-data-0101.png") << 32; + + v.clear(); + for (int i = 0; i < 1248; ++i) + v.appendLSB(0xffff, 16); + QTest::newRow("1111-31") << v << QStringLiteral("aztec-full-data-1111.png") << 32; + + v.clear(); + for (int i = 0; i < 704 * 4; ++i) + v.appendLSB(0x1, 2); + QTest::newRow("1010-15") << v << QStringLiteral("aztec-full-data-1010.png") << 16; + + v.clear(); + for (int i = 0; i < 16; ++i) + v.appendLSB(0xCC, 8); + QTest::newRow("0011-0") << v << QStringLiteral("aztec-full-data-0011.png") << 1; + } + + void testFullData() + { + QFETCH(BitVector, data); + QFETCH(QString, refName); + QFETCH(int, layer); + + AztecBarcode code; + QImage img(151, 151, QImage::Format_ARGB32_Premultiplied); + img.fill(code.backgroundColor()); + code.paintFullData(&img, data, layer); + img.save(refName); + + QImage ref(QStringLiteral(":/rendering/") + refName); + ref = ref.convertToFormat(img.format()); + QCOMPARE(img, ref); + } + + void testFullModeMessage_data() + { + QTest::addColumn("data"); + QTest::addColumn("refName"); + + BitVector v; + for (int i = 0; i < 8; ++i) + v.appendMSB(i + 1, 5); + QTest::newRow("1234") << v << QStringLiteral("aztec-full-mode-1234.png"); + + v.clear(); + for (int i = 0; i < 8; ++i) + v.appendLSB(i + 1, 5); + QTest::newRow("1234-rev") << v << QStringLiteral("aztec-full-mode-1234-rev.png"); + + v.clear(); + for (int i = 0; i < 4; ++i) + v.appendMSB(0xffff, 10); + QTest::newRow("1111") << v << QStringLiteral("aztec-full-mode-1111.png"); + } + + void testFullModeMessage() + { + QFETCH(BitVector, data); + QFETCH(QString, refName); + + AztecBarcode code; + QImage img(151, 151, QImage::Format_ARGB32_Premultiplied); + img.fill(code.backgroundColor()); + code.paintFullModeMessage(&img, data); + img.save(refName); + + QImage ref(QStringLiteral(":/rendering/") + refName); + ref = ref.convertToFormat(img.format()); + QCOMPARE(img, ref); + } + + void testCompactData_data() + { + QTest::addColumn("data"); + QTest::addColumn("refName"); + QTest::addColumn("layer"); + + BitVector v; + for (int i = 0; i < 304; ++i) + v.appendLSB(0x9249, 16); + QTest::newRow("1001-3") << v << QStringLiteral("aztec-compact-data-1001.png") << 4; + + v.clear(); + for (int i = 0; i < 608 * 4; ++i) + v.appendLSB(0x2, 2); + QTest::newRow("0101-3") << v << QStringLiteral("aztec-compact-data-0101.png") << 4; + + v.clear(); + for (int i = 0; i < 304; ++i) + v.appendLSB(0xffff, 16); + QTest::newRow("1111-3") << v << QStringLiteral("aztec-compact-data-1111.png") << 4; + + v.clear(); + for (int i = 0; i < 102 * 4; ++i) + v.appendLSB(0x1, 2); + QTest::newRow("1010-2") << v << QStringLiteral("aztec-compact-data-1010.png") << 3; + + v.clear(); + for (int i = 0; i < 13; ++i) + v.appendLSB(0xCC, 8); + QTest::newRow("0011-0") << v << QStringLiteral("aztec-compact-data-0011.png") << 1; + } + + void testCompactData() + { + QFETCH(BitVector, data); + QFETCH(QString, refName); + QFETCH(int, layer); + + AztecBarcode code; + QImage img(27, 27, QImage::Format_ARGB32_Premultiplied); + img.fill(code.backgroundColor()); + code.paintCompactData(&img, data, layer); + img.save(refName); + + QImage ref(QStringLiteral(":/rendering/") + refName); + ref = ref.convertToFormat(img.format()); + QCOMPARE(img, ref); + } + + void testCompactModeMessage_data() + { + QTest::addColumn("data"); + QTest::addColumn("refName"); + + BitVector v; + for (int i = 0; i < 4; ++i) + v.appendMSB(i + 1, 7); + QTest::newRow("1234") << v << QStringLiteral("aztec-compact-mode-1234.png"); + + v.clear(); + for (int i = 0; i < 4; ++i) + v.appendLSB(i + 1, 7); + QTest::newRow("1234-rev") << v << QStringLiteral("aztec-compact-mode-1234-rev.png"); + + v.clear(); + for (int i = 0; i < 4; ++i) + v.appendMSB(0xffff, 7); + QTest::newRow("1111") << v << QStringLiteral("aztec-compact-mode-1111.png"); + } + + void testCompactModeMessage() + { + QFETCH(BitVector, data); + QFETCH(QString, refName); + + AztecBarcode code; + QImage img(151, 151, QImage::Format_ARGB32_Premultiplied); + img.fill(code.backgroundColor()); + code.paintCompactModeMessage(&img, data); + img.save(refName); + + QImage ref(QStringLiteral(":/rendering/") + refName); + ref = ref.convertToFormat(img.format()); + QCOMPARE(img, ref); + } + + void testCodeGen_data() + { + QTest::addColumn("input"); + QTest::addColumn("refName"); + + QTest::newRow("short compact") << QStringLiteral("KF5::Prison") << "aztec-complete-compact1.png"; + QTest::newRow("compact 3 layer") << QStringLiteral("M1KRAUSE/VOLKER ABCDEFG TXLRIXBT 0212 309Y014E0063 100") << "aztec-complete-compact3.png"; + QTest::newRow("long compact") << QStringLiteral("KF5::Prison - the barcode generation library of KDE Frameworks 5!") << "aztec-complete-compact4.png"; + QTest::newRow("short full") << QStringLiteral("KF5::Prison - the MIT licensed free software barcode generation library of KDE Frameworks 5!") << "aztec-complete-full5.png"; + QTest::newRow("long full") << QString::fromLatin1( + "Permission is hereby granted, free of charge, to any person\n" + "obtaining a copy of this software and associated documentation\n" + "files (the \"Software\"), to deal in the Software without\n" + "restriction, including without limitation the rights to use,\n" + "copy, modify, merge, publish, distribute, sublicense, and/or sell\n" + "copies of the Software, and to permit persons to whom the\n" + "Software is furnished to do so, subject to the following\n" + "conditions:\n\n" + "The above copyright notice and this permission notice shall be\n" + "included in all copies or substantial portions of the Software.\n\n" + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n" + "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n" + "OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n" + "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n" + "HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n" + "WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n" + "FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n" + "OTHER DEALINGS IN THE SOFTWARE.") << "aztec-complete-big.png"; + } + + void testCodeGen() + { + QFETCH(QString, input); + QFETCH(QString, refName); + + AztecBarcode code; + code.setData(input); + const auto img = code.paintImage({200, 200}); + img.save(refName); + + QImage ref(QStringLiteral(":/encoding/") + refName); + ref = ref.convertToFormat(img.format()); + QCOMPARE(img, ref); + } +}; + +QTEST_APPLESS_MAIN(AztecBarcodeTest) + +#include "aztecbarcodetest.moc" diff --git a/autotests/reedsolomontest.cpp b/autotests/reedsolomontest.cpp new file mode 100644 --- /dev/null +++ b/autotests/reedsolomontest.cpp @@ -0,0 +1,79 @@ +/* + Copyright (c) 2017 Volker Krause + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include "../src/lib/bitvector_p.h" +#include "../src/lib/reedsolomon_p.h" + +#include +#include +#include + +Q_DECLARE_METATYPE(Prison::BitVector) + +using namespace Prison; + +class ReedSolomonTest : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void rsTest_data() + { + QTest::addColumn("poly"); + QTest::addColumn("symCount"); + QTest::addColumn("input"); + QTest::addColumn("output"); + + BitVector in, out; + out.appendMSB(0, 20); + QTest::newRow("empty") << (int)ReedSolomon::GF16 << 5 << in << out; + + in.clear(); out.clear(); + in.appendMSB(0x5c, 8); + out.appendMSB(7, 6); out.appendMSB(5, 7); out.appendMSB(0x4d, 7); + QTest::newRow("GF16") << (int)ReedSolomon::GF16 << 5 << in << out; + } + + void rsTest() + { + QFETCH(int, poly); + QFETCH(int, symCount); + QFETCH(BitVector, input); + QFETCH(BitVector, output); + + ReedSolomon rs(poly, symCount); + const auto res = rs.encode(input); + QCOMPARE(res.size(), output.size()); + if (res != output) { + qDebug() << "Actual :" << res; + qDebug() << "Expected:" << output; + } + QCOMPARE(res, output); + } +}; + +QTEST_APPLESS_MAIN(ReedSolomonTest) + +#include "reedsolomontest.moc" diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -2,13 +2,17 @@ set(prison_SRCS abstractbarcode.cpp + aztecbarcode.cpp + bitvector.cpp code39barcode.cpp code93barcode.cpp datamatrixbarcode.cpp qrcodebarcode.cpp prison.cpp + reedsolomon.cpp ) +ecm_qt_declare_logging_category(prison_SRCS HEADER prison_debug.h IDENTIFIER Prison::Log CATEGORY_NAME kf5.prison) add_library(KF5Prison ${prison_SRCS}) generate_export_header(KF5Prison BASE_NAME Prison) diff --git a/src/lib/aztecbarcode.h b/src/lib/aztecbarcode.h new file mode 100644 --- /dev/null +++ b/src/lib/aztecbarcode.h @@ -0,0 +1,67 @@ +/* + Copyright (c) 2017 Volker Krause + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#ifndef PRISON_AZTECBARCODE_H +#define PRISON_AZTECBARCODE_H + +#include "abstractbarcode.h" + +class AztecBarcodeTest; + +namespace Prison { + +class BitVector; + +/** Aztec code generator. */ +class AztecBarcode : public AbstractBarcode +{ +public: + AztecBarcode(); + ~AztecBarcode(); + +protected: + QImage paintImage(const QSizeF& size) override; + +private: + friend class ::AztecBarcodeTest; + + BitVector aztecEncode(const QByteArray &data) const; + BitVector bitStuffAndPad(const BitVector &input, int codeWordSize) const; + + void paintFullGrid(QImage *img) const; + void paintFullData(QImage *img, const BitVector &data, int layerCount) const; + void paintFullModeMessage(QImage *img, const BitVector &modeData) const; + QImage cropAndScaleFull(QImage *img, int layerCount, int size); + + void paintCompactGrid(QImage *img) const; + void paintCompactData(QImage *img, const BitVector &data, int layerCount) const; + void paintCompactModeMessage(QImage *img, const BitVector &modeData) const; + QImage cropAndScaleCompact(QImage *img, int layerCount, int size); +}; + +} + +#endif // PRISON_AZTECCODE_H diff --git a/src/lib/aztecbarcode.cpp b/src/lib/aztecbarcode.cpp new file mode 100644 --- /dev/null +++ b/src/lib/aztecbarcode.cpp @@ -0,0 +1,794 @@ +/* + Copyright (c) 2017 Volker Krause + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include "aztecbarcode.h" +#include "bitvector_p.h" +#include "reedsolomon_p.h" +#include "prison_debug.h" + +#include +#include +#include + +#include +#include + +// see https://en.wikipedia.org/wiki/Aztec_Code for encoding tables, magic numbers, etc + +using namespace Prison; + +enum { + FullMaxSize = 151, + FullRadius = 74, + FullGridInterval = 16, + FullModeMessageSize = 40, + FullLayerCount = 32, + + CompactMaxSize = 27, + CompactRadius = 13, + CompactModeMessageSize = 28, + CompactLayerCount = 4 +}; + +AztecBarcode::AztecBarcode() = default; +AztecBarcode::~AztecBarcode() = default; + +// encoding properties depending on layer count +struct aztec_layer_property_t { + uint8_t layer; + uint8_t codeWordSize; + uint16_t gf; +}; + +static const aztec_layer_property_t aztec_layer_properties[] = { + { 2, 6, ReedSolomon::GF64 }, + { 8, 8, ReedSolomon::GF256 }, + { 22, 10, ReedSolomon::GF1024 }, + { 32, 12, ReedSolomon::GF4096 } +}; + +// amounts of bits in an Aztec code depending on layer count +static int aztecCompactDataBits(int layer) +{ + return (88 + 16 * layer) * layer; +} + +static int aztecFullDataBits(int layer) +{ + return (112 + 16 * layer) * layer; +} + +QImage AztecBarcode::paintImage(const QSizeF& size) +{ + const auto inputData = aztecEncode(data().toLatin1()); + + int layerCount = 0; + int codewordCount = 0; + int availableBits = 0; + int stuffSize = 0; // extra bits added during bit stuffing, which might make us overun the available size + bool compactMode = false; + BitVector encodedData; + + do { + layerCount = 0; + // find the smallest layout we can put the data in, while leaving 23% for error correction + for (auto i = 1; i <= FullLayerCount; ++i) { + if (aztecFullDataBits(i) * 0.77 > (inputData.size() + stuffSize)) { + layerCount = i; + break; + } + } + for (auto i = 1; i <= CompactLayerCount; ++i) { + if (aztecCompactDataBits(i) * 0.77 > (inputData.size() + stuffSize)) { + layerCount = i; + compactMode = true; + break; + } + } + if (layerCount == 0) { + qCWarning(Log) << "data too large for Aztec code" << inputData.size(); + return {}; + } + + // determine code word size + const auto propIt = std::lower_bound(aztec_layer_properties, aztec_layer_properties + 4, layerCount, [](const aztec_layer_property_t &lhs, int rhs) { + return lhs.layer < rhs; + }); + + // bit stuffing + auto stuffedData = bitStuffAndPad(inputData, (*propIt).codeWordSize); + stuffSize = stuffedData.size() - inputData.size(); + + availableBits = compactMode ? aztecCompactDataBits(layerCount) : aztecFullDataBits(layerCount); + codewordCount = stuffedData.size() / (*propIt).codeWordSize; + const auto rsWordCount = availableBits / (*propIt).codeWordSize - codewordCount; + + // compute error correction + ReedSolomon rs((*propIt).gf, rsWordCount); + const auto rsData = rs.encode(stuffedData); + + // pad with leading 0 bits to align to code word boundaries + encodedData.reserve(availableBits); + if (int diff = availableBits - stuffedData.size() - rsData.size()) { + encodedData.appendMSB(0, diff); + } + encodedData.append(stuffedData); + encodedData.append(rsData); + + // try again in the rare case that we overrun the available bits due to bit stuffing and padding + } while (encodedData.size() > availableBits); + + // determine mode message + BitVector modeMsg; + if (compactMode) { + modeMsg.appendMSB(layerCount - 1, 2); + modeMsg.appendMSB(codewordCount - 1, 6); + ReedSolomon rs(ReedSolomon::GF16, 5); + modeMsg.append(rs.encode(modeMsg)); + } else { + modeMsg.appendMSB(layerCount - 1, 5); + modeMsg.appendMSB(codewordCount - 1, 11); + ReedSolomon rs(ReedSolomon::GF16, 6); + modeMsg.append(rs.encode(modeMsg)); + } + + // render the result + if (compactMode) { + QImage img(CompactMaxSize, CompactMaxSize, QImage::Format_RGB32); + img.fill(backgroundColor()); + paintCompactGrid(&img); + paintCompactData(&img, encodedData, layerCount); + paintCompactModeMessage(&img, modeMsg); + return cropAndScaleCompact(&img, layerCount, std::max(size.width(), size.height())); + } else { + QImage img(FullMaxSize, FullMaxSize, QImage::Format_RGB32); + img.fill(backgroundColor()); + paintFullGrid(&img); + paintFullData(&img, encodedData, layerCount); + paintFullModeMessage(&img, modeMsg); + return cropAndScaleFull(&img, layerCount, std::max(size.width(), size.height())); + } + + return {}; +} + +// code points and encoding modes for each of the first 127 ASCII characters, the rest is encoded in Binary mode +enum Mode { + NoMode, + Upper, + Lower, + Mixed, + Punct, + Digit, + Binary, + MODE_COUNT, + Special +}; + +enum SpecialChar { + Space, + CarriageReturn, + Comma, + Dot, + SPECIAL_CHAR_COUNT +}; + +struct aztec_code_t { + uint8_t code; + uint8_t mode; +}; + +static const aztec_code_t aztec_code_table[] = { + { 0, Binary }, // 0 + { 2, Mixed }, + { 3, Mixed }, + { 4, Mixed }, + { 5, Mixed }, + { 6, Mixed }, + { 7, Mixed }, + { 8, Mixed }, // 7 BEL \a + { 9, Mixed }, + { 10, Mixed }, + { 11, Mixed }, // 10 LF / ^J + { 12, Mixed }, + { 13, Mixed }, + { CarriageReturn, Special }, // 13 CR / ^M - but also 1 Punct + { 14, Binary }, + { 15, Binary }, + { 16, Binary }, + { 17, Binary }, + { 18, Binary }, + { 19, Binary }, + { 20, Binary }, // 20 ^T + { 21, Binary }, + { 22, Binary }, + { 23, Binary }, + { 24, Binary }, + { 25, Binary }, + { 26, Binary }, + { 15, Mixed }, // 27 ^[ + { 16, Mixed }, + { 17, Mixed }, + { 18, Mixed }, // 30 ^^ + { 19, Mixed }, + { Space, Special }, // 32 SP + { 6, Punct }, + { 7, Punct }, + { 8, Punct }, // 35 # + { 9, Punct }, + { 10, Punct }, + { 11, Punct }, + { 12, Punct }, + { 13, Punct }, // 40 ( + { 14, Punct }, + { 15, Punct }, + { 16, Punct }, // 43 + + { Comma, Special }, // 44 , + { 18, Punct }, // 45 - + { Dot, Special }, // 46 . + { 20, Punct }, // 47 / + { 2, Digit }, // 48 0 + { 3, Digit }, + { 4, Digit }, + { 5, Digit }, + { 6, Digit }, + { 7, Digit }, + { 8, Digit }, + { 9, Digit }, + { 10, Digit }, + { 11, Digit }, // 57 9 + { 21, Punct }, // 58 : + { 22, Punct }, // 59 ; + { 23, Punct }, // 60 < + { 24, Punct }, + { 25, Punct }, // 62 > + { 26, Punct }, // 63 ? + { 20, Mixed }, // 64 @ + { 2, Upper }, // 65 A + { 3, Upper }, + { 4, Upper }, + { 5, Upper }, + { 6, Upper }, + { 7, Upper }, + { 8, Upper }, + { 9, Upper }, + { 10, Upper }, + { 11, Upper }, + { 12, Upper }, + { 13, Upper }, + { 14, Upper }, + { 15, Upper }, + { 16, Upper }, + { 17, Upper }, + { 18, Upper }, + { 19, Upper }, + { 20, Upper }, + { 21, Upper }, + { 22, Upper }, + { 23, Upper }, + { 24, Upper }, + { 25, Upper }, + { 26, Upper }, + { 27, Upper }, // 90 Z + { 27, Punct }, // 91 [ + { 21, Mixed }, // 92 backslash + { 28, Punct }, // 93 ] + { 22, Mixed }, // 94 ^ + { 23, Mixed }, // 95 _ + { 24, Mixed }, // 96 ` + { 2, Lower }, // 97 a + { 3, Lower }, + { 4, Lower }, + { 5, Lower }, + { 6, Lower }, + { 7, Lower }, + { 8, Lower }, + { 9, Lower }, + { 10, Lower }, + { 11, Lower }, + { 12, Lower }, + { 13, Lower }, + { 14, Lower }, + { 15, Lower }, + { 16, Lower }, + { 17, Lower }, + { 18, Lower }, + { 19, Lower }, + { 20, Lower }, + { 21, Lower }, + { 22, Lower }, + { 23, Lower }, + { 24, Lower }, + { 25, Lower }, + { 26, Lower }, + { 27, Lower }, // 122 z + { 29, Punct }, // 123 { + { 25, Mixed }, // 124 | + { 30, Punct }, // 125 } + { 26, Mixed }, // 126 ~ + { 27, Mixed } // 127 DEL ^? +}; +Q_STATIC_ASSERT(sizeof(aztec_code_table) == 256); + +static const struct { + uint8_t c1; + uint8_t c2; + aztec_code_t sym; +} aztec_code_double_symbols[] = { + { '\r', '\n', { 2, Punct } }, // CR LF + { '.', ' ', { 3, Punct } }, // . SP + { ',', ' ', { 4, Punct } }, // , SP + { ':', ' ', { 5, Punct } } // : SP +}; +static const int aztec_code_double_symbols_count = sizeof(aztec_code_double_symbols) / sizeof(aztec_code_double_symbols[0]); + +static const int aztec_code_size[] = { 0, 5, 5, 5, 5, 4, 8 }; +Q_STATIC_ASSERT(sizeof(aztec_code_size) / sizeof(int) == MODE_COUNT); + +// codes for ambigious characters, ie. those that can be encoded in multiple modes +static const aztec_code_t aztec_special_chars[SPECIAL_CHAR_COUNT][MODE_COUNT - 1] = { + /* NoMode Upper Lower Mixed Punct Digit */ + { { 0, NoMode }, { 1, Upper }, { 1, Lower }, { 1, Mixed }, { 1, Upper }, { 1, Digit } }, /* SP */ + { { 0, NoMode }, { 1, Punct }, { 1, Punct }, { 14, Mixed }, { 1, Punct }, { 1, Punct } }, /* CR */ + { { 0, NoMode }, { 17, Punct }, { 17, Punct }, { 17, Punct }, { 17, Punct }, { 12, Digit } }, /* Comma */ + { { 0, NoMode }, { 19, Punct }, { 19, Punct }, { 19, Punct }, { 19, Punct }, { 13, Digit } }, /* Dot */ +}; + +// shift code table, source mode -> target mode +// NoMode indicates shift is not available, use latch instead +static const aztec_code_t aztec_shift_codes[MODE_COUNT - 1][MODE_COUNT - 1] = { + /* NoMode Upper Lower Mixed Punct Digit */ + { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode } }, + { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, Punct }, { 0, NoMode } }, + { { 0, NoMode }, { 28, Upper }, { 0, NoMode }, { 0, NoMode }, { 0, Punct }, { 0, NoMode } }, + { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, Punct }, { 0, NoMode } }, + { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode } }, + { { 0, NoMode }, { 15, Upper }, { 0, NoMode }, { 0, NoMode }, { 0, Punct }, { 0, NoMode } } +}; + +// latch code table, source mode -> target mode +static const aztec_code_t aztec_latch_codes[MODE_COUNT - 1][MODE_COUNT] = { + /* NoMode Upper Lower Mixed Punct Digit Binary */ + { { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode }, { 0, NoMode } }, + { { 0, NoMode }, { 0, NoMode }, { 28, Lower }, { 29, Mixed }, { 29, Mixed }, { 30, Digit }, { 31, Binary } }, + { { 0, NoMode }, { 30, Digit }, { 0, NoMode }, { 29, Mixed }, { 29, Mixed }, { 30, Digit }, { 31, Binary } }, + { { 0, NoMode }, { 30, Digit }, { 28, Lower }, { 0, NoMode }, { 30, Punct }, { 28, Lower }, { 31, Binary } }, + { { 0, NoMode }, { 31, Upper }, { 31, Upper }, { 31, Upper }, { 0, NoMode }, { 31, Upper }, { 31, Upper } }, + { { 0, NoMode }, { 14, Upper }, { 14, Upper }, { 14, Upper }, { 14, Upper }, { 0, NoMode }, { 14, Upper } } +}; + +static Mode aztecCodeLatchTo(Mode currentMode, Mode targetMode, BitVector *v) +{ + if (currentMode == targetMode) { + return targetMode; + } + const auto latchCode = aztec_latch_codes[currentMode][targetMode]; + qCDebug(Log) << "latch" << latchCode.code << aztec_code_size[currentMode]; + v->appendMSB(latchCode.code, aztec_code_size[currentMode]); + return static_cast(latchCode.mode); +} + +static void aztecEncodeBinary(std::vector::iterator &it, const std::vector::iterator &end, BitVector *v) +{ + // determine length of the binary sequence + const auto binEndIt = std::find_if(it, end, [](aztec_code_t sym) { return sym.mode != Binary; }); + const auto length = std::distance(it, binEndIt); + + // write length field + qCDebug(Log) << "binary length" << length; + if (length < 32) { + v->appendMSB(length, 5); + } else { + v->appendMSB(0, 5); + v->appendMSB(length - 31, 11); + } + + // write data + for (; it != binEndIt; ++it) { + qCDebug(Log) << "binary data" << (*it).code; + v->appendMSB((*it).code, 8); + } +} + +static void aztecEncodeResolveAmbigious(Mode currentMode, const std::vector::iterator &begin, const std::vector::iterator &end) +{ + Q_ASSERT(begin != end); + Q_ASSERT(currentMode != (*begin).mode); + Q_ASSERT((*begin).mode == Special); + + // forward search + auto it = begin; + for (; it != end && (*it).mode == Special; ++it) { + if (aztec_special_chars[(*it).code][currentMode].mode == currentMode) { + qCDebug(Log) << "special resolved to current mode by forward search"; + (*it).mode = aztec_special_chars[(*it).code][currentMode].mode; + (*it).code = aztec_special_chars[(*it).code][currentMode].code; + } + } + + // backward search + auto backIt = it; + while (std::distance(begin, backIt) >= 1 && it != end) { + --backIt; + if ((*backIt).mode == Special && aztec_special_chars[(*backIt).code][(*it).mode].mode == (*it).mode) { + qCDebug(Log) << "special resolved by backward search"; + (*backIt).mode = aztec_special_chars[(*backIt).code][(*it).mode].mode; + (*backIt).code = aztec_special_chars[(*backIt).code][(*it).mode].code; + } else { + break; + } + } + + // pick one if we still have an ambigious symbol + if ((*begin).mode != Special) { + return; + } + (*begin).mode = aztec_special_chars[(*begin).code][currentMode].mode; + (*begin).code = aztec_special_chars[(*begin).code][currentMode].code; + it = begin + 1; + if (it != end && (*it).mode == Special) { + aztecEncodeResolveAmbigious(static_cast((*begin).mode), it, end); + } +} + +static Mode aztecNextMode(Mode currentMode, const std::vector::iterator &nextSym, const std::vector::iterator &end, bool &tryShift) +{ + Q_ASSERT(currentMode != (*nextSym).mode); + Q_ASSERT(nextSym != end); + Q_ASSERT((*nextSym).mode != Special); + auto it = nextSym; + ++it; + if (it != end && (*it).mode == Special) { + aztecEncodeResolveAmbigious(static_cast((*nextSym).mode), it, end); + } + + if ((it == end || (*it).mode == currentMode) && std::distance(nextSym, it) == 1) { + tryShift = true; + } + + qCDebug(Log) << currentMode << (*nextSym).mode << tryShift << std::distance(nextSym, it); + return static_cast((*nextSym).mode); +} + +BitVector AztecBarcode::aztecEncode(const QByteArray &data) const +{ + // phase one: translate single and double chars to code points + std::vector codes; + codes.reserve(data.size()); + for (int i = 0; i < data.size(); ++i) { + const uint8_t c1 = data.at(i); + // double char codes + if (i < data.size() - 1) { + const uint8_t c2 = data.at(i + 1); + bool found = false; + for (int j = 0; j < aztec_code_double_symbols_count; ++j) { + const auto dblCode = aztec_code_double_symbols[j]; + if (dblCode.c1 != c1 || dblCode.c2 != c2) { + continue; + } + codes.push_back(dblCode.sym); + ++i; + found = true; + } + if (found) { + continue; + } + } + + // > 127 binary-only range + if (c1 > 127) { + codes.push_back({c1, Binary}); + // encodable single ASCII character + } else { + codes.push_back(aztec_code_table[c1]); + } + } + + // phase two: insert shift and latch codes, translate to bit stream + Mode currentMode = Upper; + BitVector result; + for (auto it = codes.begin(); it != codes.end();) { + if ((*it).mode == Binary) { + auto newMode = aztecCodeLatchTo(currentMode, Binary, &result); + while (newMode != Binary) { + currentMode = newMode; + newMode = aztecCodeLatchTo(currentMode, Binary, &result); + } + aztecEncodeBinary(it, codes.end(), &result); + continue; + } + // resolve special codes + if ((*it).mode == Special) { + aztecEncodeResolveAmbigious(currentMode, it, codes.end()); + } + + // deal with mode changes + Mode nextMode = currentMode; + if ((*it).mode != currentMode) { + bool tryShift = false; + const auto newMode = aztecNextMode(currentMode, it, codes.end(), tryShift); + + // shift to new mode if desired and possible + if (tryShift && aztec_shift_codes[currentMode][newMode].mode != NoMode) { + qCDebug(Log) << "shift" << aztec_shift_codes[currentMode][newMode].code << aztec_code_size[currentMode]; + result.appendMSB(aztec_shift_codes[currentMode][newMode].code, aztec_code_size[currentMode]); + currentMode = newMode; + } + + // latch to new mode + while (currentMode != newMode && newMode != NoMode && currentMode != NoMode) { + currentMode = aztecCodeLatchTo(currentMode, newMode, &result); + nextMode = currentMode; + } + } + + qCDebug(Log) << (*it).code << aztec_code_size[currentMode]; + result.appendMSB((*it).code, aztec_code_size[currentMode]); + ++it; + + currentMode = nextMode; + } + + return result; +} + +BitVector AztecBarcode::bitStuffAndPad(const BitVector& input, int codeWordSize) const +{ + BitVector res; + res.reserve(input.size()); + + // bit stuff codewords with leading codeWordSize 0/1 bits + int i = 0; + while(i < input.size() - (codeWordSize - 1)) { + int v = input.valueAtMSB(i, codeWordSize - 1); + res.appendMSB(v, codeWordSize - 1); + i += codeWordSize - 1; + if (v == 0) { + res.appendBit(true); + } else if (v == (1 << (codeWordSize - 1)) - 1) { + res.appendBit(false); + } else { + res.appendBit(input.at(i++)); + } + } + while (i < input.size()) { + res.appendBit(input.at(i++)); + } + + // pad to nearest code word boundary + while (res.size() % codeWordSize) { + res.appendBit(true); + } + + return res; +} + +void AztecBarcode::paintFullGrid(QImage *img) const +{ + QPainter p(img); + p.translate(img->width() / 2, img->height() / 2); + + // alignment grids + QPen pen(foregroundColor()); + pen.setDashPattern({1, 1}); + p.setPen(pen); + for (int i = 0; i < img->width() / 2; i += FullGridInterval) { + p.drawLine(-i, -FullRadius, -i, FullRadius); + p.drawLine( i, -FullRadius, i, FullRadius); + p.drawLine(-FullRadius, -i, FullRadius, -i); + p.drawLine(-FullRadius, i, FullRadius, i); + } + + // bullseye background + p.setBrush(backgroundColor()); + p.setPen(Qt::NoPen); + p.drawRect(-7, -7, 14, 14); + + // bullseye + p.setBrush(Qt::NoBrush); + p.setPen(foregroundColor()); + p.drawPoint(0, 0); + p.drawRect(-2, -2, 4, 4); + p.drawRect(-4, -4, 8, 8); + p.drawRect(-6, -6, 12, 12); + + // bullseye orientation marker + p.drawRect(-7, -7, 1, 1); + p.drawRect(7, -7, 0, 1); + p.drawPoint(7, 6); +} + +static const int aztecFullLayerOffset[] = { +// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 66, 64, 62, 60, 57, 55, 53, 51, 49, 47, 45, 42, 40, 38, 36, 34, 32, 30, 28, 25, 23, 21, 19, 17, 15, 13, 10, 8, 6, 4, 2, 0 +}; + +void AztecBarcode::paintFullData(QImage* img, const BitVector &data, int layerCount) const +{ + QPainter p(img); + p.setPen(foregroundColor()); + + auto it = data.begin(); + for (int layer = layerCount - 1; layer >= 0; --layer) { + const auto x1 = aztecFullLayerOffset[layer]; + const auto y1 = x1; + const auto gridInMiddle = (x1 - FullRadius) % FullGridInterval == 0; + const auto x2 = gridInMiddle ? x1 + 2 : x1 + 1; + const auto segmentLength = FullMaxSize - 2 * y1 - 2 - (gridInMiddle ? 1 : 0); + + for (int rotation = 0; rotation < 4; ++rotation) { + p.resetTransform(); + p.translate(img->width() / 2, img->height() / 2); + p.rotate(-90 * rotation); + p.translate(-img->width() / 2, -img->height() / 2); + + for (int i = 0; it != data.end(); ++i, ++it) { + const auto x = (i % 2 == 0) ? x1 : x2; + auto y = i / 2 + y1; + if (((y - FullRadius - 1) % FullGridInterval) == 0) { // skip grid lines + ++y; + i += 2; + } + if (y >= y1 + segmentLength) { + break; + } + if (*it) { + p.drawPoint(x, y); + } + } + } + } +} + +void AztecBarcode::paintFullModeMessage(QImage *img, const BitVector &modeData) const +{ + Q_ASSERT(modeData.size() == FullModeMessageSize); + + QPainter p(img); + p.setPen(foregroundColor()); + + auto it = modeData.begin(); + for (int rotation = 0; rotation < 4; ++rotation) { + p.resetTransform(); + p.translate(img->width() / 2, img->height() / 2); + p.rotate(90 * rotation); + + for (int i = -5; i <= 5; ++i) { + if (i == 0) { // skip grid line + continue; + } + if (*it) { + p.drawPoint(i, -7); + } + ++it; + } + } +} + +QImage AztecBarcode::cropAndScaleFull(QImage *img, int layerCount, int size) +{ + const auto offset = aztecFullLayerOffset[layerCount - 1]; + const auto minSize = FullMaxSize - 2 * offset; + setMinimumSize(QSizeF(minSize, minSize) * 4); // *4 taken from what QR does + const int scale = std::max(1, size / minSize); + + QImage out(minSize * scale, minSize * scale, img->format()); + QPainter p(&out); + p.setRenderHint(QPainter::SmoothPixmapTransform, false); + const auto srcRect = img->rect().adjusted(offset, offset, -offset, -offset); + p.drawImage(out.rect(), *img, srcRect); + return out; +} + +void AztecBarcode::paintCompactGrid(QImage *img) const +{ + QPainter p(img); + p.translate(img->width() / 2, img->height() / 2); + + // bullseye + p.setPen(foregroundColor()); + p.drawPoint(0, 0); + p.drawRect(-2, -2, 4, 4); + p.drawRect(-4, -4, 8, 8); + + // bullseye orientation marker + p.drawRect(-5, -5, 1, 1); + p.drawRect(5, -5, 0, 1); + p.drawPoint(5, 4); +} + +static const int aztecCompactLayerOffset[] = { + 6, 4, 2, 0 +}; + +void AztecBarcode::paintCompactData(QImage *img, const BitVector &data, int layerCount) const +{ + QPainter p(img); + p.setPen(foregroundColor()); + + auto it = data.begin(); + for (int layer = layerCount - 1; layer >= 0; --layer) { + const auto x1 = aztecCompactLayerOffset[layer]; + const auto y1 = x1; + const auto x2 = x1 + 1; + const auto segmentLength = CompactMaxSize - 2 * y1 - 2; + + for (int rotation = 0; rotation < 4; ++rotation) { + p.resetTransform(); + p.translate(img->width() / 2, img->height() / 2); + p.rotate(-90 * rotation); + p.translate(-img->width() / 2, -img->height() / 2); + + for (int i = 0; it != data.end(); ++i, ++it) { + const auto x = (i % 2 == 0) ? x1 : x2; + auto y = i / 2 + y1; + if (y >= y1 + segmentLength) { + break; + } + if (*it) { + p.drawPoint(x, y); + } + } + } + } +} + +void AztecBarcode::paintCompactModeMessage(QImage *img, const BitVector &modeData) const +{ + Q_ASSERT(modeData.size() == CompactModeMessageSize); + + QPainter p(img); + p.setPen(foregroundColor()); + + auto it = modeData.begin(); + for (int rotation = 0; rotation < 4; ++rotation) { + p.resetTransform(); + p.translate(img->width() / 2, img->height() / 2); + p.rotate(90 * rotation); + + for (int i = -3; i <= 3; ++i) { + if (*it) { + p.drawPoint(i, -5); + } + ++it; + } + } +} + +QImage AztecBarcode::cropAndScaleCompact(QImage *img, int layerCount, int size) +{ + const auto offset = aztecCompactLayerOffset[layerCount - 1]; + const auto minSize = CompactMaxSize - 2 * offset; + setMinimumSize(QSizeF(minSize, minSize) * 4); // *4 taken from what QR does + const int scale = std::max(1, size / minSize); + + QImage out(minSize * scale, minSize * scale, img->format()); + QPainter p(&out); + p.setRenderHint(QPainter::SmoothPixmapTransform, false); + const auto srcRect = img->rect().adjusted(offset, offset, -offset, -offset); + p.drawImage(out.rect(), *img, srcRect); + return out; +} diff --git a/src/lib/bitvector.cpp b/src/lib/bitvector.cpp new file mode 100644 --- /dev/null +++ b/src/lib/bitvector.cpp @@ -0,0 +1,130 @@ +/* + Copyright (c) 2017 Volker Krause + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include "bitvector_p.h" + +using namespace Prison; + +BitVector::BitVector() = default; +BitVector::~BitVector() = default; + +void BitVector::appendLSB(int data, int bits) +{ + for (int i = 0; i < bits; ++i) { + appendBit(data & (1 << i)); + } +} + +void BitVector::appendMSB(int data, int bits) +{ + for (int i = bits - 1; i >= 0; --i) { + appendBit(data & (1 << i)); + } +} + +void BitVector::appendBit(bool bit) +{ + const auto subIdx = m_size % 8; + if (subIdx == 0) { + m_data.append('\0'); + } + if (bit) { + m_data.data()[m_data.size() - 1] |= (1 << subIdx); + } + ++m_size; +} + +void BitVector::append(const BitVector &other) +{ + for (int i = 0; i < other.size(); ++i) { + appendBit(other.at(i)); + } +} + +bool BitVector::at(int index) const +{ + const auto majIdx = index / 8; + const auto minIdx = index % 8; + return (m_data.at(majIdx) & (1 << minIdx)) >> minIdx; +} + +void BitVector::clear() +{ + m_data.clear(); + m_size = 0; +} + +void BitVector::reserve(int size) +{ + m_data.reserve((size / 8) + 1); +} + +int BitVector::size() const +{ + return m_size; +} + +int BitVector::valueAtMSB(int index, int size) const +{ + int res = 0; + for (int i = 0; i < size; ++i) { + res = res << 1; + res |= (at(index + i) ? 1 : 0); + } + return res; +} + +BitVector::iterator BitVector::begin() const +{ + iterator it; + it.m_index = 0; + it.m_vector = this; + return it; +} + +BitVector::iterator BitVector::end() const +{ + iterator it; + it.m_index = m_size; + it.m_vector = this; + return it; +} + +bool BitVector::operator==(const BitVector &other) const +{ + return m_size == other.m_size && m_data == other.m_data; +} + +bool BitVector::operator!=(const Prison::BitVector& other) const +{ + return m_size != other.m_size || m_data != other.m_data; +} + +QDebug operator<<(QDebug dbg, const Prison::BitVector& v) +{ + dbg << v.m_data.toHex(); + return dbg; +} diff --git a/src/lib/bitvector_p.h b/src/lib/bitvector_p.h new file mode 100644 --- /dev/null +++ b/src/lib/bitvector_p.h @@ -0,0 +1,93 @@ +/* + Copyright (c) 2017 Volker Krause + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#ifndef PRISON_BITVECTOR_P_H +#define PRISON_BITVECTOR_P_H + +#include +#include + +namespace Prison { class BitVector; } +QDebug operator<<(QDebug dbg, const Prison::BitVector &v); + +namespace Prison { + +/** Vector for working with a set of bits without byte alignment. */ +class BitVector +{ +public: + BitVector(); + ~BitVector(); + + class iterator { + public: + inline bool operator!=(const iterator &other) + { + return m_index != other.m_index; + } + inline bool operator*() const + { + return m_vector->at(m_index); + } + inline iterator operator++() + { + ++m_index; + return *this; + } + private: + friend class BitVector; + const BitVector* m_vector; + int m_index; + }; + + /** Append the lowest @p bits of @p data with the least significant bit first. */ + void appendLSB(int data, int bits); + /** Append the lowest @p bits of @p data with the most significant bit first. */ + void appendMSB(int data, int bits); + void appendBit(bool bit); + void append(const BitVector &other); + /** Returns the bit at index @p index. */ + bool at(int index) const; + void clear(); + void reserve(int size); + int size() const; + /** Returns the value starting at @p index of size @p size. */ + int valueAtMSB(int index, int size) const; + iterator begin() const; + iterator end() const; + + bool operator==(const BitVector &other) const; + bool operator!=(const BitVector &other) const; + +private: + friend QDebug (::operator<<)(QDebug dbg, const Prison::BitVector &v); + QByteArray m_data; + int m_size = 0; +}; + +} + +#endif // PRISON_BITVECTOR_P_H diff --git a/src/lib/prison.cpp b/src/lib/prison.cpp --- a/src/lib/prison.cpp +++ b/src/lib/prison.cpp @@ -25,6 +25,7 @@ */ #include "prison.h" +#include "aztecbarcode.h" #include "datamatrixbarcode.h" #include "qrcodebarcode.h" #include "code39barcode.h" @@ -41,7 +42,7 @@ case Prison::DataMatrix: return new DataMatrixBarcode; case Prison::Aztec: - return nullptr; + return new AztecBarcode; case Prison::Code39: return new Code39Barcode; case Prison::Code93: diff --git a/src/lib/reedsolomon.cpp b/src/lib/reedsolomon.cpp new file mode 100644 --- /dev/null +++ b/src/lib/reedsolomon.cpp @@ -0,0 +1,111 @@ +/* + Copyright (c) 2017 Volker Krause + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include "reedsolomon_p.h" +#include "bitvector_p.h" + +#include + +#include + +using namespace Prison; + +// See https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders + +static int highestBit(int n) +{ + int i = 0; + while (n >= (1 << i)) + ++i; + return i - 1; +} + +ReedSolomon::ReedSolomon(int polynom, int symbolCount) + : m_symCount(symbolCount) +{ + m_symSize = highestBit(polynom); + + // calculate the log/alog tables + const auto logmod = (1 << m_symSize) - 1; + m_logTable.reset(new int[logmod + 1]); + m_antiLogTable.reset(new int [logmod]); + + for (int p = 1, v = 0; v < logmod; v++) { + m_antiLogTable[v] = p; + m_logTable[p] = v; + p <<= 1; + if (p & (1 << m_symSize)) { + p ^= polynom; + } + } + + // compute the encoding polynom + m_polynom.reset(new int[m_symCount + 1]); + m_polynom[0] = 1; + for (int i = 1; i <= m_symCount; ++i) { + m_polynom[i] = 1; + for (int k = i - 1; k > 0; --k) { + if (m_polynom[k]) { + m_polynom[k] = m_antiLogTable[(m_logTable[m_polynom[k]] + i) % logmod]; + } + m_polynom[k] ^= m_polynom[k - 1]; + } + m_polynom[0] = m_antiLogTable[(m_logTable[m_polynom[0]] + i) % logmod]; + } +} + +ReedSolomon::~ReedSolomon() = default; + +BitVector ReedSolomon::encode(const BitVector& input) const +{ + std::unique_ptr result(new int[m_symCount]); + for (int i = 0; i < m_symCount; ++i) { + result[i] = 0; + } + + const auto logmod = (1 << m_symSize) - 1; + for (int i = 0; i < input.size() / m_symSize; i++) { + auto m = result[m_symCount - 1] ^ input.valueAtMSB(i * m_symSize, m_symSize); + for (int k = m_symCount - 1; k > 0; --k) { + if (m && m_polynom[k]) { + result[k] = result[k - 1] ^ m_antiLogTable[(m_logTable[m] + m_logTable[m_polynom[k]]) % logmod]; + } else { + result[k] = result[k - 1]; + } + } + if (m && m_polynom[0]) { + result[0] = m_antiLogTable[(m_logTable[m] + m_logTable[m_polynom[0]]) % logmod]; + } else { + result[0] = 0; + } + } + + BitVector v; + for (int i = m_symCount - 1; i >= 0; --i) { + v.appendMSB(result[i], m_symSize); + } + return v; +} diff --git a/src/lib/prison.cpp b/src/lib/reedsolomon_p.h copy from src/lib/prison.cpp copy to src/lib/reedsolomon_p.h --- a/src/lib/prison.cpp +++ b/src/lib/reedsolomon_p.h @@ -1,5 +1,5 @@ /* - Copyright (c) 2010-2016 Sune Vuorela + Copyright (c) 2017 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation @@ -24,29 +24,48 @@ */ -#include "prison.h" -#include "datamatrixbarcode.h" -#include "qrcodebarcode.h" -#include "code39barcode.h" -#include "code93barcode.h" +#ifndef PRISON_REEDSOLOMON_P_H +#define PRISON_REEDSOLOMON_P_H -Prison::AbstractBarcode *Prison::createBarcode(BarcodeType type) +#include + +namespace Prison { + +class BitVector; + +/** Reed Solomon checksum generator. */ +class ReedSolomon { - switch(type) - { - case Prison::Null: - return nullptr; - case Prison::QRCode: - return new QRCodeBarcode; - case Prison::DataMatrix: - return new DataMatrixBarcode; - case Prison::Aztec: - return nullptr; - case Prison::Code39: - return new Code39Barcode; - case Prison::Code93: - return new Code93Barcode; - } - return nullptr; +public: + enum GF { + GF16 = 0x13, + GF64 = 0x43, + GF256 = 0x12d, + GF1024 = 0x409, + GF4096 = 0x1069 + }; + + /** Initialize a Reed Solomon encoder with the Galois Field + * described by the bit pattern of @p polynom, for generating + * @p symbolCount error correction symbols. + */ + explicit ReedSolomon(int polynom, int symbolCount); + ReedSolomon(const ReedSolomon&) = delete; + ~ReedSolomon(); + + /** Encode the content of @p input and return the resulting + * code words. + */ + BitVector encode(const BitVector &input) const; + +private: + std::unique_ptr m_logTable; + std::unique_ptr m_antiLogTable; + std::unique_ptr m_polynom; + int m_symCount = 0; + int m_symSize = 0; +}; } + +#endif // PRISON_REEDSOLOMON_P_H