fix tricky regression noticed by Vyacheslav Tokarev on Google Reader.
[kdelibs.git] / kio / kio / previewjob.cpp
blobc5c428e68cad7924ec6eb3913f594e84a61a09af
1 // -*- c++ -*-
2 // vim: ts=4 sw=4 et
3 /* This file is part of the KDE libraries
4 Copyright (C) 2000 David Faure <faure@kde.org>
5 2000 Carsten Pfeiffer <pfeiffer@kde.org>
6 2001 Malte Starostik <malte.starostik@t-online.de>
8 This library is free software; you can redistribute it and/or
9 modify it under the terms of the GNU Library General Public
10 License as published by the Free Software Foundation; either
11 version 2 of the License, or (at your option) any later version.
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Library General Public License for more details.
18 You should have received a copy of the GNU Library General Public License
19 along with this library; see the file COPYING.LIB. If not, write to
20 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 Boston, MA 02110-1301, USA.
24 #include "previewjob.h"
26 #include <sys/stat.h>
27 #ifdef __FreeBSD__
28 #include <machine/param.h>
29 #endif
30 #include <sys/types.h>
32 #ifdef Q_OS_UNIX
33 #include <sys/ipc.h>
34 #include <sys/shm.h>
35 #endif
37 #include <QtCore/QDir>
38 #include <QtCore/QFile>
39 #include <QtGui/QImage>
40 #include <QtCore/QTimer>
41 #include <QtCore/QRegExp>
43 #include <kfileitem.h>
44 #include <kapplication.h>
45 #include <kde_file.h>
46 #include <ktemporaryfile.h>
47 #include <kservicetypetrader.h>
48 #include <kcodecs.h>
49 #include <kglobal.h>
50 #include <kstandarddirs.h>
51 #include <kservice.h>
52 #include <QtCore/QLinkedList>
53 #include <kconfiggroup.h>
55 #include "jobuidelegate.h"
56 #include "job_p.h"
58 namespace KIO { struct PreviewItem; }
59 using namespace KIO;
61 struct KIO::PreviewItem
63 KFileItem item;
64 KService::Ptr plugin;
67 class KIO::PreviewJobPrivate: public KIO::JobPrivate
69 public:
70 enum { STATE_STATORIG, // if the thumbnail exists
71 STATE_GETORIG, // if we create it
72 STATE_CREATETHUMB // thumbnail:/ slave
73 } state;
74 PreviewJob *q;
76 KFileItemList initialItems;
77 QStringList enabledPlugins;
78 // Our todo list :)
79 // We remove the first item at every step, so use QLinkedList
80 QLinkedList<PreviewItem> items;
81 // The current item
82 PreviewItem currentItem;
83 // The modification time of that URL
84 time_t tOrig;
85 // Path to thumbnail cache for the current size
86 QString thumbPath;
87 // Original URL of current item in TMS format
88 // (file:///path/to/file instead of file:/path/to/file)
89 QString origName;
90 // Thumbnail file name for current item
91 QString thumbName;
92 // Size of thumbnail
93 int width;
94 int height;
95 // Unscaled size of thumbnail (128 or 256 if cache is enabled)
96 int cacheWidth;
97 int cacheHeight;
98 // Whether the thumbnail should be scaled
99 bool bScale;
100 // Whether we should save the thumbnail
101 bool bSave;
102 bool ignoreMaximumSize;
103 bool succeeded;
104 // If the file to create a thumb for was a temp file, this is its name
105 QString tempName;
106 // Over that, it's too much
107 KIO::filesize_t maximumSize;
108 // the size for the icon overlay
109 int iconSize;
110 // the transparency of the blended mimetype icon
111 int iconAlpha;
112 // Shared memory segment Id. The segment is allocated to a size
113 // of extent x extent x 4 (32 bit image) on first need.
114 int shmid;
115 // And the data area
116 uchar *shmaddr;
117 // Root of thumbnail cache
118 QString thumbRoot;
120 void getOrCreateThumbnail();
121 bool statResultThumbnail();
122 void createThumbnail( const QString& );
123 void determineNextFile();
124 void emitPreview(const QImage &thumb);
126 void startPreview();
127 void slotThumbData(KIO::Job *, const QByteArray &);
129 Q_DECLARE_PUBLIC(PreviewJob)
132 PreviewJob::PreviewJob( const KFileItemList &items, int width, int height,
133 int iconSize, int iconAlpha, bool scale, bool save,
134 const QStringList *enabledPlugins )
135 : KIO::Job(*new PreviewJobPrivate)
137 Q_D(PreviewJob);
138 d->tOrig = 0;
139 d->shmid = -1;
140 d->shmaddr = 0;
141 d->initialItems = items;
142 d->enabledPlugins = enabledPlugins ? *enabledPlugins : QStringList();
143 d->width = width;
144 d->height = height ? height : width;
145 d->cacheWidth = d->width;
146 d->cacheHeight = d->height;
147 d->iconSize = iconSize;
148 d->iconAlpha = iconAlpha;
149 d->bScale = scale;
150 d->bSave = save && scale;
151 d->succeeded = false;
152 d->thumbRoot = QDir::homePath() + "/.thumbnails/";
153 d->ignoreMaximumSize = false;
155 // Return to event loop first, determineNextFile() might delete this;
156 QTimer::singleShot(0, this, SLOT(startPreview()));
159 PreviewJob::~PreviewJob()
161 #ifdef Q_OS_UNIX
162 Q_D(PreviewJob);
163 if (d->shmaddr) {
164 shmdt((char*)d->shmaddr);
165 shmctl(d->shmid, IPC_RMID, 0);
167 #endif
170 void PreviewJobPrivate::startPreview()
172 Q_Q(PreviewJob);
173 // Load the list of plugins to determine which mimetypes are supported
174 const KService::List plugins = KServiceTypeTrader::self()->query("ThumbCreator");
175 QMap<QString, KService::Ptr> mimeMap;
177 for (KService::List::ConstIterator it = plugins.constBegin(); it != plugins.constEnd(); ++it) {
178 if (enabledPlugins.isEmpty() || enabledPlugins.contains((*it)->desktopEntryName()))
180 const QStringList mimeTypes = (*it)->serviceTypes();
181 for (QStringList::ConstIterator mt = mimeTypes.constBegin(); mt != mimeTypes.constEnd(); ++mt)
182 mimeMap.insert(*mt, *it);
186 // Look for images and store the items in our todo list :)
187 bool bNeedCache = false;
188 KFileItemList::const_iterator kit = initialItems.constBegin();
189 const KFileItemList::const_iterator kend = initialItems.constEnd();
190 for ( ; kit != kend; ++kit )
192 PreviewItem item;
193 item.item = *kit;
194 const QString mimeType = item.item.mimetype();
195 QMap<QString, KService::Ptr>::ConstIterator plugin = mimeMap.constFind(mimeType);
196 if (plugin == mimeMap.constEnd())
199 QString groupMimeType = mimeType;
200 groupMimeType.replace(QRegExp("/.*"), "/*");
201 plugin = mimeMap.constFind(groupMimeType);
203 if (plugin == mimeMap.constEnd())
205 // check mime type inheritance, resolve aliases
206 const KMimeType::Ptr mimeInfo = KMimeType::mimeType(mimeType, KMimeType::ResolveAliases);
207 if (mimeInfo) {
208 const QStringList parentMimeTypes = mimeInfo->allParentMimeTypes();
209 Q_FOREACH(const QString& parentMimeType, parentMimeTypes) {
210 plugin = mimeMap.constFind(parentMimeType);
211 if (plugin != mimeMap.constEnd()) break;
215 #if 0 // KDE4: should be covered by inheritance above, all text mimetypes inherit from text/plain
216 // if that's not enough, we need to invent something else
217 if (plugin == mimeMap.end())
219 // check X-KDE-Text property
220 KMimeType::Ptr mimeInfo = KMimeType::mimeType(mimeType);
221 QVariant textProperty = mimeInfo->property("X-KDE-text");
222 if (textProperty.isValid() && textProperty.type() == QVariant::Bool)
224 if (textProperty.toBool())
226 plugin = mimeMap.find("text/plain");
227 if (plugin == mimeMap.end())
229 plugin = mimeMap.find( "text/*" );
234 #endif
237 if (plugin != mimeMap.constEnd())
239 item.plugin = *plugin;
240 items.append(item);
241 if (!bNeedCache && bSave &&
242 ((*kit).url().protocol() != "file" ||
243 !(*kit).url().directory( KUrl::AppendTrailingSlash ).startsWith(thumbRoot)) &&
244 (*plugin)->property("CacheThumbnail").toBool())
245 bNeedCache = true;
247 else
249 emit q->failed( *kit );
253 // Read configuration value for the maximum allowed size
254 maximumSize = PreviewJob::maximumFileSize();
256 if (bNeedCache)
258 if (width <= 128 && height <= 128) cacheWidth = cacheHeight = 128;
259 else cacheWidth = cacheHeight = 256;
260 thumbPath = thumbRoot + (cacheWidth == 128 ? "normal/" : "large/");
261 KStandardDirs::makeDir(thumbPath, 0700);
263 else
264 bSave = false;
266 initialItems.clear();
267 determineNextFile();
270 void PreviewJob::removeItem( const KUrl& url )
272 Q_D(PreviewJob);
273 for (QLinkedList<PreviewItem>::Iterator it = d->items.begin(); it != d->items.end(); ++it)
274 if ((*it).item.url() == url)
276 d->items.erase(it);
277 break;
280 if (d->currentItem.item.url() == url)
282 KJob* job = subjobs().first();
283 job->kill();
284 removeSubjob( job );
285 d->determineNextFile();
289 void PreviewJob::setIgnoreMaximumSize(bool ignoreSize)
291 d_func()->ignoreMaximumSize = ignoreSize;
294 void PreviewJobPrivate::determineNextFile()
296 Q_Q(PreviewJob);
297 if (!currentItem.item.isNull())
299 if (!succeeded)
300 emit q->failed( currentItem.item );
302 // No more items ?
303 if ( items.isEmpty() )
305 q->emitResult();
306 return;
308 else
310 // First, stat the orig file
311 state = PreviewJobPrivate::STATE_STATORIG;
312 currentItem = items.first();
313 succeeded = false;
314 items.removeFirst();
315 KIO::Job *job = KIO::stat( currentItem.item.url(), KIO::HideProgressInfo );
316 job->addMetaData( "no-auth-prompt", "true" );
317 q->addSubjob(job);
321 void PreviewJob::slotResult( KJob *job )
323 Q_D(PreviewJob);
325 removeSubjob(job);
326 Q_ASSERT ( !hasSubjobs() ); // We should have only one job at a time ...
327 switch ( d->state )
329 case PreviewJobPrivate::STATE_STATORIG:
331 if (job->error()) // that's no good news...
333 // Drop this one and move on to the next one
334 d->determineNextFile();
335 return;
337 const KIO::UDSEntry entry = static_cast<KIO::StatJob*>(job)->statResult();
338 d->tOrig = entry.numberValue( KIO::UDSEntry::UDS_MODIFICATION_TIME, 0 );
339 if ( !d->ignoreMaximumSize &&
340 (KIO::filesize_t)entry.numberValue( KIO::UDSEntry::UDS_SIZE, 0 ) > d->maximumSize &&
341 !d->currentItem.plugin->property("IgnoreMaximumSize").toBool()
343 d->determineNextFile();
344 return;
347 if ( !d->currentItem.plugin->property( "CacheThumbnail" ).toBool() )
349 // This preview will not be cached, no need to look for a saved thumbnail
350 // Just create it, and be done
351 d->getOrCreateThumbnail();
352 return;
355 if ( d->statResultThumbnail() )
356 return;
358 d->getOrCreateThumbnail();
359 return;
361 case PreviewJobPrivate::STATE_GETORIG:
363 if (job->error())
365 d->determineNextFile();
366 return;
369 d->createThumbnail( static_cast<KIO::FileCopyJob*>(job)->destUrl().toLocalFile() );
370 return;
372 case PreviewJobPrivate::STATE_CREATETHUMB:
374 if (!d->tempName.isEmpty())
376 QFile::remove(d->tempName);
377 d->tempName.clear();
379 d->determineNextFile();
380 return;
385 bool PreviewJobPrivate::statResultThumbnail()
387 if ( thumbPath.isEmpty() )
388 return false;
390 KUrl url = currentItem.item.url();
391 // Don't include the password if any
392 url.setPass(QString());
393 origName = url.url();
395 KMD5 md5( QFile::encodeName( origName ) );
396 thumbName = QFile::encodeName( md5.hexDigest() ) + ".png";
398 QImage thumb;
399 if ( !thumb.load( thumbPath + thumbName ) ) return false;
401 if ( thumb.text( "Thumb::URI", 0 ) != origName ||
402 thumb.text( "Thumb::MTime", 0 ).toInt() != tOrig ) return false;
404 // Found it, use it
405 emitPreview( thumb );
406 succeeded = true;
407 determineNextFile();
408 return true;
412 void PreviewJobPrivate::getOrCreateThumbnail()
414 Q_Q(PreviewJob);
415 // We still need to load the orig file ! (This is getting tedious) :)
416 const KFileItem& item = currentItem.item;
417 const QString localPath = item.localPath();
418 if ( !localPath.isEmpty() )
419 createThumbnail( localPath );
420 else
422 state = PreviewJobPrivate::STATE_GETORIG;
423 KTemporaryFile localFile;
424 localFile.setAutoRemove(false);
425 localFile.open();
426 KUrl localURL;
427 localURL.setPath( tempName = localFile.fileName() );
428 const KUrl currentURL = item.url();
429 KIO::Job * job = KIO::file_copy( currentURL, localURL, -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */ );
430 job->addMetaData("thumbnail","1");
431 q->addSubjob(job);
435 void PreviewJobPrivate::createThumbnail( const QString &pixPath )
437 Q_Q(PreviewJob);
438 state = PreviewJobPrivate::STATE_CREATETHUMB;
439 KUrl thumbURL;
440 thumbURL.setProtocol("thumbnail");
441 thumbURL.setPath(pixPath);
442 KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo);
443 q->addSubjob(job);
444 q->connect(job, SIGNAL(data(KIO::Job *, const QByteArray &)), SLOT(slotThumbData(KIO::Job *, const QByteArray &)));
445 bool save = bSave && currentItem.plugin->property("CacheThumbnail").toBool();
446 job->addMetaData("mimeType", currentItem.item.mimetype());
447 job->addMetaData("width", QString().setNum(save ? cacheWidth : width));
448 job->addMetaData("height", QString().setNum(save ? cacheHeight : height));
449 job->addMetaData("iconSize", QString().setNum(save ? 64 : iconSize));
450 job->addMetaData("iconAlpha", QString().setNum(iconAlpha));
451 job->addMetaData("plugin", currentItem.plugin->library());
452 #ifdef Q_OS_UNIX
453 if (shmid == -1)
455 if (shmaddr) {
456 shmdt((char*)shmaddr);
457 shmctl(shmid, IPC_RMID, 0);
459 shmid = shmget(IPC_PRIVATE, cacheWidth * cacheHeight * 4, IPC_CREAT|0600);
460 if (shmid != -1)
462 shmaddr = (uchar *)(shmat(shmid, 0, SHM_RDONLY));
463 if (shmaddr == (uchar *)-1)
465 shmctl(shmid, IPC_RMID, 0);
466 shmaddr = 0;
467 shmid = -1;
470 else
471 shmaddr = 0;
473 if (shmid != -1)
474 job->addMetaData("shmid", QString().setNum(shmid));
475 #endif
478 void PreviewJobPrivate::slotThumbData(KIO::Job *, const QByteArray &data)
480 bool save = bSave &&
481 currentItem.plugin->property("CacheThumbnail").toBool() &&
482 (currentItem.item.url().protocol() != "file" ||
483 !currentItem.item.url().directory( KUrl::AppendTrailingSlash ).startsWith(thumbRoot));
484 QImage thumb;
485 #ifdef Q_OS_UNIX
486 if (shmaddr)
488 // Keep this in sync with kdebase/kioslave/thumbnail.cpp
489 QDataStream str(data);
490 int width, height;
491 quint8 iFormat;
492 str >> width >> height >> iFormat;
493 QImage::Format format = static_cast<QImage::Format>( iFormat );
494 thumb = QImage(shmaddr, width, height, format );
496 else
497 #endif
498 thumb.loadFromData(data);
500 if (thumb.isNull()) {
501 QDataStream s(data);
502 s >> thumb;
505 if (save)
507 thumb.setText("Thumb::URI", origName);
508 thumb.setText("Thumb::MTime", QString::number(tOrig));
509 thumb.setText("Thumb::Size", number(currentItem.item.size()));
510 thumb.setText("Thumb::Mimetype", currentItem.item.mimetype());
511 thumb.setText("Software", "KDE Thumbnail Generator");
512 KTemporaryFile temp;
513 temp.setPrefix(thumbPath + "kde-tmp-");
514 temp.setSuffix(".png");
515 temp.setAutoRemove(false);
516 if (temp.open()) //Only try to write out the thumbnail if we
517 { //actually created the temp file.
518 thumb.save(temp.fileName(), "PNG");
519 KDE::rename(temp.fileName(), thumbPath + thumbName);
522 emitPreview( thumb );
523 succeeded = true;
526 void PreviewJobPrivate::emitPreview(const QImage &thumb)
528 Q_Q(PreviewJob);
529 QPixmap pix;
530 if (thumb.width() > width || thumb.height() > height)
531 pix = QPixmap::fromImage( thumb.scaled(QSize(width, height), Qt::KeepAspectRatio, Qt::SmoothTransformation) );
532 else
533 pix = QPixmap::fromImage( thumb );
534 emit q->gotPreview(currentItem.item, pix);
537 QStringList PreviewJob::availablePlugins()
539 QStringList result;
540 const KService::List plugins = KServiceTypeTrader::self()->query("ThumbCreator");
541 for (KService::List::ConstIterator it = plugins.begin(); it != plugins.end(); ++it)
542 if (!result.contains((*it)->desktopEntryName()))
543 result.append((*it)->desktopEntryName());
544 return result;
547 QStringList PreviewJob::supportedMimeTypes()
549 QStringList result;
550 const KService::List plugins = KServiceTypeTrader::self()->query("ThumbCreator");
551 for (KService::List::ConstIterator it = plugins.begin(); it != plugins.end(); ++it)
552 result += (*it)->serviceTypes();
553 return result;
556 PreviewJob *KIO::filePreview( const KFileItemList &items, int width, int height,
557 int iconSize, int iconAlpha, bool scale, bool save,
558 const QStringList *enabledPlugins )
560 return new PreviewJob(items, width, height, iconSize, iconAlpha,
561 scale, save, enabledPlugins);
564 PreviewJob *KIO::filePreview( const KUrl::List &items, int width, int height,
565 int iconSize, int iconAlpha, bool scale, bool save,
566 const QStringList *enabledPlugins )
568 KFileItemList fileItems;
569 for (KUrl::List::ConstIterator it = items.begin(); it != items.end(); ++it) {
570 Q_ASSERT( (*it).isValid() ); // please call us with valid urls only
571 fileItems.append(KFileItem(KFileItem::Unknown, KFileItem::Unknown, *it, true));
573 return new PreviewJob(fileItems, width, height, iconSize, iconAlpha,
574 scale, save, enabledPlugins);
577 KIO::filesize_t PreviewJob::maximumFileSize()
579 KConfigGroup cg( KGlobal::config(), "PreviewSettings" );
580 return cg.readEntry( "MaximumSize", 5*1024*1024LL /* 5MB */ );
583 #include "previewjob.moc"