Coverage Report

Created: 2026-03-12 07:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/kio-extras/thumbnail/comiccreator.cpp
Line
Count
Source
1
/**
2
 * This file is part of the KDE libraries
3
 *
4
 * Comic Book Thumbnailer for KDE 4 v0.1
5
 * Creates cover page previews for comic-book files (.cbr/z/t).
6
 * SPDX-FileCopyrightText: 2009 Harsh J <harsh@harshj.com>
7
 *
8
 * Some code borrowed from Okular's comicbook generators,
9
 * by Tobias Koenig <tokoe@kde.org>
10
 *
11
 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
12
 */
13
14
// comiccreator.cpp
15
16
#include "comiccreator.h"
17
#include "thumbnail-comic-logsettings.h"
18
19
#include <K7Zip>
20
#include <KConfigGroup>
21
#include <KIO/Global>
22
#include <KPluginFactory>
23
#include <KSharedConfig>
24
#include <KTar>
25
#include <KZip>
26
27
#include <memory>
28
29
#include <QEventLoop>
30
#include <QFile>
31
#include <QMimeDatabase>
32
#include <QMimeType>
33
#include <QProcess>
34
#include <QStandardPaths>
35
#include <QTemporaryDir>
36
37
0
K_PLUGIN_CLASS_WITH_JSON(ComicCreator, "comicbookthumbnail.json")
Unexecuted instantiation: comicbookthumbnail_factory::tr(char const*, char const*, int)
Unexecuted instantiation: comicbookthumbnail_factory::~comicbookthumbnail_factory()
38
0
39
0
ComicCreator::ComicCreator(QObject *parent, const QVariantList &args)
40
14.2k
    : KIO::ThumbnailCreator(parent, args)
