thumbnail.cpp 31.4 KB
Newer Older
Malte Starostik's avatar
Malte Starostik committed
1
/*  This file is part of the KDE libraries
2
3
    SPDX-FileCopyrightText: 2000 Malte Starostik <malte@kde.org>
    SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
Malte Starostik's avatar
Malte Starostik committed
4

5
    SPDX-License-Identifier: LGPL-2.0-or-later
Malte Starostik's avatar
Malte Starostik committed
6
7
*/

8
#include "thumbnail.h"
9
#include "thumbnail-logsettings.h"
10

Malte Starostik's avatar
Malte Starostik committed
11
#include <stdlib.h>
Carsten Pfeiffer's avatar
Carsten Pfeiffer committed
12
#ifdef __FreeBSD__
13
#include <machine/param.h>
Carsten Pfeiffer's avatar
Carsten Pfeiffer committed
14
#endif
Carsten Pfeiffer's avatar
Carsten Pfeiffer committed
15
#include <sys/types.h>
16
#ifndef Q_OS_WIN
Carsten Pfeiffer's avatar
Carsten Pfeiffer committed
17
#include <sys/ipc.h>
18
#include <sys/shm.h>
Kevin Funk's avatar
Kevin Funk committed
19
#include <unistd.h> // nice()
20
#endif
Malte Starostik's avatar
Malte Starostik committed
21

22
#include <QApplication>
23
24
25
26
27
28
#include <QBuffer>
#include <QFile>
#include <QSaveFile>
#include <QBitmap>
#include <QCryptographicHash>
#include <QImage>
29
#include <QIcon>
30
31
#include <QPainter>
#include <QPixmap>
David Faure's avatar
David Faure committed
32
#include <QUrl>
33
34
35
36
#include <QMimeType>
#include <QMimeDatabase>
#include <QLibrary>
#include <QDebug>
37
#include <QRandomGenerator>
38

39
#include <KFileItem>
40
41
42
43
44
#include <KLocalizedString>
#include <KSharedConfig>
#include <KConfigGroup>
#include <KMimeTypeTrader>
#include <KServiceTypeTrader>
45
#include <KPluginLoader>
46

47
#include <kio/previewjob.h>
48
#include <kio/thumbcreator.h>
49
#include <kio/thumbdevicepixelratiodependentcreator.h>
50
#include <kio/thumbsequencecreator.h>
51
#include <kio/previewjob.h>
52
#include <kio_version.h>
Malte Starostik's avatar
Malte Starostik committed
53

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
54
#include <limits>
55
#include <QDirIterator>
56

57
58
59
60
// Fix thumbnail: protocol
#define THUMBNAIL_HACK (1)

#ifdef THUMBNAIL_HACK
David Johnson's avatar
David Johnson committed
61
# include <QFileInfo>
62
63
#endif

64
65
#include "imagefilter.h"

66
67
// Recognized metadata entries:
// mimeType     - the mime type of the file, used for the overlay icon if any
68
69
// width        - maximum width for the thumbnail
// height       - maximum height for the thumbnail
70
71
// iconSize     - the size of the overlay icon to use if any (deprecated, ignored)
// iconAlpha    - the transparency value used for icon overlays (deprecated, ignored)
72
// plugin       - the name of the plugin library to be used for thumbnail creation.
73
74
//                Provided by the application to save an addition KTrader
//                query here.
75
76
// devicePixelRatio - the devicePixelRatio to use for the output,
//                     the dimensions of the output is multiplied by it and output pixmap will have devicePixelRatio
77
78
79
80
// enabledPlugins - a list of enabled thumbnailer plugins. PreviewJob does not call
//                  this thumbnail slave when a given plugin isn't enabled. However,
//                  for directory thumbnails it doesn't know that the thumbnailer
//                  internally also loads the plugins.
81
82
// shmid        - the shared memory segment id to write the image's data to.
//                The segment is assumed to provide enough space for a 32-bit
83
//                image sized width x height pixels.
84
85
86
87
88
//                If this is given, the data returned by the slave will be:
//                    int width
//                    int height
//                    int depth
//                Otherwise, the data returned is the image in PNG format.
89

Malte Starostik's avatar
Malte Starostik committed
90
91
using namespace KIO;

92
93
94
95
96
97
98
// Pseudo plugin class to embed meta data
class KIOPluginForMetaData : public QObject
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.kde.kio.slave.thumbnail" FILE "thumbnail.json")
};

Marco Martin's avatar
Marco Martin committed
99
extern "C" Q_DECL_EXPORT int kdemain( int argc, char **argv )
Malte Starostik's avatar
Malte Starostik committed
100
{
101
#ifdef HAVE_NICE
Malte Starostik's avatar
Malte Starostik committed
102
    nice( 5 );
103
104
#endif

105
106
    QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);

107
    // creating KApplication in a slave in not a very good idea,
