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