File indexing completed on 2024-04-28 15:40:11

0001 /* SPDX-FileCopyrightText: 2010-2020 The KPhotoAlbum Development Team
0002 
0003    SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "AutoStackImages.h"
0007 
0008 #include "Window.h"
0009 
0010 #include <DB/FileInfo.h>
0011 #include <DB/ImageDB.h>
0012 #include <DB/ImageDate.h>
0013 #include <DB/ImageInfo.h>
0014 #include <DB/MD5Map.h>
0015 #include <Utilities/FileUtil.h>
0016 #include <Utilities/ShowBusyCursor.h>
0017 #include <kpabase/SettingsData.h>
0018 
0019 #include <KLocalizedString>
0020 #include <QApplication>
0021 #include <QCheckBox>
0022 #include <QDialogButtonBox>
0023 #include <QEventLoop>
0024 #include <QGroupBox>
0025 #include <QLabel>
0026 #include <QLayout>
0027 #include <QPushButton>
0028 #include <QRadioButton>
0029 #include <QSpinBox>
0030 #include <QVBoxLayout>
0031 
0032 using namespace MainWindow;
0033 
0034 AutoStackImages::AutoStackImages(QWidget *parent, const DB::FileNameList &list)
0035     : QDialog(parent)
0036     , m_list(list)
0037 {
0038     setWindowTitle(i18nc("@title:window", "Automatically Stack Images"));
0039 
0040     QWidget *top = new QWidget;
0041     QVBoxLayout *lay1 = new QVBoxLayout(top);
0042     setLayout(lay1);
0043 
0044     QWidget *containerMd5 = new QWidget(this);
0045     lay1->addWidget(containerMd5);
0046     QHBoxLayout *hlayMd5 = new QHBoxLayout(containerMd5);
0047 
0048     m_matchingMD5 = new QCheckBox(i18n("Stack images with identical MD5 sum"));
0049     m_matchingMD5->setChecked(false);
0050     hlayMd5->addWidget(m_matchingMD5);
0051 
0052     QWidget *containerFile = new QWidget(this);
0053     lay1->addWidget(containerFile);
0054     QHBoxLayout *hlayFile = new QHBoxLayout(containerFile);
0055 
0056     m_matchingFile = new QCheckBox(i18n("Stack images based on file version detection"));
0057     m_matchingFile->setChecked(true);
0058     hlayFile->addWidget(m_matchingFile);
0059 
0060     m_origTop = new QCheckBox(i18n("Original to top"));
0061     m_origTop->setChecked(false);
0062     hlayFile->addWidget(m_origTop);
0063 
0064     QWidget *containerContinuous = new QWidget(this);
0065     lay1->addWidget(containerContinuous);
0066     QHBoxLayout *hlayContinuous = new QHBoxLayout(containerContinuous);
0067 
0068     // Would minutes not be a more sane time unit here? (schwarzer)
0069     // jzarl: this feature is used to group "burst" images - usually they are taken within a few seconds.
0070     // a typical value would be somewhere between 2 and 20 seconds
0071     m_continuousShooting = new QCheckBox(i18nc("Label for a QSpinbox that selects a number of seconds.", "Stack images created within the following time-frame:"));
0072     m_continuousShooting->setChecked(false);
0073     hlayContinuous->addWidget(m_continuousShooting);
0074 
0075     m_continuousThreshold = new QSpinBox;
0076     m_continuousThreshold->setRange(1, 999);
0077     m_continuousThreshold->setSingleStep(1);
0078     m_continuousThreshold->setValue(2);
0079     m_continuousThreshold->setSuffix(i18nc("Unit suffix on a QSpinBox; note the space at the beginning.", " seconds"));
0080     hlayContinuous->addWidget(m_continuousThreshold);
0081 
0082     QGroupBox *grpOptions = new QGroupBox(i18n("AutoStacking Options"));
0083     QVBoxLayout *grpLayOptions = new QVBoxLayout(grpOptions);
0084     lay1->addWidget(grpOptions);
0085 
0086     m_autostackDefault = new QRadioButton(i18n("Include matching image to appropriate stack (if one exists)"));
0087     m_autostackDefault->setChecked(true);
0088     grpLayOptions->addWidget(m_autostackDefault);
0089 
0090     m_autostackUnstack = new QRadioButton(i18n("Unstack images from their current stack and create new one for the matches"));
0091     m_autostackUnstack->setChecked(false);
0092     grpLayOptions->addWidget(m_autostackUnstack);
0093 
0094     m_autostackSkip = new QRadioButton(i18n("Skip images that are already in a stack"));
0095     m_autostackSkip->setChecked(false);
0096     grpLayOptions->addWidget(m_autostackSkip);
0097 
0098     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
0099     QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
0100     okButton->setDefault(true);
0101     okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0102     connect(buttonBox, &QDialogButtonBox::accepted, this, &AutoStackImages::accept);
0103     connect(buttonBox, &QDialogButtonBox::rejected, this, &AutoStackImages::reject);
0104     lay1->addWidget(buttonBox);
0105 }
0106 
0107 /*
0108  * This function searches for images with matching MD5 sums
0109  * Matches are automatically stacked
0110  */
0111 
0112 void AutoStackImages::matchingMD5(DB::FileNameList &toBeShown)
0113 {
0114     QMap<DB::MD5, DB::FileNameList> tostack;
0115     DB::FileNameList showIfStacked;
0116 
0117     // Stacking all images that have the same MD5 sum
0118     // First make a map of MD5 sums with corresponding images
0119     for (const DB::FileName &fileName : qAsConst(m_list)) {
0120         const auto info = DB::ImageDB::instance()->info(fileName);
0121         DB::MD5 sum = info->MD5Sum();
0122         if (DB::ImageDB::instance()->md5Map()->contains(sum)) {
0123             if (tostack[sum].isEmpty())
0124                 tostack.insert(sum, DB::FileNameList() << fileName);
0125             else
0126                 tostack[sum].append(fileName);
0127         }
0128     }
0129 
0130     // Then add images to stack (depending on configuration options)
0131     for (QMap<DB::MD5, DB::FileNameList>::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it) {
0132         if (tostack[it.key()].count() > 1) {
0133             DB::FileNameList stack;
0134             for (int i = 0; i < tostack[it.key()].count(); ++i) {
0135                 if (!DB::ImageDB::instance()->getStackFor(tostack[it.key()][i]).isEmpty()) {
0136                     if (m_autostackUnstack->isChecked())
0137                         DB::ImageDB::instance()->unstack(DB::FileNameList() << tostack[it.key()][i]);
0138                     else if (m_autostackSkip->isChecked())
0139                         continue;
0140                 }
0141 
0142                 showIfStacked.append(tostack[it.key()][i]);
0143                 stack.append(tostack[it.key()][i]);
0144             }
0145             if (stack.size() > 1) {
0146 
0147                 for (const DB::FileName &a : qAsConst(showIfStacked)) {
0148                     if (!DB::ImageDB::instance()->getStackFor(a).isEmpty()) {
0149                         const auto stackedImages = DB::ImageDB::instance()->getStackFor(a);
0150                         for (const DB::FileName &b : stackedImages)
0151                             toBeShown.append(b);
0152                     } else
0153                         toBeShown.append(a);
0154                 }
0155                 DB::ImageDB::instance()->stack(stack);
0156             }
0157             showIfStacked.clear();
0158         }
0159     }
0160 }
0161 
0162 /*
0163  * This function searches for images based on file version detection configuration.
0164  * Images that are detected to be versions of same file are stacked together.
0165  */
0166 
0167 void AutoStackImages::matchingFile(DB::FileNameList &toBeShown)
0168 {
0169     QMap<DB::MD5, DB::FileNameList> tostack;
0170     DB::FileNameList showIfStacked;
0171     QString modifiedFileCompString;
0172     QRegExp modifiedFileComponent;
0173     QStringList originalFileComponents;
0174 
0175     modifiedFileCompString = Settings::SettingsData::instance()->modifiedFileComponent();
0176     modifiedFileComponent = QRegExp(modifiedFileCompString);
0177 
0178     originalFileComponents << Settings::SettingsData::instance()->originalFileComponent();
0179     originalFileComponents = originalFileComponents.at(0).split(QString::fromLatin1(";"));
0180 
0181     // Stacking all images based on file version detection
0182     // First round prepares the stacking
0183     for (const DB::FileName &fileName : qAsConst(m_list)) {
0184         if (modifiedFileCompString.length() >= 0 && fileName.relative().contains(modifiedFileComponent)) {
0185 
0186             for (QStringList::const_iterator it = originalFileComponents.constBegin();
0187                  it != originalFileComponents.constEnd(); ++it) {
0188                 QString tmp = fileName.relative();
0189                 tmp.replace(modifiedFileComponent, (*it));
0190                 DB::FileName originalFileName = DB::FileName::fromRelativePath(tmp);
0191 
0192                 if (originalFileName != fileName && m_list.contains(originalFileName)) {
0193                     const auto info = DB::ImageDB::instance()->info(fileName);
0194                     DB::MD5 sum = info->MD5Sum();
0195                     if (tostack[sum].isEmpty()) {
0196                         if (m_origTop->isChecked()) {
0197                             tostack.insert(sum, DB::FileNameList() << originalFileName);
0198                             tostack[sum].append(fileName);
0199                         } else {
0200                             tostack.insert(sum, DB::FileNameList() << fileName);
0201                             tostack[sum].append(originalFileName);
0202                         }
0203                     } else
0204                         tostack[sum].append(fileName);
0205                     break;
0206                 }
0207             }
0208         }
0209     }
0210 
0211     // Then add images to stack (depending on configuration options)
0212     for (QMap<DB::MD5, DB::FileNameList>::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it) {
0213         if (tostack[it.key()].count() > 1) {
0214             DB::FileNameList stack;
0215             for (int i = 0; i < tostack[it.key()].count(); ++i) {
0216                 if (!DB::ImageDB::instance()->getStackFor(tostack[it.key()][i]).isEmpty()) {
0217                     if (m_autostackUnstack->isChecked())
0218                         DB::ImageDB::instance()->unstack(DB::FileNameList() << tostack[it.key()][i]);
0219                     else if (m_autostackSkip->isChecked())
0220                         continue;
0221                 }
0222 
0223                 showIfStacked.append(tostack[it.key()][i]);
0224                 stack.append(tostack[it.key()][i]);
0225             }
0226             if (stack.size() > 1) {
0227 
0228                 for (const DB::FileName &a : qAsConst(showIfStacked)) {
0229                     if (!DB::ImageDB::instance()->getStackFor(a).isEmpty()) {
0230                         const auto stackedImages = DB::ImageDB::instance()->getStackFor(a);
0231                         for (const DB::FileName &b : stackedImages)
0232                             toBeShown.append(b);
0233                     } else
0234                         toBeShown.append(a);
0235                 }
0236                 DB::ImageDB::instance()->stack(stack);
0237             }
0238             showIfStacked.clear();
0239         }
0240     }
0241 }
0242 
0243 /**
0244  * This function searches for images that are shot within specified time frame
0245  */
0246 void AutoStackImages::continuousShooting(DB::FileNameList &toBeShown)
0247 {
0248     DB::ImageInfoPtr prev;
0249     for (const DB::FileName &fileName : qAsConst(m_list)) {
0250         const auto info = DB::ImageDB::instance()->info(fileName);
0251         // Skipping images that do not have exact time stamp
0252         if (info->date().start() != info->date().end())
0253             continue;
0254         if (prev && (prev->date().start().secsTo(info->date().start()) < m_continuousThreshold->value())) {
0255             DB::FileNameList stack;
0256 
0257             if (!DB::ImageDB::instance()->getStackFor(prev->fileName()).isEmpty()) {
0258                 if (m_autostackUnstack->isChecked())
0259                     DB::ImageDB::instance()->unstack(DB::FileNameList() << prev->fileName());
0260                 else if (m_autostackSkip->isChecked())
0261                     continue;
0262             }
0263 
0264             if (!DB::ImageDB::instance()->getStackFor(fileName).isEmpty()) {
0265                 if (m_autostackUnstack->isChecked())
0266                     DB::ImageDB::instance()->unstack(DB::FileNameList() << fileName);
0267                 else if (m_autostackSkip->isChecked())
0268                     continue;
0269             }
0270 
0271             stack.append(prev->fileName());
0272             stack.append(info->fileName());
0273             if (!toBeShown.isEmpty()) {
0274                 if (toBeShown.at(toBeShown.size() - 1) != prev->fileName())
0275                     toBeShown.append(prev->fileName());
0276             } else {
0277                 // if this is first insert, we have to include also the stacked images from previuous image
0278                 if (!DB::ImageDB::instance()->getStackFor(info->fileName()).isEmpty()) {
0279                     const auto stackedImages = DB::ImageDB::instance()->getStackFor(prev->fileName());
0280                     for (const DB::FileName &a : stackedImages)
0281                         toBeShown.append(a);
0282                 } else
0283                     toBeShown.append(prev->fileName());
0284             }
0285             // Inserting stacked images from the current image
0286             if (!DB::ImageDB::instance()->getStackFor(info->fileName()).isEmpty()) {
0287                 const auto stackedImages = DB::ImageDB::instance()->getStackFor(fileName);
0288                 for (const DB::FileName &a : stackedImages)
0289                     toBeShown.append(a);
0290             } else
0291                 toBeShown.append(info->fileName());
0292             DB::ImageDB::instance()->stack(stack);
0293         }
0294 
0295         prev = info;
0296     }
0297 }
0298 
0299 void AutoStackImages::accept()
0300 {
0301     QDialog::accept();
0302     Utilities::ShowBusyCursor dummy;
0303     DB::FileNameList toBeShown;
0304 
0305     if (m_matchingMD5->isChecked())
0306         matchingMD5(toBeShown);
0307     if (m_matchingFile->isChecked())
0308         matchingFile(toBeShown);
0309     if (m_continuousShooting->isChecked())
0310         continuousShooting(toBeShown);
0311 
0312     MainWindow::Window::theMainWindow()->showThumbNails(toBeShown);
0313 }
0314 
0315 // vi:expandtab:tabstop=4 shiftwidth=4:
0316 
0317 #include "moc_AutoStackImages.cpp"