108
109
110
    // as dispatchLoop() doesn't allow it to process its messages,
    // so it for example wouldn't reply to ksmserver - on the other
    // hand, this slave uses QPixmaps for some reason, and they
111
    // need QGuiApplication
112
113
    putenv(strdup("SESSION_MANAGER="));

114
    // Some thumbnail plugins use QWidget classes for the rendering,
115
    // so use QApplication here, not just QGuiApplication
116
    QApplication app(argc, argv);
117

118
    if (argc != 4) {
119
        qCritical() << "Usage: kio_thumbnail protocol domain-socket1 domain-socket2";
Malte Starostik's avatar
Malte Starostik committed
120
121
122
123
124
125
126
127
128
        exit(-1);
    }

    ThumbnailProtocol slave(argv[2], argv[3]);
    slave.dispatchLoop();

    return 0;
}

Marco Martin's avatar
Marco Martin committed
129

Laurent Montel's avatar
Laurent Montel committed
130
ThumbnailProtocol::ThumbnailProtocol(const QByteArray &pool, const QByteArray &app)
131
    : SlaveBase("thumbnail", pool, app),
132
133
134
      m_width(0),
      m_height(0),
      m_devicePixelRatio(1),
135
136
      m_maxFileSize(0),
      m_randomGenerator()
137
{}
Malte Starostik's avatar
Malte Starostik committed
138
139
140

ThumbnailProtocol::~ThumbnailProtocol()
{
Méven Car's avatar
Méven Car committed
141
    qDeleteAll(m_creators);
142
    m_creators.clear();
Malte Starostik's avatar
Malte Starostik committed
143
144
}

145
146
147
148
149
150
151
152
153
154
155
156
/**
 * Scales down the image \p img in a way that it fits into the given maximum width and height
 */
void scaleDownImage(QImage& img, int maxWidth, int maxHeight)
{
    if (img.width() > maxWidth || img.height() > maxHeight) {
        img = img.scaled(maxWidth,
                         maxHeight,
                         Qt::KeepAspectRatio, Qt::SmoothTransformation);
    }
}

Marco Martin's avatar
Marco Martin committed
157
void ThumbnailProtocol::get(const QUrl &url)
Malte Starostik's avatar
Malte Starostik committed
158
159
{
    m_mimeType = metaData("mimeType");
160
    m_enabledPlugins = metaData("enabledPlugins").split(QLatin1Char(','), Qt::SkipEmptyParts);
161
162
163
164
    if (m_enabledPlugins.isEmpty()) {
        const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
        m_enabledPlugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins());
    }
165
166
167
168
169
170

    Q_ASSERT(url.scheme() == "thumbnail");
    QFileInfo info(url.path());
    Q_ASSERT(info.isAbsolute());

    if (!info.exists()) {
171
172
173
        // The file does not exist
        error(KIO::ERR_DOES_NOT_EXIST, url.path());
        return;
174
    } else if (!info.isReadable()) {
175
176
177
        // The file is not readable!
        error(KIO::ERR_CANNOT_READ, url.path());
        return;
178
179
    }

180
    //qDebug() << "Wanting MIME Type:" << m_mimeType;
181
#ifdef THUMBNAIL_HACK
182
    // ### HACK
183
    bool direct=false;
184
    if (m_mimeType.isEmpty()) {
185
        //qDebug() << "PATH: " << url.path() << "isDir:" << info.isDir();
186
        if (info.isDir()) {
187
            m_mimeType = "inode/directory";
188
        } else {
189
190
            const QMimeDatabase db;

191
            m_mimeType = db.mimeTypeForFile(info).name();
192
        }
193

194
        //qDebug() << "Guessing MIME Type:" << m_mimeType;
195
        direct=true; // thumbnail: URL was probably typed in Konqueror
196
197
198
    }
#endif

199
    if (m_mimeType.isEmpty()) {
200
        error(KIO::ERR_INTERNAL, i18n("No MIME Type specified."));
Malte Starostik's avatar
Malte Starostik committed
201
202
        return;
    }
203
204
205

    m_width = metaData("width").toInt();
    m_height = metaData("height").toInt();
206

207
    if (m_width < 0 || m_height < 0) {
208
        error(KIO::ERR_INTERNAL, i18n("No or invalid size specified."));
209
210
        return;
    }
211
#ifdef THUMBNAIL_HACK
212
    else if (!m_width || !m_height) {
213
        //qDebug() << "Guessing height, width, icon size!";
214
215
        m_width = 128;
        m_height = 128;
216
217
    }
#endif
218
219
220
221
222
223
224
225
    bool ok;
    m_devicePixelRatio = metaData("devicePixelRatio").toInt(&ok);
    if (!ok || m_devicePixelRatio == 0) {
        m_devicePixelRatio = 1;
    } else {
        m_width *= m_devicePixelRatio;
        m_height *= m_devicePixelRatio;
    }
226

227
    QImage img;
