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 }