DlgAnimationRenderer.cpp 20.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*
 *  Copyright (c) 2016 Boudewijn Rempt <boud@valdyas.org>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "DlgAnimationRenderer.h"

21
#include <QStandardPaths>
22 23
#include <QPluginLoader>
#include <QJsonObject>
24
#include <QMessageBox>
25
#include <QStringList>
26
#include <QProcess>
27 28 29 30

#include <klocalizedstring.h>
#include <kpluginfactory.h>

31
#include <KoResourcePaths.h>
32 33 34 35 36
#include <kis_properties_configuration.h>
#include <kis_debug.h>
#include <KisMimeDatabase.h>
#include <KoJsonTrader.h>
#include <KisImportExportFilter.h>
37 38 39
#include <kis_image.h>
#include <kis_image_animation_interface.h>
#include <kis_time_range.h>
40
#include <KisImportExportManager.h>
41 42 43
#include <kis_config_widget.h>
#include <KisDocument.h>
#include <QHBoxLayout>
44
#include <KisImportExportFilter.h>
45 46
#include <kis_config.h>
#include <kis_file_name_requester.h>
47
#include <KisDocument.h>
48
#include <KoDialog.h>
49

50
DlgAnimationRenderer::DlgAnimationRenderer(KisDocument *doc, QWidget *parent)
51
    : KoDialog(parent)
52
    , m_image(doc->image())
53
    , m_doc(doc)
54
    , m_defaultFileName(QFileInfo(doc->url().toLocalFile()).completeBaseName())
55
{
56 57
    KisConfig cfg;

58 59 60 61
    setCaption(i18n("Render Animation"));
    setButtons(Ok | Cancel);
    setDefaultButton(Ok);

62 63 64 65
    if (m_defaultFileName.isEmpty()) {
        m_defaultFileName = i18n("Untitled");
    }

66 67
    m_page = new WdgAnimaterionRenderer(this);
    m_page->layout()->setMargin(0);
68

69
    m_page->dirRequester->setMode(KoFileDialog::OpenDirectory);
70
    QString lastLocation = cfg.readEntry<QString>("AnimationRenderer/last_sequence_export_location", QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));
71
    m_page->dirRequester->setFileName(lastLocation);
72

73 74 75
    m_page->intStart->setMinimum(doc->image()->animationInterface()->fullClipRange().start());
    m_page->intStart->setMaximum(doc->image()->animationInterface()->fullClipRange().end());
    m_page->intStart->setValue(doc->image()->animationInterface()->playbackRange().start());
76

77 78 79
    m_page->intEnd->setMinimum(doc->image()->animationInterface()->fullClipRange().start());
    m_page->intEnd->setMaximum(doc->image()->animationInterface()->fullClipRange().end());
    m_page->intEnd->setValue(doc->image()->animationInterface()->playbackRange().end());
80

81 82 83 84 85
    QFileInfo audioFileInfo(doc->image()->animationInterface()->audioChannelFileName());
    const bool hasAudio = audioFileInfo.exists();
    m_page->chkIncludeAudio->setEnabled(hasAudio);
    m_page->chkIncludeAudio->setChecked(hasAudio && !doc->image()->animationInterface()->isAudioMuted());

86 87 88 89 90 91 92 93 94 95 96 97 98 99
    QStringList mimes = KisImportExportManager::mimeFilter(KisImportExportManager::Export);
    mimes.sort();
    Q_FOREACH(const QString &mime, mimes) {
        QString description = KisMimeDatabase::descriptionForMimeType(mime);
        if (description.isEmpty()) {
            description = mime;
        }
        m_page->cmbMimetype->addItem(description, mime);
        if (mime == "image/png") {
            m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->count() - 1);
        }

    }

100 101 102 103 104 105
    setMainWidget(m_page);

    KoJsonTrader trader;
    QList<QPluginLoader *>list = trader.query("Krita/AnimationExporter", "");
    Q_FOREACH(QPluginLoader *loader, list) {
        QJsonObject json = loader->metaData().value("MetaData").toObject();
106 107
        QStringList mimetypes = json.value("X-KDE-Export").toString().split(",");
        Q_FOREACH(const QString &mime, mimetypes) {
108 109 110 111 112 113 114

            KLibFactory *factory = qobject_cast<KLibFactory *>(loader->instance());
            if (!factory) {
                warnUI << loader->errorString();
                continue;
            }

115
            QObject* obj = factory->create<KisImportExportFilter>(0);
116 117 118 119 120
            if (!obj || !obj->inherits("KisImportExportFilter")) {
                delete obj;
                continue;
            }

121
            QSharedPointer<KisImportExportFilter>filter(static_cast<KisImportExportFilter*>(obj));
122 123 124 125 126
            if (!filter) {
                delete obj;
                continue;
            }

Boudewijn Rempt's avatar
Boudewijn Rempt committed
127
            m_renderFilters.append(filter);
128

129 130 131 132 133
            QString description = KisMimeDatabase::descriptionForMimeType(mime);
            if (description.isEmpty()) {
                description = mime;
            }
            m_page->cmbRenderType->addItem(description, mime);
134

135 136
        }
    }
