diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a9108c..9f4c6e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,12 @@ cmake_minimum_required(VERSION 3.14...3.25) set (CXX_STANDARD 23) +project( + qr + VERSION 1.1.0 + LANGUAGES CXX +) + if (DEFINED ENV{ARCH}) set(ARCH $ENV{ARCH}) else() @@ -10,12 +16,6 @@ endif() set(EXE_NAME "qr_${CMAKE_PROJECT_VERSION}-${ARCH}") -project( - qr - VERSION 1.0.1 - LANGUAGES CXX -) - message(STATUS "ARCH:: ${ARCH}") configure_file(main.cpp main_substitute.cpp) @@ -27,5 +27,13 @@ set_target_properties(qr PROPERTIES OUTPUT_NAME ${EXE_NAME}) target_compile_options(qr PUBLIC -Wall -std=c++20 -arch ${ARCH}) target_include_directories(qr PRIVATE src lib) +add_custom_target(man ALL) +add_custom_command( + TARGET man + COMMAND help2man -h "-h" -v "-v" -o qr.7 ${EXE_NAME} + COMMAND gzip -f qr.7 +) + install(TARGETS qr RUNTIME DESTINATION bin) -install(PROGRAMS build/${EXE_NAME} DESTINATION bin RENAME qr) \ No newline at end of file +install(PROGRAMS build/${EXE_NAME} DESTINATION bin RENAME qr) +install(FILES build/qr.7.gz DESTINATION /usr/local/share/man/man7/) diff --git a/README.md b/README.md index 9404d84..16a25a1 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,54 @@ ![qr](docs/logo_qr.svg) +## Usage +This software currently supports outputting to the following formats: + +* svg +* PNG +* JPG +* Bitmap + +or directly to the console, as kind of an *"ascii art"*. +after installing via `make install`, you can read more in the man page by calling `man qr`. +### Options +``` + -f --format output file format. can be one of "cli, png, svg, jpg, bmp" + -h --help show this help + -i --input take data from this argument instead of stdin + -o --output output file name without extension + -s --size desired output file size in pixels + -t --type output QR code type. can be one of "small, medium, large" + -v --version shows version info +``` + +### Examples +```bash +bash> qr -i "this is from parameter" -f png -s 512 -o my_qrcode_file + +bash> echo "this is from stdin" | qr -t small -f png -s 512 -o my_qrcode_file_with_low_ecc +``` + +## Installation +You can just pick the newest version from the ["Releases" page][releases], or build the software yourself. + +### Building +You will need: +* cmake +* make +* a modern C++ compiler that supports `c++20` + +```bash +# clone the repo +bash ~/> git clone https://git.mike-ochmann.de/MassiveDynamic/qr.git +# move into the repository +bash ~/> cd qr +# create the build directory and move into it +bash ~/qr> mkdir build && cd build +# run `cmake` +bash ~/qr/build> cmake .. +# make and install the software +bash ~/qr/build> make install +``` + +[releases]: releases/ \ No newline at end of file diff --git a/docs/logo_qr.svg b/docs/logo_qr.svg index 3b3d41a..172ae6b 100644 --- a/docs/logo_qr.svg +++ b/docs/logo_qr.svg @@ -1,231 +1,232 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main.cpp b/main.cpp index 2114099..bcf4613 100644 --- a/main.cpp +++ b/main.cpp @@ -3,6 +3,7 @@ #include #include +#include "qrcodegen.hpp" #include "QR.hpp" #define UNUSED(var) (void) var; @@ -12,20 +13,40 @@ constexpr const char* VERSION = "@qr_VERSION@"; void printHelp() { std::string helpText = R"EOF( -(C) 2023, MikO -– a tool for generating QR codes – -Released under MIT license. - -Usage: - -f --format output file format. can be one of "cli, png, svg" - -h --help show this help - -i --input take data from this argument instead of stdin - -o --output output file name - -s --size desired output file size in pixels - -t --type output QR code type. can be one of "small, medium, large" - -In pixel based renderers (i.e. PNG), the output size may not be exactly as -specified but be the nearest multiple of the actual cell size for the QR code. + +qr is a tool for generating QR codes from the commandline + +Usage: qr [OPTION]... --input "data to encapsulate" + +*Caveats* + With no `--input` parameter defined, read STDIN + + In pixel based renderers (i.e. PNG), the output code may not be exactly + centered, as the amount of segment may not correspond to the specified + output size. + +Options: + -f --format output file format. can be one of "cli, png, svg, jpg, bmp" + -h --help show this help + -i --input take data from this argument instead of stdin + -o --output output file name without extension + -s --size desired output file size in pixels + -t --type output QR code type. can be one of "small, medium, large" + -v --version shows version info + +Examples: + qr -i "this is from parameter" -f png -s 512 -o my_qrcode_file + + echo "this is from stdin" | qr -t small -f png -s 512 -o my_qrcode_file_with_low_ecc + +Copyright: + (C) 2023, MikO + +*License* + Released under MIT license + +Report bugs and issues at out issue tracker: + https://git.mike-ochmann.de/MassiveDynamic/qr/issues )EOF"; std::cout << "qr " << VERSION; std::cout << helpText << std::endl; @@ -37,28 +58,32 @@ int main(int argc, char* argv[]) { exit(1); } const option options[] = { - {"help", no_argument, nullptr, 'h'}, - {"input", required_argument, nullptr, 'i'}, - {"output", required_argument, nullptr, 'o'}, - {"size", required_argument, nullptr, 's'}, - {"type", required_argument, nullptr, 't'}, - {"format", required_argument, nullptr, 'f'}, + {"help", no_argument, nullptr, 'h'}, + {"input", required_argument, nullptr, 'i'}, + {"output", required_argument, nullptr, 'o'}, + {"size", required_argument, nullptr, 's'}, + {"type", required_argument, nullptr, 't'}, + {"format", required_argument, nullptr, 'f'}, + {"version", no_argument, nullptr, 'v'}, {nullptr} }; std::string outputFile; + std::string paramData; size_t segmentSize = 0; - massivedynamic::Type type = massivedynamic::Type::MEDIUM; - massivedynamic::Format format = massivedynamic::Format::CONSOLE; bool fromStdin = true; - std::string paramData = ""; + bool anyParameterSet = false; + qrcodegen::QrCode::Ecc type = qrcodegen::QrCode::Ecc::LOW; + massivedynamic::Format format = massivedynamic::Format::CONSOLE; for(;;) { int index = -1; - int result = getopt_long(argc, argv, "ho:s:t:f:i:", options, &index); + int result = getopt_long(argc, argv, "vho:s:t:f:i:", options, &index); if (result == -1) break; + + anyParameterSet = true; const option* opt = &options[index]; UNUSED(opt); switch(result) { @@ -74,12 +99,13 @@ int main(int argc, char* argv[]) { case 't': { std::string value = optarg; if (value == "small") - type = massivedynamic::Type::SMALL; + type = qrcodegen::QrCode::Ecc::LOW; else if (value == "medium") - type = massivedynamic::Type::MEDIUM; + type = qrcodegen::QrCode::Ecc::MEDIUM; else if (value == "large") - type = massivedynamic::Type::LARGE; + type = qrcodegen::QrCode::Ecc::HIGH; else { + std::cerr << "ERROR: type (-t, --type) has to be one of 'small', 'medium' or 'large'" << std::endl; printHelp(); return 1; } @@ -87,12 +113,17 @@ int main(int argc, char* argv[]) { } case 'f': { std::string value = optarg; + static_assert(massivedynamic::FormatLength == 5, "exhaustive formats: did you miss to add something here?"); if (value == "cli") format = massivedynamic::Format::CONSOLE; else if (value == "svg") format = massivedynamic::Format::SVG; else if (value == "png") format = massivedynamic::Format::PNG; + else if (value == "jpg") + format = massivedynamic::Format::JPG; + else if (value == "bmp") + format = massivedynamic::Format::BMP; else { printHelp(); return 1; @@ -104,6 +135,10 @@ int main(int argc, char* argv[]) { paramData = optarg; break; } + case 'v': { + std::cout << "qr " << VERSION << std::endl; + exit(0); + } case 'h': default: printHelp(); @@ -127,6 +162,8 @@ int main(int argc, char* argv[]) { if (isatty(fileno(stdin)) || data.str().empty()) { std::cerr << "ERROR: no data from stdinput." << std::endl; + if(!anyParameterSet) + printHelp(); exit(1); } } else @@ -142,7 +179,8 @@ int main(int argc, char* argv[]) { return 1; } - std::unique_ptr qr = std::make_unique(data.str(), outputFile, segmentSize, type, format); + std::unique_ptr qr = std::make_unique(data.str(), outputFile, segmentSize, type); + qr->render(format); return 0; -} \ No newline at end of file +} diff --git a/src/QR.cpp b/src/QR.cpp index cee692b..5be5764 100644 --- a/src/QR.cpp +++ b/src/QR.cpp @@ -1,45 +1,38 @@ - #include #include -#include "qrcodegen.hpp" -#include "QR.hpp" -#include "ConsoleRenderer.hpp" -#include "PNGRenderer.hpp" -#include "SVGRenderer.hpp" +#include -namespace massivedynamic { +#include "QR.hpp" -QR::QR(const std::string& data, std::string outputFile, size_t size, Type type, Format format) : outputFile(std::move(outputFile)) { +#include "renderers/ConsoleRenderer.hpp" +#include "renderers/PNGRenderer.hpp" +#include "renderers/SVGRenderer.hpp" +#include "renderers/JPGRenderer.hpp" +#include "renderers/BMPRenderer.hpp" - qrcodegen::QrCode::Ecc errorCorrectionLevel = qrcodegen::QrCode::Ecc::HIGH; +namespace massivedynamic { - switch(type) { - case Type::SMALL: - errorCorrectionLevel = qrcodegen::QrCode::Ecc::LOW; - break; - case Type::MEDIUM: - errorCorrectionLevel = qrcodegen::QrCode::Ecc::MEDIUM; - break; - default: - case Type::LARGE: - errorCorrectionLevel = qrcodegen::QrCode::Ecc::HIGH; - break; +QR::QR(const std::string& data, std::string outputFile, size_t size, qrcodegen::QrCode::Ecc type) : outputFile(std::move(outputFile)), + qr(qrcodegen::QrCode::encodeText(data.c_str(), type)), size(size) { + // this is inherently stupid, but "qrcodegen::QrCode" does not give access to the + // `segments` vector member and the class itself is marked final. + for (int y = 0; y < qr.getSize(); y++) { + for (int x = 0; x < qr.getSize(); x++) + this->pixels.push_back(qr.getModule(x, y)); } - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.c_str(), errorCorrectionLevel); - - this->renderers.insert({Format::CONSOLE, std::make_unique(this->pixels, qr.getSize(), size)}); - this->renderers.insert({Format::PNG, std::make_unique(this->pixels, qr.getSize(), size)}); - this->renderers.insert({Format::SVG, std::make_unique(this->pixels, qr.getSize(), size)}); +} - for (int y = 0; y < qr.getSize(); y++) { - for (int x = 0; x < qr.getSize(); x++) { - bool isFilled = qr.getModule(x, y); - this->pixels.push_back(isFilled ? true : false); - } +void QR::render(Format format) { + static_assert(FormatLength == 5, "exhaustive formats: did you miss to add something here?"); + switch(format) { + case Format::CONSOLE : this->renderTyped(); break; + case Format::PNG : this->renderTyped(); break; + case Format::SVG : this->renderTyped(); break; + case Format::JPG : this->renderTyped(); break; + case Format::BMP : this->renderTyped(); break; + UNUSED_CASE(Format::END); } - - this->renderers.at(format)->render(this->outputFile); } } \ No newline at end of file diff --git a/src/QR.hpp b/src/QR.hpp index 98cb31a..df9f4e7 100644 --- a/src/QR.hpp +++ b/src/QR.hpp @@ -2,33 +2,44 @@ #include #include -#include -#include + #include "qrcodegen.hpp" #include "Renderer.hpp" +#define UNUSED_CASE(name) case name: break; + namespace massivedynamic { typedef uint32_t Color; - enum class Type { - SMALL, - MEDIUM, - LARGE - }; - enum class Format { CONSOLE, SVG, - PNG + PNG, + JPG, + BMP, + END }; + constexpr size_t FormatLength = static_cast(Format::END); class QR { private: std::string outputFile; std::vector pixels; - std::unordered_map> renderers; + qrcodegen::QrCode qr; + size_t size; public: - QR(const std::string& data, std::string outputFile, size_t size, Type type, Format format); + QR(const std::string& data, std::string outputFile, size_t size, qrcodegen::QrCode::Ecc type); + QR(const QR&&) = delete; + QR(QR&) = delete; + ~QR() = default; + + void render(Format format); + + template + void renderTyped() { + T renderer = T(this->pixels, this->qr.getSize(), this->size); + renderer.render(this->outputFile); + } }; } \ No newline at end of file diff --git a/src/Renderer.hpp b/src/Renderer.hpp index b91e954..4f968ec 100644 --- a/src/Renderer.hpp +++ b/src/Renderer.hpp @@ -11,7 +11,10 @@ class Renderer { size_t targetSize; public: Renderer(const std::vector& pixels, size_t sourceSize, size_t targetSize) : pixels(pixels), sourceSize(sourceSize), targetSize(targetSize) {} - virtual ~Renderer() = default; + Renderer(const Renderer&&) = delete; + Renderer(Renderer&) = delete; + virtual ~Renderer() = default; + virtual void render(const std::string& filename) = 0; }; diff --git a/src/renderers/BMPRenderer.hpp b/src/renderers/BMPRenderer.hpp new file mode 100644 index 0000000..0448068 --- /dev/null +++ b/src/renderers/BMPRenderer.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "PixelRenderer.hpp" + +namespace massivedynamic { + +class BMPRenderer final : public PixelRenderer { + public: + BMPRenderer(const std::vector& pixels, size_t sourceSize, size_t targetSize) : PixelRenderer(pixels, sourceSize, targetSize) {} + + virtual void render(const std::string& filename) override { + this->generateBuffer(); + + stbi_write_bmp((filename + ".bmp").c_str(), targetSize, targetSize, 4, this->bitmap.data()); + } + +}; + +} \ No newline at end of file diff --git a/src/ConsoleRenderer.hpp b/src/renderers/ConsoleRenderer.hpp similarity index 96% rename from src/ConsoleRenderer.hpp rename to src/renderers/ConsoleRenderer.hpp index 8896974..1299bad 100644 --- a/src/ConsoleRenderer.hpp +++ b/src/renderers/ConsoleRenderer.hpp @@ -21,7 +21,7 @@ class ConsoleRenderer : public Renderer { std::cout << '\n'; } std::cout << std::string(border, '\n'); - std::cout << std::endl; + std::cout.flush(); } }; diff --git a/src/renderers/JPGRenderer.hpp b/src/renderers/JPGRenderer.hpp new file mode 100644 index 0000000..1d2a63d --- /dev/null +++ b/src/renderers/JPGRenderer.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "PixelRenderer.hpp" + +namespace massivedynamic { + +class JPGRenderer final: public PixelRenderer { + public: + JPGRenderer(const std::vector& pixels, size_t sourceSize, size_t targetSize) : PixelRenderer(pixels, sourceSize, targetSize) {} + + virtual void render(const std::string& filename) override { + this->generateBuffer(); + + stbi_write_jpg((filename + ".jpg").c_str(), targetSize, targetSize, 4, this->bitmap.data(), sizeof(Color) * targetSize); + } + +}; + +} \ No newline at end of file diff --git a/src/PNGRenderer.hpp b/src/renderers/PNGRenderer.hpp similarity index 66% rename from src/PNGRenderer.hpp rename to src/renderers/PNGRenderer.hpp index ddd8b54..25c2875 100644 --- a/src/PNGRenderer.hpp +++ b/src/renderers/PNGRenderer.hpp @@ -6,14 +6,14 @@ namespace massivedynamic { -class PNGRenderer : public PixelRenderer { +class PNGRenderer final: public PixelRenderer { public: PNGRenderer(const std::vector& pixels, size_t sourceSize, size_t targetSize) : PixelRenderer(pixels, sourceSize, targetSize) {} virtual void render(const std::string& filename) override { this->generateBuffer(); - stbi_write_png(filename.c_str(), targetSize, targetSize, 4, this->bitmap.data(), sizeof(Color) * targetSize); + stbi_write_png((filename + ".png").c_str(), targetSize, targetSize, 4, this->bitmap.data(), sizeof(Color) * targetSize); } }; diff --git a/src/PixelRenderer.hpp b/src/renderers/PixelRenderer.hpp similarity index 72% rename from src/PixelRenderer.hpp rename to src/renderers/PixelRenderer.hpp index ec71b6a..e8e1179 100644 --- a/src/PixelRenderer.hpp +++ b/src/renderers/PixelRenderer.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "Renderer.hpp" #define STB_IMAGE_WRITE_IMPLEMENTATION @@ -13,18 +14,16 @@ namespace massivedynamic { class PixelRenderer : public Renderer { protected: size_t pixelSize; + size_t border; std::vector bitmap; void drawPixelScaled(size_t x, size_t y, Color color) { size_t pixelSize = this->pixelSize; - if (pixelSize < 1) { - std::cerr << "ERROR: output file size is too small" << std::endl; - exit(1); - } + assert(pixelSize > 1 && "ERROR: output file size is too small"); - size_t absoluteX = pixelSize * x + pixelSize; - size_t absoluteY = pixelSize * y + pixelSize; + size_t absoluteX = pixelSize * x + this->border; + size_t absoluteY = pixelSize * y + this->border; for (size_t localY = absoluteY; localY < absoluteY + pixelSize; localY++) { for (size_t localX = absoluteX; localX < absoluteX + pixelSize; localX++) { @@ -40,15 +39,13 @@ namespace massivedynamic { PixelRenderer(const std::vector& pixels, size_t sourceSize, size_t targetSize) : Renderer(pixels, sourceSize, targetSize), pixelSize(0) { this->targetSize = this->targetSize == 0 ? (sourceSize + 2) * 2 : this->targetSize; - - // here we make shure, `targetSize` will be a multiple of `sourceSize` - this->pixelSize = round(static_cast(this->targetSize) / static_cast(this->sourceSize + 2)); - this->targetSize = (this->sourceSize + 2) * this->pixelSize; + this->pixelSize = round(static_cast(this->targetSize) / static_cast(this->sourceSize + 2)); + this->border = round(static_cast(this->targetSize - this->pixelSize * this->sourceSize) / 2.0f); this->bitmap = std::vector(this->targetSize * this->targetSize, Colors::WHITE); } - void generateBuffer() { + virtual void generateBuffer() { for (size_t y = 0; y < sourceSize; y++) { for (size_t x = 0; x < sourceSize; x++) { if (!pixels.at(y * sourceSize + x)) diff --git a/src/SVGRenderer.hpp b/src/renderers/SVGRenderer.hpp similarity index 83% rename from src/SVGRenderer.hpp rename to src/renderers/SVGRenderer.hpp index a7a564f..e8c5b16 100644 --- a/src/SVGRenderer.hpp +++ b/src/renderers/SVGRenderer.hpp @@ -17,14 +17,16 @@ namespace massivedynamic { this->targetSize = sourceSize * (2 * SVGRenderer::BORDER_WIDTH); } virtual void render(const std::string& filename) override { - std::stringstream file; + std::ofstream file; + file.open(filename + ".svg"); std::string base = R"EOF( - +targetSize << " " << this->targetSize << "\" xml:space=\"preserve\">" << '\n'; + file << "viewBox=\"0 0 " << this->targetSize << " " << this->targetSize << "\" width=\"" << this->targetSize << "\" height=\"" << this->targetSize << "\" xml:space=\"preserve\">" << '\n'; file << " targetSize << "\" height=\"" << this->targetSize << "\" style=\"fill: white;\" />" << '\n'; float pixelSize = static_cast(this->targetSize) / static_cast(this->sourceSize + 2); @@ -40,12 +42,9 @@ namespace massivedynamic { file << "" << std::endl; - std::ofstream outFile; - outFile.open(filename); - outFile << file.str(); - outFile.close(); + file.close(); } }; -} \ No newline at end of file +}