Krita Source Code Documentation
Loading...
Searching...
No Matches
KisAnimationRender.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2020 Eoin O 'Neill <eoinoneill1991@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <QFile>
10#include <QFileInfo>
11#include <QDir>
12#include <QMessageBox>
13#include <QApplication>
14
15#include "KisDocument.h"
16#include "KisViewManager.h"
18#include "KisMimeDatabase.h"
20#include "kis_time_span.h"
21#include "KisMainWindow.h"
22
24
25#include "KisVideoSaver.h"
26
27#ifdef Q_OS_ANDROID
28#include <QTemporaryDir>
29#include <memory>
30#endif
31
32namespace
33{
34
35bool looksLikeMp4(const QString &videoType)
36{
37#ifdef Q_OS_ANDROID
38 return videoType.contains(QStringLiteral("mp4"));
39#else
40 return videoType == QStringLiteral("video/mp4");
41#endif
42}
43
44bool looksLikeMatroska(const QString &videoType)
45{
46#ifdef Q_OS_ANDROID
47 return videoType.contains(QStringLiteral("matroska"));
48#else
49 return videoType == QStringLiteral("video/x-matroska");
50#endif
51}
52
53} // namespace
54
56 bool isTemporaryFramesDirectory = false;
57 QString framesDirectory;
58#ifdef Q_OS_ANDROID
59 // The user may cancel the dialog prompting them for a video file or a frames
60 // directory and we can't implicitly create them next to the document like on
61 // desktop due to file system restrictions. So if we don't get those paths
62 // here, we just bail out. The user knows they pressed cancel on the file
63 // dialog, so no message dialog is necessary.
64 if (encoderOptions.shouldEncodeVideo) {
65 if (encoderOptions.videoFileName.isEmpty()) {
66 return false;
67 }
68 } else if (encoderOptions.directory.isEmpty()) {
69 return false;
70 }
71
72 // Android uses weird content URIs instead of file paths and isn't allowed
73 // to scribble around in the file system without asking the user for access.
74 // We'll have to take the frames directory as it is given and if we don't
75 // get one then we'll create a temporary directory to stick our frames into.
76 std::unique_ptr<QTemporaryDir> tempDir;
77 if (encoderOptions.shouldEncodeVideo) {
78 tempDir = std::make_unique<QTemporaryDir>();
79 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(tempDir->isValid(), false);
80 framesDirectory = tempDir->path();
81 isTemporaryFramesDirectory = true;
82 } else {
83 framesDirectory = encoderOptions.directory;
84 }
85#else
86 framesDirectory = encoderOptions.resolveAbsoluteFramesDirectory();
87#endif
88
89 const QString frameMimeType = encoderOptions.frameMimeType;
90 const QString extension = KisMimeDatabase::suffixesForMimeType(frameMimeType).first();
91 const QString baseFileName = QString("%1/%2.%3").arg(framesDirectory, encoderOptions.basename, extension);
92
93#ifdef Q_OS_ANDROID
94 QString videoType = encoderOptions.videoFormatKey;
95#else
96 QString videoType = encoderOptions.videoMimeType;
97#endif
98 if (mustHaveEvenDimensions(videoType, encoderOptions.renderMode())) {
99 if (hasEvenDimensions(encoderOptions.width, encoderOptions.height) != true) {
100 encoderOptions.width = encoderOptions.width + (encoderOptions.width & 0x1);
101 encoderOptions.height = encoderOptions.height + (encoderOptions.height & 0x1);
102 }
103 }
104
105 const QSize scaledSize = doc->image()->bounds().size().scaled(encoderOptions.width, encoderOptions.height, Qt::IgnoreAspectRatio);
106
107 if (mustHaveEvenDimensions(videoType, encoderOptions.renderMode())) {
108 if (hasEvenDimensions(scaledSize.width(), scaledSize.height()) != true) {
109 QString type = looksLikeMp4(videoType) ? "Mpeg4 (.mp4) " : "Matroska (.mkv) ";
110
111 qWarning() << type <<"requires width and height to be even, resize and try again!";
112 doc->setErrorMessage(i18n("%1 requires width and height to be even numbers. Please resize or crop the image before exporting.", type));
113 QMessageBox::critical(qApp->activeWindow(), i18nc("@title:window", "Krita"), i18n("Could not render animation:\n%1", doc->errorMessage()));
114
115 return false;
116 }
117 }
118
119 const bool batchMode = false; // TODO: fetch correctly!
122 encoderOptions.lastFrame),
123 baseFileName,
124 encoderOptions.sequenceStart,
125 encoderOptions.wantsOnlyUniqueFrameSequence && !encoderOptions.shouldEncodeVideo,
126 encoderOptions.frameExportConfig);
127 exporter.setBatchMode(batchMode);
128
130 exporter.regenerateRange(viewManager->mainWindow()->viewManager());
131
132 bool delayReturnSuccess = (result == KisAsyncAnimationFramesSaveDialog::RenderComplete);
133
134 // the folder could have been read-only or something else could happen
135 if ((encoderOptions.shouldEncodeVideo || encoderOptions.wantsOnlyUniqueFrameSequence) &&
137
138 const QString savedFilesMask = exporter.savedFilesMask();
139
140 if (encoderOptions.shouldEncodeVideo) {
141 bool videoFileWriteAllowed = true;
142 // Android's weird file system doesn't work this way, the target
143 // file path is going to be a sandbox URL. Making it absolute,
144 // creating a directory above it or checking for its existence
145 // neither make sense nor are they necessary.
146#ifndef Q_OS_ANDROID
147 const QString videoOutputFilePath = encoderOptions.resolveAbsoluteVideoFilePath();
148 KIS_SAFE_ASSERT_RECOVER_NOOP(QFileInfo(videoOutputFilePath).isAbsolute());
149
150 const QFileInfo videoOutputFile(videoOutputFilePath);
151 QDir outputDir(videoOutputFile.absolutePath());
152
153 if (!outputDir.exists()) {
154 outputDir.mkpath(videoOutputFile.absolutePath());
155 }
156 KIS_SAFE_ASSERT_RECOVER_NOOP(outputDir.exists());
157
158 // If file exists at output path, prompt user for overwrite..
159 if (videoOutputFile.exists()) {
160 QMessageBox videoOverwritePrompt;
161
162 videoOverwritePrompt.setText(i18n("Overwrite existing video?"));
163 videoOverwritePrompt.setInformativeText(i18n("A file already exists at the path where you want to render your video [%1]... \n\
164 Are you sure you want to overwrite the existing file?", videoOutputFilePath));
165 videoOverwritePrompt.setStandardButtons(QMessageBox::Ok | QMessageBox::Abort);
166
167 videoFileWriteAllowed = videoOverwritePrompt.exec() == QMessageBox::Ok ? true : false;
168 }
169#endif
170
171 // Write the video..
172 if (videoFileWriteAllowed) {
174
175 // Let's not mess with the file on Android like this, it's slow
176 // and could cause weird behavior depending on the provider.
177 // We'll notice that the file can't be opened later anyway.
178#ifndef Q_OS_ANDROID
179 QFile videoFile(videoOutputFilePath);
180 if (!videoFile.open(QIODevice::WriteOnly)) {
181 qWarning() << "Could not open" << videoFile.fileName() << "for writing! Do you have permission to write to this file?";
182 exportResult = KisImportExportErrorCannotWrite(videoFile.error());
183 } else {
184 videoFile.close();
185 }
186#endif
187
188 if (exportResult.isOk()) {
189 QScopedPointer<KisAnimationVideoSaver> encoder(new KisAnimationVideoSaver(doc, batchMode));
190 exportResult = encoder->convert(doc,
191 framesDirectory,
192 savedFilesMask,
193 exporter.savedFiles(),
194 encoderOptions,
195 batchMode);
196 }
197
198 if (!exportResult.isOk()) {
199 QMessageBox::critical(qApp->activeWindow(), i18nc("@title:window", "Krita"), i18n("Could not render animation:\n%1", exportResult.errorMessage()));
200
201 delayReturnSuccess = false; // Delay return to clean up exported frames.
202 }
203 }
204 }
205
206 //File cleanup
207 if (!isTemporaryFramesDirectory) {
208 QDir d(framesDirectory);
209
210#ifdef Q_OS_ANDROID
211 bool shouldDeleteSequence = false;
212#else
213 bool shouldDeleteSequence = encoderOptions.shouldDeleteSequence;
214#endif
215 if (shouldDeleteSequence || !delayReturnSuccess) {
216 QStringList savedFiles = exporter.savedFiles();
217
218 Q_FOREACH(const QString &f, savedFiles) {
219 if (d.exists(f)) {
220 d.remove(f);
221 }
222 }
223 } else if(encoderOptions.wantsOnlyUniqueFrameSequence) {
224 const QStringList fileNames = exporter.savedFiles();
225 const QStringList uniqueFrameNames = exporter.savedUniqueFiles();
226
227 Q_FOREACH(const QString &f, fileNames) {
228 if (!uniqueFrameNames.contains(f)) {
229 d.remove(f);
230 }
231 }
232 }
233
234 // We don't generate palette files on Android, that's done in memory.
235#ifndef Q_OS_ANDROID
236 QStringList paletteFiles = d.entryList(QStringList() << "KritaTempPalettegen_*.png", QDir::Files);
237
238 Q_FOREACH(const QString &f, paletteFiles) {
239 d.remove(f);
240 }
241#endif
242 }
244 QMessageBox::critical(qApp->activeWindow(), i18nc("@title:window", "Rendering error"), "Animation frame rendering has timed out. Output files are incomplete.\nTry to increase \"Frame Rendering Timeout\" or reduce \"Frame Rendering Clones Limit\" in Krita settings");
246 QMessageBox::critical(qApp->activeWindow(), i18nc("@title:window", "Rendering error"), i18n("Failed to render animation frames! Output files are incomplete."));
247 }
248
249 return delayReturnSuccess;
250}
251
252bool KisAnimationRender::mustHaveEvenDimensions(const QString &videoType,
254{
256 && (looksLikeMp4(videoType) || looksLikeMatroska(videoType));
257}
258
259bool KisAnimationRender::hasEvenDimensions(int width, int height)
260{
261 return !((width & 0x1) || (height & 0x1));
262}
QList< QString > QStringList
KisPropertiesConfigurationSP frameExportConfig
QString resolveAbsoluteVideoFilePath(const QString &documentPath) const
QString resolveAbsoluteFramesDirectory(const QString &documentPath) const
Result regenerateRange(KisViewManager *viewManager) override
start generation of frames and (if not in batch mode) show the dialog
void setBatchMode(bool value)
setting batch mode to true will prevent any dialogs or message boxes from showing on screen....
KisImageSP image
void setErrorMessage(const QString &errMsg)
QString errorMessage() const
QRect bounds() const override
KisViewManager * viewManager
static QStringList suffixesForMimeType(const QString &mimeType)
static KisTimeSpan fromTimeToTime(int start, int end)
KisMainWindow * mainWindow() const
Encoder * encoder(Imf::OutputFile &file, const ExrPaintLayerSaveInfo &info, int width)
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
bool hasEvenDimensions(int width, int height)
bool mustHaveEvenDimensions(const QString &videoType, KisAnimationRenderingOptions::RenderMode renderMode)
KRITAUI_EXPORT bool render(KisDocument *doc, KisViewManager *viewManager, KisAnimationRenderingOptions encoderOptions)