diff --git a/approot/images/unknown-cover.jpg b/approot/images/unknown-cover.jpg deleted file mode 100755 index 3adc6dc1..00000000 Binary files a/approot/images/unknown-cover.jpg and /dev/null differ diff --git a/approot/images/unknown-cover.svg b/approot/images/unknown-cover.svg new file mode 100755 index 00000000..52ff34f6 --- /dev/null +++ b/approot/images/unknown-cover.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/libs/image/CMakeLists.txt b/src/libs/image/CMakeLists.txt index 32391b0f..f6228210 100644 --- a/src/libs/image/CMakeLists.txt +++ b/src/libs/image/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(lmsimage SHARED + impl/SvgImage.cpp ) target_include_directories(lmsimage INTERFACE diff --git a/src/libs/image/impl/SvgImage.cpp b/src/libs/image/impl/SvgImage.cpp new file mode 100644 index 00000000..433c58ad --- /dev/null +++ b/src/libs/image/impl/SvgImage.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "SvgImage.hpp" + +#include +#include "image/Exception.hpp" + +namespace lms::image +{ + std::unique_ptr readSvgFile(const std::filesystem::path& p) + { + if (p.extension() != ".svg") + throw Exception{ "Unexpected file extension: '" + p.extension().string() + "', expected .svg" }; + + std::ifstream ifs{ p.string(), std::ios::binary }; + if (!ifs.is_open()) + throw Exception{ "Cannot open file '" + p.string() + "' for reading purpose" }; + + std::vector data; + // read file content + ifs.seekg(0, std::ios::end); + std::streamsize size = ifs.tellg(); + if (size < 0) + throw Exception{ "Cannot determine file size for '" + p.string() + "'" }; + + ifs.seekg(0, std::ios::beg); + data.resize(size); + if (!ifs.read(reinterpret_cast(data.data()), size)) + throw Exception{ "Cannot read file content for '" + p.string() + "'" }; + + return std::make_unique(std::move(data)); + } +} \ No newline at end of file diff --git a/src/libs/image/impl/SvgImage.hpp b/src/libs/image/impl/SvgImage.hpp new file mode 100644 index 00000000..ce54cb30 --- /dev/null +++ b/src/libs/image/impl/SvgImage.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "image/IEncodedImage.hpp" + +namespace lms::image +{ + class SvgImage : public IEncodedImage + { + public: + SvgImage(std::vector&& data) : _data{ std::move(data) } {} + + const std::byte* getData() const { return &_data.front(); } + std::size_t getDataSize() const { return _data.size(); } + std::string_view getMimeType() const { return "image/svg+xml"; } + + private: + const std::vector _data; + }; +} \ No newline at end of file diff --git a/src/libs/image/include/image/Exception.hpp b/src/libs/image/include/image/Exception.hpp index 1991be18..7e0d83ba 100644 --- a/src/libs/image/include/image/Exception.hpp +++ b/src/libs/image/include/image/Exception.hpp @@ -23,11 +23,9 @@ namespace lms::image { - class Exception : public core::LmsException - { - public: - using LmsException::LmsException; - }; - + class Exception : public core::LmsException + { + public: + using LmsException::LmsException; + }; } // namespace lms::cover - diff --git a/src/libs/image/include/image/IEncodedImage.hpp b/src/libs/image/include/image/IEncodedImage.hpp index 0613f214..89685143 100644 --- a/src/libs/image/include/image/IEncodedImage.hpp +++ b/src/libs/image/include/image/IEncodedImage.hpp @@ -24,16 +24,15 @@ namespace lms::image { - using ImageSize = std::size_t; + using ImageSize = std::size_t; - class IEncodedImage - { - public: - virtual ~IEncodedImage() = default; - - virtual const std::byte* getData() const = 0; - virtual std::size_t getDataSize() const = 0; - virtual std::string_view getMimeType() const = 0; - }; -} + class IEncodedImage + { + public: + virtual ~IEncodedImage() = default; + virtual const std::byte* getData() const = 0; + virtual std::size_t getDataSize() const = 0; + virtual std::string_view getMimeType() const = 0; + }; +} \ No newline at end of file diff --git a/src/libs/image/include/image/IRawImage.hpp b/src/libs/image/include/image/IRawImage.hpp index e0949b2c..178da958 100644 --- a/src/libs/image/include/image/IRawImage.hpp +++ b/src/libs/image/include/image/IRawImage.hpp @@ -19,23 +19,16 @@ #pragma once -#include -#include - #include "image/IEncodedImage.hpp" namespace lms::image { - class IRawImage - { - public: - virtual ~IRawImage() = default; - virtual void resize(ImageSize width) = 0; - virtual std::unique_ptr encodeToJPEG(unsigned quality) const = 0; - }; - - void init(const std::filesystem::path& path); - std::unique_ptr decodeImage(const std::byte* encodedData, std::size_t encodedDataSize); - std::unique_ptr decodeImage(const std::filesystem::path& path); + class IRawImage + { + public: + virtual ~IRawImage() = default; + virtual void resize(ImageSize width) = 0; + virtual std::unique_ptr encodeToJPEG(unsigned quality) const = 0; + }; } diff --git a/src/libs/image/include/image/Image.hpp b/src/libs/image/include/image/Image.hpp new file mode 100644 index 00000000..a0ee4182 --- /dev/null +++ b/src/libs/image/include/image/Image.hpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "image/IEncodedImage.hpp" +#include "image/IRawImage.hpp" + +namespace lms::image +{ + void init(const std::filesystem::path& path); + std::unique_ptr decodeImage(const std::byte* encodedData, std::size_t encodedDataSize); + std::unique_ptr decodeImage(const std::filesystem::path& path); + std::unique_ptr readSvgFile(const std::filesystem::path& path); +} \ No newline at end of file diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 49540e48..605e5a92 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -30,7 +30,7 @@ #include "database/Track.hpp" #include "image/Exception.hpp" -#include "image/IRawImage.hpp" +#include "image/Image.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Path.hpp" @@ -107,18 +107,16 @@ namespace lms::cover } } - std::unique_ptr createCoverService(db::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath) + std::unique_ptr createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath) { - return std::make_unique(db, execPath, defaultCoverPath); + return std::make_unique(db, defaultSvgCoverPath); } using namespace image; CoverService::CoverService(db::Db& db, - const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath) + const std::filesystem::path& defaultSvgCoverPath) : _db{ db } - , _defaultCoverPath{ defaultCoverPath } , _maxCacheSize{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } , _maxFileSize{ core::Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 } , _preferredFileNames{ constructPreferredFileNames() } @@ -126,25 +124,12 @@ namespace lms::cover { setJpegQuality(core::Service::get()->getULong("cover-jpeg-quality", 75)); - LMS_LOG(COVER, INFO, "Default cover path = '" << _defaultCoverPath.string() << "'"); + LMS_LOG(COVER, INFO, "Default cover path = '" << defaultSvgCoverPath.string() << "'"); LMS_LOG(COVER, INFO, "Max cache size = " << _maxCacheSize); LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize); LMS_LOG(COVER, INFO, "Preferred file names: " << core::stringUtils::joinStrings(_preferredFileNames, ",")); -#if LMS_SUPPORT_IMAGE_GM - GraphicsMagick::init(execPath); -#else - (void)execPath; -#endif - - try - { - getDefault(512); - } - catch (const image::Exception& e) - { - throw core::LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what()); - } + _defaultCover = image::readSvgFile(defaultSvgCoverPath); // may throw } std::unique_ptr CoverService::getFromAvMediaFile(const av::IAudioFile& input, ImageSize width) const @@ -189,27 +174,9 @@ namespace lms::cover return image; } - std::shared_ptr CoverService::getDefault(ImageSize width) + std::shared_ptr CoverService::getDefaultSvgCover() { - { - std::shared_lock lock{ _cacheMutex }; - - if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache)) - return it->second; - } - - { - std::unique_lock lock{ _cacheMutex }; - - if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache)) - return it->second; - - std::shared_ptr image{ getFromCoverFile(_defaultCoverPath, width) }; - _defaultCoverCache[width] = image; - LMS_LOG(COVER, DEBUG, "Default cache entries = " << _defaultCoverCache.size()); - - return image; - } + return _defaultCover; } std::unique_ptr CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width, const std::vector& preferredFileNames, bool allowPickRandom) const diff --git a/src/libs/services/cover/impl/CoverService.hpp b/src/libs/services/cover/impl/CoverService.hpp index 30f24bad..40fffe4c 100644 --- a/src/libs/services/cover/impl/CoverService.hpp +++ b/src/libs/services/cover/impl/CoverService.hpp @@ -84,16 +84,16 @@ namespace lms::cover class CoverService : public ICoverService { public: - CoverService(db::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); + CoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath); + private: CoverService(const CoverService&) = delete; CoverService& operator=(const CoverService&) = delete; - private: std::shared_ptr getFromTrack(db::TrackId trackId, image::ImageSize width) override; std::shared_ptr getFromRelease(db::ReleaseId releaseId, image::ImageSize width) override; std::shared_ptr getFromArtist(db::ArtistId artistId, image::ImageSize width) override; - std::shared_ptr getDefault(image::ImageSize width) override; + std::shared_ptr getDefaultSvgCover() override; void flushCache() override; void setJpegQuality(unsigned quality) override; @@ -112,7 +112,7 @@ namespace lms::cover std::shared_mutex _cacheMutex; std::unordered_map> _cache; - std::unordered_map> _defaultCoverCache; + std::shared_ptr _defaultCover; std::atomic _cacheMisses{}; std::atomic _cacheHits{}; std::size_t _cacheSize{}; @@ -120,7 +120,6 @@ namespace lms::cover void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); - const std::filesystem::path _defaultCoverPath; const std::size_t _maxCacheSize; static inline const std::vector _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize const std::size_t _maxFileSize; diff --git a/src/libs/services/cover/include/services/cover/ICoverService.hpp b/src/libs/services/cover/include/services/cover/ICoverService.hpp index 4825cc2d..6c740d23 100644 --- a/src/libs/services/cover/include/services/cover/ICoverService.hpp +++ b/src/libs/services/cover/include/services/cover/ICoverService.hpp @@ -43,14 +43,14 @@ namespace lms::cover virtual std::shared_ptr getFromRelease(db::ReleaseId releaseId, image::ImageSize width) = 0; virtual std::shared_ptr getFromArtist(db::ArtistId artistId, image::ImageSize width) = 0; - virtual std::shared_ptr getDefault(image::ImageSize width) = 0; + virtual std::shared_ptr getDefaultSvgCover() = 0; virtual void flushCache() = 0; virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 }; - std::unique_ptr createCoverService(db::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); + std::unique_ptr createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath); } // namespace lms::coverArt diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp index 2c729447..7d4ef6b2 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp @@ -272,7 +272,7 @@ namespace lms::api::subsonic cover = core::Service::get()->getFromArtist(*artistId, size); if (!cover && context.enableDefaultCover && !artistId) - cover = core::Service::get()->getDefault(size); + cover = core::Service::get()->getDefaultSvgCover(); if (!cover) { diff --git a/src/lms/main.cpp b/src/lms/main.cpp index ed32dba8..4380005f 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -25,7 +25,7 @@ #include #include -#include "image/IRawImage.hpp" +#include "image/Image.hpp" #include "services/auth/IAuthTokenService.hpp" #include "services/auth/IPasswordService.hpp" #include "services/auth/IEnvService.hpp" @@ -310,7 +310,7 @@ namespace lms throw core::LmsException{ "Bad value '" + authenticationBackend + "' for 'authentication-backend'" }; image::init(argv[0]); - core::Service coverService{ cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg") }; + core::Service coverService{ cover::createCoverService(database, server.appRoot() + "/images/unknown-cover.svg") }; core::Service recommendationService{ recommendation::createRecommendationService(database) }; core::Service playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) }; core::Service scannerService{ scanner::createScannerService(database) }; diff --git a/src/lms/ui/resource/CoverResource.cpp b/src/lms/ui/resource/CoverResource.cpp index d0e7d0bb..6768a560 100644 --- a/src/lms/ui/resource/CoverResource.cpp +++ b/src/lms/ui/resource/CoverResource.cpp @@ -98,7 +98,7 @@ namespace lms::ui cover = core::Service::get()->getFromTrack(*trackId, *size); if (!cover) - cover = core::Service::get()->getDefault(*size); + cover = core::Service::get()->getDefaultSvgCover(); } else if (releaseIdStr) { @@ -110,7 +110,7 @@ namespace lms::ui cover = core::Service::get()->getFromRelease(*releaseId, *size); if (!cover) - cover = core::Service::get()->getDefault(*size); + cover = core::Service::get()->getDefaultSvgCover(); } else { diff --git a/src/tools/cover/LmsCover.cpp b/src/tools/cover/LmsCover.cpp index 2867cacf..b7b22d1a 100644 --- a/src/tools/cover/LmsCover.cpp +++ b/src/tools/cover/LmsCover.cpp @@ -24,15 +24,16 @@ #include -#include "database/Db.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "services/cover/ICoverService.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Service.hpp" #include "core/StreamLogger.hpp" +#include "database/Db.hpp" +#include "database/Release.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "image/Image.hpp" +#include "services/cover/ICoverService.hpp" namespace lms { @@ -83,9 +84,10 @@ int main(int argc, char* argv[]) return EXIT_SUCCESS; } + image::init(argv[0]); core::Service config{ core::createConfig(vm["conf"].as()) }; db::Db db{ config->getPath("working-dir") / "lms.db" }; - core::Service coverArtService{ cover::createCoverService(db, argv[0], vm["default-cover"].as()) }; + core::Service coverArtService{ cover::createCoverService(db, vm["default-cover"].as()) }; coverArtService->setJpegQuality(config->getULong("cover-jpeg-quality", vm["quality"].as()));