1 Commits

Author SHA1 Message Date
69810e1c5c Refactor cli setup and printer selection logic into separate classes
All checks were successful
Build ptprnt / build (push) Successful in 3m49s
2025-10-13 20:29:14 +02:00
54 changed files with 433 additions and 2203 deletions

View File

@@ -14,7 +14,7 @@ jobs:
- name: install meson - name: install meson
run: apt-get -yq install meson run: apt-get -yq install meson
- name: Install build dependencies - name: Install build dependencies
run: apt-get -yq install libusb-1.0-0-dev libpango1.0-dev libcairo2-dev gcovr run: apt-get -yq install libusb-1.0-0-dev libspdlog-dev libfmt-dev libpango1.0-dev libcairo2-dev gcovr
- name: get build environment versions - name: get build environment versions
run: | run: |
echo "=== Start meson version ===" echo "=== Start meson version ==="
@@ -32,25 +32,23 @@ jobs:
echo "=== Start dependency package version ===" echo "=== Start dependency package version ==="
apt-cache policy libpango1.0-dev apt-cache policy libpango1.0-dev
apt-cache policy libcairo2-dev apt-cache policy libcairo2-dev
apt-cache policy libfmt-dev
apt-cache policy libspdlog-dev
apt-cache policy libusb-1.0-0-dev apt-cache policy libusb-1.0-0-dev
echo "=== End dependency package version ===" echo "=== End dependency package version ==="
- name: Build ptprnt debug - name: setup builddir
run: scripts/build.sh debug --coverage --test run: meson setup builddir -Db_coverage=true
- name: Generate coverage - name: build and test dist package
run: scripts/generate_coverage.sh --text run: ninja -C builddir dist
- name: run unit tests
run: ninja -C builddir test
- name: calculate coverage
run: ninja -C builddir coverage-text
- name: Coverage report - name: Coverage report
run: cat ./coverageReport/coverage.txt run: cat ./builddir/meson-logs/coverage.txt
- name: build release - name: upload dist package
run: scripts/build.sh release
- name: upload release binary
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: ptprnt name: ptprnt-dist
path: ./builddir/ptprnt path: ./builddir/meson-dist/*
if-no-files-found: error
- name: upload coverage report
uses: actions/upload-artifact@v3
with:
name: coverage.txt
path: ./coverageReport/coverage.txt
if-no-files-found: error if-no-files-found: error

16
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# Folder # Folder
builddir*/ builddir/
ptouch-print/
subprojects/* subprojects/*
.cache/ .cache/
coverageReport/ coverageReport/
@@ -11,16 +12,3 @@ ptprnt.log
!.vscode/c_cpp_properties.json !.vscode/c_cpp_properties.json
!.vscode/settings.json !.vscode/settings.json
!.vscode/launch.json !.vscode/launch.json
# ignore generated testlabels
fakelabel_*.png
# ignore package capture files
*.pcapng
*.pcapng.gz
*.pcap
*.pcap.gz
# ignore coverage temporaries
*.gcov.json
*.gcov.json.gz

View File

@@ -88,14 +88,9 @@
}, },
"clangd.onConfigChanged": "restart", "clangd.onConfigChanged": "restart",
"cSpell.words": [ "cSpell.words": [
"fakelabel",
"fontsize", "fontsize",
"gboolean",
"gint",
"gobject",
"halign", "halign",
"libusb", "libusb",
"ptrnt", "ptrnt"
"strv"
], ],
} }

View File

@@ -1,70 +0,0 @@
# Changelog
All notable changes to ptprnt will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.0] - v0.2.0
### Added
- Multi-label support - print multiple labels in sequence
- LabelBuilder API with fluent interface for constructing labels
- FakePrinter driver for testing without hardware (outputs PNG files)
- PrinterService core service for printer operations
- CliParser component with ICliParser interface
- ICairoWrapper interface for testable graphics rendering
- MockCairoWrapper for unit testing
- Pre-commit hook for automatic copyright updates
- USB trace mode for debugging (`-Dusb_trace_only=true`)
### Changed
- **Major refactoring**: Reorganized codebase into layered architecture
- Application layer: PtouchPrint, CliParser, PrinterService
- Printer drivers: Moved to `src/printers/` with factory pattern
- Graphics system: Added builder pattern and Cairo abstraction
- Core services: Separated into `src/core/` directory
- Label class now uses dependency injection for Cairo/Pango
- CLI parsing separated from main application logic
- Updated dependencies to latest versions
- Improved project documentation (README.md, CLAUDE.md)
### Fixed
- Label corruption issues resolved
- Printer info retrieval bugs
- USB attachment logic
- Multiple lines handling in labels
- Printer selection logic
## [0.1.0] - 2024
### Added
- Initial release of ptprnt
- Basic label printing functionality for Brother P-touch P700 series
- Pango/Cairo-based text rendering
- USB device communication via libusb-1.0
- Template-based Bitmap class supporting multiple pixel formats (ALPHA8, RGBX8, RGBA8, ARGB8)
- Monochrome bitmap conversion for printer output
- PrinterDriverFactory for creating printer instances
- USB device abstraction layer (IUsbDevice, UsbDevice, UsbDeviceFactory)
- Command-line interface using CLI11
- Logging with spdlog and file output
- Unit tests using GoogleTest
- Code coverage reporting with gcovr
- Meson build system with C++20 support
- CI/CD pipeline with automated testing
### Core Components (v0.1.0)
- PtouchPrint: Main application orchestrator
- P700Printer: Brother P-touch P700 driver implementation
- Bitmap: Template-based image storage
- Label: Text rendering with Pango/Cairo
- Monochrome: Bitmap to monochrome conversion
- UsbDevice: libusb wrapper for device communication
---
## Project Origins
ptprnt is a modern C++20 rewrite of [ptouch-print](https://git.familie-radermacher.ch/linux/ptouch-print.git).
All credits for reverse engineering the Brother P-touch USB protocol go to Dominic Rademacher.

263
README.md
View File

@@ -1,252 +1,57 @@
# ptprnt # ptprnt
A command-line label printer driver for Brother P-touch printers on Linux. Prints text labels directly from your terminal. This is a rewrite of [ptouch-print](https://git.familie-radermacher.ch/linux/ptouch-print.git) as a toy project for my personal amusement. The currently available solutions are good enough for generating labels, but i wanted to explore libusb and maybe improve the functionality of my label printer. All credits for reverse engineering the USB commands to Dominic Rademacher.
## Example ## Dependencies
This project requires:
- spdlog
- libusb
- pango
- cairo
- meson
- gtest (optional, for testing, will be installed by meson)
- gcov (optional, for coverage reports)
Too print a label, provide your text and optionally a font and a font size. This command will print the label below on a Brother P-Touch P700: Install dependencies on Arch Linux
``` bash
```bash pacman -S libusb spdlog pango cairo meson gcovr
ptprnt --font "NotoMono Nerd Font" --fontsize 32 --text "🖶 ptprnt v0.2.0 🥰"
``` ```
![Printed label that proudly reads 🖶 ptprnt v0.2.0 🥰](docs/assets/label_print_v0_2_0.jpg) Install dependencies on Debian/Ubuntu
``` bash
## Quick Start apt-get install libusb-1.0-0-dev libspdlog-dev libfmt-dev libpango1.0-dev libcairo2-dev meson gcovr
## Quick Start
**Binary dependencies**
Arch Linux:
```bash
pacman -S pango cairo libusb
``` ```
Debian/Ubuntu: ## Build
Clone the repository and simply let meson do the heavy lifting.
```bash ```bash
apt install libpangocairo-1.0-0 libusb-1.0-0
```
**Build dependencies:**
Arch Linux:
```bash
pacman -S libusb spdlog pango cairo meson
```
Debian/Ubuntu:
```bash
apt install libusb-1.0-0-dev libspdlog-dev libfmt-dev libpango1.0-dev libcairo2-dev meson
```
Note: spdlog is built as a subproject and statically linked, so it's not required as a system dependency.
**Build and run:**
Clone this repository first and enter the directory. Then build:
```bash
# Using the build script (recommended)
./scripts/build.sh release
builddir/ptprnt --help
# Or manually with meson
meson setup builddir meson setup builddir
```
If you want to generate coverage reports, enable them via the command line switch
```bash
meson setup builddir -Db_coverage=true
```
Rebuild by simply invoking ninja
```bash
ninja -C builddir ninja -C builddir
builddir/ptprnt --help
``` ```
## Usage ## Run
Run the binary from your builddir
### Basic Text Printing
Print a simple label with text:
```bash ```bash
ptprnt --text "Hello World" builddir/ptprnt
``` ```
### Formatting Options ## Test
Testing is done via gtest. To run your test simply invoke ninja with the "test" target.
Control the appearance of your labels with these options:
- `--font FONT_NAME` - Set the font (e.g., "NotoMono Nerd Font", "DejaVu Sans")
- `--fontsize SIZE` - Set font size in points (default: 24)
- `--halign ALIGNMENT` - Horizontal alignment: `left`, `center`, `right` (default: center)
- `--valign ALIGNMENT` - Vertical alignment: `top`, `center`, `bottom` (default: center)
**Example with formatting:**
```bash ```bash
ptprnt --font "DejaVu Sans Mono" --fontsize 28 --halign left --text "Left aligned text"
```
### Multiple Text Elements
You can add multiple text elements to a single label. Formatting options apply to all subsequent `--text` arguments until changed:
```bash
ptprnt \
--font "DejaVu Sans" --fontsize 32 --text "Large Title" \
--fontsize 18 --text "Smaller subtitle"
```
### Multiple Labels (Stitching)
Create multiple labels that will be stitched together horizontally using the `--new` flag:
```bash
ptprnt \
--text "Label 1" \
--new \
--text "Label 2" \
--new \
--text "Label 3"
```
Each `--new` starts a fresh label. The labels are automatically stitched together with 60 pixels of spacing.
**Example - Creating a series of address labels:**
```bash
ptprnt \
--font "DejaVu Sans" --fontsize 20 \
--text "Peter Lustig" --text "Am Bauwagen 1" \
--new \
--text "Donald Duck" --text "Blumenstraße 13" \
--new \
--text "Homer Simpson" --text "742 Evergreen Terrace"
```
**Example - Mixed formatting across labels:**
```bash
ptprnt \
--fontsize 32 --text "BIG" \
--new \
--fontsize 16 --text "small" \
--new \
--fontsize 24 --text "medium"
```
### Printer Selection
By default, ptprnt auto-detects your printer. You can explicitly select a printer:
```bash
ptprnt --printer P700 --text "Hello"
```
List all available printer drivers:
```bash
ptprnt --list-all-drivers
```
### Testing with Fake Printer
Before printing to your real printer, you can test your label output using the built-in fake printer. This generates a PNG image file instead of printing:
```bash
ptprnt --printer FakePrinter --text "Test Label"
```
This will create a file named `fakelabel_YYYYMMDD_HHMMSS.png` in your current directory with a preview of your label. Use this to:
- Verify text formatting and layout
- Test multi-label stitching
- Preview before wasting label tape
**Example workflow:**
```bash
# First, test your label design
ptprnt --printer FakePrinter \
--font "DejaVu Sans" --fontsize 28 \
--text "Test Design" --text "Check Layout"
# View the generated PNG file to verify
# If satisfied, print to real printer
ptprnt --printer P700 \
--font "DejaVu Sans" --fontsize 28 \
--text "Test Design" --text "Check Layout"
```
### Verbose Output
Enable detailed logging for debugging:
```bash
ptprnt --verbose --text "Debug mode"
```
Enable USB trace to see raw USB communication:
```bash
ptprnt --trace --text "USB trace mode"
```
### Complete Example
An example using a mixed bag of features:
```bash
ptprnt \
--verbose \
--printer P700 \
--font "NotoMono Nerd Font" \
--fontsize 28 --halign center --text "Product Label" \
--fontsize 20 --text "SKU: 12345" \
--new \
--fontsize 24 --text "Backup Label" \
--fontsize 18 --text "Date: 2025-10-16"
```
## Supported Printers
(I need more printers for verification 😉)
- Brother P-touch P700 series
## Developer info
This is a modern C++20 rewrite of [ptouch-print](https://git.familie-radermacher.ch/linux/ptouch-print.git). Credits to Dominic Rademacher for reverse engineering the USB protocol.
**Build script:**
```bash
# Release build (tests disabled for faster builds)
./scripts/build.sh release
# Debug build (tests enabled)
./scripts/build.sh debug
# Debug with tests
./scripts/build.sh debug --test
# Debug with coverage
./scripts/build.sh debug --coverage
# Clean all build directories
./scripts/build.sh clean
# Show all options
./scripts/build.sh --help
```
**Note:** Tests are only built in debug mode to keep release builds fast and small. Release builds do not include test binaries or link against gtest/gmock.
**Running tests:**
```bash
# Using build script
./scripts/build.sh --test
# Or manually
ninja -C builddir test ninja -C builddir test
``` ```
**Coverage reports:** Coverage reports can be generated via gcov if you enabled them (see Build section) by building the `coverage-text` target.
```bash
# 1. Build with coverage enabled and run tests
./scripts/build.sh debug --coverage --test
# 2. Generate coverage reports
./scripts/generate_coverage.sh # All formats (html, xml, text)
./scripts/generate_coverage.sh --html # HTML only
./scripts/generate_coverage.sh --text # Text only
./scripts/generate_coverage.sh --xml # XML only (for CI/CD)
./scripts/generate_coverage.sh --html --xml # HTML and XML
```
## License ## License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 KiB

27
generate_coverage.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
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"
ninja -C builddir
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
rm *.gcov

View File

@@ -1,43 +0,0 @@
# Git Hooks
This directory contains git hooks for the ptprnt repository.
## Installation
To install the hooks, run:
```bash
./hooks/install_hooks.sh
```
This will copy all hooks from this directory to `.git/hooks/` and make them executable.
## Available Hooks
### pre-commit
The pre-commit hook automatically updates copyright headers in source files before each commit.
**What it does:**
- Runs `scripts/update_copyright.sh` to update copyright years in source files
- Automatically re-stages any modified files
- Ensures copyright headers are always up-to-date
**Requirements:**
- `scripts/update_copyright.sh` must exist and be executable
## Skipping Hooks
If you need to skip the pre-commit hook for a specific commit (not recommended), use:
```bash
git commit --no-verify
```
## Uninstalling
To remove a hook, simply delete it from `.git/hooks/`:
```bash
rm .git/hooks/pre-commit
```

View File

@@ -1,96 +0,0 @@
#!/bin/bash
# Install git hooks for ptprnt repository
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get the root directory of the git repository
ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null)
if [ -z "$ROOT_DIR" ]; then
echo -e "${RED}Error: Not in a git repository${NC}"
exit 1
fi
HOOKS_SOURCE_DIR="$ROOT_DIR/hooks"
HOOKS_TARGET_DIR="$ROOT_DIR/.git/hooks"
echo "Installing git hooks..."
echo " Source: $HOOKS_SOURCE_DIR"
echo " Target: $HOOKS_TARGET_DIR"
echo ""
# Check if hooks directory exists
if [ ! -d "$HOOKS_SOURCE_DIR" ]; then
echo -e "${RED}Error: Hooks source directory not found: $HOOKS_SOURCE_DIR${NC}"
exit 1
fi
# Check if .git/hooks directory exists
if [ ! -d "$HOOKS_TARGET_DIR" ]; then
echo -e "${RED}Error: Git hooks directory not found: $HOOKS_TARGET_DIR${NC}"
exit 1
fi
# Install each hook
installed_count=0
for hook_file in "$HOOKS_SOURCE_DIR"/*; do
# Skip the install script itself
if [[ "$(basename "$hook_file")" == "install_hooks.sh" ]]; then
continue
fi
# Skip if not a file
if [ ! -f "$hook_file" ]; then
continue
fi
hook_name=$(basename "$hook_file")
target_file="$HOOKS_TARGET_DIR/$hook_name"
# Check if hook already exists
if [ -f "$target_file" ]; then
echo -e "${YELLOW}Warning: Hook already exists: $hook_name${NC}"
read -p " Overwrite? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo " Skipped: $hook_name"
continue
fi
fi
# Copy and make executable
cp "$hook_file" "$target_file"
chmod +x "$target_file"
echo -e "${GREEN}${NC} Installed: $hook_name"
((installed_count++))
done
echo ""
if [ $installed_count -eq 0 ]; then
echo -e "${YELLOW}No hooks were installed${NC}"
else
echo -e "${GREEN}Successfully installed $installed_count hook(s)${NC}"
fi
# Verify update_copyright.sh exists and is executable
if [ -f "$ROOT_DIR/scripts/update_copyright.sh" ]; then
if [ ! -x "$ROOT_DIR/scripts/update_copyright.sh" ]; then
echo ""
echo -e "${YELLOW}Making update_copyright.sh executable...${NC}"
chmod +x "$ROOT_DIR/scripts/update_copyright.sh"
echo -e "${GREEN}${NC} update_copyright.sh is now executable"
fi
else
echo ""
echo -e "${YELLOW}Warning: scripts/update_copyright.sh not found${NC}"
echo " The pre-commit hook requires this script to function properly"
fi
echo ""
echo "Hook installation complete!"

View File

@@ -1,43 +0,0 @@
#!/bin/bash
# Pre-commit hook to update copyright headers
# Get the root directory of the git repository
ROOT_DIR=$(git rev-parse --show-toplevel)
# Check if update_copyright.sh exists and is executable
if [ ! -x "$ROOT_DIR/scripts/update_copyright.sh" ]; then
echo "Warning: scripts/update_copyright.sh not found or not executable"
exit 1
fi
# Get list of staged C++ source files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|hpp|h|c|cc)$' || true)
if [ -z "$STAGED_FILES" ]; then
# No C++ files staged, nothing to do
exit 0
fi
echo "Updating copyright headers for staged files..."
# Update copyright for each staged file
updated=0
for file in $STAGED_FILES; do
if [ -f "$ROOT_DIR/$file" ]; then
# Run update_copyright.sh on the file
if "$ROOT_DIR/scripts/update_copyright.sh" "$ROOT_DIR/$file" > /dev/null 2>&1; then
# Re-stage the file if it was modified
git add "$ROOT_DIR/$file"
echo " ✓ Updated: $file"
((updated++))
fi
fi
done
if [ $updated -gt 0 ]; then
echo "Updated copyright headers in $updated file(s)"
else
echo "No copyright headers needed updating"
fi
exit 0

View File

@@ -1,11 +1,11 @@
project( project(
'ptprnt', 'ptprnt',
'cpp', 'cpp',
version: 'v0.2.0-' version: 'v0.1.0-' + run_command(
+ run_command(
'git', 'git',
'rev-parse', 'rev-parse',
'--short', 'HEAD', '--short',
'HEAD',
check: true, check: true,
).stdout().strip(), ).stdout().strip(),
license: 'GPLv3', license: 'GPLv3',
@@ -20,16 +20,10 @@ project(
) )
usb_dep = dependency('libusb-1.0') usb_dep = dependency('libusb-1.0')
log_dep = dependency('spdlog')
fmt_dep = dependency('fmt')
pangocairo_dep = dependency('pangocairo') pangocairo_dep = dependency('pangocairo')
# spdlog with std::format (C++20) - static library
spdlog_proj = subproject('spdlog', default_options: ['std_format=enabled', 'default_library=static', 'compile_library=true'])
log_dep = spdlog_proj.get_variable('spdlog_dep')
if not log_dep.found()
error('spdlog not found, can not proceed')
endif
# CLI11 # CLI11
cli11_proj = subproject('cli11') cli11_proj = subproject('cli11')
cli11_dep = cli11_proj.get_variable('CLI11_dep') cli11_dep = cli11_proj.get_variable('CLI11_dep')
@@ -47,36 +41,27 @@ cpp_args = ['-DPROJ_VERSION="' + meson.project_version() + '"']
# USB trace mode option (for debugging without sending to hardware) # USB trace mode option (for debugging without sending to hardware)
if get_option('usb_trace_only') if get_option('usb_trace_only')
cpp_args += ['-DUSB_TRACE_ONLY'] cpp_args += ['-DUSB_TRACE_ONLY']
message( message('USB_TRACE_ONLY enabled: USB data will be logged but not sent to device')
'USB_TRACE_ONLY enabled: USB data will be logged but not sent to device',
)
endif endif
ptprnt_exe = executable( ptprnt_exe = executable(
'ptprnt', 'ptprnt',
'src/main.cpp', 'src/main.cpp',
install: true, install: true,
dependencies: [usb_dep, log_dep, pangocairo_dep, cli11_dep], dependencies: [usb_dep, log_dep, fmt_dep, pangocairo_dep, cli11_dep],
include_directories: incdir, include_directories: incdir,
sources: [ptprnt_srcs], sources: [ptprnt_srcs],
cpp_args: cpp_args, cpp_args: cpp_args,
) )
### Unit tests ### Unit tests
# Only build tests for debug builds or when explicitly enabled
build_tests = get_option('buildtype') == 'debug' or get_option('build_tests')
if build_tests # GTest
# GTest and GMock gtest_proj = subproject('gtest')
gtest_proj = subproject('gtest') gtest_dep = gtest_proj.get_variable('gtest_main_dep')
gtest_dep = gtest_proj.get_variable('gtest_main_dep') if not gtest_dep.found()
gmock_dep = gtest_proj.get_variable('gmock_main_dep') error('MESON_SKIP_TEST: gtest not installed.')
if not gtest_dep.found()
error('MESON_SKIP_TEST: gtest not installed.')
endif
subdir('tests')
message('Tests enabled (buildtype=' + get_option('buildtype') + ')')
else
message('Tests disabled (use debug build or -Dbuild_tests=true to enable)')
endif endif
subdir('tests')

View File

@@ -2,8 +2,3 @@ option('usb_trace_only',
type: 'boolean', type: 'boolean',
value: false, value: false,
description: 'Enable USB trace mode: log USB data without sending to device (saves label tape during debugging)') description: 'Enable USB trace mode: log USB data without sending to device (saves label tape during debugging)')
option('build_tests',
type: 'boolean',
value: false,
description: 'Build unit tests (automatically enabled for debug builds)')

View File

@@ -1,162 +0,0 @@
#!/bin/bash
# Build script for ptprnt - simplifies common build configurations
# Usage: ./scripts/build.sh [release|debug|clean] [options]
set -e
SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
PROJECT_ROOT="${SCRIPT_PATH}/.."
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_usage() {
echo "Usage: $0 [build-type] [options]"
echo ""
echo "Build Types:"
echo " release Build optimized release version (default)"
echo " debug Build debug version with symbols"
echo " clean Clean build directories"
echo ""
echo "Options:"
echo " --coverage Enable coverage reporting (debug builds only)"
echo " --reconfigure Force reconfiguration"
echo " --test Run tests after building"
echo " -j N Use N parallel jobs (default: auto)"
echo ""
echo "Examples:"
echo " $0 # Build release"
echo " $0 debug --test # Build debug and run tests"
echo " $0 debug --coverage # Build debug with coverage"
echo " $0 clean # Clean all build directories"
}
# Default values
BUILD_TYPE="release"
BUILDDIR="builddir"
COVERAGE=false
RECONFIGURE=false
RUN_TESTS=false
JOBS=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
release|debug|clean)
BUILD_TYPE="$1"
shift
;;
--coverage)
COVERAGE=true
shift
;;
--reconfigure)
RECONFIGURE=true
shift
;;
--test)
RUN_TESTS=true
shift
;;
-j)
JOBS="-j $2"
shift 2
;;
-h|--help)
print_usage
exit 0
;;
*)
echo -e "${RED}Error: Unknown option: $1${NC}"
print_usage
exit 1
;;
esac
done
cd "${PROJECT_ROOT}"
# Handle clean
if [[ "${BUILD_TYPE}" == "clean" ]]; then
echo -e "${YELLOW}Cleaning build directories...${NC}"
rm -rf builddir builddir-debug
echo -e "${GREEN}Clean complete!${NC}"
exit 0
fi
# Set build directory and options based on build type
if [[ "${BUILD_TYPE}" == "debug" ]]; then
BUILDDIR="builddir-debug"
MESON_OPTS="--buildtype=debug"
if [[ "${COVERAGE}" == true ]]; then
MESON_OPTS="${MESON_OPTS} -Db_coverage=true"
echo -e "${BLUE}Building debug with coverage enabled${NC}"
else
echo -e "${BLUE}Building debug version${NC}"
fi
else
BUILDDIR="builddir"
MESON_OPTS="--buildtype=release"
if [[ "${COVERAGE}" == true ]]; then
echo -e "${YELLOW}Warning: Coverage is only supported for debug builds, ignoring --coverage${NC}"
fi
if [[ "${RUN_TESTS}" == true ]]; then
echo -e "${YELLOW}Warning: Tests are not built for release builds (use debug build for testing)${NC}"
RUN_TESTS=false
fi
echo -e "${BLUE}Building release version (tests disabled)${NC}"
fi
# Setup or reconfigure build directory
if [[ ! -d "${BUILDDIR}" ]] || [[ "${RECONFIGURE}" == true ]]; then
if [[ "${RECONFIGURE}" == true ]]; then
echo -e "${YELLOW}Reconfiguring build...${NC}"
meson setup "${BUILDDIR}" ${MESON_OPTS} --wipe --reconfigure
else
echo -e "${YELLOW}Setting up build directory...${NC}"
meson setup "${BUILDDIR}" ${MESON_OPTS}
fi
fi
# Build
echo -e "${YELLOW}Building...${NC}"
ninja -C "${BUILDDIR}" ${JOBS}
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}Build successful!${NC}"
echo -e "Binary: ${BUILDDIR}/ptprnt"
else
echo -e "${RED}Build failed!${NC}"
exit 1
fi
# Run tests if requested
if [[ "${RUN_TESTS}" == true ]]; then
echo -e "${YELLOW}Running tests...${NC}"
ninja -C "${BUILDDIR}" test
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}All tests passed!${NC}"
else
echo -e "${RED}Tests failed!${NC}"
exit 1
fi
fi
# Show binary info
echo ""
echo -e "${BLUE}Build Information:${NC}"
echo " Build type: ${BUILD_TYPE}"
echo " Build dir: ${BUILDDIR}"
echo " Binary: $(ls -lh ${BUILDDIR}/ptprnt | awk '{print $5, $9}')"
echo ""
echo -e "${GREEN}Done!${NC}"

View File

@@ -1,195 +0,0 @@
#!/bin/bash
# Coverage report generator for ptprnt
# Usage: ./scripts/generate_coverage.sh [options]
set -e
SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
PROJECT_ROOT="${SCRIPT_PATH}/.."
# Output paths
HTML_COV_PATH="coverageReport/html"
XML_COV_PATH="coverageReport/xml"
TEXT_COV_PATH="coverageReport"
HTML_START_FILE="index.html"
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values - all formats enabled by default
GENERATE_HTML=false
GENERATE_XML=false
GENERATE_TEXT=false
BUILDDIR="builddir-debug"
# Common gcovr options
GCOVR_OPTS="--filter src --root ."
print_usage() {
echo "Usage: $0 [options]"
echo ""
echo "Coverage Report Generator - generates coverage reports from existing coverage data"
echo ""
echo "Prerequisites:"
echo " Build with coverage enabled first:"
echo " ./scripts/build.sh debug --coverage --test"
echo ""
echo "Format Options (if none specified, all formats are generated):"
echo " --html Generate HTML coverage report"
echo " --xml Generate XML coverage report (for CI/CD)"
echo " --text Generate text coverage report (terminal output)"
echo ""
echo "Build Options:"
echo " --builddir DIR Use custom build directory (default: builddir-debug)"
echo ""
echo "Other Options:"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Generate all formats (html, xml, text)"
echo " $0 --html # Generate only HTML report"
echo " $0 --html --text # Generate HTML and text reports"
echo " $0 --xml # Generate XML for CI/CD"
echo " $0 --builddir builddir # Use release build directory"
}
# Parse arguments
# If no format arguments provided, enable all
if [[ $# -eq 0 ]]; then
GENERATE_HTML=true
GENERATE_XML=true
GENERATE_TEXT=true
fi
while [[ $# -gt 0 ]]; do
case $1 in
--html)
GENERATE_HTML=true
shift
;;
--xml)
GENERATE_XML=true
shift
;;
--text)
GENERATE_TEXT=true
shift
;;
--builddir)
BUILDDIR="$2"
shift 2
;;
-h|--help)
print_usage
exit 0
;;
*)
echo -e "${RED}Error: Unknown option: $1${NC}"
print_usage
exit 1
;;
esac
done
# Check if any format was selected when arguments were provided
if [[ $GENERATE_HTML == false && $GENERATE_XML == false && $GENERATE_TEXT == false ]]; then
echo -e "${RED}Error: No output format specified. Use --html, --xml, and/or --text${NC}"
print_usage
exit 1
fi
cd "${PROJECT_ROOT}"
# Check if build directory exists
if [[ ! -d "${BUILDDIR}" ]]; then
echo -e "${RED}Error: Build directory '${BUILDDIR}' does not exist${NC}"
echo ""
echo "Build with coverage enabled first:"
echo " ./scripts/build.sh debug --coverage --test"
exit 1
fi
# Check if coverage data exists by looking for .gcda files
if ! find "${BUILDDIR}" -name "*.gcda" -print -quit | grep -q .; then
echo -e "${RED}Error: No coverage data found in '${BUILDDIR}'${NC}"
echo ""
echo "Make sure you built with coverage enabled and ran the tests:"
echo " ./scripts/build.sh debug --coverage --test"
exit 1
fi
echo -e "${BLUE}Generating Coverage report for ptprnt${NC}"
echo "Build directory: ${BUILDDIR}"
echo "Formats: $(${GENERATE_HTML} && echo -n "html ")$(${GENERATE_XML} && echo -n "xml ")$(${GENERATE_TEXT} && echo -n "text")"
echo ""
# Check if gcovr is available
if ! command -v gcovr &> /dev/null; then
echo -e "${RED}Error: gcovr is not installed${NC}"
exit 1
fi
if [[ "${GENERATE_HTML}" == true ]]; then
echo -e "${YELLOW}Generating HTML coverage report...${NC}"
mkdir -p "${HTML_COV_PATH}"
gcovr ${GCOVR_OPTS} \
--html --html-details --html-syntax-highlighting \
--output "${HTML_COV_PATH}/${HTML_START_FILE}"
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}✓ HTML report generated${NC}"
echo ""
echo -e "${BLUE}To view HTML report, open:${NC}"
echo " file://${SCRIPT_PATH}/../${HTML_COV_PATH}/${HTML_START_FILE}"
else
echo -e "${RED}✗ HTML report failed${NC}"
fi
fi
if [[ "${GENERATE_XML}" == true ]]; then
echo -e "${YELLOW}Generating XML coverage report...${NC}"
mkdir -p "${XML_COV_PATH}"
gcovr ${GCOVR_OPTS} \
--xml-pretty \
--output "${XML_COV_PATH}/cov.xml"
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}✓ XML report generated${NC}"
echo ""
echo -e "${BLUE}To view XML report, open:${NC}"
echo " file://${SCRIPT_PATH}/../${XML_COV_PATH}/cov.xml"
else
echo -e "${RED}✗ XML report failed${NC}"
fi
fi
if [[ "${GENERATE_TEXT}" == true ]]; then
echo -e "${YELLOW}Generating text coverage report...${NC}"
mkdir -p "${TEXT_COV_PATH}"
# Save to file
gcovr ${GCOVR_OPTS} --output "${TEXT_COV_PATH}/coverage.txt"
if [[ $? -eq 0 ]]; then
echo -e "${GREEN}✓ Text report generated${NC}"
echo ""
echo -e "${BLUE}To view TXT report, open:${NC}"
echo "file://${SCRIPT_PATH}/../${TEXT_COV_PATH}/coverage.txt"
else
echo -e "${RED}✗ Text report failed${NC}"
fi
fi
# Clean up gcov files
rm -f *.gcov 2>/dev/null
# Summary
echo ""
echo -e "${GREEN}Coverage reports generated successfully!${NC}"
echo ""

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2024-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2024-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -21,7 +21,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "printers/interface/IPrinterDriver.hpp" #include "interface/IPrinterDriver.hpp"
#include "libusbwrap/LibUsbTypes.hpp" #include "libusbwrap/LibUsbTypes.hpp"
namespace ptprnt { namespace ptprnt {

View File

@@ -18,29 +18,23 @@
*/ */
#include "PtouchPrint.hpp" #include "PtouchPrint.hpp"
#include <format> #include <fmt/core.h>
#include <iostream>
#include <spdlog/sinks/basic_file_sink.h> #include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h> #include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "PrinterDriverFactory.hpp"
#include "cli/CliParser.hpp" #include "cli/CliParser.hpp"
#include "cli/interface/ICliParser.hpp"
#include "constants.hpp" #include "constants.hpp"
#include "core/PrinterDriverFactory.hpp"
#include "core/PrinterService.hpp" #include "core/PrinterService.hpp"
#include "core/interface/IPrinterService.hpp"
#include "graphics/LabelBuilder.hpp" #include "graphics/LabelBuilder.hpp"
namespace ptprnt { namespace ptprnt {
PtouchPrint::PtouchPrint(const char* versionString) PtouchPrint::PtouchPrint(const char* versionString)
: PtouchPrint(versionString, std::make_unique<cli::CliParser>(ptprnt::APP_DESC, versionString), : mVersionString(versionString),
std::make_unique<core::PrinterService>()) {} mCliParser(std::make_unique<cli::CliParser>(ptprnt::APP_DESC, versionString)),
mPrinterService(std::make_unique<core::PrinterService>()) {}
PtouchPrint::PtouchPrint(const char* versionString, std::unique_ptr<cli::ICliParser> cliParser,
std::unique_ptr<core::IPrinterService> printerService)
: mVersionString(versionString), mCliParser(std::move(cliParser)), mPrinterService(std::move(printerService)) {}
PtouchPrint::~PtouchPrint() = default; PtouchPrint::~PtouchPrint() = default;
@@ -103,13 +97,13 @@ void PtouchPrint::setupLogger() {
bool PtouchPrint::handleListDrivers() { bool PtouchPrint::handleListDrivers() {
auto driverFactory = std::make_unique<PrinterDriverFactory>(); auto driverFactory = std::make_unique<PrinterDriverFactory>();
auto drivers = driverFactory->listAllDrivers(); auto drivers = driverFactory->listAllDrivers();
std::cout << "Available printer drivers:\n"; fmt::print("Available printer drivers:\n");
for (const auto& driver : drivers) { for (const auto& driver : drivers) {
std::cout << std::format(" - {}\n", driver); fmt::print(" - {}\n", driver);
} }
std::cout << "\nUse with: -p <driver_name> or --printer <driver_name>\n"; fmt::print("\nUse with: -p <driver_name> or --printer <driver_name>\n");
return true; return true;
} }
@@ -133,48 +127,13 @@ bool PtouchPrint::handlePrinting() {
return true; return true;
} }
// Build label incrementally, appending when --new is encountered // Build label using LabelBuilder
graphics::LabelBuilder labelBuilder(printer->getPrinterInfo().pixelLines); graphics::LabelBuilder labelBuilder(printer->getPrinterInfo().pixelLines);
std::unique_ptr<graphics::ILabel> finalLabel = nullptr;
// Debug: print command sequence
spdlog::debug("Processing {} commands:", options.commands.size());
for (size_t i = 0; i < options.commands.size(); ++i) {
const auto& [cmdType, value] = options.commands[i];
spdlog::debug(" Command {}: type={}, value='{}'", i, static_cast<int>(cmdType), value);
}
for (const auto& [cmdType, value] : options.commands) { for (const auto& [cmdType, value] : options.commands) {
switch (cmdType) { switch (cmdType) {
case cli::CommandType::NewLabel: {
// Finish current label and append to final label
spdlog::debug("Encountered --new, finishing current label segment");
auto currentLabel = labelBuilder.build();
if (!finalLabel) {
// First label becomes the base
finalLabel = std::move(currentLabel);
} else {
// If finalLabel is empty (width=0), replace it instead of appending
if (finalLabel->getWidth() == 0) {
spdlog::debug("Final label is empty, replacing instead of appending");
finalLabel = std::move(currentLabel);
} else if (currentLabel->getWidth() == 0) {
// Current label is empty, skip appending
spdlog::debug("Current label is empty, skipping append");
} else {
// Both labels have content, append
if (!finalLabel->append(*currentLabel)) {
spdlog::error("Failed to append label");
return false;
}
}
}
// Reset builder for next label
labelBuilder = graphics::LabelBuilder(printer->getPrinterInfo().pixelLines);
break;
}
case cli::CommandType::Text: case cli::CommandType::Text:
labelBuilder.addText(value); labelBuilder.addText(value + "\n");
break; break;
case cli::CommandType::Font: case cli::CommandType::Font:
spdlog::debug("Setting font to {}", value); spdlog::debug("Setting font to {}", value);
@@ -214,31 +173,9 @@ bool PtouchPrint::handlePrinting() {
} }
} }
// Build and append final label segment // Build and print the label
auto lastLabel = labelBuilder.build(); auto label = labelBuilder.build();
if (!finalLabel) { if (!mPrinterService->printLabel(std::move(label))) {
// Only one label, no --new was used
finalLabel = std::move(lastLabel);
} else {
// Handle empty labels
if (finalLabel->getWidth() == 0) {
// Final label is empty, replace it
spdlog::debug("Final label is empty, replacing with last segment");
finalLabel = std::move(lastLabel);
} else if (lastLabel->getWidth() == 0) {
// Last segment is empty, skip appending
spdlog::debug("Last label segment is empty, skipping append");
} else {
// Both have content, append
if (!finalLabel->append(*lastLabel)) {
spdlog::error("Failed to append final label segment");
return false;
}
}
}
// Print the final label
if (!mPrinterService->printLabel(std::move(finalLabel))) {
spdlog::error("An error occurred while printing"); spdlog::error("An error occurred while printing");
return false; return false;
} }

View File

@@ -21,18 +21,13 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
namespace ptprnt::cli { namespace ptprnt::cli {
class ICliParser; class CliParser;
} }
namespace ptprnt::core { namespace ptprnt::core {
class IPrinterService; class PrinterService;
}
namespace ptprnt::graphics {
class ILabel;
} }
namespace ptprnt { namespace ptprnt {
@@ -42,27 +37,14 @@ namespace ptprnt {
* *
* Acts as a thin glue layer coordinating CLI parsing and core printer functionality. * Acts as a thin glue layer coordinating CLI parsing and core printer functionality.
* Separates CLI frontend concerns from the core library. * Separates CLI frontend concerns from the core library.
*
* Uses interfaces (ICliParser, IPrinterService) to enable dependency injection
* and facilitate unit testing with mocks.
*/ */
class PtouchPrint { class PtouchPrint {
public: public:
/** /**
* @brief Construct the application with default implementations * @brief Construct the application
* @param versionString Version string to display * @param versionString Version string to display
*/ */
PtouchPrint(const char* versionString); PtouchPrint(const char* versionString);
/**
* @brief Construct with custom implementations (for testing)
* @param versionString Version string to display
* @param cliParser Custom CLI parser implementation
* @param printerService Custom printer service implementation
*/
PtouchPrint(const char* versionString, std::unique_ptr<cli::ICliParser> cliParser,
std::unique_ptr<core::IPrinterService> printerService);
~PtouchPrint(); // Must be defined in .cpp where complete types are visible ~PtouchPrint(); // Must be defined in .cpp where complete types are visible
// This is basically a singleton application class, no need to copy or move // This is basically a singleton application class, no need to copy or move
@@ -91,8 +73,8 @@ class PtouchPrint {
bool handlePrinting(); bool handlePrinting();
std::string mVersionString; std::string mVersionString;
std::unique_ptr<cli::ICliParser> mCliParser; std::unique_ptr<cli::CliParser> mCliParser;
std::unique_ptr<core::IPrinterService> mPrinterService; std::unique_ptr<core::PrinterService> mPrinterService;
}; };
} // namespace ptprnt } // namespace ptprnt

View File

@@ -19,13 +19,12 @@
#include "CliParser.hpp" #include "CliParser.hpp"
#include <format> #include <fmt/core.h>
#include <iostream>
namespace ptprnt::cli { namespace ptprnt::cli {
CliParser::CliParser(std::string appDescription, std::string versionString) CliParser::CliParser(const std::string& appDescription, std::string versionString)
: mApp(std::move(appDescription)), mVersionString(std::move(versionString)) { : mApp(appDescription), mVersionString(std::move(versionString)) {
setupParser(); setupParser();
} }
@@ -44,56 +43,13 @@ int CliParser::parse(int argc, char** argv) {
mApp.exit(e); mApp.exit(e);
return -1; // Signal: error return -1; // Signal: error
} }
// Post-process: Re-order commands based on actual command line order
// This is needed because CLI11 groups options by type
reorderCommandsByArgv(argc, argv);
return 0; return 0;
} }
void CliParser::reorderCommandsByArgv(int argc, char** argv) {
std::vector<Command> reorderedCommands;
// Parse argv to determine the actual order
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--new") {
reorderedCommands.emplace_back(CommandType::NewLabel, "");
} else if (arg == "-t" || arg == "--text") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::Text, argv[++i]);
}
} else if (arg == "-f" || arg == "--font") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::Font, argv[++i]);
}
} else if (arg == "-s" || arg == "--fontsize") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::FontSize, argv[++i]);
}
} else if (arg == "--valign") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::VAlign, argv[++i]);
}
} else if (arg == "--halign") {
if (i + 1 < argc) {
reorderedCommands.emplace_back(CommandType::HAlign, argv[++i]);
}
}
}
// Only replace if we found relevant commands
if (!reorderedCommands.empty()) {
mOptions.commands = std::move(reorderedCommands);
}
}
void CliParser::setupParser() { void CliParser::setupParser() {
// Version callback // Version callback
auto printVersion = [this](std::size_t) { auto printVersion = [this](std::size_t) {
std::cout << std::format("ptprnt version: {}\n", mVersionString); fmt::print("ptprnt version: {}\n", mVersionString);
throw CLI::CallForVersion(); throw CLI::CallForVersion();
}; };
@@ -111,9 +67,8 @@ void CliParser::setupParser() {
// Text printing options // Text printing options
// Note: CLI11 options are processed in order when using ->each() with callbacks // Note: CLI11 options are processed in order when using ->each() with callbacks
mApp.add_option( mApp.add_option("-t,--text",
"-t,--text", "Text to print (can be used multiple times, use formatting options before to influence text layout)")
"Text to print (can be used multiple times, use formatting options before to influence text layout)")
->multi_option_policy(CLI::MultiOptionPolicy::TakeAll) ->multi_option_policy(CLI::MultiOptionPolicy::TakeAll)
->each([this](const std::string& text) { mOptions.commands.emplace_back(CommandType::Text, text); }); ->each([this](const std::string& text) { mOptions.commands.emplace_back(CommandType::Text, text); });
@@ -133,12 +88,6 @@ void CliParser::setupParser() {
mApp.add_option("--halign", "Horizontal alignment of the following text occurrences") mApp.add_option("--halign", "Horizontal alignment of the following text occurrences")
->multi_option_policy(CLI::MultiOptionPolicy::TakeAll) ->multi_option_policy(CLI::MultiOptionPolicy::TakeAll)
->each([this](const std::string& align) { mOptions.commands.emplace_back(CommandType::HAlign, align); }); ->each([this](const std::string& align) { mOptions.commands.emplace_back(CommandType::HAlign, align); });
// Label separator - use an option with multi_option_policy to maintain parse order
// We need to use a dummy string parameter since .each() expects a string callback
mApp.add_flag("--new", "Start a new label (multiple labels will be stitched together)")
->multi_option_policy(CLI::MultiOptionPolicy::TakeAll)
->each([this](const std::string&) { mOptions.commands.emplace_back(CommandType::NewLabel, ""); });
} }
} // namespace ptprnt::cli } // namespace ptprnt::cli

View File

@@ -22,28 +22,47 @@
#include <CLI/CLI.hpp> #include <CLI/CLI.hpp>
#include <string> #include <string>
#include <vector>
#include "interface/ICliParser.hpp"
namespace ptprnt::cli { namespace ptprnt::cli {
/**
* @brief Types of CLI commands that can be issued
*/
enum class CommandType { None = 0, Text = 1, FontSize = 2, Font = 3, VAlign = 4, HAlign = 5 };
/**
* @brief A command with its type and value
*/
using Command = std::pair<CommandType, std::string>;
/**
* @brief Parsed CLI options and commands
*/
struct CliOptions {
bool verbose{false};
bool trace{false};
bool listDrivers{false};
std::string printerSelection{"auto"};
std::vector<Command> commands{};
};
/** /**
* @brief CLI argument parser for ptprnt * @brief CLI argument parser for ptprnt
* *
* Concrete implementation of ICliParser using CLI11. * Handles all command-line argument parsing using CLI11.
* Handles all command-line argument parsing.
* Separates CLI concerns from core library functionality. * Separates CLI concerns from core library functionality.
*/ */
class CliParser : public ICliParser { class CliParser {
public: public:
/** /**
* @brief Construct a CLI parser * @brief Construct a CLI parser
* @param appDescription Application description for help text * @param appDescription Application description for help text
* @param versionString Version string to display * @param versionString Version string to display
*/ */
CliParser(std::string appDescription, std::string versionString); CliParser(const std::string& appDescription, std::string versionString);
~CliParser() override = default; ~CliParser() = default;
CliParser(const CliParser&) = delete; CliParser(const CliParser&) = delete;
CliParser& operator=(const CliParser&) = delete; CliParser& operator=(const CliParser&) = delete;
@@ -56,17 +75,16 @@ class CliParser : public ICliParser {
* @param argv Argument values * @param argv Argument values
* @return 0 on success, positive value if should exit immediately (help/version), negative on error * @return 0 on success, positive value if should exit immediately (help/version), negative on error
*/ */
int parse(int argc, char** argv) override; int parse(int argc, char** argv);
/** /**
* @brief Get the parsed options * @brief Get the parsed options
* @return Reference to parsed options * @return Reference to parsed options
*/ */
[[nodiscard]] const CliOptions& getOptions() const override { return mOptions; } [[nodiscard]] const CliOptions& getOptions() const { return mOptions; }
private: private:
void setupParser(); void setupParser();
void reorderCommandsByArgv(int argc, char** argv);
CLI::App mApp; CLI::App mApp;
std::string mVersionString; std::string mVersionString;

View File

@@ -1,79 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <vector>
namespace ptprnt::cli {
/**
* @brief Types of CLI commands that can be issued
*/
enum class CommandType { None = 0, Text = 1, FontSize = 2, Font = 3, VAlign = 4, HAlign = 5, NewLabel = 6 };
/**
* @brief A command with its type and value
*/
using Command = std::pair<CommandType, std::string>;
/**
* @brief Parsed CLI options and commands
*/
struct CliOptions {
bool verbose{false};
bool trace{false};
bool listDrivers{false};
std::string printerSelection{"auto"};
std::vector<Command> commands{};
};
/**
* @brief Interface for CLI argument parsing
*
* This interface allows for mocking CLI parsing in unit tests
* and provides a clear contract for CLI parser implementations.
*/
class ICliParser {
public:
virtual ~ICliParser() = default;
ICliParser() = default;
ICliParser(const ICliParser&) = default;
ICliParser& operator=(const ICliParser&) = default;
ICliParser(ICliParser&&) noexcept = default;
ICliParser& operator=(ICliParser&&) noexcept = default;
/**
* @brief Parse command line arguments
* @param argc Argument count
* @param argv Argument values
* @return 0 on success, positive value if should exit immediately (help/version), negative on error
*/
virtual int parse(int argc, char** argv) = 0;
/**
* @brief Get the parsed options
* @return Reference to parsed options
*/
[[nodiscard]] virtual const CliOptions& getOptions() const = 0;
};
} // namespace ptprnt::cli

View File

@@ -21,7 +21,7 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "core/PrinterDriverFactory.hpp" #include "PrinterDriverFactory.hpp"
namespace ptprnt::core { namespace ptprnt::core {
@@ -38,22 +38,14 @@ bool PrinterService::initialize() {
std::vector<std::shared_ptr<IPrinterDriver>> PrinterService::detectPrinters() { std::vector<std::shared_ptr<IPrinterDriver>> PrinterService::detectPrinters() {
spdlog::debug("Detecting printers..."); spdlog::debug("Detecting printers...");
auto usbDevs = mUsbDeviceFactory.findAllDevices(); auto usbDevs = mUsbDeviceFactory.findAllDevices();
auto driverFactory = std::make_unique<PrinterDriverFactory>(); auto driverFactory = std::make_unique<PrinterDriverFactory>();
mDetectedPrinters.clear(); mDetectedPrinters.clear();
for (auto& usbDev : usbDevs) { for (auto& usbDev : usbDevs) {
auto driver = driverFactory->create(usbDev->getUsbId()); auto driver = driverFactory->create(usbDev->getUsbId());
if (driver != nullptr) { if (driver != nullptr) {
// Attach the USB device to the printer driver mDetectedPrinters.push_back(driver);
// Convert unique_ptr to shared_ptr for attachment
std::shared_ptr<libusbwrap::IUsbDevice> sharedUsbDev = std::move(usbDev);
if (driver->attachUsbDevice(sharedUsbDev)) {
mDetectedPrinters.push_back(driver);
spdlog::debug("Successfully attached USB device to printer driver: {}", driver->getName());
} else {
spdlog::warn("Failed to attach USB device to printer driver: {}", driver->getName());
}
} }
} }
@@ -62,23 +54,6 @@ std::vector<std::shared_ptr<IPrinterDriver>> PrinterService::detectPrinters() {
} }
std::shared_ptr<IPrinterDriver> PrinterService::selectPrinter(const std::string& printerName) { std::shared_ptr<IPrinterDriver> PrinterService::selectPrinter(const std::string& printerName) {
// If a specific printer is requested by name (not "auto"), try to create it directly
if (printerName != "auto") {
auto driverFactory = std::make_unique<PrinterDriverFactory>();
auto printer = driverFactory->createByName(printerName);
if (printer) {
// For virtual/fake printers, call attachUsbDevice with nullptr to initialize
// For real printers selected explicitly, they would need actual USB device
printer->attachUsbDevice(nullptr);
mCurrentPrinter = printer;
spdlog::info("Using explicitly selected printer: {}", printerName);
return mCurrentPrinter;
}
spdlog::error("Printer driver '{}' not found", printerName);
return nullptr;
}
// Auto mode: detect USB printers
if (mDetectedPrinters.empty()) { if (mDetectedPrinters.empty()) {
detectPrinters(); detectPrinters();
} }
@@ -88,10 +63,24 @@ std::shared_ptr<IPrinterDriver> PrinterService::selectPrinter(const std::string&
return nullptr; return nullptr;
} }
// Auto-select first detected printer // Auto-select first printer
mCurrentPrinter = mDetectedPrinters.front(); if (printerName == "auto") {
spdlog::info("Auto-selected printer: {}", mCurrentPrinter->getName()); mCurrentPrinter = mDetectedPrinters.front();
return mCurrentPrinter; spdlog::info("Auto-selected printer: {}", mCurrentPrinter->getName());
return mCurrentPrinter;
}
// Select printer by name
for (auto& printer : mDetectedPrinters) {
if (printer->getDriverName() == printerName) {
mCurrentPrinter = printer;
spdlog::info("Using explicitly selected printer: {}", printerName);
return mCurrentPrinter;
}
}
spdlog::error("Printer '{}' not found", printerName);
return nullptr;
} }
bool PrinterService::printLabel(std::unique_ptr<graphics::ILabel> label) { bool PrinterService::printLabel(std::unique_ptr<graphics::ILabel> label) {

View File

@@ -23,25 +23,23 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "interface/IPrinterService.hpp" #include "interface/IPrinterDriver.hpp"
#include "libusbwrap/UsbDeviceFactory.hpp" #include "libusbwrap/UsbDeviceFactory.hpp"
#include "printers/interface/IPrinterDriver.hpp"
namespace ptprnt::core { namespace ptprnt::core {
/** /**
* @brief Core service for printer operations * @brief Core service for printer operations
* *
* Concrete implementation of IPrinterService.
* Provides the core library functionality for: * Provides the core library functionality for:
* - Detecting printers * - Detecting printers
* - Selecting printers * - Selecting printers
* - Building and printing labels * - Building and printing labels
*/ */
class PrinterService : public IPrinterService { class PrinterService {
public: public:
PrinterService(); PrinterService();
~PrinterService() override = default; ~PrinterService() = default;
PrinterService(const PrinterService&) = delete; PrinterService(const PrinterService&) = delete;
PrinterService& operator=(const PrinterService&) = delete; PrinterService& operator=(const PrinterService&) = delete;
@@ -52,33 +50,33 @@ class PrinterService : public IPrinterService {
* @brief Initialize USB device factory * @brief Initialize USB device factory
* @return true on success, false on failure * @return true on success, false on failure
*/ */
bool initialize() override; bool initialize();
/** /**
* @brief Detect all compatible printers * @brief Detect all compatible printers
* @return Vector of detected printers * @return Vector of detected printers
*/ */
std::vector<std::shared_ptr<IPrinterDriver>> detectPrinters() override; std::vector<std::shared_ptr<IPrinterDriver>> detectPrinters();
/** /**
* @brief Select a printer by name or auto-detect * @brief Select a printer by name or auto-detect
* @param printerName Printer driver name, or "auto" for first detected * @param printerName Printer driver name, or "auto" for first detected
* @return Printer driver, or nullptr if not found * @return Printer driver, or nullptr if not found
*/ */
std::shared_ptr<IPrinterDriver> selectPrinter(const std::string& printerName) override; std::shared_ptr<IPrinterDriver> selectPrinter(const std::string& printerName);
/** /**
* @brief Get the currently selected printer * @brief Get the currently selected printer
* @return Current printer, or nullptr if none selected * @return Current printer, or nullptr if none selected
*/ */
[[nodiscard]] std::shared_ptr<IPrinterDriver> getCurrentPrinter() const override { return mCurrentPrinter; } [[nodiscard]] std::shared_ptr<IPrinterDriver> getCurrentPrinter() const { return mCurrentPrinter; }
/** /**
* @brief Print a label * @brief Print a label
* @param label The label to print * @param label The label to print
* @return true on success, false on failure * @return true on success, false on failure
*/ */
bool printLabel(std::unique_ptr<graphics::ILabel> label) override; bool printLabel(std::unique_ptr<graphics::ILabel> label);
private: private:
libusbwrap::UsbDeviceFactory mUsbDeviceFactory; libusbwrap::UsbDeviceFactory mUsbDeviceFactory;

View File

@@ -1,74 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "graphics/interface/ILabel.hpp"
#include "printers/interface/IPrinterDriver.hpp"
namespace ptprnt::core {
/**
* @brief Interface for core printer service operations
*
* This interface allows for mocking printer operations in unit tests
* and provides a clear contract for printer service implementations.
*/
class IPrinterService {
public:
virtual ~IPrinterService() = default;
/**
* @brief Initialize the printer service
* @return true on success, false on failure
*/
virtual bool initialize() = 0;
/**
* @brief Detect all compatible printers
* @return Vector of detected printers
*/
virtual std::vector<std::shared_ptr<IPrinterDriver>> detectPrinters() = 0;
/**
* @brief Select a printer by name or auto-detect
* @param printerName Printer driver name, or "auto" for first detected
* @return Printer driver, or nullptr if not found
*/
virtual std::shared_ptr<IPrinterDriver> selectPrinter(const std::string& printerName) = 0;
/**
* @brief Get the currently selected printer
* @return Current printer, or nullptr if none selected
*/
[[nodiscard]] virtual std::shared_ptr<IPrinterDriver> getCurrentPrinter() const = 0;
/**
* @brief Print a label
* @param label The label to print
* @return true on success, false on failure
*/
virtual bool printLabel(std::unique_ptr<graphics::ILabel> label) = 0;
};
} // namespace ptprnt::core

View File

@@ -20,6 +20,8 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <memory>
#include <span>
#include <vector> #include <vector>
namespace ptprnt::graphics { namespace ptprnt::graphics {

View File

@@ -1,139 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "graphics/interface/ICairoWrapper.hpp"
namespace ptprnt::graphics {
/**
* @brief Real implementation of ICairoWrapper that forwards to actual Cairo/Pango C API
*
* This class simply forwards all calls to the real Cairo and Pango library functions.
* It's used as the default implementation in production code.
*/
class CairoWrapper : public ICairoWrapper {
public:
~CairoWrapper() override = default;
// Cairo image surface functions
cairo_surface_t* cairo_image_surface_create(cairo_format_t format, int width, int height) override {
return ::cairo_image_surface_create(format, width, height);
}
void cairo_surface_destroy(cairo_surface_t* surface) override { ::cairo_surface_destroy(surface); }
void cairo_surface_flush(cairo_surface_t* surface) override { ::cairo_surface_flush(surface); }
void cairo_surface_mark_dirty(cairo_surface_t* surface) override { ::cairo_surface_mark_dirty(surface); }
cairo_status_t cairo_surface_status(cairo_surface_t* surface) override { return ::cairo_surface_status(surface); }
cairo_format_t cairo_image_surface_get_format(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_format(surface);
}
int cairo_image_surface_get_width(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_width(surface);
}
int cairo_image_surface_get_height(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_height(surface);
}
int cairo_image_surface_get_stride(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_stride(surface);
}
unsigned char* cairo_image_surface_get_data(cairo_surface_t* surface) override {
return ::cairo_image_surface_get_data(surface);
}
cairo_status_t cairo_surface_write_to_png(cairo_surface_t* surface, const char* filename) override {
return ::cairo_surface_write_to_png(surface, filename);
}
// Cairo context functions
cairo_t* cairo_create(cairo_surface_t* surface) override { return ::cairo_create(surface); }
void cairo_destroy(cairo_t* cr) override { ::cairo_destroy(cr); }
void cairo_move_to(cairo_t* cr, double x, double y) override { ::cairo_move_to(cr, x, y); }
void cairo_set_source_rgb(cairo_t* cr, double red, double green, double blue) override {
::cairo_set_source_rgb(cr, red, green, blue);
}
// Pango-Cairo functions
PangoFontMap* pango_cairo_font_map_new() override { return ::pango_cairo_font_map_new(); }
PangoContext* pango_cairo_create_context(cairo_t* cr) override { return ::pango_cairo_create_context(cr); }
void pango_cairo_show_layout(cairo_t* cr, PangoLayout* layout) override { ::pango_cairo_show_layout(cr, layout); }
// Pango layout functions
PangoLayout* pango_layout_new(PangoContext* context) override { return ::pango_layout_new(context); }
void pango_layout_set_font_description(PangoLayout* layout, const PangoFontDescription* desc) override {
::pango_layout_set_font_description(layout, desc);
}
void pango_layout_set_text(PangoLayout* layout, const char* text, int length) override {
::pango_layout_set_text(layout, text, length);
}
void pango_layout_set_height(PangoLayout* layout, int height) override {
::pango_layout_set_height(layout, height);
}
void pango_layout_set_alignment(PangoLayout* layout, PangoAlignment alignment) override {
::pango_layout_set_alignment(layout, alignment);
}
void pango_layout_set_justify(PangoLayout* layout, gboolean justify) override {
::pango_layout_set_justify(layout, justify);
}
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
void pango_layout_set_justify_last_line(PangoLayout* layout, gboolean justify) override {
::pango_layout_set_justify_last_line(layout, justify);
}
#endif
void pango_layout_get_size(PangoLayout* layout, int* width, int* height) override {
::pango_layout_get_size(layout, width, height);
}
// Pango font description functions
PangoFontDescription* pango_font_description_new() override { return ::pango_font_description_new(); }
void pango_font_description_set_size(PangoFontDescription* desc, gint size) override {
::pango_font_description_set_size(desc, size);
}
void pango_font_description_set_family(PangoFontDescription* desc, const char* family) override {
::pango_font_description_set_family(desc, family);
}
// GObject reference counting
void g_object_unref(gpointer object) override { ::g_object_unref(object); }
};
} // namespace ptprnt::graphics

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -25,64 +25,37 @@
#include <cassert> #include <cassert>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include "cairo.h" #include "cairo.h"
#include "graphics/CairoWrapper.hpp"
#include "graphics/interface/ICairoWrapper.hpp"
#include "graphics/interface/ILabel.hpp" #include "graphics/interface/ILabel.hpp"
#include "pango/pango-font.h" #include "pango/pango-font.h"
#include "pango/pango-layout.h" #include "pango/pango-layout.h"
#include "pango/pango-types.h" #include "pango/pango-types.h"
#include "pango/pangocairo.h"
namespace ptprnt::graphics { namespace ptprnt::graphics {
Label::Label(const uint16_t heightPixel)
// Deleter implementations : mPrinterHeight(heightPixel) {
void CairoSurfaceDeleter::operator()(cairo_surface_t* surface) const {
if (surface && wrapper)
wrapper->cairo_surface_destroy(surface);
}
void CairoDeleter::operator()(cairo_t* cr) const {
if (cr && wrapper)
wrapper->cairo_destroy(cr);
}
void GObjectDeleter::operator()(gpointer obj) const {
if (obj && wrapper)
wrapper->g_object_unref(obj);
}
// Default constructor - creates real Cairo/Pango wrapper
Label::Label(const uint16_t heightPixel) : Label(heightPixel, std::make_shared<CairoWrapper>()) {}
// Constructor with dependency injection
Label::Label(const uint16_t heightPixel, std::shared_ptr<ICairoWrapper> cairoWrapper)
: mCairoWrapper(std::move(cairoWrapper)), mPrinterHeight(heightPixel) {
// Initialize resources in correct order with RAII // Initialize resources in correct order with RAII
// Pass wrapper to deleter so cleanup uses the wrapper mFontMap.reset(pango_cairo_font_map_new());
GObjectDeleter deleter;
deleter.wrapper = mCairoWrapper;
mFontMap = std::unique_ptr<PangoFontMap, GObjectDeleter>(mCairoWrapper->pango_cairo_font_map_new(), deleter);
} }
std::vector<uint8_t> Label::getRaw() const { std::vector<uint8_t> Label::getRaw() {
assert(mSurface != nullptr); assert(mSurface != nullptr);
auto* surface = mSurface.get(); auto* surface = mSurface.get();
mCairoWrapper->cairo_surface_flush(surface); cairo_surface_flush(surface);
assert(mCairoWrapper->cairo_image_surface_get_format(surface) == CAIRO_FORMAT_A8); assert(cairo_image_surface_get_format(surface) == CAIRO_FORMAT_A8);
int width = mCairoWrapper->cairo_image_surface_get_width(surface); int width = cairo_image_surface_get_width(surface);
int height = mCairoWrapper->cairo_image_surface_get_height(surface); int height = cairo_image_surface_get_height(surface);
int stride = mCairoWrapper->cairo_image_surface_get_stride(surface); int stride = cairo_image_surface_get_stride(surface);
spdlog::debug("Cairo Surface data: W: {}; H: {}; S:{}", width, height, stride); spdlog::debug("Cairo Surface data: W: {}; H: {}; S:{}", width, height, stride);
auto data = mCairoWrapper->cairo_image_surface_get_data(surface); auto data = cairo_image_surface_get_data(surface);
// If stride equals width, we can return data directly // If stride equals width, we can return data directly
if (stride == width) { if (stride == width) {
@@ -107,41 +80,41 @@ uint8_t Label::getNumLines(std::string_view strv) {
return std::count(strv.begin(), strv.end(), '\n'); return std::count(strv.begin(), strv.end(), '\n');
} }
int Label::getWidth() const { int Label::getWidth() {
// Return the actual Cairo surface width (which is the layout width) // Return the actual Cairo surface width (which is the layout width)
return mLayoutWidth; return mLayoutWidth;
} }
int Label::getHeight() const { int Label::getHeight() {
// Return the actual Cairo surface height (which is the printer height) // Return the actual Cairo surface height (which is the printer height)
return mPrinterHeight; return mPrinterHeight;
} }
void Label::configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc) { void Label::configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc) {
mCairoWrapper->pango_layout_set_font_description(layout, fontDesc); pango_layout_set_font_description(layout, fontDesc);
mCairoWrapper->pango_layout_set_text(layout, text.c_str(), static_cast<int>(text.length())); pango_layout_set_text(layout, text.c_str(), static_cast<int>(text.length()));
mCairoWrapper->pango_layout_set_height(layout, getNumLines(text) * -1); pango_layout_set_height(layout, getNumLines(text) * -1);
} }
void Label::applyHorizontalAlignment(PangoLayout* layout) { void Label::applyHorizontalAlignment(PangoLayout* layout) {
switch (mHAlign) { switch (mHAlign) {
case HAlignPosition::LEFT: case HAlignPosition::LEFT:
mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
break; break;
case HAlignPosition::RIGHT: case HAlignPosition::RIGHT:
mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_RIGHT); pango_layout_set_alignment(layout, PANGO_ALIGN_RIGHT);
break; break;
case HAlignPosition::JUSTIFY: case HAlignPosition::JUSTIFY:
mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
mCairoWrapper->pango_layout_set_justify(layout, true); pango_layout_set_justify(layout, true);
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50 #if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
mCairoWrapper->pango_layout_set_justify_last_line(layout, true); pango_layout_set_justify_last_line(layout, true);
#endif #endif
break; break;
case HAlignPosition::CENTER: case HAlignPosition::CENTER:
[[fallthrough]]; [[fallthrough]];
default: default:
mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER);
break; break;
} }
} }
@@ -160,49 +133,40 @@ bool Label::create(const std::string& labelText) {
// see: https://gist.github.com/CallumDev/7c66b3f9cf7a876ef75f // see: https://gist.github.com/CallumDev/7c66b3f9cf7a876ef75f
// Create a temporary surface for layout size calculations // Create a temporary surface for layout size calculations
auto* tempSurface = mCairoWrapper->cairo_image_surface_create(CAIRO_FORMAT_A8, 1, 1); auto* tempSurface = cairo_image_surface_create(CAIRO_FORMAT_A8, 1, 1);
auto* tempCr = mCairoWrapper->cairo_create(tempSurface); auto* tempCr = cairo_create(tempSurface);
auto* tempPangoCtx = mCairoWrapper->pango_cairo_create_context(tempCr); auto* tempPangoCtx = pango_cairo_create_context(tempCr);
auto* tempPangoLyt = mCairoWrapper->pango_layout_new(tempPangoCtx); auto* tempPangoLyt = pango_layout_new(tempPangoCtx);
PangoFontDescription* regularFont = mCairoWrapper->pango_font_description_new(); PangoFontDescription* regularFont = pango_font_description_new();
mCairoWrapper->pango_font_description_set_size(regularFont, static_cast<int>(mFontSize * PANGO_SCALE)); pango_font_description_set_size(regularFont, static_cast<int>(mFontSize * PANGO_SCALE));
mCairoWrapper->pango_font_description_set_family(regularFont, mFontFamily.c_str()); pango_font_description_set_family(regularFont, mFontFamily.c_str());
// Configure temporary layout for size calculation // Configure temporary layout for size calculation
configureLayout(tempPangoLyt, labelText, regularFont); configureLayout(tempPangoLyt, labelText, regularFont);
applyHorizontalAlignment(tempPangoLyt); applyHorizontalAlignment(tempPangoLyt);
// Calculate label size from temporary layout // Calculate label size from temporary layout
mCairoWrapper->pango_layout_get_size(tempPangoLyt, &mLayoutWidth, &mLayoutHeight); pango_layout_get_size(tempPangoLyt, &mLayoutWidth, &mLayoutHeight);
mLayoutWidth /= PANGO_SCALE; mLayoutWidth /= PANGO_SCALE;
mLayoutHeight /= PANGO_SCALE; mLayoutHeight /= PANGO_SCALE;
spdlog::debug("Layout width: {}, height: {}", mLayoutWidth, mLayoutHeight); 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 // Clean up temporary resources
mCairoWrapper->g_object_unref(tempPangoLyt); g_object_unref(tempPangoLyt);
mCairoWrapper->g_object_unref(tempPangoCtx); g_object_unref(tempPangoCtx);
mCairoWrapper->cairo_destroy(tempCr); cairo_destroy(tempCr);
mCairoWrapper->cairo_surface_destroy(tempSurface); cairo_surface_destroy(tempSurface);
// Now create the final surface and Pango context for actual rendering // Now create the final surface and Pango context for actual rendering
// Create deleters with wrapper reference mSurface.reset(cairo_image_surface_create(CAIRO_FORMAT_A8, mLayoutWidth, mPrinterHeight));
CairoSurfaceDeleter surfaceDeleter; cairo_t* cr = cairo_create(mSurface.get());
surfaceDeleter.wrapper = mCairoWrapper; mCairoCtx.reset(cr);
CairoDeleter cairoDeleter; mPangoCtx.reset(pango_cairo_create_context(cr));
cairoDeleter.wrapper = mCairoWrapper; mPangoLyt.reset(pango_layout_new(mPangoCtx.get()));
GObjectDeleter gobjectDeleter;
gobjectDeleter.wrapper = mCairoWrapper;
mSurface = std::unique_ptr<cairo_surface_t, CairoSurfaceDeleter>(
mCairoWrapper->cairo_image_surface_create(CAIRO_FORMAT_A8, mLayoutWidth, mPrinterHeight), surfaceDeleter);
cairo_t* cr = mCairoWrapper->cairo_create(mSurface.get());
mCairoCtx = std::unique_ptr<cairo_t, CairoDeleter>(cr, cairoDeleter);
mPangoCtx =
std::unique_ptr<PangoContext, GObjectDeleter>(mCairoWrapper->pango_cairo_create_context(cr), gobjectDeleter);
mPangoLyt =
std::unique_ptr<PangoLayout, GObjectDeleter>(mCairoWrapper->pango_layout_new(mPangoCtx.get()), gobjectDeleter);
// Configure final layout with same settings // Configure final layout with same settings
configureLayout(mPangoLyt.get(), labelText, regularFont); configureLayout(mPangoLyt.get(), labelText, regularFont);
@@ -213,104 +177,31 @@ bool Label::create(const std::string& labelText) {
case VAlignPosition::TOP: case VAlignPosition::TOP:
break; break;
case VAlignPosition::BOTTOM: case VAlignPosition::BOTTOM:
mCairoWrapper->cairo_move_to(mCairoCtx.get(), 0.0, mPrinterHeight - mLayoutHeight); cairo_move_to(mCairoCtx.get(), 0.0, mPrinterHeight - mLayoutHeight);
break; break;
case VAlignPosition::MIDDLE: case VAlignPosition::MIDDLE:
mCairoWrapper->cairo_move_to(mCairoCtx.get(), 0.0, (mPrinterHeight - mLayoutHeight) / 2); cairo_move_to(mCairoCtx.get(), 0.0, (mPrinterHeight - mLayoutHeight) / 2);
break; break;
default: default:
break; break;
} }
// Finally show the layout on the Cairo surface // Finally show the layout on the Cairo surface
mCairoWrapper->pango_cairo_show_layout(mCairoCtx.get(), mPangoLyt.get()); pango_cairo_show_layout(mCairoCtx.get(), mPangoLyt.get());
mCairoWrapper->cairo_set_source_rgb(mCairoCtx.get(), 0.0, 0.0, 0.0); cairo_set_source_rgb(mCairoCtx.get(), 0.0, 0.0, 0.0);
mCairoWrapper->cairo_surface_flush(mSurface.get()); cairo_surface_flush(mSurface.get());
// mCairoCtx smart pointer will handle cleanup // mCairoCtx smart pointer will handle cleanup
return true; return true;
} }
void Label::writeToPng(const std::string& file) { void Label::writeToPng(const std::string& file) {
if (mSurface) { if (mSurface) {
mCairoWrapper->cairo_surface_flush(mSurface.get()); cairo_surface_flush(mSurface.get());
mCairoWrapper->cairo_surface_write_to_png(mSurface.get(), file.c_str()); cairo_surface_write_to_png(mSurface.get(), file.c_str());
} }
} }
bool Label::append(const ILabel& other, uint32_t spacingPx) {
// Check that heights match
if (getHeight() != other.getHeight()) {
spdlog::error("Cannot append labels with different heights: {} vs {}", getHeight(), other.getHeight());
return false;
}
int currentWidth = getWidth();
int otherWidth = other.getWidth();
int height = getHeight();
int spacing = static_cast<int>(spacingPx);
int newWidth = currentWidth + spacing + otherWidth;
spdlog::debug("Appending label: current={}x{}, other={}x{}, spacing={}, new={}x{}", currentWidth, height,
otherWidth, height, spacing, newWidth, height);
// Get current and other label data
auto currentData = getRaw();
auto otherData = other.getRaw();
// Create new surface with extended width
CairoSurfaceDeleter surfaceDeleter;
surfaceDeleter.wrapper = mCairoWrapper;
auto newSurface = std::unique_ptr<cairo_surface_t, CairoSurfaceDeleter>(
mCairoWrapper->cairo_image_surface_create(CAIRO_FORMAT_A8, newWidth, height), surfaceDeleter);
if (mCairoWrapper->cairo_surface_status(newSurface.get()) != CAIRO_STATUS_SUCCESS) {
spdlog::error("Failed to create new surface for appended label");
return false;
}
// Get data pointer and stride
mCairoWrapper->cairo_surface_flush(newSurface.get());
unsigned char* newData = mCairoWrapper->cairo_image_surface_get_data(newSurface.get());
int newStride = mCairoWrapper->cairo_image_surface_get_stride(newSurface.get());
// Clear the new surface (set to transparent/white)
memset(newData, 0x00, newStride * height);
// Copy current label data
for (int y = 0; y < height; ++y) {
for (int x = 0; x < currentWidth; ++x) {
size_t srcIdx = y * currentWidth + x;
size_t dstIdx = y * newStride + x;
if (srcIdx < currentData.size()) {
newData[dstIdx] = currentData[srcIdx];
}
}
}
// Copy other label data (with spacing offset)
int xOffset = currentWidth + spacing;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < otherWidth; ++x) {
size_t srcIdx = y * otherWidth + x;
size_t dstIdx = y * newStride + (xOffset + x);
if (srcIdx < otherData.size()) {
newData[dstIdx] = otherData[srcIdx];
}
}
}
mCairoWrapper->cairo_surface_mark_dirty(newSurface.get());
// Replace current surface with new one
mSurface = std::move(newSurface);
// Update layout dimensions
mLayoutWidth = newWidth;
return true;
}
void Label::setFontSize(const double fontSize) { void Label::setFontSize(const double fontSize) {
mFontSize = fontSize; mFontSize = fontSize;
} }

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -32,34 +32,31 @@
namespace ptprnt::graphics { namespace ptprnt::graphics {
// Forward declaration // Custom deleters for Cairo/Pango resources
class ICairoWrapper;
// Custom deleters for Cairo/Pango resources that use the wrapper
// Implementation in Label.cpp to avoid incomplete type issues
struct CairoSurfaceDeleter { struct CairoSurfaceDeleter {
std::shared_ptr<ICairoWrapper> wrapper; void operator()(cairo_surface_t* surface) const {
void operator()(cairo_surface_t* surface) const; if (surface)
cairo_surface_destroy(surface);
}
}; };
struct CairoDeleter { struct CairoDeleter {
std::shared_ptr<ICairoWrapper> wrapper; void operator()(cairo_t* cr) const {
void operator()(cairo_t* cr) const; if (cr)
cairo_destroy(cr);
}
}; };
struct GObjectDeleter { struct GObjectDeleter {
std::shared_ptr<ICairoWrapper> wrapper; void operator()(gpointer obj) const {
void operator()(gpointer obj) const; if (obj)
g_object_unref(obj);
}
}; };
class Label : public ILabel { class Label : public ILabel {
public: public:
// Default constructor using real Cairo/Pango implementation Label(const uint16_t heightPixel);
explicit Label(uint16_t heightPixel);
// Constructor for dependency injection (testing)
Label(uint16_t heightPixel, std::shared_ptr<ICairoWrapper> cairoWrapper);
~Label() override; ~Label() override;
Label(const Label&) = delete; Label(const Label&) = delete;
@@ -70,9 +67,9 @@ class Label : public ILabel {
bool create(PrintableText printableText) override; bool create(PrintableText printableText) override;
bool create(const std::string& labelText) override; bool create(const std::string& labelText) override;
void writeToPng(const std::string& file); void writeToPng(const std::string& file);
[[nodiscard]] int getWidth() const override; [[nodiscard]] int getWidth() override;
[[nodiscard]] int getHeight() const override; [[nodiscard]] int getHeight() override;
[[nodiscard]] std::vector<uint8_t> getRaw() const override; [[nodiscard]] std::vector<uint8_t> getRaw() override;
void setFontSize(const double fontSize) override; void setFontSize(const double fontSize) override;
void setFontFamily(const std::string& fontFamily) override; void setFontFamily(const std::string& fontFamily) override;
@@ -80,8 +77,6 @@ class Label : public ILabel {
void setHAlign(HAlignPosition hpos) override; void setHAlign(HAlignPosition hpos) override;
void setVAlign(VAlignPosition vpos) override; void setVAlign(VAlignPosition vpos) override;
bool append(const ILabel& other, uint32_t spacingPx = 60) override;
private: private:
// methods // methods
[[nodiscard]] uint8_t getNumLines(std::string_view str); [[nodiscard]] uint8_t getNumLines(std::string_view str);
@@ -89,9 +84,6 @@ class Label : public ILabel {
void configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc); void configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc);
void applyHorizontalAlignment(PangoLayout* layout); void applyHorizontalAlignment(PangoLayout* layout);
// Cairo/Pango wrapper for dependency injection
std::shared_ptr<ICairoWrapper> mCairoWrapper;
std::unique_ptr<cairo_surface_t, CairoSurfaceDeleter> mSurface{nullptr}; std::unique_ptr<cairo_surface_t, CairoSurfaceDeleter> mSurface{nullptr};
std::unique_ptr<cairo_t, CairoDeleter> mCairoCtx{nullptr}; std::unique_ptr<cairo_t, CairoDeleter> mCairoCtx{nullptr};
std::unique_ptr<PangoContext, GObjectDeleter> mPangoCtx{nullptr}; std::unique_ptr<PangoContext, GObjectDeleter> mPangoCtx{nullptr};

View File

@@ -1,83 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <cairo.h>
#include <pango/pango.h>
#include <pango/pangocairo.h>
namespace ptprnt::graphics {
/**
* @brief Interface wrapper for Cairo and Pango C API functions
*
* This interface allows for dependency injection and mocking of Cairo/Pango
* functionality in unit tests, making the Label class fully testable.
*/
class ICairoWrapper {
public:
virtual ~ICairoWrapper() = default;
// Cairo image surface functions
virtual cairo_surface_t* cairo_image_surface_create(cairo_format_t format, int width, int height) = 0;
virtual void cairo_surface_destroy(cairo_surface_t* surface) = 0;
virtual void cairo_surface_flush(cairo_surface_t* surface) = 0;
virtual void cairo_surface_mark_dirty(cairo_surface_t* surface) = 0;
virtual cairo_status_t cairo_surface_status(cairo_surface_t* surface) = 0;
virtual cairo_format_t cairo_image_surface_get_format(cairo_surface_t* surface) = 0;
virtual int cairo_image_surface_get_width(cairo_surface_t* surface) = 0;
virtual int cairo_image_surface_get_height(cairo_surface_t* surface) = 0;
virtual int cairo_image_surface_get_stride(cairo_surface_t* surface) = 0;
virtual unsigned char* cairo_image_surface_get_data(cairo_surface_t* surface) = 0;
virtual cairo_status_t cairo_surface_write_to_png(cairo_surface_t* surface, const char* filename) = 0;
// Cairo context functions
virtual cairo_t* cairo_create(cairo_surface_t* surface) = 0;
virtual void cairo_destroy(cairo_t* cr) = 0;
virtual void cairo_move_to(cairo_t* cr, double x, double y) = 0;
virtual void cairo_set_source_rgb(cairo_t* cr, double red, double green, double blue) = 0;
// Pango-Cairo functions
virtual PangoFontMap* pango_cairo_font_map_new() = 0;
virtual PangoContext* pango_cairo_create_context(cairo_t* cr) = 0;
virtual void pango_cairo_show_layout(cairo_t* cr, PangoLayout* layout) = 0;
// Pango layout functions
virtual PangoLayout* pango_layout_new(PangoContext* context) = 0;
virtual void pango_layout_set_font_description(PangoLayout* layout, const PangoFontDescription* desc) = 0;
virtual void pango_layout_set_text(PangoLayout* layout, const char* text, int length) = 0;
virtual void pango_layout_set_height(PangoLayout* layout, int height) = 0;
virtual void pango_layout_set_alignment(PangoLayout* layout, PangoAlignment alignment) = 0;
virtual void pango_layout_set_justify(PangoLayout* layout, gboolean justify) = 0;
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
virtual void pango_layout_set_justify_last_line(PangoLayout* layout, gboolean justify) = 0;
#endif
virtual void pango_layout_get_size(PangoLayout* layout, int* width, int* height) = 0;
// Pango font description functions
virtual PangoFontDescription* pango_font_description_new() = 0;
virtual void pango_font_description_set_size(PangoFontDescription* desc, gint size) = 0;
virtual void pango_font_description_set_family(PangoFontDescription* desc, const char* family) = 0;
// GObject reference counting
virtual void g_object_unref(gpointer object) = 0;
};
} // namespace ptprnt::graphics

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2024-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -65,24 +65,16 @@ class ILabel {
public: public:
virtual ~ILabel() = default; virtual ~ILabel() = default;
virtual bool create(PrintableText printableText) = 0; virtual bool create(PrintableText printableText) = 0;
virtual bool create(const std::string& labelText) = 0; virtual bool create(const std::string& labelText) = 0;
virtual std::vector<uint8_t> getRaw() const = 0; virtual std::vector<uint8_t> getRaw() = 0;
virtual int getWidth() const = 0; virtual int getWidth() = 0;
virtual int getHeight() const = 0; virtual int getHeight() = 0;
virtual void setText(const std::string& text) = 0; virtual void setText(const std::string& text) = 0;
virtual void setFontSize(const double fontSize) = 0; virtual void setFontSize(const double fontSize) = 0;
virtual void setFontFamily(const std::string& fontFamily) = 0; virtual void setFontFamily(const std::string& fontFamily) = 0;
virtual void setHAlign(HAlignPosition hpos) = 0; virtual void setHAlign(HAlignPosition hpos) = 0;
virtual void setVAlign(VAlignPosition vpos) = 0; virtual void setVAlign(VAlignPosition vpos) = 0;
/**
* @brief Append another label horizontally with spacing
* @param other The label to append
* @param spacingPx Spacing between labels in pixels (default: 60px ~5mm at 300dpi)
* @return true on success, false if heights don't match
*/
virtual bool append(const ILabel& other, uint32_t spacingPx = 60) = 0;
}; };
} // namespace ptprnt::graphics } // namespace ptprnt::graphics

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023-2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -22,22 +22,22 @@
#include <memory> #include <memory>
#include <string_view> #include <string_view>
#include "IPrinterTypes.hpp"
#include "graphics/Bitmap.hpp" #include "graphics/Bitmap.hpp"
#include "graphics/Monochrome.hpp" #include "graphics/Monochrome.hpp"
#include "graphics/interface/ILabel.hpp" #include "graphics/interface/ILabel.hpp"
#include "interface/IPrinterTypes.hpp"
#include "libusbwrap/interface/IUsbDevice.hpp" #include "libusbwrap/interface/IUsbDevice.hpp"
namespace ptprnt { namespace ptprnt {
class IPrinterDriver { class IPrinterDriver {
public: public:
virtual ~IPrinterDriver() = default; virtual ~IPrinterDriver() = default;
[[nodiscard]] virtual std::string_view getDriverName() = 0; [[nodiscard]] virtual const std::string_view getDriverName() = 0;
[[nodiscard]] virtual std::string_view getName() = 0; [[nodiscard]] virtual const std::string_view getName() = 0;
[[nodiscard]] virtual std::string_view getVersion() = 0; [[nodiscard]] virtual const std::string_view getVersion() = 0;
[[nodiscard]] virtual libusbwrap::usbId getUsbId() = 0; [[nodiscard]] virtual const libusbwrap::usbId getUsbId() = 0;
[[nodiscard]] virtual PrinterInfo getPrinterInfo() = 0; [[nodiscard]] virtual const PrinterInfo getPrinterInfo() = 0;
[[nodiscard]] virtual PrinterStatus getPrinterStatus() = 0; [[nodiscard]] virtual const PrinterStatus getPrinterStatus() = 0;
virtual bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) = 0; virtual bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) = 0;
virtual bool detachUsbDevice() = 0; virtual bool detachUsbDevice() = 0;
virtual bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) = 0; virtual bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) = 0;

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023-2024 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -22,6 +22,7 @@
#include <sys/types.h> #include <sys/types.h>
#include <cstdint> #include <cstdint>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -43,9 +44,8 @@ enum class Speed {
class IUsbDevice { class IUsbDevice {
public: public:
virtual ~IUsbDevice() = default; virtual ~IUsbDevice() = default;
virtual bool open() = 0;
virtual bool open() = 0; virtual void close() = 0;
virtual void close() = 0;
// libusb wrappers // libusb wrappers
virtual bool detachKernelDriver(int interfaceNo) = 0; virtual bool detachKernelDriver(int interfaceNo) = 0;

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2022-2025 Moritz Martinius Copyright (C) 2022-2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,34 +1,34 @@
ptprnt_hpps = files( ptprnt_hpps = files (
'cli/CliParser.hpp', 'libusbwrap/interface/IUsbDeviceFactory.hpp',
'core/PrinterDriverFactory.hpp', 'libusbwrap/interface/IUsbDevice.hpp',
'core/PrinterService.hpp', 'libusbwrap/UsbDeviceFactory.hpp',
'libusbwrap/LibUsbTypes.hpp',
'libusbwrap/UsbDevice.hpp',
'interface/IPrinterDriver.hpp',
'interface/IPrinterTypes.hpp',
'printers/P700Printer.hpp',
'printers/FakePrinter.hpp',
'PtouchPrint.hpp',
'PrinterDriverFactory.hpp',
'graphics/Bitmap.hpp', 'graphics/Bitmap.hpp',
'graphics/Label.hpp', 'graphics/Label.hpp',
'graphics/LabelBuilder.hpp', 'graphics/LabelBuilder.hpp',
'graphics/Monochrome.hpp', 'graphics/Monochrome.hpp',
'libusbwrap/LibUsbTypes.hpp', 'cli/CliParser.hpp',
'libusbwrap/UsbDevice.hpp', 'core/PrinterService.hpp'
'libusbwrap/UsbDeviceFactory.hpp',
'libusbwrap/interface/IUsbDevice.hpp',
'libusbwrap/interface/IUsbDeviceFactory.hpp',
'printers/FakePrinter.hpp',
'printers/P700Printer.hpp',
'printers/interface/IPrinterDriver.hpp',
'printers/interface/IPrinterTypes.hpp',
'PtouchPrint.hpp',
) )
ptprnt_srcs = files( ptprnt_srcs = files (
'cli/CliParser.cpp', 'PtouchPrint.cpp',
'core/PrinterDriverFactory.cpp', 'PrinterDriverFactory.cpp',
'core/PrinterService.cpp', 'printers/P700Printer.cpp',
'graphics/Bitmap.cpp', 'printers/FakePrinter.cpp',
'graphics/Label.cpp', 'graphics/Label.cpp',
'graphics/LabelBuilder.cpp', 'graphics/LabelBuilder.cpp',
'graphics/Bitmap.cpp',
'graphics/Monochrome.cpp', 'graphics/Monochrome.cpp',
'libusbwrap/UsbDevice.cpp',
'libusbwrap/UsbDeviceFactory.cpp', 'libusbwrap/UsbDeviceFactory.cpp',
'printers/FakePrinter.cpp', 'libusbwrap/UsbDevice.cpp',
'printers/P700Printer.cpp', 'cli/CliParser.cpp',
'PtouchPrint.cpp', 'core/PrinterService.cpp'
) )

View File

@@ -19,50 +19,49 @@
#include "FakePrinter.hpp" #include "FakePrinter.hpp"
#include <cairo.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <cairo.h>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <iomanip>
#include <sstream>
#include <stdexcept> #include <stdexcept>
#include <vector> #include <vector>
#include <chrono>
#include <iomanip>
#include <sstream>
#include "graphics/Monochrome.hpp" #include "../graphics/Monochrome.hpp"
namespace ptprnt::printer { namespace ptprnt::printer {
const PrinterInfo FakePrinter::mInfo = {.driverName = "FakePrinter", const PrinterInfo FakePrinter::mInfo = {
.name = "Virtual Test Printer", .driverName = "FakePrinter",
.version = "v1.0", .name = "Virtual Test Printer",
.usbId{0x0000, 0x0000}, // No USB ID - virtual printer created explicitly .version = "v1.0",
.pixelLines = 128}; .usbId{0x0000, 0x0000}, // No USB ID - virtual printer created explicitly
.pixelLines = 128
};
std::string_view FakePrinter::getDriverName() { const std::string_view FakePrinter::getDriverName() {
return mInfo.driverName; return mInfo.driverName;
} }
std::string_view FakePrinter::getName() { const std::string_view FakePrinter::getName() {
return mInfo.name; return mInfo.name;
} }
std::string_view FakePrinter::getVersion() { const std::string_view FakePrinter::getVersion() {
return mInfo.version; return mInfo.version;
} }
PrinterInfo FakePrinter::getPrinterInfo() { const PrinterInfo FakePrinter::getPrinterInfo() {
return mInfo; return mInfo;
} }
PrinterStatus FakePrinter::getPrinterStatus() { const PrinterStatus FakePrinter::getPrinterStatus() {
if (!mHasAttachedDevice) {
return {};
}
return mStatus; return mStatus;
} }
libusbwrap::usbId FakePrinter::getUsbId() { const libusbwrap::usbId FakePrinter::getUsbId() {
return mInfo.usbId; return mInfo.usbId;
} }
@@ -81,25 +80,22 @@ bool FakePrinter::detachUsbDevice() {
bool FakePrinter::printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) { bool FakePrinter::printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) {
// Convert bitmap to MonochromeData and delegate // Convert bitmap to MonochromeData and delegate
auto pixels = bitmap.getPixelsCpy(); auto pixels = bitmap.getPixelsCpy();
auto mono = graphics::Monochrome(pixels, bitmap.getWidth(), bitmap.getHeight()); auto mono = graphics::Monochrome(pixels, bitmap.getWidth(), bitmap.getHeight());
auto monoData = mono.get(); auto monoData = mono.get();
return printMonochromeData(monoData); return printMonochromeData(monoData);
} }
bool FakePrinter::printMonochromeData(const graphics::MonochromeData& data) { bool FakePrinter::printMonochromeData(const graphics::MonochromeData& data) {
if (!mHasAttachedDevice) {
return false;
}
spdlog::debug("FakePrinter: Simulating printing of {}x{} bitmap", data.width, data.height); spdlog::debug("FakePrinter: Simulating printing of {}x{} bitmap", data.width, data.height);
// Simulate the printing process by reconstructing the bitmap // Simulate the printing process by reconstructing the bitmap
auto printed = simulatePrinting(data); auto printed = simulatePrinting(data);
mLastPrint = std::make_unique<graphics::Bitmap<graphics::ALPHA8>>(std::move(printed)); mLastPrint = std::make_unique<graphics::Bitmap<graphics::ALPHA8>>(std::move(printed));
spdlog::info("FakePrinter: Successfully 'printed' label ({}x{} pixels)", mLastPrint->getWidth(), spdlog::info("FakePrinter: Successfully 'printed' label ({}x{} pixels)",
mLastPrint->getHeight()); mLastPrint->getWidth(), mLastPrint->getHeight());
// Save to timestamped PNG file // Save to timestamped PNG file
std::string filename = generateTimestampedFilename(); std::string filename = generateTimestampedFilename();
@@ -124,16 +120,12 @@ bool FakePrinter::printLabel(const std::unique_ptr<graphics::ILabel> label) {
// Transform to portrait orientation for printing // Transform to portrait orientation for printing
monoData.transformTo(graphics::Orientation::PORTRAIT); monoData.transformTo(graphics::Orientation::PORTRAIT);
spdlog::debug("FakePrinter: Label surface is {}x{}, transformed to portrait", label->getWidth(), spdlog::debug("FakePrinter: Label surface is {}x{}, transformed to portrait", label->getWidth(), label->getHeight());
label->getHeight());
return printMonochromeData(monoData); return printMonochromeData(monoData);
} }
bool FakePrinter::print() { bool FakePrinter::print() {
if (!mHasAttachedDevice) {
return false;
}
spdlog::debug("FakePrinter: Print command (no-op for virtual printer)"); spdlog::debug("FakePrinter: Print command (no-op for virtual printer)");
return true; return true;
} }
@@ -168,7 +160,7 @@ graphics::Bitmap<graphics::ALPHA8> FakePrinter::simulatePrinting(const graphics:
// Now "print" this column by unpacking the bytes back to pixels // Now "print" this column by unpacking the bytes back to pixels
for (size_t byteIdx = 0; byteIdx < columnBytes.size(); byteIdx++) { for (size_t byteIdx = 0; byteIdx < columnBytes.size(); byteIdx++) {
uint8_t byte = columnBytes[byteIdx]; uint8_t byte = columnBytes[byteIdx];
uint32_t baseRow = byteIdx * 8; uint32_t baseRow = byteIdx * 8;
for (int bit = 0; bit < 8 && (baseRow + bit) < data.height; bit++) { for (int bit = 0; bit < 8 && (baseRow + bit) < data.height; bit++) {
@@ -176,7 +168,7 @@ graphics::Bitmap<graphics::ALPHA8> FakePrinter::simulatePrinting(const graphics:
uint32_t row = baseRow + bit; uint32_t row = baseRow + bit;
// Write to output bitmap // Write to output bitmap
size_t pixelIdx = row * data.width + col; size_t pixelIdx = row * data.width + col;
pixels[pixelIdx] = pixelOn ? 255 : 0; // 255 = black, 0 = white pixels[pixelIdx] = pixelOn ? 255 : 0; // 255 = black, 0 = white
} }
} }
@@ -185,8 +177,8 @@ graphics::Bitmap<graphics::ALPHA8> FakePrinter::simulatePrinting(const graphics:
// Set the pixels in the result bitmap // Set the pixels in the result bitmap
result.setPixels(pixels); result.setPixels(pixels);
spdlog::debug("FakePrinter: Simulation complete, reconstructed {}x{} bitmap", result.getWidth(), spdlog::debug("FakePrinter: Simulation complete, reconstructed {}x{} bitmap",
result.getHeight()); result.getWidth(), result.getHeight());
return result; return result;
} }
@@ -209,8 +201,8 @@ bool FakePrinter::saveLastPrintToPng(const std::string& filename) const {
bool FakePrinter::saveBitmapToPng(const graphics::Bitmap<graphics::ALPHA8>& bitmap, const std::string& filename) const { bool FakePrinter::saveBitmapToPng(const graphics::Bitmap<graphics::ALPHA8>& bitmap, const std::string& filename) const {
// Create Cairo surface from bitmap data // Create Cairo surface from bitmap data
auto pixels = bitmap.getPixelsCpy(); auto pixels = bitmap.getPixelsCpy();
uint16_t width = bitmap.getWidth(); uint16_t width = bitmap.getWidth();
uint16_t height = bitmap.getHeight(); uint16_t height = bitmap.getHeight();
// Cairo expects ARGB32 format, but we have ALPHA8 // Cairo expects ARGB32 format, but we have ALPHA8
@@ -225,9 +217,14 @@ bool FakePrinter::saveBitmapToPng(const graphics::Bitmap<graphics::ALPHA8>& bitm
} }
// Create Cairo surface // Create Cairo surface
int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width);
cairo_surface_t* surface = cairo_image_surface_create_for_data(reinterpret_cast<unsigned char*>(argbPixels.data()), cairo_surface_t* surface = cairo_image_surface_create_for_data(
CAIRO_FORMAT_ARGB32, width, height, stride); reinterpret_cast<unsigned char*>(argbPixels.data()),
CAIRO_FORMAT_ARGB32,
width,
height,
stride
);
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
spdlog::error("FakePrinter: Failed to create Cairo surface: {}", spdlog::error("FakePrinter: Failed to create Cairo surface: {}",
@@ -251,12 +248,14 @@ bool FakePrinter::saveBitmapToPng(const graphics::Bitmap<graphics::ALPHA8>& bitm
std::string FakePrinter::generateTimestampedFilename() const { std::string FakePrinter::generateTimestampedFilename() const {
// Get current time // Get current time
auto now = std::chrono::system_clock::now(); auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now); auto time = std::chrono::system_clock::to_time_t(now);
// Format: fakelabel_YYYYMMDD_HHMMSS.png // Format: fakelabel_YYYYMMDD_HHMMSS.png
std::stringstream ss; std::stringstream ss;
ss << "fakelabel_" << std::put_time(std::localtime(&time), "%Y%m%d_%H%M%S") << ".png"; ss << "fakelabel_"
<< std::put_time(std::localtime(&time), "%Y%m%d_%H%M%S")
<< ".png";
return ss.str(); return ss.str();
} }

View File

@@ -19,15 +19,15 @@
#pragma once #pragma once
#include <cstdint>
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <cstdint>
#include "graphics/Bitmap.hpp" #include "../interface/IPrinterDriver.hpp"
#include "interface/IPrinterDriver.hpp" #include "../interface/IPrinterTypes.hpp"
#include "interface/IPrinterTypes.hpp" #include "../libusbwrap/LibUsbTypes.hpp"
#include "libusbwrap/LibUsbTypes.hpp" #include "../libusbwrap/interface/IUsbDevice.hpp"
#include "libusbwrap/interface/IUsbDevice.hpp" #include "../graphics/Bitmap.hpp"
namespace ptprnt::printer { namespace ptprnt::printer {
@@ -40,7 +40,7 @@ namespace ptprnt::printer {
*/ */
class FakePrinter : public ::ptprnt::IPrinterDriver { class FakePrinter : public ::ptprnt::IPrinterDriver {
public: public:
FakePrinter() = default; FakePrinter() = default;
~FakePrinter() override = default; ~FakePrinter() override = default;
FakePrinter(const FakePrinter&) = delete; FakePrinter(const FakePrinter&) = delete;
@@ -52,12 +52,12 @@ class FakePrinter : public ::ptprnt::IPrinterDriver {
static const PrinterInfo mInfo; static const PrinterInfo mInfo;
// IPrinterDriver interface // IPrinterDriver interface
[[nodiscard]] std::string_view getDriverName() override; [[nodiscard]] const std::string_view getDriverName() override;
[[nodiscard]] std::string_view getName() override; [[nodiscard]] const std::string_view getName() override;
[[nodiscard]] libusbwrap::usbId getUsbId() override; [[nodiscard]] const libusbwrap::usbId getUsbId() override;
[[nodiscard]] std::string_view getVersion() override; [[nodiscard]] const std::string_view getVersion() override;
[[nodiscard]] PrinterInfo getPrinterInfo() override; [[nodiscard]] const PrinterInfo getPrinterInfo() override;
[[nodiscard]] PrinterStatus getPrinterStatus() override; [[nodiscard]] const PrinterStatus getPrinterStatus() override;
bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) override; bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) override;
bool detachUsbDevice() override; bool detachUsbDevice() override;
bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) override; bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) override;

View File

@@ -28,9 +28,9 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
#include "graphics/Bitmap.hpp" #include "../graphics/Bitmap.hpp"
#include "graphics/Monochrome.hpp" #include "../graphics/Monochrome.hpp"
#include "libusbwrap/LibUsbTypes.hpp" #include "../libusbwrap/LibUsbTypes.hpp"
#include "spdlog/fmt/bin_to_hex.h" #include "spdlog/fmt/bin_to_hex.h"
namespace ptprnt::printer { namespace ptprnt::printer {
@@ -48,30 +48,24 @@ P700Printer::~P700Printer() {
} }
} }
std::string_view P700Printer::getDriverName() { const std::string_view P700Printer::getDriverName() {
return mInfo.driverName; return mInfo.driverName;
} }
std::string_view P700Printer::getName() { const std::string_view P700Printer::getName() {
return mInfo.name; return mInfo.name;
} }
std::string_view P700Printer::getVersion() { const std::string_view P700Printer::getVersion() {
return mInfo.version; return mInfo.version;
} }
PrinterInfo P700Printer::getPrinterInfo() { const PrinterInfo P700Printer::getPrinterInfo() {
return mInfo; return mInfo;
} }
PrinterStatus P700Printer::getPrinterStatus() { const PrinterStatus P700Printer::getPrinterStatus() {
using namespace std::chrono_literals; using namespace std::chrono_literals;
if (!mUsbHndl) {
spdlog::error("USB Handle is invalid!");
return {};
}
send(p700::commands::GET_STATUS); send(p700::commands::GET_STATUS);
int tx = 0; int tx = 0;
@@ -85,7 +79,7 @@ PrinterStatus P700Printer::getPrinterStatus() {
return PrinterStatus{.tapeWidthMm = recvBuf[10]}; return PrinterStatus{.tapeWidthMm = recvBuf[10]};
} }
libusbwrap::usbId P700Printer::getUsbId() { const libusbwrap::usbId P700Printer::getUsbId() {
return mInfo.usbId; return mInfo.usbId;
} }

View File

@@ -17,8 +17,6 @@
*/ */
#pragma once
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <sys/types.h> #include <sys/types.h>
@@ -30,13 +28,15 @@
#include "libusbwrap/LibUsbTypes.hpp" #include "libusbwrap/LibUsbTypes.hpp"
#include "libusbwrap/interface/IUsbDevice.hpp" #include "libusbwrap/interface/IUsbDevice.hpp"
#pragma once
namespace ptprnt::printer { namespace ptprnt::printer {
namespace p700::commands { namespace p700::commands {
const cmd_T INITIALIZE{0x1b, 0x40}; // ESC @ - Initialize const cmd_T INITIALIZE{0x1b, 0x40}; // ESC @ - Initialize
const cmd_T GET_STATUS{0x1b, 0x69, 0x53}; // ESC i S - Status query 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 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 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 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 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 INFO{0x1b, 0x69, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const cmd_T PACKBITSON{0x02}; const cmd_T PACKBITSON{0x02};
@@ -66,12 +66,12 @@ class P700Printer : public ::ptprnt::IPrinterDriver {
static const PrinterInfo mInfo; static const PrinterInfo mInfo;
// IPrinterDriver // IPrinterDriver
[[nodiscard]] std::string_view getDriverName() override; [[nodiscard]] const std::string_view getDriverName() override;
[[nodiscard]] std::string_view getName() override; [[nodiscard]] const std::string_view getName() override;
[[nodiscard]] libusbwrap::usbId getUsbId() override; [[nodiscard]] const libusbwrap::usbId getUsbId() override;
[[nodiscard]] std::string_view getVersion() override; [[nodiscard]] const std::string_view getVersion() override;
[[nodiscard]] PrinterInfo getPrinterInfo() override; [[nodiscard]] const PrinterInfo getPrinterInfo() override;
[[nodiscard]] PrinterStatus getPrinterStatus() override; [[nodiscard]] const PrinterStatus getPrinterStatus() override;
bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) override; bool attachUsbDevice(std::shared_ptr<libusbwrap::IUsbDevice> usbHndl) override;
bool detachUsbDevice() override; bool detachUsbDevice() override;
bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) override; bool printBitmap(const graphics::Bitmap<graphics::ALPHA8>& bitmap) override;

0
src/ptprnt.log Normal file
View File

View File

@@ -1,10 +1,9 @@
[wrap-file] [wrap-file]
directory = CLI11-2.5.0 directory = CLI11-2.3.2
source_url = https://github.com/CLIUtils/CLI11/archive/refs/tags/v2.5.0.tar.gz source_url = https://github.com/CLIUtils/CLI11/archive/refs/tags/v2.3.2.tar.gz
source_filename = CLI11-2.5.0.tar.gz source_filename = CLI11-2.3.2.tar.gz
source_hash = 17e02b4cddc2fa348e5dbdbb582c59a3486fa2b2433e70a0c3bacb871334fd55 source_hash = aac0ab42108131ac5d3344a9db0fdf25c4db652296641955720a4fbe52334e22
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/cli11_2.5.0-2/CLI11-2.5.0.tar.gz wrapdb_version = 2.3.2-1
wrapdb_version = 2.5.0-2
[provide] [provide]
dependency_names = CLI11 cli11 = CLI11_dep

View File

@@ -1,13 +1,13 @@
[wrap-file] [wrap-file]
directory = googletest-1.17.0 directory = googletest-1.14.0
source_url = https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz source_url = https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz
source_filename = googletest-1.17.0.tar.gz source_filename = gtest-1.14.0.tar.gz
source_hash = 65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c source_hash = 8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7
patch_filename = gtest_1.17.0-4_patch.zip patch_filename = gtest_1.14.0-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.17.0-4/get_patch patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.14.0-1/get_patch
patch_hash = 3abf7662d09db706453a5b064a1e914678c74b9d9b0b19382747ca561d0d8750 patch_hash = 2e693c7d3f9370a7aa6dac802bada0874d3198ad4cfdf75647b818f691182b50
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.17.0-4/googletest-1.17.0.tar.gz source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.14.0-1/gtest-1.14.0.tar.gz
wrapdb_version = 1.17.0-4 wrapdb_version = 1.14.0-1
[provide] [provide]
gtest = gtest_dep gtest = gtest_dep

View File

@@ -1,13 +0,0 @@
[wrap-file]
directory = spdlog-1.15.3
source_url = https://github.com/gabime/spdlog/archive/refs/tags/v1.15.3.tar.gz
source_filename = spdlog-1.15.3.tar.gz
source_hash = 15a04e69c222eb6c01094b5c7ff8a249b36bb22788d72519646fb85feb267e67
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/spdlog_1.15.3-5/spdlog-1.15.3.tar.gz
patch_filename = spdlog_1.15.3-5_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.15.3-5/get_patch
patch_hash = 5e0eaf0002ff589cd8dac58e1b38c297422e7a0404d7d47ff0d2e285ed18169c
wrapdb_version = 1.15.3-5
[provide]
dependency_names = spdlog

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -19,244 +19,8 @@
#include "graphics/Label.hpp" #include "graphics/Label.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <memory>
#include <vector>
#include "../../tests/mocks/MockCairoWrapper.hpp"
#include "graphics/interface/ILabel.hpp"
using ::testing::_;
using ::testing::DoAll;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::SetArgPointee;
namespace ptprnt::graphics {
// Test fixture for Label tests with comprehensive mock setup
class LabelTest : public ::testing::Test {
protected:
void SetUp() override {
mockWrapper = std::make_shared<NiceMock<MockCairoWrapper>>();
// Mock pointers for temporary surface (used in size calculation)
mockTempSurface = reinterpret_cast<cairo_surface_t*>(0x2000);
mockTempCr = reinterpret_cast<cairo_t*>(0x2001);
mockTempCtx = reinterpret_cast<PangoContext*>(0x2002);
mockTempLayout = reinterpret_cast<PangoLayout*>(0x2003);
// Mock pointers for final surface (used in rendering)
mockFinalSurface = reinterpret_cast<cairo_surface_t*>(0x3000);
mockFinalCr = reinterpret_cast<cairo_t*>(0x3001);
mockFinalCtx = reinterpret_cast<PangoContext*>(0x3002);
mockFinalLayout = reinterpret_cast<PangoLayout*>(0x3003);
// Mock font description
mockFontDesc = reinterpret_cast<PangoFontDescription*>(0x2004);
// Default layout size: 100x30 pixels (in PANGO_SCALE units)
defaultLayoutWidth = 100;
defaultLayoutHeight = 30;
SetupDefaultBehaviors();
}
void SetupDefaultBehaviors() {
// Font map
ON_CALL(*mockWrapper, pango_cairo_font_map_new())
.WillByDefault(Return(reinterpret_cast<PangoFontMap*>(0x1000)));
// Temporary surface creation (for size calculation)
ON_CALL(*mockWrapper, cairo_image_surface_create(CAIRO_FORMAT_A8, 1, 1)).WillByDefault(Return(mockTempSurface));
ON_CALL(*mockWrapper, cairo_create(mockTempSurface)).WillByDefault(Return(mockTempCr));
ON_CALL(*mockWrapper, pango_cairo_create_context(mockTempCr)).WillByDefault(Return(mockTempCtx));
ON_CALL(*mockWrapper, pango_layout_new(mockTempCtx)).WillByDefault(Return(mockTempLayout));
// Final surface creation (for rendering) - use _ for width since it varies
ON_CALL(*mockWrapper, cairo_image_surface_create(CAIRO_FORMAT_A8, _, _))
.WillByDefault(Return(mockFinalSurface));
ON_CALL(*mockWrapper, cairo_create(mockFinalSurface)).WillByDefault(Return(mockFinalCr));
ON_CALL(*mockWrapper, pango_cairo_create_context(mockFinalCr)).WillByDefault(Return(mockFinalCtx));
ON_CALL(*mockWrapper, pango_layout_new(mockFinalCtx)).WillByDefault(Return(mockFinalLayout));
// Font description
ON_CALL(*mockWrapper, pango_font_description_new()).WillByDefault(Return(mockFontDesc));
// Layout size - return default dimensions
ON_CALL(*mockWrapper, pango_layout_get_size(_, _, _))
.WillByDefault(DoAll(SetArgPointee<1>(defaultLayoutWidth * PANGO_SCALE),
SetArgPointee<2>(defaultLayoutHeight * PANGO_SCALE)));
// Surface status - always success
ON_CALL(*mockWrapper, cairo_surface_status(_)).WillByDefault(Return(CAIRO_STATUS_SUCCESS));
// Surface properties for getRaw()
ON_CALL(*mockWrapper, cairo_image_surface_get_format(_)).WillByDefault(Return(CAIRO_FORMAT_A8));
ON_CALL(*mockWrapper, cairo_image_surface_get_width(_)).WillByDefault(Return(defaultLayoutWidth));
ON_CALL(*mockWrapper, cairo_image_surface_get_height(_)).WillByDefault(Return(128));
ON_CALL(*mockWrapper, cairo_image_surface_get_stride(_)).WillByDefault(Return(defaultLayoutWidth));
// Mock data pointer
mockSurfaceData.resize(defaultLayoutWidth * 128, 0xFF);
ON_CALL(*mockWrapper, cairo_image_surface_get_data(_)).WillByDefault(Return(mockSurfaceData.data()));
}
// Helper method to set custom layout dimensions
void SetLayoutSize(int width, int height) {
defaultLayoutWidth = width;
defaultLayoutHeight = height;
// Update the mock to return new dimensions
ON_CALL(*mockWrapper, pango_layout_get_size(_, _, _))
.WillByDefault(DoAll(SetArgPointee<1>(width * PANGO_SCALE), SetArgPointee<2>(height * PANGO_SCALE)));
ON_CALL(*mockWrapper, cairo_image_surface_get_width(_)).WillByDefault(Return(width));
ON_CALL(*mockWrapper, cairo_image_surface_get_stride(_)).WillByDefault(Return(width));
// Resize mock data
mockSurfaceData.resize(width * 128, 0xFF);
ON_CALL(*mockWrapper, cairo_image_surface_get_data(_)).WillByDefault(Return(mockSurfaceData.data()));
}
std::shared_ptr<NiceMock<MockCairoWrapper>> mockWrapper;
// Mock pointers
cairo_surface_t* mockTempSurface;
cairo_t* mockTempCr;
PangoContext* mockTempCtx;
PangoLayout* mockTempLayout;
cairo_surface_t* mockFinalSurface;
cairo_t* mockFinalCr;
PangoContext* mockFinalCtx;
PangoLayout* mockFinalLayout;
PangoFontDescription* mockFontDesc;
// Default layout dimensions
int defaultLayoutWidth;
int defaultLayoutHeight;
// Mock surface data
std::vector<unsigned char> mockSurfaceData;
};
// Smoke test with real Cairo/Pango
TEST(basic_test, Label_smokeTest_succeeds) { TEST(basic_test, Label_smokeTest_succeeds) {
auto label = Label(128); auto im = ptprnt::graphics::Label(4711);
EXPECT_EQ(label.getHeight(), 128);
EXPECT_EQ(label.getWidth(), 0); // No label created yet
} }
// Constructor test with mock
TEST_F(LabelTest, Constructor_InitializesFontMap) {
EXPECT_CALL(*mockWrapper, pango_cairo_font_map_new()).Times(1);
auto label = Label(128, mockWrapper);
EXPECT_EQ(label.getHeight(), 128);
EXPECT_EQ(label.getWidth(), 0);
}
// Test getters before label creation
TEST_F(LabelTest, Getters_BeforeCreate_ReturnDefaults) {
auto label = Label(256, mockWrapper);
EXPECT_EQ(label.getHeight(), 256);
EXPECT_EQ(label.getWidth(), 0);
}
// Test setters
TEST_F(LabelTest, Setters_ModifyProperties) {
auto label = Label(128, mockWrapper);
label.setFontSize(24.0);
label.setFontFamily("Arial");
label.setText("Test");
label.setHAlign(HAlignPosition::CENTER);
label.setVAlign(VAlignPosition::BOTTOM);
// Properties are set (no way to verify without create, but no crash is good)
SUCCEED();
}
// Test create() - basic functionality with simplified setup
TEST_F(LabelTest, Create_WithText_Succeeds) {
auto label = Label(128, mockWrapper);
label.setFontSize(12.0);
label.setFontFamily("Sans");
bool result = label.create("Hello");
EXPECT_TRUE(result);
EXPECT_EQ(label.getWidth(), defaultLayoutWidth);
EXPECT_EQ(label.getHeight(), 128);
}
// Test horizontal alignment - RIGHT
TEST_F(LabelTest, Create_WithRightAlignment_SetsCorrectPangoAlignment) {
auto label = Label(128, mockWrapper);
label.setHAlign(HAlignPosition::RIGHT);
// Verify RIGHT alignment is set (temp + final layout)
EXPECT_CALL(*mockWrapper, pango_layout_set_alignment(_, PANGO_ALIGN_RIGHT)).Times(2);
label.create("Right");
}
// Test horizontal alignment - JUSTIFY
TEST_F(LabelTest, Create_WithJustifyAlignment_SetsJustifyAndAlignment) {
auto label = Label(128, mockWrapper);
label.setHAlign(HAlignPosition::JUSTIFY);
// Verify JUSTIFY requires LEFT alignment + justify flag
EXPECT_CALL(*mockWrapper, pango_layout_set_alignment(_, PANGO_ALIGN_LEFT)).Times(2);
EXPECT_CALL(*mockWrapper, pango_layout_set_justify(_, true)).Times(2);
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
EXPECT_CALL(*mockWrapper, pango_layout_set_justify_last_line(_, true)).Times(2);
#endif
label.create("Justify");
}
// Test vertical alignment - TOP (no cairo_move_to)
TEST_F(LabelTest, Create_WithTopAlignment_NoMoveToCall) {
auto label = Label(128, mockWrapper);
label.setVAlign(VAlignPosition::TOP);
// TOP alignment should NOT call cairo_move_to
EXPECT_CALL(*mockWrapper, cairo_move_to(_, _, _)).Times(0);
label.create("Top");
}
// Test vertical alignment - BOTTOM
TEST_F(LabelTest, Create_WithBottomAlignment_CallsMoveToWithCorrectOffset) {
auto label = Label(128, mockWrapper);
label.setVAlign(VAlignPosition::BOTTOM);
SetLayoutSize(50, 20); // Use helper to set custom size
// BOTTOM alignment: offset = printerHeight - layoutHeight = 128 - 20 = 108
EXPECT_CALL(*mockWrapper, cairo_move_to(mockFinalCr, 0.0, 108.0)).Times(1);
label.create("Bottom");
}
// Test vertical alignment - MIDDLE
TEST_F(LabelTest, Create_WithMiddleAlignment_CallsMoveToWithCenteredOffset) {
auto label = Label(128, mockWrapper);
label.setVAlign(VAlignPosition::MIDDLE);
SetLayoutSize(50, 20); // Use helper to set custom size
// MIDDLE alignment: offset = (printerHeight - layoutHeight) / 2 = (128 - 20) / 2 = 54
EXPECT_CALL(*mockWrapper, cairo_move_to(mockFinalCr, 0.0, 54.0)).Times(1);
label.create("Middle");
}
} // namespace ptprnt::graphics

View File

@@ -1,29 +1,40 @@
# Consolidated test binary - all tests in one executable for faster linking tests = [
[
test_sources = [ 'bitmap_test',
# Test files 'bitmap_test_exe',
'bitmap_test/bitmap_test.cpp', ['../src/graphics/Bitmap.cpp', 'bitmap_test/bitmap_test.cpp'],
'monochrome_test/monochrome_test.cpp', ],
'label_test/label_test.cpp', [
'label_test',
# Source files under test 'label_test_exe',
'../src/graphics/Bitmap.cpp', ['../src/graphics/Label.cpp', 'label_test/label_test.cpp'],
'../src/graphics/Monochrome.cpp', ],
'../src/graphics/Label.cpp', [
'monochrome_test',
'monochrome_test_exe',
[
'../src/graphics/Monochrome.cpp',
'monochrome_test/monochrome_test.cpp',
],
],
] ]
test_exe = executable( foreach test : tests
'ptprnt_tests', test(
sources: test_sources, test.get(0),
include_directories: incdir, executable(
dependencies: [ test.get(1),
gmock_dep, # GMock includes GTest sources: test.get(2),
usb_dep, include_directories: incdir,
log_dep, dependencies: [
pangocairo_dep, gtest_dep,
cli11_dep, usb_dep,
], log_dep,
) pangocairo_dep,
cli11_dep,
],
),
)
endforeach
# Single test that runs all test suites
test('all_tests', test_exe)

View File

@@ -1,84 +0,0 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <gmock/gmock.h>
#include "graphics/interface/ICairoWrapper.hpp"
namespace ptprnt::graphics {
/**
* @brief GMock implementation of ICairoWrapper for unit testing
*
* This mock allows tests to verify that the Label class correctly interacts
* with the Cairo/Pango API without requiring actual graphics rendering.
*/
class MockCairoWrapper : public ICairoWrapper {
public:
// Cairo image surface functions
MOCK_METHOD(cairo_surface_t*, cairo_image_surface_create, (cairo_format_t format, int width, int height),
(override));
MOCK_METHOD(void, cairo_surface_destroy, (cairo_surface_t * surface), (override));
MOCK_METHOD(void, cairo_surface_flush, (cairo_surface_t * surface), (override));
MOCK_METHOD(void, cairo_surface_mark_dirty, (cairo_surface_t * surface), (override));
MOCK_METHOD(cairo_status_t, cairo_surface_status, (cairo_surface_t * surface), (override));
MOCK_METHOD(cairo_format_t, cairo_image_surface_get_format, (cairo_surface_t * surface), (override));
MOCK_METHOD(int, cairo_image_surface_get_width, (cairo_surface_t * surface), (override));
MOCK_METHOD(int, cairo_image_surface_get_height, (cairo_surface_t * surface), (override));
MOCK_METHOD(int, cairo_image_surface_get_stride, (cairo_surface_t * surface), (override));
MOCK_METHOD(unsigned char*, cairo_image_surface_get_data, (cairo_surface_t * surface), (override));
MOCK_METHOD(cairo_status_t, cairo_surface_write_to_png, (cairo_surface_t * surface, const char* filename),
(override));
// Cairo context functions
MOCK_METHOD(cairo_t*, cairo_create, (cairo_surface_t * surface), (override));
MOCK_METHOD(void, cairo_destroy, (cairo_t * cr), (override));
MOCK_METHOD(void, cairo_move_to, (cairo_t * cr, double x, double y), (override));
MOCK_METHOD(void, cairo_set_source_rgb, (cairo_t * cr, double red, double green, double blue), (override));
// Pango-Cairo functions
MOCK_METHOD(PangoFontMap*, pango_cairo_font_map_new, (), (override));
MOCK_METHOD(PangoContext*, pango_cairo_create_context, (cairo_t * cr), (override));
MOCK_METHOD(void, pango_cairo_show_layout, (cairo_t * cr, PangoLayout* layout), (override));
// Pango layout functions
MOCK_METHOD(PangoLayout*, pango_layout_new, (PangoContext * context), (override));
MOCK_METHOD(void, pango_layout_set_font_description, (PangoLayout * layout, const PangoFontDescription* desc),
(override));
MOCK_METHOD(void, pango_layout_set_text, (PangoLayout * layout, const char* text, int length), (override));
MOCK_METHOD(void, pango_layout_set_height, (PangoLayout * layout, int height), (override));
MOCK_METHOD(void, pango_layout_set_alignment, (PangoLayout * layout, PangoAlignment alignment), (override));
MOCK_METHOD(void, pango_layout_set_justify, (PangoLayout * layout, gboolean justify), (override));
#if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50
MOCK_METHOD(void, pango_layout_set_justify_last_line, (PangoLayout * layout, gboolean justify), (override));
#endif
MOCK_METHOD(void, pango_layout_get_size, (PangoLayout * layout, int* width, int* height), (override));
// Pango font description functions
MOCK_METHOD(PangoFontDescription*, pango_font_description_new, (), (override));
MOCK_METHOD(void, pango_font_description_set_size, (PangoFontDescription * desc, gint size), (override));
MOCK_METHOD(void, pango_font_description_set_family, (PangoFontDescription * desc, const char* family),
(override));
// GObject reference counting
MOCK_METHOD(void, g_object_unref, (gpointer object), (override));
};
} // namespace ptprnt::graphics

View File

@@ -1,6 +1,6 @@
/* /*
ptrnt - print labels on linux ptrnt - print labels on linux
Copyright (C) 2023-2025 Moritz Martinius Copyright (C) 2023 Moritz Martinius
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by