File indexing completed on 2025-01-05 03:55:17

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2010-06-02
0007  * Description : class for manipulating modifications changeset for non-destruct. editing
0008  *
0009  * SPDX-FileCopyrightText: 2010 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0010  * SPDX-FileCopyrightText: 2010 by Martin Klapetek <martin dot klapetek at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "dimagehistory.h"
0017 
0018 // Qt includes
0019 
0020 #include <QFile>
0021 #include <QSharedData>
0022 #include <QBuffer>
0023 #include <QHashIterator>
0024 #include <QFileInfo>
0025 #include <QUrl>
0026 
0027 // Local includes
0028 
0029 #include "digikam_debug.h"
0030 
0031 namespace Digikam
0032 {
0033 
0034 class Q_DECL_HIDDEN DImageHistory::Private : public QSharedData
0035 {
0036 public:
0037 
0038     explicit Private()
0039     {
0040     }
0041 
0042     QList<DImageHistory::Entry> entries;
0043 };
0044 
0045 // -----------------------------------------------------------------------------------------------
0046 
0047 class Q_DECL_HIDDEN PrivateSharedNull : public QSharedDataPointer<DImageHistory::Private>
0048 {
0049 public:
0050 
0051     PrivateSharedNull()
0052         : QSharedDataPointer<DImageHistory::Private>(new DImageHistory::Private)
0053     {
0054     }
0055 };
0056 
0057 Q_GLOBAL_STATIC(PrivateSharedNull, imageHistoryPrivSharedNull)
0058 
0059 // -----------------------------------------------------------------------------------------------
0060 
0061 DImageHistory::DImageHistory()
0062     : d(*imageHistoryPrivSharedNull)
0063 {
0064 }
0065 
0066 DImageHistory::DImageHistory(const DImageHistory& other)
0067     : d(other.d)
0068 {
0069 }
0070 
0071 DImageHistory::~DImageHistory()
0072 {
0073 }
0074 
0075 DImageHistory& DImageHistory::operator=(const DImageHistory& other)
0076 {
0077     d = other.d;
0078 
0079     return *this;
0080 }
0081 
0082 bool DImageHistory::isNull() const
0083 {
0084     return (d == *imageHistoryPrivSharedNull);
0085 }
0086 
0087 bool DImageHistory::isEmpty() const
0088 {
0089     return d->entries.isEmpty();
0090 }
0091 
0092 bool DImageHistory::isValid() const
0093 {
0094     if      (d->entries.isEmpty())
0095     {
0096         return false;
0097     }
0098     else if ((d->entries.count() == 1)                        &&
0099              (d->entries.first().referredImages.count() == 1) &&
0100              d->entries.first().referredImages.first().isCurrentFile())
0101     {
0102         return false;
0103     }
0104     else
0105     {
0106         Q_FOREACH (const Entry& e, d->entries)
0107         {
0108             if (!e.action.isNull())
0109             {
0110                 return true;
0111             }
0112 
0113             Q_FOREACH (const HistoryImageId& id, e.referredImages)
0114             {
0115                 if (id.isValid() && !id.isCurrentFile())
0116                 {   // cppcheck-suppress useStlAlgorithm
0117                     return true;
0118                 }
0119             }
0120         }
0121     }
0122     return false;
0123 }
0124 
0125 int DImageHistory::size() const
0126 {
0127     return d->entries.size();
0128 }
0129 
0130 static bool operator==(const DImageHistory::Entry& e1, const DImageHistory::Entry& e2)
0131 {
0132     return ((e1.action         == e2.action) &&
0133             (e1.referredImages == e2.referredImages));
0134 }
0135 
0136 bool DImageHistory::operator==(const DImageHistory& other) const
0137 {
0138     return (d->entries == other.d->entries);
0139 }
0140 
0141 bool DImageHistory::operator<(const Digikam::DImageHistory& other) const
0142 {
0143     if (d->entries.size() < other.size())
0144     {
0145         return true;
0146     }
0147 
0148     return false;
0149 }
0150 
0151 bool DImageHistory::operator>(const Digikam::DImageHistory& other) const
0152 {
0153     if (d->entries.size() > other.size())
0154     {
0155         return true;
0156     }
0157 
0158     return false;
0159 }
0160 
0161 QList<DImageHistory::Entry> &DImageHistory::entries()
0162 {
0163     return d->entries;
0164 }
0165 
0166 const QList<DImageHistory::Entry> &DImageHistory::entries() const
0167 {
0168     return d->entries;
0169 }
0170 
0171 DImageHistory::Entry& DImageHistory::operator[](int i)
0172 {
0173     return d->entries[i];
0174 }
0175 
0176 const DImageHistory::Entry& DImageHistory::operator[](int i) const
0177 {
0178     return d->entries.at(i);
0179 }
0180 
0181 DImageHistory& DImageHistory::operator<<(const FilterAction& action)
0182 {
0183     if (action.isNull())
0184     {
0185         return *this;
0186     }
0187 
0188     Entry entry;
0189     entry.action = action;
0190     d->entries << entry;
0191 /*
0192     qCDebug(DIGIKAM_DIMG_LOG) << "Entry added, total count " << d->entries.count();
0193 */
0194     return *this;
0195 }
0196 
0197 DImageHistory& DImageHistory::operator<<(const HistoryImageId& id)
0198 {
0199     appendReferredImage(id);
0200 
0201     return *this;
0202 }
0203 
0204 void DImageHistory::appendReferredImage(const HistoryImageId& id)
0205 {
0206     insertReferredImage(d->entries.size() - 1, id);
0207 }
0208 
0209 void DImageHistory::insertReferredImage(int index, const HistoryImageId& id)
0210 {
0211     if (!id.isValid())
0212     {
0213         qCWarning(DIGIKAM_DIMG_LOG) << "Attempt to add an invalid HistoryImageId";
0214         return;
0215     }
0216 
0217     // NOTE: Qt6::qBound() generate an exception if d->entries is empty, Qt5 no.
0218 
0219     index = qMax(0, qMin(d->entries.size() - 1, index));
0220 
0221     if (id.isCurrentFile())
0222     {
0223         // enforce to have exactly one Current id
0224 
0225         adjustReferredImages();
0226     }
0227 
0228     if (d->entries.isEmpty())
0229     {
0230         d->entries << Entry();
0231     }
0232 
0233     d->entries[index].referredImages << id;
0234 }
0235 
0236 void DImageHistory::removeLast()
0237 {
0238     if (!d->entries.isEmpty())
0239     {
0240         d->entries.removeLast();
0241     }
0242 }
0243 
0244 const FilterAction& DImageHistory::action(int i) const
0245 {
0246     return d->entries.at(i).action;
0247 }
0248 
0249 QList<FilterAction> DImageHistory::allActions() const
0250 {
0251     QList<FilterAction> actions;
0252 
0253     Q_FOREACH (const Entry& entry, d->entries)
0254     {
0255         if (!entry.action.isNull())
0256         {
0257             actions << entry.action;
0258         }
0259     }
0260 
0261     return actions;
0262 }
0263 
0264 int DImageHistory::actionCount() const
0265 {
0266     int count = 0;
0267 
0268     Q_FOREACH (const Entry& entry, d->entries)
0269     {
0270         if (!entry.action.isNull())
0271         {
0272             ++count;  // cppcheck-suppress useStlAlgorithm
0273         }
0274     }
0275 
0276     return count;
0277 }
0278 
0279 bool DImageHistory::hasActions() const
0280 {
0281     Q_FOREACH (const Entry& entry, d->entries)
0282     {
0283         if (!entry.action.isNull())
0284         {   // cppcheck-suppress useStlAlgorithm
0285             return true;
0286         }
0287     }
0288 
0289     return false;
0290 }
0291 
0292 QList<HistoryImageId> &DImageHistory::referredImages(int i)
0293 {
0294     return d->entries[i].referredImages;
0295 }
0296 
0297 const QList<HistoryImageId> &DImageHistory::referredImages(int i) const
0298 {
0299     return d->entries.at(i).referredImages;
0300 }
0301 
0302 QList<HistoryImageId> DImageHistory::allReferredImages() const
0303 {
0304     QList<HistoryImageId> ids;
0305 
0306     Q_FOREACH (const Entry& entry, d->entries)
0307     {
0308         ids << entry.referredImages;
0309     }
0310 
0311     return ids;
0312 }
0313 
0314 bool DImageHistory::hasReferredImages() const
0315 {
0316     Q_FOREACH (const Entry& entry, d->entries)
0317     {
0318         if (!entry.referredImages.isEmpty())
0319         {   // cppcheck-suppress useStlAlgorithm
0320             return true;
0321         }
0322     }
0323 
0324     return false;
0325 }
0326 
0327 bool DImageHistory::hasReferredImageOfType(HistoryImageId::Type type) const
0328 {
0329     Q_FOREACH (const Entry& entry, d->entries)
0330     {
0331         Q_FOREACH (const HistoryImageId& id, entry.referredImages)
0332         {
0333             if (id.m_type == type)
0334             {   // cppcheck-suppress useStlAlgorithm
0335                 return true;
0336             }
0337         }
0338     }
0339 
0340     return false;
0341 }
0342 
0343 bool DImageHistory::hasCurrentReferredImage() const
0344 {
0345     return hasReferredImageOfType(HistoryImageId::Current);
0346 }
0347 
0348 bool DImageHistory::hasOriginalReferredImage() const
0349 {
0350     return hasReferredImageOfType(HistoryImageId::Original);
0351 }
0352 
0353 QList<HistoryImageId> DImageHistory::referredImagesOfType(HistoryImageId::Type type) const
0354 {
0355     QList<HistoryImageId> ids;
0356 
0357     Q_FOREACH (const Entry& entry, d->entries)
0358     {
0359         Q_FOREACH (const HistoryImageId& id, entry.referredImages)
0360         {
0361             if (id.m_type == type)
0362             {
0363                 ids << id;
0364             }
0365         }
0366     }
0367 
0368     return ids;
0369 }
0370 
0371 HistoryImageId DImageHistory::currentReferredImage() const
0372 {
0373     Q_FOREACH (const Entry& entry, d->entries)
0374     {
0375         Q_FOREACH (const HistoryImageId& id, entry.referredImages)
0376         {
0377             if (id.isCurrentFile())
0378             {    // cppcheck-suppress useStlAlgorithm
0379                 return id;
0380             }
0381         }
0382     }
0383 
0384     return HistoryImageId();
0385 }
0386 
0387 HistoryImageId DImageHistory::originalReferredImage() const
0388 {
0389     Q_FOREACH (const Entry& entry, d->entries)
0390     {
0391         Q_FOREACH (const HistoryImageId& id, entry.referredImages)
0392         {
0393             if (id.isOriginalFile())
0394             {   // cppcheck-suppress useStlAlgorithm
0395                 return id;
0396             }
0397         }
0398     }
0399 
0400     return HistoryImageId();
0401 }
0402 
0403 void DImageHistory::clearReferredImages()
0404 {
0405     for (int i = 0 ; i < d->entries.size() ; ++i)
0406     {
0407         d->entries[i].referredImages.clear();
0408     }
0409 }
0410 
0411 void DImageHistory::adjustReferredImages()
0412 {
0413     for (int i = 0 ; i < d->entries.size() ; ++i)
0414     {
0415         Entry& entry = d->entries[i];
0416 
0417         for (int e = 0 ; e < entry.referredImages.size() ; ++e)
0418         {
0419             HistoryImageId& id = entry.referredImages[e];
0420 
0421             if (id.isCurrentFile())
0422             {
0423                 id.m_type = (i == 0) ? HistoryImageId::Original
0424                                      : HistoryImageId::Intermediate;
0425             }
0426         }
0427     }
0428 }
0429 
0430 void DImageHistory::adjustCurrentUuid(const QString& uuid)
0431 {
0432     for (int i = 0 ; i < d->entries.size() ; ++i)
0433     {
0434         Entry& entry = d->entries[i];
0435 
0436         for (int e = 0 ; e < entry.referredImages.size() ; ++e)
0437         {
0438             HistoryImageId& id = entry.referredImages[e];
0439 
0440             if (id.isCurrentFile())
0441             {
0442                 if (id.m_uuid.isNull())
0443                 {
0444                     id.m_uuid = uuid;
0445                 }
0446             }
0447         }
0448     }
0449 }
0450 
0451 void DImageHistory::purgePathFromReferredImages(const QString& path, const QString& fileName)
0452 {
0453     for (int i = 0 ; i < d->entries.size() ; ++i)
0454     {
0455         Entry& entry = d->entries[i];
0456 
0457         for (int e = 0 ; e < entry.referredImages.size() ; ++e)
0458         {
0459             HistoryImageId& id = entry.referredImages[e];
0460             {
0461                 if ((id.m_filePath == path) && (id.m_fileName == fileName))
0462                 {
0463                     id.m_filePath.clear();
0464                     id.m_fileName.clear();
0465                 }
0466             }
0467         }
0468     }
0469 }
0470 
0471 void DImageHistory::moveCurrentReferredImage(const QString& newPath, const QString& newFileName)
0472 {
0473     for (int i = 0 ; i < d->entries.size() ; ++i)
0474     {
0475         Entry& entry = d->entries[i];
0476 
0477         for (int e = 0 ; e < entry.referredImages.size() ; ++e)
0478         {
0479             HistoryImageId& id = entry.referredImages[e];
0480 
0481             if (id.isCurrentFile())
0482             {
0483                 id.setPath(newPath);
0484                 id.setFileName(newFileName);
0485             }
0486         }
0487     }
0488 }
0489 
0490 QString DImageHistory::toXml() const
0491 {
0492     QString xmlHistory;
0493 
0494     QXmlStreamWriter stream(&xmlHistory);
0495     stream.setAutoFormatting(true);
0496     stream.writeStartDocument();
0497     stream.writeStartElement(QLatin1String("history"));
0498     stream.writeAttribute(QLatin1String("version"), QString::number(1));
0499 
0500     for (int i = 0 ; i < entries().count() ; ++i)
0501     {
0502         const Entry& step = entries().at(i);
0503 
0504         if (!step.action.isNull())
0505         {
0506             stream.writeStartElement(QLatin1String("filter"));
0507             stream.writeAttribute(QLatin1String("filterName"), step.action.identifier());
0508             stream.writeAttribute(QLatin1String("filterDisplayName"), step.action.displayableName());
0509             stream.writeAttribute(QLatin1String("filterVersion"), QString::number(step.action.version()));
0510 
0511             switch (step.action.category())
0512             {
0513                 case FilterAction::ReproducibleFilter:
0514                     stream.writeAttribute(QLatin1String("filterCategory"), QLatin1String("reproducible"));
0515                     break;
0516 
0517                 case FilterAction::ComplexFilter:
0518                     stream.writeAttribute(QLatin1String("filterCategory"), QLatin1String("complex"));
0519                     break;
0520 
0521                 case FilterAction::DocumentedHistory:
0522                     stream.writeAttribute(QLatin1String("filterCategory"), QLatin1String("documentedHistory"));
0523                     break;
0524             }
0525 
0526             if (step.action.flags() & FilterAction::ExplicitBranch)
0527             {
0528                 stream.writeAttribute(QLatin1String("branch"), QLatin1String("true"));
0529             }
0530 
0531             stream.writeStartElement(QLatin1String("params"));
0532 
0533             const QHash<QString,QVariant>& params = step.action.parameters();
0534 
0535             if (!params.isEmpty())
0536             {
0537                 QList<QString> keys = params.keys();
0538                 std::sort(keys.begin(), keys.end());
0539 
0540                 Q_FOREACH (const QString& key, keys)
0541                 {
0542                     QHash<QString, QVariant>::const_iterator it;
0543 
0544                     for (it = params.find(key) ; it != params.end() && it.key() == key ; ++it)
0545                     {
0546                         stream.writeStartElement(QLatin1String("param"));
0547                         stream.writeAttribute(QLatin1String("name"), key);
0548                         stream.writeAttribute(QLatin1String("value"), it.value().toString());
0549                         stream.writeEndElement(); //param
0550                     }
0551                 }
0552             }
0553 
0554             stream.writeEndElement(); //params
0555             stream.writeEndElement(); //filter
0556         }
0557 
0558         if (!step.referredImages.isEmpty())
0559         {
0560             Q_FOREACH (const HistoryImageId& imageId, step.referredImages)
0561             {
0562                 if (!imageId.isValid())
0563                 {
0564                     continue;
0565                 }
0566 
0567                 if (imageId.isCurrentFile())
0568                 {
0569                     continue;
0570                 }
0571 
0572                 stream.writeStartElement(QLatin1String("file"));
0573 
0574                 if (!imageId.m_uuid.isNull())
0575                 {
0576                     stream.writeAttribute(QLatin1String("uuid"), imageId.m_uuid);
0577                 }
0578 
0579                 if      (imageId.isOriginalFile())
0580                 {
0581                     stream.writeAttribute(QLatin1String("type"), QLatin1String("original"));
0582                 }
0583                 else if (imageId.isSourceFile())
0584                 {
0585                     stream.writeAttribute(QLatin1String("type"), QLatin1String("source"));
0586                 }
0587 
0588                 stream.writeStartElement(QLatin1String("fileParams"));
0589 
0590                 if (!imageId.m_fileName.isNull())
0591                 {
0592                     stream.writeAttribute(QLatin1String("fileName"), imageId.m_fileName);
0593                 }
0594 
0595                 if (!imageId.m_filePath.isNull())
0596                 {
0597                     stream.writeAttribute(QLatin1String("filePath"), imageId.m_filePath);
0598                 }
0599 
0600                 if (!imageId.m_uniqueHash.isNull())
0601                 {
0602                     stream.writeAttribute(QLatin1String("fileHash"), imageId.m_uniqueHash);
0603                 }
0604 
0605                 if (imageId.m_fileSize)
0606                 {
0607                     stream.writeAttribute(QLatin1String("fileSize"), QString::number(imageId.m_fileSize));
0608                 }
0609 
0610                 if (imageId.isOriginalFile() && !imageId.m_creationDate.isNull())
0611                 {
0612                     stream.writeAttribute(QLatin1String("creationDate"), imageId.m_creationDate.toString(Qt::ISODate));
0613                 }
0614 
0615                 stream.writeEndElement(); //fileParams
0616 
0617                 stream.writeEndElement(); //file
0618             }
0619         }
0620     }
0621 
0622     stream.writeEndElement(); //history
0623 
0624     stream.writeEndDocument();
0625 
0626     //qCDebug(DIGIKAM_DIMG_LOG) << xmlHistory;
0627 
0628     return xmlHistory;
0629 }
0630 
0631 DImageHistory DImageHistory::fromXml(const QString& xml) //DImageHistory
0632 {
0633 /*
0634     qCDebug(DIGIKAM_DIMG_LOG) << "Parsing image history XML";
0635 */
0636 
0637     DImageHistory h;
0638 
0639     if (xml.isEmpty())
0640     {
0641         return h;
0642     }
0643 
0644     QXmlStreamReader stream(xml);
0645 
0646     if (!stream.readNextStartElement())
0647     {
0648         return h;
0649     }
0650 
0651     if (stream.name() != QLatin1String("history"))
0652     {
0653         return h;
0654     }
0655 
0656     QString originalUUID;
0657     QDateTime originalCreationDate;
0658 
0659     while (stream.readNextStartElement())
0660     {
0661         if (stream.name() == QLatin1String("file"))
0662         {
0663             //qCDebug(DIGIKAM_DIMG_LOG) << "Parsing file tag";
0664 
0665             HistoryImageId imageId(stream.attributes().value(QLatin1String("uuid")).toString());
0666 
0667             if      (stream.attributes().value(QLatin1String("type")) == QLatin1String("original"))
0668             {
0669                 imageId.m_type = HistoryImageId::Original;
0670             }
0671             else if (stream.attributes().value(QLatin1String("type")) == QLatin1String("source"))
0672             {
0673                 imageId.m_type = HistoryImageId::Source;
0674             }
0675             else
0676             {
0677                 imageId.m_type = HistoryImageId::Intermediate;
0678             }
0679 
0680             while (stream.readNextStartElement())
0681             {
0682                 if (stream.name() == QLatin1String("fileParams"))
0683                 {
0684                     imageId.setFileName(stream.attributes().value(QLatin1String("fileName")).toString());
0685                     imageId.setPath(stream.attributes().value(QLatin1String("filePath")).toString());
0686                     QString date = stream.attributes().value(QLatin1String("creationDate")).toString();
0687 
0688                     if (!date.isNull())
0689                     {
0690                         imageId.setCreationDate(QDateTime::fromString(date, Qt::ISODate));
0691                     }
0692 
0693                     QString size = stream.attributes().value(QLatin1String("fileSize")).toString();
0694 
0695                     if (stream.attributes().hasAttribute(QLatin1String("fileHash")) && !size.isNull())
0696                     {
0697                         imageId.setUniqueHash(stream.attributes().value(QLatin1String("fileHash")).toString(), size.toInt());
0698                     }
0699 
0700                     stream.skipCurrentElement();
0701                 }
0702                 else
0703                 {
0704                     stream.skipCurrentElement();
0705                 }
0706             }
0707 
0708             if (imageId.isOriginalFile())
0709             {
0710                 originalUUID         = imageId.m_uuid;
0711                 originalCreationDate = imageId.m_creationDate;
0712             }
0713             else
0714             {
0715                 imageId.m_originalUUID = originalUUID;
0716 
0717                 if (imageId.m_creationDate.isNull())
0718                 {
0719                     imageId.m_creationDate = originalCreationDate;
0720                 }
0721             }
0722 
0723             if (imageId.isValid())
0724             {
0725                 h << imageId;
0726             }
0727 
0728         }
0729         else if (stream.name() == QLatin1String("filter"))
0730         {
0731 /*
0732             qCDebug(DIGIKAM_DIMG_LOG) << "Parsing filter tag";
0733 */
0734             FilterAction::Category c  = FilterAction::ComplexFilter;
0735             QStringView categoryString = stream.attributes().value(QLatin1String("filterCategory"));
0736 
0737             if      (categoryString == QLatin1String("reproducible"))
0738             {
0739                 c = FilterAction::ReproducibleFilter;
0740             }
0741             else if (categoryString == QLatin1String("complex"))
0742             {
0743                 c = FilterAction::ComplexFilter;
0744             }
0745             else if (categoryString == QLatin1String("documentedHistory"))
0746             {
0747                 c = FilterAction::DocumentedHistory;
0748             }
0749 
0750             FilterAction action(stream.attributes().value(QLatin1String("filterName")).toString(),
0751                                 stream.attributes().value(QLatin1String("filterVersion")).toString().toInt(), c);
0752             action.setDisplayableName(stream.attributes().value(QLatin1String("filterDisplayName")).toString());
0753 
0754             if (stream.attributes().value(QLatin1String("branch")) == QLatin1String("true"))
0755             {
0756                 action.addFlag(FilterAction::ExplicitBranch);
0757             }
0758 
0759             while (stream.readNextStartElement())
0760             {
0761                 if (stream.name() == QLatin1String("params"))
0762                 {
0763                     while (stream.readNextStartElement())
0764                     {
0765                         if ((stream.name() == QLatin1String("param")) &&
0766                             stream.attributes().hasAttribute(QLatin1String("name")))
0767                         {
0768                             action.addParameter(stream.attributes().value(QLatin1String("name")).toString(),
0769                                                 stream.attributes().value(QLatin1String("value")).toString());
0770                             stream.skipCurrentElement();
0771                         }
0772                         else
0773                         {
0774                             stream.skipCurrentElement();
0775                         }
0776                     }
0777 
0778                 }
0779                 else
0780                 {
0781                     stream.skipCurrentElement();
0782                 }
0783             }
0784 
0785             h << action;
0786         }
0787         else
0788         {
0789             stream.skipCurrentElement();
0790         }
0791     }
0792 
0793     if (stream.hasError())
0794     {
0795         // TODO: error handling
0796 
0797         qCDebug(DIGIKAM_DIMG_LOG) << "An error occurred during parsing: " << stream.errorString();
0798     }
0799 
0800 /*
0801     qCDebug(DIGIKAM_DIMG_LOG) << "Parsing done";
0802 */
0803     return h;
0804 }
0805 
0806 } // namespace digikam