Commit 06bec48f authored by Dmitry Kazakov's avatar Dmitry Kazakov

Refactor Animation Export rendering code and HDR video support

1) Remove 'video' impex plugin. Now the exporting classes are built in
   into animationrenderer plugin. It allows us not to pass the options
   via untyped KisPropertiesConfiguration, but via a type-safe and
   compiler-tracked KisAnimationRenderingOptions.

2) Add options for configuring HDR video encoding with HEVC (H265) codec

3) When HDR video option is activated, then PNG export filter is automatically
   forced to output HDR PNG files (see a hack in DlgAnimationRenderer::
   getFrameExportFilterConfiguration())

4) !!! Rendering dialog now supports relative paths for video and frame
   export locations. As a base it uses either document location, or the
   location of animation/frames, if it is present and absolute.
parent 1e91e0bf
......@@ -137,11 +137,11 @@
<isCheckable>false</isCheckable>
<statusTip></statusTip>
</Action>
<Action name="render_image_sequence_again">
<Action name="render_animation_again">
<icon></icon>
<text>&amp;Render Image Sequence Again</text>
<text>&amp;Render Animation Again</text>
<whatsThis></whatsThis>
<toolTip>Render Animation to Image Sequence Again</toolTip>
<toolTip>Render Animation Again</toolTip>
<iconText>Render Animation</iconText>
<activationFlags>1000</activationFlags>
<activationConditions>0</activationConditions>
......
......@@ -21,6 +21,8 @@
#include <QVariant>
#include <KisActionPlugin.h>
class KisAnimationRenderingOptions;
class KisDocument;
class AnimaterionRenderer : public KisActionPlugin
{
......@@ -50,6 +52,9 @@ private Q_SLOTS:
*/
void slotRenderSequenceAgain();
private:
void renderAnimationImpl(KisDocument *doc, KisAnimationRenderingOptions encoderOptions);
};
#endif // ANIMATIONRENDERERIMAGE_H
set(kritaanimationrenderer_SOURCES AnimationRenderer.cpp DlgAnimationRenderer.cpp )
ki18n_wrap_ui(kritaanimationrenderer_SOURCES wdg_animationrenderer.ui )
set(kritaanimationrenderer_SOURCES
AnimationRenderer.cpp
DlgAnimationRenderer.cpp
KisAnimationRenderingOptions.cpp
video_export_options_dialog.cpp
video_saver.cpp
)
ki18n_wrap_ui(kritaanimationrenderer_SOURCES
wdg_animationrenderer.ui
video_export_options_dialog.ui)
add_library(kritaanimationrenderer MODULE ${kritaanimationrenderer_SOURCES})
target_link_libraries(kritaanimationrenderer kritaui)
install(TARGETS kritaanimationrenderer DESTINATION ${KRITA_PLUGIN_INSTALL_DIR})
......@@ -24,12 +24,15 @@
#include "ui_wdg_animationrenderer.h"
#include <QSharedPointer>
#include <QScopedPointer>
#include <kis_types.h>
class KisDocument;
class KisImportExportFilter;
class KisConfigWidget;
class QHBoxLayout;
class VideoSaver;
class KisAnimationRenderingOptions;
class WdgAnimationRenderer : public QWidget, public Ui::WdgAnimaterionRenderer
{
......@@ -53,37 +56,17 @@ public:
DlgAnimationRenderer(KisDocument *doc, QWidget *parent = 0);
~DlgAnimationRenderer() override;
KisPropertiesConfigurationSP getSequenceConfiguration() const;
void setSequenceConfiguration(KisPropertiesConfigurationSP cfg);
KisPropertiesConfigurationSP getFrameExportConfiguration() const;
KisPropertiesConfigurationSP getVideoConfiguration() const;
void setVideoConfiguration(KisPropertiesConfigurationSP cfg);
KisPropertiesConfigurationSP getEncoderConfiguration() const;
void setEncoderConfiguration(KisPropertiesConfigurationSP cfg);
QSharedPointer<KisImportExportFilter> encoderFilter() const;
// fires when the render animation action is called. makes sure the correct export type is selected for the UI
void updateExportUIOptions();
KisAnimationRenderingOptions getEncoderOptions() const;
private Q_SLOTS:
void selectRenderType(int i);
void selectRenderOptions();
/**
* @brief createEncoderWidget
* creates a new settings widget for the filetype.
*/
void createEncoderWidget(int index);
/**
* @brief sequenceMimeTypeSelected
* calls the dialog for the export widget.
*/
void sequenceMimeTypeSelected();
void ffmpegLocationChanged(const QString&);
void slotLockAspectRatioDimensionsWidth(int width);
void slotLockAspectRatioDimensionsHeight(int height);
......@@ -94,22 +77,29 @@ private Q_SLOTS:
protected Q_SLOTS:
void slotButtonClicked(int button) override;
void slotDialogAccepted();
private:
QString fetchRenderingDirectory() const;
QString fetchRenderingFileName() const;
void loadAnimationOptions(const KisAnimationRenderingOptions &options);
static QString defaultVideoFileName(KisDocument *doc, const QString &mimeType);
static void getDefaultVideoEncoderOptions(const QString &mimeType,
KisPropertiesConfigurationSP cfg,
QString *customFFMpegOptionsString,
bool *forceHDRVideo);
private:
static QString findFFMpeg();
static QString findFFMpeg(const QString &customLocation);
KisImageSP m_image;
KisDocument *m_doc;
WdgAnimationRenderer *m_page {0};
QList<QSharedPointer<KisImportExportFilter>> m_renderFilters;
KisConfigWidget *m_encoderConfigWidget {0};
KisConfigWidget *m_frameExportConfigWidget {0};
QString m_defaultFileName;
KisPropertiesConfigurationSP m_frameExportConfig;
QString m_customFFMpegOptionsString;
bool m_forceHDRVideo = false;
};
#endif // DLG_ANIMATIONRENDERERIMAGE
#include "KisAnimationRenderingOptions.h"
#include <QStandardPaths>
#include <QFileInfo>
#include <QDir>
KisAnimationRenderingOptions::KisAnimationRenderingOptions()
: videoMimeType("video/mp4"),
frameMimeType("image/png"),
basename("frame"),
directory("")
{
}
inline QString composePath(const QString &pathChunk, const QString &fileNameChunk)
{
if (QFileInfo(fileNameChunk).isAbsolute()) {
return fileNameChunk;
}
return QFileInfo(QDir(QFileInfo(pathChunk).absolutePath()),
fileNameChunk).absoluteFilePath();
}
QString KisAnimationRenderingOptions::resolveAbsoluteVideoFilePath() const
{
return composePath(lastDocuemntPath, videoFileName);
}
QString KisAnimationRenderingOptions::resolveAbsoluteFramesDirectory() const
{
if (renderMode() == RENDER_VIDEO_ONLY) {
return QFileInfo(resolveAbsoluteVideoFilePath()).absolutePath();
}
return composePath(lastDocuemntPath, directory);
}
KisAnimationRenderingOptions::RenderMode KisAnimationRenderingOptions::renderMode() const
{
if (shouldDeleteSequence) {
KIS_SAFE_ASSERT_RECOVER_NOOP(shouldEncodeVideo);
return RENDER_VIDEO_ONLY;
} else if (!shouldEncodeVideo) {
KIS_SAFE_ASSERT_RECOVER_NOOP(!shouldDeleteSequence);
return RENDER_FRAMES_ONLY;
} else {
return RENDER_FRAMES_AND_VIDEO;
}
}
KisPropertiesConfigurationSP KisAnimationRenderingOptions::toProperties() const
{
KisPropertiesConfigurationSP config = new KisPropertiesConfiguration();
config->setProperty("basename", basename);
config->setProperty("last_document_path", lastDocuemntPath);
config->setProperty("directory", directory);
config->setProperty("first_frame", firstFrame);
config->setProperty("last_frame", lastFrame);
config->setProperty("sequence_start", sequenceStart);
config->setProperty("video_mimetype", videoMimeType);
config->setProperty("frame_mimetype", frameMimeType);
config->setProperty("encode_video", shouldEncodeVideo);
config->setProperty("delete_sequence", shouldDeleteSequence);
config->setProperty("ffmpeg_path", ffmpegPath);
config->setProperty("framerate", frameRate);
config->setProperty("height", height);
config->setProperty("width", width);
config->setProperty("include_audio", includeAudio);
config->setProperty("filename", videoFileName);
config->setProperty("custom_ffmpeg_options", customFFMpegOptions);
config->setPrefixedProperties("frame_export/", frameExportConfig);
return config;
}
void KisAnimationRenderingOptions::fromProperties(KisPropertiesConfigurationSP config)
{
basename = config->getPropertyLazy("basename", basename);
lastDocuemntPath = config->getPropertyLazy("last_document_path", QString());
directory = config->getPropertyLazy("directory", directory);
firstFrame = config->getPropertyLazy("first_frame", 0);
lastFrame = config->getPropertyLazy("last_frame", 0);
sequenceStart = config->getPropertyLazy("sequence_start", 0);
videoMimeType = config->getPropertyLazy("video_mimetype", videoMimeType);
frameMimeType = config->getPropertyLazy("frame_mimetype", frameMimeType);
shouldEncodeVideo = config->getPropertyLazy("encode_video", false);
shouldDeleteSequence = config->getPropertyLazy("delete_sequence", false);
ffmpegPath = config->getPropertyLazy("ffmpeg_path", QString());
frameRate = config->getPropertyLazy("framerate", 25);
height = config->getPropertyLazy("height", 0);
width = config->getPropertyLazy("width", 0);
includeAudio = config->getPropertyLazy("include_audio", true);
videoFileName = config->getPropertyLazy("filename", QString());
customFFMpegOptions = config->getPropertyLazy("custom_ffmpeg_options", QString());
frameExportConfig = new KisPropertiesConfiguration();
frameExportConfig->setPrefixedProperties("frame_export/", frameExportConfig);
}
#ifndef KISANIMATIONRENDERINGOPTIONS_H
#define KISANIMATIONRENDERINGOPTIONS_H
#include <QString>
#include "kis_properties_configuration.h"
struct KisAnimationRenderingOptions
{
KisAnimationRenderingOptions();
QString lastDocuemntPath;
QString videoMimeType;
QString frameMimeType;
QString basename;
QString directory;
int firstFrame = 0;
int lastFrame = 0;
int sequenceStart = 0;
bool shouldEncodeVideo = false;
bool shouldDeleteSequence = false;
bool includeAudio = false;
QString ffmpegPath;
int frameRate = 25;
int width = 0;
int height = 0;
QString videoFileName;
QString customFFMpegOptions;
KisPropertiesConfigurationSP frameExportConfig;
QString resolveAbsoluteVideoFilePath() const;
QString resolveAbsoluteFramesDirectory() const;
enum RenderMode {
RENDER_FRAMES_ONLY,
RENDER_VIDEO_ONLY,
RENDER_FRAMES_AND_VIDEO
};
RenderMode renderMode() const;
KisPropertiesConfigurationSP toProperties() const;
void fromProperties(KisPropertiesConfigurationSP config);
};
#endif // KISANIMATIONRENDERINGOPTIONS_H
......@@ -35,18 +35,26 @@ class VideoExportOptionsDialog : public KisConfigWidget
Q_OBJECT
public:
enum CodecIndex {
enum ContainerType {
DEFAULT,
OGV
};
enum CodecPageIndex {
CODEC_H264 = 0,
CODEC_H265,
CODEC_THEORA
};
public:
explicit VideoExportOptionsDialog(QWidget *parent = 0);
explicit VideoExportOptionsDialog(ContainerType containerType, QWidget *parent = 0);
~VideoExportOptionsDialog() override;
void setCodec(CodecIndex index);
void setSupportsHDR(bool value);
QStringList customUserOptions() const;
QString customUserOptionsString() const;
bool forceHDRModeForFrames() const;
void setConfiguration(const KisPropertiesConfigurationSP config) override;
KisPropertiesConfigurationSP configuration() const override;
......@@ -56,12 +64,19 @@ private Q_SLOTS:
void slotSaveCustomLine();
void slotResetCustomLine();
void slotCodecSelected(int index);
void slotH265ProfileChanged(int index);
void slotEditHDRMetadata();
private:
Ui::VideoExportOptionsDialog *ui;
private:
QStringList generateCustomLine() const;
QString currentCodecId() const;
private:
struct Private;
const QScopedPointer<Private> m_d;
......
......@@ -12,35 +12,75 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0,1">
<item>
<widget class="QComboBox" name="cmbCodec">
<item>
<property name="text">
<string>H.264, MPEG-4 Part 10</string>
</property>
</item>
<item>
<property name="text">
<string>Theora</string>
</property>
</item>
</widget>
<widget class="QComboBox" name="cmbCodec"/>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
<number>1</number>
</property>
<widget class="QWidget" name="page">
<widget class="QWidget" name="pgH264">
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<widget class="QLabel" name="lblCRFH264">
<property name="text">
<string>Constant Rate Factor:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="KisSliderSpinBox" name="intCRFH264" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblPresetH264">
<property name="text">
<string>Preset:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cmbPresetH264"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lblProfileH264">
<property name="text">
<string>Profile:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="cmbProfileH264"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblTuneH264">
<property name="text">
<string>Tune:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="cmbTuneH264"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="pgH265">
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="lblCRFH265">
<property name="text">
<string>Constant Rate Factor:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="KisSliderSpinBox" name="intConstantRateFactor" native="true">
<widget class="KisSliderSpinBox" name="intCRFH265" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
......@@ -50,38 +90,52 @@
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<widget class="QLabel" name="lblPresetH265">
<property name="text">
<string>Preset:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cmbPreset"/>
<widget class="QComboBox" name="cmbPresetH265"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<widget class="QLabel" name="lblProfileH265">
<property name="text">
<string>Profile:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="cmbProfile"/>
<widget class="QComboBox" name="cmbProfileH265"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblTune">
<widget class="QLabel" name="lblTuneH265">
<property name="text">
<string>Tune:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="cmbTune"/>
<widget class="QComboBox" name="cmbTuneH265"/>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="chkUseHDRMetadata">
<property name="text">
<string>HDR Mode</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="btnHdrMetadata">
<property name="text">
<string>HDR Metadata...</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<widget class="QWidget" name="pgTheora">
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
......
......@@ -36,6 +36,7 @@
#include "kis_config.h"
#include "KisAnimationRenderingOptions.h"
#include <QFileSystemWatcher>
#include <QProcess>
#include <QProgressDialog>
......@@ -186,12 +187,10 @@ private:
};
VideoSaver::VideoSaver(KisDocument *doc, const QString &ffmpegPath, bool batchMode)
VideoSaver::VideoSaver(KisDocument *doc, bool batchMode)
: m_image(doc->image())
, m_doc(doc)
, m_batchMode(batchMode)
, m_ffmpegPath(ffmpegPath)
, m_runner(new KisFFMpegRunner(ffmpegPath))
{
}
......@@ -204,83 +203,51 @@ KisImageSP VideoSaver::image()
return m_image;
}
bool VideoSaver::hasFFMpeg() const
KisImageBuilder_Result VideoSaver::encode(const QString &savedFilesMask, const KisAnimationRenderingOptions &options)
{
return !m_ffmpegPath.isEmpty();
}
KisImageBuilder_Result VideoSaver::encode(const QString &filename, KisPropertiesConfigurationSP configuration)
{
qDebug() << "ffmpeg" << m_ffmpegPath << "filename" << filename << "configuration" << configuration->toXML();
if (m_ffmpegPath.isEmpty()) {
m_ffmpegPath = configuration->getString("ffmpeg_path");
if (!QFileInfo(m_ffmpegPath).exists()) {
m_doc->setErrorMessage(i18n("ffmpeg could not be found at %1", m_ffmpegPath));
return KisImageBuilder_RESULT_FAILURE;
}
if (!QFileInfo(options.ffmpegPath).exists()) {
m_doc->setErrorMessage(i18n("ffmpeg could not be found at %1", options.ffmpegPath));
return KisImageBuilder_RESULT_FAILURE;
}
KisImageBuilder_Result result = KisImageBuilder_RESULT_OK;
KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("first_frame"));
KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("last_frame"));
KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("height"));
KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("width"));
KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("include_audio"));
KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("directory"));
KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("framerate"));
KisImageAnimationInterface *animation = m_image->animationInterface();
const KisTimeRange fullRange = animation->fullClipRange();
const int frameRate = configuration->getInt("framerate", animation->framerate());
const int sequenceNumberingOffset = configuration->getInt("sequence_start", 0);
const KisTimeRange clipRange(sequenceNumberingOffset + configuration->getInt("first_frame", fullRange.start()),
sequenceNumberingOffset + configuration->getInt("last_frame", fullRange.end())
);
const bool includeAudio = configuration->getBool("include_audio", true);
const int exportHeight = configuration->getInt("height", int(m_image->height()));
const int exportWidth = configuration->getInt("width", int(m_image->width()));
const int sequenceNumberingOffset = options.sequenceStart;
const KisTimeRange clipRange(sequenceNumberingOffset + options.firstFrame,
sequenceNumberingOffset + options.lastFrame);
// export dimensions could be off a little bit, so the last force option tweaks the pixels for the export to work
const QString exportDimensions = QString("scale=w=").append(QString::number(exportWidth)).append(":h=")
.append(QString::number(exportHeight)).append(":force_original_aspect_ratio=decrease");
const QString exportDimensions =
QString("scale=w=")
.append(QString::number(options.width))
.append(":h=")
.append(QString::number(options.height))
.append(":force_original_aspect_ratio=decrease");
const QDir framesDir(configuration->getString("directory"));
const QString resultFile = options.resolveAbsoluteVideoFilePath();
const QDir videoDir(QFileInfo(resultFile).absolutePath());
QString resultFile;
if (QFileInfo(filename).isAbsolute()) {
resultFile = filename;
}
else {
resultFile = framesDir.absolutePath() + "/" + filename;
}
const QFileInfo info(resultFile);
const QString suffix = info.suffix().toLower();
const QString palettePath = framesDir.filePath("palette.png");
const QString savedFilesMask = configuration->getString("savedFilesMask");
const QStringList additionalOptionsList = configuration->getString("customUserOptions").split(' ', QString::SkipEmptyParts);
const QString palettePath = videoDir.filePath("palette.png");
const QStringList additionalOptionsList = options.customFFMpegOptions.split(' ', QString::SkipEmptyParts);
QScopedPointer<KisFFMpegRunner> runner(new KisFFMpegRunner(options.ffmpegPath));
if (suffix == "gif") {
{
QStringList args;
args << "-r" << QString::number(frameRate)
args << "-r" << QString::number(options.frameRate)
<< "-start_number" << QString::number(clipRange.start())
<< "-i" << savedFilesMask
<< "-vf" << "palettegen"
<< "-y" << palettePath;
KisImageBuilder_Result result =
m_runner->runFFMpeg(args, i18n("Fetching palette..."),
framesDir.filePath("log_generate_palette_gif.log"),
runner->runFFMpeg(args, i18n("Fetching palette..."),
videoDir.filePath("log_generate_palette_gif.log"),
clipRange.duration());
if (result != KisImageBuilder_RESULT_OK) {
......@@ -290,7 +257,7 @@ KisImageBuilder_Result VideoSaver::encode(const QString &filename, KisProperties
{
QStringList args;
args << "-r" << QString::number(frameRate)
args << "-r" << QString::number(options.frameRate)
<< "-start_number" << QString::number(clipRange.start())
<< "-i" << savedFilesMask
<< "-i" << palettePath
......@@ -299,7 +266,7 @@ KisImageBuilder_Result VideoSaver::encode(const QString &filename, KisProperties
<< "-y" << resultFile;
// if we are exporting out at a different image size, we apply scaling filter
if (m_image->height() != exportHeight || m_image->width() != exportWidth) {
if (m_image->width() != options.width || m_image->height() != options.height) {
args << "-vf" << exportDimensions;
}