diff --git a/.clang-format b/.clang-format index a043404..08a6e94 100644 --- a/.clang-format +++ b/.clang-format @@ -41,7 +41,7 @@ BreakBeforeBinaryOperators: None BreakBeforeTernaryOperators: true BreakConstructorInitializers: BeforeColon BreakInheritanceList: BeforeColon -ColumnLimit: 100 +ColumnLimit: 120 CompactNamespaces: false ContinuationIndentWidth: 4 Cpp11BracedListStyle: true diff --git a/.clang-tidy b/.clang-tidy index 63d1f0d..6b7a80a 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,7 +2,6 @@ Checks: "clang-diagnostic-*,clang-analyzer-*,cppcoreguidelines-*,modernize-*,-modernize-use-trailing-return-type" WarningsAsErrors: true HeaderFilterRegex: "" -AnalyzeTemporaryDtors: false FormatStyle: google CheckOptions: - key: cert-dcl16-c.NewSuffixes diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 84aeb11..eb6a4b8 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -4,7 +4,7 @@ "name": "Linux", "compilerPath": "/usr/bin/clang", "cStandard": "c11", - "cppStandard": "c++17", + "cppStandard": "c++20", "compileCommands": "${workspaceFolder}/builddir/compile_commands.json", "browse": { "path": ["${workspaceFolder}"] diff --git a/.vscode/launch.json b/.vscode/launch.json index d5c96ac..d694d44 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,9 +10,21 @@ "request": "launch", "program": "${workspaceFolder}/builddir/ptprnt", "args": [ - "-t Hello" + "-t i" ], - "cwd": "${fileDirname}", + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "setupCommands": [ + { + "description": "Automatische Strukturierung und EinrΓΌckung fΓΌr \"gdb\" aktivieren", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d512f3b..351793d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { - "clangd.arguments": ["-background-index", "-compile-commands-dir=builddir/"], + "clangd.arguments": [ + "-background-index", + "-compile-commands-dir=builddir/" + ], "editor.formatOnType": false, "editor.formatOnSave": true, "files.associations": { @@ -83,8 +86,8 @@ "charconv": "cpp", "*.ipp": "cpp" }, - "clang-tidy.buildPath": "builddir/", "clangd.onConfigChanged": "restart", - "C_Cpp.default.compileCommands": "/home/moritz/src/ptouch-prnt/builddir/compile_commands.json", - "C_Cpp.default.configurationProvider": "mesonbuild.mesonbuild" -} + "cSpell.words": [ + "ptrnt" + ], +} \ No newline at end of file diff --git a/generate_coverage.sh b/generate_coverage.sh index 32b1ef4..0494499 100755 --- a/generate_coverage.sh +++ b/generate_coverage.sh @@ -2,6 +2,7 @@ SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" HTML_COV_PATH="coverageReport/html" +XML_COV_PATH="coverageReport/xml" HTML_START_FILE="index.html" echo "Generating Coverage report for ptouch-prnt" @@ -12,10 +13,15 @@ ninja -C builddir test mkdir -p ${HTML_COV_PATH} gcovr --html --html-details --html-syntax-highlighting --filter src --output ${HTML_COV_PATH}/${HTML_START_FILE} +mkdir -p ${XML_COV_PATH} +gcovr --xml-pretty --filter src --output ${XML_COV_PATH}/cov.xml + if [ $? ] then echo "Coverage report successful generated!" echo "Open: file://${SCRIPT_PATH}/${HTML_COV_PATH}/${HTML_START_FILE}" else echo "Error generating coverage report!" -fi \ No newline at end of file +fi + +rm *.gcov \ No newline at end of file diff --git a/meson.build b/meson.build index fdc0c4a..358f0fb 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,22 @@ -project('ptprnt', 'cpp', - version: 'v0.1.0-'+run_command('git', 'rev-parse', '--short', 'HEAD', check: true).stdout().strip(), +project( + 'ptprnt', + 'cpp', + version: 'v0.1.0-' + run_command( + 'git', + 'rev-parse', + '--short', + 'HEAD', + check: true, + ).stdout().strip(), license: 'GPLv3', - default_options : ['c_std=c11', 'cpp_std=c++17'] + default_options: [ + 'c_std=c11', + 'cpp_std=c++20', + 'b_sanitize=none', + 'b_lto=true', + 'b_lto_mode=thin', + 'b_thinlto_cache=true', + ], ) usb_dep = dependency('libusb-1.0') @@ -20,14 +35,23 @@ incdir = include_directories('src') subdir('src') +# Build arguments +cpp_args = ['-DPROJ_VERSION="' + meson.project_version() + '"'] + +# USB trace mode option (for debugging without sending to hardware) +if get_option('usb_trace_only') + cpp_args += ['-DUSB_TRACE_ONLY'] + message('USB_TRACE_ONLY enabled: USB data will be logged but not sent to device') +endif + ptprnt_exe = executable( - 'ptprnt', + 'ptprnt', 'src/main.cpp', install: true, - dependencies : [usb_dep, log_dep, fmt_dep, pangocairo_dep, cli11_dep], + dependencies: [usb_dep, log_dep, fmt_dep, pangocairo_dep, cli11_dep], include_directories: incdir, sources: [ptprnt_srcs], - cpp_args : ['-DPROJ_VERSION="'+meson.project_version()+'"'], + cpp_args: cpp_args, ) @@ -40,4 +64,4 @@ if not gtest_dep.found() error('MESON_SKIP_TEST: gtest not installed.') endif -subdir('tests') \ No newline at end of file +subdir('tests') diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..9cc746f --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,4 @@ +option('usb_trace_only', + type: 'boolean', + value: false, + description: 'Enable USB trace mode: log USB data without sending to device (saves label tape during debugging)') diff --git a/src/P700Printer.cpp b/src/P700Printer.cpp deleted file mode 100644 index d022084..0000000 --- a/src/P700Printer.cpp +++ /dev/null @@ -1,224 +0,0 @@ -/* - ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - -#include "P700Printer.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "graphics/Bitmap.hpp" -#include "graphics/Image.hpp" -#include "graphics/Monochrome.hpp" -#include "libusb.h" -#include "libusbwrap/LibUsbTypes.hpp" -#include "spdlog/fmt/bin_to_hex.h" - -// as long as DRYRUN is defined, no data is actually send to the printer, we need to save some tape ;) -//#define DRYRUN - -namespace ptprnt::printer { - -const PrinterInfo P700Printer::mInfo = {.driverName = "P700", - .name = "Brother P-touch P700", - .version = "v1.0", - .usbId{0x04f9, 0x2061}}; - -P700Printer::~P700Printer() { - detachUsbDevice(); - if (mUsbHndl) { - mUsbHndl->close(); - } -} - -const std::string_view P700Printer::getDriverName() { - return mInfo.driverName; -} - -const std::string_view P700Printer::getName() { - return mInfo.name; -} - -const std::string_view P700Printer::getVersion() { - return mInfo.version; -} - -const PrinterInfo P700Printer::getPrinterInfo() { - return mInfo; -} - -const PrinterStatus P700Printer::getPrinterStatus() { - using namespace std::chrono_literals; - std::vector getStatusCmd({0x1b, 0x69, 0x53}); // status info request - send(getStatusCmd); - - int tx = 0; - int tries = 0; - std::vector recvBuf(32); - while (tries++ < MAX_TRIES_GET_STATUS) { - std::this_thread::sleep_for(100ms); - mUsbHndl->bulkTransfer(commands["printerinfo"][0], recvBuf, &tx, 0); - } - - return PrinterStatus{.tapeWidthMm = recvBuf[10]}; -} - -const libusbwrap::usbId P700Printer::getUsbId() { - return mInfo.usbId; -} - -bool P700Printer::attachUsbDevice(std::shared_ptr usbHndl) { - if (!usbHndl->open()) { - spdlog::error("Unable to open USB device: {}", usbHndl->getLastErrorString()); - return false; - } - - if (!usbHndl->detachKernelDriver(0)) { - spdlog::error("Device is already in use or couldn't be detached from kernel: {}", - usbHndl->getLastErrorString()); - return false; - } - - if (!usbHndl->claimInterface(0)) { - spdlog::error("Could not claim interface 0: {}", usbHndl->getLastErrorString()); - return false; - } - - mUsbHndl = std::move(usbHndl); - return true; -} - -bool P700Printer::detachUsbDevice() { - if (!mUsbHndl) { - spdlog::warn("No device to detach..."); - return true; - } - if (!mUsbHndl->releaseInterface(0)) { - spdlog::error("Could not release interface 0: {}", mUsbHndl->getLastErrorString()); - return false; - } - return true; -} - -bool P700Printer::printBitmap(const graphics::Bitmap& bitmap) { - auto bm = graphics::Bitmap(512, 128); - { - auto img = graphics::Image(); - bm.setPixels(std::vector(img.getRaw(), img.getRaw() + 512 * 128)); - } - - send(commands["rasterstart"]); - - std::vector rastercmd(4); - rastercmd[0] = 0x47; - rastercmd[1] = 0x00; // size +1 - rastercmd[2] = 0x00; - rastercmd[3] = 0x00; // size -1 - for (unsigned int i = 0; i < bm.getWidth(); i++) { - auto bmcol = bm.getCol(i); - if (!bmcol) { - spdlog::error("Out of bounds bitmap access"); - break; - } - auto monocol = graphics::Monochrome(*bmcol); - auto col = monocol.get(); - - std::vector buf(0); - buf.insert(buf.begin(), rastercmd.begin(), rastercmd.end()); - buf.insert(std::next(buf.begin(), 4), col.begin(), col.end()); - - buf[1] = col.size() + 1; - buf[3] = col.size() - 1; - if (!send(buf)) { - break; - }; - } - - send(commands["eject"]); - return true; -} - -bool P700Printer::setText(const std::string& text) { - return true; -}; - -bool P700Printer::setFont(const std::string& text) { - return true; -}; - -bool P700Printer::setFontSize(uint8_t fontSize) { - return true; -}; - -bool P700Printer::setHAlign(HAlignPosition hpos) { - return true; -}; - -bool P700Printer::setVAlign(VAlignPosition vpos) { - return true; -} - -bool P700Printer::print() { - send(commands["lf"]); - send(commands["ff"]); - send(commands["eject"]); - return true; -} - -bool P700Printer::send(std::vector& data) { - - if (mUsbHndl == nullptr || data.size() > 128) { - spdlog::error("Invalid device handle or invalid data."); - return false; - } - - int tx = 0; - -#ifndef DRYRUN - if (!mUsbHndl->bulkTransfer(0x02, data, &tx, 0)) { - spdlog::error("Error writing command to Printer: {}", mUsbHndl->getLastErrorString()); - return false; - } -#else - tx = data.size(); - spdlog::debug("USB raw data(len {}): {}", data.size(), spdlog::to_hex(data)); -#endif - - if (tx != static_cast(data.size())) { - spdlog::error("Could not transfer all data via USB bulk transfer. Only send {} of {} bytes", - tx, data.size()); - return false; - } - - return true; -} - -bool P700Printer::init() { - std::vector cmd(102); - cmd[100] = 0x1b; /* ESC */ - cmd[101] = 0x40; /* @ */ - return send(cmd); -} -} // namespace ptprnt::printer \ No newline at end of file diff --git a/src/PrinterDriverFactory.cpp b/src/PrinterDriverFactory.cpp index ce34d68..29cd109 100644 --- a/src/PrinterDriverFactory.cpp +++ b/src/PrinterDriverFactory.cpp @@ -1,11 +1,32 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2024-2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + #include "PrinterDriverFactory.hpp" +#include #include #include -#include "P700Printer.hpp" #include "libusbwrap/LibUsbTypes.hpp" +#include "printers/FakePrinter.hpp" +#include "printers/P700Printer.hpp" namespace ptprnt { @@ -20,4 +41,29 @@ std::shared_ptr PrinterDriverFactory::create(libusbwrap::usbId i return nullptr; } +std::shared_ptr PrinterDriverFactory::createFakePrinter() { + spdlog::info("Creating FakePrinter (virtual test printer)"); + return std::make_shared(); +} + +std::shared_ptr PrinterDriverFactory::createByName(const std::string& driverName) { + // Convert to lowercase for case-insensitive comparison + std::string nameLower = driverName; + std::ranges::transform(nameLower, nameLower.begin(), [](unsigned char c) { return std::tolower(c); }); + + if (nameLower == "p700" || nameLower == "p700printer") { + spdlog::info("Creating P700 printer driver by name"); + return std::make_shared(); + } else if (nameLower == "fakeprinter" || nameLower == "fake") { + return createFakePrinter(); + } + + spdlog::warn("Unknown printer driver name: {}", driverName); + return nullptr; +} + +std::vector PrinterDriverFactory::listAllDrivers() const { + return {std::string(printer::P700Printer::mInfo.driverName), std::string(printer::FakePrinter::mInfo.driverName)}; +} + } // namespace ptprnt \ No newline at end of file diff --git a/src/PrinterDriverFactory.hpp b/src/PrinterDriverFactory.hpp index 2d75586..a20cd7b 100644 --- a/src/PrinterDriverFactory.hpp +++ b/src/PrinterDriverFactory.hpp @@ -1,4 +1,26 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2024-2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + #include +#include +#include + #include "interface/IPrinterDriver.hpp" #include "libusbwrap/LibUsbTypes.hpp" @@ -14,9 +36,33 @@ class PrinterDriverFactory { PrinterDriverFactory(PrinterDriverFactory&&) = delete; PrinterDriverFactory& operator=(PrinterDriverFactory&&) = delete; + /** + * @brief Create a printer driver based on USB ID + * @param id USB vendor and product ID + * @return Printer driver instance or nullptr if no match + */ std::shared_ptr create(libusbwrap::usbId id); + /** + * @brief Create a virtual FakePrinter for testing without hardware + * @return FakePrinter instance + */ + std::shared_ptr createFakePrinter(); + + /** + * @brief Create a printer driver by name + * @param driverName Name of the driver (from PrinterInfo.driverName) + * @return Printer driver instance or nullptr if no match + */ + std::shared_ptr createByName(const std::string& driverName); + + /** + * @brief Get list of all available printer driver names + * @return Vector of driver names + */ + std::vector listAllDrivers() const; + private: }; -} \ No newline at end of file +} // namespace ptprnt \ No newline at end of file diff --git a/src/PtouchPrint.cpp b/src/PtouchPrint.cpp index ee08748..f2137f2 100644 --- a/src/PtouchPrint.cpp +++ b/src/PtouchPrint.cpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2025 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,12 +16,10 @@ along with this program. If not, see . */ -#include -#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_TRACE -#define SPDLOG_DEBUG_ON -#define SPDLOG_TRACE_ON +#include "PtouchPrint.hpp" #include +#include #include #include #include @@ -31,14 +29,16 @@ #include #include +#include #include #include +#include #include #include "CLI/Option.hpp" #include "PrinterDriverFactory.hpp" -#include "PtouchPrint.hpp" -#include "graphics/Bitmap.hpp" +#include "graphics/Label.hpp" +#include "graphics/interface/ILabel.hpp" #include "libusbwrap/UsbDeviceFactory.hpp" namespace ptprnt { @@ -55,10 +55,13 @@ int PtouchPrint::init(int argc, char** argv) { return -1; } - if (mVerboseFlag) { + // Set log level based on flags + if (mTraceFlag) { + setupLogger(spdlog::level::trace); + } else if (mVerboseFlag) { setupLogger(spdlog::level::debug); } else { - setupLogger(spdlog::level::err); + setupLogger(spdlog::level::warn); } if (!mUsbDeviceFactory.init()) { @@ -70,66 +73,153 @@ int PtouchPrint::init(int argc, char** argv) { int PtouchPrint::run() { spdlog::info("ptprnt version {}", mVersionString); - SPDLOG_TRACE("testing trace"); - mDetectedPrinters = getCompatiblePrinters(); - auto numFoundPrinters = mDetectedPrinters.size(); - if (numFoundPrinters == 0) { - spdlog::error( - "No compatible printers found, please make sure that they are turned on and connected"); - return -1; - } else if (numFoundPrinters > 1) { - spdlog::warn("Found more than one compatible printer. Currently not supported."); - return -1; + + // Handle --list-all-drivers flag + if (mListDriversFlag) { + auto driverFactory = std::make_unique(); + auto drivers = driverFactory->listAllDrivers(); + + fmt::print("Available printer drivers:\n"); + for (const auto& driver : drivers) { + fmt::print(" - {}\n", driver); + } + fmt::print("\nUse with: -p or --printer \n"); + return 0; } - auto printer = mDetectedPrinters[0]; - const auto printerUsbId = printer->getUsbId(); - auto devices = mUsbDeviceFactory.findDevices(printerUsbId.first, printerUsbId.second); - if (devices.size() != 1) { - spdlog::warn( - "Found more than one device of the same printer on bus. Currently not supported"); - return -1; + // Determine which printer to use + std::shared_ptr printer = nullptr; + + if (mPrinterSelection != "auto") { + // Explicit printer selection by name + auto driverFactory = std::make_unique(); + printer = driverFactory->createByName(mPrinterSelection); + + if (!printer) { + spdlog::error("Failed to create printer driver '{}'", mPrinterSelection); + spdlog::info("Use --list-all-drivers to see available drivers"); + return -1; + } + + spdlog::info("Using explicitly selected printer: {}", mPrinterSelection); + + // FakePrinter doesn't need USB device attachment + if (mPrinterSelection == "FakePrinter" || mPrinterSelection == "fake") { + printer->attachUsbDevice(nullptr); + } else { + // Real printer needs USB device + const auto printerUsbId = printer->getUsbId(); + auto devices = mUsbDeviceFactory.findDevices(printerUsbId.first, printerUsbId.second); + + if (devices.empty()) { + spdlog::error("No USB device found for printer {}. Is it connected and powered on?", mPrinterSelection); + return -1; + } + + if (devices.size() > 1) { + spdlog::warn("Found more than one device of the same printer on bus. Using first one."); + } + + if (!printer->attachUsbDevice(std::move(devices[0]))) { + spdlog::error("Failed to attach USB device to printer"); + return -1; + } + } + } else { + // Auto-detect printer from USB devices + mDetectedPrinters = getCompatiblePrinters(); + auto numFoundPrinters = mDetectedPrinters.size(); + + if (numFoundPrinters == 0) { + spdlog::error("No compatible printers found, please make sure that they are turned on and connected"); + spdlog::info("Tip: Use -p FakePrinter for testing without hardware"); + return -1; + } else if (numFoundPrinters > 1) { + spdlog::warn("Found more than one compatible printer. Use -p to select explicitly."); + return -1; + } + + printer = mDetectedPrinters[0]; + const auto printerUsbId = printer->getUsbId(); + auto devices = mUsbDeviceFactory.findDevices(printerUsbId.first, printerUsbId.second); + + if (devices.size() != 1) { + spdlog::warn("Found more than one device of the same printer on bus. Currently not supported"); + return -1; + } + + if (!printer->attachUsbDevice(std::move(devices[0]))) { + spdlog::error("Failed to attach USB device to printer"); + return -1; + } } - printer->attachUsbDevice(devices[0]); + auto status = printer->getPrinterStatus(); spdlog::info("Detected tape width is {}mm", status.tapeWidthMm); - auto bm = ptprnt::graphics::Bitmap(512, 128); - //printer->printBitmap(bm); - //printer->printText("wurst", 1); - for (auto& cmd : mCommands) { - switch (cmd.first) { + if (0 == mCommands.size()) { + spdlog::warn("No command specified, nothing to do..."); + return 0; + } + + auto label = std::make_unique(printer->getPrinterInfo().pixelLines); + std::string labelText{}; + // TODO: refactor + for (const auto& [cmd, value] : mCommands) { + switch (cmd) { case CliCmdType::Text: - spdlog::debug("Setting text to {}", cmd.second); - printer->setText(cmd.second); + if (labelText.empty()) { + labelText = value; + } else { + labelText = labelText + '\n' + value; + } break; - case CliCmdType::Font:; - spdlog::debug("Setting font to {}", cmd.second); - printer->setFont(cmd.second); + case CliCmdType::Font: + spdlog::debug("Setting font to {}", value); + label->setFontFamily(value); break; - case CliCmdType::FontSize:; - spdlog::debug("Setting font size to {}", cmd.second); - printer->setFontSize(static_cast(std::atoi(cmd.second.c_str()))); + case CliCmdType::FontSize: + spdlog::debug("Setting font size to {}", std::stod(value)); + label->setFontSize(std::stod(value)); break; - case CliCmdType::HAlign:; - spdlog::debug("[Not implemented] Setting text horizontal alignment to {}", - cmd.second); + case CliCmdType::HAlign: + spdlog::debug("Setting text horizontal alignment to {}", value); + { + auto hPos = HALignPositionMap.find(value); + if (hPos == HALignPositionMap.end()) { + spdlog::warn("Invalid horizontal alignment specified!"); + label->setHAlign(HAlignPosition::UNKNOWN); + } else { + label->setHAlign(hPos->second); + } + } break; - case CliCmdType::VAlign:; - spdlog::debug("[Not implemented] Setting text vertical alignment to {}", - cmd.second); + case CliCmdType::VAlign: + spdlog::debug("Setting text vertical alignment to {}", value); + { + auto vPos = VALignPositionMap.find(value); + if (vPos == VALignPositionMap.end()) { + spdlog::warn("Invalid verical alignment specified!"); + label->setVAlign(VAlignPosition::UNKNOWN); + } else { + label->setVAlign(vPos->second); + } + } break; - case CliCmdType::None:; + case CliCmdType::None: [[fallthrough]]; default: spdlog::warn("This command is currently not supported."); break; } } - if (!printer->print()) { + label->create(labelText); + label->writeToPng("./testlabel.png"); + if (!printer->printLabel(std::move(label))) { spdlog::error("An error occured while printing"); return -1; } + return 0; } @@ -139,7 +229,7 @@ std::vector> PtouchPrint::getCompatiblePrinters( auto driverFactory = std::make_unique(); std::vector> foundPrinterDrivers{}; - for (auto usbDev : usbDevs) { + for (auto& usbDev : usbDevs) { auto driver = driverFactory->create(usbDev->getUsbId()); if (driver != nullptr) { foundPrinterDrivers.push_back(driver); @@ -151,7 +241,13 @@ std::vector> PtouchPrint::getCompatiblePrinters( void PtouchPrint::setupLogger(spdlog::level::level_enum lvl) { auto consoleSink = std::make_shared(); consoleSink->set_level(lvl); - consoleSink->set_pattern("%^%L:%$ %v"); + if (spdlog::level::level_enum::debug == lvl || spdlog::level::level_enum::trace == lvl) { + // This will enable file and line number for debug and trace macros + // TODO: line number and functions only work with macros + consoleSink->set_pattern("%^%L:%$ %v"); + } else { + consoleSink->set_pattern("%^%L:%$ %v"); + } auto fileSink = std::make_shared("ptprnt.log", true); fileSink->set_level(spdlog::level::trace); @@ -162,6 +258,7 @@ void PtouchPrint::setupLogger(spdlog::level::level_enum lvl) { spdlog::set_default_logger(logger); } +// TODO: CLI parsing should be a seperate class/file void PtouchPrint::setupCliParser() { auto printVersion = [this](std::size_t) { fmt::print("ptprnt version: {}\n", mVersionString); @@ -169,8 +266,16 @@ void PtouchPrint::setupCliParser() { // General options mApp.add_flag("-v,--verbose", mVerboseFlag, "Enable verbose output"); + mApp.add_flag("--trace", mTraceFlag, "Enable trace output (shows USB communication)"); mApp.add_flag("-V,--version", printVersion, "Prints the ptprnt's version"); + // Printer selection + mApp.add_option("-p,--printer", mPrinterSelection, + "Select printer driver (default: auto). Use --list-all-drivers to see available options") + ->default_val("auto"); + + mApp.add_flag("--list-all-drivers", mListDriversFlag, "List all available printer drivers and exit"); + // Text printing options mApp.add_option("-t,--text", "Text to print (can be used multple times, use formatting options before to " @@ -181,27 +286,42 @@ void PtouchPrint::setupCliParser() { ->each([this](std::string text) { mCommands.emplace_back(CliCmdType::Text, text); }); mApp.add_option("-f,--font", "Font used for the following text occurences") ->group("Text printing ") - ->multi_option_policy(CLI::MultiOptionPolicy::TakeAll) + ->multi_option_policy(CLI::MultiOptionPolicy::TakeFirst) ->trigger_on_parse() ->each([this](std::string font) { mCommands.emplace_back(CliCmdType::Font, font); }); mApp.add_option("-s,--fontsize", "Font size of the following text occurences") ->group("Text printing ") - ->multi_option_policy(CLI::MultiOptionPolicy::TakeAll) + ->multi_option_policy(CLI::MultiOptionPolicy::TakeFirst) ->trigger_on_parse() ->each([this](std::string size) { mCommands.emplace_back(CliCmdType::FontSize, size); }); mApp.add_option("--valign", "Vertical alignment of the following text occurences") ->group("Text printing ") - ->multi_option_policy(CLI::MultiOptionPolicy::TakeAll) + ->multi_option_policy(CLI::MultiOptionPolicy::TakeFirst) ->trigger_on_parse() + ->transform([](std::string in) -> std::string { + std::unordered_set validValignOptions{"top", "middle", "bottom"}; + std::ranges::transform(in, in.begin(), [](unsigned char c) { return std::tolower(c); }); + if (validValignOptions.find(in) == validValignOptions.end()) { + return {""}; + } + return in; + }) ->each([this](std::string valign) { mCommands.emplace_back(CliCmdType::VAlign, valign); }); mApp.add_option("--halign", "Vertical alignment of the following text occurences") ->group("Text printing ") - ->multi_option_policy(CLI::MultiOptionPolicy::TakeAll) + ->multi_option_policy(CLI::MultiOptionPolicy::TakeFirst) ->trigger_on_parse() + ->transform([](std::string in) -> std::string { + std::unordered_set validValignOptions{"left", "center", "right", "justify"}; + std::transform(in.begin(), in.end(), in.begin(), [](unsigned char c) { return std::tolower(c); }); + if (validValignOptions.find(in) == validValignOptions.end()) { + return {""}; + } + return in; + }) ->each([this](std::string halign) { mCommands.emplace_back(CliCmdType::HAlign, halign); }); // Image options - mApp.add_option("-i,--image", "Image to print. Excludes all text printing ") - ->group("Image printing"); + mApp.add_option("-i,--image", "Image to print. Excludes all text printing ")->group("Image printing"); } } // namespace ptprnt \ No newline at end of file diff --git a/src/PtouchPrint.hpp b/src/PtouchPrint.hpp index 75b04b3..911eea8 100644 --- a/src/PtouchPrint.hpp +++ b/src/PtouchPrint.hpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2025 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -58,7 +58,10 @@ class PtouchPrint { std::vector mCommands{}; std::string mVersionString = ""; - // CLI flags + // CLI flags and options bool mVerboseFlag = false; + bool mTraceFlag = false; + std::string mPrinterSelection = "auto"; + bool mListDriversFlag = false; }; } // namespace ptprnt \ No newline at end of file diff --git a/src/graphics/Bitmap.cpp b/src/graphics/Bitmap.cpp index bffb917..9b29204 100644 --- a/src/graphics/Bitmap.cpp +++ b/src/graphics/Bitmap.cpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2025 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,32 +19,30 @@ #include "Bitmap.hpp" -#include #include -#include -#include +#include +#include #include namespace ptprnt::graphics { template -Bitmap::Bitmap(uint16_t width, uint16_t height) - : mWidth{width}, mHeight{height}, mPixels(width * height) {} +Bitmap::Bitmap(uint16_t width, uint16_t height) : mWidth{width}, mHeight{height}, mPixels(width * height) {} template -uint16_t Bitmap::getWidth() { +[[nodiscard]] uint16_t Bitmap::getWidth() const { return mWidth; } template -uint16_t Bitmap::getHeight() { +[[nodiscard]] uint16_t Bitmap::getHeight() const { return mHeight; } template bool Bitmap::setPixels(const std::vector& pixels) { if (pixels.size() != mPixels.size()) { - spdlog::error("Invalid pixel buffer size."); + spdlog::error("Invalid pixel buffer size (got {} vs. {} bitmap size).", pixels.size(), mPixels.size()); return false; } @@ -53,39 +51,34 @@ bool Bitmap::setPixels(const std::vector& pixels) { } template -std::vector Bitmap::getPixelsCpy() { +[[nodiscard]] std::vector Bitmap::getPixelsCpy() const { return mPixels; } template -std::optional> Bitmap::getLine(uint16_t line) { - if (line >= mHeight) { - // out of bound - return std::nullopt; +[[nodiscard]] std::vector Bitmap::getLine(const uint16_t lineNo) const { + if (lineNo >= mHeight) { + throw(std::out_of_range("Line is out of range!")); } - - auto lineStart = mPixels.begin() + (line * mWidth); - auto lineEnd = mPixels.begin() + ((line + 1) * mWidth); + auto lineStart = mPixels.begin() + (lineNo * mWidth); + auto lineEnd = mPixels.begin() + ((lineNo + 1) * mWidth); return std::vector(lineStart, lineEnd); } +// TODO: I guess this is borked template -std::optional> Bitmap::getCol(uint16_t col) { - if (col >= mWidth) { - // out of bound - return std::nullopt; +[[nodiscard]] std::vector Bitmap::getCol(const uint16_t colNo) const { + if (colNo >= mWidth) { + throw(std::out_of_range("Col is out of range!")); } - // first pixel is always beginning of the col - std::vector colPixels(mHeight); - auto it = std::next(mPixels.begin(), col); - - for (auto& colElement : colPixels) { - colElement = *it; - std::advance(it, mWidth); + std::vector col{}; + col.reserve(mHeight); + for (size_t i{0}; i <= mPixels.size(); i++) { + if (i % mWidth == colNo) { + col.push_back(mPixels[i]); + } } - - return colPixels; + return col; } - } // namespace ptprnt::graphics \ No newline at end of file diff --git a/src/graphics/Bitmap.hpp b/src/graphics/Bitmap.hpp index 16325e8..09fefc5 100644 --- a/src/graphics/Bitmap.hpp +++ b/src/graphics/Bitmap.hpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2025 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,19 +19,17 @@ #pragma once -#include - #include #include -#include +#include #include namespace ptprnt::graphics { -typedef uint8_t ALPHA8; // Alpha only, 8 bit per pixel -typedef uint32_t RGBX8; // RGB, least significant byte unused, 8 bit per channel -typedef uint32_t RGBA8; // RGB, least significant byte alpha, 8 bit per channel -typedef uint32_t ARGB8; // RGB, most significant byte alpha, 8 bit per channel +using ALPHA8 = std::uint8_t; // Alpha only, 8 bit per pixel +using RGBX8 = std::uint32_t; // RGB, least significant byte unused, 8 bit per channel +using RGBA8 = std::uint32_t; // RGB, least significant byte alpha, 8 bit per channel +using ARGB8 = std::uint32_t; // RGB, most significant byte alpha, 8 bit per channel template class Bitmap { @@ -39,12 +37,18 @@ class Bitmap { Bitmap(uint16_t width, uint16_t height); ~Bitmap() = default; - uint16_t getWidth(); - uint16_t getHeight(); + Bitmap(const Bitmap&) = default; + Bitmap& operator=(const Bitmap&) = default; + Bitmap(Bitmap&&) = default; + Bitmap& operator=(Bitmap&&) = default; + + [[nodiscard]] uint16_t getWidth() const; + [[nodiscard]] uint16_t getHeight() const; bool setPixels(const std::vector& pixels); - std::vector getPixelsCpy(); - std::optional> getLine(uint16_t line); - std::optional> getCol(uint16_t col); + [[nodiscard]] std::vector getPixelsCpy() const; + [[nodiscard]] std::vector getLine(uint16_t line) const; + [[nodiscard]] std::vector getCol(uint16_t col) const; + void visualize() const; private: uint16_t mWidth; diff --git a/src/graphics/Image.cpp b/src/graphics/Image.cpp deleted file mode 100644 index c28e8c0..0000000 --- a/src/graphics/Image.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - -#include "Image.hpp" - -#include - -#include "pango/pango-font.h" - -namespace ptprnt::graphics { -Image::Image() { - mSurface = cairo_image_surface_create(CAIRO_FORMAT_A8, 512, 128); - cairo_t* cr = cairo_create(mSurface); - mFontDescription = pango_font_description_new(); - pango_font_description_set_family(mFontDescription, "sans"); - pango_font_description_set_weight(mFontDescription, PANGO_WEIGHT_SEMIBOLD); - pango_font_description_set_size(mFontDescription, 60 * PANGO_SCALE); - - std::string printThis("Mist πŸ’©"); - - mLayout = pango_cairo_create_layout(cr); - pango_layout_set_font_description(mLayout, mFontDescription); - pango_layout_set_text(mLayout, printThis.c_str(), -1); - - cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); - cairo_move_to(cr, 0.0, 94.0); - pango_cairo_show_layout_line(cr, pango_layout_get_line(mLayout, 0)); - - cairo_surface_write_to_png(mSurface, "hello.png"); -} - -uint8_t* Image::getRaw() { - cairo_surface_flush(mSurface); - return cairo_image_surface_get_data(mSurface); -} - -Image::~Image() { - g_object_unref(mLayout); - pango_font_description_free(mFontDescription); -} -} // namespace ptprnt::graphics \ No newline at end of file diff --git a/src/graphics/Image.hpp b/src/graphics/Image.hpp deleted file mode 100644 index 2302787..0000000 --- a/src/graphics/Image.hpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - */ - -#pragma once - -#include - -#include - -namespace ptprnt::graphics { -class Image { - public: - Image(); - ~Image(); - uint8_t* getRaw(); - - private: - PangoLayout* mLayout; - PangoFontDescription* mFontDescription; - cairo_surface_t* mSurface; -}; -} // namespace ptprnt::graphics \ No newline at end of file diff --git a/src/graphics/Label.cpp b/src/graphics/Label.cpp new file mode 100644 index 0000000..c4d112f --- /dev/null +++ b/src/graphics/Label.cpp @@ -0,0 +1,229 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2023-2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#include "graphics/Label.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +#include "cairo.h" +#include "graphics/interface/ILabel.hpp" +#include "pango/pango-font.h" +#include "pango/pango-layout.h" +#include "pango/pango-types.h" +#include "pango/pangocairo.h" + +namespace ptprnt::graphics { +Label::Label(const uint16_t heightPixel) + : mPrinterHeight(heightPixel) { + // Initialize resources in correct order with RAII + mFontMap.reset(pango_cairo_font_map_new()); +} + +std::vector Label::getRaw() { + assert(mSurface != nullptr); + auto* surface = mSurface.get(); + + cairo_surface_flush(surface); + assert(cairo_image_surface_get_format(surface) == CAIRO_FORMAT_A8); + + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + int stride = cairo_image_surface_get_stride(surface); + + spdlog::debug("Cairo Surface data: W: {}; H: {}; S:{}", width, height, stride); + + auto data = cairo_image_surface_get_data(surface); + + // If stride equals width, we can return data directly + if (stride == width) { + size_t len = height * stride; + return {data, data + len}; + } + + // Otherwise, we need to copy row by row, removing stride padding + std::vector result; + result.reserve(width * height); + + for (int y = 0; y < height; ++y) { + uint8_t* row_start = data + (y * stride); + result.insert(result.end(), row_start, row_start + width); + } + + spdlog::debug("getRaw: Removed stride padding, returning {} bytes ({}x{})", result.size(), width, height); + return result; +} + +uint8_t Label::getNumLines(std::string_view strv) { + return std::count(strv.begin(), strv.end(), '\n'); +} + +int Label::getWidth() { + // Return the actual Cairo surface width (which is the layout width) + return mLayoutWidth; +} + +int Label::getHeight() { + // Return the actual Cairo surface height (which is the printer height) + return mPrinterHeight; +} + +void Label::configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc) { + pango_layout_set_font_description(layout, fontDesc); + pango_layout_set_text(layout, text.c_str(), static_cast(text.length())); + pango_layout_set_height(layout, getNumLines(text) * -1); +} + +void Label::applyHorizontalAlignment(PangoLayout* layout) { + switch (mHAlign) { + case HAlignPosition::LEFT: + pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); + break; + case HAlignPosition::RIGHT: + pango_layout_set_alignment(layout, PANGO_ALIGN_RIGHT); + break; + case HAlignPosition::JUSTIFY: + pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); + pango_layout_set_justify(layout, true); +#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50 + pango_layout_set_justify_last_line(layout, true); +#endif + break; + case HAlignPosition::CENTER: + [[fallthrough]]; + default: + pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); + break; + } +} + +bool Label::create(PrintableText printableText) { + setFontFamily(printableText.fontFamily); + setFontSize(printableText.fontSize); + + return create(printableText.text); +} + +bool Label::create(const std::string& labelText) { + // TODO: we need to create a custom font config here so that Noto Emoji does not load the systems default + // font config here. For this, we need to create a PangoFcFontMap and a custom FcConfig + // see: https://docs.gtk.org/PangoFc/method.FontMap.set_config.html + // see: https://gist.github.com/CallumDev/7c66b3f9cf7a876ef75f + + // Create a temporary surface for layout size calculations + auto* tempSurface = cairo_image_surface_create(CAIRO_FORMAT_A8, 1, 1); + auto* tempCr = cairo_create(tempSurface); + auto* tempPangoCtx = pango_cairo_create_context(tempCr); + auto* tempPangoLyt = pango_layout_new(tempPangoCtx); + + PangoFontDescription* regularFont = pango_font_description_new(); + pango_font_description_set_size(regularFont, static_cast(mFontSize * PANGO_SCALE)); + pango_font_description_set_family(regularFont, mFontFamily.c_str()); + + // Configure temporary layout for size calculation + configureLayout(tempPangoLyt, labelText, regularFont); + applyHorizontalAlignment(tempPangoLyt); + + // Calculate label size from temporary layout + pango_layout_get_size(tempPangoLyt, &mLayoutWidth, &mLayoutHeight); + mLayoutWidth /= PANGO_SCALE; + mLayoutHeight /= PANGO_SCALE; + + spdlog::debug("Layout width: {}, height: {}", mLayoutWidth, mLayoutHeight); + //auto alignedWidth = mLayoutWidth + (8 - (mLayoutWidth % 8)); + //spdlog::debug("Aligned Layout width: {}, height: {}", alignedWidth, mLayoutHeight); + + // Clean up temporary resources + g_object_unref(tempPangoLyt); + g_object_unref(tempPangoCtx); + cairo_destroy(tempCr); + cairo_surface_destroy(tempSurface); + + // Now create the final surface and Pango context for actual rendering + mSurface.reset(cairo_image_surface_create(CAIRO_FORMAT_A8, mLayoutWidth, mPrinterHeight)); + cairo_t* cr = cairo_create(mSurface.get()); + mCairoCtx.reset(cr); + mPangoCtx.reset(pango_cairo_create_context(cr)); + mPangoLyt.reset(pango_layout_new(mPangoCtx.get())); + + // Configure final layout with same settings + configureLayout(mPangoLyt.get(), labelText, regularFont); + applyHorizontalAlignment(mPangoLyt.get()); + + // Adjust Cairo cursor position to respect the vertical alignment + switch (mVAlign) { + case VAlignPosition::TOP: + break; + case VAlignPosition::BOTTOM: + cairo_move_to(mCairoCtx.get(), 0.0, mPrinterHeight - mLayoutHeight); + break; + case VAlignPosition::MIDDLE: + cairo_move_to(mCairoCtx.get(), 0.0, (mPrinterHeight - mLayoutHeight) / 2); + break; + default: + break; + } + + // Finally show the layout on the Cairo surface + pango_cairo_show_layout(mCairoCtx.get(), mPangoLyt.get()); + + cairo_set_source_rgb(mCairoCtx.get(), 0.0, 0.0, 0.0); + cairo_surface_flush(mSurface.get()); + // mCairoCtx smart pointer will handle cleanup + return true; +} + +void Label::writeToPng(const std::string& file) { + if (mSurface) { + cairo_surface_flush(mSurface.get()); + cairo_surface_write_to_png(mSurface.get(), file.c_str()); + } +} + +void Label::setFontSize(const double fontSize) { + mFontSize = fontSize; +} + +void Label::setFontFamily(const std::string& fontFamily) { + mFontFamily = fontFamily; +} + +void Label::setHAlign(HAlignPosition hAlign) { + mHAlign = hAlign; +} + +void Label::setVAlign(VAlignPosition vAlign) { + mVAlign = vAlign; +} + +void Label::setText(const std::string& text) { + mText = text; +} + +Label::~Label() { + spdlog::debug("Image dtor..."); + // RAII smart pointers handle cleanup automatically +} +} // namespace ptprnt::graphics \ No newline at end of file diff --git a/src/graphics/Label.hpp b/src/graphics/Label.hpp new file mode 100644 index 0000000..cee53e3 --- /dev/null +++ b/src/graphics/Label.hpp @@ -0,0 +1,100 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2023-2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "cairo.h" +#include "graphics/interface/ILabel.hpp" +#include "pango/pango-types.h" + +namespace ptprnt::graphics { + +// Custom deleters for Cairo/Pango resources +struct CairoSurfaceDeleter { + void operator()(cairo_surface_t* surface) const { + if (surface) + cairo_surface_destroy(surface); + } +}; + +struct CairoDeleter { + void operator()(cairo_t* cr) const { + if (cr) + cairo_destroy(cr); + } +}; + +struct GObjectDeleter { + void operator()(gpointer obj) const { + if (obj) + g_object_unref(obj); + } +}; + +class Label : public ILabel { + public: + Label(const uint16_t heightPixel); + ~Label() override; + + Label(const Label&) = delete; + Label& operator=(const Label&) = delete; + Label(Label&&) = delete; + Label& operator=(Label&&) = delete; + + bool create(PrintableText printableText) override; + bool create(const std::string& labelText) override; + void writeToPng(const std::string& file); + [[nodiscard]] int getWidth() override; + [[nodiscard]] int getHeight() override; + [[nodiscard]] std::vector getRaw() override; + void setFontSize(const double fontSize) override; + void setFontFamily(const std::string& fontFamily) override; + + void setText(const std::string& text) override; + void setHAlign(HAlignPosition hpos) override; + void setVAlign(VAlignPosition vpos) override; + + private: + // methods + [[nodiscard]] uint8_t getNumLines(std::string_view str); + [[nodiscard]] PangoFontMap* createCustomFontMap(); + void configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc); + void applyHorizontalAlignment(PangoLayout* layout); + + std::unique_ptr mSurface{nullptr}; + std::unique_ptr mCairoCtx{nullptr}; + std::unique_ptr mPangoCtx{nullptr}; + std::unique_ptr mPangoLyt{nullptr}; + std::unique_ptr mFontMap{nullptr}; + double mFontSize{DEFAULT_FONT_SIZE}; + std::string mFontFamily{DEFAULT_FONT_FAMILY}; + HAlignPosition mHAlign = HAlignPosition::LEFT; + VAlignPosition mVAlign = VAlignPosition::MIDDLE; + std::string mText{""}; + int mLayoutWidth = 0, mLayoutHeight = 0; + int mPrinterHeight = 0; +}; +} // namespace ptprnt::graphics \ No newline at end of file diff --git a/src/graphics/Monochrome.cpp b/src/graphics/Monochrome.cpp index 596ccbc..07f1100 100644 --- a/src/graphics/Monochrome.cpp +++ b/src/graphics/Monochrome.cpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2025 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,39 +21,239 @@ #include -#include +#include #include namespace ptprnt::graphics { -Monochrome::Monochrome(const std::vector& grayscale) : mPixels(std::move(grayscale)) {} +// Constructor from grayscale data +MonochromeData::MonochromeData(const std::vector& grayscale, uint32_t width, uint32_t height, + Orientation orient) + : stride(0), + orientation(orient), + width(width), + height(height), + mPixels(grayscale), + mIsProcessed(false) {} -void Monochrome::setThreshold(uint8_t threshhold) { - mThreshhold = threshhold; +MonochromeData::MonochromeData(const std::span grayscale, uint32_t width, uint32_t height, + Orientation orient) + : stride(0), + orientation(orient), + width(width), + height(height), + mPixels(grayscale.begin(), grayscale.end()), + mIsProcessed(false) {} + +void MonochromeData::setThreshold(uint8_t threshold) { + mThreshold = threshold; + mIsProcessed = false; // Mark as needing reprocessing } -void Monochrome::invert(bool shouldInvert) { +void MonochromeData::invert(bool shouldInvert) { mShouldInvert = shouldInvert; + mIsProcessed = false; // Mark as needing reprocessing } -std::vector Monochrome::get() { - std::vector outPixels( - (static_cast((mPixels.size() / 8)) + (std::floor(mPixels.size() % 8 + 0.9)))); +MonochromeData MonochromeData::get() { + if (!mIsProcessed) { + processGrayscaleToMonochrome(); + mIsProcessed = true; + } - unsigned int outIndex = 0; + // Return a copy of the processed data + MonochromeData result; + result.bytes = bytes; + result.stride = stride; + result.orientation = orientation; + result.width = width; + result.height = height; + result.mIsProcessed = true; + return result; +} - for (unsigned int byteNum = 0; byteNum < mPixels.size(); byteNum += 8) { - for (unsigned int bitNo = 0; bitNo < 8; bitNo++) { - if (mPixels[byteNum + bitNo] > mThreshhold) { - outPixels[outIndex] |= (1 << (7 - bitNo)); - } else { - outPixels[outIndex] &= ~(1 << (7 - bitNo)); +void MonochromeData::processGrayscaleToMonochrome() { + // Calculate stride based on packed monochrome data (1 bit per pixel, 8 pixels per byte) + stride = static_cast((width + 7) / 8); + + // Create the monochrome byte array + bytes.clear(); + bytes.resize(stride * height, 0); + + // Convert grayscale to monochrome + for (uint32_t y = 0; y < height; ++y) { + for (uint32_t x = 0; x < width; ++x) { + uint32_t pixelIndex = y * width + x; + if (pixelIndex < mPixels.size()) { + uint8_t pixelValue = mPixels[pixelIndex]; + + // Apply threshold + bool isSet = pixelValue >= mThreshold; + + // Apply inversion if needed + if (mShouldInvert) { + isSet = !isSet; + } + + // Set the bit in the monochrome data + if (isSet) { + setBit(x, y, true); + } } } - if (mShouldInvert) { - outPixels[outIndex] = ~outPixels[outIndex]; - } - outIndex++; } - return outPixels; } + +// Transformation methods implementation +void MonochromeData::transformTo(Orientation targetOrientation) { + if (orientation == targetOrientation) { + return; // No transformation needed + } + + auto rotatedData = createRotatedData(targetOrientation); + bytes = std::move(rotatedData); + + // Update dimensions and stride based on rotation + switch (targetOrientation) { + case Orientation::PORTRAIT: + case Orientation::PORTRAIT_FLIPPED: + // Swap width and height for portrait orientations + std::swap(width, height); + stride = (width + 7) / 8; // Recalculate stride for new width + break; + case Orientation::LANDSCAPE: + case Orientation::LANDSCAPE_FLIPPED: + // Keep original stride calculation + stride = (width + 7) / 8; + break; + } + + orientation = targetOrientation; +} + +bool MonochromeData::getBit(uint32_t x, uint32_t y) const { + if (x >= width || y >= height) { + return false; + } + + uint32_t byteIndex = y * stride + x / 8; + uint32_t bitIndex = 7 - (x % 8); // MSB first + + if (byteIndex >= bytes.size()) { + return false; + } + + return (bytes[byteIndex] >> bitIndex) & 1; +} + +void MonochromeData::setBit(uint32_t x, uint32_t y, bool value) { + if (x >= width || y >= height) { + return; + } + + uint32_t byteIndex = y * stride + x / 8; + uint32_t bitIndex = 7 - (x % 8); // MSB first + + if (byteIndex >= bytes.size()) { + return; + } + + if (value) { + bytes[byteIndex] |= (1 << bitIndex); + } else { + bytes[byteIndex] &= ~(1 << bitIndex); + } +} + +std::vector MonochromeData::createRotatedData(Orientation targetOrientation) const { + uint32_t newWidth = 0, newHeight = 0; + + // Determine new dimensions + switch (targetOrientation) { + case Orientation::PORTRAIT: + case Orientation::PORTRAIT_FLIPPED: + newWidth = height; + newHeight = width; + break; + case Orientation::LANDSCAPE: + case Orientation::LANDSCAPE_FLIPPED: + default: + newWidth = width; + newHeight = height; + break; + } + + uint32_t newStride = (newWidth + 7) / 8; + std::vector newBytes(newStride * newHeight, 0); + + // Create a temporary MonochromeData for the new image + MonochromeData tempData; + tempData.bytes = std::move(newBytes); + tempData.stride = newStride; + tempData.width = newWidth; + tempData.height = newHeight; + tempData.orientation = targetOrientation; + + // Copy pixels with appropriate transformation + for (uint32_t y = 0; y < height; ++y) { + for (uint32_t x = 0; x < width; ++x) { + bool pixel = getBit(x, y); + uint32_t newX = 0, newY = 0; + + switch (targetOrientation) { + case Orientation::LANDSCAPE: + newX = x; + newY = y; + break; + case Orientation::PORTRAIT: // 90 degrees clockwise + newX = height - 1 - y; + newY = x; + break; + case Orientation::LANDSCAPE_FLIPPED: // 180 degrees + newX = width - 1 - x; + newY = height - 1 - y; + break; + case Orientation::PORTRAIT_FLIPPED: // 270 degrees clockwise + newX = y; + newY = width - 1 - x; + break; + } + + tempData.setBit(newX, newY, pixel); + } + } + + return std::move(tempData.bytes); +} + +void MonochromeData::visualize() const { + std::cout << "MonochromeData visualization (" << width << "x" << height << ", orientation: "; + + switch (orientation) { + case Orientation::LANDSCAPE: + std::cout << "LANDSCAPE"; + break; + case Orientation::PORTRAIT: + std::cout << "PORTRAIT"; + break; + case Orientation::LANDSCAPE_FLIPPED: + std::cout << "LANDSCAPE_FLIPPED"; + break; + case Orientation::PORTRAIT_FLIPPED: + std::cout << "PORTRAIT_FLIPPED"; + break; + } + + std::cout << "):" << std::endl; + + // Print the image row by row + for (uint32_t y = 0; y < height; ++y) { + for (uint32_t x = 0; x < width; ++x) { + bool pixel = getBit(x, y); + std::cout << (pixel ? "β–ˆ" : "."); + } + std::cout << std::endl; + } + std::cout << std::endl; +} + } // namespace ptprnt::graphics \ No newline at end of file diff --git a/src/graphics/Monochrome.hpp b/src/graphics/Monochrome.hpp index b029de9..ec6b3c0 100644 --- a/src/graphics/Monochrome.hpp +++ b/src/graphics/Monochrome.hpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2025 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,22 +20,81 @@ #pragma once #include +#include +#include #include "graphics/Bitmap.hpp" namespace ptprnt::graphics { -class Monochrome { - public: - Monochrome(const std::vector& grayscale); - ~Monochrome() = default; - void setThreshold(uint8_t); +enum class Orientation { + LANDSCAPE = 0, // 0 degrees + PORTRAIT = 1, // 90 degrees clockwise + LANDSCAPE_FLIPPED = 2, // 180 degrees + PORTRAIT_FLIPPED = 3 // 270 degrees clockwise (90 counter-clockwise) +}; + +class MonochromeData { + public: + // Constructors + MonochromeData() : stride(0), orientation(Orientation::LANDSCAPE), width(0), height(0) {} + + MonochromeData(std::vector data, uint32_t stride_bytes, Orientation orient = Orientation::LANDSCAPE, + uint32_t w = 0, uint32_t h = 0) + : bytes(std::move(data)), stride(stride_bytes), orientation(orient), width(w), height(h) {} + + // Constructor from grayscale data (replaces old Monochrome class) + MonochromeData(const std::vector& grayscale, uint32_t width, uint32_t height, + Orientation orient = Orientation::LANDSCAPE); + MonochromeData(const std::span grayscale, uint32_t width, uint32_t height, + Orientation orient = Orientation::LANDSCAPE); + + ~MonochromeData() = default; + + // Copy constructor and assignment + MonochromeData(const MonochromeData&) = default; + MonochromeData& operator=(const MonochromeData&) = default; + + // Move constructor and assignment + MonochromeData(MonochromeData&&) = default; + MonochromeData& operator=(MonochromeData&&) = default; + + // Configuration methods + void setThreshold(uint8_t threshold); void invert(bool shouldInvert); - std::vector get(); + + // Get processed monochrome data + MonochromeData get(); + + // Transform the image data to the target orientation + void transformTo(Orientation targetOrientation); + + // Visualize the monochrome data on stdout + void visualize() const; + + // Helper methods for orientation transformations + [[nodiscard]] bool getBit(uint32_t x, uint32_t y) const; + void setBit(uint32_t x, uint32_t y, bool value); + [[nodiscard]] std::vector createRotatedData(Orientation targetOrientation) const; + + // Public member access for backward compatibility + std::vector bytes; + uint32_t stride; + Orientation orientation; + uint32_t width; // Width in pixels + uint32_t height; // Height in pixels private: - const std::vector& mPixels; - uint8_t mThreshhold = 127; - bool mShouldInvert = false; + // Processing parameters (for old Monochrome class compatibility) + std::vector mPixels; // Original grayscale pixels + uint8_t mThreshold = UINT8_MAX / 2; + bool mShouldInvert = false; + bool mIsProcessed = false; // Flag to indicate if conversion has been done + + // Helper method to convert grayscale to monochrome + void processGrayscaleToMonochrome(); }; + +// For backward compatibility, create a type alias +using Monochrome = MonochromeData; } // namespace ptprnt::graphics \ No newline at end of file diff --git a/src/graphics/interface/ILabel.hpp b/src/graphics/interface/ILabel.hpp new file mode 100644 index 0000000..5c5c059 --- /dev/null +++ b/src/graphics/interface/ILabel.hpp @@ -0,0 +1,80 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2024-2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#pragma once + +#include +#include +#include +#include + +constexpr const char* DEFAULT_FONT_FAMILY = "sans"; +constexpr const double DEFAULT_FONT_SIZE = 32.0; + +enum class HAlignPosition { + UNKNOWN = 0, + LEFT = 1, + CENTER = 2, + RIGHT = 3, + JUSTIFY = 4, +}; +const std::map HALignPositionMap{{"", HAlignPosition::UNKNOWN}, + {"left", HAlignPosition::LEFT}, + {"center", HAlignPosition::CENTER}, + {"right", HAlignPosition::RIGHT}, + {"justify", HAlignPosition::JUSTIFY}}; + +enum class VAlignPosition { + UNKNOWN = 0, + TOP = 1, + MIDDLE = 2, + BOTTOM = 3, +}; + +const std::map VALignPositionMap{{"", VAlignPosition::UNKNOWN}, + {"top", VAlignPosition::TOP}, + {"middle", VAlignPosition::MIDDLE}, + {"bottom", VAlignPosition::BOTTOM}}; + +struct PrintableText { + std::string text{""}; + std::string fontFamily{DEFAULT_FONT_FAMILY}; + double fontSize{DEFAULT_FONT_SIZE}; + HAlignPosition hAlign{HAlignPosition::LEFT}; + VAlignPosition vAlign{VAlignPosition::MIDDLE}; +}; + +namespace ptprnt::graphics { +class ILabel { + public: + virtual ~ILabel() = default; + + virtual bool create(PrintableText printableText) = 0; + virtual bool create(const std::string& labelText) = 0; + virtual std::vector getRaw() = 0; + virtual int getWidth() = 0; + virtual int getHeight() = 0; + + virtual void setText(const std::string& text) = 0; + virtual void setFontSize(const double fontSize) = 0; + virtual void setFontFamily(const std::string& fontFamily) = 0; + virtual void setHAlign(HAlignPosition hpos) = 0; + virtual void setVAlign(VAlignPosition vpos) = 0; +}; +} // namespace ptprnt::graphics \ No newline at end of file diff --git a/src/interface/IPrinterDriver.hpp b/src/interface/IPrinterDriver.hpp index 3b461bc..48106fb 100644 --- a/src/interface/IPrinterDriver.hpp +++ b/src/interface/IPrinterDriver.hpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2025 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,16 +19,16 @@ #pragma once -#include #include #include #include "graphics/Bitmap.hpp" +#include "graphics/Monochrome.hpp" +#include "graphics/interface/ILabel.hpp" #include "interface/IPrinterTypes.hpp" #include "libusbwrap/interface/IUsbDevice.hpp" namespace ptprnt { - class IPrinterDriver { public: virtual ~IPrinterDriver() = default; @@ -40,12 +40,9 @@ class IPrinterDriver { [[nodiscard]] virtual const PrinterStatus getPrinterStatus() = 0; virtual bool attachUsbDevice(std::shared_ptr usbHndl) = 0; virtual bool detachUsbDevice() = 0; - virtual bool setText(const std::string& text) = 0; - virtual bool setFont(const std::string& text) = 0; - virtual bool setFontSize(uint8_t fontSize) = 0; - virtual bool setHAlign(HAlignPosition hpos) = 0; - virtual bool setVAlign(VAlignPosition vpos) = 0; virtual bool printBitmap(const graphics::Bitmap& bitmap) = 0; + virtual bool printMonochromeData(const graphics::MonochromeData& data) = 0; + virtual bool printLabel(const std::unique_ptr label) = 0; virtual bool print() = 0; }; diff --git a/src/interface/IPrinterTypes.hpp b/src/interface/IPrinterTypes.hpp index 38fd954..908a482 100644 --- a/src/interface/IPrinterTypes.hpp +++ b/src/interface/IPrinterTypes.hpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2024 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ #include #include #include +#include #include "libusbwrap/LibUsbTypes.hpp" @@ -32,6 +33,7 @@ struct PrinterInfo { std::string_view name = ""; std::string_view version = ""; libusbwrap::usbId usbId{0x00, 0x00}; + uint16_t pixelLines = 0; }; struct PrinterStatus { @@ -39,27 +41,6 @@ struct PrinterStatus { unsigned int tapeWidthMm = 0.0; }; -enum class HAlignPosition { - UNKNOWN = 0, - LEFT = 1, - CENTER = 2, - RIGHT = 3, - JUSTIFY = 4, -}; - -enum class VAlignPosition { - UNKNOWN = 0, - TOP = 1, - MIDDLE = 2, - BOTTOM = 3, -}; - -struct PrintableText { - std::string text{""}; - std::string font{"Noto"}; - uint8_t fontSize{0}; - HAlignPosition hAlign{HAlignPosition::LEFT}; - VAlignPosition vAlign{VAlignPosition::MIDDLE}; -}; +using cmd_T = std::vector; } // namespace ptprnt \ No newline at end of file diff --git a/src/libusbwrap/LibUsbTypes.hpp b/src/libusbwrap/LibUsbTypes.hpp index 054210f..f11c79b 100644 --- a/src/libusbwrap/LibUsbTypes.hpp +++ b/src/libusbwrap/LibUsbTypes.hpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2024 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/libusbwrap/UsbDevice.cpp b/src/libusbwrap/UsbDevice.cpp index dc073d8..dd22aee 100644 --- a/src/libusbwrap/UsbDevice.cpp +++ b/src/libusbwrap/UsbDevice.cpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2024 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,12 +27,12 @@ #include "libusbwrap/interface/IUsbDevice.hpp" namespace libusbwrap { -UsbDevice::UsbDevice(libusb_context* ctx, libusb_device* dev) : mLibusbCtx(ctx), mLibusbDev(dev) { +UsbDevice::UsbDevice(libusb_context* ctx, usbDevice_ptr dev) : mLibusbCtx(ctx), mLibusbDev(std::move(dev)) { if (mLibusbCtx == nullptr || mLibusbDev == nullptr) { throw std::invalid_argument("ctx or device are nullptr"); } - libusb_get_device_descriptor(dev, &mLibusbDevDesc); + libusb_get_device_descriptor(mLibusbDev.get(), &mLibusbDevDesc); } UsbDevice::~UsbDevice() { @@ -44,7 +44,7 @@ UsbDevice::~UsbDevice() { } bool UsbDevice::open() { - int openStatus = libusb_open(mLibusbDev, &mLibusbDevHandle); + int openStatus = libusb_open(mLibusbDev.get(), &mLibusbDevHandle); if (openStatus != 0) { mLastError = static_cast(openStatus); return false; @@ -90,13 +90,12 @@ bool UsbDevice::releaseInterface(int interfaceNo) { return true; } -bool UsbDevice::bulkTransfer(uint8_t endpoint, std::vector& data, int* tx, - unsigned int timeout) { +bool UsbDevice::bulkTransfer(uint8_t endpoint, const std::vector& data, int* tx, unsigned int timeout) { // TODO: implement error handling for incomplete transactions (tx length != data length) int bulkTransferStatus = 0; - bulkTransferStatus = - libusb_bulk_transfer(mLibusbDevHandle, endpoint, data.data(), data.size(), tx, timeout); + bulkTransferStatus = libusb_bulk_transfer(mLibusbDevHandle, endpoint, const_cast(data.data()), + data.size(), tx, timeout); if (bulkTransferStatus != 0) { mLastError = static_cast(bulkTransferStatus); return false; @@ -109,15 +108,15 @@ const usbId UsbDevice::getUsbId() { } const device::Speed UsbDevice::getSpeed() { - return static_cast(libusb_get_device_speed(mLibusbDev)); + return static_cast(libusb_get_device_speed(mLibusbDev.get())); } const uint8_t UsbDevice::getBusNumber() { - return libusb_get_bus_number(mLibusbDev); + return libusb_get_bus_number(mLibusbDev.get()); } const uint8_t UsbDevice::getPortNumber() { - return libusb_get_port_number(mLibusbDev); + return libusb_get_port_number(mLibusbDev.get()); } const Error UsbDevice::getLastError() { diff --git a/src/libusbwrap/UsbDevice.hpp b/src/libusbwrap/UsbDevice.hpp index 712e3a7..475b1a3 100644 --- a/src/libusbwrap/UsbDevice.hpp +++ b/src/libusbwrap/UsbDevice.hpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2024 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,21 +20,34 @@ #pragma once #include -#include +#include #include "libusb.h" #include "libusbwrap/LibUsbTypes.hpp" #include "libusbwrap/interface/IUsbDevice.hpp" namespace libusbwrap { + +struct usbDevice_deleter { + void operator()(libusb_device* dev_ptr) const { + if (nullptr != dev_ptr) { + libusb_unref_device(dev_ptr); + } + } +}; + +using usbDevice_ptr = std::unique_ptr; + class UsbDevice : public IUsbDevice { public: - explicit UsbDevice(libusb_context* ctx, libusb_device* dev); + explicit UsbDevice(libusb_context* ctx, usbDevice_ptr dev); ~UsbDevice() override; // delete copy ctor and assignment - UsbDevice(const UsbDevice&) = delete; - UsbDevice& operator=(UsbDevice&) = delete; + UsbDevice(const UsbDevice&) = delete; + UsbDevice& operator=(const UsbDevice&) = delete; + UsbDevice(UsbDevice&&) = delete; + UsbDevice& operator=(UsbDevice&&) = delete; bool open() override; void close() override; @@ -43,8 +56,7 @@ class UsbDevice : public IUsbDevice { bool detachKernelDriver(int interfaceNo) override; bool claimInterface(int interfaceNo) override; bool releaseInterface(int interfaceNo) override; - bool bulkTransfer(uint8_t endpoint, std::vector& data, int* tx, - unsigned int timeout) override; + bool bulkTransfer(uint8_t endpoint, const std::vector& data, int* tx, unsigned int timeout) override; // getters const usbId getUsbId() override; @@ -58,9 +70,9 @@ class UsbDevice : public IUsbDevice { private: libusb_context* mLibusbCtx{nullptr}; - libusb_device* mLibusbDev{nullptr}; + usbDevice_ptr mLibusbDev{nullptr}; libusb_device_handle* mLibusbDevHandle{nullptr}; - libusb_device_descriptor mLibusbDevDesc; + libusb_device_descriptor mLibusbDevDesc{0}; std::atomic mIsOpen = false; Error mLastError = Error::SUCCESS; }; diff --git a/src/libusbwrap/UsbDeviceFactory.cpp b/src/libusbwrap/UsbDeviceFactory.cpp index 7ad09e9..5ad5349 100644 --- a/src/libusbwrap/UsbDeviceFactory.cpp +++ b/src/libusbwrap/UsbDeviceFactory.cpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2023-2025 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,9 +20,11 @@ #include "libusbwrap/UsbDeviceFactory.hpp" #include +#include #include #include +#include #include "libusb.h" #include "libusbwrap/UsbDevice.hpp" @@ -31,49 +33,54 @@ namespace libusbwrap { UsbDeviceFactory::~UsbDeviceFactory() { - if (mDeviceListInitialized) { - libusb_free_device_list(mLibusbDeviceList, 1); + if (mLibusbCtx) { + libusb_exit(mLibusbCtx); } -} +}; -std::vector> UsbDeviceFactory::findAllDevices() { +std::vector> UsbDeviceFactory::findAllDevices() { refreshDeviceList(); return buildMaskedDeviceVector(0x0, 0x0, 0x0, 0x0); } -std::vector> UsbDeviceFactory::findDevices(uint16_t vid, uint16_t pid) { +std::vector> UsbDeviceFactory::findDevices(uint16_t vid, uint16_t pid) { refreshDeviceList(); - return buildMaskedDeviceVector(0xffff, 0xffff, vid, pid); + return buildMaskedDeviceVector(LIBUSB_BITMASK_ALL, LIBUSB_BITMASK_ALL, vid, pid); } -int UsbDeviceFactory::refreshDeviceList() { - int ret = libusb_get_device_list(mLibusbCtx, &mLibusbDeviceList); +ssize_t UsbDeviceFactory::refreshDeviceList() { + libusb_device** list{nullptr}; + + ssize_t ret = libusb_get_device_list(mLibusbCtx, &list); + mLibusbDeviceList.clear(); if (ret < 0) { spdlog::error("Error enumarating USB devices"); } else if (ret == 0) { spdlog::warn("No USB devices found"); } - mDeviceListInitialized = true; + + for (ssize_t i = 0; i < ret; i++) { + mLibusbDeviceList.emplace_back(list[i]); + } + + libusb_free_device_list(list, false); return ret; } -std::vector> UsbDeviceFactory::buildMaskedDeviceVector(uint16_t vidMask, - uint16_t pidMask, - uint16_t vid, - uint16_t pid) { - std::vector> matchedDevices; +std::vector> UsbDeviceFactory::buildMaskedDeviceVector(uint16_t vidMask, uint16_t pidMask, + uint16_t vid, uint16_t pid) { + std::vector> matchedDevices; // see libusb/examples/listdevs.c - int i = 0; - libusb_device* currDev = nullptr; - while ((currDev = mLibusbDeviceList[i++]) != nullptr) { - struct libusb_device_descriptor currDevDesc; - int ret = libusb_get_device_descriptor(currDev, &currDevDesc); + for (auto& currDev : mLibusbDeviceList) { + struct libusb_device_descriptor currDevDesc{}; + + int ret = libusb_get_device_descriptor(currDev.get(), &currDevDesc); + spdlog::trace("Detected Device {:04x}:{:04x} ", currDevDesc.idVendor, currDevDesc.idProduct); if (ret < 0) { continue; } - if (((currDevDesc.idVendor & vidMask) == vid) && - ((currDevDesc.idProduct & pidMask) == pid)) { - matchedDevices.push_back(std::make_shared(mLibusbCtx, currDev)); + if (((currDevDesc.idVendor & vidMask) == vid) && ((currDevDesc.idProduct & pidMask) == pid)) { + matchedDevices.push_back(std::make_unique(mLibusbCtx, std::move(currDev))); } } return matchedDevices; diff --git a/src/libusbwrap/UsbDeviceFactory.hpp b/src/libusbwrap/UsbDeviceFactory.hpp index e2f4d70..558a077 100644 --- a/src/libusbwrap/UsbDeviceFactory.hpp +++ b/src/libusbwrap/UsbDeviceFactory.hpp @@ -19,9 +19,16 @@ #pragma once +#include + +#include "libusb.h" +#include "libusbwrap/UsbDevice.hpp" #include "libusbwrap/interface/IUsbDeviceFactory.hpp" namespace libusbwrap { + +constexpr const uint16_t LIBUSB_BITMASK_ALL = 0xffff; + class UsbDeviceFactory : public IUsbDeviceFactory { public: UsbDeviceFactory() = default; @@ -39,7 +46,7 @@ class UsbDeviceFactory : public IUsbDeviceFactory { * * @return std::vector> Vector of all detected USB devices */ - std::vector> findAllDevices() override; + std::vector> findAllDevices() override; /** * @brief Gets all devices with certain vid/pid combination. * If only one device of certain type is connected, vector is usually only one element @@ -48,17 +55,17 @@ class UsbDeviceFactory : public IUsbDeviceFactory { * @param pid ProductId of the devices to find * @return std::vector> Vector of detected USB devices based on vid/pid */ - std::vector> findDevices(uint16_t vid, uint16_t pid) override; + std::vector> findDevices(uint16_t vid, uint16_t pid) override; private: // methods - int refreshDeviceList(); - std::vector> buildMaskedDeviceVector(uint16_t vidMask, + ssize_t refreshDeviceList(); + std::vector> buildMaskedDeviceVector(uint16_t vidMask, uint16_t pidMask, uint16_t vid, uint16_t pid); // members libusb_context* mLibusbCtx{nullptr}; - libusb_device** mLibusbDeviceList{}; + std::vector mLibusbDeviceList{}; bool mDeviceListInitialized = false; }; } // namespace libusbwrap \ No newline at end of file diff --git a/src/libusbwrap/interface/IUsbDevice.hpp b/src/libusbwrap/interface/IUsbDevice.hpp index e547beb..fe80de7 100644 --- a/src/libusbwrap/interface/IUsbDevice.hpp +++ b/src/libusbwrap/interface/IUsbDevice.hpp @@ -1,3 +1,22 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2023-2024 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + #pragma once #include @@ -29,11 +48,10 @@ class IUsbDevice { virtual void close() = 0; // libusb wrappers - virtual bool detachKernelDriver(int interfaceNo) = 0; - virtual bool claimInterface(int interfaceNo) = 0; - virtual bool releaseInterface(int interfaceNo) = 0; - virtual bool bulkTransfer(uint8_t endpoint, std::vector& data, int* tx, - unsigned int timeout) = 0; + virtual bool detachKernelDriver(int interfaceNo) = 0; + virtual bool claimInterface(int interfaceNo) = 0; + virtual bool releaseInterface(int interfaceNo) = 0; + virtual bool bulkTransfer(uint8_t endpoint, const std::vector& data, int* tx, unsigned int timeout) = 0; // getters virtual const usbId getUsbId() = 0; diff --git a/src/libusbwrap/interface/IUsbDeviceFactory.hpp b/src/libusbwrap/interface/IUsbDeviceFactory.hpp index a31cdc7..725edad 100644 --- a/src/libusbwrap/interface/IUsbDeviceFactory.hpp +++ b/src/libusbwrap/interface/IUsbDeviceFactory.hpp @@ -1,3 +1,22 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2023 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + #pragma once #include @@ -9,8 +28,8 @@ namespace libusbwrap { class IUsbDeviceFactory { public: - virtual ~IUsbDeviceFactory() = default; - virtual std::vector> findAllDevices() = 0; - virtual std::vector> findDevices(uint16_t vid, uint16_t pid) = 0; + virtual ~IUsbDeviceFactory() = default; + virtual std::vector> findAllDevices() = 0; + virtual std::vector> findDevices(uint16_t vid, uint16_t pid) = 0; }; } // namespace libusbwrap \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index ed35bcc..19a160a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2022-2023 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/meson.build b/src/meson.build index 9061231..7f184df 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,19 +6,21 @@ ptprnt_hpps = files ( 'libusbwrap/UsbDevice.hpp', 'interface/IPrinterDriver.hpp', 'interface/IPrinterTypes.hpp', - 'P700Printer.hpp', + 'printers/P700Printer.hpp', + 'printers/FakePrinter.hpp', 'PtouchPrint.hpp', 'PrinterDriverFactory.hpp', 'graphics/Bitmap.hpp', - 'graphics/Image.hpp', + 'graphics/Label.hpp', 'graphics/Monochrome.hpp' ) ptprnt_srcs = files ( 'PtouchPrint.cpp', 'PrinterDriverFactory.cpp', - 'P700Printer.cpp', - 'graphics/Image.cpp', + 'printers/P700Printer.cpp', + 'printers/FakePrinter.cpp', + 'graphics/Label.cpp', 'graphics/Bitmap.cpp', 'graphics/Monochrome.cpp', 'libusbwrap/UsbDeviceFactory.cpp', diff --git a/src/printers/FakePrinter.cpp b/src/printers/FakePrinter.cpp new file mode 100644 index 0000000..caf3e67 --- /dev/null +++ b/src/printers/FakePrinter.cpp @@ -0,0 +1,263 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#include "FakePrinter.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "../graphics/Monochrome.hpp" + +namespace ptprnt::printer { + +const PrinterInfo FakePrinter::mInfo = { + .driverName = "FakePrinter", + .name = "Virtual Test Printer", + .version = "v1.0", + .usbId{0x0000, 0x0000}, // No USB ID - virtual printer created explicitly + .pixelLines = 128 +}; + +const std::string_view FakePrinter::getDriverName() { + return mInfo.driverName; +} + +const std::string_view FakePrinter::getName() { + return mInfo.name; +} + +const std::string_view FakePrinter::getVersion() { + return mInfo.version; +} + +const PrinterInfo FakePrinter::getPrinterInfo() { + return mInfo; +} + +const PrinterStatus FakePrinter::getPrinterStatus() { + return mStatus; +} + +const libusbwrap::usbId FakePrinter::getUsbId() { + return mInfo.usbId; +} + +bool FakePrinter::attachUsbDevice(std::shared_ptr usbHndl) { + // FakePrinter doesn't need a real USB device + mHasAttachedDevice = true; + spdlog::debug("FakePrinter: Simulated USB device attachment"); + return true; +} + +bool FakePrinter::detachUsbDevice() { + mHasAttachedDevice = false; + spdlog::debug("FakePrinter: Simulated USB device detachment"); + return true; +} + +bool FakePrinter::printBitmap(const graphics::Bitmap& bitmap) { + // Convert bitmap to MonochromeData and delegate + auto pixels = bitmap.getPixelsCpy(); + auto mono = graphics::Monochrome(pixels, bitmap.getWidth(), bitmap.getHeight()); + auto monoData = mono.get(); + + return printMonochromeData(monoData); +} + +bool FakePrinter::printMonochromeData(const graphics::MonochromeData& data) { + spdlog::debug("FakePrinter: Simulating printing of {}x{} bitmap", data.width, data.height); + + // Simulate the printing process by reconstructing the bitmap + auto printed = simulatePrinting(data); + mLastPrint = std::make_unique>(std::move(printed)); + + spdlog::info("FakePrinter: Successfully 'printed' label ({}x{} pixels)", + mLastPrint->getWidth(), mLastPrint->getHeight()); + + // Save to timestamped PNG file + std::string filename = generateTimestampedFilename(); + if (saveBitmapToPng(*mLastPrint, filename)) { + spdlog::info("FakePrinter: Saved output to {}", filename); + } else { + spdlog::error("FakePrinter: Failed to save output to {}", filename); + } + + return true; +} + +bool FakePrinter::printLabel(const std::unique_ptr label) { + // Convert label directly to MonochromeData + // getRaw() returns data in Cairo surface coordinates matching getWidth() Γ— getHeight() + auto pixels = label->getRaw(); + + // Create monochrome data in landscape orientation (as stored in Cairo surface) + auto mono = graphics::Monochrome(pixels, label->getWidth(), label->getHeight(), graphics::Orientation::LANDSCAPE); + auto monoData = mono.get(); + + // Transform to portrait orientation for printing + monoData.transformTo(graphics::Orientation::PORTRAIT); + + spdlog::debug("FakePrinter: Label surface is {}x{}, transformed to portrait", label->getWidth(), label->getHeight()); + + return printMonochromeData(monoData); +} + +bool FakePrinter::print() { + spdlog::debug("FakePrinter: Print command (no-op for virtual printer)"); + return true; +} + +graphics::Bitmap FakePrinter::simulatePrinting(const graphics::MonochromeData& data) { + spdlog::debug("FakePrinter: Simulating column-by-column printing like real hardware"); + + // Create output bitmap with same dimensions + graphics::Bitmap result(data.width, data.height); + std::vector pixels(data.width * data.height, 0); + + // Simulate printer behavior: process column by column + // This mimics how label printers physically print one vertical line at a time + for (uint32_t col = 0; col < data.width; col++) { + spdlog::trace("FakePrinter: Processing column {}/{}", col + 1, data.width); + + // Extract column data bit by bit (simulating what would be sent to printer) + std::vector columnBytes; + + for (uint32_t row = 0; row < data.height; row += 8) { + uint8_t byte = 0; + + // Pack 8 vertical pixels into one byte (printer data format) + for (int bit = 0; bit < 8 && (row + bit) < data.height; bit++) { + if (data.getBit(col, row + bit)) { + byte |= (1 << (7 - bit)); + } + } + + columnBytes.push_back(byte); + } + + // Now "print" this column by unpacking the bytes back to pixels + for (size_t byteIdx = 0; byteIdx < columnBytes.size(); byteIdx++) { + uint8_t byte = columnBytes[byteIdx]; + uint32_t baseRow = byteIdx * 8; + + for (int bit = 0; bit < 8 && (baseRow + bit) < data.height; bit++) { + bool pixelOn = (byte & (1 << (7 - bit))) != 0; + uint32_t row = baseRow + bit; + + // Write to output bitmap + size_t pixelIdx = row * data.width + col; + pixels[pixelIdx] = pixelOn ? 255 : 0; // 255 = black, 0 = white + } + } + } + + // Set the pixels in the result bitmap + result.setPixels(pixels); + + spdlog::debug("FakePrinter: Simulation complete, reconstructed {}x{} bitmap", + result.getWidth(), result.getHeight()); + + return result; +} + +const graphics::Bitmap& FakePrinter::getLastPrint() const { + if (!mLastPrint) { + throw std::runtime_error("FakePrinter: No print data available"); + } + return *mLastPrint; +} + +bool FakePrinter::saveLastPrintToPng(const std::string& filename) const { + if (!mLastPrint || mLastPrint->getWidth() == 0 || mLastPrint->getHeight() == 0) { + spdlog::error("FakePrinter: No print data available to save"); + return false; + } + + return saveBitmapToPng(*mLastPrint, filename); +} + +bool FakePrinter::saveBitmapToPng(const graphics::Bitmap& bitmap, const std::string& filename) const { + // Create Cairo surface from bitmap data + auto pixels = bitmap.getPixelsCpy(); + uint16_t width = bitmap.getWidth(); + uint16_t height = bitmap.getHeight(); + + // Cairo expects ARGB32 format, but we have ALPHA8 + // Convert ALPHA8 (grayscale) to ARGB32 + std::vector argbPixels(width * height); + + for (size_t i = 0; i < pixels.size(); i++) { + uint8_t gray = pixels[i]; + // ARGB32 format: 0xAARRGGBB + // For grayscale: use gray value for R, G, B and 255 for alpha + argbPixels[i] = 0xFF000000 | (gray << 16) | (gray << 8) | gray; + } + + // Create Cairo surface + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + cairo_surface_t* surface = cairo_image_surface_create_for_data( + reinterpret_cast(argbPixels.data()), + CAIRO_FORMAT_ARGB32, + width, + height, + stride + ); + + if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { + spdlog::error("FakePrinter: Failed to create Cairo surface: {}", + cairo_status_to_string(cairo_surface_status(surface))); + cairo_surface_destroy(surface); + return false; + } + + // Write to PNG file + cairo_status_t status = cairo_surface_write_to_png(surface, filename.c_str()); + + cairo_surface_destroy(surface); + + if (status != CAIRO_STATUS_SUCCESS) { + spdlog::error("FakePrinter: Failed to write PNG file: {}", cairo_status_to_string(status)); + return false; + } + + return true; +} + +std::string FakePrinter::generateTimestampedFilename() const { + // Get current time + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + + // Format: fakelabel_YYYYMMDD_HHMMSS.png + std::stringstream ss; + ss << "fakelabel_" + << std::put_time(std::localtime(&time), "%Y%m%d_%H%M%S") + << ".png"; + + return ss.str(); +} + +} // namespace ptprnt::printer diff --git a/src/printers/FakePrinter.hpp b/src/printers/FakePrinter.hpp new file mode 100644 index 0000000..12cd1ff --- /dev/null +++ b/src/printers/FakePrinter.hpp @@ -0,0 +1,108 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#pragma once + +#include +#include +#include + +#include "../interface/IPrinterDriver.hpp" +#include "../interface/IPrinterTypes.hpp" +#include "../libusbwrap/LibUsbTypes.hpp" +#include "../libusbwrap/interface/IUsbDevice.hpp" +#include "../graphics/Bitmap.hpp" + +namespace ptprnt::printer { + +/** + * @brief Virtual printer driver for testing without hardware + * + * FakePrinter simulates a real label printer by processing bitmap data + * column by column and reconstructing it into a new bitmap, mimicking + * the physical printing process of label printers. + */ +class FakePrinter : public ::ptprnt::IPrinterDriver { + public: + FakePrinter() = default; + ~FakePrinter() override = default; + + FakePrinter(const FakePrinter&) = delete; + FakePrinter& operator=(const FakePrinter&) = delete; + FakePrinter(FakePrinter&&) = default; + FakePrinter& operator=(FakePrinter&&) = default; + + // Printer info - static to be accessed without instantiation + static const PrinterInfo mInfo; + + // IPrinterDriver interface + [[nodiscard]] const std::string_view getDriverName() override; + [[nodiscard]] const std::string_view getName() override; + [[nodiscard]] const libusbwrap::usbId getUsbId() override; + [[nodiscard]] const std::string_view getVersion() override; + [[nodiscard]] const PrinterInfo getPrinterInfo() override; + [[nodiscard]] const PrinterStatus getPrinterStatus() override; + bool attachUsbDevice(std::shared_ptr usbHndl) override; + bool detachUsbDevice() override; + bool printBitmap(const graphics::Bitmap& bitmap) override; + bool printMonochromeData(const graphics::MonochromeData& data) override; + bool printLabel(const std::unique_ptr label) override; + bool print() override; + + /** + * @brief Get the last printed bitmap + * @return The reconstructed bitmap from simulated printing + */ + [[nodiscard]] const graphics::Bitmap& getLastPrint() const; + + /** + * @brief Save the last print to a PNG file + * @param filename Path to save the PNG + * @return true if successful + */ + bool saveLastPrintToPng(const std::string& filename) const; + + private: + /** + * @brief Simulate printing by reconstructing bitmap column by column + * @param data Monochrome data to "print" + * @return Reconstructed bitmap + */ + graphics::Bitmap simulatePrinting(const graphics::MonochromeData& data); + + /** + * @brief Save bitmap to PNG file using Cairo + * @param bitmap The bitmap to save + * @param filename Output filename + * @return true if successful + */ + bool saveBitmapToPng(const graphics::Bitmap& bitmap, const std::string& filename) const; + + /** + * @brief Generate timestamped filename for fake label output + * @return Filename like "fakelabel_20231011_123456.png" + */ + std::string generateTimestampedFilename() const; + + std::unique_ptr> mLastPrint; + bool mHasAttachedDevice = false; + PrinterStatus mStatus{.tapeWidthMm = 12}; // Default to 12mm tape +}; + +} // namespace ptprnt::printer diff --git a/src/printers/P700Printer.cpp b/src/printers/P700Printer.cpp new file mode 100644 index 0000000..b3f8e22 --- /dev/null +++ b/src/printers/P700Printer.cpp @@ -0,0 +1,238 @@ +/* + ptrnt - print labels on linux + Copyright (C) 2025 Moritz Martinius + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + */ + +#include "P700Printer.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +#include "../graphics/Bitmap.hpp" +#include "../graphics/Monochrome.hpp" +#include "../libusbwrap/LibUsbTypes.hpp" +#include "spdlog/fmt/bin_to_hex.h" + +namespace ptprnt::printer { + +const PrinterInfo P700Printer::mInfo = {.driverName = "P700", + .name = "Brother P-touch P700", + .version = "v1.0", + .usbId{0x04f9, 0x2061}, + .pixelLines = 128}; + +P700Printer::~P700Printer() { + P700Printer::detachUsbDevice(); + if (mUsbHndl) { + mUsbHndl->close(); + } +} + +const std::string_view P700Printer::getDriverName() { + return mInfo.driverName; +} + +const std::string_view P700Printer::getName() { + return mInfo.name; +} + +const std::string_view P700Printer::getVersion() { + return mInfo.version; +} + +const PrinterInfo P700Printer::getPrinterInfo() { + return mInfo; +} + +const PrinterStatus P700Printer::getPrinterStatus() { + using namespace std::chrono_literals; + send(p700::commands::GET_STATUS); + + int tx = 0; + int tries = 0; + std::vector recvBuf(32); + while (tries++ < MAX_TRIES_GET_STATUS) { + std::this_thread::sleep_for(100ms); + mUsbHndl->bulkTransfer(p700::commands::PRINTER_INFO[0], recvBuf, &tx, 0); + } + + return PrinterStatus{.tapeWidthMm = recvBuf[10]}; +} + +const libusbwrap::usbId P700Printer::getUsbId() { + return mInfo.usbId; +} + +bool P700Printer::attachUsbDevice(std::shared_ptr usbHndl) { + if (!usbHndl->open()) { + spdlog::error("Unable to open USB device: {}", usbHndl->getLastErrorString()); + return false; + } + + if (!usbHndl->detachKernelDriver(0)) { + + spdlog::error("Device is already in use or couldn't be detached from kernel: {}", + usbHndl->getLastErrorString()); + return false; + } + + if (!usbHndl->claimInterface(0)) { + spdlog::error("Could not claim interface 0: {}", usbHndl->getLastErrorString()); + return false; + } + + mUsbHndl = std::move(usbHndl); + return true; +} + +bool P700Printer::detachUsbDevice() { + if (!mUsbHndl) { + spdlog::warn("No device to detach..."); + return true; + } + if (!mUsbHndl->releaseInterface(0)) { + spdlog::error("Could not release interface 0: {}", mUsbHndl->getLastErrorString()); + return false; + } + return true; +} + +bool P700Printer::printBitmap(const graphics::Bitmap& bitmap) { + // Convert bitmap to MonochromeData and delegate to printMonochromeData + auto pixels = bitmap.getPixelsCpy(); + auto mono = graphics::Monochrome(pixels, bitmap.getWidth(), bitmap.getHeight()); + auto monoData = mono.get(); + + return printMonochromeData(monoData); +} + +bool P700Printer::printMonochromeData(const graphics::MonochromeData& data) { + // Send initialization sequence + // The INITIALIZE command needs to be sent as a 128-byte packet with ESC @ at the end + std::vector initCmd(128, 0x00); + initCmd[126] = p700::commands::INITIALIZE[0]; // ESC + initCmd[127] = p700::commands::INITIALIZE[1]; // @ + send(initCmd); + + // Status is already queried in getPrinterStatus() + send(p700::commands::PRINT_MODE); + send(p700::commands::AUTO_STATUS); + send(p700::commands::MODE_SETTING); + + // Send raster data row by row in reverse order (bottom to top) + // The printer feeds tape as it prints, so first row sent appears at the end + for (int row = data.height - 1; row >= 0; row--) { + std::vector rowData; + + // Extract row data byte by byte + for (uint32_t col = 0; col < data.width; col += 8) { + uint8_t byte = 0; + for (int bit = 0; bit < 8 && (col + bit) < data.width; bit++) { + if (data.getBit(col + bit, row)) { + byte |= (1 << (7 - bit)); + } + } + rowData.push_back(byte); + } + + // Build raster line command: G + length_high + 0x00 + length_low + data + std::vector buf; + buf.push_back(0x47); // 'G' command + buf.push_back((rowData.size() + 1) & 0xFF); // length + 1 (low byte) + buf.push_back(0x00); // high byte (always 0 for our data size) + buf.push_back((rowData.size() - 1) & 0xFF); // length - 1 + buf.insert(buf.end(), rowData.begin(), rowData.end()); + + if (!send(buf)) { + spdlog::error("Error sending raster line {} to printer", row); + return false; + } + } + + // Send print finalization commands + send(p700::commands::EJECT); + + return true; +} + +bool P700Printer::printLabel(std::unique_ptr label) { + // Convert label directly to MonochromeData + // getRaw() returns data in Cairo surface coordinates matching getWidth() Γ— getHeight() + auto pixels = label->getRaw(); + + // Create monochrome data in landscape orientation (as stored in Cairo surface) + auto mono = graphics::Monochrome(pixels, label->getWidth(), label->getHeight(), graphics::Orientation::LANDSCAPE); + auto monoData = mono.get(); + + // Transform to portrait orientation for printing + monoData.transformTo(graphics::Orientation::PORTRAIT); + + spdlog::debug("Label surface was {}x{}, after transform to portrait: {}x{}", label->getWidth(), label->getHeight(), + monoData.width, monoData.height); + + return printMonochromeData(monoData); +} + +bool P700Printer::print() { + send(p700::commands::LF); + send(p700::commands::FF); + send(p700::commands::EJECT); + return true; +} + +bool P700Printer::send(const std::vector& data) { + + if (mUsbHndl == nullptr || data.size() > 128) { + spdlog::error("Invalid device handle or invalid data."); + return false; + } + + spdlog::trace("USB Tx β†’ 0x02 {:03d} bytes: {:Xn}", data.size(), spdlog::to_hex(data)); +#ifdef USB_TRACE_ONLY + // Trace mode: Log the data that would be sent without actually sending it + return true; +#else + int tx = 0; + + if (!mUsbHndl->bulkTransfer(0x02, data, &tx, 0)) { + spdlog::error("Error writing command to Printer: {}", mUsbHndl->getLastErrorString()); + return false; + } + + assert(tx > 0); + if (static_cast(tx) != data.size()) { + spdlog::error("Could not transfer all data via USB bulk transfer. Only sent {} of {} bytes", tx, data.size()); + return false; + } + + return true; +#endif +} + +bool P700Printer::init() { + std::vector cmd(102); + cmd[100] = 0x1b; /* ESC */ + cmd[101] = 0x40; /* @ */ + return send(cmd); +} +} // namespace ptprnt::printer diff --git a/src/P700Printer.hpp b/src/printers/P700Printer.hpp similarity index 66% rename from src/P700Printer.hpp rename to src/printers/P700Printer.hpp index ae72a3c..e14f500 100644 --- a/src/P700Printer.hpp +++ b/src/printers/P700Printer.hpp @@ -1,6 +1,6 @@ /* ptrnt - print labels on linux - Copyright (C) 2023 Moritz Martinius + Copyright (C) 2025 Moritz Martinius This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,9 +17,9 @@ */ +#include #include -#include #include #include @@ -31,13 +31,30 @@ #pragma once namespace ptprnt::printer { +namespace p700::commands { +const cmd_T INITIALIZE{0x1b, 0x40}; // ESC @ - Initialize +const cmd_T GET_STATUS{0x1b, 0x69, 0x53}; // ESC i S - Status query +const cmd_T PRINT_MODE{0x4d, 0x02}; // M 0x02 - Print mode +const cmd_T AUTO_STATUS{0x1b, 0x69, 0x61, 0x01}; // ESC i a - Auto status +const cmd_T MODE_SETTING{0x1b, 0x69, 0x4d, 0x40}; // ESC i M @ - Advanced mode +const cmd_T RASTER_START{0x1b, 0x69, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const cmd_T INFO{0x1b, 0x69, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const cmd_T PACKBITSON{0x02}; +const cmd_T LF{0x5a}; +const cmd_T FF{0x0c}; +const cmd_T EJECT{0x1a}; +const cmd_T PRINTER_INFO{0x81}; +} // namespace p700::commands constexpr uint8_t MAX_TRIES_GET_STATUS = 10; +// TODO: +// Remove Text-layout specific parts, add them to label + class P700Printer : public ::ptprnt::IPrinterDriver { public: P700Printer() = default; - ~P700Printer(); + ~P700Printer() override; // delete copy ctor and assignment P700Printer(const P700Printer&) = default; @@ -57,29 +74,15 @@ class P700Printer : public ::ptprnt::IPrinterDriver { [[nodiscard]] const PrinterStatus getPrinterStatus() override; bool attachUsbDevice(std::shared_ptr usbHndl) override; bool detachUsbDevice() override; - bool setText(const std::string& text) override; - bool setFont(const std::string& text) override; - bool setFontSize(uint8_t fontSize) override; - bool setHAlign(HAlignPosition hpos) override; - bool setVAlign(VAlignPosition vpos) override; bool printBitmap(const graphics::Bitmap& bitmap) override; + bool printMonochromeData(const graphics::MonochromeData& data) override; + bool printLabel(const std::unique_ptr label) override; bool print() override; private: - bool send(std::vector& data); + bool send(const std::vector& data); bool init(); std::shared_ptr mUsbHndl{nullptr}; - std::map> commands{ - {"rasterstart", - {0x1b, 0x69, 0x61, - 0x01}}, // unique for P700, other printers have the 2 byte set to 0x52 instead of 0x61 - {"info", {0x1b, 0x69, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}, - {"packbitson", {0x02}}, - {"lf", {0x5a}}, - {"ff", {0x0c}}, - {"eject", {0x1a}}, - {"printerinfo", {0x81}}}; }; - } // namespace ptprnt::printer \ No newline at end of file diff --git a/src/ptprnt.log b/src/ptprnt.log new file mode 100644 index 0000000..e69de29 diff --git a/tests/bitmap_test/bitmap_test.cpp b/tests/bitmap_test/bitmap_test.cpp index 3185b0e..4e71d4f 100644 --- a/tests/bitmap_test/bitmap_test.cpp +++ b/tests/bitmap_test/bitmap_test.cpp @@ -22,7 +22,6 @@ #include #include -#include #include TEST(basic_test, Bitmap_createBitmapWithCertainSize_yieldsSpecifiedSize) { @@ -36,34 +35,28 @@ TEST(basic_test, Bitmap_createBitmapWithCertainSize_yieldsSpecifiedSize) { TEST(basic_test, Bitmap_getBitmapLineOutsideOfImage_yieldsNullopt) { auto bm = ptprnt::graphics::Bitmap(16, 8); // line 8 is out of bounds, count begins with 0 - auto outOfBoundsLine = bm.getLine(8); - ASSERT_EQ(std::nullopt, outOfBoundsLine); + EXPECT_ANY_THROW(auto outOfBoundsLine = bm.getLine(8)); } TEST(basic_test, Bitmap_getBitmapLineInsideOfImage_yieldsValidLineSize) { - auto bm = ptprnt::graphics::Bitmap(16, 8); - auto line = bm.getLine(7); - if (!line) { - FAIL() << "Returned line is invalid"; - } - auto lineSize = line->size(); + auto bm = ptprnt::graphics::Bitmap(16, 8); + auto line = bm.getLine(7); + auto lineSize = line.size(); ASSERT_EQ(16, lineSize); } TEST(basic_test, Bitmap_getBitmapColOutsideOfImage_yieldsNullopt) { auto bm = ptprnt::graphics::Bitmap(16, 8); // col 16 is out of bounds, count begins with 0 - auto outOfBoundsCol = bm.getCol(16); - ASSERT_EQ(std::nullopt, outOfBoundsCol); + + EXPECT_ANY_THROW(auto outOfBoundsCol = bm.getCol(16)); } TEST(basic_test, Bitmap_getBitmapColInsideOfImage_yieldsValidColSize) { auto bm = ptprnt::graphics::Bitmap(16, 8); auto col = bm.getCol(15); - if (!col) { - FAIL() << "Returned Col is invalid"; - } - auto colSize = col->size(); + + auto colSize = col.size(); ASSERT_EQ(8, colSize); } diff --git a/tests/image_test/image_test.cpp b/tests/label_test/label_test.cpp similarity index 86% rename from tests/image_test/image_test.cpp rename to tests/label_test/label_test.cpp index 46d0f08..08e3ce7 100644 --- a/tests/image_test/image_test.cpp +++ b/tests/label_test/label_test.cpp @@ -17,10 +17,10 @@ */ -#include "graphics/Image.hpp" +#include "graphics/Label.hpp" #include -TEST(basic_test, Image_smokeTest_succeeds) { - auto im = ptprnt::graphics::Image(); +TEST(basic_test, Label_smokeTest_succeeds) { + auto im = ptprnt::graphics::Label(4711); } diff --git a/tests/meson.build b/tests/meson.build index 1d45232..4d59e27 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -5,9 +5,9 @@ tests = [ ['../src/graphics/Bitmap.cpp', 'bitmap_test/bitmap_test.cpp'], ], [ - 'image_test', - 'image_test_exe', - ['../src/graphics/Image.cpp', 'image_test/image_test.cpp'], + 'label_test', + 'label_test_exe', + ['../src/graphics/Label.cpp', 'label_test/label_test.cpp'], ], [ 'monochrome_test', @@ -36,3 +36,5 @@ foreach test : tests ), ) endforeach + + diff --git a/tests/monochrome_test/monochrome_test.cpp b/tests/monochrome_test/monochrome_test.cpp index 43b8342..2b5decf 100644 --- a/tests/monochrome_test/monochrome_test.cpp +++ b/tests/monochrome_test/monochrome_test.cpp @@ -22,50 +22,142 @@ #include TEST(basic_test, Monochrome_convertGrayscale_yieldsMonochrome) { - const std::vector pixels({0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, - 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00}); + const std::vector pixels( + {0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00}); const std::vector expected({0b10101010, 0b10101010}); - auto mono = ptprnt::graphics::Monochrome(pixels); + auto mono = ptprnt::graphics::Monochrome(pixels, 16, 1); auto out = mono.get(); - EXPECT_EQ(out, expected); + EXPECT_EQ(out.bytes, expected); } TEST(basic_test, Monochrome_convertInvertedGrayscale_yieldsInvertedMonochrome) { - const std::vector pixels({0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, - 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00}); + const std::vector pixels( + {0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00}); const std::vector expected({0b01010101, 0b01010101}); - auto mono = ptprnt::graphics::Monochrome(pixels); + auto mono = ptprnt::graphics::Monochrome(pixels, 16, 1); mono.invert(true); auto out = mono.get(); - EXPECT_EQ(out, expected); + EXPECT_EQ(out.bytes, expected); } TEST(basic_test, Monochrome_convertWithCustomThreshhold_yieldsMonochromeRespectingThreshhold) { - const std::vector pixels({0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11, - 0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11}); + const std::vector pixels( + {0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11, 0x0F, 0x11}); const std::vector expected({0b01010101, 0b01010101}); - auto mono = ptprnt::graphics::Monochrome(pixels); + auto mono = ptprnt::graphics::Monochrome(pixels, 16, 1); mono.setThreshold(16); auto out = mono.get(); - EXPECT_EQ(out, expected); + EXPECT_EQ(out.bytes, expected); } TEST(basic_test, Monochrome_convertNonAlignedPixels_spillsOverIntoNewByte) { // TODO: We need to find to access the vector without the possiblity of out-of-bounds access // Ideas: constexpr? compile time check? GTEST_SKIP() << "Skipping this test, as ASAN will halt as this is an out-of-bounds access"; - const std::vector pixels({0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, - 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF}); + const std::vector pixels( + {0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF}); const std::vector expected({0b10101010, 0b10101010, 0b10000000}); - auto mono = ptprnt::graphics::Monochrome(pixels); + auto mono = ptprnt::graphics::Monochrome(pixels, 17, 1); auto out = mono.get(); - EXPECT_EQ(out, expected); + EXPECT_EQ(out.bytes, expected); +} + +TEST(MonochromeData_test, MonochromeData_getMonochromeData_returnsStructWithCorrectData) { + const std::vector pixels({0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00}); + + auto mono = ptprnt::graphics::Monochrome(pixels, 8, 1); + auto monoData = mono.get(); + + EXPECT_EQ(monoData.bytes.size(), 1); + EXPECT_EQ(monoData.bytes[0], 0b10101010); + EXPECT_EQ(monoData.width, 8); + EXPECT_EQ(monoData.height, 1); + EXPECT_EQ(monoData.stride, 1); + EXPECT_EQ(monoData.orientation, ptprnt::graphics::Orientation::LANDSCAPE); +} + +TEST(MonochromeData_test, MonochromeData2x2_transformToPortrait_rotatesCorrectly) { + // Create a 2x2 image with a specific pattern + // Pixels are laid out row-major: row0_col0, row0_col1, row1_col0, ... + const std::vector pixels({0xFF, 0x00, 0x00, 0xFF}); + + auto mono = ptprnt::graphics::Monochrome(pixels, 2, 2); + auto monoData = mono.get(); + + monoData.transformTo(ptprnt::graphics::Orientation::PORTRAIT); + + // After 90Β° clockwise rotation: + // Original: β–ˆ . -> Rotated: . β–ˆ + // . β–ˆ β–ˆ . + EXPECT_EQ(monoData.width, 2); + EXPECT_EQ(monoData.height, 2); + EXPECT_EQ(monoData.orientation, ptprnt::graphics::Orientation::PORTRAIT); + + // check pixel data ...................................... x,y = value + EXPECT_EQ(monoData.getBit(0, 0), false); // 0,0 = white + EXPECT_EQ(monoData.getBit(1, 0), true); // 0,1 = black + EXPECT_EQ(monoData.getBit(0, 1), true); // 1,0 = black + EXPECT_EQ(monoData.getBit(1, 1), false); // 1,1 = white +} + +TEST(MonochromeData_test, MonochromeData3x2_transformToPortrait_rotatesCorrectly) { + // Create a 2x3 image with a specific pattern + // Pixels are laid out row-major: row0_col0, row0_col1, row0_col2, row1_col0, ... + const std::vector pixels({0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF}); + + auto mono = ptprnt::graphics::Monochrome(pixels, 3, 2); + auto monoData = mono.get(); + + monoData.transformTo(ptprnt::graphics::Orientation::PORTRAIT); + + // After 90Β° clockwise rotation: + // Original: β–ˆ . . -> Rotated: β–ˆ β–ˆ + // β–ˆ . β–ˆ . . + // β–ˆ . + EXPECT_EQ(monoData.width, 2); + EXPECT_EQ(monoData.height, 3); + EXPECT_EQ(monoData.orientation, ptprnt::graphics::Orientation::PORTRAIT); + + // check pixel data ...................................... x,y = value + EXPECT_EQ(monoData.getBit(0, 0), true); // 1,1 = black + EXPECT_EQ(monoData.getBit(1, 0), true); // 1,2 = black + EXPECT_EQ(monoData.getBit(0, 1), false); // 2,1 = white + EXPECT_EQ(monoData.getBit(1, 1), false); // 2,2 = white + EXPECT_EQ(monoData.getBit(0, 2), true); // 3,1 = black + EXPECT_EQ(monoData.getBit(1, 2), false); // 3,2 = white +} + +TEST(MonochromeData_test, MonochromeData3x2_transformToPortrait_rotatesCorrectlyCounterclockwise) { + // Create a 2x3 image with a specific pattern + // Pixels are laid out row-major: row0_col0, row0_col1, row0_col2, row1_col0, ... + const std::vector pixels({0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF}); + + auto mono = ptprnt::graphics::Monochrome(pixels, 3, 2); + auto monoData = mono.get(); + + monoData.transformTo(ptprnt::graphics::Orientation::PORTRAIT_FLIPPED); + + // After 90Β° anti-clockwise rotation: + // Original: β–ˆ . . -> Rotated: . β–ˆ + // β–ˆ . β–ˆ . . + // β–ˆ β–ˆ + EXPECT_EQ(monoData.width, 2); + EXPECT_EQ(monoData.height, 3); + EXPECT_EQ(monoData.orientation, ptprnt::graphics::Orientation::PORTRAIT_FLIPPED); + + // check pixel data ...................................... x,y = value + EXPECT_EQ(monoData.getBit(0, 0), false); // 1,1 = white + EXPECT_EQ(monoData.getBit(1, 0), true); // 1,2 = black + EXPECT_EQ(monoData.getBit(0, 1), false); // 2,1 = white + EXPECT_EQ(monoData.getBit(1, 1), false); // 2,2 = white + EXPECT_EQ(monoData.getBit(0, 2), true); // 3,1 = black + EXPECT_EQ(monoData.getBit(1, 2), true); // 3,2 = black } \ No newline at end of file diff --git a/update_copyright.sh b/update_copyright.sh new file mode 100755 index 0000000..0823e8d --- /dev/null +++ b/update_copyright.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# update_copyright.sh +# Updates copyright year ranges in a source file based on git history +# +# Usage: ./update_copyright.sh [--dry-run] +# +# Examples: +# # Update a single file +# ./update_copyright.sh src/main.cpp +# +# # Dry-run on a single file +# ./update_copyright.sh --dry-run src/main.cpp +# +# # Update all C++ files using find -exec +# find src \( -name "*.cpp" -o -name "*.hpp" \) -exec ./update_copyright.sh {} \; +# +# # Dry-run on all C++ files +# find src \( -name "*.cpp" -o -name "*.hpp" \) -exec ./update_copyright.sh --dry-run {} \; + +set -e + +# Check for dry-run flag +DRY_RUN=false +FILE="" + +if [ "$1" = "--dry-run" ]; then + DRY_RUN=true + FILE="$2" +elif [ "$1" = "--help" ] || [ "$1" = "-h" ]; then + grep "^#" "$0" | sed 's/^# \?//' + exit 0 +else + FILE="$1" +fi + +# Check if file argument provided +if [ -z "$FILE" ]; then + echo "Error: No file specified" + echo "Usage: $0 [--dry-run] " + echo "Run '$0 --help' for more information" + exit 1 +fi + +# Check if file exists +if [ ! -f "$FILE" ]; then + echo "Error: File not found: $FILE" + exit 1 +fi + +# Get the repository root +REPO_ROOT=$(git rev-parse --show-toplevel) +cd "$REPO_ROOT" + +# Copyright holder name +COPYRIGHT_HOLDER="Moritz Martinius" + +# Function to get first and last commit years for a file +get_years_for_file() { + local file="$1" + + # Get the year of the first commit that touched this file + first_year=$(git log --follow --format=%ad --date=format:%Y --reverse "$file" 2>/dev/null | head -1) + + # Get the year of the last commit that touched this file + last_year=$(git log --follow --format=%ad --date=format:%Y -1 "$file" 2>/dev/null) + + # If file is not in git, use current year + if [ -z "$first_year" ]; then + first_year=$(date +%Y) + last_year=$(date +%Y) + fi + + echo "$first_year $last_year" +} + +# Get years from git history +read -r first_year last_year <<< "$(get_years_for_file "$FILE")" + +# Determine the copyright year string +if [ "$first_year" = "$last_year" ]; then + year_string="$first_year" +else + year_string="$first_year-$last_year" +fi + +# Check if file has a copyright notice +if ! grep -q "Copyright (C)" "$FILE"; then + echo "No copyright notice found in $FILE, skipping" + exit 0 +fi + +if [ "$DRY_RUN" = true ]; then + # Just show what would be changed + current_year=$(grep "Copyright (C)" "$FILE" | sed -n 's/.*Copyright (C) \([0-9]\{4\}\(-[0-9]\{4\}\)\?\).*/\1/p' | head -1) + if [ "$current_year" != "$year_string" ]; then + echo "Would update $FILE: $current_year β†’ $year_string" + else + echo "No change needed for $FILE (already $year_string)" + fi +else + # Update the copyright line + # Matches patterns like "Copyright (C) 2023 Moritz Martinius" or "Copyright (C) 2023-2024 Moritz Martinius" + # Handle variable whitespace between year and name + + # Get current year from file for comparison + current_year=$(grep "Copyright (C)" "$FILE" | sed -n 's/.*Copyright (C) \([0-9]\{4\}\(-[0-9]\{4\}\)\?\).*/\1/p' | head -1) + + if [ "$current_year" = "$year_string" ]; then + echo "No changes needed for $FILE (already $year_string)" + else + sed -i "s/Copyright (C) [0-9]\{4\}\(-[0-9]\{4\}\)\? \+$COPYRIGHT_HOLDER/Copyright (C) $year_string $COPYRIGHT_HOLDER/" "$FILE" + echo "βœ“ Updated $FILE: $current_year β†’ $year_string" + fi +fi