File indexing completed on 2025-03-09 03:52:55
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2007-09-19 0007 * Description : Scanning a single item - history metadata helper. 0008 * 0009 * SPDX-FileCopyrightText: 2007-2013 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de> 0010 * SPDX-FileCopyrightText: 2013-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0011 * 0012 * SPDX-License-Identifier: GPL-2.0-or-later 0013 * 0014 * ============================================================ */ 0015 0016 #include "itemscanner_p.h" 0017 0018 namespace Digikam 0019 { 0020 0021 void ItemScanner::scanImageHistory() 0022 { 0023 /** Stage 1 of history scanning */ 0024 0025 d->commit.historyXml = d->metadata->getItemHistory(); 0026 d->commit.uuid = d->metadata->getItemUniqueId(); 0027 } 0028 0029 void ItemScanner::commitImageHistory() 0030 { 0031 if (!d->commit.historyXml.isEmpty()) 0032 { 0033 CoreDbAccess().db()->setItemHistory(d->scanInfo.id, d->commit.historyXml); 0034 0035 // Delay history resolution by setting this tag: 0036 // Resolution depends on the presence of other images, possibly only when the scanning process has finished 0037 0038 CoreDbAccess().db()->addItemTag(d->scanInfo.id, TagsCache::instance()-> 0039 getOrCreateInternalTag(InternalTagName::needResolvingHistory())); 0040 d->hasHistoryToResolve = true; 0041 } 0042 0043 if (!d->commit.uuid.isNull()) 0044 { 0045 CoreDbAccess().db()->setImageUuid(d->scanInfo.id, d->commit.uuid); 0046 } 0047 } 0048 0049 void ItemScanner::scanImageHistoryIfModified() 0050 { 0051 // If a file has a modified history, it must have a new UUID 0052 0053 QString previousUuid = CoreDbAccess().db()->getImageUuid(d->scanInfo.id); 0054 QString currentUuid = d->metadata->getItemUniqueId(); 0055 0056 if (!currentUuid.isEmpty() && previousUuid != currentUuid) 0057 { 0058 scanImageHistory(); 0059 } 0060 } 0061 0062 bool ItemScanner::resolveImageHistory(qlonglong id, QList<qlonglong>* needTaggingIds) 0063 { 0064 ImageHistoryEntry history = CoreDbAccess().db()->getItemHistory(id); 0065 return resolveImageHistory(id, history.history, needTaggingIds); 0066 } 0067 0068 bool ItemScanner::resolveImageHistory(qlonglong imageId, const QString& historyXml, 0069 QList<qlonglong>* needTaggingIds) 0070 { 0071 /** Stage 2 of history scanning */ 0072 0073 if (historyXml.isNull()) 0074 { 0075 return true; // "true" means nothing is left to resolve 0076 } 0077 0078 DImageHistory history = DImageHistory::fromXml(historyXml); 0079 0080 if (history.isNull()) 0081 { 0082 return true; 0083 } 0084 0085 ItemHistoryGraph graph; 0086 graph.addScannedHistory(history, imageId); 0087 0088 if (!graph.hasEdges()) 0089 { 0090 return true; 0091 } 0092 0093 QPair<QList<qlonglong>, QList<qlonglong> > cloud = graph.relationCloudParallel(); 0094 CoreDbAccess().db()->addImageRelations(cloud.first, cloud.second, DatabaseRelation::DerivedFrom); 0095 0096 int needResolvingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needResolvingHistory()); 0097 int needTaggingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needTaggingHistoryGraph()); 0098 0099 // remove the needResolvingHistory tag from all images in graph 0100 0101 CoreDbAccess().db()->removeTagsFromItems(graph.allImageIds(), QList<int>() << needResolvingTag); 0102 0103 // mark a single image from the graph (sufficient for find the full relation cloud) 0104 0105 QList<ItemInfo> roots = graph.rootImages(); 0106 0107 if (!roots.isEmpty()) 0108 { 0109 CoreDbAccess().db()->addItemTag(roots.first().id(), needTaggingTag); 0110 0111 if (needTaggingIds) 0112 { 0113 *needTaggingIds << roots.first().id(); 0114 } 0115 } 0116 0117 return !graph.hasUnresolvedEntries(); 0118 } 0119 0120 void ItemScanner::tagItemHistoryGraph(qlonglong id) 0121 { 0122 /** Stage 3 of history scanning */ 0123 0124 ItemInfo info(id); 0125 0126 if (info.isNull()) 0127 { 0128 return; 0129 } 0130 0131 //qCDebug(DIGIKAM_DATABASE_LOG) << "tagItemHistoryGraph" << id; 0132 0133 // Load relation cloud, history of info and of all leaves of the tree into the graph, fully resolved 0134 0135 ItemHistoryGraph graph = ItemHistoryGraph::fromInfo(info, ItemHistoryGraph::LoadAll, ItemHistoryGraph::NoProcessing); 0136 qCDebug(DIGIKAM_DATABASE_LOG) << graph; 0137 0138 int originalVersionTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::originalVersion()); 0139 int currentVersionTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::currentVersion()); 0140 int intermediateVersionTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::intermediateVersion()); 0141 0142 int needTaggingTag = TagsCache::instance()->getOrCreateInternalTag(InternalTagName::needTaggingHistoryGraph()); 0143 0144 // Remove all relevant tags 0145 0146 CoreDbAccess().db()->removeTagsFromItems(graph.allImageIds(), 0147 QList<int>() << originalVersionTag 0148 << currentVersionTag 0149 << intermediateVersionTag 0150 << needTaggingTag); 0151 0152 if (!graph.hasEdges()) 0153 { 0154 return; 0155 } 0156 0157 // get category info 0158 QList<qlonglong> originals, intermediates, currents; 0159 QHash<ItemInfo, HistoryImageId::Types> grpTypes = graph.categorize(); 0160 QHash<ItemInfo, HistoryImageId::Types>::const_iterator it; 0161 0162 for (it = grpTypes.constBegin() ; it != grpTypes.constEnd() ; ++it) 0163 { 0164 qCDebug(DIGIKAM_DATABASE_LOG) << "Image" << it.key().id() << "type" << it.value(); 0165 HistoryImageId::Types types = it.value(); 0166 0167 if (types & HistoryImageId::Original) 0168 { 0169 originals << it.key().id(); 0170 } 0171 0172 if (types & HistoryImageId::Intermediate) 0173 { 0174 intermediates << it.key().id(); 0175 } 0176 0177 if (types & HistoryImageId::Current) 0178 { 0179 currents << it.key().id(); 0180 } 0181 } 0182 0183 if (!originals.isEmpty()) 0184 { 0185 CoreDbAccess().db()->addTagsToItems(originals, QList<int>() << originalVersionTag); 0186 } 0187 0188 if (!intermediates.isEmpty()) 0189 { 0190 CoreDbAccess().db()->addTagsToItems(intermediates, QList<int>() << intermediateVersionTag); 0191 } 0192 0193 if (!currents.isEmpty()) 0194 { 0195 CoreDbAccess().db()->addTagsToItems(currents, QList<int>() << currentVersionTag); 0196 } 0197 } 0198 0199 DImageHistory ItemScanner::resolvedImageHistory(const DImageHistory& history, bool mustBeAvailable) 0200 { 0201 DImageHistory h; 0202 0203 Q_FOREACH (const DImageHistory::Entry& e, history.entries()) 0204 { 0205 // Copy entry, without referredImages 0206 0207 DImageHistory::Entry entry; 0208 entry.action = e.action; 0209 0210 // resolve referredImages 0211 0212 Q_FOREACH (const HistoryImageId& id, e.referredImages) 0213 { 0214 QList<qlonglong> imageIds = resolveHistoryImageId(id); 0215 0216 // append each image found in collection to referredImages 0217 0218 Q_FOREACH (const qlonglong& imageId, imageIds) 0219 { 0220 ItemInfo info(imageId); 0221 0222 if (info.isNull()) 0223 { 0224 continue; 0225 } 0226 0227 if (mustBeAvailable) 0228 { 0229 CollectionLocation location = CollectionManager::instance()->locationForAlbumRootId(info.albumRootId()); 0230 0231 if (!location.isAvailable()) 0232 { 0233 continue; 0234 } 0235 } 0236 0237 HistoryImageId newId = info.historyImageId(); 0238 newId.setType(id.m_type); 0239 entry.referredImages << newId; 0240 } 0241 } 0242 0243 // add to history 0244 0245 h.entries() << entry; 0246 } 0247 0248 return h; 0249 } 0250 0251 bool ItemScanner::sameReferredImage(const HistoryImageId& id1, const HistoryImageId& id2) 0252 { 0253 if (!id1.isValid() || !id2.isValid()) 0254 { 0255 return false; 0256 } 0257 0258 /* 0259 * We give the UUID the power of equivalence that none of the other criteria has: 0260 * For two images a,b with uuids x,y, where x and y not null, 0261 * a (same image as) b <=> x == y 0262 */ 0263 0264 if (id1.hasUuid() && id2.hasUuid()) 0265 { 0266 return (id1.m_uuid == id2.m_uuid); 0267 } 0268 0269 if (id1.hasUniqueHashIdentifier() && 0270 (id1.m_uniqueHash == id2.m_uniqueHash) && 0271 (id1.m_fileSize == id2.m_fileSize)) 0272 { 0273 return true; 0274 } 0275 0276 if (id1.hasFileName() && 0277 id1.hasCreationDate() && 0278 (id1.m_fileName == id2.m_fileName) && 0279 (id1.m_creationDate == id2.m_creationDate)) 0280 { 0281 return true; 0282 } 0283 0284 if (id1.hasFileOnDisk() && 0285 (id1.m_filePath == id2.m_filePath) && 0286 (id1.m_fileName == id2.m_fileName)) 0287 { 0288 return true; 0289 } 0290 0291 return false; 0292 } 0293 0294 // Returns true if both have the same UUID, or at least one of the two has no UUID 0295 // Returns false iff both have a UUID and the UUIDs differ 0296 0297 static bool uuidDoesNotDiffer(const HistoryImageId& referenceId, qlonglong id) 0298 { 0299 if (referenceId.hasUuid()) 0300 { 0301 QString uuid = CoreDbAccess().db()->getImageUuid(id); 0302 0303 if (!uuid.isEmpty()) 0304 { 0305 return referenceId.m_uuid == uuid; 0306 } 0307 } 0308 0309 return true; 0310 } 0311 0312 static QList<qlonglong> mergedIdLists(const HistoryImageId& referenceId, 0313 const QList<qlonglong>& uuidList, 0314 const QList<qlonglong>& candidates) 0315 { 0316 QList<qlonglong> results; 0317 0318 // uuidList are definite results 0319 0320 results = uuidList; 0321 0322 // Add a candidate if it has the same UUID, or either reference or candidate have a UUID 0323 // (other way round: do not add a candidate which positively has a different UUID) 0324 0325 Q_FOREACH (const qlonglong& candidate, candidates) 0326 { 0327 if (results.contains(candidate)) 0328 { 0329 continue; // already in list, skip 0330 } 0331 0332 if (uuidDoesNotDiffer(referenceId, candidate)) 0333 { 0334 results << candidate; 0335 } 0336 } 0337 0338 return results; 0339 } 0340 0341 QList<qlonglong> ItemScanner::resolveHistoryImageId(const HistoryImageId& historyId) 0342 { 0343 // first and foremost: UUID 0344 0345 QList<qlonglong> uuidList; 0346 0347 if (historyId.hasUuid()) 0348 { 0349 uuidList = CoreDbAccess().db()->getItemsForUuid(historyId.m_uuid); 0350 0351 // If all images had a UUID, we would be finished and could return here with a result: 0352 /* 0353 if (!uuidList.isEmpty()) 0354 { 0355 return uuidList; 0356 } 0357 */ 0358 // But as identical images may have no UUID yet, we need to continue 0359 } 0360 0361 // Second: uniqueHash + fileSize. Sufficient to assume that a file is identical, but subject to frequent change. 0362 0363 if (historyId.hasUniqueHashIdentifier() && CoreDbAccess().db()->isUniqueHashV2()) 0364 { 0365 QList<ItemScanInfo> infos = CoreDbAccess().db()->getIdenticalFiles(historyId.m_uniqueHash, historyId.m_fileSize); 0366 0367 if (!infos.isEmpty()) 0368 { 0369 QList<qlonglong> ids; 0370 0371 Q_FOREACH (const ItemScanInfo& info, infos) 0372 { 0373 if (info.status != DatabaseItem::Status::Trashed && info.status != DatabaseItem::Status::Obsolete) 0374 { 0375 ids << info.id; 0376 } 0377 } 0378 0379 return mergedIdLists(historyId, uuidList, ids); 0380 } 0381 } 0382 0383 // As a third combination, we try file name and creation date. Susceptible to renaming, 0384 // but not to metadata changes. 0385 0386 if (historyId.hasFileName() && historyId.hasCreationDate()) 0387 { 0388 QList<qlonglong> ids = CoreDbAccess().db()->findByNameAndCreationDate(historyId.m_fileName, historyId.m_creationDate); 0389 0390 if (!ids.isEmpty()) 0391 { 0392 return mergedIdLists(historyId, uuidList, ids); 0393 } 0394 } 0395 0396 // Another possibility: If the original UUID is given, we can find all relations for the image with this UUID, 0397 // and make an assumption from this group of images. Currently not implemented. 0398 0399 // resolve old-style by full file path 0400 0401 if (historyId.hasFileOnDisk()) 0402 { 0403 QFileInfo file(historyId.filePath()); 0404 0405 if (file.exists()) 0406 { 0407 CollectionLocation location = CollectionManager::instance()->locationForPath(historyId.path()); 0408 0409 if (!location.isNull()) 0410 { 0411 QString album = CollectionManager::instance()->album(file.path()); 0412 QString name = file.fileName(); 0413 ItemShortInfo info = CoreDbAccess().db()->getItemShortInfo(location.id(), album, name); 0414 0415 if (info.id) 0416 { 0417 return mergedIdLists(historyId, uuidList, QList<qlonglong>() << info.id); 0418 } 0419 } 0420 } 0421 } 0422 0423 return uuidList; 0424 } 0425 0426 bool ItemScanner::hasHistoryToResolve() const 0427 { 0428 return d->hasHistoryToResolve; 0429 } 0430 0431 QString ItemScanner::uniqueHash() const 0432 { 0433 // the QByteArray is an ASCII hex string 0434 0435 if (d->scanInfo.category == DatabaseItem::Image) 0436 { 0437 if (CoreDbAccess().db()->isUniqueHashV2()) 0438 { 0439 return QString::fromUtf8(d->img.getUniqueHashV2()); 0440 } 0441 else 0442 { 0443 return QString::fromUtf8(d->img.getUniqueHash()); 0444 } 0445 } 0446 else 0447 { 0448 if (CoreDbAccess().db()->isUniqueHashV2()) 0449 { 0450 return QString::fromUtf8(DImg::getUniqueHashV2(d->fileInfo.filePath())); 0451 } 0452 else 0453 { 0454 return QString::fromUtf8(DImg::getUniqueHash(d->fileInfo.filePath())); 0455 } 0456 } 0457 } 0458 0459 } // namespace Digikam