10#include <QStandardPaths>
11#include <QPluginLoader>
18#include <klocalizedstring.h>
19#include <kpluginfactory.h>
48#include <QButtonGroup>
49#include <QJsonDocument>
60 , m_image(doc->image())
70 m_page->layout()->setContentsMargins(0, 0, 0, 0);
74 m_page->bnExportOptions->setIcon(editIcon);
75 m_page->bnRenderOptions->setIcon(editIcon);
78 m_page->lblWarnings->hide();
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();
100 m_page->wdgExportButtons->hide();
101 m_page->stkExport->hide();
106 m_page->intStart->setMinimum(0);
110 m_page->intHeight->setMinimum(1);
111 m_page->intHeight->setMaximum(100000);
113 m_page->intWidth->setMinimum(1);
114 m_page->intWidth->setMaximum(100000);
118 const bool hasAudio = audioFiles.count() > 0;
119 m_page->chkIncludeAudio->setEnabled(hasAudio);
125 Q_FOREACH(
const QString &mime, mimes) {
127 if (description.isEmpty()) {
131 m_page->cmbMimetype->addItem(description, mime);
133 if (mime ==
"image/png") {
134 m_page->cmbMimetype->setCurrentIndex(
m_page->cmbMimetype->count() - 1);
140 m_videoFormatPreferences = loadVideoFormatPreferences();
143 m_page->cmbRenderType->addItem(videoFormat->title(), QVariant(videoFormat->key()));
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");
158 m_page->cmbRenderType->setPlaceholderText(i18nc(
"Not applicable. No render types without valid ffmpeg path.",
"N/A"));
165 connect(m_exportButtonGroup,
166 QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked),
180 connect(
m_page->ffmpegLocation, SIGNAL(fileSelected(QString)), SLOT(
setFFmpegPath(QString)));
216 if (cfgFFmpegPath.isEmpty()) {
218 cfgFFmpegPath = (ffmpegInfo[
"enabled"].toBool()) ? ffmpegInfo[
"path"].toString() :
"";
223 QString likelyFFmpegPath = [&]() {
225 if (!lastUsedOptions.
ffmpegPath.isEmpty() && QFileInfo(lastUsedOptions.
ffmpegPath).isExecutable()) {
230 if (!cfgFFmpegPath.isEmpty() && QFileInfo(cfgFFmpegPath).isExecutable()) {
231 return cfgFFmpegPath;
235 QString systemFFmpeg = QStandardPaths::findExecutable(
"ffmpeg");
236 if (!systemFFmpeg.isEmpty() && QFileInfo(systemFFmpeg).isExecutable()) {
242 return (ffmpegJsonObj[
"enabled"].toBool()) ? ffmpegJsonObj[
"path"].toString() :
"";
245 if (!likelyFFmpegPath.isEmpty() && QFileInfo(likelyFFmpegPath).isExecutable()) {
264 m_page->dirRequester->setStartDir(documentPath);
278 m_page->dirRequester->setStartDir(documentPath);
288 for (
int i = 0; i <
m_page->cmbMimetype->count(); ++i) {
290 m_page->cmbMimetype->setCurrentIndex(i);
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);
304 const QString &lastVideoType = lastUsedOptions.videoFormatKey;
306 const QString &lastVideoType = lastUsedOptions.
videoMimeType;
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);
318 m_page->bnExportVideo->setChecked(
true);
320 m_page->bnExportImages->setChecked(
true);
349 m_page->ffmpegLocation->setFileName(likelyFFmpegPath);
351 m_page->ffmpegLocation->setReadOnlyText(
true);
374 m_page->chkIncludeAudio->setChecked(hasAudioLoaded);
383 return m_page->shouldExportOnlyImageSequence->isChecked();
390 return m_page->bnExportVideo->isChecked();
392 return m_page->shouldExportOnlyVideo->isChecked();
400 QString *customFFMpegOptionsString,
406 QScopedPointer<KisVideoExportOptionsDialog> encoderConfigWidget(
410 encoderConfigWidget->setSupportsHDR(
true);
411 encoderConfigWidget->setConfiguration(cfg);
412 *customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString();
413 *renderHDR = encoderConfigWidget->videoConfiguredForHDR();
420 return (type.startsWith(
"image/")
421 || (type.startsWith(
"application/") &&
422 !type.startsWith(
"application/x-spriter")));
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";
438 return supportedMimeTypes;
443 Q_FOREACH(
const KoID &
encoder, encodersExpected ) {
444 if (encodersPresent.contains(
encoder.id())) {
457 Q_FOREACH(
const QString& mime, input) {
458 if ( mime ==
"video/x-matroska" ) {
465 }
else if (mime ==
"video/mp4") {
472 }
else if (mime ==
"video/webm") {
479 }
else if (mime ==
"image/gif") {
483 }
else if (mime ==
"image/apng") {
487 }
else if (mime ==
"image/webp") {
491 }
else if (mime ==
"video/ogg") {
504 return (mime ==
"image/png");
508QVariantMap KisDlgAnimationRenderer::loadVideoFormatPreferences()
511 QString s = settings->getString(QStringLiteral(
"format_preferences"));
513 QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8());
514 if (doc.isObject()) {
515 return doc.toVariant().toMap();
518 return QVariantMap();
521void KisDlgAnimationRenderer::saveVideoFormatPreferences(
const QVariantMap &
value)
524 settings->setProperty(QStringLiteral(
"format_preferences"),
525 QString::fromUtf8(QJsonDocument::fromVariant(
value).toJson(QJsonDocument::Compact)));
544 return videoType.contains(QStringLiteral(
":gif"));
546 return videoType == QStringLiteral(
"image/gif");
556 return !videoType.startsWith(QStringLiteral(
"image/"));
580 m_page->cmbRenderType->setDisabled(
true);
581 m_page->bnRenderOptions->setDisabled(
true);
583 QString previousMimeType =
m_page->cmbRenderType->currentData().toString();
585 m_page->cmbRenderType->clear();
591 ffmpegVersion = ffmpegJsonObj[
"enabled"].toBool() ? ffmpegJsonObj[
"version"].toString() : i18n(
"No valid FFmpeg binary supplied...");
596 QJsonObject codecjson = ffmpegJsonObj[
"codecs"].toObject()[codec].toObject();
597 if ( codecjson[
"encoding"].toBool() ) {
598 QJsonArray codecEncoders = codecjson[
"encoders"].toArray();
604 if (codecEncoders.size() == 0) {
605 codecEncoders.push_back(QJsonValue(codec));
608 Q_FOREACH(
const QJsonValue&
value, codecEncoders) {
626 int previousMimeTypeIndex = -1;
627 Q_FOREACH (
const QString &mime, supportedMimeTypes) {
629 if (description.isEmpty()) {
633 m_page->cmbRenderType->addItem(description, mime);
634 if (mime == previousMimeType) {
635 previousMimeTypeIndex =
m_page->cmbRenderType->count() - 1;
639 const int indexCount =
m_page->cmbRenderType->count();
640 if (indexCount > 0) {
641 if (previousMimeTypeIndex >= 0) {
642 m_page->cmbRenderType->setCurrentIndex(previousMimeTypeIndex % indexCount);
644 m_page->cmbRenderType->setCurrentIndex(0);
648 m_page->cmbRenderType->setDisabled(
false);
649 m_page->bnRenderOptions->setDisabled(
false);
666 setUpdatesEnabled(
false);
669 setUpdatesEnabled(
true);
675 bool exportMayFail =
false;
678 QString videoType =
m_page->cmbRenderType->itemData(
m_page->cmbRenderType->currentIndex()).toString();
682 const QRegularExpression minVerFFMpegRX(R
"(^n{0,1}(?:[0-3]|4\.[01])[\.\-])");
683 const QRegularExpressionMatch minVerFFMpegMatch = minVerFFMpegRX.match(
ffmpegVersion);
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");
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");
699 exportMayFail =
true;
700 warnings << i18nc(
"ffmpeg warning checks",
"FPS beyond 30 are not widely supported.");
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.");
709 if (warnings.isEmpty()) {
710 m_page->lblWarnings->hide();
712 QString text = QString(
"<p><b>%1</b>").arg(i18n(
"Warning(s):"));
714 Q_FOREACH (
const QString &warning, warnings) {
716 text.append(warning.toHtmlEscaped());
717 text.append(
"</li>");
719 text.append(
"</ul></p>");
721 text.append(QStringLiteral(
"<p>"));
723 i18nc(
"ffmpeg warning checks",
"The export may fail and some devices may not be able to play it.")
725 text.append(QStringLiteral(
"</p>"));
727 m_page->lblWarnings->setText(text);
728 m_page->lblWarnings->show();
737 if (!mimeType.isEmpty()) {
738 return QString(
"%1.%2").arg(QFileInfo(docFileName).completeBaseName(),
747 if (
m_page->cmbRenderType->count() == 0)
return;
749 const QString mimeType =
m_page->cmbRenderType->itemData(index).toString();
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();
760 videoFileName = QString(
"%1%2%3.%4")
764 m_page->videoFilename->setFileName(videoFileName);
789 QString key =
m_page->cmbRenderType->currentData().toString();
794 if (dlg.exec() == QDialog::Accepted) {
795 m_videoFormatPreferences.insert(key, dlg.
preferences());
796 saveVideoFormatPreferences(m_videoFormatPreferences);
799 const int index =
m_page->cmbRenderType->currentIndex();
800 const QString mimetype =
m_page->cmbRenderType->itemData(index).toString();
825 dlg.setMainWidget(encoderConfigWidget);
827 if (dlg.exec() == QDialog::Accepted) {
835 dlg.setMainWidget(0);
836 encoderConfigWidget->deleteLater();
842 int index =
m_page->cmbMimetype->currentIndex();
846 QString mimetype =
m_page->cmbMimetype->itemData(index).toString();
851 if (frameExportConfigWidget) {
863 exportConfig->setProperty(
"forceSRGB",
false);
872 if (dlg.exec() == QDialog::Accepted) {
879 frameExportConfigWidget->hide();
881 frameExportConfigWidget->setParent(0);
882 frameExportConfigWidget->deleteLater();
894 QString videoType =
m_page->cmbRenderType->currentData().toString();
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));
950 if (forceNecessaryHDRSettings) {
952 cfg->setProperty(
"forceSRGB",
false);
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)) {
976 }
else if (ffmpegBinary.exists()) {
988 QString fileName =
m_page->videoFilename->fileName();
990 if (fileName.isEmpty()) {
991 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"Please enter a file name to render to."));
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."));
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."));
1014 m_imageDirectory.clear();
1015 m_videoFileName.clear();
1023 m_videoFileName = dialog.filename();
1024 if (m_videoFileName.isEmpty()) {
1030 m_imageDirectory = dialog.filename();
1031 if (m_imageDirectory.isEmpty()) {
1048 cfg->setProperty(
"storeColorSpaceInfo", enable);
1049 cfg->setProperty(
"writeCicpIfPossible", enable);
1051 cfg->setProperty(
"storeExtraColorChunks",
false);
1052 cfg->setProperty(
"floatingPointConversionOption",
"Rec2100PQ");
1058 return cfg->getPropertyLazy(
"storeColorSpaceInfo", wantsRenderWithHdr)
1059 && cfg->getPropertyLazy(
"writeCicpIfPossible", wantsRenderWithHdr)
1060 && (cfg->getPropertyLazy(
"floatingPointConversionOption", wantsRenderWithHdr?
"Rec2100PQ":
"KeepSame") ==
"Rec2100PQ");
1065 setUpdatesEnabled(
false);
1071 m_page->lblWarnings->hide();
1076 if (
m_page->shouldExportOnlyVideo->isChecked()) {
1078 m_page->cmbMimetype->setEnabled(
false);
1079 m_page->cmbMimetype->setCurrentIndex(
m_page->cmbMimetype->findData(
"image/png"));
1085 if (!
m_page->shouldExportOnlyVideo->isChecked() &&
1086 !
m_page->shouldExportOnlyImageSequence->isChecked()) {
1089 m_page->shouldExportOnlyImageSequence->setChecked(
true);
1095 setUpdatesEnabled(
true);
1100 m_page->chkIncludeAudio->setVisible(
m_page->cmbRenderType->count() != 0
1112 float newHeight =
m_page->intWidth->value() / aspectRatio ;
1114 m_page->intHeight->setValue(newHeight);
1125 float newWidth = aspectRatio *
m_page->intHeight->value();
1127 m_page->intWidth->setValue(newWidth);
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
QString customFFMpegOptions
bool shouldDeleteSequence
bool wantsOnlyUniqueFrameSequence
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)
bool m_wantsRenderWithHDR
QStringList filterMimeTypeListByAvailableEncoders(const QStringList &mimeTypes)
KisDlgAnimationRenderer(KisDocument *doc, QWidget *parent=0)
bool wantVideoExport() const
void slotExportTypeChanged()
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)
bool wantImageSequenceExport() const
void slotLockAspectRatioDimensionsHeight(int height)
static void setPngHdr(KisPropertiesConfigurationSP cfg, bool enable=true)
QString m_customFFMpegOptionsString
void slotLockAspectRatioDimensionsWidth(int width)
static void getDefaultVideoEncoderOptions(const QString &mimeType, KisPropertiesConfigurationSP cfg, const QStringList &availableEncoders, QString *customFFMpegOptionsString, bool *forceHDRVideo)
void slotDialogAccepted()
void slotRenderTypeChanged()
void selectRenderOptions()
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
~KisDlgAnimationRenderer() override
static void saveLastUsedConfiguration(QString configurationID, KisPropertiesConfigurationSP config)
static void filterSequenceMimeTypes(QStringList &mimeTypes)
void slotButtonClicked(int button) override
void selectRenderType(int i)
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)
QString exportSequenceBaseName()
const KisTimeSpan & activePlaybackRange() const
activePlaybackRange
const KisTimeSpan & documentPlaybackRange() const
documentPlaybackRange
QString exportSequenceFilePath()
int exportInitialFrameNumber()
void setExportInitialFrameNumber(const int frameNum)
void setExportSequenceFilePath(const QString &filePath)
std::optional< KisRelativeContentLightLevelInformation > relativeContentLightLevelInformation() const
relativeContentLightLevelInformation This returns (optionally) a KisRelativeContentLightLevelInformat...
KisImageAnimationInterface * animationInterface() const
std::optional< double > hdrReferenceWhiteLightLevel() const
HDR Reference White livel.
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)
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.
KisPropertiesConfigurationSP configuration() const override
static ContainerType mimeToContainer(const QString &mimeType)
QString customUserOptionsString() const
void setHDRConfiguration(bool value)
void setConfiguration(const KisPropertiesConfigurationSP config) override
bool videoConfiguredForHDR() const
static QVector< KoID > encoderIdentifiers(ContainerType type)
void setSupportsHDR(bool value)
A dialog base class with standard buttons and predefined layouts.
virtual void slotButtonClicked(int button)
QPushButton * button(ButtonCode id) const
void setMainWidget(QWidget *widget)
virtual void setCaption(const QString &caption)
void setButtons(ButtonCodes buttonMask)
void setDefaultButton(ButtonCode id)
@ Ok
Show Ok button. (this button accept()s the dialog; result set to QDialog::Accepted)
@ Cancel
Show Cancel-button. (this button reject()s the dialog; result set to QDialog::Rejected)
Encoder * encoder(Imf::OutputFile &file, const ExrPaintLayerSaveInfo &info, int width)
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
QString button(const QWheelEvent &ev)
QIcon loadIcon(const QString &name)
auto filterContainer(C &container, KeepIfFunction keepIf) -> decltype(bool(keepIf(container[0])), void())