diff --git a/ImageManager/ThumbnailCache.cpp b/ImageManager/ThumbnailCache.cpp index 957f2670..60f35c68 100644 --- a/ImageManager/ThumbnailCache.cpp +++ b/ImageManager/ThumbnailCache.cpp @@ -1,499 +1,528 @@ /* Copyright (C) 2003-2020 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ThumbnailCache.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include namespace { // We split the thumbnails into chunks to avoid a huge file changing over and over again, with a bad hit for backups constexpr int MAX_FILE_SIZE = 32 * 1024 * 1024; constexpr int THUMBNAIL_FILE_VERSION_MIN = 4; // We map some thumbnail files into memory and manage them in a least-recently-used fashion constexpr size_t LRU_SIZE = 2; constexpr int THUMBNAIL_CACHE_SAVE_INTERNAL_MS = (5 * 1000); const auto INDEXFILE_NAME = QString::fromLatin1("thumbnailindex"); } namespace ImageManager { /** * The ThumbnailMapping wraps the memory-mapped data of a QFile. * Upon initialization with a file name, the corresponding file is opened * and its contents mapped into memory (as a QByteArray). * * Deleting the ThumbnailMapping unmaps the memory and closes the file. */ class ThumbnailMapping { public: ThumbnailMapping(const QString &filename) : file(filename) , map(nullptr) { if (!file.open(QIODevice::ReadOnly)) qCWarning(ImageManagerLog, "Failed to open thumbnail file"); uchar *data = file.map(0, file.size()); if (!data || QFile::NoError != file.error()) { qCWarning(ImageManagerLog, "Failed to map thumbnail file"); } else { map = QByteArray::fromRawData(reinterpret_cast(data), file.size()); } } bool isValid() { return !map.isEmpty(); } // we need to keep the file around to keep the data mapped: QFile file; QByteArray map; }; QString defaultThumbnailDirectory() { return QString::fromLatin1(".thumbnails/"); } } ImageManager::ThumbnailCache::ThumbnailCache(const QString &baseDirectory) : m_baseDir(baseDirectory) , m_currentFile(0) , m_currentOffset(0) , m_timer(new QTimer) , m_needsFullSave(true) , m_isDirty(false) , m_memcache(new QCache(LRU_SIZE)) , m_currentWriter(nullptr) { const QString dir = thumbnailPath(QString()); if (!QFile::exists(dir)) QDir().mkpath(dir); // set a default value for version 4 files and new databases: m_thumbnailSize = Settings::SettingsData::instance()->thumbnailSize(); load(); connect(this, &ImageManager::ThumbnailCache::doSave, this, &ImageManager::ThumbnailCache::saveImpl); connect(m_timer, &QTimer::timeout, this, &ImageManager::ThumbnailCache::saveImpl); m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); m_timer->setSingleShot(true); m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); } ImageManager::ThumbnailCache::~ThumbnailCache() { m_needsFullSave = true; saveInternal(); delete m_memcache; delete m_timer; if (m_currentWriter) delete m_currentWriter; } void ImageManager::ThumbnailCache::insert(const DB::FileName &name, const QImage &image) { QMutexLocker thumbnailLocker(&m_thumbnailWriterLock); if (!m_currentWriter) { m_currentWriter = new QFile(fileNameForIndex(m_currentFile)); if (!m_currentWriter->open(QIODevice::ReadWrite)) { qCWarning(ImageManagerLog, "Failed to open thumbnail file for inserting"); return; } } if (!m_currentWriter->seek(m_currentOffset)) { qCWarning(ImageManagerLog, "Failed to seek in thumbnail file"); return; } QMutexLocker dataLocker(&m_dataLock); // purge in-memory cache for the current file: m_memcache->remove(m_currentFile); QByteArray data; QBuffer buffer(&data); bool OK = buffer.open(QIODevice::WriteOnly); Q_ASSERT(OK); Q_UNUSED(OK); OK = image.save(&buffer, "JPG"); Q_ASSERT(OK); const int size = data.size(); if (!(m_currentWriter->write(data.data(), size) == size && m_currentWriter->flush())) { qCWarning(ImageManagerLog, "Failed to write image data to thumbnail file"); return; } if (m_currentOffset + size > MAX_FILE_SIZE) { delete m_currentWriter; m_currentWriter = nullptr; } thumbnailLocker.unlock(); if (m_hash.contains(name)) { CacheFileInfo info = m_hash[name]; if (info.fileIndex == m_currentFile && info.offset == m_currentOffset && info.size == size) { qCDebug(ImageManagerLog) << "Found duplicate thumbnail " << name.relative() << "but no change in information"; dataLocker.unlock(); return; } else { // File has moved; incremental save does no good. qCDebug(ImageManagerLog) << "Found duplicate thumbnail " << name.relative() << " at new location, need full save! "; m_saveLock.lock(); m_needsFullSave = true; m_saveLock.unlock(); } } m_hash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, size)); m_isDirty = true; m_unsavedHash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, size)); // Update offset m_currentOffset += size; if (m_currentOffset > MAX_FILE_SIZE) { m_currentFile++; m_currentOffset = 0; } int unsaved = m_unsavedHash.count(); dataLocker.unlock(); // Thumbnail building is a lot faster now. Even on an HDD this corresponds to less // than 1 minute of work. // // We need to call the internal version that does not interact with the timer. // We can't simply signal from here because if we're in the middle of loading new // images the signal won't get invoked until we return to the main application loop. if (unsaved >= 100) { saveInternal(); } } QString ImageManager::ThumbnailCache::fileNameForIndex(int index) const { return thumbnailPath(QString::fromLatin1("thumb-") + QString::number(index)); } QPixmap ImageManager::ThumbnailCache::lookup(const DB::FileName &name) const { auto array = lookupRawData(name); if (array.isNull()) return QPixmap(); QBuffer buffer(&array); buffer.open(QIODevice::ReadOnly); QImage image; image.load(&buffer, "JPG"); // Notice the above image is sharing the bits with the file, so I can't just return it as it then will be invalid when the file goes out of scope. // PENDING(blackie) Is that still true? return QPixmap::fromImage(image); } QByteArray ImageManager::ThumbnailCache::lookupRawData(const DB::FileName &name) const { m_dataLock.lock(); CacheFileInfo info = m_hash[name]; m_dataLock.unlock(); ThumbnailMapping *t = m_memcache->object(info.fileIndex); if (!t || !t->isValid()) { t = new ThumbnailMapping(fileNameForIndex(info.fileIndex)); if (!t->isValid()) { qCWarning(ImageManagerLog, "Failed to map thumbnail file"); return QByteArray(); } m_memcache->insert(info.fileIndex, t); } QByteArray array(t->map.mid(info.offset, info.size)); return array; } void ImageManager::ThumbnailCache::saveFull() const { // First ensure that any dirty thumbnails are written to disk m_thumbnailWriterLock.lock(); if (m_currentWriter) { delete m_currentWriter; m_currentWriter = nullptr; } m_thumbnailWriterLock.unlock(); QMutexLocker dataLocker(&m_dataLock); if (!m_isDirty) { return; } QTemporaryFile file; if (!file.open()) { qCWarning(ImageManagerLog, "Failed to create temporary file"); return; } QHash tempHash = m_hash; m_unsavedHash.clear(); m_needsFullSave = false; // Clear the dirty flag early so that we can allow further work to proceed. // If the save fails, we'll set the dirty flag again. m_isDirty = false; m_fileVersion = preferredFileVersion(); dataLocker.unlock(); QDataStream stream(&file); stream << preferredFileVersion() << m_thumbnailSize << m_currentFile << m_currentOffset << m_hash.count(); for (auto it = tempHash.constBegin(); it != tempHash.constEnd(); ++it) { const CacheFileInfo &cacheInfo = it.value(); stream << it.key().relative() << cacheInfo.fileIndex << cacheInfo.offset << cacheInfo.size; } file.close(); const QString realFileName = thumbnailPath(INDEXFILE_NAME); QFile::remove(realFileName); if (!file.copy(realFileName)) { qCWarning(ImageManagerLog, "Failed to copy the temporary file %s to %s", qPrintable(file.fileName()), qPrintable(realFileName)); dataLocker.relock(); m_isDirty = true; m_needsFullSave = true; } else { QFile realFile(realFileName); realFile.open(QIODevice::ReadOnly); realFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther); realFile.close(); } } // Incremental save does *not* clear the dirty flag. We always want to do a full // save eventually. void ImageManager::ThumbnailCache::saveIncremental() const { m_thumbnailWriterLock.lock(); if (m_currentWriter) { delete m_currentWriter; m_currentWriter = nullptr; } m_thumbnailWriterLock.unlock(); QMutexLocker dataLocker(&m_dataLock); if (m_unsavedHash.count() == 0) { return; } QHash tempUnsavedHash = m_unsavedHash; m_unsavedHash.clear(); m_isDirty = true; const QString realFileName = thumbnailPath(INDEXFILE_NAME); QFile file(realFileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) { qCWarning(ImageManagerLog, "Failed to open thumbnail cache for appending"); m_needsFullSave = true; return; } QDataStream stream(&file); for (auto it = tempUnsavedHash.constBegin(); it != tempUnsavedHash.constEnd(); ++it) { const CacheFileInfo &cacheInfo = it.value(); stream << it.key().relative() << cacheInfo.fileIndex << cacheInfo.offset << cacheInfo.size; } file.close(); } void ImageManager::ThumbnailCache::saveInternal() const { m_saveLock.lock(); const QString realFileName = thumbnailPath(INDEXFILE_NAME); // If something has asked for a full save, do it! if (m_needsFullSave || !QFile(realFileName).exists()) { saveFull(); } else { saveIncremental(); } m_saveLock.unlock(); } void ImageManager::ThumbnailCache::saveImpl() const { m_timer->stop(); saveInternal(); m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); m_timer->setSingleShot(true); m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); } void ImageManager::ThumbnailCache::save() const { m_saveLock.lock(); m_needsFullSave = true; m_saveLock.unlock(); emit doSave(); } void ImageManager::ThumbnailCache::load() { QFile file(thumbnailPath(INDEXFILE_NAME)); if (!file.exists()) return; QElapsedTimer timer; timer.start(); file.open(QIODevice::ReadOnly); QDataStream stream(&file); stream >> m_fileVersion; if (m_fileVersion != preferredFileVersion() && m_fileVersion != THUMBNAIL_FILE_VERSION_MIN) return; //Discard cache // We can't allow anything to modify the structure while we're doing this. QMutexLocker dataLocker(&m_dataLock); if (m_fileVersion == THUMBNAIL_FILE_VERSION_MIN) { qCInfo(ImageManagerLog) << "Loading thumbnail index version " << m_fileVersion << "- assuming thumbnail size" << m_thumbnailSize << "px"; } else { stream >> m_thumbnailSize; qCDebug(ImageManagerLog) << "Thumbnail cache has thumbnail size" << m_thumbnailSize << "px"; } int count = 0; stream >> m_currentFile >> m_currentOffset >> count; while (!stream.atEnd()) { QString name; int fileIndex; int offset; int size; stream >> name >> fileIndex >> offset >> size; m_hash.insert(DB::FileName::fromRelativePath(name), CacheFileInfo(fileIndex, offset, size)); if (fileIndex > m_currentFile) { m_currentFile = fileIndex; m_currentOffset = offset + size; } else if (fileIndex == m_currentFile && offset + size > m_currentOffset) { m_currentOffset = offset + size; } if (m_currentOffset > MAX_FILE_SIZE) { m_currentFile++; m_currentOffset = 0; } count++; } qCDebug(TimingLog) << "Loaded thumbnails in " << timer.elapsed() / 1000.0 << " seconds"; } bool ImageManager::ThumbnailCache::contains(const DB::FileName &name) const { QMutexLocker dataLocker(&m_dataLock); bool answer = m_hash.contains(name); return answer; } QString ImageManager::ThumbnailCache::thumbnailPath(const QString &file) const { return m_baseDir + file; } int ImageManager::ThumbnailCache::thumbnailSize() const { return m_thumbnailSize; } int ImageManager::ThumbnailCache::actualFileVersion() const { return m_fileVersion; } int ImageManager::ThumbnailCache::preferredFileVersion() { return 5; } +DB::FileNameList ImageManager::ThumbnailCache::findIncorrectlySizedThumbnails() const +{ + QMutexLocker dataLocker(&m_dataLock); + const QHash tempHash = m_hash; + dataLocker.unlock(); + + // accessing the data directly instead of using the lookupRawData() method + // may be more efficient, but this method should be called rarely + // and readability therefore trumps performance + DB::FileNameList resultList; + for (auto it = m_hash.constBegin(); it != m_hash.constEnd(); ++it) { + const auto filename = it.key(); + auto jpegData = lookupRawData(filename); + Q_ASSERT(!jpegData.isNull()); + + QBuffer buffer(&jpegData); + buffer.open(QIODevice::ReadOnly); + QImage image; + image.load(&buffer, "JPG"); + const auto size = image.size(); + if (size.width() != m_thumbnailSize && size.height() != m_thumbnailSize) { + qCInfo(ImageManagerLog) << "Thumbnail for file " << filename.relative() << "has incorrect size:" << size; + resultList.append(filename); + } + } + + return resultList; +} + void ImageManager::ThumbnailCache::flush() { QMutexLocker dataLocker(&m_dataLock); for (int i = 0; i <= m_currentFile; ++i) QFile::remove(fileNameForIndex(i)); m_currentFile = 0; m_currentOffset = 0; m_isDirty = true; m_hash.clear(); m_unsavedHash.clear(); m_memcache->clear(); dataLocker.unlock(); save(); } void ImageManager::ThumbnailCache::removeThumbnail(const DB::FileName &fileName) { QMutexLocker dataLocker(&m_dataLock); m_isDirty = true; m_hash.remove(fileName); dataLocker.unlock(); save(); } void ImageManager::ThumbnailCache::removeThumbnails(const DB::FileNameList &files) { QMutexLocker dataLocker(&m_dataLock); m_isDirty = true; for (const DB::FileName &fileName : files) { m_hash.remove(fileName); } dataLocker.unlock(); save(); } void ImageManager::ThumbnailCache::setThumbnailSize(int thumbSize) { if (thumbSize < 0) return; if (thumbSize != m_thumbnailSize) { m_thumbnailSize = thumbSize; flush(); emit cacheInvalidated(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ThumbnailCache.h b/ImageManager/ThumbnailCache.h index 0b0d9b79..49f78300 100644 --- a/ImageManager/ThumbnailCache.h +++ b/ImageManager/ThumbnailCache.h @@ -1,214 +1,223 @@ /* Copyright (C) 2003-2020 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef THUMBNAILCACHE_H #define THUMBNAILCACHE_H #include "CacheFileInfo.h" #include #include #include #include #include template class QCache; namespace ImageManager { class ThumbnailMapping; /** * @brief The ThumbnailCache implements thumbnail storage optimized for speed. * * ## On-disk storage * The problem with the FreeDesktop.org thumbnail storage is that there is one file per image. * This means that showing a full page of thumbnails, containing dozens of images requires many * file operations. * * Our storage scheme introduces a single index file (\c thumbnailindex) that contains * the index of known thumbnails and their location in the thumbnail storage files (\c thumb-N). * The thumbnail storage files contain raw JPEG thumbnail data. * * This layout creates far less files on the filesystem, * the files can be memory-mapped, and because similar sets of images are often * shown together, data locality is used to our advantage. * * ## Caveats * Note that thumbnails are only ever added, never deleted from the thumbnail files. * Old images remain in the thumbnail files - they are just removed from the index file. * * ## Further reading * - https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html */ class ThumbnailCache : public QObject { Q_OBJECT public: /** * @brief ThumbnailCache * Provide access to a KPhotoAlbum-style thumbnail cache in the given directory. * @param baseDirectory the directory in which the \c thumbnailindex file resides. */ ThumbnailCache(const QString &baseDirectory); ~ThumbnailCache() override; /** * @brief Insert a thumbnail for the given file. * @param name the image file name * @param image the thumbnail data */ void insert(const DB::FileName &name, const QImage &image); /** * @brief lookup and return the thumbnail for the given file. + * Note: this method requires a GuiApplication to exist. * @param name the image file name * @return a QPixmap containing the thumbnail, or a null QPixmap if no thumbnail was found. */ QPixmap lookup(const DB::FileName &name) const; /** * @brief lookupRawData * @param name the image file name * @return the raw JPEG thumbnail data or a null QByteArray. */ QByteArray lookupRawData(const DB::FileName &name) const; /** * @brief Check if the ThumbnailCache contains a thumbnail for the given file. * @param name the image file name * @return \c true if the thumbnail exists, \c false otherwise. */ bool contains(const DB::FileName &name) const; /** * @brief "Forget" the thumbnail for an image. * @param name the image file name */ void removeThumbnail(const DB::FileName &name); /** * @brief "Forget" the thumbnails for the given images. * Like removeThumbnail(), but for a list of images * @param names a list of image file names */ void removeThumbnails(const DB::FileNameList &names); /** * @brief thumbnailSize * Usually, this is the size of the thumbnails in the cache. * If the index file was converted from an older file version (4), * the size is read from the configuration file. * @return the current thumbnail size. */ int thumbnailSize() const; /** * @brief Returns the file format version of the thumbnailindex file currently on disk. * * Usually, this is equal to the current version, but if an old ThumbnailCache * that is still compatible with this version of KPhotoAlbum is loaded and was not yet stored, * it may differ. * @return 4 or 5 */ int actualFileVersion() const; /** * @brief Version of the tumbnailindex file when saved. * @return The file format version of the thumbnailindex file. */ static int preferredFileVersion(); + /** + * @brief Check all thumbnails for consistency with thumbnailSize(). + * Only the thumbnails which are saved to disk are checked. + * If you have changed changed the cache you need to save if to guarantee correct results. + * @return all thumbnails that do not match the expected image dimensions. + */ + DB::FileNameList findIncorrectlySizedThumbnails() const; + public slots: /** * @brief Save the thumbnail cache to disk. */ void save() const; /** * @brief Invalidate the ThumbnailCache and remove the thumbnail files and index. */ void flush(); /** * @brief setThumbnailSize sets the thumbnail size recorded in the thumbnail index file. * If the value changes, the thumbnail cache is invalidated. * Except minimal sanity checks, no bounds for thumbnailSize are enforced. * @param thumbnailSize */ void setThumbnailSize(int thumbnailSize); signals: /** * @brief doSave is emitted when save() is called. * This signal is more or less an internal signal. */ void doSave() const; /** * @brief cacheInvalidated is emitted when the thumbnails are no longer valid. * This usually happens when the thumbnail size changed. * This signal is *not* emitted when the cache was flushed by explicit request. */ void cacheInvalidated(); private: /** * @brief load the \c thumbnailindex file if possible. * This function populates the thumbnail hash, but does not * load any actual thumbnail data. * If the file does not exist, or if it is not compatible, * then it is discarded. */ void load(); QString fileNameForIndex(int index) const; QString thumbnailPath(const QString &fileName) const; // mutable because saveIncremental is const mutable int m_fileVersion = -1; int m_thumbnailSize = -1; const QString m_baseDir; QHash m_hash; mutable QHash m_unsavedHash; /* Protects accesses to the data (hash and unsaved hash) */ mutable QMutex m_dataLock; /* Prevents multiple saves from happening simultaneously */ mutable QMutex m_saveLock; /* Protects writing thumbnails to disk */ mutable QMutex m_thumbnailWriterLock; int m_currentFile; int m_currentOffset; mutable QTimer *m_timer; mutable bool m_needsFullSave; mutable bool m_isDirty; void saveFull() const; void saveIncremental() const; void saveInternal() const; void saveImpl() const; /** * Holds an in-memory cache of thumbnail files. */ mutable QCache *m_memcache; mutable QFile *m_currentWriter; }; /** * @brief defaultThumbnailDirectory * @return the default thumbnail (sub-)directory name, e.g. ".thumbnails" */ QString defaultThumbnailDirectory(); } #endif /* THUMBNAILCACHE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/kpa-thumbnailtool/main.cpp b/kpa-thumbnailtool/main.cpp index 193bbc28..f982579b 100644 --- a/kpa-thumbnailtool/main.cpp +++ b/kpa-thumbnailtool/main.cpp @@ -1,122 +1,140 @@ /* Copyright (C) 2020 The KPhotoAlbum development team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Logging.h" #include "ThumbnailCacheConverter.h" #include "version.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KPAThumbnailTool; int main(int argc, char **argv) { KLocalizedString::setApplicationDomain("kphotoalbum"); QCoreApplication app(argc, argv); KAboutData aboutData( QStringLiteral("kpa-thumbnailtool"), //component name i18n("KPhotoAlbum Thumbnail Tool"), // display name QStringLiteral(KPA_VERSION), i18n("Tool for inspecting and editing the KPhotoAlbum thumbnail cache"), // short description KAboutLicense::GPL, i18n("Copyright (C) 2020 The KPhotoAlbum Development Team"), // copyright statement QString(), // other text QStringLiteral("https://www.kphotoalbum.org") // homepage ); aboutData.setOrganizationDomain("kde.org"); // maintainer is expected to be the first entry // Note: I like to sort by name, grouped by active/inactive; // Jesper gets ranked with the active authors for obvious reasons aboutData.addAuthor(i18n("Johannes Zarl-Zierl"), i18n("Development, Maintainer"), QStringLiteral("johannes@zarl-zierl.at")); aboutData.addAuthor(i18n("Robert Krawitz"), i18n("Development, Optimization"), QStringLiteral("rlk@alum.mit.edu")); aboutData.addAuthor(i18n("Tobias Leupold"), i18n("Development, Releases, Website"), QStringLiteral("tobias.leupold@gmx.de")); aboutData.addAuthor(i18n("Jesper K. Pedersen"), i18n("Former Maintainer, Project Creator"), QStringLiteral("blackie@kde.org")); // not currently active: aboutData.addAuthor(i18n("Hassan Ibraheem"), QString(), QStringLiteral("hasan.ibraheem@gmail.com")); aboutData.addAuthor(i18n("Jan Kundrát"), QString(), QStringLiteral("jkt@gentoo.org")); aboutData.addAuthor(i18n("Andreas Neustifter"), QString(), QStringLiteral("andreas.neustifter@gmail.com")); aboutData.addAuthor(i18n("Tuomas Suutari"), QString(), QStringLiteral("thsuut@utu.fi")); aboutData.addAuthor(i18n("Miika Turkia"), QString(), QStringLiteral("miika.turkia@gmail.com")); aboutData.addAuthor(i18n("Henner Zeller"), QString(), QStringLiteral("h.zeller@acm.org")); // initialize the commandline parser QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); QCommandLineOption infoOption { QString::fromUtf8("info"), i18nc("@info:shell", "Print information about thumbnail cache.") }; parser.addOption(infoOption); QCommandLineOption convertV5ToV4Option { QString::fromUtf8("convertV5ToV4"), i18nc("@info:shell", "Convert thumbnailindex to format suitable for KPhotoAlbum >= 4.3.") }; parser.addOption(convertV5ToV4Option); + QCommandLineOption verifyOption { QString::fromUtf8("verify"), i18nc("@info:shell", "Verify thumbnail cache consistency.") }; + parser.addOption(verifyOption); parser.addPositionalArgument(QString::fromUtf8("imageDir"), i18nc("@info:shell", "The directory containing the .thumbnail directory.")); KAboutData::setApplicationData(aboutData); aboutData.setupCommandLine(&parser); parser.process(app); aboutData.processCommandLine(&parser); QTextStream console { stdout }; const auto args = parser.positionalArguments(); if (args.empty()) { qWarning("Missing argument!"); return 1; } const auto imageDir = QDir { args.first() }; if (!imageDir.exists()) { qWarning("Not a directory!"); return 1; } if (parser.isSet(convertV5ToV4Option)) { const QString indexFile = imageDir.absoluteFilePath(QString::fromUtf8(".thumbnails/thumbnailindex")); return convertV5ToV4Cache(indexFile); } + + int returnValue = 0; + DB::DummyUIDelegate uiDelegate; + Settings::SettingsData::setup(imageDir.path(), uiDelegate); + const auto thumbnailDir = imageDir.absoluteFilePath(ImageManager::defaultThumbnailDirectory()); + const ImageManager::ThumbnailCache cache { thumbnailDir }; if (parser.isSet(infoOption)) { - DB::DummyUIDelegate uiDelegate; - Settings::SettingsData::setup(imageDir.path(), uiDelegate); - const auto thumbnailDir = imageDir.absoluteFilePath(ImageManager::defaultThumbnailDirectory()); - const ImageManager::ThumbnailCache cache { thumbnailDir }; console << i18nc("@info:shell", "Thumbnail cache directory: %1\n", thumbnailDir); console << i18nc("@info:shell", "Thumbnailindex file version: %1\n", cache.actualFileVersion()); console << i18nc("@info:shell", "Maximum supported thumbnailindex file version: %1\n", cache.preferredFileVersion()); console << i18nc("@info:shell", "Thumbnail storage size: %1\n", cache.thumbnailSize()); if (cache.actualFileVersion() < 5) { console << i18nc("@info:shell", "Note: Thumbnail storage size is defined in the configuration file prior to v5.\n"); } } + if (parser.isSet(verifyOption)) { + const auto incorrectDimensions = cache.findIncorrectlySizedThumbnails(); + if (incorrectDimensions.isEmpty()) { + console << i18nc("@info:shell", "No inconsistencies found.\n"); + } else { + returnValue = 1; + console << i18nc("@info:shell This line is printed before a list of file names.", "The following thumbnails appear to have incorrect sizes:\n"); + for (const auto &filename : incorrectDimensions) { + console << filename.absolute() << "\n"; + } + } + } - // immediately quit the event loop: - QTimer::singleShot(0, &app, &QCoreApplication::quit); - return QCoreApplication::exec(); + return returnValue; + // so far, we don't need an event loop: + //// immediately quit the event loop: + //QTimer::singleShot(0, &app, &QCoreApplication::quit); + //return QCoreApplication::exec(); } // vi:expandtab:tabstop=4 shiftwidth=4: