/* 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 "graphics/Label.hpp" #include #include #include #include #include #include #include #include #include "cairo.h" #include "graphics/CairoWrapper.hpp" #include "graphics/interface/ICairoWrapper.hpp" #include "graphics/interface/ILabel.hpp" #include "pango/pango-font.h" #include "pango/pango-layout.h" #include "pango/pango-types.h" namespace ptprnt::graphics { // Deleter implementations 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()) {} // Constructor with dependency injection Label::Label(const uint16_t heightPixel, std::shared_ptr cairoWrapper) : mCairoWrapper(std::move(cairoWrapper)), mPrinterHeight(heightPixel) { // Initialize resources in correct order with RAII // Pass wrapper to deleter so cleanup uses the wrapper GObjectDeleter deleter; deleter.wrapper = mCairoWrapper; mFontMap = std::unique_ptr(mCairoWrapper->pango_cairo_font_map_new(), deleter); } std::vector Label::getRaw() const { assert(mSurface != nullptr); auto* surface = mSurface.get(); mCairoWrapper->cairo_surface_flush(surface); assert(mCairoWrapper->cairo_image_surface_get_format(surface) == CAIRO_FORMAT_A8); int width = mCairoWrapper->cairo_image_surface_get_width(surface); int height = mCairoWrapper->cairo_image_surface_get_height(surface); int stride = mCairoWrapper->cairo_image_surface_get_stride(surface); spdlog::debug("Cairo Surface data: W: {}; H: {}; S:{}", width, height, stride); auto data = mCairoWrapper->cairo_image_surface_get_data(surface); // If stride equals width, we can return data directly if (stride == width) { size_t len = height * stride; return {data, data + len}; } // Otherwise, we need to copy row by row, removing stride padding std::vector result; result.reserve(width * height); for (int y = 0; y < height; ++y) { uint8_t* row_start = data + (y * stride); result.insert(result.end(), row_start, row_start + width); } spdlog::debug("getRaw: Removed stride padding, returning {} bytes ({}x{})", result.size(), width, height); return result; } uint8_t Label::getNumLines(std::string_view strv) { return std::count(strv.begin(), strv.end(), '\n'); } int Label::getWidth() const { // Return the actual Cairo surface width (which is the layout width) return mLayoutWidth; } int Label::getHeight() const { // Return the actual Cairo surface height (which is the printer height) return mPrinterHeight; } void Label::configureLayout(PangoLayout* layout, const std::string& text, PangoFontDescription* fontDesc) { mCairoWrapper->pango_layout_set_font_description(layout, fontDesc); mCairoWrapper->pango_layout_set_text(layout, text.c_str(), static_cast(text.length())); mCairoWrapper->pango_layout_set_height(layout, getNumLines(text) * -1); } void Label::applyHorizontalAlignment(PangoLayout* layout) { switch (mHAlign) { case HAlignPosition::LEFT: mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); break; case HAlignPosition::RIGHT: mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_RIGHT); break; case HAlignPosition::JUSTIFY: mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); mCairoWrapper->pango_layout_set_justify(layout, true); #if PANGO_VERSION_MAJOR >= 1 && PANGO_VERSION_MINOR >= 50 mCairoWrapper->pango_layout_set_justify_last_line(layout, true); #endif break; case HAlignPosition::CENTER: [[fallthrough]]; default: mCairoWrapper->pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); break; } } bool Label::create(PrintableText printableText) { setFontFamily(printableText.fontFamily); setFontSize(printableText.fontSize); return create(printableText.text); } bool Label::create(const std::string& labelText) { // TODO: we need to create a custom font config here so that Noto Emoji does not load the systems default // font config here. For this, we need to create a PangoFcFontMap and a custom FcConfig // see: https://docs.gtk.org/PangoFc/method.FontMap.set_config.html // see: https://gist.github.com/CallumDev/7c66b3f9cf7a876ef75f // Create a temporary surface for layout size calculations auto* tempSurface = mCairoWrapper->cairo_image_surface_create(CAIRO_FORMAT_A8, 1, 1); auto* tempCr = mCairoWrapper->cairo_create(tempSurface); auto* tempPangoCtx = mCairoWrapper->pango_cairo_create_context(tempCr); auto* tempPangoLyt = mCairoWrapper->pango_layout_new(tempPangoCtx); PangoFontDescription* regularFont = mCairoWrapper->pango_font_description_new(); mCairoWrapper->pango_font_description_set_size(regularFont, static_cast(mFontSize * PANGO_SCALE)); mCairoWrapper->pango_font_description_set_family(regularFont, mFontFamily.c_str()); // Configure temporary layout for size calculation configureLayout(tempPangoLyt, labelText, regularFont); applyHorizontalAlignment(tempPangoLyt); // Calculate label size from temporary layout mCairoWrapper->pango_layout_get_size(tempPangoLyt, &mLayoutWidth, &mLayoutHeight); mLayoutWidth /= PANGO_SCALE; mLayoutHeight /= PANGO_SCALE; spdlog::debug("Layout width: {}, height: {}", mLayoutWidth, mLayoutHeight); // Clean up temporary resources mCairoWrapper->g_object_unref(tempPangoLyt); mCairoWrapper->g_object_unref(tempPangoCtx); mCairoWrapper->cairo_destroy(tempCr); mCairoWrapper->cairo_surface_destroy(tempSurface); // Now create the final surface and Pango context for actual rendering // Create deleters with wrapper reference CairoSurfaceDeleter surfaceDeleter; surfaceDeleter.wrapper = mCairoWrapper; CairoDeleter cairoDeleter; cairoDeleter.wrapper = mCairoWrapper; GObjectDeleter gobjectDeleter; gobjectDeleter.wrapper = mCairoWrapper; mSurface = std::unique_ptr( mCairoWrapper->cairo_image_surface_create(CAIRO_FORMAT_A8, mLayoutWidth, mPrinterHeight), surfaceDeleter); cairo_t* cr = mCairoWrapper->cairo_create(mSurface.get()); mCairoCtx = std::unique_ptr(cr, cairoDeleter); mPangoCtx = std::unique_ptr(mCairoWrapper->pango_cairo_create_context(cr), gobjectDeleter); mPangoLyt = std::unique_ptr(mCairoWrapper->pango_layout_new(mPangoCtx.get()), gobjectDeleter); // Configure final layout with same settings configureLayout(mPangoLyt.get(), labelText, regularFont); applyHorizontalAlignment(mPangoLyt.get()); // Adjust Cairo cursor position to respect the vertical alignment switch (mVAlign) { case VAlignPosition::TOP: break; case VAlignPosition::BOTTOM: mCairoWrapper->cairo_move_to(mCairoCtx.get(), 0.0, mPrinterHeight - mLayoutHeight); break; case VAlignPosition::MIDDLE: mCairoWrapper->cairo_move_to(mCairoCtx.get(), 0.0, (mPrinterHeight - mLayoutHeight) / 2); break; default: break; } // Finally show the layout on the Cairo surface mCairoWrapper->pango_cairo_show_layout(mCairoCtx.get(), mPangoLyt.get()); mCairoWrapper->cairo_set_source_rgb(mCairoCtx.get(), 0.0, 0.0, 0.0); mCairoWrapper->cairo_surface_flush(mSurface.get()); // mCairoCtx smart pointer will handle cleanup return true; } void Label::writeToPng(const std::string& file) { if (mSurface) { mCairoWrapper->cairo_surface_flush(mSurface.get()); mCairoWrapper->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(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( 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) std::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) { mFontSize = fontSize; } void Label::setFontFamily(const std::string& fontFamily) { mFontFamily = fontFamily; } void Label::setHAlign(HAlignPosition hAlign) { mHAlign = hAlign; } void Label::setVAlign(VAlignPosition vAlign) { mVAlign = vAlign; } void Label::setText(const std::string& text) { mText = text; } Label::~Label() { spdlog::debug("Image dtor..."); // RAII smart pointers handle cleanup automatically } } // namespace ptprnt::graphics