Méven Car's avatar
Méven Car committed
228
229
230
    QString plugin = metaData("plugin");
    if ((plugin.isEmpty() || plugin == "directorythumbnail") && m_mimeType == "inode/directory") {
        img = thumbForDirectory(info.canonicalFilePath());
231
        if (img.isNull()) {
Méven Car's avatar
Méven Car committed
232
233
234
235
            error(KIO::ERR_INTERNAL, i18n("Cannot create thumbnail for directory"));
            return;
        }
    } else {
236
#ifdef THUMBNAIL_HACK
Méven Car's avatar
Méven Car committed
237
238
239
        if (plugin.isEmpty()) {
            plugin = pluginForMimeType(m_mimeType);
        }
240

Méven Car's avatar
Méven Car committed
241
        //qDebug() << "Guess plugin: " << plugin;
242
#endif
Méven Car's avatar
Méven Car committed
243
244
245
246
247
        if (plugin.isEmpty()) {
            error(KIO::ERR_INTERNAL, i18n("No plugin specified."));
            return;
        }

248
249
        ThumbCreatorWithMetadata* creator = getThumbCreator(plugin);
        if (!creator) {
250
            error(KIO::ERR_INTERNAL, i18n("Cannot load ThumbCreator %1", plugin));
Méven Car's avatar
Méven Car committed
251
252
253
            return;
        }

254
255
256
257
        if (creator->handleSequences) {
            ThumbSequenceCreator* sequenceCreator = dynamic_cast<ThumbSequenceCreator*>(creator->creator);
            if (sequenceCreator) {
                sequenceCreator->setSequenceIndex(sequenceIndex());
Méven Car's avatar
Méven Car committed
258

259
260
                setMetaData("handlesSequences", QStringLiteral("1"));
            }
Méven Car's avatar
Méven Car committed
261
262
        }

263
        if (!createThumbnail(creator, info.canonicalFilePath(), m_width, m_height, img)) {
Méven Car's avatar
Méven Car committed
264
265
266
267
            error(KIO::ERR_INTERNAL, i18n("Cannot create thumbnail for %1", info.canonicalFilePath()));
            return;
        }

268
269
        if (creator->handleSequences) {
            ThumbSequenceCreator* sequenceCreator = dynamic_cast<ThumbSequenceCreator*>(creator->creator);
Méven Car's avatar
Méven Car committed
270
271
272
            // We MUST do this after calling create(), because the create() call itself might change it.
            const float wp = sequenceCreator->sequenceIndexWraparoundPoint();
            setMetaData("sequenceIndexWraparoundPoint", QString().setNum(wp));
273
        }
Malte Starostik's avatar
Malte Starostik committed
274
    }
Carsten Pfeiffer's avatar
Carsten Pfeiffer committed
275

276
    scaleDownImage(img, m_width, m_height);
Malte Starostik's avatar
Malte Starostik committed
277

278
    if (img.isNull()) {
279
280
281
282
283
        error(KIO::ERR_INTERNAL, i18n("Failed to create a thumbnail."));
        return;
    }

    const QString shmid = metaData("shmid");
284
    if (shmid.isEmpty()) {
285
#ifdef THUMBNAIL_HACK
286
        if (direct) {
287
            // If thumbnail was called directly from Konqueror, then the image needs to be raw
Sebastian Kügler's avatar
Sebastian Kügler committed
288
            //qDebug() << "RAW IMAGE TO STREAM";
289
            QBuffer buf;
290
            if (!buf.open(QIODevice::WriteOnly)) {
291
292
293
294
295
                error(KIO::ERR_INTERNAL, i18n("Could not write image."));
                return;
            }
            img.save(&buf,"PNG");
            buf.close();
296
            mimeType("image/png");
297
            data(buf.buffer());
298
        }
299
        else
300
#endif
301
302
        {
            QByteArray imgData;
303
            QDataStream stream( &imgData, QIODevice::WriteOnly );
Sebastian Kügler's avatar
Sebastian Kügler committed
304
            //qDebug() << "IMAGE TO STREAM";
305
            stream << img;
306
            mimeType("application/octet-stream");
307
308
            data(imgData);
        }
309
    } else {
310
#ifndef Q_OS_WIN
311
        QByteArray imgData;
312
        QDataStream stream( &imgData, QIODevice::WriteOnly );
Sebastian Kügler's avatar
Sebastian Kügler committed
313
        //qDebug() << "IMAGE TO SHMID";
Kevin Funk's avatar
Kevin Funk committed
314
        void *shmaddr = shmat(shmid.toInt(), nullptr, 0);
315
        if (shmaddr == (void *)-1) {
316
            error(KIO::ERR_INTERNAL, i18n("Failed to attach to shared memory segment %1", shmid));
317
318
            return;
        }
319
320
321
322
        if( img.format() != QImage::Format_ARGB32 ) { // KIO::PreviewJob and this code below completely ignores colortable :-/,
            img = img.convertToFormat(QImage::Format_ARGB32); //  so make sure there is none
        }
        struct shmid_ds shmStat;
323
        if (shmctl(shmid.toInt(), IPC_STAT, &shmStat) == -1 || shmStat.shm_segsz < (uint)img.sizeInBytes()) {
324
            error(KIO::ERR_INTERNAL, i18n("Image is too big for the shared memory segment"));
325
326
327
            shmdt((char*)shmaddr);
            return;
        }
328
329
330
        // Keep in sync with kio/src/previewjob.cpp
        const quint8 format = img.format() | 0x80;
        stream << img.width() << img.height() << format << ((int)img.devicePixelRatio());
331
        memcpy(shmaddr, img.bits(), img.sizeInBytes());
332
        shmdt((char*)shmaddr);
333
        mimeType("application/octet-stream");
334
        data(imgData);
335
#endif
336
    }
