All checks were successful
Build ptprnt / build (push) Successful in 3m47s
Reviewed-on: moritz/ptouch-prnt#17
265 lines
9.0 KiB
C++
265 lines
9.0 KiB
C++
/*
|
||
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/>.
|
||
|
||
*/
|
||
|
||
#include "FakePrinter.hpp"
|
||
|
||
#include <cairo.h>
|
||
#include <spdlog/spdlog.h>
|
||
|
||
#include <chrono>
|
||
#include <cstdint>
|
||
#include <iomanip>
|
||
#include <sstream>
|
||
#include <stdexcept>
|
||
#include <vector>
|
||
|
||
#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<libusbwrap::IUsbDevice> 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<graphics::ALPHA8>& 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<graphics::Bitmap<graphics::ALPHA8>>(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<graphics::ILabel> 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<graphics::ALPHA8> 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<graphics::ALPHA8> result(data.width, data.height);
|
||
std::vector<uint8_t> 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<uint8_t> 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<graphics::ALPHA8>& 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<graphics::ALPHA8>& 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<uint32_t> 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<unsigned char*>(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
|