CaptureMoment 0.1.0
Free and open-source non-destructive photo editor
Loading...
Searching...
No Matches
sgs_image_item.cpp
Go to the documentation of this file.
1
9#include <spdlog/spdlog.h>
10#include <QMutexLocker>
11#include <QSGSimpleTextureNode>
12#include <QSGTexture>
13#include <QQuickWindow>
14#include <QImage>
15#include <algorithm>
16#include <cstring>
17#include <QColorSpace>
18
20
21// Constructor: Initializes the item and sets the flag for custom content.
22SGSImageItem::SGSImageItem(QQuickItem* parent)
23 : QQuickItem(parent)
24{
25 // Indicate to Qt Quick that this item has custom content rendered via the scene graph.
26 setFlag(QQuickItem::ItemHasContents, true);
27 spdlog::debug("[SGSImageItem::SGSImageItem]: Created");
28}
29
30// Destructor: Cleans up resources.
32{
33 // The deletion of textures managed by QSGNodes is handled by Qt Quick
34 // when the nodes are destroyed.
35 spdlog::debug("[SGSImageItem::~SGSImageItem()]: Destroyed");
36}
37
38// Sets the full image to be displayed.
39// Updates the internal image data and marks the internal state for update.
40void SGSImageItem::setImage(std::unique_ptr<Core::Common::ImageRegion> image)
41{
42 if (!image || !image->isValid())
43 {
44 spdlog::warn("[SGSImageItem::setImage]: Invalid image region");
45 return;
46 }
47
48 spdlog::info("[SGSImageItem::setImage]: {}x{}", image->width(), image->height());
49
50 // This lock is held while updating the core image data members.
51 {
52 QMutexLocker lock(&m_image_mutex);
53 m_full_image = std::move(image);
54 m_image_dirty = true;
55 }
56
57 // Emit signal for QML binding (BaseImageItem manages the signal emission)
58 emit imageSizeChanged();
59
60 // Trigger a repaint to reflect the new image.
61 QMetaObject::invokeMethod(this, &QQuickItem::update, Qt::QueuedConnection);
62}
63
64// Updates a specific tile of the displayed image.
65// Merges the tile data into the full image buffer (CPU side) and marks the internal state for update.
66void SGSImageItem::updateTile(std::unique_ptr<Core::Common::ImageRegion> tile)
67{
68 if (!tile || !tile->isValid()) {
69 spdlog::warn("[SGSImageItem::updateTile]: Invalid tile");
70 return;
71 }
72
73 {
74 QMutexLocker lock(&m_image_mutex);
75
76 if (!m_full_image) {
77 spdlog::warn("[SGSImageItem::updateTile]: No base image");
78 return;
79 }
80
81 // Full replacement if same dimensions (most common case)
82 if (tile->width() == m_full_image->width() &&
83 tile->height() == m_full_image->height())
84 {
85 // Copy data instead of move to avoid destructor issues
86 const size_t data_size { tile->width() * tile->height() * tile->channels() };
87 std::copy(tile->getBuffer().data(),
88 tile->getBuffer().data() + data_size,
89 m_full_image->getBuffer().data());
90 m_image_dirty = true;
91 spdlog::info("[SGSImageItem::updateTile]: Full copy {}x{}", tile->width(), tile->height());
92 } else
93 {
94 // Partial tile update
95 if (tile->x() < 0 || tile->y() < 0 ||
96 tile->x() + tile->width() > m_full_image->width() ||
97 tile->y() + tile->height() > m_full_image->height()) {
98 spdlog::warn("[SGSImageItem::updateTile]: Tile out of bounds");
99 return;
100 }
101
102 // Copy the tile data row by row
103 const size_t row_size { tile->width() * tile->channels() };
104 const int full_w { static_cast<int>(m_full_image->width()) };
105 const int tile_x { tile->x() };
106 const int tile_y { tile->y() } ;
107 const int tile_h { static_cast<int>(tile->height()) };
108
109 for (size_t y = 0; y < tile_h; ++y)
110 {
111 const float* src = tile->getBuffer().data() + y * row_size;
112 // FIXED: (tile_y + y) not (tile_x + y)
113 float* dst = m_full_image->getBuffer().data() +
114 ((tile_y + y) * full_w + tile_x) * m_full_image->channels();
115 std::copy(src, src + row_size, dst);
116 }
117 m_image_dirty = true;
118 spdlog::debug("[SGSImageItem::updateTile]: Partial copy at ({}, {})", tile_x, tile_y);
119 }
120 }
121
122 spdlog::debug("[SGSImageItem::updateTile]: Merged tile at ({}, {})", tile->x(), tile->y());
123 QMetaObject::invokeMethod(this, &QQuickItem::update, Qt::QueuedConnection);
124}
125
126// Updates the scene graph node for this item.
127// Creates or updates the QSGSimpleTextureNode responsible for rendering.
128QSGNode* SGSImageItem::updatePaintNode(QSGNode* node, UpdatePaintNodeData* data)
129{
130 Q_UNUSED(data);
131
132 if (!isImageValid()) {
133 return node;
134 }
135
136 auto* texture_node { static_cast<QSGSimpleTextureNode*>(node) };
137
138 int img_w { 0 };
139 int img_h { 0 };
140 bool needs_texture_update { false };
141 QImage new_qimage;
142
143 {
144 QMutexLocker lock(&m_image_mutex);
145
146 if (m_full_image && m_full_image->isValid())
147 {
148 img_w = static_cast<int>(m_full_image->width());
149 img_h = static_cast<int>(m_full_image->height());
150
151 if (m_image_dirty)
152 {
153 needs_texture_update = true;
154
155 QImage linear_img(
156 reinterpret_cast<const uchar*>(m_full_image->getBuffer().data()),
157 img_w, img_h,
158 img_w * 4 * static_cast<int>(sizeof(float)),
159 QImage::Format_RGBA32FPx4
160 );
161
162 const QColorSpace linear_cs { QColorSpace::SRgbLinear };
163 linear_img.setColorSpace(linear_cs);
164
165 new_qimage = linear_img.convertedToColorSpace(QColorSpace::SRgb)
166 .convertToFormat(QImage::Format_RGBA8888);
167
168 m_image_dirty = false;
169 }
170 }
171 }
172
173 QQuickWindow* win { window() };
174
175 if (!win) {
176 spdlog::warn("[SGSImageItem::updatePaintNode]: No window attached");
177 return node;
178 }
179
180 if (!texture_node) {
181 texture_node = new QSGSimpleTextureNode();
182 }
183
184 if (needs_texture_update && !new_qimage.isNull())
185 {
186 QSGTexture* new_texture { win->createTextureFromImage(
187 new_qimage,
188 QQuickWindow::TextureHasAlphaChannel
189 ) };
190
191 if (new_texture) {
192 texture_node->setTexture(new_texture);
193 texture_node->setOwnsTexture(true);
194 } else {
195 spdlog::warn("[SGSImageItem::updatePaintNode]: Failed to create texture");
196 }
197 }
198
199 if (texture_node->texture())
200 {
201 float display_w, display_h;
202 if (img_w > 0 && img_h > 0) {
203 display_w = img_w * m_zoom;
204 display_h = img_h * m_zoom;
205 } else {
206 display_w = imageWidth() * m_zoom;
207 display_h = imageHeight() * m_zoom;
208 }
209
210 const float x_pos = (width() - display_w) / 2.0f + m_pan.x();
211 const float y_pos = (height() - display_h) / 2.0f + m_pan.y();
212
213 texture_node->setRect(x_pos, y_pos, display_w, display_h);
214 texture_node->setFiltering(QSGTexture::Linear);
215 }
216
217 return texture_node;
218}
219
220void SGSImageItem::onZoomChanged(float new_zoom)
221{
222 emit zoomChanged(new_zoom);
223 update();
224}
225
226void SGSImageItem::onPanChanged(const QPointF& new_pan)
227{
228 emit panChanged(new_pan);
229 update();
230}
231
233{
234 emit imageSizeChanged();
235 update();
236}
237
238} // namespace CaptureMoment::UI::Rendering
bool isImageValid() const override
Checks if the full image data is loaded and valid. This method provides thread-safe access to check t...
std::unique_ptr< Core::Common::ImageRegion > m_full_image
Unique pointer to the full image data displayed by this item.
QMutex m_image_mutex
Mutex protecting access to m_full_image and related state. Ensures thread-safe updates to the image d...
int imageHeight() const override
Gets the height of the image. This method provides thread-safe access to m_image_height.
QPointF m_pan
Current pan offset applied to the image.
float m_zoom
Current zoom level applied to the image.
int imageWidth() const override
Gets the width of the image. This method provides thread-safe access to m_image_width.
void panChanged(const QPointF &pan)
Signal emitted when the pan offset changes.
void onZoomChanged(float new_zoom) override
void imageSizeChanged()
Signal emitted when the image dimensions change (width or height).
~SGSImageItem() override
Destroys the SGSImageItem and releases associated resources.
void updateTile(std::unique_ptr< Core::Common::ImageRegion > tile) override
Updates a specific tile of the displayed image.
void onPanChanged(const QPointF &new_pan) override
SGSImageItem(QQuickItem *parent=nullptr)
Constructs a new SGSImageItem.
void setImage(std::unique_ptr< Core::Common::ImageRegion > image) override
Sets the full image to be displayed.
void zoomChanged(float zoom)
Signal emitted when the zoom value changes.
QSGNode * updatePaintNode(QSGNode *node, UpdatePaintNodeData *data) override
Updates the scene graph node for this item.
Namespace containing Qt-specific UI components for CaptureMoment.
Simple texture-based image display (Qt6 - QSGSimpleTextureNode) using BaseImageItem for common state.
CaptureMoment — footer