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