137
    m_page->videoFilename->setMode(KoFileDialog::SaveFile);
138
    m_page->videoFilename->setStartDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));
139

140
    qDeleteAll(list);
141

142
    connect(m_page->bnExportOptions, SIGNAL(clicked()), this, SLOT(sequenceMimeTypeSelected()));
143
    connect(m_page->bnRenderOptions, SIGNAL(clicked()), this, SLOT(selectRenderOptions()));
144

145
    m_page->ffmpegLocation->setFileName(findFFMpeg());
146
    m_page->ffmpegLocation->setMode(KoFileDialog::OpenFile);
147
    connect(m_page->ffmpegLocation, SIGNAL(fileSelected(QString)), this, SLOT(ffmpegLocationChanged(QString)));
148

149
    m_page->cmbRenderType->setCurrentIndex(cfg.readEntry<int>("AnimationRenderer/render_type", 0));
150

151 152 153 154 155 156 157

    connect(m_page->shouldExportOnlyImageSequence, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged()));
    connect(m_page->shouldExportOnlyVideo, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged()));
    connect(m_page->shouldExportAll, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged()));

    updateExportUIOptions();

158
    // connect and cold init
159
    connect(m_page->cmbRenderType, SIGNAL(currentIndexChanged(int)), this, SLOT(selectRenderType(int)));
160
    selectRenderType(m_page->cmbRenderType->currentIndex());
161 162

    resize(m_page->sizeHint());
163 164
}

165
DlgAnimationRenderer::~DlgAnimationRenderer()
166
{
167
    KisConfig cfg;
168

169 170
    cfg.writeEntry<QString>("AnimationRenderer/last_sequence_export_location", m_page->dirRequester->fileName());
    cfg.writeEntry<int>("AnimationRenderer/render_type", m_page->cmbRenderType->currentIndex());
171
    cfg.setCustomFFMpegPath(m_page->ffmpegLocation->fileName());
172 173 174 175 176 177 178 179 180

    if (m_encoderConfigWidget)  {
        m_encoderConfigWidget->setParent(0);
        m_encoderConfigWidget->deleteLater();
    }
    if (m_frameExportConfigWidget) {
        m_frameExportConfigWidget->setParent(0);
        m_frameExportConfigWidget->deleteLater();
    }
181

182
    delete m_page;
183

184 185
}

186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
QString DlgAnimationRenderer::fetchRenderingDirectory() const
{
    QString result = m_page->dirRequester->fileName();

    if (m_page->shouldExportOnlyVideo->isChecked()) {
        const QFileInfo info(fetchRenderingFileName());

        if (info.isAbsolute()) {
            result = info.absolutePath();
        }
    }

    return result;
}

QString DlgAnimationRenderer::fetchRenderingFileName() const
{
    QString filename = m_page->videoFilename->fileName();
204

205 206 207 208
    if (QFileInfo(filename).completeSuffix().isEmpty()) {
        QString mimetype = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString();
        filename += "." + KisMimeDatabase::suffixesForMimeType(mimetype).first();
    }
209 210 211 212 213 214 215 216 217 218 219 220 221 222

    if (QFileInfo(filename).isRelative()) {
        QDir baseDir(m_page->dirRequester->fileName());

        if (m_page->shouldExportOnlyVideo->isChecked()) {
            QString documentDir = QFileInfo(m_doc->url().toLocalFile()).absolutePath();
            if (!documentDir.isEmpty()) {
                baseDir = documentDir;
            }
        }

        filename = baseDir.absoluteFilePath(filename);
    }

223 224 225
    return filename;
}

226
KisPropertiesConfigurationSP DlgAnimationRenderer::getSequenceConfiguration() const
227 228 229
{
    KisPropertiesConfigurationSP cfg = new KisPropertiesConfiguration();
    cfg->setProperty("basename", m_page->txtBasename->text());
230
    cfg->setProperty("last_document_path", m_doc->localFilePath());
231
    cfg->setProperty("directory", fetchRenderingDirectory());
232 233 234
    cfg->setProperty("first_frame", m_page->intStart->value());
    cfg->setProperty("last_frame", m_page->intEnd->value());
    cfg->setProperty("sequence_start", m_page->sequenceStart->value());
235
    cfg->setProperty("mimetype", m_page->cmbMimetype->currentData().toString());
236 237 238
    return cfg;
}

