34 m_mainWindow(mainWindow),
35 m_activeView(activeView)
39 setWindowTitle(i18nc(
"@title:window",
"Import Video Animation"));
41 QWidget *page =
new QWidget(
this);
48 QFileInfo ffmpegFileInfo(config->getPropertyLazy(
"ffmpeg_path",
""));
49 QFileInfo ffprobeFileInfo(config->getPropertyLazy(
"ffprobe_path",
""));
51 dbgFile <<
"Config data =" <<
"ffmpeg:" << ffmpegFileInfo.absoluteFilePath() <<
"ffprobe:" << ffprobeFileInfo.absoluteFilePath();
55 if (ffmpegInfo[
"enabled"].toBool()) {
56 m_ui.cmbFFMpegLocation->addItem(ffmpegInfo[
"path"].toString(),ffmpegInfo);
58 if (ffprobeFileInfo.filePath().isEmpty())
59 ffprobeFileInfo.setFile(ffmpegFileInfo.absoluteDir().filePath(
"ffprobe"));
63 m_ui.tabGeneral->setEnabled(
false);
64 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"FFMpeg not found! Please add a path to FFMpeg in the \"Advanced\" tab"));
69 if (ffprobeInfo[
"enabled"].toBool())
70 m_ui.cmbFFProbeLocation->addItem(ffprobeInfo[
"path"].toString(),ffprobeInfo);
72 m_ui.cmbFFProbeLocation->addItem(
"[Disabled]",QJsonObject({{
"path",
""},{
"enabled",
false}}));
79 m_ui.fpsSpinbox->setValue(24.0);
80 m_ui.fpsSpinbox->setSuffix(i18nc(
"FPS as a unit following a value, like 60 FPS",
" FPS"));
82 m_ui.frameSkipSpinbox->setValue(1);
83 m_ui.frameSkipSpinbox->setRange(1,20);
85 m_ui.startExportingAtSpinbox->setValue(0.0);
86 m_ui.startExportingAtSpinbox->setRange(0.0, 9999.0);
87 m_ui.startExportingAtSpinbox->setSuffix(i18nc(
"Second as a unit following a value, like 60 s",
" s"));
89 m_ui.videoPreviewSlider->setTickInterval(1);
90 m_ui.videoPreviewSlider->setValue(0);
92 m_ui.exportDurationSpinbox->setValue(3.0);
93 m_ui.exportDurationSpinbox->setSuffix(i18nc(
"Second as a unit following a value, like 60 s",
" s"));
95 m_ui.lblWarning->hide();
99 m_ui.cmbDocumentHandler->addItem(i18nc(
"Import video to New Document",
"New Document"),
"0");
102 m_ui.cmbDocumentHandler->addItem(i18nc(
"Import video to Current Document",
"Current Document"),
"1");
103 m_ui.cmbDocumentHandler->setCurrentIndex(1);
104 m_ui.fpsDocumentLabel->setText(i18nc(
"Video importer: fps of the document you're importing into"
105 ,
"<small>Document:\n %1 FPS</small>"
110 m_ui.documentWidthSpinbox->setValue(0);
111 m_ui.documentHeightSpinbox->setValue(0);
112 m_ui.documentWidthSpinbox->setRange(1,100000);
113 m_ui.documentHeightSpinbox->setRange(1,100000);
115 m_ui.videoWidthSpinbox->setValue(0);
116 m_ui.videoHeightSpinbox->setValue(0);
117 m_ui.videoWidthSpinbox->setRange(1,100000);
118 m_ui.videoHeightSpinbox->setRange(1,100000);
120 m_ui.sensitivitySpinbox->setValue(50.0f);
122 m_ui.cmbVideoScaleFilter->addItem(i18n(
"Bicubic"),
"bicubic");
123 m_ui.cmbVideoScaleFilter->addItem(i18n(
"Bilinear"),
"bilinear");
124 m_ui.cmbVideoScaleFilter->addItem(i18n(
"Lanczos3"),
"lanczos");
125 m_ui.cmbVideoScaleFilter->addItem(i18n(
"Nearest Neighbor"),
"neighbor");
126 m_ui.cmbVideoScaleFilter->addItem(i18nc(
"An interpolation method",
"Spline"),
"spline");
128 m_ui.tabWidget->setCurrentIndex(0);
171 if ( !directory.mkpath(
".") ) {
172 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"Failed to create a work directory, make sure you have write permission"));
177 const float exportDuration =
m_ui.exportDurationSpinbox->value();
178 const float fps =
m_ui.fpsSpinbox->value();
179 const float nDuplicateSensitivity =
m_ui.sensitivitySpinbox->value() /
m_ui.sensitivitySpinbox->maximum();
181 if (exportDuration / fps > 100.0) {
182 if (QMessageBox::warning(
this, i18nc(
"Title for a messagebox",
"Krita"),
183 i18n(
"Warning: you are trying to import more than 100 frames into Krita.\n\n"
184 "This means you might be overloading your system.\n"
185 "If you want to edit a clip larger than 100 frames, consider using a real video editor, like Kdenlive (https://kdenlive.org)."),
186 QMessageBox::Ok | QMessageBox::Cancel,
187 QMessageBox::Cancel) == QMessageBox::Cancel) {
193 args <<
"-ss" << QString::number(
m_ui.startExportingAtSpinbox->value())
196 const float sceneFiltrationFilterThreshold =
rerange( 1 - nDuplicateSensitivity, 0.0f, 1.0f, 0.0005f, 0.2f);
198 if (
m_ui.optionFilterDuplicates->isChecked()) {
199 args <<
"-filter:v" << QString(
"select=gt(scene\\,%1)+eq(n\\,0)").arg(sceneFiltrationFilterThreshold)
203 args <<
"-t" << QString::number(exportDuration)
204 <<
"-r" << QString::number(fps);
207 args <<
"-vf" << QString(
"scale=w=")
208 .append(QString::number(
m_ui.videoWidthSpinbox->value()))
210 .append(QString::number(
m_ui.videoHeightSpinbox->value()))
212 .append(
m_ui.cmbVideoScaleFilter->currentData().toString());
215 QJsonObject ffmpegInfo =
m_ui.cmbFFMpegLocation->currentData().toJsonObject();
216 QJsonObject ffprobeInfo =
m_ui.cmbFFProbeLocation->currentData().toJsonObject();
220 config->setProperty(
"ffmpeg_path", ffmpegInfo[
"path"].toString());
221 config->setProperty(
"ffprobe_path", ffprobeInfo[
"path"].toString());
228 ffmpegSettings.
processPath = ffmpegInfo[
"path"].toString();
229 ffmpegSettings.
args = args;
230 ffmpegSettings.
outputFile = directory.filePath(
"output_%04d.png");
231 ffmpegSettings.
logPath = QDir::tempPath() + QDir::separator() +
"krita" + QDir::separator() +
"ffmpeg.log";
232 ffmpegSettings.
totalFrames = qCeil(exportDuration * fps);
233 ffmpegSettings.
progressMessage = i18nc(
"FFMPEG animated video import message. arg1: frame progress number. arg2: file suffix."
234 ,
"Extracted %1 frames from %2 video.",
"[progress]",
"[suffix]");
238 ffmpeg->startNonBlocking(ffmpegSettings);
240 bool ffmpegSuccess = ffmpeg->waitForFinished();
241 if (!ffmpegSuccess) {
245 frameFileList = directory.entryList(
QStringList() <<
"output_*.png",QDir::Files);
246 frameFileList.replaceInStrings(
"output_", directory.absolutePath() + QDir::separator() +
"output_");
248 dbgFile <<
"Import frames list:" << frameFileList;
251 if (
m_ui.optionFilterDuplicates->isChecked()){
254 ffmpegSettings.
processPath = ffprobeInfo[
"path"].toString();
256 QString filter =
"movie=" +
m_videoInfo.
file + QString(
",setpts=N+1,select=gt(scene\\,%1)").arg(sceneFiltrationFilterThreshold);
258 <<
"-show_entries" <<
"frame=pkt_pts"
259 <<
"-of" <<
"compact=p=0:nk=1"
260 <<
"-f" <<
"lavfi" << filter;
266 QString out = QString(arr);
268 QStringList integerOuts = out.split(
"\n", Qt::SkipEmptyParts);
269 Q_FOREACH(const QString& str, integerOuts){
271 const int value = str.toUInt(&ok);
273 frameTimeList.push_back(value);
278 ffmpeg->startNonBlocking(ffmpegSettings);
280 bool ffmpegSuccess = ffmpeg->waitForFinished();
281 if (!ffmpegSuccess) {
288 if ( frameFileList.isEmpty() ) {
289 QMessageBox::critical(
this, i18nc(
"@title:window",
"Krita"), i18n(
"Failed to export frames from video"));
369 const QFileInfo resultFileInfo(filename);
370 const QDir videoDir(resultFileInfo.absolutePath());
378 textInfo.append(i18nc(
"video importer: video file statistics",
"Width: %1 px", QString::number(
m_videoInfo.
width)));
379 textInfo.append(i18nc(
"video importer: video file statistics",
"Height: %1 px", QString::number(
m_videoInfo.
height)));
382 textInfo.append(i18nc(
"video importer: video file statistics"
383 ,
"Color Primaries: %1"
385 textInfo.append(i18nc(
"video importer: video file statistics"
386 ,
"Color Transfer: %1"
389 textInfo.append(i18nc(
"video importer: video file statistics",
"Duration: %1 s", QString::number(
m_videoInfo.
duration,
'f', 2)));
390 textInfo.append(i18nc(
"video importer: video file statistics",
"Frames: %1", QString::number(
m_videoInfo.
frames)));
391 textInfo.append(i18nc(
"video importer: video file statistics",
"FPS: %1", QString::number(
m_videoInfo.
fps)));
395 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>"));
399 m_ui.fileLoadedDetails->setText(textInfo.join(
"\n"));
404 m_ui.exportDurationSpinbox->setRange(0, 9999.0);
406 if (
m_ui.cmbDocumentHandler->currentIndex() == 0) {
434 args <<
"-ss" << QString::number(currentDuration)
438 <<
"-vcodec" <<
"mjpeg"
439 <<
"-f" <<
"image2pipe"
444 QJsonObject ffmpegInfo =
m_ui.cmbFFMpegLocation->currentData().toJsonObject();
447 if ( byteImage.isEmpty() ) {
450 QPixmap thumbnailPixmap;
451 thumbnailPixmap.loadFromData(byteImage,
"JFIF");
453 m_ui.thumbnailImageHolder->clear();
454 const QSize previewSize =
455 m_ui.thumbnailImageHolder->contentsRect().size() *
m_ui.thumbnailImageHolder->devicePixelRatioF();
456 QPixmap img = thumbnailPixmap.scaled(previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
457 img.setDevicePixelRatio(
m_ui.thumbnailImageHolder->devicePixelRatioF());
458 m_ui.thumbnailImageHolder->setPixmap(img);
473 const KFormat format;
476 quint32 pixelSize = 4;
482 const qint64 frames = std::lround(qreal(
m_videoInfo.
fps) * time + 2);
490 const QString text_frames = i18nc(
"part of warning in video importer."
491 ,
"<b>Warning:</b> you are trying to import %1 frames, the maximum amount you can import is %2."
497 const QString text_video_editor = i18nc(
"part of warning in video importer.",
498 "Use a <a href=\"https://kdenlive.org\">video editor</a> instead!");
500 if (maxFrames < frames) {
501 warnings.append(text_frames);
502 text_memory = i18nc(
"part of warning in video importer."
503 ,
"You do not have enough memory to load this many frames, the computer will be overloaded.");
504 warnings.insert(0,
"<span style=\"color:#ff692e;\">");
505 warnings.append(text_memory);
506 warnings.append(text_video_editor);
507 m_ui.lblWarning->setVisible(
true);
508 }
else if (maxFrames < frames * 2) {
509 warnings.append(text_frames);
510 text_memory = i18nc(
"part of warning in video importer."
511 ,
"This will take over half the available memory, editing will be difficult.");
512 warnings.insert(0,
"<span style=\"color:#ffee00;\">");
513 warnings.append(text_memory);
514 warnings.append(text_video_editor);
515 m_ui.lblWarning->setVisible(
true);
518 warnings.append(text_frames);
519 QString text_trc = i18nc(
"part of warning in video importer."
520 ,
"Krita does not support the video transfer curve (%1), it will be loaded as linear."
522 warnings.append(text_trc);
525 if (warnings.isEmpty()) {
526 m_ui.lblWarning->setVisible(
false);
528 m_ui.lblWarning->setText(warnings.join(
" "));
529 m_ui.lblWarning->setPixmap(
530 m_ui.lblWarning->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(32, 32)));
531 m_ui.lblWarning->setVisible(
true);
654 QJsonObject ffmpegInfo =
m_ui.cmbFFMpegLocation->currentData().toJsonObject();
655 QJsonObject ffprobeInfo =
m_ui.cmbFFProbeLocation->currentData().toJsonObject();
659 QJsonObject ffprobeJsonObj;
661 std::function<
void(
void)> warnFFmpegFormatSupport = [
this](){
662 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"Your FFMpeg version does not support this format"));
665 if (ffprobeInfo[
"enabled"].toBool()) {
666 ffprobeJsonObj = ffprobe->ffprobe(inputFile, ffprobeInfo[
"path"].toString());
673 ffprobeJsonObj = ffmpeg->
ffmpegProbe(inputFile, ffmpegInfo[
"path"].toString(),
false);
675 dbgFile <<
"ffmpeg probe1" << ffprobeJsonObj;
677 QJsonObject ffprobeProgress = ffprobeJsonObj[
"progress"].toObject();
679 videoInfoData.
frames = ffprobeProgress[
"frame"].toString().toInt();
684 warnFFmpegFormatSupport();
688 QJsonObject ffprobeFormat = ffprobeJsonObj[
"format"].toObject();
689 QJsonArray ffprobeStreams = ffprobeJsonObj[
"streams"].toArray();
691 videoInfoData.
file = inputFile;
692 for (
const QJsonValueRef &streamItemRef : ffprobeStreams) {
693 QJsonObject streamItemObj = streamItemRef.toObject();
695 if ( streamItemObj[
"codec_type"].toString() ==
"video" ) {
696 videoInfoData.
stream = streamItemObj[
"index"].toInt();
701 if ( videoInfoData.
stream == -1 ) {
702 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"No video stream could be found!"));
706 const QJsonObject ffprobeSelectedStream = ffprobeStreams[videoInfoData.
stream].toObject();
708 const QJsonObject decoders = ffmpegInfo.value(
"codecs").toObject();
710 const QString codecName = ffprobeSelectedStream[
"codec_name"].toString();
712 const auto decoder = decoders.constFind(codecName);
714 if (decoder == decoders.constEnd() ) {
715 dbgFile <<
"Codec missing or unsupported:" << codecName;
716 warnFFmpegFormatSupport();
718 }
else if (!decoder->toObject().value(
"decoding").toBool()) {
719 dbgFile <<
"Codec not supported for decoding:" << codecName;
720 warnFFmpegFormatSupport();
724 videoInfoData.
width = ffprobeSelectedStream[
"width"].toInt();
725 videoInfoData.
height = ffprobeSelectedStream[
"height"].toInt();
726 videoInfoData.
encoding = ffprobeSelectedStream[
"codec_name"].toString();
731 if (ffprobeSelectedStream.value(
"bits_per_raw_sample").toInt() > 8) {
739 QStringList rawFrameRate = ffprobeSelectedStream[
"r_frame_rate"].toString().split(
'/');
741 if (!rawFrameRate.isEmpty())
742 videoInfoData.
fps = qCeil(rawFrameRate[0].toFloat() / rawFrameRate[1].toFloat());
744 if ( !ffprobeSelectedStream[
"nb_frames"].isNull() ) {
745 videoInfoData.
frames = ffprobeSelectedStream[
"nb_frames"].toString().toInt();
749 if ( !ffprobeSelectedStream[
"duration"].isNull() ) {
750 videoInfoData.
duration = ffprobeSelectedStream[
"duration"].toString().toFloat();
751 }
else if ( !ffprobeFormat[
"duration"].isNull() ) {
752 videoInfoData.
duration = ffprobeFormat[
"duration"].toString().toFloat();
753 }
else if ( videoInfoData.
frames ) {
757 dbgFile <<
"Initial video info from probe: "
758 <<
"stream:" << videoInfoData.
stream
759 <<
"frames:" << videoInfoData.
frames
760 <<
"duration:" << videoInfoData.
duration
761 <<
"fps:" << videoInfoData.
fps
762 <<
"encoding:" << videoInfoData.
encoding;
767 QJsonObject ffmpegJsonObj = ffmpeg->
ffmpegProbe(inputFile, ffmpegInfo[
"path"].toString(),
false);
769 dbgFile <<
"ffmpeg probe2" << ffmpegJsonObj;
772 QMessageBox::warning(
this, i18nc(
"@title:window",
"Krita"), i18n(
"Failed to load video information"));
776 QJsonObject ffmpegProgressJsonObj = ffmpegJsonObj[
"progress"].toObject();
777 float ffmpegFPS = ffmpegProgressJsonObj[
"ffmpeg_fps"].toString().toFloat();
779 videoInfoData.
frames = ffmpegProgressJsonObj[
"frame"].toString().toInt();
781 if (ffmpegFPS > 0 && videoInfoData.
frames) {
783 videoInfoData.
fps = ffmpegFPS;
785 videoInfoData.
duration = ffmpegProgressJsonObj[
"out_time_ms"].toString().toFloat() / 1000000;
797 dbgFile <<
"Final video info from probe: "
798 <<
"stream:" << videoInfoData.
stream
799 <<
"frames:" << videoInfoData.
frames
800 <<
"duration:" << videoInfoData.
duration
801 <<
"fps:" << videoInfoData.
fps
802 <<
"encoding:" << videoInfoData.
encoding;
804 const float calculatedFrameRateByDuration = videoInfoData.
frames / videoInfoData.
duration;
805 const int frameRateDifference = qAbs(videoInfoData.
fps - calculatedFrameRateByDuration);
807 if (frameRateDifference > 1) {
811 videoInfoData.
fps = calculatedFrameRateByDuration;
816 return videoInfoData;