diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 2e7b5fd..6c74ae3 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,172 +1,176 @@ find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test) find_package(PythonInterp) set_package_properties(PythonInterp PROPERTIES DESCRIPTION "Python Interpreter" URL "https://www.python.org" TYPE OPTIONAL PURPOSE "Python interpreter is needed to execute test for external extractors or writers") remove_definitions(-DQT_NO_CAST_FROM_ASCII) configure_file(indexerextractortestsconfig.h.in ${CMAKE_CURRENT_BINARY_DIR}/indexerextractortestsconfig.h @ONLY) set(KfileMetaDataAutotest_SRCS) ecm_qt_declare_logging_category(KfileMetaDataAutotest_SRCS HEADER kfilemetadata_debug.h IDENTIFIER KFILEMETADATA_LOG CATEGORY_NAME kf5.kfilemetadata) set(indexerextractor_SRCS indexerextractortests.cpp ../src/extractors/plaintextextractor.cpp ) ecm_add_test(${indexerextractor_SRCS} TEST_NAME "indexextractortest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData ) # # Office # if(KF5Archive_FOUND) ecm_add_test(odfextractortest.cpp ../src/extractors/odfextractor.cpp TEST_NAME "odfextractortest" LINK_LIBRARIES Qt5::Test Qt5::Xml KF5::FileMetaData KF5::Archive ) endif() if(KF5Archive_FOUND) ecm_add_test(office2007extractortest.cpp ../src/extractors/office2007extractor.cpp TEST_NAME "officeextractortest" LINK_LIBRARIES Qt5::Test Qt5::Xml KF5::FileMetaData KF5::Archive ) endif() # # Poppler # if(Poppler_Qt5_FOUND) ecm_add_test(popplerextractortest.cpp ../src/extractors/popplerextractor.cpp TEST_NAME "popplerextractortest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData Poppler::Qt5 ) endif() # # EPub # if(EPUB_FOUND) include_directories(${EPUB_INCLUDE_DIR}) ecm_add_test(epubextractortest.cpp ../src/extractors/epubextractor.cpp TEST_NAME "epubextractortest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData ${EPUB_LIBRARIES} ) endif() # # Mobi # if (QMOBIPOCKET_FOUND) include_directories(${QMOBIPOCKET_INCLUDE_DIR}) ecm_add_test(mobiextractortest.cpp ../src/extractors/mobiextractor.cpp TEST_NAME "mobiextractortest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData ${QMOBIPOCKET_LIBRARIES} ) endif() # # Property Info # ecm_add_test(propertyinfotest.cpp TEST_NAME "propertyinfotest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData ) # # Exiv2 # if(EXIV2_FOUND) include_directories(${EXIV2_INCLUDE_DIR}) kde_enable_exceptions() ecm_add_test(exiv2extractortest.cpp ../src/extractors/exiv2extractor.cpp TEST_NAME "exiv2extractortest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData ${EXIV2_LIBRARIES} ) endif() # # TagLib # if(TAGLIB_FOUND) include_directories(${TAGLIB_INCLUDES}) kde_enable_exceptions() ecm_add_test(taglibextractortest.cpp ../src/extractors/taglibextractor.cpp TEST_NAME "taglibextractortest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData ${TAGLIB_LIBRARIES} ) + ecm_add_test(embeddedimagedatatest.cpp ../src/embeddedimagedata.cpp + TEST_NAME "embeddedimagedatatest" + LINK_LIBRARIES Qt5::Test KF5::FileMetaData ${TAGLIB_LIBRARIES} + ) endif() if(PYTHONINTERP_FOUND) configure_file(samplefiles/testexternalextractor/main.py samplefiles/testexternalextractor/main.py) configure_file(samplefiles/testexternalextractor/manifest.json samplefiles/testexternalextractor/manifest.json COPYONLY) ecm_add_test(externalextractortest.cpp ../src/externalextractor.cpp ${KfileMetaDataAutotest_SRCS} TEST_NAME "externalextractortest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData KF5::I18n ) endif() # # Collection # set(extractorcollection_SRCS extractorcollectiontest.cpp ) ecm_add_test(${extractorcollection_SRCS} TEST_NAME "extractorcollectiontest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData ) ################ # Writer tests # ################ # # UserMetaData # if(CMAKE_SYSTEM_NAME MATCHES "Linux") kde_enable_exceptions() ecm_add_test(usermetadatawritertest.cpp ../src/usermetadata.cpp TEST_NAME "usermetadatawritertest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData ) endif() # # TagLib # if(TAGLIB_FOUND) include_directories(${TAGLIB_INCLUDES}) kde_enable_exceptions() ecm_add_test(taglibwritertest.cpp ../src/writers/taglibwriter.cpp TEST_NAME "taglibwritertest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData ${TAGLIB_LIBRARIES} ) endif() if(PYTHONINTERP_FOUND) configure_file(samplefiles/testexternalwriter/main.py samplefiles/testexternalwriter/main.py) configure_file(samplefiles/testexternalwriter/manifest.json samplefiles/testexternalwriter/manifest.json COPYONLY) ecm_add_test(externalwritertest.cpp ../src/externalwriter.cpp ${KfileMetaDataAutotest_SRCS} TEST_NAME "externalwritertest" LINK_LIBRARIES Qt5::Test KF5::FileMetaData KF5::I18n ) endif() diff --git a/autotests/embeddedimagedatatest.cpp b/autotests/embeddedimagedatatest.cpp new file mode 100644 index 0000000..df1e73b --- /dev/null +++ b/autotests/embeddedimagedatatest.cpp @@ -0,0 +1,63 @@ +/* + * EmbeddedImageData tests. + * + * Copyright (C) 2018 Alexander Stippich + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "embeddedimagedatatest.h" +#include "embeddedimagedata.h" +#include "indexerextractortestsconfig.h" + +#include + +using namespace KFileMetaData; + +QString EmbeddedImageDataTest::testFilePath(const QString& fileName) const +{ + return QLatin1String(INDEXER_TESTS_SAMPLE_FILES_PATH) + QDir::separator() + fileName; +} + +void EmbeddedImageDataTest::test() +{ + QMap images; + QByteArray originalFrontCoverImage; + QFile testFile(testFilePath("test.jpg")); + testFile.open(QIODevice::ReadOnly); + originalFrontCoverImage = testFile.readAll(); + + EmbeddedImageData imageData; + images = imageData.imageData(testFilePath("test.opus")); + QCOMPARE(images.value(EmbeddedImageData::FrontCover), originalFrontCoverImage); + + images = imageData.imageData(testFilePath("test.ogg")); + QCOMPARE(images.value(EmbeddedImageData::FrontCover), originalFrontCoverImage); + + images = imageData.imageData(testFilePath("test.flac")); + QCOMPARE(images.value(EmbeddedImageData::FrontCover), originalFrontCoverImage); + + images = imageData.imageData(testFilePath("test.mp3")); + QCOMPARE(images.value(EmbeddedImageData::FrontCover), originalFrontCoverImage); + + images = imageData.imageData(testFilePath("test.m4a")); + QCOMPARE(images.value(EmbeddedImageData::FrontCover), originalFrontCoverImage); + + images = imageData.imageData(testFilePath("test.mpc")); + QCOMPARE(images.value(EmbeddedImageData::FrontCover), originalFrontCoverImage); +} + +QTEST_GUILESS_MAIN(EmbeddedImageDataTest) diff --git a/autotests/embeddedimagedatatest.h b/autotests/embeddedimagedatatest.h new file mode 100644 index 0000000..f0e45d9 --- /dev/null +++ b/autotests/embeddedimagedatatest.h @@ -0,0 +1,38 @@ +/* + * EmbeddedImageData tests. + * + * Copyright (C) 2018 Alexander Stippich + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef EMBEDDEDIMAGEDATATEST_H +#define EMBEDDEDIMAGEDATATEST_H + +#include +#include "properties.h" + +class EmbeddedImageDataTest : public QObject +{ + Q_OBJECT +private: + QString testFilePath(const QString& fileName) const; + +private Q_SLOTS: + void test(); +}; + +#endif // EMBEDDEDIMAGEDATATEST_H diff --git a/autotests/samplefiles/test.mpc b/autotests/samplefiles/test.mpc index a7a4d46..221b1b9 100644 Binary files a/autotests/samplefiles/test.mpc and b/autotests/samplefiles/test.mpc differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e69ad73..70cf2df 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,118 +1,135 @@ set(KF5FileMetaData_SRCS extractionresult.cpp simpleextractionresult.cpp extractor.cpp extractorplugin.cpp extractorcollection.cpp externalextractor.cpp propertyinfo.cpp typeinfo.cpp usermetadata.cpp writedata.cpp writer.cpp writerplugin.cpp writercollection.cpp externalwriter.cpp ) ecm_qt_declare_logging_category(KF5FileMetaData_SRCS HEADER kfilemetadata_debug.h IDENTIFIER KFILEMETADATA_LOG CATEGORY_NAME kf5.kfilemetadata) +if(TAGLIB_FOUND) + set(KF5FileMetaData_SRCS + ${KF5FileMetaData_SRCS} + embeddedimagedata.cpp + ) +endif() + add_library(KF5FileMetaData ${KF5FileMetaData_SRCS} ) add_library(KF5::FileMetaData ALIAS KF5FileMetaData) target_link_libraries(KF5FileMetaData PUBLIC Qt5::Core PRIVATE KF5::I18n ) +if(TAGLIB_FOUND) + include_directories(${TAGLIB_INCLUDES}) + + target_link_libraries(KF5FileMetaData + PUBLIC + ${TAGLIB_LIBRARIES} + ) +endif() + generate_export_header(KF5FileMetaData BASE_NAME KFileMetaData EXPORT_FILE_NAME kfilemetadata_export.h) set_target_properties(KF5FileMetaData PROPERTIES VERSION ${KFILEMETADATA_VERSION_STRING} SOVERSION ${KFILEMETADATA_SOVERSION} EXPORT_NAME FileMetaData ) target_include_directories(KF5FileMetaData INTERFACE "$") ecm_generate_headers(KF5FileMetaData_CamelCase_HEADERS HEADER_NAMES ExtractionResult SimpleExtractionResult Extractor ExtractorPlugin ExtractorCollection Properties PropertyInfo Types TypeInfo UserMetaData WriteData Writer WriterPlugin WriterCollection + EmbeddedImageData PREFIX kfilemetadata REQUIRED_HEADERS KF5FileMetaData_HEADERS ) install(TARGETS KF5FileMetaData EXPORT KF5FileMetaDataTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(EXPORT KF5FileMetaDataTargets NAMESPACE KF5:: DESTINATION ${LIB_INSTALL_DIR}/cmake/KF5FileMetaData FILE KF5FileMetaDataTargets.cmake) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kfilemetadata_export.h ${KF5FileMetaData_HEADERS} DESTINATION ${KF5_INCLUDE_INSTALL_DIR}/KFileMetaData/kfilemetadata COMPONENT Devel ) install(FILES ${KF5FileMetaData_CamelCase_HEADERS} DESTINATION ${KF5_INCLUDE_INSTALL_DIR}/KFileMetaData/KFileMetaData COMPONENT Devel ) configure_file(config-kfilemetadata.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-kfilemetadata.h) if(BUILD_QCH) ecm_add_qch( KF5FileMetaData_QCH NAME KFileMetaData BASE_NAME KF5FileMetaData VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${KF5FileMetaData_HEADERS} MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" LINK_QCHS Qt5Core_QCH BLANK_MACROS KFILEMETADATA_EXPORT KFILEMETADATA_DEPRECATED KFILEMETADATA_DEPRECATED_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() include(ECMGeneratePriFile) ecm_generate_pri_file( BASE_NAME KFileMetaData LIB_NAME KF5FileMetaData DEPS "core" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KF5_INCLUDE_INSTALL_DIR}/KFileMetaData ) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) add_subdirectory(extractors) add_subdirectory(writers) diff --git a/src/embeddedimagedata.cpp b/src/embeddedimagedata.cpp new file mode 100644 index 0000000..5f32051 --- /dev/null +++ b/src/embeddedimagedata.cpp @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2018 Alexander Stippich + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "embeddedimagedata.h" +// Taglib includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace KFileMetaData; + +class Q_DECL_HIDDEN EmbeddedImageData::Private +{ +public: + QMimeDatabase mMimeDatabase; + QByteArray getFrontCover(const QString &fileUrl, const QString &mimeType) const; + +}; + +EmbeddedImageData::EmbeddedImageData() + : d(std::unique_ptr(new Private())) +{ +} + +EmbeddedImageData::~EmbeddedImageData() += default; + +QMap +EmbeddedImageData::imageData(const QString &fileUrl, + const EmbeddedImageData::ImageTypes types) const +{ + QMap imageData; + + const auto &fileMimeType = d->mMimeDatabase.mimeTypeForFile(fileUrl); + if (fileMimeType.name().startsWith(QStringLiteral("audio/"))) { + if (types & EmbeddedImageData::FrontCover) { + imageData.insert(EmbeddedImageData::FrontCover, d->getFrontCover(fileUrl, fileMimeType.name())); + } + } + + return imageData; +} + +QByteArray +EmbeddedImageData::Private::getFrontCover(const QString &fileUrl, + const QString &mimeType) const +{ + TagLib::FileStream stream(fileUrl.toUtf8().constData(), true); + if (!stream.isOpen()) { + qWarning() << "Unable to open file readonly: " << fileUrl; + return QByteArray(); + } + if ((mimeType == QStringLiteral("audio/mpeg")) + || (mimeType == QStringLiteral("audio/mpeg3")) + || (mimeType == QStringLiteral("audio/x-mpeg"))) { + + // Handling multiple tags in mpeg files. + TagLib::MPEG::File mpegFile(&stream, TagLib::ID3v2::FrameFactory::instance(), true); + if (!mpegFile.ID3v2Tag() || mpegFile.ID3v2Tag()->isEmpty()) { + return QByteArray(); + } + + TagLib::ID3v2::FrameList lstID3v2; + // Attached Front Picture. + lstID3v2 = mpegFile.ID3v2Tag()->frameListMap()["APIC"]; + if (!lstID3v2.isEmpty()) { + for (TagLib::ID3v2::FrameList::ConstIterator it = lstID3v2.begin(); it != lstID3v2.end(); ++it) { + auto *frontCoverFrame = static_cast(*it); + if (frontCoverFrame->type() == frontCoverFrame->FrontCover) { + return QByteArray(frontCoverFrame->picture().data(), frontCoverFrame->picture().size()); + } + } + } + + } else if (mimeType == QStringLiteral("audio/mp4")) { + + TagLib::MP4::File mp4File(&stream, true); + if (!mp4File.tag() || mp4File.tag()->isEmpty()) { + return QByteArray(); + } + TagLib::MP4::Item coverArtItem = mp4File.tag()->item("covr"); + if (coverArtItem.isValid()) + { + TagLib::MP4::CoverArtList coverArtList = coverArtItem.toCoverArtList(); + TagLib::MP4::CoverArt& frontCover = coverArtList.front(); + return QByteArray(frontCover.data().data(), frontCover.data().size()); + } + + } else if (mimeType == QStringLiteral("audio/x-musepack")) { + + TagLib::MPC::File mpcFile(&stream, true); + if (!mpcFile.tag() || mpcFile.tag()->isEmpty()) { + return QByteArray(); + } + + TagLib::APE::ItemListMap lstMusepack = mpcFile.APETag()->itemListMap(); + TagLib::APE::ItemListMap::ConstIterator itMPC; + + /* The cover art tag for APEv2 tags starts with the filename as string + * with zero termination followed by the actual picture data */ + itMPC = lstMusepack.find("COVER ART (FRONT)"); + if (itMPC != lstMusepack.end()) { + TagLib::ByteVector pictureData = (*itMPC).second.binaryData(); + int dataPosition = pictureData.find('\0') + 1; + return QByteArray(pictureData.data() + dataPosition, pictureData.size() - dataPosition); + } + + } else if (mimeType == QStringLiteral("audio/flac")) { + + TagLib::FLAC::File flacFile(&stream, TagLib::ID3v2::FrameFactory::instance(), true); + TagLib::List lstPic = flacFile.pictureList(); + + if (!lstPic.isEmpty()) { + for (TagLib::List::Iterator it = lstPic.begin(); it != lstPic.end(); ++it) { + TagLib::FLAC::Picture *picture = *it; + if (picture->type() == picture->FrontCover) { + return QByteArray(picture->data().data(), picture->data().size()); + } + } + } + + } else { + + TagLib::List lstPic; + if (mimeType == QStringLiteral("audio/ogg") || mimeType == QStringLiteral("audio/x-vorbis+ogg")) { + TagLib::Ogg::Vorbis::File oggFile(&stream, true); + if (oggFile.tag() && !oggFile.tag()->isEmpty()) { + lstPic = oggFile.tag()->pictureList(); + } + } + if (mimeType == QStringLiteral("audio/opus") || mimeType == QStringLiteral("audio/x-opus+ogg")) { + TagLib::Ogg::Opus::File opusFile(&stream, true); + if (opusFile.tag() && !opusFile.tag()->isEmpty()) { + lstPic = opusFile.tag()->pictureList(); + } + } + if (!lstPic.isEmpty()) { + for (TagLib::List::Iterator it = lstPic.begin(); it != lstPic.end(); ++it) { + TagLib::FLAC::Picture *picture = *it; + if (picture->type() == picture->FrontCover) { + return QByteArray(picture->data().data(), picture->data().size()); + } + } + } + } + return QByteArray(); +} diff --git a/src/embeddedimagedata.h b/src/embeddedimagedata.h new file mode 100644 index 0000000..5ca142c --- /dev/null +++ b/src/embeddedimagedata.h @@ -0,0 +1,65 @@ +/* + * EmbeddedImageData extracts binary data of cover art files. + * Copyright (C) 2018 Alexander Stippich + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef KFILEMETADATA_EMBEDDEDIMAGEDATA_H +#define KFILEMETADATA_EMBEDDEDIMAGEDATA_H + +#include "kfilemetadata_export.h" +#include +#include +#include + +namespace KFileMetaData { + +/** + * \class EmbeddedImageData embeddedimagedata.h + * + * \brief The EmbeddedImageData is a class which extracts the images stored + * in the metadata tags of files as byte arrays. For example, the front cover + * art of music albums may be extracted with this class. The byte array will + * mostly contain jpeg or png files, which can be loaded into e.g. QImage. + * + * \author Alexander Stippich + */ +class KFILEMETADATA_EXPORT EmbeddedImageData { +public: + EmbeddedImageData(); + virtual ~EmbeddedImageData(); + enum ImageType { + FrontCover = 0x01, + AllImages = FrontCover + }; + Q_DECLARE_FLAGS(ImageTypes, ImageType) + + /** + * Extracts the images stored in the metadata tags from a file. + * By default, the front cover is extracted. + */ + QMap imageData(const QString &fileUrl, const EmbeddedImageData::ImageTypes types = FrontCover) const; + +private: + class Private; + std::unique_ptr d; + EmbeddedImageData& operator=(const EmbeddedImageData&); +}; + +} + +#endif // KFILEMETADATA_EMBEDDEDIMAGEDATA_H