35 m_mainWindow(mainWindow),
36 m_activeView(activeView)
40 setWindowTitle(i18nc(
"@title:window",
"Import Video Animation"));
42 QWidget *page =
new QWidget(
this);
49 QFileInfo ffmpegFileInfo(config->getPropertyLazy(
"ffmpeg_path",
""));
50 QFileInfo ffprobeFileInfo(config->getPropertyLazy(
"ffprobe_path",
""));
52 dbgFile <<
"Config data =" <<
"ffmpeg:" << ffmpegFileInfo.absoluteFilePath() <<
"ffprobe:" << ffprobeFileInfo.absoluteFilePath();
56 if (ffmpegInfo[
"enabled"].toBool()) {
57 m_ui.cmbFFMpegLocation->addItem(ffmpegInfo[
"path"].toString(),ffmpegInfo);
59 if (ffprobeFileInfo.filePath().isEmpty())
60 ffprobeFileInfo.setFile(ffmpegFileInfo.absoluteDir().filePath(
"ffprobe"));
64 m_ui.tabGeneral->setEnabled(
false);
65 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"FFMpeg not found! Please add a path to FFMpeg in the \"Advanced\" tab"));
70 if (ffprobeInfo[
"enabled"].toBool())
71 m_ui.cmbFFProbeLocation->addItem(ffprobeInfo[
"path"].toString(),ffprobeInfo);
73 m_ui.cmbFFProbeLocation->addItem(
"[Disabled]",QJsonObject({{
"path",
""},{
"enabled",
false}}));
80 m_ui.fpsSpinbox->setValue(24.0);
81 m_ui.fpsSpinbox->setSuffix(i18nc(
"FPS as a unit following a value, like 60 FPS",
" FPS"));
83 m_ui.frameSkipSpinbox->setValue(1);
84 m_ui.frameSkipSpinbox->setRange(1,20);
86 m_ui.startExportingAtSpinbox->setValue(0.0);
87 m_ui.startExportingAtSpinbox->setRange(0.0, 9999.0);
88 m_ui.startExportingAtSpinbox->setSuffix(i18nc(
"Second as a unit following a value, like 60 s",
" s"));
90 m_ui.videoPreviewSlider->setTickInterval(1);
91 m_ui.videoPreviewSlider->setValue(0);
93 m_ui.exportDurationSpinbox->setValue(3.0);
94 m_ui.exportDurationSpinbox->setSuffix(i18nc(
"Second as a unit following a value, like 60 s",
" s"));
96 m_ui.lblWarning->hide();
100 m_ui.cmbDocumentHandler->addItem(i18nc(
"Import video to New Document",
"New Document"),
"0");
103 m_ui.cmbDocumentHandler->addItem(i18nc(
"Import video to Current Document",
"Current Document"),
"1");
104 m_ui.cmbDocumentHandler->setCurrentIndex(1);
105 m_ui.fpsDocumentLabel->setText(i18nc(
"Video importer: fps of the document you're importing into"
106 ,
"<small>Document:\n %1 FPS</small>"
111 m_ui.documentWidthSpinbox->setValue(0);
112 m_ui.documentHeightSpinbox->setValue(0);
113 m_ui.documentWidthSpinbox->setRange(1,100000);
114 m_ui.documentHeightSpinbox->setRange(1,100000);
116 m_ui.videoWidthSpinbox->setValue(0);
117 m_ui.videoHeightSpinbox->setValue(0);
118 m_ui.videoWidthSpinbox->setRange(1,100000);
119 m_ui.videoHeightSpinbox->setRange(1,100000);
121 m_ui.sensitivitySpinbox->setValue(50.0f);
123 m_ui.cmbVideoScaleFilter->addItem(i18n(
"Bicubic"),
"bicubic");
124 m_ui.cmbVideoScaleFilter->addItem(i18n(
"Bilinear"),
"bilinear");
125 m_ui.cmbVideoScaleFilter->addItem(i18n(
"Lanczos3"),
"lanczos");
126 m_ui.cmbVideoScaleFilter->addItem(i18n(
"Nearest Neighbor"),
"neighbor");
127 m_ui.cmbVideoScaleFilter->addItem(i18nc(
"An interpolation method",
"Spline"),
"spline");
129 m_ui.tabWidget->setCurrentIndex(0);
172 if ( !directory.mkpath(
".") ) {
173 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"Failed to create a work directory, make sure you have write permission"));
178 const float exportDuration =
m_ui.exportDurationSpinbox->value();
179 const float fps =
m_ui.fpsSpinbox->value();
180 const float nDuplicateSensitivity =
m_ui.sensitivitySpinbox->value() /
m_ui.sensitivitySpinbox->maximum();
182 if (exportDuration / fps > 100.0) {
183 if (QMessageBox::warning(
this, i18nc(
"Title for a messagebox",
"Krita"),
184 i18n(
"Warning: you are trying to import more than 100 frames into Krita.\n\n"
185 "This means you might be overloading your system.\n"
186 "If you want to edit a clip larger than 100 frames, consider using a real video editor, like Kdenlive (https://kdenlive.org)."),
187 QMessageBox::Ok | QMessageBox::Cancel,
188 QMessageBox::Cancel) == QMessageBox::Cancel) {
194 args <<
"-ss" << QString::number(
m_ui.startExportingAtSpinbox->value())
197 const float sceneFiltrationFilterThreshold =
rerange( 1 - nDuplicateSensitivity, 0.0f, 1.0f, 0.0005f, 0.2f);
199 if (
m_ui.optionFilterDuplicates->isChecked()) {
200 args <<
"-filter:v" << QString(
"select=gt(scene\\,%1)+eq(n\\,0)").arg(sceneFiltrationFilterThreshold)
204 args <<
"-t" << QString::number(exportDuration)
205 <<
"-r" << QString::number(fps);
208 args <<
"-vf" << QString(
"scale=w=")
209 .append(QString::number(
m_ui.videoWidthSpinbox->value()))
211 .append(QString::number(
m_ui.videoHeightSpinbox->value()))
213 .append(
m_ui.cmbVideoScaleFilter->currentData().toString());
216 QJsonObject ffmpegInfo =
m_ui.cmbFFMpegLocation->currentData().toJsonObject();
217 QJsonObject ffprobeInfo =
m_ui.cmbFFProbeLocation->currentData().toJsonObject();
221 config->setProperty(
"ffmpeg_path", ffmpegInfo[
"path"].toString());
222 config->setProperty(
"ffprobe_path", ffprobeInfo[
"path"].toString());
229 ffmpegSettings.
processPath = ffmpegInfo[
"path"].toString();
230 ffmpegSettings.
args = args;
231 ffmpegSettings.
outputFile = directory.filePath(
"output_%04d.png");
232 ffmpegSettings.
logPath = QDir::tempPath() + QDir::separator() +
"krita" + QDir::separator() +
"ffmpeg.log";
233 ffmpegSettings.
totalFrames = qCeil(exportDuration * fps);
234 ffmpegSettings.
progressMessage = i18nc(
"FFMPEG animated video import message. arg1: frame progress number. arg2: file suffix."
235 ,
"Extracted %1 frames from %2 video.",
"[progress]",
"[suffix]");
239 ffmpeg->startNonBlocking(ffmpegSettings);
241 bool ffmpegSuccess = ffmpeg->waitForFinished();
242 if (!ffmpegSuccess) {
246 frameFileList = directory.entryList(
QStringList() <<
"output_*.png",QDir::Files);
247 frameFileList.replaceInStrings(
"output_", directory.absolutePath() + QDir::separator() +
"output_");
249 dbgFile <<
"Import frames list:" << frameFileList;
252 if (
m_ui.optionFilterDuplicates->isChecked()){
255 ffmpegSettings.
processPath = ffprobeInfo[
"path"].toString();
257 QString filter =
"movie=" +
m_videoInfo.
file + QString(
",setpts=N+1,select=gt(scene\\,%1)").arg(sceneFiltrationFilterThreshold);
259 <<
"-show_entries" <<
"frame=pkt_pts"
260 <<
"-of" <<
"compact=p=0:nk=1"
261 <<
"-f" <<
"lavfi" << filter;
267 QString out = QString(arr);
269 QStringList integerOuts = out.split(
"\n", Qt::SkipEmptyParts);
270 Q_FOREACH(const QString& str, integerOuts){
272 const int value = str.toUInt(&ok);
274 frameTimeList.push_back(value);
279 ffmpeg->startNonBlocking(ffmpegSettings);
281 bool ffmpegSuccess = ffmpeg->waitForFinished();
282 if (!ffmpegSuccess) {
289 if ( frameFileList.isEmpty() ) {
290 QMessageBox::critical(
this, i18nc(
"@title:window",
"Krita"), i18n(
"Failed to export frames from video"));
370 const QFileInfo resultFileInfo(filename);
371 const QDir videoDir(resultFileInfo.absolutePath());
379 textInfo.append(i18nc(
"video importer: video file statistics",
"Width: %1 px", QString::number(
m_videoInfo.
width)));
380 textInfo.append(i18nc(
"video importer: video file statistics",
"Height: %1 px", QString::number(
m_videoInfo.
height)));
383 textInfo.append(i18nc(
"video importer: video file statistics"
384 ,
"Color Primaries: %1"
386 textInfo.append(i18nc(
"video importer: video file statistics"
387 ,
"Color Transfer: %1"
390 textInfo.append(i18nc(
"video importer: video file statistics",
"Duration: %1 s", QString::number(
m_videoInfo.
duration,
'f', 2)));
391 textInfo.append(i18nc(
"video importer: video file statistics",
"Frames: %1", QString::number(
m_videoInfo.
frames)));
392 textInfo.append(i18nc(
"video importer: video file statistics",
"FPS: %1", QString::number(
m_videoInfo.
fps)));
396 textInfo.append(i18nc(
"video importer: video file statistics",
"*<font size='0.5em'><em>*FPS not right in file. Modified to see full duration</em></font>"));
400 m_ui.fileLoadedDetails->setText(textInfo.join(
"\n"));
405 m_ui.exportDurationSpinbox->setRange(0, 9999.0);
407 if (
m_ui.cmbDocumentHandler->currentIndex() == 0) {
435 args <<
"-ss" << QString::number(currentDuration)
439 <<
"-vcodec" <<
"mjpeg"
440 <<
"-f" <<
"image2pipe"
445 QJsonObject ffmpegInfo =
m_ui.cmbFFMpegLocation->currentData().toJsonObject();
448 if ( byteImage.isEmpty() ) {
451 QPixmap thumbnailPixmap;
452 thumbnailPixmap.loadFromData(byteImage,
"JFIF");
454 m_ui.thumbnailImageHolder->clear();
455 const QSize previewSize =
456 m_ui.thumbnailImageHolder->contentsRect().size() *
m_ui.thumbnailImageHolder->devicePixelRatioF();
457 QPixmap img = thumbnailPixmap.scaled(previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
458 img.setDevicePixelRatio(
m_ui.thumbnailImageHolder->devicePixelRatioF());
459 m_ui.thumbnailImageHolder->setPixmap(img);
474 const KFormat format;
477 quint32 pixelSize = 4;
483 const qint64 frames = std::lround(qreal(
m_videoInfo.
fps) * time + 2);
491 const QString text_frames = i18nc(
"part of warning in video importer."
492 ,
"<b>Warning:</b> you are trying to import %1 frames, the maximum amount you can import is %2."
498 const QString text_video_editor = i18nc(
"part of warning in video importer.",
499 "Use a <a href=\"https://kdenlive.org\">video editor</a> instead!");
501 if (maxFrames < frames) {
502 warnings.append(text_frames);
503 text_memory = i18nc(
"part of warning in video importer."
504 ,
"You do not have enough memory to load this many frames, the computer will be overloaded.");
505 warnings.insert(0,
"<span style=\"color:#ff692e;\">");
506 warnings.append(text_memory);
507 warnings.append(text_video_editor);
508 m_ui.lblWarning->setVisible(
true);
509 }
else if (maxFrames < frames * 2) {
510 warnings.append(text_frames);
511 text_memory = i18nc(
"part of warning in video importer."
512 ,
"This will take over half the available memory, editing will be difficult.");
513 warnings.insert(0,
"<span style=\"color:#ffee00;\">");
514 warnings.append(text_memory);
515 warnings.append(text_video_editor);
516 m_ui.lblWarning->setVisible(
true);
519 warnings.append(text_frames);
520 QString text_trc = i18nc(
"part of warning in video importer."
521 ,
"Krita does not support the video transfer curve (%1), it will be loaded as linear."
523 warnings.append(text_trc);
526 if (warnings.isEmpty()) {
527 m_ui.lblWarning->setVisible(
false);
529 m_ui.lblWarning->setText(warnings.join(
" "));
530 m_ui.lblWarning->setPixmap(
531 m_ui.lblWarning->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(32, 32)));
532 m_ui.lblWarning->setVisible(
true);
655 QJsonObject ffmpegInfo =
m_ui.cmbFFMpegLocation->currentData().toJsonObject();
656 QJsonObject ffprobeInfo =
m_ui.cmbFFProbeLocation->currentData().toJsonObject();
660 QJsonObject ffprobeJsonObj;
662 std::function<
void(
void)> warnFFmpegFormatSupport = [
this](){
663 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"Your FFMpeg version does not support this format"));
666 if (ffprobeInfo[
"enabled"].toBool()) {
667 ffprobeJsonObj = ffprobe->ffprobe(inputFile, ffprobeInfo[
"path"].toString());
674 ffprobeJsonObj = ffmpeg->
ffmpegProbe(inputFile, ffmpegInfo[
"path"].toString(),
false);
676 dbgFile <<
"ffmpeg probe1" << ffprobeJsonObj;
678 QJsonObject ffprobeProgress = ffprobeJsonObj[
"progress"].toObject();
680 videoInfoData.
frames = ffprobeProgress[
"frame"].toString().toInt();
685 warnFFmpegFormatSupport();
689 QJsonObject ffprobeFormat = ffprobeJsonObj[
"format"].toObject();
690 QJsonArray ffprobeStreams = ffprobeJsonObj[
"streams"].toArray();
692 videoInfoData.
file = inputFile;
693 for (
const QJsonValueRef &streamItemRef : ffprobeStreams) {
694 QJsonObject streamItemObj = streamItemRef.toObject();
696 if ( streamItemObj[
"codec_type"].toString() ==
"video" ) {
697 videoInfoData.
stream = streamItemObj[
"index"].toInt();
702 if ( videoInfoData.
stream == -1 ) {
703 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"No video stream could be found!"));
707 const QJsonObject ffprobeSelectedStream = ffprobeStreams[videoInfoData.
stream].toObject();
709 const QJsonObject decoders = ffmpegInfo.value(
"codecs").toObject();
711 const QString codecName = ffprobeSelectedStream[
"codec_name"].toString();
713 const auto decoder = decoders.constFind(codecName);
715 if (decoder == decoders.constEnd() ) {
716 dbgFile <<
"Codec missing or unsupported:" << codecName;
717 warnFFmpegFormatSupport();
719 }
else if (!decoder->toObject().value(
"decoding").toBool()) {
720 dbgFile <<
"Codec not supported for decoding:" << codecName;
721 warnFFmpegFormatSupport();
725 videoInfoData.
width = ffprobeSelectedStream[
"width"].toInt();
726 videoInfoData.
height = ffprobeSelectedStream[
"height"].toInt();
727 videoInfoData.
encoding = ffprobeSelectedStream[
"codec_name"].toString();
732 if (ffprobeSelectedStream.value(
"bits_per_raw_sample").toInt() > 8) {
740 QStringList rawFrameRate = ffprobeSelectedStream[
"r_frame_rate"].toString().split(
'/');
742 if (!rawFrameRate.isEmpty())
743 videoInfoData.
fps = qCeil(rawFrameRate[0].toFloat() / rawFrameRate[1].toFloat());
745 if ( !ffprobeSelectedStream[
"nb_frames"].isNull() ) {
746 videoInfoData.
frames = ffprobeSelectedStream[
"nb_frames"].toString().toInt();
750 if ( !ffprobeSelectedStream[
"duration"].isNull() ) {
751 videoInfoData.
duration = ffprobeSelectedStream[
"duration"].toString().toFloat();
752 }
else if ( !ffprobeFormat[
"duration"].isNull() ) {
753 videoInfoData.
duration = ffprobeFormat[
"duration"].toString().toFloat();
754 }
else if ( videoInfoData.
frames ) {
758 dbgFile <<
"Initial video info from probe: "
759 <<
"stream:" << videoInfoData.
stream
760 <<
"frames:" << videoInfoData.
frames
761 <<
"duration:" << videoInfoData.
duration
762 <<
"fps:" << videoInfoData.
fps
763 <<
"encoding:" << videoInfoData.
encoding;
768 QJsonObject ffmpegJsonObj = ffmpeg->
ffmpegProbe(inputFile, ffmpegInfo[
"path"].toString(),
false);
770 dbgFile <<
"ffmpeg probe2" << ffmpegJsonObj;
773 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"Failed to load video information"));
777 QJsonObject ffmpegProgressJsonObj = ffmpegJsonObj[
"progress"].toObject();
778 float ffmpegFPS = ffmpegProgressJsonObj[
"ffmpeg_fps"].toString().toFloat();
780 videoInfoData.
frames = ffmpegProgressJsonObj[
"frame"].toString().toInt();
782 if (ffmpegFPS > 0 && videoInfoData.
frames) {
784 videoInfoData.
fps = ffmpegFPS;
786 videoInfoData.
duration = ffmpegProgressJsonObj[
"out_time_ms"].toString().toFloat() / 1000000;
798 dbgFile <<
"Final video info from probe: "
799 <<
"stream:" << videoInfoData.
stream
800 <<
"frames:" << videoInfoData.
frames
801 <<
"duration:" << videoInfoData.
duration
802 <<
"fps:" << videoInfoData.
fps
803 <<
"encoding:" << videoInfoData.
encoding;
805 const float calculatedFrameRateByDuration = videoInfoData.
frames / videoInfoData.
duration;
806 const int frameRateDifference = qAbs(videoInfoData.
fps - calculatedFrameRateByDuration);
808 if (frameRateDifference > 1) {
812 videoInfoData.
fps = calculatedFrameRateByDuration;
817 return videoInfoData;