File indexing completed on 2025-03-09 03:52:05

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2007-02-11
0007  * Description : a tool to show image using an OpenGL interface.
0008  *
0009  * SPDX-FileCopyrightText: 2007-2008 by Markus Leuthold <kusi at forum dot titlis dot org>
0010  * SPDX-FileCopyrightText: 2008-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "glviewertexture.h"
0017 
0018 // Qt includes
0019 
0020 #include <QUrl>
0021 #include <QScopedPointer>
0022 
0023 // Local includes
0024 
0025 #include "dimg.h"
0026 #include "iccmanager.h"
0027 #include "iccsettings.h"
0028 #include "iccsettingscontainer.h"
0029 #include "previewloadthread.h"
0030 #include "dmetadata.h"
0031 #include "digikam_debug.h"
0032 #include "glviewertimer.h"
0033 
0034 namespace DigikamGenericGLViewerPlugin
0035 {
0036 
0037 class Q_DECL_HIDDEN GLViewerTexture::Private
0038 {
0039 public:
0040 
0041     explicit Private()
0042       : rdx          (0.0),
0043         rdy          (0.0),
0044         z            (0.0),
0045         ux           (0.0),
0046         uy           (0.0),
0047         rtx          (0.0),
0048         rty          (0.0),
0049         vtop         (0.0),
0050         vbottom      (0.0),
0051         vleft        (0.0),
0052         vright       (0.0),
0053         display_x    (0),
0054         display_y    (0),
0055         rotate_idx   (0),
0056         iface        (nullptr),
0057         displayWidget(nullptr)
0058     {
0059         rotate_list[0] = DMetadata::ORIENTATION_ROT_90;
0060         rotate_list[1] = DMetadata::ORIENTATION_ROT_180;
0061         rotate_list[2] = DMetadata::ORIENTATION_ROT_270;
0062         rotate_list[3] = DMetadata::ORIENTATION_ROT_180;
0063     }
0064 
0065     float                       rdx, rdy, z, ux, uy, rtx, rty;
0066     float                       vtop, vbottom, vleft, vright;
0067     int                         display_x, display_y;
0068     QString                     filename;
0069     QImage                      qimage;
0070     QImage                      fimage;
0071     DMetadata::ImageOrientation rotate_list[4];
0072     int                         rotate_idx;
0073     IccProfile                  iccProfile;
0074     DInfoInterface*             iface;
0075     QWidget*                    displayWidget;
0076 
0077 private:
0078 
0079     /// No copy constructor
0080     Private(const Private&);
0081 };
0082 
0083 GLViewerTexture::GLViewerTexture(DInfoInterface* const iface, QWidget* const display)
0084     : QOpenGLTexture(QOpenGLTexture::TargetRectangle),
0085       d             (new Private)
0086 {
0087     d->iface                      = iface;
0088     d->displayWidget              = display;
0089     ICCSettingsContainer settings = IccSettings::instance()->settings();
0090 
0091     if (settings.enableCM && settings.useManagedPreviews)
0092     {
0093         d->iccProfile = IccProfile(IccManager::displayProfile(display));
0094     }
0095 
0096     reset();
0097 }
0098 
0099 GLViewerTexture::~GLViewerTexture()
0100 {
0101     destroy();
0102 
0103     delete d;
0104 }
0105 
0106 /*!
0107     \fn GLViewerTexture::load(QString fn, QSize size)
0108     \brief load file from disc and save it in texture
0109     \param fn filename to load
0110     \param size the size of image which is downloaded to texture mem
0111     if "size" is set to image size, scaling is only performed by the GPU but not
0112     by the CPU, however the AGP usage to texture memory is increased (20MB for a 5mp image)
0113  */
0114 bool GLViewerTexture::load(const QString& fn, const QSize& size)
0115 {
0116     d->filename   = fn;
0117     d->qimage     = PreviewLoadThread::loadFastSynchronously(d->filename,
0118                                                              qMax(size.width()  * 1.2,
0119                                                                   size.height() * 1.2),
0120                                                              d->iccProfile).copyQImage();
0121 
0122     if (d->qimage.isNull())
0123     {
0124         return false;
0125     }
0126 
0127     loadInternal();
0128     reset();
0129 
0130     d->rotate_idx = 0;
0131 
0132     return true;
0133 }
0134 
0135 /*!
0136     \fn GLViewerTexture::load(QImage im, QSize size)
0137     \brief copy file from QImage to texture
0138     \param im Qimage to be copied from
0139  */
0140 bool GLViewerTexture::load(const QImage& im)
0141 {
0142     d->qimage = im;
0143 
0144     loadInternal();
0145     reset();
0146 
0147     d->rotate_idx   = 0;
0148 
0149     return true;
0150 }
0151 
0152 /*!
0153     \brief load full size image from disc and save it in texture
0154  */
0155 bool GLViewerTexture::loadFullSize()
0156 {
0157     if (!d->fimage.isNull())
0158     {
0159         return false;
0160     }
0161 
0162     d->fimage     = PreviewLoadThread::loadHighQualitySynchronously(d->filename,
0163                                                                     PreviewSettings::RawPreviewAutomatic,
0164                                                                     d->iccProfile).copyQImage();
0165 
0166     if (d->fimage.isNull())
0167     {
0168         return false;
0169     }
0170 
0171     loadInternal();
0172     reset();
0173 
0174     d->rotate_idx = 0;
0175 
0176     return true;
0177 }
0178 
0179 /*!
0180     \fn GLViewerTexture::load()
0181     internal load function
0182     rt[xy] <= 1
0183  */
0184 bool GLViewerTexture::loadInternal()
0185 {
0186     destroy();
0187     create();
0188 
0189     QImage texImg = d->fimage.isNull() ? d->qimage : d->fimage;
0190     setData(texImg.mirrored(), QOpenGLTexture::DontGenerateMipMaps);
0191 
0192     setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
0193     setMagnificationFilter(QOpenGLTexture::Linear);
0194 
0195     int w = width();
0196     int h = height();
0197 
0198     if (h < w)
0199     {
0200         d->rtx = 1;
0201         d->rty = float(h) / float(w);
0202     }
0203     else
0204     {
0205         d->rtx = float(w) / float(h);
0206         d->rty = 1;
0207     }
0208 
0209     return true;
0210 }
0211 
0212 /*!
0213     \fn GLViewerTexture::zoom(float delta, QPoint mousepos)
0214     \brief calculate new tex coords on zooming
0215     \param delta the delta between previous zoom and current zoom
0216     \param mousepos mouse position returned by QT
0217     \TODO rename mousepos to something more generic
0218 */
0219 void GLViewerTexture::zoom(float delta, const QPoint& mousepos)
0220 // u: start in texture, u = [0..1], u = 0 is begin, u = 1 is end of texture
0221 // z = [0..1], z = 1 -> no zoom
0222 // l: length of tex in glFrustum coordinate system
0223 // rt: ratio of tex, rt <= 1, see loadInternal() for definition
0224 // rd: ratio of display, rd >= 1
0225 // m: mouse pos normalized, cd = [0..rd]
0226 // c: mouse pos normalized to zoom * l, c = [0..1]
0227 {
0228     d->z    *= delta;
0229     delta    =  d->z * (1.0 / delta - 1.0); //convert to real delta = z_old - z_new
0230 
0231     float mx = mousepos.x() / (float)d->display_x * d->rdx;
0232     float cx = (mx-d->rdx / 2.0 + d->rtx / 2.0) / d->rtx;
0233     float vx = d->ux + cx * d->z;
0234     d->ux    = d->ux + (vx - d->ux) * delta / d->z;
0235 
0236     float my = mousepos.y() / (float)d->display_y * d->rdy;
0237     float cy = (my-d->rdy / 2.0 + d->rty / 2.0) / d->rty;
0238     cy       = 1 - cy;
0239     float vy = d->uy + cy * d->z;
0240     d->uy    = d->uy + (vy - d->uy) * delta / d->z;
0241 
0242     calcVertex();
0243 }
0244 
0245 /*!
0246     \fn GLViewerTexture::calcVertex()
0247     Calculate vertices according internal state variables
0248     z, ux, uy are calculated in GLViewerTexture::zoom()
0249  */
0250 void GLViewerTexture::calcVertex()
0251 // rt: ratio of tex, rt <= 1, see loadInternal() for definition
0252 // u: start in texture, u = [0..1], u=0 is begin, u=1 is end of texture
0253 // l: length of tex in glFrustum coordinate system
0254 // halftexel: the color of a texel is determined by a corner of the texel and not its center point
0255 //            this seems to introduce a visible jump on changing the tex-size.
0256 //
0257 // the glFrustum coord-sys is visible in [-rdx..rdx] ([-1..1] for square screen) for z = 1 (no zoom)
0258 // the tex coord-sys goes from [-rtx..rtx] ([-1..1] for square texture)
0259 {
0260     // x part
0261     float lx          = 2 * d->rtx / d->z;   //length of tex
0262     float tsx         = lx / (float)width(); //texelsize in glFrustum coordinates
0263     float halftexel_x = tsx / 2.0;
0264     float wx          = lx * (1 - d->ux - d->z);
0265     d->vleft          = -d->rtx - d->ux * lx - halftexel_x; //left
0266     d->vright         = d->rtx + wx - halftexel_x;          //right
0267 
0268     // y part
0269     float ly          = 2 * d->rty / d->z;
0270     float tsy         = ly / (float)height(); //texelsize in glFrustum coordinates
0271     float halftexel_y = tsy / 2.0;
0272     float wy          = ly * (1 - d->uy - d->z);
0273     d->vbottom        = -d->rty - d->uy * ly + halftexel_y; //bottom
0274     d->vtop           = d->rty + wy + halftexel_y;          //top
0275 }
0276 
0277 /*!
0278     \fn GLViewerTexture::vertex_bottom() const
0279  */
0280 GLfloat GLViewerTexture::vertex_bottom() const
0281 {
0282     return (GLfloat) d->vbottom;
0283 }
0284 
0285 /*!
0286     \fn GLViewerTexture::vertex_top() const
0287  */
0288 GLfloat GLViewerTexture::vertex_top() const
0289 {
0290     return (GLfloat) d->vtop;
0291 }
0292 
0293 /*!
0294     \fn GLViewerTexture::vertex_left() const
0295  */
0296 GLfloat GLViewerTexture::vertex_left() const
0297 {
0298     return (GLfloat) d->vleft;
0299 }
0300 
0301 /*!
0302     \fn GLViewerTexture::vertex_right() const
0303  */
0304 GLfloat GLViewerTexture::vertex_right() const
0305 {
0306     return (GLfloat) d->vright;
0307 }
0308 
0309 /*!
0310     \fn GLViewerTexture::setViewport(int w, int h)
0311     \param w width of window
0312     \param h height of window
0313     Set widget's viewport. Ensures that rdx & rdy are always > 1
0314  */
0315 void GLViewerTexture::setViewport(int w, int h)
0316 {
0317     if (h > w)
0318     {
0319         d->rdx = 1.0;
0320         d->rdy = h / float(w);
0321     }
0322     else
0323     {
0324         d->rdx = w / float(h);
0325         d->rdy = 1.0;
0326     }
0327 
0328     d->display_x = w;
0329     d->display_y = h;
0330 }
0331 
0332 /*!
0333     \fn GLViewerTexture::move(QPoint diff)
0334     new tex coordinates have to be calculated if the view is panned
0335  */
0336 void GLViewerTexture::move(const QPoint& diff)
0337 {
0338     d->ux = d->ux - diff.x() / float(d->display_x) * d->z * d->rdx / d->rtx;
0339     d->uy = d->uy + diff.y() / float(d->display_y) * d->z * d->rdy / d->rty;
0340     calcVertex();
0341 }
0342 
0343 /*!
0344     \fn GLViewerTexture::reset()
0345  */
0346 void GLViewerTexture::reset(bool resetFullImage)
0347 {
0348     d->ux           = 0;
0349     d->uy           = 0;
0350     d->z            = 1.0;
0351     float zoomdelta = 0;
0352 
0353     if ((d->rtx < d->rty) && (d->rdx < d->rdy) && ((d->rtx / d->rty) < (d->rdx / d->rdy)))
0354     {
0355         zoomdelta = d->z - d->rdx / d->rdy;
0356     }
0357 
0358     if ((d->rtx < d->rty) && ((d->rtx / d->rty) > (d->rdx / d->rdy)))
0359     {
0360         zoomdelta = d->z - d->rtx;
0361     }
0362 
0363     if ((d->rtx >= d->rty) && (d->rdy < d->rdx) && ((d->rty / d->rtx) < (d->rdy / d->rdx)))
0364     {
0365         zoomdelta = d->z - d->rdy / d->rdx;
0366     }
0367 
0368     if ((d->rtx >= d->rty) && ((d->rty / d->rtx) > (d->rdy / d->rdx)))
0369     {
0370         zoomdelta = d->z - d->rty;
0371     }
0372 
0373     QPoint p  = QPoint(d->display_x / 2, d->display_y / 2);
0374     zoom(1.0 - zoomdelta, p);
0375 
0376     if (resetFullImage)
0377     {
0378         d->fimage = QImage();
0379     }
0380 
0381     calcVertex();
0382 }
0383 
0384 /*!
0385     \fn GLViewerTexture::setNewSize(QSize size)
0386     \param size desired texture size. QSize(0,0) will take the full image
0387     \return true if size has changed, false otherwise
0388     set new texture size in order to reduce AGP bandwidth
0389  */
0390 bool GLViewerTexture::setNewSize(QSize size)
0391 {
0392     if (d->qimage.isNull())
0393     {
0394         return false;
0395     }
0396 
0397     // don't allow larger textures than the original image. the image will be upsampled by
0398     // OpenGL if necessary and not by QImage::scale
0399 
0400     QImage texImg = d->fimage.isNull() ? d->qimage : d->fimage;
0401     size          = size.boundedTo(texImg.size());
0402 
0403     if (width() == size.width())
0404     {
0405         return false;
0406     }
0407 
0408     int w = size.width();
0409     int h = size.height();
0410 
0411     destroy();
0412     create();
0413 
0414     if (w == 0)
0415     {
0416         setData(texImg.mirrored(), QOpenGLTexture::DontGenerateMipMaps);
0417     }
0418     else
0419     {
0420         setData(texImg.scaled(w, h, Qt::KeepAspectRatio,
0421                               Qt::SmoothTransformation).mirrored(),
0422                               QOpenGLTexture::DontGenerateMipMaps);
0423     }
0424 
0425     setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
0426     setMagnificationFilter(QOpenGLTexture::Linear);
0427 
0428     // recalculate half-texel offset
0429 
0430     calcVertex();
0431 
0432     return true;
0433 }
0434 
0435 /*!
0436     \fn GLViewerTexture::rotate()
0437     \brief smart image rotation
0438     since the two most frequent usecases are a CW or CCW rotation of 90,
0439     perform these rotation with one (+90) or two (-90) calls of rotation()
0440  */
0441 void GLViewerTexture::rotate()
0442 {
0443     QScopedPointer<DMetadata> meta(new DMetadata);
0444 
0445     if (!d->fimage.isNull())
0446     {
0447         meta->rotateExifQImage(d->fimage, (DMetadata::ImageOrientation)d->rotate_list[d->rotate_idx % 4]);
0448     }
0449 
0450     meta->rotateExifQImage(d->qimage, (DMetadata::ImageOrientation)d->rotate_list[d->rotate_idx % 4]);
0451 
0452     loadInternal();
0453 
0454     // save new rotation in host application
0455 
0456     DInfoInterface::DInfoMap info;
0457     DItemInfo item(info);
0458     item.setOrientation(d->rotate_list[d->rotate_idx % 4]);
0459     d->iface->setItemInfo(QUrl::fromLocalFile(d->filename), info);
0460 
0461     reset();
0462     d->rotate_idx++;
0463 }
0464 
0465 /*!
0466     \fn GLViewerTexture::setToOriginalSize()
0467     zoom image such that each pixel of the screen corresponds to a pixel in the jpg
0468     remember that OpenGL is not a pixel exact specification, and the image will still be filtered by OpenGL
0469  */
0470 void GLViewerTexture::zoomToOriginal()
0471 {
0472     QSize imgSize = d->fimage.isNull() ? d->qimage.size() : d->fimage.size();
0473     float zoomfactorToOriginal;
0474     reset();
0475 
0476     if (float(imgSize.width()) / float(imgSize.height()) > float(d->display_x) / float(d->display_y))
0477     {
0478         // Image touches right and left edge of window
0479 
0480         zoomfactorToOriginal = float(d->display_x) / imgSize.width();
0481     }
0482     else
0483     {
0484         // Image touches upper and lower edge of window
0485 
0486         zoomfactorToOriginal = float(d->display_y) / imgSize.height();
0487     }
0488 
0489     zoomfactorToOriginal *= d->displayWidget->devicePixelRatio();
0490 
0491     zoom(zoomfactorToOriginal, QPoint(d->display_x / 2, d->display_y / 2));
0492 }
0493 
0494 } // namespace DigikamGenericGLViewerPlugin