File indexing completed on 2024-04-28 04:20:50
0001 // SPDX-FileCopyrightText: 2003-2020 Jesper K. Pedersen <blackie@kde.org> 0002 // SPDX-FileCopyrightText: 2022 Johannes Zarl-Zierl <johannes@zarl-zierl.at> 0003 // 0004 // SPDX-License-Identifier: GPL-2.0-or-later 0005 0006 #include "ImagePreview.h" 0007 0008 #include "Logging.h" 0009 #include "ResizableFrame.h" 0010 0011 #include <DB/CategoryCollection.h> 0012 #include <DB/ImageDB.h> 0013 #include <ImageManager/AsyncLoader.h> 0014 #include <Utilities/ImageUtil.h> 0015 0016 #include <KLocalizedString> 0017 #include <KMessageBox> 0018 #include <QImageReader> 0019 #include <QMouseEvent> 0020 #include <QRubberBand> 0021 #include <QTimer> 0022 #include <math.h> 0023 0024 using namespace AnnotationDialog; 0025 0026 ImagePreview::ImagePreview(QWidget *parent) 0027 : QLabel(parent) 0028 , m_reloadTimer(new QTimer(this)) 0029 { 0030 setAlignment(Qt::AlignCenter); 0031 setMinimumSize(64, 64); 0032 // "the widget can make use of extra space, so it should get as much space as possible" 0033 setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); 0034 0035 m_reloadTimer->setSingleShot(true); 0036 connect(m_reloadTimer, &QTimer::timeout, this, &ImagePreview::resizeFinished); 0037 } 0038 0039 void ImagePreview::resizeEvent(QResizeEvent *ev) 0040 { 0041 qCDebug(AnnotationDialogLog) << "Resizing from" << ev->oldSize() << "to" << ev->size(); 0042 // during resizing, a scaled image will do 0043 QImage scaledImage = m_currentImage.getImage().scaled(size(), Qt::KeepAspectRatio); 0044 setPixmap(QPixmap::fromImage(scaledImage)); 0045 updateScaleFactors(); 0046 0047 // (re)start the timer to do a full reload 0048 m_reloadTimer->start(200); 0049 0050 QLabel::resizeEvent(ev); 0051 } 0052 0053 int ImagePreview::heightForWidth(int width) const 0054 { 0055 int height = width * m_aspectRatio; 0056 return height; 0057 } 0058 0059 QSize ImagePreview::sizeHint() const 0060 { 0061 QSize hint = m_info.size(); 0062 qCDebug(AnnotationDialogLog) << "Preview size hint is" << hint; 0063 return hint; 0064 } 0065 0066 void ImagePreview::rotate(int angle) 0067 { 0068 if (!m_info.isNull()) { 0069 m_currentImage.setAngle(m_info.angle()); 0070 m_info.rotate(angle, DB::RotateImageInfoOnly); 0071 } else { 0072 // Can this really happen? 0073 m_angle += angle; 0074 } 0075 0076 m_preloader.cancelPreload(); 0077 m_lastImage.reset(); 0078 reload(); 0079 0080 rotateAreas(angle); 0081 } 0082 0083 void ImagePreview::setImage(const DB::ImageInfo &info) 0084 { 0085 m_info = info; 0086 reload(); 0087 } 0088 0089 /** 0090 This method should only be used for the non-user images. Currently this includes 0091 two images: the search image and the configure several images at a time image. 0092 */ 0093 void ImagePreview::setImage(const QString &fileName) 0094 { 0095 m_fileName = fileName; 0096 m_info = DB::ImageInfo(); 0097 m_angle = 0; 0098 // Set the current angle that will be passed to m_lastImage 0099 m_currentImage.setAngle(m_info.angle()); 0100 reload(); 0101 } 0102 0103 void ImagePreview::reload() 0104 { 0105 m_aspectRatio = 1; 0106 if (!m_info.isNull()) { 0107 if (m_preloader.has(m_info.fileName(), m_info.angle())) { 0108 qCDebug(AnnotationDialogLog) << "reload(): set preloader image"; 0109 setCurrentImage(m_preloader.getImage()); 0110 } else if (m_lastImage.has(m_info.fileName(), m_info.angle())) { 0111 qCDebug(AnnotationDialogLog) << "reload(): set last image"; 0112 // don't pass by reference, the additional constructor is needed here 0113 // see setCurrentImage for the reason (where m_lastImage is changed...) 0114 setCurrentImage(QImage(m_lastImage.getImage())); 0115 } else { 0116 if (!m_currentImage.has(m_info.fileName(), m_info.angle())) { 0117 // erase old image to prevent a laggy feel, 0118 // but only erase old image if it is a different image 0119 // (otherwise we get flicker when resizing) 0120 setPixmap(QPixmap()); 0121 } 0122 qCDebug(AnnotationDialogLog) << "reload(): set another image"; 0123 ImageManager::AsyncLoader::instance()->stop(this); 0124 ImageManager::ImageRequest *request = new ImageManager::ImageRequest(m_info.fileName(), size(), m_info.angle(), this); 0125 request->setPriority(ImageManager::Viewer); 0126 ImageManager::AsyncLoader::instance()->load(request); 0127 } 0128 } else { 0129 qCDebug(AnnotationDialogLog) << "reload(): set image from file"; 0130 QImage img(m_fileName); 0131 img = rotateAndScale(img, width(), height(), m_angle); 0132 setPixmap(QPixmap::fromImage(img)); 0133 } 0134 } 0135 0136 int ImagePreview::angle() const 0137 { 0138 Q_ASSERT(!m_info.isNull()); 0139 return m_angle; 0140 } 0141 0142 QSize ImagePreview::getActualImageSize() 0143 { 0144 if (!m_info.size().isValid()) { 0145 // We have to fetch the size from the image 0146 m_info.setSize(QImageReader(m_info.fileName().absolute()).size()); 0147 m_aspectRatio = m_info.size().height() / m_info.size().width(); 0148 } 0149 return m_info.size(); 0150 } 0151 0152 void ImagePreview::setCurrentImage(const QImage &image) 0153 { 0154 // Cache the current image as the last image before changing it 0155 m_lastImage.set(m_currentImage); 0156 0157 m_currentImage.set(m_info.fileName(), image, m_info.angle()); 0158 setPixmap(QPixmap::fromImage(image)); 0159 0160 if (!m_anticipated.m_fileName.isNull()) 0161 m_preloader.preloadImage(m_anticipated.m_fileName, width(), height(), m_anticipated.m_angle); 0162 0163 updateScaleFactors(); 0164 0165 // Clear the full size image (if we have loaded one) 0166 m_fullSizeImage = QImage(); 0167 } 0168 0169 void ImagePreview::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) 0170 { 0171 const DB::FileName fileName = request->databaseFileName(); 0172 const bool loadedOK = request->loadedOK(); 0173 0174 if (loadedOK && !m_info.isNull()) { 0175 if (m_info.fileName() == fileName) 0176 setCurrentImage(image); 0177 } 0178 } 0179 0180 void ImagePreview::anticipate(DB::ImageInfo &info1) 0181 { 0182 // We cannot call m_preloader.preloadImage right here: 0183 // this function is called before reload(), so if we preload here, 0184 // the preloader will always be loading the image after the next image. 0185 m_anticipated.set(info1.fileName(), info1.angle()); 0186 } 0187 0188 ImagePreview::PreloadInfo::PreloadInfo() 0189 : m_angle(0) 0190 { 0191 } 0192 0193 void ImagePreview::PreloadInfo::set(const DB::FileName &fileName, int angle) 0194 { 0195 m_fileName = fileName; 0196 m_angle = angle; 0197 } 0198 0199 bool ImagePreview::PreviewImage::has(const DB::FileName &fileName, int angle) const 0200 { 0201 return fileName == m_fileName && !m_image.isNull() && angle == m_angle; 0202 } 0203 0204 QImage &ImagePreview::PreviewImage::getImage() 0205 { 0206 return m_image; 0207 } 0208 0209 void ImagePreview::PreviewImage::set(const DB::FileName &fileName, const QImage &image, int angle) 0210 { 0211 m_fileName = fileName; 0212 m_image = image; 0213 m_angle = angle; 0214 } 0215 0216 void ImagePreview::PreviewImage::set(const PreviewImage &other) 0217 { 0218 m_fileName = other.m_fileName; 0219 m_image = other.m_image; 0220 m_angle = other.m_angle; 0221 } 0222 0223 void ImagePreview::PreviewImage::setAngle(int angle) 0224 { 0225 m_angle = angle; 0226 } 0227 0228 void ImagePreview::PreviewImage::reset() 0229 { 0230 m_fileName = DB::FileName(); 0231 m_image = QImage(); 0232 } 0233 0234 void ImagePreview::PreviewLoader::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) 0235 { 0236 if (request->loadedOK()) { 0237 const DB::FileName fileName = request->databaseFileName(); 0238 set(fileName, image, request->angle()); 0239 } 0240 } 0241 0242 void ImagePreview::PreviewLoader::preloadImage(const DB::FileName &fileName, int width, int height, int angle) 0243 { 0244 // no need to worry about concurrent access: everything happens in the event loop thread 0245 reset(); 0246 ImageManager::AsyncLoader::instance()->stop(this); 0247 ImageManager::ImageRequest *request = new ImageManager::ImageRequest(fileName, QSize(width, height), angle, this); 0248 request->setPriority(ImageManager::ViewerPreload); 0249 ImageManager::AsyncLoader::instance()->load(request); 0250 } 0251 0252 void ImagePreview::PreviewLoader::cancelPreload() 0253 { 0254 reset(); 0255 ImageManager::AsyncLoader::instance()->stop(this); 0256 } 0257 0258 QImage ImagePreview::rotateAndScale(QImage img, int width, int height, int angle) const 0259 { 0260 if (angle != 0) { 0261 QTransform matrix; 0262 matrix.rotate(angle); 0263 img = img.transformed(matrix); 0264 } 0265 img = Utilities::scaleImage(img, QSize(width, height), Qt::KeepAspectRatio); 0266 return img; 0267 } 0268 0269 void ImagePreview::updateScaleFactors() 0270 { 0271 if (m_info.isNull()) 0272 return; // search mode 0273 0274 // Calculate a scale factor from the original image's size and it's current preview 0275 QSize actualSize = getActualImageSize(); 0276 // TODO(jzarl): remove once we don't care about Debian 11 anymore: 0277 #if QT_DEPRECATED_SINCE(5, 15) 0278 QSize previewSize = pixmap()->size(); 0279 #else 0280 QSize previewSize = pixmap().size(); 0281 #endif 0282 m_scaleWidth = double(actualSize.width()) / double(previewSize.width()); 0283 m_scaleHeight = double(actualSize.height()) / double(previewSize.height()); 0284 0285 // Calculate the min and max coordinates inside the preview widget 0286 int previewWidth = previewSize.width(); 0287 int previewHeight = previewSize.height(); 0288 int widgetWidth = this->frameGeometry().width(); 0289 int widgetHeight = this->frameGeometry().height(); 0290 m_minX = (widgetWidth - previewWidth) / 2; 0291 m_maxX = m_minX + previewWidth - 1; 0292 m_minY = (widgetHeight - previewHeight) / 2; 0293 m_maxY = m_minY + previewHeight - 1; 0294 0295 // Put all areas to their respective position on the preview 0296 remapAreas(); 0297 } 0298 0299 void ImagePreview::mousePressEvent(QMouseEvent *event) 0300 { 0301 if (!m_areaCreationEnabled) { 0302 return; 0303 } 0304 0305 if (event->button() & Qt::LeftButton) { 0306 if (!m_selectionRect) { 0307 m_selectionRect = new QRubberBand(QRubberBand::Rectangle, this); 0308 } 0309 0310 m_areaStart = event->pos(); 0311 if (m_areaStart.x() < m_minX || m_areaStart.x() > m_maxX || m_areaStart.y() < m_minY || m_areaStart.y() > m_maxY) { 0312 // Dragging started outside of the preview image 0313 return; 0314 } 0315 0316 m_selectionRect->setGeometry(QRect(m_areaStart, QSize())); 0317 m_selectionRect->show(); 0318 } 0319 } 0320 0321 void ImagePreview::mouseMoveEvent(QMouseEvent *event) 0322 { 0323 if (!m_areaCreationEnabled) { 0324 return; 0325 } 0326 0327 if (m_selectionRect && m_selectionRect->isVisible()) { 0328 m_currentPos = event->pos(); 0329 0330 // Restrict the coordinates to the preview images's size 0331 if (m_currentPos.x() < m_minX) { 0332 m_currentPos.setX(m_minX); 0333 } 0334 if (m_currentPos.y() < m_minY) { 0335 m_currentPos.setY(m_minY); 0336 } 0337 if (m_currentPos.x() > m_maxX) { 0338 m_currentPos.setX(m_maxX); 0339 } 0340 if (m_currentPos.y() > m_maxY) { 0341 m_currentPos.setY(m_maxY); 0342 } 0343 0344 m_selectionRect->setGeometry(QRect(m_areaStart, m_currentPos).normalized()); 0345 } 0346 } 0347 0348 void ImagePreview::mouseReleaseEvent(QMouseEvent *event) 0349 { 0350 if (!m_areaCreationEnabled) { 0351 return; 0352 } 0353 0354 if (event->button() & Qt::LeftButton && m_selectionRect->isVisible()) { 0355 m_areaEnd = event->pos(); 0356 processNewArea(); 0357 m_selectionRect->hide(); 0358 } 0359 } 0360 0361 QPixmap ImagePreview::grabAreaImage(QRect area) 0362 { 0363 return QPixmap::fromImage(m_currentImage.getImage().copy(area.left() - m_minX, 0364 area.top() - m_minY, 0365 area.width(), 0366 area.height())); 0367 } 0368 0369 QRect ImagePreview::areaPreviewToActual(QRect area) const 0370 { 0371 return QRect(QPoint(int(double(area.left() - m_minX) * m_scaleWidth), 0372 int(double(area.top() - m_minY) * m_scaleHeight)), 0373 QPoint(int(double(area.right() - m_minX) * m_scaleWidth), 0374 int(double(area.bottom() - m_minY) * m_scaleHeight))); 0375 } 0376 0377 QRect ImagePreview::areaActualToPreview(QRect area) const 0378 { 0379 return QRect(QPoint(int(double(area.left() / m_scaleWidth)) + m_minX, 0380 int(double(area.top() / m_scaleHeight)) + m_minY), 0381 QPoint(int(double(area.right() / m_scaleWidth)) + m_minX, 0382 int(double(area.bottom() / m_scaleHeight)) + m_minY)); 0383 } 0384 0385 void ImagePreview::createNewArea(QRect geometry, QRect actualGeometry) 0386 { 0387 // Create a ResizableFrame (cleaned up in Dialog::tidyAreas()) 0388 ResizableFrame *newArea = new ResizableFrame(this); 0389 0390 newArea->setGeometry(geometry); 0391 // Be sure not to create an invisible area 0392 newArea->checkGeometry(); 0393 // In case the geometry has been changed by checkGeometry() 0394 actualGeometry = areaPreviewToActual(newArea->geometry()); 0395 // Store the coordinates on the real image (not on the preview) 0396 newArea->setActualCoordinates(actualGeometry); 0397 Q_EMIT areaCreated(newArea); 0398 0399 newArea->show(); 0400 newArea->showContextMenu(); 0401 } 0402 0403 void ImagePreview::processNewArea() 0404 { 0405 if (m_areaStart == m_areaEnd) { 0406 // It was just a click, no area has been dragged 0407 return; 0408 } 0409 0410 QRect newAreaPreview = QRect(m_areaStart, m_currentPos).normalized(); 0411 createNewArea(newAreaPreview, areaPreviewToActual(newAreaPreview)); 0412 } 0413 0414 void ImagePreview::remapAreas() 0415 { 0416 const auto allAreas = this->findChildren<ResizableFrame *>(); 0417 for (ResizableFrame *area : allAreas) { 0418 area->setGeometry(areaActualToPreview(area->actualCoordinates())); 0419 } 0420 } 0421 0422 QRect ImagePreview::rotateArea(QRect originalAreaGeometry, int angle) 0423 { 0424 // This is the current state of the image. We need the state before, so ... 0425 QSize unrotatedOriginalImageSize = getActualImageSize(); 0426 // ... un-rotate it 0427 unrotatedOriginalImageSize.transpose(); 0428 0429 QRect rotatedAreaGeometry; 0430 rotatedAreaGeometry.setWidth(originalAreaGeometry.height()); 0431 rotatedAreaGeometry.setHeight(originalAreaGeometry.width()); 0432 0433 if (angle == 90) { 0434 rotatedAreaGeometry.moveTo( 0435 unrotatedOriginalImageSize.height() - (originalAreaGeometry.height() + originalAreaGeometry.y()), 0436 originalAreaGeometry.x()); 0437 } else { 0438 rotatedAreaGeometry.moveTo( 0439 originalAreaGeometry.y(), 0440 unrotatedOriginalImageSize.width() - (originalAreaGeometry.width() + originalAreaGeometry.x())); 0441 } 0442 0443 return rotatedAreaGeometry; 0444 } 0445 0446 void ImagePreview::rotateAreas(int angle) 0447 { 0448 // Map all areas to their respective coordinates on the rotated actual image 0449 const auto allAreas = this->findChildren<ResizableFrame *>(); 0450 for (ResizableFrame *area : allAreas) { 0451 area->setActualCoordinates(rotateArea(area->actualCoordinates(), angle)); 0452 } 0453 } 0454 0455 void ImagePreview::resizeFinished() 0456 { 0457 qCDebug(AnnotationDialogLog) << "Reloading image after resize"; 0458 m_preloader.cancelPreload(); 0459 m_lastImage.reset(); 0460 reload(); 0461 } 0462 0463 QRect ImagePreview::minMaxAreaPreview() const 0464 { 0465 return QRect(m_minX, m_minY, m_maxX, m_maxY); 0466 } 0467 0468 void ImagePreview::createTaggedArea(QString category, QString tag, QRect geometry, bool showArea) 0469 { 0470 // Create a ResizableFrame (cleaned up in Dialog::tidyAreas()) 0471 ResizableFrame *newArea = new ResizableFrame(this); 0472 0473 Q_EMIT areaCreated(newArea); 0474 0475 newArea->setGeometry(areaActualToPreview(geometry)); 0476 newArea->setActualCoordinates(geometry); 0477 newArea->setTagData(category, tag, AutomatedChange); 0478 newArea->setVisible(showArea); 0479 } 0480 0481 void ImagePreview::setAreaCreationEnabled(bool state) 0482 { 0483 m_areaCreationEnabled = state; 0484 } 0485 0486 // Currently only called when face detection/recognition is used 0487 void ImagePreview::fetchFullSizeImage() 0488 { 0489 if (m_fullSizeImage.isNull()) { 0490 m_fullSizeImage = QImage(m_info.fileName().absolute()); 0491 } 0492 0493 if (m_angle != m_info.angle()) { 0494 QTransform matrix; 0495 matrix.rotate(m_info.angle()); 0496 m_fullSizeImage = m_fullSizeImage.transformed(matrix); 0497 } 0498 } 0499 0500 void ImagePreview::acceptProposedTag(QPair<QString, QString> tagData, ResizableFrame *area) 0501 { 0502 // Be sure that we do have the category the proposed tag belongs to 0503 bool categoryFound = false; 0504 0505 // Any warnings should only happen when the recognition database is e. g. copied from another 0506 // database location or has been changed outside of KPA. Anyways, this m_can_ happen, so we 0507 // have to handle it. 0508 0509 QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); 0510 for (QList<DB::CategoryPtr>::ConstIterator categoryIt = categories.constBegin(); 0511 categoryIt != categories.constEnd(); ++categoryIt) { 0512 if ((*categoryIt)->name() == tagData.first) { 0513 if (!(*categoryIt)->positionable()) { 0514 KMessageBox::error(this, i18n("<p><b>Can't associate tag \"%2\"</b></p>" 0515 "<p>The category \"%1\" the tag \"%2\" belongs to is not positionable.</p>" 0516 "<p>If you want to use this tag, change this in the settings dialog. " 0517 "If this tag shouldn't be in the recognition database anymore, it can " 0518 "be deleted in the settings.</p>", 0519 tagData.first, tagData.second)); 0520 return; 0521 } 0522 categoryFound = true; 0523 break; 0524 } 0525 } 0526 0527 if (!categoryFound) { 0528 KMessageBox::error(this, i18n("<p><b>Can't associate tag \"%2\"</b></p>" 0529 "<p>The category \"%1\" the tag \"%2\" belongs to does not exist.</p>" 0530 "<p>If you want to use this tag, add this category and mark it as positionable. " 0531 "If this tag shouldn't be in the recognition database anymore, it can " 0532 "be deleted in the settings dialog.</p>", 0533 tagData.first, tagData.second)); 0534 return; 0535 } 0536 0537 // Tell all ListSelects that we accepted a proposed tag, so that the ListSelect 0538 // holding the respective category can ensure that the tag is checked 0539 Q_EMIT proposedTagSelected(tagData.first, tagData.second); 0540 0541 // Associate the area with the proposed tag 0542 area->setTagData(tagData.first, tagData.second); 0543 } 0544 0545 bool ImagePreview::fuzzyAreaExists(QList<QRect> &existingAreas, QRect area) 0546 { 0547 float maximumDeviation; 0548 for (int i = 0; i < existingAreas.size(); ++i) { 0549 // maximumDeviation is 15% of the mean value of the width and height of each area 0550 maximumDeviation = float(existingAreas.at(i).width() + existingAreas.at(i).height()) * 0.075; 0551 if ( 0552 distance(existingAreas.at(i).topLeft(), area.topLeft()) < maximumDeviation && distance(existingAreas.at(i).topRight(), area.topRight()) < maximumDeviation && distance(existingAreas.at(i).bottomLeft(), area.bottomLeft()) < maximumDeviation && distance(existingAreas.at(i).bottomRight(), area.bottomRight()) < maximumDeviation) { 0553 return true; 0554 } 0555 } 0556 0557 return false; 0558 } 0559 0560 float ImagePreview::distance(QPoint point1, QPoint point2) 0561 { 0562 QPoint difference = point1 - point2; 0563 return sqrt(pow(difference.x(), 2) + pow(difference.y(), 2)); 0564 } 0565 0566 // vi:expandtab:tabstop=4 shiftwidth=4: 0567 0568 #include "moc_ImagePreview.cpp"