/* * Copyright 2016 Nu-book Inc. * Copyright 2019 Axel Waggershauser */ // SPDX-License-Identifier: Apache-2.0 #include "GTIN.h" #include "ZXingCpp.h" #include #include #include #include #include #include #include #include #include #include #define STB_IMAGE_IMPLEMENTATION #include #define STB_IMAGE_WRITE_IMPLEMENTATION #include #ifdef ZXING_USE_WEBP #include #endif using namespace ZXing; struct CLI { std::vector filePaths; std::string outPath; int forceChannels = 0; int rotate = 0; bool oneLine = false; bool bytesOnly = false; bool showSymbol = false; bool json = false; }; static void PrintUsage(const char* exePath) { std::cout << "Usage: " << exePath << " [options] ...\n" << " -fast Skip some lines/pixels during detection (faster)\n" << " -norotate Don't try rotated image during detection (faster)\n" << " -noinvert Don't search for inverted codes during detection (faster)\n" << " -noscale Don't try downscaled images during detection (faster)\n" << " -formats \n" << " Only detect given format(s) (faster)\n" << " -single Stop after the first barcode is detected (faster)\n" << " -ispure Assume the image contains only a 'pure'/perfect code (faster)\n" << " -errors Include barcodes with errors (like checksum error)\n" << " -binarizer \n" << " Binarizer to be used for gray to binary conversion\n" << " -mode \n" << " Text mode used to render the raw byte content into text\n" << " -1 Print only file name, content/error on one line per file/barcode (implies '-mode Escaped')\n" << " -symbol Print the detected symbol (if available)\n" << " -json Print a complete JSON formatted serialization\n" #ifdef ZXING_EXPERIMENTAL_API << " -denoise Use extra denoiseing (closing operation)\n" #endif << " -bytes Write (only) the bytes content of the symbol(s) to stdout\n" << " -pngout \n" << " Write a copy of the input image with barcodes outlined by a green line\n" << " -help Print usage information\n" << " -version Print version information\n" << "\n" << "Supported formats are (Symbology : Variants):"; for (auto f : BarcodeFormats::list(BarcodeFormat::AllReadable)) { if (Symbology(f) == f) std::cout << "\n " << std::setw(13) << ToString(f) << " : "; else std::cout << ToString(f) << ", "; } std::cout << "\n\n"; std::cout << "BarcodeFormats can be lowercase, with or without any of ' -_/', separated by ',' or '|'\n"; } static bool ParseOptions(int argc, char* argv[], ReaderOptions& options, CLI& cli) { for (int i = 1; i < argc; ++i) { auto is = [&](const char* str) { return strlen(argv[i]) > 1 && strncmp(argv[i], str, strlen(argv[i])) == 0; }; if (is("-fast")) { options.tryHarder(false); } else if (is("-norotate")) { options.tryRotate(false); } else if (is("-noinvert")) { options.tryInvert(false); } else if (is("-noscale")) { options.tryDownscale(false); #ifdef ZXING_EXPERIMENTAL_API } else if (is("-denoise")) { options.tryDenoise(true); #endif } else if (is("-single")) { options.maxNumberOfSymbols(1); } else if (is("-ispure")) { options.isPure(true); options.binarizer(Binarizer::FixedThreshold); } else if (is("-errors")) { options.returnErrors(true); } else if (is("-formats")) { if (++i == argc) return false; try { options.formats(BarcodeFormatsFromString(argv[i])); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n\n"; return false; } } else if (is("-binarizer")) { if (++i == argc) return false; else if (is("local")) options.binarizer(Binarizer::LocalAverage); else if (is("global")) options.binarizer(Binarizer::GlobalHistogram); else if (is("fixed")) options.binarizer(Binarizer::FixedThreshold); else return false; } else if (is("-mode")) { if (++i == argc) return false; else if (is("plain")) options.textMode(TextMode::Plain); else if (is("eci")) options.textMode(TextMode::ECI); else if (is("hri")) options.textMode(TextMode::HRI); else if (is("escaped")) options.textMode(TextMode::Escaped); else return false; } else if (is("-1")) { cli.oneLine = true; } else if (is("-bytes")) { cli.bytesOnly = true; } else if (is("-symbol")) { cli.showSymbol = true; } else if (is("-json")) { cli.json = true; } else if (is("-pngout")) { if (++i == argc) return false; cli.outPath = argv[i]; } else if (is("-channels")) { if (++i == argc) return false; cli.forceChannels = std::stoi(argv[i]); } else if (is("-rotate")) { if (++i == argc) return false; cli.rotate = std::stoi(argv[i]); } else if (is("-help") || is("--help")) { PrintUsage(argv[0]); exit(0); } else if (is("-version") || is("--version")) { std::cout << "ZXingReader version " << Version() << "\n"; exit(0); } else { cli.filePaths.push_back(argv[i]); } } return !cli.filePaths.empty(); } void drawLine(const ImageView& iv, PointI a, PointI b, bool error) { int steps = maxAbsComponent(b - a); PointF dir = bresenhamDirection(PointF(b - a)); int R = RedIndex(iv.format()), G = GreenIndex(iv.format()), B = BlueIndex(iv.format()); for (int i = 0; i < steps; ++i) { auto p = PointI(centered(a + i * dir)); auto* dst = const_cast(iv.data(p.x, p.y)); if (dst < iv.data(0, 0) || dst > iv.data(iv.width() - 1, iv.height() - 1)) continue; dst[R] = error ? 0xff : 0; dst[G] = error ? 0 : 0xff; dst[B] = 0; } } void drawRect(const ImageView& image, const Position& pos, bool error) { for (int i = 0; i < 4; ++i) drawLine(image, pos[i], pos[(i + 1) % 4], error); } std::unique_ptr LoadWebP(const std::string& filePath, int* width, int* height, int* channels) { #ifdef ZXING_USE_WEBP std::ifstream file(filePath, std::ios::binary); if (!file) throw std::runtime_error("Failed to read image: " + filePath + " (failed to open file)"); std::vector compressedData(std::istreambuf_iterator(file), {}); if (compressedData.empty()) throw std::runtime_error("Failed to read image: " + filePath + " (empty file)"); *channels = 3; uint8_t* img = WebPDecodeRGB(compressedData.data(), compressedData.size(), width, height); if (img == nullptr) throw std::runtime_error("Failed to read image: " + filePath + " (WebP decode failed)"); return std::unique_ptr(img, WebPFree); #else std::cerr << "Failed to read image: " << filePath << " (WebP support is not enabled in this build)" << "\n"; return std::unique_ptr(nullptr, nullptr); #endif } std::unique_ptr LoadStbi(const std::string& filePath, int* width, int* height, int* channels, int forceChannels) { auto buffer = std::unique_ptr( filePath == "-" ? stbi_load_from_file(stdin, width, height, channels, forceChannels) : stbi_load(filePath.c_str(), width, height, channels, forceChannels), stbi_image_free); if (buffer == nullptr) std::cerr << "Failed to read image: " << filePath << " (" << stbi_failure_reason() << ")" << "\n"; return buffer; } bool IEndsWith(std::string_view str, std::string_view suffix) { return str.size() > suffix.size() && std::equal(str.end() - suffix.size(), str.end(), suffix.begin(), [](char a, char b) { return std::tolower(static_cast(a)) == b; }); } int main(int argc, char* argv[]) { ReaderOptions options; CLI cli; Barcodes allBarcodes; int ret = 0; options.textMode(TextMode::HRI); options.eanAddOnSymbol(EanAddOnSymbol::Read); if (!ParseOptions(argc, argv, options, cli)) { PrintUsage(argv[0]); return -1; } std::cout.setf(std::ios::boolalpha); if (!cli.outPath.empty()) cli.forceChannels = 3; // the drawing code only works for RGB data for (const auto& filePath : cli.filePaths) { int width, height, channels; auto buffer = IEndsWith(filePath, ".webp") ? LoadWebP(filePath, &width, &height, &channels) : LoadStbi(filePath, &width, &height, &channels, cli.forceChannels); if (buffer == nullptr) continue; channels = cli.forceChannels ? cli.forceChannels : channels; auto ImageFormatFromChannels = std::array{ImageFormat::None, ImageFormat::Lum, ImageFormat::LumA, ImageFormat::RGB, ImageFormat::RGBA}; ImageView image{buffer.get(), width, height, ImageFormatFromChannels.at(channels)}; auto barcodes = ReadBarcodes(image.rotated(cli.rotate), options); // if we did not find anything, insert a dummy to produce some output for each file if (barcodes.empty()) barcodes.emplace_back(); allBarcodes.insert(allBarcodes.end(), barcodes.begin(), barcodes.end()); if (filePath == cli.filePaths.back()) { auto merged = MergeStructuredAppendSequences(allBarcodes); // report all merged sequences as part of the last file to make the logic not overly complicated here barcodes.insert(barcodes.end(), std::make_move_iterator(merged.begin()), std::make_move_iterator(merged.end())); } for (auto&& barcode : barcodes) { if (!cli.outPath.empty()) drawRect(image, barcode.position(), bool(barcode.error())); ret |= static_cast(barcode.error().type()); if (cli.bytesOnly) { std::cout.write(reinterpret_cast(barcode.bytes().data()), barcode.bytes().size()); continue; } if (cli.json) { if (barcode.format() != BarcodeFormat::None) std::cout << "{\"FilePath\":\"" << filePath << "\"," << barcode.extra("ALL").substr(1) << "\n"; continue; } if (cli.oneLine) { std::cout << filePath << " " << ToString(barcode.format()); if (barcode.isValid()) std::cout << " \"" << barcode.text(TextMode::Escaped) << "\""; else if (barcode.error()) std::cout << " " << ToString(barcode.error()); std::cout << "\n"; continue; } if (cli.filePaths.size() > 1 || barcodes.size() > 1) { static bool firstFile = true; if (!firstFile) std::cout << "\n"; if (cli.filePaths.size() > 1) std::cout << "File: " << filePath << "\n"; firstFile = false; } if (barcode.format() == BarcodeFormat::None) { std::cout << "No barcode found\n"; continue; } std::cout << "Text: \"" << barcode.text() << "\"\n" << "Bytes: " << barcode.text(options.textMode() == TextMode::ECI ? TextMode::HexECI : TextMode::Hex) << "\n" << "Format: " << ToString(barcode.format()) << "\n" << "Symbology: " << ToString(barcode.symbology()) << "\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"; auto printOptional = [](const char* key, const std::string& v) { if (!v.empty()) std::cout << key << v << "\n"; }; printOptional("ECLevel: ", barcode.ecLevel()); printOptional("Version: ", barcode.version()); printOptional("Error: ", ToString(barcode.error())); if (barcode.lineCount()) std::cout << "Lines: " << barcode.lineCount() << "\n"; if (barcode.symbology() == BarcodeFormat::EANUPC) { printOptional("Country: ", GTIN::LookupCountryIdentifier(barcode.text(), barcode.format())); printOptional("Add-On: ", GTIN::EanAddOn(barcode)); printOptional("Price: ", GTIN::Price(GTIN::EanAddOn(barcode))); printOptional("Issue #: ", GTIN::IssueNr(GTIN::EanAddOn(barcode))); } else if (barcode.format() == BarcodeFormat::ITF && barcode.bytes().size() == 14) { printOptional("Country: ", GTIN::LookupCountryIdentifier(barcode.text(), barcode.format())); } if (barcode.isPartOfSequence()) std::cout << "Structured Append: symbol " << barcode.sequenceIndex() + 1 << " of " << barcode.sequenceSize() << " (parity/id: '" << barcode.sequenceId() << "')\n"; else if (barcode.sequenceSize() > 0) std::cout << "Structured Append: merged result from " << barcode.sequenceSize() << " symbols (parity/id: '" << barcode.sequenceId() << "')\n"; printOptional("Extra: ", barcode.extra()); if (cli.showSymbol && barcode.symbol().data()) std::cout << "Symbol:\n" << WriteBarcodeToUtf8(barcode); } if (cli.filePaths.size() == 1 && !cli.outPath.empty()) stbi_write_png(cli.outPath.c_str(), image.width(), image.height(), 3, image.data(), image.rowStride()); #ifdef NDEBUG if (getenv("MEASURE_PERF")) { auto startTime = std::chrono::high_resolution_clock::now(); auto duration = startTime - startTime; int N = 0; int blockSize = 1; do { for (int i = 0; i < blockSize; ++i) ReadBarcodes(image, options); N += blockSize; duration = std::chrono::high_resolution_clock::now() - startTime; if (blockSize < 1000 && duration < std::chrono::milliseconds(100)) blockSize *= 10; } while (duration < std::chrono::seconds(1)); fprintf(stderr, "time: %5.2f ms per frame\n", double(std::chrono::duration_cast(duration).count()) / N); } #endif } return ret; }