You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
490 lines
16 KiB
490 lines
16 KiB
/* |
|
* Copyright 2024 Axel Waggershauser |
|
* Copyright 2025 gitlost |
|
*/ |
|
// SPDX-License-Identifier: Apache-2.0 |
|
|
|
#include "CreateBarcode.h" |
|
|
|
#include "BarcodeData.h" |
|
#include "BitMatrix.h" |
|
#include "ByteArray.h" |
|
#include "DecoderResult.h" |
|
#include "DetectorResult.h" |
|
#include "JSON.h" |
|
#include "Version.h" |
|
#include "ZXAlgorithms.h" |
|
|
|
#ifdef ZXING_READERS |
|
#include "ReadBarcode.h" |
|
#endif |
|
|
|
#ifdef ZXING_USE_ZINT |
|
#include "ECI.h" |
|
#include "TextEncoder.h" |
|
#include <zint.h> |
|
#else |
|
#include "MultiFormatWriter.h" |
|
#endif // ZXING_USE_ZINT |
|
|
|
#include <charconv> |
|
#include <optional> |
|
|
|
using namespace std::literals; |
|
|
|
namespace ZXing { |
|
|
|
struct CreatorOptions::Data |
|
{ |
|
BarcodeFormat format; |
|
std::string options; |
|
|
|
// symbol size (qrcode, datamatrix, etc), map from I, 'WxH' |
|
// structured_append (idx, cnt, ID) |
|
|
|
mutable unique_zint_symbol zint; |
|
|
|
#ifndef __cpp_aggregate_paren_init // make XCode 14.x happy |
|
Data(BarcodeFormat format, std::string options) : format(format), options(std::move(options)) {} |
|
#endif |
|
}; |
|
|
|
// TODO: check return type |
|
#define ZX_PROPERTY(TYPE, NAME) \ |
|
const TYPE& CreatorOptions::NAME() const noexcept { return d->NAME; } \ |
|
CreatorOptions& CreatorOptions::NAME(TYPE v)& { return d->NAME = std::move(v), *this; } \ |
|
CreatorOptions&& CreatorOptions::NAME(TYPE v)&& { return d->NAME = std::move(v), std::move(*this); } |
|
|
|
ZX_PROPERTY(BarcodeFormat, format) |
|
ZX_PROPERTY(std::string, options) |
|
|
|
#undef ZX_PROPERTY |
|
|
|
#define ZX_RO_PROPERTY(TYPE, NAME) \ |
|
std::optional<TYPE> CreatorOptions::NAME() const noexcept { return JsonGet<TYPE>(d->options, #NAME); } |
|
|
|
ZX_RO_PROPERTY(std::string, ecLevel); |
|
ZX_RO_PROPERTY(std::string, eci); |
|
ZX_RO_PROPERTY(bool, gs1); |
|
ZX_RO_PROPERTY(bool, readerInit); |
|
ZX_RO_PROPERTY(bool, forceSquare); |
|
ZX_RO_PROPERTY(int, columns); |
|
ZX_RO_PROPERTY(int, rows); |
|
ZX_RO_PROPERTY(int, version); |
|
ZX_RO_PROPERTY(int, dataMask); |
|
|
|
#undef ZX_RO_PROPERTY |
|
|
|
CreatorOptions::CreatorOptions(BarcodeFormat format, std::string options) : d(std::make_unique<Data>(format, std::move(options))) {} |
|
CreatorOptions::~CreatorOptions() = default; |
|
CreatorOptions::CreatorOptions(CreatorOptions&&) noexcept = default; |
|
CreatorOptions& CreatorOptions::operator=(CreatorOptions&&) noexcept = default; |
|
|
|
inline bool IsAscii(ByteView bv) |
|
{ |
|
return std::all_of(bv.begin(), bv.end(), [](uint8_t c) { return c <= 127; }); |
|
} |
|
|
|
#ifdef ZXING_WRITERS |
|
#ifdef ZXING_USE_ZINT |
|
|
|
struct String2Int |
|
{ |
|
const char* str; |
|
int val; |
|
}; |
|
|
|
static int ParseECLevel(int symbology, std::string_view s) |
|
{ |
|
constexpr std::string_view EC_LABELS_QR[4] = {"L", "M", "Q", "H"}; |
|
int res = 0; |
|
|
|
// Convert L/M/Q/H to Zint 1-4 |
|
if (Contains({BARCODE_QRCODE, BARCODE_MICROQR, BARCODE_RMQR}, symbology)) |
|
if ((res = IndexOf(EC_LABELS_QR, s)) != -1) |
|
return res + 1; |
|
|
|
if (std::from_chars(s.data(), s.data() + s.size() - (s.back() == '%'), res).ec != std::errc{}) |
|
throw std::invalid_argument(StrCat("Invalid ecLevel: '", s, "'")); |
|
|
|
auto findClosestECLevel = [](const std::vector<int>& list, int val) { |
|
int mIdx = -2, mAbs = 100; |
|
for (int i = 0; i < Size(list); ++i) |
|
if (int abs = std::abs(val - list[i]); abs < mAbs) { |
|
mIdx = i; |
|
mAbs = abs; |
|
} |
|
return mIdx + 1; |
|
}; |
|
|
|
// Convert percentage to Zint |
|
if (s.back() == '%') { |
|
switch (symbology) { |
|
case BARCODE_QRCODE: return findClosestECLevel({20, 37, 55, 65}, res); |
|
case BARCODE_MICROQR: return findClosestECLevel({20, 37, 55}, res); |
|
case BARCODE_RMQR: return res <= 46 ? 2 : 4; |
|
case BARCODE_AZTEC: return findClosestECLevel({10, 23, 36, 50}, res); |
|
case BARCODE_PDF417: |
|
// TODO: do something sensible with PDF417? |
|
default: |
|
return -1; |
|
} |
|
} |
|
|
|
return res; |
|
}; |
|
|
|
static constexpr struct { BarcodeFormat format; SymbologyIdentifier si; } barcodeFormat2SymbologyIdentifier[] = { |
|
{BarcodeFormat::Aztec, {'z', '0', 3}}, // '1' GS1, '2' AIM |
|
{BarcodeFormat::Codabar, {'F', '0'}}, // if checksum processing were implemented and checksum present and stripped then modifier would be 4 |
|
// {BarcodeFormat::CodablockF, {'O', '4'}}, // '5' GS1 |
|
{BarcodeFormat::Code128, {'C', '0'}}, // '1' GS1, '2' AIM |
|
// {BarcodeFormat::Code16K, {'K', '0'}}, // '1' GS1, '2' AIM, '4' D1 PAD |
|
// {BarcodeFormat::Code39, {'A', '0'}}, // '3' checksum, '4' extended, '7' checksum,extended |
|
{BarcodeFormat::Code39Ext, {'A', '4'}}, // '3' checksum, '4' extended, '7' checksum,extended |
|
{BarcodeFormat::DataBar, {'e', '0', 0, AIFlag::GS1}}, |
|
{BarcodeFormat::DataBarOmni, {'e', '0', 0, AIFlag::GS1}}, |
|
{BarcodeFormat::DataBarStk, {'e', '0', 0, AIFlag::GS1}}, |
|
{BarcodeFormat::DataBarStkOmni, {'e', '0', 0, AIFlag::GS1}}, |
|
{BarcodeFormat::DataBarExp, {'e', '0', 0, AIFlag::GS1}}, |
|
{BarcodeFormat::DataBarExpStk, {'e', '0', 0, AIFlag::GS1}}, |
|
{BarcodeFormat::DataBarLtd, {'e', '0', 0, AIFlag::GS1}}, |
|
{BarcodeFormat::DataMatrix, {'d', '1', 3}}, // '2' GS1, '3' AIM |
|
// {BarcodeFormat::DotCode, {'J', '0', 3}}, // '1' GS1, '2' AIM |
|
{BarcodeFormat::DXFilmEdge, {'X', 'F'}}, |
|
{BarcodeFormat::EAN8, {'E', '4'}}, |
|
// {BarcodeFormat::HanXin, {'h', '0', 1}}, // '2' GS1 |
|
// {BarcodeFormat::ITF, {'I', '0'}}, // '1' check digit |
|
{BarcodeFormat::MaxiCode, {'U', '0', 2}}, // '1' mode 2 or 3 |
|
{BarcodeFormat::MicroPDF417, {'L', '2', -1}}, |
|
{BarcodeFormat::MicroQRCode, {'Q', '1', 1}}, |
|
{BarcodeFormat::PDF417, {'L', '2', -1}}, |
|
{BarcodeFormat::QRCode, {'Q', '1', 1}}, // '3' GS1, '5' AIM |
|
{BarcodeFormat::RMQRCode, {'Q', '1', 1}}, // '3' GS1, '5' AIM |
|
}; |
|
|
|
static SymbologyIdentifier SymbologyIdentifierZint2ZXing(const CreatorOptions& opts, const ByteArray& ba) |
|
{ |
|
using enum BarcodeFormat; |
|
|
|
const BarcodeFormat format = opts.format(); |
|
|
|
auto i = FindIf(barcodeFormat2SymbologyIdentifier, [format](auto& v) { return v.format == format; }); |
|
SymbologyIdentifier ret = i != std::end(barcodeFormat2SymbologyIdentifier) ? i->si : SymbologyIdentifier{SymbologyKey(format), '0'}; |
|
|
|
if (format & (EAN13 | UPCA | UPCE)) { |
|
if (ba.size() > 13) // Have EAN-2/5 add-on? |
|
ret.modifier = '3'; // Combined packet, EAN-13, UPC-A, UPC-E, with add-on |
|
} else if (opts.gs1() && format & AllGS1) { |
|
if (format & (Aztec | Code128)) |
|
ret.modifier = '1'; |
|
else if (format == DataMatrix) |
|
ret.modifier = '2'; |
|
else if (format & (QRCode | RMQRCode)) |
|
ret.modifier = '3'; |
|
ret.aiFlag = AIFlag::GS1; |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
static std::string ECLevelZint2ZXing(const zint_symbol* zint) |
|
{ |
|
constexpr char EC_LABELS_QR[4] = {'L', 'M', 'Q', 'H'}; |
|
|
|
const int symbology = zint->symbology; |
|
const int option_1 = zint->option_1; |
|
|
|
switch (symbology) { |
|
case BARCODE_AZTEC: |
|
if ((option_1 >> 8) >= 0 && (option_1 >> 8) <= 99) |
|
return std::to_string(option_1 >> 8) + "%"; |
|
break; |
|
case BARCODE_MAXICODE: |
|
// Mode |
|
if (option_1 >= 2 && option_1 <= 6) |
|
return std::to_string(option_1); |
|
break; |
|
case BARCODE_PDF417: |
|
case BARCODE_PDF417COMP: |
|
// Convert to percentage |
|
if (option_1 >= 0 && option_1 <= 8) { |
|
int overhead = symbology == BARCODE_PDF417COMP ? 35 : 69; |
|
int cols = (zint->width - overhead) / 17; |
|
int tot_cws = zint->rows * cols; |
|
assert(tot_cws); |
|
return std::to_string((2 << option_1) * 100 / tot_cws) + "%"; |
|
} |
|
break; |
|
// case BARCODE_MICROPDF417: |
|
// if ((option_1 >> 8) >= 0 && (option_1 >> 8) <= 99) |
|
// return std::to_string(option_1 >> 8) + "%"; |
|
// break; |
|
case BARCODE_QRCODE: |
|
case BARCODE_MICROQR: |
|
case BARCODE_RMQR: |
|
// Convert to L/M/Q/H |
|
if (option_1 >= 1 && option_1 <= 4) |
|
return {EC_LABELS_QR[option_1 - 1]}; |
|
break; |
|
// case BARCODE_HANXIN: |
|
// if (option_1 >= 1 && option_1 <= 4) |
|
// return "L" + std::to_string(option_1); |
|
// break; |
|
default: |
|
break; |
|
} |
|
|
|
return {}; |
|
} |
|
|
|
#ifndef ZXING_READERS |
|
// Convert bytes to UTF-8 code points 0-255 |
|
static std::string BinaryToUtf8(ByteView ba) |
|
{ |
|
std::string utf8; |
|
utf8.reserve(ba.size() * 2); |
|
for (auto c : ba) |
|
if (c < 0x80) { |
|
utf8.push_back(c); |
|
} else if (c < 0xC0) { |
|
utf8.push_back(0xC2); |
|
utf8.push_back(c); |
|
} else { |
|
utf8.push_back(0xC3); |
|
utf8.push_back(c - 0x40); |
|
} |
|
return utf8; |
|
} |
|
#endif |
|
|
|
zint_symbol* CreatorOptions::zint() const |
|
{ |
|
using enum BarcodeFormat; |
|
|
|
auto& zint = d->zint; |
|
|
|
if (!zint) { |
|
#ifdef PRINT_DEBUG |
|
// printf("zint version: %d, sizeof(zint_symbol): %ld, options: %s\n", ZBarcode_Version(), sizeof(zint_symbol), options().c_str()); |
|
#endif |
|
zint.reset(ZBarcode_Create()); |
|
|
|
switch (format()) { |
|
#define X(NAME, SYM, VAR, FLAGS, ZINT, ENABLED, HRI) \ |
|
case BarcodeFormat(ZX_BCF_ID(SYM, VAR)): zint->symbology = ZINT; break; |
|
ZX_BCF_LIST(X) |
|
#undef X |
|
}; |
|
|
|
if (zint->symbology == 0) |
|
throw std::invalid_argument(StrCat("Unsupported barcode format for creation: ", ToString(format()))); |
|
|
|
if (format() == Code128 && gs1()) |
|
zint->symbology = BARCODE_GS1_128; |
|
|
|
zint->scale = 0.5f; |
|
|
|
if (auto val = ecLevel(); val) |
|
zint->option_1 = ParseECLevel(zint->symbology, *val); |
|
|
|
if (auto val = version(); val && !(format() & AllLinear)) |
|
zint->option_2 = *val; |
|
|
|
if (auto val = columns(); val && format() & (DataBarExpStk | PDF417 | MicroPDF417 | CompactPDF417)) |
|
zint->option_2 = *val; |
|
|
|
if (auto val = rows(); val && format() & (DataBarExpStk | PDF417)) |
|
zint->option_3 = *val; |
|
|
|
if (auto val = dataMask(); val && format() & (QRCode | MicroQRCode)) |
|
zint->option_3 = (zint->option_3 & 0xFF) | (*val + 1) << 8; |
|
|
|
if (format() == DataMatrix) |
|
zint->option_3 = (forceSquare() ? DM_SQUARE : DM_DMRE) | DM_ISO_144; |
|
} |
|
|
|
return zint.get(); |
|
} |
|
|
|
#define CHECK_WARN(ZINT_CALL, WARN) \ |
|
if (WARN = (ZINT_CALL); WARN >= ZINT_ERROR) \ |
|
throw std::invalid_argument(StrCat(zint->errtxt, " (retval: ", std::to_string(WARN), ")")); |
|
|
|
Barcode CreateBarcode(const void* data, int size, int mode, const CreatorOptions& opts) |
|
{ |
|
if (!data || size < 1) |
|
throw std::invalid_argument("Can not create a barcode from NULL or empty data"); |
|
|
|
auto zint = opts.zint(); |
|
|
|
zint->input_mode = mode == UNICODE_MODE && opts.gs1() && (opts.format() & BarcodeFormat::AllGS1) ? GS1_MODE : mode; |
|
if (mode == UNICODE_MODE && static_cast<const char*>(data)[0] != '[') |
|
zint->input_mode |= GS1PARENS_MODE; |
|
zint->output_options |= OUT_BUFFER_INTERMEDIATE | BARCODE_NO_QUIET_ZONES | BARCODE_CONTENT_SEGS; |
|
if (opts.readerInit()) |
|
zint->output_options |= READER_INIT; |
|
|
|
if (ZBarcode_Cap(zint->symbology, ZINT_CAP_ECI)) { |
|
if (auto eci = opts.eci(); eci) { |
|
if (auto cs = CharacterSetFromString(*eci); cs != CharacterSet::Unknown) { |
|
zint->eci = static_cast<int>(ToECI(cs)); |
|
} else if (std::all_of(eci->begin(), eci->end(), IsDigit<char>)) { |
|
zint->eci = std::stoi(*eci); |
|
} |
|
} else if (mode == DATA_MODE) { |
|
zint->eci = static_cast<int>(ECI::Binary); |
|
} |
|
// the following would make sure that non-ASCII text is encoded as UTF-8, hence the GuessTextEncoding for |
|
// non-ECI data can't produce a wrong result. |
|
// else if (mode == UNICODE_MODE && !IsAscii({data, narrow_cast<size_t>(size)})) |
|
// zint->eci = static_cast<int>(ECI::UTF8); |
|
} |
|
|
|
if (opts.format() == BarcodeFormat::Telepen && (std::all_of((const char*)data, (const char*)data + size, IsDigit<char>))) |
|
zint->symbology = BARCODE_TELEPEN_NUM; |
|
|
|
int warning; |
|
CHECK_WARN(ZBarcode_Encode_and_Buffer(zint, (uint8_t*)data, size, 0), warning); |
|
|
|
#ifdef PRINT_DEBUG |
|
printf("create symbol with size: %dx%d\n", zint->width, zint->rows); |
|
#endif |
|
|
|
#if 0 // use ReadBarcode to create Barcode object |
|
auto buffer = std::vector<uint8_t>(zint->bitmap_width * zint->bitmap_height); |
|
std::transform(zint->bitmap, zint->bitmap + zint->bitmap_width * zint->bitmap_height, buffer.data(), |
|
[](unsigned char v) { return (v == '0') * 0xff; }); |
|
|
|
auto res = ReadBarcode({buffer.data(), zint->bitmap_width, zint->bitmap_height, ImageFormat::Lum}, |
|
ReaderOptions().formats(opts.format()).isPure(true).binarizer(Binarizer::BoolCast)); |
|
#else |
|
assert(zint->content_seg_count == 1); |
|
const auto& content_seg = zint->content_segs[0]; |
|
const size_t content_seg_len = |
|
static_cast<size_t>(content_seg.length - (opts.format() == BarcodeFormat::Code93 && content_seg.length >= 2 ? 2 : 0)); |
|
|
|
Content content; |
|
|
|
if (zint->eci || warning == ZINT_WARN_USES_ECI) |
|
content.switchEncoding(ECI(content_seg.eci)); |
|
else |
|
content.switchEncoding(ToCharacterSet(ECI(content_seg.eci))); |
|
|
|
if ((zint->input_mode & 0x07) == UNICODE_MODE) { |
|
// `content_segs` returned as UTF-8 |
|
std::string utf8(reinterpret_cast<const char *>(content_seg.source), content_seg_len); |
|
content.append(TextEncoder::FromUnicode(utf8, ToCharacterSet(ECI(content_seg.eci)))); |
|
#ifndef ZXING_READERS |
|
content.utf8Cache.push_back(std::move(utf8)); |
|
#endif |
|
} else { |
|
content.append({content_seg.source, content_seg_len}); |
|
#ifndef ZXING_READERS |
|
content.utf8Cache.push_back(BinaryToUtf8(content.bytes)); |
|
#endif |
|
} |
|
|
|
content.symbology = SymbologyIdentifierZint2ZXing(opts, content.bytes); |
|
|
|
DecoderResult decRes(std::move(content)); |
|
decRes.setEcLevel(ECLevelZint2ZXing(zint)); |
|
decRes.setReaderInit(zint->output_options & READER_INIT); |
|
auto bits = BitMatrix(zint->bitmap_width, zint->bitmap_height); |
|
std::transform(zint->bitmap, zint->bitmap + zint->bitmap_width * zint->bitmap_height, bits.row(0).begin(), |
|
[](unsigned char v) { return (v == '1') * BitMatrix::SET_V; }); |
|
int left, top, width, height; |
|
bits.findBoundingBox(left, top, width, height); |
|
|
|
auto res = MatrixBarcode(std::move(decRes), {std::move(bits), Rectangle<PointI>(left, top, width, height)}, opts.format()); |
|
#endif |
|
|
|
res.zint = std::move(opts.d->zint); |
|
res.zintMutex = std::make_unique<std::mutex>(); |
|
|
|
return res; |
|
} |
|
|
|
Barcode CreateBarcodeFromText(std::string_view contents, const CreatorOptions& options) |
|
{ |
|
return CreateBarcode(contents.data(), contents.size(), UNICODE_MODE, options); |
|
} |
|
|
|
Barcode CreateBarcodeFromText(std::u8string_view contents, const CreatorOptions& options) |
|
{ |
|
return CreateBarcode(contents.data(), contents.size(), UNICODE_MODE, options); |
|
} |
|
|
|
Barcode CreateBarcodeFromBytes(const void* data, int size, const CreatorOptions& options) |
|
{ |
|
return CreateBarcode(data, size, DATA_MODE, options); |
|
} |
|
|
|
#else // ZXING_USE_ZINT |
|
|
|
zint_symbol* CreatorOptions::zint() const { return nullptr; } |
|
|
|
static Barcode CreateBarcode(BitMatrix&& bits, std::string_view contents, const CreatorOptions& opts) |
|
{ |
|
Content content; |
|
content.append(contents); |
|
|
|
DecoderResult decRes(std::move(content)); |
|
DetectorResult detRes(std::move(bits), Rectangle<PointI>(0, 0, bits.width(), bits.height())); |
|
return MatrixBarcode(std::move(decRes), std::move(detRes), opts.format()); |
|
} |
|
|
|
Barcode CreateBarcodeFromText(std::string_view contents, const CreatorOptions& opts) |
|
{ |
|
auto writer = MultiFormatWriter(opts.format()).setMargin(0); |
|
if (auto ecLevel = opts.ecLevel(); ecLevel && ecLevel->size() == 1 && strchr("012345678", (*ecLevel)[0])) |
|
writer.setEccLevel(std::stoi(*ecLevel)); |
|
if (!IsAscii({(const uint8_t*)contents.data(), contents.size()})) |
|
writer.setEncoding(CharacterSet::UTF8); // write UTF8 (ECI value 26) for maximum compatibility |
|
|
|
return CreateBarcode(writer.encode(std::string(contents), 0, opts.format() & BarcodeFormat::AllLinear ? 50 : 0), contents, opts); |
|
} |
|
|
|
Barcode CreateBarcodeFromText(std::u8string_view contents, const CreatorOptions& opts) |
|
{ |
|
return CreateBarcodeFromText({reinterpret_cast<const char*>(contents.data()), contents.size()}, opts); |
|
} |
|
|
|
Barcode CreateBarcodeFromBytes(const void* data, int size, const CreatorOptions& opts) |
|
{ |
|
std::wstring bytes; |
|
for (uint8_t c : ByteView(data, size)) |
|
bytes.push_back(c); |
|
|
|
auto writer = MultiFormatWriter(opts.format()).setMargin(0); |
|
if (auto ecLevel = opts.ecLevel(); ecLevel && ecLevel->size() == 1 && strchr("012345678", (*ecLevel)[0])) |
|
writer.setEccLevel(std::stoi(*ecLevel)); |
|
writer.setEncoding(CharacterSet::BINARY); |
|
|
|
return CreateBarcode(writer.encode(bytes, 0, opts.format() & BarcodeFormat::AllLinear ? 50 : 0), {(const char*)data, (size_t)size}, opts); |
|
} |
|
|
|
#endif // ZXING_USE_ZINT |
|
|
|
#else // ZXING_WRITERS |
|
|
|
zint_symbol* CreatorOptions::zint() const { return nullptr; } |
|
|
|
Barcode CreateBarcodeFromText(std::string_view, const CreatorOptions&) |
|
{ |
|
throw std::runtime_error("This build of zxing-cpp does not support creating barcodes."); |
|
} |
|
|
|
Barcode CreateBarcodeFromText(std::u8string_view, const CreatorOptions&) |
|
{ |
|
throw std::runtime_error("This build of zxing-cpp does not support creating barcodes."); |
|
} |
|
|
|
Barcode CreateBarcodeFromBytes(const void*, int, const CreatorOptions&) |
|
{ |
|
throw std::runtime_error("This build of zxing-cpp does not support creating barcodes."); |
|
} |
|
|
|
#endif // ZXING_WRITERS |
|
|
|
} // namespace ZXing
|
|
|