diff --git a/plugins/extensions/animationrenderer/AnimationRenderer.cpp b/plugins/extensions/animationrenderer/AnimationRenderer.cpp index 26904d0def..a6f8ffa37e 100644 --- a/plugins/extensions/animationrenderer/AnimationRenderer.cpp +++ b/plugins/extensions/animationrenderer/AnimationRenderer.cpp @@ -1,244 +1,238 @@ /* * Copyright (c) 2016 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "AnimationRenderer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DlgAnimationRenderer.h" #include #include "video_saver.h" #include "KisAnimationRenderingOptions.h" K_PLUGIN_FACTORY_WITH_JSON(AnimaterionRendererFactory, "kritaanimationrenderer.json", registerPlugin();) AnimaterionRenderer::AnimaterionRenderer(QObject *parent, const QVariantList &) : KisActionPlugin(parent) { // Shows the big dialog KisAction *action = createAction("render_animation"); action->setActivationFlags(KisAction::IMAGE_HAS_ANIMATION); connect(action, SIGNAL(triggered()), this, SLOT(slotRenderAnimation())); // Re-renders the image sequence as defined in the last render action = createAction("render_animation_again"); action->setActivationFlags(KisAction::IMAGE_HAS_ANIMATION); connect(action, SIGNAL(triggered()), this, SLOT(slotRenderSequenceAgain())); } AnimaterionRenderer::~AnimaterionRenderer() { } void AnimaterionRenderer::slotRenderAnimation() { KisImageWSP image = viewManager()->image(); if (!image) return; if (!image->animationInterface()->hasAnimation()) return; KisDocument *doc = viewManager()->document(); DlgAnimationRenderer dlgAnimationRenderer(doc, viewManager()->mainWindow()); - dlgAnimationRenderer.setCaption(i18n("Render Animation")); - if (dlgAnimationRenderer.exec() == QDialog::Accepted) { KisAnimationRenderingOptions encoderOptions = dlgAnimationRenderer.getEncoderOptions(); renderAnimationImpl(doc, encoderOptions); } } void AnimaterionRenderer::slotRenderSequenceAgain() { KisImageWSP image = viewManager()->image(); if (!image) return; if (!image->animationInterface()->hasAnimation()) return; KisDocument *doc = viewManager()->document(); KisConfig cfg(true); KisPropertiesConfigurationSP settings = cfg.exportConfiguration("ANIMATION_EXPORT"); KisAnimationRenderingOptions encoderOptions; encoderOptions.fromProperties(settings); renderAnimationImpl(doc, encoderOptions); } void AnimaterionRenderer::renderAnimationImpl(KisDocument *doc, KisAnimationRenderingOptions encoderOptions) { const QString frameMimeType = encoderOptions.frameMimeType; const QString framesDirectory = encoderOptions.resolveAbsoluteFramesDirectory(); const QString extension = KisMimeDatabase::suffixesForMimeType(frameMimeType).first(); const QString baseFileName = QString("%1/%2.%3").arg(framesDirectory) - .arg(encoderOptions.basename) - .arg(extension); - - - /** - * The dialog should ensure that the size of the video is even - */ - KIS_SAFE_ASSERT_RECOVER( - !((encoderOptions.width & 0x1 || encoderOptions.height & 0x1) - && (encoderOptions.videoMimeType == "video/mp4" || - encoderOptions.videoMimeType == "video/x-matroska") && !(encoderOptions.renderMode() == encoderOptions.RENDER_FRAMES_ONLY))) { + .arg(encoderOptions.basename) + .arg(extension); - encoderOptions.width = encoderOptions.width + (encoderOptions.width & 0x1); - encoderOptions.height = encoderOptions.height + (encoderOptions.height & 0x1); + if (mustHaveEvenDimensions(encoderOptions.videoMimeType, encoderOptions.renderMode())) { + if (hasEvenDimensions(encoderOptions.width, encoderOptions.height) != true) { + encoderOptions.width = encoderOptions.width + (encoderOptions.width & 0x1); + encoderOptions.height = encoderOptions.height + (encoderOptions.height & 0x1); + } } - const QSize scaledSize = - doc->image()->bounds().size().scaled( - encoderOptions.width, encoderOptions.height, - Qt::KeepAspectRatio); - + const QSize scaledSize = doc->image()->bounds().size().scaled(encoderOptions.width, encoderOptions.height, Qt::IgnoreAspectRatio); - if ((scaledSize.width() & 0x1 || scaledSize.height() & 0x1) - && (encoderOptions.videoMimeType == "video/mp4" || - encoderOptions.videoMimeType == "video/x-matroska") && !(encoderOptions.renderMode() == encoderOptions.RENDER_FRAMES_ONLY)) { - QString m = "Mastroska (.mkv)"; - if (encoderOptions.videoMimeType == "video/mp4") { - m = "Mpeg4 (.mp4)"; + if (mustHaveEvenDimensions(encoderOptions.videoMimeType, encoderOptions.renderMode())) { + if (hasEvenDimensions(scaledSize.width(), scaledSize.height()) != true) { + QString type = encoderOptions.videoMimeType == "video/mp4" ? "Mpeg4 (.mp4) " : "Mastroska (.mkv) "; + qWarning() << type <<"requires width and height to be even, resize and try again!"; + doc->setErrorMessage(i18n("%1 requires width and height to be even numbers. Please resize or crop the image before exporting.", type)); + QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not render animation:\n%1", doc->errorMessage())); + return; } - qWarning() << m <<"requires width and height to be even, resize and try again!"; - doc->setErrorMessage(i18n("%1 requires width and height to be even numbers. Please resize or crop the image before exporting.", m)); - QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not render animation:\n%1", doc->errorMessage())); - return; } - const bool batchMode = false; // TODO: fetch correctly! KisAsyncAnimationFramesSaveDialog exporter(doc->image(), KisTimeRange::fromTime(encoderOptions.firstFrame, encoderOptions.lastFrame), baseFileName, encoderOptions.sequenceStart, encoderOptions.wantsOnlyUniqueFrameSequence && !encoderOptions.shouldEncodeVideo, encoderOptions.frameExportConfig); exporter.setBatchMode(batchMode); KisAsyncAnimationFramesSaveDialog::Result result = exporter.regenerateRange(viewManager()->mainWindow()->viewManager()); // the folder could have been read-only or something else could happen if ((encoderOptions.shouldEncodeVideo || encoderOptions.wantsOnlyUniqueFrameSequence) && result == KisAsyncAnimationFramesSaveDialog::RenderComplete) { const QString savedFilesMask = exporter.savedFilesMask(); if (encoderOptions.shouldEncodeVideo) { const QString resultFile = encoderOptions.resolveAbsoluteVideoFilePath(); KIS_SAFE_ASSERT_RECOVER_NOOP(QFileInfo(resultFile).isAbsolute()); { const QFileInfo info(resultFile); QDir dir(info.absolutePath()); if (!dir.exists()) { dir.mkpath(info.absolutePath()); } KIS_SAFE_ASSERT_RECOVER_NOOP(dir.exists()); } KisImportExportErrorCode res; QFile fi(resultFile); if (!fi.open(QIODevice::WriteOnly)) { qWarning() << "Could not open" << fi.fileName() << "for writing!"; res = KisImportExportErrorCannotWrite(fi.error()); } else { fi.close(); } QScopedPointer encoder(new VideoSaver(doc, batchMode)); res = encoder->convert(doc, savedFilesMask, encoderOptions, batchMode); if (!res.isOk()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not render animation:\n%1", res.errorMessage())); } } //File cleanup if (encoderOptions.shouldDeleteSequence) { QDir d(framesDirectory); QStringList sequenceFiles = d.entryList(QStringList() << encoderOptions.basename + "*." + extension, QDir::Files); Q_FOREACH(const QString &f, sequenceFiles) { d.remove(f); } } else if(encoderOptions.wantsOnlyUniqueFrameSequence) { QDir d(framesDirectory); const QList uniques = exporter.getUniqueFrames(); QStringList uniqueFrameNames = getNamesForFrames(encoderOptions.basename, extension, encoderOptions.sequenceStart, uniques); QStringList sequenceFiles = d.entryList(QStringList() << encoderOptions.basename + "*." + extension, QDir::Files); //Filter out unique files. KritaUtils::filterContainer(sequenceFiles, [uniqueFrameNames](QString &framename){ return !uniqueFrameNames.contains(framename); }); Q_FOREACH(const QString &f, sequenceFiles) { d.remove(f); } } } else if (result == KisAsyncAnimationFramesSaveDialog::RenderFailed) { viewManager()->mainWindow()->viewManager()->showFloatingMessage(i18n("Failed to render animation frames!"), QIcon()); } } QString AnimaterionRenderer::getNameForFrame(QString basename, QString extension, int sequenceStart, int frame) { QString frameNumberText = QString("%1").arg(frame + sequenceStart, 4, 10, QChar('0')); return basename + frameNumberText + "." + extension; } QStringList AnimaterionRenderer::getNamesForFrames(QString basename, QString extension, int sequenceStart, const QList &frames) { QStringList list; Q_FOREACH(const int &i, frames) { list.append(getNameForFrame(basename, extension, sequenceStart, i)); } return list; } +const bool AnimaterionRenderer::mustHaveEvenDimensions(QString mimeType, KisAnimationRenderingOptions::RenderMode renderMode) +{ + return (mimeType == "video/mp4" || mimeType == "video/x-matroska") && renderMode != KisAnimationRenderingOptions::RENDER_FRAMES_ONLY; +} + +const bool AnimaterionRenderer::hasEvenDimensions(int width, int height) +{ + return !((width & 0x1) || (height & 0x1)); +} + #include "AnimationRenderer.moc" diff --git a/plugins/extensions/animationrenderer/AnimationRenderer.h b/plugins/extensions/animationrenderer/AnimationRenderer.h index f5cfc9d0f3..ec743690a4 100644 --- a/plugins/extensions/animationrenderer/AnimationRenderer.h +++ b/plugins/extensions/animationrenderer/AnimationRenderer.h @@ -1,64 +1,66 @@ /* * Copyright (c) 2016 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef ANIMATIONRENDERERIMAGE_H #define ANIMATIONRENDERERIMAGE_H #include #include -class KisAnimationRenderingOptions; +#include class KisDocument; class AnimaterionRenderer : public KisActionPlugin { Q_OBJECT public: AnimaterionRenderer(QObject *parent, const QVariantList &); ~AnimaterionRenderer() override; private Q_SLOTS: /** * @brief slotRenderAnimation * * Triggered from the renderanimation action. This calls a dialog * to set the animation settings and then takes that to call the appropriate exporter, * this can be a frame exporter, or it is a KisVideoExport object as defined in * plugins/extensions/impex. */ void slotRenderAnimation(); /** * @brief slotRenderSequenceAgain * * triggered from the renderanimationagain action. This does not call a dialog, but * instead uses the settings to set the animation settings and then takes that to * call the appropriate exporter, this can be a frame exporter, or it is a * KisVideoExport object as defined in plugins/extensions/impex. */ void slotRenderSequenceAgain(); private: void renderAnimationImpl(KisDocument *doc, KisAnimationRenderingOptions encoderOptions); QString getNameForFrame(QString basename, QString extension, int sequenceStart, int frame); QStringList getNamesForFrames(QString basename, QString extension, int sequenceStart, const QList &frames); + const bool mustHaveEvenDimensions(QString mimeType, KisAnimationRenderingOptions::RenderMode renderMode); + const bool hasEvenDimensions(int width, int height); }; #endif // ANIMATIONRENDERERIMAGE_H diff --git a/plugins/extensions/animationrenderer/DlgAnimationRenderer.cpp b/plugins/extensions/animationrenderer/DlgAnimationRenderer.cpp index 9fbfc0a2a5..047b3b450c 100644 --- a/plugins/extensions/animationrenderer/DlgAnimationRenderer.cpp +++ b/plugins/extensions/animationrenderer/DlgAnimationRenderer.cpp @@ -1,665 +1,661 @@ /* * Copyright (c) 2016 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "DlgAnimationRenderer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_slider_spin_box.h" #include "kis_acyclic_signal_connector.h" #include "video_saver.h" #include "KisAnimationRenderingOptions.h" #include "video_export_options_dialog.h" DlgAnimationRenderer::DlgAnimationRenderer(KisDocument *doc, QWidget *parent) : KoDialog(parent) , m_image(doc->image()) , m_doc(doc) { KisConfig cfg(true); setCaption(i18n("Render Animation")); setButtons(Ok | Cancel); setDefaultButton(Ok); m_page = new WdgAnimationRenderer(this); m_page->layout()->setMargin(0); m_page->dirRequester->setMode(KoFileDialog::OpenDirectory); m_page->intStart->setMinimum(0); m_page->intStart->setMaximum(doc->image()->animationInterface()->fullClipRange().end()); m_page->intStart->setValue(doc->image()->animationInterface()->playbackRange().start()); m_page->intEnd->setMinimum(doc->image()->animationInterface()->fullClipRange().start()); - // animators sometimes want to export after end frame - //m_page->intEnd->setMaximum(doc->image()->animationInterface()->fullClipRange().end()); m_page->intEnd->setValue(doc->image()->animationInterface()->playbackRange().end()); m_page->intHeight->setMinimum(1); m_page->intHeight->setMaximum(100000); m_page->intHeight->setValue(doc->image()->height()); m_page->intWidth->setMinimum(1); m_page->intWidth->setMaximum(100000); m_page->intWidth->setValue(doc->image()->width()); // try to lock the width and height being updated KisAcyclicSignalConnector *constrainsConnector = new KisAcyclicSignalConnector(this); constrainsConnector->createCoordinatedConnector()->connectBackwardInt(m_page->intWidth, SIGNAL(valueChanged(int)), this, SLOT(slotLockAspectRatioDimensionsWidth(int))); constrainsConnector->createCoordinatedConnector()->connectForwardInt(m_page->intHeight, SIGNAL(valueChanged(int)), this, SLOT(slotLockAspectRatioDimensionsHeight(int))); m_page->intFramesPerSecond->setValue(doc->image()->animationInterface()->framerate()); QFileInfo audioFileInfo(doc->image()->animationInterface()->audioChannelFileName()); const bool hasAudio = audioFileInfo.exists(); m_page->chkIncludeAudio->setEnabled(hasAudio); m_page->chkIncludeAudio->setChecked(hasAudio && !doc->image()->animationInterface()->isAudioMuted()); QStringList mimes = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Export); mimes.sort(); filterSequenceMimeTypes(mimes); Q_FOREACH(const QString &mime, mimes) { QString description = KisMimeDatabase::descriptionForMimeType(mime); if (description.isEmpty()) { description = mime; } m_page->cmbMimetype->addItem(description, mime); if (mime == "image/png") { m_page->cmbMimetype->setCurrentIndex(m_page->cmbMimetype->count() - 1); } } setMainWidget(m_page); QStringList supportedMimeType = makeVideoMimeTypesList(); Q_FOREACH (const QString &mime, supportedMimeType) { QString description = KisMimeDatabase::descriptionForMimeType(mime); if (description.isEmpty()) { description = mime; } m_page->cmbRenderType->addItem(description, mime); } m_page->videoFilename->setMode(KoFileDialog::SaveFile); connect(m_page->bnExportOptions, SIGNAL(clicked()), this, SLOT(sequenceMimeTypeOptionsClicked())); connect(m_page->bnRenderOptions, SIGNAL(clicked()), this, SLOT(selectRenderOptions())); m_page->ffmpegLocation->setMode(KoFileDialog::OpenFile); m_page->cmbRenderType->setCurrentIndex(cfg.readEntry("AnimationRenderer/render_type", 0)); connect(m_page->shouldExportOnlyImageSequence, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged())); connect(m_page->shouldExportOnlyVideo, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged())); connect(m_page->shouldExportAll, SIGNAL(toggled(bool)), this, SLOT(slotExportTypeChanged())); connect(m_page->intFramesPerSecond, SIGNAL(valueChanged(int)), SLOT(frameRateChanged(int))); // connect and cold init connect(m_page->cmbRenderType, SIGNAL(currentIndexChanged(int)), this, SLOT(selectRenderType(int))); selectRenderType(m_page->cmbRenderType->currentIndex()); resize(m_page->sizeHint()); connect(this, SIGNAL(accepted()), SLOT(slotDialogAccepted())); { KisPropertiesConfigurationSP settings = cfg.exportConfiguration("ANIMATION_EXPORT"); KisAnimationRenderingOptions options; options.fromProperties(settings); loadAnimationOptions(options); } - - } DlgAnimationRenderer::~DlgAnimationRenderer() { delete m_page; } void DlgAnimationRenderer::getDefaultVideoEncoderOptions(const QString &mimeType, KisPropertiesConfigurationSP cfg, QString *customFFMpegOptionsString, bool *renderHDR) { const VideoExportOptionsDialog::ContainerType containerType = mimeType == "video/ogg" ? VideoExportOptionsDialog::OGV : VideoExportOptionsDialog::DEFAULT; QScopedPointer encoderConfigWidget( new VideoExportOptionsDialog(containerType, 0)); // we always enable HDR, letting the user to force it encoderConfigWidget->setSupportsHDR(true); encoderConfigWidget->setConfiguration(cfg); *customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString(); *renderHDR = encoderConfigWidget->videoConfiguredForHDR(); } void DlgAnimationRenderer::filterSequenceMimeTypes(QStringList &mimeTypes) { KritaUtils::filterContainer(mimeTypes, [](QString type) { return (type.startsWith("image/") || (type.startsWith("application/") && !type.startsWith("application/x-spriter"))); }); } QStringList DlgAnimationRenderer::makeVideoMimeTypesList() { QStringList supportedMimeTypes = QStringList(); supportedMimeTypes << "video/x-matroska"; supportedMimeTypes << "image/gif"; supportedMimeTypes << "video/ogg"; supportedMimeTypes << "video/mp4"; return supportedMimeTypes; } bool DlgAnimationRenderer::imageMimeSupportsHDR(QString &mime) { return (mime == "image/png"); } void DlgAnimationRenderer::loadAnimationOptions(const KisAnimationRenderingOptions &options) { const QString documentPath = m_doc->localFilePath(); m_page->txtBasename->setText(options.basename); if (!options.lastDocuemntPath.isEmpty() && options.lastDocuemntPath == documentPath) { m_page->intStart->setValue(options.firstFrame); m_page->intEnd->setValue(options.lastFrame); m_page->sequenceStart->setValue(options.sequenceStart); m_page->intWidth->setValue(options.width); m_page->intHeight->setValue(options.height); m_page->intFramesPerSecond->setValue(options.frameRate); m_page->videoFilename->setStartDir(options.resolveAbsoluteDocumentFilePath(documentPath)); m_page->videoFilename->setFileName(options.videoFileName); m_page->dirRequester->setStartDir(options.resolveAbsoluteDocumentFilePath(documentPath)); m_page->dirRequester->setFileName(options.directory); } else { m_page->intStart->setValue(m_image->animationInterface()->playbackRange().start()); m_page->intEnd->setValue(m_image->animationInterface()->playbackRange().end()); m_page->sequenceStart->setValue(m_image->animationInterface()->playbackRange().start()); m_page->intWidth->setValue(m_image->width()); m_page->intHeight->setValue(m_image->height()); m_page->intFramesPerSecond->setValue(m_image->animationInterface()->framerate()); m_page->videoFilename->setStartDir(options.resolveAbsoluteDocumentFilePath(documentPath)); m_page->videoFilename->setFileName(defaultVideoFileName(m_doc, options.videoMimeType)); m_page->dirRequester->setStartDir(options.resolveAbsoluteDocumentFilePath(documentPath)); m_page->dirRequester->setFileName(options.directory); } for (int i = 0; i < m_page->cmbMimetype->count(); ++i) { if (m_page->cmbMimetype->itemData(i).toString() == options.frameMimeType) { m_page->cmbMimetype->setCurrentIndex(i); break; } } for (int i = 0; i < m_page->cmbRenderType->count(); ++i) { if (m_page->cmbRenderType->itemData(i).toString() == options.videoMimeType) { m_page->cmbRenderType->setCurrentIndex(i); break; } } m_page->chkIncludeAudio->setChecked(options.includeAudio); m_page->chkOnlyUniqueFrames->setChecked(options.wantsOnlyUniqueFrameSequence); if (options.shouldDeleteSequence) { KIS_SAFE_ASSERT_RECOVER_NOOP(options.shouldEncodeVideo); m_page->shouldExportOnlyVideo->setChecked(true); } else if (!options.shouldEncodeVideo) { KIS_SAFE_ASSERT_RECOVER_NOOP(!options.shouldDeleteSequence); m_page->shouldExportOnlyImageSequence->setChecked(true); } else { m_page->shouldExportAll->setChecked(true); // export to both } { KisConfig cfg(true); KisPropertiesConfigurationSP settings = cfg.exportConfiguration("VIDEO_ENCODER"); getDefaultVideoEncoderOptions(options.videoMimeType, settings, &m_customFFMpegOptionsString, &m_wantsRenderWithHDR); } { KisConfig cfg(true); KisPropertiesConfigurationSP settings = cfg.exportConfiguration("img_sequence/" + options.frameMimeType); m_wantsRenderWithHDR = settings->getPropertyLazy("saveAsHDR", m_wantsRenderWithHDR); } m_page->ffmpegLocation->setStartDir(QFileInfo(m_doc->localFilePath()).path()); m_page->ffmpegLocation->setFileName(findFFMpeg(options.ffmpegPath)); } QString DlgAnimationRenderer::defaultVideoFileName(KisDocument *doc, const QString &mimeType) { const QString docFileName = !doc->localFilePath().isEmpty() ? doc->localFilePath() : i18n("Untitled"); return QString("%1.%2") .arg(QFileInfo(docFileName).completeBaseName()) .arg(KisMimeDatabase::suffixesForMimeType(mimeType).first()); } void DlgAnimationRenderer::selectRenderType(int index) { const QString mimeType = m_page->cmbRenderType->itemData(index).toString(); m_page->bnRenderOptions->setEnabled(mimeType != "image/gif"); m_page->lblGifWarning->setVisible((mimeType == "image/gif" && m_page->intFramesPerSecond->value() > 50)); QString videoFileName = defaultVideoFileName(m_doc, mimeType); if (!m_page->videoFilename->fileName().isEmpty()) { const QFileInfo info = QFileInfo(m_page->videoFilename->fileName()); const QString baseName = info.completeBaseName(); const QString path = info.path(); videoFileName = QString("%1%2%3.%4").arg(path).arg(QDir::separator()).arg(baseName).arg(KisMimeDatabase::suffixesForMimeType(mimeType).first()); } m_page->videoFilename->setMimeTypeFilters(QStringList() << mimeType, mimeType); m_page->videoFilename->setFileName(videoFileName); m_wantsRenderWithHDR = (mimeType == "video/mp4") ? m_wantsRenderWithHDR : false; } void DlgAnimationRenderer::selectRenderOptions() { const int index = m_page->cmbRenderType->currentIndex(); const QString mimetype = m_page->cmbRenderType->itemData(index).toString(); const VideoExportOptionsDialog::ContainerType containerType = mimetype == "video/ogg" ? VideoExportOptionsDialog::OGV : VideoExportOptionsDialog::DEFAULT; VideoExportOptionsDialog *encoderConfigWidget = new VideoExportOptionsDialog(containerType, this); // we always enable HDR, letting the user to force it encoderConfigWidget->setSupportsHDR(true); { KisConfig cfg(true); KisPropertiesConfigurationSP settings = cfg.exportConfiguration("VIDEO_ENCODER"); encoderConfigWidget->setConfiguration(settings); encoderConfigWidget->setHDRConfiguration(m_wantsRenderWithHDR); } KoDialog dlg(this); dlg.setMainWidget(encoderConfigWidget); dlg.setButtons(KoDialog::Ok | KoDialog::Cancel); if (dlg.exec() == QDialog::Accepted) { KisConfig cfg(false); cfg.setExportConfiguration("VIDEO_ENCODER", encoderConfigWidget->configuration()); m_customFFMpegOptionsString = encoderConfigWidget->customUserOptionsString(); m_wantsRenderWithHDR = encoderConfigWidget->videoConfiguredForHDR(); } dlg.setMainWidget(0); encoderConfigWidget->deleteLater(); } void DlgAnimationRenderer::sequenceMimeTypeOptionsClicked() { int index = m_page->cmbMimetype->currentIndex(); KisConfigWidget *frameExportConfigWidget = 0; QString mimetype = m_page->cmbMimetype->itemData(index).toString(); QSharedPointer filter(KisImportExportManager::filterForMimeType(mimetype, KisImportExportManager::Export)); if (filter) { frameExportConfigWidget = filter->createConfigurationWidget(0, KisDocument::nativeFormatMimeType(), mimetype.toLatin1()); if (frameExportConfigWidget) { KisConfig cfg(true); KisPropertiesConfigurationSP exportConfig = cfg.exportConfiguration("img_sequence/" + mimetype); if (exportConfig) { KisImportExportManager::fillStaticExportConfigurationProperties(exportConfig, m_image); } //Important -- m_useHDR allows the synchronization of both the video and image render settings. if(imageMimeSupportsHDR(mimetype)) { exportConfig->setProperty("saveAsHDR", m_wantsRenderWithHDR); if (m_wantsRenderWithHDR) { exportConfig->setProperty("forceSRGB", false); } } frameExportConfigWidget->setConfiguration(exportConfig); KoDialog dlg(this); dlg.setMainWidget(frameExportConfigWidget); dlg.setButtons(KoDialog::Ok | KoDialog::Cancel); if (dlg.exec() == QDialog::Accepted) { KisConfig cfg(false); m_wantsRenderWithHDR = frameExportConfigWidget->configuration()->getPropertyLazy("saveAsHDR", false); cfg.setExportConfiguration("img_sequence/" + mimetype, frameExportConfigWidget->configuration()); } frameExportConfigWidget->hide(); dlg.setMainWidget(0); frameExportConfigWidget->setParent(0); frameExportConfigWidget->deleteLater(); } } } inline int roundByTwo(int value) { return value + (value & 0x1); } KisAnimationRenderingOptions DlgAnimationRenderer::getEncoderOptions() const { KisAnimationRenderingOptions options; options.lastDocuemntPath = m_doc->localFilePath(); options.videoMimeType = m_page->cmbRenderType->currentData().toString(); options.frameMimeType = m_page->cmbMimetype->currentData().toString(); options.basename = m_page->txtBasename->text(); options.directory = m_page->dirRequester->fileName(); options.firstFrame = m_page->intStart->value(); options.lastFrame = m_page->intEnd->value(); options.sequenceStart = m_page->sequenceStart->value(); options.shouldEncodeVideo = !m_page->shouldExportOnlyImageSequence->isChecked(); options.shouldDeleteSequence = m_page->shouldExportOnlyVideo->isChecked(); options.includeAudio = m_page->chkIncludeAudio->isChecked(); options.wantsOnlyUniqueFrameSequence = m_page->chkOnlyUniqueFrames->isChecked(); options.ffmpegPath = m_page->ffmpegLocation->fileName(); options.frameRate = m_page->intFramesPerSecond->value(); if (options.frameRate > 50 && options.videoMimeType == "image/gif") { options.frameRate = 50; } options.width = roundByTwo(m_page->intWidth->value()); options.height = roundByTwo(m_page->intHeight->value()); options.videoFileName = m_page->videoFilename->fileName(); options.customFFMpegOptions = m_customFFMpegOptionsString; { KisConfig config(true); KisPropertiesConfigurationSP cfg = config.exportConfiguration("img_sequence/" + options.frameMimeType); if (cfg) { KisImportExportManager::fillStaticExportConfigurationProperties(cfg, m_image); } const bool forceNecessaryHDRSettings = m_wantsRenderWithHDR && imageMimeSupportsHDR(options.frameMimeType); if (forceNecessaryHDRSettings) { KIS_SAFE_ASSERT_RECOVER_NOOP(options.frameMimeType == "image/png"); cfg->setProperty("forceSRGB", false); cfg->setProperty("saveAsHDR", true); } options.frameExportConfig = cfg; } return options; } void DlgAnimationRenderer::slotButtonClicked(int button) { if (button == KoDialog::Ok && !m_page->shouldExportOnlyImageSequence->isChecked()) { QString ffmpeg = m_page->ffmpegLocation->fileName(); if (m_page->videoFilename->fileName().isEmpty()) { QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Please enter a file name to render to.")); return; } else if (ffmpeg.isEmpty()) { QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Krita can't find FFmpeg!
\ Krita depends on another free program called FFmpeg to turn frame-by-frame animations into video files. (www.ffmpeg.org)

\ To learn more about setting up Krita for rendering animations, please visit this section of our User Manual.")); return; } else { QFileInfo fi(ffmpeg); if (!fi.exists()) { QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("The location of FFmpeg is invalid. Please select the correct location of the FFmpeg executable on your system.")); return; } if (fi.fileName().endsWith("zip")) { QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Please extract ffmpeg from the archive first.")); return; } if (fi.fileName().endsWith("tar.bz2")) { #ifdef Q_OS_WIN QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("This is a source code archive. Please download the application archive ('Windows Builds'), unpack it and then provide the path to the extracted ffmpeg file.")); #else QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("This is a source code archive. Please install ffmpeg instead.")); #endif return; } if (!fi.isExecutable()) { QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("The location of FFmpeg is invalid. Please select the correct location of the FFmpeg executable on your system.")); return; } } } KoDialog::slotButtonClicked(button); } void DlgAnimationRenderer::slotDialogAccepted() { KisConfig cfg(false); KisAnimationRenderingOptions options = getEncoderOptions(); cfg.setExportConfiguration("ANIMATION_EXPORT", options.toProperties()); } QString DlgAnimationRenderer::findFFMpeg(const QString &customLocation) { QString result; QStringList proposedPaths; if (!customLocation.isEmpty()) { proposedPaths << customLocation; proposedPaths << customLocation + QDir::separator() + "ffmpeg"; } proposedPaths << KoResourcePaths::getApplicationRoot() + QDir::separator() + "bin" + QDir::separator() + "ffmpeg"; #ifndef Q_OS_WIN proposedPaths << QDir::homePath() + "/bin/ffmpeg"; proposedPaths << "/usr/bin/ffmpeg"; proposedPaths << "/usr/local/bin/ffmpeg"; #endif Q_FOREACH (QString path, proposedPaths) { if (path.isEmpty()) continue; #ifdef Q_OS_WIN path = QDir::toNativeSeparators(QDir::cleanPath(path)); if (path.endsWith(QDir::separator())) { continue; } if (!path.endsWith(".exe")) { if (!QFile::exists(path)) { path += ".exe"; if (!QFile::exists(path)) { continue; } } } #endif QProcess testProcess; testProcess.start(path, QStringList() << "-version"); if (testProcess.waitForStarted(1000)) { testProcess.waitForFinished(1000); } const bool successfulStart = testProcess.state() == QProcess::NotRunning && testProcess.error() == QProcess::UnknownError; if (successfulStart) { result = path; break; } } return result; } void DlgAnimationRenderer::slotExportTypeChanged() { KisConfig cfg(false); bool willEncodeVideo = m_page->shouldExportAll->isChecked() || m_page->shouldExportOnlyVideo->isChecked(); // if a video format needs to be outputted if (willEncodeVideo) { // videos always uses PNG for creating video, so disable the ability to change the format m_page->cmbMimetype->setEnabled(false); for (int i = 0; i < m_page->cmbMimetype->count(); ++i) { if (m_page->cmbMimetype->itemData(i).toString() == "image/png") { m_page->cmbMimetype->setCurrentIndex(i); break; } } } m_page->intWidth->setVisible(willEncodeVideo); m_page->intHeight->setVisible(willEncodeVideo); m_page->intFramesPerSecond->setVisible(willEncodeVideo); m_page->fpsLabel->setVisible(willEncodeVideo); m_page->lblWidth->setVisible(willEncodeVideo); m_page->lblHeight->setVisible(willEncodeVideo); // if only exporting video if (m_page->shouldExportOnlyVideo->isChecked()) { m_page->cmbMimetype->setEnabled(false); // allow to change image format m_page->imageSequenceOptionsGroup->setVisible(false); m_page->videoOptionsGroup->setVisible(false); //shrinks the horizontal space temporarily to help resize() work m_page->videoOptionsGroup->setVisible(true); } // if only an image sequence needs to be output if (m_page->shouldExportOnlyImageSequence->isChecked()) { m_page->cmbMimetype->setEnabled(true); // allow to change image format m_page->videoOptionsGroup->setVisible(false); m_page->imageSequenceOptionsGroup->setVisible(false); m_page->imageSequenceOptionsGroup->setVisible(true); } // show all options if (m_page->shouldExportAll->isChecked() ) { m_page->imageSequenceOptionsGroup->setVisible(true); m_page->videoOptionsGroup->setVisible(true); } // for the resize to work as expected, try to hide elements first before displaying other ones. // if the widget gets bigger at any point, the resize will use that, even if elements are hidden later to make it smaller resize(m_page->sizeHint()); } void DlgAnimationRenderer::frameRateChanged(int framerate) { const QString mimeType = m_page->cmbRenderType->itemData(m_page->cmbRenderType->currentIndex()).toString(); m_page->lblGifWarning->setVisible((mimeType == "image/gif" && framerate > 50)); } void DlgAnimationRenderer::slotLockAspectRatioDimensionsWidth(int width) { Q_UNUSED(width); float aspectRatio = (float)m_image->width() / (float)m_image->height(); // update height here float newHeight = m_page->intWidth->value() / aspectRatio ; m_page->intHeight->setValue(newHeight); } void DlgAnimationRenderer::slotLockAspectRatioDimensionsHeight(int height) { Q_UNUSED(height); float aspectRatio = (float)m_image->width() / (float)m_image->height(); // update width here float newWidth = aspectRatio * m_page->intHeight->value(); m_page->intWidth->setValue(newWidth); } diff --git a/plugins/extensions/animationrenderer/video_saver.cpp b/plugins/extensions/animationrenderer/video_saver.cpp index d2aa947a58..444c428cb4 100644 --- a/plugins/extensions/animationrenderer/video_saver.cpp +++ b/plugins/extensions/animationrenderer/video_saver.cpp @@ -1,335 +1,333 @@ /* * Copyright (c) 2016 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "video_saver.h" #include #include #include #include #include #include #include #include #include #include #include "kis_config.h" #include "KisAnimationRenderingOptions.h" #include #include #include #include #include #include #include #include "KisPart.h" class KisFFMpegProgressWatcher : public QObject { Q_OBJECT public: KisFFMpegProgressWatcher(QFile &progressFile, int totalFrames) : m_progressFile(progressFile), m_totalFrames(totalFrames) { connect(&m_progressWatcher, SIGNAL(fileChanged(QString)), SLOT(slotFileChanged())); m_progressWatcher.addPath(m_progressFile.fileName()); } private Q_SLOTS: void slotFileChanged() { int currentFrame = -1; bool isEnded = false; while(!m_progressFile.atEnd()) { QString line = QString(m_progressFile.readLine()).remove(QChar('\n')); QStringList var = line.split("="); if (var[0] == "frame") { currentFrame = var[1].toInt(); } else if (var[0] == "progress") { isEnded = var[1] == "end"; } } if (isEnded) { emit sigProgressChanged(100); emit sigProcessingFinished(); } else { emit sigProgressChanged(100 * currentFrame / m_totalFrames); } } Q_SIGNALS: void sigProgressChanged(int percent); void sigProcessingFinished(); private: QFileSystemWatcher m_progressWatcher; QFile &m_progressFile; int m_totalFrames; }; class KisFFMpegRunner { public: KisFFMpegRunner(const QString &ffmpegPath) : m_cancelled(false), m_ffmpegPath(ffmpegPath) {} public: KisImportExportErrorCode runFFMpeg(const QStringList &specialArgs, const QString &actionName, const QString &logPath, int totalFrames) { dbgFile << "runFFMpeg: specialArgs" << specialArgs << "actionName" << actionName << "logPath" << logPath << "totalFrames" << totalFrames; QTemporaryFile progressFile(QDir::tempPath() + QDir::separator() + "KritaFFmpegProgress.XXXXXX"); progressFile.open(); m_process.setStandardOutputFile(logPath); m_process.setProcessChannelMode(QProcess::MergedChannels); QStringList args; args << "-v" << "debug" << "-nostdin" << "-progress" << progressFile.fileName() << specialArgs; qDebug() << "\t" << m_ffmpegPath << args.join(" "); m_cancelled = false; m_process.start(m_ffmpegPath, args); return waitForFFMpegProcess(actionName, progressFile, m_process, totalFrames); } void cancel() { m_cancelled = true; m_process.kill(); } private: KisImportExportErrorCode waitForFFMpegProcess(const QString &message, QFile &progressFile, QProcess &ffmpegProcess, int totalFrames) { KisFFMpegProgressWatcher watcher(progressFile, totalFrames); QProgressDialog progress(message, "", 0, 0, KisPart::instance()->currentMainwindow()); progress.setWindowModality(Qt::ApplicationModal); progress.setCancelButton(0); progress.setMinimumDuration(0); progress.setValue(0); progress.setRange(0, 100); QEventLoop loop; loop.connect(&watcher, SIGNAL(sigProcessingFinished()), SLOT(quit())); loop.connect(&ffmpegProcess, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(quit())); loop.connect(&ffmpegProcess, SIGNAL(error(QProcess::ProcessError)), SLOT(quit())); loop.connect(&watcher, SIGNAL(sigProgressChanged(int)), &progress, SLOT(setValue(int))); if (ffmpegProcess.state() != QProcess::NotRunning) { loop.exec(); // wait for some erroneous case ffmpegProcess.waitForFinished(5000); } KisImportExportErrorCode retval = ImportExportCodes::OK; if (ffmpegProcess.state() != QProcess::NotRunning) { // sorry... ffmpegProcess.kill(); retval = ImportExportCodes::Failure; } else if (m_cancelled) { retval = ImportExportCodes::Cancelled; } else if (ffmpegProcess.exitCode()) { retval = ImportExportCodes::Failure; } return retval; } private: QProcess m_process; bool m_cancelled; QString m_ffmpegPath; }; VideoSaver::VideoSaver(KisDocument *doc, bool batchMode) : m_image(doc->image()) , m_doc(doc) , m_batchMode(batchMode) { } VideoSaver::~VideoSaver() { } KisImageSP VideoSaver::image() { return m_image; } KisImportExportErrorCode VideoSaver::encode(const QString &savedFilesMask, const KisAnimationRenderingOptions &options) { if (!QFileInfo(options.ffmpegPath).exists()) { m_doc->setErrorMessage(i18n("ffmpeg could not be found at %1", options.ffmpegPath)); return ImportExportCodes::Failure; } KisImportExportErrorCode resultOuter = ImportExportCodes::OK; KisImageAnimationInterface *animation = m_image->animationInterface(); 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(options.width)) .append(":h=") - .append(QString::number(options.height)) - .append(":force_original_aspect_ratio=decrease"); - + .append(QString::number(options.height)); + //.append(":force_original_aspect_ratio=decrease"); HOTFIX for even:odd dimension images. const QString resultFile = options.resolveAbsoluteVideoFilePath(); const QDir videoDir(QFileInfo(resultFile).absolutePath()); const QFileInfo info(resultFile); const QString suffix = info.suffix().toLower(); const QString palettePath = videoDir.filePath("palette.png"); const QStringList additionalOptionsList = options.customFFMpegOptions.split(' ', QString::SkipEmptyParts); QScopedPointer runner(new KisFFMpegRunner(options.ffmpegPath)); if (suffix == "gif") { { QStringList args; args << "-r" << QString::number(options.frameRate) << "-start_number" << QString::number(clipRange.start()) << "-i" << savedFilesMask << "-vf" << "palettegen" << "-y" << palettePath; KisImportExportErrorCode result = runner->runFFMpeg(args, i18n("Fetching palette..."), videoDir.filePath("log_generate_palette_gif.log"), clipRange.duration()); if (!result.isOk()) { return result; } } { QStringList args; args << "-r" << QString::number(options.frameRate) << "-start_number" << QString::number(clipRange.start()) << "-i" << savedFilesMask << "-i" << palettePath << "-lavfi"; QString filterArgs; // if we are exporting out at a different image size, we apply scaling filter if (m_image->width() != options.width || m_image->height() != options.height) { filterArgs.append(exportDimensions + "[0:v];"); } args << filterArgs.append("[0:v][1:v] paletteuse") << "-y" << resultFile; dbgFile << "savedFilesMask" << savedFilesMask << "start" << QString::number(clipRange.start()) << "duration" << clipRange.duration(); KisImportExportErrorCode result = runner->runFFMpeg(args, i18n("Encoding frames..."), videoDir.filePath("log_encode_gif.log"), clipRange.duration()); if (!result.isOk()) { return result; } } } else { QStringList args; args << "-r" << QString::number(options.frameRate) << "-start_number" << QString::number(clipRange.start()) << "-i" << savedFilesMask; QFileInfo audioFileInfo = animation->audioChannelFileName(); if (options.includeAudio && audioFileInfo.exists()) { const int msecStart = clipRange.start() * 1000 / animation->framerate(); const int msecDuration = clipRange.duration() * 1000 / animation->framerate(); const QTime startTime = QTime::fromMSecsSinceStartOfDay(msecStart); const QTime durationTime = QTime::fromMSecsSinceStartOfDay(msecDuration); const QString ffmpegTimeFormat("H:m:s.zzz"); args << "-ss" << startTime.toString(ffmpegTimeFormat); args << "-t" << durationTime.toString(ffmpegTimeFormat); args << "-i" << audioFileInfo.absoluteFilePath(); } - // if we are exporting out at a different image size, we apply scaling filter // export options HAVE to go after input options, so make sure this is after the audio import if (m_image->width() != options.width || m_image->height() != options.height) { args << "-vf" << exportDimensions; } args << additionalOptionsList << "-y" << resultFile; resultOuter = runner->runFFMpeg(args, i18n("Encoding frames..."), videoDir.filePath("log_encode.log"), clipRange.duration()); } return resultOuter; } KisImportExportErrorCode VideoSaver::convert(KisDocument *document, const QString &savedFilesMask, const KisAnimationRenderingOptions &options, bool batchMode) { VideoSaver videoSaver(document, batchMode); KisImportExportErrorCode res = videoSaver.encode(savedFilesMask, options); return res; } #include "video_saver.moc"