Coverage Report

Created: 2026-01-25 07:18

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 <KPluginFactory>
21
#include <KTar>
22
#include <KZip>
23
24
#include <memory>
25
26
#include <QEventLoop>
27
#include <QFile>
28
#include <QMimeDatabase>
29
#include <QMimeType>
30
#include <QProcess>
31
#include <QStandardPaths>
32
#include <QTemporaryDir>
33
34
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()
35
0
36
0
ComicCreator::ComicCreator(QObject *parent, const QVariantList &args)
37
9.91k
    : KIO::ThumbnailCreator(parent, args)
38
9.91k
{
39
9.91k
}
40
41
KIO::ThumbnailResult ComicCreator::create(const KIO::ThumbnailRequest &request)
42
9.91k
{
43
9.91k
    const QString path = request.url().toLocalFile();
44
9.91k
    QImage cover;
45
46
    // Detect mime type.
47
9.91k
    QMimeDatabase db;
48
9.91k
    const QMimeType mime = db.mimeTypeForName(request.mimeType());
49
50
9.91k
    if (mime.inherits("application/x-cbz") || mime.inherits("application/zip")) {
51
        // ZIP archive.
52
3.62k
        cover = extractArchiveImage(path, ZIP);
53
6.29k
    } else if (mime.inherits("application/x-cbt") || mime.inherits("application/x-gzip") || mime.inherits("application/x-tar")) {
54
        // TAR archive
55
5.85k
        cover = extractArchiveImage(path, TAR);
56
5.85k
    } else if (mime.inherits("application/x-cb7") || mime.inherits("application/x-7z-compressed")) {
57
36
        cover = extractArchiveImage(path, SEVENZIP);
58
397
    } else if (mime.inherits("application/x-cbr") || mime.inherits("application/x-rar")) {
59
        // RAR archive.
60
2
        cover = extractRARImage(path);
61
2
    }
62
63
9.91k
    if (cover.isNull()) {
64
9.65k
        qCDebug(KIO_THUMBNAIL_COMIC_LOG) << "Error creating the comic book thumbnail for" << path;
65
9.65k
        return KIO::ThumbnailResult::fail();
66
9.65k
    }
67
68
261
    return KIO::ThumbnailResult::pass(cover);
69
9.91k
}
70
71
void ComicCreator::filterImages(QStringList &entries)
72
5.69k
{
73
    /// Sort case-insensitive, then remove non-image entries.
74
5.69k
    QMap<QString, QString> entryMap;
75
6.17k
    for (const QString &entry : qAsConst(entries)) {
76
        // Skip MacOS resource forks
77
6.17k
        if (entry.startsWith(QLatin1String("__MACOSX"), Qt::CaseInsensitive) || entry.startsWith(QLatin1String(".DS_Store"), Qt::CaseInsensitive)) {
78
0
            continue;
79
0
        }
80
6.17k
        if (entry.endsWith(QLatin1String(".avif"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".bmp"), Qt::CaseInsensitive)
81
6.17k
            || entry.endsWith(QLatin1String(".gif"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".heif"), Qt::CaseInsensitive)
82
6.16k
            || entry.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)
83
6.15k
            || entry.endsWith(QLatin1String(".jxl"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)
84
4.85k
            || entry.endsWith(QLatin1String(".webp"), Qt::CaseInsensitive)) {
85
4.85k
            entryMap.insert(entry.toLower(), entry);
86
4.85k
        }
87
6.17k
    }
88
5.69k
    entries = entryMap.values();
89
5.69k
}
90
91
QImage ComicCreator::extractArchiveImage(const QString &path, const ComicCreator::Type type)
92
9.51k
{
93
    /// Extracts the cover image out of the .cbz or .cbt file.
94
9.51k
    QScopedPointer<KArchive> cArchive;
95
96
9.51k
    if (type == ZIP) {
97
        // Open the ZIP archive.
98
3.62k
        cArchive.reset(new KZip(path));
99
5.89k
    } else if (type == TAR) {
100
        // Open the TAR archive.
101
5.85k
        cArchive.reset(new KTar(path));
102
5.85k
    } else if (type == SEVENZIP) {
103
        // Open the 7z archive.
104
36
        cArchive.reset(new K7Zip(path));
105
36
    } else {
106
        // Reject all other types for this method.
107
0
        return QImage();
108
0
    }
109
110
    // Can our archive be opened?
111
9.51k
    if (!cArchive->open(QIODevice::ReadOnly)) {
112
3.81k
        return QImage();
113
3.81k
    }
114
115
    // Get the archive's directory.
116
5.69k
    const KArchiveDirectory *cArchiveDir = nullptr;
117
5.69k
    cArchiveDir = cArchive->directory();
118
5.69k
    if (!cArchiveDir) {
119
0
        return QImage();
120
0
    }
121
122
5.69k
    QStringList entries;
123
124
    // Get and filter the entries from the archive.
125
5.69k
    getArchiveFileList(entries, QString(), cArchiveDir);
126
5.69k
    filterImages(entries);
127
5.69k
    if (entries.isEmpty()) {
128
1.15k
        return QImage();
129
1.15k
    }
130
131
    // Extract the cover file.
132
4.54k
    const KArchiveFile *coverFile = static_cast<const KArchiveFile *>(cArchiveDir->entry(entries[0]));
133
4.54k
    if (!coverFile) {
134
0
        return QImage();
135
0
    }
136
137
4.54k
    return QImage::fromData(coverFile->data());
138
4.54k
}
139
140
void ComicCreator::getArchiveFileList(QStringList &entries, const QString &prefix, const KArchiveDirectory *dir)
141
36.7k
{
142
    /// Recursively list all files in the ZIP archive into 'entries'.
143
36.7k
    const auto dirEntries = dir->entries();
144
37.2k
    for (const QString &entry : dirEntries) {
145
37.2k
        const KArchiveEntry *e = dir->entry(entry);
146
37.2k
        if (e->isDirectory()) {
147
31.0k
            getArchiveFileList(entries, prefix + entry + '/', static_cast<const KArchiveDirectory *>(e));
148
31.0k
        } else if (e->isFile()) {
149
6.17k
            entries.append(prefix + entry);
150
6.17k
        }
151
37.2k
    }
152
36.7k
}
153
154
QImage ComicCreator::extractRARImage(const QString &path)
155
2
{
156
    /// Extracts the cover image out of the .cbr file.
157
158
    // Check if unrar is available. Get its path in 'unrarPath'.
159
2
    static const QString unrar = unrarPath();
160
2
    if (unrar.isEmpty()) {
161
2
        return QImage();
162
2
    }
163
164
    // Get the files and filter the images out.
165
0
    QStringList entries = getRARFileList(path, unrar);
166
0
    filterImages(entries);
167
0
    if (entries.isEmpty()) {
168
0
        return QImage();
169
0
    }
170
171
    // Extract the cover file alone. Use verbose paths.
172
    // unrar x -n<file> path/to/archive /path/to/temp
173
0
    QTemporaryDir cUnrarTempDir;
174
0
    runProcess(unrar, {"x", "-n" + entries[0], path, cUnrarTempDir.path()});
175
176
    // Load cover file data into image.
177
0
    QImage cover;
178
0
    cover.load(cUnrarTempDir.path() + QDir::separator() + entries[0]);
179
180
0
    return cover;
181
0
}
182
183
QStringList ComicCreator::getRARFileList(const QString &path, const QString &unrarPath)
184
0
{
185
    /// Get a verbose unrar listing so we can extract a single file later.
186
    // CMD: unrar vb /path/to/archive
187
0
    QStringList entries;
188
0
    runProcess(unrarPath, {"vb", path});
189
0
    entries = QString::fromLocal8Bit(m_stdOut).split('\n', Qt::SkipEmptyParts);
190
0
    return entries;
191
0
}
192
193
QString ComicCreator::unrarPath() const
194
1
{
195
    /// Check the standard paths to see if a suitable unrar is available.
196
1
    QString unrar = QStandardPaths::findExecutable("unrar");
197
1
    if (unrar.isEmpty()) {
198
1
        unrar = QStandardPaths::findExecutable("unrar-nonfree");
199
1
    }
200
1
    if (unrar.isEmpty()) {
201
1
        unrar = QStandardPaths::findExecutable("rar");
202
1
    }
203
1
    if (!unrar.isEmpty()) {
204
0
        QProcess proc;
205
0
        proc.start(unrar, {"-version"});
206
0
        proc.waitForFinished(-1);
207
0
        const QStringList lines = QString::fromLocal8Bit(proc.readAllStandardOutput()).split('\n', Qt::SkipEmptyParts);
208
0
        if (!lines.isEmpty()) {
209
0
            if (lines.first().startsWith(QLatin1String("RAR ")) || lines.first().startsWith(QLatin1String("UNRAR "))) {
210
0
                return unrar;
211
0
            }
212
0
        }
213
0
    }
214
1
    qCWarning(KIO_THUMBNAIL_COMIC_LOG) << "A suitable version of unrar is not available.";
215
1
    return QString();
216
1
}
217
218
int ComicCreator::runProcess(const QString &processPath, const QStringList &args)
219
0
{
220
    /// Run a process and store stdout data in a buffer.
221
222
0
    QProcess process;
223
0
    process.setProcessChannelMode(QProcess::SeparateChannels);
224
225
0
    process.setProgram(processPath);
226
0
    process.setArguments(args);
227
0
    process.start(QIODevice::ReadWrite | QIODevice::Unbuffered);
228
229
0
    auto ret = process.waitForFinished(-1);
230
0
    m_stdOut = process.readAllStandardOutput();
231
232
0
    return ret;
233
0
}
234
235
#include "comiccreator.moc"
236
#include "moc_comiccreator.cpp"