Malte Starostik's avatar
Malte Starostik committed
337
338
339
    finished();
}

340
QString ThumbnailProtocol::pluginForMimeType(const QString& mimeType) {
341
342
    KService::List offers = KMimeTypeTrader::self()->query( mimeType, QLatin1String("ThumbCreator"));
    if (!offers.isEmpty()) {
343
344
345
346
        KService::Ptr serv;
        serv = offers.first();
        return serv->library();
    }
347

348
349
350
    //Match group mimetypes
    ///@todo Move this into some central location together with the related matching code in previewjob.cpp. This doesn't handle inheritance and such
    const KService::List plugins = KServiceTypeTrader::self()->query("ThumbCreator");
351
    for (const KService::Ptr& plugin : plugins) {
David Nolden's avatar
David Nolden committed
352
        const QStringList mimeTypes = plugin->serviceTypes();
353
        for (const QString& mime : mimeTypes) {
David Nolden's avatar
David Nolden committed
354
            if(mime.endsWith('*')) {
355
356
                const auto mimeGroup = mime.leftRef(mime.length()-1);
                if(mimeType.startsWith(mimeGroup))
David Nolden's avatar
David Nolden committed
357
358
                    return plugin->library();
            }
359
360
361
        }
    }

362
363
364
    return QString();
}

365
366
367
368
float ThumbnailProtocol::sequenceIndex() const {
    return metaData("sequence-index").toFloat();
}

369
370
371
372
373
374
375
376
377
bool ThumbnailProtocol::isOpaque(const QImage &image) const
{
    // Test the corner pixels
    return qAlpha(image.pixel(QPoint(0, 0))) == 255 &&
           qAlpha(image.pixel(QPoint(image.width()-1, 0))) == 255 &&
           qAlpha(image.pixel(QPoint(0, image.height()-1))) == 255 &&
           qAlpha(image.pixel(QPoint(image.width()-1, image.height()-1))) == 255;
}

378
void ThumbnailProtocol::drawPictureFrame(QPainter *painter, const QPoint &centerPos,
379
        const QImage &image, int borderStrokeWidth, QSize imageTargetSize, int rotationAngle) const
380
{
381
    // Scale the image down so it matches the aspect ratio
382
    float scaling = 1.0;
383

384
385
386
387
388
389
390
391
392
    const bool landscapeDimension = image.width() > image.height();
    const bool hasTargetSizeWidth = imageTargetSize.width() != 0;
    const bool hasTargetSizeHeight = imageTargetSize.height() != 0;
    const int widthWithFrames = image.width() + (2 * borderStrokeWidth);
    const int heightWithFrames = image.height() + (2 * borderStrokeWidth);
    if (landscapeDimension && (widthWithFrames > imageTargetSize.width()) && hasTargetSizeWidth) {
        scaling = float(imageTargetSize.width()) / float(widthWithFrames);
    } else if ((heightWithFrames > imageTargetSize.height()) && hasTargetSizeHeight) {
        scaling = float(imageTargetSize.height()) / float(heightWithFrames);
393
394
    }

395
    const float scaledFrameWidth = borderStrokeWidth / scaling;
396

397
    QTransform m;
398
    m.rotate(rotationAngle);
399
    m.scale(scaling, scaling);
400

401
402
    const QRectF frameRect(QPointF(0, 0), QPointF(image.width() / image.devicePixelRatio() + scaledFrameWidth*2,
                                                  image.height() / image.devicePixelRatio() + scaledFrameWidth*2));
403

404
    QRect r = m.mapRect(QRectF(frameRect)).toAlignedRect();
405

406
    QImage transformed(r.size(), QImage::Format_ARGB32);
407
    transformed.fill(0);
408
    QPainter p(&transformed);
409
    p.setRenderHint(QPainter::SmoothPixmapTransform);
410
    p.setRenderHint(QPainter::Antialiasing);
411
    p.setCompositionMode(QPainter::CompositionMode_Source);
412

413
414
    p.translate(-r.topLeft());
    p.setWorldTransform(m, true);
415

416
417
418
419
420
    if (isOpaque(image)) {
        p.setPen(Qt::NoPen);
        p.setBrush(Qt::white);
        p.drawRoundedRect(frameRect, scaledFrameWidth / 2, scaledFrameWidth / 2);
    }
421
    p.drawImage(scaledFrameWidth, scaledFrameWidth, image);
422
423
    p.end();

424
    int radius = qMax(borderStrokeWidth, 1);
425

426
    QImage shadow(r.size() + QSize(radius * 2, radius * 2), QImage::Format_ARGB32);
427
428
429
430
431
432
433
    shadow.fill(0);

    p.begin(&shadow);
    p.setCompositionMode(QPainter::CompositionMode_Source);
    p.drawImage(radius, radius, transformed);
    p.end();

434
    ImageFilter::shadowBlur(shadow, radius, QColor(0, 0, 0, 128));
435
436
437
438
439
440
441

    r.moveCenter(centerPos);

    painter->drawImage(r.topLeft() - QPoint(radius / 2, radius / 2), shadow);
    painter->drawImage(r.topLeft(), transformed);
}

