File indexing completed on 2024-05-12 04:19:40

0001 /*
0002 Gwenview: an image viewer
0003 Copyright 2021 Arjen Hiemstra <ahiemstra@heimr.nl>
0004 
0005 This program is free software; you can redistribute it and/or
0006 modify it under the terms of the GNU General Public License
0007 as published by the Free Software Foundation; either version 2
0008 of the License, or (at your option) any later version.
0009 
0010 This program is distributed in the hope that it will be useful,
0011 but WITHOUT ANY WARRANTY; without even the implied warranty of
0012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013 GNU General Public License for more details.
0014 
0015 You should have received a copy of the GNU General Public License
0016 along with this program; if not, write to the Free Software
0017 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
0018 
0019 */
0020 
0021 #include "rasterimageitem.h"
0022 
0023 #include <cmath>
0024 
0025 #include <QGraphicsScene>
0026 #include <QGraphicsView>
0027 #include <QPainter>
0028 
0029 #include "gvdebug.h"
0030 #include "lib/cms/cmsprofile.h"
0031 #include "rasterimageview.h"
0032 
0033 using namespace Gwenview;
0034 
0035 // Convenience constants for one third and one sixth.
0036 static const qreal Third = 1.0 / 3.0;
0037 static const qreal Sixth = 1.0 / 6.0;
0038 
0039 RasterImageItem::RasterImageItem(Gwenview::RasterImageView *parent)
0040     : QGraphicsItem(parent)
0041     , mParentView(parent)
0042 {
0043 }
0044 
0045 RasterImageItem::~RasterImageItem()
0046 {
0047     if (mDisplayTransform) {
0048         cmsDeleteTransform(mDisplayTransform);
0049     }
0050 }
0051 
0052 void RasterImageItem::setRenderingIntent(RenderingIntent::Enum intent)
0053 {
0054     mRenderingIntent = intent;
0055     update();
0056 }
0057 
0058 void Gwenview::RasterImageItem::updateCache()
0059 {
0060     auto document = mParentView->document();
0061 
0062     // Save a shallow copy of the image to make sure that it will not get
0063     // destroyed by another thread.
0064     mOriginalImage = document->image();
0065 
0066     // Cache two scaled down versions of the image, one at a third of the size
0067     // and one at a sixth. These are used instead of the document image at small
0068     // zoom levels, to avoid having to copy around the entire image which can be
0069     // very slow for large images.
0070     mThirdScaledImage = mOriginalImage.scaled(document->size() * Third, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0071     mSixthScaledImage = mOriginalImage.scaled(document->size() * Sixth, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0072 }
0073 
0074 void RasterImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
0075 {
0076     if (mOriginalImage.isNull() || mThirdScaledImage.isNull() || mSixthScaledImage.isNull()) {
0077         return;
0078     }
0079 
0080     const auto dpr = mParentView->devicePixelRatio();
0081     const auto zoom = mParentView->zoom();
0082 
0083     // This assumes we always have at least a single view of the graphics scene,
0084     // which should be true when painting a graphics item.
0085     const auto viewportRect = mParentView->scene()->views().first()->rect();
0086 
0087     // Map the viewport to the image so we get the area of the image that is
0088     // visible.
0089     auto imageRect = mParentView->mapToImage(viewportRect);
0090 
0091     // Grow the resulting rect by an arbitrary but small amount to avoid pixel
0092     // alignment issues. This results in the image being drawn slightly larger
0093     // than the viewport.
0094     imageRect = imageRect.marginsAdded(QMargins(5 * dpr, 5 * dpr, 5 * dpr, 5 * dpr));
0095 
0096     // Constrain the visible area rect by the image's rect so we don't try to
0097     // copy pixels that are outside the image.
0098     imageRect = imageRect.intersected(mOriginalImage.rect());
0099 
0100     QImage image;
0101     qreal targetZoom = zoom;
0102 
0103     // Copy the visible area from the document's image into a new image. This
0104     // allows us to modify the resulting image without affecting the original
0105     // image data. If we are zoomed out far enough, we instead use one of the
0106     // cached scaled copies to avoid having to copy a lot of data.
0107     if (zoom > Third) {
0108         image = mOriginalImage.copy(imageRect);
0109     } else if (zoom > Sixth) {
0110         auto sourceRect = QRect{imageRect.topLeft() * Third, imageRect.size() * Third};
0111         targetZoom = zoom / Third;
0112         image = mThirdScaledImage.copy(sourceRect);
0113     } else {
0114         auto sourceRect = QRect{imageRect.topLeft() * Sixth, imageRect.size() * Sixth};
0115         targetZoom = zoom / Sixth;
0116         image = mSixthScaledImage.copy(sourceRect);
0117     }
0118 
0119     const QImage::Format originalImageFormat = image.format();
0120 
0121     // We want nearest neighbour at high zoom since that provides the most
0122     // accurate representation of pixels, but at low zoom or when zooming out it
0123     // will not look very nice, so use smoothing instead. Switch at an arbitrary
0124     // threshold of 400% zoom
0125     const auto transformationMode = zoom < 4.0 ? Qt::SmoothTransformation : Qt::FastTransformation;
0126 
0127     // Scale the visible image to the requested zoom.
0128     image = image.scaled(image.size() * targetZoom, Qt::IgnoreAspectRatio, transformationMode);
0129 
0130     // Scaling may convert image to premultiplied formats (unsupported by color correction engine),
0131     // so we convert image back to originalImageFormat.
0132     if (image.format() != originalImageFormat) {
0133         image.convertTo(originalImageFormat);
0134     }
0135 
0136     // Perform color correction on the visible image.
0137     applyDisplayTransform(image);
0138 
0139     const auto destinationRect = QRect{// Ceil the top left corner to avoid pixel alignment issues on higher DPI because QPoint/QSize/QRect
0140                                        // round instead of flooring when converting from float to int.
0141                                        QPoint{int(std::ceil(imageRect.left() * (zoom / dpr))), int(std::ceil(imageRect.top() * (zoom / dpr)))},
0142                                        // Floor the size, similarly to above.
0143                                        QSize{int(image.size().width() / dpr), int(image.size().height() / dpr)}};
0144 
0145     painter->drawImage(destinationRect, image);
0146 }
0147 
0148 QRectF RasterImageItem::boundingRect() const
0149 {
0150     return QRectF{QPointF{0, 0}, mParentView->documentSize() * mParentView->zoom()};
0151 }
0152 
0153 void RasterImageItem::applyDisplayTransform(QImage &image)
0154 {
0155     if (mApplyDisplayTransform) {
0156         updateDisplayTransform(image.format());
0157         if (mDisplayTransform) {
0158             quint8 *bytes = image.bits();
0159             cmsDoTransform(mDisplayTransform, bytes, bytes, image.width() * image.height());
0160         }
0161     }
0162 }
0163 
0164 void RasterImageItem::updateDisplayTransform(QImage::Format format)
0165 {
0166     if (format == QImage::Format_Invalid) {
0167         return;
0168     }
0169 
0170     mApplyDisplayTransform = false;
0171     if (mDisplayTransform) {
0172         cmsDeleteTransform(mDisplayTransform);
0173     }
0174     mDisplayTransform = nullptr;
0175 
0176     Cms::Profile::Ptr profile = mParentView->document()->cmsProfile();
0177     if (!profile) {
0178         // The assumption that something unmarked is *probably* sRGB is better than failing to apply any transform when one
0179         // has a wide-gamut screen.
0180         profile = Cms::Profile::getSRgbProfile();
0181     }
0182     Cms::Profile::Ptr monitorProfile = Cms::Profile::getMonitorProfile();
0183     if (!monitorProfile) {
0184         qCWarning(GWENVIEW_LIB_LOG) << "Could not get monitor color profile";
0185         return;
0186     }
0187 
0188     cmsUInt32Number cmsFormat = 0;
0189     switch (format) {
0190     case QImage::Format_RGB32:
0191     case QImage::Format_ARGB32:
0192         cmsFormat = TYPE_BGRA_8;
0193         break;
0194     case QImage::Format_Grayscale8:
0195         cmsFormat = TYPE_GRAY_8;
0196         break;
0197     case QImage::Format_RGB888:
0198         cmsFormat = TYPE_RGB_8;
0199         break;
0200     case QImage::Format_RGBX8888:
0201     case QImage::Format_RGBA8888:
0202         cmsFormat = TYPE_RGBA_8;
0203         break;
0204     case QImage::Format_Grayscale16:
0205         cmsFormat = TYPE_GRAY_16;
0206         break;
0207     case QImage::Format_RGBA64:
0208     case QImage::Format_RGBX64:
0209         cmsFormat = TYPE_RGBA_16;
0210         break;
0211     case QImage::Format_BGR888:
0212         cmsFormat = TYPE_BGR_8;
0213         break;
0214     default:
0215         qCWarning(GWENVIEW_LIB_LOG) << "Gwenview cannot apply color profile on" << format << "images";
0216         return;
0217     }
0218 
0219     mDisplayTransform =
0220         cmsCreateTransform(profile->handle(), cmsFormat, monitorProfile->handle(), cmsFormat, mRenderingIntent, cmsFLAGS_BLACKPOINTCOMPENSATION);
0221     mApplyDisplayTransform = true;
0222 }