1 /* Copyright (C) 2003-2006 Jesper K. Pedersen <blackie@kde.org>
3 This program is free software; you can redistribute it and/or
4 modify it under the terms of the GNU General Public
5 License as published by the Free Software Foundation; either
6 version 2 of the License, or (at your option) any later version.
8 This program is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 General Public License for more details.
13 You should have received a copy of the GNU General Public License
14 along with this program; see the file COPYING. If not, write to
15 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 Boston, MA 02110-1301, USA.
18 #include "NewImageFinder.h"
20 #include <sys/types.h>
24 #include "DB/ImageDB.h"
25 #include "DB/ResultId.h"
26 #include "DB/Result.h"
28 #include <qfileinfo.h>
29 #include <QStringList>
30 #include <QProgressDialog>
32 #include <qapplication.h>
33 #include <qeventloop.h>
34 #include <kmessagebox.h>
35 #include "DB/MD5Map.h"
37 #include "config-kpa-exiv2.h"
39 # include "Exif/Database.h"
42 #include "ImageManager/Manager.h"
43 #include "ImageManager/RawImageDecoder.h"
44 #include "Settings/SettingsData.h"
45 #include "Utilities/Util.h"
49 bool NewImageFinder::findImages()
51 // Load the information from the XML file.
52 QSet
<QString
> loadedFiles
;
54 // TODO: maybe the databas interface should allow to query if it
55 // knows about an image ? Here we've to iterate through all of them and it
56 // might be more efficient do do this in the database without fetching the
59 const DB::ImageInfoPtr
& info
,
60 DB::ImageDB::instance()->images().fetchInfos()) {
61 loadedFiles
.insert(info
->fileName(DB::AbsolutePath
));
65 searchForNewFiles( loadedFiles
, Settings::SettingsData::instance()->imageDirectory() );
68 // To avoid deciding if the new images are shown in a given thumbnail view or in a given search
69 // we rather just go to home.
70 return (!_pendingLoad
.isEmpty()); // returns if new images was found.
73 // FastDir is used in place of QDir because QDir stat()s every file in
74 // the directory, even if we tell it not to restrict anything. When
75 // scanning for new images, we don't want to look at files we already
76 // have in our database, and we also don't want to look at files whose
77 // names indicate that we don't care about them. So what we do is
78 // simply read the names from the directory and let the higher layers
79 // decide what to do with them.
81 // On my sample database with ~20,000 images, this improves the time
82 // to rescan for images on a cold system from about 100 seconds to
85 // -- Robert Krawitz, rlk@alum.mit.edu 2007-07-22
89 FastDir(const QString
&path
);
90 QStringList
entryList() const;
95 FastDir::FastDir(const QString
&path
)
100 QStringList
FastDir::entryList() const
105 dir
= opendir( QFile::encodeName(_path
) );
107 return answer
; // cannot read the directory
109 #if defined(QT_THREAD_SUPPORT) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_CYGWIN)
111 struct dirent mt_file
;
112 char b
[sizeof(struct dirent
) + MAXNAMLEN
+ 1];
113 } *u
= new union dirent_buf
;
114 while ( readdir_r(dir
, &(u
->mt_file
), &file
) == 0 && file
)
116 while ( (file
= readdir(dir
)) )
117 #endif // QT_THREAD_SUPPORT && _POSIX_THREAD_SAFE_FUNCTIONS
118 answer
.append(QFile::decodeName(file
->d_name
));
119 #if defined(QT_THREAD_SUPPORT) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_CYGWIN)
122 (void) closedir(dir
);
126 void NewImageFinder::searchForNewFiles( const QSet
<QString
>& loadedFiles
, QString directory
)
128 qApp
->processEvents( QEventLoop::AllEvents
);
129 if ( directory
.endsWith( QString::fromLatin1("/") ) )
130 directory
= directory
.mid( 0, directory
.length()-1 );
132 QString imageDir
= Settings::SettingsData::instance()->imageDirectory();
133 if ( imageDir
.endsWith( QString::fromLatin1("/") ) )
134 imageDir
= imageDir
.mid( 0, imageDir
.length()-1 );
136 FastDir
dir( directory
);
137 QStringList dirList
= dir
.entryList( );
138 ImageManager::RAWImageDecoder dec
; // TODO: DEPENDENCY: DB:: should not reference other directories
139 QStringList excluded
;
140 excluded
<< Settings::SettingsData::instance()->excludeDirectories();
141 excluded
= excluded
.at(0).split(QString::fromLatin1(","));
142 for( QStringList::const_iterator it
= dirList
.constBegin(); it
!= dirList
.constEnd(); ++it
) {
143 QString file
= directory
+ QString::fromLatin1("/") + *it
;
144 if ( (*it
) == QString::fromLatin1(".") || (*it
) == QString::fromLatin1("..") ||
145 excluded
.contains( (*it
) ) || loadedFiles
.contains( file
) ||
146 dec
._skipThisFile(loadedFiles
, file
) ||
147 (*it
) == QString::fromLatin1("CategoryImages") )
150 QFileInfo
fi( file
);
152 if ( !fi
.isReadable() )
156 QString baseName
= file
.mid( imageDir
.length()+1 );
157 if ( ! DB::ImageDB::instance()->isBlocking( baseName
) ) {
158 if ( Utilities::canReadImage(file
) )
159 _pendingLoad
.append( qMakePair( baseName
, DB::Image
) );
160 else if ( Utilities::isVideo( file
) )
161 _pendingLoad
.append( qMakePair( baseName
, DB::Video
) );
163 } else if ( fi
.isDir() ) {
164 searchForNewFiles( loadedFiles
, file
);
169 void NewImageFinder::loadExtraFiles()
171 // FIXME: should be converted to a threadpool for SMP stuff and whatnot :]
172 QProgressDialog dialog
;
173 dialog
.setLabelText( i18n("<p><b>Loading information from new files</b></p>"
174 "<p>Depending on the number of images, this may take some time.<br/>"
175 "However, there is only a delay when new images are found.</p>") );
176 dialog
.setMaximum( _pendingLoad
.count() );
177 dialog
.setMinimumDuration( 1000 );
180 ImageInfoList newImages
;
181 for( LoadList::Iterator it
= _pendingLoad
.begin(); it
!= _pendingLoad
.end(); ++it
, ++count
) {
182 dialog
.setValue( count
); // ensure to call setProgress(0)
183 qApp
->processEvents( QEventLoop::AllEvents
);
185 if ( dialog
.wasCanceled() )
187 ImageInfoPtr info
= loadExtraFile( (*it
).first
, (*it
).second
);
190 newImages
.append(info
);
193 DB::ImageDB::instance()->addImages( newImages
);
197 ImageInfoPtr
NewImageFinder::loadExtraFile( const QString
& relativeNewFileName
, DB::MediaType type
)
199 QString absoluteNewFileName
= Utilities::absoluteImageFileName( relativeNewFileName
);
200 MD5 sum
= Utilities::MD5Sum( absoluteNewFileName
);
201 if ( DB::ImageDB::instance()->md5Map()->contains( sum
) ) {
202 QString relativeMatchedFileName
= DB::ImageDB::instance()->md5Map()->lookup(sum
);
203 QString absoluteMatchedFileName
= Utilities::absoluteImageFileName( relativeMatchedFileName
);
204 QFileInfo
fi( absoluteMatchedFileName
);
206 if ( !fi
.exists() ) {
207 // The file we had a collapse with didn't exists anymore so it is likely moved to this new name
208 ImageInfoPtr info
= DB::ImageDB::instance()->info( relativeMatchedFileName
, DB::RelativeToImageRoot
);
210 qWarning("How did that happen? We couldn't find info for the images %s", qPrintable(relativeMatchedFileName
));
212 info
->delaySavingChanges(true);
213 fi
= QFileInfo ( relativeMatchedFileName
);
214 if ( info
->label() == fi
.completeBaseName() ) {
215 fi
= QFileInfo( relativeNewFileName
);
216 info
->setLabel( fi
.completeBaseName() );
219 DB::ImageDB::instance()->renameImage( info
, relativeNewFileName
);
221 // We need to insert the new name into the MD5 map,
222 // as it is a map, the value for the moved file will automatically be deleted.
223 DB::ImageDB::instance()->md5Map()->insert( sum
, info
->fileName(DB::RelativeToImageRoot
) );
226 Exif::Database::instance()->remove( absoluteMatchedFileName
);
227 Exif::Database::instance()->add( absoluteNewFileName
);
229 return DB::ImageInfoPtr();
234 QString err
= Settings::SettingsData::instance()->modifiedFileComponent();
235 QRegExp modifiedFileComponent
=
236 QRegExp(Settings::SettingsData::instance()->modifiedFileComponent());
238 // check to see if this is a new version of a previous image
239 ImageInfoPtr info
= ImageInfoPtr(new ImageInfo( relativeNewFileName
, type
));
240 ImageInfoPtr originalInfo
;
241 QString originalFileName
;
243 if (Settings::SettingsData::instance()->detectModifiedFiles()) {
244 // should be cached because loading once per image is expensive
245 QString err
= Settings::SettingsData::instance()->modifiedFileComponent();
246 QRegExp modifiedFileComponent
=
247 QRegExp(Settings::SettingsData::instance()->modifiedFileComponent());
248 // requires at least *something* in the modifiedFileComponent
249 if (err
.length() >= 0 &&
250 relativeNewFileName
.contains(modifiedFileComponent
)) {
252 originalFileName
= relativeNewFileName
;
253 QString originalFileComponent
=
254 Settings::SettingsData::instance()->originalFileComponent();
255 originalFileName
.replace(modifiedFileComponent
, originalFileComponent
);
257 MD5 originalSum
= Utilities::MD5Sum( Utilities::absoluteImageFileName( originalFileName
) );
258 if ( DB::ImageDB::instance()->md5Map()->contains( originalSum
) ) {
259 // we have a previous copy of this file; copy it's data
260 // from the original.
261 originalInfo
= DB::ImageDB::instance()->info( originalFileName
, DB::RelativeToImageRoot
);
262 if ( !originalInfo
) {
263 qWarning("How did that happen? We couldn't find info for the original image %s; can't copy the original data to %s", qPrintable(originalFileName
), qPrintable(relativeNewFileName
));
265 info
->copyExtraData(*originalInfo
);
268 /* if requested to move, then delete old data from original */
269 if (Settings::SettingsData::instance()->moveOriginalContents() ) {
270 originalInfo
->removeExtraData();
276 // also inserts image into exif db if present:
277 info
->setMD5Sum(sum
);
278 DB::ImageDB::instance()->md5Map()->insert( sum
, info
->fileName(DB::RelativeToImageRoot
) );
281 Settings::SettingsData::instance()->autoStackNewFiles() ) {
282 // we have to do this immediately to get the ids
283 ImageInfoList newImages
;
285 newImages
.append(info
);
286 DB::ImageDB::instance()->addImages( newImages
);
288 // stack the files together
289 DB::ResultId olderfile
= DB::ImageDB::instance()->ID_FOR_FILE(originalFileName
);
290 DB::ResultId newerfile
= DB::ImageDB::instance()->ID_FOR_FILE(info
->fileName(DB::AbsolutePath
));
291 DB::Result tostack
= DB::Result();
293 tostack
.append(newerfile
);
294 tostack
.append(olderfile
);
295 DB::ImageDB::instance()->stack(tostack
);
297 // ordering: XXX we ideally want to place the new image right
298 // after the older one in the list.
300 // XXX: deal with already-stacked items; currently a silent fail
302 info
= NULL
; // we already added it, so don't process again
308 bool NewImageFinder::calculateMD5sums(
309 const DB::Result
& list
,
313 // FIXME: should be converted to a threadpool for SMP stuff and whatnot :]
314 QProgressDialog dialog
;
316 i18n("<p><b>Calculating checksum for %1 files<b></p>"
317 "<p>By storing a checksum for each image "
318 "KPhotoAlbum is capable of finding images "
319 "even when you have moved them on the disk.</p>", list
.size()));
320 dialog
.setMaximum(list
.size());
321 dialog
.setMinimumDuration( 1000 );
324 QStringList cantRead
;
327 Q_FOREACH(DB::ImageInfoPtr info
, list
.fetchInfos()) {
328 const QString absoluteFileName
= info
->fileName(DB::AbsolutePath
);
329 if ( count
% 10 == 0 ) {
330 dialog
.setValue( count
); // ensure to call setProgress(0)
331 qApp
->processEvents( QEventLoop::AllEvents
);
333 if ( dialog
.wasCanceled() ) {
340 MD5 md5
= Utilities::MD5Sum( absoluteFileName
);
342 cantRead
<< absoluteFileName
;
346 if ( info
->MD5Sum() != md5
) {
347 info
->setMD5Sum( md5
);
349 ImageManager::Manager::instance()->removeThumbnail( absoluteFileName
);
352 md5Map
->insert( md5
, info
->fileName(DB::RelativeToImageRoot
) );
357 *wasCanceled
= false;
359 if ( !cantRead
.empty() )
360 KMessageBox::informationList( 0, i18n("Following files could not be read:"), cantRead
);
365 void DB::NewImageFinder::markUnTagged( ImageInfoPtr info
)
367 if ( Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() ) {
368 info
->addCategoryInfo( Settings::SettingsData::instance()->untaggedCategory(),
369 Settings::SettingsData::instance()->untaggedTag() );