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 &centerPos,
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"