Krita Source Code Documentation
Loading...
Searching...
No Matches
KisDlgAnimationRenderer.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2016 Boudewijn Rempt <boud@valdyas.org>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <QStandardPaths>
10#include <QPluginLoader>
11#include <QJsonObject>
12#include <QJsonArray>
13#include <QMessageBox>
14#include <QStringList>
15#include <QProcess>
16
17#include <klocalizedstring.h>
18#include <kpluginfactory.h>
19
20#include <KoResourcePaths.h>
22#include <kis_debug.h>
23#include <KisMimeDatabase.h>
24#include <KoJsonTrader.h>
27#include <kis_image.h>
29#include <kis_time_span.h>
31#include <kis_config_widget.h>
32#include <kis_signals_blocker.h>
33#include <KisDocument.h>
34#include <QHBoxLayout>
35#include <kis_config.h>
37#include <KoDialog.h>
38#include "kis_slider_spin_box.h"
40#include "KisVideoSaver.h"
44#include "kis_image_config.h"
45
46
48 : KoDialog(parent)
49 , m_image(doc->image())
50 , m_doc(doc)
51{
52 KisConfig cfg(true);
53
54 setCaption(i18n("Render Animation"));
57
58 m_page = new WdgAnimationRenderer(this);
59 m_page->layout()->setContentsMargins(0, 0, 0, 0);
60
61 m_page->dirRequester->setMode(KoFileDialog::OpenDirectory);
62
63 m_page->intStart->setMinimum(0);
64 m_page->intStart->setMaximum(doc->image()->animationInterface()->documentPlaybackRange().end());
65 m_page->intEnd->setMinimum(doc->image()->animationInterface()->documentPlaybackRange().start());
66
67 m_page->intHeight->setMinimum(1);
68 m_page->intHeight->setMaximum(100000);
69
70 m_page->intWidth->setMinimum(1);
71 m_page->intWidth->setMaximum(100000);
72
73 // Setup audio...
74 QVector<QFileInfo> audioFiles = doc->getAudioTracks();
75 const bool hasAudio = audioFiles.count() > 0;
76 m_page->chkIncludeAudio->setEnabled(hasAudio);
77
78 // Setup image mimeTypes...
80 mimes.sort();
82 Q_FOREACH(const QString &mime, mimes) {
83 QString description = KisMimeDatabase::descriptionForMimeType(mime);
84 if (description.isEmpty()) {
85 description = mime;
86 }
87
88 m_page->cmbMimetype->addItem(description, mime);
89
90 if (mime == "image/png") {
91 m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->count() - 1);
92 }
93 }
94
95 m_page->cmbScaleFilter->addItem(i18nc("bicubic filtering", "bicubic"), "bicubic");
96 m_page->cmbScaleFilter->addItem(i18nc("bilinear filtering", "bilinear"), "bilinear");
97 m_page->cmbScaleFilter->addItem(i18nc("lanczos3 filtering", "lanczos3"), "lanczos");
98 m_page->cmbScaleFilter->addItem(i18nc("nearest neighbor filtering", "neighbor"), "neighbor");
99 m_page->cmbScaleFilter->addItem(i18nc("spline filtering", "spline"), "spline");
100
101 m_page->videoFilename->setMode(KoFileDialog::SaveFile);
102
103 m_page->ffmpegLocation->setMode(KoFileDialog::OpenFile);
104
105 m_page->cmbRenderType->setPlaceholderText(i18nc("Not applicable. No render types without valid ffmpeg path.", "N/A"));
106
107 { // Establish connections...
108 connect(m_page->bnExportOptions, SIGNAL(clicked()), this, SLOT(sequenceMimeTypeOptionsClicked()));
109 connect(m_page->bnRenderOptions, SIGNAL(clicked()), this, SLOT(selectRenderOptions()));
110
111 connect(m_page->shouldExportOnlyImageSequence, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged()));
112 connect(m_page->shouldExportOnlyVideo, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged()));
113
114 connect(m_page->intFramesPerSecond, SIGNAL(valueChanged(int)), SLOT(frameRateChanged(int)));
115
116 connect(m_page->ffmpegLocation, SIGNAL(fileSelected(QString)), SLOT(setFFmpegPath(QString)));
117
118 connect(this, SIGNAL(accepted()), SLOT(slotDialogAccepted()));
119 }
120
121
122 // try to lock the width and height being updated
123 KisAcyclicSignalConnector *constrainsConnector = new KisAcyclicSignalConnector(this);
124 constrainsConnector->createCoordinatedConnector()->connectBackwardInt(m_page->intWidth, SIGNAL(valueChanged(int)), this, SLOT(slotLockAspectRatioDimensionsWidth(int)));
125 constrainsConnector->createCoordinatedConnector()->connectForwardInt(m_page->intHeight, SIGNAL(valueChanged(int)), this, SLOT(slotLockAspectRatioDimensionsHeight(int)));
126
127 { // Initialize settings from last configuration...
128 KisPropertiesConfigurationSP animProperties = loadLastConfiguration("ANIMATION_EXPORT");
130 options.fromProperties(animProperties);
131
132 initializeRenderSettings(*doc, options);
133 }
134
136}
137
142
144{
145 // Initialize FFmpeg location... (!)
146 KisConfig cfg(false);
147 QString cfgFFmpegPath = cfg.ffmpegLocation();
148#ifdef Q_OS_MACOS
149 if (cfgFFmpegPath.isEmpty()) {
150 QJsonObject ffmpegInfo = KisFFMpegWrapper::findFFMpeg(cfgFFmpegPath);
151 cfgFFmpegPath = (ffmpegInfo["enabled"].toBool()) ? ffmpegInfo["path"].toString() : "";
152 }
153#endif
154
155 // Check known ffmpeg locations..
156 QString likelyFFmpegPath = [&]() {
157 // Check last used
158 if (!lastUsedOptions.ffmpegPath.isEmpty() && QFileInfo(lastUsedOptions.ffmpegPath).isExecutable()) {
159 return lastUsedOptions.ffmpegPath;
160 }
161
162 // Check krita config
163 if (!cfgFFmpegPath.isEmpty() && QFileInfo(cfgFFmpegPath).isExecutable()) {
164 return cfgFFmpegPath;
165 }
166
167 // Check standard paths
168 QString systemFFmpeg = QStandardPaths::findExecutable("ffmpeg");
169 if (!systemFFmpeg.isEmpty() && QFileInfo(systemFFmpeg).isExecutable()) {
170 return systemFFmpeg;
171 }
172
173 // Find ffmpeg elsewhere...
174 QJsonObject ffmpegJsonObj = KisFFMpegWrapper::findFFMpeg("");
175 return (ffmpegJsonObj["enabled"].toBool()) ? ffmpegJsonObj["path"].toString() : "";
176 }();
177
178 if (!likelyFFmpegPath.isEmpty() && QFileInfo(likelyFFmpegPath).isExecutable()) {
179 setFFmpegPath(likelyFFmpegPath);
180 }
181
182 const QString documentPath = m_doc->localFilePath();
183
184 // Initialize these settings based on last used configuration when possible..
185 if (!lastUsedOptions.lastDocumentPath.isEmpty() &&
186 lastUsedOptions.lastDocumentPath == documentPath) {
187
188 // If the file is the same as last time, we use the last used basename.
189 m_page->txtBasename->setText(lastUsedOptions.basename);
190
191 m_page->sequenceStart->setValue(lastUsedOptions.sequenceStart);
192 m_page->intWidth->setValue(lastUsedOptions.width);
193 m_page->intHeight->setValue(lastUsedOptions.height);
194
195 m_page->videoFilename->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
196 m_page->videoFilename->setFileName(lastUsedOptions.videoFileName);
197
198 m_page->dirRequester->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
199 m_page->dirRequester->setFileName(lastUsedOptions.directory);
200 } else {
201 m_page->sequenceStart->setValue(m_image->animationInterface()->activePlaybackRange().start());
202 m_page->intWidth->setValue(m_image->width());
203 m_page->intHeight->setValue(m_image->height());
204
205 m_page->videoFilename->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
206 m_page->videoFilename->setFileName(defaultVideoFileName(m_doc, lastUsedOptions.videoMimeType));
207
208 m_page->dirRequester->setStartDir(lastUsedOptions.resolveAbsoluteDocumentFilePath(documentPath));
209 m_page->dirRequester->setFileName(lastUsedOptions.directory);
210 }
211
212 // Initialize FRAME render format...
213 for (int i = 0; i < m_page->cmbMimetype->count(); ++i) {
214 if (m_page->cmbMimetype->itemData(i).toString() == lastUsedOptions.frameMimeType) {
215 m_page->cmbMimetype->setCurrentIndex(i);
216 break;
217 }
218 }
219
220 for (int i = 0; i < m_page->cmbScaleFilter->count(); ++i) {
221 if (m_page->cmbScaleFilter->itemData(i).toString() == lastUsedOptions.scaleFilter) {
222 m_page->cmbScaleFilter->setCurrentIndex(i);
223 break;
224 }
225 }
226
227 // Initialize VIDEO render format...
228 for (int i = 0; i < m_page->cmbRenderType->count(); ++i) {
229 if (m_page->cmbRenderType->itemData(i).toString() == lastUsedOptions.videoMimeType) {
230 m_page->cmbRenderType->setCurrentIndex(i);
231 break;
232 }
233 }
234
235 m_page->chkOnlyUniqueFrames->setChecked(lastUsedOptions.wantsOnlyUniqueFrameSequence);
236
237 m_page->shouldExportOnlyVideo->setChecked(lastUsedOptions.shouldEncodeVideo);
238 m_page->shouldExportOnlyImageSequence->setChecked(!lastUsedOptions.shouldDeleteSequence);
240
241 {
242 KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
243
244 QStringList encodersPresent;
245 Q_FOREACH(const QString& key, ffmpegEncoderTypes.keys()) {
246 encodersPresent << ffmpegEncoderTypes[key];
247 }
248
249 getDefaultVideoEncoderOptions(lastUsedOptions.videoMimeType, settings,
250 encodersPresent,
253 }
254
255 {
256 KisPropertiesConfigurationSP settings = loadLastConfiguration("img_sequence/" + lastUsedOptions.frameMimeType);
257 m_wantsRenderWithHDR = settings->getPropertyLazy("saveAsHDR", m_wantsRenderWithHDR);
258 }
259
260 m_page->ffmpegLocation->setFileName(likelyFFmpegPath);
261 m_page->ffmpegLocation->setStartDir(QFileInfo(m_doc->localFilePath()).path());
262 m_page->ffmpegLocation->setReadOnlyText(true);
263
264 // Initialize these settings based on the current document context..
265 m_page->intStart->setValue(doc.image()->animationInterface()->activePlaybackRange().start());
266 m_page->intEnd->setValue(doc.image()->animationInterface()->activePlaybackRange().end());
267 m_page->intFramesPerSecond->setValue(doc.image()->animationInterface()->framerate());
268
269 if (!doc.image()->animationInterface()->exportSequenceFilePath().isEmpty()
270 && QDir(doc.image()->animationInterface()->exportSequenceFilePath()).exists() ) {
271 m_page->dirRequester->setStartDir(doc.image()->animationInterface()->exportSequenceFilePath());
272 m_page->dirRequester->setFileName(doc.image()->animationInterface()->exportSequenceFilePath());
273 }
274
275 if (!doc.image()->animationInterface()->exportSequenceBaseName().isEmpty()) {
276 m_page->txtBasename->setText(doc.image()->animationInterface()->exportSequenceBaseName());
277 }
278
279 if (doc.image()->animationInterface()->exportInitialFrameNumber() != -1) {
280 m_page->sequenceStart->setValue(doc.image()->animationInterface()->exportInitialFrameNumber());
281 }
282
283 bool hasAudioLoaded = doc.getAudioTracks().count() > 0;
284 m_page->chkIncludeAudio->setChecked(hasAudioLoaded);
285}
286
289 const QStringList &availableEncoders,
290 QString *customFFMpegOptionsString,
291 bool *renderHDR)
292{
295
296 QScopedPointer<KisVideoExportOptionsDialog> encoderConfigWidget(
297 new KisVideoExportOptionsDialog(containerType, availableEncoders, 0));
298
299 // we always enable HDR, letting the user to force it
300 encoderConfigWidget->setSupportsHDR(true);
301 encoderConfigWidget->setConfiguration(cfg);
302 *customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString();
303 *renderHDR = encoderConfigWidget->videoConfiguredForHDR();
304}
305
307{
308 KritaUtils::filterContainer(mimeTypes, [](QString type) {
309 return (type.startsWith("image/")
310 || (type.startsWith("application/") &&
311 !type.startsWith("application/x-spriter")));
312 });
313}
314
316{
317 QStringList supportedMimeTypes = QStringList();
318 supportedMimeTypes << "video/x-matroska";
319 supportedMimeTypes << "video/mp4";
320 supportedMimeTypes << "video/webm";
321 supportedMimeTypes << "image/gif";
322 supportedMimeTypes << "image/apng";
323 supportedMimeTypes << "image/webp";
324 supportedMimeTypes << "video/ogg";
325
326 return supportedMimeTypes;
327}
328
331 Q_FOREACH(const KoID &encoder, encodersExpected ) {
332 if (encodersPresent.contains(encoder.id())) {
333 return true;
334 }
335 }
336 return false;
337}
338
339// For dependencies, see here:
340// https://en.wikipedia.org/wiki/Comparison_of_video_container_formats
342{
343 QStringList retValue;
344
345 Q_FOREACH(const QString& mime, input) {
346 if ( mime == "video/x-matroska" ) {
347 if ( ffmpegCodecs.contains("h264") || ffmpegCodecs.contains("vp9") ) {
348 QList<QString> encodersPresent;
349 encodersPresent << ffmpegEncoderTypes["h264"] << ffmpegEncoderTypes["vp9"];
351 retValue << mime;
352 }
353 } else if (mime == "video/mp4") {
354 if ( ffmpegCodecs.contains("h264") || ffmpegCodecs.contains("vp9") ) {
355 QList<QString> encodersPresent;
356 encodersPresent << ffmpegEncoderTypes["h264"] << ffmpegEncoderTypes["vp9"];
358 retValue << mime;
359 }
360 } else if (mime == "video/webm") {
361 if ( ffmpegCodecs.contains("vp9") ) {
362 QList<QString> encodersPresent;
363 encodersPresent << ffmpegEncoderTypes["vp9"];
365 retValue << mime;
366 }
367 } else if (mime == "image/gif") {
368 if ( ffmpegCodecs.contains("gif") ) {
369 retValue << mime;
370 }
371 } else if (mime == "image/apng") {
372 if ( ffmpegCodecs.contains("apng") ) {
373 retValue << mime;
374 }
375 } else if (mime == "image/webp") {
376 if ( ffmpegCodecs.contains("webp") ) {
377 retValue << mime;
378 }
379 } else if (mime == "video/ogg") {
380 if ( ffmpegCodecs.contains("theora") ) {
381 retValue << mime;
382 }
383 }
384 }
385
386 return retValue;
387}
388
390{
391 return (mime == "image/png");
392}
393
395 KisConfig globalConfig(true);
396 return globalConfig.exportConfiguration(configurationID);
397}
398
400{
401 KisConfig globalConfig(false);
402 globalConfig.setExportConfiguration(configurationID, config);
403}
404
405void KisDlgAnimationRenderer::setFFmpegPath(const QString& path) {
406 // Let's START with the assumption that user-specified ffmpeg path is invalid
407 // and clear out all of the ffmpeg-specific fields to fill post-validation...
408 m_page->cmbRenderType->setDisabled(true);
409 m_page->bnRenderOptions->setDisabled(true);
410
411 QString previousMimeType = m_page->cmbRenderType->currentData().toString();
412
413 m_page->cmbRenderType->clear();
414 ffmpegEncoderTypes.clear();
415
416 // Validate FFmpeg binary and setup FFMpeg...
417 if (validateFFmpeg(path)) {
418 QJsonObject ffmpegJsonObj = KisFFMpegWrapper::findFFMpeg(path);
419 ffmpegVersion = ffmpegJsonObj["enabled"].toBool() ? ffmpegJsonObj["version"].toString() : i18n("No valid FFmpeg binary supplied...");
421
422 // Build map of encoding types to their specific encoder support (e.g. h264 => libopenh264, h264, h264_vaapi or whatever)
423 Q_FOREACH(const QString& codec, ffmpegCodecs) {
424 QJsonObject codecjson = ffmpegJsonObj["codecs"].toObject()[codec].toObject();
425 if ( codecjson["encoding"].toBool() ) {
426 QJsonArray codecEncoders = codecjson["encoders"].toArray();
427
428 // In the case where no specific codec "library" is specified but we do support
429 // encoding, we simply push the type onto the list regardless. This basically
430 // means that there's no "specific" requirements that we need to meet and
431 // encoding should be possible.
432 if (codecEncoders.size() == 0) {
433 codecEncoders.push_back(QJsonValue(codec));
434 }
435
436 Q_FOREACH(const QJsonValue& value, codecEncoders) {
437 if (ffmpegEncoderTypes.contains(codec)) {
438 ffmpegEncoderTypes[codec].push_back(value.toString());
439 } else {
440 ffmpegEncoderTypes.insert(codec, {value.toString()} );
441 }
442 }
443 }
444 }
445
446 KisConfig cfg(false);
447
448 { // Build list of supported container types and repopulate cmbRenderType.
449 KisSignalsBlocker(m_page->cmbRenderType);
450
451 QStringList supportedMimeTypes = makeVideoMimeTypesList();
452 supportedMimeTypes = filterMimeTypeListByAvailableEncoders(supportedMimeTypes);
453
454 int previousMimeTypeIndex = -1;
455 Q_FOREACH (const QString &mime, supportedMimeTypes) {
456 QString description = KisMimeDatabase::descriptionForMimeType(mime);
457 if (description.isEmpty()) {
458 description = mime;
459 }
460
461 m_page->cmbRenderType->addItem(description, mime);
462 if (mime == previousMimeType) {
463 previousMimeTypeIndex = m_page->cmbRenderType->count() - 1;
464 }
465 }
466
467 const int indexCount = m_page->cmbRenderType->count();
468 if (indexCount > 0) {
469 if (previousMimeTypeIndex >= 0) {
470 m_page->cmbRenderType->setCurrentIndex(previousMimeTypeIndex % indexCount);
471 } else {
472 m_page->cmbRenderType->setCurrentIndex(0);
473 }
474
475 selectRenderType(m_page->cmbRenderType->currentIndex());
476 m_page->cmbRenderType->setDisabled(false);
477 m_page->bnRenderOptions->setDisabled(false);
478 connect(m_page->cmbRenderType, SIGNAL(currentIndexChanged(int)), this, SLOT(selectRenderType(int)));
479 }
480 }
481
482 m_page->lblFFMpegVersion->setText(ffmpegVersion);
483
484 // Store configuration..
485 cfg.setFFMpegLocation(ffmpegJsonObj["path"].toString());
486
488 }
489}
490
492 const QString mimeType = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString();
493
494 const QRegularExpression minVerFFMpegRX(R"(^n{0,1}(?:[0-3]|4\.[01])[\.\-])");
495 const QRegularExpressionMatch minVerFFMpegMatch = minVerFFMpegRX.match(ffmpegVersion);
496
497 QStringList warnings;
498
499 if (mimeType == "image/gif" && minVerFFMpegMatch.hasMatch()) {
500 warnings << i18nc("ffmpeg warning checks", "FFmpeg must be at least version 4.2+ for GIF transparency to work");
501 }
502
503 // m_page->bnRenderOptions->setEnabled(mimeType != "image/gif" && mimeType != "image/webp" && mimeType !=
504 // "image/png" );
505 if (mimeType == "image/gif" && m_page->intFramesPerSecond->value() > 50) {
506 warnings << i18nc("ffmpeg warning checks",
507 "Animated GIF images cannot have a framerate higher than 50. The framerate will be reduced "
508 "to 50 frames per second");
509 }
510
511 m_page->lblWarnings->setVisible(!warnings.isEmpty());
512
513 if (!warnings.isEmpty()) {
514 QString text = QString("<p><b>%1</b>").arg(i18n("Warning(s):"));
515 text.append("<ul>");
516 Q_FOREACH (const QString &warning, warnings) {
517 text.append("<li>");
518 text.append(warning.toHtmlEscaped());
519 text.append("</li>");
520 }
521 text.append("</ul></p>");
522 m_page->lblWarnings->setText(text);
523
524 m_page->lblWarnings->setPixmap(
525 m_page->lblWarnings->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(32, 32)));
526 }
527
528 m_page->adjustSize();
529}
530
531QString KisDlgAnimationRenderer::defaultVideoFileName(KisDocument *doc, const QString &mimeType)
532{
533 const QString docFileName = !doc->localFilePath().isEmpty() ? doc->localFilePath() : i18n("Untitled");
534
535 if (!mimeType.isEmpty()) {
536 return QString("%1.%2").arg(QFileInfo(docFileName).completeBaseName(),
537 KisMimeDatabase::suffixesForMimeType(mimeType).first());
538 } else {
539 return docFileName;
540 }
541}
542
544{
545 if (m_page->cmbRenderType->count() == 0) return;
546
547 const QString mimeType = m_page->cmbRenderType->itemData(index).toString();
548
549 /*
550 m_page->bnRenderOptions->setEnabled(mimeType != "image/gif" && mimeType != "image/webp" && mimeType != "image/png");
551 */
552
554
555 QString videoFileName = defaultVideoFileName(m_doc, mimeType);
556
557 if (!m_page->videoFilename->fileName().isEmpty()) {
558 const QFileInfo info = QFileInfo(m_page->videoFilename->fileName());
559 const QString baseName = info.completeBaseName();
560 const QString path = info.path();
561
562 videoFileName = QString("%1%2%3.%4")
563 .arg(path, "/", baseName, KisMimeDatabase::suffixesForMimeType(mimeType).first());
564 }
565 m_page->videoFilename->setMimeTypeFilters(QStringList() << mimeType, mimeType);
566 m_page->videoFilename->setFileName(videoFileName);
567
568 m_wantsRenderWithHDR = (mimeType == "video/mp4") ? m_wantsRenderWithHDR : false;
569
570 { // We've got to reload the render settings to account for the user changing render type without configuration.
571 // If this is removed from the configuration, ogg vorbis can fail to render on first attempt. BUG:421658
572 // This should be revisited at some point, too much configuration juggling in this class makes it error-prone...
573
574 QStringList encodersPresent;
575 Q_FOREACH(const QString& key, ffmpegEncoderTypes.keys()) {
576 encodersPresent << ffmpegEncoderTypes[key];
577 }
578
579 KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
580 getDefaultVideoEncoderOptions(mimeType, settings,
581 encodersPresent,
584 }
585}
586
588{
589 const int index = m_page->cmbRenderType->currentIndex();
590 const QString mimetype = m_page->cmbRenderType->itemData(index).toString();
591
594
595 QStringList encodersPresent;
596 Q_FOREACH(const QString& key, ffmpegEncoderTypes.keys()) {
597 encodersPresent << ffmpegEncoderTypes[key];
598 }
599
600 KisVideoExportOptionsDialog *encoderConfigWidget =
601 new KisVideoExportOptionsDialog(containerType, encodersPresent, this);
602
603 // we always enable HDR, letting the user to force it
604 encoderConfigWidget->setSupportsHDR(true);
605
606 {
607 KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
608 encoderConfigWidget->setConfiguration(settings);
609 encoderConfigWidget->setHDRConfiguration(m_wantsRenderWithHDR);
610 }
611
612 KoDialog dlg(this);
613 dlg.setMainWidget(encoderConfigWidget);
615 if (dlg.exec() == QDialog::Accepted) {
616 saveLastUsedConfiguration("VIDEO_ENCODER", encoderConfigWidget->configuration());
618 m_wantsRenderWithHDR = encoderConfigWidget->videoConfiguredForHDR();
619 }
620
621 dlg.setMainWidget(0);
622 encoderConfigWidget->deleteLater();
623}
624
626{
627 int index = m_page->cmbMimetype->currentIndex();
628
629 KisConfigWidget *frameExportConfigWidget = 0;
630
631 QString mimetype = m_page->cmbMimetype->itemData(index).toString();
633 if (filter) {
634 frameExportConfigWidget = filter->createConfigurationWidget(0, KisDocument::nativeFormatMimeType(), mimetype.toLatin1());
635
636 if (frameExportConfigWidget) {
637
638 KisPropertiesConfigurationSP exportConfig = loadLastConfiguration("img_sequence/" + mimetype);
639 if (exportConfig) {
641 }
642
643 //Important -- m_useHDR allows the synchronization of both the video and image render settings.
644 if(imageMimeSupportsHDR(mimetype)) {
645 exportConfig->setProperty("saveAsHDR", m_wantsRenderWithHDR);
647 exportConfig->setProperty("forceSRGB", false);
648 }
649 }
650
651 frameExportConfigWidget->setConfiguration(exportConfig);
652 KoDialog dlg(this);
653 dlg.setMainWidget(frameExportConfigWidget);
655 if (dlg.exec() == QDialog::Accepted) {
656 m_wantsRenderWithHDR = frameExportConfigWidget->configuration()->getPropertyLazy("saveAsHDR", false);
657 saveLastUsedConfiguration("img_sequence/" + mimetype, frameExportConfigWidget->configuration());
658 }
659
660 frameExportConfigWidget->hide();
661 dlg.setMainWidget(0);
662 frameExportConfigWidget->setParent(0);
663 frameExportConfigWidget->deleteLater();
664
665 }
666 }
667}
668
669
671{
673
675 options.videoMimeType = m_page->cmbRenderType->currentData().toString();
676 options.frameMimeType = m_page->cmbMimetype->currentData().toString();
677 options.scaleFilter = m_page->cmbScaleFilter->currentData().toString();
678
679 options.basename = m_page->txtBasename->text();
680 options.directory = m_page->dirRequester->fileName();
681 options.firstFrame = m_page->intStart->value();
682 options.lastFrame = m_page->intEnd->value();
683 options.sequenceStart = m_page->sequenceStart->value();
684
685 options.shouldEncodeVideo = m_page->shouldExportOnlyVideo->isChecked();
686 options.shouldDeleteSequence = !m_page->shouldExportOnlyImageSequence->isChecked();
687 options.includeAudio = m_page->chkIncludeAudio->isChecked();
688 options.wantsOnlyUniqueFrameSequence = m_page->chkOnlyUniqueFrames->isChecked();
689
690 options.ffmpegPath = m_page->ffmpegLocation->fileName();
691 options.frameRate = m_page->intFramesPerSecond->value();
692
693 if (options.frameRate > 50 && options.videoMimeType == "image/gif") {
694 options.frameRate = 50;
695 }
696
697 options.width = m_page->intWidth->value();
698 options.height = m_page->intHeight->value();
699 options.videoFileName = m_page->videoFilename->fileName();
700
702
703 {
705 if (cfg) {
707 }
708
709 const bool forceNecessaryHDRSettings = m_wantsRenderWithHDR && imageMimeSupportsHDR(options.frameMimeType);
710 if (forceNecessaryHDRSettings) {
711 KIS_SAFE_ASSERT_RECOVER_NOOP(options.frameMimeType == "image/png");
712 cfg->setProperty("forceSRGB", false);
713 cfg->setProperty("saveAsHDR", true);
714 }
715
716 options.frameExportConfig = cfg;
717 }
718
719 return options;
720}
721
723{
724 if (!ffmpegPath.isEmpty()) {
725 QFileInfo ffmpegBinary(ffmpegPath);
726 if (ffmpegBinary.exists() && ffmpegBinary.isExecutable()) {
727 QStringList commpressedFormats{"zip", "7z", "tar.bz2"};
728 Q_FOREACH(const QString& compressedFormat, commpressedFormats) {
729 if (ffmpegBinary.fileName().endsWith(compressedFormat)) {
731 }
732 }
734 } else if (ffmpegBinary.exists()) {
736 }
737 }
739}
740
742{
743 if (button == KoDialog::Ok && !m_page->shouldExportOnlyImageSequence->isChecked()) {
744 QString fileName = m_page->videoFilename->fileName();
745
746 if (fileName.isEmpty()) {
747 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Please enter a file name to render to."));
748 return;
749 }
750 else {
751 switch (validateFFmpeg(m_page->ffmpegLocation->fileName())) {
753 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."));
754 return;
756 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."));
757 return;
758 default:
759 break;
760 }
761 }
762 }
764}
765
776
778{
779 KisConfig cfg(false);
780
781 const bool willEncodeVideo = m_page->shouldExportOnlyVideo->isChecked();
782
783 // if a video format needs to be outputted
784 if (willEncodeVideo) {
785 // videos always uses PNG for creating video, so disable the ability to change the format
786 m_page->cmbMimetype->setEnabled(false);
787 m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->findData("image/png"));
788 }
789
793 if (!m_page->shouldExportOnlyVideo->isChecked() &&
794 !m_page->shouldExportOnlyImageSequence->isChecked()) {
795
796 KisSignalsBlocker b(m_page->shouldExportOnlyImageSequence);
797 m_page->shouldExportOnlyImageSequence->setChecked(true);
798 }
799}
800
802{
803 Q_UNUSED(framerate);
805}
806
808{
809 Q_UNUSED(width);
810
811 float aspectRatio = (float)m_image->width() / (float)m_image->height();
812
813 // update height here
814 float newHeight = m_page->intWidth->value() / aspectRatio ;
815
816 m_page->intHeight->setValue(newHeight);
817
818}
819
821{
822 Q_UNUSED(height);
823
824 float aspectRatio = (float)m_image->width() / (float)m_image->height();
825
826 // update width here
827 float newWidth = aspectRatio * m_page->intHeight->value();
828
829 m_page->intWidth->setValue(newWidth);
830}
float value(const T *src, size_t ch)
bool meetsEncoderRequirementsForContainer(KisVideoExportOptionsDialog::ContainerType encoderType, const QStringList &encodersPresent)
QList< QString > QStringList
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
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)
void sequenceMimeTypeOptionsClicked()
sequenceMimeTypeSelected calls the dialog for the export widget.
KisAnimationRenderingOptions getEncoderOptions() const
void setFFmpegPath(const QString &path)
void slotLockAspectRatioDimensionsHeight(int height)
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)
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)
KisImageAnimationInterface * animationInterface() const
qint32 width() const
qint32 height() const
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)
static QStringList suffixesForMimeType(const QString &mimeType)
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:820
QPushButton * button(ButtonCode id) const
Definition KoDialog.cpp:591
void setMainWidget(QWidget *widget)
Definition KoDialog.cpp:354
virtual void setCaption(const QString &caption)
Definition KoDialog.cpp:498
void setButtons(ButtonCodes buttonMask)
Definition KoDialog.cpp:195
void setDefaultButton(ButtonCode id)
Definition KoDialog.cpp:302
@ 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_NOOP(cond)
Definition kis_assert.h:130
QString button(const QWheelEvent &ev)
auto filterContainer(C &container, KeepIfFunction keepIf) -> decltype(bool(keepIf(container[0])), void())