// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochSimpleImageViewer.h" #include #include #include #include #include #include #include #include "../../common/JFJochException.h" static inline double normalize_to_unit(double raw, double bg, double fg) { const double denom = std::max(1e-12, double(fg) - double(bg)); double v = (raw - bg) / denom; if (v < 0.0) v = 0.0; if (v > 1.0) v = 1.0; return v; } JFJochSimpleImageViewer::JFJochSimpleImageViewer(QWidget *parent) : QGraphicsView(parent) { setDragMode(QGraphicsView::NoDrag); setTransformationAnchor(QGraphicsView::AnchorUnderMouse); setRenderHint(QPainter::Antialiasing); setRenderHint(QPainter::SmoothPixmapTransform); setFocusPolicy(Qt::ClickFocus); auto *scn = new QGraphicsScene(this); setScene(scn); // Optional: a sensible default colormap color_scale_.Select(ColorScaleEnum::Indigo); // Keep overlays in pixel units independent of zoom (for labels font sizing) setViewportUpdateMode(QGraphicsView::FullViewportUpdate); } void JFJochSimpleImageViewer::clear() { has_image_ = false; image_.reset(); if (scene()) scene()->clear(); } void JFJochSimpleImageViewer::setImage(const std::shared_ptr &img) { image_ = img; has_image_ = true; renderImage(); updateScene(); } void JFJochSimpleImageViewer::setBackground(float v) { background_ = v; emit backgroundChanged(background_); if (has_image_) { renderImage(); // Preserve current transform while updating updateScene(); } } void JFJochSimpleImageViewer::setForeground(float v) { foreground_ = std::max(1e-6f, v); emit foregroundChanged(foreground_); if (has_image_) { renderImage(); updateScene(); } } void JFJochSimpleImageViewer::setColorMap(int colorMapEnumValue) { try { color_scale_.Select(static_cast(colorMapEnumValue)); if (has_image_) { renderImage(); updateScene(); } } catch (...) { // ignore invalid value } } void JFJochSimpleImageViewer::wheelEvent(QWheelEvent *event) { if (!scene() || !has_image_) return; const double zoomFactor = 1.15; const QPointF targetScenePos = mapToScene(event->position().toPoint()); if (event->modifiers() == Qt::ShiftModifier) { // Shift + wheel adjusts foreground (like your main viewer) float new_fg = foreground_ + event->angleDelta().y() / 120.0f; if (new_fg < 1.0f) new_fg = 1.0f; setForeground(new_fg); } else { if (event->angleDelta().y() > 0) { if (scale_factor_ * zoomFactor < 500.0) { scale_factor_ *= zoomFactor; scale(zoomFactor, zoomFactor); } } else { if (scale_factor_ > 0.2) { scale_factor_ *= 1.0 / zoomFactor; scale(1.0 / zoomFactor, 1.0 / zoomFactor); } } // Keep focus under mouse const QPointF updatedCenter = mapToScene(viewport()->rect().center()); const QPointF delta = targetScenePos - updatedCenter; translate(delta.x(), delta.y()); // Only labels depend on redraw; pixmap stays. Rebuild scene to re-place labels updateScene(); } } void JFJochSimpleImageViewer::mousePressEvent(QMouseEvent *event) { if (!scene() || !has_image_) return; if (event->button() == Qt::LeftButton) { panning_ = true; setCursor(Qt::ClosedHandCursor); last_mouse_pos_ = event->pos(); } QGraphicsView::mousePressEvent(event); } void JFJochSimpleImageViewer::mouseMoveEvent(QMouseEvent *event) { if (!scene() || !has_image_) return; if (panning_) { const QPointF delta = mapToScene(event->pos()) - mapToScene(last_mouse_pos_); last_mouse_pos_ = event->pos(); translate(delta.x(), delta.y()); // labels positions are in scene coords, scene items re-used; no special handling } else { auto coord = mapToScene(event->pos()); if ((coord.x() >= 0) && (coord.x() < image_->image.GetWidth()) && (coord.y() >= 0) && (coord.y() < image_->image.GetHeight())) { emit writeStatusBar(QString("x=%1 y=%2 %3 ") .arg(coord.x(), 0, 'f', 1) .arg(coord.y(), 0, 'f', 1) .arg(image_values_[int(coord.x()) + int(coord.y()) * image_->image.GetWidth()] ), 6000); } else emit writeStatusBar("", 6000); } QGraphicsView::mouseMoveEvent(event); } void JFJochSimpleImageViewer::mouseReleaseEvent(QMouseEvent *event) { if (!scene() || !has_image_) return; if (event->button() == Qt::LeftButton && panning_) { panning_ = false; setCursor(Qt::ArrowCursor); } QGraphicsView::mouseReleaseEvent(event); } void JFJochSimpleImageViewer::resizeEvent(QResizeEvent *event) { QGraphicsView::resizeEvent(event); // Nothing special; keep current transform and scene } void JFJochSimpleImageViewer::renderImage() { const size_t W = image_->image.GetWidth(); const size_t H = image_->image.GetHeight(); if (W == 0 || H == 0) return; // Prepare destination QImage (RGB888) QImage qimg(int(W), int(H), QImage::Format_RGB888); image_rgb_.resize(W * H); image_values_.resize(W * H); std::vector image_buffer; // Access uncompressed data const uint8_t *src = image_->image.GetUncompressedPtr(image_buffer); const auto mode = image_->image.GetMode(); auto write_rgb_row = [&](int y) -> uchar * { return qimg.scanLine(y); }; auto write_pixel = [&](size_t idx, uchar *dstRow, const rgb &c) { dstRow[idx * 3 + 0] = c.r; dstRow[idx * 3 + 1] = c.g; dstRow[idx * 3 + 2] = c.b; image_rgb_[idx] = c; }; // Fast path: packed RGB (assumed 3 bytes per pixel) if (mode == CompressedImageMode::RGB) { for (int y = 0; y < int(H); ++y) { const rgb *row = reinterpret_cast(src + y * W * sizeof(rgb)); uchar *dst = write_rgb_row(y); for (size_t x = 0; x < W; ++x) { const rgb c = row[x]; write_pixel(x, dst, c); } } } else { switch (mode) { case CompressedImageMode::Uint8: renderImage(qimg, src); break; case CompressedImageMode::Int8: renderImage(qimg, src); break; case CompressedImageMode::Uint16: renderImage(qimg, src); break; case CompressedImageMode::Int16: renderImage(qimg, src); break; case CompressedImageMode::Uint32: renderImage(qimg, src); break; case CompressedImageMode::Int32: renderImage(qimg, src); break; case CompressedImageMode::Float32: renderImage(qimg, src); break; case CompressedImageMode::Float64: renderImage(qimg, src); break; case CompressedImageMode::Float16: throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Float16 not supported"); break; } } // Build pixmap pixmap_ = QPixmap::fromImage(qimg); } void JFJochSimpleImageViewer::updateScene() { if (!scene()) return; scene()->clear(); if (!has_image_) return; // Add base image scene()->addItem(new QGraphicsPixmapItem(pixmap_)); QFont font("Arial", 1); // Font for pixel value text font.setPixelSize(1); // This will render very small text (1-pixel high). // Get the visible area in the scene coordinates QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect(); // Calculate the range of pixels to process const int W = int(image_->image.GetWidth()); const int H = int(image_->image.GetHeight()); const int startX = std::max(0, static_cast(std::floor(visibleRect.left()))); const int endX = std::min(W, static_cast(std::ceil(visibleRect.right()))); const int startY = std::max(0, static_cast(std::floor(visibleRect.top()))); const int endY = std::min(H, static_cast(std::ceil(visibleRect.bottom()))); // Only when zoomed in and region small (performance) if (scale_factor_ > 20.0 && (endX - startX + 1) * (endY - startY + 1) < 500) { const qreal f = std::clamp(scale_factor_, 0.5, 50.0); QFont font("Arial", 1); font.setPixelSize(1); // 1 px font; we use scaling below as needed for (int y = startY; y < endY; ++y) { const size_t base = size_t(y) * size_t(W); for (int x = startX; x < endX; ++x) { const size_t idx = base + size_t(x); const double raw = image_values_[idx]; const rgb c = image_rgb_[idx]; // If no scalar was stored (e.g., RGB source), you can skip or compute some alt text if (std::isnan(raw)) continue; // Text content from original value const QString pixelText = QString::number(raw); // Contrast against colored pixel QGraphicsTextItem *textItem = scene()->addText(pixelText, font); if (luminance(c) > 128.0) textItem->setDefaultTextColor(Qt::black); else textItem->setDefaultTextColor(Qt::white); textItem->setPos(x - 0.7, y - 0.8); textItem->setScale(0.2); textItem->setZValue(10.0); } } } } template void JFJochSimpleImageViewer::renderImage(QImage &qimg, const uint8_t *input) { const size_t W = image_->image.GetWidth(); const size_t H = image_->image.GetHeight(); auto ptr = reinterpret_cast(input); int64_t val; for (int i = 0; i < W * H; i++) { if (std::is_floating_point_v) val = std::lround(ptr[i]); else val = static_cast(ptr[i]); image_values_[i] = val; } if (auto_fgbg) { auto vec1(image_values_); std::sort(vec1.begin(), vec1.end()); background_ = vec1[0]; foreground_ = vec1[vec1.size() - 1]; emit backgroundChanged(background_); emit foregroundChanged(foreground_); } for (int y = 0; y < H; ++y) { uchar *scanLine = qimg.scanLine(y); // Get writable pointer to the row for (int x = 0; x < W; ++x) { image_rgb_[y * W + x] = color_scale_.Apply(image_values_[y * W + x], background_, foreground_); scanLine[x * 3 + 0] = image_rgb_[y * W + x].r; // Red scanLine[x * 3 + 1] = image_rgb_[y * W + x].g; // Green scanLine[x * 3 + 2] = image_rgb_[y * W + x].b; // Blue } } }