diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -16,3 +16,12 @@ TEST_NAME prison-reedsolomontest LINK_LIBRARIES Qt5::Test KF5::Prison ) + +set(code128barcodetest_srcs + code128barcodetest.cpp + ../src/lib/code128barcode.cpp + ../src/lib/bitvector.cpp + ${CMAKE_CURRENT_BINARY_DIR}/../src/lib/prison_debug.cpp +) + +ecm_add_test(${code128barcodetest_srcs} TEST_NAME prison-code128barcodetest LINK_LIBRARIES Qt5::Test KF5::Prison) diff --git a/autotests/code128barcodetest.cpp b/autotests/code128barcodetest.cpp new file mode 100644 --- /dev/null +++ b/autotests/code128barcodetest.cpp @@ -0,0 +1,118 @@ +/* + Copyright (c) 2018 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/code128barcode.h" +#include "../src/lib/bitvector_p.h" + +#include +#include +#include + +Q_DECLARE_METATYPE(Prison::BitVector) + +using namespace Prison; + +class Code128BarcodeTest : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void testEncode_data() + { + QTest::addColumn("input"); + QTest::addColumn("output"); + + BitVector v; + QTest::newRow("empty") << QByteArray() << v; + v.appendMSB(1680, 11); v.appendMSB(1554, 11); v.appendMSB(1062, 11); v.appendMSB(1424, 11); v.appendMSB(1220, 11); v.appendMSB(6379, 13); + QTest::newRow("all lower") << QByteArray("kde") << v; + v.clear(); + v.appendMSB(1680, 11); v.appendMSB(1422, 11); v.appendMSB(1416, 11); v.appendMSB(1128, 11); v.appendMSB(1764, 11); v.appendMSB(6379, 13); + QTest::newRow("all uppper") << QByteArray("KDE") << v; + + v.clear(); + v.appendMSB(1680, 11); v.appendMSB(1614, 11); v.appendMSB(1764, 11); v.appendMSB(6379, 13); + QTest::newRow("1 digit") << QByteArray("4") << v; + v.clear(); + v.appendMSB(1692, 11); v.appendMSB(1464, 11); v.appendMSB(1134, 11); v.appendMSB(6379, 13); + QTest::newRow("2 digits") << QByteArray("42") << v; + v.clear(); + v.appendMSB(1680, 11); v.appendMSB(1614, 11); v.appendMSB(1650, 11); v.appendMSB(1650, 11); v.appendMSB(1124, 11); v.appendMSB(6379, 13); + QTest::newRow("3 digits") << QByteArray("422") << v; + v.clear(); + v.appendMSB(1692, 11); v.appendMSB(1464, 11); v.appendMSB(1902, 11); v.appendMSB(1782, 11); v.appendMSB(6379, 13); + QTest::newRow("4 digits") << QByteArray("4223") << v; + + v.clear(); + v.appendMSB(1680, 11); v.appendMSB(1814, 11); v.appendMSB(1260, 11); v.appendMSB(1260, 11); v.appendMSB(1896, 11); v.appendMSB(1814, 11); v.appendMSB(6379, 13); + QTest::newRow("mixed") << QByteArray("X00Y") << v; + + v.clear(); + v.appendMSB(1668, 11); v.appendMSB(1292, 11); v.appendMSB(1292, 11); v.appendMSB(6379, 13); + QTest::newRow("null") << QByteArray("\0", 1) << v; + v.clear(); + v.appendMSB(1668, 11); v.appendMSB(1422, 11); v.appendMSB(1416, 11); v.appendMSB(1128, 11); v.appendMSB(1292, 11); v.appendMSB(1412, 11); v.appendMSB(6379, 13); + QTest::newRow("Code A only") << QByteArray("KDE\0", 4) << v; + + v.clear(); + v.appendMSB(1668, 11); v.appendMSB(1292, 11); v.appendMSB(1518, 11); v.appendMSB(1554, 11); v.appendMSB(1062, 11); v.appendMSB(1424, 11); v.appendMSB(1616, 11); v.appendMSB(6379, 13); + QTest::newRow("Start A -> Latch B") << QByteArray("\0kde", 4) << v; + v.clear(); + v.appendMSB(1680, 11); v.appendMSB(1554, 11); v.appendMSB(1062, 11); v.appendMSB(1886, 11); v.appendMSB(1292, 11); v.appendMSB(1292, 11); v.appendMSB(1602, 11); v.appendMSB(6379, 13); + QTest::newRow("Start B -> Latch A") << QByteArray("kd\0\0", 4) << v; + + v.clear(); + v.appendMSB(1668, 11); v.appendMSB(1292, 11); v.appendMSB(1954, 11); v.appendMSB(1118, 11); v.appendMSB(1590, 11); v.appendMSB(1292, 11); v.appendMSB(1328, 11); v.appendMSB(6379, 13); + QTest::newRow("Start A -> Shift B") << QByteArray("\0~@\0", 4) << v; + v.clear(); + v.appendMSB(1680, 11); v.appendMSB(1974, 11); v.appendMSB(1954, 11); v.appendMSB(1292, 11); v.appendMSB(1310, 11); v.appendMSB(1844, 11); v.appendMSB(6379, 13); + QTest::newRow("Start B -> Shift A") << QByteArray("{\0}", 3) << v; + + v.clear(); + v.appendMSB(1692, 11); v.appendMSB(1436, 11); v.appendMSB(1112, 11); v.appendMSB(1518, 11); v.appendMSB(1304, 11); v.appendMSB(1112, 11); v.appendMSB(1158, 11); v.appendMSB(6379, 13); + QTest::newRow("Start C -> Latch B") << QByteArray("1234AB") << v; + v.clear(); + v.appendMSB(1680, 11); v.appendMSB(1304, 11); v.appendMSB(1112, 11); v.appendMSB(1502, 11); v.appendMSB(1436, 11); v.appendMSB(1112, 11); v.appendMSB(1966, 11); v.appendMSB(6379, 13); + QTest::newRow("Start B -> Latch C") << QByteArray("AB1234") << v; + } + + void testEncode() + { + QFETCH(QByteArray, input); + QFETCH(BitVector, output); + + Code128Barcode code; + const auto v = code.encode(input); + if (v != output) { + qDebug() << "Actual :" << v; + qDebug() << "Expected:" << output; + } + QCOMPARE(v, output); + } +}; + +QTEST_APPLESS_MAIN(Code128BarcodeTest) + +#include "code128barcodetest.moc" diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -10,6 +10,7 @@ bitvector.cpp code39barcode.cpp code93barcode.cpp + code128barcode.cpp qrcodebarcode.cpp prison.cpp reedsolomon.cpp diff --git a/src/lib/prison.cpp b/src/lib/code128barcode.h copy from src/lib/prison.cpp copy to src/lib/code128barcode.h --- a/src/lib/prison.cpp +++ b/src/lib/code128barcode.h @@ -1,5 +1,5 @@ /* - Copyright (c) 2010-2016 Sune Vuorela + Copyright (c) 2018 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation @@ -24,35 +24,34 @@ */ -#include -#include "prison.h" -#include "aztecbarcode.h" -#include "datamatrixbarcode.h" -#include "qrcodebarcode.h" -#include "code39barcode.h" -#include "code93barcode.h" +#ifndef PRISON_CODE128BARCODE_H +#define PRISON_CODE128BARCODE_H -Prison::AbstractBarcode *Prison::createBarcode(BarcodeType type) +#include "abstractbarcode.h" + +class Code128BarcodeTest; + +namespace Prison { + +class BitVector; + +/** Code 128 barcode + * @see https://en.wikipedia.org/wiki/Code_128 + */ +class Code128Barcode : public AbstractBarcode { - switch(type) - { - case Prison::Null: - return nullptr; - case Prison::QRCode: - return new QRCodeBarcode; - case Prison::DataMatrix: -#ifdef HAVE_DMTX - return new DataMatrixBarcode; -#else - return nullptr; -#endif - case Prison::Aztec: - return new AztecBarcode; - case Prison::Code39: - return new Code39Barcode; - case Prison::Code93: - return new Code93Barcode; - } - return nullptr; +public: + Code128Barcode(); + ~Code128Barcode() override; + +protected: + QImage paintImage(const QSizeF& size) override; + +private: + friend class ::Code128BarcodeTest; + BitVector encode(const QByteArray &data) const; +}; } + +#endif // PRISON_CODE128BARCODE_H diff --git a/src/lib/code128barcode.cpp b/src/lib/code128barcode.cpp new file mode 100644 --- /dev/null +++ b/src/lib/code128barcode.cpp @@ -0,0 +1,356 @@ +/* + Copyright (c) 2018 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 "code128barcode.h" +#include "bitvector_p.h" +#include "prison_debug.h" + +using namespace Prison; + +#include +#include + +enum { + SymbolSize = 11, + StopPatternSize = 13, + StopPattern = 108, + QuietZone = 10, +}; + +enum CodeSet : uint8_t { + CodeSetA = 0, + CodeSetB = 1, + CodeSetC = 2, + CodeSetUnknown = 3 +}; + +enum CodeSetOp : uint8_t { + None = 255, + StartA = 103, + StartB = 104, + StartC = 105, + Shift = 98, + LatchA = 101, + LatchB = 100, + LatchC = 99 +}; + +Code128Barcode::Code128Barcode() = default; +Code128Barcode::~Code128Barcode() = default; + +QImage Code128Barcode::paintImage(const QSizeF& size) +{ + if (size.height() < 1) + return {}; + + const auto bits = encode(data().toLatin1()); + const auto width = bits.size() + 2 * QuietZone; + setMinimumSize(QSizeF(width, 10)); + + const auto moduleSize = size.width() / width; + if (moduleSize < 1) // too small for this + return {}; + + QImage img(moduleSize * width, size.height(), QImage::Format_ARGB32); + img.fill(backgroundColor()); + QPainter p(&img); + for (int i = 0; i < bits.size(); ++i) { + if (bits.at(i)) + p.fillRect((QuietZone + i) * moduleSize, 0, moduleSize, img.height(), foregroundColor()); + } + + return img; +} + +// Code 128 symbol table +// ### this is the perfect use-case for binary literals (as the binary pattern +// corresponds to the line pattern), adjust this once KF5 moves to C++14 +static const uint16_t code128_symbols[] = +{ + 1740, // 0b11011001100 // 0 + 1644, // 0b11001101100 + 1638, // 0b11001100110 + 1176, // 0b10010011000 + 1164, // 0b10010001100 + 1100, // 0b10001001100 + 1224, // 0b10011001000 + 1220, // 0b10011000100 + 1124, // 0b10001100100 + 1608, // 0b11001001000 + 1604, // 0b11001000100 // 10 + 1572, // 0b11000100100 + 1436, // 0b10110011100 + 1244, // 0b10011011100 + 1230, // 0b10011001110 + 1484, // 0b10111001100 + 1260, // 0b10011101100 + 1254, // 0b10011100110 + 1650, // 0b11001110010 + 1628, // 0b11001011100 + 1614, // 0b11001001110 // 20 + 1764, // 0b11011100100 + 1652, // 0b11001110100 + 1902, // 0b11101101110 + 1868, // 0b11101001100 + 1836, // 0b11100101100 + 1830, // 0b11100100110 + 1892, // 0b11101100100 + 1844, // 0b11100110100 + 1842, // 0b11100110010 + 1752, // 0b11011011000 // 30 + 1734, // 0b11011000110 + 1590, // 0b11000110110 + 1304, // 0b10100011000 + 1112, // 0b10001011000 + 1094, // 0b10001000110 + 1416, // 0b10110001000 + 1128, // 0b10001101000 + 1122, // 0b10001100010 + 1672, // 0b11010001000 + 1576, // 0b11000101000 // 40 + 1570, // 0b11000100010 + 1464, // 0b10110111000 + 1422, // 0b10110001110 + 1134, // 0b10001101110 + 1496, // 0b10111011000 + 1478, // 0b10111000110 + 1142, // 0b10001110110 + 1910, // 0b11101110110 + 1678, // 0b11010001110 + 1582, // 0b11000101110 // 50 + 1768, // 0b11011101000 + 1762, // 0b11011100010 + 1774, // 0b11011101110 + 1880, // 0b11101011000 + 1862, // 0b11101000110 + 1814, // 0b11100010110 + 1896, // 0b11101101000 + 1890, // 0b11101100010 + 1818, // 0b11100011010 + 1914, // 0b11101111010 // 60 + 1602, // 0b11001000010 + 1930, // 0b11110001010 + 1328, // 0b10100110000 + 1292, // 0b10100001100 + 1200, // 0b10010110000 + 1158, // 0b10010000110 + 1068, // 0b10000101100 + 1062, // 0b10000100110 + 1424, // 0b10110010000 + 1412, // 0b10110000100 // 70 + 1232, // 0b10011010000 + 1218, // 0b10011000010 + 1076, // 0b10000110100 + 1074, // 0b10000110010 + 1554, // 0b11000010010 + 1616, // 0b11001010000 + 1978, // 0b11110111010 + 1556, // 0b11000010100 + 1146, // 0b10001111010 + 1340, // 0b10100111100 // 80 + 1212, // 0b10010111100 + 1182, // 0b10010011110 + 1508, // 0b10111100100 + 1268, // 0b10011110100 + 1266, // 0b10011110010 + 1956, // 0b11110100100 + 1940, // 0b11110010100 + 1938, // 0b11110010010 + 1758, // 0b11011011110 + 1782, // 0b11011110110 // 90 + 1974, // 0b11110110110 + 1400, // 0b10101111000 + 1310, // 0b10100011110 + 1118, // 0b10001011110 + 1512, // 0b10111101000 + 1506, // 0b10111100010 + 1960, // 0b11110101000 + 1954, // 0b11110100010 + 1502, // 0b10111011110 + 1518, // 0b10111101110 // 100 + 1886, // 0b11101011110 + 1966, // 0b11110101110 + 1668, // 0b11010000100 + 1680, // 0b11010010000 + 1692, // 0b11010011100 + 1594, // 0b11000111010 + 1720, // 0b11010111000 + 6379 // 0b1100011101011 +}; + +static uint8_t symbolForCharacter(const QByteArray &data, int index, CodeSet set) +{ + const auto c1 = data.at(index); + switch (set) { + case CodeSetA: + return (c1 < ' ') ? c1 + 64 : c1 - ' '; + case CodeSetB: + return c1 - ' '; + case CodeSetC: + { + const auto c2 = data.at(index + 1); + return ((c1 - '0') * 10) + c2 - '0'; + } + case CodeSetUnknown: + Q_UNREACHABLE(); + } + + Q_UNREACHABLE(); + return {}; +} + +struct CodeSetChange { CodeSet set; CodeSetOp symbol; }; + +static bool isInCodeSetA(char c) +{ + return c <= 95; +} + +static bool isInCodeSetB(char c) +{ + // ### this does not consider FNC4 high byte encoding + return c >= 32; +} + +static CodeSetChange opForData(const QByteArray &data, int index, CodeSet currentSet) +{ + // determine if Code C makes sense at this point + int codeC = 0; + for (int i = index; i < data.size(); ++i, ++codeC) { + if (data.at(i) < '0' || data.at(i) > '9') + break; + } + if (currentSet == CodeSetC && codeC >= 2) { // already in C + return { CodeSetC, None }; + } + if (codeC >= 6 // that's always good enough + || (index == 0 && codeC >= 4) // beginning of data + || (index + codeC == data.size() && codeC >= 4) // end of data + || (codeC == data.size() && codeC == 2) // 2 ... + || (codeC == data.size() && codeC == 4)) // ... or 4 as the entire data + { + return currentSet == CodeSetUnknown ? CodeSetChange{ CodeSetC, StartC } : CodeSetChange{ CodeSetC, LatchC }; + } + + // if we are in Code A or Code B, check if we need to switch for the next char + // this is a shortcut to prevent the below more extensive search from making this O(n²) in the common case + if ((currentSet == CodeSetA && isInCodeSetA(data.at(index))) || (currentSet == CodeSetB && isInCodeSetB(data.at(index)))) { + return { currentSet, None }; + } + + // we need to switch to A or B, select which one, and select whether to use start, shift or latch + const auto nextA = isInCodeSetA(data.at(index)); + const auto nextB = isInCodeSetB(data.at(index)); + + // count how many following characters we could encode in A or B + int countA = 0; + for (int i = index + 1; i < data.size(); ++i, ++countA) { + if (!isInCodeSetA(data.at(i))) + break; + } + int countB = 0; + for (int i = index + 1; i < data.size(); ++i, ++countB) { + if (!isInCodeSetB(data.at(i))) + break; + } + + // select how we want to switch to Code A or Code B, biased to B as that's the more useful one in general + switch (currentSet) { + case CodeSetUnknown: + // if we are at the start, take whichever code will get us further, or the only one that works + if (nextA && nextB) { + return countA > countB ? CodeSetChange{ CodeSetA, StartA } : CodeSetChange{ CodeSetB, StartB }; + } + return nextA ? CodeSetChange{ CodeSetA, StartA } : CodeSetChange{ CodeSetB, StartB }; + case CodeSetC: + // same for Code C + if (nextA && nextB) { + return countA > countB ? CodeSetChange{ CodeSetA, LatchA } : CodeSetChange{ CodeSetB, LatchB }; + } + return nextA ? CodeSetChange{ CodeSetA, LatchA } : CodeSetChange{ CodeSetB, LatchB }; + case CodeSetA: + // switch or latch to B? + return CodeSetChange{ CodeSetB, countB >= countA ? LatchB : Shift }; + case CodeSetB: + // switch or latch to A? + return CodeSetChange{ CodeSetA, countA > countB ? LatchA : Shift }; + } + + Q_UNREACHABLE(); + return CodeSetChange{ currentSet, None }; +} + +BitVector Code128Barcode::encode(const QByteArray& data) const +{ + BitVector v; + if (data.isEmpty()) { + return v; + } + + // determine code set for start + const auto op = opForData(data, 0, CodeSetUnknown); + auto currentSet = op.set; + + // write start code + qCDebug(Log) << "start symbol:" << op.symbol << code128_symbols[op.symbol]; + v.appendMSB(code128_symbols[op.symbol], SymbolSize); + + uint32_t checksum = op.symbol; + uint32_t checksumWeight = 1; + + for (int i = 0; i < data.size(); i += currentSet == CodeSetC ? 2 : 1) { + if (static_cast(data.at(i)) > 127) { // FNC4 encoding not implemented yet + continue; + } + + // perform code switch if needed + const auto op = opForData(data, i, currentSet); + if (op.symbol != None) { + qCDebug(Log) << "op symbol:" << op.symbol << code128_symbols[op.symbol]; + v.appendMSB(code128_symbols[op.symbol], SymbolSize); + checksum += op.symbol * checksumWeight++; + } + + // encode current symbol + const auto symbol = symbolForCharacter(data, i, op.set); + qCDebug(Log) << "data symbol:" << symbol << code128_symbols[symbol]; + v.appendMSB(code128_symbols[symbol], SymbolSize); + checksum += symbol * checksumWeight++; + + // update current code set + if (op.symbol != Shift) { + currentSet = op.set; + } + } + + // encode checksum + qCDebug(Log) << "checksum:" << checksum << code128_symbols[checksum % 103]; + v.appendMSB(code128_symbols[checksum % 103], SymbolSize); + + // add stop pattern + v.appendMSB(code128_symbols[StopPattern], StopPatternSize); + return v; +} diff --git a/src/lib/prison.h b/src/lib/prison.h --- a/src/lib/prison.h +++ b/src/lib/prison.h @@ -42,12 +42,14 @@ QRCode = 1, /** DataMatrix 2d barcode */ DataMatrix = 2, - /** Aztec 2d barcode (currently unsupported)*/ + /** Aztec 2d barcode */ Aztec, /** Code39 barcode */ Code39, /** Code93 barcode */ - Code93 + Code93, + /** Code 128 barcode */ + Code128 }; /** diff --git a/src/lib/prison.cpp b/src/lib/prison.cpp --- a/src/lib/prison.cpp +++ b/src/lib/prison.cpp @@ -31,6 +31,7 @@ #include "qrcodebarcode.h" #include "code39barcode.h" #include "code93barcode.h" +#include "code128barcode.h" Prison::AbstractBarcode *Prison::createBarcode(BarcodeType type) { @@ -52,6 +53,8 @@ return new Code39Barcode; case Prison::Code93: return new Code93Barcode; + case Prison::Code128: + return new Code128Barcode; } return nullptr; diff --git a/tests/barcode.qml b/tests/barcode.qml --- a/tests/barcode.qml +++ b/tests/barcode.qml @@ -44,7 +44,7 @@ } ComboBox { id: typeCombobox - model: [ "Null", "QRCode", "DataMatrix", "Aztec", "Code39", "Code93" ] + model: [ "Null", "QRCode", "DataMatrix", "Aztec", "Code39", "Code93", "Code128" ] currentIndex: 3 } }