File indexing completed on 2024-04-28 15:39:57

0001 // SPDX-FileCopyrightText: 2003 Simon Hausmann <hausmann@kde.org>
0002 // SPDX-FileCopyrightText: 2003-2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0003 // SPDX-FileCopyrightText: 2004 Malcolm Hunter <malcolm.hunter@gmx.co.uk>
0004 // SPDX-FileCopyrightText: 2004-2005 Andrew Coles <andrew.i.coles@googlemail.com>
0005 // SPDX-FileCopyrightText: 2004-2005 Stephan Binner <binner@kde.org>
0006 // SPDX-FileCopyrightText: 2005 Steffen Hansen <hansen@kde.org>
0007 // SPDX-FileCopyrightText: 2006-2010 Tuomas Suutari <tuomas@nepnep.net>
0008 // SPDX-FileCopyrightText: 2007 Dirk Mueller <mueller@kde.org>
0009 // SPDX-FileCopyrightText: 2007-2010 Jan Kundrát <jkt@flaska.net>
0010 // SPDX-FileCopyrightText: 2008-2009 Henner Zeller <h.zeller@acm.org>
0011 // SPDX-FileCopyrightText: 2009 Laurent Montel <montel@kde.org>
0012 // SPDX-FileCopyrightText: 2012 Miika Turkia <miika.turkia@gmail.com>
0013 // SPDX-FileCopyrightText: 2012-2015 Andreas Neustifter <andreas.neustifter@gmail.com>
0014 // SPDX-FileCopyrightText: 2013-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0015 // SPDX-FileCopyrightText: 2014-2022 Tobias Leupold <tl@stonemx.de>
0016 // SPDX-FileCopyrightText: 2017-2020 Robert Krawitz <rlk@alum.mit.edu>
0017 //
0018 // SPDX-License-Identifier: GPL-2.0-or-later
0019 
0020 #include "ImageDB.h"
0021 
0022 #include "CategoryCollection.h"
0023 #include "MediaCount.h"
0024 #include "TagInfo.h"
0025 
0026 #include <DB/GroupCounter.h>
0027 #include <DB/XML/FileReader.h>
0028 #include <DB/XML/FileWriter.h>
0029 #include <Utilities/FastDateTime.h>
0030 #include <kpabase/FileExtensions.h>
0031 #include <kpabase/FileName.h>
0032 #include <kpabase/Logging.h>
0033 #include <kpabase/SettingsData.h>
0034 #include <kpabase/UIDelegate.h>
0035 
0036 #include <KLocalizedString>
0037 #include <QApplication>
0038 #include <QElapsedTimer>
0039 #include <QFileInfo>
0040 #include <QMutex>
0041 #include <QProgressDialog>
0042 
0043 using namespace DB;
0044 
0045 using Utilities::StringSet;
0046 
0047 namespace
0048 {
0049 void checkForBackupFile(const QString &fileName, DB::UIDelegate &ui)
0050 {
0051     QString backupName = QFileInfo(fileName).absolutePath() + QString::fromLatin1("/.#") + QFileInfo(fileName).fileName();
0052     QFileInfo backUpFile(backupName);
0053     QFileInfo indexFile(fileName);
0054 
0055     if (!backUpFile.exists() || indexFile.lastModified() > backUpFile.lastModified() || backUpFile.size() == 0)
0056         return;
0057 
0058     const long backupSizeKB = backUpFile.size() >> 10;
0059     const DB::UserFeedback choice = ui.questionYesNo(
0060         DB::LogMessage { DBLog(), QString::fromUtf8("Autosave file found: '%1', %2KB.").arg(backupName).arg(backupSizeKB) },
0061         i18n("Autosave file '%1' exists (size %3 KB) and is newer than '%2'. "
0062              "Should the autosave file be used?",
0063              backupName, fileName, backupSizeKB),
0064         i18n("Found Autosave File"));
0065 
0066     if (choice == DB::UserFeedback::Confirm) {
0067         qCInfo(DBLog) << "Using autosave file:" << backupName;
0068         QFile in(backupName);
0069         if (in.open(QIODevice::ReadOnly)) {
0070             QFile out(fileName);
0071             if (out.open(QIODevice::WriteOnly)) {
0072                 char data[1024];
0073                 int len;
0074                 while ((len = in.read(data, 1024)))
0075                     out.write(data, len);
0076             }
0077         }
0078     }
0079 }
0080 
0081 // During profiling of loading, I found that a significant amount of time was spent in Utilities::FastDateTime::fromString.
0082 // Reviewing the code, I fount that it did a lot of extra checks we don't need (like checking if the string have
0083 // timezone information (which they won't in KPA), this function is a replacement that is faster than the original.
0084 Utilities::FastDateTime dateTimeFromString(const QString &str)
0085 {
0086     // Caching the last used date/time string will help for photographers
0087     // who frequently take bursts.
0088     static QString s_lastDateTimeString;
0089     static Utilities::FastDateTime s_lastDateTime;
0090     static QMutex s_lastDateTimeLocker;
0091     QMutexLocker dummy(&s_lastDateTimeLocker);
0092     static const QChar T = QChar::fromLatin1('T');
0093     if (str != s_lastDateTimeString) {
0094         if (str[10] == T)
0095             s_lastDateTime = QDateTime(QDate::fromString(str.left(10), Qt::ISODate), QTime::fromString(str.mid(11), Qt::ISODate));
0096         else
0097             s_lastDateTime = QDateTime::fromString(str, Qt::ISODate);
0098         s_lastDateTimeString = str;
0099     }
0100     return s_lastDateTime;
0101 }
0102 
0103 } // namespace
0104 
0105 bool ImageDB::s_anyImageWithEmptySize = false;
0106 ImageDB *ImageDB::s_instance = nullptr;
0107 
0108 ImageDB *DB::ImageDB::instance()
0109 {
0110     if (s_instance == nullptr)
0111         exit(0); // Either we are closing down or ImageDB::instance was called before ImageDB::setup
0112     return s_instance;
0113 }
0114 
0115 void ImageDB::setupXMLDB(const QString &configFile, UIDelegate &delegate)
0116 {
0117     if (s_instance)
0118         qFatal("ImageDB::setupXMLDB: Setup must be called only once.");
0119     s_instance = new ImageDB(configFile, delegate);
0120     connectSlots();
0121 }
0122 
0123 void ImageDB::deleteInstance()
0124 {
0125     delete s_instance;
0126     s_instance = nullptr;
0127 }
0128 
0129 void ImageDB::connectSlots()
0130 {
0131     connect(Settings::SettingsData::instance(), QOverload<bool, bool>::of(&Settings::SettingsData::locked), s_instance, &ImageDB::lockDB);
0132     connect(&s_instance->memberMap(), &MemberMap::dirty, s_instance, &ImageDB::markDirty);
0133 }
0134 
0135 void ImageDB::forceUpdate(const ImageInfoList &images)
0136 {
0137     // FIXME: merge stack information
0138     DB::ImageInfoList newImages = images.sort();
0139     if (m_images.count() == 0) {
0140         // case 1: The existing imagelist is empty.
0141         for (const DB::ImageInfoPtr &imageInfo : qAsConst(newImages))
0142             m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo);
0143         m_images = newImages;
0144     } else if (newImages.count() == 0) {
0145         // case 2: No images to merge in - that's easy ;-)
0146         return;
0147     } else if (newImages.first()->date().start() > m_images.last()->date().start()) {
0148         // case 2: The new list is later than the existsing
0149         for (const DB::ImageInfoPtr &imageInfo : qAsConst(newImages))
0150             m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo);
0151         m_images.appendList(newImages);
0152     } else if (m_images.isSorted()) {
0153         // case 3: The lists overlaps, and the existsing list is sorted
0154         for (const DB::ImageInfoPtr &imageInfo : qAsConst(newImages))
0155             m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo);
0156         m_images.mergeIn(newImages);
0157     } else {
0158         // case 4: The lists overlaps, and the existsing list is not sorted in the overlapping range.
0159         for (const DB::ImageInfoPtr &imageInfo : qAsConst(newImages))
0160             m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo);
0161         m_images.appendList(newImages);
0162     }
0163 }
0164 
0165 QString ImageDB::NONE()
0166 {
0167     static QString none = QString::fromLatin1("**NONE**");
0168     return none;
0169 }
0170 
0171 DB::FileNameList ImageDB::currentScope(bool requireOnDisk) const
0172 {
0173     return search(m_currentScope, requireOnDisk).files();
0174 }
0175 
0176 void ImageDB::renameItem(Category *category, const QString &oldName, const QString &newName)
0177 {
0178     for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) {
0179         (*it)->renameItem(category->name(), oldName, newName);
0180     }
0181 }
0182 
0183 void ImageDB::deleteItem(Category *category, const QString &value)
0184 {
0185     for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) {
0186         (*it)->removeCategoryInfo(category->name(), value);
0187     }
0188 }
0189 
0190 void ImageDB::lockDB(bool lock, bool exclude)
0191 {
0192     auto lockData = Settings::SettingsData::instance()->currentLock();
0193     DB::ImageSearchInfo info = DB::ImageSearchInfo::loadLock(lockData);
0194     for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) {
0195         if (lock) {
0196             bool match = info.match(*it);
0197             if (!exclude)
0198                 match = !match;
0199             (*it)->setLocked(match);
0200         } else
0201             (*it)->setLocked(false);
0202     }
0203 }
0204 
0205 void ImageDB::markDirty()
0206 {
0207     Q_EMIT dirty();
0208 }
0209 
0210 void ImageDB::setDateRange(const ImageDate &range, bool includeFuzzyCounts)
0211 {
0212     m_selectionRange = range;
0213     m_includeFuzzyCounts = includeFuzzyCounts;
0214 }
0215 
0216 void ImageDB::clearDateRange()
0217 {
0218     m_selectionRange = ImageDate();
0219 }
0220 
0221 UIDelegate &DB::ImageDB::uiDelegate() const
0222 {
0223     return m_UI;
0224 }
0225 
0226 ImageDB::ImageDB(const QString &configFile, UIDelegate &delegate)
0227     : m_UI(delegate)
0228     , m_exifDB(std::make_unique<Exif::Database>(::Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("/exif-info.db"), delegate))
0229     , m_includeFuzzyCounts(false)
0230     , m_untaggedTag()
0231     , m_fileName(configFile)
0232 {
0233     checkForBackupFile(configFile, uiDelegate());
0234     DB::FileReader reader(this);
0235     reader.read(configFile);
0236     m_nextStackId = reader.nextStackId();
0237 
0238     // if reading an index.xml file version < 9, the untaggedTag is stored in the settings, not the database
0239     if (!untaggedCategoryFeatureConfigured()) {
0240         const auto untaggedCategory = Settings::SettingsData::instance()->untaggedCategory();
0241         const auto untaggedTag = Settings::SettingsData::instance()->untaggedTag();
0242         auto untaggedCategoryPtr = categoryCollection()->categoryForName(untaggedCategory);
0243         if (untaggedCategoryPtr) {
0244             if (!untaggedCategoryPtr->items().contains(untaggedTag)) {
0245                 qCInfo(DBLog) << "Adding 'untagged' tag to database:" << untaggedTag;
0246                 untaggedCategoryPtr->addItem(untaggedTag);
0247             }
0248             qCInfo(DBLog) << "No designated 'untagged' tag found in database. Using value configured in settings.";
0249             setUntaggedTag(untaggedCategoryPtr->itemForName(untaggedTag));
0250         } else {
0251             qCWarning(DBLog) << "No designated 'untagged' tag found in database and no viable value configured in settings.";
0252         }
0253     }
0254 
0255     connect(categoryCollection(), &DB::CategoryCollection::itemRemoved,
0256             this, &ImageDB::deleteItem);
0257     connect(categoryCollection(), &DB::CategoryCollection::itemRenamed,
0258             this, &ImageDB::renameItem);
0259 
0260     connect(categoryCollection(), &DB::CategoryCollection::itemRemoved,
0261             &m_members, &DB::MemberMap::deleteItem);
0262     connect(categoryCollection(), &DB::CategoryCollection::itemRenamed,
0263             &m_members, &DB::MemberMap::renameItem);
0264     connect(categoryCollection(), &DB::CategoryCollection::categoryRemoved,
0265             &m_members, &DB::MemberMap::deleteCategory);
0266 }
0267 
0268 bool ImageDB::rangeInclude(ImageInfoPtr info) const
0269 {
0270     if (m_selectionRange.start().isNull())
0271         return true;
0272 
0273     using MatchType = DB::ImageDate::MatchType;
0274     MatchType tp = info->date().isIncludedIn(m_selectionRange);
0275     if (m_includeFuzzyCounts)
0276         return (tp == MatchType::IsContained || tp == MatchType::Overlap);
0277     else
0278         return (tp == MatchType::IsContained);
0279 }
0280 
0281 // Remove all the images from the database that match the given selection and
0282 // return that sublist.
0283 // This returns the selected and erased images in the order in which they appear
0284 // in the image list itself.
0285 ImageInfoList ImageDB::takeImagesFromSelection(const FileNameList &selection)
0286 {
0287     DB::ImageInfoList result;
0288     if (selection.isEmpty())
0289         return result;
0290 
0291     // iterate over all images (expensive!!)
0292     for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); /**/) {
0293         const DB::FileName imagefile = (*it)->fileName();
0294         DB::FileNameList::const_iterator si = selection.begin();
0295         // for each image, iterate over selection, break on match
0296         for (/**/; si != selection.end(); ++si) {
0297             const DB::FileName file = *si;
0298             if (imagefile == file) {
0299                 break;
0300             }
0301         }
0302         // if image is not in selection, simply advance to next, if not add to result and erase
0303         if (si == selection.end()) {
0304             ++it;
0305         } else {
0306             result << *it;
0307             m_imageCache.remove((*it)->fileName().absolute());
0308             it = m_images.erase(it);
0309         }
0310         // if all images from selection are in result (size of lists is equal) break.
0311         if (result.size() == selection.size())
0312             break;
0313     }
0314 
0315     return result;
0316 }
0317 
0318 void ImageDB::insertList(const FileName &fileName, const ImageInfoList &list, bool after)
0319 {
0320     DB::ImageInfoListIterator imageIt = m_images.begin();
0321     for (; imageIt != m_images.end(); ++imageIt) {
0322         if ((*imageIt)->fileName() == fileName) {
0323             break;
0324         }
0325     }
0326     // since insert() inserts before iterator increment when inserting AFTER image
0327     if (after)
0328         imageIt++;
0329     for (DB::ImageInfoListConstIterator it = list.begin(); it != list.end(); ++it) {
0330         // the call to insert() destroys the given iterator so use the new one after the call
0331         imageIt = m_images.insert(imageIt, *it);
0332         m_imageCache.insert((*it)->fileName().absolute(), *it);
0333         // increment always to retain order of selected images
0334         imageIt++;
0335     }
0336     Q_EMIT dirty();
0337 }
0338 
0339 void ImageDB::readOptions(ImageInfoPtr info, DB::ReaderPtr reader, const QMap<QString, QString> *newToOldCategory)
0340 {
0341     static QString _name_ = QString::fromUtf8("name");
0342     static QString _value_ = QString::fromUtf8("value");
0343     static QString _option_ = QString::fromUtf8("option");
0344     static QString _area_ = QString::fromUtf8("area");
0345 
0346     while (reader->readNextStartOrStopElement(_option_).isStartToken) {
0347         QString name = DB::FileReader::unescape(reader->attribute(_name_));
0348         // If the silent update to db version 6 has been done, use the updated category names.
0349         if (newToOldCategory) {
0350             name = newToOldCategory->key(name, name);
0351         }
0352 
0353         if (!name.isNull()) {
0354             // Read values
0355             while (reader->readNextStartOrStopElement(_value_).isStartToken) {
0356                 QString value = reader->attribute(_value_);
0357 
0358                 if (reader->hasAttribute(_area_)) {
0359                     QStringList areaData = reader->attribute(_area_).split(QString::fromUtf8(" "));
0360                     int x = areaData[0].toInt();
0361                     int y = areaData[1].toInt();
0362                     int w = areaData[2].toInt();
0363                     int h = areaData[3].toInt();
0364                     QRect area = QRect(QPoint(x, y), QPoint(x + w - 1, y + h - 1));
0365 
0366                     if (!value.isNull()) {
0367                         info->addCategoryInfo(name, value, area);
0368                     }
0369                 } else {
0370                     if (!value.isNull()) {
0371                         info->addCategoryInfo(name, value);
0372                     }
0373                 }
0374                 reader->readEndElement();
0375             }
0376         }
0377     }
0378 }
0379 
0380 DB::MediaCount ImageDB::count(const ImageSearchInfo &searchInfo)
0381 {
0382     uint images = 0;
0383     uint videos = 0;
0384     for (const auto &imageInfo : search(searchInfo)) {
0385         if (imageInfo->mediaType() == Image)
0386             ++images;
0387         else
0388             ++videos;
0389     }
0390     return MediaCount(images, videos);
0391 }
0392 
0393 void ImageDB::slotReread(const DB::FileNameList &list, DB::ExifMode mode)
0394 {
0395     // Do here a reread of the exif info and change the info correctly in the database without loss of previous added data
0396     QProgressDialog dialog(i18n("Loading information from images"),
0397                            i18n("Cancel"), 0, list.count());
0398 
0399     uint count = 0;
0400     for (DB::FileNameList::ConstIterator it = list.begin(); it != list.end(); ++it, ++count) {
0401         if (count % 10 == 0) {
0402             dialog.setValue(count); // ensure to call setProgress(0)
0403             qApp->processEvents(QEventLoop::AllEvents);
0404 
0405             if (dialog.wasCanceled())
0406                 return;
0407         }
0408 
0409         QFileInfo fi((*it).absolute());
0410 
0411         if (fi.exists())
0412             info(*it)->readExif(*it, mode);
0413         markDirty();
0414     }
0415 }
0416 
0417 void ImageDB::setCurrentScope(const ImageSearchInfo &info)
0418 {
0419     m_currentScope = info;
0420 }
0421 
0422 DB::FileName ImageDB::findFirstItemInRange(const DB::FileNameList &images,
0423                                            const ImageDate &range,
0424                                            bool includeRanges) const
0425 {
0426     DB::FileName candidate;
0427     Utilities::FastDateTime candidateDateStart;
0428     for (const DB::FileName &fileName : images) {
0429         ImageInfoPtr iInfo = info(fileName);
0430 
0431         using MatchType = DB::ImageDate::MatchType;
0432         MatchType match = iInfo->date().isIncludedIn(range);
0433         if (match == MatchType::IsContained || (includeRanges && match == MatchType::Overlap)) {
0434             if (candidate.isNull() || iInfo->date().start() < candidateDateStart) {
0435                 candidate = fileName;
0436                 // Looking at this, can't this just be iInfo->date().start()?
0437                 // Just in the middle of refactoring other stuff, so leaving
0438                 // this alone now. TODO(hzeller): revisit.
0439                 candidateDateStart = info(candidate)->date().start();
0440             }
0441         }
0442     }
0443     return candidate;
0444 }
0445 
0446 bool ImageDB::untaggedCategoryFeatureConfigured() const
0447 {
0448     return m_untaggedTag && m_untaggedTag->isValid();
0449 }
0450 
0451 int ImageDB::totalCount() const
0452 {
0453     return m_images.count();
0454 }
0455 
0456 ImageInfoList ImageDB::search(const ImageSearchInfo &info, bool requireOnDisk) const
0457 {
0458     return search(info, (requireOnDisk ? DB::SearchOption::RequireOnDisk : DB::SearchOption::NoOption));
0459 }
0460 
0461 ImageInfoList ImageDB::search(const ImageSearchInfo &searchInfo, DB::SearchOptions options) const
0462 {
0463     const bool onlyItemsMatchingRange = !options.testFlag(DB::SearchOption::AllowRangeMatch);
0464     const bool requireOnDisk = options.testFlag(DB::SearchOption::RequireOnDisk);
0465 
0466     // When searching for images counts for the datebar, we want matches outside the range too.
0467     // When searching for images for the thumbnail view, we only want matches inside the range.
0468     DB::ImageInfoList result;
0469     for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it) {
0470         bool match = !(*it)->isLocked() && searchInfo.match(*it) && (!onlyItemsMatchingRange || rangeInclude(*it));
0471         match &= !requireOnDisk || DB::ImageInfo::imageOnDisk((*it)->fileName());
0472 
0473         if (match)
0474             result.append((*it));
0475     }
0476     return result;
0477 }
0478 
0479 void ImageDB::renameCategory(const QString &oldName, const QString newName)
0480 {
0481     for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) {
0482         (*it)->renameCategory(oldName, newName);
0483     }
0484 }
0485 
0486 /**
0487  * Jesper:
0488  * I was considering merging the two calls to this method (one for images, one for video), but then I
0489  * realized that all the work is really done after the check for whether the given
0490  * imageInfo is of the right type, and as a match can't be both, this really
0491  * would buy me nothing.
0492  */
0493 QMap<QString, CountWithRange> ImageDB::classify(const ImageSearchInfo &info, const QString &category, MediaType typemask, ClassificationMode mode)
0494 {
0495     QElapsedTimer timer;
0496     timer.start();
0497     QMap<QString, DB::CountWithRange> map;
0498     DB::GroupCounter counter(category);
0499     Utilities::StringSet alreadyMatched = info.findAlreadyMatched(category);
0500 
0501     DB::ImageSearchInfo noMatchInfo = info;
0502     QString currentMatchTxt = noMatchInfo.categoryMatchText(category);
0503     if (currentMatchTxt.isEmpty())
0504         noMatchInfo.setCategoryMatchText(category, DB::ImageDB::NONE());
0505     else
0506         noMatchInfo.setCategoryMatchText(category, QString::fromLatin1("%1 & %2").arg(currentMatchTxt, DB::ImageDB::NONE()));
0507     noMatchInfo.setCacheable(false);
0508 
0509     // Iterate through the whole database of images.
0510     for (const auto &imageInfo : qAsConst(m_images)) {
0511         bool match = ((imageInfo)->mediaType() & typemask) && !(imageInfo)->isLocked() && info.match(imageInfo) && rangeInclude(imageInfo);
0512         if (match) { // If the given image is currently matched.
0513 
0514             // Now iterate through all the categories the current image
0515             // contains, and increase them in the map mapping from category
0516             // to count.
0517             StringSet items = (imageInfo)->itemsOfCategory(category);
0518             counter.count(items, imageInfo->date());
0519             for (const auto &categoryName : qAsConst(items)) {
0520                 if (!alreadyMatched.contains(categoryName)) // We do not want to match "Jesper & Jesper"
0521                     map[categoryName].add(imageInfo->date());
0522             }
0523 
0524             // Find those with no other matches
0525             if (noMatchInfo.match(imageInfo))
0526                 map[DB::ImageDB::NONE()].count++;
0527 
0528             // this is a shortcut for the browser overview page,
0529             // where we are only interested whether there are sub-categories to a category
0530             if (mode == DB::ClassificationMode::PartialCount && map.size() > 1) {
0531                 qCInfo(TimingLog) << "ImageDB::classify(partial): " << timer.restart() << "ms.";
0532                 return map;
0533             }
0534         }
0535     }
0536 
0537     QMap<QString, DB::CountWithRange> groups = counter.result();
0538     for (QMap<QString, DB::CountWithRange>::iterator it = groups.begin(); it != groups.end(); ++it) {
0539         map[it.key()] = it.value();
0540     }
0541 
0542     qCInfo(TimingLog) << "ImageDB::classify(): " << timer.restart() << "ms.";
0543     return map;
0544 }
0545 
0546 FileNameList ImageDB::files(MediaType type) const
0547 {
0548     return m_images.files(type);
0549 }
0550 
0551 ImageInfoList ImageDB::images() const
0552 {
0553     return m_images;
0554 }
0555 
0556 void ImageDB::addImages(const ImageInfoList &images, bool doUpdate)
0557 {
0558     for (const DB::ImageInfoPtr &info : images) {
0559         info->addCategoryInfo(i18n("Media Type"),
0560                               info->mediaType() == DB::Image ? i18n("Image") : i18n("Video"));
0561         m_delayedCache.insert(info->fileName().absolute(), info);
0562         m_delayedUpdate << info;
0563     }
0564     if (doUpdate) {
0565         commitDelayedImages();
0566     }
0567 }
0568 
0569 void ImageDB::commitDelayedImages()
0570 {
0571     uint imagesAdded = m_delayedUpdate.count();
0572     if (imagesAdded > 0) {
0573         forceUpdate(m_delayedUpdate);
0574         m_delayedCache.clear();
0575         m_delayedUpdate.clear();
0576         // It's the responsibility of the caller to add the Exif information.
0577         // It's more efficient from an I/O perspective to minimize the number
0578         // of passes over the images, and with the ability to add the Exif
0579         // data in a transaction, there's no longer any need to read it here.
0580         Q_EMIT totalChanged(m_images.count());
0581         Q_EMIT dirty();
0582     }
0583 }
0584 
0585 void ImageDB::clearDelayedImages()
0586 {
0587     m_delayedCache.clear();
0588     m_delayedUpdate.clear();
0589 }
0590 
0591 void ImageDB::renameImage(const ImageInfoPtr info, const FileName &newName)
0592 {
0593     info->setFileName(newName);
0594 }
0595 
0596 void ImageDB::addToBlockList(const FileNameList &list)
0597 {
0598     for (const DB::FileName &fileName : list) {
0599         m_blockList.insert(fileName);
0600     }
0601     deleteList(list);
0602 }
0603 
0604 bool ImageDB::isBlocking(const FileName &fileName)
0605 {
0606     return m_blockList.contains(fileName);
0607 }
0608 
0609 void ImageDB::deleteList(const FileNameList &list)
0610 {
0611     for (const DB::FileName &fileName : list) {
0612         const DB::ImageInfoPtr imageInfo = info(fileName);
0613         StackMap::iterator found = m_stackMap.find(imageInfo->stackId());
0614         if (imageInfo->isStacked() && found != m_stackMap.end()) {
0615             const DB::FileNameList origCache = found.value();
0616             DB::FileNameList newCache;
0617             for (const DB::FileName &cacheName : origCache) {
0618                 if (fileName != cacheName)
0619                     newCache.append(cacheName);
0620             }
0621             if (newCache.size() <= 1) {
0622                 // we're destroying a stack
0623                 for (const DB::FileName &cacheName : qAsConst(newCache)) {
0624                     DB::ImageInfoPtr cacheInfo = info(cacheName);
0625                     cacheInfo->setStackId(0);
0626                     cacheInfo->setStackOrder(0);
0627                 }
0628                 m_stackMap.remove(imageInfo->stackId());
0629             } else {
0630                 m_stackMap.insert(imageInfo->stackId(), newCache);
0631             }
0632         }
0633         m_imageCache.remove(imageInfo->fileName().absolute());
0634         m_images.remove(imageInfo);
0635     }
0636     exifDB()->remove(list);
0637     Q_EMIT totalChanged(m_images.count());
0638     Q_EMIT imagesDeleted(list);
0639     Q_EMIT dirty();
0640 }
0641 
0642 ImageInfoPtr ImageDB::info(const FileName &fileName) const
0643 {
0644     if (fileName.isNull())
0645         return DB::ImageInfoPtr();
0646 
0647     const QString name = fileName.absolute();
0648 
0649     if (m_imageCache.contains(name))
0650         return m_imageCache[name];
0651 
0652     if (m_delayedCache.contains(name))
0653         return m_delayedCache[name];
0654 
0655     for (const DB::ImageInfoPtr &imageInfo : qAsConst(m_images))
0656         m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo);
0657 
0658     if (m_imageCache.contains(name)) {
0659         return m_imageCache[name];
0660     }
0661 
0662     return DB::ImageInfoPtr();
0663 }
0664 
0665 MemberMap &ImageDB::memberMap()
0666 {
0667     return m_members;
0668 }
0669 
0670 void ImageDB::save(const QString &fileName, bool isAutoSave)
0671 {
0672     DB::FileWriter saver(this);
0673     saver.save(fileName, isAutoSave);
0674 }
0675 
0676 MD5Map *ImageDB::md5Map()
0677 {
0678     return &m_md5map;
0679 }
0680 
0681 void ImageDB::sortAndMergeBackIn(const FileNameList &fileNameList)
0682 {
0683     DB::ImageInfoList infoList;
0684     for (const DB::FileName &fileName : fileNameList)
0685         infoList.append(info(fileName));
0686     m_images.sortAndMergeBackIn(infoList);
0687 }
0688 
0689 CategoryCollection *ImageDB::categoryCollection()
0690 {
0691     return &m_categoryCollection;
0692 }
0693 
0694 const CategoryCollection *ImageDB::categoryCollection() const
0695 {
0696     return &m_categoryCollection;
0697 }
0698 
0699 void ImageDB::reorder(const FileName &item, const FileNameList &selection, bool after)
0700 {
0701     Q_ASSERT(!item.isNull());
0702     DB::ImageInfoList list = takeImagesFromSelection(selection);
0703     insertList(item, list, after);
0704 }
0705 
0706 bool ImageDB::stack(const FileNameList &items)
0707 {
0708     unsigned int changed = 0;
0709     QSet<DB::StackID> stacks;
0710     QList<DB::ImageInfoPtr> images;
0711     unsigned int stackOrder = 1;
0712 
0713     for (const DB::FileName &fileName : items) {
0714         DB::ImageInfoPtr imgInfo = info(fileName);
0715         Q_ASSERT(imgInfo);
0716         if (imgInfo->isStacked()) {
0717             stacks << imgInfo->stackId();
0718             stackOrder = qMax(stackOrder, imgInfo->stackOrder() + 1);
0719         } else {
0720             images << imgInfo;
0721         }
0722     }
0723 
0724     if (stacks.size() > 1)
0725         return false; // images already in different stacks -> can't stack
0726 
0727     DB::StackID stackId = (stacks.size() == 1) ? *(stacks.begin()) : m_nextStackId++;
0728     for (DB::ImageInfoPtr info : qAsConst(images)) {
0729         info->setStackOrder(stackOrder);
0730         info->setStackId(stackId);
0731         m_stackMap[stackId].append(info->fileName());
0732         ++changed;
0733         ++stackOrder;
0734     }
0735 
0736     if (changed)
0737         Q_EMIT dirty();
0738 
0739     return changed;
0740 }
0741 
0742 void ImageDB::unstack(const FileNameList &items)
0743 {
0744     for (const DB::FileName &fileName : items) {
0745         const DB::FileNameList allInStack = getStackFor(fileName);
0746         if (allInStack.size() <= 2) {
0747             // we're destroying stack here
0748             for (const DB::FileName &stackFileName : allInStack) {
0749                 DB::ImageInfoPtr imgInfo = info(stackFileName);
0750                 Q_ASSERT(imgInfo);
0751                 if (imgInfo->isStacked()) {
0752                     m_stackMap.remove(imgInfo->stackId());
0753                     imgInfo->setStackId(0);
0754                     imgInfo->setStackOrder(0);
0755                 }
0756             }
0757         } else {
0758             DB::ImageInfoPtr imgInfo = info(fileName);
0759             Q_ASSERT(imgInfo);
0760             if (imgInfo->isStacked()) {
0761                 m_stackMap[imgInfo->stackId()].removeAll(fileName);
0762                 imgInfo->setStackId(0);
0763                 imgInfo->setStackOrder(0);
0764             }
0765         }
0766     }
0767 
0768     if (!items.isEmpty())
0769         Q_EMIT dirty();
0770 }
0771 
0772 FileNameList ImageDB::getStackFor(const FileName &referenceImage) const
0773 {
0774     DB::ImageInfoPtr imageInfo = info(referenceImage);
0775 
0776     if (!imageInfo || !imageInfo->isStacked())
0777         return DB::FileNameList();
0778 
0779     StackMap::iterator found = m_stackMap.find(imageInfo->stackId());
0780     if (found != m_stackMap.end())
0781         return found.value();
0782 
0783     // it wasn't in the cache -> rebuild it
0784     m_stackMap.clear();
0785     for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it) {
0786         if ((*it)->isStacked()) {
0787             DB::StackID stackid = (*it)->stackId();
0788             m_stackMap[stackid].append((*it)->fileName());
0789         }
0790     }
0791 
0792     found = m_stackMap.find(imageInfo->stackId());
0793     if (found != m_stackMap.end())
0794         return found.value();
0795     else
0796         return DB::FileNameList();
0797 }
0798 
0799 void ImageDB::copyData(const FileName &from, const FileName &to)
0800 {
0801     (*info(to)).merge(*info(from));
0802 }
0803 
0804 Exif::Database *ImageDB::exifDB() const
0805 {
0806     return m_exifDB.get();
0807 }
0808 
0809 const DB::TagInfo *ImageDB::untaggedTag() const
0810 {
0811     return m_untaggedTag;
0812 }
0813 
0814 int ImageDB::fileVersion()
0815 {
0816     // File format version, bump it up every time the format for the file changes.
0817     return 10;
0818 }
0819 
0820 ImageInfoPtr ImageDB::createImageInfo(const FileName &fileName, DB::ReaderPtr reader, ImageDB *db, const QMap<QString, QString> *newToOldCategory)
0821 {
0822     static QString _label_ = QString::fromUtf8("label");
0823     static QString _description_ = QString::fromUtf8("description");
0824     static QString _startDate_ = QString::fromUtf8("startDate");
0825     static QString _endDate_ = QString::fromUtf8("endDate");
0826     static QString _yearFrom_ = QString::fromUtf8("yearFrom");
0827     static QString _monthFrom_ = QString::fromUtf8("monthFrom");
0828     static QString _dayFrom_ = QString::fromUtf8("dayFrom");
0829     static QString _hourFrom_ = QString::fromUtf8("hourFrom");
0830     static QString _minuteFrom_ = QString::fromUtf8("minuteFrom");
0831     static QString _secondFrom_ = QString::fromUtf8("secondFrom");
0832     static QString _yearTo_ = QString::fromUtf8("yearTo");
0833     static QString _monthTo_ = QString::fromUtf8("monthTo");
0834     static QString _dayTo_ = QString::fromUtf8("dayTo");
0835     static QString _angle_ = QString::fromUtf8("angle");
0836     static QString _md5sum_ = QString::fromUtf8("md5sum");
0837     static QString _width_ = QString::fromUtf8("width");
0838     static QString _height_ = QString::fromUtf8("height");
0839     static QString _rating_ = QString::fromUtf8("rating");
0840     static QString _stackId_ = QString::fromUtf8("stackId");
0841     static QString _stackOrder_ = QString::fromUtf8("stackOrder");
0842     static QString _videoLength_ = QString::fromUtf8("videoLength");
0843     static QString _options_ = QString::fromUtf8("options");
0844     static QString _0_ = QString::fromUtf8("0");
0845     static QString _minus1_ = QString::fromUtf8("-1");
0846     static QString _MediaType_ = i18n("Media Type");
0847     static QString _Image_ = i18n("Image");
0848     static QString _Video_ = i18n("Video");
0849 
0850     QString label;
0851     if (reader->hasAttribute(_label_))
0852         label = reader->attribute(_label_);
0853     else
0854         label = QFileInfo(fileName.relative()).completeBaseName();
0855     QString description;
0856     if (reader->hasAttribute(_description_))
0857         description = reader->attribute(_description_);
0858 
0859     DB::ImageDate date;
0860     if (reader->hasAttribute(_startDate_)) {
0861         Utilities::FastDateTime start;
0862 
0863         QString str = reader->attribute(_startDate_);
0864         if (!str.isEmpty())
0865             start = dateTimeFromString(str);
0866 
0867         str = reader->attribute(_endDate_);
0868         if (!str.isEmpty())
0869             date = DB::ImageDate(start, dateTimeFromString(str));
0870         else
0871             date = DB::ImageDate(start);
0872     } else {
0873         int yearFrom = 0, monthFrom = 0, dayFrom = 0, yearTo = 0, monthTo = 0, dayTo = 0, hourFrom = -1, minuteFrom = -1, secondFrom = -1;
0874 
0875         yearFrom = reader->attribute(_yearFrom_, _0_).toInt();
0876         monthFrom = reader->attribute(_monthFrom_, _0_).toInt();
0877         dayFrom = reader->attribute(_dayFrom_, _0_).toInt();
0878         hourFrom = reader->attribute(_hourFrom_, _minus1_).toInt();
0879         minuteFrom = reader->attribute(_minuteFrom_, _minus1_).toInt();
0880         secondFrom = reader->attribute(_secondFrom_, _minus1_).toInt();
0881 
0882         yearTo = reader->attribute(_yearTo_, _0_).toInt();
0883         monthTo = reader->attribute(_monthTo_, _0_).toInt();
0884         dayTo = reader->attribute(_dayTo_, _0_).toInt();
0885         date = DB::ImageDate(yearFrom, monthFrom, dayFrom, yearTo, monthTo, dayTo, hourFrom, minuteFrom, secondFrom);
0886     }
0887 
0888     int angle = reader->attribute(_angle_, _0_).toInt();
0889     DB::MD5 md5sum(reader->attribute(_md5sum_));
0890 
0891     s_anyImageWithEmptySize |= !reader->hasAttribute(_width_);
0892 
0893     int w = reader->attribute(_width_, _minus1_).toInt();
0894     int h = reader->attribute(_height_, _minus1_).toInt();
0895     QSize size = QSize(w, h);
0896 
0897     DB::MediaType mediaType = KPABase::isVideo(fileName) ? DB::Video : DB::Image;
0898 
0899     short rating = reader->attribute(_rating_, _minus1_).toShort();
0900     DB::StackID stackId = reader->attribute(_stackId_, _0_).toULong();
0901     unsigned int stackOrder = reader->attribute(_stackOrder_, _0_).toULong();
0902 
0903     DB::ImageInfo *info = new DB::ImageInfo(fileName, label, description, date,
0904                                             angle, md5sum, size, mediaType, rating, stackId, stackOrder);
0905 
0906     if (reader->hasAttribute(_videoLength_))
0907         info->setVideoLength(reader->attribute(_videoLength_).toInt());
0908 
0909     DB::ImageInfoPtr result(info);
0910 
0911     possibleLoadCompressedCategories(reader, result, db, newToOldCategory);
0912 
0913     while (reader->readNextStartOrStopElement(_options_).isStartToken) {
0914         readOptions(result, reader, newToOldCategory);
0915     }
0916 
0917     info->addCategoryInfo(_MediaType_,
0918                           info->mediaType() == DB::Image ? _Image_ : _Video_);
0919 
0920     return result;
0921 }
0922 
0923 void ImageDB::possibleLoadCompressedCategories(DB::ReaderPtr reader, ImageInfoPtr info, ImageDB *db, const QMap<QString, QString> *newToOldCategory)
0924 {
0925     if (db == nullptr)
0926         return;
0927 
0928     const auto categories = db->m_categoryCollection.categories();
0929     for (const DB::CategoryPtr &categoryPtr : categories) {
0930         const QString categoryName = categoryPtr->name();
0931         QString oldCategoryName;
0932         if (newToOldCategory) {
0933             // translate to old categoryName, defaulting to the original name if not found:
0934             oldCategoryName = newToOldCategory->value(categoryName, categoryName);
0935         } else {
0936             oldCategoryName = categoryName;
0937         }
0938         QString str = reader->attribute(DB::FileWriter::escape(oldCategoryName));
0939         if (!str.isEmpty()) {
0940             const QStringList list = str.split(QString::fromLatin1(","), Qt::SkipEmptyParts);
0941             for (const QString &tagString : list) {
0942                 int id = tagString.toInt();
0943                 if (id != 0 || categoryPtr->isSpecialCategory()) {
0944                     const QString name = categoryPtr->nameForId(id);
0945                     info->addCategoryInfo(categoryName, name);
0946                 } else {
0947                     QStringList tags = categoryPtr->namesForIdZero();
0948                     if (tags.size() == 1) {
0949                         qCInfo(DBLog) << "Fixing tag " << categoryName << "/" << tags[0] << "with id=0 for image" << info->fileName().relative();
0950                     } else {
0951                         // insert marker category
0952                         QString markerTag = i18n("KPhotoAlbum - manual repair needed (%1)",
0953                                                  tags.join(i18nc("Separator in a list of tags", ", ")));
0954                         categoryPtr->addItem(markerTag);
0955                         info->addCategoryInfo(categoryName, markerTag);
0956                         qCWarning(DBLog) << "Manual fix required for image" << info->fileName().relative();
0957                         qCWarning(DBLog) << "Image was marked with tag " << categoryName << "/" << markerTag;
0958                     }
0959                     for (const auto &name : qAsConst(tags)) {
0960                         info->addCategoryInfo(categoryName, name);
0961                     }
0962                 }
0963             }
0964         }
0965     }
0966 }
0967 
0968 void ImageDB::setUntaggedTag(DB::TagInfo *tag)
0969 {
0970     if (m_untaggedTag) {
0971         m_untaggedTag->deleteLater();
0972     }
0973     m_untaggedTag = tag;
0974     if (m_untaggedTag && m_untaggedTag->isValid()) {
0975         const QSignalBlocker signalBlocker { this };
0976         Settings::SettingsData::instance()->setUntaggedCategory(m_untaggedTag->categoryName());
0977         Settings::SettingsData::instance()->setUntaggedTag(m_untaggedTag->tagName());
0978         connect(Settings::SettingsData::instance(), &Settings::SettingsData::untaggedTagChanged, this, QOverload<const QString &, const QString &>::of(&DB::ImageDB::setUntaggedTag));
0979     }
0980 }
0981 
0982 void ImageDB::setUntaggedTag(const QString &category, const QString &tag)
0983 {
0984     const auto categoryPtr = categoryCollection()->categoryForName(category);
0985     DB::TagInfo *tagInfo = nullptr;
0986     if (categoryPtr) {
0987         tagInfo = categoryPtr->itemForName(tag);
0988     }
0989     setUntaggedTag(tagInfo);
0990 }
0991 
0992 #include "moc_ImageDB.cpp"
0993 
0994 // vi:expandtab:tabstop=4 shiftwidth=4: