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