442
QImage ThumbnailProtocol::thumbForDirectory(const QString& directory)
443
444
{
    QImage img;
445
446
    if (m_propagationDirectories.isEmpty()) {
        // Directories that the directory preview will be propagated into if there is no direct sub-directories
447
        const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
448
449
        const QStringList propagationDirectoriesList = globalConfig.readEntry("PropagationDirectories", QStringList() << "VIDEO_TS");
        m_propagationDirectories = QSet<QString>(propagationDirectoriesList.begin(), propagationDirectoriesList.end());
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
450
        m_maxFileSize = globalConfig.readEntry("MaximumSize", std::numeric_limits<qint64>::max());
451
    }
452

453
    const int tiles = 2; //Count of items shown on each dimension
454
    const int spacing = 1 * m_devicePixelRatio;
455
    const int visibleCount = tiles * tiles;
456
457
458
459

    // TODO: the margins are optimized for the Oxygen iconset
    // Provide a fallback solution for other iconsets (e. g. draw folder
    // only as small overlay, use no margins)
460

461
    KFileItem item(QUrl::fromLocalFile(directory));
462
463
    const int extent = qMin(m_width, m_height);
    QPixmap folder = QIcon::fromTheme(item.iconName()).pixmap(extent);
464
    folder.setDevicePixelRatio(m_devicePixelRatio);
465
466
467
468
469
470

    // Scale up base icon to ensure overlays are rendered with
    // the best quality possible even for low-res custom folder icons
    if (qMax(folder.width(), folder.height()) < extent) {
        folder = folder.scaled(extent, extent, Qt::KeepAspectRatio, Qt::SmoothTransformation);
    }
471

472
473
474
475
476
477
    const int folderWidth  = folder.width();
    const int folderHeight = folder.height();

    const int topMargin = folderHeight * 30 / 100;
    const int bottomMargin = folderHeight / 6;
    const int leftMargin = folderWidth / 13;
478
    const int rightMargin = leftMargin;
479
480
481
    // the picture border stroke width 1/170 rounded up
    // (i.e for each 170px the folder width increases those border increase by 1 px)
    const int borderStrokeWidth = qRound(folderWidth / 170.);
482

483
484
    const int segmentWidth  = (folderWidth  - leftMargin - rightMargin  + spacing) / tiles - spacing;
    const int segmentHeight = (folderHeight - topMargin  - bottomMargin + spacing) / tiles - spacing;
485
    if ((segmentWidth < 5 * m_devicePixelRatio) || (segmentHeight < 5 * m_devicePixelRatio)) {
486
487
488
        // the segment size is too small for a useful preview
        return img;
    }
489

490
491
    // Advance to the next tile page each second
    int skipValidItems = ((int)sequenceIndex()) * visibleCount;
492

493
    img = QImage(QSize(folderWidth, folderHeight), QImage::Format_ARGB32);
494
    img.setDevicePixelRatio(m_devicePixelRatio);
495
    img.fill(0);
496

497
498
499
    QPainter p;
    p.begin(&img);

500
    p.setCompositionMode(QPainter::CompositionMode_Source);
501
    p.drawPixmap(0, 0, folder);
502
    p.setCompositionMode(QPainter::CompositionMode_SourceOver);
503

504
505
    int xPos = leftMargin;
    int yPos = topMargin;
506

507
    int iterations = 0;
508
    QString hadFirstThumbnail;
509
    QImage firstThumbnail;
510

511
    int validThumbnails = 0;
512
    int totalValidThumbs = -1;
513

514
    while (true) {
515
        QDirIterator dir(directory, QDir::Files | QDir::Readable);
516
        int skipped = 0;
517

518
519
        // Seed the random number generator so that it always returns the same result
        // for the same directory and sequence-item
520
        m_randomGenerator.seed(qHash(directory) + skipValidItems);
Stefan Brüns's avatar
Stefan Brüns committed
521
        while (dir.hasNext()) {
522
523
524
525
526
            ++iterations;
            if (iterations > 500) {
                skipValidItems = skipped = 0;
                break;
            }
527

528
            dir.next();
529

530
531
532
533
534
535
536
537
            if (dir.fileInfo().isSymbolicLink()) {
                // Skip symbolic links, as these may point to e.g. network file
                // systems or other slow storage. The calling code already
                // checks for the directory itself, and if it is fine any
                // contained plain file is fine as well.
                continue;
            }

538
539
            auto fileSize = dir.fileInfo().size();
            if ((fileSize == 0) || (fileSize > m_maxFileSize)) {
540
                // don't create thumbnails for files that exceed
541
                // the maximum set file size or are empty
542
543
544
                continue;
            }

545
546
            QImage subThumbnail;
            if (!createSubThumbnail(subThumbnail, dir.filePath(), segmentWidth, segmentHeight)) {
547
548
                continue;
            }
549

550
551
552
553
            if (skipped < skipValidItems) {
                ++skipped;
                continue;
            }
554

555
            drawSubThumbnail(p, subThumbnail, segmentWidth, segmentHeight, xPos, yPos, borderStrokeWidth);
556

557
            if (hadFirstThumbnail.isEmpty()) {
558
                hadFirstThumbnail = dir.filePath();
559
                firstThumbnail = subThumbnail;
560
            }
561

562
            ++validThumbnails;
Stefan Brüns's avatar
Stefan Brüns committed
563
564
565
            if (validThumbnails >= visibleCount) {
                break;
            }
566

567
568
569
570
571
            xPos += segmentWidth + spacing;
            if (xPos > folderWidth - rightMargin - segmentWidth) {
                xPos = leftMargin;
                yPos += segmentHeight + spacing;
            }
572
        }
573

574
575
576
577
578
579
        if (!dir.hasNext() && totalValidThumbs < 0) {
            // We iterated over the entire directory for the first time, so now we know how many thumbs
            // were actually created.
            totalValidThumbs = skipped+validThumbnails;
        }

Stefan Brüns's avatar
Stefan Brüns committed
580
581
582
583
        if (validThumbnails > 0) {
            break;
        }

584
585
586
        if (skipped == 0) {
            break; // No valid items were found
        }
587

588
589
        // Calculate number of (partial) pages for all valid items in the directory
        auto skippedPages = (skipped + visibleCount - 1) / visibleCount;
590

591
592
        // The sequence is continously repeated after all valid items, calculate remainder
        skipValidItems = (((int)sequenceIndex()) % skippedPages) * visibleCount;
593
    }
594

595
    p.end();
596

597
598
599
600
601
602
603
604
    if (totalValidThumbs >= 0) {
        // We only know this once we've iterated over the entire directory, so this will only be
        // set for large enough sequence indices.
        const int wraparoundPoint = (totalValidThumbs-1)/visibleCount + 1;
        setMetaData("sequenceIndexWraparoundPoint", QString().setNum(wraparoundPoint));
    }
    setMetaData("handlesSequences", QStringLiteral("1"));

605
    if (validThumbnails == 0) {
606
        // Eventually propagate the contained items from a sub-directory
607
        QDirIterator dir(directory, QDir::Dirs);
608
        int max = 50;
609
        while (dir.hasNext() && max > 0) {
610
611
            --max;
            dir.next();
612
            if (m_propagationDirectories.contains(dir.fileName())) {
613
                return thumbForDirectory(dir.filePath());
614
            }
615
        }
616
617
618
619

        // If no thumbnail could be found, return an empty image which indicates
        // that no preview for the directory is available.
        img = QImage();
620
    }
621

622
    // If only for one file a thumbnail could be generated then paint an image with only one tile
623
    if (validThumbnails == 1) {
624
        QImage oneTileImg(folder.size(), QImage::Format_ARGB32);
625
        oneTileImg.setDevicePixelRatio(m_devicePixelRatio);
626
627
628
629
630
631
632
633
634
635
        oneTileImg.fill(0);

        QPainter oneTilePainter(&oneTileImg);
        oneTilePainter.setCompositionMode(QPainter::CompositionMode_Source);
        oneTilePainter.drawPixmap(0, 0, folder);
        oneTilePainter.setCompositionMode(QPainter::CompositionMode_SourceOver);

        const int oneTileWidth = folderWidth - leftMargin - rightMargin;
        const int oneTileHeight = folderHeight - topMargin - bottomMargin;

636
637
638
        if (firstThumbnail.width() < oneTileWidth && firstThumbnail.height() < oneTileHeight) {
            createSubThumbnail(firstThumbnail, hadFirstThumbnail, oneTileWidth, oneTileHeight);
        }
639
        drawSubThumbnail(oneTilePainter, firstThumbnail, oneTileWidth, oneTileHeight, leftMargin, topMargin, borderStrokeWidth);
640
641
642
        return oneTileImg;
    }

643
    return img;
644
645
}