41
14.2k
{
42
14.2k
}
43
44
KIO::ThumbnailResult ComicCreator::create(const KIO::ThumbnailRequest &request)
45
14.2k
{
46
14.2k
    const QString path = request.url().toLocalFile();
47
14.2k
    QImage cover;
48
49
    // Detect mime type.
50
14.2k
    QMimeDatabase db;
51
14.2k
    const QMimeType mime = db.mimeTypeForName(request.mimeType());
52
53
14.2k
    if (mime.inherits("application/x-cbz") || mime.inherits("application/zip")) {
54
        // ZIP archive.
55
5.11k
        cover = extractArchiveImage(path, ZIP);
56
9.17k
    } else if (mime.inherits("application/x-cbt") || mime.inherits("application/x-gzip") || mime.inherits("application/x-tar")) {
57
        // TAR archive
58
8.65k
        cover = extractArchiveImage(path, TAR);
59
8.65k
    } else if (mime.inherits("application/x-cb7") || mime.inherits("application/x-7z-compressed")) {
60
45
        cover = extractArchiveImage(path, SEVENZIP);
61
475
    } else if (mime.inherits("application/x-cbr") || mime.inherits("application/x-rar")) {
62
        // RAR archive.
63
2
        cover = extractRARImage(path);
64
2
    }
65
66
14.2k
    if (cover.isNull()) {
67
13.6k
        qCDebug(KIO_THUMBNAIL_COMIC_LOG) << "Error creating the comic book thumbnail for" << path;
68
13.6k
        return KIO::ThumbnailResult::fail();
69
13.6k
    }
70
71
606
    return KIO::ThumbnailResult::pass(cover);
72
14.2k
}
73
74
void ComicCreator::filterImages(QStringList &entries)
75
9.65k
{
76
    /// Sort case-insensitive, then remove non-image entries.
77
9.65k
    QMap<QString, QString> entryMap;
78
13.0k
    for (const QString &entry : qAsConst(entries)) {
79
        // Skip MacOS resource forks
80
13.0k
        if (entry.startsWith(QLatin1String("__MACOSX"), Qt::CaseInsensitive) || entry.startsWith(QLatin1String(".DS_Store"), Qt::CaseInsensitive)) {
81
0
            continue;
82
0
        }
83
13.0k
        if (entry.endsWith(QLatin1String(".avif"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".bmp"), Qt::CaseInsensitive)
84
13.0k
            || entry.endsWith(QLatin1String(".gif"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".heif"), Qt::CaseInsensitive)
85
13.0k
            || entry.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)
86
13.0k
            || entry.endsWith(QLatin1String(".jxl"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)
87
9.47k
            || entry.endsWith(QLatin1String(".webp"), Qt::CaseInsensitive)) {
88
9.47k
            entryMap.insert(entry.toLower(), entry);
89
9.47k
        }
90
13.0k
    }
91
9.65k
    entries = entryMap.values();
92
9.65k
}
93
94
QImage ComicCreator::extractArchiveImage(const QString &path, const ComicCreator::Type type)
95
13.8k
{
96
    /// Extracts the cover image out of the .cbz or .cbt file.
97
13.8k
    QScopedPointer<KArchive> cArchive;
98
99
13.8k
    if (type == ZIP) {
100
        // Open the ZIP archive.
101
5.11k
        cArchive.reset(new KZip(path));
102
8.70k
    } else if (type == TAR) {
103
        // Open the TAR archive.
104
8.65k
        cArchive.reset(new KTar(path));
105
8.65k
    } else if (type == SEVENZIP) {
106
        // Open the 7z archive.
107
45
        cArchive.reset(new K7Zip(path));
108
45
    } else {
109
        // Reject all other types for this method.
110
0
        return QImage();
111
0
    }
112
113
    // Can our archive be opened?
114
13.8k
    if (!cArchive->open(QIODevice::ReadOnly)) {
115
4.16k
        return QImage();
116
4.16k
    }
117
118
    // Get the archive's directory.
119
9.65k
    const KArchiveDirectory *cArchiveDir = nullptr;
120
9.65k
    cArchiveDir = cArchive->directory();
121
9.65k
    if (!cArchiveDir) {
122
0
        return QImage();
123
0
    }
124
125
9.65k
    QStringList entries;
126
127
    // Get and filter the entries from the archive.
128
9.65k
    getArchiveFileList(entries, QString(), cArchiveDir);
129
9.65k
    filterImages(entries);
130
9.65k
    if (entries.isEmpty()) {
131
2.02k
        return QImage();
132
2.02k
    }
133
134
7.63k
#if KIO_VERSION >= QT_VERSION_CHECK(6, 23, 0)
135
7.63k
    const KIO::filesize_t maxFileSize = KIO::ThumbnailRequest::maximumFileSize();
136
#else
137
    const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings"));
138
    const KIO::filesize_t maxFileSize = globalConfig.readEntry("MaximumSize", std::numeric_limits<KIO::filesize_t>::max());
139
#endif
140
141
    // Extract the cover file.
142
7.63k
    for (const QString &entry : entries) {
143
7.63k
        const KArchiveFile *coverFile = static_cast<const KArchiveFile *>(cArchiveDir->entry(entry));
144
7.63k
        if (!coverFile || coverFile->size() < 0) {
145
0
            continue;
146
0
        }
147
148
7.63k
        const KIO::filesize_t coverFileSize_t = static_cast<KIO::filesize_t>(coverFile->size());
149
7.63k
        if (coverFileSize_t > maxFileSize) {
150
53
            continue;
151
53
        }
152
153
7.58k
        return QImage::fromData(coverFile->data());
154
7.63k
    }
155
156
50
    return {};
157
7.63k
}
158
159
void ComicCreator::getArchiveFileList(QStringList &entries, const QString &prefix, const KArchiveDirectory *dir)
160
72.9k
{
161
    /// Recursively list all files in the ZIP archive into 'entries'.
162
72.9k
    const auto dirEntries = dir->entries();
163
76.3k
    for (const QString &entry : dirEntries) {
164
76.3k
        const KArchiveEntry *e = dir->entry(entry);
165
76.3k
        if (e->isDirectory()) {
166
63.3k
            getArchiveFileList(entries, prefix + entry + '/', static_cast<const KArchiveDirectory *>(e));
167
63.3k
        } else if (e->isFile()) {
168
13.0k
            entries.append(prefix + entry);
169
13.0k
        }
170
76.3k
    }
171
72.9k
}
172
173
QImage ComicCreator::extractRARImage(const QString &path)
174
2
{
175
    /// Extracts the cover image out of the .cbr file.
176
177
    // Check if unrar is available. Get its path in 'unrarPath'.
178
2
    static const QString unrar = unrarPath();
179
2
    if (unrar.isEmpty()) {
180
2
        return QImage();
181
2
    }
182
183
    // Get the files and filter the images out.
184
0
    QStringList entries = getRARFileList(path, unrar);
185
0
    filterImages(entries);
186
0
    if (entries.isEmpty()) {
187
0
        return QImage();
188
0
    }
189
190
    // Extract the cover file alone. Use verbose paths.
191
    // unrar x -n<file> path/to/archive /path/to/temp
192
0
    QTemporaryDir cUnrarTempDir;
193
0
    runProcess(unrar, {"x", "-n" + entries[0], path, cUnrarTempDir.path()});
194
195
    // Load cover file data into image.
196
0
    QImage cover;
197
0
    cover.load(cUnrarTempDir.path() + QDir::separator() + entries[0]);
198
199
0
    return cover;
200
0
}
201
202
QStringList ComicCreator::getRARFileList(const QString &path, const QString &unrarPath)
203
0
{
204
    /// Get a verbose unrar listing so we can extract a single file later.
205
    // CMD: unrar vb /path/to/archive
206
0
    QStringList entries;
207
0
    runProcess(unrarPath, {"vb", path});
208
0
    entries = QString::fromLocal8Bit(m_stdOut).split('\n', Qt::SkipEmptyParts);
209
0
    return entries;
210
0
}
211
212
QString ComicCreator::unrarPath() const
213
1
{
214
    /// Check the standard paths to see if a suitable unrar is available.
215
1
    QString unrar = QStandardPaths::findExecutable("unrar");
216
1
    if (unrar.isEmpty()) {
217
1
        unrar = QStandardPaths::findExecutable("unrar-nonfree");
218
1
    }
219
1
    if (unrar.isEmpty()) {
220
1
        unrar = QStandardPaths::findExecutable("rar");
221
1
    }
222
1
    if (!unrar.isEmpty()) {
223
0
        QProcess proc;
224
0
        proc.start(unrar, {"-version"});
225
0
        proc.waitForFinished(-1);
226
0
        const QStringList lines = QString::fromLocal8Bit(proc.readAllStandardOutput()).split('\n', Qt::SkipEmptyParts);
227
0
        if (!lines.isEmpty()) {
228
0
            if (lines.first().startsWith(QLatin1String("RAR ")) || lines.first().startsWith(QLatin1String("UNRAR "))) {
229
0
                return unrar;
230
0
            }
231
0
        }
232
0
    }
233
1
    qCWarning(KIO_THUMBNAIL_COMIC_LOG) << "A suitable version of unrar is not available.";
234
1
    return QString();
235
1
}
236
237
int ComicCreator::runProcess(const QString &processPath, const QStringList &args)
238
0
{
239
    /// Run a process and store stdout data in a buffer.
240
241
0
    QProcess process;
242
0
    process.setProcessChannelMode(QProcess::SeparateChannels);
243
244
0
    process.setProgram(processPath);
245
0
    process.setArguments(args);
246
0
    process.start(QIODevice::ReadWrite | QIODevice::Unbuffered);
247
248
0
    auto ret = process.waitForFinished(-1);
249
0
    m_stdOut = process.readAllStandardOutput();
250
251
0
    return ret;
252
0
}
253
254
#include "comiccreator.moc"
255
#include "moc_comiccreator.cpp"