Pencil2D Animation
Download Community News Docs Contribute
  • Overview
  • Articles
  • Code
  •  
  • Class List
  • Class Index
  • Class Hierarchy
  • Class Members
  • File List
Loading...
Searching...
No Matches
  • core_lib
  • src
movieexporter.cpp
1/*
2
3Pencil2D - Traditional Animation Software
4Copyright (C) 2012-2020 Matthew Chiawen Chang
5
6This program is free software; you can redistribute it and/or
7modify it under the terms of the GNU General Public License
8as published by the Free Software Foundation; version 2 of the License.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
14
15*/
16
17#include "movieexporter.h"
18
19#include <ctime>
20#include <vector>
21#include <cstdint>
22#include <QDir>
23#include <QDebug>
24#include <QProcess>
25#include <QApplication>
26#include <QStandardPaths>
27#include <QThread>
28#include <QtMath>
29#include <QPainter>
30#include <QRegularExpression>
31
32#include "object.h"
33#include "layercamera.h"
34#include "layersound.h"
35#include "soundclip.h"
36#include "util.h"
37
38#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
39using Qt::SplitBehaviorFlags;
40#else
41using SplitBehaviorFlags = QString::SplitBehavior;
42#endif
43
44MovieExporter::MovieExporter()
45{
46}
47
48MovieExporter::~MovieExporter()
49{
50}
51
78Status MovieExporter::run(const Object* obj,
79 const ExportMovieDesc& desc,
80 std::function<void(float, float)> majorProgress,
81 std::function<void(float)> minorProgress,
82 std::function<void(QString)> progressMessage)
83{
84 majorProgress(0.f, 0.03f);
85 minorProgress(0.f);
86 progressMessage(tr("Checking environment..."));
87
88 clock_t t1 = clock();
89
90 QString ffmpegPath = ffmpegLocation();
91 qDebug() << ffmpegPath;
92 if (!QFile::exists(ffmpegPath))
93 {
94#ifdef _WIN32
95 qCritical() << "Please place ffmpeg.exe in " << ffmpegPath << " directory";
96#else
97 qCritical() << "Please place ffmpeg in " << ffmpegPath << " directory";
98#endif
99 return Status::ERROR_FFMPEG_NOT_FOUND;
100 }
101
102 STATUS_CHECK(checkInputParameters(desc))
103 mDesc = desc;
104
105 qDebug() << "OutFile: " << mDesc.strFileName;
106
107 // Setup temporary folder
108 if (!mTempDir.isValid())
109 {
110 Status status = Status::FAIL;
111 status.setTitle(tr("Error creating folder"));
112 status.setDescription(tr("Unable to create a temporary folder, cannot export animation."));
113 DebugDetails dd;
114 dd << QString("Parent directory: ").append(QDir::tempPath())
115 << QString("Error: ").append(mTempDir.errorString());
116 status.setDetails(dd);
117 return status;
118 }
119
120 mTempWorkDir = mTempDir.path();
121
122 minorProgress(0.f);
123 if (desc.strFileName.endsWith("gif", Qt::CaseInsensitive))
124 {
125 majorProgress(0.03f, 1.f);
126 progressMessage(tr("Generating GIF..."));
127 minorProgress(0.f);
128 STATUS_CHECK(generateGif(obj, ffmpegPath, desc.strFileName, minorProgress))
129 }
130 else
131 {
132 majorProgress(0.03f, 0.25f);
133 progressMessage(tr("Assembling audio..."));
134 minorProgress(0.f);
135 STATUS_CHECK(assembleAudio(obj, ffmpegPath, minorProgress))
136 minorProgress(1.f);
137 majorProgress(0.25f, 1.f);
138 progressMessage(tr("Generating movie..."));
139 STATUS_CHECK(generateMovie(obj, ffmpegPath, desc.strFileName, minorProgress))
140 }
141 minorProgress(1.f);
142 majorProgress(1.f, 1.f);
143 progressMessage(tr("Done"));
144
145 clock_t t2 = clock() - t1;
146 qDebug("MOVIE = %.1f sec", static_cast<double>(t2 / CLOCKS_PER_SEC));
147
148 return Status::OK;
149}
150
151QString MovieExporter::error()
152{
153 return QString();
154}
155
168Status MovieExporter::assembleAudio(const Object* obj,
169 QString ffmpegPath,
170 std::function<void(float)> progress)
171{
172 // Quicktime assemble call
173 const int startFrame = mDesc.startFrame;
174 const int endFrame = mDesc.endFrame;
175 const int fps = mDesc.fps;
176
177 Q_ASSERT(startFrame >= 0);
178 Q_ASSERT(endFrame >= startFrame);
179
180 QDir dir(mTempWorkDir);
181 Q_ASSERT(dir.exists());
182
183 QString tempAudioPath = QDir(mTempWorkDir).filePath("tmpaudio.wav");
184 qDebug() << "TempAudio=" << tempAudioPath;
185
186 std::vector< SoundClip* > allSoundClips;
187
188 std::vector< LayerSound* > allSoundLayers = obj->getLayersByType<LayerSound>();
189 for (LayerSound* layer : allSoundLayers)
190 {
191 if (!layer->visible()) { continue; }
192 layer->foreachKeyFrame([&allSoundClips](KeyFrame* key)
193 {
194 if (!key->fileName().isEmpty())
195 {
196 allSoundClips.push_back(static_cast<SoundClip*>(key));
197 }
198 });
199 }
200
201 if (allSoundClips.empty()) return Status::SAFE;
202
203 int clipCount = 0;
204
205 QString filterComplex, amergeInput, panChannelLayout;
206 QStringList args;
207
208 int wholeLen = qCeil(endFrame * 44100.0 / fps);
209 for (auto clip : allSoundClips)
210 {
211 if (mCanceled)
212 {
213 return Status::CANCELED;
214 }
215
216 // Add sound file as input
217 args << "-i" << clip->fileName();
218
219 // Offset the sound to its correct position
220 // See https://superuser.com/questions/716320/ffmpeg-placing-audio-at-specific-location
221 filterComplex += QString("[%1:a:0] aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=mono,volume=1,adelay=%2S|%2S,apad=whole_len=%3[ad%1];")
222 .arg(clipCount).arg(qRound(44100.0 * (clip->pos() - 1) / fps)).arg(wholeLen);
223 amergeInput += QString("[ad%1]").arg(clipCount);
224 panChannelLayout += QString("c%1+").arg(clipCount);
225
226 clipCount++;
227 }
228 // Remove final '+'
229 panChannelLayout.chop(1);
230 // Output arguments
231 // Mix audio
232 args << "-filter_complex";
233 if (clipCount == 1)
234 {
235 // If there is only one sound clip there is no need to use amerge
236 // Prior to ffmpeg 3.2, amerge does not support inputs=1
237 filterComplex.chop(1); // Remove final semicolon since there are no more filters added after
238 args << filterComplex << "-map" << amergeInput;
239 }
240 else {
241 args << QString("%1%2 amerge=inputs=%3, pan=mono|c0=%4 [out]")
242 .arg(filterComplex).arg(amergeInput).arg(clipCount).arg(panChannelLayout);
243 args << "-map" << "[out]";
244 }
245 // Convert audio file: 44100Hz sampling rate, stereo, signed 16 bit little endian
246 // Supported audio file types: wav, mp3, ogg... ( all file types supported by ffmpeg )
247 args << "-ar" << "44100" << "-acodec" << "pcm_s16le" << "-ac" << "2" << "-y";
248 // Trim audio
249 args << "-ss" << QString::number((startFrame - 1) / static_cast<double>(fps));
250 args << "-to" << QString::number(endFrame / static_cast<double>(fps));
251 // Output path
252 args << tempAudioPath;
253
254 STATUS_CHECK(MovieExporter::executeFFmpeg(ffmpegPath, args, [&progress, this] (int frame) { progress(frame / static_cast<float>(mDesc.endFrame - mDesc.startFrame)); return !mCanceled; }))
255 qDebug() << "audio file: " + tempAudioPath;
256
257 return Status::OK;
258}
259
276Status MovieExporter::generateMovie(
277 const Object* obj,
278 QString ffmpegPath,
279 QString strOutputFile,
280 std::function<void(float)> progress)
281{
282 if (mCanceled)
283 {
284 return Status::CANCELED;
285 }
286
287 // Frame generation setup
288
289 int frameStart = mDesc.startFrame;
290 int frameEnd = mDesc.endFrame;
291 const QSize exportSize = mDesc.exportSize;
292 bool transparency = mDesc.alpha;
293 QString strCameraName = mDesc.strCameraName;
294 bool loop = mDesc.loop;
295
296 auto cameraLayer = static_cast<LayerCamera*>(obj->findLayerByName(strCameraName, Layer::CAMERA));
297 if (cameraLayer == nullptr)
298 {
299 cameraLayer = obj->getLayersByType< LayerCamera >().front();
300 }
301 int currentFrame = frameStart;
302
303 /* We create an image with the correct dimensions and background
304 * color here and then copy this and draw over top of it to
305 * generate each frame. This is faster than having to generate
306 * a new background image for each frame.
307 */
308 QImage imageToExportBase(exportSize, QImage::Format_ARGB32_Premultiplied);
309 QColor bgColor = Qt::white;
310 if (transparency)
311 {
312 bgColor.setAlpha(0);
313 }
314 imageToExportBase.fill(bgColor);
315
316 QSize camSize = cameraLayer->getViewSize();
317 QTransform centralizeCamera;
318 centralizeCamera.translate(camSize.width() / 2, camSize.height() / 2);
319
320 int failCounter = 0;
321 /* Movie export uses a "sliding window" to reduce memory usage
322 * while having a relatively small impact on speed. This basically
323 * means that there is a maximum number of frames that can be waiting
324 * to be encoded by ffmpeg at any one time. The limit is set by the
325 * frameWindow variable which is designed to take up a maximum of
326 * about 1GB of memory
327 */
328 int frameWindow = static_cast<int>(1e9 / (camSize.width() * camSize.height() * 4.0));
329
330 // Build FFmpeg command
331
332 //int exportFps = mDesc.videoFps;
333 const QString tempAudioPath = QDir(mTempWorkDir).filePath("tmpaudio.wav");
334
335 QStringList args = {"-f", "rawvideo", "-pixel_format", "bgra"};
336 args << "-video_size" << QString("%1x%2").arg(exportSize.width()).arg(exportSize.height());
337 args << "-framerate" << QString::number(mDesc.fps);
338
339 //args << "-r" << QString::number(exportFps);
340 args << "-i" << "-";
341 args << "-threads" << (QThread::idealThreadCount() == 1 ? "0" : QString::number(QThread::idealThreadCount()));
342
343 if (QFile::exists(tempAudioPath))
344 {
345 args << "-i" << tempAudioPath;
346 }
347
348 if (strOutputFile.endsWith(".apng", Qt::CaseInsensitive))
349 {
350 args << "-plays" << (loop ? "0" : "1");
351 }
352
353 if (strOutputFile.endsWith(".mp4", Qt::CaseInsensitive))
354 {
355 args << "-pix_fmt" << "yuv420p";
356 }
357
358 if (strOutputFile.endsWith(".avi", Qt::CaseInsensitive))
359 {
360 args << "-q:v" << "5";
361 }
362
363 args << "-y";
364 args << strOutputFile;
365
366 // Run FFmpeg command
367
368 Status status = executeFFMpegPipe(ffmpegPath, args, progress, [&](QProcess& ffmpeg, int framesProcessed)
369 {
370 if(framesProcessed < 0)
371 {
372 failCounter++;
373 }
374
375 if(currentFrame > frameEnd)
376 {
377 ffmpeg.closeWriteChannel();
378 return false;
379 }
380
381 if((currentFrame - frameStart <= framesProcessed + frameWindow || failCounter > 10) && currentFrame <= frameEnd)
382 {
383 QImage imageToExport = imageToExportBase.copy();
384 QPainter painter(&imageToExport);
385
386 QTransform view = cameraLayer->getViewAtFrame(currentFrame);
387 painter.setWorldTransform(view * centralizeCamera);
388 painter.setWindow(QRect(0, 0, camSize.width(), camSize.height()));
389
390 obj->paintImage(painter, currentFrame, false, true);
391 painter.end();
392
393#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
394 int bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.sizeInBytes());
395 Q_ASSERT(bytesWritten == imageToExport.sizeInBytes());
396#else
397 int bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.byteCount());
398 Q_ASSERT(bytesWritten == imageToExport.byteCount());
399#endif
400
401 currentFrame++;
402 failCounter = 0;
403 return true;
404 }
405
406 return false;
407 });
408 STATUS_CHECK(status);
409
410 return Status::OK;
411}
412
425Status MovieExporter::generateGif(
426 const Object* obj,
427 QString ffmpegPath,
428 QString strOut,
429 std::function<void(float)> progress)
430{
431
432 if (mCanceled)
433 {
434 return Status::CANCELED;
435 }
436
437 // Frame generation setup
438
439 int frameStart = mDesc.startFrame;
440 int frameEnd = mDesc.endFrame;
441 const QSize exportSize = mDesc.exportSize;
442 bool transparency = false;
443 QString strCameraName = mDesc.strCameraName;
444 bool loop = mDesc.loop;
445 int bytesWritten;
446
447 auto cameraLayer = static_cast<LayerCamera*>(obj->findLayerByName(strCameraName, Layer::CAMERA));
448 if (cameraLayer == nullptr)
449 {
450 cameraLayer = obj->getLayersByType< LayerCamera >().front();
451 }
452 int currentFrame = frameStart;
453
454 /* We create an image with the correct dimensions and background
455 * color here and then copy this and draw over top of it to
456 * generate each frame. This is faster than having to generate
457 * a new background image for each frame.
458 */
459 QImage imageToExportBase(exportSize, QImage::Format_ARGB32_Premultiplied);
460 QColor bgColor = Qt::white;
461 if (transparency)
462 {
463 bgColor.setAlpha(0);
464 }
465 imageToExportBase.fill(bgColor);
466
467 QSize camSize = cameraLayer->getViewSize();
468 QTransform centralizeCamera;
469 centralizeCamera.translate(camSize.width() / 2, camSize.height() / 2);
470
471 // Build FFmpeg command
472
473 QStringList args = {"-f", "rawvideo", "-pixel_format", "bgra"};
474 args << "-video_size" << QString("%1x%2").arg(exportSize.width()).arg(exportSize.height());
475 args << "-framerate" << QString::number(mDesc.fps);
476
477 args << "-i" << "-";
478
479 args << "-y";
480
481 args << "-filter_complex" << "[0:v]palettegen [p]; [0:v][p] paletteuse";
482
483 args << "-loop" << (loop ? "0" : "-1");
484 args << strOut;
485
486 // Run FFmpeg command
487
488 Status status = executeFFMpegPipe(ffmpegPath, args, progress, [&](QProcess& ffmpeg, int framesProcessed)
489 {
490 /* The GIF FFmpeg command requires the entires stream to be
491 * written before FFmpeg can encode the GIF. This is because
492 * the generated pallete is based off of the colors in all
493 * frames. The only way to avoid this would be to generate
494 * all the frames twice and run two separate commands, which
495 * would likely have unacceptable speed costs.
496 */
497
498 Q_UNUSED(framesProcessed);
499 if(currentFrame > frameEnd)
500 {
501 ffmpeg.closeWriteChannel();
502 return false;
503 }
504
505 QImage imageToExport = imageToExportBase.copy();
506 QPainter painter(&imageToExport);
507
508 QTransform view = cameraLayer->getViewAtFrame(currentFrame);
509 painter.setWorldTransform(view * centralizeCamera);
510 painter.setWindow(QRect(0, 0, camSize.width(), camSize.height()));
511
512 obj->paintImage(painter, currentFrame, false, true);
513
514#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
515 bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.sizeInBytes());
516 Q_ASSERT(bytesWritten == imageToExport.sizeInBytes());
517#else
518 bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.byteCount());
519 Q_ASSERT(bytesWritten == imageToExport.byteCount());
520#endif
521
522 currentFrame++;
523
524 return true;
525 });
526 STATUS_CHECK(status);
527
528 return Status::OK;
529}
530
547Status MovieExporter::executeFFmpeg(const QString& cmd, const QStringList& args, std::function<bool(int)> progress)
548{
549 qDebug() << cmd;
550
551 QProcess ffmpeg;
552 ffmpeg.setReadChannel(QProcess::StandardOutput);
553 // FFmpeg writes to stderr only for some reason, so we just read both channels together
554 ffmpeg.setProcessChannelMode(QProcess::MergedChannels);
555 ffmpeg.start(cmd, args);
556
557 Status status = Status::OK;
558 DebugDetails dd;
559 dd << QStringLiteral("Command: %1 %2").arg(cmd).arg(args.join(' '));
560 if (ffmpeg.waitForStarted())
561 {
562 while(ffmpeg.state() == QProcess::Running)
563 {
564 if(!ffmpeg.waitForReadyRead()) break;
565
566 QString output(ffmpeg.readAll());
567 QStringList sList = output.split(QRegularExpression("[\r\n]"), SplitBehaviorFlags::SkipEmptyParts);
568 for (const QString& s : sList)
569 {
570 qDebug() << "[ffmpeg]" << s;
571 dd << s;
572 }
573
574 if(output.startsWith("frame="))
575 {
576 QString frame = output.mid(6, output.indexOf(' '));
577
578 bool shouldContinue = progress(frame.toInt());
579 if (!shouldContinue)
580 {
581 ffmpeg.terminate();
582 ffmpeg.waitForFinished(3000);
583 if (ffmpeg.state() == QProcess::Running) ffmpeg.kill();
584 ffmpeg.waitForFinished();
585 return Status::CANCELED;
586 }
587 }
588 }
589
590 QString output(ffmpeg.readAll());
591 QStringList sList = output.split(QRegularExpression("[\r\n]"), SplitBehaviorFlags::SkipEmptyParts);
592 for (const QString& s : sList)
593 {
594 qDebug() << "[ffmpeg]" << s;
595 dd << s;
596 }
597
598 if(ffmpeg.exitStatus() != QProcess::NormalExit || ffmpeg.exitCode() != 0)
599 {
600 status = Status::FAIL;
601 status.setTitle(tr("Something went wrong"));
602 status.setDescription(tr("Looks like our video backend did not exit normally. Your movie may not have exported correctly. Please try again and report this if it persists."));
603 dd << QString("Exit status: ").append(QProcess::NormalExit ? "NormalExit": "CrashExit")
604 << QString("Exit code: %1").arg(ffmpeg.exitCode());
605 status.setDetails(dd);
606 return status;
607 }
608 }
609 else
610 {
611 qDebug() << "ERROR: Could not execute FFmpeg.";
612 status = Status::FAIL;
613 status.setTitle(tr("Something went wrong"));
614 status.setDescription(tr("Couldn't start the video backend, please try again."));
615 status.setDetails(dd);
616 }
617 return status;
618}
619
663Status MovieExporter::executeFFMpegPipe(const QString& cmd, const QStringList& args, std::function<void(float)> progress, std::function<bool(QProcess&, int)> writeFrame)
664{
665 qDebug() << cmd;
666
667 QProcess ffmpeg;
668 ffmpeg.setReadChannel(QProcess::StandardOutput);
669 // FFmpeg writes to stderr only for some reason, so we just read both channels together
670 ffmpeg.setProcessChannelMode(QProcess::MergedChannels);
671 ffmpeg.start(cmd, args);
672
673 Status status = Status::OK;
674 DebugDetails dd;
675 dd << QStringLiteral("Command: %1 %2").arg(cmd).arg(args.join(' '));
676 if (ffmpeg.waitForStarted())
677 {
678 int framesGenerated = 0;
679 int lastFrameProcessed = 0;
680 const int frameStart = mDesc.startFrame;
681 const int frameEnd = mDesc.endFrame;
682 while(ffmpeg.state() == QProcess::Running)
683 {
684 if (mCanceled)
685 {
686 ffmpeg.terminate();
687 if (ffmpeg.state() == QProcess::Running) ffmpeg.kill();
688 return Status::CANCELED;
689 }
690
691 // Check FFmpeg progress
692
693 int framesProcessed = -1;
694 if(ffmpeg.waitForReadyRead(10))
695 {
696 QString output(ffmpeg.readAll());
697 QStringList sList = output.split(QRegularExpression("[\r\n]"), SplitBehaviorFlags::SkipEmptyParts);
698 for (const QString& s : sList)
699 {
700 qDebug() << "[ffmpeg]" << s;
701 dd << s;
702 }
703 if(output.startsWith("frame="))
704 {
705 lastFrameProcessed = framesProcessed = output.mid(6, output.indexOf(' ')).toInt();
706 }
707 }
708
709 if(!ffmpeg.isWritable())
710 {
711 continue;
712 }
713
714 while(writeFrame(ffmpeg, framesProcessed))
715 {
716 framesGenerated++;
717
718 const float percentGenerated = framesGenerated / static_cast<float>(frameEnd - frameStart);
719 const float percentConverted = lastFrameProcessed / static_cast<float>(frameEnd - frameStart);
720 progress((percentGenerated + percentConverted) / 2);
721 }
722 const float percentGenerated = framesGenerated / static_cast<float>(frameEnd - frameStart);
723 const float percentConverted = lastFrameProcessed / static_cast<float>(frameEnd - frameStart);
724 progress((percentGenerated + percentConverted) / 2);
725 }
726
727 QString output(ffmpeg.readAll());
728 QStringList sList = output.split(QRegularExpression("[\r\n]"), SplitBehaviorFlags::SkipEmptyParts);
729 for (const QString& s : sList)
730 {
731 qDebug() << "[ffmpeg]" << s;
732 dd << s;
733 }
734
735 if(ffmpeg.exitStatus() != QProcess::NormalExit || ffmpeg.exitCode() != 0)
736 {
737 status = Status::FAIL;
738 status.setTitle(tr("Something went wrong"));
739 status.setDescription(tr("Looks like our video backend did not exit normally. Your movie may not have exported correctly. Please try again and report this if it persists."));
740 dd << QString("Exit status: ").append(QProcess::NormalExit ? "NormalExit": "CrashExit")
741 << QString("Exit code: %1").arg(ffmpeg.exitCode());
742 status.setDetails(dd);
743 return status;
744 }
745 }
746 else
747 {
748 qDebug() << "ERROR: Could not execute FFmpeg.";
749 status = Status::FAIL;
750 status.setTitle(tr("Something went wrong"));
751 status.setDescription(tr("Couldn't start the video backend, please try again."));
752 status.setDetails(dd);
753 }
754
755 return status;
756}
757
758Status MovieExporter::checkInputParameters(const ExportMovieDesc& desc)
759{
760 bool b = true;
761 b &= (!desc.strFileName.isEmpty());
762 b &= (desc.startFrame > 0);
763 b &= (desc.endFrame >= desc.startFrame);
764 b &= (desc.fps > 0);
765 b &= (!desc.strCameraName.isEmpty());
766
767 return b ? Status::OK : Status::INVALID_ARGUMENT;
768}
DebugDetails
Definition: pencilerror.h:25
KeyFrame
Definition: keyframe.h:30
LayerCamera
Definition: layercamera.h:30
LayerSound
Definition: layersound.h:26
MovieExporter::run
Status run(const Object *obj, const ExportMovieDesc &desc, std::function< void(float, float)> majorProgress, std::function< void(float)> minorProgress, std::function< void(QString)> progressMessage)
Begin exporting the movie described by exportDesc.
Definition: movieexporter.cpp:78
MovieExporter::assembleAudio
Status assembleAudio(const Object *obj, QString ffmpegPath, std::function< void(float)> progress)
Combines all audio tracks in obj into a single file.
Definition: movieexporter.cpp:168
MovieExporter::generateMovie
Status generateMovie(const Object *obj, QString ffmpegPath, QString strOutputFile, std::function< void(float)> progress)
Exports obj to a movie image at strOut using FFmpeg.
Definition: movieexporter.cpp:276
MovieExporter::generateGif
Status generateGif(const Object *obj, QString ffmpeg, QString strOut, std::function< void(float)> progress)
Exports obj to a gif image at strOut using FFmpeg.
Definition: movieexporter.cpp:425
MovieExporter::executeFFMpegPipe
Status executeFFMpegPipe(const QString &cmd, const QStringList &args, std::function< void(float)> progress, std::function< bool(QProcess &, int)> writeFrame)
Runs the specified command (should be ffmpeg), and lets writeFrame pipe data into it 1 frame at a tim...
Definition: movieexporter.cpp:663
MovieExporter::executeFFmpeg
static Status executeFFmpeg(const QString &cmd, const QStringList &args, std::function< bool(int)> progress)
Runs the specified command (should be ffmpeg) and allows for progress feedback.
Definition: movieexporter.cpp:547
Object
Definition: object.h:42
SoundClip
Definition: soundclip.h:27
Status
Definition: pencilerror.h:40
QColor
QColor::setAlpha
void setAlpha(int alpha)
QDir
QDir::exists
bool exists() const const
QDir::filePath
QString filePath(const QString &fileName) const const
QDir::tempPath
QString tempPath()
QImage::byteCount
int byteCount() const const
QImage
QImage::Format_ARGB32_Premultiplied
Format_ARGB32_Premultiplied
QImage::constBits
const uchar * constBits() const const
QImage::copy
QImage copy(const QRect &rectangle) const const
QImage::fill
void fill(uint pixelValue)
QImage::sizeInBytes
int sizeInBytes() const const
QIODevice::isWritable
bool isWritable() const const
QIODevice::readAll
QByteArray readAll()
QIODevice::write
qint64 write(const char *data, qint64 maxSize)
QPainter
QPainter::end
bool end()
QPainter::setWindow
void setWindow(const QRect &rectangle)
QPainter::setWorldTransform
void setWorldTransform(const QTransform &matrix, bool combine)
QProcess
QProcess::NormalExit
NormalExit
QProcess::StandardOutput
StandardOutput
QProcess::MergedChannels
MergedChannels
QProcess::Running
Running
QProcess::closeWriteChannel
void closeWriteChannel()
QProcess::exitCode
int exitCode() const const
QProcess::exitStatus
QProcess::ExitStatus exitStatus() const const
QProcess::kill
void kill()
QProcess::setProcessChannelMode
void setProcessChannelMode(QProcess::ProcessChannelMode mode)
QProcess::setReadChannel
void setReadChannel(QProcess::ProcessChannel channel)
QProcess::start
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
QProcess::state
QProcess::ProcessState state() const const
QProcess::terminate
void terminate()
QProcess::waitForFinished
bool waitForFinished(int msecs)
QProcess::waitForReadyRead
virtual bool waitForReadyRead(int msecs) override
QProcess::waitForStarted
bool waitForStarted(int msecs)
QRect
QRegularExpression
QSize
QSize::height
int height() const const
QSize::width
int width() const const
QString::SplitBehavior
SplitBehavior
QString::split
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString
QString::append
QString & append(QChar ch)
QString::arg
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString::chop
void chop(int n)
QString::endsWith
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString::indexOf
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QString::isEmpty
bool isEmpty() const const
QString::mid
QString mid(int position, int n) const const
QString::number
QString number(int n, int base)
QString::startsWith
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString::toInt
int toInt(bool *ok, int base) const const
QStringList
QStringList::join
QString join(const QString &separator) const const
Qt::CaseInsensitive
CaseInsensitive
Qt::white
white
Qt::SplitBehaviorFlags
SplitBehaviorFlags
QTemporaryDir::errorString
QString errorString() const const
QTemporaryDir::isValid
bool isValid() const const
QTemporaryDir::path
QString path() const const
QThread::idealThreadCount
int idealThreadCount()
QTransform
QTransform::translate
QTransform & translate(qreal dx, qreal dy)
ExportMovieDesc
Definition: movieexporter.h:31
Generated on Thu Oct 22 2026 09:23:32 for Pencil2D by doxygen 1.9.6 based on revision 9c5106d551fd53c116521f24bb1db87ae7e6180c