diff --git a/AndroidRemoteControl/RemoteInterface.h b/AndroidRemoteControl/RemoteInterface.h index dc2beb94..0cd50977 100644 --- a/AndroidRemoteControl/RemoteInterface.h +++ b/AndroidRemoteControl/RemoteInterface.h @@ -1,141 +1,141 @@ /* Copyright (C) 2014 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 REMOTEINTERFACE_H #define REMOTEINTERFACE_H #include "CategoryModel.h" #include "RemoteCommand.h" #include "SearchInfo.h" #include "ThumbnailModel.h" #include "DiscoveryModel.h" #include #include #include #include #include "History.h" #include "Types.h" class QTcpSocket; namespace RemoteControl { class Client; class RemoteInterface : public QObject { Q_OBJECT Q_PROPERTY( bool connected READ isConnected NOTIFY connectionChanged ) Q_PROPERTY(RemoteControl::CategoryModel* categories MEMBER m_categories NOTIFY categoriesChanged) Q_PROPERTY(ThumbnailModel* categoryItems MEMBER m_categoryItems NOTIFY categoryItemsChanged) Q_PROPERTY(QImage home MEMBER m_homeImage NOTIFY homeImageChanged) Q_PROPERTY(QImage kphotoalbum MEMBER m_kphotoalbumImage NOTIFY kphotoalbumImageChange) Q_PROPERTY(QImage discoveryImage MEMBER m_discoveryImage NOTIFY discoveryImageChanged) Q_PROPERTY(RemoteControl::Types::Page currentPage MEMBER m_currentPage NOTIFY currentPageChanged) Q_PROPERTY(ThumbnailModel* thumbnailModel MEMBER m_thumbnailModel NOTIFY thumbnailModelChanged) Q_PROPERTY(QStringList listCategoryValues MEMBER m_listCategoryValues NOTIFY listCategoryValuesChanged) Q_PROPERTY(DiscoveryModel* discoveryModel MEMBER m_discoveryModel NOTIFY discoveryModelChanged) Q_PROPERTY(ThumbnailModel* activeThumbnailModel MEMBER m_activeThumbnailModel NOTIFY activeThumbnailModelChanged) Q_PROPERTY(QString networkAddress READ networkAddress NOTIFY networkAddressChanged) Q_PROPERTY(QStringList tokens READ tokens NOTIFY tokensChanged) public: static RemoteInterface& instance(); bool isConnected() const; void sendCommand(const RemoteCommand& command); QString currentCategory() const; QImage discoveryImage() const; enum class ModelType {Thumbnail,Discovery}; void setActiveThumbnailModel(ModelType); public slots: void goHome(); void goBack(); void goForward(); void selectCategory(const QString& category, int /*CategoryViewType*/ type); void selectCategoryValue(const QString& value); void showThumbnails(); void showImage(int imageId); void requestDetails(int imageId); void activateSearch(const QString& search); void doDiscovery(); void showOverviewPage(); void setToken(int imageId, const QString& token); void removeToken(int imageId, const QString& token); void rerequestOverviewPageData(); void pushAwayFromStartupState(); signals: void connectionChanged(); void categoriesChanged(); void homeImageChanged(); - void kphotoalbumImageChange(); + void kphotoalbumImageChange(); void categoryItemsChanged(); void currentPageChanged(); void thumbnailModelChanged(); void jumpToImage(int index); void listCategoryValuesChanged(); void discoveryImageChanged(); void discoveryModelChanged(); void activeThumbnailModelChanged(); void networkAddressChanged(); void tokensChanged(); public: void setCurrentView(int imageId); QString networkAddress() const; QStringList tokens() const; private slots: void requestInitialData(); void handleCommand(const RemoteCommand&); void updateImage(const ThumbnailResult&); void updateCategoryList(const CategoryListResult&); void gotSearchResult(const SearchResult&); void requestHomePageImages(); void gotDisconnected(); private: RemoteInterface(); friend class Action; void setCurrentPage(Page page); void setListCategoryValues(const QStringList& values); void setHomePageImages(const StaticImageResult& command); Client* m_connection = nullptr; CategoryModel* m_categories; QImage m_homeImage; QImage m_kphotoalbumImage; SearchInfo m_search; ThumbnailModel* m_categoryItems; RemoteControl::Page m_currentPage = RemoteControl::Page::Startup; ThumbnailModel* m_thumbnailModel; History m_history; QStringList m_listCategoryValues; QImage m_discoveryImage; DiscoveryModel* m_discoveryModel; ThumbnailModel* m_activeThumbnailModel = nullptr; }; } #endif // REMOTEINTERFACE_H diff --git a/AndroidRemoteControl/ScreenInfo.h b/AndroidRemoteControl/ScreenInfo.h index ded907df..6011bee5 100644 --- a/AndroidRemoteControl/ScreenInfo.h +++ b/AndroidRemoteControl/ScreenInfo.h @@ -1,77 +1,77 @@ /* Copyright (C) 2014 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 REMOTECONTROL_SCREENINFO_H #define REMOTECONTROL_SCREENINFO_H #include #include class QScreen; namespace RemoteControl { class ScreenInfo :public QObject { Q_OBJECT Q_PROPERTY(double dotsPerMM MEMBER m_dotsPerMM CONSTANT) Q_PROPERTY(int overviewIconSize READ overviewIconSize NOTIFY overviewIconSizeChanged) Q_PROPERTY(int overviewColumnCount MEMBER m_overviewColumnCount NOTIFY overviewColumnCountChanged) Q_PROPERTY(int overviewSpacing READ overviewSpacing NOTIFY overviewSpacingChanged) Q_PROPERTY(int viewWidth MEMBER m_viewWidth NOTIFY viewWidthChanged) Q_PROPERTY(int viewHeight MEMBER m_viewHeight NOTIFY viewHeightChanged) Q_PROPERTY(int textHeight MEMBER m_textHeight NOTIFY textHeightChanged) public: static ScreenInfo& instance(); void setScreen(QScreen*); QSize pixelForSizeInMM(int size) const; void setCategoryCount(int count); QSize screenSize() const; QSize viewSize() const; int overviewIconSize() const; int overviewSpacing() const; signals: - void overviewIconSizeChanged(); + void overviewIconSizeChanged(); void overviewColumnCountChanged(); void overviewSpacingChanged(); void viewWidthChanged(); void viewHeightChanged(); void textHeightChanged(); private slots: void updateLayout(); private: ScreenInfo(); int possibleColumns(); int iconHeight(); QScreen* m_screen; double m_dotsPerMM; int m_categoryCount = 0; int m_overviewColumnCount = 0; int m_viewWidth = 0; int m_viewHeight; int m_textHeight; }; } // namespace RemoteControl #endif // REMOTECONTROL_SCREENINFO_H diff --git a/BackgroundJobs/ExtractOneThumbnailJob.cpp b/BackgroundJobs/ExtractOneThumbnailJob.cpp index c8d57347..e7b98a12 100644 --- a/BackgroundJobs/ExtractOneThumbnailJob.cpp +++ b/BackgroundJobs/ExtractOneThumbnailJob.cpp @@ -1,99 +1,99 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #include "ExtractOneThumbnailJob.h" #include #include #include #include #include #include #include #include namespace BackgroundJobs { ExtractOneThumbnailJob::ExtractOneThumbnailJob(const DB::FileName& fileName, int index, BackgroundTaskManager::Priority priority) : JobInterface(priority), m_fileName(fileName), m_index(index), m_wasCanceled(false) { Q_ASSERT( index >= 0 && index <= 9 ); } void ExtractOneThumbnailJob::execute() -{ +{ if ( m_wasCanceled || frameName().exists() ) emit completed(); else { DB::ImageInfoPtr info = DB::ImageDB::instance()->info(m_fileName); const int length = info->videoLength(); ImageManager::ExtractOneVideoFrame::extract(m_fileName, length*m_index/10.0, this, SLOT(frameLoaded(QImage))); } } QString ExtractOneThumbnailJob::title() const { return i18n("Extracting Thumbnail"); } QString ExtractOneThumbnailJob::details() const { return QString::fromLatin1("%1 #%2").arg(m_fileName.relative()).arg(m_index); } int ExtractOneThumbnailJob::index() const { return m_index; } void ExtractOneThumbnailJob::cancel() { m_wasCanceled = true; } void ExtractOneThumbnailJob::frameLoaded(const QImage& image) { if ( !image.isNull() ) { #if 0 QImage img = image; { QPainter painter(&img); QFont fnt; fnt.setPointSize(24); painter.setFont(fnt); painter.drawText(QPoint(100,100),QString::number(m_index)); } #endif Utilities::saveImage(frameName(), image, "JPEG"); } else { // Create empty file to avoid that we recheck at next start up. QFile file(frameName().absolute()); file.open(QFile::WriteOnly); file.close(); } emit completed(); } DB::FileName ExtractOneThumbnailJob::frameName() const { return BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(m_fileName, m_index); } } // namespace BackgroundJobs // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundJobs/ExtractOneThumbnailJob.h b/BackgroundJobs/ExtractOneThumbnailJob.h index 8c3bfcd2..62615c3f 100644 --- a/BackgroundJobs/ExtractOneThumbnailJob.h +++ b/BackgroundJobs/ExtractOneThumbnailJob.h @@ -1,60 +1,60 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef BACKGROUNDJOBS_EXTRACTONETHUMBNAILJOB_H #define BACKGROUNDJOBS_EXTRACTONETHUMBNAILJOB_H #include #include class QImage; namespace BackgroundJobs { /** \brief \ref BackgroundTaskManager::JobInterface "background job" for extracting the length of a video file. \see \ref videothumbnails */ class ExtractOneThumbnailJob : public BackgroundTaskManager::JobInterface { Q_OBJECT public: ExtractOneThumbnailJob(const DB::FileName& fileName, int index, BackgroundTaskManager::Priority priority); void execute() override; QString title() const override; QString details() const override; int index() const; void cancel(); private slots: void frameLoaded(const QImage& ); private: DB::FileName frameName() const; DB::FileName m_fileName; int m_index; bool m_wasCanceled; }; } // namespace BackgroundJobs #endif // BACKGROUNDJOBS_EXTRACTONETHUMBNAILJOB_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundJobs/HandleVideoThumbnailRequestJob.h b/BackgroundJobs/HandleVideoThumbnailRequestJob.h index cd73291a..5fa520a1 100644 --- a/BackgroundJobs/HandleVideoThumbnailRequestJob.h +++ b/BackgroundJobs/HandleVideoThumbnailRequestJob.h @@ -1,61 +1,61 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef BACKGROUNDJOBS_HANDLEVIDEOTHUMBNAILREQUESTJOB_H #define BACKGROUNDJOBS_HANDLEVIDEOTHUMBNAILREQUESTJOB_H #include #include namespace ImageManager { class ImageRequest; } namespace DB { class FileName; } class QImage; namespace BackgroundJobs { class HandleVideoThumbnailRequestJob : public BackgroundTaskManager::JobInterface { Q_OBJECT public: explicit HandleVideoThumbnailRequestJob(ImageManager::ImageRequest* request, BackgroundTaskManager::Priority priority); QString title() const override; QString details() const override; static void saveFullScaleFrame( const DB::FileName& fileName, const QImage& image ); static DB::FileName pathForRequest( const DB::FileName& fileName ); static DB::FileName frameName(const DB::FileName& videoName, int frameNumber ); static void removeFullScaleFrame( const DB::FileName& fileName ); protected: void execute() override; private slots: void frameLoaded(QImage); private: void sendResult( QImage image ); QImage brokenImage() const; ImageManager::ImageRequest* m_request; }; } // namespace BackgroundJobs #endif // BACKGROUNDJOBS_HANDLEVIDEOTHUMBNAILREQUESTJOB_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/Priority.h b/BackgroundTaskManager/Priority.h index b8ad80a7..3672f800 100644 --- a/BackgroundTaskManager/Priority.h +++ b/BackgroundTaskManager/Priority.h @@ -1,37 +1,37 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef BACKGROUNDTASKAMANAGER_PRIORITY_H #define BACKGROUNDTASKAMANAGER_PRIORITY_H namespace BackgroundTaskManager { enum Priority { ForegroundCycleRequest = 0, ForegroundThumbnailRequest = 1, BackgroundTask = 2, // This is only a marker between foreground and background, do not use as a priority. BackgroundVideoInfoRequest = 2, BackgroundVideoThumbnailRequest = 3, BackgroundVideoPreviewRequest = 4, SIZE_OF_PRIORITY_QUEUE // Must be after the last one, and the last one MUST be the highest. }; } #endif // BACKGROUNDTASKAMANAGER_PRIORITY_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/PriorityQueue.cpp b/BackgroundTaskManager/PriorityQueue.cpp index a6b4952a..631ae84a 100644 --- a/BackgroundTaskManager/PriorityQueue.cpp +++ b/BackgroundTaskManager/PriorityQueue.cpp @@ -1,81 +1,81 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #include "PriorityQueue.h" #include "Utilities/AlgorithmHelper.h" #include using namespace Utilities; namespace BackgroundTaskManager { PriorityQueue::PriorityQueue() { m_jobs.resize(SIZE_OF_PRIORITY_QUEUE); } bool PriorityQueue::isEmpty() const { return all_of( m_jobs, std::mem_fn(&QueueType::isEmpty) ); } int PriorityQueue::count() const { return sum( m_jobs, std::mem_fn(&QueueType::length ) ); } void PriorityQueue::enqueue(JobInterface *job, Priority priority) { m_jobs[priority].enqueue(job); } JobInterface *PriorityQueue::dequeue() { for( QueueType& queue : m_jobs ) { if ( !queue.isEmpty() ) return queue.dequeue(); } Q_ASSERT( false && "Queue was empty"); return nullptr; } JobInterface *PriorityQueue::peek(int index) const { int offset = 0; for ( const QueueType& queue : m_jobs) { if ( index-offset < queue.count() ) return queue[index-offset]; else offset += queue.count(); } Q_ASSERT( false && "index beyond queue"); return nullptr; } bool PriorityQueue::hasForegroundTasks() const { for (int i = 0; i < BackgroundTask; ++i ) { if (!m_jobs[i].isEmpty()) return true; } return false; } } // namespace BackgroundTaskManager // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/PriorityQueue.h b/BackgroundTaskManager/PriorityQueue.h index 17f37f7b..9379b7f6 100644 --- a/BackgroundTaskManager/PriorityQueue.h +++ b/BackgroundTaskManager/PriorityQueue.h @@ -1,50 +1,50 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef BACKGROUNDTASKMANAGER_PRIORITYQUEUE_H #define BACKGROUNDTASKMANAGER_PRIORITYQUEUE_H #include "Priority.h" #include #include namespace BackgroundTaskManager { class JobInterface; class PriorityQueue { public: - PriorityQueue(); + PriorityQueue(); bool isEmpty() const; int count() const; void enqueue( JobInterface* job, Priority priority); JobInterface* dequeue(); JobInterface* peek(int index) const; bool hasForegroundTasks() const; private: typedef QQueue QueueType; QVector< QueueType > m_jobs; }; } // namespace BackgroundTaskManager #endif // BACKGROUNDTASKMANAGER_PRIORITYQUEUE_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/CategoryListView/documentation.h b/CategoryListView/documentation.h index 8b7a76e5..96d8e4ab 100644 --- a/CategoryListView/documentation.h +++ b/CategoryListView/documentation.h @@ -1,7 +1,7 @@ //krazy:skip /** \namespace CategoryListView - \brief + \brief **/ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FastDir.cpp b/DB/FastDir.cpp index 6ed7ad72..01e445e2 100644 --- a/DB/FastDir.cpp +++ b/DB/FastDir.cpp @@ -1,192 +1,192 @@ /* Copyright (C) 2010-2018 Jesper Pedersen and Robert Krawitz 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 "FastDir.h" #include "Logging.h" #include #include #include extern "C" { #include #include #include /* * Ideally the order of entries returned by readdir() should be close * to optimal; intuitively, it should reflect the order in which inodes * are returned from getdents() or equivalent, which should be the order * in which they're stored on the disk. Experimentally, that isn't always * true. One test, involving 10839 files totaling 90 GB resulted in * readdir() returning files in random order, where "find" returned them * sorted (and the same version of find does *not* in general return files * in alphabetical order). * * By repeated measurement, loading the files in the order returned by * readdir took about 16:30, where loading them in alphanumeric sorted order * took about 15:00. Running a similar test outside of kpa (using the order * returned by readdir() vs. sorted to cat the files through dd and measuring * the time) yielded if anything an even greater discrepancy (17:35 vs. 14:10). * * This issue is filesystem dependent, but is known to affect the extN * filesystems commonly used on Linux that use a hashed tree structure to * store directories. See e. g. * http://home.ifi.uio.no/paalh/publications/files/ipccc09.pdf and its * accompanying presentation * http://www.linux-kongress.org/2009/slides/linux_disk_io_performance_havard_espeland.pdf * * We could do even better by sorting by block position, but that would * greatly increase complexity. */ #ifdef __linux__ # include # include # define HAVE_STATFS # define STATFS_FSTYPE_EXT2 EXT2_SUPER_MAGIC // Includes EXT3_SUPER_MAGIC, EXT4_SUPER_MAGIC #else #ifdef __FreeBSD__ # include # include # include # define HAVE_STATFS # define STATFS_FSTYPE_EXT2 FS_EXT2FS #endif // other platforms fall back to known-safe (but slower) implementation #endif // __linux__ } typedef QMap InodeMap; typedef QSet StringSet; DB::FastDir::FastDir(const QString &path) : m_path(path) { InodeMap tmpAnswer; DIR *dir; dirent *file; QByteArray bPath(QFile::encodeName(path)); dir = opendir( bPath.constData() ); if ( !dir ) return; const bool doSortByInode = sortByInode(bPath); const bool doSortByName = sortByName(bPath); #if defined(QT_THREAD_SUPPORT) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_CYGWIN) // ZaJ (2016-03-23): while porting to Qt5/KF5, this code-path is disabled on my system // I don't want to touch this right now since I can't verify correctness in any way. // rlk 2018-05-20: readdir_r is deprecated as of glibc 2.24; see // http://man7.org/linux/man-pages/man3/readdir_r.3.html. // There are problems with MAXNAMLEN/NAME_MAX and friends, that // can differ from filesystem to filesystem. It's also expected // that POSIX will (if it hasn't already) deprecate readdir_r // and require readdir to be thread safe. union dirent_buf { struct KDE_struct_dirent mt_file; char b[sizeof(struct dirent) + MAXNAMLEN + 1]; } *u = new union dirent_buf; while ( readdir_r(dir, &(u->mt_file), &file ) == 0 && file ) #else // FIXME: use 64bit versions of readdir and dirent? while ( (file = readdir(dir)) ) #endif // QT_THREAD_SUPPORT && _POSIX_THREAD_SAFE_FUNCTIONS { if ( doSortByInode ) tmpAnswer.insert(file->d_ino, QFile::decodeName(file->d_name)); else m_sortedList.append(QFile::decodeName(file->d_name)); } #if defined(QT_THREAD_SUPPORT) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_CYGWIN) delete u; #endif (void) closedir(dir); - + if ( doSortByInode ) { for ( InodeMap::iterator it = tmpAnswer.begin(); it != tmpAnswer.end(); ++it ) { m_sortedList << it.value(); } } else if ( doSortByName ) { m_sortedList.sort(); } } // No currently known filesystems where sort by name is optimal constexpr bool DB::sortByName(const QByteArray &) { return false; } bool DB::sortByInode(const QByteArray &path) { #ifdef HAVE_STATFS struct statfs buf; if ( statfs( path.constData(), &buf ) == -1 ) return -1; // Add other filesystems as appropriate switch ( buf.f_type ) { case STATFS_FSTYPE_EXT2: return true; default: return false; } #else // HAVE_STATFS Q_UNUSED(path); return false; #endif // HAVE_STATFS } const QStringList DB::FastDir::entryList() const { return m_sortedList; } QStringList DB::FastDir::sortFileList(const StringSet &files) const { QStringList answer; StringSet tmp(files); for ( const QString &fileName : m_sortedList ) { if ( tmp.contains( fileName ) ) { answer << fileName; tmp.remove( fileName ); } else if ( tmp.contains( m_path + fileName ) ) { answer << m_path + fileName; tmp.remove( m_path + fileName ); } } if ( tmp.count() > 0 ) { qCDebug(FastDirLog) << "Files left over after sorting on " << m_path; for ( const QString &fileName : tmp ) { qCDebug(FastDirLog) << fileName; answer << fileName; } } return answer; } QStringList DB::FastDir::sortFileList(const QStringList &files) const { StringSet tmp; for ( const QString &fileName : files ) { tmp << fileName; } return sortFileList(tmp); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileName.cpp b/DB/FileName.cpp index 123f3526..d0469951 100644 --- a/DB/FileName.cpp +++ b/DB/FileName.cpp @@ -1,103 +1,103 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #include "FileName.h" #include "ImageDB.h" #include #include #include #include DB::FileName::FileName() : m_isNull(true) { } DB::FileName DB::FileName::fromAbsolutePath(const QString &fileName) { const QString imageRoot = Utilities::stripEndingForwardSlash( Settings::SettingsData::instance()->imageDirectory() ) + QLatin1String("/"); if (!fileName.startsWith(imageRoot)) return FileName(); FileName res; res.m_isNull = false; res.m_absoluteFilePath = fileName; res.m_relativePath = fileName.mid(imageRoot.length()); return res; } DB::FileName DB::FileName::fromRelativePath(const QString &fileName) { Q_ASSERT(!fileName.startsWith(QChar::fromLatin1('/'))); FileName res; res.m_isNull = false; res.m_relativePath = fileName; res.m_absoluteFilePath = Utilities::stripEndingForwardSlash( Settings::SettingsData::instance()->imageDirectory() ) + QLatin1String("/") + fileName; return res; } QString DB::FileName::absolute() const { Q_ASSERT(!isNull()); return m_absoluteFilePath; } QString DB::FileName::relative() const { Q_ASSERT(!m_isNull); return m_relativePath; } bool DB::FileName::isNull() const { return m_isNull; } bool DB::FileName::operator ==(const DB::FileName &other) const { return m_isNull == other.m_isNull && m_relativePath == other.m_relativePath; } bool DB::FileName::operator !=(const DB::FileName &other) const { return !(*this == other); } bool DB::FileName::operator <(const DB::FileName &other) const { return relative() < other.relative(); } bool DB::FileName::exists() const { return QFile::exists(absolute()); } DB::ImageInfoPtr DB::FileName::info() const { return ImageDB::instance()->info(*this); } uint DB::qHash( const DB::FileName& fileName ) { return qHash(fileName.relative()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileName.h b/DB/FileName.h index bb46b4ce..5ade91c9 100644 --- a/DB/FileName.h +++ b/DB/FileName.h @@ -1,62 +1,62 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef FILENAME_H #define FILENAME_H #include #include #include "ImageInfoPtr.h" #include namespace DB { class FileName { public: FileName(); static FileName fromAbsolutePath( const QString& fileName ); static FileName fromRelativePath( const QString& fileName ); QString absolute() const; QString relative() const; bool isNull() const; bool operator==( const FileName& other ) const; bool operator!=( const FileName& other ) const; bool operator<( const FileName& other ) const; bool exists() const; ImageInfoPtr info() const; private: // During previous profilation it showed that converting between absolute and relative took quite some time, // so to avoid that, I store both. QString m_relativePath; QString m_absoluteFilePath; bool m_isNull; }; uint qHash( const DB::FileName& fileName ); typedef QSet FileNameSet; } Q_DECLARE_METATYPE(DB::FileName) #endif // FILENAME_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileNameList.cpp b/DB/FileNameList.cpp index 902ff2cd..ef756f9a 100644 --- a/DB/FileNameList.cpp +++ b/DB/FileNameList.cpp @@ -1,60 +1,60 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #include "FileNameList.h" DB::FileNameList::FileNameList(const QList& other ) { QList::operator=(other); } DB::FileNameList::FileNameList(const QStringList &files) { for (const QString& file: files) append(DB::FileName::fromAbsolutePath(file)); } QStringList DB::FileNameList::toStringList(DB::PathType type) const { QStringList res; for (const DB::FileName& fileName : *this) { if ( type == DB::RelativeToImageRoot ) res.append( fileName.relative() ); else res.append( fileName.absolute()); } return res; } DB::FileNameList &DB::FileNameList::operator <<(const DB::FileName & fileName) { QList::operator<<(fileName); return *this; } DB::FileNameList DB::FileNameList::reversed() const { FileNameList res; for (const FileName& fileName : *this) { res.prepend(fileName); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileNameList.h b/DB/FileNameList.h index 8b6634dc..1437401c 100644 --- a/DB/FileNameList.h +++ b/DB/FileNameList.h @@ -1,49 +1,49 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef FILENAMELIST_H #define FILENAMELIST_H #include #include #include "FileName.h" #include "ImageInfo.h" namespace DB { class FileNameList : public QList { public: FileNameList() {} FileNameList( const QList& ); /** * @brief Create a FileNameList from a list of absolute filenames. * @param files */ explicit FileNameList(const QStringList &files); QStringList toStringList(DB::PathType) const; FileNameList& operator<<(const DB::FileName& ); FileNameList reversed() const; }; } #endif // FILENAMELIST_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfoList.cpp b/DB/ImageInfoList.cpp index bb2eff04..e7738534 100644 --- a/DB/ImageInfoList.cpp +++ b/DB/ImageInfoList.cpp @@ -1,171 +1,171 @@ /* Copyright (C) 2003-2010 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. */ #include "ImageInfoList.h" #include "ImageInfo.h" #include "Logging.h" #include #include #include #include #include #include using namespace DB; class SortableImageInfo { public: SortableImageInfo(const QDateTime& datetime, const QString& string, const ImageInfoPtr &info) : m_dt(datetime), m_st(string), m_in(info) {} SortableImageInfo(const SortableImageInfo& in) : m_dt(in.m_dt), m_st(in.m_st), m_in(in.m_in) {} SortableImageInfo() {} ~SortableImageInfo() {} const QDateTime& DateTime(void) const { return m_dt; } const QString& String(void) const { return m_st; } const ImageInfoPtr& ImageInfo(void) const { return m_in; } bool operator== (const SortableImageInfo& other) const { return m_dt == other.m_dt && m_st == other.m_st; } bool operator!= (const SortableImageInfo& other) const { return m_dt != other.m_dt || m_st != other.m_st; } bool operator> (const SortableImageInfo& other) const { if (m_dt != other.m_dt) { return m_dt > other.m_dt; } else { return m_st > other.m_st; }} bool operator< (const SortableImageInfo& other) const { if (m_dt != other.m_dt) { return m_dt < other.m_dt; } else { return m_st < other.m_st; }} bool operator>= (const SortableImageInfo& other) const { return *this == other || *this > other; } bool operator<= (const SortableImageInfo& other) const { return *this == other || *this < other; } private: QDateTime m_dt; QString m_st; ImageInfoPtr m_in; }; ImageInfoList ImageInfoList::sort() const { QVector vec; for( ImageInfoListConstIterator it = constBegin(); it != constEnd(); ++it ) { vec.append(SortableImageInfo((*it)->date().start(),(*it)->fileName().absolute(), *it)); } std::sort(vec.begin(),vec.end()); - + ImageInfoList res; for( QVector::ConstIterator mapIt = vec.constBegin(); mapIt != vec.constEnd(); ++mapIt ) { res.append(mapIt->ImageInfo()); } return res; } void ImageInfoList::sortAndMergeBackIn( ImageInfoList& subListToSort ) { ImageInfoList sorted = subListToSort.sort(); const int insertIndex = indexOf(subListToSort[0]); Q_ASSERT(insertIndex >= 0); // Delete the items we will merge in. for( ImageInfoListIterator it = sorted.begin(); it != sorted.end(); ++it ) remove( *it ); ImageInfoListIterator insertIt = begin() + insertIndex; // Now merge in the items for( ImageInfoListIterator it = sorted.begin(); it != sorted.end(); ++it ) { insertIt = insert( insertIt, *it ); ++insertIt; } } ImageInfoList::~ImageInfoList() { } void ImageInfoList::appendList( ImageInfoList& list ) { for ( ImageInfoListConstIterator it = list.constBegin(); it != list.constEnd(); ++it ) { append( *it ); } } void ImageInfoList::printItems() { for ( ImageInfoListConstIterator it = constBegin(); it != constEnd(); ++it ) { qCDebug(DBLog) << (*it)->fileName().absolute(); } } bool ImageInfoList::isSorted() { if ( count() == 0 ) return true; QDateTime prev = first()->date().start(); QString prevFile = first()->fileName().absolute(); for ( ImageInfoListConstIterator it = constBegin(); it != constEnd(); ++it ) { QDateTime cur = (*it)->date().start(); QString curFile = (*it)->fileName().absolute(); if ( prev > cur || ( prev == cur && prevFile > curFile ) ) return false; prev = cur; prevFile = curFile; } return true; } void ImageInfoList::mergeIn( ImageInfoList other) { ImageInfoList tmp; for ( ImageInfoListConstIterator it = constBegin(); it != constEnd(); ++it ) { QDateTime thisDate = (*it)->date().start(); QString thisFileName = (*it)->fileName().absolute(); while ( other.count() != 0 ) { QDateTime otherDate = other.first()->date().start(); QString otherFileName = other.first()->fileName().absolute(); if ( otherDate < thisDate || ( otherDate == thisDate && otherFileName < thisFileName ) ) { tmp.append( other[0] ); other.pop_front(); } else break; } tmp.append( *it ); } tmp.appendList( other ); *this = tmp; } void ImageInfoList::remove( const ImageInfoPtr& info ) { for( ImageInfoListIterator it = begin(); it != end(); ++it ) { if ( (*(*it)) == *info ) { QList::erase(it); return; } } } DB::FileNameList ImageInfoList::files() const { DB::FileNameList res; for ( const ImageInfoPtr& info : *this) res.append(info->fileName()); return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageScout.cpp b/DB/ImageScout.cpp index 34313ec8..ec6792ef 100644 --- a/DB/ImageScout.cpp +++ b/DB/ImageScout.cpp @@ -1,271 +1,271 @@ /* Copyright (C) 2018 Robert Krawitz 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 "ImageScout.h" #include "Logging.h" #include #include #include #include #include extern "C" { #include #include } using namespace DB; namespace { constexpr int DEFAULT_SCOUT_BUFFER_SIZE = 1048576; // *sizeof(int) bytes // We might want this to be bytes rather than images. constexpr int DEFAULT_MAX_SEEKAHEAD_IMAGES = 10; constexpr int SEEKAHEAD_WAIT_MS = 10; // 10 milliseconds, and retry constexpr int TERMINATION_WAIT_MS = 10; // 10 milliseconds, and retry } // 1048576 with a single scout thread empirically yields best performance // on a Seagate 2TB 2.5" disk, sustaining throughput in the range of // 95-100 MB/sec with 100-110 IO/sec on large files. This is close to what // would be expected. A SATA SSD (Crucial MX300) is much less sensitive to // I/O size and scout thread, achieving about 340 MB/sec with high CPU // utilization. class DB::ImageScoutThread :public QThread { friend class DB::ImageScout; public: ImageScoutThread( ImageScoutQueue &, QMutex *, QAtomicInt &count, QAtomicInt &preloadCount, QAtomicInt &skippedCount ); protected: virtual void run(); void setBufSize(int); int getBufSize(); void setMaxSeekAhead(int); int getMaxSeekAhead(); void setReadLimit(int); int getReadLimit(); private: void doRun(char *); ImageScoutQueue& m_queue; QMutex *m_mutex; QAtomicInt& m_loadedCount; QAtomicInt& m_preloadedCount; QAtomicInt& m_skippedCount; int m_scoutBufSize; int m_maxSeekAhead; int m_readLimit; bool m_isStarted; }; ImageScoutThread::ImageScoutThread( ImageScoutQueue &queue, QMutex *mutex, - QAtomicInt &count, - QAtomicInt &preloadedCount, + QAtomicInt &count, + QAtomicInt &preloadedCount, QAtomicInt &skippedCount ) : m_queue(queue), m_mutex(mutex), m_loadedCount(count), m_preloadedCount(preloadedCount), m_skippedCount(skippedCount), m_scoutBufSize(DEFAULT_SCOUT_BUFFER_SIZE), m_maxSeekAhead(DEFAULT_MAX_SEEKAHEAD_IMAGES), m_readLimit(-1), m_isStarted(false) { } void ImageScoutThread::doRun(char *tmpBuf) { while ( !isInterruptionRequested() ) { QMutexLocker locker(m_mutex); if ( m_queue.isEmpty() ) { return; } DB::FileName fileName = m_queue.dequeue(); locker.unlock(); // If we're behind the reader, move along m_preloadedCount++; if ( m_loadedCount.load() >= m_preloadedCount.load() ) { m_skippedCount++; continue; } else { // Don't get too far ahead of the loader, or we just waste memory // TODO: wait on something rather than polling while (m_preloadedCount.load() >= m_loadedCount.load() + m_maxSeekAhead && ! isInterruptionRequested()) { QThread::msleep(SEEKAHEAD_WAIT_MS); } // qCDebug(DBImageScoutLog) << ">>>>>Scout: preload" << m_preloadedCount.load() << "load" << m_loadedCount.load() << fileName.relative(); } int inputFD = open( QFile::encodeName( fileName.absolute()).constData(), O_RDONLY ); int bytesRead = 0; if ( inputFD >= 0 ) { while ( read( inputFD, tmpBuf, m_scoutBufSize ) && ( m_readLimit < 0 || ( (bytesRead += m_scoutBufSize) < m_readLimit ) ) && ! isInterruptionRequested() ) { } (void) close( inputFD ); } } } void ImageScoutThread::setBufSize(int bufSize) { if ( ! m_isStarted ) m_scoutBufSize = bufSize; } int ImageScoutThread::getBufSize() { return m_scoutBufSize; } void ImageScoutThread::setMaxSeekAhead(int maxSeekAhead) { if ( ! m_isStarted ) m_maxSeekAhead = maxSeekAhead; } int ImageScoutThread::getMaxSeekAhead() { return m_maxSeekAhead; } void ImageScoutThread::setReadLimit(int readLimit) { if ( ! m_isStarted ) m_readLimit = readLimit; } int ImageScoutThread::getReadLimit() { return m_readLimit; } void ImageScoutThread::run() { m_isStarted = true; char *tmpBuf = new char[m_scoutBufSize]; doRun( tmpBuf ); delete[] tmpBuf; } ImageScout::ImageScout(ImageScoutQueue &images, QAtomicInt &count, int threads) : m_preloadedCount(0), m_skippedCount(0), m_isStarted(false), m_scoutBufSize(DEFAULT_SCOUT_BUFFER_SIZE), m_maxSeekAhead(DEFAULT_MAX_SEEKAHEAD_IMAGES), m_readLimit(-1) { if (threads > 0) { for (int i = 0; i < threads; i++) { - ImageScoutThread *t = + ImageScoutThread *t = new ImageScoutThread( images, threads > 1 ? &m_mutex : nullptr, count, m_preloadedCount, m_skippedCount ); m_scoutList.append( t ); } } } ImageScout::~ImageScout() { if ( m_scoutList.count() > 0 ) { for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { if (m_isStarted) { if ( ! (*it)->isFinished() ) { (*it)->requestInterruption(); while ( ! (*it)->isFinished() ) QThread::msleep(TERMINATION_WAIT_MS); } } delete (*it); } } qCDebug(DBImageScoutLog) << "Total files:" << m_preloadedCount << "skipped" << m_skippedCount; } void ImageScout::start() { // Yes, there's a race condition here between isStartd and setting // the buf size or seek ahead...but this isn't a hot code path! if ( ! m_isStarted && m_scoutList.count() > 0 ) { m_isStarted = true; for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { (*it)->start(); } } } void ImageScout::setBufSize(int bufSize) { if ( ! m_isStarted && bufSize > 0 ) { m_scoutBufSize = bufSize; for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { (*it)->setBufSize( m_scoutBufSize ); } } } int ImageScout::getBufSize() { return m_scoutBufSize; } void ImageScout::setMaxSeekAhead(int maxSeekAhead) { if ( ! m_isStarted && maxSeekAhead > 0 ) { m_maxSeekAhead = maxSeekAhead; for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { (*it)->setMaxSeekAhead( m_maxSeekAhead ); } } } int ImageScout::getMaxSeekAhead() { return m_maxSeekAhead; } void ImageScout::setReadLimit(int readLimit) { if ( ! m_isStarted && readLimit > 0 ) { m_readLimit = readLimit; for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { (*it)->setReadLimit( m_readLimit ); } } } int ImageScout::getReadLimit() { return m_readLimit; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageSearchInfo.cpp b/DB/ImageSearchInfo.cpp index 8c87037a..7f617dda 100644 --- a/DB/ImageSearchInfo.cpp +++ b/DB/ImageSearchInfo.cpp @@ -1,634 +1,634 @@ /* Copyright (C) 2003-2015 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. */ #include "ImageSearchInfo.h" #include "AndCategoryMatcher.h" #include "CategoryMatcher.h" #include "ContainerCategoryMatcher.h" #include "ExactCategoryMatcher.h" #include "ImageDB.h" #include "Logging.h" #include "NegationCategoryMatcher.h" #include "NoTagCategoryMatcher.h" #include "OrCategoryMatcher.h" #include "ValueCategoryMatcher.h" #include #include #include #include #include #include #include using namespace DB; static QAtomicInt s_matchGeneration; static int nextGeneration() { return s_matchGeneration++; } ImageSearchInfo::ImageSearchInfo( const ImageDate& date, const QString& label, const QString& description ) : m_date( date), m_label( label ), m_description( description ), m_rating( -1 ), m_megapixel( 0 ), m_max_megapixel( 0 ), m_ratingSearchMode( 0 ), m_searchRAW( false ), m_isNull( false ), m_isCacheable( true ), m_compiled( false ), m_matchGeneration(nextGeneration()) { } ImageSearchInfo::ImageSearchInfo( const ImageDate& date, const QString& label, const QString& description, const QString& fnPattern ) : m_date( date), m_label( label ), m_description( description ), m_fnPattern( fnPattern ), m_rating( -1 ), m_megapixel( 0 ), m_max_megapixel( 0 ), m_ratingSearchMode( 0 ), m_searchRAW( false ), m_isNull( false ), m_isCacheable( true ), m_compiled( false ), m_matchGeneration(nextGeneration()) { } QString ImageSearchInfo::label() const { return m_label; } QRegExp ImageSearchInfo::fnPattern() const { return m_fnPattern; } QString ImageSearchInfo::description() const { return m_description; } ImageSearchInfo::ImageSearchInfo() : m_rating( -1 ), m_megapixel( 0 ), m_max_megapixel( 0 ), m_ratingSearchMode( 0 ), m_searchRAW( false ), m_isNull( true ), m_isCacheable( true ), m_compiled( false ), m_matchGeneration(nextGeneration()) { } bool ImageSearchInfo::isNull() const { return m_isNull; } bool ImageSearchInfo::isCacheable() const { return m_isCacheable; } void ImageSearchInfo::setCacheable(bool cacheable) { m_isCacheable = cacheable; } bool ImageSearchInfo::match( ImageInfoPtr info ) const { if ( m_isNull ) return true; if ( m_isCacheable && info->matchGeneration() == m_matchGeneration ) return info->isMatched(); bool ok = doMatch( info ); if ( m_isCacheable ) { info->setMatchGeneration(m_matchGeneration); info->setIsMatched(ok); } return ok; } bool ImageSearchInfo::doMatch( ImageInfoPtr info ) const { if ( !m_compiled ) compile(); // -------------------------------------------------- Rating //ok = ok && (_rating == -1 ) || ( _rating == info->rating() ); if (m_rating != -1) { switch( m_ratingSearchMode ) { case 1: // Image rating at least selected if ( m_rating > info->rating() ) return false; break; case 2: // Image rating less than selected if ( m_rating < info->rating() ) return false; break; case 3: // Image rating not equal if ( m_rating == info->rating() ) return false; break; default: if ( m_rating != info->rating() ) return false; break; } } // -------------------------------------------------- Resolution - if ( m_megapixel && + if ( m_megapixel && ( m_megapixel * 1000000 > info->size().width() * info->size().height() ) ) return false; if ( m_max_megapixel && m_max_megapixel < m_megapixel && ( m_max_megapixel * 1000000 < info->size().width() * info->size().height() ) ) return false; // -------------------------------------------------- Date QDateTime actualStart = info->date().start(); QDateTime actualEnd = info->date().end(); if ( m_date.start().isValid() ) { if ( actualEnd < m_date.start() || ( m_date.end().isValid() && actualStart > m_date.end() ) ) return false; } else if ( m_date.end().isValid() && actualStart > m_date.end() ) { return false; } // -------------------------------------------------- Label if ( m_label.isEmpty() && info->label().indexOf(m_label) == -1 ) return false; // -------------------------------------------------- RAW if ( m_searchRAW && !ImageManager::RAWImageDecoder::isRAW( info->fileName()) ) return false; #ifdef HAVE_KGEOMAP // Search for GPS Position if (m_usingRegionSelection) { if ( !info->coordinates().hasCoordinates() ) return false; float infoLat = info->coordinates().lat(); - if ( m_regionSelectionMinLat > infoLat || + if ( m_regionSelectionMinLat > infoLat || m_regionSelectionMaxLat < infoLat ) return false; float infoLon = info->coordinates().lon(); if ( m_regionSelectionMinLon > infoLon || m_regionSelectionMaxLon < infoLon ) return false; } #endif // -------------------------------------------------- File name pattern if ( !m_fnPattern.isEmpty() && m_fnPattern.indexIn( info->fileName().relative() ) == -1 ) return false; // -------------------------------------------------- Options // alreadyMatched map is used to make it possible to search for // Jesper & None QMap alreadyMatched; for (CategoryMatcher* optionMatcher : m_categoryMatchers) { if ( ! optionMatcher->eval(info, alreadyMatched) ) return false; } // -------------------------------------------------- Text if ( !m_description.isEmpty() ) { const QString &txt(info->description()); QStringList list = m_description.split(QChar::fromLatin1(' '), QString::SkipEmptyParts); Q_FOREACH( const QString &word, list ) { if ( txt.indexOf( word, 0, Qt::CaseInsensitive ) == -1 ) return false; } } // -------------------------------------------------- EXIF if ( ! m_exifSearchInfo.matches( info->fileName() ) ) return false; return true; } QString ImageSearchInfo::categoryMatchText( const QString& name ) const { return m_categoryMatchText[name]; } void ImageSearchInfo::setCategoryMatchText( const QString& name, const QString& value ) { m_categoryMatchText[name] = value; m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::addAnd( const QString& category, const QString& value ) { // Escape literal "&"s in value by doubling it QString escapedValue = value; escapedValue.replace(QString::fromUtf8("&"), QString::fromUtf8("&&")); QString val = categoryMatchText( category ); if ( !val.isEmpty() ) val += QString::fromLatin1( " & " ) + escapedValue; else val = escapedValue; setCategoryMatchText( category, val ); m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setRating( short rating ) { m_rating = rating; m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMegaPixel( short megapixel ) { m_megapixel = megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMaxMegaPixel( short max_megapixel ) { m_max_megapixel = max_megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchMode(int index) { m_ratingSearchMode = index; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchRAW( bool searchRAW ) { m_searchRAW = searchRAW; m_matchGeneration = nextGeneration(); } QString ImageSearchInfo::toString() const { QString res; bool first = true; for( QMap::ConstIterator it= m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it ) { if ( ! it.value().isEmpty() ) { if ( first ) first = false; else res += QString::fromLatin1( " / " ); QString txt = it.value(); if ( txt == ImageDB::NONE() ) txt = i18nc( "As in No persons, no locations etc. I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No %1", it.key() ); if ( txt.contains( QString::fromLatin1("|") ) ) txt.replace( QString::fromLatin1( "&" ), QString::fromLatin1( " %1 " ).arg( i18n("and") ) ); else txt.replace( QString::fromLatin1( "&" ), QString::fromLatin1( " / " ) ); txt.replace( QString::fromLatin1( "|" ), QString::fromLatin1( " %1 " ).arg( i18n("or") ) ); txt.replace( QString::fromLatin1( "!" ), QString::fromLatin1( " %1 " ).arg( i18n("not") ) ); txt.replace( ImageDB::NONE(), i18nc( "As in no other persons, or no other locations. " "I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No other %1", it.key() ) ); res += txt.simplified(); } } return res; } void ImageSearchInfo::debug() { for( QMap::Iterator it= m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it ) { qCDebug(DBCategoryMatcherLog) << it.key() << ", " << it.value(); } } // PENDING(blackie) move this into the Options class instead of having it here. void ImageSearchInfo::saveLock() const { KConfigGroup config = KSharedConfig::openConfig()->group( Settings::SettingsData::instance()->groupForDatabase( "Privacy Settings")); config.writeEntry( QString::fromLatin1("label"), m_label ); config.writeEntry( QString::fromLatin1("description"), m_description ); config.writeEntry( QString::fromLatin1("categories"), m_categoryMatchText.keys() ); for( QMap::ConstIterator it= m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it ) { config.writeEntry( it.key(), it.value() ); } config.sync(); } ImageSearchInfo ImageSearchInfo::loadLock() { KConfigGroup config = KSharedConfig::openConfig()->group( Settings::SettingsData::instance()->groupForDatabase( "Privacy Settings" )); ImageSearchInfo info; info.m_label = config.readEntry( "label" ); info.m_description = config.readEntry( "description" ); QStringList categories = config.readEntry( QString::fromLatin1("categories"), QStringList() ); for( QStringList::ConstIterator it = categories.constBegin(); it != categories.constEnd(); ++it ) { info.setCategoryMatchText( *it, config.readEntry( *it, QString() ) ); } return info; } ImageSearchInfo::ImageSearchInfo( const ImageSearchInfo& other ) { m_date = other.m_date; m_categoryMatchText = other.m_categoryMatchText; m_label = other.m_label; m_description = other.m_description; m_fnPattern = other.m_fnPattern; m_isNull = other.m_isNull; m_compiled = false; m_rating = other.m_rating; m_ratingSearchMode = other.m_ratingSearchMode; m_megapixel = other.m_megapixel; m_max_megapixel = other.m_max_megapixel; m_searchRAW = other.m_searchRAW; m_exifSearchInfo = other.m_exifSearchInfo; m_matchGeneration = other.m_matchGeneration; m_isCacheable = other.m_isCacheable; #ifdef HAVE_KGEOMAP m_regionSelection = other.m_regionSelection; #endif } void ImageSearchInfo::compile() const { m_exifSearchInfo.search(); #ifdef HAVE_KGEOMAP // Prepare Search for GPS Position m_usingRegionSelection = m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates(); if (m_usingRegionSelection) { using std::min; using std::max; m_regionSelectionMinLat = min(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMaxLat = max(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMinLon = min(m_regionSelection.first.lon(), m_regionSelection.second.lon()); m_regionSelectionMaxLon = max(m_regionSelection.first.lon(), m_regionSelection.second.lon()); } #endif deleteMatchers(); for( QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it ) { QString category = it.key(); QString matchText = it.value(); QStringList orParts = matchText.split(QString::fromLatin1("|"), QString::SkipEmptyParts); DB::ContainerCategoryMatcher* orMatcher = new DB::OrCategoryMatcher; Q_FOREACH( QString orPart, orParts ) { // Split by " & ", not only by "&", so that the doubled "&"s won't be used as a split point QStringList andParts = orPart.split(QString::fromLatin1(" & "), QString::SkipEmptyParts); DB::ContainerCategoryMatcher* andMatcher; bool exactMatch=false; bool negate = false; andMatcher = new DB::AndCategoryMatcher; Q_FOREACH( QString str, andParts ) { static QRegExp regexp( QString::fromLatin1("^\\s*!\\s*(.*)$") ); if ( regexp.exactMatch( str ) ) { // str is preceded with NOT negate = true; str = regexp.cap(1); } str = str.trimmed(); CategoryMatcher* valueMatcher; if ( str == ImageDB::NONE() ) { // mark AND-group as containing a "No other" condition exactMatch = true; continue; } else { valueMatcher = new DB::ValueCategoryMatcher( category, str ); if ( negate ) valueMatcher = new DB::NegationCategoryMatcher( valueMatcher ); } andMatcher->addElement( valueMatcher ); } if ( exactMatch ) { DB::CategoryMatcher *exactMatcher = nullptr; // if andMatcher has exactMatch set, but no CategoryMatchers, then // matching "category / None" is what we want: if ( andMatcher->mp_elements.count() == 0 ) { exactMatcher = new DB::NoTagCategoryMatcher( category ); } else { ExactCategoryMatcher *noOtherMatcher = new ExactCategoryMatcher( category ); if ( andMatcher->mp_elements.count() == 1 ) noOtherMatcher->setMatcher( andMatcher->mp_elements[0] ); else noOtherMatcher->setMatcher( andMatcher ); exactMatcher = noOtherMatcher; } if ( negate ) exactMatcher = new DB::NegationCategoryMatcher( exactMatcher ); orMatcher->addElement( exactMatcher ); } else if ( andMatcher->mp_elements.count() == 1 ) orMatcher->addElement( andMatcher->mp_elements[0] ); else if ( andMatcher->mp_elements.count() > 1 ) orMatcher->addElement( andMatcher ); } CategoryMatcher* matcher = nullptr; if ( orMatcher->mp_elements.count() == 1 ) matcher = orMatcher->mp_elements[0]; else if ( orMatcher->mp_elements.count() > 1 ) matcher = orMatcher; if ( matcher ) { m_categoryMatchers.append( matcher ); if ( DBCategoryMatcherLog().isDebugEnabled() ) { qCDebug(DBCategoryMatcherLog) << "Matching text '" << matchText << "' in category "<< category <<":"; matcher->debug(0); qCDebug(DBCategoryMatcherLog) << "."; } } } m_compiled = true; } ImageSearchInfo::~ImageSearchInfo() { deleteMatchers(); } void ImageSearchInfo::debugMatcher() const { if ( !m_compiled ) compile(); qCDebug(DBCategoryMatcherLog, "And:"); for (CategoryMatcher* optionMatcher : m_categoryMatchers) { optionMatcher->debug(1); } } QList > ImageSearchInfo::query() const { if ( !m_compiled ) compile(); // Combine _optionMachers to one list of lists in Disjunctive // Normal Form and return it. QList::Iterator it = m_categoryMatchers.begin(); QList > result; if ( it == m_categoryMatchers.end() ) return result; result = convertMatcher( *it ); ++it; for( ; it != m_categoryMatchers.end(); ++it ) { QList > current = convertMatcher( *it ); QList > oldResult = result; result.clear(); for (QList resultIt : oldResult) { for (QList currentIt : current) { QList tmp; tmp += resultIt; tmp += currentIt; result.append( tmp ); } } } return result; } Utilities::StringSet ImageSearchInfo::findAlreadyMatched( const QString &group ) const { Utilities::StringSet result; QString str = categoryMatchText( group ); if ( str.contains( QString::fromLatin1( "|" ) ) ) { return result; } QStringList list = str.split(QString::fromLatin1( "&" ), QString::SkipEmptyParts); Q_FOREACH( QString part, list ) { QString nm = part.trimmed(); if (! nm.contains( QString::fromLatin1( "!" ) ) ) result.insert(nm); } return result; } void ImageSearchInfo::deleteMatchers() const { qDeleteAll(m_categoryMatchers); m_categoryMatchers.clear(); } QList ImageSearchInfo::extractAndMatcher( CategoryMatcher* matcher ) const { QList result; AndCategoryMatcher* andMatcher; SimpleCategoryMatcher* simpleMatcher; if ( ( andMatcher = dynamic_cast( matcher ) ) ) { for (CategoryMatcher* child : andMatcher->mp_elements) { SimpleCategoryMatcher* simpleMatcher = dynamic_cast( child ); Q_ASSERT( simpleMatcher ); result.append( simpleMatcher ); } } else if ( ( simpleMatcher = dynamic_cast( matcher ) ) ) result.append( simpleMatcher ); else Q_ASSERT( false ); return result; } /** Convert matcher to Disjunctive Normal Form. * * @return OR-list of AND-lists. (e.g. OR(AND(a,b),AND(c,d))) */ QList > ImageSearchInfo::convertMatcher( CategoryMatcher* item ) const { QList > result; OrCategoryMatcher* orMacther; if ( ( orMacther = dynamic_cast( item ) ) ) { for (CategoryMatcher* child : orMacther->mp_elements) { result.append( extractAndMatcher( child ) ); } } else result.append( extractAndMatcher( item ) ); return result; } ImageDate ImageSearchInfo::date() const { return m_date; } void ImageSearchInfo::addExifSearchInfo( const Exif::SearchInfo info ) { m_exifSearchInfo = info; m_isNull = false; } void DB::ImageSearchInfo::renameCategory( const QString& oldName, const QString& newName ) { m_categoryMatchText[newName] = m_categoryMatchText[oldName]; m_categoryMatchText.remove( oldName ); m_compiled = false; } #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates::Pair ImageSearchInfo::regionSelection() const { return m_regionSelection; } void ImageSearchInfo::setRegionSelection(const KGeoMap::GeoCoordinates::Pair& actRegionSelection) { m_regionSelection = actRegionSelection; m_compiled = false; if (m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates()) { m_isNull = false; } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/NewImageFinder.cpp b/DB/NewImageFinder.cpp index c87e25d1..e2d28b85 100644 --- a/DB/NewImageFinder.cpp +++ b/DB/NewImageFinder.cpp @@ -1,756 +1,756 @@ /* Copyright (C) 2003-2019 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 "NewImageFinder.h" #include "FastDir.h" #include "Logging.h" #include "ImageScout.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace DB; /***************************************************************** * * NOTES ON PERFORMANCE * ===== == =========== * * - Robert Krawitz 2018-05-24 * * * GENERAL NOTES ON STORAGE I/O * ------- ----- -- ------- --- * * The two main gates to loading new images are: * * 1) I/O (how fast can we read images off mass storage) * * Different I/O devices have different characteristics in terms of * througput, media latency, and protocol latency. * * - Throughput is the raw speed at which data can be transferred, * limited by the physical and/or electronic characteristics of * the medium and the interface. Short of reducing the amount of * data that's transferred, or clever games with using the most * efficient part of the medium (the outer tracks only for HDD's, * a practice referred to as "short stroking" because it reduces * the distance the head has to seek, at the cost of wasting a * lot of capacity), there's nothing that can be done about this. * * - Media latency is the latency component due to characteristics * of the underlying storage medium. For spinning disks, this is * a function of rotational latency and sek latency. In some * cases, particularly with hard disks, it is possible to reduce * media latency by arranging to access the data in a way that * reduces seeking. See DB/FastDir.cpp for an example of this. * * While media latency can sometimes be hidden by overlapping * I/O, generally not possible to avoid it. Sometimes trying too * hard can actually increase media latency if it results in I/O * operations competing against each other requiring additional * seeks. * * Overlapping I/O with computation is another matter; that can * easily yield benefit, especially if it eliminates rotational * latency. * * - Protocol latency. This refers to things like SATA overhead, * network overhead (for images stored on a network), and so * forth. This can encompass multiple things, and often they can * be pipelined by means of multiple queued I/O operations. For * example, multiple commands can be issued to modern interfaces * (SATA, NVMe) and many network interfaces without waiting for * earlier operations to return. * * If protocol latency is high compared with media latency, * having multiple requests outstanding simultaneously can * yield significant benefits. * * iostat is a valuable tool for investigating throughput and * looking for possible optimizations. The IO/sec and data * read/written per second when compared against known media * characteristics (disk and SSD throughput, network bandwidth) * provides valuable information about whether we're getting close * to full performance from the I/O, and user and system CPU time * give us additional clues about whether we're I/O-bound or * CPU-bound. * * Historically in the computer field, operations that require * relatively simple processing on large volumes of data are I/O * bound. But with very fast I/O devices such as NVMe SSDs, some * of which reach 3 GB/sec, that's not always the case. * * 2) Image (mostly JPEG) loading. * * This is a function of image characteristics and image processing * libraries. Sometimes it's possible to apply parameters to * the underlying image loader to speed it up. This shows up as user * CPU time. Usually the only way to improve this performance * characteristic is to use more or faster CPU cores (sometimes GPUs * can assist here) or use better image loading routines (better * libraries). * * * DESCRIPTION OF KPHOTOALBUM IMAGE LOAD PROCESS * ----------- -- ----------- ----- ---- ------- * * KPhotoAlbum, when it loads an image, performs three processing steps: * * 1) Compute the MD5 checksum * * 2) Extract the Exif metadata * * 3) Generate a thumbnail * * Previous to this round of performance tuning, the first two steps * were performed in the first pass, and thumbnails were generated in * a separate pass. Assuming that the set of new images is large enough * that they cannot all fit in RAM buffers, this results in the I/O * being performed twice. The rewrite results in I/O being performed once. * * In addition, I have made many other changes: * * 1) Prior to the MD5 calculation step, a new thread, called a "scout * thread", reads the files into memory. While this memory is not * directly used in the later computations, it results in the images * being in RAM when they are later needed, making the I/O very fast * (copying data in memory rather than reading it from storage). * * This is a way to overlap I/O with computation. * * 2) The MD5 checksum uses its own I/O to read the data in in larger * chunks than the Qt MD5 routine does. The Qt routine reads it in * in 4KiB chunks; my experimentation has found that 256KiB chunks * are more efficient, even with a scout thread (it reduces the * number of system calls). * * 3) When searching for other images to stack with the image being * loaded, the new image loader no longer attempts to determine * whether other candidate filenames are present, nor does it * compute the MD5 checksum of any such files it does find. Rather, * it only checks for files that are already in KPhotoAlbum, either * previously or as a result of the current load. Merely checking * for the presence of another file is not cheap, and it's not * necessary; if an image will belong to a stack, we'll either know * it now or when other images that can be stacked are loaded. * * 4) The Exif metadata extraction is now done only once; previously * it was performed several times at different stages of the loading * process. * * 5) The thumbnail index is now written out incrementally rather than * the entire index (which can be many megabytes in a large image * database) being rewritten frequently. The index is fully rewritten * prior to exit. * * * BASELINE PERFORMANCE * -------- ----------- * * These measurements were all taken on a Lenovo ThinkPad P70 with 32 * GB of dual-channel DDR4-2400 DRAM, a Xeon E3-1505M CPU (4 cores/8 * total hyperthreads, 2.8-3.7 GHz Skylake; usually runs around * 3.1-3.2 GHz in practice), a Seagate ST2000LM015-2E8174 2TB HDD, and * a Crucial MX300 1TB SATA SSD. Published numbers and measurements I * took otherwise indicate that the HDD can handle about 105-110 * MB/sec with a maximum of 180 IO/sec (in a favorable case). The SSD * is rated to handle 530 MB/sec read, 510 MB/sec write, 92K random * reads/sec, and 83K random writes/sec. * * The image set I used for all measurements, except as noted, * consists of 10839 total files of which about 85% are 20 MP JPEG and * the remainder (with a few exceptions are 20 MP RAW files from a * Canon EOS 7D mkII camera. The total dataset is about 92 GB in * size. * * I baselined both drives by reading the same dataset by means of * * % ls | xargs cat | dd bs=1048576 of=/dev/null * * The HDD required between 850 and 870 seconds (14'10" to 14'30") to * perform this operation, yielding about 105-108 MB/sec. The SSD * achieved about 271 MB/sec, which is well under its rated throughput * (hdparm -Tt yields 355 MB/sec, which is likewise nowhere close to * its rated throughput). hdparm -Tt on the HDD yields about 120 * MB/sec, but throughput to an HDD depends upon which part of the * disk is being read. The outer tracks have a greater angular * density to achieve the same linear density (in other words, the * circumference of an outer track is longer than that of an inner * track, and the data is stored at a constant linear density). So * hdparm isn't very useful on an HDD except as a best case. * * Note also that hdparm does a single stream read from the device. * It does not take advantage of the ability to queue multiple * requests. - * + * * * ANALYSIS OF KPHOTOALBUM LOAD PERFORMANCE * -------- -- ----------- ---- ----------- * * I analyzed the following cases, with images stored both on the * HDD and the SSD: * * 1) Images loaded (All, JPEG only, RAW only) * * B) Thumbnail creation (Including, Excluding) * * C) Scout threads (0, 1, 2, 3) * * The JPG image set constitutes 9293 images totaling about 55 GB. The * JPEG files are mostly 20 MP high quality files, in the range of * 6-10 MB. * The RAW image set constitutes 1544 images totaling about 37 GB. The * RAW files are 20 MP files, in the range of 25 MB. * The ALL set consists of 10839 or 10840 images totaling about 92 GB * (the above set plus 2 .MOV files and in some cases one additional * JPEG file). - * + * * Times are elapsed times; CPU consumption is approximate user+system * CPU consumption. Numbers in parentheses are with thumbnail * building disabled. Note that in the cases with no scout threads on * the SSD the times were reproducibly shorter with thumbnail building * enabled (reasons are not determined at this time). - * + * * Cases building RAW thumbnails generally consumed somewhat more * system CPU (in the range of 10-15%) than JPEG-only cases. This may * be due to custom I/O routines used for generating thumbnails with * JPEG files; RAW files used the I/O provided by libkdcraw, which * uses smaller I/O operations. * * Estimating CPU time for mixed workloads proved very problematic, * as there were significant changes over time. - * + * * Elapsed Time * ------- ---- - * + * * SSD HDD - * + * * JPG - 0 scouts 4:03 (3:59) * JPG - 1 scout 2:46 (2:44) * JPG - 2 scouts 2:20 (2:07) * JPG - 3 scouts 2:21 (1:58) - * + * * ALL - 0 scouts 6:32 (7:03) 16:01 * ALL - 1 scout 4:33 (4:33) 15:01 * ALL - 2 scouts 3:37 (3:28) 16:59 * ALL - 3 scouts 3:36 (3:15) - * + * * RAW - 0 scouts 2:18 (2:46) * RAW - 1 scout 1:46 (1:46) * RAW - 2 scouts 1:17 (1:17) * RAW - 3 scouts 1:13 (1:13) - * + * * User+System CPU * ----------- --- - * + * * SSD HDD - * + * * JPG - 0 scouts 40% (12%) * JPG - 1 scout 70% (20%) * JPG - 2 scouts 85% (15%) * JPG - 3 scouts 85% (15%) - * + * * RAW - 0 scouts 15% (10%) * RAW - 1 scout 18% (12%) * RAW - 2 scouts 25% (15%) * RAW - 3 scouts 25% (15%) * * I also used kcachegrind to measure CPU consumption on smaller * subsets of images (with and without thumbnail creation). In terms * of user CPU consumption, thumbnail creation constitutes the large * majority of CPU cycles for processing JPEG files, followed by MD5 * computation, with Exif parsing lagging far behind. For RAW files, * MD5 computation consumes more cycles, likely in part due to the * larger size of RAW files but possibly also related to the smaller * filesize of embedded thumbnails (on the Canon 7D mkII, the embedded * thumbnail is full size but low quality). - * + * * With thumbnail generation: * ---- --------- ----------- - * + * * RAW JPEG - * + * * Thumbnail generation 44% 82% * libjpeg processing 43% 82% * MD5 computation 51% 13% * Read Exif 1% 1.0% - * + * * Without thumbnail generation: * ------- --------- ----------- - * + * * RAW JPEG - * + * * MD5 computation 92% 80% * Read Exif 4% 10% * * * CONCLUSIONS * ----------- * * For loading files from hard disk (likely the most common case), * there's no reason to consider any loading method other than using a * single scout thread and computing thumbnails concurrently. Even * with thumbnail computation, there is very little CPU utilization. * * Loading from SATA SSD benefits from two scout threads, and possibly * more. For minimal time to regain control, there is some benefit * seen from separating thumbnail generation from the rest of the * processing stages at the cost of more total elapsed time. This is * more evident with JPEG files than with RAW files in this test case. * RAW files typically have smaller thumbnail images which can be * extracted and processed more quickly than full-size JPEG files. On * a slower CPU, it may be desirable to return control to the user * even if the thumbnails are not built yet. * * Two other cases would be NVMe (or other very fast) SSDs and network * storage. Since we're seeing evidence of CPU saturation on SATA * SSDs, we would likely see this even more strongly with NVMe; with * large numbers of images it may be desirable to separate the * thumbnail building from the rest of the processing. It may also be * beneficial to use more scout threads. * * Network storage presents a different problem. It is likely to have * lower throughput -- and certainly much higher latency -- than even * HDD, unless the underlying storage medium is SSD and the data is * located on a very fast, low latency network. So there would be no * benefit to separating thumbnail processing. However, due to * protocol vs. media latency discussed above, it may well work to use * more scout threads. However, this may saturate the network and the * storage, to the detriment of other users, and there's probably no * general (or easily discoverable) optimum for this. * * It's my judgment that most images will be stored on HDDs for at * least the next few years, so tuning for that use case is probably * the best single choice to be made. * *****************************************************************/ namespace { // Number of scout threads for preloading images. More than one scout thread // yields about 10% less performance with higher IO/sec but lower I/O throughput, // most probably due to thrashing. constexpr int IMAGE_SCOUT_THREAD_COUNT = 1; bool canReadImage( const DB::FileName& fileName ) { bool fastMode = !Settings::SettingsData::instance()->ignoreFileExtension(); QMimeDatabase::MatchMode mode = fastMode ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault; QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile( fileName.absolute(), mode ); return QImageReader::supportedMimeTypes().contains( mimeType.name().toUtf8() ) || ImageManager::ImageDecoder::mightDecode( fileName ); } } bool NewImageFinder::findImages() { // Load the information from the XML file. DB::FileNameSet loadedFiles; QElapsedTimer timer; timer.start(); // TODO: maybe the databas interface should allow to query if it // knows about an image ? Here we've to iterate through all of them and it // might be more efficient do do this in the database without fetching the // whole info. for ( const DB::FileName& fileName : DB::ImageDB::instance()->images()) { loadedFiles.insert(fileName); } m_pendingLoad.clear(); searchForNewFiles( loadedFiles, Settings::SettingsData::instance()->imageDirectory() ); int filesToLoad = m_pendingLoad.count(); loadExtraFiles(); qCDebug(TimingLog) << "Loaded " << filesToLoad << " images in " << timer.elapsed() / 1000.0 << " seconds"; // Man this is not super optimal, but will be changed onces the image finder moves to become a background task. if ( MainWindow::FeatureDialog::hasVideoThumbnailer() ) { BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob ); } // To avoid deciding if the new images are shown in a given thumbnail view or in a given search // we rather just go to home. return (!m_pendingLoad.isEmpty()); // returns if new images was found. } void NewImageFinder::searchForNewFiles( const DB::FileNameSet& loadedFiles, QString directory ) { qApp->processEvents( QEventLoop::AllEvents ); directory = Utilities::stripEndingForwardSlash(directory); const QString imageDir = Utilities::stripEndingForwardSlash(Settings::SettingsData::instance()->imageDirectory()); FastDir dir( directory ); const QStringList dirList = dir.entryList( ); ImageManager::RAWImageDecoder dec; QStringList excluded; excluded << Settings::SettingsData::instance()->excludeDirectories(); excluded = excluded.at(0).split(QString::fromLatin1(",")); bool skipSymlinks = Settings::SettingsData::instance()->skipSymlinks(); // Keep files within a directory more local by processing all files within the // directory, and then all subdirectories. QStringList subdirList; for( QStringList::const_iterator it = dirList.constBegin(); it != dirList.constEnd(); ++it ) { const DB::FileName file = DB::FileName::fromAbsolutePath(directory + QString::fromLatin1("/") + *it); if ( (*it) == QString::fromLatin1(".") || (*it) == QString::fromLatin1("..") || excluded.contains( (*it) ) || loadedFiles.contains( file ) || dec._skipThisFile(loadedFiles, file) || (*it) == QString::fromLatin1("CategoryImages") ) continue; QFileInfo fi( file.absolute() ); if ( !fi.isReadable() ) continue; if ( skipSymlinks && fi.isSymLink() ) continue; if ( fi.isFile() ) { if ( ! DB::ImageDB::instance()->isBlocking( file ) ) { if ( canReadImage(file) ) m_pendingLoad.append( qMakePair( file, DB::Image ) ); else if ( Utilities::isVideo( file ) ) m_pendingLoad.append( qMakePair( file, DB::Video ) ); } } else if ( fi.isDir() ) { subdirList.append( file.absolute() ); } } for( QStringList::const_iterator it = subdirList.constBegin(); it != subdirList.constEnd(); ++it ) searchForNewFiles( loadedFiles, *it ); } void NewImageFinder::loadExtraFiles() { // FIXME: should be converted to a threadpool for SMP stuff and whatnot :] QProgressDialog dialog; QElapsedTimer timeSinceProgressUpdate; dialog.setLabelText( i18n("

