[frameworks/kfilemetadata] /: [FFmpegExtractor] Extract cover image from Matroska streams
Stefan Brüns <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 3543e7154cd0b46b00ccdc50a6ada9f61cfccaff by Stefan Brüns.
Committed on 10/08/2026 at 16:38.
Pushed by bruns into branch 'master'.
[FFmpegExtractor] Extract cover image from Matroska streams
FFmpeg exposes Matroska attachments in streams with an ATTACHED_PIC
disposition.
CCBUG: 500113
M +1 -0 autotests/extractorcoveragetest.cpp
M +49 -0 autotests/ffmpegextractortest.cpp
A +- -- autotests/samplefiles/test_cover.mkv
M +46 -3 src/extractors/ffmpegextractor.cpp
https://invent.kde.org/frameworks/kfilemetadata/-/commit/3543e7154cd0b46b00ccdc50a6ada9f61cfccaff
diff --git a/autotests/extractorcoveragetest.cpp b/autotests/extractorcoveragetest.cpp
index a76c6aad..4420b592 100644
--- a/autotests/extractorcoveragetest.cpp
+++ b/autotests/extractorcoveragetest.cpp
@@ -76,6 +76,7 @@ private Q_SLOTS:
{ "test_missing_content.odt", "application/vnd.oasis.opendocument.text"},
{ "test_missing_meta.odt", "application/vnd.oasis.opendocument.text"},
{ "test.mkv", "video/x-matroska"},
+ { "test_cover.mkv", "video/x-matroska"},
{ "test.mp3", "audio/mpeg"},
{ "test.id3v1.mp3", "audio/mpeg"},
{ "test.aax", "audio/vnd.audible.aax"},
diff --git a/autotests/ffmpegextractortest.cpp b/autotests/ffmpegextractortest.cpp
index 0dc408d7..febc16a8 100644
--- a/autotests/ffmpegextractortest.cpp
+++ b/autotests/ffmpegextractortest.cpp
@@ -14,6 +14,7 @@
#include <QTest>
using namespace KFileMetaData;
+using namespace Qt::StringLiterals;
namespace KFileMetaData {
class ffmpegExtractorTest : public QObject
@@ -21,14 +22,18 @@ class ffmpegExtractorTest : public QObject
Q_OBJECT
private Q_SLOTS:
+ void initTestCase();
void testNoExtraction();
void testVideoProperties();
void testVideoProperties_data();
void testMetaData();
void testMetaData_data();
+ void testCoverimage();
+ void testCoverimage_data();
private:
QMimeDatabase mimeDb;
+ QByteArray m_coverImage;
};
} // namespace KFileMetaData
@@ -42,6 +47,13 @@ QString testFilePath(const QString& baseName, const QString& extension)
} // namespace
+void ffmpegExtractorTest::initTestCase()
+{
+ QFile imgFile(testFilePath(u"cover"_s, u"jpg"_s));
+ QVERIFY(imgFile.open(QIODevice::ReadOnly));
+ m_coverImage = imgFile.readAll();
+}
+
void ffmpegExtractorTest::testNoExtraction()
{
QString fileName = testFilePath(QStringLiteral("test"), QStringLiteral("webm"));
@@ -166,6 +178,43 @@ void ffmpegExtractorTest::testMetaData()
QCOMPARE(result.properties().value(Property::ReleaseYear).toInt(), 2019);
}
+void ffmpegExtractorTest::testCoverimage_data()
+{
+ using IT = EmbeddedImageData::ImageType;
+ QTest::addColumn<QString>("baseName");
+ QTest::addColumn<QString>("fileType");
+ QTest::addColumn<QList<IT>>("coverTypes");
+
+ // clang-format off
+ QTest::addRow("Matroska Video") << u"test"_s << u"mkv"_s << QList<IT>();
+ QTest::addRow("Matroska Video with cover") << u"test_cover"_s << u"mkv"_s << QList<IT>{IT::FrontCover};
+ // clang-format on
+}
+
+void ffmpegExtractorTest::testCoverimage()
+{
+ QFETCH(QString, baseName);
+ QFETCH(QString, fileType);
+ QFETCH(QList<EmbeddedImageData::ImageType>, coverTypes);
+
+ const QString fileName = testFilePath(baseName, fileType);
+ const QString mimeType = MimeUtils::strictMimeType(fileName, mimeDb).name();
+
+ FFmpegExtractor plugin{this};
+
+ SimpleExtractionResult result(fileName, mimeType, ExtractionResult::ExtractImageData);
+ plugin.extract(&result);
+
+ const auto imageMap = result.imageData();
+ QCOMPARE(imageMap.size(), coverTypes.size());
+ for (const auto type : coverTypes) {
+ QVERIFY(imageMap.contains(type));
+ }
+ for (const auto [type, imageData]: imageMap.asKeyValueRange()) {
+ QCOMPARE(imageData, m_coverImage);
+ }
+}
+
QTEST_GUILESS_MAIN(ffmpegExtractorTest)
#include "ffmpegextractortest.moc"
diff --git a/autotests/samplefiles/test_cover.mkv b/autotests/samplefiles/test_cover.mkv
new file mode 100644
index 00000000..71577d76
Binary files /dev/null and b/autotests/samplefiles/test_cover.mkv differ
diff --git a/src/extractors/ffmpegextractor.cpp b/src/extractors/ffmpegextractor.cpp
index 712d6724..d80d15d4 100644
--- a/src/extractors/ffmpegextractor.cpp
+++ b/src/extractors/ffmpegextractor.cpp
@@ -10,6 +10,7 @@
#include "ffmpegextractor.h"
+#include "embeddedimagedata.h"
#include "kfilemetadata_debug.h"
#ifdef __cplusplus
@@ -30,6 +31,36 @@ extern "C" {
using namespace KFileMetaData;
+namespace {
+QMap<EmbeddedImageData::ImageType, QByteArray>
+extractCover(const AVStream* stream)
+{
+ if (const auto e = av_dict_get(stream->metadata, "filename", nullptr, 0)) {
+ const std::string_view value{e->value};
+ if (!(value.starts_with("cover") || value.starts_with("small_cover"))) {
+ qCDebug(KFILEMETADATA_LOG) << "Ignore attached" << value;
+ return {};
+ }
+ if (!(value.ends_with(".png") || value.ends_with(".jpg") || value.ends_with(".jpeg"))) {
+ qCDebug(KFILEMETADATA_LOG) << "Ignore attached" << value;
+ return {};
+ }
+ }
+ if (const auto e = av_dict_get(stream->metadata, "mimetype", nullptr, 0)) {
+ const std::string_view value{e->value};
+ if (!value.starts_with("image/")) {
+ qCDebug(KFILEMETADATA_LOG) << "Ignore attached file with type" << value;
+ return {};
+ }
+ }
+ const auto &ap = stream->attached_pic;
+ if (!(ap.data && ap.size)) {
+ return {};
+ }
+ return {std::pair{EmbeddedImageData::FrontCover, QByteArray{QByteArrayView(ap.data, ap.size)}}};
+}
+} // namespace <anonymous>
+
FFmpegExtractor::FFmpegExtractor(QObject* parent)
: ExtractorPlugin(parent)
{
@@ -88,7 +119,7 @@ void FFmpegExtractor::extract(ExtractionResult* result)
result->addType(Type::Video);
- if (result->inputFlags() & ExtractionResult::ExtractMetaData) {
+ if (result->inputFlags() & (ExtractionResult::ExtractMetaData | ExtractionResult::ExtractImageData)) {
int totalSecs = fmt_ctx->duration / AV_TIME_BASE;
int bitrate = fmt_ctx->bit_rate;
@@ -96,12 +127,24 @@ void FFmpegExtractor::extract(ExtractionResult* result)
result->add(Property::BitRate, bitrate);
const int index_stream = av_find_default_stream_index(fmt_ctx);
- if (index_stream >= 0) {
- AVStream* stream = fmt_ctx->streams[index_stream];
+
+ for (unsigned int index = 0; index < fmt_ctx->nb_streams; index++) {
+ AVStream* stream = fmt_ctx->streams[index];
const AVCodecParameters* codec = stream->codecpar;
if (codec->codec_type == AVMEDIA_TYPE_VIDEO) {
+ if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC) {
+ if (auto images = extractCover(stream); !images.isEmpty()) {
+ result->addImageData(std::move(images));
+ }
+ continue;
+ } else if (index_stream < 0) {
+ continue;
+ } else if (unsigned int t = index_stream; index != t) {
+ continue;
+ }
+
result->add(Property::Width, codec->width);
result->add(Property::Height, codec->height);