File indexing completed on 2024-04-28 04:21:26

0001 // SPDX-FileCopyrightText: 2003 - 2022 Jesper K. Pedersen <blackie@kde.org>
0002 // SPDX-FileCopyrightText: 2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 #include "ImageDisplay.h"
0007 
0008 #include "Logging.h"
0009 #include "ViewHandler.h"
0010 
0011 #include <DB/ImageDB.h>
0012 #include <ImageManager/AsyncLoader.h>
0013 #include <kpabase/SettingsData.h>
0014 
0015 #include <KLocalizedString>
0016 #include <KMessageBox>
0017 #include <QApplication>
0018 #include <QCursor>
0019 #include <QMouseEvent>
0020 #include <QPaintEvent>
0021 #include <QPainter>
0022 #include <QResizeEvent>
0023 #include <QTimer>
0024 #include <cmath>
0025 
0026 /**
0027    Area displaying the actual image in the viewer.
0028 
0029    The purpose of this class is to display the actual image in the
0030    viewer. This involves controlling zooming and drawing on the images.
0031 
0032    This class is quite complicated as it had to both be fast and memory
0033    efficient. The following are dead end tried:
0034    1) Initially QPainter::setWindow was used for zooming the images, but
0035       this had the effect that if you zoom to 100x100 from a 2300x1700
0036       image on a 800x600 display, then Qt would internally create a pixmap
0037       with the size (2300/100)*800, (1700/100)*600, which takes up 1.4Gb of
0038       memory!
0039    2) I tried doing all scaling and cropping using QPixmap's as that would
0040       allow me to keep all transformations on the X Server site (making
0041       resizing fast - or I beleived so). Unfortunately it showed up that
0042       this was much slower than doing it using QImage, and the result was
0043       thus that the looking at a series of images was slow.
0044 
0045    The process is as follows:
0046    - The image loaded from disk is rotated and stored in _loadedImage.
0047      Initially this image is as large as the view, until the
0048      user starts zooming, at which time the image is reloaded to the size
0049      as it is on disk.
0050    - Then _loadedImage is cropped and scaled to _croppedAndScaledImg. This
0051      image is the size of the display. Resizing the window thus needs to
0052      redo step.
0053    - Finally in paintEvent _croppedAndScaledImg is drawn to the screen.
0054 
0055    The above might very likely be simplified. Back in the old days it needed to be that
0056    complex to allow drawing on images.
0057 
0058    To propagate the cache, we need to know which direction the
0059    images are viewed in, which is the job of the instance variable _forward.
0060 */
0061 
0062 Viewer::ImageDisplay::ImageDisplay(QWidget *parent)
0063     : AbstractDisplay(parent)
0064     , m_reloadImageInProgress(false)
0065     , m_forward(true)
0066     , m_curIndex(0)
0067     , m_busy(false)
0068 {
0069     m_viewHandler = new ViewHandler(this);
0070 
0071     setMouseTracking(true);
0072 }
0073 
0074 void Viewer::ImageDisplay::mousePressEvent(QMouseEvent *event)
0075 {
0076     QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers());
0077     double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size());
0078     bool block = m_viewHandler->mousePressEvent(&e, event->pos(), ratio);
0079     if (!block)
0080         QWidget::mousePressEvent(event);
0081     update();
0082 }
0083 
0084 void Viewer::ImageDisplay::mouseMoveEvent(QMouseEvent *event)
0085 {
0086     QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers());
0087     double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size());
0088     bool block = m_viewHandler->mouseMoveEvent(&e, event->pos(), ratio);
0089     if (!block)
0090         QWidget::mouseMoveEvent(event);
0091     update();
0092 }
0093 
0094 void Viewer::ImageDisplay::mouseReleaseEvent(QMouseEvent *event)
0095 {
0096     m_cache.remove(m_curIndex);
0097     QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers());
0098     double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size());
0099     bool block = m_viewHandler->mouseReleaseEvent(&e, event->pos(), ratio);
0100     if (!block) {
0101         QWidget::mouseReleaseEvent(event);
0102     }
0103     Q_EMIT possibleChange();
0104     update();
0105 }
0106 
0107 bool Viewer::ImageDisplay::setImageImpl(DB::ImageInfoPtr info, bool forward)
0108 {
0109     qCDebug(ViewerLog) << "setImage(" << info->fileName().relative() << "," << forward << ")";
0110     m_loadedImage = QImage();
0111 
0112     // Find the index of the current image
0113     m_curIndex = 0;
0114     for (const DB::FileName &filename : qAsConst(m_imageList)) {
0115         if (filename == info->fileName())
0116             break;
0117         ++m_curIndex;
0118     }
0119 
0120     if (m_cache.contains(m_curIndex) && m_cache[m_curIndex].angle == info->angle()) {
0121         const ViewPreloadInfo &found = m_cache[m_curIndex];
0122         m_loadedImage = found.img;
0123         updateZoomPoints(Settings::SettingsData::instance()->viewerStandardSize(), found.img.size());
0124         cropAndScale();
0125         info->setSize(found.size);
0126         Q_EMIT imageReady();
0127     } else {
0128         requestImage(info, true);
0129         busy();
0130     }
0131     m_forward = forward;
0132     updatePreload();
0133 
0134     return true;
0135 }
0136 
0137 void Viewer::ImageDisplay::resizeEvent(QResizeEvent *event)
0138 {
0139     ImageManager::AsyncLoader::instance()->stop(this, ImageManager::StopOnlyNonPriorityLoads);
0140     m_cache.clear();
0141     if (m_info) {
0142         cropAndScale();
0143         if (event->size().width() > 1.5 * this->m_loadedImage.size().width() || event->size().height() > 1.5 * this->m_loadedImage.size().height())
0144             potentiallyLoadFullSize(); // Only do if we scale much bigger.
0145     }
0146     updatePreload();
0147 }
0148 
0149 void Viewer::ImageDisplay::paintEvent(QPaintEvent *)
0150 {
0151     int x = (width() - m_croppedAndScaledImg.width()) / 2;
0152     int y = (height() - m_croppedAndScaledImg.height()) / 2;
0153 
0154     QPainter painter(this);
0155     Q_ASSERT(painter.isActive());
0156     painter.fillRect(0, 0, width(), height(), palette().base());
0157     painter.drawImage(x, y, m_croppedAndScaledImg);
0158 }
0159 
0160 QPoint Viewer::ImageDisplay::offset(int logicalWidth, int logicalHeight, int physicalWidth, int physicalHeight, double *ratio)
0161 {
0162     double rat = sizeRatio(QSize(logicalWidth, logicalHeight), QSize(physicalWidth, physicalHeight));
0163 
0164     int ox = (int)(physicalWidth - logicalWidth * rat) / 2;
0165     int oy = (int)(physicalHeight - logicalHeight * rat) / 2;
0166     if (ratio)
0167         *ratio = rat;
0168     return QPoint(ox, oy);
0169 }
0170 
0171 void Viewer::ImageDisplay::zoom(QPoint p1, QPoint p2)
0172 {
0173     qCDebug(ViewerLog, "zoom(%d,%d, %d,%d)", p1.x(), p1.y(), p2.x(), p2.y());
0174     if (!m_info) {
0175         // should not happen because the user can't click fast enough, but who knows...
0176         qCWarning(ViewerLog, "Trying to zoom without an image");
0177         return;
0178     }
0179     m_cache.remove(m_curIndex);
0180     normalize(p1, p2);
0181 
0182     double ratio;
0183     QPoint off = offset((p2 - p1).x(), (p2 - p1).y(), width(), height(), &ratio);
0184     off = off / ratio;
0185 
0186     p1.setX(p1.x() - off.x());
0187     p1.setY(p1.y() - off.y());
0188     p2.setX(p2.x() + off.x());
0189     p2.setY(p2.y() + off.y());
0190 
0191     m_zStart = p1;
0192     m_zEnd = p2;
0193     potentiallyLoadFullSize();
0194     cropAndScale();
0195 }
0196 
0197 QPoint Viewer::ImageDisplay::mapPos(QPoint p)
0198 {
0199     QPoint off = offset(qAbs(m_zEnd.x() - m_zStart.x()), qAbs(m_zEnd.y() - m_zStart.y()), width(), height(), nullptr);
0200     p -= off;
0201     int x = (int)(m_zStart.x() + (m_zEnd.x() - m_zStart.x()) * ((double)p.x() / (width() - 2 * off.x())));
0202     int y = (int)(m_zStart.y() + (m_zEnd.y() - m_zStart.y()) * ((double)p.y() / (height() - 2 * off.y())));
0203 
0204     return QPoint(x, y);
0205 }
0206 
0207 void Viewer::ImageDisplay::zoomIn()
0208 {
0209     qCDebug(ViewerLog, "zoomIn()");
0210     QPoint size = (m_zEnd - m_zStart);
0211     QPoint p1 = m_zStart + size * (0.2 / 2);
0212     QPoint p2 = m_zEnd - size * (0.2 / 2);
0213     zoom(p1, p2);
0214 }
0215 
0216 void Viewer::ImageDisplay::zoomOut()
0217 {
0218     qCDebug(ViewerLog, "zoomOut()");
0219     QPoint size = (m_zEnd - m_zStart);
0220 
0221     // Bug 150971, Qt tries to render bigger and bigger images (10000x10000), hence running out of memory.
0222     if ((size.x() * size.y() > 25 * 1024 * 1024))
0223         return;
0224 
0225     QPoint p1 = m_zStart - size * (0.25 / 2);
0226     QPoint p2 = m_zEnd + size * (0.25 / 2);
0227     zoom(p1, p2);
0228 }
0229 
0230 void Viewer::ImageDisplay::zoomFull()
0231 {
0232     qCDebug(ViewerLog, "zoomFull()");
0233     m_zStart = QPoint(0, 0);
0234     m_zEnd = QPoint(m_loadedImage.width(), m_loadedImage.height());
0235     zoom(QPoint(0, 0), QPoint(m_loadedImage.width(), m_loadedImage.height()));
0236 }
0237 
0238 void Viewer::ImageDisplay::normalize(QPoint &p1, QPoint &p2)
0239 {
0240     int minx = qMin(p1.x(), p2.x());
0241     int miny = qMin(p1.y(), p2.y());
0242     int maxx = qMax(p1.x(), p2.x());
0243     int maxy = qMax(p1.y(), p2.y());
0244     p1 = QPoint(minx, miny);
0245     p2 = QPoint(maxx, maxy);
0246 }
0247 
0248 void Viewer::ImageDisplay::pan(const QPoint &point)
0249 {
0250     m_zStart += point;
0251     m_zEnd += point;
0252     cropAndScale();
0253 }
0254 
0255 void Viewer::ImageDisplay::cropAndScale()
0256 {
0257     if (m_loadedImage.isNull()) {
0258         return;
0259     }
0260 
0261     if (m_zStart != QPoint(0, 0) || m_zEnd != QPoint(m_loadedImage.width(), m_loadedImage.height())) {
0262         qCDebug(ViewerLog) << "cropAndScale(): using cropped image" << m_zStart << "-" << m_zEnd;
0263         // we request a copy that is bigger than the loaded image
0264         // copy fills the pixels outside the loaded image with transparent pixels
0265         m_croppedAndScaledImg = m_loadedImage.copy(m_zStart.x(), m_zStart.y(), m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y());
0266     } else {
0267         qCDebug(ViewerLog) << "cropAndScale(): using full image.";
0268         m_croppedAndScaledImg = m_loadedImage;
0269     }
0270 
0271     updateZoomCaption();
0272 
0273     if (!m_croppedAndScaledImg.isNull()) // I don't know how this can happen, but it seems not to be dangerous.
0274     {
0275         qCDebug(ViewerLog) << "cropAndScale(): scaling image to" << width() << "x" << height();
0276         m_croppedAndScaledImg = m_croppedAndScaledImg.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
0277     } else {
0278         qCDebug(ViewerLog) << "cropAndScale(): image is null.";
0279     }
0280 
0281     update();
0282 
0283     Q_EMIT viewGeometryChanged(m_croppedAndScaledImg.size(), QRect(m_zStart, m_zEnd), sizeRatio(m_loadedImage.size(), m_info->size()));
0284 }
0285 
0286 void Viewer::ImageDisplay::filterNone()
0287 {
0288     cropAndScale();
0289     update();
0290 }
0291 
0292 bool Viewer::ImageDisplay::filterMono()
0293 {
0294     m_croppedAndScaledImg = m_croppedAndScaledImg.convertToFormat(m_croppedAndScaledImg.Format_Mono);
0295     update();
0296     return true;
0297 }
0298 
0299 // I can't believe there isn't a standard conversion for this??? -- WH
0300 bool Viewer::ImageDisplay::filterBW()
0301 {
0302     if (m_croppedAndScaledImg.depth() < 32) {
0303         KMessageBox::error(this, i18n("Insufficient color depth for this filter"));
0304         return false;
0305     }
0306 
0307     for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) {
0308         for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) {
0309             int pixel = m_croppedAndScaledImg.pixel(x, y);
0310             int gray = qGray(pixel);
0311             int alpha = qAlpha(pixel);
0312             m_croppedAndScaledImg.setPixel(x, y, qRgba(gray, gray, gray, alpha));
0313         }
0314     }
0315     update();
0316     return true;
0317 }
0318 
0319 bool Viewer::ImageDisplay::filterContrastStretch()
0320 {
0321     int redMin, redMax, greenMin, greenMax, blueMin, blueMax;
0322 
0323     redMin = greenMin = blueMin = 255;
0324     redMax = greenMax = blueMax = 0;
0325 
0326     if (m_croppedAndScaledImg.depth() < 32) {
0327         KMessageBox::error(this, i18n("Insufficient color depth for this filter"));
0328         return false;
0329     }
0330 
0331     // Look for minimum and maximum intensities within each color channel
0332     for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) {
0333         for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) {
0334             int pixel = m_croppedAndScaledImg.pixel(x, y);
0335             int red = qRed(pixel);
0336             int green = qGreen(pixel);
0337             int blue = qBlue(pixel);
0338             redMin = redMin < red ? redMin : red;
0339             redMax = redMax > red ? redMax : red;
0340             greenMin = greenMin < green ? greenMin : green;
0341             greenMax = greenMax > green ? greenMax : green;
0342             blueMin = blueMin < blue ? blueMin : blue;
0343             blueMax = blueMax > blue ? blueMax : blue;
0344         }
0345     }
0346 
0347     // Calculate factor for stretching each color intensity throughout the
0348     // whole range
0349     float redFactor, greenFactor, blueFactor;
0350     redFactor = ((float)(255) / (float)(redMax - redMin));
0351     greenFactor = ((float)(255) / (float)(greenMax - greenMin));
0352     blueFactor = ((float)(255) / (float)(blueMax - blueMin));
0353 
0354     // Perform the contrast stretching
0355     for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) {
0356         for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) {
0357             int pixel = m_croppedAndScaledImg.pixel(x, y);
0358             int red = qRed(pixel);
0359             int green = qGreen(pixel);
0360             int blue = qBlue(pixel);
0361             int alpha = qAlpha(pixel);
0362 
0363             red = (red - redMin) * redFactor;
0364             red = red < 255 ? red : 255;
0365             red = red > 0 ? red : 0;
0366             green = (green - greenMin) * greenFactor;
0367             green = green < 255 ? green : 255;
0368             green = green > 0 ? green : 0;
0369             blue = (blue - blueMin) * blueFactor;
0370             blue = blue < 255 ? blue : 255;
0371             blue = blue > 0 ? blue : 0;
0372             m_croppedAndScaledImg.setPixel(x, y, qRgba(red, green, blue, alpha));
0373         }
0374     }
0375     update();
0376     return true;
0377 }
0378 
0379 bool Viewer::ImageDisplay::filterHistogramEqualization()
0380 {
0381     int width, height;
0382     float R_histogram[256];
0383     float G_histogram[256];
0384     float B_histogram[256];
0385     float d;
0386 
0387     if (m_croppedAndScaledImg.depth() < 32) {
0388         KMessageBox::error(this, i18n("Insufficient color depth for this filter"));
0389         return false;
0390     }
0391     memset(R_histogram, 0, sizeof(R_histogram));
0392     memset(G_histogram, 0, sizeof(G_histogram));
0393     memset(B_histogram, 0, sizeof(B_histogram));
0394 
0395     width = m_croppedAndScaledImg.width();
0396     height = m_croppedAndScaledImg.height();
0397     d = 1.0 / width / height;
0398 
0399     // Populate histogram for each color channel
0400     for (int y = 0; y < height; ++y) {
0401         for (int x = 1; x < width; ++x) {
0402             int pixel = m_croppedAndScaledImg.pixel(x, y);
0403 
0404             R_histogram[qRed(pixel)] += d;
0405             G_histogram[qGreen(pixel)] += d;
0406             B_histogram[qBlue(pixel)] += d;
0407         }
0408     }
0409 
0410     // Transfer histogram table to cumulative distribution table
0411     float R_sum = 0.0;
0412     float G_sum = 0.0;
0413     float B_sum = 0.0;
0414     for (int i = 0; i < 256; ++i) {
0415         R_sum += R_histogram[i];
0416         G_sum += G_histogram[i];
0417         B_sum += B_histogram[i];
0418 
0419         R_histogram[i] = R_sum * 255 + 0.5;
0420         G_histogram[i] = G_sum * 255 + 0.5;
0421         B_histogram[i] = B_sum * 255 + 0.5;
0422     }
0423 
0424     // Equalize the image
0425     for (int y = 0; y < height; ++y) {
0426         for (int x = 0; x < width; ++x) {
0427             int pixel = m_croppedAndScaledImg.pixel(x, y);
0428 
0429             m_croppedAndScaledImg.setPixel(
0430                 x, y, qRgba(R_histogram[qRed(pixel)], G_histogram[qGreen(pixel)], B_histogram[qBlue(pixel)], qAlpha(pixel)));
0431         }
0432     }
0433     update();
0434     return true;
0435 }
0436 
0437 void Viewer::ImageDisplay::updateZoomCaption()
0438 {
0439     const QSize imgSize = m_loadedImage.size();
0440     // similar to sizeRatio(), but we take the _highest_ factor.
0441     double ratio = ((double)imgSize.width()) / (m_zEnd.x() - m_zStart.x());
0442     if (ratio * (m_zEnd.y() - m_zStart.y()) < imgSize.height()) {
0443         ratio = ((double)imgSize.height()) / (m_zEnd.y() - m_zStart.y());
0444     }
0445 
0446     Q_EMIT imageZoomCaptionChanged((ratio > 1.05)
0447                                        ? ki18n("[ zoom x%1 ]").subs(ratio, 0, 'f', 1).toString()
0448                                        : QString());
0449 }
0450 
0451 QImage Viewer::ImageDisplay::currentViewAsThumbnail() const
0452 {
0453     if (m_croppedAndScaledImg.isNull())
0454         return QImage();
0455     else
0456         return m_croppedAndScaledImg.scaled(512, 512, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0457 }
0458 
0459 bool Viewer::ImageDisplay::isImageZoomed(const Settings::StandardViewSize type, const QSize &imgSize)
0460 {
0461     if (type == Settings::FullSize)
0462         return true;
0463 
0464     if (type == Settings::NaturalSizeIfFits)
0465         return !(imgSize.width() < width() && imgSize.height() < height());
0466 
0467     return false;
0468 }
0469 
0470 void Viewer::ImageDisplay::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image)
0471 {
0472     const DB::FileName fileName = request->databaseFileName();
0473     const QSize imgSize = request->size();
0474     const QSize fullSize = request->fullSize();
0475     const int angle = request->angle();
0476     const bool loadedOK = request->loadedOK();
0477 
0478     // the image might have changed (even to a null value) while the pixmap was loaded
0479     if (loadedOK && m_info && fileName == m_info->fileName()) {
0480         if (fullSize.isValid() && !m_info->size().isValid())
0481             m_info->setSize(fullSize);
0482 
0483         if (!m_reloadImageInProgress)
0484             updateZoomPoints(Settings::SettingsData::instance()->viewerStandardSize(), image.size());
0485         else {
0486             // See documentation for zoomPixelForPixel for details.
0487             // We just loaded a likely much larger image, so the zoom points
0488             // need to be scaled. Notice m_loadedImage is the size of the
0489             // old image.
0490             // when using raw images, the decoded image may be a preview
0491             // and have a size different from m_info->size(). Therefore, use fullSize here:
0492             double ratio = sizeRatio(m_loadedImage.size(), fullSize);
0493 
0494             qCDebug(ViewerLog) << "Old size:" << m_loadedImage.size() << "; new size:" << m_info->size();
0495             qCDebug(ViewerLog) << "Req size:" << imgSize << "fullsize:" << fullSize;
0496             qCDebug(ViewerLog) << "pixmapLoaded(): Zoom region was" << m_zStart << "-" << m_zEnd;
0497             m_zStart *= ratio;
0498             m_zEnd *= ratio;
0499             qCDebug(ViewerLog) << "pixmapLoaded(): Zoom region changed to" << m_zStart << "-" << m_zEnd;
0500 
0501             m_reloadImageInProgress = false;
0502         }
0503 
0504         // for zoom operations we need a format with alpha channel (see cropAndScale())
0505         m_loadedImage = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
0506         cropAndScale();
0507         Q_EMIT imageReady();
0508     } else {
0509         if (imgSize != size())
0510             return; // Might be an old preload version, or a loaded version that never made it in time
0511 
0512         ViewPreloadInfo info(image, fullSize, angle);
0513         m_cache.insert(indexOf(fileName), info);
0514         updatePreload();
0515     }
0516     unbusy();
0517     Q_EMIT possibleChange();
0518 }
0519 
0520 void Viewer::ImageDisplay::setImageList(const DB::FileNameList &list)
0521 {
0522     m_imageList = list;
0523     m_cache.clear();
0524 }
0525 
0526 void Viewer::ImageDisplay::updatePreload()
0527 {
0528     // cacheSize: number of images at current window dimensions (at 4 byte per pixel)
0529     const int cacheSize = (int)((long long)(Settings::SettingsData::instance()->viewerCacheSize() * 1024LL * 1024LL) / (width() * height() * 4));
0530     bool cacheFull = (m_cache.count() > cacheSize);
0531 
0532     int incr = (m_forward ? 1 : -1);
0533     int nextOnesInCache = 0;
0534     // Iterate from the current image in the direction of the viewing
0535     for (int i = m_curIndex + incr; cacheSize; i += incr) {
0536         if (m_forward ? (i >= (int)m_imageList.count()) : (i < 0))
0537             break;
0538 
0539         DB::ImageInfoPtr info = DB::ImageDB::instance()->info(m_imageList[i]);
0540         if (!info) {
0541             qCWarning(ViewerLog, "Info was null for index %d!", i);
0542             return;
0543         }
0544 
0545         if (m_cache.contains(i)) {
0546             nextOnesInCache++;
0547             if (nextOnesInCache >= ceil(cacheSize / 2.0) && cacheFull) {
0548                 // Ok enough images in cache
0549                 return;
0550             }
0551         } else {
0552             requestImage(info);
0553 
0554             if (cacheFull) {
0555                 // The cache was full, we need to delete an item from the cache.
0556 
0557                 // First try to find an item from the direction we came from
0558                 for (int j = (m_forward ? 0 : m_imageList.count() - 1);
0559                      j != m_curIndex;
0560                      j += (m_forward ? 1 : -1)) {
0561                     if (m_cache.contains(j)) {
0562                         m_cache.remove(j);
0563                         return;
0564                     }
0565                 }
0566 
0567                 // OK We found no item in the direction we came from (think of home/end keys)
0568                 for (int j = (m_forward ? m_imageList.count() - 1 : 0);
0569                      j != m_curIndex;
0570                      j += (m_forward ? -1 : 1)) {
0571                     if (m_cache.contains(j)) {
0572                         m_cache.remove(j);
0573                         return;
0574                     }
0575                 }
0576 
0577                 Q_ASSERT(false); // We should never get here.
0578             }
0579 
0580             return;
0581         }
0582     }
0583 }
0584 
0585 int Viewer::ImageDisplay::indexOf(const DB::FileName &fileName)
0586 {
0587     int i = 0;
0588     for (const DB::FileName &name : qAsConst(m_imageList)) {
0589         if (name == fileName)
0590             break;
0591         ++i;
0592     }
0593     return i;
0594 }
0595 
0596 void Viewer::ImageDisplay::busy()
0597 {
0598     if (!m_busy)
0599         qApp->setOverrideCursor(Qt::WaitCursor);
0600     m_busy = true;
0601 }
0602 
0603 void Viewer::ImageDisplay::unbusy()
0604 {
0605     if (m_busy)
0606         qApp->restoreOverrideCursor();
0607     m_busy = false;
0608 }
0609 
0610 void Viewer::ImageDisplay::zoomPixelForPixel()
0611 {
0612     qCDebug(ViewerLog, "zoomPixelForPixel()");
0613     if (!m_info) {
0614         // should not happen because the user can't click fast enough, but who knows...
0615         qCWarning(ViewerLog, "Trying to zoom without an image");
0616         return;
0617     }
0618     // This is rather tricky.
0619     // We want to zoom to a pixel level for the real image, which we might
0620     // or might not have loaded yet.
0621     //
0622     // First we ask for zoom points as they would look like had we had the
0623     // real image loaded now. (We need to ask for them, for the real image,
0624     // otherwise we would just zoom to the pixel level of the view size
0625     // image)
0626     updateZoomPoints(Settings::NaturalSize, m_info->size());
0627 
0628     // The points now, however might not match the current visible image -
0629     // as this image might be be only view size large. We therefore need
0630     // to scale the coordinates.
0631     double ratio = sizeRatio(m_loadedImage.size(), m_info->size());
0632     qCDebug(ViewerLog) << "zoomPixelForPixel(): Zoom region was" << m_zStart << "-" << m_zEnd;
0633     m_zStart /= ratio;
0634     m_zEnd /= ratio;
0635     qCDebug(ViewerLog) << "zoomPixelForPixel(): Zoom region changed to" << m_zStart << "-" << m_zEnd;
0636     cropAndScale();
0637     potentiallyLoadFullSize();
0638 }
0639 
0640 void Viewer::ImageDisplay::rotate(const DB::ImageInfoPtr &info)
0641 {
0642     setImage(info, m_forward);
0643 }
0644 
0645 void Viewer::ImageDisplay::updateZoomPoints(const Settings::StandardViewSize type, const QSize &imgSize)
0646 {
0647     const int iw = imgSize.width();
0648     const int ih = imgSize.height();
0649 
0650     if (isImageZoomed(type, imgSize)) {
0651         m_zStart = QPoint(0, 0);
0652         m_zEnd = QPoint(iw, ih);
0653         qCDebug(ViewerLog) << "updateZoomPoints(): Zoom region reset to" << m_zStart << "-" << m_zEnd;
0654     } else {
0655         m_zStart = QPoint(-(width() - iw) / 2, -(height() - ih) / 2);
0656         m_zEnd = QPoint(iw + (width() - iw) / 2, ih + (height() - ih) / 2);
0657         qCDebug(ViewerLog) << "updateZoomPoints(): Zoom region set to" << m_zStart << "-" << m_zEnd;
0658     }
0659 }
0660 
0661 void Viewer::ImageDisplay::potentiallyLoadFullSize()
0662 {
0663     if (m_info && m_info->size() != m_loadedImage.size()) {
0664         qCDebug(ViewerLog) << "Loading full size image for " << m_info->fileName().relative();
0665         ImageManager::ImageRequest *request = new ImageManager::ImageRequest(m_info->fileName(), QSize(-1, -1), m_info->angle(), this);
0666         request->setPriority(ImageManager::Viewer);
0667         ImageManager::AsyncLoader::instance()->load(request);
0668         busy();
0669         m_reloadImageInProgress = true;
0670     }
0671 }
0672 
0673 /**
0674  * return the ratio of the two sizes. That is  newSize/baseSize.
0675  */
0676 double Viewer::ImageDisplay::sizeRatio(const QSize &baseSize, const QSize &newSize) const
0677 {
0678     double res = ((double)newSize.width()) / baseSize.width();
0679 
0680     if (res * baseSize.height() > newSize.height()) {
0681         res = ((double)newSize.height()) / baseSize.height();
0682     }
0683     return res;
0684 }
0685 
0686 void Viewer::ImageDisplay::requestImage(const DB::ImageInfoPtr &info, bool priority)
0687 {
0688     Settings::StandardViewSize viewSize = Settings::SettingsData::instance()->viewerStandardSize();
0689     QSize s = size();
0690     if (viewSize == Settings::NaturalSize)
0691         s = QSize(-1, -1);
0692 
0693     ImageManager::ImageRequest *request = new ImageManager::ImageRequest(info->fileName(), s, info->angle(), this);
0694     request->setUpScale(viewSize == Settings::FullSize);
0695     request->setPriority(priority ? ImageManager::Viewer : ImageManager::ViewerPreload);
0696     ImageManager::AsyncLoader::instance()->load(request);
0697 }
0698 
0699 void Viewer::ImageDisplay::hideEvent(QHideEvent *)
0700 {
0701     m_viewHandler->hideEvent();
0702 }
0703 
0704 // vi:expandtab:tabstop=4 shiftwidth=4:
0705 
0706 #include "moc_ImageDisplay.cpp"