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: