Krita Source Code Documentation
Loading...
Searching...
No Matches
KisDlgAnimationRenderer.cpp
Go to the documentation of this file.
1
2/*
3 * SPDX-FileCopyrightText: 2016 Boudewijn Rempt <boud@valdyas.org>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
9
10#include <QStandardPaths>
11#include <QPluginLoader>
12#include <QJsonObject>
13#include <QJsonArray>
14#include <QMessageBox>
15#include <QStringList>
16#include <QProcess>
17
18#include <klocalizedstring.h>
19#include <kpluginfactory.h>
20
21#include <KoResourcePaths.h>
23#include <kis_debug.h>
24#include <KisMimeDatabase.h>
25#include <KoJsonTrader.h>
28#include <kis_icon_utils.h>
29#include <kis_image.h>
31#include <kis_time_span.h>
33#include <kis_config_widget.h>
34#include <kis_signals_blocker.h>
35#include <KisDocument.h>
36#include <QHBoxLayout>
37#include <kis_config.h>
39#include <KoDialog.h>
41#include "kis_slider_spin_box.h"
43#include "KisVideoSaver.h"
45#include "kis_image_config.h"
46
47#ifdef Q_OS_ANDROID
48#include <QButtonGroup>
49#include <QJsonDocument>
52#else
55#endif
56
57
59 : KoDialog(parent)
60 , m_image(doc->image())
61 , m_doc(doc)
62{
63 KisConfig cfg(true);
64
65 setCaption(i18n("Render Animation"));
68
69 m_page = new WdgAnimationRenderer(this);
70 m_page->layout()->setContentsMargins(0, 0, 0, 0);
71
72 {
73 QIcon editIcon = KisIconUtils::loadIcon("document-edit");
74 m_page->bnExportOptions->setIcon(editIcon);
75 m_page->bnRenderOptions->setIcon(editIcon);
76 }
77
78 m_page->lblWarnings->hide();
79 m_page->lblWarnings->setPixmap(KisIconUtils::loadIcon(QStringLiteral("dialog-warning")).pixmap(32, 32));
80
81#ifdef Q_OS_ANDROID
82 m_exportButtonGroup = new QButtonGroup(this);
83 m_exportButtonGroup->addButton(m_page->bnExportImages);
84 m_exportButtonGroup->addButton(m_page->bnExportVideo);
85 m_page->shouldExportOnlyImageSequence->setChecked(false);
86 m_page->shouldExportOnlyVideo->setChecked(false);
87 m_page->shouldExportOnlyImageSequence->setCheckable(false);
88 m_page->shouldExportOnlyVideo->setCheckable(false);
89 m_page->pgImages->layout()->addWidget(m_page->shouldExportOnlyImageSequence);
90 m_page->pgVideo->layout()->addWidget(m_page->shouldExportOnlyVideo);
91 m_page->lblVideoFilenameTitle->hide();
92 m_page->videoFilename->hide();
93 m_page->lblDirRequester->hide();
94 m_page->dirRequester->hide();
95 m_page->lblFFMpegLocationTitle->hide();
96 m_page->ffmpegLocation->hide();
97 m_page->lblFFMpegVersionTitle->hide();
98 m_page->lblFFMpegVersion->hide();
99#else
100 m_page->wdgExportButtons->hide();
101 m_page->stkExport->hide();
102#endif
103
104 m_page->dirRequester->setMode(KoFileDialog::OpenDirectory);
105
106 m_page->intStart->setMinimum(0);
107 m_page->intStart->setMaximum(doc->image()->animationInterface()->documentPlaybackRange().end());
108 m_page->intEnd->setMinimum(doc->image()->animationInterface()->documentPlaybackRange().start());
109
110 m_page->intHeight->setMinimum(1);
111 m_page->intHeight->setMaximum(100000);
112
113 m_page->intWidth->setMinimum(1);
114 m_page->intWidth->setMaximum(100000);
115
116 // Setup audio...
117 QVector<QFileInfo> audioFiles = doc->getAudioTracks();
118 const bool hasAudio = audioFiles.count() > 0;
119 m_page->chkIncludeAudio->setEnabled(hasAudio);
120
121 // Setup image mimeTypes...
123 mimes.sort();
125 Q_FOREACH(const QString &mime, mimes) {
126 QString description = KisMimeDatabase::descriptionForMimeType(mime);
127 if (description.isEmpty()) {
128 description = mime;
129 }
130
131 m_page->cmbMimetype->addItem(description, mime);
132
133 if (mime == "image/png") {
134 m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->count() - 1);
135 }
136 }
137
138#ifdef Q_OS_ANDROID
139 // Set up video export formats on Android. No ffmpeg here.
140 m_videoFormatPreferences = loadVideoFormatPreferences();
142 for (KisMediaEncoderFormat *videoFormat : videoFormats) {
143 m_page->cmbRenderType->addItem(videoFormat->title(), QVariant(videoFormat->key()));
144 }
145#endif
146
147 m_page->cmbScaleFilter->addItem(i18nc("bicubic filtering", "bicubic"), "bicubic");
148 m_page->cmbScaleFilter->addItem(i18nc("bilinear filtering", "bilinear"), "bilinear");
149 m_page->cmbScaleFilter->addItem(i18nc("lanczos3 filtering", "lanczos3"), "lanczos");
150 m_page->cmbScaleFilter->addItem(i18nc("nearest neighbor filtering", "neighbor"), "neighbor");
151 m_page->cmbScaleFilter->addItem(i18nc("spline filtering", "spline"), "spline");
152
153#ifndef Q_OS_ANDROID
154 m_page->videoFilename->setMode(KoFileDialog::SaveFile);
155 m_page->ffmpegLocation->setMode(KoFileDialog::OpenFile);
156#endif
157
158 m_page->cmbRenderType->setPlaceholderText(i18nc("Not applicable. No render types without valid ffmpeg path.", "N/A"));
159
160 { // Establish connections...
161 connect(m_page->bnExportOptions, SIGNAL(clicked()), this, SLOT(sequenceMimeTypeOptionsClicked()));
162 connect(m_page->bnRenderOptions, SIGNAL(clicked()), this, SLOT(selectRenderOptions()));
163
164#ifdef Q_OS_ANDROID
165 connect(m_exportButtonGroup,
166 QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked),
167 this,
169#else
170 connect(m_page->shouldExportOnlyImageSequence, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged()));
171 connect(m_page->shouldExportOnlyVideo, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged()));
172#endif
173 connect(m_page->cmbRenderType, SIGNAL(currentIndexChanged(int)), SLOT(slotRenderTypeChanged()));
174
175 connect(m_page->intFramesPerSecond, SIGNAL(valueChanged(int)), SLOT(slotCheckWarnings()));
176 connect(m_page->intWidth, SIGNAL(valueChanged(int)), SLOT(slotCheckWarnings()));
177 connect(m_page->intHeight, SIGNAL(valueChanged(int)), SLOT(slotCheckWarnings()));
178
179#ifndef Q_OS_ANDROID
180 connect(m_page->ffmpegLocation, SIGNAL(fileSelected(QString)), SLOT(setFFmpegPath(QString)));
181#endif
182
183 connect(this, SIGNAL(accepted()), SLOT(slotDialogAccepted()));
184 }
185
186
187 // try to lock the width and height being updated
188 KisAcyclicSignalConnector *constrainsConnector = new KisAcyclicSignalConnector(this);
189 constrainsConnector->createCoordinatedConnector()->connectBackwardInt(m_page->intWidth, SIGNAL(valueChanged(int)), this, SLOT(slotLockAspectRatioDimensionsWidth(int)));
190 constrainsConnector->createCoordinatedConnector()->connectForwardInt(m_page->intHeight, SIGNAL(valueChanged(int)), this, SLOT(slotLockAspectRatioDimensionsHeight(int)));
191
192 { // Initialize settings from last configuration...
193 KisPropertiesConfigurationSP animProperties = loadLastConfiguration("ANIMATION_EXPORT");
195 options.fromProperties(animProperties);
196
197 initializeRenderSettings(*doc, options);
198 }
199
202}
203
208
210{
211#ifndef Q_OS_ANDROID
212 // Initialize FFmpeg location... (!)
213 KisConfig cfg(false);
214 QString cfgFFmpegPath = cfg.ffmpegLocation();
215#ifdef Q_OS_MACOS
216 if (cfgFFmpegPath.isEmpty()) {
217 QJsonObject ffmpegInfo = KisFFMpegWrapper::findFFMpeg(cfgFFmpegPath);
218 cfgFFmpegPath = (ffmpegInfo["enabled"].toBool()) ? ffmpegInfo["path"].toString() : "";
219 }
220#endif
221
222 // Check known ffmpeg locations..
223 QString likelyFFmpegPath = [&]() {
224 // Check last used
225 if (!lastUsedOptions.ffmpegPath.isEmpty() && QFileInfo(lastUsedOptions.ffmpegPath).isExecutable()) {
226 return lastUsedOptions.ffmpegPath;
227 }
228
229 // Check krita config
230 if (!cfgFFmpegPath.isEmpty() && QFileInfo(cfgFFmpegPath).isExecutable()) {
231 return cfgFFmpegPath;
232 }
233
234 // Check standard paths
235 QString systemFFmpeg = QStandardPaths::findExecutable("ffmpeg");
236 if (!systemFFmpeg.isEmpty() && QFileInfo(systemFFmpeg).isExecutable()) {
237 return systemFFmpeg;
238 }
239
240 // Find ffmpeg elsewhere...
241 QJsonObject ffmpegJsonObj = KisFFMpegWrapper::findFFMpeg("");
242 return (ffmpegJsonObj["enabled"].toBool()) ? ffmpegJsonObj["path"].toString() : "";
243 }();
244
245 if (!likelyFFmpegPath.isEmpty() && QFileInfo(likelyFFmpegPath).isExecutable()) {
246 setFFmpegPath(likelyFFmpegPath);
247 }
248#endif
249
250 const QString documentPath = m_doc->localFilePath();
251
252 // Initialize these settings based on last used configuration when possible..
253 if (!lastUsedOptions.lastDocumentPath.isEmpty() &&
254 lastUsedOptions.lastDocumentPath == documentPath) {
255
256 // If the file is the same as last time, we use the last used basename.
257 m_page->txtBasename->setText(lastUsedOptions.basename);
258
259 m_page->sequenceStart->setValue(lastUsedOptions.sequenceStart);
260 m_page->intWidth->setValue(lastUsedOptions.width);
261 m_page->intHeight->setValue(lastUsedOptions.height);
262
263#ifdef Q_OS_ANDROID
264 m_page->dirRequester->setStartDir(documentPath);
265#else
266 m_page->videoFilename->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
267 m_page->videoFilename->setFileName(lastUsedOptions.videoFileName);
268 m_page->dirRequester->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
269#endif
270 m_page->dirRequester->setFileName(lastUsedOptions.directory);
271
272 } else {
273 m_page->sequenceStart->setValue(m_image->animationInterface()->activePlaybackRange().start());
274 m_page->intWidth->setValue(m_image->width());
275 m_page->intHeight->setValue(m_image->height());
276
277#ifdef Q_OS_ANDROID
278 m_page->dirRequester->setStartDir(documentPath);
279#else
280 m_page->videoFilename->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
281 m_page->videoFilename->setFileName(defaultVideoFileName(m_doc, lastUsedOptions.videoMimeType));
282 m_page->dirRequester->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
283#endif
284 m_page->dirRequester->setFileName(lastUsedOptions.directory);
285 }
286
287 // Initialize FRAME render format...
288 for (int i = 0; i < m_page->cmbMimetype->count(); ++i) {
289 if (m_page->cmbMimetype->itemData(i).toString() == lastUsedOptions.frameMimeType) {
290 m_page->cmbMimetype->setCurrentIndex(i);
291 break;
292 }
293 }
294
295 for (int i = 0; i < m_page->cmbScaleFilter->count(); ++i) {
296 if (m_page->cmbScaleFilter->itemData(i).toString() == lastUsedOptions.scaleFilter) {
297 m_page->cmbScaleFilter->setCurrentIndex(i);
298 break;
299 }
300 }
301
302 // Initialize VIDEO render format...
303#ifdef Q_OS_ANDROID
304 const QString &lastVideoType = lastUsedOptions.videoFormatKey;
305#else
306 const QString &lastVideoType = lastUsedOptions.videoMimeType;
307#endif
308 for (int i = 0; i < m_page->cmbRenderType->count(); ++i) {
309 if (m_page->cmbRenderType->itemData(i).toString() == lastVideoType) {
310 m_page->cmbRenderType->setCurrentIndex(i);
311 break;
312 }
313 }
314
315 m_page->chkOnlyUniqueFrames->setChecked(lastUsedOptions.wantsOnlyUniqueFrameSequence);
316#ifdef Q_OS_ANDROID
317 if (lastUsedOptions.shouldEncodeVideo) {
318 m_page->bnExportVideo->setChecked(true);
319 } else {
320 m_page->bnExportImages->setChecked(true);
321 }
322#else
323 m_page->shouldExportOnlyVideo->setChecked(lastUsedOptions.shouldEncodeVideo);
324 m_page->shouldExportOnlyImageSequence->setChecked(!lastUsedOptions.shouldDeleteSequence);
325#endif
326
328
329#ifndef Q_OS_ANDROID
330 {
331 KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
332
333 QStringList encodersPresent;
334 Q_FOREACH(const QString& key, ffmpegEncoderTypes.keys()) {
335 encodersPresent << ffmpegEncoderTypes[key];
336 }
337
338 getDefaultVideoEncoderOptions(lastUsedOptions.videoMimeType, settings,
339 encodersPresent,
342 }
343
344 {
345 KisPropertiesConfigurationSP settings = loadLastConfiguration("img_sequence/" + lastUsedOptions.frameMimeType);
347 }
348
349 m_page->ffmpegLocation->setFileName(likelyFFmpegPath);
350 m_page->ffmpegLocation->setStartDir(QFileInfo(m_doc->localFilePath()).path());
351 m_page->ffmpegLocation->setReadOnlyText(true);
352#endif
353
354 // Initialize these settings based on the current document context..
355 m_page->intStart->setValue(doc.image()->animationInterface()->activePlaybackRange().start());
356 m_page->intEnd->setValue(doc.image()->animationInterface()->activePlaybackRange().end());
357 m_page->intFramesPerSecond->setValue(doc.image()->animationInterface()->framerate());
358
359 if (!doc.image()->animationInterface()->exportSequenceFilePath().isEmpty()
360 && QDir(doc.image()->animationInterface()->exportSequenceFilePath()).exists() ) {
361 m_page->dirRequester->setStartDir(doc.image()->animationInterface()->exportSequenceFilePath());
362 m_page->dirRequester->setFileName(doc.image()->animationInterface()->exportSequenceFilePath());
363 }
364
365 if (!doc.image()->animationInterface()->exportSequenceBaseName().isEmpty()) {
366 m_page->txtBasename->setText(doc.image()->animationInterface()->exportSequenceBaseName());
367 }
368
369 if (doc.image()->animationInterface()->exportInitialFrameNumber() != -1) {
370 m_page->sequenceStart->setValue(doc.image()->animationInterface()->exportInitialFrameNumber());
371 }
372
373 bool hasAudioLoaded = doc.getAudioTracks().count() > 0;
374 m_page->chkIncludeAudio->setChecked(hasAudioLoaded);
376}
377
379{
380#ifdef Q_OS_ANDROID
381 return !wantVideoExport();
382#else
383 return m_page->shouldExportOnlyImageSequence->isChecked();
384#endif
385}
386
388{
389#ifdef Q_OS_ANDROID
390 return m_page->bnExportVideo->isChecked();
391#else
392 return m_page->shouldExportOnlyVideo->isChecked();
393#endif
394}
395
396#ifndef Q_OS_ANDROID
399 const QStringList &availableEncoders,
400 QString *customFFMpegOptionsString,
401 bool *renderHDR)
402{
405
406 QScopedPointer<KisVideoExportOptionsDialog> encoderConfigWidget(
407 new KisVideoExportOptionsDialog(containerType, availableEncoders, KisHDRMetadataOptions(), 0));
408
409 // we always enable HDR, letting the user to force it
410 encoderConfigWidget->setSupportsHDR(true);
411 encoderConfigWidget->setConfiguration(cfg);
412 *customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString();
413 *renderHDR = encoderConfigWidget->videoConfiguredForHDR();
414}
415#endif
416
418{
419 KritaUtils::filterContainer(mimeTypes, [](QString type) {
420 return (type.startsWith("image/")
421 || (type.startsWith("application/") &&
422 !type.startsWith("application/x-spriter")));
423 });
424}
425
426#ifndef Q_OS_ANDROID
428{
429 QStringList supportedMimeTypes = QStringList();
430 supportedMimeTypes << "video/x-matroska";
431 supportedMimeTypes << "video/mp4";
432 supportedMimeTypes << "video/webm";
433 supportedMimeTypes << "image/gif";
434 supportedMimeTypes << "image/apng";
435 supportedMimeTypes << "image/webp";
436 supportedMimeTypes << "video/ogg";
437
438 return supportedMimeTypes;
439}
440
443 Q_FOREACH(const KoID &encoder, encodersExpected ) {
444 if (encodersPresent.contains(encoder.id())) {
445 return true;
446 }
447 }
448 return false;
449}
450
451// For dependencies, see here:
452// https://en.wikipedia.org/wiki/Comparison_of_video_container_formats
454{
455 QStringList retValue;
456
457 Q_FOREACH(const QString& mime, input) {
458 if ( mime == "video/x-matroska" ) {
459 if ( ffmpegCodecs.contains("h264") || ffmpegCodecs.contains("vp9") ) {
460 QList<QString> encodersPresent;
461 encodersPresent << ffmpegEncoderTypes["h264"] << ffmpegEncoderTypes["vp9"];
463 retValue << mime;
464 }
465 } else if (mime == "video/mp4") {
466 if ( ffmpegCodecs.contains("h264") || ffmpegCodecs.contains("vp9") ) {
467 QList<QString> encodersPresent;
468 encodersPresent << ffmpegEncoderTypes["h264"] << ffmpegEncoderTypes["vp9"];
470 retValue << mime;
471 }
472 } else if (mime == "video/webm") {
473 if ( ffmpegCodecs.contains("vp9") ) {
474 QList<QString> encodersPresent;
475 encodersPresent << ffmpegEncoderTypes["vp9"];
477 retValue << mime;
478 }
479 } else if (mime == "image/gif") {
480 if ( ffmpegCodecs.contains("gif") ) {
481 retValue << mime;
482 }
483 } else if (mime == "image/apng") {
484 if ( ffmpegCodecs.contains("apng") ) {
485 retValue << mime;
486 }
487 } else if (mime == "image/webp") {
488 if ( ffmpegCodecs.contains("webp") ) {
489 retValue << mime;
490 }
491 } else if (mime == "video/ogg") {
492 if ( ffmpegCodecs.contains("theora") ) {
493 retValue << mime;
494 }
495 }
496 }
497
498 return retValue;
499}
500#endif
501
503{
504 return (mime == "image/png");
505}
506
507#ifdef Q_OS_ANDROID
508QVariantMap KisDlgAnimationRenderer::loadVideoFormatPreferences()
509{
510 KisPropertiesConfigurationSP settings = loadLastConfiguration(QStringLiteral("VIDEO_ENCODER"));
511 QString s = settings->getString(QStringLiteral("format_preferences"));
512 if (!s.isEmpty()) {
513 QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8());
514 if (doc.isObject()) {
515 return doc.toVariant().toMap();
516 }
517 }
518 return QVariantMap();
519}
520
521void KisDlgAnimationRenderer::saveVideoFormatPreferences(const QVariantMap &value)
522{
524 settings->setProperty(QStringLiteral("format_preferences"),
525 QString::fromUtf8(QJsonDocument::fromVariant(value).toJson(QJsonDocument::Compact)));
526 saveLastUsedConfiguration(QStringLiteral("VIDEO_ENCODER"), settings);
527}
528#endif
529
531 KisConfig globalConfig(true);
532 return globalConfig.exportConfiguration(configurationID);
533}
534
536{
537 KisConfig globalConfig(false);
538 globalConfig.setExportConfiguration(configurationID, config);
539}
540
541bool KisDlgAnimationRenderer::looksLikeGif(const QString &videoType)
542{
543#ifdef Q_OS_ANDROID
544 return videoType.contains(QStringLiteral(":gif"));
545#else
546 return videoType == QStringLiteral("image/gif");
547#endif
548}
549
550bool KisDlgAnimationRenderer::supportsAudio(const QString &videoType)
551{
552#ifdef Q_OS_ANDROID
554 return format && format->supportsAudio();
555#else
556 return !videoType.startsWith(QStringLiteral("image/"));
557#endif
558}
559
574
575#ifndef Q_OS_ANDROID
576
577void KisDlgAnimationRenderer::setFFmpegPath(const QString& path) {
578 // Let's START with the assumption that user-specified ffmpeg path is invalid
579 // and clear out all of the ffmpeg-specific fields to fill post-validation...
580 m_page->cmbRenderType->setDisabled(true);
581 m_page->bnRenderOptions->setDisabled(true);
582
583 QString previousMimeType = m_page->cmbRenderType->currentData().toString();
584
585 m_page->cmbRenderType->clear();
586 ffmpegEncoderTypes.clear();
587
588 // Validate FFmpeg binary and setup FFMpeg...
589 if (validateFFmpeg(path)) {
590 QJsonObject ffmpegJsonObj = KisFFMpegWrapper::findFFMpeg(path);
591 ffmpegVersion = ffmpegJsonObj["enabled"].toBool() ? ffmpegJsonObj["version"].toString() : i18n("No valid FFmpeg binary supplied...");
593
594 // Build map of encoding types to their specific encoder support (e.g. h264 => libopenh264, h264, h264_vaapi or whatever)
595 Q_FOREACH(const QString& codec, ffmpegCodecs) {
596 QJsonObject codecjson = ffmpegJsonObj["codecs"].toObject()[codec].toObject();
597 if ( codecjson["encoding"].toBool() ) {
598 QJsonArray codecEncoders = codecjson["encoders"].toArray();
599
600 // In the case where no specific codec "library" is specified but we do support
601 // encoding, we simply push the type onto the list regardless. This basically
602 // means that there's no "specific" requirements that we need to meet and
603 // encoding should be possible.
604 if (codecEncoders.size() == 0) {
605 codecEncoders.push_back(QJsonValue(codec));
606 }
607
608 Q_FOREACH(const QJsonValue& value, codecEncoders) {
609 if (ffmpegEncoderTypes.contains(codec)) {
610 ffmpegEncoderTypes[codec].push_back(value.toString());
611 } else {
612 ffmpegEncoderTypes.insert(codec, {value.toString()} );
613 }
614 }
615 }
616 }
617
618 KisConfig cfg(false);
619
620 { // Build list of supported container types and repopulate cmbRenderType.
621 KisSignalsBlocker(m_page->cmbRenderType);
622
623 QStringList supportedMimeTypes = makeVideoMimeTypesList();
624 supportedMimeTypes = filterMimeTypeListByAvailableEncoders(supportedMimeTypes);
625
626 int previousMimeTypeIndex = -1;
627 Q_FOREACH (const QString &mime, supportedMimeTypes) {
628 QString description = KisMimeDatabase::descriptionForMimeType(mime);
629 if (description.isEmpty()) {
630 description = mime;
631 }
632
633 m_page->cmbRenderType->addItem(description, mime);
634 if (mime == previousMimeType) {
635 previousMimeTypeIndex = m_page->cmbRenderType->count() - 1;
636 }
637 }
638
639 const int indexCount = m_page->cmbRenderType->count();
640 if (indexCount > 0) {
641 if (previousMimeTypeIndex >= 0) {
642 m_page->cmbRenderType->setCurrentIndex(previousMimeTypeIndex % indexCount);
643 } else {
644 m_page->cmbRenderType->setCurrentIndex(0);
645 }
646
647 selectRenderType(m_page->cmbRenderType->currentIndex());
648 m_page->cmbRenderType->setDisabled(false);
649 m_page->bnRenderOptions->setDisabled(false);
650 connect(m_page->cmbRenderType, SIGNAL(currentIndexChanged(int)), this, SLOT(selectRenderType(int)));
651 }
652 }
653
654 m_page->lblFFMpegVersion->setText(ffmpegVersion);
655
656 // Store configuration..
657 cfg.setFFMpegLocation(ffmpegJsonObj["path"].toString());
658
660 }
661}
662#endif
663
665{
666 setUpdatesEnabled(false);
668 m_page->adjustSize();
669 setUpdatesEnabled(true);
670}
671
673{
674 QStringList warnings;
675 bool exportMayFail = false;
676
677 if (wantVideoExport()) {
678 QString videoType = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString();
679 bool gif = looksLikeGif(videoType);
680
681#ifndef Q_OS_ANDROID
682 const QRegularExpression minVerFFMpegRX(R"(^n{0,1}(?:[0-3]|4\.[01])[\.\-])");
683 const QRegularExpressionMatch minVerFFMpegMatch = minVerFFMpegRX.match(ffmpegVersion);
684
685 if (gif && minVerFFMpegMatch.hasMatch()) {
686 warnings << i18nc("ffmpeg warning checks",
687 "FFmpeg must be at least version 4.2+ for GIF transparency to work");
688 }
689#endif
690
691 int fps = m_page->intFramesPerSecond->value();
692 if (gif && fps > 50) {
693 warnings << i18nc("ffmpeg warning checks",
694 "Animated GIF images cannot have a framerate higher than 50. The framerate will be "
695 "reduced to 50 frames per second");
696 }
697
698 if (fps > 30) {
699 exportMayFail = true;
700 warnings << i18nc("ffmpeg warning checks", "FPS beyond 30 are not widely supported.");
701 }
702
703 if (m_page->intWidth->value() > 1920 || m_page->intHeight->value() > 1920) {
704 exportMayFail = true;
705 warnings << i18nc("ffmpeg warnings checks", "Dimensions larger than 1920 pixels are not widely supported.");
706 }
707 }
708
709 if (warnings.isEmpty()) {
710 m_page->lblWarnings->hide();
711 } else {
712 QString text = QString("<p><b>%1</b>").arg(i18n("Warning(s):"));
713 text.append("<ul>");
714 Q_FOREACH (const QString &warning, warnings) {
715 text.append("<li>");
716 text.append(warning.toHtmlEscaped());
717 text.append("</li>");
718 }
719 text.append("</ul></p>");
720 if (exportMayFail) {
721 text.append(QStringLiteral("<p>"));
722 text.append(
723 i18nc("ffmpeg warning checks", "The export may fail and some devices may not be able to play it.")
724 .toHtmlEscaped());
725 text.append(QStringLiteral("</p>"));
726 }
727 m_page->lblWarnings->setText(text);
728 m_page->lblWarnings->show();
729 }
730}
731
732#ifndef Q_OS_ANDROID
733QString KisDlgAnimationRenderer::defaultVideoFileName(KisDocument *doc, const QString &mimeType)
734{
735 const QString docFileName = !doc->localFilePath().isEmpty() ? doc->localFilePath() : i18n("Untitled");
736
737 if (!mimeType.isEmpty()) {
738 return QString("%1.%2").arg(QFileInfo(docFileName).completeBaseName(),
739 KisMimeDatabase::suffixesForMimeType(mimeType).first());
740 } else {
741 return docFileName;
742 }
743}
744
746{
747 if (m_page->cmbRenderType->count() == 0) return;
748
749 const QString mimeType = m_page->cmbRenderType->itemData(index).toString();
750
752
753 QString videoFileName = defaultVideoFileName(m_doc, mimeType);
754
755 if (!m_page->videoFilename->fileName().isEmpty()) {
756 const QFileInfo info = QFileInfo(m_page->videoFilename->fileName());
757 const QString baseName = info.completeBaseName();
758 const QString path = info.path();
759
760 videoFileName = QString("%1%2%3.%4")
761 .arg(path, "/", baseName, KisMimeDatabase::suffixesForMimeType(mimeType).first());
762 }
763 m_page->videoFilename->setMimeTypeFilters(QStringList() << mimeType, mimeType);
764 m_page->videoFilename->setFileName(videoFileName);
765
766 m_wantsRenderWithHDR = (mimeType == "video/mp4") ? m_wantsRenderWithHDR : false;
767
768 { // We've got to reload the render settings to account for the user changing render type without configuration.
769 // If this is removed from the configuration, ogg vorbis can fail to render on first attempt. BUG:421658
770 // This should be revisited at some point, too much configuration juggling in this class makes it error-prone...
771
772 QStringList encodersPresent;
773 Q_FOREACH(const QString& key, ffmpegEncoderTypes.keys()) {
774 encodersPresent << ffmpegEncoderTypes[key];
775 }
776
777 KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
778 getDefaultVideoEncoderOptions(mimeType, settings,
779 encodersPresent,
782 }
783}
784#endif
785
787{
788#ifdef Q_OS_ANDROID
789 QString key = m_page->cmbRenderType->currentData().toString();
792
793 KisMediaEncoderPreferencesDialog dlg(format, m_videoFormatPreferences.value(key).toMap(), this);
794 if (dlg.exec() == QDialog::Accepted) {
795 m_videoFormatPreferences.insert(key, dlg.preferences());
796 saveVideoFormatPreferences(m_videoFormatPreferences);
797 }
798#else
799 const int index = m_page->cmbRenderType->currentIndex();
800 const QString mimetype = m_page->cmbRenderType->itemData(index).toString();
801
804
805 QStringList encodersPresent;
806 Q_FOREACH(const QString& key, ffmpegEncoderTypes.keys()) {
807 encodersPresent << ffmpegEncoderTypes[key];
808 }
809
811
812 KisVideoExportOptionsDialog *encoderConfigWidget =
813 new KisVideoExportOptionsDialog(containerType, encodersPresent, hdrOptions, this);
814
815 // we always enable HDR, letting the user to force it
816 encoderConfigWidget->setSupportsHDR(true);
817
818 {
819 KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
820 encoderConfigWidget->setConfiguration(settings);
821 encoderConfigWidget->setHDRConfiguration(m_wantsRenderWithHDR);
822 }
823
824 KoDialog dlg(this);
825 dlg.setMainWidget(encoderConfigWidget);
826 dlg.setButtons(KoDialog::Ok | KoDialog::Cancel);
827 if (dlg.exec() == QDialog::Accepted) {
828 saveLastUsedConfiguration("VIDEO_ENCODER", encoderConfigWidget->configuration());
829#ifndef Q_OS_ANDROID
831 m_wantsRenderWithHDR = encoderConfigWidget->videoConfiguredForHDR();
832#endif
833 }
834
835 dlg.setMainWidget(0);
836 encoderConfigWidget->deleteLater();
837#endif
838}
839
841{
842 int index = m_page->cmbMimetype->currentIndex();
843
844 KisConfigWidget *frameExportConfigWidget = 0;
845
846 QString mimetype = m_page->cmbMimetype->itemData(index).toString();
848 if (filter) {
849 frameExportConfigWidget = filter->createConfigurationWidget(0, KisDocument::nativeFormatMimeType(), mimetype.toLatin1());
850
851 if (frameExportConfigWidget) {
852
853 KisPropertiesConfigurationSP exportConfig = loadLastConfiguration("img_sequence/" + mimetype);
854 if (exportConfig) {
856 }
857
858 //Important -- m_useHDR allows the synchronization of both the video and image render settings.
859#ifndef Q_OS_ANDROID
860 if(imageMimeSupportsHDR(mimetype)) {
861 setPngHdr(exportConfig, m_wantsRenderWithHDR);
863 exportConfig->setProperty("forceSRGB", false);
864 }
865 }
866#endif
867
868 frameExportConfigWidget->setConfiguration(exportConfig);
869 KoDialog dlg(this);
870 dlg.setMainWidget(frameExportConfigWidget);
872 if (dlg.exec() == QDialog::Accepted) {
873#ifndef Q_OS_ANDROID
874 m_wantsRenderWithHDR = getPngHdr(frameExportConfigWidget->configuration(), false);
875#endif
876 saveLastUsedConfiguration("img_sequence/" + mimetype, frameExportConfigWidget->configuration());
877 }
878
879 frameExportConfigWidget->hide();
880 dlg.setMainWidget(0);
881 frameExportConfigWidget->setParent(0);
882 frameExportConfigWidget->deleteLater();
883
884 }
885 }
886}
887
888
890{
892
894 QString videoType = m_page->cmbRenderType->currentData().toString();
895
896#ifdef Q_OS_ANDROID
897 bool video = wantVideoExport();
898 options.shouldEncodeVideo = video;
899 options.includeAudio = video && supportsAudio(videoType) && m_page->chkIncludeAudio->isChecked();
900 options.wantsOnlyUniqueFrameSequence = !video && m_page->chkOnlyUniqueFrames->isChecked();
901
902 if (video) {
903 options.frameMimeType = QStringLiteral("image/png");
904 options.videoFileName = m_videoFileName;
905 options.videoFormatKey = videoType;
906 QVariantMap videoFormatPreferences = m_videoFormatPreferences.value(videoType).toMap();
907 if (!videoFormatPreferences.isEmpty()) {
908 options.videoFormatPreferencesJson =
909 QString::fromUtf8(QJsonDocument::fromVariant(videoFormatPreferences).toJson(QJsonDocument::Compact));
910 }
911 } else {
912 options.directory = m_imageDirectory;
913 options.frameMimeType = m_page->cmbMimetype->currentData().toString();
914 }
915#else
916 options.videoMimeType = videoType;
917 options.videoFileName = m_page->videoFilename->fileName();
918 options.ffmpegPath = m_page->ffmpegLocation->fileName();
920 options.directory = m_page->dirRequester->fileName();
923 options.includeAudio = supportsAudio(videoType) && m_page->chkIncludeAudio->isChecked();
924 options.wantsOnlyUniqueFrameSequence = m_page->chkOnlyUniqueFrames->isChecked();
925 options.frameMimeType = m_page->cmbMimetype->currentData().toString();
926#endif
927 options.scaleFilter = m_page->cmbScaleFilter->currentData().toString();
928
929 options.basename = m_page->txtBasename->text();
930 options.firstFrame = m_page->intStart->value();
931 options.lastFrame = m_page->intEnd->value();
932 options.sequenceStart = m_page->sequenceStart->value();
933
934 options.frameRate = m_page->intFramesPerSecond->value();
935 if (options.frameRate > 50 && looksLikeGif(videoType)) {
936 options.frameRate = 50;
937 }
938
939 options.width = m_page->intWidth->value();
940 options.height = m_page->intHeight->value();
941
942 {
944 if (cfg) {
946 }
947
948#ifndef Q_OS_ANDROID
949 const bool forceNecessaryHDRSettings = m_wantsRenderWithHDR && imageMimeSupportsHDR(options.frameMimeType);
950 if (forceNecessaryHDRSettings) {
951 KIS_SAFE_ASSERT_RECOVER_NOOP(options.frameMimeType == "image/png");
952 cfg->setProperty("forceSRGB", false);
953 setPngHdr(cfg);
954 }
955#endif
956
957 options.frameExportConfig = cfg;
958 }
959
960 return options;
961}
962
963#ifndef Q_OS_ANDROID
965{
966 if (!ffmpegPath.isEmpty()) {
967 QFileInfo ffmpegBinary(ffmpegPath);
968 if (ffmpegBinary.exists() && ffmpegBinary.isExecutable()) {
969 QStringList commpressedFormats{"zip", "7z", "tar.bz2"};
970 Q_FOREACH(const QString& compressedFormat, commpressedFormats) {
971 if (ffmpegBinary.fileName().endsWith(compressedFormat)) {
973 }
974 }
976 } else if (ffmpegBinary.exists()) {
978 }
979 }
981}
982#endif
983
984#ifndef Q_OS_ANDROID
986{
987 if (button == KoDialog::Ok && !m_page->shouldExportOnlyImageSequence->isChecked()) {
988 QString fileName = m_page->videoFilename->fileName();
989
990 if (fileName.isEmpty()) {
991 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Please enter a file name to render to."));
992 return;
993 }
994 else {
995 switch (validateFFmpeg(m_page->ffmpegLocation->fileName())) {
997 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("The FFmpeg that you've given us appears to be compressed. Please try to extract FFmpeg from the archive first."));
998 return;
1000 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("The FFmpeg that you've given us appears to be invalid. Please select the correct location of an FFmpeg executable on your system."));
1001 return;
1002 default:
1003 break;
1004 }
1005 }
1006 }
1008}
1009#endif
1010
1012{
1013#ifdef Q_OS_ANDROID
1014 m_imageDirectory.clear();
1015 m_videoFileName.clear();
1016
1017 if (wantVideoExport()) {
1018 KisMediaEncoderFormat *format = KisMediaEncoderWrapper::getFormatByKey(m_page->cmbRenderType->currentData().toString());
1020 KoFileDialog dialog(this, KoFileDialog::SaveFile, QStringLiteral("ExportAnimation"));
1021 dialog.setMimeTypeFilters(QStringList(KisMimeDatabase::mimeTypeForSuffix(format->extension())));
1022 dialog.setDefaultDir(m_doc->localFilePath());
1023 m_videoFileName = dialog.filename();
1024 if (m_videoFileName.isEmpty()) {
1025 return;
1026 }
1027
1028 } else {
1029 KoFileDialog dialog(this, KoFileDialog::OpenDirectory, QStringLiteral("ExportAnimation"));
1030 m_imageDirectory = dialog.filename();
1031 if (m_imageDirectory.isEmpty()) {
1032 return;
1033 }
1034 }
1035#endif
1036
1037 KisConfig cfg(false);
1039 saveLastUsedConfiguration("ANIMATION_EXPORT", options.toProperties());
1040
1044}
1045
1047{
1048 cfg->setProperty("storeColorSpaceInfo", enable);
1049 cfg->setProperty("writeCicpIfPossible", enable);
1050 if (enable) {
1051 cfg->setProperty("storeExtraColorChunks", false);
1052 cfg->setProperty("floatingPointConversionOption", "Rec2100PQ");
1053 }
1054}
1055
1057{
1058 return cfg->getPropertyLazy("storeColorSpaceInfo", wantsRenderWithHdr)
1059 && cfg->getPropertyLazy("writeCicpIfPossible", wantsRenderWithHdr)
1060 && (cfg->getPropertyLazy("floatingPointConversionOption", wantsRenderWithHdr? "Rec2100PQ": "KeepSame") == "Rec2100PQ");
1061}
1062
1064{
1065 setUpdatesEnabled(false);
1066
1067#ifdef Q_OS_ANDROID
1068 if (wantVideoExport()) {
1069 m_page->stkExport->setCurrentWidget(m_page->pgVideo);
1070 } else {
1071 m_page->lblWarnings->hide();
1072 m_page->stkExport->setCurrentWidget(m_page->pgImages);
1073 }
1074#else
1075 // if a video format needs to be outputted
1076 if (m_page->shouldExportOnlyVideo->isChecked()) {
1077 // videos always uses PNG for creating video, so disable the ability to change the format
1078 m_page->cmbMimetype->setEnabled(false);
1079 m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->findData("image/png"));
1080 }
1081
1085 if (!m_page->shouldExportOnlyVideo->isChecked() &&
1086 !m_page->shouldExportOnlyImageSequence->isChecked()) {
1087
1088 KisSignalsBlocker b(m_page->shouldExportOnlyImageSequence);
1089 m_page->shouldExportOnlyImageSequence->setChecked(true);
1090 }
1091#endif
1092
1094 m_page->adjustSize();
1095 setUpdatesEnabled(true);
1096}
1097
1099{
1100 m_page->chkIncludeAudio->setVisible(m_page->cmbRenderType->count() != 0
1101 && supportsAudio(m_page->cmbRenderType->currentData().toString()));
1102 m_page->adjustSize();
1103}
1104
1106{
1107 Q_UNUSED(width);
1108
1109 float aspectRatio = (float)m_image->width() / (float)m_image->height();
1110
1111 // update height here
1112 float newHeight = m_page->intWidth->value() / aspectRatio ;
1113
1114 m_page->intHeight->setValue(newHeight);
1115
1116}
1117
1119{
1120 Q_UNUSED(height);
1121
1122 float aspectRatio = (float)m_image->width() / (float)m_image->height();
1123
1124 // update width here
1125 float newWidth = aspectRatio * m_page->intHeight->value();
1126
1127 m_page->intWidth->setValue(newWidth);
1128}
float value(const T *src, size_t ch)
bool meetsEncoderRequirementsForContainer(KisVideoExportOptionsDialog::ContainerType encoderType, const QStringList &encodersPresent)
QList< QString > QStringList
KisAcyclicSignalConnector * createCoordinatedConnector()
create a coordinated connector that can be used for extending the number of self-locking connection.
void connectBackwardInt(QObject *sender, const char *signal, QObject *receiver, const char *method)
void connectForwardInt(QObject *sender, const char *signal, QObject *receiver, const char *method)
QString resolveAbsoluteDocumentFilePath(const QString &documentPath) const
void fromProperties(KisPropertiesConfigurationSP config)
KisPropertiesConfigurationSP frameExportConfig
KisPropertiesConfigurationSP toProperties() const
virtual KisPropertiesConfigurationSP configuration() const =0
virtual void setConfiguration(const KisPropertiesConfigurationSP config)=0
void setFFMpegLocation(const QString &value)
KisPropertiesConfigurationSP exportConfiguration(const QString &filterId, bool defaultValue=false) const
QString ffmpegLocation(bool defaultValue=false) const
void setExportConfiguration(const QString &filterId, KisPropertiesConfigurationSP properties) const
void initializeRenderSettings(const KisDocument &doc, const KisAnimationRenderingOptions &lastUsedOptions)
QStringList filterMimeTypeListByAvailableEncoders(const QStringList &mimeTypes)
KisDlgAnimationRenderer(KisDocument *doc, QWidget *parent=0)
KisHDRMetadataOptions optionsFromImage()
static bool looksLikeGif(const QString &videoType)
static bool supportsAudio(const QString &videoType)
void sequenceMimeTypeOptionsClicked()
sequenceMimeTypeSelected calls the dialog for the export widget.
KisAnimationRenderingOptions getEncoderOptions() const
void setFFmpegPath(const QString &path)
void slotLockAspectRatioDimensionsHeight(int height)
static void setPngHdr(KisPropertiesConfigurationSP cfg, bool enable=true)
void slotLockAspectRatioDimensionsWidth(int width)
static void getDefaultVideoEncoderOptions(const QString &mimeType, KisPropertiesConfigurationSP cfg, const QStringList &availableEncoders, QString *customFFMpegOptionsString, bool *forceHDRVideo)
static KisPropertiesConfigurationSP loadLastConfiguration(QString configurationID)
WdgAnimationRenderer * m_page
static QString defaultVideoFileName(KisDocument *doc, const QString &mimeType)
static bool getPngHdr(KisPropertiesConfigurationSP cfg, bool wantsRenderWithHdr)
FFmpegValidationResult validateFFmpeg(const QString &ffmpegPath)
static bool imageMimeSupportsHDR(QString &hdr)
static QStringList makeVideoMimeTypesList()
QMap< QString, QStringList > ffmpegEncoderTypes
static void saveLastUsedConfiguration(QString configurationID, KisPropertiesConfigurationSP config)
static void filterSequenceMimeTypes(QStringList &mimeTypes)
void slotButtonClicked(int button) override
KisImageSP image
QString localFilePath() const
static QByteArray nativeFormatMimeType()
QVector< QFileInfo > getAudioTracks() const
static QStringList getSupportedCodecs(const QJsonObject &ffmpegJsonProcessInput)
static QJsonObject findFFMpeg(const QString &customLocation)
void setExportSequenceBaseName(const QString &baseName)
const KisTimeSpan & activePlaybackRange() const
activePlaybackRange
const KisTimeSpan & documentPlaybackRange() const
documentPlaybackRange
void setExportInitialFrameNumber(const int frameNum)
void setExportSequenceFilePath(const QString &filePath)
std::optional< KisRelativeContentLightLevelInformation > relativeContentLightLevelInformation() const
relativeContentLightLevelInformation This returns (optionally) a KisRelativeContentLightLevelInformat...
KisImageAnimationInterface * animationInterface() const
qint32 width() const
std::optional< double > hdrReferenceWhiteLightLevel() const
HDR Reference White livel.
qint32 height() const
std::optional< KisColorVolumeInformation > colorVolumeInformation() const
colorVolumeInformation
static KisImportExportFilter * filterForMimeType(const QString &mimetype, Direction direction)
filterForMimeType loads the relevant import/export plugin and returns it. The caller is responsible f...
static void fillStaticExportConfigurationProperties(KisPropertiesConfigurationSP exportConfiguration, KisImageSP image)
static QStringList supportedMimeTypes(Direction direction)
virtual bool supportsAudio() const =0
virtual QString extension() const =0
static KisMediaEncoderFormat * getFormatByKey(const QString &key)
static const QVector< KisMediaEncoderFormat * > & getSupportedFormats()
static QStringList suffixesForMimeType(const QString &mimeType)
static QString mimeTypeForSuffix(const QString &suffix)
Find the mimetype for a given extension. The extension may have the form "*.xxx" or "xxx".
static QString descriptionForMimeType(const QString &mimeType)
Find the user-readable description for the given mimetype.
int start() const
int end() const
KisPropertiesConfigurationSP configuration() const override
static ContainerType mimeToContainer(const QString &mimeType)
void setConfiguration(const KisPropertiesConfigurationSP config) override
static QVector< KoID > encoderIdentifiers(ContainerType type)
A dialog base class with standard buttons and predefined layouts.
Definition KoDialog.h:116
virtual void slotButtonClicked(int button)
Definition KoDialog.cpp:811
QPushButton * button(ButtonCode id) const
Definition KoDialog.cpp:582
void setMainWidget(QWidget *widget)
Definition KoDialog.cpp:345
virtual void setCaption(const QString &caption)
Definition KoDialog.cpp:489
void setButtons(ButtonCodes buttonMask)
Definition KoDialog.cpp:195
void setDefaultButton(ButtonCode id)
Definition KoDialog.cpp:293
@ Ok
Show Ok button. (this button accept()s the dialog; result set to QDialog::Accepted)
Definition KoDialog.h:127
@ Cancel
Show Cancel-button. (this button reject()s the dialog; result set to QDialog::Rejected)
Definition KoDialog.h:130
Definition KoID.h:30
Encoder * encoder(Imf::OutputFile &file, const ExrPaintLayerSaveInfo &info, int width)
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
QString button(const QWheelEvent &ev)
QIcon loadIcon(const QString &name)
auto filterContainer(C &container, KeepIfFunction keepIf) -> decltype(bool(keepIf(container[0])), void())
KisRelativeContentLightLevelInformation clli
KisColorVolumeInformation cvi