239
void DlgAnimationRenderer::setSequenceConfiguration(KisPropertiesConfigurationSP cfg)
240 241
{
    m_page->txtBasename->setText(cfg->getString("basename", "frame"));
242 243 244 245 246 247 248

    if (cfg->getString("last_document_path") != m_doc->localFilePath()) {
        cfg->removeProperty("first_frame");
        cfg->removeProperty("last_frame");
        cfg->removeProperty("sequence_start");
    }

249 250 251 252
    m_page->dirRequester->setFileName(cfg->getString("directory", QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)));
    m_page->intStart->setValue(cfg->getInt("first_frame", m_image->animationInterface()->playbackRange().start()));
    m_page->intEnd->setValue(cfg->getInt("last_frame", m_image->animationInterface()->playbackRange().end()));
    m_page->sequenceStart->setValue(cfg->getInt("sequence_start", m_image->animationInterface()->playbackRange().start()));
253 254 255 256 257 258 259
    QString mimetype = cfg->getString("mimetype");
    for (int i = 0; i < m_page->cmbMimetype->count(); ++i) {
        if (m_page->cmbMimetype->itemData(i).toString() == mimetype) {
            m_page->cmbMimetype->setCurrentIndex(i);
            break;
        }
    }
260 261
}

262
KisPropertiesConfigurationSP DlgAnimationRenderer::getFrameExportConfiguration() const
263
{
264
    if (m_frameExportConfigWidget) {
265 266
        KisPropertiesConfigurationSP cfg = m_frameExportConfigWidget->configuration();
        cfg->setProperty("basename", m_page->txtBasename->text());
267
        cfg->setProperty("directory", fetchRenderingDirectory());
268 269 270
        cfg->setProperty("first_frame", m_page->intStart->value());
        cfg->setProperty("last_frame", m_page->intEnd->value());
        cfg->setProperty("sequence_start", m_page->sequenceStart->value());
271
        cfg->setProperty("ffmpeg_path", m_page->ffmpegLocation->fileName());
272

273
        return m_frameExportConfigWidget->configuration();
274 275 276 277
    }
    return 0;
}

278
KisPropertiesConfigurationSP DlgAnimationRenderer::getVideoConfiguration() const
279
{
280 281
    // don't continue if we are only exporting image sequence
    if (m_page->shouldExportOnlyImageSequence->isChecked()) {
282 283
        return 0;
    }
284

285
    KisPropertiesConfigurationSP cfg = new KisPropertiesConfiguration();
286
    cfg->setProperty("filename", fetchRenderingFileName());
287 288 289
    cfg->setProperty("first_frame", m_page->intStart->value());
    cfg->setProperty("last_frame", m_page->intEnd->value());
    cfg->setProperty("sequence_start", m_page->sequenceStart->value());
290 291 292 293 294 295


    // delete image sequence if we are only exporting out video
    cfg->setProperty("delete_sequence", m_page->shouldExportOnlyVideo->isChecked());


296 297 298
    return cfg;
}

299
void DlgAnimationRenderer::setVideoConfiguration(KisPropertiesConfigurationSP /*cfg*/)
300 301 302
{
}

303
KisPropertiesConfigurationSP DlgAnimationRenderer::getEncoderConfiguration() const
304
{
305 306
    // don't continue if we are only exporting image sequence
    if (m_page->shouldExportOnlyImageSequence->isChecked()) {
307 308
        return 0;
    }
309

310
    KisPropertiesConfigurationSP cfg = new KisPropertiesConfiguration();
311 312 313
    if (m_encoderConfigWidget) {
        cfg = m_encoderConfigWidget->configuration();
    }
314
    cfg->setProperty("mimetype", m_page->cmbRenderType->currentData().toString());
315
    cfg->setProperty("directory", fetchRenderingDirectory());
316 317 318
    cfg->setProperty("first_frame", m_page->intStart->value());
    cfg->setProperty("last_frame", m_page->intEnd->value());
    cfg->setProperty("sequence_start", m_page->sequenceStart->value());
319
    cfg->setProperty("include_audio", m_page->chkIncludeAudio->isChecked());
320

321 322 323
    return cfg;
}

324
void DlgAnimationRenderer::setEncoderConfiguration(KisPropertiesConfigurationSP /*cfg*/)
325 326 327 328
{

}

