diff --git a/plugins/extensions/animationrenderer/AnimationRenderer.cpp b/plugins/extensions/animationrenderer/AnimationRenderer.cpp index a6f8ffa37e..f1d8354f5f 100644 --- a/plugins/extensions/animationrenderer/AnimationRenderer.cpp +++ b/plugins/extensions/animationrenderer/AnimationRenderer.cpp @@ -1,238 +1,260 @@ /* * 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 #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); 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::IgnoreAspectRatio); 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; } } + int firsrNonExistingFrame = 0; //stores the first un-rendered image frame's index + if (encoderOptions.useSavedFrames && encoderOptions.shouldEncodeVideo){ + QDir d(framesDirectory); + + QStringList existingFiles = d.entryList(QStringList() << encoderOptions.basename + "*." + extension, QDir::Files); + + if( existingFiles.size() != 0){ + QCollator collator; + collator.setNumericMode(true); + std::sort(existingFiles.begin(), existingFiles.end(), collator); // sort existing files base on the numeric suffix + + QRegularExpression rx( "([0-9]+)"); + QRegularExpressionMatch match = rx.match(existingFiles[existingFiles.size()-1]); + + if( match.hasMatch()){ + QString matched = match.captured(1); + firsrNonExistingFrame = matched.toInt() + 1; + } + } + } 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); + KisTimeRange::fromTime( + std::max(firsrNonExistingFrame, 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/DlgAnimationRenderer.cpp b/plugins/extensions/animationrenderer/DlgAnimationRenderer.cpp index 8d9879cf52..cdbce74eaa 100644 --- a/plugins/extensions/animationrenderer/DlgAnimationRenderer.cpp +++ b/plugins/extensions/animationrenderer/DlgAnimationRenderer.cpp @@ -1,647 +1,667 @@ /* * 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->chkUseSavedFrames->setChecked(false); + m_page->chkUseSavedFrames->setEnabled(false); 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()); 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->chkUseSavedFrames, SIGNAL(clicked(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); + m_page->chkUseSavedFrames->setEnabled(options.shouldEncodeVideo); //the only setEnabled for chkUseSavedFrames + m_page->chkUseSavedFrames->setChecked(options.shouldEncodeVideo && options.useSavedFrames); // and the only setChecked + } 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.useSavedFrames = m_page->chkUseSavedFrames->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.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; } if (fi.fileName().endsWith("zip")) { QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Please extract ffmpeg from the archive first.")); 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); + m_page->dirRequester->setEnabled(true); + m_page->txtBasename->setEnabled(true); + m_page->sequenceStart->setEnabled(true); + m_page->videoOptionsGroup->setEnabled(true); // 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); + m_page->chkUseSavedFrames->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); + m_page->chkUseSavedFrames->setVisible(false); } // show all options if (m_page->shouldExportAll->isChecked() ) { - m_page->imageSequenceOptionsGroup->setVisible(true); - m_page->videoOptionsGroup->setVisible(true); - } + m_page->imageSequenceOptionsGroup->setVisible(true); + m_page->videoOptionsGroup->setVisible(true); + m_page->chkUseSavedFrames->setVisible(true); + } + if (m_page->chkUseSavedFrames->isChecked() && m_page->chkUseSavedFrames->isEnabled()){ + m_page->dirRequester->setEnabled(false); + m_page->txtBasename->setEnabled(false); + m_page->sequenceStart->setEnabled(false); + m_page->videoOptionsGroup->setEnabled(false); + } // 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/KisAnimationRenderingOptions.cpp b/plugins/extensions/animationrenderer/KisAnimationRenderingOptions.cpp index 4d0b7ac746..4d35d4fee5 100644 --- a/plugins/extensions/animationrenderer/KisAnimationRenderingOptions.cpp +++ b/plugins/extensions/animationrenderer/KisAnimationRenderingOptions.cpp @@ -1,139 +1,141 @@ /* * Copyright (c) 2019 Dmitry Kazakov * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KisAnimationRenderingOptions.h" #include #include #include KisAnimationRenderingOptions::KisAnimationRenderingOptions() : videoMimeType("video/mp4"), frameMimeType("image/png"), basename("frame"), directory("") { } QString KisAnimationRenderingOptions::resolveAbsoluteDocumentFilePath(const QString &documentPath) const { return !documentPath.isEmpty() ? documentPath : QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); } QString KisAnimationRenderingOptions::resolveAbsoluteVideoFilePath(const QString &documentPath) const { const QString basePath = resolveAbsoluteDocumentFilePath(documentPath); return KritaUtils::resolveAbsoluteFilePath(basePath, videoFileName); } QString KisAnimationRenderingOptions::resolveAbsoluteFramesDirectory(const QString &documentPath) const { if (renderMode() == RENDER_VIDEO_ONLY) { return QFileInfo(resolveAbsoluteVideoFilePath()).absolutePath(); } const QString basePath = resolveAbsoluteDocumentFilePath(documentPath); return KritaUtils::resolveAbsoluteFilePath(basePath, directory); } QString KisAnimationRenderingOptions::resolveAbsoluteVideoFilePath() const { return resolveAbsoluteVideoFilePath(lastDocuemntPath); } QString KisAnimationRenderingOptions::resolveAbsoluteFramesDirectory() const { return resolveAbsoluteFramesDirectory(lastDocuemntPath); } 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("only_unique_frames", wantsOnlyUniqueFrameSequence); 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->setProperty("use_saved_frames", useSavedFrames); config->setPrefixedProperties("frame_export/", frameExportConfig); return config; } void KisAnimationRenderingOptions::fromProperties(KisPropertiesConfigurationSP config) { basename = config->getPropertyLazy("basename", basename); lastDocuemntPath = config->getPropertyLazy("last_document_path", ""); 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); wantsOnlyUniqueFrameSequence = config->getPropertyLazy("only_unique_frames", false); ffmpegPath = config->getPropertyLazy("ffmpeg_path", ""); 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", ""); customFFMpegOptions = config->getPropertyLazy("custom_ffmpeg_options", ""); + useSavedFrames = config->getPropertyLazy("use_saved_frames", false); frameExportConfig = new KisPropertiesConfiguration(); frameExportConfig->setPrefixedProperties("frame_export/", frameExportConfig); } diff --git a/plugins/extensions/animationrenderer/KisAnimationRenderingOptions.h b/plugins/extensions/animationrenderer/KisAnimationRenderingOptions.h index 2e62ee18ce..c069d014ac 100644 --- a/plugins/extensions/animationrenderer/KisAnimationRenderingOptions.h +++ b/plugins/extensions/animationrenderer/KisAnimationRenderingOptions.h @@ -1,77 +1,78 @@ /* * Copyright (c) 2019 Dmitry Kazakov * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KISANIMATIONRENDERINGOPTIONS_H #define KISANIMATIONRENDERINGOPTIONS_H #include #include "kis_properties_configuration.h" class KisAnimationRenderingOptions { public: 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; bool wantsOnlyUniqueFrameSequence = false; + bool useSavedFrames = false; QString ffmpegPath; int frameRate = 25; int width = 0; int height = 0; QString videoFileName; QString customFFMpegOptions; KisPropertiesConfigurationSP frameExportConfig; QString resolveAbsoluteDocumentFilePath(const QString &documentPath) const; QString resolveAbsoluteVideoFilePath(const QString &documentPath) const; QString resolveAbsoluteFramesDirectory(const QString &documentPath) const; 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 diff --git a/plugins/extensions/animationrenderer/wdg_animationrenderer.ui b/plugins/extensions/animationrenderer/wdg_animationrenderer.ui index d7554efb3f..98c01ef902 100644 --- a/plugins/extensions/animationrenderer/wdg_animationrenderer.ui +++ b/plugins/extensions/animationrenderer/wdg_animationrenderer.ui @@ -1,488 +1,495 @@ WdgAnimaterionRenderer 0 0 - 621 - 381 + 678 + 495 0 0 AnimationRenderer Image 20 1 Export: Image Se&quence &Video Both Qt::Horizontal 40 20 + + + + Use already Saved frames + + + 0 0 First frame: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::LeftToRight FPS: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 1 0 0 10000 Last frame: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 px 0 0 Width: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Height: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 px 25 0 0 Video Options 1 0 Select the ffmpeg render options. ... FF&Mpeg: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ffmpegLocation Render as: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Video Location: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Include Audio <html><head/><body><p><span style=" font-weight:600;">Warning:</span> animated gif images cannot have a framerate higher than 50. The framerate will be reduced to 50 frames per second.</p></body></html> true Qt::Vertical 20 10 Image Sequence Options Fi&le format: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter label_8 2 0 Select the file format for the image sequence. If you want to render to video or animated gif, you can only select PNG Select the frame export options ... Start numbering at: 1 0 999 0 0 Base name: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter frame Image location: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 0 Only Unique Frames Qt::Vertical 20 10 Qt::Vertical 20 10 KisFileNameRequester QWidget
kis_file_name_requester.h
1
cmbMimetype cmbRenderType