diff --git a/src/gui/src/settings/options-window.cpp b/src/gui/src/settings/options-window.cpp index 228b65ff2..e81460aca 100644 --- a/src/gui/src/settings/options-window.cpp +++ b/src/gui/src/settings/options-window.cpp @@ -23,6 +23,7 @@ #include "custom-buttons.h" #include "external/exiftool.h" #include "external/ffmpeg.h" +#include "external/image-magick.h" #include "functions.h" #include "filename/conditional-filename.h" #include "helpers.h" @@ -44,7 +45,7 @@ #include "viewer/viewer-window-buttons.h" -void disableItem(QComboBox *combo, const int index, const QString &toolTip) { +void disableItem(QComboBox *combo, const int index, const QString &toolTip = {}) { auto *model = qobject_cast(combo->model()); QStandardItem *item = model->item(index); item->setFlags(item->flags() & ~Qt::ItemIsEnabled); @@ -139,20 +140,59 @@ OptionsWindow::OptionsWindow(Profile *profile, ThemeLoader *themeLoader, QWidget ui->keyAcceptDialogue->setKeySequence(getKeySequence(settings, "keyAcceptDialog", Qt::CTRL | Qt::Key_Y)); ui->keyDeclineDialogue->setKeySequence(getKeySequence(settings, "keyDeclineDialog", Qt::CTRL | Qt::Key_N)); - // FFmpeg format conversion + // FFmpeg version QFuture ffmpegVersion = QtConcurrent::run([=]() { return FFmpeg::version(); }); auto *ffmpegVersionWatcher = new QFutureWatcher(this); connect(ffmpegVersionWatcher, &QFutureWatcher::finished, [=]() { const QString &version = ffmpegVersion.result(); - ui->labelConversionFFmpegVersion->setText(version.isEmpty() ? tr("ffmpeg not found") : version); + ui->labelConversionFFmpegVersion->setText(version.isEmpty() ? tr("FFmpeg not found") : version); if (version.isEmpty()) { ui->labelConversionFFmpegVersion->setStyleSheet("color: red"); + disableItem(ui->comboConversionImageBackend, 1); } }); ffmpegVersionWatcher->setFuture(ffmpegVersion); + + // ImageMagick version + QFuture imageMagickVersion = QtConcurrent::run([=]() { + return ImageMagick::version(); + }); + auto *imageMagickVersionWatcher = new QFutureWatcher(this); + connect(imageMagickVersionWatcher, &QFutureWatcher::finished, [=]() { + const QString &version = imageMagickVersion.result(); + ui->labelConversionImageMagickVersion->setText(version.isEmpty() ? tr("ImageMagick not found") : version); + if (version.isEmpty()) { + ui->labelConversionImageMagickVersion->setStyleSheet("color: red"); + disableItem(ui->comboConversionImageBackend, 0); + } + }); + imageMagickVersionWatcher->setFuture(imageMagickVersion); + + // Video conversion ui->checkConversionFFmpegRemuxWebmToMp4->setChecked(settings->value("Save/FFmpegRemuxWebmToMp4", false).toBool()); + + // Image conversion + ui->comboConversionImageBackend->setCurrentText(settings->value("Save/ImageConversionBackend", "ImageMagick").toString()); + settings->beginGroup("Save/ImageConversion"); + for (const QString &from : settings->childGroups()) { + settings->beginGroup(from); + const QString to = settings->value("to").toString(); + settings->endGroup(); + + auto *leFrom = new QLineEdit(from, this); + auto *leTo = new QLineEdit(to, this); + m_imageConversion.append(QPair { leFrom, leTo }); + + auto *layout = new QHBoxLayout(this); + layout->addWidget(leFrom); + layout->addWidget(leTo); + ui->layoutConversionImageList->addLayout(layout); + } + settings->endGroup(); + + // Ugoira conversion ui->checkConversionUgoiraEnabled->setChecked(settings->value("Save/ConvertUgoira", false).toBool()); ui->comboConversionUgoiraTargetExtension->setCurrentText(settings->value("Save/ConvertUgoiraFormat", "gif").toString().toUpper()); ui->checkConversionUgoiraDelete->setChecked(settings->value("Save/ConvertUgoiraDeleteOriginal", false).toBool()); @@ -601,6 +641,18 @@ void OptionsWindow::on_buttonMetadataExiftoolAdd_clicked() m_metadataExiftool.append(QPair { leKey, leValue }); } +void OptionsWindow::on_buttonConversionImageListAdd_clicked() +{ + auto *leFrom = new QLineEdit(this); + auto *leTo = new QLineEdit(this); + m_imageConversion.append(QPair { leFrom, leTo }); + + auto *layout = new QHBoxLayout(this); + layout->addWidget(leFrom); + layout->addWidget(leTo); + ui->layoutConversionImageList->addLayout(layout); +} + void OptionsWindow::reloadSourceRegistries() { @@ -1288,7 +1340,26 @@ void OptionsWindow::save() tokenSettings->save(); } + // Video conversion settings->setValue("FFmpegRemuxWebmToMp4", ui->checkConversionFFmpegRemuxWebmToMp4->isChecked()); + + // Image conversion + settings->setValue("ImageConversionBackend", ui->comboConversionImageBackend->currentText()); + settings->beginGroup("ImageConversion"); + settings->remove(""); + for (int i = 0, j = 0; i < m_imageConversion.count(); ++i) { + const QString &from = m_imageConversion[i].first->text(); + const QString &to = m_imageConversion[i].second->text(); + if (!from.isEmpty() && !to.isEmpty()) { + settings->beginGroup(from.toUpper()); + settings->setValue("to", to.toUpper()); + settings->endGroup(); + ++j; + } + } + settings->endGroup(); + + // Ugoira conversion settings->setValue("ConvertUgoira", ui->checkConversionUgoiraEnabled->isChecked()); settings->setValue("ConvertUgoiraFormat", ui->comboConversionUgoiraTargetExtension->currentText().toLower()); settings->setValue("ConvertUgoiraDeleteOriginal", ui->checkConversionUgoiraDelete->isChecked()); diff --git a/src/gui/src/settings/options-window.h b/src/gui/src/settings/options-window.h index 637282e14..c10f8ebb8 100644 --- a/src/gui/src/settings/options-window.h +++ b/src/gui/src/settings/options-window.h @@ -89,6 +89,7 @@ class OptionsWindow : public QDialog void addFilename(const QString &, const QString &, const QString &); void on_buttonMetadataPropsysAdd_clicked(); void on_buttonMetadataExiftoolAdd_clicked(); + void on_buttonConversionImageListAdd_clicked(); // Custom image window buttons void initButtonSettingPairs(); @@ -137,6 +138,7 @@ class OptionsWindow : public QDialog QList m_tokenSettings; QList> m_metadataPropsys, m_metadataExiftool; QList> m_buttonSettingPairs; + QList> m_imageConversion; }; #endif // OPTIONS_WINDOW_H diff --git a/src/gui/src/settings/options-window.ui b/src/gui/src/settings/options-window.ui index 5852b132a..8d4ce2514 100644 --- a/src/gui/src/settings/options-window.ui +++ b/src/gui/src/settings/options-window.ui @@ -1182,7 +1182,7 @@ 0 0 476 - 493 + 475 @@ -1437,14 +1437,35 @@ - + FFmpeg version - - + + + + Loading... + + + + + + + ImageMagick version + + + + + + + Loading... + + + + + Video conversion @@ -1459,15 +1480,48 @@ - - - - Loading... + + + + Image conversion + + + + + Back-end + + + + + + + + ImageMagick + + + + + FFmpeg + + + + + + + + + + + Add new image conversion + + + + - - + + Ugoira (ZIP) conversion @@ -1518,7 +1572,7 @@ - Delete original ugoira ZIP files + Delete original file on success @@ -1977,8 +2031,8 @@ 0 0 - 261 - 622 + 323 + 682 @@ -2687,8 +2741,8 @@ 0 0 - 329 - 660 + 388 + 710 @@ -4137,8 +4191,8 @@ 0 0 - 194 - 765 + 235 + 840 diff --git a/src/lib/src/external/ffmpeg.cpp b/src/lib/src/external/ffmpeg.cpp index af85aacfd..941da6d63 100644 --- a/src/lib/src/external/ffmpeg.cpp +++ b/src/lib/src/external/ffmpeg.cpp @@ -41,7 +41,7 @@ QString FFmpeg::version(int msecs) } -QString FFmpeg::remux(const QString &file, const QString &extension, bool deleteOriginal, int msecs) +QString FFmpeg::convert(const QString &file, const QString &extension, bool deleteOriginal, int msecs) { // Since the method takes an extension, build an absolute path to the input file with that extension const QFileInfo info(file); @@ -49,34 +49,43 @@ QString FFmpeg::remux(const QString &file, const QString &extension, bool delete // Ensure the operation is safe to do if (!QFile::exists(file)) { - log(QStringLiteral("Cannot remux file that does not exist: `%1`").arg(file), Logger::Error); + log(QStringLiteral("Cannot convert file that does not exist: `%1`").arg(file), Logger::Error); return file; } if (QFile::exists(destination)) { - log(QStringLiteral("Remuxing the file `%1` would overwrite another file: `%2`").arg(file, destination), Logger::Error); + log(QStringLiteral("Converting the file `%1` would overwrite another file: `%2`").arg(file, destination), Logger::Error); return file; } // Execute the conversion command - const QStringList params = { "-n", "-loglevel", "error", "-i", file, "-c", "copy", destination }; - if (!execute(params, msecs)) { - // Clean-up failed conversions - if (QFile::exists(destination)) { - log(QStringLiteral("Cleaning up failed conversion target file: `%1`").arg(destination), Logger::Warning); - QFile::remove(destination); - } - + const QStringList params = { "-n", "-loglevel", "error", "-i", file, destination }; + if (!executeConvert(file, destination, deleteOriginal, params, msecs)) { return file; } + return destination; +} - // Copy file creation information - setFileCreationDate(destination, info.lastModified()); +QString FFmpeg::remux(const QString &file, const QString &extension, bool deleteOriginal, int msecs) +{ + // Since the method takes an extension, build an absolute path to the input file with that extension + const QFileInfo info(file); + QString destination = info.path() + QDir::separator() + info.completeBaseName() + "." + extension; - // On success, delete the original file if requested - if (deleteOriginal) { - QFile::remove(file); + // Ensure the operation is safe to do + if (!QFile::exists(file)) { + log(QStringLiteral("Cannot remux file that does not exist: `%1`").arg(file), Logger::Error); + return file; + } + if (QFile::exists(destination)) { + log(QStringLiteral("Remuxing the file `%1` would overwrite another file: `%2`").arg(file, destination), Logger::Error); + return file; } + // Execute the conversion command + const QStringList params = { "-n", "-loglevel", "error", "-i", file, "-c", "copy", destination }; + if (!executeConvert(file, destination, deleteOriginal, params, msecs)) { + return file; + } return destination; } @@ -145,22 +154,37 @@ QString FFmpeg::convertUgoira(const QString &file, const QList> &frameInformation, const QString &extension, bool deleteOriginal = true, int msecs = 30000); protected: + static bool executeConvert(const QString &file, const QString &destination, bool deleteOriginal, const QStringList ¶ms, int msecs = 30000); static bool execute(const QStringList ¶ms, int msecs = 30000); }; diff --git a/src/lib/src/models/image.cpp b/src/lib/src/models/image.cpp index 73d77176a..8331c6d05 100644 --- a/src/lib/src/models/image.cpp +++ b/src/lib/src/models/image.cpp @@ -13,6 +13,7 @@ #include "downloader/extension-rotator.h" #include "external/exiftool.h" #include "external/ffmpeg.h" +#include "external/image-magick.h" #include "favorite.h" #include "filename/filename.h" #include "filtering/tag-filter-list.h" @@ -812,11 +813,22 @@ QString Image::postSaving(const QString &originalPath, Size size, bool addMd5, b path = FFmpeg::remux(path, "mp4"); } + // Image conversion + const QString targetImgExt = m_settings->value("Save/ImageConversion/" + ext.toUpper() + "/to").toString().toLower(); + if (!targetImgExt.isEmpty()) { + const QString backend = m_settings->value("Save/ImageConversionBackend", "ImageMagick").toString(); + if (backend == QStringLiteral("ImageMagick")) { + path = ImageMagick::convert(path, targetImgExt); + } else if (backend == QStringLiteral("FFmpeg")) { + path = FFmpeg::remux(path, targetImgExt); + } + } + // Ugoira conversion if (ext == QStringLiteral("zip") && m_settings->value("Save/ConvertUgoira", false).toBool()) { - const QString targetExt = m_settings->value("Save/ConvertUgoiraFormat", "gif").toString(); + const QString targetUgoiraExt = m_settings->value("Save/ConvertUgoiraFormat", "gif").toString(); const bool deleteOriginal = m_settings->value("Save/ConvertUgoiraDeleteOriginal", false).toBool(); - path = FFmpeg::convertUgoira(path, ugoiraFrameInformation(), targetExt, deleteOriginal); + path = FFmpeg::convertUgoira(path, ugoiraFrameInformation(), targetUgoiraExt, deleteOriginal); } // Metadata