329 330 331 332 333
QSharedPointer<KisImportExportFilter> DlgAnimationRenderer::encoderFilter() const
{
    if (m_page->cmbRenderType->currentIndex() < m_renderFilters.size()) {
        return m_renderFilters[m_page->cmbRenderType->currentIndex()];
    }
Boudewijn Rempt's avatar
Boudewijn Rempt committed
334
    return QSharedPointer<KisImportExportFilter>(0);
335 336
}

337 338 339 340 341 342 343 344 345
void DlgAnimationRenderer::selectRenderType(int index)
{
    if (index >= m_renderFilters.size()) return;
    QString mimetype = m_page->cmbRenderType->itemData(index).toString();

    if (!m_page->videoFilename->fileName().isEmpty() && QFileInfo(m_page->videoFilename->fileName()).completeBaseName() != m_defaultFileName) {
        m_defaultFileName = QFileInfo(m_page->videoFilename->fileName()).completeBaseName();
    }
    m_page->videoFilename->setMimeTypeFilters(QStringList() << mimetype, mimetype);
346

347 348 349 350
    m_page->videoFilename->setFileName(m_defaultFileName + "." + KisMimeDatabase::suffixesForMimeType(mimetype).first());
}

void DlgAnimationRenderer::selectRenderOptions()
351
{
352 353
    int index = m_page->cmbRenderType->currentIndex();

354 355 356 357 358 359 360 361
    if (m_encoderConfigWidget) {
        m_encoderConfigWidget->deleteLater();
        m_encoderConfigWidget = 0;
    }

    if (index >= m_renderFilters.size()) return;

    QSharedPointer<KisImportExportFilter> filter = m_renderFilters[index];
362
    QString mimetype = m_page->cmbRenderType->itemData(index).toString();
363
    if (filter) {
364
        m_encoderConfigWidget = filter->createConfigurationWidget(0, KisDocument::nativeFormatMimeType(), mimetype.toLatin1());
365
        if (m_encoderConfigWidget) {
366 367
            m_encoderConfigWidget->setConfiguration(filter->lastSavedConfiguration("", mimetype.toLatin1()));
            KoDialog dlg(this);
368 369 370 371 372 373 374 375
            dlg.setMainWidget(m_encoderConfigWidget);
            dlg.setButtons(KoDialog::Ok | KoDialog::Cancel);
            if (!dlg.exec()) {
                m_encoderConfigWidget->setConfiguration(filter->lastSavedConfiguration());
            }
            dlg.setMainWidget(0);
            m_encoderConfigWidget->hide();
            m_encoderConfigWidget->setParent(0);
376 377
        }
    }
378 379 380
    else {
        m_encoderConfigWidget = 0;
    }
381

382
}
383

384
void DlgAnimationRenderer::sequenceMimeTypeSelected()
385
{
386 387
    int index = m_page->cmbMimetype->currentIndex();

388 389 390
    if (m_frameExportConfigWidget) {
        m_frameExportConfigWidget->deleteLater();
        m_frameExportConfigWidget = 0;
391
    }
392

393
    QString mimetype = m_page->cmbMimetype->itemData(index).toString();
394
    QSharedPointer<KisImportExportFilter> filter(KisImportExportManager::filterForMimeType(mimetype, KisImportExportManager::Export));
395
    if (filter) {
396
        m_frameExportConfigWidget = filter->createConfigurationWidget(0, KisDocument::nativeFormatMimeType(), mimetype.toLatin1());
397
        if (m_frameExportConfigWidget) {
398 399
            m_frameExportConfigWidget->setConfiguration(filter->lastSavedConfiguration("", mimetype.toLatin1()));
            KoDialog dlg(this);
400 401 402 403 404 405 406 407
            dlg.setMainWidget(m_frameExportConfigWidget);
            dlg.setButtons(KoDialog::Ok | KoDialog::Cancel);
            if (!dlg.exec()) {
                m_frameExportConfigWidget->setConfiguration(filter->lastSavedConfiguration());
            }
            m_frameExportConfigWidget->hide();
            m_frameExportConfigWidget->setParent(0);
            dlg.setMainWidget(0);
408
        }
409 410
    }
}
411

412 413 414
void DlgAnimationRenderer::ffmpegLocationChanged(const QString &s)
{
    KisConfig cfg;
415
    cfg.setCustomFFMpegPath(s);
416 417
}

