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 if constexpr (PLATFORM_SUPPORTS_FFMPEG) {
238 m_page->shouldExportOnlyVideo->setChecked(lastUsedOptions.shouldEncodeVideo);
239 m_page->shouldExportOnlyImageSequence->setChecked(!lastUsedOptions.shouldDeleteSequence);
240 } else {
241 m_page->shouldExportOnlyVideo->setChecked(false);
242 m_page->shouldExportOnlyVideo->setEnabled(false);
243 m_page->shouldExportOnlyVideo->setVisible(false);
244 m_page->shouldExportOnlyImageSequence->setChecked(true);
245 m_page->shouldExportOnlyImageSequence->setCheckable(false);
246 }
247
249
250 {
251 KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
252
253 QStringList encodersPresent;
254 Q_FOREACH(const QString& key, ffmpegEncoderTypes.keys()) {
255 encodersPresent << ffmpegEncoderTypes[key];
256 }
257
258 getDefaultVideoEncoderOptions(lastUsedOptions.videoMimeType, settings,
259 encodersPresent,
262 }
263
264 {
265 KisPropertiesConfigurationSP settings = loadLastConfiguration("img_sequence/" + lastUsedOptions.frameMimeType);
266 m_wantsRenderWithHDR = settings->getPropertyLazy("saveAsHDR", m_wantsRenderWithHDR);
267 }
268
269 m_page->ffmpegLocation->setFileName(likelyFFmpegPath);
270 m_page->ffmpegLocation->setStartDir(QFileInfo(m_doc->localFilePath()).path());
271 m_page->ffmpegLocation->setReadOnlyText(true);
272
273 // Initialize these settings based on the current document context..
274 m_page->intStart->setValue(doc.image()->animationInterface()->activePlaybackRange().start());
275 m_page->intEnd->setValue(doc.image()->animationInterface()->activePlaybackRange().end());
276 m_page->intFramesPerSecond->setValue(doc.image()->animationInterface()->framerate());
277
278 if (!doc.image()->animationInterface()->exportSequenceFilePath().isEmpty()
279 && QDir(doc.image()->animationInterface()->exportSequenceFilePath()).exists() ) {
280 m_page->dirRequester->setStartDir(doc.image()->animationInterface()->exportSequenceFilePath());
281 m_page->dirRequester->setFileName(doc.image()->animationInterface()->exportSequenceFilePath());
282 }
283
284 if (!doc.image()->animationInterface()->exportSequenceBaseName().isEmpty()) {
285 m_page->txtBasename->setText(doc.image()->animationInterface()->exportSequenceBaseName());
286 }
287
288 if (doc.image()->animationInterface()->exportInitialFrameNumber() != -1) {
289 m_page->sequenceStart->setValue(doc.image()->animationInterface()->exportInitialFrameNumber());
290 }
291
292 bool hasAudioLoaded = doc.getAudioTracks().count() > 0;
293 m_page->chkIncludeAudio->setChecked(hasAudioLoaded);
294}
295
298 const QStringList &availableEncoders,
299 QString *customFFMpegOptionsString,
300 bool *renderHDR)
301{
304
305 QScopedPointer<KisVideoExportOptionsDialog> encoderConfigWidget(
306 new KisVideoExportOptionsDialog(containerType, availableEncoders, 0));
307
308 // we always enable HDR, letting the user to force it
309 encoderConfigWidget->setSupportsHDR(true);
310 encoderConfigWidget->setConfiguration(cfg);
311 *customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString();
312 *renderHDR = encoderConfigWidget->videoConfiguredForHDR();
313}
314
316{
317 KritaUtils::filterContainer(mimeTypes, [](QString type) {
318 return (type.startsWith("image/")
319 || (type.startsWith("application/") &&
320 !type.startsWith("application/x-spriter")));
321 });
322}
323
325{
326 QStringList supportedMimeTypes = QStringList();
327 supportedMimeTypes << "video/x-matroska";
328 supportedMimeTypes << "video/mp4";
329 supportedMimeTypes << "video/webm";
330 supportedMimeTypes << "image/gif";
331 supportedMimeTypes << "image/apng";
332 supportedMimeTypes << "image/webp";
333 supportedMimeTypes << "video/ogg";
334
335 return supportedMimeTypes;
336}
337
340 Q_FOREACH(const KoID &encoder, encodersExpected ) {
341 if (encodersPresent.contains(encoder.id())) {
342 return true;
343 }
344 }
345 return false;
346}
347
348// For dependencies, see here:
349// https://en.wikipedia.org/wiki/Comparison_of_video_container_formats
351{
352 QStringList retValue;
353
354 Q_FOREACH(const QString& mime, input) {
355 if ( mime == "video/x-matroska" ) {
356 if ( ffmpegCodecs.contains("h264") || ffmpegCodecs.contains("vp9") ) {
357 QList<QString> encodersPresent;
358 encodersPresent << ffmpegEncoderTypes["h264"] << ffmpegEncoderTypes["vp9"];
360 retValue << mime;
361 }
362 } else if (mime == "video/mp4") {
363 if ( ffmpegCodecs.contains("h264") || ffmpegCodecs.contains("vp9") ) {
364 QList<QString> encodersPresent;
365 encodersPresent << ffmpegEncoderTypes["h264"] << ffmpegEncoderTypes["vp9"];
367 retValue << mime;
368 }
369 } else if (mime == "video/webm") {
370 if ( ffmpegCodecs.contains("vp9") ) {
371 QList<QString> encodersPresent;
372 encodersPresent << ffmpegEncoderTypes["vp9"];
374 retValue << mime;
375 }
376 } else if (mime == "image/gif") {
377 if ( ffmpegCodecs.contains("gif") ) {
378 retValue << mime;
379 }
380 } else if (mime == "image/apng") {
381 if ( ffmpegCodecs.contains("apng") ) {
382 retValue << mime;
383 }
384 } else if (mime == "image/webp") {
385 if ( ffmpegCodecs.contains("webp") ) {
386 retValue << mime;
387 }
388 } else if (mime == "video/ogg") {
389 if ( ffmpegCodecs.contains("theora") ) {
390 retValue << mime;
391 }
392 }
393 }
394
395 return retValue;
396}
397
399{
400 return (mime == "image/png");
401}
402
404 KisConfig globalConfig(true);
405 return globalConfig.exportConfiguration(configurationID);
406}
407
409{
410 KisConfig globalConfig(false);
411 globalConfig.setExportConfiguration(configurationID, config);
412}
413
414void KisDlgAnimationRenderer::setFFmpegPath(const QString& path) {
415 // Let's START with the assumption that user-specified ffmpeg path is invalid
416 // and clear out all of the ffmpeg-specific fields to fill post-validation...
417 m_page->cmbRenderType->setDisabled(true);
418 m_page->bnRenderOptions->setDisabled(true);
419
420 QString previousMimeType = m_page->cmbRenderType->currentData().toString();
421
422 m_page->cmbRenderType->clear();
423 ffmpegEncoderTypes.clear();
424
425 // Validate FFmpeg binary and setup FFMpeg...
426 if (validateFFmpeg(path)) {
427 QJsonObject ffmpegJsonObj = KisFFMpegWrapper::findFFMpeg(path);
428 ffmpegVersion = ffmpegJsonObj["enabled"].toBool() ? ffmpegJsonObj["version"].toString() : i18n("No valid FFmpeg binary supplied...");
430
431 // Build map of encoding types to their specific encoder support (e.g. h264 => libopenh264, h264, h264_vaapi or whatever)
432 Q_FOREACH(const QString& codec, ffmpegCodecs) {
433 QJsonObject codecjson = ffmpegJsonObj["codecs"].toObject()[codec].toObject();
434 if ( codecjson["encoding"].toBool() ) {
435 QJsonArray codecEncoders = codecjson["encoders"].toArray();
436
437 // In the case where no specific codec "library" is specified but we do support
438 // encoding, we simply push the type onto the list regardless. This basically
439 // means that there's no "specific" requirements that we need to meet and
440 // encoding should be possible.
441 if (codecEncoders.size() == 0) {
442 codecEncoders.push_back(QJsonValue(codec));
443 }
444
445 Q_FOREACH(const QJsonValue& value, codecEncoders) {
446 if (ffmpegEncoderTypes.contains(codec)) {
447 ffmpegEncoderTypes[codec].push_back(value.toString());
448 } else {
449 ffmpegEncoderTypes.insert(codec, {value.toString()} );
450 }
451 }
452 }
453 }
454
455 KisConfig cfg(false);
456
457 { // Build list of supported container types and repopulate cmbRenderType.
458 KisSignalsBlocker(m_page->cmbRenderType);
459
460 QStringList supportedMimeTypes = makeVideoMimeTypesList();
461 supportedMimeTypes = filterMimeTypeListByAvailableEncoders(supportedMimeTypes);
462
463 int previousMimeTypeIndex = -1;
464 Q_FOREACH (const QString &mime, supportedMimeTypes) {
465 QString description = KisMimeDatabase::descriptionForMimeType(mime);
466 if (description.isEmpty()) {
467 description = mime;
468 }
469
470 m_page->cmbRenderType->addItem(description, mime);
471 if (mime == previousMimeType) {
472 previousMimeTypeIndex = m_page->cmbRenderType->count() - 1;
473 }
474 }
475
476 const int indexCount = m_page->cmbRenderType->count();
477 if (indexCount > 0) {
478 if (previousMimeTypeIndex >= 0) {
479 m_page->cmbRenderType->setCurrentIndex(previousMimeTypeIndex % indexCount);
480 } else {
481 m_page->cmbRenderType->setCurrentIndex(0);
482 }
483
484 selectRenderType(m_page->cmbRenderType->currentIndex());
485 m_page->cmbRenderType->setDisabled(false);
486 m_page->bnRenderOptions->setDisabled(false);
487 connect(m_page->cmbRenderType, SIGNAL(currentIndexChanged(int)), this, SLOT(selectRenderType(int)));
488 }
489 }
490
491 m_page->lblFFMpegVersion->setText(ffmpegVersion);
492
493 // Store configuration..
494 cfg.setFFMpegLocation(ffmpegJsonObj["path"].toString());
495
497 }
498}
499
501 const QString mimeType = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString();
502
503 const QRegularExpression minVerFFMpegRX(R"(^n{0,1}(?:[0-3]|4\.[01])[\.\-])");
504 const QRegularExpressionMatch minVerFFMpegMatch = minVerFFMpegRX.match(ffmpegVersion);
505
506 QStringList warnings;
507
508 if (mimeType == "image/gif" && minVerFFMpegMatch.hasMatch()) {
509 warnings << i18nc("ffmpeg warning checks", "FFmpeg must be at least version 4.2+ for GIF transparency to work");
510 }
511
512 // m_page->bnRenderOptions->setEnabled(mimeType != "image/gif" && mimeType != "image/webp" && mimeType !=
513 // "image/png" );
514 if (mimeType == "image/gif" && m_page->intFramesPerSecond->value() > 50) {
515 warnings << i18nc("ffmpeg warning checks",
516 "Animated GIF images cannot have a framerate higher than 50. The framerate will be reduced "
517 "to 50 frames per second");
518 }
519
520 m_page->lblWarnings->setVisible(!warnings.isEmpty());
521
522 if (!warnings.isEmpty()) {
523 QString text = QString("<p><b>%1</b>").arg(i18n("Warning(s):"));
524 text.append("<ul>");
525 Q_FOREACH (const QString &warning, warnings) {
526 text.append("<li>");
527 text.append(warning.toHtmlEscaped());
528 text.append("</li>");
529 }
530 text.append("</ul></p>");
531 m_page->lblWarnings->setText(text);
532
533 m_page->lblWarnings->setPixmap(
534 m_page->lblWarnings->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(32, 32)));
535 }
536
537 m_page->adjustSize();
538}
539
540QString KisDlgAnimationRenderer::defaultVideoFileName(KisDocument *doc, const QString &mimeType)
541{
542 const QString docFileName = !doc->localFilePath().isEmpty() ? doc->localFilePath() : i18n("Untitled");
543
544 if (!mimeType.isEmpty()) {
545 return QString("%1.%2").arg(QFileInfo(docFileName).completeBaseName(),
546 KisMimeDatabase::suffixesForMimeType(mimeType).first());
547 } else {
548 return docFileName;
549 }
550}
551
553{
554 if (m_page->cmbRenderType->count() == 0) return;
555
556 const QString mimeType = m_page->cmbRenderType->itemData(index).toString();
557
558 /*
559 m_page->bnRenderOptions->setEnabled(mimeType != "image/gif" && mimeType != "image/webp" && mimeType != "image/png");
560 */
561
563
564 QString videoFileName = defaultVideoFileName(m_doc, mimeType);
565
566 if (!m_page->videoFilename->fileName().isEmpty()) {
567 const QFileInfo info = QFileInfo(m_page->videoFilename->fileName());
568 const QString baseName = info.completeBaseName();
569 const QString path = info.path();
570
571 videoFileName = QString("%1%2%3.%4")
572 .arg(path, "/", baseName, KisMimeDatabase::suffixesForMimeType(mimeType).first());
573 }
574 m_page->videoFilename->setMimeTypeFilters(QStringList() << mimeType, mimeType);
575 m_page->videoFilename->setFileName(videoFileName);
576
577 m_wantsRenderWithHDR = (mimeType == "video/mp4") ? m_wantsRenderWithHDR : false;
578
579 { // We've got to reload the render settings to account for the user changing render type without configuration.
580 // If this is removed from the configuration, ogg vorbis can fail to render on first attempt. BUG:421658
581 // This should be revisited at some point, too much configuration juggling in this class makes it error-prone...
582
583 QStringList encodersPresent;
584 Q_FOREACH(const QString& key, ffmpegEncoderTypes.keys()) {
585 encodersPresent << ffmpegEncoderTypes[key];
586 }
587
588 KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
589 getDefaultVideoEncoderOptions(mimeType, settings,
590 encodersPresent,
593 }
594}
595
597{
598 const int index = m_page->cmbRenderType->currentIndex();
599 const QString mimetype = m_page->cmbRenderType->itemData(index).toString();
600
603
604 QStringList encodersPresent;
605 Q_FOREACH(const QString& key, ffmpegEncoderTypes.keys()) {
606 encodersPresent << ffmpegEncoderTypes[key];
607 }
608
609 KisVideoExportOptionsDialog *encoderConfigWidget =
610 new KisVideoExportOptionsDialog(containerType, encodersPresent, this);
611
612 // we always enable HDR, letting the user to force it
613 encoderConfigWidget->setSupportsHDR(true);
614
615 {
616 KisPropertiesConfigurationSP settings = loadLastConfiguration("VIDEO_ENCODER");
617 encoderConfigWidget->setConfiguration(settings);
618 encoderConfigWidget->setHDRConfiguration(m_wantsRenderWithHDR);
619 }
620
621 KoDialog dlg(this);
622 dlg.setMainWidget(encoderConfigWidget);
624 if (dlg.exec() == QDialog::Accepted) {
625 saveLastUsedConfiguration("VIDEO_ENCODER", encoderConfigWidget->configuration());
627 m_wantsRenderWithHDR = encoderConfigWidget->videoConfiguredForHDR();
628 }
629
630 dlg.setMainWidget(0);
631 encoderConfigWidget->deleteLater();
632}
633
635{
636 int index = m_page->cmbMimetype->currentIndex();
637
638 KisConfigWidget *frameExportConfigWidget = 0;
639
640 QString mimetype = m_page->cmbMimetype->itemData(index).toString();
642 if (filter) {
643 frameExportConfigWidget = filter->createConfigurationWidget(0, KisDocument::nativeFormatMimeType(), mimetype.toLatin1());
644
645 if (frameExportConfigWidget) {
646
647 KisPropertiesConfigurationSP exportConfig = loadLastConfiguration("img_sequence/" + mimetype);
648 if (exportConfig) {
650 }
651
652 //Important -- m_useHDR allows the synchronization of both the video and image render settings.
653 if(imageMimeSupportsHDR(mimetype)) {
654 exportConfig->setProperty("saveAsHDR", m_wantsRenderWithHDR);
656 exportConfig->setProperty("forceSRGB", false);
657 }
658 }
659
660 frameExportConfigWidget->setConfiguration(exportConfig);
661 KoDialog dlg(this);
662 dlg.setMainWidget(frameExportConfigWidget);
664 if (dlg.exec() == QDialog::Accepted) {
665 m_wantsRenderWithHDR = frameExportConfigWidget->configuration()->getPropertyLazy("saveAsHDR", false);
666 saveLastUsedConfiguration("img_sequence/" + mimetype, frameExportConfigWidget->configuration());
667 }
668
669 frameExportConfigWidget->hide();
670 dlg.setMainWidget(0);
671 frameExportConfigWidget->setParent(0);
672 frameExportConfigWidget->deleteLater();
673
674 }
675 }
676}
677
678
680{
682
684 options.videoMimeType = m_page->cmbRenderType->currentData().toString();
685 options.frameMimeType = m_page->cmbMimetype->currentData().toString();
686 options.scaleFilter = m_page->cmbScaleFilter->currentData().toString();
687
688 options.basename = m_page->txtBasename->text();
689 options.directory = m_page->dirRequester->fileName();
690 options.firstFrame = m_page->intStart->value();
691 options.lastFrame = m_page->intEnd->value();
692 options.sequenceStart = m_page->sequenceStart->value();
693
694 if constexpr (PLATFORM_SUPPORTS_FFMPEG) {
695 options.shouldEncodeVideo = m_page->shouldExportOnlyVideo->isChecked();
696 options.shouldDeleteSequence = !m_page->shouldExportOnlyImageSequence->isChecked();
697 } else {
698 options.shouldEncodeVideo = false;
699 options.shouldDeleteSequence = false;
700 }
701 options.includeAudio = m_page->chkIncludeAudio->isChecked();
702 options.wantsOnlyUniqueFrameSequence = m_page->chkOnlyUniqueFrames->isChecked();
703
704 options.ffmpegPath = m_page->ffmpegLocation->fileName();
705 options.frameRate = m_page->intFramesPerSecond->value();
706
707 if (options.frameRate > 50 && options.videoMimeType == "image/gif") {
708 options.frameRate = 50;
709 }
710
711 options.width = m_page->intWidth->value();
712 options.height = m_page->intHeight->value();
713 options.videoFileName = m_page->videoFilename->fileName();
714
716
717 {
719 if (cfg) {
721 }
722
723 const bool forceNecessaryHDRSettings = m_wantsRenderWithHDR && imageMimeSupportsHDR(options.frameMimeType);
724 if (forceNecessaryHDRSettings) {
725 KIS_SAFE_ASSERT_RECOVER_NOOP(options.frameMimeType == "image/png");
726 cfg->setProperty("forceSRGB", false);
727 cfg->setProperty("saveAsHDR", true);
728 }
729
730 options.frameExportConfig = cfg;
731 }
732
733 return options;
734}
735
737{
738 if (!ffmpegPath.isEmpty()) {
739 QFileInfo ffmpegBinary(ffmpegPath);
740 if (ffmpegBinary.exists() && ffmpegBinary.isExecutable()) {
741 QStringList commpressedFormats{"zip", "7z", "tar.bz2"};
742 Q_FOREACH(const QString& compressedFormat, commpressedFormats) {
743 if (ffmpegBinary.fileName().endsWith(compressedFormat)) {
745 }
746 }
748 } else if (ffmpegBinary.exists()) {
750 }
751 }
753}
754
756{
757 if (button == KoDialog::Ok && (PLATFORM_SUPPORTS_FFMPEG && !m_page->shouldExportOnlyImageSequence->isChecked())) {
758 QString fileName = m_page->videoFilename->fileName();
759
760 if (fileName.isEmpty()) {
761 QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Please enter a file name to render to."));
762 return;
763 }
764 else {
765 switch (validateFFmpeg(m_page->ffmpegLocation->fileName())) {
767 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."));
768 return;
770 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."));
771 return;
772 default:
773 break;
774 }
775 }
776 }
778}
779
790
792{
793 if constexpr (PLATFORM_SUPPORTS_FFMPEG) {
794 // if a video format needs to be outputted
795 if (m_page->shouldExportOnlyVideo->isChecked()) {
796 // videos always uses PNG for creating video, so disable the ability to change the format
797 m_page->cmbMimetype->setEnabled(false);
798 m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->findData("image/png"));
799 }
800
804 if (!m_page->shouldExportOnlyVideo->isChecked() &&
805 !m_page->shouldExportOnlyImageSequence->isChecked()) {
806
807 KisSignalsBlocker b(m_page->shouldExportOnlyImageSequence);
808 m_page->shouldExportOnlyImageSequence->setChecked(true);
809 }
810 } else {
811 KisSignalsBlocker b(m_page->shouldExportOnlyImageSequence);
812 m_page->shouldExportOnlyImageSequence->setChecked(true);
813 }
814}
815
817{
818 Q_UNUSED(framerate);
820}
821
823{
824 Q_UNUSED(width);
825
826 float aspectRatio = (float)m_image->width() / (float)m_image->height();
827
828 // update height here
829 float newHeight = m_page->intWidth->value() / aspectRatio ;
830
831 m_page->intHeight->setValue(newHeight);
832
833}
834
836{
837 Q_UNUSED(height);
838
839 float aspectRatio = (float)m_image->width() / (float)m_image->height();
840
841 // update width here
842 float newWidth = aspectRatio * m_page->intHeight->value();
843
844 m_page->intWidth->setValue(newWidth);
845}
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)
static constexpr bool PLATFORM_SUPPORTS_FFMPEG
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: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_NOOP(cond)
Definition kis_assert.h:130
QString button(const QWheelEvent &ev)
auto filterContainer(C &container, KeepIfFunction keepIf) -> decltype(bool(keepIf(container[0])), void())