646
ThumbCreatorWithMetadata* ThumbnailProtocol::getThumbCreator(const QString& plugin)
647
{
648
649
    auto it = m_creators.constFind(plugin);
    if (it != m_creators.constEnd()) {
650
        return *it;
651
    }
652

653
654
    // Don't use KPluginFactory here, this is not a QObject and
    // neither is ThumbCreator
655
    ThumbCreator *creator = nullptr;
656
657
    QLibrary library(KPluginLoader::findPlugin((plugin)));
    if (library.load()) {
658
659
660
661
        auto createFn = (newCreator)library.resolve("new_creator");
        if (createFn) {
            creator = createFn();
        }
662
    }
663
664
665
666
667
668
669
670
671
672

    ThumbCreatorWithMetadata *thumbCreator = nullptr;
    if (creator) {
        const KService::List plugins = KServiceTypeTrader::self()->query(QStringLiteral("ThumbCreator"), QStringLiteral("Library == '%1'").arg(plugin));
        if (plugins.size() == 0) {
            qCWarning(KIO_THUMBNAIL_LOG) << "Plugin not found:" << plugin;
        } else {
            auto service = plugins.first();

            QVariant cacheThumbnails = service->property("CacheThumbnail");
673
            QVariant devicePixelRatioDependent = service->property("DevicePixelRatioDependent");
674
675
676
677
678
            QVariant handleSequences = service->property("HandleSequences");

            thumbCreator = new ThumbCreatorWithMetadata{
                creator,
                cacheThumbnails.isValid() ? cacheThumbnails.toBool() : true,
679
                devicePixelRatioDependent.isValid() ? devicePixelRatioDependent.toBool() : false,
680
681
682
683
                handleSequences.isValid() ? handleSequences.toBool() : false
            };
        }
    } else {
684
        qCWarning(KIO_THUMBNAIL_LOG) << "Failed to load" << plugin << library.errorString();
685
686
    }

687
    m_creators.insert(plugin, thumbCreator);
688

689
    return thumbCreator;
690
691
}

