previewjob.cpp
00001 // -*- c++ -*- 00002 // vim: ts=4 sw=4 et 00003 /* This file is part of the KDE libraries 00004 Copyright (C) 2000 David Faure <faure@kde.org> 00005 2000 Carsten Pfeiffer <pfeiffer@kde.org> 00006 2001 Malte Starostik <malte.starostik@t-online.de> 00007 00008 This library is free software; you can redistribute it and/or 00009 modify it under the terms of the GNU Library General Public 00010 License as published by the Free Software Foundation; either 00011 version 2 of the License, or (at your option) any later version. 00012 00013 This library is distributed in the hope that it will be useful, 00014 but WITHOUT ANY WARRANTY; without even the implied warranty of 00015 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00016 Library General Public License for more details. 00017 00018 You should have received a copy of the GNU Library General Public License 00019 along with this library; see the file COPYING.LIB. If not, write to 00020 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 00021 Boston, MA 02110-1301, USA. 00022 */ 00023 00024 #include "previewjob.h" 00025 00026 #include <sys/stat.h> 00027 #ifdef __FreeBSD__ 00028 #include <machine/param.h> 00029 #endif 00030 #include <sys/types.h> 00031 00032 #ifdef Q_OS_UNIX 00033 #include <sys/ipc.h> 00034 #include <sys/shm.h> 00035 #endif 00036 00037 #include <tqdir.h> 00038 #include <tqfile.h> 00039 #include <tqimage.h> 00040 #include <tqtimer.h> 00041 #include <tqregexp.h> 00042 00043 #include <kdatastream.h> // Do not remove, needed for correct bool serialization 00044 #include <kfileitem.h> 00045 #include <kapplication.h> 00046 #include <ktempfile.h> 00047 #include <ktrader.h> 00048 #include <kmdcodec.h> 00049 #include <kglobal.h> 00050 #include <kstandarddirs.h> 00051 00052 #include <kio/kservice.h> 00053 00054 #include "previewjob.moc" 00055 00056 namespace KIO { struct PreviewItem; } 00057 using namespace KIO; 00058 00059 struct KIO::PreviewItem 00060 { 00061 KFileItem *item; 00062 KService::Ptr plugin; 00063 }; 00064 00065 struct KIO::PreviewJobPrivate 00066 { 00067 enum { STATE_STATORIG, // if the thumbnail exists 00068 STATE_GETORIG, // if we create it 00069 STATE_CREATETHUMB // thumbnail:/ slave 00070 } state; 00071 KFileItemList initialItems; 00072 const TQStringList *enabledPlugins; 00073 // Our todo list :) 00074 TQValueList<PreviewItem> items; 00075 // The current item 00076 PreviewItem currentItem; 00077 // The modification time of that URL 00078 time_t tOrig; 00079 // Path to thumbnail cache for the current size 00080 TQString thumbPath; 00081 // Original URL of current item in TMS format 00082 // (file:///path/to/file instead of file:/path/to/file) 00083 TQString origName; 00084 // Thumbnail file name for current item 00085 TQString thumbName; 00086 // Size of thumbnail 00087 int width; 00088 int height; 00089 // Unscaled size of thumbnail (128 or 256 if cache is enabled) 00090 int cacheWidth; 00091 int cacheHeight; 00092 // Whether the thumbnail should be scaled 00093 bool bScale; 00094 // Whether we should save the thumbnail 00095 bool bSave; 00096 // If the file to create a thumb for was a temp file, this is its name 00097 TQString tempName; 00098 // Over that, it's too much 00099 unsigned long maximumSize; 00100 // the size for the icon overlay 00101 int iconSize; 00102 // the transparency of the blended mimetype icon 00103 int iconAlpha; 00104 // Shared memory segment Id. The segment is allocated to a size 00105 // of extent x extent x 4 (32 bit image) on first need. 00106 int shmid; 00107 // And the data area 00108 uchar *shmaddr; 00109 // Delete the KFileItems when done? 00110 bool deleteItems; 00111 bool succeeded; 00112 // Root of thumbnail cache 00113 TQString thumbRoot; 00114 bool ignoreMaximumSize; 00115 TQTimer startPreviewTimer; 00116 }; 00117 00118 PreviewJob::PreviewJob( const KFileItemList &items, int width, int height, 00119 int iconSize, int iconAlpha, bool scale, bool save, 00120 const TQStringList *enabledPlugins, bool deleteItems ) 00121 : KIO::Job( false /* no GUI */ ) 00122 { 00123 d = new PreviewJobPrivate; 00124 d->tOrig = 0; 00125 d->shmid = -1; 00126 d->shmaddr = 0; 00127 d->initialItems = items; 00128 d->enabledPlugins = enabledPlugins; 00129 d->width = width; 00130 d->height = height ? height : width; 00131 d->cacheWidth = d->width; 00132 d->cacheHeight = d->height; 00133 d->iconSize = iconSize; 00134 d->iconAlpha = iconAlpha; 00135 d->deleteItems = deleteItems; 00136 d->bScale = scale; 00137 d->bSave = save && scale; 00138 d->succeeded = false; 00139 d->currentItem.item = 0; 00140 d->thumbRoot = TQDir::homeDirPath() + "/.thumbnails/"; 00141 d->ignoreMaximumSize = false; 00142 00143 // Return to event loop first, determineNextFile() might delete this; 00144 connect(&d->startPreviewTimer, TQT_SIGNAL(timeout()), TQT_SLOT(startPreview()) ); 00145 d->startPreviewTimer.start(0, true); 00146 } 00147 00148 PreviewJob::~PreviewJob() 00149 { 00150 #ifdef Q_OS_UNIX 00151 if (d->shmaddr) { 00152 shmdt((char*)d->shmaddr); 00153 shmctl(d->shmid, IPC_RMID, 0); 00154 } 00155 #endif 00156 delete d; 00157 } 00158 00159 void PreviewJob::startPreview() 00160 { 00161 // Load the list of plugins to determine which mimetypes are supported 00162 KTrader::OfferList plugins = KTrader::self()->query("ThumbCreator"); 00163 TQMap<TQString, KService::Ptr> mimeMap; 00164 00165 for (KTrader::OfferList::ConstIterator it = plugins.begin(); it != plugins.end(); ++it) 00166 if (!d->enabledPlugins || d->enabledPlugins->contains((*it)->desktopEntryName())) 00167 { 00168 TQStringList mimeTypes = (*it)->property("MimeTypes").toStringList(); 00169 for (TQStringList::ConstIterator mt = mimeTypes.begin(); mt != mimeTypes.end(); ++mt) 00170 mimeMap.insert(*mt, *it); 00171 } 00172 00173 // Look for images and store the items in our todo list :) 00174 bool bNeedCache = false; 00175 for (KFileItemListIterator it(d->initialItems); it.current(); ++it ) 00176 { 00177 PreviewItem item; 00178 item.item = it.current(); 00179 TQMap<TQString, KService::Ptr>::ConstIterator plugin = mimeMap.find(it.current()->mimetype()); 00180 if (plugin == mimeMap.end() 00181 && (it.current()->mimetype() != "application/x-desktop") 00182 && (it.current()->mimetype() != "media/builtin-mydocuments") 00183 && (it.current()->mimetype() != "media/builtin-mycomputer") 00184 && (it.current()->mimetype() != "media/builtin-mynetworkplaces") 00185 && (it.current()->mimetype() != "media/builtin-printers") 00186 && (it.current()->mimetype() != "media/builtin-trash") 00187 && (it.current()->mimetype() != "media/builtin-webbrowser")) 00188 { 00189 TQString mimeType = it.current()->mimetype(); 00190 plugin = mimeMap.find(mimeType.replace(TQRegExp("/.*"), "/*")); 00191 00192 if (plugin == mimeMap.end()) 00193 { 00194 // check mime type inheritance 00195 KMimeType::Ptr mimeInfo = KMimeType::mimeType(it.current()->mimetype()); 00196 TQString parentMimeType = mimeInfo->parentMimeType(); 00197 while (!parentMimeType.isEmpty()) 00198 { 00199 plugin = mimeMap.find(parentMimeType); 00200 if (plugin != mimeMap.end()) break; 00201 00202 KMimeType::Ptr parentMimeInfo = KMimeType::mimeType(parentMimeType); 00203 if (!parentMimeInfo) break; 00204 00205 parentMimeType = parentMimeInfo->parentMimeType(); 00206 } 00207 } 00208 00209 if (plugin == mimeMap.end()) 00210 { 00211 // check X-KDE-Text property 00212 KMimeType::Ptr mimeInfo = KMimeType::mimeType(it.current()->mimetype()); 00213 TQVariant textProperty = mimeInfo->property("X-KDE-text"); 00214 if (textProperty.isValid() && textProperty.type() == TQVariant::Bool) 00215 { 00216 if (textProperty.toBool()) 00217 { 00218 plugin = mimeMap.find("text/plain"); 00219 if (plugin == mimeMap.end()) 00220 { 00221 plugin = mimeMap.find( "text/*" ); 00222 } 00223 } 00224 } 00225 } 00226 } 00227 00228 if (plugin != mimeMap.end()) 00229 { 00230 item.plugin = *plugin; 00231 d->items.append(item); 00232 if (!bNeedCache && d->bSave && 00233 (it.current()->url().protocol() != "file" || 00234 !it.current()->url().directory( false ).startsWith(d->thumbRoot)) && 00235 (*plugin)->property("CacheThumbnail").toBool()) 00236 bNeedCache = true; 00237 } 00238 else 00239 { 00240 emitFailed(it.current()); 00241 if (d->deleteItems) 00242 delete it.current(); 00243 } 00244 } 00245 00246 // Read configuration value for the maximum allowed size 00247 KConfig * config = KGlobal::config(); 00248 KConfigGroupSaver cgs( config, "PreviewSettings" ); 00249 d->maximumSize = config->readNumEntry( "MaximumSize", 1024*1024 /* 1MB */ ); 00250 00251 if (bNeedCache) 00252 { 00253 if (d->width <= 128 && d->height <= 128) d->cacheWidth = d->cacheHeight = 128; 00254 else d->cacheWidth = d->cacheHeight = 256; 00255 d->thumbPath = d->thumbRoot + (d->cacheWidth == 128 ? "normal/" : "large/"); 00256 KStandardDirs::makeDir(d->thumbPath, 0700); 00257 } 00258 else 00259 d->bSave = false; 00260 determineNextFile(); 00261 } 00262 00263 void PreviewJob::removeItem( const KFileItem *item ) 00264 { 00265 for (TQValueList<PreviewItem>::Iterator it = d->items.begin(); it != d->items.end(); ++it) 00266 if ((*it).item == item) 00267 { 00268 d->items.remove(it); 00269 break; 00270 } 00271 00272 if (d->currentItem.item == item) 00273 { 00274 subjobs.first()->kill(); 00275 subjobs.removeFirst(); 00276 determineNextFile(); 00277 } 00278 } 00279 00280 void PreviewJob::setIgnoreMaximumSize(bool ignoreSize) 00281 { 00282 d->ignoreMaximumSize = ignoreSize; 00283 } 00284 00285 void PreviewJob::determineNextFile() 00286 { 00287 if (d->currentItem.item) 00288 { 00289 if (!d->succeeded) 00290 emitFailed(); 00291 if (d->deleteItems) { 00292 delete d->currentItem.item; 00293 d->currentItem.item = 0L; 00294 } 00295 } 00296 // No more items ? 00297 if ( d->items.isEmpty() ) 00298 { 00299 emitResult(); 00300 return; 00301 } 00302 else 00303 { 00304 // First, stat the orig file 00305 d->state = PreviewJobPrivate::STATE_STATORIG; 00306 d->currentItem = d->items.first(); 00307 d->succeeded = false; 00308 d->items.remove(d->items.begin()); 00309 KIO::Job *job = KIO::stat( d->currentItem.item->url(), false ); 00310 job->addMetaData( "no-auth-prompt", "true" ); 00311 addSubjob(job); 00312 } 00313 } 00314 00315 void PreviewJob::slotResult( KIO::Job *job ) 00316 { 00317 subjobs.remove( job ); 00318 Q_ASSERT ( subjobs.isEmpty() ); // We should have only one job at a time ... 00319 switch ( d->state ) 00320 { 00321 case PreviewJobPrivate::STATE_STATORIG: 00322 { 00323 if (job->error()) // that's no good news... 00324 { 00325 // Drop this one and move on to the next one 00326 determineNextFile(); 00327 return; 00328 } 00329 KIO::UDSEntry entry = ((KIO::StatJob*)job)->statResult(); 00330 KIO::UDSEntry::ConstIterator it = entry.begin(); 00331 d->tOrig = 0; 00332 int found = 0; 00333 for( ; it != entry.end() && found < 2; it++ ) 00334 { 00335 if ( (*it).m_uds == KIO::UDS_MODIFICATION_TIME ) 00336 { 00337 d->tOrig = (time_t)((*it).m_long); 00338 found++; 00339 } 00340 else if ( (*it).m_uds == KIO::UDS_SIZE ) 00341 { 00342 if ( filesize_t((*it).m_long) > d->maximumSize && 00343 !d->ignoreMaximumSize && 00344 !d->currentItem.plugin->property("IgnoreMaximumSize").toBool() ) 00345 { 00346 determineNextFile(); 00347 return; 00348 } 00349 found++; 00350 } 00351 } 00352 00353 if ( !d->currentItem.plugin->property( "CacheThumbnail" ).toBool() ) 00354 { 00355 // This preview will not be cached, no need to look for a saved thumbnail 00356 // Just create it, and be done 00357 getOrCreateThumbnail(); 00358 return; 00359 } 00360 00361 if ( statResultThumbnail() ) 00362 return; 00363 00364 getOrCreateThumbnail(); 00365 return; 00366 } 00367 case PreviewJobPrivate::STATE_GETORIG: 00368 { 00369 if (job->error()) 00370 { 00371 determineNextFile(); 00372 return; 00373 } 00374 00375 createThumbnail( static_cast<KIO::FileCopyJob*>(job)->destURL().path() ); 00376 return; 00377 } 00378 case PreviewJobPrivate::STATE_CREATETHUMB: 00379 { 00380 if (!d->tempName.isEmpty()) 00381 { 00382 TQFile::remove(d->tempName); 00383 d->tempName = TQString::null; 00384 } 00385 determineNextFile(); 00386 return; 00387 } 00388 } 00389 } 00390 00391 bool PreviewJob::statResultThumbnail() 00392 { 00393 if ( d->thumbPath.isEmpty() ) 00394 return false; 00395 00396 KURL url = d->currentItem.item->url(); 00397 // Don't include the password if any 00398 url.setPass(TQString::null); 00399 // The TMS defines local files as file:///path/to/file instead of KDE's 00400 // way (file:/path/to/file) 00401 #ifdef KURL_TRIPLE_SLASH_FILE_PROT 00402 d->origName = url.url(); 00403 #else 00404 if (url.protocol() == "file") d->origName = "file://" + url.path(); 00405 else d->origName = url.url(); 00406 #endif 00407 00408 KMD5 md5( TQFile::encodeName( d->origName ).data() ); 00409 d->thumbName = TQFile::encodeName( md5.hexDigest() ) + ".png"; 00410 00411 TQImage thumb; 00412 if ( !thumb.load( d->thumbPath + d->thumbName ) ) return false; 00413 00414 if ( thumb.text( "Thumb::URI", 0 ) != d->origName || 00415 thumb.text( "Thumb::MTime", 0 ).toInt() != d->tOrig ) return false; 00416 00417 // Found it, use it 00418 emitPreview( thumb ); 00419 d->succeeded = true; 00420 determineNextFile(); 00421 return true; 00422 } 00423 00424 00425 void PreviewJob::getOrCreateThumbnail() 00426 { 00427 // We still need to load the orig file ! (This is getting tedious) :) 00428 const KFileItem* item = d->currentItem.item; 00429 const TQString localPath = item->localPath(); 00430 if ( !localPath.isEmpty() ) 00431 createThumbnail( localPath ); 00432 else 00433 { 00434 d->state = PreviewJobPrivate::STATE_GETORIG; 00435 KTempFile localFile; 00436 KURL localURL; 00437 localURL.setPath( d->tempName = localFile.name() ); 00438 const KURL currentURL = item->url(); 00439 KIO::Job * job = KIO::file_copy( currentURL, localURL, -1, true, 00440 false, false /* No GUI */ ); 00441 job->addMetaData("thumbnail","1"); 00442 addSubjob(job); 00443 } 00444 } 00445 00446 // KDE 4: Make it const TQString & 00447 void PreviewJob::createThumbnail( TQString pixPath ) 00448 { 00449 d->state = PreviewJobPrivate::STATE_CREATETHUMB; 00450 KURL thumbURL; 00451 thumbURL.setProtocol("thumbnail"); 00452 thumbURL.setPath(pixPath); 00453 KIO::TransferJob *job = KIO::get(thumbURL, false, false); 00454 addSubjob(job); 00455 connect(job, TQT_SIGNAL(data(KIO::Job *, const TQByteArray &)), TQT_SLOT(slotThumbData(KIO::Job *, const TQByteArray &))); 00456 bool save = d->bSave && d->currentItem.plugin->property("CacheThumbnail").toBool(); 00457 job->addMetaData("mimeType", d->currentItem.item->mimetype()); 00458 job->addMetaData("width", TQString().setNum(save ? d->cacheWidth : d->width)); 00459 job->addMetaData("height", TQString().setNum(save ? d->cacheHeight : d->height)); 00460 job->addMetaData("iconSize", TQString().setNum(save ? 64 : d->iconSize)); 00461 job->addMetaData("iconAlpha", TQString().setNum(d->iconAlpha)); 00462 job->addMetaData("plugin", d->currentItem.plugin->library()); 00463 #ifdef Q_OS_UNIX 00464 if (d->shmid == -1) 00465 { 00466 if (d->shmaddr) { 00467 shmdt((char*)d->shmaddr); 00468 shmctl(d->shmid, IPC_RMID, 0); 00469 } 00470 d->shmid = shmget(IPC_PRIVATE, d->cacheWidth * d->cacheHeight * 4, IPC_CREAT|0600); 00471 if (d->shmid != -1) 00472 { 00473 d->shmaddr = (uchar *)(shmat(d->shmid, 0, SHM_RDONLY)); 00474 if (d->shmaddr == (uchar *)-1) 00475 { 00476 shmctl(d->shmid, IPC_RMID, 0); 00477 d->shmaddr = 0; 00478 d->shmid = -1; 00479 } 00480 } 00481 else 00482 d->shmaddr = 0; 00483 } 00484 if (d->shmid != -1) 00485 job->addMetaData("shmid", TQString().setNum(d->shmid)); 00486 #endif 00487 } 00488 00489 void PreviewJob::slotThumbData(KIO::Job *, const TQByteArray &data) 00490 { 00491 bool save = d->bSave && 00492 d->currentItem.plugin->property("CacheThumbnail").toBool() && 00493 (d->currentItem.item->url().protocol() != "file" || 00494 !d->currentItem.item->url().directory( false ).startsWith(d->thumbRoot)); 00495 TQImage thumb; 00496 #ifdef Q_OS_UNIX 00497 if (d->shmaddr) 00498 { 00499 TQDataStream str(data, IO_ReadOnly); 00500 int width, height, depth; 00501 bool alpha; 00502 str >> width >> height >> depth >> alpha; 00503 thumb = TQImage(d->shmaddr, width, height, depth, 0, 0, TQImage::IgnoreEndian); 00504 thumb.setAlphaBuffer(alpha); 00505 } 00506 else 00507 #endif 00508 thumb.loadFromData(data); 00509 00510 if (save) 00511 { 00512 thumb.setText("Thumb::URI", 0, d->origName); 00513 thumb.setText("Thumb::MTime", 0, TQString::number(d->tOrig)); 00514 thumb.setText("Thumb::Size", 0, number(d->currentItem.item->size())); 00515 thumb.setText("Thumb::Mimetype", 0, d->currentItem.item->mimetype()); 00516 thumb.setText("Software", 0, "KDE Thumbnail Generator"); 00517 KTempFile temp(d->thumbPath + "kde-tmp-", ".png"); 00518 if (temp.status() == 0) //Only try to write out the thumbnail if we 00519 { //actually created the temp file. 00520 thumb.save(temp.name(), "PNG"); 00521 rename(TQFile::encodeName(temp.name()), TQFile::encodeName(d->thumbPath + d->thumbName)); 00522 } 00523 } 00524 emitPreview( thumb ); 00525 d->succeeded = true; 00526 } 00527 00528 void PreviewJob::emitPreview(const TQImage &thumb) 00529 { 00530 TQPixmap pix; 00531 if (thumb.width() > d->width || thumb.height() > d->height) 00532 { 00533 double imgRatio = (double)thumb.height() / (double)thumb.width(); 00534 if (imgRatio > (double)d->height / (double)d->width) 00535 pix.convertFromImage( 00536 thumb.smoothScale((int)TQMAX((double)d->height / imgRatio, 1), d->height)); 00537 else pix.convertFromImage( 00538 thumb.smoothScale(d->width, (int)TQMAX((double)d->width * imgRatio, 1))); 00539 } 00540 else pix.convertFromImage(thumb); 00541 emit gotPreview(d->currentItem.item, pix); 00542 } 00543 00544 void PreviewJob::emitFailed(const KFileItem *item) 00545 { 00546 if (!item) 00547 item = d->currentItem.item; 00548 emit failed(item); 00549 } 00550 00551 TQStringList PreviewJob::availablePlugins() 00552 { 00553 TQStringList result; 00554 KTrader::OfferList plugins = KTrader::self()->query("ThumbCreator"); 00555 for (KTrader::OfferList::ConstIterator it = plugins.begin(); it != plugins.end(); ++it) 00556 if (!result.contains((*it)->desktopEntryName())) 00557 result.append((*it)->desktopEntryName()); 00558 return result; 00559 } 00560 00561 TQStringList PreviewJob::supportedMimeTypes() 00562 { 00563 TQStringList result; 00564 KTrader::OfferList plugins = KTrader::self()->query("ThumbCreator"); 00565 for (KTrader::OfferList::ConstIterator it = plugins.begin(); it != plugins.end(); ++it) 00566 result += (*it)->property("MimeTypes").toStringList(); 00567 return result; 00568 } 00569 00570 void PreviewJob::kill( bool quietly ) 00571 { 00572 d->startPreviewTimer.stop(); 00573 Job::kill( quietly ); 00574 } 00575 00576 PreviewJob *KIO::filePreview( const KFileItemList &items, int width, int height, 00577 int iconSize, int iconAlpha, bool scale, bool save, 00578 const TQStringList *enabledPlugins ) 00579 { 00580 return new PreviewJob(items, width, height, iconSize, iconAlpha, 00581 scale, save, enabledPlugins); 00582 } 00583 00584 PreviewJob *KIO::filePreview( const KURL::List &items, int width, int height, 00585 int iconSize, int iconAlpha, bool scale, bool save, 00586 const TQStringList *enabledPlugins ) 00587 { 00588 KFileItemList fileItems; 00589 for (KURL::List::ConstIterator it = items.begin(); it != items.end(); ++it) 00590 fileItems.append(new KFileItem(KFileItem::Unknown, KFileItem::Unknown, *it, true)); 00591 return new PreviewJob(fileItems, width, height, iconSize, iconAlpha, 00592 scale, save, enabledPlugins, true); 00593 } 00594 00595 void PreviewJob::virtual_hook( int id, void* data ) 00596 { KIO::Job::virtual_hook( id, data ); } 00597