Loading information from new files

" "

Depending on the number of images, this may take some time.
" "However, there is only a delay when new images are found.

") ); QProgressBar *progressBar = new QProgressBar; progressBar->setFormat( QLatin1String("%v/%m") ); dialog.setBar(progressBar); dialog.setMaximum( m_pendingLoad.count() ); dialog.setMinimumDuration( 1000 ); QAtomicInt loadedCount = 0; setupFileVersionDetection(); int count = 0; ImageScoutQueue asyncPreloadQueue; for( LoadList::Iterator it = m_pendingLoad.begin(); it != m_pendingLoad.end(); ++it ) { asyncPreloadQueue.enqueue((*it).first); } ImageScout scout(asyncPreloadQueue, loadedCount, IMAGE_SCOUT_THREAD_COUNT); scout.start(); Exif::Database::instance()->startInsertTransaction(); dialog.setValue( count ); // ensure to call setProgress(0) timeSinceProgressUpdate.start(); for( LoadList::Iterator it = m_pendingLoad.begin(); it != m_pendingLoad.end(); ++it, ++count ) { qApp->processEvents( QEventLoop::AllEvents ); if ( dialog.wasCanceled() ) { m_pendingLoad.clear(); Exif::Database::instance()->abortInsertTransaction(); return; } // (*it).first: DB::FileName // (*it).second: DB::MediaType loadExtraFile( (*it).first, (*it).second ); loadedCount++; // Atomic if ( timeSinceProgressUpdate.elapsed() >= 1000 ) { dialog.setValue( count ); timeSinceProgressUpdate.restart(); } } dialog.setValue( count ); // loadExtraFile() has already inserted all images into the // database, but without committing the changes DB::ImageDB::instance()->commitDelayedImages(); Exif::Database::instance()->commitInsertTransaction(); ImageManager::ThumbnailBuilder::instance()->save(); } void NewImageFinder::setupFileVersionDetection() { // should be cached because loading once per image is expensive m_modifiedFileCompString = Settings::SettingsData::instance()->modifiedFileComponent(); m_modifiedFileComponent = QRegExp(m_modifiedFileCompString); m_originalFileComponents << Settings::SettingsData::instance()->originalFileComponent(); m_originalFileComponents = m_originalFileComponents.at(0).split(QString::fromLatin1(";")); } void NewImageFinder::loadExtraFile( const DB::FileName& newFileName, DB::MediaType type ) { MD5 sum = MD5Sum( newFileName ); if ( handleIfImageHasBeenMoved(newFileName, sum) ) return; // check to see if this is a new version of a previous image // We'll get the Exif data later, when we get the MD5 checksum. ImageInfoPtr info = ImageInfoPtr(new ImageInfo( newFileName, type, false, false )); ImageInfoPtr originalInfo; DB::FileName originalFileName; if (Settings::SettingsData::instance()->detectModifiedFiles()) { // requires at least *something* in the modifiedFileComponent if (m_modifiedFileCompString.length() >= 0 && newFileName.relative().contains(m_modifiedFileComponent)) { for( QStringList::const_iterator it = m_originalFileComponents.constBegin(); it != m_originalFileComponents.constEnd(); ++it ) { QString tmp = newFileName.relative(); tmp.replace(m_modifiedFileComponent, (*it)); originalFileName = DB::FileName::fromRelativePath(tmp); MD5 originalSum; if (newFileName == originalFileName) originalSum = sum; else if (DB::ImageDB::instance()->md5Map()->containsFile( originalFileName ) ) originalSum = DB::ImageDB::instance()->md5Map()->lookupFile( originalFileName ); else // Do *not* attempt to compute the checksum here. It forces a filesystem // lookup on a file that may not exist and substantially degrades // performance by about 25% on an SSD and about 30% on a spinning disk. // If one of these other files exist, it will be found later in // the image search at which point we'll detect the modified file. continue; if ( DB::ImageDB::instance()->md5Map()->contains( originalSum ) ) { // we have a previous copy of this file; copy it's data // from the original. originalInfo = DB::ImageDB::instance()->info( originalFileName ); if ( !originalInfo ) { qCDebug(DBLog) << "Original info not found by name for " << originalFileName.absolute() << ", trying by MD5 sum."; originalFileName = DB::ImageDB::instance()->md5Map()->lookup( originalSum ); if (!originalFileName.isNull()) { qCDebug(DBLog) << "Substitute image " << originalFileName.absolute() << " found."; originalInfo = DB::ImageDB::instance()->info( originalFileName ); } if ( !originalInfo ) { qCWarning(DBLog,"How did that happen? We couldn't find info for the original image %s; can't copy the original data to %s", qPrintable(originalFileName.absolute()), qPrintable(newFileName.absolute())); continue; } } info->copyExtraData(*originalInfo); /* if requested to move, then delete old data from original */ if (Settings::SettingsData::instance()->moveOriginalContents() ) { originalInfo->removeExtraData(); } break; } } } } ImageInfoList newImages; newImages.append( info ); DB::ImageDB::instance()->addImages( newImages, false ); // also inserts image into exif db if present: info->setMD5Sum( sum ); DB::ImageDB::instance()->md5Map()->insert( sum, info->fileName()); if (originalInfo && Settings::SettingsData::instance()->autoStackNewFiles() ) { // stack the files together DB::FileName olderfile = originalFileName; DB::FileName newerfile = info->fileName(); DB::FileNameList tostack; // the newest file should go to the top of the stack tostack.append(newerfile); DB::FileNameList oldStack; if ( ( oldStack = DB::ImageDB::instance()->getStackFor( olderfile)).isEmpty() ) { tostack.append(olderfile); } else { for ( const DB::FileName& tmp : oldStack ) { tostack.append( tmp ); } } DB::ImageDB::instance()->stack(tostack); MainWindow::Window::theMainWindow()->setStackHead(newerfile); // ordering: XXX we ideally want to place the new image right // after the older one in the list. } markUnTagged(info); ImageManager::ThumbnailBuilder::instance()->buildOneThumbnail( info ); if ( info->isVideo() && MainWindow::FeatureDialog::hasVideoThumbnailer() ) { // needs to be done *after* insertion into database BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::ReadVideoLengthJob(info->fileName(), BackgroundTaskManager::BackgroundVideoPreviewRequest)); } } bool NewImageFinder::handleIfImageHasBeenMoved(const FileName &newFileName, const MD5& sum) { if ( DB::ImageDB::instance()->md5Map()->contains( sum ) ) { const DB::FileName matchedFileName = DB::ImageDB::instance()->md5Map()->lookup(sum); QFileInfo fi( matchedFileName.absolute() ); if ( !fi.exists() ) { // The file we had a collapse with didn't exists anymore so it is likely moved to this new name ImageInfoPtr info = DB::ImageDB::instance()->info( matchedFileName); if ( !info ) qCWarning(DBLog, "How did that happen? We couldn't find info for the images %s", qPrintable(matchedFileName.relative())); else { info->delaySavingChanges(true); fi = QFileInfo ( matchedFileName.relative() ); if ( info->label() == fi.completeBaseName() ) { fi = QFileInfo( newFileName.absolute() ); info->setLabel( fi.completeBaseName() ); } DB::ImageDB::instance()->renameImage( info, newFileName ); // We need to insert the new name into the MD5 map, // as it is a map, the value for the moved file will automatically be deleted. DB::ImageDB::instance()->md5Map()->insert( sum, info->fileName()); Exif::Database::instance()->remove( matchedFileName ); Exif::Database::instance()->add( newFileName); ImageManager::ThumbnailBuilder::instance()->buildOneThumbnail( info ); return true; } } } return false; // The image wasn't just moved } bool NewImageFinder::calculateMD5sums( const DB::FileNameList& list, DB::MD5Map* md5Map, bool* wasCanceled) { // FIXME: should be converted to a threadpool for SMP stuff and whatnot :] QProgressDialog dialog; dialog.setLabelText( i18np("

Calculating checksum for %1 file

","

Calculating checksums for %1 files

", list.size()) + i18n("

By storing a checksum for each image " "KPhotoAlbum is capable of finding images " "even when you have moved them on the disk.

")); dialog.setMaximum(list.size()); dialog.setMinimumDuration( 1000 ); int count = 0; DB::FileNameList cantRead; bool dirty = false; for (const FileName& fileName : list) { if ( count % 10 == 0 ) { dialog.setValue( count ); // ensure to call setProgress(0) qApp->processEvents( QEventLoop::AllEvents ); if ( dialog.wasCanceled() ) { if ( wasCanceled ) *wasCanceled = true; return dirty; } } MD5 md5 = MD5Sum( fileName ); if (md5.isNull()) { cantRead << fileName; continue; } ImageInfoPtr info = ImageDB::instance()->info(fileName); if ( info->MD5Sum() != md5 ) { info->setMD5Sum( md5 ); dirty = true; ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } md5Map->insert( md5, fileName ); ++count; } if ( wasCanceled ) *wasCanceled = false; if ( !cantRead.empty() ) KMessageBox::informationList( nullptr, i18n("Following files could not be read:"), cantRead.toStringList(DB::RelativeToImageRoot) ); return dirty; } void DB::NewImageFinder::markUnTagged( ImageInfoPtr info ) { if ( Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() ) { info->addCategoryInfo( Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag() ); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/documentation.h b/Exif/documentation.h index dbe685d3..08fb4942 100644 --- a/Exif/documentation.h +++ b/Exif/documentation.h @@ -1,7 +1,7 @@ //krazy:skip /** \namespace Exif - \brief + \brief **/ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/AsyncLoader.cpp b/ImageManager/AsyncLoader.cpp index 0d7b4c69..179482c5 100644 --- a/ImageManager/AsyncLoader.cpp +++ b/ImageManager/AsyncLoader.cpp @@ -1,248 +1,248 @@ /* Copyright (C) 2003-2010 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. */ #include "AsyncLoader.h" #include #include #include #include #include #include #include #include "CancelEvent.h" #include "ImageEvent.h" #include "ImageLoaderThread.h" #include "ThumbnailCache.h" #include "ThumbnailBuilder.h" ImageManager::AsyncLoader* ImageManager::AsyncLoader::s_instance = nullptr; // -- Manager -- ImageManager::AsyncLoader* ImageManager::AsyncLoader::instance() { if ( !s_instance ) { s_instance = new AsyncLoader; s_instance->init(); } return s_instance; } // We need this as a separate method as the s_instance variable will otherwise not be initialized // corrected before the thread starts. void ImageManager::AsyncLoader::init() { // Use up to three cores for thumbnail generation. No more than three as that // likely will make it less efficient due to three cores hitting the harddisk at the same time. // This might limit the throughput on SSD systems, but we likely have a few years before people // put all of their pictures on SSDs. // rlk 20180515: with improvements to the thumbnail generation code, I've conducted // experiments demonstrating benefit even at 2x the number of hyperthreads, even on // an HDD. However, we need to reserve a thread for the UI or it gets very sluggish // We need one more core in the computer for the GUI thread, but we won't dedicate it to GUI, // as that'd mean that a dual-core box would only have one core decoding images, which would be // suboptimal. // In case of only one core in the computer, use one core for thumbnail generation // TODO(isilmendil): It seems that many people have their images on NFS-mounts. // Should we somehow detect this and allocate less threads there? // rlk 20180515: IMO no; if anything, we need more threads to hide // the latency of NFS. const int cores = qMax( 1, qMin( 16, QThread::idealThreadCount() - 1 ) ); m_exitRequested = false; for ( int i = 0; i < cores; ++i) { ImageLoaderThread* imageLoader = new ImageLoaderThread(); // The thread is set to the lowest priority to ensure that it doesn't starve the GUI thread. m_threadList << imageLoader; imageLoader->start( QThread::IdlePriority ); } } bool ImageManager::AsyncLoader::load( ImageRequest* request ) { if (m_exitRequested) return false; // rlk 2018-05-15: Skip this check here. Even if the check // succeeds at this point, it may fail later, and if we're suddenly // processing a lot of requests (e. g. a thumbnail build), // this may be very I/O-intensive since it actually has to // read the inode. // silently ignore images not (currently) on disk: // if ( ! request->fileSystemFileName().exists() ) // return false; if ( Utilities::isVideo( request->fileSystemFileName() ) ) { if (!loadVideo( request )) return false; } else { loadImage( request ); } return true; } bool ImageManager::AsyncLoader::loadVideo( ImageRequest* request) { if (m_exitRequested) return false; if ( ! MainWindow::FeatureDialog::hasVideoThumbnailer() ) return false; BackgroundTaskManager::Priority priority = (request->priority() > ThumbnailInvisible) ? BackgroundTaskManager::ForegroundThumbnailRequest : BackgroundTaskManager::BackgroundVideoThumbnailRequest; BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::HandleVideoThumbnailRequestJob(request,priority)); return true; } void ImageManager::AsyncLoader::loadImage( ImageRequest* request ) { QMutexLocker dummy( &m_lock ); if (m_exitRequested) return; QSet::const_iterator req = m_currentLoading.find( request ); if ( req != m_currentLoading.end() && m_loadList.isRequestStillValid( request ) ) { // The last part of the test above is needed to not fail on a race condition from AnnotationDialog::ImagePreview, where the preview // at startup request the same image numerous time (likely from resize event). Q_ASSERT ( *req != request); delete request; return; // We are currently loading it, calm down and wait please ;-) } // Try harder to find a pending request. Unfortunately, we can't simply use // m_currentLoading.contains() because that will compare pointers // when we want to compare values. for (req = m_currentLoading.begin(); req != m_currentLoading.end(); req++) { ImageRequest *r = *req; if (*request == *r) { delete request; return; // We are currently loading it, calm down and wait please ;-) } } - + // if request is "fresh" (not yet pending): if (m_loadList.addRequest( request )) m_sleepers.wakeOne(); } void ImageManager::AsyncLoader::stop( ImageClientInterface* client, StopAction action ) { // remove from pending map. QMutexLocker requestLocker( &m_lock ); m_loadList.cancelRequests( client, action ); // PENDING(blackie) Reintroduce this // VideoManager::instance().stop( client, action ); // Was implemented as m_pending.cancelRequests( client, action ); // Where m_pending is the RequestQueue } int ImageManager::AsyncLoader::activeCount() const { QMutexLocker dummy( &m_lock ); return m_currentLoading.count(); } bool ImageManager::AsyncLoader::isExiting() const { return m_exitRequested; } ImageManager::ImageRequest* ImageManager::AsyncLoader::next() { QMutexLocker dummy( &m_lock ); ImageRequest* request = nullptr; while ( !( request = m_loadList.popNext() ) ) m_sleepers.wait( &m_lock ); m_currentLoading.insert( request ); return request; } void ImageManager::AsyncLoader::requestExit() { m_exitRequested = true; ImageManager::ThumbnailBuilder::instance()->cancelRequests(); m_sleepers.wakeAll(); // TODO(jzarl): check if we can just connect the finished() signal of the threads to deleteLater() // and exit this function without waiting for (QList::iterator it = m_threadList.begin(); it != m_threadList.end(); ++it ) { while (! (*it)->isFinished()) { QThread::msleep(10); } delete (*it); } } void ImageManager::AsyncLoader::customEvent( QEvent* ev ) { if ( ev->type() == ImageEventID ) { ImageEvent* iev = dynamic_cast( ev ); if ( !iev ) { Q_ASSERT( iev ); return; } ImageRequest* request = iev->loadInfo(); QMutexLocker requestLocker( &m_lock ); const bool requestStillNeeded = m_loadList.isRequestStillValid( request ); m_loadList.removeRequest(request); m_currentLoading.remove( request ); requestLocker.unlock(); QImage image = iev->image(); if ( !request->loadedOK() ) { if ( m_brokenImage.size() != request->size() ) { // we can ignore the krazy warning here because we have a valid fallback QIcon brokenFileIcon = QIcon::fromTheme( QLatin1String("file-broken") ); // krazy:exclude=iconnames if ( brokenFileIcon.isNull() ) { brokenFileIcon = QIcon::fromTheme( QLatin1String("image-x-generic") ); } m_brokenImage = brokenFileIcon.pixmap( request->size() ).toImage(); } image = m_brokenImage; } if ( request->isThumbnailRequest() ) ImageManager::ThumbnailCache::instance()->insert( request->databaseFileName(), image ); if ( requestStillNeeded && request->client() ) { request->client()->pixmapLoaded(request, image); } delete request; } else if ( ev->type() == CANCELEVENTID ) { CancelEvent* cancelEvent = dynamic_cast(ev); cancelEvent->request()->client()->requestCanceled(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ExtractOneVideoFrame.h b/ImageManager/ExtractOneVideoFrame.h index 123f0b96..3778fba8 100644 --- a/ImageManager/ExtractOneVideoFrame.h +++ b/ImageManager/ExtractOneVideoFrame.h @@ -1,66 +1,66 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef IMAGEMANAGER_EXTRACTONEVIDEOFRAME_H #define IMAGEMANAGER_EXTRACTONEVIDEOFRAME_H #include #include #include class QImage; namespace Utilities { class Process; } namespace ImageManager { /** \brief Extract a thumbnail given a filename and offset. \see \ref videothumbnails */ class ExtractOneVideoFrame : public QObject { Q_OBJECT public: static void extract(const DB::FileName& filename, double offset, QObject* receiver, const char* slot); private slots: void frameFetched(); void handleError(QProcess::ProcessError); signals: void result(const QImage&); private: ExtractOneVideoFrame(const DB::FileName& filename, double offset, QObject* receiver, const char* slot); void setupWorkingDirectory(); void deleteWorkingDirectory(); void markShortVideo(const DB::FileName& fileName); QString m_workingDirectory; Utilities::Process* m_process; DB::FileName m_fileName; static QString s_tokenForShortVideos; }; } // namespace ImageManager #endif // IMAGEMANAGER_EXTRACTONEVIDEOFRAME_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ImageLoaderThread.cpp b/ImageManager/ImageLoaderThread.cpp index c114223d..6613dbc6 100644 --- a/ImageManager/ImageLoaderThread.cpp +++ b/ImageManager/ImageLoaderThread.cpp @@ -1,159 +1,159 @@ /* Copyright (C) 2003-2010 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. */ #include "ImageLoaderThread.h" #include "ThumbnailCache.h" #include "ImageDecoder.h" #include "AsyncLoader.h" #include "RawImageDecoder.h" #include "Utilities/FastJpeg.h" #include "Utilities/ImageUtil.h" #include #include extern "C" { #include #include #include #include #include #include #include } #include #include #include "ImageEvent.h" namespace ImageManager { // Create a global instance. Its constructor will itself register it. RAWImageDecoder rawdecoder; } ImageManager::ImageLoaderThread::ImageLoaderThread( size_t bufsize ) : m_imageLoadBuffer( new char[bufsize] ), m_bufSize( bufsize ) { } ImageManager::ImageLoaderThread::~ImageLoaderThread() { delete[] m_imageLoadBuffer; } void ImageManager::ImageLoaderThread::run() { while ( true ) { ImageRequest* request = AsyncLoader::instance()->next(); Q_ASSERT( request ); if ( request->isExitRequest() ) { return; } bool ok; QImage img = loadImage( request, ok ); if ( ok ) { img = scaleAndRotate( request, img ); } request->setLoadedOK( ok ); ImageEvent* iew = new ImageEvent( request, img ); QApplication::postEvent( AsyncLoader::instance(), iew ); } } QImage ImageManager::ImageLoaderThread::loadImage( ImageRequest* request, bool& ok ) { int dim = calcLoadSize( request ); QSize fullSize; ok = false; if ( !request->fileSystemFileName().exists() ) return QImage(); QImage img; if (Utilities::isJPEG(request->fileSystemFileName())) { - ok = Utilities::loadJPEG( &img, request->fileSystemFileName(), &fullSize, dim, + ok = Utilities::loadJPEG( &img, request->fileSystemFileName(), &fullSize, dim, m_imageLoadBuffer, m_bufSize ); if (ok == true) request->setFullSize( fullSize ); } else { // At first, we have to give our RAW decoders a try. If we allowed // QImage's load() method, it'd for example load a tiny thumbnail from // NEF files, which is not what we want. ok = ImageDecoder::decode( &img, request->fileSystemFileName(), &fullSize, dim); if (ok) request->setFullSize( img.size() ); } if (!ok) { // Now we can try QImage's stuff as a fallback... ok = img.load( request->fileSystemFileName().absolute() ); if (ok) request->setFullSize( img.size() ); } return img; } int ImageManager::ImageLoaderThread::calcLoadSize( ImageRequest* request ) { return qMax( request->width(), request->height() ); } QImage ImageManager::ImageLoaderThread::scaleAndRotate( ImageRequest* request, QImage img ) { if ( request->angle() != 0 ) { QMatrix matrix; matrix.rotate( request->angle() ); img = img.transformed( matrix ); int angle = (request->angle() + 360)%360; Q_ASSERT( angle >= 0 && angle <= 360 ); if ( angle == 90 || angle == 270 ) request->setFullSize( QSize( request->fullSize().height(), request->fullSize().width() ) ); } // If we are looking for a scaled version, then scale if ( shouldImageBeScale( img, request ) ) img = Utilities::scaleImage(img, request->size(), Qt::KeepAspectRatio ); return img; } bool ImageManager::ImageLoaderThread::shouldImageBeScale( const QImage& img, ImageRequest* request ) { // No size specified, meaning we want it full size. if ( request->width() == -1 ) return false; if ( img.width() < request->width() && img.height() < request->height() ) { // The image is smaller than the requets. return request->doUpScale(); } return true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ThumbnailBuilder.cpp b/ImageManager/ThumbnailBuilder.cpp index 7c15c5fc..0835f4a6 100644 --- a/ImageManager/ThumbnailBuilder.cpp +++ b/ImageManager/ThumbnailBuilder.cpp @@ -1,215 +1,215 @@ /* Copyright (C) 2003-2018 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. */ #include "AsyncLoader.h" #include "PreloadRequest.h" #include "Logging.h" #include "ThumbnailBuilder.h" #include "ThumbnailCache.h" #include #include #include #include #include #include #include #include #include ImageManager::ThumbnailBuilder* ImageManager::ThumbnailBuilder::s_instance = nullptr; ImageManager::ThumbnailBuilder::ThumbnailBuilder( MainWindow::StatusBar* statusBar, QObject* parent ) - :QObject( parent ), + :QObject( parent ), m_statusBar( statusBar ), m_count( 0 ), m_isBuilding( false ), m_loadedCount( 0 ), m_preloadQueue( nullptr ), m_scout( nullptr ) { connect(m_statusBar, &MainWindow::StatusBar::cancelRequest, this, &ThumbnailBuilder::cancelRequests); s_instance = this; // Make sure that this is created early, in the main thread, so it // can receive signals. ThumbnailCache::instance(); m_startBuildTimer = new QTimer(this); m_startBuildTimer->setSingleShot(true); connect(m_startBuildTimer, &QTimer::timeout, this, &ThumbnailBuilder::doThumbnailBuild); } void ImageManager::ThumbnailBuilder::cancelRequests() { ImageManager::AsyncLoader::instance()->stop( this, ImageManager::StopAll ); m_isBuilding = false; m_statusBar->setProgressBarVisible(false); m_startBuildTimer->stop(); } void ImageManager::ThumbnailBuilder::terminateScout() { if (m_scout) { delete m_scout; m_scout = nullptr; } if (m_preloadQueue) { delete m_preloadQueue; m_preloadQueue = nullptr; } } void ImageManager::ThumbnailBuilder::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& /*image*/) { const DB::FileName fileName = request->databaseFileName(); const QSize fullSize = request->fullSize(); if ( fullSize.width() != -1 ) { DB::ImageInfoPtr info = DB::ImageDB::instance()->info( fileName ); info->setSize( fullSize ); } m_loadedCount++; m_statusBar->setProgress( ++m_count ); if ( m_count >= m_expectedThumbnails ) { terminateScout(); } } void ImageManager::ThumbnailBuilder::buildAll( ThumbnailBuildStart when ) { QMessageBox msgBox; msgBox.setText(QString::fromLatin1("Buliding all thumbnails may take a long time.")); msgBox.setInformativeText(QString::fromLatin1("Do you want to rebuild all of your thumbnails?")); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if ( ret == QMessageBox::Yes ) { ImageManager::ThumbnailCache::instance()->flush(); scheduleThumbnailBuild( DB::ImageDB::instance()->images(), when ); } } ImageManager::ThumbnailBuilder* ImageManager::ThumbnailBuilder::instance() { Q_ASSERT( s_instance ); return s_instance; } void ImageManager::ThumbnailBuilder::buildMissing() { const DB::FileNameList images = DB::ImageDB::instance()->images(); DB::FileNameList needed; for ( const DB::FileName& fileName : images ) { if ( ! ImageManager::ThumbnailCache::instance()->contains( fileName ) ) needed.append( fileName ); } scheduleThumbnailBuild( needed, StartDelayed ); } void ImageManager::ThumbnailBuilder::scheduleThumbnailBuild( const DB::FileNameList& list, ThumbnailBuildStart when ) { if ( list.count() == 0 ) return; if ( m_isBuilding ) cancelRequests(); DB::OptimizedFileList files(list); m_thumbnailsToBuild = files.optimizedDbFiles(); m_startBuildTimer->start( when == StartNow ? 0 : 5000 ); } void ImageManager::ThumbnailBuilder::buildOneThumbnail( const DB::ImageInfoPtr& info ) { ImageManager::ImageRequest* request = new ImageManager::PreloadRequest( info->fileName(), ThumbnailView::CellGeometry::preferredIconSize(), info->angle(), this ); request->setIsThumbnailRequest(true); request->setPriority( ImageManager::BuildThumbnails ); ImageManager::AsyncLoader::instance()->load( request ); } void ImageManager::ThumbnailBuilder::doThumbnailBuild() { m_isBuilding = true; int numberOfThumbnailsToBuild = 0; terminateScout(); m_count = 0; m_loadedCount = 0; m_preloadQueue = new DB::ImageScoutQueue; for (const DB::FileName& fileName : m_thumbnailsToBuild ) { m_preloadQueue->enqueue(fileName); } qCDebug(ImageManagerLog) << "thumbnail builder starting scout"; m_scout = new DB::ImageScout(*m_preloadQueue, m_loadedCount, 1); m_scout->setMaxSeekAhead(10); m_scout->setReadLimit(10 * 1048576); m_scout->start(); m_statusBar->startProgress( i18n("Building thumbnails"), qMax( m_thumbnailsToBuild.size() - 1, 1 ) ); // We'll update this later. Meanwhile, we want to make sure that the scout // isn't prematurely terminated because the expected number of thumbnails // is less than (i. e. zero) the number of thumbnails actually built. m_expectedThumbnails = m_thumbnailsToBuild.size(); for (const DB::FileName& fileName : m_thumbnailsToBuild ) { DB::ImageInfoPtr info = fileName.info(); if ( ImageManager::AsyncLoader::instance()->isExiting() ) { cancelRequests(); break; } if ( info->isNull()) { m_loadedCount++; m_count++; continue; } ImageManager::ImageRequest* request = new ImageManager::PreloadRequest( fileName, ThumbnailView::CellGeometry::preferredIconSize(), info->angle(), this ); request->setIsThumbnailRequest(true); request->setPriority( ImageManager::BuildThumbnails ); if (ImageManager::AsyncLoader::instance()->load( request )) ++numberOfThumbnailsToBuild; } m_expectedThumbnails = numberOfThumbnailsToBuild; if (numberOfThumbnailsToBuild == 0) { m_statusBar->setProgressBarVisible(false); terminateScout(); } } void ImageManager::ThumbnailBuilder::save() { ImageManager::ThumbnailCache::instance()->save(); } void ImageManager::ThumbnailBuilder::requestCanceled() { m_statusBar->setProgress( ++m_count ); m_loadedCount++; if ( m_count >= m_expectedThumbnails ) { terminateScout(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ThumbnailCache.cpp b/ImageManager/ThumbnailCache.cpp index c78d0421..d6817748 100644 --- a/ImageManager/ThumbnailCache.cpp +++ b/ImageManager/ThumbnailCache.cpp @@ -1,483 +1,483 @@ /* Copyright (C) 2003-2010 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. */ #include "ThumbnailCache.h" #include "Logging.h" #include #include #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=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); } 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; }; } ImageManager::ThumbnailCache* ImageManager::ThumbnailCache::s_instance = nullptr; ImageManager::ThumbnailCache::ThumbnailCache() : 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); 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; } 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 ) { m_currentWriter->close(); 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 QString dir ) const { return thumbnailPath(QString::fromLatin1("thumb-") + QString::number(index), dir ); } QPixmap ImageManager::ThumbnailCache::lookup( 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 QPixmap(); } m_memcache->insert(info.fileIndex,t); } QByteArray array( t->map.mid(info.offset , info.size ) ); 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 ) { m_currentWriter->close(); 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; dataLocker.unlock(); QDataStream stream(&file); stream << THUMBNAIL_FILE_VERSION << m_currentFile << m_currentOffset << m_hash.count(); for( QHash::ConstIterator it = tempHash.begin(); it != tempHash.end(); ++it ) { const CacheFileInfo& cacheInfo = it.value(); stream << it.key().relative() << cacheInfo.fileIndex << cacheInfo.offset << cacheInfo.size; } file.close(); const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex")); 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 ) { m_currentWriter->close(); 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(QString::fromLatin1("thumbnailindex")); 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( QHash::ConstIterator it = tempUnsavedHash.begin(); it != tempUnsavedHash.end(); ++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(QString::fromLatin1("thumbnailindex")); // 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( QString::fromLatin1("thumbnailindex")) ); if ( !file.exists() ) return; QElapsedTimer timer; timer.start(); file.open(QIODevice::ReadOnly); QDataStream stream(&file); int version; stream >> version; if ( version != THUMBNAIL_FILE_VERSION ) return; //Discard cache // We can't allow anything to modify the structure while we're doing this. QMutexLocker dataLocker(&m_dataLock); 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 QString dir) const { QString base = QDir(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath( dir ); return base + file; } ImageManager::ThumbnailCache* ImageManager::ThumbnailCache::instance() { if (!s_instance) { s_instance = new ThumbnailCache; } return s_instance; } void ImageManager::ThumbnailCache::deleteInstance() { delete s_instance; s_instance = nullptr; } 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; Q_FOREACH(const DB::FileName &fileName, files) { m_hash.remove( fileName ); } dataLocker.unlock(); save(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/VideoLengthExtractor.h b/ImageManager/VideoLengthExtractor.h index ff925dcb..89ef758a 100644 --- a/ImageManager/VideoLengthExtractor.h +++ b/ImageManager/VideoLengthExtractor.h @@ -1,56 +1,56 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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. If not, see . */ #ifndef VIDEOLENGTHEXTRACTOR_H #define VIDEOLENGTHEXTRACTOR_H #include #include namespace Utilities { class Process; } namespace ImageManager { /** \brief \todo \see \ref videothumbnails */ class VideoLengthExtractor : public QObject { Q_OBJECT public: explicit VideoLengthExtractor(QObject *parent = nullptr); void extract(const DB::FileName& fileName ); - + signals: void lengthFound(int length); void unableToDetermineLength(); private slots: void processEnded(); private: Utilities::Process* m_process; DB::FileName m_fileName; }; } #endif // VIDEOLENGTHEXTRACTOR_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/documentation.h b/ImportExport/documentation.h index 5f5fb8f1..56ad3314 100644 --- a/ImportExport/documentation.h +++ b/ImportExport/documentation.h @@ -1,7 +1,7 @@ //krazy:skip /** \namespace ImportExport - \brief + \brief **/ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/AutoStackImages.cpp b/MainWindow/AutoStackImages.cpp index 64b15bf2..26e7f0d2 100644 --- a/MainWindow/AutoStackImages.cpp +++ b/MainWindow/AutoStackImages.cpp @@ -1,327 +1,327 @@ /* Copyright (C) 2010-2019 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 "AutoStackImages.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MainWindow; AutoStackImages::AutoStackImages( QWidget* parent, const DB::FileNameList& list ) :QDialog( parent ), m_list( list ) { setWindowTitle( i18nc("@title:window", "Automatically Stack Images" ) ); QWidget* top = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( top ); setLayout(lay1); QWidget* containerMd5 = new QWidget( this ); lay1->addWidget( containerMd5 ); QHBoxLayout* hlayMd5 = new QHBoxLayout( containerMd5 ); m_matchingMD5 = new QCheckBox( i18n( "Stack images with identical MD5 sum") ); m_matchingMD5->setChecked( false ); hlayMd5->addWidget( m_matchingMD5 ); QWidget* containerFile = new QWidget( this ); lay1->addWidget( containerFile ); QHBoxLayout* hlayFile = new QHBoxLayout( containerFile ); m_matchingFile = new QCheckBox( i18n( "Stack images based on file version detection") ); m_matchingFile->setChecked( true ); hlayFile->addWidget( m_matchingFile ); - + m_origTop = new QCheckBox( i18n( "Original to top") ); m_origTop ->setChecked( false ); hlayFile->addWidget( m_origTop ); QWidget* containerContinuous = new QWidget( this ); lay1->addWidget( containerContinuous ); QHBoxLayout* hlayContinuous = new QHBoxLayout( containerContinuous ); //FIXME: This is hard to translate because of the split sentence. It is better //to use a single sentence here like "Stack images that are (were?) shot //within this time:" and use the spin method setSuffix() to set the "seconds". //Also: Would minutes not be a more sane time unit here? (schwarzer) m_continuousShooting = new QCheckBox( i18nc( "The whole sentence should read: *Stack images that are shot within x seconds of each other*. So images that are shot in one burst are automatically stacked together. (This sentence is before the x.)", "Stack images that are shot within" ) ); m_continuousShooting->setChecked( false ); hlayContinuous->addWidget( m_continuousShooting ); m_continuousThreshold = new QSpinBox; m_continuousThreshold->setRange( 1, 999 ); m_continuousThreshold->setSingleStep( 1 ); m_continuousThreshold->setValue( 2 ); hlayContinuous->addWidget( m_continuousThreshold ); QLabel* sec = new QLabel( i18nc( "The whole sentence should read: *Stack images that are shot within x seconds of each other*. (This being the text after x.)", "seconds" ), containerContinuous ); hlayContinuous->addWidget( sec ); QGroupBox* grpOptions = new QGroupBox( i18n("AutoStacking Options") ); QVBoxLayout* grpLayOptions = new QVBoxLayout( grpOptions ); lay1->addWidget( grpOptions ); m_autostackDefault = new QRadioButton( i18n( "Include matching image to appropriate stack (if one exists)") ); m_autostackDefault->setChecked( true ); grpLayOptions->addWidget( m_autostackDefault ); m_autostackUnstack = new QRadioButton( i18n( "Unstack images from their current stack and create new one for the matches") ); m_autostackUnstack->setChecked( false ); grpLayOptions->addWidget( m_autostackUnstack ); m_autostackSkip = new QRadioButton( i18n( "Skip images that are already in a stack") ); m_autostackSkip->setChecked( false ); grpLayOptions->addWidget( m_autostackSkip ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &AutoStackImages::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &AutoStackImages::reject); lay1->addWidget(buttonBox); } /* * This function searches for images with matching MD5 sums * Matches are automatically stacked */ void AutoStackImages::matchingMD5( DB::FileNameList& toBeShown ) { QMap< DB::MD5, DB::FileNameList > tostack; DB::FileNameList showIfStacked; // Stacking all images that have the same MD5 sum // First make a map of MD5 sums with corresponding images Q_FOREACH(const DB::FileName& fileName, m_list) { DB::MD5 sum = fileName.info()->MD5Sum(); if ( DB::ImageDB::instance()->md5Map()->contains( sum ) ) { if (tostack[sum].isEmpty()) tostack.insert(sum, DB::FileNameList() << fileName); else tostack[sum].append(fileName); } } // Then add images to stack (depending on configuration options) for( QMap::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it ) { if ( tostack[it.key()].count() > 1 ) { DB::FileNameList stack; for ( int i = 0; i < tostack[it.key()].count(); ++i ) { if ( !DB::ImageDB::instance()->getStackFor( tostack[it.key()][i]).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << tostack[it.key()][i]); else if ( m_autostackSkip->isChecked() ) continue; } showIfStacked.append( tostack[it.key()][i] ); stack.append( tostack[it.key()][i]); } if ( stack.size() > 1 ) { Q_FOREACH( const DB::FileName& a, showIfStacked ) { if ( !DB::ImageDB::instance()->getStackFor(a).isEmpty() ) Q_FOREACH( const DB::FileName& b, DB::ImageDB::instance()->getStackFor(a)) toBeShown.append( b ); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images based on file version detection configuration. * Images that are detected to be versions of same file are stacked together. */ void AutoStackImages::matchingFile( DB::FileNameList& toBeShown ) { QMap< DB::MD5, DB::FileNameList > tostack; DB::FileNameList showIfStacked; QString modifiedFileCompString; QRegExp modifiedFileComponent; QStringList originalFileComponents; - + modifiedFileCompString = Settings::SettingsData::instance()->modifiedFileComponent(); modifiedFileComponent = QRegExp( modifiedFileCompString ); originalFileComponents << Settings::SettingsData::instance()->originalFileComponent(); originalFileComponents = originalFileComponents.at( 0 ).split( QString::fromLatin1(";") ); // Stacking all images based on file version detection // First round prepares the stacking Q_FOREACH( const DB::FileName& fileName, m_list ) { if ( modifiedFileCompString.length() >= 0 && fileName.relative().contains( modifiedFileComponent ) ) { for( QStringList::const_iterator it = originalFileComponents.constBegin(); it != originalFileComponents.constEnd(); ++it ) { QString tmp = fileName.relative(); tmp.replace( modifiedFileComponent, ( *it )); DB::FileName originalFileName = DB::FileName::fromRelativePath( tmp ); if ( originalFileName != fileName && m_list.contains( originalFileName ) ) { DB::MD5 sum = originalFileName.info()->MD5Sum(); if ( tostack[sum].isEmpty() ) { if ( m_origTop->isChecked() ) { tostack.insert( sum, DB::FileNameList() << originalFileName ); tostack[sum].append( fileName ); } else { tostack.insert( sum, DB::FileNameList() << fileName ); tostack[sum].append( originalFileName ); } } else tostack[sum].append(fileName); break; } } } } - + // Then add images to stack (depending on configuration options) for( QMap::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it ) { if ( tostack[it.key()].count() > 1 ) { DB::FileNameList stack; for ( int i = 0; i < tostack[it.key()].count(); ++i ) { if ( !DB::ImageDB::instance()->getStackFor( tostack[it.key()][i]).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << tostack[it.key()][i]); else if ( m_autostackSkip->isChecked() ) continue; } showIfStacked.append( tostack[it.key()][i] ); stack.append( tostack[it.key()][i]); } if ( stack.size() > 1 ) { Q_FOREACH( const DB::FileName& a, showIfStacked ) { if ( !DB::ImageDB::instance()->getStackFor(a).isEmpty() ) Q_FOREACH( const DB::FileName& b, DB::ImageDB::instance()->getStackFor(a)) toBeShown.append( b ); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images that are shot within specified time frame */ void AutoStackImages::continuousShooting(DB::FileNameList &toBeShown ) { DB::ImageInfoPtr prev; Q_FOREACH(const DB::FileName& fileName, m_list) { DB::ImageInfoPtr info = fileName.info(); // Skipping images that do not have exact time stamp if ( info->date().start() != info->date().end() ) continue; if ( prev && ( prev->date().start().secsTo( info->date().start() ) < m_continuousThreshold->value() ) ) { DB::FileNameList stack; if ( !DB::ImageDB::instance()->getStackFor( prev->fileName() ).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << prev->fileName()); else if ( m_autostackSkip->isChecked() ) continue; } if ( !DB::ImageDB::instance()->getStackFor(fileName).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << fileName); else if ( m_autostackSkip->isChecked() ) continue; } stack.append(prev->fileName()); stack.append(info->fileName()); if ( !toBeShown.isEmpty() ) { if ( toBeShown.at( toBeShown.size() - 1 ).info()->fileName() != prev->fileName() ) toBeShown.append(prev->fileName()); } else { // if this is first insert, we have to include also the stacked images from previuous image if ( !DB::ImageDB::instance()->getStackFor( info->fileName() ).isEmpty() ) Q_FOREACH( const DB::FileName& a, DB::ImageDB::instance()->getStackFor( prev->fileName() ) ) toBeShown.append( a ); else toBeShown.append(prev->fileName()); } // Inserting stacked images from the current image if ( !DB::ImageDB::instance()->getStackFor( info->fileName() ).isEmpty() ) Q_FOREACH( const DB::FileName& a, DB::ImageDB::instance()->getStackFor(fileName)) toBeShown.append( a ); else toBeShown.append(info->fileName()); DB::ImageDB::instance()->stack(stack); } prev = info; } } void AutoStackImages::accept() { QDialog::accept(); Utilities::ShowBusyCursor dummy; DB::FileNameList toBeShown; if ( m_matchingMD5->isChecked() ) matchingMD5( toBeShown ); if ( m_matchingFile->isChecked() ) matchingFile( toBeShown ); if ( m_continuousShooting->isChecked() ) continuousShooting( toBeShown ); MainWindow::Window::theMainWindow()->showThumbNails(toBeShown); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/UpdateVideoThumbnail.cpp b/MainWindow/UpdateVideoThumbnail.cpp index 4d5428a3..f64a17bf 100644 --- a/MainWindow/UpdateVideoThumbnail.cpp +++ b/MainWindow/UpdateVideoThumbnail.cpp @@ -1,86 +1,86 @@ /* Copyright (C) 2012-2019 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #include "UpdateVideoThumbnail.h" #include #include #include #include #include "Window.h" #include namespace MainWindow { void UpdateVideoThumbnail::useNext(const DB::FileNameList& list) { update(list,+1); } void UpdateVideoThumbnail::usePrevious(const DB::FileNameList& list) { update(list, -1); } void UpdateVideoThumbnail::update(const DB::FileNameList& list, int direction) { Q_FOREACH(const DB::FileName& fileName, list) { if (Utilities::isVideo(fileName)) update(fileName, direction); } } void UpdateVideoThumbnail::update(const DB::FileName &fileName, int direction) { const DB::FileName baseImageName = BackgroundJobs::HandleVideoThumbnailRequestJob::pathForRequest(fileName); QImage baseImage(baseImageName.absolute()); int frame = 0; for (; frame < 10; ++frame) { const DB::FileName frameFile = BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(fileName, frame); QImage frameImage(frameFile.absolute()); if (frameImage.isNull()) continue; if ( baseImage == frameImage ) { break; } } const DB::FileName newImageName = nextExistingImage(fileName, frame, direction); Utilities::copyOrOverwrite(newImageName.absolute(),baseImageName.absolute()); QImage image = QImage(newImageName.absolute()).scaled(ThumbnailView::CellGeometry::preferredIconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation ); ImageManager::ThumbnailCache::instance()->insert(fileName,image); MainWindow::Window::theMainWindow()->reloadThumbnails(); } DB::FileName UpdateVideoThumbnail::nextExistingImage(const DB::FileName &fileName, int frame, int direction) { for (int i = 1; i <10; ++i) { const int nextIndex = (frame + 10 + direction*i) % 10; const DB::FileName file = BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(fileName, nextIndex); if ( file.exists() ) return file; } Q_ASSERT(false && "We should always find at least the current frame"); return DB::FileName(); } } // namespace MainWindow // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/UpdateVideoThumbnail.h b/MainWindow/UpdateVideoThumbnail.h index 02574ea1..24598ae5 100644 --- a/MainWindow/UpdateVideoThumbnail.h +++ b/MainWindow/UpdateVideoThumbnail.h @@ -1,41 +1,41 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef MAINWINDOW_UPDATEVIDEOTHUMBNAIL_H #define MAINWINDOW_UPDATEVIDEOTHUMBNAIL_H #include namespace MainWindow { class UpdateVideoThumbnail { public: static void useNext(const DB::FileNameList& ); static void usePrevious( const DB::FileNameList& ); private: static void update(const DB::FileNameList&, int direction); static void update(const DB::FileName& fileName, int direction); static DB::FileName nextExistingImage(const DB::FileName& fileName, int frame, int direction); }; } // namespace MainWindow #endif // MAINWINDOW_UPDATEVIDEOTHUMBNAIL_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/UploadImageCollection.cpp b/Plugins/UploadImageCollection.cpp index 7938478b..92c2777c 100644 --- a/Plugins/UploadImageCollection.cpp +++ b/Plugins/UploadImageCollection.cpp @@ -1,59 +1,59 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #include "UploadImageCollection.h" #include #include namespace Plugins { UploadImageCollection::UploadImageCollection(const QString& path) :m_path(path) { } QList UploadImageCollection::images() { return QList(); } QString UploadImageCollection::name() { return QString(); } QUrl UploadImageCollection::uploadUrl() { return QUrl::fromLocalFile(m_path); } QUrl UploadImageCollection::uploadRootUrl() { QUrl url = QUrl::fromLocalFile(Settings::SettingsData::instance()->imageDirectory() ); return url; } QString UploadImageCollection::uploadRootName() { return i18nc("'Name' of the image directory", "Image/Video root directory"); } } // namespace Plugins // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/UploadImageCollection.h b/Plugins/UploadImageCollection.h index 8a1ecd9a..a445f295 100644 --- a/Plugins/UploadImageCollection.h +++ b/Plugins/UploadImageCollection.h @@ -1,46 +1,46 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef PLUGINS_UPLOADIMAGECOLLECTION_H #define PLUGINS_UPLOADIMAGECOLLECTION_H #include namespace Plugins { class UploadImageCollection : public KIPI::ImageCollectionShared { public: explicit UploadImageCollection(const QString& path); QList images() override; QString name() override; QUrl uploadUrl() override; virtual QUrl uploadRootUrl() override; virtual QString uploadRootName() override; private: QString m_path; }; } // namespace Plugins #endif // PLUGINS_UPLOADIMAGECOLLECTION_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/UploadWidget.cpp b/Plugins/UploadWidget.cpp index f641225d..1bfde417 100644 --- a/Plugins/UploadWidget.cpp +++ b/Plugins/UploadWidget.cpp @@ -1,61 +1,61 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #include "UploadWidget.h" #include #include #include #include #include "ImageCollection.h" #include "UploadImageCollection.h" namespace Plugins { UploadWidget::UploadWidget( QWidget* parent ) : KIPI::UploadWidget( parent ) { QTreeView* listView = new QTreeView(this); QHBoxLayout* layout = new QHBoxLayout(this); layout->addWidget(listView); m_model = new QFileSystemModel(this); m_model->setFilter( QDir::Dirs | QDir::NoDotDot); listView->setModel(m_model); m_path = Settings::SettingsData::instance()->imageDirectory(); const QModelIndex index = m_model->setRootPath( m_path ); listView->setRootIndex(index); connect(listView, &QTreeView::activated, this, &UploadWidget::newIndexSelected); } KIPI::ImageCollection UploadWidget::selectedImageCollection() const { return KIPI::ImageCollection( new Plugins::UploadImageCollection( m_path ) ); } void UploadWidget::newIndexSelected(const QModelIndex& index ) { m_path = m_model->filePath(index); } } // namespace Plugins // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/UploadWidget.h b/Plugins/UploadWidget.h index 41f6e716..cf02bfde 100644 --- a/Plugins/UploadWidget.h +++ b/Plugins/UploadWidget.h @@ -1,50 +1,50 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef PLUGINS_UPLOADWIDGET_H #define PLUGINS_UPLOADWIDGET_H #include #include class QFileSystemModel; class QModelIndex; namespace Plugins { class UploadWidget : public KIPI::UploadWidget { Q_OBJECT public: explicit UploadWidget(QWidget* parent); KIPI::ImageCollection selectedImageCollection() const override; private slots: void newIndexSelected(const QModelIndex& index); private: QFileSystemModel* m_model; QString m_path; }; } // namespace Plugins #endif // PLUGINS_UPLOADWIDGET_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailModel.cpp b/ThumbnailView/ThumbnailModel.cpp index 448c69a9..4aff2dde 100644 --- a/ThumbnailView/ThumbnailModel.cpp +++ b/ThumbnailView/ThumbnailModel.cpp @@ -1,465 +1,465 @@ /* Copyright (C) 2003-2019 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 "ThumbnailModel.h" #include #include #include #include #include #include #include #include #include "CellGeometry.h" #include "ThumbnailRequest.h" #include "ThumbnailWidget.h" #include "SelectionMaintainer.h" ThumbnailView::ThumbnailModel::ThumbnailModel( ThumbnailFactory* factory) : ThumbnailComponent( factory ) , m_sortDirection( Settings::SettingsData::instance()->showNewestThumbnailFirst() ? NewestFirst : OldestFirst ) , m_firstVisibleRow(-1) , m_lastVisibleRow(-1) { connect( DB::ImageDB::instance(), SIGNAL(imagesDeleted(DB::FileNameList)), this, SLOT(imagesDeletedFromDB(DB::FileNameList)) ); m_ImagePlaceholder = QIcon::fromTheme( QLatin1String("image-x-generic") ).pixmap( cellGeometryInfo()->preferredIconSize() ); m_VideoPlaceholder = QIcon::fromTheme( QLatin1String("video-x-generic") ).pixmap( cellGeometryInfo()->preferredIconSize() ); } static bool stackOrderComparator(const DB::FileName& a, const DB::FileName& b) { return a.info()->stackOrder() < b.info()->stackOrder(); } void ThumbnailView::ThumbnailModel::updateDisplayModel() { beginResetModel(); ImageManager::AsyncLoader::instance()->stop( model(), ImageManager::StopOnlyNonPriorityLoads ); // Note, this can be simplified, if we make the database backend already // return things in the right order. Then we only need one pass while now // we need to go through the list two times. /* Extract all stacks we have first. Different stackid's might be * intermingled in the result so we need to know this ahead before * creating the display list. */ typedef QList StackList; typedef QMap StackMap; StackMap stackContents; Q_FOREACH(const DB::FileName& fileName, m_imageList) { DB::ImageInfoPtr imageInfo = fileName.info(); if ( imageInfo && imageInfo->isStacked() ) { DB::StackID stackid = imageInfo->stackId(); stackContents[stackid].append(fileName); } } /* * All stacks need to be ordered in their stack order. We don't rely that * the images actually came in the order necessary. */ for (StackMap::iterator it = stackContents.begin(); it != stackContents.end(); ++it) { std::stable_sort(it->begin(), it->end(), stackOrderComparator); } /* Build the final list to be displayed. That is basically the sequence * we got from the original, but the stacks shown with all images together * in the right sequence or collapsed showing only the top image. */ m_displayList = DB::FileNameList(); QSet alreadyShownStacks; Q_FOREACH( const DB::FileName& fileName, m_imageList) { DB::ImageInfoPtr imageInfo = fileName.info(); if ( imageInfo && imageInfo->isStacked()) { DB::StackID stackid = imageInfo->stackId(); if (alreadyShownStacks.contains(stackid)) continue; StackMap::iterator found = stackContents.find(stackid); Q_ASSERT(found != stackContents.end()); const StackList& orderedStack = *found; if (m_expandedStacks.contains(stackid)) { Q_FOREACH( const DB::FileName& fileName, orderedStack) { m_displayList.append(fileName); } } else { m_displayList.append(orderedStack.at(0)); } alreadyShownStacks.insert(stackid); } else { m_displayList.append(fileName); } } if ( m_sortDirection != OldestFirst ) m_displayList = m_displayList.reversed(); updateIndexCache(); emit collapseAllStacksEnabled( m_expandedStacks.size() > 0); emit expandAllStacksEnabled( m_allStacks.size() != model()->m_expandedStacks.size() ); endResetModel(); } void ThumbnailView::ThumbnailModel::toggleStackExpansion(const DB::FileName& fileName) { DB::ImageInfoPtr imageInfo = fileName.info(); if (imageInfo) { DB::StackID stackid = imageInfo->stackId(); model()->beginResetModel(); if (m_expandedStacks.contains(stackid)) m_expandedStacks.remove(stackid); else m_expandedStacks.insert(stackid); updateDisplayModel(); model()->endResetModel(); } } void ThumbnailView::ThumbnailModel::collapseAllStacks() { m_expandedStacks.clear(); updateDisplayModel(); } void ThumbnailView::ThumbnailModel::expandAllStacks() { m_expandedStacks = m_allStacks; updateDisplayModel(); } void ThumbnailView::ThumbnailModel::setImageList(const DB::FileNameList& items) { m_imageList = items; m_allStacks.clear(); Q_FOREACH( const DB::FileName& fileName, items) { DB::ImageInfoPtr info = fileName.info(); if ( info && info->isStacked() ) m_allStacks << info->stackId(); } updateDisplayModel(); preloadThumbnails(); } // TODO(hzeller) figure out if this should return the m_imageList or m_displayList. DB::FileNameList ThumbnailView::ThumbnailModel::imageList(Order order) const { if ( order == SortedOrder && m_sortDirection == NewestFirst ) return m_displayList.reversed(); else return m_displayList; } void ThumbnailView::ThumbnailModel::imagesDeletedFromDB( const DB::FileNameList& list ) { SelectionMaintainer dummy(widget(),model()); Q_FOREACH( const DB::FileName& fileName, list ) { m_displayList.removeAll(fileName); m_imageList.removeAll(fileName); } updateDisplayModel(); } int ThumbnailView::ThumbnailModel::indexOf(const DB::FileName& fileName) { Q_ASSERT( !fileName.isNull() ); if ( !m_fileNameToIndex.contains(fileName) ) m_fileNameToIndex.insert(fileName, m_displayList.indexOf(fileName)); - + return m_fileNameToIndex[fileName]; } int ThumbnailView::ThumbnailModel::indexOf(const DB::FileName& fileName) const { Q_ASSERT( !fileName.isNull() ); if ( !m_fileNameToIndex.contains(fileName) ) return -1; - + return m_fileNameToIndex[fileName]; } void ThumbnailView::ThumbnailModel::updateIndexCache() { m_fileNameToIndex.clear(); int index = 0; Q_FOREACH( const DB::FileName& fileName, m_displayList) { m_fileNameToIndex[fileName] = index; ++index; } } DB::FileName ThumbnailView::ThumbnailModel::rightDropItem() const { return m_rightDrop; } void ThumbnailView::ThumbnailModel::setRightDropItem( const DB::FileName& item ) { m_rightDrop = item; } DB::FileName ThumbnailView::ThumbnailModel::leftDropItem() const { return m_leftDrop; } void ThumbnailView::ThumbnailModel::setLeftDropItem( const DB::FileName& item ) { m_leftDrop = item; } void ThumbnailView::ThumbnailModel::setSortDirection( SortDirection direction ) { if ( direction == m_sortDirection ) return; Settings::SettingsData::instance()->setShowNewestFirst( direction == NewestFirst ); m_displayList = m_displayList.reversed(); updateIndexCache(); m_sortDirection = direction; } bool ThumbnailView::ThumbnailModel::isItemInExpandedStack( const DB::StackID& id ) const { return m_expandedStacks.contains(id); } int ThumbnailView::ThumbnailModel::imageCount() const { return m_displayList.size(); } void ThumbnailView::ThumbnailModel::setOverrideImage(const DB::FileName& fileName, const QPixmap &pixmap) { if ( pixmap.isNull() ) m_overrideFileName = DB::FileName(); else { m_overrideFileName = fileName; m_overrideImage = pixmap; } emit dataChanged( fileNameToIndex(fileName), fileNameToIndex(fileName)); } DB::FileName ThumbnailView::ThumbnailModel::imageAt( int index ) const { Q_ASSERT( index >= 0 && index < imageCount() ); return m_displayList.at(index); } int ThumbnailView::ThumbnailModel::rowCount(const QModelIndex&) const { return imageCount(); } QVariant ThumbnailView::ThumbnailModel::data(const QModelIndex& index, int role ) const { if ( !index.isValid() || index.row() >= m_displayList.size()) return QVariant(); if ( role == Qt::DecorationRole ) { const DB::FileName fileName = m_displayList.at(index.row()); return pixmap( fileName ); } if ( role == Qt::DisplayRole ) return thumbnailText( index ); return QVariant(); } void ThumbnailView::ThumbnailModel::requestThumbnail( const DB::FileName& fileName, const ImageManager::Priority priority ) { DB::ImageInfoPtr imageInfo = fileName.info(); if ( !imageInfo ) return; // request the thumbnail in the size that is set in the settings, not in the current grid size: const QSize cellSize = cellGeometryInfo()->baseIconSize(); const int angle = imageInfo->angle(); const int row = indexOf(fileName); ThumbnailRequest* request = new ThumbnailRequest( row, fileName, cellSize, angle, this ); request->setPriority( priority ); ImageManager::AsyncLoader::instance()->load( request ); } void ThumbnailView::ThumbnailModel::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& /*image*/) { const DB::FileName fileName = request->databaseFileName(); const QSize fullSize = request->fullSize(); // As a result of the image being loaded, we emit the dataChanged signal, which in turn asks the delegate to paint the cell // The delegate now fetches the newly loaded image from the cache. DB::ImageInfoPtr imageInfo = fileName.info(); // TODO(hzeller): figure out, why the size is set here. We do an implicit // write here to the database. if ( fullSize.isValid() && imageInfo ) { imageInfo->setSize( fullSize ); } emit dataChanged(fileNameToIndex(fileName), fileNameToIndex(fileName)); } QString ThumbnailView::ThumbnailModel::thumbnailText( const QModelIndex& index ) const { const DB::FileName fileName = imageAt( index.row() ); QString text; const QSize cellSize = cellGeometryInfo()->preferredIconSize(); const int thumbnailHeight = cellSize.height() - 2 * Settings::SettingsData::instance()->thumbnailSpace(); const int thumbnailWidth = cellSize.width(); // no subtracting here const int maxCharacters = thumbnailHeight / QFontMetrics( widget()->font() ).maxWidth() * 2; if ( Settings::SettingsData::instance()->displayLabels()) { QString line = fileName.info()->label(); if ( QFontMetrics( widget()->font() ).width( line ) > thumbnailWidth ) { line = line.left( maxCharacters ); line += QString::fromLatin1( " ..." ); } text += line + QString::fromLatin1("\n"); } if ( Settings::SettingsData::instance()->displayCategories()) { QStringList grps = fileName.info()->availableCategories(); for( QStringList::const_iterator it = grps.constBegin(); it != grps.constEnd(); ++it ) { QString category = *it; if ( category != i18n( "Folder" ) && category != i18n( "Media Type" ) ) { Utilities::StringSet items = fileName.info()->itemsOfCategory( category ); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && ! Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (category == Settings::SettingsData::instance()->untaggedCategory()) { if (items.contains(Settings::SettingsData::instance()->untaggedTag())) { items.remove(Settings::SettingsData::instance()->untaggedTag()); } } } if (!items.empty()) { QString line; bool first = true; for( Utilities::StringSet::const_iterator it2 = items.begin(); it2 != items.end(); ++it2 ) { QString item = *it2; if ( first ) first = false; else line += QString::fromLatin1( ", " ); line += item; } if ( QFontMetrics( widget()->font() ).width( line ) > thumbnailWidth ) { line = line.left( maxCharacters ); line += QString::fromLatin1( " ..." ); } text += line + QString::fromLatin1( "\n" ); } } } } if(text.isEmpty()) text = QString::fromLatin1( "" ); return text.trimmed(); } void ThumbnailView::ThumbnailModel::updateCell( int row ) { updateCell( index( row, 0 ) ); } void ThumbnailView::ThumbnailModel::updateCell( const QModelIndex& index ) { emit dataChanged( index, index ); } void ThumbnailView::ThumbnailModel::updateCell( const DB::FileName& fileName ) { updateCell( indexOf(fileName) ); } QModelIndex ThumbnailView::ThumbnailModel::fileNameToIndex( const DB::FileName& fileName ) const { if ( fileName.isNull() ) return QModelIndex(); else return index( indexOf(fileName), 0 ); } QPixmap ThumbnailView::ThumbnailModel::pixmap( const DB::FileName& fileName ) const { if ( m_overrideFileName == fileName) return m_overrideImage; const DB::ImageInfoPtr imageInfo = fileName.info(); if (imageInfo == DB::ImageInfoPtr(nullptr) ) return QPixmap(); if ( ImageManager::ThumbnailCache::instance()->contains( fileName ) ) { // the cached thumbnail needs to be scaled to the actual thumbnail size: return ImageManager::ThumbnailCache::instance()->lookup( fileName ).scaled( cellGeometryInfo()->preferredIconSize(), Qt::KeepAspectRatio ); } const_cast(this)->requestThumbnail( fileName, ImageManager::ThumbnailVisible ); if ( imageInfo->isVideo() ) return m_VideoPlaceholder; else return m_ImagePlaceholder; } bool ThumbnailView::ThumbnailModel::thumbnailStillNeeded( int row ) const { return ( row >= m_firstVisibleRow && row <= m_lastVisibleRow ); } void ThumbnailView::ThumbnailModel::updateVisibleRowInfo() { m_firstVisibleRow = widget()->indexAt( QPoint(0,0) ).row(); const int columns = widget()->width() / cellGeometryInfo()->cellSize().width(); const int rows = widget()->height() / cellGeometryInfo()->cellSize().height(); m_lastVisibleRow = qMin(m_firstVisibleRow + columns*(rows+1), rowCount(QModelIndex())); // the cellGeometry has changed -> update placeholders m_ImagePlaceholder = QIcon::fromTheme( QLatin1String("image-x-generic") ).pixmap( cellGeometryInfo()->preferredIconSize() ); m_VideoPlaceholder = QIcon::fromTheme( QLatin1String("video-x-generic") ).pixmap( cellGeometryInfo()->preferredIconSize() ); } void ThumbnailView::ThumbnailModel::preloadThumbnails() { // FIXME: it would make a lot of sense to merge preloadThumbnails() with pixmap() // and maybe also move the caching stuff into the ImageManager Q_FOREACH( const DB::FileName& fileName, m_displayList) { if ( fileName.isNull() ) continue; if ( ImageManager::ThumbnailCache::instance()->contains( fileName ) ) continue; const_cast(this)->requestThumbnail( fileName, ImageManager::ThumbnailInvisible ); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailWidget.cpp b/ThumbnailView/ThumbnailWidget.cpp index 28199c3c..7c97d44b 100644 --- a/ThumbnailView/ThumbnailWidget.cpp +++ b/ThumbnailView/ThumbnailWidget.cpp @@ -1,442 +1,442 @@ /* Copyright (C) 2003-2019 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 "ThumbnailWidget.h" #include #include #include "Delegate.h" #include "ThumbnailDND.h" #include "KeyboardEventHandler.h" #include "ThumbnailFactory.h" #include "ThumbnailModel.h" #include "CellGeometry.h" #include #include #include #include #include #include "Browser/BrowserWidget.h" #include "DB/ImageDB.h" #include "DB/ImageInfoPtr.h" #include "Settings/SettingsData.h" #include "SelectionMaintainer.h" /** * \class ThumbnailView::ThumbnailWidget * This is the widget which shows the thumbnails. * * In previous versions this was implemented using a QIconView, but there * simply was too many problems, so after years of tears and pains I * rewrote it. */ ThumbnailView::ThumbnailWidget::ThumbnailWidget( ThumbnailFactory* factory) :QListView(), ThumbnailComponent( factory ), m_isSettingDate(false), m_gridResizeInteraction( factory ), m_wheelResizing( false ), m_externallyResizing( false ), m_selectionInteraction( factory ), m_mouseTrackingHandler( factory ), m_mouseHandler( &m_mouseTrackingHandler ), m_dndHandler( new ThumbnailDND( factory ) ), m_pressOnStackIndicator( false ), m_keyboardHandler( new KeyboardEventHandler( factory ) ), m_videoThumbnailCycler( new VideoThumbnailCycler(model()) ) { setModel( ThumbnailComponent::model() ); setResizeMode( QListView::Adjust ); setViewMode( QListView::IconMode ); setUniformItemSizes(true); setSelectionMode( QAbstractItemView::ExtendedSelection ); // It beats me why I need to set mouse tracking on both, but without it doesn't work. viewport()->setMouseTracking( true ); setMouseTracking( true ); connect( selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(scheduleDateChangeSignal()) ); viewport()->setAcceptDrops( true ); setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOn ); setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); connect(&m_mouseTrackingHandler, &MouseTrackingInteraction::fileIdUnderCursorChanged, this, &ThumbnailWidget::fileIdUnderCursorChanged); connect(m_keyboardHandler, &KeyboardEventHandler::showSelection, this, &ThumbnailWidget::showSelection); updatePalette(); setItemDelegate( new Delegate(factory, this) ); connect( selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(emitSelectionChangedSignal()) ); setDragEnabled(false); // We run our own dragging, so disable QListView's version. connect( verticalScrollBar(), SIGNAL(valueChanged(int)), model(), SLOT(updateVisibleRowInfo()) ); setupDateChangeTimer(); } bool ThumbnailView::ThumbnailWidget::isGridResizing() const { return m_mouseHandler->isResizingGrid() || m_wheelResizing || m_externallyResizing; } void ThumbnailView::ThumbnailWidget::keyPressEvent( QKeyEvent* event ) { if ( !m_keyboardHandler->keyPressEvent( event ) ) QListView::keyPressEvent( event ); } void ThumbnailView::ThumbnailWidget::keyReleaseEvent( QKeyEvent* event ) { const bool propagate = m_keyboardHandler->keyReleaseEvent( event ); if ( propagate ) QListView::keyReleaseEvent(event); } bool ThumbnailView::ThumbnailWidget::isMouseOverStackIndicator( const QPoint& point ) { // first check if image is stack, if not return. DB::ImageInfoPtr imageInfo = mediaIdUnderCursor().info(); if (!imageInfo) return false; if (!imageInfo->isStacked()) return false; const QModelIndex index = indexUnderCursor(); const QRect itemRect = visualRect( index ); const QPixmap pixmap = index.data( Qt::DecorationRole ).value(); if ( pixmap.isNull() ) return false; const QRect pixmapRect = cellGeometryInfo()->iconGeometry( pixmap ).translated( itemRect.topLeft() ); const QRect blackOutRect = pixmapRect.adjusted( 0,0, -10, -10 ); return pixmapRect.contains(point) && !blackOutRect.contains( point ); } static bool isMouseResizeGesture( QMouseEvent* event ) { return (event->button() & Qt::MidButton) || ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::AltModifier)); } void ThumbnailView::ThumbnailWidget::mousePressEvent( QMouseEvent* event ) { if ( (!(event->modifiers() & ( Qt::ControlModifier | Qt::ShiftModifier ) )) && isMouseOverStackIndicator( event->pos() ) ) { model()->toggleStackExpansion(mediaIdUnderCursor()); m_pressOnStackIndicator = true; return; } if ( isMouseResizeGesture( event ) ) m_mouseHandler = &m_gridResizeInteraction; else m_mouseHandler = &m_selectionInteraction; if ( !m_mouseHandler->mousePressEvent( event ) ) QListView::mousePressEvent( event ); if (event->button() & Qt::RightButton) //get out of selection mode if this is a right click m_mouseHandler = &m_mouseTrackingHandler; } void ThumbnailView::ThumbnailWidget::mouseMoveEvent( QMouseEvent* event ) { if ( m_pressOnStackIndicator ) return; if ( !m_mouseHandler->mouseMoveEvent( event ) ) QListView::mouseMoveEvent( event ); } void ThumbnailView::ThumbnailWidget::mouseReleaseEvent( QMouseEvent* event ) { if ( m_pressOnStackIndicator ) { m_pressOnStackIndicator = false; return; } if ( !m_mouseHandler->mouseReleaseEvent( event ) ) QListView::mouseReleaseEvent( event ); m_mouseHandler = &m_mouseTrackingHandler; } void ThumbnailView::ThumbnailWidget::mouseDoubleClickEvent( QMouseEvent * event ) { if ( isMouseOverStackIndicator( event->pos() ) ) { model()->toggleStackExpansion(mediaIdUnderCursor()); m_pressOnStackIndicator = true; } else if ( !( event->modifiers() & Qt::ControlModifier ) ) { DB::FileName id = mediaIdUnderCursor(); if ( !id.isNull() ) emit showImage( id ); } } void ThumbnailView::ThumbnailWidget::wheelEvent( QWheelEvent* event ) { if ( event->modifiers() & Qt::ControlModifier ) { event->setAccepted(true); if ( !m_wheelResizing) m_gridResizeInteraction.enterGridResizingMode(); m_wheelResizing = true; model()->beginResetModel(); const int delta = -event->delta() / 20; static int _minimum_ = Settings::SettingsData::instance()->minimumThumbnailSize(); Settings::SettingsData::instance()->setActualThumbnailSize( qMax( _minimum_, Settings::SettingsData::instance()->actualThumbnailSize() + delta ) ); cellGeometryInfo()->calculateCellSize(); model()->endResetModel(); } else { int delta = event->delta() / 5; QWheelEvent newevent = QWheelEvent(event->pos(), delta, event->buttons(), nullptr); QListView::wheelEvent(&newevent); } } void ThumbnailView::ThumbnailWidget::emitDateChange() { if ( m_isSettingDate ) return; int row = currentIndex().row(); if (row == -1) return; DB::FileName fileName = model()->imageAt( row ); if ( fileName.isNull() ) return; static QDateTime lastDate; QDateTime date = fileName.info()->date().start(); if ( date != lastDate ) { lastDate = date; if ( date.date().year() != 1900 ) emit currentDateChanged( date ); } } /** * scroll to the date specified with the parameter date. * The boolean includeRanges tells whether we accept range matches or not. */ void ThumbnailView::ThumbnailWidget::gotoDate( const DB::ImageDate& date, bool includeRanges ) { m_isSettingDate = true; DB::FileName candidate = DB::ImageDB::instance() ->findFirstItemInRange(model()->imageList(ViewOrder), date, includeRanges); if ( !candidate.isNull() ) setCurrentItem( candidate ); m_isSettingDate = false; } void ThumbnailView::ThumbnailWidget::setExternallyResizing( bool state ) { m_externallyResizing = state; } void ThumbnailView::ThumbnailWidget::reload(SelectionUpdateMethod method ) { SelectionMaintainer maintainer( this, model()); ThumbnailComponent::model()->beginResetModel(); cellGeometryInfo()->flushCache(); updatePalette(); ThumbnailComponent::model()->endResetModel(); if ( method == ClearSelection ) maintainer.disable(); } DB::FileName ThumbnailView::ThumbnailWidget::mediaIdUnderCursor() const { const QModelIndex index = indexUnderCursor(); if ( index.isValid() ) return model()->imageAt(index.row()); else return DB::FileName(); } QModelIndex ThumbnailView::ThumbnailWidget::indexUnderCursor() const { return indexAt( mapFromGlobal( QCursor::pos() ) ); } void ThumbnailView::ThumbnailWidget::dragMoveEvent( QDragMoveEvent* event ) { m_dndHandler->contentsDragMoveEvent(event); } void ThumbnailView::ThumbnailWidget::dragLeaveEvent( QDragLeaveEvent* event ) { m_dndHandler->contentsDragLeaveEvent( event ); } void ThumbnailView::ThumbnailWidget::dropEvent( QDropEvent* event ) { m_dndHandler->contentsDropEvent( event ); } void ThumbnailView::ThumbnailWidget::dragEnterEvent( QDragEnterEvent * event ) { m_dndHandler->contentsDragEnterEvent( event ); } void ThumbnailView::ThumbnailWidget::setCurrentItem( const DB::FileName& fileName ) { if ( fileName.isNull() ) return; const int row = model()->indexOf(fileName); setCurrentIndex( QListView::model()->index( row, 0 ) ); } DB::FileName ThumbnailView::ThumbnailWidget::currentItem() const { if ( !currentIndex().isValid() ) return DB::FileName(); return model()->imageAt( currentIndex().row()); } void ThumbnailView::ThumbnailWidget::updatePalette() { QPalette pal = palette(); pal.setBrush( QPalette::Base, QColor(Settings::SettingsData::instance()->backgroundColor()) ); pal.setBrush( QPalette::Text, contrastColor( QColor(Settings::SettingsData::instance()->backgroundColor() ) ) ); setPalette( pal ); } int ThumbnailView::ThumbnailWidget::cellWidth() const { return visualRect( QListView::model()->index(0,0) ).size().width(); } void ThumbnailView::ThumbnailWidget::emitSelectionChangedSignal() { emit selectionCountChanged( selection( ExpandCollapsedStacks ).size() ); } void ThumbnailView::ThumbnailWidget::scheduleDateChangeSignal() { m_dateChangedTimer->start(200); } /** * During profiling, I found that emitting the dateChanged signal was * rather expensive, so now I delay that signal, so it is only emitted 200 * msec after the scroll, which means it will not be emitted when the user * holds down, say the page down key for scrolling. */ void ThumbnailView::ThumbnailWidget::setupDateChangeTimer() { m_dateChangedTimer = new QTimer(this); m_dateChangedTimer->setSingleShot(true); connect(m_dateChangedTimer, &QTimer::timeout, this, &ThumbnailWidget::emitDateChange); } void ThumbnailView::ThumbnailWidget::showEvent( QShowEvent* event ) { model()->updateVisibleRowInfo(); QListView::showEvent( event ); } DB::FileNameList ThumbnailView::ThumbnailWidget::selection( ThumbnailView::SelectionMode mode ) const { DB::FileNameList res; Q_FOREACH(const QModelIndex& index, selectedIndexes()) { DB::FileName currFileName = model()->imageAt(index.row()); bool includeAllStacks = false; switch ( mode ) { case IncludeAllStacks: includeAllStacks = true; /* FALLTHROUGH */ case ExpandCollapsedStacks: { // if the selected image belongs to a collapsed thread, // imply that all images in the stack are selected: DB::ImageInfoPtr imageInfo = currFileName.info(); if ( imageInfo && imageInfo->isStacked() - && ( includeAllStacks || ! model()->isItemInExpandedStack( imageInfo->stackId() ) ) + && ( includeAllStacks || ! model()->isItemInExpandedStack( imageInfo->stackId() ) ) ) { // add all images in the same stack res.append(DB::ImageDB::instance()->getStackFor(currFileName)); } else res.append(currFileName); - } + } break; case NoExpandCollapsedStacks: res.append(currFileName); break; } } return res; } bool ThumbnailView::ThumbnailWidget::isSelected( const DB::FileName& fileName ) const { return selection( NoExpandCollapsedStacks ).indexOf(fileName) != -1; } /** This very specific method will make the item specified by id selected, if there only are one item selected. This is used from the Viewer when you start it without a selection, and are going forward or backward. */ void ThumbnailView::ThumbnailWidget::changeSingleSelection(const DB::FileName& fileName) { if ( selection( NoExpandCollapsedStacks ).size() == 1 ) { QItemSelectionModel* selection = selectionModel(); selection->select( model()->fileNameToIndex(fileName), QItemSelectionModel::ClearAndSelect ); setCurrentItem(fileName); } } void ThumbnailView::ThumbnailWidget::select(const DB::FileNameList& items ) { Q_FOREACH( const DB::FileName& fileName, items ) selectionModel()->select(model()->fileNameToIndex(fileName), QItemSelectionModel::Select); } bool ThumbnailView::ThumbnailWidget::isItemUnderCursorSelected() const { return widget()->selection(ExpandCollapsedStacks).contains(mediaIdUnderCursor()); } QColor ThumbnailView::contrastColor(const QColor &color) { if ( color.red() < 127 && color.green() < 127 && color.blue() < 127 ) return Qt::white; else return Qt::black; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/enums.h b/ThumbnailView/enums.h index 5cc39deb..791fff09 100644 --- a/ThumbnailView/enums.h +++ b/ThumbnailView/enums.h @@ -1,44 +1,44 @@ /* Copyright (C) 2003-2010 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 THUMBNAILVIEW_ENUMS_H #define THUMBNAILVIEW_ENUMS_H namespace ThumbnailView { enum SortDirection { NewestFirst, OldestFirst }; enum Order { ViewOrder, SortedOrder }; enum CoordinateSystem {ViewportCoordinates, ContentsCoordinates }; enum VisibleState { FullyVisible, PartlyVisible }; enum SelectionUpdateMethod { ClearSelection, MaintainSelection }; /** @short Operation mode of selection in ThumbnailView. * - * The SelectionMode determines how collapsed stacks and partially + * The SelectionMode determines how collapsed stacks and partially * selected stacks are handled when determining which images to include * in the selection. */ enum SelectionMode { NoExpandCollapsedStacks, //< @short Only include images that have been explicitly marked. ExpandCollapsedStacks, //< @short For collapsed stacks, include the whole stack instead of just the stack head. IncludeAllStacks //< @short Include the whole stack, even if the stack is expanded and only parts of it are selected. }; } #endif /* THUMBNAILVIEW_ENUMS_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/DeleteFiles.cpp b/Utilities/DeleteFiles.cpp index 871735a0..e1ad1b4b 100644 --- a/Utilities/DeleteFiles.cpp +++ b/Utilities/DeleteFiles.cpp @@ -1,100 +1,100 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #include "DeleteFiles.h" #include "ShowBusyCursor.h" #include #include #include #include #include #include #include #include #include #include namespace Utilities { DeleteFiles* DeleteFiles::s_instance; bool DeleteFiles::deleteFiles( const DB::FileNameList& files, DeleteMethod method ) { if (!s_instance) s_instance = new DeleteFiles; return s_instance->deleteFilesPrivate(files,method); } void DeleteFiles::slotKIOJobCompleted(KJob* job) { if ( job->error() ) KMessageBox::error( MainWindow::Window::theMainWindow(), job->errorString(), i18n( "Error Deleting Files" ) ); } bool DeleteFiles::deleteFilesPrivate(const DB::FileNameList &files, DeleteMethod method) { Utilities::ShowBusyCursor dummy; DB::FileNameList filenamesToRemove; QList filesToDelete; Q_FOREACH(const DB::FileName &fileName, files) { if ( DB::ImageInfo::imageOnDisk( fileName ) ) { if ( method == DeleteFromDisk || method == MoveToTrash ){ filesToDelete.append( QUrl::fromLocalFile( fileName.absolute()) ); filenamesToRemove.append(fileName); } else { filenamesToRemove.append(fileName); } } else filenamesToRemove.append(fileName); } ImageManager::ThumbnailCache::instance()->removeThumbnails( files ); if ( method == DeleteFromDisk || method == MoveToTrash ) { KJob* job; if ( method == MoveToTrash ) job = KIO::trash( filesToDelete ); else job = KIO::del( filesToDelete ); connect( job, SIGNAL(result(KJob*)), this, SLOT(slotKIOJobCompleted(KJob*)) ); } if(!filenamesToRemove.isEmpty()) { if ( method == MoveToTrash || method == DeleteFromDisk ) DB::ImageDB::instance()->deleteList( filenamesToRemove ); else DB::ImageDB::instance()->addToBlockList( filenamesToRemove ); MainWindow::DirtyIndicator::markDirty(); return true; } else return false; } } // namespace Utilities // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/DeleteFiles.h b/Utilities/DeleteFiles.h index 18d70907..a56dd23a 100644 --- a/Utilities/DeleteFiles.h +++ b/Utilities/DeleteFiles.h @@ -1,51 +1,51 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #ifndef UTILITIES_DELETEFILES_H #define UTILITIES_DELETEFILES_H #include #include "DB/FileNameList.h" class KJob; namespace Utilities { enum DeleteMethod { DeleteFromDisk, MoveToTrash, BlockFromDatabase }; class DeleteFiles :public QObject { Q_OBJECT public: static bool deleteFiles( const DB::FileNameList& files, DeleteMethod method ); private slots: void slotKIOJobCompleted( KJob* ); private: static DeleteFiles* s_instance; bool deleteFilesPrivate( const DB::FileNameList& files, DeleteMethod method ); }; } // namespace Utilities #endif // UTILITIES_DELETEFILES_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/ToolTip.cpp b/Utilities/ToolTip.cpp index 35c301cd..48c414cc 100644 --- a/Utilities/ToolTip.cpp +++ b/Utilities/ToolTip.cpp @@ -1,101 +1,101 @@ /* Copyright 2012 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. - + 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. If not, see . */ #include "ToolTip.h" #include "Settings/SettingsData.h" #include "DB/ImageDB.h" #include "ImageManager/ImageRequest.h" #include "ImageManager/AsyncLoader.h" #include #include "Utilities/DescriptionUtil.h" namespace Utilities { ToolTip::ToolTip(QWidget *parent, Qt::WindowFlags f) : QLabel(parent, f), m_tmpFileForThumbnailView(nullptr) { setAlignment( Qt::AlignLeft | Qt::AlignTop ); setLineWidth(1); setMargin(1); setWindowOpacity(0.8); setAutoFillBackground(true); QPalette p = palette(); p.setColor(QPalette::Background, QColor(0,0,0,170)); // r,g,b,A p.setColor(QPalette::WindowText, Qt::white ); setPalette(p); } void ToolTip::requestImage( const DB::FileName& fileName ) { int size = Settings::SettingsData::instance()->previewSize(); DB::ImageInfoPtr info = DB::ImageDB::instance()->info( fileName ); if ( size != 0 ) { ImageManager::ImageRequest* request = new ImageManager::ImageRequest( fileName, QSize( size, size ), info->angle(), this ); request->setPriority( ImageManager::Viewer ); ImageManager::AsyncLoader::instance()->load( request ); } else renderToolTip(); } void ToolTip::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) { const DB::FileName fileName = request->databaseFileName(); delete m_tmpFileForThumbnailView; m_tmpFileForThumbnailView = new QTemporaryFile(this); m_tmpFileForThumbnailView->open(); image.save(m_tmpFileForThumbnailView, "PNG" ); if ( fileName == m_currentFileName ) renderToolTip(); } void ToolTip::requestToolTip(const DB::FileName &fileName) { if ( fileName.isNull() || fileName == m_currentFileName) return; m_currentFileName = fileName; requestImage( fileName ); } void ToolTip::renderToolTip() { const int size = Settings::SettingsData::instance()->previewSize(); if ( size != 0 ) { setText( QString::fromLatin1("") .arg(m_tmpFileForThumbnailView->fileName()). arg(Utilities::createInfoText( DB::ImageDB::instance()->info( m_currentFileName ), nullptr ) ) ); } else setText( QString::fromLatin1("

%1

").arg( Utilities::createInfoText( DB::ImageDB::instance()->info( m_currentFileName ), nullptr ) ) ); setWordWrap( true ); resize( sizeHint() ); // m_view->setFocus(); show(); placeWindow(); } } // namespace Utilities // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/documentation.h b/XMLDB/documentation.h index 5e346caf..c3e6a49d 100644 --- a/XMLDB/documentation.h +++ b/XMLDB/documentation.h @@ -1,7 +1,7 @@ //krazy:skip /** \namespace XMLDB - \brief + \brief **/ // vi:expandtab:tabstop=4 shiftwidth=4:
%2