692
693
694
695
696
697
698
699
700
void ThumbnailProtocol::ensureDirsCreated()
{
    if (m_thumbBasePath.isEmpty()) {
        m_thumbBasePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/");
        QDir basePath(m_thumbBasePath);
        basePath.mkpath("normal/");
        QFile::setPermissions(basePath.absoluteFilePath("normal"), QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
        basePath.mkpath("large/");
        QFile::setPermissions(basePath.absoluteFilePath("large"), QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
701
702
703
704
705
706
        if (m_devicePixelRatio > 1) {
            basePath.mkpath("x-large/");
            QFile::setPermissions(basePath.absoluteFilePath("x-large"), QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
            basePath.mkpath("xx-large/");
            QFile::setPermissions(basePath.absoluteFilePath("xx-large"), QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
        }
707
708
709
    }
}

Méven Car's avatar
Méven Car committed
710
711
bool ThumbnailProtocol::createSubThumbnail(QImage &thumbnail, const QString &filePath,
                                           int segmentWidth, int segmentHeight)
712
{
713
    auto getSubCreator = [&filePath, this]() -> ThumbCreatorWithMetadata* {
714
        const QMimeDatabase db;
715
        const QString subPlugin = pluginForMimeType(db.mimeTypeForFile(filePath).name());
716
717
718
719
720
        if (subPlugin.isEmpty() || !m_enabledPlugins.contains(subPlugin)) {
            return nullptr;
        }
        return getThumbCreator(subPlugin);
    };
721

722
723
    const auto maxDimension = qMin(1024, 512 * m_devicePixelRatio);
    if ((segmentWidth <= maxDimension) && (segmentHeight <= maxDimension)) {
724
        // check whether a cached version of the file is available for
725
        // 128 x 128, 256 x 256 pixels or 512 x 512 pixels taking into account devicePixelRatio
726
        int cacheSize = 0;
Sebastian Kügler's avatar
Sebastian Kügler committed
727
        QCryptographicHash md5(QCryptographicHash::Md5);
728
729
730
        const QByteArray fileUrl = QUrl::fromLocalFile(filePath).toEncoded();
        md5.addData(fileUrl);
        const QString thumbName = QString::fromLatin1(md5.result().toHex()).append(".png");
Sebastian Kügler's avatar
Sebastian Kügler committed
731

732
        ensureDirsCreated();
733

734
735
736
737
738
739
740
741
        struct CachePool {
            QString path;
            int minSize;
        };

        static const auto pools = {
            CachePool{QStringLiteral("normal/"), 128},
            CachePool{QStringLiteral("large/"), 256},
742
743
            CachePool{QStringLiteral("x-large/"), 512},
            CachePool{QStringLiteral("xx-large/"), 1024},
744
745
746
747
748
749
750
751
752
753
754
755
        };

        const int wants = std::max(segmentWidth, segmentHeight);
        for (const auto &pool : pools) {
            if (pool.minSize < wants) {
                continue;
            } else if (cacheSize == 0) {
                // the lowest cache size the thumbnail could be at
                cacheSize = pool.minSize;
            }
            // try in folders with higher image quality as well
            if (thumbnail.load(m_thumbBasePath + pool.path + thumbName, "png")) {
756
                thumbnail.setDevicePixelRatio(m_devicePixelRatio);
757
                break;
758
759
760
            }
        }

761
        // no cached version is available, a new thumbnail must be created
762
763
764
765
        if (thumbnail.isNull()) {
            ThumbCreatorWithMetadata* subCreator = getSubCreator();
            if (subCreator && createThumbnail(subCreator, filePath, cacheSize, cacheSize, thumbnail)) {
                scaleDownImage(thumbnail, cacheSize, cacheSize);
766

767
768
                // The thumbnail has been created successfully. Check if we can store
                // the thumbnail to the cache for future access.
769
#if KIO_VERSION >= QT_VERSION_CHECK(5, 83, 0)
770
                if (subCreator->cacheThumbnail && metaData("cache").toInt() && !thumbnail.isNull()) {
771
#else
772
                if (subCreator->cacheThumbnail && !thumbnail.isNull()) {
773
#endif
774
775
776
777
778
779
780
781
782
                    QString thumbPath;
                    const int wants = std::max(thumbnail.width(), thumbnail.height());
                    for (const auto &pool : pools) {
                        if (pool.minSize < wants) {
                            continue;
                        } else if (thumbPath.isEmpty()) {
                            // that's the appropriate path for this thumbnail
                            thumbPath = m_thumbBasePath + pool.path;
                        }
783
                    }
784

785
786
787
788
789
790
791
792
793
794
795
796
                    // The thumbnail has been created successfully. Store the thumbnail
                    // to the cache for future access.
                    QSaveFile thumbnailfile(QDir(thumbPath).absoluteFilePath(thumbName));
                    if (thumbnailfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
                        QFileInfo fi(filePath);
                        thumbnail.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(fileUrl));
                        thumbnail.setText(QStringLiteral("Thumb::MTime"), QString::number(fi.lastModified().toSecsSinceEpoch()));
                        thumbnail.setText(QStringLiteral("Thumb::Size"), QString::number(fi.size()));

                        if (thumbnail.save(&thumbnailfile, "png")) {
                            thumbnailfile.commit();
                        }
797
                    }
798
799
                }
            }
800
        }
801

802
        if (thumbnail.isNull()) {
803
            return false;
804
        }
805

806
    } else {
807
808
809
810
811
812
        // image requested is too big to be stored in the cache
        // create an image on demand
        ThumbCreatorWithMetadata* subCreator = getSubCreator();
        if (!subCreator || !createThumbnail(subCreator, filePath, segmentWidth, segmentHeight, thumbnail)) {
            return false;
        }
813
    }
814
815
816
817

    // Make sure the image fits in the segments
    // Some thumbnail creators do not respect the width / height parameters
    scaleDownImage(thumbnail, segmentWidth, segmentHeight);
818
819
820
    return true;
}

821
822
bool ThumbnailProtocol::createThumbnail(ThumbCreatorWithMetadata* thumbCreator, const QString& filePath, int width, int height, QImage& thumbnail)
{
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
    int scaleWidth = width;
    int scaledHeight = height;

    if (thumbCreator->devicePixelRatioDependent) {
        KIO::ThumbDevicePixelRatioDependentCreator *dprDependentCreator =
                static_cast<KIO::ThumbDevicePixelRatioDependentCreator *>(
                    thumbCreator->creator);

        if (dprDependentCreator) {
            dprDependentCreator->setDevicePixelRatio(m_devicePixelRatio);
            scaleWidth /= m_devicePixelRatio;
            scaledHeight /= m_devicePixelRatio;
        }
    }

    if (thumbCreator->creator->create(filePath, scaleWidth, scaledHeight, thumbnail)) {
839
840
841
        // make sure the image is not bigger than the expected size
        scaleDownImage(thumbnail, width, height);

842
843
        thumbnail.setDevicePixelRatio(m_devicePixelRatio);

844
845
846
847
848
849
        return true;
    }

    return false;
}

850
void ThumbnailProtocol::drawSubThumbnail(QPainter& p, QImage subThumbnail, int width, int height, int xPos, int yPos, int borderStrokeWidth)
851
{
852
    scaleDownImage(subThumbnail, width, height);
853
854

    // center the image inside the segment boundaries
855
    const QPoint centerPos((xPos + width/ 2) / m_devicePixelRatio, (yPos + height / 2) / m_devicePixelRatio);
856
    const int rotationAngle = m_randomGenerator.bounded(-8, 9); // Random rotation ±8°
857
    drawPictureFrame(&p, centerPos, subThumbnail, borderStrokeWidth, QSize(width, height), rotationAngle);
858
}
859
860

#include "thumbnail.moc"