File indexing completed on 2025-01-05 03:57:25

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2010-06-18
0007  * Description : class for determining new file name in terms of version management
0008  *
0009  * SPDX-FileCopyrightText: 2010-2011 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 "versionmanager.h"
0017 
0018 // Qt includes
0019 
0020 #include <QDir>
0021 #include <QUrl>
0022 
0023 // KDE includes
0024 
0025 #include <kconfiggroup.h>
0026 
0027 // Local includes
0028 
0029 #include "digikam_debug.h"
0030 #include "dimgfiltermanager.h"
0031 
0032 namespace Digikam
0033 {
0034 
0035 class Q_DECL_HIDDEN VersionNameCreator
0036 {
0037 public:
0038 
0039     VersionNameCreator(const VersionFileInfo& loadedFile,
0040                        const DImageHistory& m_resolvedInitialHistory, const DImageHistory& m_currentHistory,
0041                        VersionManager* const q);
0042 
0043     void checkNeedNewVersion();
0044     void fork();
0045     void setSaveDirectory();
0046     void setSaveFormat();
0047     void setSaveFileName();
0048     void setSaveDirectory(const QString& path);
0049     void setSaveFormat(const QString& format);
0050     void setSaveFileName(const QString& name);
0051     void initOperation();
0052     void checkIntermediates();
0053 
0054 protected:
0055 
0056     VersionFileInfo nextIntermediate(const QString& format);
0057     void setFileSuffix(QString& fileName, const QString& format) const;
0058     void addFileSuffix(QString& baseName, const QString& format, const QString& originalName = QString()) const;
0059 
0060 public:
0061 
0062     VersionManagerSettings m_settings;
0063 
0064     VersionFileInfo        m_result;
0065     VersionFileInfo        m_loadedFile;
0066 
0067     VersionFileOperation   m_operation;
0068 
0069     const DImageHistory    m_resolvedInitialHistory;
0070     const DImageHistory    m_currentHistory;
0071 
0072     bool                   m_fromRaw;
0073     bool                   m_newVersion;
0074 
0075     QVariant               m_version;
0076     QVariant               m_intermediateCounter;
0077     QString                m_baseName;
0078     QString                m_intermediatePath;
0079 
0080     VersionManager* const  q;
0081 };
0082 
0083 VersionNameCreator::VersionNameCreator(const VersionFileInfo& loadedFile,
0084                                        const DImageHistory& m_resolvedInitialHistory,
0085                                        const DImageHistory& m_currentHistory,
0086                                        VersionManager* const q)
0087     : m_settings(q->settings()),
0088       m_loadedFile(loadedFile),
0089       m_resolvedInitialHistory(m_resolvedInitialHistory),
0090       m_currentHistory(m_currentHistory),
0091       m_fromRaw(false),
0092       m_newVersion(false), q(q)
0093 {
0094     m_loadedFile.format   = m_loadedFile.format.toUpper();
0095     m_fromRaw             = (m_loadedFile.format.startsWith(QLatin1String("RAW"))); // also accept RAW-... format
0096     m_version             = q->namingScheme()->initialCounter();
0097     m_intermediateCounter = q->namingScheme()->initialCounter();
0098 }
0099 
0100 void VersionNameCreator::checkNeedNewVersion()
0101 {
0102     // First we check if we have any other files available.
0103     // The resolved initial history contains only referred files found in the collection
0104     // Note: The loaded file will have type Current
0105 
0106     qCDebug(DIGIKAM_GENERAL_LOG) << m_resolvedInitialHistory.hasReferredImageOfType(HistoryImageId::Original)
0107                                  << m_resolvedInitialHistory.hasReferredImageOfType(HistoryImageId::Intermediate)
0108                                  << m_fromRaw << q->workspaceFileFormats().contains(m_loadedFile.format);
0109 
0110     if       (!m_resolvedInitialHistory.hasReferredImageOfType(HistoryImageId::Original) &&
0111               !m_resolvedInitialHistory.hasReferredImageOfType(HistoryImageId::Intermediate))
0112     {
0113         m_newVersion = true;
0114     }
0115     else if (m_fromRaw || !q->workspaceFileFormats().contains(m_loadedFile.format))
0116     {
0117         // We check the loaded format: If it is not one of the workspace formats, or even raw, we need a new version
0118         m_newVersion = true;
0119     }
0120     else
0121     {
0122         m_newVersion = false;
0123     }
0124 }
0125 
0126 void VersionNameCreator::fork()
0127 {
0128     m_newVersion = true;
0129 }
0130 
0131 void VersionNameCreator::setSaveDirectory()
0132 {
0133     m_result.path      = q->namingScheme()->directory(m_loadedFile.path, m_loadedFile.fileName);
0134     m_intermediatePath = q->namingScheme()->directory(m_loadedFile.path, m_loadedFile.fileName);
0135 }
0136 
0137 void VersionNameCreator::setSaveDirectory(const QString& path)
0138 {
0139     m_result.path      = path;
0140     m_intermediatePath = path;
0141 }
0142 
0143 void VersionNameCreator::setSaveFormat()
0144 {
0145     m_result.format = m_settings.format;
0146 /*
0147     if (m_fromRaw)
0148     {
0149         m_result.format = m_settings.formatForStoringRAW;
0150     }
0151     else
0152     {
0153         m_result.format = m_settings.formatForStoringSubversions;
0154     }
0155 */
0156 }
0157 
0158 void VersionNameCreator::setSaveFormat(const QString& override)
0159 {
0160     m_result.format = override;
0161 }
0162 
0163 void VersionNameCreator::setSaveFileName()
0164 {
0165     qCDebug(DIGIKAM_GENERAL_LOG) << "need new version" << m_newVersion;
0166 
0167     VersionNamingScheme* const scheme = q->namingScheme();
0168 
0169     // initialize m_baseName, m_version, and m_intermediateCounter for intermediates
0170 
0171     m_baseName                        = scheme->baseName(m_loadedFile.path,
0172                                                          m_loadedFile.fileName,
0173                                                          &m_version,
0174                                                          &m_intermediateCounter);
0175 
0176     qCDebug(DIGIKAM_GENERAL_LOG) << "analyzing file" << m_loadedFile.fileName << m_version << m_intermediateCounter;
0177 
0178     // Update the result format for TIFF and JPG to the existing original suffix in short or long format.
0179 
0180     int lastDot          = m_loadedFile.fileName.lastIndexOf(QLatin1Char('.'));
0181     const bool tifFormat = (m_result.format == QLatin1String("TIFF"));
0182     const bool jpgFormat = (m_result.format == QLatin1String("JPG"));
0183 
0184     if ((lastDot != -1) && (tifFormat || jpgFormat))
0185     {
0186         int extSize    = m_loadedFile.fileName.size() - lastDot - 1;
0187         QString suffix = m_loadedFile.fileName.right(extSize).toUpper();
0188 
0189         if (((suffix == QLatin1String("TIF"))  && tifFormat) ||
0190             ((suffix == QLatin1String("JPEG")) && jpgFormat))
0191         {
0192             m_result.format = suffix;
0193         }
0194     }
0195 
0196     if (!m_newVersion)
0197     {
0198         m_result.fileName = m_loadedFile.fileName;
0199 
0200         if (m_loadedFile.format != m_result.format)
0201         {
0202             setFileSuffix(m_result.fileName, m_result.format);
0203         }
0204     }
0205     else
0206     {
0207         QDir dirInfo(m_result.path);
0208 
0209         // To find the right number for the new version, go through all the items in the given dir,
0210         // the version number won't be bigger than count()
0211 
0212         for (uint i = 0 ; i <= dirInfo.count() ; ++i)
0213         {
0214             QString suggestedName = scheme->versionFileName(m_result.path, m_baseName, m_version);
0215 
0216             // Note: Always give a hard guarantee that the file does not exist
0217 
0218             if (dirInfo.entryList(QStringList() << suggestedName + QLatin1String(".*"), QDir::Files).isEmpty())
0219             {
0220                 m_result.fileName = suggestedName;
0221                 addFileSuffix(m_result.fileName, m_result.format, m_loadedFile.fileName);
0222                 break;
0223             }
0224 
0225             // increment for next attempt
0226 
0227             m_version = scheme->incrementedCounter(m_version);
0228         }
0229     }
0230 }
0231 
0232 void VersionNameCreator::setSaveFileName(const QString& fileName)
0233 {
0234     m_result.fileName = fileName;
0235     m_baseName        = fileName.section(QLatin1Char('.'), 0, 0);
0236 
0237     // m_version remains unknown
0238 }
0239 
0240 void VersionNameCreator::initOperation()
0241 {
0242     m_operation.loadedFile = m_loadedFile;
0243     m_operation.saveFile   = m_result;
0244 
0245     if (m_newVersion)
0246     {
0247         m_operation.tasks |= VersionFileOperation::NewFile;
0248     }
0249     else
0250     {
0251         if (m_result.fileName == m_loadedFile.fileName)
0252         {
0253             m_operation.tasks |= VersionFileOperation::Replace;
0254         }
0255         else
0256         {
0257             m_operation.tasks |= VersionFileOperation::SaveAndDelete;
0258         }
0259     }
0260 }
0261 
0262 void VersionNameCreator::checkIntermediates()
0263 {
0264     // call when task has been determined
0265 
0266     qCDebug(DIGIKAM_GENERAL_LOG) << "Will replace" << bool(m_operation.tasks & VersionFileOperation::Replace)
0267                                  << "save after each session" << bool(m_settings.saveIntermediateVersions & VersionManagerSettings::AfterEachSession)
0268                                  << "save after raw" << bool(m_settings.saveIntermediateVersions & VersionManagerSettings::AfterRawConversion)
0269                                  << "save when not repro" << bool(m_settings.saveIntermediateVersions & VersionManagerSettings::WhenNotReproducible);
0270 
0271     if ((m_settings.saveIntermediateVersions & VersionManagerSettings::AfterEachSession) &&
0272         (m_operation.tasks & VersionFileOperation::Replace))
0273     {
0274         // We want a snapshot after each session. The main file will be overwritten by the current state.
0275         // So we consider the current file as snapshot of the last session and move
0276         // it to an intermediate before it is overwritten
0277 
0278         m_operation.tasks |= VersionFileOperation::MoveToIntermediate;
0279         m_operation.intermediateForLoadedFile = nextIntermediate(m_loadedFile.format);
0280 
0281         // this amounts to storing firstStep - 1
0282     }
0283 
0284     // These are, inclusively, the first and last step the state after which we may have to store.
0285     // m_resolvedInitialHistory.size() - 1 is the loaded file
0286     // m_currentHistory.size() - 1 is the current version
0287 
0288     int firstStep = m_resolvedInitialHistory.size();
0289     int lastStep  = m_currentHistory.size() - 2; // index of last but one entry
0290 
0291     qCDebug(DIGIKAM_GENERAL_LOG) << "initial history" << m_resolvedInitialHistory.size()
0292                                  << "current history" << m_currentHistory.size()
0293                                  << "first step" << firstStep << "last step" << lastStep;
0294 
0295     if (lastStep < firstStep)
0296     {
0297         // only a single editing step, or even "backwards" in history (possible with redo)
0298         return;
0299     }
0300 
0301     if (!firstStep)
0302     {
0303         qCDebug(DIGIKAM_GENERAL_LOG) << "Invalid history: resolved initial history has no entries";
0304         firstStep = 1;
0305     }
0306 
0307     if (m_settings.saveIntermediateVersions & VersionManagerSettings::AfterRawConversion)
0308     {
0309         int rawConversionStep = -1;
0310 
0311         for (int i = firstStep ; i <= lastStep ; ++i)
0312         {
0313             if (DImgFilterManager::instance()->isRawConversion(m_currentHistory.action(i).identifier()))
0314             {
0315                 rawConversionStep = i;
0316                 // break? multiple raw conversion steps??
0317             }
0318         }
0319 
0320         if (rawConversionStep != -1)
0321         {
0322             m_operation.intermediates.insert(rawConversionStep, VersionFileInfo());
0323         }
0324     }
0325 
0326     if (m_settings.saveIntermediateVersions & VersionManagerSettings::WhenNotReproducible)
0327     {
0328         for (int i = firstStep ; i <= lastStep ; ++i)
0329         {
0330             qCDebug(DIGIKAM_GENERAL_LOG) << "step" << i
0331                                          << "is reproducible"
0332                                          << (m_currentHistory.action(i).category() == FilterAction::ReproducibleFilter);
0333 
0334             switch (m_currentHistory.action(i).category())
0335             {
0336                 case FilterAction::ReproducibleFilter:
0337                     break;
0338 
0339                 case FilterAction::ComplexFilter:
0340                 case FilterAction::DocumentedHistory:
0341                     m_operation.intermediates.insert(i, VersionFileInfo());
0342                     break;
0343             }
0344         }
0345     }
0346 
0347     qCDebug(DIGIKAM_GENERAL_LOG) << "Save intermediates after steps" << m_operation.intermediates.keys();
0348 
0349     if (!m_operation.intermediates.isEmpty())
0350     {
0351         m_operation.tasks |= VersionFileOperation::StoreIntermediates;
0352 
0353         // now all steps are available, already ordered thanks to QMap. Just fill in the empty VersionFileInfos.
0354 
0355         QMap<int,VersionFileInfo>::iterator it;
0356 
0357         for (it = m_operation.intermediates.begin() ; it != m_operation.intermediates.end() ; ++it)
0358         {
0359             it.value() = nextIntermediate(m_result.format);
0360         }
0361     }
0362 }
0363 
0364 VersionFileInfo VersionNameCreator::nextIntermediate(const QString& format)
0365 {
0366     VersionNamingScheme* const scheme = q->namingScheme();
0367 
0368     VersionFileInfo intermediate;
0369     intermediate.path                 = m_intermediatePath;
0370     intermediate.format               = format;
0371 
0372     QDir dirInfo(m_intermediatePath);
0373 
0374     for (uint i = 0 ; i <= dirInfo.count() ; ++i)
0375     {
0376         QString suggestedName = scheme->intermediateFileName(m_intermediatePath, m_baseName,
0377                                                              m_version, m_intermediateCounter);
0378 
0379         // it is important to increment before returning - we may have to produce a number of files
0380 
0381         m_intermediateCounter = scheme->incrementedCounter(m_intermediateCounter);
0382 
0383         // Note: Always give a hard guarantee that the file does not exist
0384 
0385         if (dirInfo.entryList(QStringList() << suggestedName + QLatin1String(".*"), QDir::Files).isEmpty())
0386         {
0387             intermediate.fileName = suggestedName;
0388             setFileSuffix(intermediate.fileName, format);
0389             break;
0390         }
0391     }
0392 
0393     return intermediate;
0394 }
0395 
0396 void VersionNameCreator::setFileSuffix(QString& fileName, const QString& format) const
0397 {
0398     if (fileName.isEmpty())
0399     {
0400         return;
0401     }
0402 
0403     // QFileInfo uses the same string manipulation
0404 
0405     int lastDot  = fileName.lastIndexOf(QLatin1Char('.'));
0406     bool isLower = true;
0407 
0408     if (lastDot == -1)
0409     {
0410         fileName += QLatin1Char('.');
0411         lastDot   = fileName.size() - 1;
0412     }
0413     else
0414     {
0415         isLower = fileName.at(fileName.size() - 1).isLower();
0416     }
0417 
0418     int suffixBegin = lastDot + 1;
0419 
0420     if (fileName.mid(suffixBegin).compare(format, Qt::CaseInsensitive))
0421     {
0422         fileName.replace(suffixBegin, fileName.size() - suffixBegin, isLower ? format.toLower() : format);
0423     }
0424 }
0425 
0426 void VersionNameCreator::addFileSuffix(QString& fileName, const QString& format, const QString& originalName) const
0427 {
0428     if (fileName.isEmpty())
0429     {
0430         return;
0431     }
0432 
0433     bool isLower = true;
0434 
0435     if (!originalName.isEmpty())
0436     {
0437         // if original name had upper case suffix, continue using upper case
0438 
0439         isLower = originalName.at(originalName.size() - 1).isLower();
0440     }
0441 
0442     if (!fileName.endsWith(QLatin1Char('.')))
0443     {
0444         fileName += QLatin1Char('.');
0445     }
0446 
0447     fileName += (isLower ? format.toLower() : format);
0448 }
0449 
0450 // ------------------------------------------------------------------------------------------------------
0451 
0452 class Q_DECL_HIDDEN VersionManager::VersionManagerPriv
0453 {
0454 public:
0455 
0456     VersionManagerPriv()
0457     {
0458         scheme = nullptr;
0459     }
0460 
0461     VersionManagerSettings     settings;
0462     VersionNamingScheme*       scheme;
0463 
0464     DefaultVersionNamingScheme defaultScheme;
0465 };
0466 
0467 // ------------------------------------------------------------------------------------------------------
0468 
0469 VersionManager::VersionManager()
0470     : d(new VersionManagerPriv)
0471 {
0472 }
0473 
0474 VersionManager::~VersionManager()
0475 {
0476     delete d->scheme;
0477     delete d;
0478 }
0479 
0480 void VersionManager::setSettings(const VersionManagerSettings& settings)
0481 {
0482     d->settings = settings;
0483 }
0484 
0485 VersionManagerSettings VersionManager::settings() const
0486 {
0487     return d->settings;
0488 }
0489 
0490 void VersionManager::setNamingScheme(VersionNamingScheme* scheme)
0491 {
0492     d->scheme = scheme;
0493 }
0494 
0495 VersionNamingScheme* VersionManager::namingScheme() const
0496 {
0497     if (d->scheme)
0498     {
0499         return d->scheme;
0500     }
0501     else
0502     {
0503         return &d->defaultScheme;
0504     }
0505 }
0506 
0507 VersionFileOperation VersionManager::operation(FileNameType request, const VersionFileInfo& loadedFile,
0508                                                const DImageHistory& initialResolvedHistory,
0509                                                const DImageHistory& currentHistory)
0510 {
0511     VersionNameCreator name(loadedFile, initialResolvedHistory, currentHistory, this);
0512 
0513     if (request == CurrentVersionName)
0514     {
0515         name.checkNeedNewVersion();
0516     }
0517     else if (request == NewVersionName)
0518     {
0519         name.fork();
0520     }
0521 
0522     name.setSaveDirectory();
0523     name.setSaveFormat();
0524     name.setSaveFileName();
0525     name.initOperation();
0526     name.checkIntermediates();
0527 
0528     return name.m_operation;
0529 }
0530 
0531 VersionFileOperation VersionManager::operationNewVersionInFormat(const VersionFileInfo& loadedFile,
0532                                                                  const QString& format,
0533                                                                  const DImageHistory& initialResolvedHistory,
0534                                                                  const DImageHistory& currentHistory)
0535 {
0536     VersionNameCreator name(loadedFile, initialResolvedHistory, currentHistory, this);
0537 
0538     name.fork();
0539     name.setSaveDirectory();
0540     name.setSaveFormat(format);
0541     name.setSaveFileName();
0542     name.initOperation();
0543     name.checkIntermediates();
0544 
0545     return name.m_operation;
0546 }
0547 
0548 VersionFileOperation VersionManager::operationNewVersionAs(const VersionFileInfo& loadedFile,
0549                                                            const VersionFileInfo& saveLocation,
0550                                                            const DImageHistory& initialResolvedHistory,
0551                                                            const DImageHistory& currentHistory)
0552 {
0553     VersionNameCreator name(loadedFile, initialResolvedHistory, currentHistory, this);
0554 
0555     name.fork();
0556     name.setSaveDirectory(saveLocation.path);
0557     name.setSaveFormat(saveLocation.format);
0558     name.setSaveFileName(saveLocation.fileName);
0559     name.initOperation();
0560     name.checkIntermediates();
0561 
0562     return name.m_operation;
0563 }
0564 
0565 QString VersionManager::toplevelDirectory(const QString& path)
0566 {
0567     Q_UNUSED(path);
0568 
0569     return QLatin1String("/");
0570 }
0571 
0572 QStringList VersionManager::workspaceFileFormats() const
0573 {
0574     QStringList formats;
0575     formats << QLatin1String("JPG") << QLatin1String("PNG") << QLatin1String("TIFF") << QLatin1String("PGF") << QLatin1String("JP2");
0576     QString f = d->settings.format.toUpper();
0577 
0578     if (!formats.contains(f))
0579     {
0580         formats << f;
0581     }
0582 /*
0583     f = d->settings.formatForStoringSubversions.toUpper();
0584 
0585     if (!formats.contains(f))
0586     {
0587         formats << f;
0588     }
0589 */
0590     return formats;
0591 }
0592 
0593 } // namespace Digikam