/*
ptrnt - print labels on linux
Copyright (C) 2025 Moritz Martinius
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include "FakePrinter.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include "graphics/Monochrome.hpp"
namespace ptprnt::printer {
const PrinterInfo FakePrinter::mInfo = {.driverName = "FakePrinter",
.name = "Virtual Test Printer",
.version = "v1.0",
.usbId{0x0000, 0x0000}, // No USB ID - virtual printer created explicitly
.pixelLines = 128};
std::string_view FakePrinter::getDriverName() {
return mInfo.driverName;
}
std::string_view FakePrinter::getName() {
return mInfo.name;
}
std::string_view FakePrinter::getVersion() {
return mInfo.version;
}
PrinterInfo FakePrinter::getPrinterInfo() {
return mInfo;
}
PrinterStatus FakePrinter::getPrinterStatus() {
if (!mHasAttachedDevice) {
return {};
}
return mStatus;
}
libusbwrap::usbId FakePrinter::getUsbId() {
return mInfo.usbId;
}
bool FakePrinter::attachUsbDevice(std::shared_ptr usbHndl) {
// FakePrinter doesn't need a real USB device
mHasAttachedDevice = true;
spdlog::debug("FakePrinter: Simulated USB device attachment");
return true;
}
bool FakePrinter::detachUsbDevice() {
mHasAttachedDevice = false;
spdlog::debug("FakePrinter: Simulated USB device detachment");
return true;
}
bool FakePrinter::printBitmap(const graphics::Bitmap& bitmap) {
// Convert bitmap to MonochromeData and delegate
auto pixels = bitmap.getPixelsCpy();
auto mono = graphics::Monochrome(pixels, bitmap.getWidth(), bitmap.getHeight());
auto monoData = mono.get();
return printMonochromeData(monoData);
}
bool FakePrinter::printMonochromeData(const graphics::MonochromeData& data) {
if (!mHasAttachedDevice) {
return false;
}
spdlog::debug("FakePrinter: Simulating printing of {}x{} bitmap", data.width, data.height);
// Simulate the printing process by reconstructing the bitmap
auto printed = simulatePrinting(data);
mLastPrint = std::make_unique>(std::move(printed));
spdlog::info("FakePrinter: Successfully 'printed' label ({}x{} pixels)", mLastPrint->getWidth(),
mLastPrint->getHeight());
// Save to timestamped PNG file
std::string filename = generateTimestampedFilename();
if (saveBitmapToPng(*mLastPrint, filename)) {
spdlog::info("FakePrinter: Saved output to {}", filename);
} else {
spdlog::error("FakePrinter: Failed to save output to {}", filename);
}
return true;
}
bool FakePrinter::printLabel(const std::unique_ptr label) {
// Convert label directly to MonochromeData
// getRaw() returns data in Cairo surface coordinates matching getWidth() × getHeight()
auto pixels = label->getRaw();
// Create monochrome data in landscape orientation (as stored in Cairo surface)
auto mono = graphics::Monochrome(pixels, label->getWidth(), label->getHeight(), graphics::Orientation::LANDSCAPE);
auto monoData = mono.get();
// Transform to portrait orientation for printing
monoData.transformTo(graphics::Orientation::PORTRAIT);
spdlog::debug("FakePrinter: Label surface is {}x{}, transformed to portrait", label->getWidth(),
label->getHeight());
return printMonochromeData(monoData);
}
bool FakePrinter::print() {
if (!mHasAttachedDevice) {
return false;
}
spdlog::debug("FakePrinter: Print command (no-op for virtual printer)");
return true;
}
graphics::Bitmap FakePrinter::simulatePrinting(const graphics::MonochromeData& data) {
spdlog::debug("FakePrinter: Simulating column-by-column printing like real hardware");
// Create output bitmap with same dimensions
graphics::Bitmap result(data.width, data.height);
std::vector pixels(data.width * data.height, 0);
// Simulate printer behavior: process column by column
// This mimics how label printers physically print one vertical line at a time
for (uint32_t col = 0; col < data.width; col++) {
spdlog::trace("FakePrinter: Processing column {}/{}", col + 1, data.width);
// Extract column data bit by bit (simulating what would be sent to printer)
std::vector columnBytes;
for (uint32_t row = 0; row < data.height; row += 8) {
uint8_t byte = 0;
// Pack 8 vertical pixels into one byte (printer data format)
for (int bit = 0; bit < 8 && (row + bit) < data.height; bit++) {
if (data.getBit(col, row + bit)) {
byte |= (1 << (7 - bit));
}
}
columnBytes.push_back(byte);
}
// Now "print" this column by unpacking the bytes back to pixels
for (size_t byteIdx = 0; byteIdx < columnBytes.size(); byteIdx++) {
uint8_t byte = columnBytes[byteIdx];
uint32_t baseRow = byteIdx * 8;
for (int bit = 0; bit < 8 && (baseRow + bit) < data.height; bit++) {
bool pixelOn = (byte & (1 << (7 - bit))) != 0;
uint32_t row = baseRow + bit;
// Write to output bitmap
size_t pixelIdx = row * data.width + col;
pixels[pixelIdx] = pixelOn ? 255 : 0; // 255 = black, 0 = white
}
}
}
// Set the pixels in the result bitmap
result.setPixels(pixels);
spdlog::debug("FakePrinter: Simulation complete, reconstructed {}x{} bitmap", result.getWidth(),
result.getHeight());
return result;
}
const graphics::Bitmap& FakePrinter::getLastPrint() const {
if (!mLastPrint) {
throw std::runtime_error("FakePrinter: No print data available");
}
return *mLastPrint;
}
bool FakePrinter::saveLastPrintToPng(const std::string& filename) const {
if (!mLastPrint || mLastPrint->getWidth() == 0 || mLastPrint->getHeight() == 0) {
spdlog::error("FakePrinter: No print data available to save");
return false;
}
return saveBitmapToPng(*mLastPrint, filename);
}
bool FakePrinter::saveBitmapToPng(const graphics::Bitmap& bitmap, const std::string& filename) const {
// Create Cairo surface from bitmap data
auto pixels = bitmap.getPixelsCpy();
uint16_t width = bitmap.getWidth();
uint16_t height = bitmap.getHeight();
// Cairo expects ARGB32 format, but we have ALPHA8
// Convert ALPHA8 (grayscale) to ARGB32
std::vector argbPixels(width * height);
for (size_t i = 0; i < pixels.size(); i++) {
uint8_t gray = pixels[i];
// ARGB32 format: 0xAARRGGBB
// For grayscale: use gray value for R, G, B and 255 for alpha
argbPixels[i] = 0xFF000000 | (gray << 16) | (gray << 8) | gray;
}
// Create Cairo surface
int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width);
cairo_surface_t* surface = cairo_image_surface_create_for_data(reinterpret_cast(argbPixels.data()),
CAIRO_FORMAT_ARGB32, width, height, stride);
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
spdlog::error("FakePrinter: Failed to create Cairo surface: {}",
cairo_status_to_string(cairo_surface_status(surface)));
cairo_surface_destroy(surface);
return false;
}
// Write to PNG file
cairo_status_t status = cairo_surface_write_to_png(surface, filename.c_str());
cairo_surface_destroy(surface);
if (status != CAIRO_STATUS_SUCCESS) {
spdlog::error("FakePrinter: Failed to write PNG file: {}", cairo_status_to_string(status));
return false;
}
return true;
}
std::string FakePrinter::generateTimestampedFilename() const {
// Get current time
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
// Format: fakelabel_YYYYMMDD_HHMMSS.png
std::stringstream ss;
ss << "fakelabel_" << std::put_time(std::localtime(&time), "%Y%m%d_%H%M%S") << ".png";
return ss.str();
}
} // namespace ptprnt::printer