223 lines
8.0 KiB
C++
223 lines
8.0 KiB
C++
|
/*
|
||
|
Gwenview: an image viewer
|
||
|
Copyright 2021 Arjen Hiemstra <ahiemstra@heimr.nl>
|
||
|
|
||
|
This program is free software; you can redistribute it and/or
|
||
|
modify it under the terms of the GNU General Public License
|
||
|
as published by the Free Software Foundation; either version 2
|
||
|
of the License, or (at your option) any later version.
|
||
|
|
||
|
This program is distributed in the hope that it will be useful,
|
||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
GNU General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU General Public License
|
||
|
along with this program; if not, write to the Free Software
|
||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
|
||
|
|
||
|
*/
|
||
|
|
||
|
#include "rasterimageitem.h"
|
||
|
|
||
|
#include <cmath>
|
||
|
|
||
|
#include <QGraphicsScene>
|
||
|
#include <QGraphicsView>
|
||
|
#include <QPainter>
|
||
|
|
||
|
#include "gvdebug.h"
|
||
|
#include "lib/cms/cmsprofile.h"
|
||
|
#include "rasterimageview.h"
|
||
|
|
||
|
using namespace Gwenview;
|
||
|
|
||
|
// Convenience constants for one third and one sixth.
|
||
|
static const qreal Third = 1.0 / 3.0;
|
||
|
static const qreal Sixth = 1.0 / 6.0;
|
||
|
|
||
|
RasterImageItem::RasterImageItem(Gwenview::RasterImageView *parent)
|
||
|
: QGraphicsItem(parent)
|
||
|
, mParentView(parent)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
RasterImageItem::~RasterImageItem()
|
||
|
{
|
||
|
if (mDisplayTransform) {
|
||
|
cmsDeleteTransform(mDisplayTransform);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void RasterImageItem::setRenderingIntent(RenderingIntent::Enum intent)
|
||
|
{
|
||
|
mRenderingIntent = intent;
|
||
|
update();
|
||
|
}
|
||
|
|
||
|
void Gwenview::RasterImageItem::updateCache()
|
||
|
{
|
||
|
auto document = mParentView->document();
|
||
|
|
||
|
// Save a shallow copy of the image to make sure that it will not get
|
||
|
// destroyed by another thread.
|
||
|
mOriginalImage = document->image();
|
||
|
|
||
|
// Cache two scaled down versions of the image, one at a third of the size
|
||
|
// and one at a sixth. These are used instead of the document image at small
|
||
|
// zoom levels, to avoid having to copy around the entire image which can be
|
||
|
// very slow for large images.
|
||
|
mThirdScaledImage = mOriginalImage.scaled(document->size() * Third, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||
|
mSixthScaledImage = mOriginalImage.scaled(document->size() * Sixth, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||
|
}
|
||
|
|
||
|
void RasterImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||
|
{
|
||
|
if (mOriginalImage.isNull() || mThirdScaledImage.isNull() || mSixthScaledImage.isNull()) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const auto dpr = mParentView->devicePixelRatio();
|
||
|
const auto zoom = mParentView->zoom();
|
||
|
|
||
|
// This assumes we always have at least a single view of the graphics scene,
|
||
|
// which should be true when painting a graphics item.
|
||
|
const auto viewportRect = mParentView->scene()->views().first()->rect();
|
||
|
|
||
|
// Map the viewport to the image so we get the area of the image that is
|
||
|
// visible.
|
||
|
auto imageRect = mParentView->mapToImage(viewportRect);
|
||
|
|
||
|
// Grow the resulting rect by an arbitrary but small amount to avoid pixel
|
||
|
// alignment issues. This results in the image being drawn slightly larger
|
||
|
// than the viewport.
|
||
|
imageRect = imageRect.marginsAdded(QMargins(5 * dpr, 5 * dpr, 5 * dpr, 5 * dpr));
|
||
|
|
||
|
// Constrain the visible area rect by the image's rect so we don't try to
|
||
|
// copy pixels that are outside the image.
|
||
|
imageRect = imageRect.intersected(mOriginalImage.rect());
|
||
|
|
||
|
QImage image;
|
||
|
qreal targetZoom = zoom;
|
||
|
|
||
|
// Copy the visible area from the document's image into a new image. This
|
||
|
// allows us to modify the resulting image without affecting the original
|
||
|
// image data. If we are zoomed out far enough, we instead use one of the
|
||
|
// cached scaled copies to avoid having to copy a lot of data.
|
||
|
if (zoom > Third) {
|
||
|
image = mOriginalImage.copy(imageRect);
|
||
|
} else if (zoom > Sixth) {
|
||
|
auto sourceRect = QRect{imageRect.topLeft() * Third, imageRect.size() * Third};
|
||
|
targetZoom = zoom / Third;
|
||
|
image = mThirdScaledImage.copy(sourceRect);
|
||
|
} else {
|
||
|
auto sourceRect = QRect{imageRect.topLeft() * Sixth, imageRect.size() * Sixth};
|
||
|
targetZoom = zoom / Sixth;
|
||
|
image = mSixthScaledImage.copy(sourceRect);
|
||
|
}
|
||
|
|
||
|
const QImage::Format originalImageFormat = image.format();
|
||
|
|
||
|
// We want nearest neighbour at high zoom since that provides the most
|
||
|
// accurate representation of pixels, but at low zoom or when zooming out it
|
||
|
// will not look very nice, so use smoothing instead. Switch at an arbitrary
|
||
|
// threshold of 400% zoom
|
||
|
const auto transformationMode = zoom < 4.0 ? Qt::SmoothTransformation : Qt::FastTransformation;
|
||
|
|
||
|
// Scale the visible image to the requested zoom.
|
||
|
image = image.scaled(image.size() * targetZoom, Qt::IgnoreAspectRatio, transformationMode);
|
||
|
|
||
|
// Scaling may convert image to premultiplied formats (unsupported by color correction engine),
|
||
|
// so we convert image back to originalImageFormat.
|
||
|
if (image.format() != originalImageFormat) {
|
||
|
image.convertTo(originalImageFormat);
|
||
|
}
|
||
|
|
||
|
// Perform color correction on the visible image.
|
||
|
applyDisplayTransform(image);
|
||
|
|
||
|
const auto destinationRect = QRect{// Ceil the top left corner to avoid pixel alignment issues on higher DPI because QPoint/QSize/QRect
|
||
|
// round instead of flooring when converting from float to int.
|
||
|
QPoint{int(std::ceil(imageRect.left() * (zoom / dpr))), int(std::ceil(imageRect.top() * (zoom / dpr)))},
|
||
|
// Floor the size, similarly to above.
|
||
|
QSize{int(image.size().width() / dpr), int(image.size().height() / dpr)}};
|
||
|
|
||
|
painter->drawImage(destinationRect, image);
|
||
|
}
|
||
|
|
||
|
QRectF RasterImageItem::boundingRect() const
|
||
|
{
|
||
|
return QRectF{QPointF{0, 0}, mParentView->documentSize() * mParentView->zoom()};
|
||
|
}
|
||
|
|
||
|
void RasterImageItem::applyDisplayTransform(QImage &image)
|
||
|
{
|
||
|
if (mApplyDisplayTransform) {
|
||
|
updateDisplayTransform(image.format());
|
||
|
if (mDisplayTransform) {
|
||
|
quint8 *bytes = image.bits();
|
||
|
cmsDoTransform(mDisplayTransform, bytes, bytes, image.width() * image.height());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void RasterImageItem::updateDisplayTransform(QImage::Format format)
|
||
|
{
|
||
|
if (format == QImage::Format_Invalid) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mApplyDisplayTransform = false;
|
||
|
if (mDisplayTransform) {
|
||
|
cmsDeleteTransform(mDisplayTransform);
|
||
|
}
|
||
|
mDisplayTransform = nullptr;
|
||
|
|
||
|
Cms::Profile::Ptr profile = mParentView->document()->cmsProfile();
|
||
|
if (!profile) {
|
||
|
// The assumption that something unmarked is *probably* sRGB is better than failing to apply any transform when one
|
||
|
// has a wide-gamut screen.
|
||
|
profile = Cms::Profile::getSRgbProfile();
|
||
|
}
|
||
|
Cms::Profile::Ptr monitorProfile = Cms::Profile::getMonitorProfile();
|
||
|
if (!monitorProfile) {
|
||
|
qCWarning(GWENVIEW_LIB_LOG) << "Could not get monitor color profile";
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
cmsUInt32Number cmsFormat = 0;
|
||
|
switch (format) {
|
||
|
case QImage::Format_RGB32:
|
||
|
case QImage::Format_ARGB32:
|
||
|
cmsFormat = TYPE_BGRA_8;
|
||
|
break;
|
||
|
case QImage::Format_Grayscale8:
|
||
|
cmsFormat = TYPE_GRAY_8;
|
||
|
break;
|
||
|
case QImage::Format_RGB888:
|
||
|
cmsFormat = TYPE_RGB_8;
|
||
|
break;
|
||
|
case QImage::Format_RGBX8888:
|
||
|
case QImage::Format_RGBA8888:
|
||
|
cmsFormat = TYPE_RGBA_8;
|
||
|
break;
|
||
|
case QImage::Format_Grayscale16:
|
||
|
cmsFormat = TYPE_GRAY_16;
|
||
|
break;
|
||
|
case QImage::Format_RGBA64:
|
||
|
case QImage::Format_RGBX64:
|
||
|
cmsFormat = TYPE_RGBA_16;
|
||
|
break;
|
||
|
case QImage::Format_BGR888:
|
||
|
cmsFormat = TYPE_BGR_8;
|
||
|
break;
|
||
|
default:
|
||
|
qCWarning(GWENVIEW_LIB_LOG) << "Gwenview cannot apply color profile on" << format << "images";
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mDisplayTransform =
|
||
|
cmsCreateTransform(profile->handle(), cmsFormat, monitorProfile->handle(), cmsFormat, mRenderingIntent, cmsFLAGS_BLACKPOINTCOMPENSATION);
|
||
|
mApplyDisplayTransform = true;
|
||
|
}
|