/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" |