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