thumbnail.cpp 25.1 KB
Newer Older
Malte Starostik's avatar
Malte Starostik committed
1
/*  This file is part of the KDE libraries
2
    Copyright (C) 2000 Malte Starostik <malte@kde.org>
3
                  2000 Carsten Pfeiffer <pfeiffer@kde.org>
Malte Starostik's avatar
Malte Starostik committed
4
5
6
7
8
9
10
11
12
13
14
15
16

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to
Dirk Mueller's avatar
Dirk Mueller committed
17
    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Dirk Mueller's avatar
Dirk Mueller committed
18
    Boston, MA 02110-1301, USA.
Malte Starostik's avatar
Malte Starostik committed
19
20
*/

21
22
#include "thumbnail.h"

Malte Starostik's avatar
Malte Starostik committed
23
#include <stdlib.h>
Carsten Pfeiffer's avatar
Carsten Pfeiffer committed
24
25
26
#ifdef __FreeBSD__
    #include <machine/param.h>
#endif
Carsten Pfeiffer's avatar
Carsten Pfeiffer committed
27
#include <sys/types.h>
28
#ifndef Q_OS_WIN
Carsten Pfeiffer's avatar
Carsten Pfeiffer committed
29
#include <sys/ipc.h>
30
#include <sys/shm.h>
Kevin Funk's avatar
Kevin Funk committed
31
#include <unistd.h> // nice()
32
#endif
Malte Starostik's avatar
Malte Starostik committed
33

34
35
36
37
38
39
40
#include <QApplication>
#include <QBuffer>
#include <QFile>
#include <QSaveFile>
#include <QBitmap>
#include <QCryptographicHash>
#include <QImage>
41
#include <QIcon>
42
43
#include <QPainter>
#include <QPixmap>
David Faure's avatar
David Faure committed
44
#include <QUrl>
45
46
47
48
49
50
#include <QMimeType>
#include <QMimeDatabase>
#include <QLibrary>
#include <QTemporaryFile>
#include <QDebug>

51
#include <KFileItem>
52
53
54
55
56
#include <KLocalizedString>
#include <KSharedConfig>
#include <KConfigGroup>
#include <KMimeTypeTrader>
#include <KServiceTypeTrader>
57
#include <KPluginLoader>
58

59
#include <kaboutdata.h>
Malte Starostik's avatar
Malte Starostik committed
60

61
#include <kio/thumbcreator.h>
62
#include <kio/thumbsequencecreator.h>
63
#include <kio/previewjob.h>
Malte Starostik's avatar
Malte Starostik committed
64

65
#include <iostream>
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
66
#include <limits>
67
#include <QDirIterator>
68

69
// Use correctly KComponentData instead of KApplication (but then no QPixmap)
70
71
72
73
74
#undef USE_KINSTANCE
// Fix thumbnail: protocol
#define THUMBNAIL_HACK (1)

#ifdef THUMBNAIL_HACK
David Johnson's avatar
David Johnson committed
75
# include <QFileInfo>
76
77
#endif

78
79
#include "imagefilter.h"

80
81
// Recognized metadata entries:
// mimeType     - the mime type of the file, used for the overlay icon if any
82
83
// width        - maximum width for the thumbnail
// height       - maximum height for the thumbnail
84
85
// iconSize     - the size of the overlay icon to use if any (deprecated, ignored)
// iconAlpha    - the transparency value used for icon overlays (deprecated, ignored)
86
// plugin       - the name of the plugin library to be used for thumbnail creation.
87
88
//                Provided by the application to save an addition KTrader
//                query here.
89
90
91
92
// 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.
93
94
// 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
95
//                image sized width x height pixels.
96
97
98
99
100
//                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.
101

Malte Starostik's avatar
Malte Starostik committed
102
using namespace KIO;
Sebastian Kügler's avatar
Sebastian Kügler committed
103
//using namespace KCodecs;
Malte Starostik's avatar
Malte Starostik committed
104

Marco Martin's avatar
Marco Martin committed
105
extern "C" Q_DECL_EXPORT int kdemain( int argc, char **argv )
Malte Starostik's avatar
Malte Starostik committed
106
{
107
#ifdef HAVE_NICE
Malte Starostik's avatar
Malte Starostik committed
108
    nice( 5 );
109
110
#endif

111
#ifdef USE_KINSTANCE
112
    KComponentData componentData("kio_thumbnail");
113
#else
114
115
116

    QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);

