521 lines
20 KiB
C++
521 lines
20 KiB
C++
// SPDX-FileCopyrightText: 2022 - 2023 UnionTech Software Technology Co., Ltd.
|
|
//
|
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
#include <QGuiApplication>
|
|
#include <QImageReader>
|
|
#include <QCommandLineParser>
|
|
#include <QDirIterator>
|
|
#include <QBuffer>
|
|
#include <QDebug>
|
|
|
|
#include <QtConcurrent/QtConcurrent>
|
|
#include <DDciFile>
|
|
#include <stdexcept>
|
|
#include <atomic>
|
|
|
|
DCORE_USE_NAMESPACE
|
|
|
|
// Custom exception for DCI processing errors
|
|
class DciProcessingError : public std::runtime_error {
|
|
public:
|
|
explicit DciProcessingError(const QString &message, int code = -1)
|
|
: std::runtime_error(message.toStdString()), errorCode(code) {}
|
|
|
|
int getErrorCode() const { return errorCode; }
|
|
|
|
private:
|
|
int errorCode;
|
|
};
|
|
|
|
#define MAX_SCALE 10
|
|
#define INVALIDE_QUALITY -2
|
|
#define SCALABLE_SIZE 256
|
|
static int quality4Scaled[MAX_SCALE] = {};
|
|
static inline void initQuality() {
|
|
for (int i = 0; i < MAX_SCALE; ++i)
|
|
quality4Scaled[i] = INVALIDE_QUALITY;
|
|
}
|
|
|
|
static inline void dciChecker(bool result, std::function<const QString()> cb) {
|
|
if (!result) {
|
|
qWarning() << "Failed on writing dci file" << cb();
|
|
throw DciProcessingError("Failed on writing dci file", -6);
|
|
}
|
|
}
|
|
|
|
// TODO 应该使用xdg图标查找规范解析index.theme来查找尺寸
|
|
static int foundSize(const QFileInfo &fileInfo) {
|
|
QDir dir = fileInfo.absoluteDir();
|
|
|
|
// 解析尺寸
|
|
auto parseSize = [](const QString &dirName) -> int {
|
|
bool ok;
|
|
if (int size = dirName.toUInt(&ok); ok) {
|
|
return size;
|
|
}
|
|
|
|
if (dirName.contains('x') && dirName.split('x').size() == 2) {
|
|
if (int size = dirName.split('x').first().toUInt(&ok); ok) {
|
|
return size;
|
|
}
|
|
}
|
|
|
|
if (dirName == "scalable") {
|
|
return SCALABLE_SIZE;
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
if (int size = parseSize(dir.dirName()); size > 0) {
|
|
return size;
|
|
}
|
|
|
|
// 尝试找上一级目录
|
|
if (!dir.cdUp())
|
|
return 0;
|
|
|
|
return parseSize(dir.dirName());
|
|
}
|
|
|
|
static inline QByteArray webpImageData(const QImage &image, int quality) {
|
|
QByteArray data;
|
|
QBuffer buffer(&data);
|
|
bool ok = buffer.open(QIODevice::WriteOnly);
|
|
Q_ASSERT(ok);
|
|
dciChecker(image.save(&buffer, "webp", quality), []{return "failed to save webp image";});
|
|
return data;
|
|
}
|
|
|
|
static bool writeScaledImage(DDciFile &dci, const QImage &image, const QString &targetDir, const int baseSize, int scale/* = 2*/)
|
|
{
|
|
int size = scale * baseSize;
|
|
QImage img;
|
|
if (image.width() == size) {
|
|
img = image;
|
|
} else {
|
|
img = image.scaledToWidth(size, Qt::SmoothTransformation);
|
|
}
|
|
|
|
dciChecker(dci.mkdir(targetDir + QString("/%1").arg(scale)), [&]{return dci.lastErrorString();});
|
|
int quality = quality4Scaled[scale - 1];
|
|
const QByteArray &data = webpImageData(img, quality);
|
|
dciChecker(dci.writeFile(targetDir + QString("/%1/1.webp").arg(scale), data), [&]{return dci.lastErrorString();});
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool writeImage(DDciFile &dci, const QString &imageFile, const QString &targetDir)
|
|
{
|
|
QString sizeDir = targetDir.mid(1, targetDir.indexOf("/", 1) - 1);
|
|
bool ok = false;
|
|
int baseSize = sizeDir.toInt(&ok);
|
|
if (!ok)
|
|
baseSize = 256;
|
|
|
|
QImageReader reader(imageFile);
|
|
if (!reader.canRead()) {
|
|
qWarning() << "Ignore the null image file:" << imageFile;
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < MAX_SCALE; ++i) {
|
|
if (quality4Scaled[i] == INVALIDE_QUALITY)
|
|
continue;
|
|
int scale = i + 1;
|
|
|
|
reader.setScaledSize(QSize(baseSize * scale, baseSize * scale));
|
|
auto image = reader.read();
|
|
if (!writeScaledImage(dci, image, targetDir, baseSize, scale))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool recursionLink(DDciFile &dci, const QString &fromDir, const QString &targetDir)
|
|
{
|
|
for (const auto &i : dci.list(fromDir, true)) {
|
|
const QString file(fromDir + "/" + i);
|
|
const QString targetFile(targetDir + "/" + i);
|
|
if (dci.type(file) == DDciFile::Directory) {
|
|
if (!dci.mkdir(targetFile))
|
|
return false;
|
|
if (!recursionLink(dci, file, targetFile))
|
|
return false;
|
|
} else {
|
|
if (!dci.link(file, targetFile))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static QByteArray readNextSection(QIODevice *io) {
|
|
QByteArray section;
|
|
char ch;
|
|
|
|
bool oneLine = true;
|
|
while (io->getChar(&ch)) {
|
|
if (!oneLine && ch == '"') { // ["] end
|
|
if (io->peek(1) == ",")
|
|
io->skip(1); // skip [,]
|
|
break;
|
|
} else if (ch == ',') {
|
|
break;
|
|
} else if (ch == '"') { // ["] begin
|
|
oneLine = false;
|
|
continue;
|
|
}
|
|
|
|
section.append(ch);
|
|
|
|
if(oneLine && io->peek(1) == "\n")
|
|
break;
|
|
}
|
|
|
|
return section.trimmed();
|
|
}
|
|
|
|
QMultiHash<QString, QString> parseIconFileSymlinkMap(const QString &csvFile) {
|
|
QFile file(csvFile);
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
qWarning() << "Failed on open symlink map file:" << csvFile;
|
|
throw DciProcessingError("Failed on open symlink map file", -7);
|
|
}
|
|
|
|
QMultiHash<QString, QString> map;
|
|
while (!file.atEnd()) {
|
|
QByteArray key = readNextSection(&file);
|
|
QByteArray value = readNextSection(&file);
|
|
for (const auto &i : value.split('\n')) {
|
|
map.insert(QString::fromLocal8Bit(key), QString::fromLocal8Bit(i));
|
|
}
|
|
|
|
char ch = 0;
|
|
while (file.getChar(&ch) && ch != '\n');
|
|
}
|
|
|
|
qInfo() << "Got symlinks:" << map.size();
|
|
|
|
return map;
|
|
}
|
|
|
|
void makeLink(const QFileInfo &file, const QDir &outputDir, const QString &dciFilePath,
|
|
const QMultiHash<QString, QString> &symlinksMap)
|
|
{
|
|
if (symlinksMap.contains(file.completeBaseName())) {
|
|
const QString symlinkKey = QFileInfo(dciFilePath).fileName();
|
|
for (const auto &symTarget : symlinksMap.values(file.completeBaseName())) {
|
|
const QString newSymlink = outputDir.absoluteFilePath(symTarget + ".dci");
|
|
qInfo() << "Create symlink from" << symlinkKey << "to" << newSymlink;
|
|
if (!QFile::link(symlinkKey, newSymlink)) {
|
|
qWarning() << "Failed on create symlink from" << symlinkKey << "to" << newSymlink;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void doFixDarkTheme(const QFileInfo &file, const QDir &outputDir, const QMultiHash<QString, QString> &symlinksMap)
|
|
{
|
|
const QString &newFile = outputDir.absoluteFilePath(file.fileName());
|
|
|
|
DDciFile dciFile(file.absoluteFilePath());
|
|
if (!dciFile.isValid()) {
|
|
qWarning() << "Skip invalid dci file:" << file.absoluteFilePath();
|
|
return;
|
|
}
|
|
|
|
for (const auto &i : dciFile.list("/")) {
|
|
if (dciFile.type(i) != DDciFile::Directory)
|
|
continue;
|
|
|
|
for (const auto &j : dciFile.list(i)) {
|
|
if (dciFile.type(j) != DDciFile::Directory || !j.endsWith(".light"))
|
|
continue;
|
|
|
|
const QString darkDir(j.left(j.size() - 5) + "dark");
|
|
Q_ASSERT(darkDir.endsWith(".dark"));
|
|
if (!dciFile.exists(darkDir)) {
|
|
dciChecker(dciFile.mkdir(darkDir), [&]{return dciFile.lastErrorString();});
|
|
dciChecker(recursionLink(dciFile, j, darkDir), [&]{return dciFile.lastErrorString();});
|
|
}
|
|
}
|
|
}
|
|
|
|
dciChecker(dciFile.writeToFile(newFile), [&]{return dciFile.lastErrorString();});
|
|
|
|
makeLink(file, outputDir, newFile, symlinksMap);
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
QCommandLineOption fileFilter({"m", "match"}, "Give wildcard rules on search icon files, "
|
|
"Each eligible icon will be packaged to a dci file, "
|
|
"If the icon have the dark mode, it needs to store "
|
|
"the dark icon file at \"dark/\" directory relative "
|
|
"to current icon file, and the file name should be "
|
|
"consistent.", "wildcard palette");
|
|
QCommandLineOption outputDirectory({"o", "output"}, "Save the *.dci files to the given directory.",
|
|
"directory");
|
|
QCommandLineOption symlinkMap({"s", "symlink"}, "Give a csv file to create symlinks for the output icon file."
|
|
"\nThe content of symlink.csv like:\n"
|
|
"\t\t sublime-text, com.sublimetext.2\n"
|
|
"\t\t deb, \"\n"
|
|
"\t\t application-vnd.debian.binary-package\n"
|
|
"\t\t application-x-deb\n"
|
|
"\t\t gnome-mime-application-x-deb\n"
|
|
"\t\t \"\n"
|
|
,
|
|
"csv file");
|
|
QCommandLineOption fixDarkTheme("fix-dark-theme", "Create symlinks from light theme for dark theme files.");
|
|
QCommandLineOption scaleQuality({"O","scale-quality"}, "Quility of dci scaled icon image\n"
|
|
"The value may like <scale size>=<quality value> e.g. 2=98:3=95\n"
|
|
"The quality factor must be in the range 0 to 100 or -1.\n"
|
|
"Specify 0 to obtain small compressed files, 100 for large uncompressed files "
|
|
"and -1 to use the image handler default settings.\n"
|
|
"The higher the quality, the larger the dci icon file size", "scale quality");
|
|
|
|
QGuiApplication a(argc, argv);
|
|
|
|
a.setApplicationName("dci-icon-theme");
|
|
a.setApplicationVersion(QString("%1.%2.%3")
|
|
.arg(DTK_VERSION_MAJOR)
|
|
.arg(DTK_VERSION_MINOR)
|
|
.arg(DTK_VERSION_PATCH));
|
|
|
|
QCommandLineParser cp;
|
|
cp.setApplicationDescription("dci-icon-theme tool is a command tool that generate dci icons from common icons.\n"
|
|
"For example, the tool is used in the following ways: \n"
|
|
"\t dci-icon-theme /usr/share/icons/hicolor/256x256/apps -o ~/Desktop/hicolor -O 3=95\n"
|
|
"\t dci-icon-theme -m *.png /usr/share/icons/hicolor/256x256/apps -o ~/Desktop/hicolor -O 3=95\n"
|
|
"\t dci-icon-theme --fix-dark-theme <input dci files directory> -o <output directory path> \n"
|
|
"\t dci-icon-theme <input file directory> -o <output directory path> -s <csv file> -O <qualities>\n"""
|
|
);
|
|
|
|
cp.addOptions({fileFilter, outputDirectory, symlinkMap, fixDarkTheme, scaleQuality});
|
|
cp.addPositionalArgument("source", "Search the given directory and it's subdirectories, "
|
|
"get the files conform to rules of --match.",
|
|
"~/dci-png-icons");
|
|
cp.addHelpOption();
|
|
cp.addVersionOption();
|
|
cp.process(a);
|
|
|
|
if (a.arguments().size() == 1)
|
|
cp.showHelp(-1);
|
|
|
|
if (cp.positionalArguments().isEmpty()) {
|
|
qWarning() << "Not give a source directory.";
|
|
cp.showHelp(-2);
|
|
}
|
|
|
|
if (!cp.isSet(outputDirectory)) {
|
|
qWarning() << "Not give -o argument";
|
|
cp.showHelp(-4);
|
|
}
|
|
|
|
if (!cp.isSet(scaleQuality) && !cp.isSet(fixDarkTheme)) {
|
|
qWarning() << "Not give -O argument"; scaleQuality.flags();
|
|
cp.showHelp(-5);
|
|
}
|
|
|
|
initQuality();
|
|
QString surfix;
|
|
if (cp.isSet(scaleQuality)) {
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
|
auto behavior = Qt::SkipEmptyParts;
|
|
#else
|
|
auto behavior = QString::SkipEmptyParts;
|
|
#endif
|
|
QStringList qualityList = cp.value(scaleQuality).split(":", behavior);
|
|
|
|
for (const QString &kv : qualityList) {
|
|
auto sq = kv.split("=");
|
|
if (sq.size() != 2) {
|
|
qWarning() << "Invalid quality value:" << kv;
|
|
continue;
|
|
}
|
|
|
|
int scaleSize = sq.value(0).toInt();
|
|
if (scaleSize < 1 || scaleSize > MAX_SCALE) {
|
|
qWarning() << "Invalid scale size:" << kv;
|
|
continue;
|
|
}
|
|
int validQuality = qMax(qMin(sq.value(1).toInt(), 100), -1); // -1, 0~100
|
|
quality4Scaled[scaleSize - 1] = validQuality;
|
|
}
|
|
}
|
|
|
|
QDir outputDir(cp.value(outputDirectory));
|
|
if (!outputDir.exists()) {
|
|
if (!QDir::current().mkpath(outputDir.absolutePath())) {
|
|
qWarning() << "Can't create the" << outputDir.absolutePath() << "directory";
|
|
cp.showHelp(-5);
|
|
}
|
|
} else {
|
|
qErrnoWarning("The output directory have been exists.");
|
|
#ifndef QT_DEBUG
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
QMultiHash<QString, QString> symlinksMap;
|
|
if (cp.isSet(symlinkMap)) {
|
|
try {
|
|
symlinksMap = parseIconFileSymlinkMap(cp.value(symlinkMap));
|
|
} catch (const DciProcessingError &e) {
|
|
qWarning() << "Error parsing symlink map:" << e.what();
|
|
return e.getErrorCode();
|
|
}
|
|
}
|
|
|
|
const QStringList nameFilter = cp.isSet(fileFilter) ? cp.values(fileFilter) : QStringList();
|
|
const auto sourceDirectory = cp.positionalArguments();
|
|
for (const auto &sd : sourceDirectory) {
|
|
QDir sourceDir(sd);
|
|
if (!sourceDir.exists()) {
|
|
qWarning() << "Ignore the non-exists directory:" << sourceDir;
|
|
continue;
|
|
}
|
|
|
|
// read all links first
|
|
{
|
|
QDirIterator di(sourceDir.absolutePath(), nameFilter,
|
|
QDir::NoDotAndDotDot | QDir::Files,
|
|
QDirIterator::Subdirectories);
|
|
while (di.hasNext()) {
|
|
di.next();
|
|
QFileInfo file = di.fileInfo();
|
|
if (!file.isSymLink())
|
|
continue;
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
|
|
auto link = file.symLinkTarget();
|
|
#else
|
|
auto link = file.readLink();
|
|
#endif
|
|
const QString &linkTarget = QFileInfo(link).completeBaseName();
|
|
if (!symlinksMap.values(linkTarget).contains(file.completeBaseName())) {
|
|
symlinksMap.insert(linkTarget, file.completeBaseName());
|
|
qInfo() << "Add link" << file.completeBaseName() << "->" << linkTarget;
|
|
} else {
|
|
// qDebug() << "** link already existed in symlinksMap **";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect all files first, grouped by icon name to avoid concurrent access to same DCI file
|
|
QMap<QString, QList<QFileInfo>> iconGroups;
|
|
QDirIterator di(sourceDir.absolutePath(), nameFilter,
|
|
QDir::NoDotAndDotDot | QDir::Files,
|
|
QDirIterator::Subdirectories);
|
|
while (di.hasNext()) {
|
|
di.next();
|
|
QFileInfo file = di.fileInfo();
|
|
|
|
if (file.isSymLink())
|
|
continue;
|
|
|
|
if (cp.isSet(fixDarkTheme)) {
|
|
try {
|
|
doFixDarkTheme(file, outputDir, symlinksMap);
|
|
} catch (const DciProcessingError &e) {
|
|
qWarning() << "Error fixing dark theme for file" << file.absoluteFilePath() << ":" << e.what();
|
|
return e.getErrorCode();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (file.path().endsWith(QStringLiteral("/dark"))) {
|
|
qInfo() << "Ignore the dark icon file:" << file;
|
|
continue;
|
|
}
|
|
|
|
iconGroups[file.completeBaseName()].append(file);
|
|
}
|
|
|
|
// Process with proper exception handling
|
|
std::atomic<bool> hasError{false};
|
|
int errorCode = 0;
|
|
|
|
// Process icon groups concurrently (each group shares same DCI file)
|
|
QList<QString> iconNames = iconGroups.keys();
|
|
QtConcurrent::blockingMap(iconNames, [&](const QString &iconName) {
|
|
if (hasError.load()) return; // Skip if already has error
|
|
|
|
try {
|
|
const QList<QFileInfo> &files = iconGroups[iconName];
|
|
const QString dciFilePath(outputDir.absoluteFilePath(iconName) + surfix + ".dci");
|
|
QScopedPointer<DDciFile> dciFile;
|
|
|
|
for (const QFileInfo &file : files) {
|
|
QString dirName = file.absoluteDir().dirName();
|
|
uint iconSize = foundSize(file);
|
|
dirName = iconSize > 0 ? QString("/%1").arg(iconSize) : dirName.prepend("/");
|
|
|
|
// Initialize DCI file once per icon group
|
|
if (dciFile.isNull()) {
|
|
if (QFileInfo::exists(dciFilePath)) {
|
|
dciFile.reset(new DDciFile(dciFilePath));
|
|
}
|
|
if (dciFile.isNull() || !dciFile->isValid()) {
|
|
dciFile.reset(new DDciFile);
|
|
}
|
|
}
|
|
if (dciFile->exists(dirName)) {
|
|
qWarning() << "Skip exists dci file:" << dciFilePath << dirName << dciFile->list(dirName);
|
|
continue;
|
|
}
|
|
|
|
qInfo() << "Writing to dci file:" << file.absoluteFilePath() << "==>" << dciFilePath;
|
|
|
|
QString sizeDir = iconSize > 0 ? dirName : "/256"; // "/256" as default
|
|
QString normalLight = sizeDir + "/normal.light"; // "/256/normal.light"
|
|
QString normalDark = sizeDir + "/normal.dark"; // "/256/normal.dark"
|
|
|
|
if (dciFile->exists(sizeDir)) {
|
|
qWarning() << "Skip exists dci file:" << dciFilePath << sizeDir << dciFile->list(sizeDir);
|
|
continue;
|
|
}
|
|
|
|
dciChecker(dciFile->mkdir(sizeDir), [&]{return dciFile->lastErrorString();});
|
|
dciChecker(dciFile->mkdir(normalLight), [&]{return dciFile->lastErrorString();});
|
|
if (!writeImage(*dciFile, file.filePath(), normalLight))
|
|
continue;
|
|
|
|
dciChecker(dciFile->mkdir(normalDark), [&]{return dciFile->lastErrorString();});
|
|
QFileInfo darkIcon(file.dir().absoluteFilePath("dark/" + file.fileName()));
|
|
if (darkIcon.exists()) {
|
|
writeImage(*dciFile, darkIcon.filePath(), normalDark);
|
|
} else {
|
|
dciChecker(recursionLink(*dciFile, normalLight, normalDark), [&]{return dciFile->lastErrorString();});
|
|
}
|
|
}
|
|
|
|
// Write DCI file once per icon group
|
|
if (!dciFile.isNull()) {
|
|
dciChecker(dciFile->writeToFile(dciFilePath), [&]{return dciFile->lastErrorString();});
|
|
|
|
// Create symlinks for all files in this group
|
|
for (const QFileInfo &file : files) {
|
|
makeLink(file, outputDir, dciFilePath, symlinksMap);
|
|
}
|
|
}
|
|
} catch (const DciProcessingError &e) {
|
|
qWarning() << "Error processing icon group" << iconName << ":" << e.what();
|
|
hasError.store(true);
|
|
errorCode = e.getErrorCode();
|
|
}
|
|
});
|
|
|
|
if (hasError.load()) {
|
|
qWarning() << "Encountered errors during DCI file writing" << errorCode;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|