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