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