File indexing completed on 2024-04-28 04:58:02

0001 /**
0002  * This file is part of the KDE libraries
0003  *
0004  * Comic Book Thumbnailer for KDE 4 v0.1
0005  * Creates cover page previews for comic-book files (.cbr/z/t).
0006  * SPDX-FileCopyrightText: 2009 Harsh J <harsh@harshj.com>
0007  *
0008  * Some code borrowed from Okular's comicbook generators,
0009  * by Tobias Koenig <tokoe@kde.org>
0010  *
0011  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0012  */
0013 
0014 // comiccreator.cpp
0015 
0016 #include "comiccreator.h"
0017 #include "thumbnail-comic-logsettings.h"
0018 
0019 #include <K7Zip>
0020 #include <KPluginFactory>
0021 #include <KTar>
0022 #include <KZip>
0023 
0024 #include <memory>
0025 
0026 #include <QEventLoop>
0027 #include <QFile>
0028 #include <QMimeDatabase>
0029 #include <QMimeType>
0030 #include <QProcess>
0031 #include <QStandardPaths>
0032 #include <QTemporaryDir>
0033 
0034 K_PLUGIN_CLASS_WITH_JSON(ComicCreator, "comicbookthumbnail.json")
0035 
0036 ComicCreator::ComicCreator(QObject *parent, const QVariantList &args)
0037     : KIO::ThumbnailCreator(parent, args)
0038 {
0039 }
0040 
0041 KIO::ThumbnailResult ComicCreator::create(const KIO::ThumbnailRequest &request)
0042 {
0043     const QString path = request.url().toLocalFile();
0044     QImage cover;
0045 
0046     // Detect mime type.
0047     QMimeDatabase db;
0048     db.mimeTypeForFile(path, QMimeDatabase::MatchContent);
0049     const QMimeType mime = db.mimeTypeForFile(path, QMimeDatabase::MatchContent);
0050 
0051     if (mime.inherits("application/x-cbz") || mime.inherits("application/zip")) {
0052         // ZIP archive.
0053         cover = extractArchiveImage(path, ZIP);
0054     } else if (mime.inherits("application/x-cbt") || mime.inherits("application/x-gzip") || mime.inherits("application/x-tar")) {
0055         // TAR archive
0056         cover = extractArchiveImage(path, TAR);
0057     } else if (mime.inherits("application/x-cb7") || mime.inherits("application/x-7z-compressed")) {
0058         cover = extractArchiveImage(path, SEVENZIP);
0059     } else if (mime.inherits("application/x-cbr") || mime.inherits("application/x-rar")) {
0060         // RAR archive.
0061         cover = extractRARImage(path);
0062     }
0063 
0064     if (cover.isNull()) {
0065         qCDebug(KIO_THUMBNAIL_COMIC_LOG) << "Error creating the comic book thumbnail for" << path;
0066         return KIO::ThumbnailResult::fail();
0067     }
0068 
0069     return KIO::ThumbnailResult::pass(cover);
0070 }
0071 
0072 void ComicCreator::filterImages(QStringList &entries)
0073 {
0074     /// Sort case-insensitive, then remove non-image entries.
0075     QMap<QString, QString> entryMap;
0076     for (const QString &entry : qAsConst(entries)) {
0077         // Skip MacOS resource forks
0078         if (entry.startsWith(QLatin1String("__MACOSX"), Qt::CaseInsensitive) || entry.startsWith(QLatin1String(".DS_Store"), Qt::CaseInsensitive)) {
0079             continue;
0080         }
0081         if (entry.endsWith(QLatin1String(".avif"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".bmp"), Qt::CaseInsensitive)
0082             || entry.endsWith(QLatin1String(".gif"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".heif"), Qt::CaseInsensitive)
0083             || entry.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive)
0084             || entry.endsWith(QLatin1String(".jxl"), Qt::CaseInsensitive) || entry.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)
0085             || entry.endsWith(QLatin1String(".webp"), Qt::CaseInsensitive)) {
0086             entryMap.insert(entry.toLower(), entry);
0087         }
0088     }
0089     entries = entryMap.values();
0090 }
0091 
0092 QImage ComicCreator::extractArchiveImage(const QString &path, const ComicCreator::Type type)
0093 {
0094     /// Extracts the cover image out of the .cbz or .cbt file.
0095     QScopedPointer<KArchive> cArchive;
0096 
0097     if (type == ZIP) {
0098         // Open the ZIP archive.
0099         cArchive.reset(new KZip(path));
0100     } else if (type == TAR) {
0101         // Open the TAR archive.
0102         cArchive.reset(new KTar(path));
0103     } else if (type == SEVENZIP) {
0104         // Open the 7z archive.
0105         cArchive.reset(new K7Zip(path));
0106     } else {
0107         // Reject all other types for this method.
0108         return QImage();
0109     }
0110 
0111     // Can our archive be opened?
0112     if (!cArchive->open(QIODevice::ReadOnly)) {
0113         return QImage();
0114     }
0115 
0116     // Get the archive's directory.
0117     const KArchiveDirectory *cArchiveDir = nullptr;
0118     cArchiveDir = cArchive->directory();
0119     if (!cArchiveDir) {
0120         return QImage();
0121     }
0122 
0123     QStringList entries;
0124 
0125     // Get and filter the entries from the archive.
0126     getArchiveFileList(entries, QString(), cArchiveDir);
0127     filterImages(entries);
0128     if (entries.isEmpty()) {
0129         return QImage();
0130     }
0131 
0132     // Extract the cover file.
0133     const KArchiveFile *coverFile = static_cast<const KArchiveFile *>(cArchiveDir->entry(entries[0]));
0134     if (!coverFile) {
0135         return QImage();
0136     }
0137 
0138     return QImage::fromData(coverFile->data());
0139 }
0140 
0141 void ComicCreator::getArchiveFileList(QStringList &entries, const QString &prefix, const KArchiveDirectory *dir)
0142 {
0143     /// Recursively list all files in the ZIP archive into 'entries'.
0144     const auto dirEntries = dir->entries();
0145     for (const QString &entry : dirEntries) {
0146         const KArchiveEntry *e = dir->entry(entry);
0147         if (e->isDirectory()) {
0148             getArchiveFileList(entries, prefix + entry + '/', static_cast<const KArchiveDirectory *>(e));
0149         } else if (e->isFile()) {
0150             entries.append(prefix + entry);
0151         }
0152     }
0153 }
0154 
0155 QImage ComicCreator::extractRARImage(const QString &path)
0156 {
0157     /// Extracts the cover image out of the .cbr file.
0158 
0159     // Check if unrar is available. Get its path in 'unrarPath'.
0160     static const QString unrar = unrarPath();
0161     if (unrar.isEmpty()) {
0162         return QImage();
0163     }
0164 
0165     // Get the files and filter the images out.
0166     QStringList entries = getRARFileList(path, unrar);
0167     filterImages(entries);
0168     if (entries.isEmpty()) {
0169         return QImage();
0170     }
0171 
0172     // Extract the cover file alone. Use verbose paths.
0173     // unrar x -n<file> path/to/archive /path/to/temp
0174     QTemporaryDir cUnrarTempDir;
0175     runProcess(unrar, {"x", "-n" + entries[0], path, cUnrarTempDir.path()});
0176 
0177     // Load cover file data into image.
0178     QImage cover;
0179     cover.load(cUnrarTempDir.path() + QDir::separator() + entries[0]);
0180 
0181     return cover;
0182 }
0183 
0184 QStringList ComicCreator::getRARFileList(const QString &path, const QString &unrarPath)
0185 {
0186     /// Get a verbose unrar listing so we can extract a single file later.
0187     // CMD: unrar vb /path/to/archive
0188     QStringList entries;
0189     runProcess(unrarPath, {"vb", path});
0190     entries = QString::fromLocal8Bit(m_stdOut).split('\n', Qt::SkipEmptyParts);
0191     return entries;
0192 }
0193 
0194 QString ComicCreator::unrarPath() const
0195 {
0196     /// Check the standard paths to see if a suitable unrar is available.
0197     QString unrar = QStandardPaths::findExecutable("unrar");
0198     if (unrar.isEmpty()) {
0199         unrar = QStandardPaths::findExecutable("unrar-nonfree");
0200     }
0201     if (unrar.isEmpty()) {
0202         unrar = QStandardPaths::findExecutable("rar");
0203     }
0204     if (!unrar.isEmpty()) {
0205         QProcess proc;
0206         proc.start(unrar, {"-version"});
0207         proc.waitForFinished(-1);
0208         const QStringList lines = QString::fromLocal8Bit(proc.readAllStandardOutput()).split('\n', Qt::SkipEmptyParts);
0209         if (!lines.isEmpty()) {
0210             if (lines.first().startsWith(QLatin1String("RAR ")) || lines.first().startsWith(QLatin1String("UNRAR "))) {
0211                 return unrar;
0212             }
0213         }
0214     }
0215     qCWarning(KIO_THUMBNAIL_COMIC_LOG) << "A suitable version of unrar is not available.";
0216     return QString();
0217 }
0218 
0219 int ComicCreator::runProcess(const QString &processPath, const QStringList &args)
0220 {
0221     /// Run a process and store stdout data in a buffer.
0222 
0223     QProcess process;
0224     process.setProcessChannelMode(QProcess::SeparateChannels);
0225 
0226     process.setProgram(processPath);
0227     process.setArguments(args);
0228     process.start(QIODevice::ReadWrite | QIODevice::Unbuffered);
0229 
0230     auto ret = process.waitForFinished(-1);
0231     m_stdOut = process.readAllStandardOutput();
0232 
0233     return ret;
0234 }
0235 
0236 #include "comiccreator.moc"
0237 #include "moc_comiccreator.cpp"