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