418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
void DlgAnimationRenderer::updateExportUIOptions() {

    KisConfig cfg;

    // read in what type to export to. Defaults to image sequence only
    QString exportType = cfg.readEntry<QString>("AnimationRenderer/export_type", "ImageSequence");
    if (exportType == "ImageSequence") {
        m_page->shouldExportOnlyImageSequence->setChecked(true);
    } else if (exportType == "Video") {
        m_page->shouldExportOnlyVideo->setChecked(true);
    } else {
        m_page->shouldExportAll->setChecked(true); // export to both
    }
}

433 434
void DlgAnimationRenderer::slotButtonClicked(int button)
{
435
    if (button == KoDialog::Ok && !m_page->shouldExportOnlyImageSequence->isChecked()) {
436
        QString ffmpeg = m_page->ffmpegLocation->fileName();
437 438 439 440 441
        if (m_page->videoFilename->fileName().isEmpty()) {
            QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Please enter a file name to render to."));
            return;
        }
        else if (ffmpeg.isEmpty()) {
442 443 444 445 446 447 448
            QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("The location of FFmpeg is unknown. Please install FFmpeg first: Krita cannot render animations without FFmpeg. (<a href=\"https://www.ffmpeg.org\">www.ffmpeg.org</a>)"));
            return;
        }
        else {
            QFileInfo fi(ffmpeg);
            if (!fi.exists()) {
                QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("The location of FFmpeg is invalid. Please select the correct location of the FFmpeg executable on your system."));
449
                return;
450 451 452 453 454 455
            }
        }
    }
    KoDialog::slotButtonClicked(button);
}

456 457 458 459 460 461 462 463 464 465
QString DlgAnimationRenderer::findFFMpeg()
{
    QString result;

    QStringList proposedPaths;

    QString customPath = KisConfig().customFFMpegPath();
    proposedPaths << customPath;
    proposedPaths << customPath + QDir::separator() + "ffmpeg";

466 467 468
    proposedPaths << QDir::homePath() + "/bin/ffmpeg";
    proposedPaths << "/usr/bin/ffmpeg";
    proposedPaths << "/usr/local/bin/ffmpeg";
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
    proposedPaths << KoResourcePaths::getApplicationRoot() +
        QDir::separator() + "bin" + QDir::separator() + "ffmpeg";

    Q_FOREACH (const QString &path, proposedPaths) {
        if (path.isEmpty()) continue;

        QProcess testProcess;
        testProcess.start(path, QStringList() << "-version");
        testProcess.waitForFinished(1000);

        const bool successfulStart =
            testProcess.state() == QProcess::NotRunning &&
            testProcess.error() == QProcess::UnknownError;

        if (successfulStart) {
            result = path;
            break;
        }
    }

    return result;
}

492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509
void DlgAnimationRenderer::slotExportTypeChanged()
{
    KisConfig cfg;

    // if a video format needs to be outputted
    if (m_page->shouldExportAll->isChecked() || m_page->shouldExportOnlyVideo->isChecked()) {

         // videos always uses PNG for creating video, so disable the ability to change the format
         m_page->cmbMimetype->setEnabled(false);
         for (int i = 0; i < m_page->cmbMimetype->count(); ++i) {
             if (m_page->cmbMimetype->itemData(i).toString() == "image/png") {
                 m_page->cmbMimetype->setCurrentIndex(i);
                 break;
             }
         }
    }

    // if only exporting video
510
    if (m_page->shouldExportOnlyVideo->isChecked()) {
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
        m_page->cmbMimetype->setEnabled(false); // allow to change image format
        m_page->imageSequenceOptionsGroup->setVisible(false);
        m_page->videoOptionsGroup->setVisible(false); //shrinks the horizontal space temporarily to help resize() work
        m_page->videoOptionsGroup->setVisible(true);

        cfg.writeEntry<QString>("AnimationRenderer/export_type", "Video");
    }


    // if only an image sequence needs to be output
    if (m_page->shouldExportOnlyImageSequence->isChecked()) {
        m_page->cmbMimetype->setEnabled(true); // allow to change image format
        m_page->videoOptionsGroup->setVisible(false);
        m_page->imageSequenceOptionsGroup->setVisible(true);

        cfg.writeEntry<QString>("AnimationRenderer/export_type", "ImageSequence");
    }

    // show all options
     if (m_page->shouldExportAll->isChecked() ) {
         m_page->imageSequenceOptionsGroup->setVisible(true);
         m_page->videoOptionsGroup->setVisible(true);

         cfg.writeEntry<QString>("AnimationRenderer/export_type", "VideoAndImageSequence");
     }


     // for the resize to work as expected, try to hide elements first before displaying other ones.
     // if the widget gets bigger at any point, the resize will use that, even if elements are hidden later to make it smaller
     resize(m_page->sizeHint());
}