File indexing completed on 2025-01-05 03:51:27

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2014-05-24
0007  * Description : user script batch tool.
0008  *
0009  * SPDX-FileCopyrightText: 2009-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2014      by Hubert Law <hhclaw dot eb at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "userscript.h"
0017 
0018 // Qt includes
0019 
0020 #include <QDir>
0021 #include <QFile>
0022 #include <QLabel>
0023 #include <QWidget>
0024 #include <QProcess>
0025 #include <QPlainTextEdit>
0026 
0027 // KDE includes
0028 
0029 #include <klocalizedstring.h>
0030 
0031 // Local includes
0032 
0033 #include "digikam_globals.h"
0034 #include "digikam_debug.h"
0035 #include "dimg.h"
0036 #include "dcombobox.h"
0037 #include "dmetadata.h"
0038 #include "tagscache.h"
0039 #include "dlayoutbox.h"
0040 #include "filereadwritelock.h"
0041 
0042 namespace DigikamBqmUserScriptPlugin
0043 {
0044 
0045 class Q_DECL_HIDDEN UserScript::Private
0046 {
0047 public:
0048 
0049     enum OutputFileType
0050     {
0051         Input = 0,
0052         JPEG,
0053         PNG,
0054         TIFF,
0055         JPEG2000,
0056         JPEGXL,
0057         AVIF,
0058         HEIF,
0059         WEBP
0060     };
0061 
0062 public:
0063 
0064     explicit Private()
0065       : comboBox      (nullptr),
0066         textEdit      (nullptr),
0067         changeSettings(true)
0068     {
0069     }
0070 
0071     DComboBox*      comboBox;
0072     QPlainTextEdit* textEdit;
0073 
0074     bool            changeSettings;
0075 };
0076 
0077 UserScript::UserScript(QObject* const parent)
0078     : BatchTool(QLatin1String("UserScript"), CustomTool, parent),
0079       d        (new Private)
0080 {
0081 }
0082 
0083 UserScript::~UserScript()
0084 {
0085     delete d;
0086 }
0087 
0088 BatchTool* UserScript::clone(QObject* const parent) const
0089 {
0090     return new UserScript(parent);
0091 }
0092 
0093 void UserScript::registerSettingsWidget()
0094 {
0095     DVBox* const vbox    = new DVBox;
0096 
0097     QLabel* const label1 = new QLabel(vbox);
0098     label1->setText(i18n("Output file type:"));
0099 
0100     d->comboBox          = new DComboBox(vbox);
0101     d->comboBox->insertItem(Private::Input,    i18n("Same as input"));
0102     d->comboBox->insertItem(Private::JPEG,     i18n("JPEG"));
0103     d->comboBox->insertItem(Private::PNG,      i18n("PNG"));
0104     d->comboBox->insertItem(Private::TIFF,     i18n("TIFF"));
0105     d->comboBox->insertItem(Private::JPEG2000, i18n("JPEG 2000"));
0106     d->comboBox->insertItem(Private::JPEGXL,   i18n("JPEG XL"));
0107     d->comboBox->insertItem(Private::AVIF,     i18n("AVIF"));
0108     d->comboBox->insertItem(Private::HEIF,     i18n("HEIF"));
0109     d->comboBox->insertItem(Private::WEBP,     i18n("WEBP"));
0110     d->comboBox->setDefaultIndex(Private::Input);
0111 
0112     QLabel* const label2 = new QLabel(vbox);
0113     label2->setText(i18n("Shell Script:"));
0114 
0115     d->textEdit          = new QPlainTextEdit(vbox);
0116     d->textEdit->setPlaceholderText(i18n("Enter script for execution. Use $INPUT and $OUTPUT for input / output filenames (with "
0117                                          "special characters escaped). These would be substituted before shell execution."));
0118 
0119     QLabel* const label3 = new QLabel(i18n("<b>Note:</b> Environment variables TITLE, COMMENTS, COLORLABEL, PICKLABEL, "
0120                                            "RATING and TAGSPATH (separated by ;) are available."), vbox);
0121     label3->setWordWrap(true);
0122     label3->setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
0123 
0124     QLabel* const space  = new QLabel(vbox);
0125     vbox->setStretchFactor(space, 10);
0126 
0127     m_settingsWidget     = vbox;
0128 
0129     connect(d->comboBox, SIGNAL(activated(int)),
0130             this, SLOT(slotSettingsChanged()));
0131 
0132     connect(d->textEdit, SIGNAL(textChanged()),
0133             this, SLOT(slotSettingsChanged()));
0134 
0135     BatchTool::registerSettingsWidget();
0136 }
0137 
0138 BatchToolSettings UserScript::defaultSettings()
0139 {
0140     BatchToolSettings settings;
0141     settings.insert(QLatin1String("Output filetype"), d->comboBox->defaultIndex());
0142     settings.insert(QLatin1String("Script"),          QString());
0143 
0144     return settings;
0145 }
0146 
0147 void UserScript::slotAssignSettings2Widget()
0148 {
0149     d->changeSettings = false;
0150     d->comboBox->setCurrentIndex(settings()[QLatin1String("Output filetype")].toInt());
0151 
0152     QString txt       = settings()[QLatin1String("Script")].toString();
0153 
0154     if (d->textEdit->toPlainText() != txt)
0155     {
0156         d->textEdit->setPlainText(txt);
0157     }
0158 
0159     d->changeSettings = true;
0160 }
0161 
0162 void UserScript::slotSettingsChanged()
0163 {
0164     if (d->changeSettings)
0165     {
0166         BatchToolSettings settings;
0167         settings.insert(QLatin1String("Output filetype"), d->comboBox->currentIndex());
0168         settings.insert(QLatin1String("Script"),          d->textEdit->toPlainText());
0169         BatchTool::slotSettingsChanged(settings);
0170     }
0171 }
0172 
0173 QString UserScript::outputSuffix () const
0174 {
0175     int filetype = settings()[QLatin1String("Output filetype")].toInt();
0176 
0177     switch (filetype)
0178     {
0179         case Private::JPEG:
0180         {
0181             return QLatin1String("jpg");
0182         }
0183 
0184         case Private::PNG:
0185         {
0186             return QLatin1String("png");
0187         }
0188 
0189         case Private::TIFF:
0190         {
0191             return QLatin1String("tif");
0192         }
0193 
0194         case Private::JPEG2000:
0195         {
0196             return QLatin1String("jp2");
0197         }
0198 
0199         case Private::JPEGXL:
0200         {
0201             return QLatin1String("jxl");
0202         }
0203 
0204         case Private::AVIF:
0205         {
0206             return QLatin1String("avif");
0207         }
0208 
0209         case Private::HEIF:
0210         {
0211             return QLatin1String("heic");
0212         }
0213 
0214         case Private::WEBP:
0215         {
0216             return QLatin1String("webp");
0217         }
0218 
0219         default:
0220         {
0221             break;
0222         }
0223     }
0224 
0225     // Return "": use original type
0226 
0227     return (BatchTool::outputSuffix());
0228 }
0229 
0230 bool UserScript::toolOperations()
0231 {
0232     QString script = settings()[QLatin1String("Script")].toString();
0233 
0234     if (script.isEmpty())
0235     {
0236         setErrorDescription(i18n("User Script: No script."));
0237 
0238         return false;
0239     }
0240 
0241     // Replace all occurrences of $INPUT and $OUTPUT in script to file names. Case sensitive.
0242 
0243     script.replace(QLatin1String("$INPUT"),  QLatin1Char('"') +
0244                                              QDir::toNativeSeparators(inputUrl().toLocalFile()) +
0245                                              QLatin1Char('"'));
0246     script.replace(QLatin1String("$OUTPUT"), QLatin1Char('"') +
0247                                              QDir::toNativeSeparators(outputUrl().toLocalFile()) +
0248                                              QLatin1Char('"'));
0249 
0250     // Empties d->image, not to pass it to the next tool in chain
0251 
0252     setImageData(DImg());
0253 
0254     QProcess process(this);
0255 
0256     QProcessEnvironment env = adjustedEnvironmentForAppImage();
0257 
0258     QString tagPath         = TagsCache::instance()->tagPaths(imageInfo().tagIds(), TagsCache::NoLeadingSlash,
0259                                                               TagsCache::NoHiddenTags).join(QLatin1Char(';'));
0260 
0261     // Populate env variables from metadata
0262 
0263     env.insert(QLatin1String("COLORLABEL"), QString::number(imageInfo().colorLabel()));
0264     env.insert(QLatin1String("PICKLABEL"),  QString::number(imageInfo().pickLabel()));
0265     env.insert(QLatin1String("RATING"),     QString::number(imageInfo().rating()));
0266     env.insert(QLatin1String("COMMENTS"),   imageInfo().comment());
0267     env.insert(QLatin1String("TITLE"),      imageInfo().title());
0268     env.insert(QLatin1String("TAGSPATH"),   tagPath);
0269 
0270     process.setProcessEnvironment(env);
0271 
0272     // call the shell script
0273 
0274 #ifdef Q_OS_WIN
0275 
0276     QString dir                   = QDir::temp().path();
0277     SafeTemporaryFile* const temp = new SafeTemporaryFile(dir + QLatin1String("/UserScript-XXXXXX.cmd"));
0278     temp->setAutoRemove(false);
0279     temp->open();
0280     QString scriptPath            = temp->safeFilePath();
0281 
0282     // Crash fix: a QTemporaryFile is not properly closed until its destructor is called.
0283 
0284     delete temp;
0285 
0286     script.replace(QLatin1Char('\n'), QLatin1String("\r\n"));
0287 
0288     QFile file(scriptPath);
0289 
0290     if (file.open(QIODevice::WriteOnly))
0291     {
0292         file.write(script.toUtf8());
0293         file.close();
0294     }
0295     else
0296     {
0297         setErrorDescription(i18n("User Script: File open error."));
0298 
0299         return false;
0300     }
0301 
0302     process.start(QLatin1String("cmd.exe"), QStringList() << QLatin1String("/C") << scriptPath);
0303 
0304 #else
0305 
0306     process.start(QLatin1String("/bin/bash"), QStringList() << QLatin1String("-c") << script);
0307 
0308 #endif
0309 
0310     bool ret = true;
0311 
0312     if (!process.waitForFinished(60000))
0313     {
0314         setErrorDescription(i18n("User Script: Timeout from script."));
0315         process.kill();
0316 
0317         ret = false;
0318     }
0319 
0320     if      (process.exitCode() == 0)
0321     {
0322         setErrorDescription(i18n("User Script: No error."));
0323     }
0324     else if (process.exitCode() == -2)
0325     {
0326         setErrorDescription(i18n("User Script: Failed to start script."));
0327 
0328         ret = false;
0329     }
0330     else if (process.exitCode() == -1)
0331     {
0332         setErrorDescription(i18n("User Script: Script process crashed."));
0333 
0334         ret = false;
0335     }
0336     else if (process.exitCode() == 127)
0337     {
0338         setErrorDescription(i18n("User Script: Command not found."));
0339 
0340         ret = false;
0341     }
0342     else
0343     {
0344         setErrorDescription(i18n("User Script: Error code returned %1.", process.exitCode()));
0345 
0346         ret = false;
0347     }
0348 
0349 #ifdef Q_OS_WIN
0350 
0351     file.remove();
0352 
0353 #endif
0354 
0355     qCDebug(DIGIKAM_DPLUGIN_BQM_LOG) << "Script stdout"     << process.readAllStandardOutput();
0356     qCDebug(DIGIKAM_DPLUGIN_BQM_LOG) << "Script stderr"     << process.readAllStandardError();
0357     qCDebug(DIGIKAM_DPLUGIN_BQM_LOG) << "Script exit code:" << process.exitCode();
0358 
0359     return ret;
0360 }
0361 
0362 } // namespace DigikamBqmUserScriptPlugin
0363 
0364 #include "moc_userscript.cpp"