117
    // creating KApplication in a slave in not a very good idea,
118
119
120
121
    // 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
    // need QApplication
122
    // and HTML previews need even KApplication :(
123
124
    putenv(strdup("SESSION_MANAGER="));

125
126
127
    // some thumbnail plugins reuse QWidget-tainted code for the rendering,
    // so use QApplication here, not just QGuiApplication
    QApplication app(argc, argv);
128
#endif
Malte Starostik's avatar
Malte Starostik committed
129

130

131
    if (argc != 4) {
132
        qCritical() << "Usage: kio_thumbnail protocol domain-socket1 domain-socket2";
Malte Starostik's avatar
Malte Starostik committed
133
134
135
136
137
138
139
140
141
        exit(-1);
    }

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

    return 0;
}

Marco Martin's avatar
Marco Martin committed
142

Laurent Montel's avatar
Laurent Montel committed
143
ThumbnailProtocol::ThumbnailProtocol(const QByteArray &pool, const QByteArray &app)
144
145
    : SlaveBase("thumbnail", pool, app),
      m_maxFileSize(0)
Malte Starostik's avatar
Malte Starostik committed
146
{
147

Malte Starostik's avatar
Malte Starostik committed
148
149
150
151
}

ThumbnailProtocol::~ThumbnailProtocol()
{
152
153
    qDeleteAll( m_creators );
    m_creators.clear();
Malte Starostik's avatar
Malte Starostik committed
154
155
}

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

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

    if (!info.exists()) {
	// The file does not exist
	error(KIO::ERR_DOES_NOT_EXIST, url.path());
	return;
    } else if (!info.isReadable()) {
	// The file is not readable!
	error(KIO::ERR_CANNOT_READ, url.path());
	return;
    }

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

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

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

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

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

206
    if (m_width < 0 || m_height < 0) {
207
        error(KIO::ERR_INTERNAL, i18n("No or invalid size specified."));
208
209
        return;
    }
210
#ifdef THUMBNAIL_HACK
211
    else if (!m_width || !m_height) {
212
        //qDebug() << "Guessing height, width, icon size!";
213
214
        m_width = 128;
        m_height = 128;
215
216
    }
#endif
217

218
219
    QImage img;

220
    KConfigGroup group( KSharedConfig::openConfig(), "PreviewSettings" );
221
    bool kfmiThumb = false; // TODO Figure out if we can use KFileMetadata as a last resource
222

223
    if (!kfmiThumb) {
224
        QString plugin = metaData("plugin");
225
        if ((plugin.isEmpty() || plugin == "directorythumbnail") && m_mimeType == "inode/directory") {
226
            img = thumbForDirectory(info.canonicalFilePath());
227
228
229
230
231
            if(img.isNull()) {
              error(KIO::ERR_INTERNAL, i18n("Cannot create thumbnail for directory"));
              return;
            }
        } else {
232
#ifdef THUMBNAIL_HACK
233
234
235
            if (plugin.isEmpty()) {
                plugin = pluginForMimeType(m_mimeType);
            }
236

237
            //qDebug() << "Guess plugin: " << plugin;
238
#endif
239
240
241
242
            if (plugin.isEmpty()) {
                error(KIO::ERR_INTERNAL, i18n("No plugin specified."));
                return;
            }
243

244
245
246
247
248
            ThumbCreator* creator = getThumbCreator(plugin);
            if(!creator) {
                error(KIO::ERR_INTERNAL, i18n("Cannot load ThumbCreator %1", plugin));
                return;
            }
249

250
251
252
253
            ThumbSequenceCreator* sequenceCreator = dynamic_cast<ThumbSequenceCreator*>(creator);
            if(sequenceCreator)
                sequenceCreator->setSequenceIndex(sequenceIndex());

254
255
            if (!creator->create(info.canonicalFilePath(), m_width, m_height, img)) {
                error(KIO::ERR_INTERNAL, i18n("Cannot create thumbnail for %1", info.canonicalFilePath()));
256
257
                return;
            }
258
        }
Malte Starostik's avatar
Malte Starostik committed
259
    }
Carsten Pfeiffer's avatar
Carsten Pfeiffer committed
260

