File indexing completed on 2024-04-28 04:58:04
0001 /* This file is part of the KDE libraries 0002 SPDX-FileCopyrightText: 2000 Malte Starostik <malte@kde.org> 0003 SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "thumbnail.h" 0009 #include "thumbnail-logsettings.h" 0010 0011 #include <stdlib.h> 0012 #ifdef __FreeBSD__ 0013 #include <machine/param.h> 0014 #endif 0015 #include <sys/types.h> 0016 #if defined(Q_OS_WINDOWS) 0017 #include <windows.h> 0018 #else 0019 #include <sys/ipc.h> 0020 #include <sys/shm.h> 0021 #include <unistd.h> // nice() 0022 #endif 0023 0024 #include <QApplication> 0025 #include <QBuffer> 0026 #include <QColorSpace> 0027 #include <QCryptographicHash> 0028 #include <QDebug> 0029 #include <QDirIterator> 0030 #include <QFile> 0031 #include <QFileInfo> 0032 #include <QIcon> 0033 #include <QImage> 0034 #include <QLibrary> 0035 #include <QMimeDatabase> 0036 #include <QMimeType> 0037 #include <QPixmap> 0038 #include <QPluginLoader> 0039 #include <QSaveFile> 0040 #include <QUrl> 0041 0042 #include <KConfigGroup> 0043 #include <KFileItem> 0044 #include <KLocalizedString> 0045 #include <KSharedConfig> 0046 0047 #include <KIO/PreviewJob> 0048 #include <KPluginFactory> 0049 0050 #include <kio_version.h> 0051 0052 #include <limits> 0053 0054 #include "imagefilter.h" 0055 0056 // Recognized metadata entries: 0057 // mimeType - the mime type of the file, used for the overlay icon if any 0058 // width - maximum width for the thumbnail 0059 // height - maximum height for the thumbnail 0060 // iconSize - the size of the overlay icon to use if any (deprecated, ignored) 0061 // iconAlpha - the transparency value used for icon overlays (deprecated, ignored) 0062 // plugin - the name of the plugin library to be used for thumbnail creation. 0063 // Provided by the application to save an addition KTrader 0064 // query here. 0065 // devicePixelRatio - the devicePixelRatio to use for the output, 0066 // the dimensions of the output is multiplied by it and output pixmap will have devicePixelRatio 0067 // enabledPlugins - a list of enabled thumbnailer plugins. PreviewJob does not call 0068 // this thumbnail worker when a given plugin isn't enabled. However, 0069 // for directory thumbnails it doesn't know that the thumbnailer 0070 // internally also loads the plugins. 0071 // shmid - the shared memory segment id to write the image's data to. 0072 // The segment is assumed to provide enough space for a 32-bit 0073 // image sized width x height pixels. 0074 // If this is given, the data returned by the worker will be: 0075 // int width 0076 // int height 0077 // int depth 0078 // Otherwise, the data returned is the image in PNG format. 0079 0080 using namespace KIO; 0081 0082 // Pseudo plugin class to embed meta data 0083 class KIOPluginForMetaData : public QObject 0084 { 0085 Q_OBJECT 0086 Q_PLUGIN_METADATA(IID "org.kde.kio.worker.thumbnail" FILE "thumbnail.json") 0087 }; 0088 0089 extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) 0090 { 0091 #if defined(Q_OS_WINDOWS) 0092 SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS); 0093 #else 0094 std::ignore = nice(5); 0095 #endif 0096 0097 QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); 0098 0099 // Creating a QApplication in a worker in not a very good idea, 0100 // as dispatchLoop() doesn't allow it to process its messages, 0101 // so it for example wouldn't reply to ksmserver - on the other 0102 // hand, this worker uses QPixmaps for some reason, and they 0103 // need QGuiApplication 0104 qunsetenv("SESSION_MANAGER"); 0105 0106 // Some thumbnail plugins use QWidget classes for the rendering, 0107 // so use QApplication here, not just QGuiApplication 0108 QApplication app(argc, argv); 0109 0110 if (argc != 4) { 0111 qCritical() << "Usage: kio_thumbnail protocol domain-socket1 domain-socket2"; 0112 exit(-1); 0113 } 0114 0115 ThumbnailProtocol worker(argv[2], argv[3]); 0116 worker.dispatchLoop(); 0117 0118 return 0; 0119 } 0120 0121 ThumbnailProtocol::ThumbnailProtocol(const QByteArray &pool, const QByteArray &app) 0122 : WorkerBase("thumbnail", pool, app) 0123 , m_width(0) 0124 , m_height(0) 0125 , m_devicePixelRatio(1.0) 0126 , m_maxFileSize(0) 0127 , m_randomGenerator() 0128 { 0129 } 0130 0131 ThumbnailProtocol::~ThumbnailProtocol() 0132 { 0133 qDeleteAll(m_creators); 0134 } 0135 0136 /** 0137 * Scales down the image \p img in a way that it fits into the given maximum width and height 0138 */ 0139 void scaleDownImage(QImage &img, int maxWidth, int maxHeight) 0140 { 0141 if (img.width() > maxWidth || img.height() > maxHeight) { 0142 img = img.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0143 } 0144 } 0145 0146 /** 0147 * @brief convertToStandardRgb 0148 * Convert preview to sRGB for proper viewing on most monitors. 0149 */ 0150 void convertToStandardRgb(QImage &img) 0151 { 0152 auto cs = img.colorSpace(); 0153 if (!cs.isValid()) { 0154 return; 0155 } 0156 if (cs.transferFunction() != QColorSpace::TransferFunction::SRgb || cs.primaries() != QColorSpace::Primaries::SRgb) { 0157 img.convertToColorSpace(QColorSpace(QColorSpace::SRgb)); 0158 } 0159 } 0160 0161 KIO::WorkerResult ThumbnailProtocol::get(const QUrl &url) 0162 { 0163 m_mimeType = metaData("mimeType"); 0164 m_enabledPlugins = metaData("enabledPlugins").split(QLatin1Char(','), Qt::SkipEmptyParts); 0165 if (m_enabledPlugins.isEmpty()) { 0166 const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings")); 0167 m_enabledPlugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins()); 0168 } 0169 0170 Q_ASSERT(url.scheme() == "thumbnail"); 0171 QFileInfo info(url.path()); 0172 Q_ASSERT(info.isAbsolute()); 0173 0174 if (!info.exists()) { 0175 // The file does not exist 0176 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.path()); 0177 } else if (!info.isReadable()) { 0178 // The file is not readable! 0179 return KIO::WorkerResult::fail(KIO::ERR_CANNOT_READ, url.path()); 0180 } 0181 0182 // qDebug() << "Wanting MIME Type:" << m_mimeType; 0183 bool direct = false; 0184 if (m_mimeType.isEmpty()) { 0185 // qDebug() << "PATH: " << url.path() << "isDir:" << info.isDir(); 0186 if (info.isDir()) { 0187 m_mimeType = "inode/directory"; 0188 } else { 0189 const QMimeDatabase db; 0190 0191 m_mimeType = db.mimeTypeForFile(info).name(); 0192 } 0193 0194 // qDebug() << "Guessing MIME Type:" << m_mimeType; 0195 direct = true; // thumbnail: URL was probably typed in Konqueror 0196 } 0197 0198 if (m_mimeType.isEmpty()) { 0199 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("No MIME Type specified.")); 0200 } 0201 0202 m_width = metaData("width").toInt(); 0203 m_height = metaData("height").toInt(); 0204 0205 if (m_width < 0 || m_height < 0) { 0206 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("No or invalid size specified.")); 0207 } else if (!m_width || !m_height) { 0208 // qDebug() << "Guessing height, width, icon size!"; 0209 m_width = 128; 0210 m_height = 128; 0211 } 0212 bool ok; 0213 m_devicePixelRatio = metaData("devicePixelRatio").toFloat(&ok); 0214 if (!ok || qFuzzyIsNull(m_devicePixelRatio)) { 0215 m_devicePixelRatio = 1.0; 0216 } else { 0217 m_width *= m_devicePixelRatio; 0218 m_height *= m_devicePixelRatio; 0219 } 0220 0221 QImage img; 0222 QString plugin = metaData("plugin"); 0223 0224 if ((plugin.isEmpty() || plugin.contains("directorythumbnail")) && m_mimeType == "inode/directory") { 0225 img = thumbForDirectory(info.canonicalFilePath()); 0226 if (img.isNull()) { 0227 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("Cannot create thumbnail for directory")); 0228 } 0229 } else { 0230 if (plugin.isEmpty()) { 0231 plugin = pluginForMimeType(m_mimeType).fileName(); 0232 } 0233 0234 // qDebug() << "Guess plugin: " << plugin; 0235 if (plugin.isEmpty()) { 0236 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("No plugin specified.")); 0237 } 0238 0239 ThumbCreatorWithMetadata *creator = getThumbCreator(plugin); 0240 if (!creator) { 0241 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("Cannot load ThumbCreator %1", plugin)); 0242 } 0243 0244 if (creator->handleSequences) { 0245 setMetaData("handlesSequences", QStringLiteral("1")); 0246 } 0247 0248 if (!createThumbnail(creator, info.canonicalFilePath(), m_width, m_height, img)) { 0249 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("Cannot create thumbnail for %1", info.canonicalFilePath())); 0250 } 0251 0252 // We MUST do this after calling create(), because the create() call itself might change it. 0253 if (creator->handleSequences) { 0254 setMetaData("sequenceIndexWraparoundPoint", QString::number(m_sequenceIndexWrapAroundPoint)); 0255 } 0256 } 0257 0258 if (img.isNull()) { 0259 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("Failed to create a thumbnail.")); 0260 } 0261 0262 // image quality and size corrections 0263 scaleDownImage(img, m_width, m_height); 0264 0265 convertToStandardRgb(img); 0266 0267 if (img.colorCount() > 0) { 0268 // images using indexed color format, are not loaded properly by QImage ctor using in shm code path 0269 // convert the format to regular RGB 0270 img = img.convertToFormat(img.hasAlphaChannel() ? QImage::Format_ARGB32 : QImage::Format_RGB32); 0271 } 0272 0273 if (direct) { 0274 // If thumbnail was called directly from Konqueror, then the image needs to be raw 0275 // qDebug() << "RAW IMAGE TO STREAM"; 0276 QBuffer buf; 0277 if (!buf.open(QIODevice::WriteOnly)) { 0278 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("Could not write image.")); 0279 } 0280 img.save(&buf, "PNG"); 0281 buf.close(); 0282 mimeType("image/png"); 0283 data(buf.buffer()); 0284 return KIO::WorkerResult::pass(); 0285 } 0286 0287 QByteArray imgData; 0288 QDataStream stream(&imgData, QIODevice::WriteOnly); 0289 0290 // Keep in sync with kio/src/previewjob.cpp 0291 stream << img.width() << img.height() << img.format() << img.devicePixelRatio(); 0292 0293 #ifndef Q_OS_WIN 0294 const QString shmid = metaData("shmid"); 0295 if (shmid.isEmpty()) 0296 #endif 0297 { 0298 // qDebug() << "IMAGE TO STREAM"; 0299 stream << img; 0300 } 0301 #ifndef Q_OS_WIN 0302 else { 0303 // qDebug() << "IMAGE TO SHMID"; 0304 void *shmaddr = shmat(shmid.toInt(), nullptr, 0); 0305 if (shmaddr == (void *)-1) { 0306 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("Failed to attach to shared memory segment %1", shmid)); 0307 } 0308 struct shmid_ds shmStat; 0309 if (shmctl(shmid.toInt(), IPC_STAT, &shmStat) == -1 || shmStat.shm_segsz < (uint)img.sizeInBytes()) { 0310 return KIO::WorkerResult::fail(KIO::ERR_INTERNAL, i18n("Image is too big for the shared memory segment")); 0311 shmdt((char *)shmaddr); 0312 } 0313 memcpy(shmaddr, img.constBits(), img.sizeInBytes()); 0314 shmdt((char *)shmaddr); 0315 } 0316 #endif 0317 mimeType("application/octet-stream"); 0318 data(imgData); 0319 0320 return KIO::WorkerResult::pass(); 0321 } 0322 0323 KPluginMetaData ThumbnailProtocol::pluginForMimeType(const QString &mimeType) 0324 { 0325 const QVector<KPluginMetaData> plugins = KIO::PreviewJob::availableThumbnailerPlugins(); 0326 for (const KPluginMetaData &plugin : plugins) { 0327 if (plugin.supportsMimeType(mimeType)) { 0328 return plugin; 0329 } 0330 } 0331 for (const auto &plugin : plugins) { 0332 const QStringList mimeTypes = plugin.mimeTypes() + plugin.value(QStringLiteral("ServiceTypes"), QStringList()); 0333 for (const QString &mime : mimeTypes) { 0334 if (mime.endsWith('*')) { 0335 const auto mimeGroup = QStringView(mime).left(mime.length() - 1); 0336 if (mimeType.startsWith(mimeGroup)) { 0337 return plugin; 0338 } 0339 } 0340 } 0341 } 0342 0343 return {}; 0344 } 0345 0346 float ThumbnailProtocol::sequenceIndex() const 0347 { 0348 return metaData("sequence-index").toFloat(); 0349 } 0350 0351 bool ThumbnailProtocol::isOpaque(const QImage &image) const 0352 { 0353 // Test the corner pixels 0354 return qAlpha(image.pixel(QPoint(0, 0))) == 255 && qAlpha(image.pixel(QPoint(image.width() - 1, 0))) == 255 0355 && qAlpha(image.pixel(QPoint(0, image.height() - 1))) == 255 && qAlpha(image.pixel(QPoint(image.width() - 1, image.height() - 1))) == 255; 0356 } 0357 0358 void ThumbnailProtocol::drawPictureFrame(QPainter *painter, 0359 const QPoint ¢erPos, 0360 const QImage &image, 0361 int borderStrokeWidth, 0362 QSize imageTargetSize, 0363 int rotationAngle) const 0364 { 0365 // Scale the image down so it matches the aspect ratio 0366 float scaling = 1.0; 0367 0368 const bool landscapeDimension = image.width() > image.height(); 0369 const bool hasTargetSizeWidth = imageTargetSize.width() != 0; 0370 const bool hasTargetSizeHeight = imageTargetSize.height() != 0; 0371 const int widthWithFrames = image.width() + (2 * borderStrokeWidth); 0372 const int heightWithFrames = image.height() + (2 * borderStrokeWidth); 0373 if (landscapeDimension && (widthWithFrames > imageTargetSize.width()) && hasTargetSizeWidth) { 0374 scaling = float(imageTargetSize.width()) / float(widthWithFrames); 0375 } else if ((heightWithFrames > imageTargetSize.height()) && hasTargetSizeHeight) { 0376 scaling = float(imageTargetSize.height()) / float(heightWithFrames); 0377 } 0378 0379 const float scaledFrameWidth = borderStrokeWidth / scaling; 0380 0381 QTransform m; 0382 m.rotate(rotationAngle); 0383 m.scale(scaling, scaling); 0384 0385 const QRectF frameRect( 0386 QPointF(0, 0), 0387 QPointF(image.width() / image.devicePixelRatio() + scaledFrameWidth * 2, image.height() / image.devicePixelRatio() + scaledFrameWidth * 2)); 0388 0389 QRect r = m.mapRect(QRectF(frameRect)).toAlignedRect(); 0390 0391 QImage transformed(r.size(), QImage::Format_ARGB32); 0392 transformed.fill(0); 0393 QPainter p(&transformed); 0394 p.setRenderHint(QPainter::SmoothPixmapTransform); 0395 p.setRenderHint(QPainter::Antialiasing); 0396 p.setCompositionMode(QPainter::CompositionMode_Source); 0397 0398 p.translate(-r.topLeft()); 0399 p.setWorldTransform(m, true); 0400 0401 if (isOpaque(image)) { 0402 p.setPen(Qt::NoPen); 0403 p.setBrush(Qt::white); 0404 p.drawRoundedRect(frameRect, scaledFrameWidth / 2, scaledFrameWidth / 2); 0405 } 0406 p.drawImage(scaledFrameWidth, scaledFrameWidth, image); 0407 p.end(); 0408 0409 int radius = qMax(borderStrokeWidth, 1); 0410 0411 QImage shadow(r.size() + QSize(radius * 2, radius * 2), QImage::Format_ARGB32); 0412 shadow.fill(0); 0413 0414 p.begin(&shadow); 0415 p.setCompositionMode(QPainter::CompositionMode_Source); 0416 p.drawImage(radius, radius, transformed); 0417 p.end(); 0418 0419 ImageFilter::shadowBlur(shadow, radius, QColor(0, 0, 0, 128)); 0420 0421 r.moveCenter(centerPos); 0422 0423 painter->drawImage(r.topLeft() - QPoint(radius / 2, radius / 2), shadow); 0424 painter->drawImage(r.topLeft(), transformed); 0425 } 0426 0427 QImage ThumbnailProtocol::thumbForDirectory(const QString &directory) 0428 { 0429 QImage img; 0430 if (m_propagationDirectories.isEmpty()) { 0431 // Directories that the directory preview will be propagated into if there is no direct sub-directories 0432 const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings")); 0433 const QStringList propagationDirectoriesList = globalConfig.readEntry("PropagationDirectories", QStringList() << "VIDEO_TS"); 0434 m_propagationDirectories = QSet<QString>(propagationDirectoriesList.begin(), propagationDirectoriesList.end()); 0435 m_maxFileSize = globalConfig.readEntry("MaximumSize", std::numeric_limits<qint64>::max()); 0436 } 0437 0438 const int tiles = 2; // Count of items shown on each dimension 0439 const int spacing = 1 * m_devicePixelRatio; 0440 const int visibleCount = tiles * tiles; 0441 0442 // TODO: the margins are optimized for the Oxygen iconset 0443 // Provide a fallback solution for other iconsets (e. g. draw folder 0444 // only as small overlay, use no margins) 0445 0446 KFileItem item(QUrl::fromLocalFile(directory)); 0447 const int extent = qMin(m_width, m_height); 0448 QPixmap folder = QIcon::fromTheme(item.iconName()).pixmap(extent); 0449 folder.setDevicePixelRatio(m_devicePixelRatio); 0450 0451 // Scale up base icon to ensure overlays are rendered with 0452 // the best quality possible even for low-res custom folder icons 0453 if (qMax(folder.width(), folder.height()) < extent) { 0454 folder = folder.scaled(extent, extent, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0455 } 0456 0457 const int folderWidth = folder.width(); 0458 const int folderHeight = folder.height(); 0459 0460 const int topMargin = folderHeight * 30 / 100; 0461 const int bottomMargin = folderHeight / 6; 0462 const int leftMargin = folderWidth / 13; 0463 const int rightMargin = leftMargin; 0464 // the picture border stroke width 1/170 rounded up 0465 // (i.e for each 170px the folder width increases those border increase by 1 px) 0466 const int borderStrokeWidth = qRound(folderWidth / 170.); 0467 0468 const int segmentWidth = (folderWidth - leftMargin - rightMargin + spacing) / tiles - spacing; 0469 const int segmentHeight = (folderHeight - topMargin - bottomMargin + spacing) / tiles - spacing; 0470 if ((segmentWidth < 5 * m_devicePixelRatio) || (segmentHeight < 5 * m_devicePixelRatio)) { 0471 // the segment size is too small for a useful preview 0472 return img; 0473 } 0474 0475 // Advance to the next tile page each second 0476 int skipValidItems = ((int)sequenceIndex()) * visibleCount; 0477 0478 img = QImage(QSize(folderWidth, folderHeight), QImage::Format_ARGB32); 0479 img.setDevicePixelRatio(m_devicePixelRatio); 0480 img.fill(0); 0481 0482 QPainter p; 0483 p.begin(&img); 0484 0485 p.setCompositionMode(QPainter::CompositionMode_Source); 0486 p.drawPixmap(0, 0, folder); 0487 p.setCompositionMode(QPainter::CompositionMode_SourceOver); 0488 0489 int xPos = leftMargin; 0490 int yPos = topMargin; 0491 0492 int iterations = 0; 0493 QString hadFirstThumbnail; 0494 QImage firstThumbnail; 0495 0496 int validThumbnails = 0; 0497 int totalValidThumbs = -1; 0498 0499 while (true) { 0500 QDirIterator dir(directory, QDir::Files | QDir::Readable); 0501 int skipped = 0; 0502 0503 // Seed the random number generator so that it always returns the same result 0504 // for the same directory and sequence-item 0505 m_randomGenerator.seed(qHash(directory) + skipValidItems); 0506 while (dir.hasNext()) { 0507 ++iterations; 0508 if (iterations > 500) { 0509 skipValidItems = skipped = 0; 0510 break; 0511 } 0512 0513 dir.next(); 0514 0515 if (dir.fileInfo().isSymbolicLink()) { 0516 // Skip symbolic links, as these may point to e.g. network file 0517 // systems or other slow storage. The calling code already 0518 // checks for the directory itself, and if it is fine any 0519 // contained plain file is fine as well. 0520 continue; 0521 } 0522 0523 auto fileSize = dir.fileInfo().size(); 0524 if ((fileSize == 0) || (fileSize > m_maxFileSize)) { 0525 // don't create thumbnails for files that exceed 0526 // the maximum set file size or are empty 0527 continue; 0528 } 0529 0530 QImage subThumbnail; 0531 if (!createSubThumbnail(subThumbnail, dir.filePath(), segmentWidth, segmentHeight)) { 0532 continue; 0533 } 0534 0535 if (skipped < skipValidItems) { 0536 ++skipped; 0537 continue; 0538 } 0539 0540 drawSubThumbnail(p, subThumbnail, segmentWidth, segmentHeight, xPos, yPos, borderStrokeWidth); 0541 0542 if (hadFirstThumbnail.isEmpty()) { 0543 hadFirstThumbnail = dir.filePath(); 0544 firstThumbnail = subThumbnail; 0545 } 0546 0547 ++validThumbnails; 0548 if (validThumbnails >= visibleCount) { 0549 break; 0550 } 0551 0552 xPos += segmentWidth + spacing; 0553 if (xPos > folderWidth - rightMargin - segmentWidth) { 0554 xPos = leftMargin; 0555 yPos += segmentHeight + spacing; 0556 } 0557 } 0558 0559 if (!dir.hasNext() && totalValidThumbs < 0) { 0560 // We iterated over the entire directory for the first time, so now we know how many thumbs 0561 // were actually created. 0562 totalValidThumbs = skipped + validThumbnails; 0563 } 0564 0565 if (validThumbnails > 0) { 0566 break; 0567 } 0568 0569 if (skipped == 0) { 0570 break; // No valid items were found 0571 } 0572 0573 // Calculate number of (partial) pages for all valid items in the directory 0574 auto skippedPages = (skipped + visibleCount - 1) / visibleCount; 0575 0576 // The sequence is continously repeated after all valid items, calculate remainder 0577 skipValidItems = (((int)sequenceIndex()) % skippedPages) * visibleCount; 0578 } 0579 0580 p.end(); 0581 0582 if (totalValidThumbs >= 0) { 0583 // We only know this once we've iterated over the entire directory, so this will only be 0584 // set for large enough sequence indices. 0585 const int wraparoundPoint = (totalValidThumbs - 1) / visibleCount + 1; 0586 setMetaData("sequenceIndexWraparoundPoint", QString().setNum(wraparoundPoint)); 0587 } 0588 setMetaData("handlesSequences", QStringLiteral("1")); 0589 0590 if (validThumbnails == 0) { 0591 // Eventually propagate the contained items from a sub-directory 0592 QDirIterator dir(directory, QDir::Dirs); 0593 int max = 50; 0594 while (dir.hasNext() && max > 0) { 0595 --max; 0596 dir.next(); 0597 if (m_propagationDirectories.contains(dir.fileName())) { 0598 return thumbForDirectory(dir.filePath()); 0599 } 0600 } 0601 0602 // If no thumbnail could be found, return an empty image which indicates 0603 // that no preview for the directory is available. 0604 img = QImage(); 0605 } 0606 0607 // If only for one file a thumbnail could be generated then paint an image with only one tile 0608 if (validThumbnails == 1) { 0609 QImage oneTileImg(folder.size(), QImage::Format_ARGB32); 0610 oneTileImg.setDevicePixelRatio(m_devicePixelRatio); 0611 oneTileImg.fill(0); 0612 0613 QPainter oneTilePainter(&oneTileImg); 0614 oneTilePainter.setCompositionMode(QPainter::CompositionMode_Source); 0615 oneTilePainter.drawPixmap(0, 0, folder); 0616 oneTilePainter.setCompositionMode(QPainter::CompositionMode_SourceOver); 0617 0618 const int oneTileWidth = folderWidth - leftMargin - rightMargin; 0619 const int oneTileHeight = folderHeight - topMargin - bottomMargin; 0620 0621 if (firstThumbnail.width() < oneTileWidth && firstThumbnail.height() < oneTileHeight) { 0622 createSubThumbnail(firstThumbnail, hadFirstThumbnail, oneTileWidth, oneTileHeight); 0623 } 0624 drawSubThumbnail(oneTilePainter, firstThumbnail, oneTileWidth, oneTileHeight, leftMargin, topMargin, borderStrokeWidth); 0625 return oneTileImg; 0626 } 0627 0628 return img; 0629 } 0630 0631 ThumbCreatorWithMetadata *ThumbnailProtocol::getThumbCreator(const QString &plugin) 0632 { 0633 auto it = m_creators.constFind(plugin); 0634 if (it != m_creators.constEnd()) { 0635 return *it; 0636 } 0637 0638 const KPluginMetaData md(plugin); 0639 const KPluginFactory::Result result = KPluginFactory::instantiatePlugin<KIO::ThumbnailCreator>(md); 0640 0641 if (result) { 0642 auto creator = new ThumbCreatorWithMetadata{ 0643 std::unique_ptr<ThumbnailCreator>(result.plugin), 0644 md.value("CacheThumbnail", true), 0645 true, // KIO::ThumbnailCreator are always dpr-aware 0646 md.value("HandleSequences", false), 0647 }; 0648 0649 m_creators.insert(plugin, creator); 0650 return creator; 0651 } 0652 0653 return nullptr; 0654 } 0655 0656 void ThumbnailProtocol::ensureDirsCreated() 0657 { 0658 if (m_thumbBasePath.isEmpty()) { 0659 m_thumbBasePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/"); 0660 QDir basePath(m_thumbBasePath); 0661 basePath.mkpath("normal/"); 0662 QFile::setPermissions(basePath.absoluteFilePath("normal"), QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); 0663 basePath.mkpath("large/"); 0664 QFile::setPermissions(basePath.absoluteFilePath("large"), QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); 0665 if (m_devicePixelRatio > 1) { 0666 basePath.mkpath("x-large/"); 0667 QFile::setPermissions(basePath.absoluteFilePath("x-large"), QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); 0668 basePath.mkpath("xx-large/"); 0669 QFile::setPermissions(basePath.absoluteFilePath("xx-large"), QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); 0670 } 0671 } 0672 } 0673 0674 bool ThumbnailProtocol::createSubThumbnail(QImage &thumbnail, const QString &filePath, int segmentWidth, int segmentHeight) 0675 { 0676 auto getSubCreator = [&filePath, this]() -> ThumbCreatorWithMetadata * { 0677 const QMimeDatabase db; 0678 const KPluginMetaData subPlugin = pluginForMimeType(db.mimeTypeForFile(filePath).name()); 0679 if (!subPlugin.isValid() || !m_enabledPlugins.contains(subPlugin.pluginId())) { 0680 return nullptr; 0681 } 0682 return getThumbCreator(subPlugin.fileName()); 0683 }; 0684 0685 const auto maxDimension = qMin(1024.0, 512.0 * m_devicePixelRatio); 0686 if ((segmentWidth <= maxDimension) && (segmentHeight <= maxDimension)) { 0687 // check whether a cached version of the file is available for 0688 // 128 x 128, 256 x 256 pixels or 512 x 512 pixels taking into account devicePixelRatio 0689 int cacheSize = 0; 0690 QCryptographicHash md5(QCryptographicHash::Md5); 0691 const QByteArray fileUrl = QUrl::fromLocalFile(filePath).toEncoded(); 0692 md5.addData(fileUrl); 0693 const QString thumbName = QString::fromLatin1(md5.result().toHex()).append(".png"); 0694 0695 ensureDirsCreated(); 0696 0697 struct CachePool { 0698 QString path; 0699 int minSize; 0700 }; 0701 0702 static const auto pools = { 0703 CachePool{QStringLiteral("normal/"), 128}, 0704 CachePool{QStringLiteral("large/"), 256}, 0705 CachePool{QStringLiteral("x-large/"), 512}, 0706 CachePool{QStringLiteral("xx-large/"), 1024}, 0707 }; 0708 0709 const int wants = std::max(segmentWidth, segmentHeight); 0710 for (const auto &pool : pools) { 0711 if (pool.minSize < wants) { 0712 continue; 0713 } else if (cacheSize == 0) { 0714 // the lowest cache size the thumbnail could be at 0715 cacheSize = pool.minSize; 0716 } 0717 // try in folders with higher image quality as well 0718 if (thumbnail.load(m_thumbBasePath + pool.path + thumbName, "png")) { 0719 thumbnail.setDevicePixelRatio(m_devicePixelRatio); 0720 break; 0721 } 0722 } 0723 0724 // no cached version is available, a new thumbnail must be created 0725 if (thumbnail.isNull()) { 0726 ThumbCreatorWithMetadata *subCreator = getSubCreator(); 0727 if (subCreator && createThumbnail(subCreator, filePath, cacheSize, cacheSize, thumbnail)) { 0728 scaleDownImage(thumbnail, cacheSize, cacheSize); 0729 0730 // The thumbnail has been created successfully. Check if we can store 0731 // the thumbnail to the cache for future access. 0732 if (subCreator->cacheThumbnail && metaData("cache").toInt() && !thumbnail.isNull()) { 0733 QString thumbPath; 0734 const int wants = std::max(thumbnail.width(), thumbnail.height()); 0735 for (const auto &pool : pools) { 0736 if (pool.minSize < wants) { 0737 continue; 0738 } else if (thumbPath.isEmpty()) { 0739 // that's the appropriate path for this thumbnail 0740 thumbPath = m_thumbBasePath + pool.path; 0741 } 0742 } 0743 0744 // The thumbnail has been created successfully. Store the thumbnail 0745 // to the cache for future access. 0746 QSaveFile thumbnailfile(QDir(thumbPath).absoluteFilePath(thumbName)); 0747 if (thumbnailfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { 0748 QFileInfo fi(filePath); 0749 thumbnail.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(fileUrl)); 0750 thumbnail.setText(QStringLiteral("Thumb::MTime"), QString::number(fi.lastModified().toSecsSinceEpoch())); 0751 thumbnail.setText(QStringLiteral("Thumb::Size"), QString::number(fi.size())); 0752 0753 if (thumbnail.save(&thumbnailfile, "png")) { 0754 thumbnailfile.commit(); 0755 } 0756 } 0757 } 0758 } 0759 } 0760 0761 if (thumbnail.isNull()) { 0762 return false; 0763 } 0764 0765 } else { 0766 // image requested is too big to be stored in the cache 0767 // create an image on demand 0768 ThumbCreatorWithMetadata *subCreator = getSubCreator(); 0769 if (!subCreator || !createThumbnail(subCreator, filePath, segmentWidth, segmentHeight, thumbnail)) { 0770 return false; 0771 } 0772 } 0773 0774 // Make sure the image fits in the segments 0775 // Some thumbnail creators do not respect the width / height parameters 0776 scaleDownImage(thumbnail, segmentWidth, segmentHeight); 0777 return true; 0778 } 0779 0780 bool ThumbnailProtocol::createThumbnail(ThumbCreatorWithMetadata *thumbCreator, const QString &filePath, int width, int height, QImage &thumbnail) 0781 { 0782 bool success = false; 0783 0784 auto result = thumbCreator->creator->create( 0785 KIO::ThumbnailRequest(QUrl::fromLocalFile(filePath), QSize(width, height), m_mimeType, m_devicePixelRatio, sequenceIndex())); 0786 0787 success = result.isValid(); 0788 thumbnail = result.image(); 0789 m_sequenceIndexWrapAroundPoint = result.sequenceIndexWraparoundPoint(); 0790 0791 if (!success) { 0792 return false; 0793 } 0794 0795 // make sure the image is not bigger than the expected size 0796 scaleDownImage(thumbnail, width, height); 0797 0798 thumbnail.setDevicePixelRatio(m_devicePixelRatio); 0799 convertToStandardRgb(thumbnail); 0800 0801 return true; 0802 } 0803 0804 void ThumbnailProtocol::drawSubThumbnail(QPainter &p, QImage subThumbnail, int width, int height, int xPos, int yPos, int borderStrokeWidth) 0805 { 0806 scaleDownImage(subThumbnail, width, height); 0807 0808 // center the image inside the segment boundaries 0809 const QPoint centerPos((xPos + width / 2) / m_devicePixelRatio, (yPos + height / 2) / m_devicePixelRatio); 0810 const int rotationAngle = m_randomGenerator.bounded(-8, 9); // Random rotation ±8° 0811 drawPictureFrame(&p, centerPos, subThumbnail, borderStrokeWidth, QSize(width, height), rotationAngle); 0812 } 0813 0814 #include "thumbnail.moc"