Commit ac47787f authored by Arjen Hiemstra's avatar Arjen Hiemstra
Browse files

Render raster images by using a copy of a portion of the image

Instead of directly rendering the image, we now copy the visible area of
the image and render that. This allows us to perform color correction on
the copy which does not impact the original data and thus should
preserve the original pixels. As a bonus, this also allows us to use
QImage's improved smooth scaling so reduces aliasing artifacts at small
zoom values.
parent 79f1c25e
......@@ -73,6 +73,7 @@ set(gwenviewlib_SRCS
documentview/messageviewadapter.cpp
documentview/rasterimageview.cpp
documentview/rasterimageviewadapter.cpp
documentview/rasterimageitem.cpp
documentview/svgviewadapter.cpp
documentview/videoviewadapter.cpp
about.cpp
......
/*
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 <QPainter>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QDebug>
#include "rasterimageview.h"
#include "gvdebug.h"
#include "lib/cms/cmsprofile.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()
{
// 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.
auto document = mParentView->document();
mThirdScaledImage = document->image().scaled(document->size() * Third, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
mSixthScaledImage = document->image().scaled(document->size() * Sixth, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
void RasterImageItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/)
{
auto document = mParentView->document();
if (document->image().isNull() || mThirdScaledImage.isNull() || mSixthScaledImage.isNull()) {
return;
}
const auto dpr = mParentView->devicePixelRatio();
const auto zoom = mParentView->zoom() / dpr;
// 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(document->image().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 = document->image().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);
}
// We want nearest neighbour when zooming in since that provides the most
// accurate representation of pixels, but when zooming out it will actually
// not look very nice, so use smoothing when zooming out.
const auto transformationMode = zoom < 1.0 ? Qt::SmoothTransformation : Qt::FastTransformation;
// Scale the visible image to the requested zoom.
image = image.scaled(image.size() * targetZoom, Qt::IgnoreAspectRatio, transformationMode);
// 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
QPoint{int(std::ceil(imageRect.left() * zoom)), int(std::ceil(imageRect.top() * zoom))},
image.size()
};
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;
default:
qCWarning(GWENVIEW_LIB_LOG) << "Gwenview can only apply color profile on RGB32 or ARGB32 images";
return;
}
mDisplayTransform = cmsCreateTransform(profile->handle(), cmsFormat,
monitorProfile->handle(), cmsFormat,
mRenderingIntent, cmsFLAGS_BLACKPOINTCOMPENSATION);
mApplyDisplayTransform = true;
}
/*
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.
*/
#ifndef RASTERIMAGEITEM_H
#define RASTERIMAGEITEM_H
#include <QGraphicsItem>
#include "lib/renderingintent.h"
namespace Gwenview
{
class RasterImageView;
/**
* A QGraphicsItem subclass responsible for rendering the main raster image.
*
* This class is resposible for painting the main image when it is a raster
* image. It will get the visible area from the main image, translate and scale
* this based on the values from the parent ImageView, then apply color
* correction. Finally the result will be drawn to the screen.
*
* For performance, two extra images are cached, one at a third of the image
* size and one at a sixth. These are used at low zoom levels, to avoid having
* to copy large amounts of image data that later gets discarded.
*/
class RasterImageItem : public QGraphicsItem
{
public:
RasterImageItem(RasterImageView* parent);
~RasterImageItem() override;
/**
* Set the rendering intent for color correction.
*/
void setRenderingIntent(RenderingIntent::Enum intent);
/**
* Update the internal, smaller cached versions of the main image.
*/
void updateCache();
/**
* Reimplemented from QGraphicsItem::paint
*/
virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override;
/**
* Reimplemented from QGraphicsItem::boundingRect
*/
virtual QRectF boundingRect() const override;
private:
void applyDisplayTransform(QImage& image);
void updateDisplayTransform(QImage::Format format);
RasterImageView* mParentView;
bool mApplyDisplayTransform = true;
cmsHTRANSFORM mDisplayTransform = nullptr;
cmsUInt32Number mRenderingIntent = INTENT_PERCEPTUAL;
QImage mThirdScaledImage;
QImage mSixthScaledImage;
};
}
#endif // RASTERIMAGEITEM_H
......@@ -28,6 +28,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA
#include <lib/paintutils.h>
#include "gwenview_lib_debug.h"
#include "alphabackgrounditem.h"
#include "rasterimageitem.h"
// KF
......@@ -38,46 +39,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA
#include <QPointer>
#include <QApplication>
#include <QCryptographicHash>
#include <QColorSpace>
namespace Gwenview
{
/*
* This is effectively QGraphicsPixmapItem that draws the document's QImage
* directly rather than going through a QPixmap intermediary. This avoids
* duplicating the image contents and the extra memory usage that comes from
* that.
*/
class RasterImageItem : public QGraphicsItem
{
public:
RasterImageItem(RasterImageView* parent)
: QGraphicsItem(parent)
, mParentView(parent)
{
}
void paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/) override
{
auto document = mParentView->document();
// We want nearest neighbour when zooming in since that provides the most
// accurate representation of pixels, but when zooming out it will actually
// not look very nice, so use smoothing when zooming out.
painter->setRenderHint(QPainter::SmoothPixmapTransform, mParentView->zoom() < 1.0);
painter->drawImage(QPointF{0, 0}, document->image());
}
QRectF boundingRect() const override
{
return QRectF{QPointF{0, 0}, mParentView->documentSize()};
}
RasterImageView* mParentView;
};
/*
* We need the tools to be painted on top of the image. However, since we are
* using RasterImageItem as a child of this item, the image gets painted on
......@@ -118,76 +84,11 @@ struct RasterImageViewPrivate
RasterImageView* q;
QGraphicsItem* mImageItem;
RasterImageItem* mImageItem;
ToolPainter* mToolItem = nullptr;
cmsUInt32Number mRenderingIntent = INTENT_PERCEPTUAL;
QPointer<AbstractRasterImageViewTool> mTool;
bool mApplyDisplayTransform = true; // Can be set to false if there is no need or no way to apply color profile
cmsHTRANSFORM mDisplayTransform = nullptr;
void updateDisplayTransform(QImage::Format format)
{
GV_RETURN_IF_FAIL(format != QImage::Format_Invalid);
mApplyDisplayTransform = false;
if (mDisplayTransform) {
cmsDeleteTransform(mDisplayTransform);
}
mDisplayTransform = nullptr;
Cms::Profile::Ptr profile = q->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;
default:
qCWarning(GWENVIEW_LIB_LOG) << "Gwenview can only apply color profile on RGB32 or ARGB32 images";
return;
}
mDisplayTransform = cmsCreateTransform(profile->handle(), cmsFormat,
monitorProfile->handle(), cmsFormat,
mRenderingIntent, cmsFLAGS_BLACKPOINTCOMPENSATION);
mApplyDisplayTransform = true;
}
void applyImageTransform()
{
if (!q->document()) {
return;
}
auto image = q->document()->image();
if (mApplyDisplayTransform) {
updateDisplayTransform(image.format());
if (mDisplayTransform) {
quint8 *bytes = const_cast<quint8*>(image.bits());
cmsDoTransform(mDisplayTransform, bytes, bytes, image.width() * image.height());
}
}
q->update();
}
void startAnimationIfNecessary()
{
if (q->document() && q->isVisible()) {
......@@ -211,8 +112,6 @@ RasterImageView::RasterImageView(QGraphicsItem* parent)
// Clip this item so we only render the visible part of the image when
// zoomed or when viewing a large image.
setFlag(QGraphicsItem::ItemClipsChildrenToShape);
setCacheMode(QGraphicsItem::DeviceCoordinateCache);
}
RasterImageView::~RasterImageView()
......@@ -220,25 +119,18 @@ RasterImageView::~RasterImageView()
if (d->mTool) {
d->mTool.data()->toolDeactivated();
}
if (d->mDisplayTransform) {
cmsDeleteTransform(d->mDisplayTransform);
}
delete d;
}
void RasterImageView::setRenderingIntent(const RenderingIntent::Enum& renderingIntent)
{
if (d->mRenderingIntent != renderingIntent) {
d->mRenderingIntent = renderingIntent;
d->applyImageTransform();
}
d->mImageItem->setRenderingIntent(renderingIntent);
}
void RasterImageView::resetMonitorICC()
{
d->mApplyDisplayTransform = true;
d->applyImageTransform();
update();
}
void RasterImageView::loadFromDocument()
......@@ -252,6 +144,8 @@ void RasterImageView::loadFromDocument()
this, &RasterImageView::slotDocumentMetaInfoLoaded);
connect(doc.data(), &Document::isAnimatedUpdated,
this, &RasterImageView::slotDocumentIsAnimatedUpdated);
connect(doc.data(), &Document::imageRectUpdated,
this, [this]() { d->mImageItem->updateCache(); });
const Document::LoadingState state = doc->loadingState();
if (state == Document::MetaInfoLoaded || state == Document::Loaded) {
......@@ -276,8 +170,6 @@ void RasterImageView::finishSetDocument()
{
GV_RETURN_IF_FAIL(document()->size().isValid());
d->applyImageTransform();
if (zoomToFit()) {
// Force the update otherwise if computeZoomToFit() returns 1, setZoom()
// will think zoom has not changed and won't update the image
......@@ -292,6 +184,8 @@ void RasterImageView::finishSetDocument()
d->startAnimationIfNecessary();
update();
d->mImageItem->updateCache();
backgroundItem()->setVisible(true);
Q_EMIT completed();
......@@ -304,7 +198,6 @@ void RasterImageView::slotDocumentIsAnimatedUpdated()
void RasterImageView::onZoomChanged()
{
d->mImageItem->setScale(zoom() / devicePixelRatio());
d->adjustItemPosition();
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment