From 09bc4a6861b3a4d0ad1e7dc214b3fe1b6b423504 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Thu, 23 Jan 2025 13:26:29 +0100 Subject: [PATCH] documentviewer: Add simple image viewer Since qtbase/b8f588bea74aae0a890e1af18b936b0bfbf8c237 removed the QtWidgets image viewer example, there is no way to exercise the image plugins. Re-add the code as documentviewer plugin. Pick-to: 6.9 Change-Id: I0eb26d02a9c11cdb0fce315ffc6f7c662e98c924 Reviewed-by: Axel Spoerl --- .../doc/src/documentviewer.qdoc | 17 ++ .../documentviewer/plugins/CMakeLists.txt | 1 + .../plugins/imageviewer/CMakeLists.txt | 41 ++++ .../plugins/imageviewer/imageviewer.cpp | 207 ++++++++++++++++++ .../plugins/imageviewer/imageviewer.h | 63 ++++++ .../plugins/imageviewer/imageviewer.json | 1 + 6 files changed, 330 insertions(+) create mode 100644 examples/demos/documentviewer/plugins/imageviewer/CMakeLists.txt create mode 100644 examples/demos/documentviewer/plugins/imageviewer/imageviewer.cpp create mode 100644 examples/demos/documentviewer/plugins/imageviewer/imageviewer.h create mode 100644 examples/demos/documentviewer/plugins/imageviewer/imageviewer.json diff --git a/examples/demos/documentviewer/doc/src/documentviewer.qdoc b/examples/demos/documentviewer/doc/src/documentviewer.qdoc index e966d3563..457489841 100644 --- a/examples/demos/documentviewer/doc/src/documentviewer.qdoc +++ b/examples/demos/documentviewer/doc/src/documentviewer.qdoc @@ -142,6 +142,23 @@ It supports editing text files, copy/cut and paste, printing, and saving changes. + \section1 ImageViewer class + + \c ImageViewer displays images as supported by QImageReader, using + a QLabel. + + In the constructor, we increase the allocation limit of QImageReader to + allow for larger photos: + + \snippet demos/documentviewer/plugins/imageviewer/imageviewer.cpp init + + In the openFile() function, we load the image and determine its size. + If it is larger than the screen, we downscale it to screen size, + maintaining the aspect ratio. This calculation has to be done in native + pixels, and the device pixel ratio needs to be set on the resulting pixmap + for it to appear crisp: + + \snippet demos/documentviewer/plugins/imageviewer/imageviewer.cpp open \section1 JsonViewer class diff --git a/examples/demos/documentviewer/plugins/CMakeLists.txt b/examples/demos/documentviewer/plugins/CMakeLists.txt index 59a7abd20..2f67c308b 100644 --- a/examples/demos/documentviewer/plugins/CMakeLists.txt +++ b/examples/demos/documentviewer/plugins/CMakeLists.txt @@ -1,6 +1,7 @@ # Copyright (C) 2023 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +add_subdirectory(imageviewer) add_subdirectory(jsonviewer) add_subdirectory(txtviewer) diff --git a/examples/demos/documentviewer/plugins/imageviewer/CMakeLists.txt b/examples/demos/documentviewer/plugins/imageviewer/CMakeLists.txt new file mode 100644 index 000000000..843319b5f --- /dev/null +++ b/examples/demos/documentviewer/plugins/imageviewer/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets + OPTIONAL_COMPONENTS PrintSupport) + +qt_add_plugin(imageviewer + CLASS_NAME ImageViewer + imageviewer.cpp imageviewer.h +) + +set_target_properties(imageviewer PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/app" +) + +target_include_directories(imageviewer PRIVATE + ../../app +) + +target_link_libraries(imageviewer PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + abstractviewer +) + +if(TARGET Qt6::PrintSupport) + target_link_libraries(imageviewer PRIVATE Qt6::PrintSupport) +endif() + +if(WIN32) + set(install_destination "${CMAKE_INSTALL_BINDIR}/app") +elseif(APPLE) + set(install_destination ".") +else() + set(install_destination "${CMAKE_INSTALL_BINDIR}") +endif() +install(TARGETS imageviewer + RUNTIME DESTINATION "${install_destination}" + LIBRARY DESTINATION "${install_destination}" +) diff --git a/examples/demos/documentviewer/plugins/imageviewer/imageviewer.cpp b/examples/demos/documentviewer/plugins/imageviewer/imageviewer.cpp new file mode 100644 index 000000000..1f7716f2c --- /dev/null +++ b/examples/demos/documentviewer/plugins/imageviewer/imageviewer.cpp @@ -0,0 +1,207 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "imageviewer.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +# include +#endif + +using namespace Qt::StringLiterals; + +static QStringList imageFormats() +{ + QStringList result; + const QByteArrayList &allFormats = QImageReader::supportedImageFormats(); + for (const auto &format : allFormats) { + if (format != "tif" && format != "cur") // duplicate/non-existent + result.append("image/"_L1 + QLatin1StringView(format)); + } + return result; +} + +static QString msgOpen(const QString &name, const QImage &image) +{ + const QString description = image.colorSpace().isValid() + ? image.colorSpace().description() : ImageViewer::tr("unknown"); + return ImageViewer::tr("Opened \"%1\", %2x%3, Depth: %4 (%5)") + .arg(QDir::toNativeSeparators(name)) + .arg(image.width()).arg(image.height()) + .arg(image.depth()).arg(description); +} + +//! [init] +ImageViewer::ImageViewer() : m_formats(imageFormats()) +{ + connect(this, &AbstractViewer::uiInitialized, this, &ImageViewer::setupImageUi); + QImageReader::setAllocationLimit(1024); // MB +} +//! [init] + +ImageViewer::~ImageViewer() = default; + +void ImageViewer::init(QFile *file, QWidget *parent, QMainWindow *mainWindow) +{ + m_imageLabel = new QLabel(parent); + m_imageLabel->setFrameShape(QFrame::Box); + m_imageLabel->setAlignment(Qt::AlignCenter); + m_imageLabel->setScaledContents(true); + + AbstractViewer::init(file, m_imageLabel, mainWindow); + + m_toolBar = addToolBar(tr("Images")); + + m_zoomInAct = m_toolBar->addAction(tr("Zoom &In"), this, &ImageViewer::zoomIn); + m_zoomInAct->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ZoomIn)); + m_zoomInAct->setShortcut(QKeySequence::ZoomIn); + + m_zoomOutAct = m_toolBar->addAction(tr("Zoom &Out"), this, &ImageViewer::zoomOut); + m_zoomOutAct->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ZoomOut)); + m_zoomOutAct->setShortcut(QKeySequence::ZoomOut); + + m_resetZoomAct = m_toolBar->addAction(tr("Reset Zoom"), this, &ImageViewer::resetZoom); + m_resetZoomAct->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ZoomFitBest)); + m_resetZoomAct->setShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_0)); +} + +QStringList ImageViewer::supportedMimeTypes() const +{ + return m_formats; +} + +void ImageViewer::clear() +{ + m_imageLabel->setPixmap({}); + m_maxScaleFactor = m_minScaleFactor = m_initialScaleFactor = m_scaleFactor = 1; +} + +void ImageViewer::setupImageUi() +{ + openFile(); +} + +//! [open] +void ImageViewer::openFile() +{ +#if QT_CONFIG(cursor) + QGuiApplication::setOverrideCursor(Qt::WaitCursor); +#endif + const QString name = m_file->fileName(); + QImageReader reader(name); + const QImage origImage = reader.read(); + if (origImage.isNull()) { + statusMessage(tr("Cannot read file %1:\n%2.") + .arg(QDir::toNativeSeparators(name), + reader.errorString()), tr("open")); + disablePrinting(); +#if QT_CONFIG(cursor) + QGuiApplication::restoreOverrideCursor(); +#endif + return; + } + + clear(); + + QImage image = origImage.colorSpace().isValid() + ? origImage.convertedToColorSpace(QColorSpace::SRgb) + : origImage; + + const auto devicePixelRatio = m_imageLabel->devicePixelRatioF(); + m_imageSize = QSizeF(image.size()) / devicePixelRatio; + + QPixmap pixmap = QPixmap::fromImage(image); + pixmap.setDevicePixelRatio(devicePixelRatio); + m_imageLabel->setPixmap(pixmap); + + const QSizeF targetSize = m_imageLabel->parentWidget()->size(); + if (m_imageSize.width() > targetSize.width() + || m_imageSize.height() > targetSize.height()) { + m_initialScaleFactor = qMin(targetSize.width() / m_imageSize.width(), + targetSize.height() / m_imageSize.height()); + } + m_maxScaleFactor = 3 * m_initialScaleFactor; + m_minScaleFactor = m_initialScaleFactor / 3; + doSetScaleFactor(m_initialScaleFactor); + + statusMessage(msgOpen(name, origImage)); +#if QT_CONFIG(cursor) + QGuiApplication::restoreOverrideCursor(); +#endif + + maybeEnablePrinting(); +} +//! [open] + +void ImageViewer::setScaleFactor(qreal scaleFactor) +{ + if (!qFuzzyCompare(m_scaleFactor, scaleFactor)) + doSetScaleFactor(scaleFactor); +} + +void ImageViewer::doSetScaleFactor(qreal scaleFactor) +{ + m_scaleFactor = scaleFactor; + const QSize labelSize = (m_imageSize * m_scaleFactor).toSize(); + m_imageLabel->setFixedSize(labelSize); + enableZoomActions(); +} + +void ImageViewer::zoomIn() +{ + setScaleFactor(m_scaleFactor * 1.25); +} + +void ImageViewer::zoomOut() +{ + setScaleFactor(m_scaleFactor * 0.8); +} + +void ImageViewer::resetZoom() +{ + setScaleFactor(m_initialScaleFactor); +} + +bool ImageViewer::hasContent() const +{ + return !m_imageLabel->pixmap().isNull(); +} + +void ImageViewer::enableZoomActions() +{ + m_resetZoomAct->setEnabled(!qFuzzyCompare(m_scaleFactor, m_initialScaleFactor)); + m_zoomInAct->setEnabled(m_scaleFactor < m_maxScaleFactor); + m_zoomOutAct->setEnabled(m_scaleFactor > m_minScaleFactor); +} + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +void ImageViewer::printDocument(QPrinter *printer) const +{ + if (!hasContent()) + return; + + QPainter painter(printer); + QPixmap pixmap = m_imageLabel->pixmap(); + QRect rect = painter.viewport(); + QSize size = pixmap.size(); + size.scale(rect.size(), Qt::KeepAspectRatio); + painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); + painter.setWindow(pixmap.rect()); + painter.drawPixmap(0, 0, pixmap); +} +#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT diff --git a/examples/demos/documentviewer/plugins/imageviewer/imageviewer.h b/examples/demos/documentviewer/plugins/imageviewer/imageviewer.h new file mode 100644 index 000000000..ea7bcb374 --- /dev/null +++ b/examples/demos/documentviewer/plugins/imageviewer/imageviewer.h @@ -0,0 +1,63 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef IMAGEVIEWER_H +#define IMAGEVIEWER_H + +#include "viewerinterfaces.h" + +#include + +QT_FORWARD_DECLARE_CLASS(QLabel) + +class ImageViewer : public ViewerInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.DocumentViewer.ViewerInterface" FILE "imageviewer.json") + Q_INTERFACES(ViewerInterface) +public: + Q_DISABLE_COPY_MOVE(ImageViewer) + + ImageViewer(); + ~ImageViewer() override; + + void init(QFile *file, QWidget *parent, QMainWindow *mainWindow) override; + QString viewerName() const override { return QLatin1StringView(staticMetaObject.className()); }; + QStringList supportedMimeTypes() const override; + bool hasContent() const override; + QByteArray saveState() const override { return {}; } + bool restoreState(QByteArray &) override { return true; } + bool supportsOverview() const override { return false; } + +#ifdef QT_DOCUMENTVIEWER_PRINTSUPPORT +protected: + void printDocument(QPrinter *printer) const override; +#endif // QT_DOCUMENTVIEWER_PRINTSUPPORT + +private slots: + void setupImageUi(); + void clear(); + void zoomIn(); + void zoomOut(); + void resetZoom(); + +private: + void openFile(); + void setScaleFactor(qreal scaleFactor); + void doSetScaleFactor(qreal scaleFactor); + void enableZoomActions(); + + QLabel *m_imageLabel{}; + QToolBar *m_toolBar{}; + QAction *m_zoomInAct{}; + QAction *m_zoomOutAct{}; + QAction *m_resetZoomAct{}; + QStringList m_formats; + qreal m_scaleFactor = 1; + qreal m_initialScaleFactor = 1; + qreal m_minScaleFactor = 1; + qreal m_maxScaleFactor = 1; + QSizeF m_imageSize; +}; + +#endif //IMAGEVIEWER_H diff --git a/examples/demos/documentviewer/plugins/imageviewer/imageviewer.json b/examples/demos/documentviewer/plugins/imageviewer/imageviewer.json new file mode 100644 index 000000000..020203bfa --- /dev/null +++ b/examples/demos/documentviewer/plugins/imageviewer/imageviewer.json @@ -0,0 +1 @@ +{ "Keys": [ "imageviewer" ] }