261
    scaleDownImage(img, m_width, m_height);
Malte Starostik's avatar
Malte Starostik committed
262

263
    if (img.isNull()) {
264
265
266
267
268
        error(KIO::ERR_INTERNAL, i18n("Failed to create a thumbnail."));
        return;
    }

    const QString shmid = metaData("shmid");
269
    if (shmid.isEmpty()) {
270
#ifdef THUMBNAIL_HACK
271
        if (direct) {
272
            // If thumbnail was called directly from Konqueror, then the image needs to be raw
Sebastian Kügler's avatar
Sebastian Kügler committed
273
            //qDebug() << "RAW IMAGE TO STREAM";
274
            QBuffer buf;
275
            if (!buf.open(QIODevice::WriteOnly)) {
276
277
278
279
280
                error(KIO::ERR_INTERNAL, i18n("Could not write image."));
                return;
            }
            img.save(&buf,"PNG");
            buf.close();
281
            mimeType("image/png");
282
            data(buf.buffer());
283
        }
284
        else
285
#endif
286
287
        {
            QByteArray imgData;
288
            QDataStream stream( &imgData, QIODevice::WriteOnly );
Sebastian Kügler's avatar
Sebastian Kügler committed
289
            //qDebug() << "IMAGE TO STREAM";
290
            stream << img;
291
            mimeType("application/octet-stream");
292
293
            data(imgData);
        }
294
    } else {
295
#ifndef Q_OS_WIN
296
        QByteArray imgData;
297
        QDataStream stream( &imgData, QIODevice::WriteOnly );
Sebastian Kügler's avatar
Sebastian Kügler committed
298
        //qDebug() << "IMAGE TO SHMID";
Kevin Funk's avatar
Kevin Funk committed
299
        void *shmaddr = shmat(shmid.toInt(), nullptr, 0);
300
        if (shmaddr == (void *)-1) {
301
            error(KIO::ERR_INTERNAL, i18n("Failed to attach to shared memory segment %1", shmid));
302
303
            return;
        }
304
        if (img.width() * img.height() > m_width * m_height) {
305
            error(KIO::ERR_INTERNAL, i18n("Image is too big for the shared memory segment"));
306
307
308
            shmdt((char*)shmaddr);
            return;
        }
309
310
        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
311
        }
Pascal Létourneau's avatar
Pascal Létourneau committed
312
313
        // Keep in sync with kdelibs/kio/kio/previewjob.cpp
        stream << img.width() << img.height() << quint8(img.format());
314
        memcpy(shmaddr, img.bits(), img.sizeInBytes());
315
        shmdt((char*)shmaddr);
316
        mimeType("application/octet-stream");
317
        data(imgData);
318
#endif
319
    }
Malte Starostik's avatar
Malte Starostik committed
320
321
322
    finished();
}

323
QString ThumbnailProtocol::pluginForMimeType(const QString& mimeType) {
324
325
    KService::List offers = KMimeTypeTrader::self()->query( mimeType, QLatin1String("ThumbCreator"));
    if (!offers.isEmpty()) {
326
327
328
329
        KService::Ptr serv;
        serv = offers.first();
        return serv->library();
    }
330

331
332
333
    //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");
334
    for (const KService::Ptr& plugin : plugins) {
David Nolden's avatar
David Nolden committed
335
        const QStringList mimeTypes = plugin->serviceTypes();
336
        for (const QString& mime : mimeTypes) {
David Nolden's avatar
David Nolden committed
337
            if(mime.endsWith('*')) {
338
339
                const auto mimeGroup = mime.leftRef(mime.length()-1);
                if(mimeType.startsWith(mimeGroup))
David Nolden's avatar
David Nolden committed
340
341
                    return plugin->library();
            }
342
343
344
        }
    }

345
346
347
    return QString();
}

348
349
350
351
float ThumbnailProtocol::sequenceIndex() const {
    return metaData("sequence-index").toFloat();
}

352
353
354
355
356
357
358
359
360
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;
}

361
void ThumbnailProtocol::drawPictureFrame(QPainter *painter, const QPoint &centerPos,
362
                                         const QImage &image, int frameWidth, QSize imageTargetSize) const
