From 3a79c38e65460d8ea31273d0d9cdc47a0be63ffc Mon Sep 17 00:00:00 2001 From: axxel Date: Sat, 2 Mar 2024 01:49:05 +0100 Subject: [PATCH] Writer: implement new experimental API (Create + Write) This adds two new operations 'create' and 'write' next to the existing 'read'. Each new operation is accompanied by an Options class: * ReadBarcodes(..., ReaderOptions) * CreateBarcodeFromText(..., CreatorOptions) * WriteBarcodeToImage(..., WriterOptions) Background info can be found here: * https://github.com/zxing-cpp/zxing-cpp/discussions/724 * https://github.com/zxing-cpp/zxing-cpp/discussions/739 --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 15 ++-- core/CMakeLists.txt | 3 + core/src/Barcode.cpp | 2 +- core/src/Barcode.h | 1 + core/src/WriteBarcode.cpp | 142 +++++++++++++++++++++++++++++++++++++ core/src/WriteBarcode.h | 129 ++++++++++++++++++++++++++++++++++ example/ZXingWriter.cpp | 143 +++++++++++++++++++++++++------------- 8 files changed, 381 insertions(+), 56 deletions(-) create mode 100644 core/src/WriteBarcode.cpp create mode 100644 core/src/WriteBarcode.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 808f13ce31..929f80797d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: run: ctest -V --test-dir build build-ios: - runs-on: macos-latest + runs-on: macos-13 steps: - uses: actions/checkout@v4 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c6cc67d41..b108daa70e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,13 +37,6 @@ if (BUILD_SHARED_LIBS) set (CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() -if (NOT CMAKE_CXX_STANDARD) - set (CMAKE_CXX_STANDARD 17) -endif() -if (NOT CMAKE_CXX_EXTENSIONS) - set (CMAKE_CXX_EXTENSIONS OFF) -endif() - if (NOT (BUILD_READERS OR BUILD_WRITERS)) message(FATAL_ERROR "At least one of BUILD_READERS/BUILD_WRITERS must be enabled.") endif() @@ -55,6 +48,7 @@ if (BUILD_UNIT_TESTS AND (NOT BUILD_WRITERS OR NOT BUILD_READERS)) endif() if (BUILD_EXPERIMENTAL_API) + set (CMAKE_CXX_STANDARD 20) add_definitions (-DZXING_BUILD_EXPERIMENTAL_API) endif() @@ -64,6 +58,13 @@ if(NOT BUILD_DEPENDENCIES IN_LIST BUILD_DEPENDENCIES_LIST) message(FATAL_ERROR "BUILD_DEPENDENCIES must be one of ${BUILD_DEPENDENCIES_LIST}") endif() +if (NOT CMAKE_CXX_STANDARD) + set (CMAKE_CXX_STANDARD 17) +endif() +if (NOT CMAKE_CXX_EXTENSIONS) + set (CMAKE_CXX_EXTENSIONS OFF) +endif() + add_subdirectory (core) enable_testing() diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 48260c4bc2..61a19f74e1 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -163,6 +163,8 @@ if (BUILD_WRITERS) src/TextEncoder.cpp src/MultiFormatWriter.h src/MultiFormatWriter.cpp + $<$:src/WriteBarcode.h> + $<$:src/WriteBarcode.cpp> ) endif() @@ -191,6 +193,7 @@ if (BUILD_READERS) src/ReaderOptions.h src/Result.h # [[deprecated]] src/StructuredAppend.h + $<$:${CMAKE_CURRENT_SOURCE_DIR}/src/WriteBarcode.h> $<$:${CMAKE_CURRENT_SOURCE_DIR}/src/ZXingC.h> ) endif() diff --git a/core/src/Barcode.cpp b/core/src/Barcode.cpp index 4f5ac3f74d..818b0ed6e5 100644 --- a/core/src/Barcode.cpp +++ b/core/src/Barcode.cpp @@ -206,4 +206,4 @@ Barcodes MergeStructuredAppendSequences(const Barcodes& barcodes) return res; } -} // ZXing +} // namespace ZXing diff --git a/core/src/Barcode.h b/core/src/Barcode.h index 1a730c6395..1ff863cdb4 100644 --- a/core/src/Barcode.h +++ b/core/src/Barcode.h @@ -164,6 +164,7 @@ class Result std::string version() const; #ifdef ZXING_BUILD_EXPERIMENTAL_API + void symbol(BitMatrix&& bits) { _symbol = std::make_shared(std::move(bits)); } const BitMatrix& symbol() const { return *_symbol; } #endif diff --git a/core/src/WriteBarcode.cpp b/core/src/WriteBarcode.cpp new file mode 100644 index 0000000000..e7e5f6446c --- /dev/null +++ b/core/src/WriteBarcode.cpp @@ -0,0 +1,142 @@ +/* +* Copyright 2024 Axel Waggershauser +*/ +// SPDX-License-Identifier: Apache-2.0 + +#ifdef ZXING_BUILD_EXPERIMENTAL_API + +#include "WriteBarcode.h" + +struct zint_symbol {}; + +namespace ZXing { + +struct CreatorOptions::Data +{ + BarcodeFormat format; + bool readerInit = false; + bool forceSquareDataMatrix = false; + std::string ecLevel; + + // symbol size (qrcode, datamatrix, etc), map from I, 'WxH' + // structured_append (idx, cnt, ID) + + mutable std::unique_ptr zint; +}; + +#define ZX_PROPERTY(TYPE, NAME) \ + 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(bool, readerInit) + ZX_PROPERTY(bool, forceSquareDataMatrix) + ZX_PROPERTY(std::string, ecLevel) + +#undef ZX_PROPERTY + +CreatorOptions::CreatorOptions(BarcodeFormat format) : d(std::make_unique(format)) {} +CreatorOptions::~CreatorOptions() = default; +CreatorOptions::CreatorOptions(CreatorOptions&&) = default; +CreatorOptions& CreatorOptions::operator=(CreatorOptions&&) = default; + + +struct WriterOptions::Data +{ + int scale = 0; + int sizeHint = 0; + int rotate = 0; + bool withHRT = false; + bool withQuietZones = true; +}; + +#define ZX_PROPERTY(TYPE, NAME) \ + TYPE WriterOptions::NAME() const noexcept { return d->NAME; } \ + WriterOptions& WriterOptions::NAME(TYPE v)& { return d->NAME = std::move(v), *this; } \ + WriterOptions&& WriterOptions::NAME(TYPE v)&& { return d->NAME = std::move(v), std::move(*this); } + +ZX_PROPERTY(int, scale) +ZX_PROPERTY(int, sizeHint) +ZX_PROPERTY(int, rotate) +ZX_PROPERTY(bool, withHRT) +ZX_PROPERTY(bool, withQuietZones) + +#undef ZX_PROPERTY + +WriterOptions::WriterOptions() : d(std::make_unique()) {} +WriterOptions::~WriterOptions() = default; +WriterOptions::WriterOptions(WriterOptions&&) = default; +WriterOptions& WriterOptions::operator=(WriterOptions&&) = default; + +} // namespace ZXing + + +#include "BitMatrix.h" +#include "MultiFormatWriter.h" +#include "ReadBarcode.h" + +namespace ZXing { + +static Barcode CreateBarcode(BitMatrix&& bits, const CreatorOptions& opts) +{ + auto img = ToMatrix(bits); + + auto res = ReadBarcode({img.data(), img.width(), img.height(), ImageFormat::Lum}, + ReaderOptions().setFormats(opts.format()).setIsPure(true).setBinarizer(Binarizer::BoolCast)); + res.symbol(std::move(bits)); + return res; +} + +static bool IsLinearCode(BarcodeFormat format) +{ + return BarcodeFormats(BarcodeFormat::LinearCodes).testFlag(format); +} + +Barcode CreateBarcodeFromText(std::string_view contents, const CreatorOptions& opts) +{ + auto writer = MultiFormatWriter(opts.format()).setMargin(0); + if (!opts.ecLevel().empty()) + writer.setEccLevel(std::stoi(opts.ecLevel())); + + return CreateBarcode(writer.encode(std::string(contents), 0, IsLinearCode(opts.format()) ? 50 : 0), opts); +} + +Barcode CreateBarcodeFromText(std::u8string_view contents, const CreatorOptions& opts) +{ + return CreateBarcodeFromText({reinterpret_cast(contents.data()), contents.size()}, opts); +} + +Barcode CreateBarcodeFromBytes(const void* data, int size, const CreatorOptions& opts) +{ + std::wstring bytes; + for (uint8_t c : std::basic_string_view((uint8_t*)data, size)) + bytes.push_back(c); + + auto writer = MultiFormatWriter(opts.format()).setMargin(0); + if (!opts.ecLevel().empty()) + writer.setEccLevel(std::stoi(opts.ecLevel())); + writer.setEncoding(CharacterSet::BINARY); + + return CreateBarcode(writer.encode(bytes, 0, IsLinearCode(opts.format()) ? 50 : 0), opts); +} + +std::string WriteBarcodeToSVG(const Barcode& barcode, [[maybe_unused]] const WriterOptions& opts) +{ + return ToSVG(barcode.symbol()); +} + +Image WriteBarcodeToImage(const Barcode& barcode, [[maybe_unused]] const WriterOptions& opts) +{ + auto symbol = Inflate(barcode.symbol().copy(), opts.sizeHint(), + IsLinearCode(barcode.format()) ? std::clamp(opts.sizeHint() / 2, 50, 300) : opts.sizeHint(), + opts.withQuietZones() ? 10 : 0); + auto bitmap = ToMatrix(symbol); + auto iv = Image(symbol.width(), symbol.height()); + std::memcpy(const_cast(iv.data()), bitmap.data(), iv.width() * iv.height()); + return iv; +} + +} // namespace ZXing + +#endif // ZXING_BUILD_EXPERIMENTAL_API diff --git a/core/src/WriteBarcode.h b/core/src/WriteBarcode.h new file mode 100644 index 0000000000..6f8b0ceeaa --- /dev/null +++ b/core/src/WriteBarcode.h @@ -0,0 +1,129 @@ +/* +* Copyright 2022 Axel Waggershauser +*/ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#ifdef ZXING_BUILD_EXPERIMENTAL_API + +#include "Barcode.h" +#include "ImageView.h" + +#include +#include + +extern "C" struct zint_symbol; + +namespace ZXing { + +class CreatorOptions +{ + struct Data; + + std::unique_ptr d; + + friend Barcode CreateBarcode(const void* data, int size, int mode, const CreatorOptions& options); + +public: + CreatorOptions(BarcodeFormat format); + + ~CreatorOptions(); + CreatorOptions(CreatorOptions&&); + CreatorOptions& operator=(CreatorOptions&&); + + zint_symbol* zint() const; + +#define ZX_PROPERTY(TYPE, NAME) \ + TYPE NAME() const noexcept; \ + CreatorOptions& NAME(TYPE v)&; \ + CreatorOptions&& NAME(TYPE v)&&; + + ZX_PROPERTY(BarcodeFormat, format) + ZX_PROPERTY(bool, readerInit) + ZX_PROPERTY(bool, forceSquareDataMatrix) + ZX_PROPERTY(std::string, ecLevel) + +#undef ZX_PROPERTY +}; + +/** + * Generate barcode from unicode text + * + * @param contents UTF-8 string to encode into a barcode + * @param options CreatorOptions (including BarcodeFormat) + * @return #Barcode generated barcode + */ +Barcode CreateBarcodeFromText(std::string_view contents, const CreatorOptions& options); + +/** + * Generate barcode from raw binary data + * + * @param data array of bytes to encode into a barcode + * @param size size of byte array + * @param options CreatorOptions (including BarcodeFormat) + * @return #Barcode generated barcode + */ +Barcode CreateBarcodeFromBytes(const void* data, int size, const CreatorOptions& options); + +#if __cplusplus > 201703L +Barcode CreateBarcodeFromText(std::u8string_view contents, const CreatorOptions& options); + +template +requires std::ranges::contiguous_range && std::ranges::sized_range && (sizeof(std::ranges::range_value_t) == 1) +Barcode CreateBarcodeFromBytes(const R& contents, const CreatorOptions& options) +{ + return CreateBarcodeFromBytes(std::ranges::data(contents), std::ranges::size(contents), options); +} +#endif + +// ================================================================================= + +class WriterOptions +{ + struct Data; + + std::unique_ptr d; + +public: + WriterOptions(); + ~WriterOptions(); + WriterOptions(WriterOptions&&); + WriterOptions& operator=(WriterOptions&&); + +#define ZX_PROPERTY(TYPE, NAME) \ + TYPE NAME() const noexcept; \ + WriterOptions& NAME(TYPE v)&; \ + WriterOptions&& NAME(TYPE v)&&; + + ZX_PROPERTY(int, scale) + ZX_PROPERTY(int, sizeHint) + ZX_PROPERTY(int, rotate) + ZX_PROPERTY(bool, withHRT) + ZX_PROPERTY(bool, withQuietZones) + +#undef ZX_PROPERTY +}; + + +/** + * Write barcode symbol to SVG + * + * @param barcode Barcode to write + * @param options WriterOptions to parameterize rendering + * @return std::string SVG representation of barcode symbol + */ +std::string WriteBarcodeToSVG(const Barcode& barcode, const WriterOptions& options = {}); + +/** + * Write barcode symbol to Image (Bitmap) + * + * @param barcode Barcode to write + * @param options WriterOptions to parameterize rendering + * @return Image Bitmap reprentation of barcode symbol + */ +Image WriteBarcodeToImage(const Barcode& barcode, const WriterOptions& options = {}); + +} // ZXing + +#endif // ZXING_BUILD_EXPERIMENTAL_API diff --git a/example/ZXingWriter.cpp b/example/ZXingWriter.cpp index 8258094b0a..8acd47f117 100644 --- a/example/ZXingWriter.cpp +++ b/example/ZXingWriter.cpp @@ -4,10 +4,15 @@ // SPDX-License-Identifier: Apache-2.0 #include "BarcodeFormat.h" -#include "BitMatrix.h" #include "BitMatrixIO.h" + +#ifdef ZXING_BUILD_EXPERIMENTAL_API +#include "WriteBarcode.h" +#else +#include "BitMatrix.h" #include "CharacterSet.h" #include "MultiFormatWriter.h" +#endif #include "ZXVersion.h" #include @@ -25,12 +30,14 @@ using namespace ZXing; static void PrintUsage(const char* exePath) { std::cout << "Usage: " << exePath - << " [-size x] [-margin ] [-encoding ] [-ecc ] \n" + << " [-size ] [-eclevel ] [-noqz] [-hrt] \n" << " -size Size of generated image\n" - << " -margin Margin around barcode\n" - << " -encoding Encoding used to encode input text\n" - << " -ecc Error correction level, [0-8]\n" +// << " -margin Margin around barcode\n" +// << " -encoding Encoding used to encode input text\n" + << " -eclevel Error correction level, [0-8]\n" << " -binary Interpret as a file name containing binary data\n" + << " -noqz Print barcode witout quiet zone\n" + << " -hrt Print human readable text below the barcode (if supported)\n" << " -help Print usage information\n" << " -version Print version information\n" << "\n" @@ -54,8 +61,21 @@ static bool ParseSize(std::string str, int* width, int* height) return false; } -static bool ParseOptions(int argc, char* argv[], int* width, int* height, int* margin, CharacterSet* encoding, - int* eccLevel, BarcodeFormat* format, std::string* text, std::string* filePath, bool* inputIsFile) +struct CLI +{ + BarcodeFormat format; + int sizeHint = 0; + std::string input; + std::string outPath; + std::string ecLevel; + bool inputIsFile = false; + bool withHRT = false; + bool withQZ = true; + bool verbose = false; +// CharacterSet encoding = CharacterSet::Unknown; +}; + +static bool ParseOptions(int argc, char* argv[], CLI& cli) { int nonOptArgCount = 0; for (int i = 1; i < argc; ++i) { @@ -63,24 +83,27 @@ static bool ParseOptions(int argc, char* argv[], int* width, int* height, int* m if (is("-size")) { if (++i == argc) return false; - if (!ParseSize(argv[i], width, height)) { - std::cerr << "Invalid size specification: " << argv[i] << std::endl; - return false; - } - } else if (is("-margin")) { - if (++i == argc) - return false; - *margin = std::stoi(argv[i]); - } else if (is("-ecc")) { - if (++i == argc) - return false; - *eccLevel = std::stoi(argv[i]); - } else if (is("-encoding")) { + cli.sizeHint = std::stoi(argv[i]); + } else if (is("-eclevel")) { if (++i == argc) return false; - *encoding = CharacterSetFromString(argv[i]); + cli.ecLevel = argv[i]; + // } else if (is("-margin")) { + // if (++i == argc) + // return false; + // cli.margin = std::stoi(argv[i]); + // } else if (is("-encoding")) { + // if (++i == argc) + // return false; + // cli.encoding = CharacterSetFromString(argv[i]); } else if (is("-binary")) { - *inputIsFile = true; + cli.inputIsFile = true; + } else if (is("-hrt")) { + cli.withHRT = true; + } else if (is("-noqz")) { + cli.withQZ = false; + } else if (is("-verbose")) { + cli.verbose = true; } else if (is("-help") || is("--help")) { PrintUsage(argv[0]); exit(0); @@ -88,17 +111,17 @@ static bool ParseOptions(int argc, char* argv[], int* width, int* height, int* m std::cout << "ZXingWriter " << ZXING_VERSION_STR << "\n"; exit(0); } else if (nonOptArgCount == 0) { - *format = BarcodeFormatFromString(argv[i]); - if (*format == BarcodeFormat::None) { + cli.format = BarcodeFormatFromString(argv[i]); + if (cli.format == BarcodeFormat::None) { std::cerr << "Unrecognized format: " << argv[i] << std::endl; return false; } ++nonOptArgCount; } else if (nonOptArgCount == 1) { - *text = argv[i]; + cli.input = argv[i]; ++nonOptArgCount; } else if (nonOptArgCount == 2) { - *filePath = argv[i]; + cli.outPath = argv[i]; ++nonOptArgCount; } else { return false; @@ -118,55 +141,81 @@ static std::string GetExtension(const std::string& path) return ext; } -static std::string ReadFile(const std::string& fn) +template +std::vector ReadFile(const std::string& fn) { - std::ifstream ifs(fn, std::ios::binary); - return ifs ? std::string(std::istreambuf_iterator(ifs), std::istreambuf_iterator()) : std::string(); + std::basic_ifstream ifs(fn, std::ios::binary); + if (!ifs.good()) + throw std::runtime_error("failed to open/read file " + fn); + return ifs ? std::vector(std::istreambuf_iterator(ifs), std::istreambuf_iterator()) : std::vector(); }; int main(int argc, char* argv[]) { - int width = 100, height = 100; - int margin = 10; - int eccLevel = -1; - bool inputIsFile = false; - CharacterSet encoding = CharacterSet::Unknown; - std::string input, filePath; - BarcodeFormat format; + CLI cli; - if (!ParseOptions(argc, argv, &width, &height, &margin, &encoding, &eccLevel, &format, &input, &filePath, &inputIsFile)) { + if (!ParseOptions(argc, argv, cli)) { PrintUsage(argv[0]); return -1; } try { - auto writer = MultiFormatWriter(format).setMargin(margin).setEncoding(encoding).setEccLevel(eccLevel); +#ifdef ZXING_BUILD_EXPERIMENTAL_API + auto cOpts = CreatorOptions(cli.format).ecLevel(cli.ecLevel); + auto barcode = cli.inputIsFile ? CreateBarcodeFromBytes(ReadFile(cli.input), cOpts) : CreateBarcodeFromText(cli.input, cOpts); + + auto wOpts = WriterOptions().sizeHint(cli.sizeHint).withQuietZones(cli.withQZ).withHRT(cli.withHRT).rotate(0); + auto bitmap = WriteBarcodeToImage(barcode, wOpts); + + if (cli.verbose) { + std::cout << "Text: \"" << barcode.text() << "\"\n" + << "Bytes: " << ToHex(barcode.bytes()) << "\n" + << "Format: " << ToString(barcode.format()) << "\n" + << "Identifier: " << barcode.symbologyIdentifier() << "\n" + << "Content: " << ToString(barcode.contentType()) << "\n" + << "HasECI: " << barcode.hasECI() << "\n" + << "Position: " << ToString(barcode.position()) << "\n" + << "Rotation: " << barcode.orientation() << " deg\n" + << "IsMirrored: " << barcode.isMirrored() << "\n" + << "IsInverted: " << barcode.isInverted() << "\n" + << "ecLevel: " << barcode.ecLevel() << "\n"; + std::cout << ToString(barcode.symbol()); + } +#else + auto writer = MultiFormatWriter(cli.format).setMargin(cli.withQZ ? 10 : 0); + if (!cli.ecLevel.empty()) + writer.setEccLevel(std::stoi(cli.ecLevel)); BitMatrix matrix; - if (inputIsFile) { - auto file = ReadFile(input); + if (cli.inputIsFile) { + auto file = ReadFile(cli.input); std::wstring bytes; for (uint8_t c : file) bytes.push_back(c); writer.setEncoding(CharacterSet::BINARY); - matrix = writer.encode(bytes, width, height); + matrix = writer.encode(bytes, cli.sizeHint, std::clamp(cli.sizeHint / 2, 50, 300)); } else { - matrix = writer.encode(input, width, height); + matrix = writer.encode(cli.input, cli.sizeHint, std::clamp(cli.sizeHint / 2, 50, 300)); } auto bitmap = ToMatrix(matrix); +#endif - auto ext = GetExtension(filePath); + auto ext = GetExtension(cli.outPath); int success = 0; if (ext == "" || ext == "png") { - success = stbi_write_png(filePath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0); + success = stbi_write_png(cli.outPath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0); } else if (ext == "jpg" || ext == "jpeg") { - success = stbi_write_jpg(filePath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0); + success = stbi_write_jpg(cli.outPath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0); } else if (ext == "svg") { - success = (std::ofstream(filePath) << ToSVG(matrix)).good(); +#ifdef ZXING_BUILD_EXPERIMENTAL_API + success = (std::ofstream(cli.outPath) << WriteBarcodeToSVG(barcode, wOpts)).good(); +#else + success = (std::ofstream(cli.outPath) << ToSVG(matrix)).good(); +#endif } if (!success) { - std::cerr << "Failed to write image: " << filePath << std::endl; + std::cerr << "Failed to write image: " << cli.outPath << std::endl; return -1; } } catch (const std::exception& e) {