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"