363
{
364
    // Scale the image down so it matches the aspect ratio
365
    float scaling = 1.0;
366
367

    if ((image.size().width() > imageTargetSize.width()) && (imageTargetSize.width() != 0)) {
368
        scaling = float(imageTargetSize.width()) / float(image.size().width());
369
370
    } else if ((image.size().height() > imageTargetSize.height()) && (imageTargetSize.height() != 0)) {
        scaling = float(imageTargetSize.height()) / float(image.size().height());
371
372
    }

373
    QImage frame(imageTargetSize + QSize(frameWidth * 2, frameWidth * 2),
374
                 QImage::Format_ARGB32);
375
376
    frame.fill(0);

377
    float scaledFrameWidth = frameWidth / scaling;
378

379
    QTransform m;
380
    m.rotate(qrand() % 17 - 8); // Random rotation ±8°
381
    m.scale(scaling, scaling);
382

383
    QRectF frameRect(QPointF(0, 0), QPointF(image.width() + scaledFrameWidth*2, image.height() + scaledFrameWidth*2));
384

385
    QRect r = m.mapRect(QRectF(frameRect)).toAlignedRect();
386

387
    QImage transformed(r.size(), QImage::Format_ARGB32);
388
    transformed.fill(0);
389
    QPainter p(&transformed);
390
    p.setRenderHint(QPainter::SmoothPixmapTransform);
391
    p.setCompositionMode(QPainter::CompositionMode_Source);
392

393
394
    p.translate(-r.topLeft());
    p.setWorldTransform(m, true);
395

396
397
398
399
400
401
    if (isOpaque(image)) {
        p.setRenderHint(QPainter::Antialiasing);
        p.setPen(Qt::NoPen);
        p.setBrush(Qt::white);
        p.drawRoundedRect(frameRect, scaledFrameWidth / 2, scaledFrameWidth / 2);
    }
402
    p.drawImage(scaledFrameWidth, scaledFrameWidth, image);
403
404
405
406
    p.end();

    int radius = qMax(frameWidth, 1);

407
    QImage shadow(r.size() + QSize(radius * 2, radius * 2), QImage::Format_ARGB32);
408
409
410
411
412
413
414
    shadow.fill(0);

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

415
    ImageFilter::shadowBlur(shadow, radius, QColor(0, 0, 0, 128));
416
417
418
419
420
421
422

    r.moveCenter(centerPos);

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

423
QImage ThumbnailProtocol::thumbForDirectory(const QString& directory)
424
425
{
    QImage img;
426
427
    if (m_propagationDirectories.isEmpty()) {
        // Directories that the directory preview will be propagated into if there is no direct sub-directories
428
        const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
429
        m_propagationDirectories = globalConfig.readEntry("PropagationDirectories", QStringList() << "VIDEO_TS").toSet();
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
430
        m_maxFileSize = globalConfig.readEntry("MaximumSize", std::numeric_limits<qint64>::max());
431
    }
432

433
    const int tiles = 2; //Count of items shown on each dimension
434
    const int spacing = 1;
435
    const int visibleCount = tiles * tiles;
436
437
438
439

    // 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)
440

441
    KFileItem item(QUrl::fromLocalFile(directory));
442
443
444
445
446
447
448
449
    const int extent = qMin(m_width, m_height);
    QPixmap folder = QIcon::fromTheme(item.iconName()).pixmap(extent);

    // 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);
    }
450

451
452
453
454
455
456
    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;
457
458
    const int rightMargin = leftMargin;

459
460
    const int segmentWidth  = (folderWidth  - leftMargin - rightMargin  + spacing) / tiles - spacing;
    const int segmentHeight = (folderHeight - topMargin  - bottomMargin + spacing) / tiles - spacing;
461
462
463
464
    if ((segmentWidth < 5) || (segmentHeight <  5)) {
        // the segment size is too small for a useful preview
        return img;
    }
465

466
467
    // Advance to the next tile page each second
    int skipValidItems = ((int)sequenceIndex()) * visibleCount;
468

469
    img = QImage(QSize(folderWidth, folderHeight), QImage::Format_ARGB32);
470
    img.fill(0);
471

472
473
474
    QPainter p;
    p.begin(&img);

475
    p.setCompositionMode(QPainter::CompositionMode_Source);
476
    p.drawPixmap(0, 0, folder);
477
    p.setCompositionMode(QPainter::CompositionMode_SourceOver);
478

479
480
    int xPos = leftMargin;
    int yPos = topMargin;
481

482
483
    int frameWidth = qRound(folderWidth / 85.);

484
    int iterations = 0;
485
    QString hadFirstThumbnail;
486
    QImage firstThumbnail;
487

488
489
    int validThumbnails = 0;

490
    while (true) {
491
        QDirIterator dir(directory, QDir::Files | QDir::Readable);
492
        int skipped = 0;
493

494
495
496
497
        // Seed the random number generator so that it always returns the same result
        // for the same directory and sequence-item
        qsrand(qHash(directory) + skipValidItems);

Stefan Brüns's avatar
Stefan Brüns committed
498
        while (dir.hasNext()) {
499
500
501
502
503
            ++iterations;
            if (iterations > 500) {
                skipValidItems = skipped = 0;
                break;
            }
504

505
            dir.next();
506

507
508
            auto fileSize = dir.fileInfo().size();
            if ((fileSize == 0) || (fileSize > m_maxFileSize)) {
509
                // don't create thumbnails for files that exceed
510
                // the maximum set file size or are empty
511
512
513
                continue;
            }

514
515
            QImage subThumbnail;
            if (!createSubThumbnail(subThumbnail, dir.filePath(), segmentWidth, segmentHeight)) {
516
517
                continue;
            }
518

519
520
521
522
            if (skipped < skipValidItems) {
                ++skipped;
                continue;
            }
523

524
525
526
527
            if (!drawSubThumbnail(p, subThumbnail, segmentWidth, segmentHeight, xPos, yPos, frameWidth)) {
                continue;
            }

528
            if (hadFirstThumbnail.isEmpty()) {
529
                hadFirstThumbnail = dir.filePath();
530
                firstThumbnail = subThumbnail;
531
            }
532

533
            ++validThumbnails;
Stefan Brüns's avatar
Stefan Brüns committed
534
535
536
            if (validThumbnails >= visibleCount) {
                break;
            }
537

538
539
540
541
542
            xPos += segmentWidth + spacing;
            if (xPos > folderWidth - rightMargin - segmentWidth) {
                xPos = leftMargin;
                yPos += segmentHeight + spacing;
            }
543
        }
544

Stefan Brüns's avatar
Stefan Brüns committed
545
546
547
548
        if (validThumbnails > 0) {
            break;
        }

549
550
551
        if (skipped == 0) {
            break; // No valid items were found
        }
552

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

556
557
        // The sequence is continously repeated after all valid items, calculate remainder
        skipValidItems = (((int)sequenceIndex()) % skippedPages) * visibleCount;
558
    }
559

560
    p.end();
561

562
    if (validThumbnails == 0) {
563
        // Eventually propagate the contained items from a sub-directory
564
        QDirIterator dir(directory, QDir::Dirs);
565
        int max = 50;
566
        while (dir.hasNext() && max > 0) {
567
568
            --max;
            dir.next();
569
            if (m_propagationDirectories.contains(dir.fileName())) {
570
                return thumbForDirectory(dir.filePath());
571
            }
572
        }
573
574
575
576

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

579
    // If only for one file a thumbnail could be generated then paint an image with only one tile
580
    if (validThumbnails == 1) {
581
582
583
584
585
586
587
588
589
590
591
        QImage oneTileImg(folder.size(), QImage::Format_ARGB32);
        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;

592
593
594
595
        if (firstThumbnail.width() < oneTileWidth && firstThumbnail.height() < oneTileHeight) {
            createSubThumbnail(firstThumbnail, hadFirstThumbnail, oneTileWidth, oneTileHeight);
        }
        drawSubThumbnail(oneTilePainter, firstThumbnail, oneTileWidth, oneTileHeight, leftMargin, topMargin, frameWidth);
596
597
598
        return oneTileImg;
    }

599
    return img;
600
601
602
603
604
}

ThumbCreator* ThumbnailProtocol::getThumbCreator(const QString& plugin)
{
    ThumbCreator *creator = m_creators[plugin];
605
    if (!creator) {
606
        // Don't use KPluginFactory here, this is not a QObject and
607
        // neither is ThumbCreator
608
        QLibrary library(KPluginLoader::findPlugin((plugin)));
609
        if (library.load()) {
610
            newCreator create = (newCreator)library.resolve("new_creator");
611
            if (create) {
612
                creator = create();
613
            }
614
        }
615
        if (!creator) {
Kevin Funk's avatar
Kevin Funk committed
616
            return nullptr;
617
        }
618

619
620
621
622
623
624
        m_creators.insert(plugin, creator);
    }

    return creator;
}

625
626
627
bool ThumbnailProtocol::createSubThumbnail(QImage& thumbnail, const QString& filePath,
                                           int segmentWidth, int segmentHeight)
{
628
    auto getSubCreator = [&filePath, this]() -> ThumbCreator* {
629
        const QMimeDatabase db;
630
        const QString subPlugin = pluginForMimeType(db.mimeTypeForFile(filePath).name());
631
632
633
634
635
        if (subPlugin.isEmpty() || !m_enabledPlugins.contains(subPlugin)) {
            return nullptr;
        }
        return getThumbCreator(subPlugin);
    };
636
637
638
639
640

    if ((segmentWidth <= 256) && (segmentHeight <= 256)) {
        // check whether a cached version of the file is available for
        // 128 x 128 or 256 x 256 pixels
        int cacheSize = 0;
Sebastian Kügler's avatar
Sebastian Kügler committed
641
        QCryptographicHash md5(QCryptographicHash::Md5);
642
643
644
        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
645

646
        if (m_thumbBasePath.isEmpty()) {
647
            m_thumbBasePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/");
648
649
650
651
652
            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);
653
654
        }

655
        QDir thumbPath(m_thumbBasePath);
656
657
        if ((segmentWidth <= 128) && (segmentHeight <= 128)) {
            cacheSize = 128;
658
            thumbPath.cd("normal");
659
660
        } else {
            cacheSize = 256;
661
            thumbPath.cd("large");
662
        }
663

664
665
        QFile thumbFile(thumbPath.absoluteFilePath(thumbName));
        if (thumbFile.open(QIODevice::ReadOnly) && thumbnail.load(&thumbFile, "png")) {
666
667
668
669
            return true;
        } else if (cacheSize == 128) {
            QDir fallbackPath(m_thumbBasePath);
            fallbackPath.cd("large");
670
671
            QFile fallbackThumbFile(fallbackPath.absoluteFilePath(thumbName));
            if (fallbackThumbFile.open(QIODevice::ReadOnly) && thumbnail.load(&fallbackThumbFile, "png")) {
672
673
674
675
                return true;
            }
        }

676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
        // no cached version is available, a new thumbnail must be created
        ThumbCreator* subCreator = getSubCreator();
        if (subCreator && subCreator->create(filePath, cacheSize, cacheSize, thumbnail)) {
            scaleDownImage(thumbnail, cacheSize, cacheSize);

            // The thumbnail has been created successfully. Store the thumbnail
            // to the cache for future access.
            QSaveFile thumbnailfile(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();
692
693
                }
            }
694
695
        } else {
            return false;
696
        }
697
698
699
    } else {
        ThumbCreator* subCreator = getSubCreator();
        return subCreator && subCreator->create(filePath, segmentWidth, segmentHeight, thumbnail);
700
701
702
703
    }
    return true;
}

704
705
706
void ThumbnailProtocol::scaleDownImage(QImage& img, int maxWidth, int maxHeight)
{
    if (img.width() > maxWidth || img.height() > maxHeight) {
707
        img = img.scaled(maxWidth, maxHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation);
708
709
710
    }
}

711
bool ThumbnailProtocol::drawSubThumbnail(QPainter& p, QImage subThumbnail, int width, int height, int xPos, int yPos, int frameWidth)
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
{
    // Apply fake smooth scaling, as seen on several blogs
    if (subThumbnail.width() > width * 4 || subThumbnail.height() > height * 4) {
        subThumbnail = subThumbnail.scaled(width*4, height*4, Qt::KeepAspectRatio, Qt::FastTransformation);
    }

    QSize targetSize(subThumbnail.size());
    targetSize.scale(width, height, Qt::KeepAspectRatio);

    // center the image inside the segment boundaries
    const QPoint centerPos(xPos + (width/ 2), yPos + (height / 2));
    drawPictureFrame(&p, centerPos, subThumbnail, frameWidth, targetSize);

    return true;
}