├── CMakeLists.txt └── lottie ├── details ├── lottie_cache.cpp ├── lottie_cache.h ├── lottie_cache_frame_storage.cpp ├── lottie_cache_frame_storage.h ├── lottie_frame_provider.h ├── lottie_frame_provider_cached.cpp ├── lottie_frame_provider_cached.h ├── lottie_frame_provider_cached_multi.cpp ├── lottie_frame_provider_cached_multi.h ├── lottie_frame_provider_direct.cpp ├── lottie_frame_provider_direct.h ├── lottie_frame_provider_shared.cpp ├── lottie_frame_provider_shared.h ├── lottie_frame_renderer.cpp └── lottie_frame_renderer.h ├── lottie_animation.cpp ├── lottie_animation.h ├── lottie_common.cpp ├── lottie_common.h ├── lottie_frame_generator.cpp ├── lottie_frame_generator.h ├── lottie_icon.cpp ├── lottie_icon.h ├── lottie_multi_player.cpp ├── lottie_multi_player.h ├── lottie_player.h ├── lottie_single_player.cpp ├── lottie_single_player.h └── lottie_wrap.h /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file is part of Desktop App Toolkit, 2 | # a set of libraries for developing nice desktop applications. 3 | # 4 | # For license and copyright information please follow this link: 5 | # https://github.com/desktop-app/legal/blob/master/LEGAL 6 | 7 | add_library(lib_lottie OBJECT) 8 | add_library(desktop-app::lib_lottie ALIAS lib_lottie) 9 | init_target(lib_lottie) 10 | 11 | get_filename_component(src_loc . REALPATH) 12 | 13 | nice_target_sources(lib_lottie ${src_loc} 14 | PRIVATE 15 | lottie/details/lottie_frame_provider.h 16 | lottie/details/lottie_frame_provider_direct.cpp 17 | lottie/details/lottie_frame_provider_direct.h 18 | lottie/details/lottie_frame_provider_shared.cpp 19 | lottie/details/lottie_frame_provider_shared.h 20 | lottie/details/lottie_frame_renderer.cpp 21 | lottie/details/lottie_frame_renderer.h 22 | lottie/lottie_animation.cpp 23 | lottie/lottie_animation.h 24 | lottie/lottie_common.cpp 25 | lottie/lottie_common.h 26 | lottie/lottie_frame_generator.cpp 27 | lottie/lottie_frame_generator.h 28 | lottie/lottie_multi_player.cpp 29 | lottie/lottie_multi_player.h 30 | lottie/lottie_player.h 31 | lottie/lottie_single_player.cpp 32 | lottie/lottie_single_player.h 33 | lottie/lottie_icon.cpp 34 | lottie/lottie_icon.h 35 | ) 36 | 37 | if (DESKTOP_APP_LOTTIE_USE_CACHE) 38 | nice_target_sources(lib_lottie ${src_loc} 39 | PRIVATE 40 | lottie/details/lottie_cache.cpp 41 | lottie/details/lottie_cache.h 42 | lottie/details/lottie_cache_frame_storage.cpp 43 | lottie/details/lottie_cache_frame_storage.h 44 | lottie/details/lottie_frame_provider_cached.cpp 45 | lottie/details/lottie_frame_provider_cached.h 46 | lottie/details/lottie_frame_provider_cached_multi.cpp 47 | lottie/details/lottie_frame_provider_cached_multi.h 48 | ) 49 | target_compile_definitions(lib_lottie PRIVATE LOTTIE_USE_CACHE) 50 | target_link_libraries(lib_lottie 51 | PRIVATE 52 | desktop-app::lib_ffmpeg 53 | desktop-app::external_lz4 54 | ) 55 | endif() 56 | 57 | if (DESKTOP_APP_LOTTIE_DISABLE_RECOLORING) 58 | target_compile_definitions(lib_lottie PRIVATE LOTTIE_DISABLE_RECOLORING) 59 | endif() 60 | 61 | target_include_directories(lib_lottie 62 | PUBLIC 63 | ${src_loc} 64 | ) 65 | 66 | target_link_libraries(lib_lottie 67 | PUBLIC 68 | desktop-app::lib_ui 69 | PRIVATE 70 | desktop-app::external_rlottie 71 | ) 72 | -------------------------------------------------------------------------------- /lottie/details/lottie_cache.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/details/lottie_cache.h" 8 | 9 | #include "lottie/details/lottie_frame_renderer.h" 10 | #include "ffmpeg/ffmpeg_utility.h" 11 | #include "base/bytes.h" 12 | #include "base/assertion.h" 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | namespace Lottie { 19 | namespace { 20 | 21 | // Must not exceed max database allowed entry size. 22 | constexpr auto kMaxCacheSize = 10 * 1024 * 1024; 23 | 24 | } // namespace 25 | 26 | Cache::Cache( 27 | const QByteArray &data, 28 | const FrameRequest &request, 29 | FnMut put) 30 | : _data(data) 31 | , _put(std::move(put)) { 32 | if (!readHeader(request)) { 33 | _framesReady = 0; 34 | _framesInData = 0; 35 | _data = QByteArray(); 36 | } 37 | } 38 | 39 | Cache::Cache(Cache &&) = default; 40 | 41 | Cache &Cache::operator=(Cache&&) = default; 42 | 43 | Cache::~Cache() { 44 | finalizeEncoding(); 45 | } 46 | 47 | void Cache::init( 48 | QSize original, 49 | int frameRate, 50 | int framesCount, 51 | const FrameRequest &request) { 52 | _size = request.size(original, sizeRounding()); 53 | _original = original; 54 | _frameRate = frameRate; 55 | _framesCount = framesCount; 56 | _framesReady = 0; 57 | _framesInData = 0; 58 | prepareBuffers(); 59 | } 60 | 61 | int Cache::sizeRounding() const { 62 | return 8; 63 | } 64 | 65 | int Cache::frameRate() const { 66 | return _frameRate; 67 | } 68 | 69 | int Cache::framesReady() const { 70 | return _framesReady; 71 | } 72 | 73 | int Cache::framesCount() const { 74 | return _framesCount; 75 | } 76 | 77 | QSize Cache::originalSize() const { 78 | return _original; 79 | } 80 | 81 | bool Cache::readHeader(const FrameRequest &request) { 82 | if (_data.isEmpty()) { 83 | return false; 84 | } 85 | QDataStream stream(&_data, QIODevice::ReadOnly); 86 | 87 | auto encoder = qint32(0); 88 | stream >> encoder; 89 | if (static_cast(encoder) != Encoder::YUV420A4_LZ4) { 90 | return false; 91 | } 92 | auto size = QSize(); 93 | auto original = QSize(); 94 | auto frameRate = qint32(0); 95 | auto framesCount = qint32(0); 96 | auto framesReady = qint32(0); 97 | stream 98 | >> size 99 | >> original 100 | >> frameRate 101 | >> framesCount 102 | >> framesReady; 103 | if (stream.status() != QDataStream::Ok 104 | || original.isEmpty() 105 | || (original.width() > kMaxSize) 106 | || (original.height() > kMaxSize) 107 | || (frameRate <= 0) 108 | || (frameRate > kNormalFrameRate && frameRate != kMaxFrameRate) 109 | || (framesCount <= 0) 110 | || (framesCount > kMaxFramesCount) 111 | || (framesReady <= 0) 112 | || (framesReady > framesCount) 113 | || request.size(original, sizeRounding()) != size) { 114 | return false; 115 | } 116 | _encoder = static_cast(encoder); 117 | _size = size; 118 | _original = original; 119 | _frameRate = frameRate; 120 | _framesCount = framesCount; 121 | _framesReady = framesReady; 122 | _framesInData = framesReady; 123 | prepareBuffers(); 124 | return (renderFrame(_firstFrame, request, 0) == FrameRenderResult::Ok); 125 | } 126 | 127 | QImage Cache::takeFirstFrame() { 128 | return std::move(_firstFrame); 129 | } 130 | 131 | FrameRenderResult Cache::renderFrame( 132 | QImage &to, 133 | const FrameRequest &request, 134 | int index) { 135 | const auto result = renderFrame(_readContext, to, request, index); 136 | if (result == FrameRenderResult::Ok) { 137 | if (index + 1 == _framesReady && _data.size() > _readContext.offset) { 138 | _data.resize(_readContext.offset); 139 | } 140 | } else if (result == FrameRenderResult::BadCacheSize) { 141 | _framesReady = 0; 142 | _framesInData = 0; 143 | _data.clear(); 144 | } 145 | return result; 146 | } 147 | 148 | FrameRenderResult Cache::renderFrame( 149 | CacheReadContext &context, 150 | QImage &to, 151 | const FrameRequest &request, 152 | int index) const { 153 | Expects(index >= _framesReady 154 | || index == context.offsetFrameIndex 155 | || index == 0); 156 | Expects(index >= _framesReady || context.ready()); 157 | 158 | if (index >= _framesReady) { 159 | return FrameRenderResult::NotReady; 160 | } else if (request.size(_original, sizeRounding()) != _size) { 161 | return FrameRenderResult::BadCacheSize; 162 | } else if (index == 0) { 163 | context.offsetFrameIndex = 0; 164 | context.offset = headerSize(); 165 | } 166 | const auto [ok, xored] = readCompressedFrame(context); 167 | if (!ok || (xored && index == 0)) { 168 | return FrameRenderResult::Failed; 169 | } 170 | if (xored) { 171 | Xor(context.previous, context.uncompressed); 172 | } else { 173 | std::swap(context.uncompressed, context.previous); 174 | } 175 | Decode(to, context.previous, _size, context.decodeContext); 176 | return FrameRenderResult::Ok; 177 | } 178 | 179 | void Cache::appendFrame( 180 | const QImage &frame, 181 | const FrameRequest &request, 182 | int index) { 183 | if (request.size(_original, sizeRounding()) != _size) { 184 | _framesReady = 0; 185 | _framesInData = 0; 186 | _data = QByteArray(); 187 | } 188 | if (index != _framesReady) { 189 | return; 190 | } else if (index == 0) { 191 | _size = request.size(_original, sizeRounding()); 192 | _encode = EncodeFields(); 193 | _encode.compressedFrames.reserve(_framesCount); 194 | prepareBuffers(); 195 | } 196 | Assert(frame.size() == _size); 197 | Assert(_readContext.ready()); 198 | Encode(_readContext.uncompressed, frame, _encode.cache, _encode.context); 199 | CompressAndSwapFrame( 200 | _encode.compressBuffer, 201 | (index != 0) ? &_encode.xorCompressBuffer : nullptr, 202 | _readContext.uncompressed, 203 | _readContext.previous); 204 | const auto compressed = _encode.compressBuffer; 205 | const auto nowSize = (_data.isEmpty() ? headerSize() : _data.size()) 206 | + _encode.totalSize; 207 | const auto totalSize = nowSize + compressed.size(); 208 | if (nowSize <= kMaxCacheSize && totalSize > kMaxCacheSize) { 209 | // Write to cache while we still can. 210 | finalizeEncoding(); 211 | } 212 | _encode.totalSize += compressed.size(); 213 | _encode.compressedFrames.push_back(compressed); 214 | _encode.compressedFrames.back().detach(); 215 | ++_readContext.offsetFrameIndex; 216 | _readContext.offset += compressed.size(); 217 | if (++_framesReady == _framesCount) { 218 | finalizeEncoding(); 219 | } 220 | } 221 | 222 | void Cache::finalizeEncoding() { 223 | if (_encode.compressedFrames.empty() || !_put) { 224 | return; 225 | } 226 | const auto size = (_data.isEmpty() ? headerSize() : _data.size()) 227 | + _encode.totalSize; 228 | if (_data.isEmpty()) { 229 | _data.reserve(size); 230 | writeHeader(); 231 | } else { 232 | updateFramesReadyCount(); 233 | } 234 | const auto offset = _data.size(); 235 | _data.resize(size); 236 | auto to = _data.data() + offset; 237 | for (const auto &block : _encode.compressedFrames) { 238 | const auto amount = qint32(block.size()); 239 | memcpy(to, block.data(), amount); 240 | to += amount; 241 | } 242 | _framesInData = _framesReady; 243 | if (_data.size() <= kMaxCacheSize) { 244 | _put(QByteArray(_data)); 245 | } 246 | _encode = EncodeFields(); 247 | } 248 | 249 | int Cache::headerSize() const { 250 | return 8 * sizeof(qint32); 251 | } 252 | 253 | void Cache::writeHeader() { 254 | Expects(_data.isEmpty()); 255 | 256 | QDataStream stream(&_data, QIODevice::WriteOnly); 257 | 258 | stream 259 | << static_cast(_encoder) 260 | << _size 261 | << _original 262 | << qint32(_frameRate) 263 | << qint32(_framesCount) 264 | << qint32(_framesReady); 265 | } 266 | 267 | void Cache::updateFramesReadyCount() { 268 | Expects(_data.size() >= headerSize()); 269 | 270 | QDataStream stream(&_data, QIODevice::ReadWrite); 271 | stream.device()->seek(headerSize() - sizeof(qint32)); 272 | stream << qint32(_framesReady); 273 | } 274 | 275 | void Cache::prepareBuffers() { 276 | prepareBuffers(_readContext); 277 | } 278 | 279 | void Cache::prepareBuffers(CacheReadContext &context) const { 280 | if (_size.isEmpty()) { 281 | return; 282 | } 283 | 284 | // 12 bit per pixel in YUV420P. 285 | const auto bytesPerLine = _size.width(); 286 | context.offset = headerSize(); 287 | context.offsetFrameIndex = 0; 288 | context.uncompressed.allocate(bytesPerLine, _size.height()); 289 | context.previous.allocate(bytesPerLine, _size.height()); 290 | } 291 | 292 | void Cache::keepUpContext(CacheReadContext &context) const { 293 | Expects(!context.ready() 294 | || context.previous.size() == _readContext.previous.size()); 295 | Expects(!context.ready() 296 | || context.uncompressed.size() == _readContext.uncompressed.size()); 297 | Expects(&context != &_readContext); 298 | 299 | if (!context.ready()) { 300 | prepareBuffers(context); 301 | } 302 | context.offset = _readContext.offset; 303 | context.offsetFrameIndex = _readContext.offsetFrameIndex; 304 | memcpy( 305 | context.previous.data(), 306 | _readContext.previous.data(), 307 | _readContext.previous.size()); 308 | memcpy( 309 | context.uncompressed.data(), 310 | _readContext.uncompressed.data(), 311 | _readContext.uncompressed.size()); 312 | } 313 | 314 | Cache::ReadResult Cache::readCompressedFrame( 315 | CacheReadContext &context) const { 316 | Expects(context.ready()); 317 | 318 | auto length = qint32(0); 319 | const auto part = [&] { 320 | if (context.offsetFrameIndex >= _framesInData) { 321 | // One reader is still accumulating compressed frames, 322 | // while second reader already started reading after the first. 323 | const auto readyIndex = context.offsetFrameIndex - _framesInData; 324 | Assert(readyIndex < _encode.compressedFrames.size()); 325 | return bytes::make_span(_encode.compressedFrames[readyIndex]); 326 | } else if (_data.size() > context.offset) { 327 | return bytes::make_span(_data).subspan(context.offset); 328 | } else { 329 | return bytes::const_span(); 330 | } 331 | }(); 332 | if (part.size() < sizeof(length)) { 333 | return { false }; 334 | } 335 | bytes::copy( 336 | bytes::object_as_span(&length), 337 | part.subspan(0, sizeof(length))); 338 | const auto bytes = part.subspan(sizeof(length)); 339 | 340 | const auto xored = (length < 0); 341 | if (xored) { 342 | length = -length; 343 | } 344 | const auto ok = (length <= bytes.size()) 345 | ? UncompressToRaw(context.uncompressed, bytes.subspan(0, length)) 346 | : false; 347 | if (ok) { 348 | context.offset += sizeof(length) + length; 349 | ++context.offsetFrameIndex; 350 | } 351 | return { ok, xored }; 352 | } 353 | 354 | } // namespace Lottie 355 | -------------------------------------------------------------------------------- /lottie/details/lottie_cache.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "ffmpeg/ffmpeg_utility.h" 10 | #include "lottie/details/lottie_cache_frame_storage.h" 11 | #include "lottie/lottie_common.h" 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace Lottie { 18 | 19 | struct FrameRequest; 20 | 21 | struct CacheReadContext { 22 | EncodedStorage uncompressed; 23 | EncodedStorage previous; 24 | FFmpeg::SwscalePointer decodeContext; 25 | int offset = 0; 26 | int offsetFrameIndex = 0; 27 | 28 | [[nodiscard]] bool ready() const { 29 | return (offset != 0); 30 | } 31 | }; 32 | 33 | class Cache { 34 | public: 35 | enum class Encoder : qint8 { 36 | YUV420A4_LZ4, 37 | }; 38 | 39 | Cache( 40 | const QByteArray &data, 41 | const FrameRequest &request, 42 | FnMut put); 43 | Cache(Cache &&); 44 | Cache &operator=(Cache&&); 45 | ~Cache(); 46 | 47 | void init( 48 | QSize original, 49 | int frameRate, 50 | int framesCount, 51 | const FrameRequest &request); 52 | [[nodiscard]] int sizeRounding() const; 53 | [[nodiscard]] int frameRate() const; 54 | [[nodiscard]] int framesReady() const; 55 | [[nodiscard]] int framesCount() const; 56 | [[nodiscard]] QSize originalSize() const; 57 | [[nodiscard]] QImage takeFirstFrame(); 58 | 59 | void prepareBuffers(CacheReadContext &context) const; 60 | void keepUpContext(CacheReadContext &context) const; 61 | 62 | [[nodiscard]] FrameRenderResult renderFrame( 63 | QImage &to, 64 | const FrameRequest &request, 65 | int index); 66 | [[nodiscard]] FrameRenderResult renderFrame( 67 | CacheReadContext &context, 68 | QImage &to, 69 | const FrameRequest &request, 70 | int index) const; 71 | void appendFrame( 72 | const QImage &frame, 73 | const FrameRequest &request, 74 | int index); 75 | 76 | private: 77 | struct ReadResult { 78 | bool ok = false; 79 | bool xored = false; 80 | }; 81 | struct EncodeFields { 82 | std::vector compressedFrames; 83 | QByteArray compressBuffer; 84 | QByteArray xorCompressBuffer; 85 | QImage cache; 86 | FFmpeg::SwscalePointer context; 87 | int totalSize = 0; 88 | }; 89 | int headerSize() const; 90 | void prepareBuffers(); 91 | void finalizeEncoding(); 92 | 93 | void writeHeader(); 94 | void updateFramesReadyCount(); 95 | [[nodiscard]] bool readHeader(const FrameRequest &request); 96 | [[nodiscard]] ReadResult readCompressedFrame( 97 | CacheReadContext &context) const; 98 | 99 | QByteArray _data; 100 | EncodeFields _encode; 101 | QSize _size; 102 | QSize _original; 103 | CacheReadContext _readContext; 104 | QImage _firstFrame; 105 | int _frameRate = 0; 106 | int _framesCount = 0; 107 | int _framesReady = 0; 108 | int _framesInData = 0; 109 | Encoder _encoder = Encoder::YUV420A4_LZ4; 110 | FnMut _put; 111 | 112 | }; 113 | 114 | } // namespace Lottie 115 | -------------------------------------------------------------------------------- /lottie/details/lottie_cache_frame_storage.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/details/lottie_cache_frame_storage.h" 8 | 9 | #include "base/assertion.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace Lottie { 16 | namespace { 17 | 18 | constexpr auto kAlignStorage = 16; 19 | 20 | void DecodeYUV2RGB( 21 | QImage &to, 22 | const EncodedStorage &from, 23 | FFmpeg::SwscalePointer &context) { 24 | context = FFmpeg::MakeSwscalePointer( 25 | to.size(), 26 | AV_PIX_FMT_YUV420P, 27 | to.size(), 28 | AV_PIX_FMT_BGRA, 29 | &context); 30 | Assert(context != nullptr); 31 | 32 | // AV_NUM_DATA_POINTERS defined in AVFrame struct 33 | const uint8_t *src[AV_NUM_DATA_POINTERS] = { 34 | from.yData(), 35 | from.uData(), 36 | from.vData(), 37 | nullptr 38 | }; 39 | int srcLineSize[AV_NUM_DATA_POINTERS] = { 40 | from.yBytesPerLine(), 41 | from.uBytesPerLine(), 42 | from.vBytesPerLine(), 43 | 0 44 | }; 45 | uint8_t *dst[AV_NUM_DATA_POINTERS] = { to.bits(), nullptr }; 46 | int dstLineSize[AV_NUM_DATA_POINTERS] = { int(to.bytesPerLine()), 0 }; 47 | 48 | sws_scale( 49 | context.get(), 50 | src, 51 | srcLineSize, 52 | 0, 53 | to.height(), 54 | dst, 55 | dstLineSize); 56 | } 57 | 58 | void DecodeAlpha(QImage &to, const EncodedStorage &from) { 59 | auto bytes = to.bits(); 60 | auto alpha = from.aData(); 61 | const auto perLine = to.bytesPerLine(); 62 | const auto width = to.width(); 63 | const auto height = to.height(); 64 | for (auto i = 0; i != height; ++i) { 65 | auto ints = reinterpret_cast(bytes); 66 | const auto till = ints + width; 67 | while (ints != till) { 68 | const auto value = uint32(*alpha++); 69 | *ints = (*ints & 0x00FFFFFFU) 70 | | ((value & 0xF0U) << 24) 71 | | ((value & 0xF0U) << 20); 72 | ++ints; 73 | *ints = (*ints & 0x00FFFFFFU) 74 | | (value << 28) 75 | | ((value & 0x0FU) << 24); 76 | ++ints; 77 | } 78 | bytes += perLine; 79 | } 80 | } 81 | 82 | void EncodeRGB2YUV( 83 | EncodedStorage &to, 84 | const QImage &from, 85 | FFmpeg::SwscalePointer &context) { 86 | context = FFmpeg::MakeSwscalePointer( 87 | from.size(), 88 | AV_PIX_FMT_BGRA, 89 | from.size(), 90 | AV_PIX_FMT_YUV420P, 91 | &context); 92 | Assert(context != nullptr); 93 | 94 | // AV_NUM_DATA_POINTERS defined in AVFrame struct 95 | const uint8_t *src[AV_NUM_DATA_POINTERS] = { from.bits(), nullptr }; 96 | int srcLineSize[AV_NUM_DATA_POINTERS] = { int(from.bytesPerLine()), 0 }; 97 | uint8_t *dst[AV_NUM_DATA_POINTERS] = { 98 | to.yData(), 99 | to.uData(), 100 | to.vData(), 101 | nullptr 102 | }; 103 | int dstLineSize[AV_NUM_DATA_POINTERS] = { 104 | to.yBytesPerLine(), 105 | to.uBytesPerLine(), 106 | to.vBytesPerLine(), 107 | 0 108 | }; 109 | 110 | sws_scale( 111 | context.get(), 112 | src, 113 | srcLineSize, 114 | 0, 115 | from.height(), 116 | dst, 117 | dstLineSize); 118 | } 119 | 120 | void EncodeAlpha(EncodedStorage &to, const QImage &from) { 121 | auto bytes = from.bits(); 122 | auto alpha = to.aData(); 123 | const auto perLine = from.bytesPerLine(); 124 | const auto width = from.width(); 125 | const auto height = from.height(); 126 | for (auto i = 0; i != height; ++i) { 127 | auto ints = reinterpret_cast(bytes); 128 | const auto till = ints + width; 129 | for (; ints != till; ints += 2) { 130 | *alpha++ = (((*ints) >> 24) & 0xF0U) | ((*(ints + 1)) >> 28); 131 | } 132 | bytes += perLine; 133 | } 134 | } 135 | 136 | int YLineSize(int width) { 137 | return ((width + kAlignStorage - 1) / kAlignStorage) * kAlignStorage; 138 | } 139 | 140 | int UVLineSize(int width) { 141 | return (((width / 2) + kAlignStorage - 1) / kAlignStorage) * kAlignStorage; 142 | } 143 | 144 | int YSize(int width, int height) { 145 | return YLineSize(width) * height; 146 | } 147 | 148 | int UVSize(int width, int height) { 149 | return UVLineSize(width) * (height / 2); 150 | } 151 | 152 | int ASize(int width, int height) { 153 | return (width * height) / 2; 154 | } 155 | 156 | } // namespace 157 | 158 | void EncodedStorage::allocate(int width, int height) { 159 | Expects((width % 2) == 0 && (height % 2) == 0); 160 | 161 | if (YSize(width, height) != YSize(_width, _height) 162 | || UVSize(width, height) != UVSize(_width, _height) 163 | || ASize(width, height) != ASize(_width, _height)) { 164 | _width = width; 165 | _height = height; 166 | reallocate(); 167 | } 168 | } 169 | 170 | void EncodedStorage::reallocate() { 171 | const auto total = YSize(_width, _height) 172 | + 2 * UVSize(_width, _height) 173 | + ASize(_width, _height); 174 | _data = QByteArray(total + kAlignStorage - 1, 0); 175 | } 176 | 177 | int EncodedStorage::width() const { 178 | return _width; 179 | } 180 | 181 | int EncodedStorage::height() const { 182 | return _height; 183 | } 184 | 185 | int EncodedStorage::size() const { 186 | return YSize(_width, _height) 187 | + 2 * UVSize(_width, _height) 188 | + ASize(_width, _height); 189 | } 190 | 191 | char *EncodedStorage::data() { 192 | const auto result = reinterpret_cast(_data.data()); 193 | return reinterpret_cast(kAlignStorage 194 | * ((result + kAlignStorage - 1) / kAlignStorage)); 195 | } 196 | 197 | const char *EncodedStorage::data() const { 198 | const auto result = reinterpret_cast(_data.data()); 199 | return reinterpret_cast(kAlignStorage 200 | * ((result + kAlignStorage - 1) / kAlignStorage)); 201 | } 202 | 203 | uint8_t *EncodedStorage::yData() { 204 | return reinterpret_cast(data()); 205 | } 206 | 207 | const uint8_t *EncodedStorage::yData() const { 208 | return reinterpret_cast(data()); 209 | } 210 | 211 | int EncodedStorage::yBytesPerLine() const { 212 | return YLineSize(_width); 213 | } 214 | 215 | uint8_t *EncodedStorage::uData() { 216 | return yData() + YSize(_width, _height); 217 | } 218 | 219 | const uint8_t *EncodedStorage::uData() const { 220 | return yData() + YSize(_width, _height); 221 | } 222 | 223 | int EncodedStorage::uBytesPerLine() const { 224 | return UVLineSize(_width); 225 | } 226 | 227 | uint8_t *EncodedStorage::vData() { 228 | return uData() + UVSize(_width, _height); 229 | } 230 | 231 | const uint8_t *EncodedStorage::vData() const { 232 | return uData() + UVSize(_width, _height); 233 | } 234 | 235 | int EncodedStorage::vBytesPerLine() const { 236 | return UVLineSize(_width); 237 | } 238 | 239 | uint8_t *EncodedStorage::aData() { 240 | return uData() + 2 * UVSize(_width, _height); 241 | } 242 | 243 | const uint8_t *EncodedStorage::aData() const { 244 | return uData() + 2 * UVSize(_width, _height); 245 | } 246 | 247 | int EncodedStorage::aBytesPerLine() const { 248 | return _width / 2; 249 | } 250 | 251 | void Xor(EncodedStorage &to, const EncodedStorage &from) { 252 | Expects(to.size() == from.size()); 253 | 254 | using Block = std::conditional_t< 255 | sizeof(void*) == sizeof(uint64), 256 | uint64, 257 | uint32>; 258 | constexpr auto kBlockSize = sizeof(Block); 259 | const auto amount = from.size(); 260 | const auto fromBytes = reinterpret_cast(from.data()); 261 | const auto toBytes = reinterpret_cast(to.data()); 262 | const auto blocks = amount / kBlockSize; 263 | const auto fromBlocks = reinterpret_cast(fromBytes); 264 | const auto toBlocks = reinterpret_cast(toBytes); 265 | for (auto i = 0; i != blocks; ++i) { 266 | toBlocks[i] ^= fromBlocks[i]; 267 | } 268 | const auto left = amount - (blocks * kBlockSize); 269 | for (auto i = amount - left; i != amount; ++i) { 270 | toBytes[i] ^= fromBytes[i]; 271 | } 272 | } 273 | 274 | void Encode( 275 | EncodedStorage &to, 276 | const QImage &from, 277 | QImage &cache, 278 | FFmpeg::SwscalePointer &context) { 279 | FFmpeg::UnPremultiply(cache, from); 280 | EncodeRGB2YUV(to, cache, context); 281 | EncodeAlpha(to, cache); 282 | } 283 | 284 | void Decode( 285 | QImage &to, 286 | const EncodedStorage &from, 287 | const QSize &fromSize, 288 | FFmpeg::SwscalePointer &context) { 289 | if (!FFmpeg::GoodStorageForFrame(to, fromSize)) { 290 | to = FFmpeg::CreateFrameStorage(fromSize); 291 | } 292 | DecodeYUV2RGB(to, from, context); 293 | DecodeAlpha(to, from); 294 | FFmpeg::PremultiplyInplace(to); 295 | } 296 | 297 | void CompressFromRaw(QByteArray &to, const EncodedStorage &from) { 298 | const auto size = from.size(); 299 | const auto max = sizeof(qint32) + LZ4_compressBound(size); 300 | to.reserve(max); 301 | to.resize(max); 302 | const auto compressed = LZ4_compress_default( 303 | from.data(), 304 | to.data() + sizeof(qint32), 305 | size, 306 | to.size() - sizeof(qint32)); 307 | Assert(compressed > 0); 308 | if (compressed >= size + sizeof(qint32)) { 309 | to.resize(size + sizeof(qint32)); 310 | memcpy(to.data() + sizeof(qint32), from.data(), size); 311 | } else { 312 | to.resize(compressed + sizeof(qint32)); 313 | } 314 | const auto length = qint32(to.size() - sizeof(qint32)); 315 | bytes::copy( 316 | bytes::make_detached_span(to), 317 | bytes::object_as_span(&length)); 318 | } 319 | 320 | void CompressAndSwapFrame( 321 | QByteArray &to, 322 | QByteArray *additional, 323 | EncodedStorage &frame, 324 | EncodedStorage &previous) { 325 | CompressFromRaw(to, frame); 326 | std::swap(frame, previous); 327 | if (!additional) { 328 | return; 329 | } 330 | 331 | // Check if XOR-d delta compresses better. 332 | Xor(frame, previous); 333 | CompressFromRaw(*additional, frame); 334 | if (additional->size() >= to.size()) { 335 | return; 336 | } 337 | std::swap(to, *additional); 338 | 339 | // Negative length means we XOR-d with the previous frame. 340 | const auto negativeLength = -qint32(to.size() - sizeof(qint32)); 341 | bytes::copy( 342 | bytes::make_detached_span(to), 343 | bytes::object_as_span(&negativeLength)); 344 | } 345 | 346 | bool UncompressToRaw(EncodedStorage &to, bytes::const_span from) { 347 | if (from.empty() || from.size() > to.size()) { 348 | return false; 349 | } else if (from.size() == to.size()) { 350 | memcpy(to.data(), from.data(), from.size()); 351 | return true; 352 | } 353 | const auto result = LZ4_decompress_safe( 354 | reinterpret_cast(from.data()), 355 | to.data(), 356 | from.size(), 357 | to.size()); 358 | return (result == to.size()); 359 | } 360 | 361 | } // namespace Lottie 362 | -------------------------------------------------------------------------------- /lottie/details/lottie_cache_frame_storage.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "ffmpeg/ffmpeg_utility.h" 10 | 11 | namespace Lottie { 12 | 13 | class EncodedStorage { 14 | public: 15 | void allocate(int width, int height); 16 | 17 | int width() const; 18 | int height() const; 19 | 20 | char *data(); 21 | const char *data() const; 22 | int size() const; 23 | 24 | uint8_t *yData(); 25 | const uint8_t *yData() const; 26 | int yBytesPerLine() const; 27 | uint8_t *uData(); 28 | const uint8_t *uData() const; 29 | int uBytesPerLine() const; 30 | uint8_t *vData(); 31 | const uint8_t *vData() const; 32 | int vBytesPerLine() const; 33 | uint8_t *aData(); 34 | const uint8_t *aData() const; 35 | int aBytesPerLine() const; 36 | 37 | private: 38 | void reallocate(); 39 | 40 | int _width = 0; 41 | int _height = 0; 42 | QByteArray _data; 43 | 44 | }; 45 | 46 | void Xor(EncodedStorage &to, const EncodedStorage &from); 47 | 48 | void Encode( 49 | EncodedStorage &to, 50 | const QImage &from, 51 | QImage &cache, 52 | FFmpeg::SwscalePointer &context); 53 | 54 | void Decode( 55 | QImage &to, 56 | const EncodedStorage &from, 57 | const QSize &fromSize, 58 | FFmpeg::SwscalePointer &context); 59 | 60 | void CompressFromRaw(QByteArray &to, const EncodedStorage &from); 61 | void CompressAndSwapFrame( 62 | QByteArray &to, 63 | QByteArray *additional, 64 | EncodedStorage &frame, 65 | EncodedStorage &previous); 66 | 67 | bool UncompressToRaw(EncodedStorage &to, bytes::const_span from); 68 | 69 | } // namespace Lottie 70 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_provider.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "lottie/lottie_common.h" 10 | 11 | namespace Lottie { 12 | 13 | struct FrameProviderToken { 14 | virtual ~FrameProviderToken() = default; 15 | 16 | FrameRenderResult result = FrameRenderResult::Ok; 17 | bool exclusive = false; 18 | }; 19 | 20 | class FrameProvider { 21 | public: 22 | virtual ~FrameProvider() = default; 23 | 24 | virtual QImage construct( 25 | std::unique_ptr &token, 26 | const FrameRequest &request) = 0; 27 | [[nodiscard]] virtual const Information &information() = 0; 28 | [[nodiscard]] virtual bool valid() = 0; 29 | 30 | [[nodiscard]] virtual int sizeRounding() = 0; 31 | 32 | [[nodiscard]] virtual std::unique_ptr createToken() { 33 | // Used for shared frame provider. 34 | return nullptr; 35 | } 36 | 37 | virtual bool render( 38 | const std::unique_ptr &token, 39 | QImage &to, 40 | const FrameRequest &request, 41 | int index) = 0; 42 | 43 | }; 44 | 45 | } // namespace Lottie 46 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_provider_cached.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/details/lottie_frame_provider_cached.h" 8 | 9 | namespace Lottie { 10 | 11 | FrameProviderCached::FrameProviderCached( 12 | const QByteArray &content, 13 | FnMut put, 14 | const QByteArray &cached, 15 | const FrameRequest &request, 16 | Quality quality, 17 | const ColorReplacements *replacements) 18 | : _cache(cached, request, std::move(put)) 19 | , _direct(quality) 20 | , _content(content) 21 | , _replacements(replacements) { 22 | if (!_cache.framesCount() 23 | || (_cache.framesReady() < _cache.framesCount())) { 24 | if (!_direct.load(content, replacements)) { 25 | return; 26 | } 27 | } else { 28 | _direct.setInformation({ 29 | .size = _cache.originalSize(), 30 | .frameRate = _cache.frameRate(), 31 | .framesCount = _cache.framesCount(), 32 | }); 33 | } 34 | } 35 | 36 | QImage FrameProviderCached::construct( 37 | std::unique_ptr &token, 38 | const FrameRequest &request) { 39 | auto cover = _cache.takeFirstFrame(); 40 | using Token = FrameProviderCachedToken; 41 | const auto my = static_cast(token.get()); 42 | if (!my || my->exclusive) { 43 | if (!cover.isNull()) { 44 | if (my) { 45 | _cache.keepUpContext(my->context); 46 | } 47 | return cover; 48 | } 49 | const auto &info = information(); 50 | _cache.init( 51 | info.size, 52 | info.frameRate, 53 | info.framesCount, 54 | request); 55 | } 56 | render(token, cover, request, 0); 57 | return cover; 58 | } 59 | 60 | const Information &FrameProviderCached::information() { 61 | return _direct.information(); 62 | } 63 | 64 | bool FrameProviderCached::valid() { 65 | return _direct.valid(); 66 | } 67 | 68 | int FrameProviderCached::sizeRounding() { 69 | return _cache.sizeRounding(); 70 | } 71 | 72 | std::unique_ptr FrameProviderCached::createToken() { 73 | auto result = std::make_unique(); 74 | _cache.prepareBuffers(result->context); 75 | return result; 76 | } 77 | 78 | bool FrameProviderCached::render( 79 | const std::unique_ptr &token, 80 | QImage &to, 81 | const FrameRequest &request, 82 | int index) { 83 | if (!valid()) { 84 | if (token) { 85 | token->result = FrameRenderResult::Failed; 86 | } 87 | return false; 88 | } 89 | 90 | const auto original = information().size; 91 | const auto size = request.box.isEmpty() 92 | ? original 93 | : request.size(original, sizeRounding()); 94 | if (!GoodStorageForFrame(to, size)) { 95 | to = CreateFrameStorage(size); 96 | } 97 | using Token = FrameProviderCachedToken; 98 | const auto my = static_cast(token.get()); 99 | if (my && !my->exclusive) { 100 | // This must be a thread-safe request. 101 | my->result = _cache.renderFrame(my->context, to, request, index); 102 | return (my->result == FrameRenderResult::Ok); 103 | } 104 | const auto result = _cache.renderFrame(to, request, index); 105 | if (result == FrameRenderResult::Ok) { 106 | if (my) { 107 | _cache.keepUpContext(my->context); 108 | } 109 | return true; 110 | } else if (result == FrameRenderResult::Failed 111 | // We don't support changing size on the fly for shared providers. 112 | || (result == FrameRenderResult::BadCacheSize && my) 113 | || (!_direct.loaded() && !_direct.load(_content, _replacements))) { 114 | _direct.setInformation({}); 115 | return false; 116 | } 117 | _direct.renderToPrepared(to, index); 118 | _cache.appendFrame(to, request, index); 119 | if (_cache.framesReady() == _cache.framesCount()) { 120 | _direct.unload(); 121 | } 122 | if (my) { 123 | _cache.keepUpContext(my->context); 124 | } 125 | return true; 126 | } 127 | 128 | } // namespace Lottie 129 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_provider_cached.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "lottie/details/lottie_frame_provider_direct.h" 10 | #include "lottie/details/lottie_cache.h" 11 | 12 | namespace Lottie { 13 | 14 | struct FrameProviderCachedToken : FrameProviderToken { 15 | CacheReadContext context; 16 | }; 17 | 18 | class FrameProviderCached final : public FrameProvider { 19 | public: 20 | FrameProviderCached( 21 | const QByteArray &content, 22 | FnMut put, 23 | const QByteArray &cached, 24 | const FrameRequest &request, 25 | Quality quality, 26 | const ColorReplacements *replacements); 27 | 28 | QImage construct( 29 | std::unique_ptr &token, 30 | const FrameRequest &request) override; 31 | const Information &information() override; 32 | bool valid() override; 33 | 34 | int sizeRounding() override; 35 | 36 | std::unique_ptr createToken() override; 37 | 38 | bool render( 39 | const std::unique_ptr &token, 40 | QImage &to, 41 | const FrameRequest &request, 42 | int index) override; 43 | 44 | private: 45 | Cache _cache; 46 | FrameProviderDirect _direct; 47 | const QByteArray _content; 48 | const ColorReplacements *_replacements = nullptr; 49 | 50 | }; 51 | 52 | } // namespace Lottie 53 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_provider_cached_multi.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/details/lottie_frame_provider_cached_multi.h" 8 | 9 | #include "base/assertion.h" 10 | 11 | #include 12 | 13 | namespace Lottie { 14 | 15 | FrameProviderCachedMulti::FrameProviderCachedMulti( 16 | const QByteArray &content, 17 | FnMut put, 18 | std::vector caches, 19 | const FrameRequest &request, 20 | Quality quality, 21 | const ColorReplacements *replacements) 22 | : _content(content) 23 | , _replacements(replacements) 24 | , _put(std::move(put)) 25 | , _direct(quality) { 26 | Expects(!caches.empty()); 27 | 28 | _caches.reserve(caches.size()); 29 | const auto emplace = [&](const QByteArray &cached) { 30 | const auto index = int(_caches.size()); 31 | _caches.emplace_back(cached, request, [=](QByteArray &&v) { 32 | // We capture reference to _put, so the provider is not movable. 33 | _put(index, std::move(v)); 34 | }); 35 | }; 36 | const auto load = [&] { 37 | if (_direct.loaded() || _direct.load(content, replacements)) { 38 | return true; 39 | } 40 | _caches.clear(); 41 | return false; 42 | }; 43 | const auto fill = [&] { 44 | if (!load()) { 45 | return false; 46 | } 47 | while (_caches.size() < caches.size()) { 48 | emplace({}); 49 | } 50 | return true; 51 | }; 52 | for (const auto &cached : caches) { 53 | emplace(cached); 54 | auto &cache = _caches.back(); 55 | const auto &first = _caches.front(); 56 | Assert(cache.sizeRounding() == first.sizeRounding()); 57 | 58 | if (!cache.framesCount()) { 59 | if (!fill()) { 60 | return; 61 | } 62 | break; 63 | } else if (cache.framesReady() < cache.framesCount() && !load()) { 64 | return; 65 | } else if (cache.frameRate() != first.frameRate() 66 | || cache.originalSize() != first.originalSize()) { 67 | _caches.pop_back(); 68 | if (!fill()) { 69 | return; 70 | } 71 | break; 72 | } 73 | } 74 | if (!_direct.loaded()) { 75 | _direct.setInformation({ 76 | .size = _caches.front().originalSize(), 77 | .frameRate = _caches.front().frameRate(), 78 | .framesCount = ranges::accumulate( 79 | _caches, 80 | 0, 81 | std::plus<>(), 82 | &Cache::framesCount), 83 | }); 84 | } 85 | if (!validateFramesPerCache() && _framesPerCache > 0) { 86 | fill(); 87 | } 88 | } 89 | 90 | bool FrameProviderCachedMulti::validateFramesPerCache() { 91 | const auto &info = information(); 92 | const auto count = int(_caches.size()); 93 | _framesPerCache = (info.framesCount + count - 1) / count; 94 | if (!_framesPerCache 95 | || (info.framesCount <= (count - 1) * _framesPerCache)) { 96 | _framesPerCache = 0; 97 | return false; 98 | } 99 | for (auto i = 0; i != count; ++i) { 100 | const auto cacheFramesCount = _caches[i].framesCount(); 101 | if (!cacheFramesCount) { 102 | break; 103 | } 104 | const auto shouldBe = (i + 1 == count 105 | ? (info.framesCount - (count - 1) * _framesPerCache) 106 | : _framesPerCache); 107 | if (cacheFramesCount != shouldBe) { 108 | _caches.erase(begin(_caches) + i, end(_caches)); 109 | return false; 110 | } 111 | } 112 | return true; 113 | } 114 | 115 | QImage FrameProviderCachedMulti::construct( 116 | std::unique_ptr &token, 117 | const FrameRequest &request) { 118 | if (!_framesPerCache) { 119 | if (token) { 120 | token->result = FrameRenderResult::Failed; 121 | } 122 | return QImage(); 123 | } 124 | auto cover = QImage(); 125 | using Token = FrameProviderCachedMultiToken; 126 | const auto my = static_cast(token.get()); 127 | if (!my || my->exclusive) { 128 | const auto &info = information(); 129 | const auto count = int(_caches.size()); 130 | for (auto i = 0; i != count; ++i) { 131 | auto cacheCover = _caches[i].takeFirstFrame(); 132 | if (cacheCover.isNull()) { 133 | _caches[i].init( 134 | info.size, 135 | info.frameRate, 136 | (i + 1 == count 137 | ? (info.framesCount - (count - 1) * _framesPerCache) 138 | : _framesPerCache), 139 | request); 140 | } else if (!i) { 141 | cover = std::move(cacheCover); 142 | } 143 | } 144 | if (!cover.isNull()) { 145 | if (my) { 146 | _caches[0].keepUpContext(my->context); 147 | } 148 | return cover; 149 | } 150 | } 151 | render(token, cover, request, 0); 152 | return cover; 153 | } 154 | 155 | const Information &FrameProviderCachedMulti::information() { 156 | return _direct.information(); 157 | } 158 | 159 | bool FrameProviderCachedMulti::valid() { 160 | return _direct.valid() && (_framesPerCache > 0); 161 | } 162 | 163 | int FrameProviderCachedMulti::sizeRounding() { 164 | return _caches.front().sizeRounding(); 165 | } 166 | 167 | std::unique_ptr FrameProviderCachedMulti::createToken() { 168 | auto result = std::make_unique(); 169 | if (!_caches.empty()) { 170 | _caches.front().prepareBuffers(result->context); 171 | } 172 | return result; 173 | } 174 | 175 | bool FrameProviderCachedMulti::render( 176 | const std::unique_ptr &token, 177 | QImage &to, 178 | const FrameRequest &request, 179 | int index) { 180 | if (!valid()) { 181 | if (token) { 182 | token->result = FrameRenderResult::Failed; 183 | } 184 | return false; 185 | } 186 | 187 | const auto original = information().size; 188 | const auto size = request.box.isEmpty() 189 | ? original 190 | : request.size(original, sizeRounding()); 191 | if (!GoodStorageForFrame(to, size)) { 192 | to = CreateFrameStorage(size); 193 | } 194 | const auto cacheIndex = index / _framesPerCache; 195 | const auto indexInCache = index % _framesPerCache; 196 | Assert(cacheIndex < _caches.size()); 197 | auto &cache = _caches[cacheIndex]; 198 | using Token = FrameProviderCachedMultiToken; 199 | const auto my = static_cast(token.get()); 200 | if (my && !my->exclusive) { 201 | // Many threads may get here simultaneously. 202 | my->result = cache.renderFrame( 203 | my->context, 204 | to, 205 | request, 206 | indexInCache); 207 | return (my->result == FrameRenderResult::Ok); 208 | } 209 | const auto result = cache.renderFrame(to, request, indexInCache); 210 | if (result == FrameRenderResult::Ok) { 211 | if (my) { 212 | cache.keepUpContext(my->context); 213 | } 214 | return true; 215 | } else if (result == FrameRenderResult::Failed 216 | // We don't support changing size on the fly for shared providers. 217 | || (result == FrameRenderResult::BadCacheSize && my) 218 | || (!_direct.loaded() && !_direct.load(_content, _replacements))) { 219 | _direct.setInformation({}); 220 | return false; 221 | } 222 | _direct.renderToPrepared(to, index); 223 | cache.appendFrame(to, request, indexInCache); 224 | if (cache.framesReady() == cache.framesCount() 225 | && cacheIndex + 1 == _caches.size()) { 226 | _direct.unload(); 227 | } 228 | if (my) { 229 | cache.keepUpContext(my->context); 230 | } 231 | return true; 232 | } 233 | 234 | } // namespace Lottie 235 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_provider_cached_multi.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "lottie/details/lottie_frame_provider_direct.h" 10 | #include "lottie/details/lottie_cache.h" 11 | 12 | namespace Lottie { 13 | 14 | struct FrameProviderCachedMultiToken : FrameProviderToken { 15 | CacheReadContext context; 16 | }; 17 | 18 | class FrameProviderCachedMulti final : public FrameProvider { 19 | public: 20 | FrameProviderCachedMulti( 21 | const QByteArray &content, 22 | FnMut put, 23 | std::vector caches, 24 | const FrameRequest &request, 25 | Quality quality, 26 | const ColorReplacements *replacements); 27 | 28 | FrameProviderCachedMulti(const FrameProviderCachedMulti &) = delete; 29 | FrameProviderCachedMulti &operator=(const FrameProviderCachedMulti &) 30 | = delete; 31 | 32 | QImage construct( 33 | std::unique_ptr &token, 34 | const FrameRequest &request) override; 35 | const Information &information() override; 36 | bool valid() override; 37 | 38 | int sizeRounding() override; 39 | 40 | std::unique_ptr createToken() override; 41 | 42 | bool render( 43 | const std::unique_ptr &token, 44 | QImage &to, 45 | const FrameRequest &request, 46 | int index) override; 47 | 48 | private: 49 | bool validateFramesPerCache(); 50 | 51 | const QByteArray _content; 52 | const ColorReplacements *_replacements = nullptr; 53 | FnMut _put; 54 | FrameProviderDirect _direct; 55 | std::vector _caches; 56 | int _framesPerCache = 0; 57 | 58 | }; 59 | 60 | } // namespace Lottie 61 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_provider_direct.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/details/lottie_frame_provider_direct.h" 8 | 9 | #include "lottie/lottie_wrap.h" 10 | #include "lottie/details/lottie_frame_renderer.h" 11 | #include "ui/image/image_prepare.h" 12 | 13 | #include 14 | 15 | namespace Lottie { 16 | namespace { 17 | 18 | int GetLottieFrameRate(not_null animation, Quality quality) { 19 | const auto rate = int(qRound(animation->frameRate())); 20 | return (quality == Quality::Default && rate == 60) ? (rate / 2) : rate; 21 | } 22 | 23 | int GetLottieFramesCount(not_null animation, Quality quality) { 24 | const auto rate = int(qRound(animation->frameRate())); 25 | const auto count = int(animation->totalFrame()); 26 | return (quality == Quality::Default && rate == 60) 27 | ? ((count + 1) / 2) 28 | : count; 29 | } 30 | 31 | int GetLottieFrameIndex(not_null animation, Quality quality, int index) { 32 | const auto rate = int(qRound(animation->frameRate())); 33 | return (quality == Quality::Default && rate == 60) ? (index * 2) : index; 34 | } 35 | 36 | [[nodiscard]] rlottie::FitzModifier MapModifier(SkinModifier modifier) { 37 | using Result = rlottie::FitzModifier; 38 | switch (modifier) { 39 | case SkinModifier::None: return Result::None; 40 | case SkinModifier::Color1: return Result::Type12; 41 | case SkinModifier::Color2: return Result::Type3; 42 | case SkinModifier::Color3: return Result::Type4; 43 | case SkinModifier::Color4: return Result::Type5; 44 | case SkinModifier::Color5: return Result::Type6; 45 | } 46 | Unexpected("Unexpected modifier in MapModifier."); 47 | } 48 | 49 | } // namespace 50 | 51 | FrameProviderDirect::FrameProviderDirect(Quality quality) 52 | : _quality(quality) { 53 | } 54 | 55 | FrameProviderDirect::~FrameProviderDirect() = default; 56 | 57 | bool FrameProviderDirect::load( 58 | const QByteArray &content, 59 | const ColorReplacements *replacements) { 60 | _information = Information(); 61 | 62 | const auto string = ReadUtf8(Images::UnpackGzip(content)); 63 | if (string.size() > kMaxFileSize) { 64 | return false; 65 | } 66 | 67 | _animation = LoadAnimationFromData( 68 | string, 69 | std::string(), 70 | std::string(), 71 | false, 72 | (replacements 73 | ? replacements->replacements 74 | : std::vector>()), 75 | (replacements 76 | ? MapModifier(replacements->modifier) 77 | : rlottie::FitzModifier::None)); 78 | if (!_animation) { 79 | return false; 80 | } 81 | auto width = size_t(0); 82 | auto height = size_t(0); 83 | _animation->size(width, height); 84 | const auto rate = GetLottieFrameRate(_animation.get(), _quality); 85 | const auto count = GetLottieFramesCount(_animation.get(), _quality); 86 | return setInformation({ 87 | .size = QSize(int(width), int(height)), 88 | .frameRate = int(rate), 89 | .framesCount = int(count), 90 | }); 91 | } 92 | 93 | bool FrameProviderDirect::loaded() const { 94 | return (_animation != nullptr); 95 | } 96 | 97 | void FrameProviderDirect::unload() { 98 | _animation = nullptr; 99 | } 100 | 101 | bool FrameProviderDirect::setInformation(Information information) { 102 | if (information.size.isEmpty() 103 | || information.size.width() > kMaxSize 104 | || information.size.height() > kMaxSize 105 | || !information.frameRate 106 | || information.frameRate > kMaxFrameRate 107 | || !information.framesCount 108 | || information.framesCount > kMaxFramesCount) { 109 | return false; 110 | } 111 | _information = information; 112 | return true; 113 | } 114 | 115 | const Information &FrameProviderDirect::information() { 116 | return _information; 117 | } 118 | 119 | bool FrameProviderDirect::valid() { 120 | return _information.framesCount > 0; 121 | } 122 | 123 | QImage FrameProviderDirect::construct( 124 | std::unique_ptr &token, 125 | const FrameRequest &request) { 126 | auto cover = QImage(); 127 | render(token, cover, request, 0); 128 | return cover; 129 | } 130 | 131 | int FrameProviderDirect::sizeRounding() { 132 | return 2; 133 | } 134 | 135 | bool FrameProviderDirect::render( 136 | const std::unique_ptr &token, 137 | QImage &to, 138 | const FrameRequest &request, 139 | int index) { 140 | if (token && !token->exclusive) { 141 | token->result = FrameRenderResult::NotReady; 142 | return false; 143 | } else if (!valid()) { 144 | return false; 145 | } 146 | const auto original = information().size; 147 | const auto size = request.box.isEmpty() 148 | ? original 149 | : request.size(original, sizeRounding()); 150 | if (!GoodStorageForFrame(to, size)) { 151 | to = CreateFrameStorage(size); 152 | } 153 | renderToPrepared(to, index); 154 | return true; 155 | } 156 | 157 | void FrameProviderDirect::renderToPrepared( 158 | QImage &to, 159 | int index) const { 160 | to.fill(Qt::transparent); 161 | auto surface = rlottie::Surface( 162 | reinterpret_cast(to.bits()), 163 | to.width(), 164 | to.height(), 165 | to.bytesPerLine()); 166 | _animation->renderSync( 167 | GetLottieFrameIndex(_animation.get(), _quality, index), 168 | surface); 169 | } 170 | 171 | } // namespace Lottie 172 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_provider_direct.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "lottie/details/lottie_frame_provider.h" 10 | #include "lottie/lottie_common.h" 11 | 12 | namespace rlottie { 13 | class Animation; 14 | } // namespace rlottie 15 | 16 | namespace Lottie { 17 | 18 | class FrameProviderDirect final : public FrameProvider { 19 | public: 20 | explicit FrameProviderDirect(Quality quality); 21 | ~FrameProviderDirect(); 22 | 23 | bool load( 24 | const QByteArray &content, 25 | const ColorReplacements *replacements); 26 | [[nodiscard]] bool loaded() const; 27 | void unload(); 28 | 29 | bool setInformation(Information information); 30 | 31 | QImage construct( 32 | std::unique_ptr &token, 33 | const FrameRequest &request) override; 34 | const Information &information() override; 35 | bool valid() override; 36 | 37 | int sizeRounding() override; 38 | 39 | bool render( 40 | const std::unique_ptr &token, 41 | QImage &to, 42 | const FrameRequest &request, 43 | int index) override; 44 | void renderToPrepared(QImage &to, int index) const; 45 | 46 | private: 47 | const FrameProviderDirect *cthis() const { 48 | return this; 49 | } 50 | 51 | std::unique_ptr _animation; 52 | Information _information; 53 | Quality _quality = Quality::Default; 54 | 55 | }; 56 | 57 | } // namespace Lottie 58 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_provider_shared.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/details/lottie_frame_provider_shared.h" 8 | 9 | #include "base/assertion.h" 10 | 11 | #include 12 | 13 | namespace Lottie { 14 | 15 | FrameProviderShared::FrameProviderShared( 16 | FnMut)>)> factory) { 17 | _mutex.lockForWrite(); 18 | factory(crl::guard(this, [=](std::unique_ptr shared) { 19 | _shared = std::move(shared); 20 | _mutex.unlock(); 21 | })); 22 | } 23 | 24 | QImage FrameProviderShared::construct( 25 | std::unique_ptr &token, 26 | const FrameRequest &request) { 27 | QWriteLocker lock(&_mutex); 28 | token = createToken(); 29 | if (token) { 30 | token->exclusive = !_constructed; 31 | } 32 | auto result = _shared->construct(token, request); 33 | _constructed = true; 34 | return result; 35 | } 36 | 37 | const Information &FrameProviderShared::information() { 38 | static auto empty = Information(); 39 | 40 | QReadLocker lock(&_mutex); 41 | return _shared ? _shared->information() : empty; 42 | } 43 | 44 | bool FrameProviderShared::valid() { 45 | QReadLocker lock(&_mutex); 46 | return _shared && _shared->valid(); 47 | } 48 | 49 | int FrameProviderShared::sizeRounding() { 50 | QReadLocker lock(&_mutex); 51 | Assert(_shared != nullptr); 52 | return _shared->sizeRounding(); 53 | } 54 | 55 | std::unique_ptr FrameProviderShared::createToken() { 56 | Expects(_shared != nullptr); 57 | 58 | return _shared->createToken(); 59 | } 60 | 61 | bool FrameProviderShared::render( 62 | const std::unique_ptr &token, 63 | QImage &to, 64 | const FrameRequest &request, 65 | int index) { 66 | QReadLocker readLock(&_mutex); 67 | if (!_shared) { 68 | if (token) { 69 | token->result = FrameRenderResult::Failed; 70 | } 71 | return false; 72 | } 73 | 74 | if (token) { 75 | token->exclusive = false; 76 | _shared->render(token, to, request, index); 77 | if (token->result == FrameRenderResult::Ok) { 78 | return true; 79 | } 80 | } 81 | readLock.unlock(); 82 | 83 | QWriteLocker lock(&_mutex); 84 | if (!_shared) { 85 | if (token) { 86 | token->result = FrameRenderResult::Failed; 87 | } 88 | return false; 89 | } 90 | if (token) { 91 | _shared->render(token, to, request, index); 92 | if (token->result == FrameRenderResult::Ok) { 93 | return true; 94 | } else if (token->result == FrameRenderResult::Failed) { 95 | _shared = nullptr; 96 | return false; 97 | } 98 | token->exclusive = true; 99 | } 100 | return _shared->render(token, to, request, index); 101 | } 102 | 103 | } // namespace Lottie 104 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_provider_shared.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "lottie/details/lottie_frame_provider.h" 10 | #include "base/weak_ptr.h" 11 | 12 | #include 13 | 14 | namespace Lottie { 15 | 16 | class FrameProviderShared final 17 | : public FrameProvider 18 | , public base::has_weak_ptr { 19 | public: 20 | explicit FrameProviderShared( 21 | FnMut)>)> factory); 22 | 23 | QImage construct( 24 | std::unique_ptr &token, 25 | const FrameRequest &request) override; 26 | const Information &information() override; 27 | bool valid() override; 28 | 29 | int sizeRounding() override; 30 | 31 | std::unique_ptr createToken() override; 32 | 33 | bool render( 34 | const std::unique_ptr &token, 35 | QImage &to, 36 | const FrameRequest &request, 37 | int index) override; 38 | 39 | private: 40 | std::unique_ptr _shared; 41 | QReadWriteLock _mutex; 42 | bool _constructed = false; 43 | 44 | }; 45 | 46 | } // namespace Lottie 47 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_renderer.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/details/lottie_frame_renderer.h" 8 | 9 | #include "lottie/lottie_player.h" 10 | #include "lottie/lottie_animation.h" 11 | #include "lottie/details/lottie_frame_provider.h" 12 | #include "ui/image/image_prepare.h" 13 | #include "base/flat_map.h" 14 | #include "base/assertion.h" 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace Lottie { 22 | namespace { 23 | 24 | std::weak_ptr GlobalInstance; 25 | 26 | } // namespace 27 | 28 | class FrameRendererObject final { 29 | public: 30 | explicit FrameRendererObject( 31 | crl::weak_on_queue weak); 32 | 33 | void append( 34 | std::unique_ptr entry, 35 | const FrameRequest &request); 36 | void frameShown(); 37 | void updateFrameRequest( 38 | not_null entry, 39 | const FrameRequest &request); 40 | void remove(not_null entry); 41 | 42 | private: 43 | struct Entry { 44 | std::unique_ptr state; 45 | FrameRequest request; 46 | }; 47 | 48 | static not_null StateFromEntry(const Entry &entry) { 49 | return entry.state.get(); 50 | } 51 | 52 | void queueGenerateFrames(); 53 | void generateFrames(); 54 | 55 | crl::weak_on_queue _weak; 56 | std::vector _entries; 57 | bool _queued = false; 58 | 59 | }; 60 | 61 | [[nodiscard]] bool GoodForRequest( 62 | const QImage &image, 63 | const FrameRequest &request) { 64 | if (request.box.isEmpty()) { 65 | return true; 66 | } else if (request.colored.alpha() != 0 || request.mirrorHorizontal) { 67 | return false; 68 | } 69 | const auto size = image.size(); 70 | return (request.box.width() == size.width()) 71 | || (request.box.height() == size.height()); 72 | } 73 | 74 | [[nodiscard]] QImage PrepareByRequest( 75 | const QImage &original, 76 | const FrameRequest &request, 77 | int sizeRounding, 78 | QImage storage) { 79 | Expects(!request.box.isEmpty()); 80 | 81 | const auto size = request.size( 82 | original.size(), 83 | sizeRounding); 84 | if (!GoodStorageForFrame(storage, size)) { 85 | storage = CreateFrameStorage(size); 86 | } 87 | storage.fill(Qt::transparent); 88 | 89 | { 90 | QPainter p(&storage); 91 | p.setRenderHint(QPainter::Antialiasing); 92 | p.setRenderHint(QPainter::SmoothPixmapTransform); 93 | p.drawImage(QRect(QPoint(), size), original); 94 | } 95 | if (request.mirrorHorizontal) { 96 | storage = std::move(storage).mirrored(true, false); 97 | } 98 | if (request.colored.alpha() != 0) { 99 | storage = Images::Colored(std::move(storage), request.colored); 100 | } 101 | return storage; 102 | } 103 | 104 | QImage PrepareFrameByRequest( 105 | not_null frame, 106 | bool useExistingPrepared = false) { 107 | Expects(!frame->original.isNull()); 108 | 109 | if (GoodForRequest(frame->original, frame->request)) { 110 | return frame->original; 111 | } else if (frame->prepared.isNull() || !useExistingPrepared) { 112 | frame->prepared = PrepareByRequest( 113 | frame->original, 114 | frame->request, 115 | frame->sizeRounding, 116 | std::move(frame->prepared)); 117 | } 118 | return frame->prepared; 119 | } 120 | 121 | FrameRendererObject::FrameRendererObject( 122 | crl::weak_on_queue weak) 123 | : _weak(std::move(weak)) { 124 | } 125 | 126 | void FrameRendererObject::append( 127 | std::unique_ptr state, 128 | const FrameRequest &request) { 129 | _entries.push_back({ std::move(state), request }); 130 | queueGenerateFrames(); 131 | } 132 | 133 | void FrameRendererObject::frameShown() { 134 | queueGenerateFrames(); 135 | } 136 | 137 | void FrameRendererObject::updateFrameRequest( 138 | not_null entry, 139 | const FrameRequest &request) { 140 | const auto i = ranges::find(_entries, entry, &StateFromEntry); 141 | Assert(i != end(_entries)); 142 | i->request = request; 143 | } 144 | 145 | void FrameRendererObject::remove(not_null entry) { 146 | const auto i = ranges::find(_entries, entry, &StateFromEntry); 147 | Assert(i != end(_entries)); 148 | _entries.erase(i); 149 | } 150 | 151 | void FrameRendererObject::generateFrames() { 152 | auto players = base::flat_map>(); 153 | const auto renderOne = [&](const Entry &entry) { 154 | const auto result = entry.state->renderNextFrame(entry.request); 155 | if (const auto player = result.notify.get()) { 156 | players.emplace(player, result.notify); 157 | } 158 | return result.rendered; 159 | }; 160 | const auto rendered = ranges::count_if(_entries, renderOne); 161 | if (rendered) { 162 | if (!players.empty()) { 163 | crl::on_main([players = std::move(players)] { 164 | for (const auto &[player, weak] : players) { 165 | if (weak) { 166 | weak->checkStep(); 167 | } 168 | } 169 | }); 170 | } 171 | queueGenerateFrames(); 172 | } 173 | } 174 | 175 | void FrameRendererObject::queueGenerateFrames() { 176 | if (_queued) { 177 | return; 178 | } 179 | _queued = true; 180 | _weak.with([](FrameRendererObject &that) { 181 | that._queued = false; 182 | that.generateFrames(); 183 | }); 184 | } 185 | 186 | SharedState::SharedState( 187 | std::shared_ptr provider, 188 | const FrameRequest &request) 189 | : _provider(std::move(provider)) { 190 | if (_provider->valid()) { 191 | init(_provider->construct(_token, request), request); 192 | } 193 | } 194 | 195 | int SharedState::sizeRounding() const { 196 | return _provider->sizeRounding(); 197 | } 198 | 199 | void SharedState::init(QImage cover, const FrameRequest &request) { 200 | Expects(!initialized()); 201 | 202 | _frames[0].request = request; 203 | _frames[0].sizeRounding = sizeRounding(); 204 | _frames[0].original = std::move(cover); 205 | _framesCount = _provider->information().framesCount; 206 | } 207 | 208 | void SharedState::start( 209 | not_null owner, 210 | crl::time started, 211 | crl::time delay, 212 | int skippedFrames) { 213 | _owner = owner; 214 | _started = started; 215 | _delay = delay; 216 | _skippedFrames = skippedFrames; 217 | _counter.store(0, std::memory_order_release); 218 | } 219 | 220 | bool IsRendered(not_null frame) { 221 | return (frame->displayed == kTimeUnknown); 222 | } 223 | 224 | void SharedState::renderNextFrame( 225 | not_null frame, 226 | const FrameRequest &request) { 227 | if (!_framesCount) { 228 | return; 229 | } 230 | const auto rendered = _provider->render( 231 | _token, 232 | frame->original, 233 | request, 234 | (++_frameIndex) % _framesCount); 235 | if (!rendered) { 236 | return; 237 | } 238 | frame->request = request; 239 | frame->sizeRounding = sizeRounding(); 240 | PrepareFrameByRequest(frame); 241 | frame->index = _frameIndex; 242 | frame->displayed = kTimeUnknown; 243 | } 244 | 245 | auto SharedState::renderNextFrame(const FrameRequest &request) 246 | -> RenderResult { 247 | const auto prerender = [&](int index) -> RenderResult { 248 | const auto frame = getFrame(index); 249 | const auto next = getFrame((index + 1) % kFramesCount); 250 | if (!IsRendered(frame)) { 251 | renderNextFrame(frame, request); 252 | return { IsRendered(frame) }; 253 | } else if (!IsRendered(next)) { 254 | renderNextFrame(next, request); 255 | return { IsRendered(next) }; 256 | } 257 | return { false }; 258 | }; 259 | const auto present = [&](int counter, int index) -> RenderResult { 260 | const auto frame = getFrame(index); 261 | if (!IsRendered(frame)) { 262 | renderNextFrame(frame, request); 263 | if (!IsRendered(frame)) { 264 | return { false }; 265 | } 266 | } 267 | frame->display = countFrameDisplayTime(frame->index); 268 | 269 | // Release this frame to the main thread for rendering. 270 | _counter.store( 271 | (counter + 1) % (2 * kFramesCount), 272 | std::memory_order_release); 273 | return { true, _owner }; 274 | }; 275 | 276 | switch (counter()) { 277 | case 0: return present(0, 1); 278 | case 1: return prerender(2); 279 | case 2: return present(2, 2); 280 | case 3: return prerender(3); 281 | case 4: return present(4, 3); 282 | case 5: return prerender(0); 283 | case 6: return present(6, 0); 284 | case 7: return prerender(1); 285 | } 286 | Unexpected("Counter value in Lottie::SharedState::renderNextFrame."); 287 | } 288 | 289 | crl::time SharedState::countFrameDisplayTime(int index) const { 290 | const auto rate = _provider->information().frameRate; 291 | return _started 292 | + _delay 293 | + crl::time(1000) * (_skippedFrames + index) / rate; 294 | } 295 | 296 | int SharedState::counter() const { 297 | return _counter.load(std::memory_order_acquire); 298 | } 299 | 300 | bool SharedState::initialized() const { 301 | return (counter() != kCounterUninitialized); 302 | } 303 | 304 | not_null SharedState::getFrame(int index) { 305 | Expects(index >= 0 && index < kFramesCount); 306 | 307 | return &_frames[index]; 308 | } 309 | 310 | not_null SharedState::getFrame(int index) const { 311 | Expects(index >= 0 && index < kFramesCount); 312 | 313 | return &_frames[index]; 314 | } 315 | 316 | Information SharedState::information() const { 317 | return _provider->information(); 318 | } 319 | 320 | not_null SharedState::frameForPaint() { 321 | const auto result = getFrame(counter() / 2); 322 | Assert(!result->original.isNull()); 323 | Assert(result->displayed != kTimeUnknown); 324 | 325 | return result; 326 | } 327 | 328 | int SharedState::framesCount() const { 329 | return _framesCount; 330 | } 331 | 332 | crl::time SharedState::nextFrameDisplayTime() const { 333 | const auto frameDisplayTime = [&](int counter) { 334 | const auto next = (counter + 1) % (2 * kFramesCount); 335 | const auto index = next / 2; 336 | const auto frame = getFrame(index); 337 | if (frame->displayed != kTimeUnknown) { 338 | // Frame already displayed, but not yet shown. 339 | return kFrameDisplayTimeAlreadyDone; 340 | } 341 | Assert(IsRendered(frame)); 342 | Assert(frame->display != kTimeUnknown); 343 | 344 | return frame->display; 345 | }; 346 | 347 | switch (counter()) { 348 | case 0: return kTimeUnknown; 349 | case 1: return frameDisplayTime(1); 350 | case 2: return kTimeUnknown; 351 | case 3: return frameDisplayTime(3); 352 | case 4: return kTimeUnknown; 353 | case 5: return frameDisplayTime(5); 354 | case 6: return kTimeUnknown; 355 | case 7: return frameDisplayTime(7); 356 | } 357 | Unexpected("Counter value in VideoTrack::Shared::nextFrameDisplayTime."); 358 | } 359 | 360 | void SharedState::addTimelineDelay(crl::time delayed, int skippedFrames) { 361 | if (!delayed && !skippedFrames) { 362 | return; 363 | } 364 | 365 | const auto recountCurrentFrame = [&](int counter) { 366 | _delay += delayed; 367 | _skippedFrames += skippedFrames; 368 | 369 | const auto next = (counter + 1) % (2 * kFramesCount); 370 | const auto index = next / 2; 371 | const auto frame = getFrame(index); 372 | if (frame->displayed != kTimeUnknown) { 373 | // Frame already displayed. 374 | return; 375 | } 376 | Assert(IsRendered(frame)); 377 | Assert(frame->display != kTimeUnknown); 378 | frame->display = countFrameDisplayTime(frame->index); 379 | }; 380 | 381 | switch (counter()) { 382 | case 0: Unexpected("Value 0 in SharedState::addTimelineDelay."); 383 | case 1: return recountCurrentFrame(1); 384 | case 2: Unexpected("Value 2 in SharedState::addTimelineDelay."); 385 | case 3: return recountCurrentFrame(3); 386 | case 4: Unexpected("Value 4 in SharedState::addTimelineDelay."); 387 | case 5: return recountCurrentFrame(5); 388 | case 6: Unexpected("Value 6 in SharedState::addTimelineDelay."); 389 | case 7: return recountCurrentFrame(7); 390 | } 391 | Unexpected("Counter value in VideoTrack::Shared::nextFrameDisplayTime."); 392 | } 393 | 394 | void SharedState::markFrameDisplayed(crl::time now) { 395 | const auto mark = [&](int counter) { 396 | const auto next = (counter + 1) % (2 * kFramesCount); 397 | const auto index = next / 2; 398 | const auto frame = getFrame(index); 399 | if (frame->displayed == kTimeUnknown) { 400 | frame->displayed = now; 401 | } 402 | }; 403 | 404 | switch (counter()) { 405 | case 0: Unexpected("Value 0 in SharedState::markFrameDisplayed."); 406 | case 1: return mark(1); 407 | case 2: Unexpected("Value 2 in SharedState::markFrameDisplayed."); 408 | case 3: return mark(3); 409 | case 4: Unexpected("Value 4 in SharedState::markFrameDisplayed."); 410 | case 5: return mark(5); 411 | case 6: Unexpected("Value 6 in SharedState::markFrameDisplayed."); 412 | case 7: return mark(7); 413 | } 414 | Unexpected("Counter value in Lottie::SharedState::markFrameDisplayed."); 415 | } 416 | 417 | bool SharedState::markFrameShown() { 418 | const auto jump = [&](int counter) { 419 | const auto next = (counter + 1) % (2 * kFramesCount); 420 | const auto index = next / 2; 421 | const auto frame = getFrame(index); 422 | if (frame->displayed == kTimeUnknown) { 423 | return false; 424 | } 425 | _counter.store( 426 | next, 427 | std::memory_order_release); 428 | return true; 429 | }; 430 | 431 | switch (counter()) { 432 | case 0: return false; 433 | case 1: return jump(1); 434 | case 2: return false; 435 | case 3: return jump(3); 436 | case 4: return false; 437 | case 5: return jump(5); 438 | case 6: return false; 439 | case 7: return jump(7); 440 | } 441 | Unexpected("Counter value in Lottie::SharedState::markFrameShown."); 442 | } 443 | 444 | SharedState::~SharedState() = default; 445 | 446 | std::shared_ptr FrameRenderer::CreateIndependent() { 447 | return std::make_shared(); 448 | } 449 | 450 | std::shared_ptr FrameRenderer::Instance() { 451 | if (auto result = GlobalInstance.lock()) { 452 | return result; 453 | } 454 | auto result = CreateIndependent(); 455 | GlobalInstance = result; 456 | return result; 457 | } 458 | 459 | void FrameRenderer::append( 460 | std::unique_ptr entry, 461 | const FrameRequest &request) { 462 | _wrapped.with([=, entry = std::move(entry)]( 463 | FrameRendererObject &unwrapped) mutable { 464 | unwrapped.append(std::move(entry), request); 465 | }); 466 | } 467 | 468 | void FrameRenderer::frameShown() { 469 | _wrapped.with([=](FrameRendererObject &unwrapped) { 470 | unwrapped.frameShown(); 471 | }); 472 | } 473 | 474 | void FrameRenderer::updateFrameRequest( 475 | not_null entry, 476 | const FrameRequest &request) { 477 | _wrapped.with([=](FrameRendererObject &unwrapped) { 478 | unwrapped.updateFrameRequest(entry, request); 479 | }); 480 | } 481 | 482 | void FrameRenderer::remove(not_null entry) { 483 | _wrapped.with([=](FrameRendererObject &unwrapped) { 484 | unwrapped.remove(entry); 485 | }); 486 | } 487 | 488 | } // namespace Lottie 489 | -------------------------------------------------------------------------------- /lottie/details/lottie_frame_renderer.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "base/basic_types.h" 10 | #include "base/weak_ptr.h" 11 | #include "lottie/lottie_common.h" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace Lottie { 20 | 21 | // Frame rate can be 1, 2, ... , 29, 30 or 60. 22 | inline constexpr auto kNormalFrameRate = 30; 23 | inline constexpr auto kMaxFrameRate = 60; 24 | inline constexpr auto kMaxSize = 4096; 25 | inline constexpr auto kMaxFramesCount = 210; 26 | inline constexpr auto kFrameDisplayTimeAlreadyDone 27 | = std::numeric_limits::max(); 28 | inline constexpr auto kDisplayedInitial = crl::time(-1); 29 | 30 | class Player; 31 | class FrameProvider; 32 | struct FrameProviderToken; 33 | 34 | struct Frame { 35 | QImage original; 36 | crl::time displayed = kDisplayedInitial; 37 | crl::time display = kTimeUnknown; 38 | int index = 0; 39 | int sizeRounding = 0; 40 | 41 | FrameRequest request; 42 | QImage prepared; 43 | }; 44 | 45 | QImage PrepareFrameByRequest( 46 | not_null frame, 47 | bool useExistingPrepared); 48 | 49 | class SharedState { 50 | public: 51 | SharedState( 52 | std::shared_ptr provider, 53 | const FrameRequest &request); 54 | 55 | void start( 56 | not_null owner, 57 | crl::time now, 58 | crl::time delay = 0, 59 | int skippedFrames = 0); 60 | 61 | [[nodiscard]] Information information() const; 62 | [[nodiscard]] bool initialized() const; 63 | 64 | [[nodiscard]] not_null frameForPaint(); 65 | [[nodiscard]] int framesCount() const; 66 | [[nodiscard]] crl::time nextFrameDisplayTime() const; 67 | void addTimelineDelay(crl::time delayed, int skippedFrames = 0); 68 | void markFrameDisplayed(crl::time now); 69 | bool markFrameShown(); 70 | 71 | struct RenderResult { 72 | bool rendered = false; 73 | base::weak_ptr notify; 74 | }; 75 | [[nodiscard]] RenderResult renderNextFrame(const FrameRequest &request); 76 | 77 | ~SharedState(); 78 | 79 | private: 80 | void init(QImage cover, const FrameRequest &request); 81 | void renderNextFrame( 82 | not_null frame, 83 | const FrameRequest &request); 84 | [[nodiscard]] int sizeRounding() const; 85 | [[nodiscard]] crl::time countFrameDisplayTime(int index) const; 86 | [[nodiscard]] not_null getFrame(int index); 87 | [[nodiscard]] not_null getFrame(int index) const; 88 | [[nodiscard]] int counter() const; 89 | 90 | // crl::queue changes 0,2,4,6 to 1,3,5,7. 91 | // main thread changes 1,3,5,7 to 2,4,6,0. 92 | static constexpr auto kCounterUninitialized = -1; 93 | std::atomic _counter = kCounterUninitialized; 94 | 95 | static constexpr auto kFramesCount = 4; 96 | std::array _frames; 97 | 98 | base::weak_ptr _owner; 99 | crl::time _started = kTimeUnknown; 100 | 101 | // (_counter % 2) == 1 main thread can write _delay. 102 | // (_counter % 2) == 0 crl::queue can read _delay. 103 | crl::time _delay = kTimeUnknown; 104 | 105 | int _frameIndex = 0; 106 | int _framesCount = 0; 107 | int _skippedFrames = 0; 108 | const std::shared_ptr _provider; 109 | std::unique_ptr _token; 110 | 111 | }; 112 | 113 | class FrameRendererObject; 114 | 115 | class FrameRenderer final { 116 | public: 117 | static std::shared_ptr CreateIndependent(); 118 | static std::shared_ptr Instance(); 119 | 120 | void append( 121 | std::unique_ptr entry, 122 | const FrameRequest &request); 123 | 124 | void updateFrameRequest( 125 | not_null entry, 126 | const FrameRequest &request); 127 | void frameShown(); 128 | void remove(not_null state); 129 | 130 | private: 131 | using Implementation = FrameRendererObject; 132 | crl::object_on_queue _wrapped; 133 | 134 | }; 135 | 136 | } // namespace Lottie 137 | -------------------------------------------------------------------------------- /lottie/lottie_animation.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/lottie_animation.h" 8 | 9 | #include "lottie/details/lottie_frame_renderer.h" 10 | #include "lottie/details/lottie_frame_provider_direct.h" 11 | #include "lottie/lottie_player.h" 12 | #include "ui/image/image_prepare.h" 13 | #include "base/algorithm.h" 14 | #include "base/assertion.h" 15 | #include "base/variant.h" 16 | 17 | #ifdef LOTTIE_USE_CACHE 18 | #include "lottie/details/lottie_frame_provider_cached.h" 19 | #include "lottie/details/lottie_frame_provider_cached_multi.h" 20 | #endif // LOTTIE_USE_CACHE 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | namespace Lottie { 28 | namespace { 29 | 30 | const auto kIdealSize = QSize(512, 512); 31 | 32 | details::InitData CheckSharedState(std::unique_ptr state) { 33 | Expects(state != nullptr); 34 | 35 | auto information = state->information(); 36 | if (!information.frameRate 37 | || information.framesCount <= 0 38 | || information.size.isEmpty()) { 39 | return Error::NotSupported; 40 | } 41 | return state; 42 | } 43 | 44 | details::InitData Init( 45 | const QByteArray &content, 46 | const FrameRequest &request, 47 | Quality quality, 48 | const ColorReplacements *replacements) { 49 | if (const auto error = ContentError(content)) { 50 | return *error; 51 | } 52 | auto provider = std::make_shared(quality); 53 | if (!provider->load(content, replacements)) { 54 | return Error::ParseFailed; 55 | } 56 | return CheckSharedState(std::make_unique( 57 | std::move(provider), 58 | request.empty() ? FrameRequest{ kIdealSize } : request)); 59 | } 60 | 61 | #ifdef LOTTIE_USE_CACHE 62 | details::InitData Init( 63 | const QByteArray &content, 64 | FnMut put, 65 | const QByteArray &cached, 66 | const FrameRequest &request, 67 | Quality quality, 68 | const ColorReplacements *replacements) { 69 | Expects(!request.empty()); 70 | 71 | if (const auto error = ContentError(content)) { 72 | return *error; 73 | } 74 | auto provider = std::make_shared( 75 | content, 76 | std::move(put), 77 | cached, 78 | request, 79 | quality, 80 | replacements); 81 | return provider->valid() 82 | ? CheckSharedState(std::make_unique( 83 | std::move(provider), 84 | request.empty() ? FrameRequest{ kIdealSize } : request)) 85 | : Error::ParseFailed; 86 | } 87 | 88 | details::InitData Init( 89 | const QByteArray &content, 90 | FnMut put, 91 | std::vector caches, 92 | const FrameRequest &request, 93 | Quality quality, 94 | const ColorReplacements *replacements) { 95 | Expects(!request.empty()); 96 | 97 | if (const auto error = ContentError(content)) { 98 | return *error; 99 | } 100 | auto provider = std::make_shared( 101 | content, 102 | std::move(put), 103 | std::move(caches), 104 | request, 105 | quality, 106 | replacements); 107 | return provider->valid() 108 | ? CheckSharedState(std::make_unique( 109 | std::move(provider), 110 | request.empty() ? FrameRequest{ kIdealSize } : request)) 111 | : Error::ParseFailed; 112 | } 113 | #endif // LOTTIE_USE_CACHE 114 | 115 | details::InitData Init( 116 | std::shared_ptr provider, 117 | const FrameRequest &request) { 118 | Expects(!request.empty()); 119 | 120 | return provider->valid() 121 | ? CheckSharedState(std::make_unique( 122 | std::move(provider), 123 | request.empty() ? FrameRequest{ kIdealSize } : request)) 124 | : Error::ParseFailed; 125 | } 126 | 127 | } // namespace 128 | 129 | std::shared_ptr MakeFrameRenderer() { 130 | return FrameRenderer::CreateIndependent(); 131 | } 132 | 133 | QImage ReadThumbnail(const QByteArray &content) { 134 | return v::match(Init(content, FrameRequest(), Quality::High, nullptr), []( 135 | const std::unique_ptr &state) { 136 | return state->frameForPaint()->original; 137 | }, [](Error) { 138 | return QImage(); 139 | }); 140 | } 141 | 142 | Animation::Animation( 143 | not_null player, 144 | const QByteArray &content, 145 | const FrameRequest &request, 146 | Quality quality, 147 | const ColorReplacements *replacements) 148 | : _player(player) { 149 | if (quality == Quality::Synchronous) { 150 | initDone(Init(content, request, quality, replacements)); 151 | } else { 152 | const auto weak = base::make_weak(this); 153 | crl::async([=] { 154 | auto result = Init(content, request, quality, replacements); 155 | crl::on_main(weak, [=, data = std::move(result)]() mutable { 156 | initDone(std::move(data)); 157 | }); 158 | }); 159 | } 160 | } 161 | 162 | Animation::Animation( 163 | not_null player, 164 | FnMut)> get, // Main thread. 165 | FnMut put, // Unknown thread. 166 | const QByteArray &content, 167 | const FrameRequest &request, 168 | Quality quality, 169 | const ColorReplacements *replacements) 170 | #ifdef LOTTIE_USE_CACHE 171 | : _player(player) { 172 | const auto weak = base::make_weak(this); 173 | get([=, put = std::move(put)](QByteArray &&cached) mutable { 174 | crl::async([=, put = std::move(put)]() mutable { 175 | auto result = Init( 176 | content, 177 | std::move(put), 178 | cached, 179 | request, 180 | quality, 181 | replacements); 182 | crl::on_main(weak, [=, data = std::move(result)]() mutable { 183 | initDone(std::move(data)); 184 | }); 185 | }); 186 | }); 187 | #else // LOTTIE_USE_CACHE 188 | : Animation(player, content, request, quality, replacements) { 189 | #endif // LOTTIE_USE_CACHE 190 | } 191 | 192 | Animation::Animation( 193 | not_null player, 194 | int keysCount, 195 | FnMut)> get, 196 | FnMut put, 197 | const QByteArray &content, 198 | const FrameRequest &request, 199 | Quality quality, 200 | const ColorReplacements *replacements) 201 | #ifdef LOTTIE_USE_CACHE 202 | : _player(player) { 203 | const auto weak = base::make_weak(this); 204 | struct State { 205 | std::atomic left = 0; 206 | std::vector caches; 207 | FnMut put; 208 | }; 209 | const auto state = std::make_shared(); 210 | state->left = keysCount; 211 | state->put = std::move(put); 212 | state->caches.resize(keysCount); 213 | for (auto i = 0; i != keysCount; ++i) { 214 | get(i, [=](QByteArray &&cached) { 215 | state->caches[i] = std::move(cached); 216 | if (--state->left) { 217 | return; 218 | } 219 | crl::async([=] { 220 | auto result = Init( 221 | content, 222 | std::move(state->put), 223 | std::move(state->caches), 224 | request, 225 | quality, 226 | replacements); 227 | crl::on_main(weak, [=, data = std::move(result)]() mutable { 228 | initDone(std::move(data)); 229 | }); 230 | }); 231 | }); 232 | } 233 | #else // LOTTIE_USE_CACHE 234 | : Animation(player, content, request, quality, replacements) { 235 | #endif // LOTTIE_USE_CACHE 236 | } 237 | 238 | Animation::Animation( 239 | not_null player, 240 | std::shared_ptr provider, 241 | const FrameRequest &request) 242 | : _player(player) { 243 | const auto weak = base::make_weak(this); 244 | crl::async([=, provider = std::move(provider)]() mutable { 245 | auto result = Init(std::move(provider), request); 246 | crl::on_main(weak, [=, data = std::move(result)]() mutable { 247 | initDone(std::move(data)); 248 | }); 249 | }); 250 | } 251 | 252 | bool Animation::ready() const { 253 | return (_state != nullptr); 254 | } 255 | 256 | void Animation::initDone(details::InitData &&data) { 257 | v::match(data, [&](std::unique_ptr &state) { 258 | parseDone(std::move(state)); 259 | }, [&](Error error) { 260 | parseFailed(error); 261 | }); 262 | } 263 | 264 | void Animation::parseDone(std::unique_ptr state) { 265 | Expects(state != nullptr); 266 | 267 | _state = state.get(); 268 | _player->start(this, std::move(state)); 269 | } 270 | 271 | void Animation::parseFailed(Error error) { 272 | _player->failed(this, error); 273 | } 274 | 275 | QImage Animation::frame() const { 276 | Expects(_state != nullptr); 277 | 278 | return PrepareFrameByRequest(_state->frameForPaint(), true); 279 | } 280 | 281 | QImage Animation::frame(const FrameRequest &request) const { 282 | Expects(_state != nullptr); 283 | 284 | const auto frame = _state->frameForPaint(); 285 | const auto changed = (frame->request != request); 286 | if (changed) { 287 | frame->request = request; 288 | _player->updateFrameRequest(this, request); 289 | } 290 | return PrepareFrameByRequest(frame, !changed); 291 | } 292 | 293 | auto Animation::frameInfo(const FrameRequest &request) const -> FrameInfo { 294 | Expects(_state != nullptr); 295 | 296 | const auto frame = _state->frameForPaint(); 297 | const auto changed = (frame->request != request); 298 | if (changed) { 299 | frame->request = request; 300 | _player->updateFrameRequest(this, request); 301 | } 302 | return { 303 | PrepareFrameByRequest(frame, !changed), 304 | frame->index % _state->framesCount() 305 | }; 306 | } 307 | 308 | int Animation::frameIndex() const { 309 | Expects(_state != nullptr); 310 | 311 | const auto frame = _state->frameForPaint(); 312 | return frame->index % _state->framesCount(); 313 | } 314 | 315 | int Animation::framesCount() const { 316 | Expects(_state != nullptr); 317 | 318 | return _state->framesCount(); 319 | } 320 | 321 | Information Animation::information() const { 322 | Expects(_state != nullptr); 323 | 324 | return _state->information(); 325 | } 326 | 327 | std::optional ContentError(const QByteArray &content) { 328 | if (content.size() > kMaxFileSize) { 329 | return Error::ParseFailed; 330 | } 331 | return std::nullopt; 332 | } 333 | 334 | } // namespace Lottie 335 | -------------------------------------------------------------------------------- /lottie/lottie_animation.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "lottie/lottie_common.h" 10 | #include "base/weak_ptr.h" 11 | 12 | #include 13 | #include 14 | 15 | class QString; 16 | class QByteArray; 17 | 18 | namespace rlottie { 19 | class Animation; 20 | } // namespace rlottie 21 | 22 | namespace Lottie { 23 | 24 | class Player; 25 | class SharedState; 26 | class FrameRenderer; 27 | class FrameProvider; 28 | 29 | std::shared_ptr MakeFrameRenderer(); 30 | 31 | QImage ReadThumbnail(const QByteArray &content); 32 | 33 | namespace details { 34 | 35 | using InitData = std::variant, Error>; 36 | 37 | } // namespace details 38 | 39 | class Animation final : public base::has_weak_ptr { 40 | public: 41 | struct FrameInfo { 42 | QImage image; 43 | int index = 0; 44 | }; 45 | 46 | Animation( 47 | not_null player, 48 | const QByteArray &content, 49 | const FrameRequest &request, 50 | Quality quality, 51 | const ColorReplacements *replacements = nullptr); 52 | Animation( 53 | not_null player, 54 | FnMut)> get, // Main thread. 55 | FnMut put, // Unknown thread. 56 | const QByteArray &content, 57 | const FrameRequest &request, 58 | Quality quality, 59 | const ColorReplacements *replacements = nullptr); 60 | Animation( // Multi-cache version. 61 | not_null player, 62 | int keysCount, 63 | FnMut)> get, 64 | FnMut put, // Unknown thread. 65 | const QByteArray &content, 66 | const FrameRequest &request, 67 | Quality quality, 68 | const ColorReplacements *replacements = nullptr); 69 | Animation( // Thread-safe version. 70 | not_null player, 71 | std::shared_ptr provider, 72 | const FrameRequest &request); 73 | 74 | [[nodiscard]] bool ready() const; 75 | [[nodiscard]] QImage frame() const; 76 | [[nodiscard]] QImage frame(const FrameRequest &request) const; 77 | [[nodiscard]] FrameInfo frameInfo(const FrameRequest &request) const; 78 | [[nodiscard]] int frameIndex() const; 79 | [[nodiscard]] int framesCount() const; 80 | [[nodiscard]] Information information() const; 81 | 82 | private: 83 | void initDone(details::InitData &&data); 84 | void parseDone(std::unique_ptr state); 85 | void parseFailed(Error error); 86 | 87 | const not_null _player; 88 | SharedState *_state = nullptr; 89 | 90 | }; 91 | 92 | [[nodiscard]] std::optional ContentError(const QByteArray &content); 93 | 94 | } // namespace Lottie 95 | -------------------------------------------------------------------------------- /lottie/lottie_common.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/lottie_common.h" 8 | 9 | #include "base/algorithm.h" 10 | 11 | #include 12 | 13 | namespace Lottie { 14 | namespace { 15 | 16 | QByteArray ReadFile(const QString &filepath) { 17 | auto f = QFile(filepath); 18 | return (f.size() <= kMaxFileSize && f.open(QIODevice::ReadOnly)) 19 | ? f.readAll() 20 | : QByteArray(); 21 | } 22 | 23 | } // namespace 24 | 25 | QSize FrameRequest::size( 26 | const QSize &original, 27 | int sizeRounding) const { 28 | Expects(!empty()); 29 | Expects(sizeRounding != 0); 30 | 31 | const auto result = original.scaled(box, Qt::KeepAspectRatio); 32 | const auto skipw = result.width() % sizeRounding; 33 | const auto skiph = result.height() % sizeRounding; 34 | return QSize( 35 | std::max(result.width() - skipw, sizeRounding), 36 | std::max(result.height() - skiph, sizeRounding)); 37 | } 38 | 39 | QByteArray ReadContent(const QByteArray &data, const QString &filepath) { 40 | return data.isEmpty() ? ReadFile(filepath) : base::duplicate(data); 41 | } 42 | 43 | std::string ReadUtf8(const QByteArray &data) { 44 | //00 00 FE FF UTF-32BE 45 | //FF FE 00 00 UTF-32LE 46 | //FE FF UTF-16BE 47 | //FF FE UTF-16LE 48 | //EF BB BF UTF-8 49 | if (data.size() < 4) { 50 | return data.toStdString(); 51 | } 52 | const auto bom = uint32(uint8(data[0])) 53 | | (uint32(uint8(data[1])) << 8) 54 | | (uint32(uint8(data[2])) << 16) 55 | | (uint32(uint8(data[3])) << 24); 56 | const auto skip = ((bom == 0xFFFE0000U) || (bom == 0x0000FEFFU)) 57 | ? 4 58 | : (((bom & 0xFFFFU) == 0xFFFEU) || ((bom & 0xFFFFU) == 0xFEFFU)) 59 | ? 2 60 | : ((bom & 0xFFFFFFU) == 0xBFBBEFU) 61 | ? 3 62 | : 0; 63 | const auto bytes = data.data() + skip; 64 | const auto length = data.size() - skip; 65 | // Old RapidJSON didn't convert encoding, just skipped BOM. 66 | // We emulate old behavior here, so don't convert as well. 67 | return std::string(bytes, length); 68 | } 69 | 70 | bool GoodStorageForFrame(const QImage &storage, QSize size) { 71 | return !storage.isNull() 72 | && (storage.format() == kImageFormat) 73 | && (storage.size() == size) 74 | && storage.isDetached(); 75 | } 76 | 77 | QImage CreateFrameStorage(QSize size) { 78 | return QImage(size, kImageFormat); 79 | } 80 | 81 | } // namespace Lottie 82 | -------------------------------------------------------------------------------- /lottie/lottie_common.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "base/basic_types.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace Lottie { 19 | 20 | inline constexpr auto kTimeUnknown = std::numeric_limits::min(); 21 | inline constexpr auto kMaxFileSize = 2 * 1024 * 1024; 22 | 23 | constexpr auto kImageFormat = QImage::Format_ARGB32_Premultiplied; 24 | 25 | class Animation; 26 | 27 | struct Information { 28 | QSize size; 29 | int frameRate = 0; 30 | int framesCount = 0; 31 | }; 32 | 33 | enum class Error { 34 | ParseFailed, 35 | NotSupported, 36 | }; 37 | 38 | struct FrameRequest { 39 | QSize box; 40 | QColor colored = QColor(0, 0, 0, 0); 41 | bool mirrorHorizontal = false; 42 | 43 | [[nodiscard]] bool empty() const { 44 | return box.isEmpty(); 45 | } 46 | [[nodiscard]] QSize size( 47 | const QSize &original, 48 | int sizeRounding) const; 49 | 50 | [[nodiscard]] bool operator==(const FrameRequest &other) const { 51 | return (box == other.box) 52 | && (colored == other.colored) 53 | && (mirrorHorizontal == other.mirrorHorizontal); 54 | } 55 | [[nodiscard]] bool operator!=(const FrameRequest &other) const { 56 | return !(*this == other); 57 | } 58 | }; 59 | 60 | enum class Quality : char { 61 | Default, 62 | High, 63 | Synchronous 64 | }; 65 | 66 | enum class SkinModifier { 67 | None, 68 | Color1, 69 | Color2, 70 | Color3, 71 | Color4, 72 | Color5, 73 | }; 74 | 75 | struct ColorReplacements { 76 | std::vector> replacements; 77 | SkinModifier modifier = SkinModifier::None; 78 | uint8 tag = 0; 79 | }; 80 | 81 | [[nodiscard]] QByteArray ReadContent( 82 | const QByteArray &data, 83 | const QString &filepath); 84 | [[nodiscard]] std::string ReadUtf8(const QByteArray &data); 85 | [[nodiscard]] bool GoodStorageForFrame(const QImage &storage, QSize size); 86 | [[nodiscard]] QImage CreateFrameStorage(QSize size); 87 | 88 | enum class FrameRenderResult { 89 | Ok, 90 | NotReady, 91 | BadCacheSize, 92 | Failed, 93 | }; 94 | 95 | } // namespace Lottie 96 | -------------------------------------------------------------------------------- /lottie/lottie_frame_generator.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/lottie_frame_generator.h" 8 | 9 | #include "lottie/lottie_common.h" 10 | #include "lottie/lottie_wrap.h" 11 | #include "ui/image/image_prepare.h" 12 | 13 | #include 14 | 15 | namespace Lottie { 16 | 17 | FrameGenerator::FrameGenerator(const QByteArray &bytes) 18 | : _rlottie( 19 | LoadAnimationFromData( 20 | ReadUtf8(Images::UnpackGzip(bytes)), 21 | std::string(), 22 | std::string(), 23 | false)) { 24 | if (_rlottie) { 25 | const auto rate = _rlottie->frameRate(); 26 | _multiplier = (rate == 60) ? 2 : 1; 27 | auto width = size_t(); 28 | auto height = size_t(); 29 | _rlottie->size(width, height); 30 | _size = QSize(width, height); 31 | _framesCount = (_rlottie->totalFrame() + _multiplier - 1) 32 | / _multiplier; 33 | _frameDuration = (rate > 0) ? (1000 * _multiplier / rate) : 0; 34 | } 35 | if (!_framesCount || !_frameDuration || _size.isEmpty()) { 36 | _rlottie = nullptr; 37 | _framesCount = _frameDuration = 0; 38 | _size = QSize(); 39 | } 40 | } 41 | 42 | FrameGenerator::~FrameGenerator() = default; 43 | 44 | int FrameGenerator::count() { 45 | return _framesCount; 46 | } 47 | 48 | double FrameGenerator::rate() { 49 | return _rlottie ? (_rlottie->frameRate() / _multiplier) : 0.; 50 | } 51 | 52 | FrameGenerator::Frame FrameGenerator::renderNext( 53 | QImage storage, 54 | QSize size, 55 | Qt::AspectRatioMode mode) { 56 | if (!_framesCount || _frameIndex == _framesCount) { 57 | return {}; 58 | } 59 | ++_frameIndex; 60 | return renderCurrent(std::move(storage), size, mode); 61 | } 62 | 63 | FrameGenerator::Frame FrameGenerator::renderCurrent( 64 | QImage storage, 65 | QSize size, 66 | Qt::AspectRatioMode mode) { 67 | Expects(_frameIndex > 0); 68 | 69 | const auto index = _frameIndex - 1; 70 | if (storage.format() != kImageFormat 71 | || storage.size() != size) { 72 | storage = CreateFrameStorage(size); 73 | } 74 | storage.fill(Qt::transparent); 75 | const auto scaled = _size.scaled(size, mode); 76 | const auto render = QSize( 77 | std::max(scaled.width(), size.width()), 78 | std::max(scaled.height(), size.height())); 79 | const auto xskip = (size.width() - render.width()) / 2; 80 | const auto yskip = (size.height() - render.height()); 81 | const auto skip = (yskip * storage.bytesPerLine() / 4) + xskip; 82 | auto surface = rlottie::Surface( 83 | reinterpret_cast(storage.bits()) + skip, 84 | render.width(), 85 | render.height(), 86 | storage.bytesPerLine()); 87 | _rlottie->renderSync(index * _multiplier, std::move(surface)); 88 | return { 89 | .duration = _frameDuration, 90 | .image = std::move(storage), 91 | .last = (_frameIndex == _framesCount), 92 | }; 93 | } 94 | 95 | void FrameGenerator::jumpToStart() { 96 | _frameIndex = 0; 97 | } 98 | 99 | } // namespace Lottie 100 | -------------------------------------------------------------------------------- /lottie/lottie_frame_generator.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "ui/effects/frame_generator.h" 10 | 11 | #include 12 | #include 13 | 14 | namespace rlottie { 15 | class Animation; 16 | } // namespace rlottie 17 | 18 | namespace Lottie { 19 | 20 | class FrameGenerator final : public Ui::FrameGenerator { 21 | public: 22 | explicit FrameGenerator(const QByteArray &bytes); 23 | ~FrameGenerator(); 24 | 25 | int count() override; 26 | double rate() override; 27 | Frame renderNext( 28 | QImage storage, 29 | QSize size, 30 | Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override; 31 | Frame renderCurrent( 32 | QImage storage, 33 | QSize size, 34 | Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override; 35 | void jumpToStart() override; 36 | 37 | private: 38 | std::unique_ptr _rlottie; 39 | QSize _size; 40 | int _multiplier = 1; 41 | int _frameDuration = 0; 42 | int _framesCount = 0; 43 | int _frameIndex = 0; 44 | 45 | }; 46 | 47 | } // namespace Lottie 48 | -------------------------------------------------------------------------------- /lottie/lottie_icon.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/lottie_icon.h" 8 | 9 | #include "lottie/lottie_common.h" 10 | #include "lottie/lottie_wrap.h" 11 | #include "ui/image/image_prepare.h" 12 | #include "ui/text/text_custom_emoji.h" 13 | #include "ui/style/style_core.h" 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace Lottie { 22 | namespace { 23 | 24 | [[nodiscard]] std::unique_ptr CreateFromContent( 25 | const QByteArray &content, 26 | QColor replacement) { 27 | auto string = ReadUtf8(Images::UnpackGzip(content)); 28 | auto list = std::vector>(); 29 | if (replacement != Qt::white) { 30 | const auto value = (uint32_t(replacement.red()) << 16) 31 | | (uint32_t(replacement.green() << 8)) 32 | | (uint32_t(replacement.blue())); 33 | list.push_back({ 0xFFFFFFU, value }); 34 | } 35 | auto result = LoadAnimationFromData( 36 | std::move(string), 37 | std::string(), 38 | std::string(), 39 | false, 40 | std::move(list)); 41 | return result; 42 | } 43 | 44 | [[nodiscard]] QColor RealRenderedColor(QColor color) { 45 | #ifndef LOTTIE_DISABLE_RECOLORING 46 | return QColor(color.red(), color.green(), color.blue(), 255); 47 | #else 48 | return Qt::white; 49 | #endif 50 | } 51 | 52 | [[nodiscard]] QByteArray ReadIconContent( 53 | const QString &name, 54 | const QByteArray &json, 55 | const QString &path) { 56 | return !json.isEmpty() 57 | ? json 58 | : !path.isEmpty() 59 | ? ReadContent(json, path) 60 | : Images::UnpackGzip( 61 | ReadContent({}, u":/animations/"_q + name + u".tgs"_q)); 62 | } 63 | 64 | class LocalLottieCustomEmoji final 65 | : public Ui::Text::CustomEmoji 66 | , public base::has_weak_ptr { 67 | public: 68 | LocalLottieCustomEmoji( 69 | Lottie::IconDescriptor &&descriptor, 70 | Fn repaint); 71 | ~LocalLottieCustomEmoji() override = default; 72 | 73 | int width() override; 74 | QString entityData() override; 75 | void paint(QPainter &p, const Context &context) override; 76 | void unload() override; 77 | bool ready() override; 78 | bool readyInDefaultState() override; 79 | 80 | private: 81 | void startAnimation(); 82 | void handleAnimationFrame(); 83 | 84 | int _width = 0; 85 | const QString _entityData; 86 | std::unique_ptr _icon; 87 | Fn _repaint; 88 | bool _looped = true; 89 | }; 90 | 91 | LocalLottieCustomEmoji::LocalLottieCustomEmoji( 92 | Lottie::IconDescriptor &&descriptor, 93 | Fn repaint) 94 | : _width(descriptor.sizeOverride.width()) 95 | , _entityData(!descriptor.name.isEmpty() 96 | ? descriptor.name 97 | : descriptor.path.isEmpty() 98 | ? descriptor.path 99 | : u"lottie_custom_emoji"_q) 100 | , _icon(Lottie::MakeIcon(std::move(descriptor))) 101 | , _repaint(std::move(repaint)) { 102 | if (!_width && _icon && _icon->valid()) { 103 | _width = _icon->width(); 104 | startAnimation(); 105 | } 106 | } 107 | 108 | int LocalLottieCustomEmoji::width() { 109 | return _width; 110 | } 111 | 112 | QString LocalLottieCustomEmoji::entityData() { 113 | return _entityData; 114 | } 115 | 116 | void LocalLottieCustomEmoji::paint(QPainter &p, const Context &context) { 117 | if (!_icon || !_icon->valid()) { 118 | return; 119 | } 120 | 121 | const auto color = context.textColor; 122 | const auto position = context.position; 123 | const auto paused = context.paused 124 | || context.internal.forceFirstFrame 125 | || context.internal.overrideFirstWithLastFrame; 126 | 127 | if (paused) { 128 | const auto frame = context.internal.forceLastFrame 129 | ? _icon->framesCount() - 1 130 | : 0; 131 | _icon->jumpTo(frame, _repaint); 132 | } else if (!_icon->animating()) { 133 | startAnimation(); 134 | } 135 | 136 | _icon->paint(p, position.x(), position.y(), color); 137 | } 138 | 139 | void LocalLottieCustomEmoji::unload() { 140 | if (_icon) { 141 | _icon->jumpTo(0, nullptr); 142 | } 143 | } 144 | 145 | bool LocalLottieCustomEmoji::ready() { 146 | return _icon && _icon->valid(); 147 | } 148 | 149 | bool LocalLottieCustomEmoji::readyInDefaultState() { 150 | return _icon && _icon->valid() && _icon->frameIndex() == 0; 151 | } 152 | 153 | void LocalLottieCustomEmoji::startAnimation() { 154 | if (!_icon || !_icon->valid() || _icon->framesCount() <= 1) { 155 | return; 156 | } 157 | 158 | _icon->animate( 159 | [weak = base::make_weak(this)] { 160 | if (const auto strong = weak.get()) { 161 | strong->handleAnimationFrame(); 162 | } 163 | }, 164 | 0, 165 | _icon->framesCount() - 1); 166 | } 167 | 168 | void LocalLottieCustomEmoji::handleAnimationFrame() { 169 | if (_repaint && _looped && _icon->frameIndex() > 0) { 170 | _repaint(); 171 | } 172 | } 173 | 174 | } // namespace 175 | 176 | struct Icon::Frame { 177 | int index = 0; 178 | QImage resizedImage; 179 | QImage renderedImage; 180 | QImage colorizedImage; 181 | QColor renderedColor; 182 | QColor colorizedColor; 183 | }; 184 | 185 | class Icon::Inner final : public std::enable_shared_from_this { 186 | public: 187 | Inner(int frameIndex, base::weak_ptr weak, bool limitFps); 188 | 189 | void prepareFromAsync( 190 | const QString &name, 191 | const QString &path, 192 | const QByteArray &json, 193 | QSize sizeOverride, 194 | QColor color); 195 | void waitTillPrepared() const; 196 | 197 | [[nodiscard]] bool valid() const; 198 | [[nodiscard]] QSize size() const; 199 | [[nodiscard]] int framesCount() const; 200 | [[nodiscard]] Frame &frame(); 201 | [[nodiscard]] const Frame &frame() const; 202 | 203 | [[nodiscard]] crl::time animationDuration( 204 | int frameFrom, 205 | int frameTo) const; 206 | void moveToFrame(int frame, QColor color, QSize updatedDesiredSize); 207 | 208 | private: 209 | enum class PreloadState { 210 | None, 211 | Preloading, 212 | Ready, 213 | }; 214 | 215 | // Called from crl::async. 216 | void renderPreloadFrame(const QColor &color); 217 | 218 | const bool _limitFps = false; 219 | std::unique_ptr _rlottie; 220 | Frame _current; 221 | QSize _desiredSize; 222 | std::atomic _preloadState = PreloadState::None; 223 | 224 | Frame _preloaded; // Changed on main or async depending on _preloadState. 225 | QSize _preloadImageSize; 226 | 227 | base::weak_ptr _weak; 228 | int _framesCount = 0; 229 | int _frameMultiplier = 1; 230 | mutable crl::semaphore _semaphore; 231 | mutable bool _ready = false; 232 | 233 | }; 234 | 235 | Icon::Inner::Inner(int frameIndex, base::weak_ptr weak, bool limitFps) 236 | : _limitFps(limitFps) 237 | , _current { .index = frameIndex } 238 | , _weak(weak) { 239 | } 240 | 241 | void Icon::Inner::prepareFromAsync( 242 | const QString &name, 243 | const QString &path, 244 | const QByteArray &json, 245 | QSize sizeOverride, 246 | QColor color) { 247 | const auto guard = gsl::finally([&] { _semaphore.release(); }); 248 | if (!_weak) { 249 | return; 250 | } 251 | auto rlottie = CreateFromContent( 252 | ReadIconContent(name, json, path), 253 | color); 254 | if (!rlottie || !_weak) { 255 | return; 256 | } 257 | auto width = size_t(); 258 | auto height = size_t(); 259 | rlottie->size(width, height); 260 | if (_limitFps && rlottie->frameRate() == 60) { 261 | _frameMultiplier = 2; 262 | } 263 | _framesCount = (rlottie->totalFrame() + _frameMultiplier - 1) 264 | / _frameMultiplier; 265 | if (!_framesCount || !width || !height) { 266 | return; 267 | } 268 | _rlottie = std::move(rlottie); 269 | while (_current.index < 0) { 270 | _current.index += _framesCount; 271 | } 272 | const auto size = sizeOverride.isEmpty() 273 | ? style::ConvertScale(QSize{ int(width), int(height) }) 274 | : sizeOverride; 275 | auto image = CreateFrameStorage(size * style::DevicePixelRatio()); 276 | image.fill(Qt::transparent); 277 | auto surface = rlottie::Surface( 278 | reinterpret_cast(image.bits()), 279 | image.width(), 280 | image.height(), 281 | image.bytesPerLine()); 282 | _rlottie->renderSync( 283 | _current.index * _frameMultiplier, 284 | std::move(surface)); 285 | _current.renderedColor = RealRenderedColor(color); 286 | _current.renderedImage = std::move(image); 287 | _current.colorizedColor = QColor(); // Mark colorizedImage as invalid. 288 | _desiredSize = size; 289 | } 290 | 291 | void Icon::Inner::waitTillPrepared() const { 292 | if (!_ready) { 293 | _semaphore.acquire(); 294 | _ready = true; 295 | } 296 | } 297 | 298 | bool Icon::Inner::valid() const { 299 | waitTillPrepared(); 300 | return (_rlottie != nullptr); 301 | } 302 | 303 | QSize Icon::Inner::size() const { 304 | waitTillPrepared(); 305 | return _desiredSize; 306 | } 307 | 308 | int Icon::Inner::framesCount() const { 309 | waitTillPrepared(); 310 | return _framesCount; 311 | } 312 | 313 | Icon::Frame &Icon::Inner::frame() { 314 | waitTillPrepared(); 315 | return _current; 316 | } 317 | 318 | const Icon::Frame &Icon::Inner::frame() const { 319 | waitTillPrepared(); 320 | return _current; 321 | } 322 | 323 | crl::time Icon::Inner::animationDuration(int frameFrom, int frameTo) const { 324 | waitTillPrepared(); 325 | const auto rate = _rlottie 326 | ? (_rlottie->frameRate() / _frameMultiplier) 327 | : 0.; 328 | const auto frames = std::abs(frameTo - frameFrom); 329 | return (rate >= 1.) 330 | ? crl::time(base::SafeRound(frames / rate * 1000.)) 331 | : 0; 332 | } 333 | 334 | void Icon::Inner::moveToFrame( 335 | int frame, 336 | QColor color, 337 | QSize updatedDesiredSize) { 338 | waitTillPrepared(); 339 | if (frame < 0) { 340 | frame += _framesCount; 341 | } 342 | const auto state = _preloadState.load(); 343 | const auto shown = _current.index; 344 | if (!updatedDesiredSize.isEmpty()) { 345 | _desiredSize = updatedDesiredSize; 346 | } 347 | const auto desiredImageSize = _desiredSize * style::DevicePixelRatio(); 348 | if (!_rlottie 349 | || state == PreloadState::Preloading 350 | || (shown == frame 351 | && (_current.renderedImage.size() == desiredImageSize))) { 352 | return; 353 | } else if (state == PreloadState::Ready) { 354 | if (_preloaded.index == frame 355 | && (shown != frame 356 | || _preloaded.renderedImage.size() == desiredImageSize)) { 357 | std::swap(_current, _preloaded); 358 | if (_current.renderedImage.size() == desiredImageSize) { 359 | return; 360 | } 361 | } else if ((shown < _preloaded.index && _preloaded.index < frame) 362 | || (shown > _preloaded.index && _preloaded.index > frame)) { 363 | std::swap(_current, _preloaded); 364 | } 365 | } 366 | _preloadImageSize = desiredImageSize; 367 | _preloaded.index = frame; 368 | _preloadState = PreloadState::Preloading; 369 | crl::async([ 370 | guard = shared_from_this(), 371 | color = RealRenderedColor(color) 372 | ] { 373 | guard->renderPreloadFrame(color); 374 | }); 375 | } 376 | 377 | void Icon::Inner::renderPreloadFrame(const QColor &color) { 378 | if (!_weak) { 379 | return; 380 | } 381 | auto &image = _preloaded.renderedImage; 382 | const auto &size = _preloadImageSize; 383 | if (!GoodStorageForFrame(image, size)) { 384 | image = GoodStorageForFrame(_preloaded.resizedImage, size) 385 | ? base::take(_preloaded.resizedImage) 386 | : CreateFrameStorage(size); 387 | } 388 | image.fill(Qt::black); 389 | auto surface = rlottie::Surface( 390 | reinterpret_cast(image.bits()), 391 | image.width(), 392 | image.height(), 393 | image.bytesPerLine()); 394 | _rlottie->renderSync( 395 | _preloaded.index * _frameMultiplier, 396 | std::move(surface)); 397 | _preloaded.renderedColor = color; 398 | _preloaded.resizedImage = QImage(); 399 | _preloaded.colorizedColor = QColor(); // Mark colorizedImage as invalid. 400 | _preloadState = PreloadState::Ready; 401 | crl::on_main(_weak, [=] { 402 | _weak->frameJumpFinished(); 403 | }); 404 | } 405 | 406 | Icon::Icon(IconDescriptor &&descriptor) 407 | : _inner(std::make_shared( 408 | descriptor.frame, 409 | base::make_weak(this), 410 | descriptor.limitFps)) 411 | , _color(descriptor.color) 412 | , _animationFrameTo(descriptor.frame) 413 | , _colorizeUsingAlpha(descriptor.colorizeUsingAlpha) { 414 | crl::async([ 415 | inner = _inner, 416 | name = descriptor.name, 417 | path = descriptor.path, 418 | bytes = descriptor.json, 419 | sizeOverride = descriptor.sizeOverride, 420 | color = (_color ? (*_color)->c : Qt::white) 421 | ] { 422 | inner->prepareFromAsync(name, path, bytes, sizeOverride, color); 423 | }); 424 | } 425 | 426 | void Icon::wait() const { 427 | _inner->waitTillPrepared(); 428 | } 429 | 430 | bool Icon::valid() const { 431 | return _inner->valid(); 432 | } 433 | 434 | int Icon::frameIndex() const { 435 | preloadNextFrame(); 436 | return _inner->frame().index; 437 | } 438 | 439 | int Icon::framesCount() const { 440 | return _inner->framesCount(); 441 | } 442 | 443 | QImage Icon::frame() const { 444 | return frame(QSize(), nullptr).image; 445 | } 446 | 447 | Icon::ResizedFrame Icon::frame( 448 | QSize desiredSize, 449 | Fn updateWithPerfect) const { 450 | preloadNextFrame(desiredSize); 451 | 452 | const auto desired = size() * style::DevicePixelRatio(); 453 | auto &frame = _inner->frame(); 454 | if (frame.renderedImage.isNull()) { 455 | return { frame.renderedImage }; 456 | } else if (!_color) { 457 | if (frame.renderedImage.size() == desired) { 458 | return { frame.renderedImage }; 459 | } else if (frame.resizedImage.size() != desired) { 460 | frame.resizedImage = frame.renderedImage.scaled( 461 | desired, 462 | Qt::IgnoreAspectRatio, 463 | Qt::SmoothTransformation); 464 | } 465 | if (updateWithPerfect) { 466 | _repaint = std::move(updateWithPerfect); 467 | } 468 | return { frame.resizedImage, true }; 469 | } 470 | Assert(frame.renderedImage.size() == desired); 471 | const auto color = (*_color)->c; 472 | if (color == frame.renderedColor) { 473 | return { frame.renderedImage }; 474 | } else if (!frame.colorizedImage.isNull() 475 | && color == frame.colorizedColor) { 476 | return { frame.colorizedImage }; 477 | } 478 | if (frame.colorizedImage.isNull()) { 479 | frame.colorizedImage = CreateFrameStorage(desired); 480 | } 481 | frame.colorizedColor = color; 482 | style::colorizeImage( 483 | frame.renderedImage, 484 | color, 485 | &frame.colorizedImage, 486 | QRect(), 487 | QPoint(), 488 | _colorizeUsingAlpha); 489 | return { frame.colorizedImage }; 490 | } 491 | 492 | int Icon::width() const { 493 | return size().width(); 494 | } 495 | 496 | int Icon::height() const { 497 | return size().height(); 498 | } 499 | 500 | QSize Icon::size() const { 501 | return _inner->size(); 502 | } 503 | 504 | void Icon::paint( 505 | QPainter &p, 506 | int x, 507 | int y, 508 | std::optional colorOverride) { 509 | preloadNextFrame(); 510 | auto &frame = _inner->frame(); 511 | const auto color = colorOverride.value_or( 512 | _color ? (*_color)->c : Qt::white); 513 | if (frame.renderedImage.isNull() || color.alpha() == 0) { 514 | return; 515 | } 516 | const auto rect = QRect{ QPoint(x, y), size() }; 517 | if (color == frame.renderedColor || !_color) { 518 | p.drawImage(rect, frame.renderedImage); 519 | } else if (color.alphaF() < 1. 520 | && (QColor(color.red(), color.green(), color.blue()) 521 | == frame.renderedColor)) { 522 | const auto o = p.opacity(); 523 | p.setOpacity(o * color.alphaF()); 524 | p.drawImage(rect, frame.renderedImage); 525 | p.setOpacity(o); 526 | } else if (!frame.colorizedImage.isNull() 527 | && color == frame.colorizedColor) { 528 | p.drawImage(rect, frame.colorizedImage); 529 | } else if (!frame.colorizedImage.isNull() 530 | && color.alphaF() < 1. 531 | && (QColor(color.red(), color.green(), color.blue()) 532 | == frame.colorizedColor)) { 533 | const auto o = p.opacity(); 534 | p.setOpacity(o * color.alphaF()); 535 | p.drawImage(rect, frame.colorizedImage); 536 | p.setOpacity(o); 537 | } else { 538 | if (frame.colorizedImage.isNull()) { 539 | frame.colorizedImage = CreateFrameStorage( 540 | frame.renderedImage.size()); 541 | } 542 | frame.colorizedColor = color; 543 | style::colorizeImage( 544 | frame.renderedImage, 545 | color, 546 | &frame.colorizedImage, 547 | QRect(), 548 | QPoint(), 549 | _colorizeUsingAlpha); 550 | p.drawImage(rect, frame.colorizedImage); 551 | } 552 | } 553 | 554 | void Icon::paintInCenter( 555 | QPainter &p, 556 | QRect rect, 557 | std::optional colorOverride) { 558 | const auto my = size(); 559 | paint( 560 | p, 561 | rect.x() + (rect.width() - my.width()) / 2, 562 | rect.y() + (rect.height() - my.height()) / 2, 563 | colorOverride); 564 | } 565 | 566 | void Icon::animate( 567 | Fn update, 568 | int frameFrom, 569 | int frameTo, 570 | std::optional duration) { 571 | jumpTo(frameFrom, std::move(update)); 572 | if (frameFrom != frameTo) { 573 | _animationFrameTo = frameTo; 574 | _animation.start( 575 | [=] { 576 | preloadNextFrame(); 577 | if (_repaint) { 578 | _repaint(); 579 | } 580 | }, 581 | frameFrom, 582 | frameTo, 583 | (duration 584 | ? *duration 585 | : _inner->animationDuration(frameFrom, frameTo))); 586 | } 587 | } 588 | 589 | void Icon::jumpTo(int frame, Fn update) { 590 | _animation.stop(); 591 | _repaint = std::move(update); 592 | _animationFrameTo = frame; 593 | preloadNextFrame(); 594 | } 595 | 596 | void Icon::frameJumpFinished() { 597 | if (_repaint && !animating()) { 598 | _repaint(); 599 | _repaint = nullptr; 600 | } 601 | } 602 | 603 | int Icon::wantedFrameIndex() const { 604 | return int(base::SafeRound(_animation.value(_animationFrameTo))); 605 | } 606 | 607 | void Icon::preloadNextFrame(QSize updatedDesiredSize) const { 608 | _inner->moveToFrame( 609 | wantedFrameIndex(), 610 | _color ? (*_color)->c : Qt::white, 611 | updatedDesiredSize); 612 | if (_animationFrameTo < 0) { 613 | _animationFrameTo += framesCount(); 614 | } 615 | } 616 | 617 | bool Icon::animating() const { 618 | return _animation.animating(); 619 | } 620 | 621 | std::unique_ptr MakeIcon(IconDescriptor &&descriptor) { 622 | return std::make_unique(std::move(descriptor)); 623 | } 624 | 625 | std::unique_ptr MakeEmoji( 626 | IconDescriptor &&descriptor, 627 | Fn repaint) { 628 | return std::make_unique( 629 | std::move(descriptor), 630 | repaint); 631 | } 632 | 633 | } // namespace Lottie 634 | -------------------------------------------------------------------------------- /lottie/lottie_icon.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "ui/style/style_core_types.h" 10 | #include "ui/effects/animations.h" 11 | #include "base/weak_ptr.h" 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace Ui::Text { 18 | class CustomEmoji; 19 | } // namespace Ui::Text 20 | 21 | namespace Lottie { 22 | 23 | struct IconDescriptor { 24 | QString name; 25 | QString path; 26 | QByteArray json; 27 | const style::color *color = nullptr; 28 | QSize sizeOverride; 29 | int frame = 0; 30 | bool limitFps = false; 31 | bool colorizeUsingAlpha = false; 32 | }; 33 | 34 | class Icon final : public base::has_weak_ptr { 35 | public: 36 | explicit Icon(IconDescriptor &&descriptor); 37 | Icon(const Icon &other) = delete; 38 | Icon &operator=(const Icon &other) = delete; 39 | Icon(Icon &&other) = delete; // _animation captures 'this'. 40 | Icon &operator=(Icon &&other) = delete; 41 | 42 | [[nodiscard]] bool valid() const; 43 | [[nodiscard]] int frameIndex() const; 44 | [[nodiscard]] int framesCount() const; 45 | [[nodiscard]] QImage frame() const; 46 | [[nodiscard]] int width() const; 47 | [[nodiscard]] int height() const; 48 | [[nodiscard]] QSize size() const; 49 | 50 | struct ResizedFrame { 51 | QImage image; 52 | bool scaled = false; 53 | }; 54 | [[nodiscard]] ResizedFrame frame( 55 | QSize desiredSize, 56 | Fn updateWithPerfect) const; 57 | 58 | void paint( 59 | QPainter &p, 60 | int x, 61 | int y, 62 | std::optional colorOverride = std::nullopt); 63 | void paintInCenter( 64 | QPainter &p, 65 | QRect rect, 66 | std::optional colorOverride = std::nullopt); 67 | void animate( 68 | Fn update, 69 | int frameFrom, 70 | int frameTo, 71 | std::optional duration = std::nullopt); 72 | void jumpTo(int frame, Fn update); 73 | [[nodiscard]] bool animating() const; 74 | 75 | private: 76 | struct Frame; 77 | class Inner; 78 | friend class Inner; 79 | 80 | void wait() const; 81 | [[nodiscard]] int wantedFrameIndex() const; 82 | void preloadNextFrame(QSize updatedDesiredSize = QSize()) const; 83 | void frameJumpFinished(); 84 | 85 | std::shared_ptr _inner; 86 | const style::color *_color = nullptr; 87 | Ui::Animations::Simple _animation; 88 | mutable int _animationFrameTo = 0; 89 | const bool _colorizeUsingAlpha = false; 90 | mutable Fn _repaint; 91 | 92 | }; 93 | 94 | [[nodiscard]] std::unique_ptr MakeIcon(IconDescriptor &&descriptor); 95 | [[nodiscard]] std::unique_ptr MakeEmoji( 96 | IconDescriptor &&descriptor, 97 | Fn repaint); 98 | 99 | } // namespace Lottie 100 | -------------------------------------------------------------------------------- /lottie/lottie_multi_player.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/lottie_multi_player.h" 8 | 9 | #include "lottie/details/lottie_frame_renderer.h" 10 | #include "lottie/lottie_animation.h" 11 | 12 | #include 13 | 14 | namespace Lottie { 15 | 16 | MultiPlayer::MultiPlayer( 17 | Quality quality, 18 | std::shared_ptr renderer) 19 | : _quality(quality) 20 | , _timer([=] { checkNextFrameRender(); }) 21 | , _renderer(renderer ? std::move(renderer) : FrameRenderer::Instance()) { 22 | crl::on_main_update_requests( 23 | ) | rpl::start_with_next([=] { 24 | checkStep(); 25 | }, _lifetime); 26 | } 27 | 28 | MultiPlayer::~MultiPlayer() { 29 | for (const auto &[animation, state] : _active) { 30 | _renderer->remove(state); 31 | } 32 | for (const auto &[animation, info] : _paused) { 33 | _renderer->remove(info.state); 34 | } 35 | } 36 | 37 | not_null MultiPlayer::append( 38 | FnMut)> get, // Main thread. 39 | FnMut put, // Unknown thread. 40 | const QByteArray &content, 41 | const FrameRequest &request) { 42 | #ifdef LOTTIE_USE_CACHE 43 | _animations.push_back(std::make_unique( 44 | this, 45 | std::move(get), 46 | std::move(put), 47 | content, 48 | request, 49 | _quality)); 50 | return _animations.back().get(); 51 | #else // LOTTIE_USE_CACHE 52 | return append(content, request); 53 | #endif // LOTTIE_USE_CACHE 54 | } 55 | 56 | not_null MultiPlayer::append( 57 | const QByteArray &content, 58 | const FrameRequest &request) { 59 | _animations.push_back(std::make_unique( 60 | this, 61 | content, 62 | request, 63 | _quality)); 64 | return _animations.back().get(); 65 | } 66 | 67 | void MultiPlayer::startAtRightTime(std::unique_ptr state) { 68 | if (_started == kTimeUnknown) { 69 | _started = crl::now(); 70 | _lastSyncTime = kTimeUnknown; 71 | _delay = 0; 72 | } 73 | const auto lastSyncTime = (_lastSyncTime != kTimeUnknown) 74 | ? _lastSyncTime 75 | : _started; 76 | const auto frameIndex = countFrameIndex( 77 | state.get(), 78 | lastSyncTime, 79 | _delay); 80 | state->start(this, _started, _delay, frameIndex); 81 | const auto request = state->frameForPaint()->request; 82 | _renderer->append(std::move(state), request); 83 | } 84 | 85 | int MultiPlayer::countFrameIndex( 86 | not_null state, 87 | crl::time time, 88 | crl::time delay) const { 89 | Expects(time != kTimeUnknown); 90 | 91 | const auto rate = state->information().frameRate; 92 | Assert(rate != 0); 93 | 94 | const auto framesTime = time - _started - delay; 95 | return ((framesTime + 1) * rate - 1) / 1000; 96 | } 97 | 98 | void MultiPlayer::start( 99 | not_null animation, 100 | std::unique_ptr state) { 101 | Expects(state != nullptr); 102 | 103 | const auto paused = _pausedBeforeStart.remove(animation); 104 | auto info = StartingInfo{ std::move(state), paused }; 105 | if (_active.empty() 106 | || (_lastSyncTime == kTimeUnknown 107 | && _nextFrameTime == kTimeUnknown)) { 108 | addNewToActive(animation, std::move(info)); 109 | } else { 110 | // We always try to mark as shown at the same time, so we start a new 111 | // animation at the same time we mark all existing as shown. 112 | _pendingToStart.emplace(animation, std::move(info)); 113 | } 114 | _updates.fire({}); 115 | } 116 | 117 | void MultiPlayer::addNewToActive( 118 | not_null animation, 119 | StartingInfo &&info) { 120 | _active.emplace(animation, info.state.get()); 121 | startAtRightTime(std::move(info.state)); 122 | if (info.paused) { 123 | _pendingPause.emplace(animation); 124 | } 125 | } 126 | 127 | void MultiPlayer::processPending() { 128 | Expects(_lastSyncTime != kTimeUnknown); 129 | 130 | for (const auto &animation : base::take(_pendingPause)) { 131 | pauseAndSaveState(animation); 132 | } 133 | for (const auto &animation : base::take(_pendingUnpause)) { 134 | unpauseAndKeepUp(animation); 135 | } 136 | for (auto &[animation, info] : base::take(_pendingToStart)) { 137 | addNewToActive(animation, std::move(info)); 138 | } 139 | for (const auto &animation : base::take(_pendingRemove)) { 140 | removeNow(animation); 141 | } 142 | } 143 | 144 | void MultiPlayer::remove(not_null animation) { 145 | if (!_active.empty()) { 146 | _pendingRemove.emplace(animation); 147 | } else { 148 | removeNow(animation); 149 | } 150 | } 151 | 152 | void MultiPlayer::removeNow(not_null animation) { 153 | const auto i = _active.find(animation); 154 | if (i != end(_active)) { 155 | _renderer->remove(i->second); 156 | _active.erase(i); 157 | } 158 | const auto j = _paused.find(animation); 159 | if (j != end(_paused)) { 160 | _renderer->remove(j->second.state); 161 | _paused.erase(j); 162 | } 163 | 164 | _pendingRemove.remove(animation); 165 | _pendingToStart.remove(animation); 166 | _pendingPause.remove(animation); 167 | _pendingUnpause.remove(animation); 168 | _pausedBeforeStart.remove(animation); 169 | _animations.erase( 170 | ranges::remove( 171 | _animations, 172 | animation.get(), 173 | &std::unique_ptr::get), 174 | end(_animations)); 175 | 176 | if (_active.empty()) { 177 | _nextFrameTime = kTimeUnknown; 178 | _timer.cancel(); 179 | if (_paused.empty()) { 180 | _started = kTimeUnknown; 181 | _lastSyncTime = kTimeUnknown; 182 | _delay = 0; 183 | } 184 | } 185 | } 186 | 187 | void MultiPlayer::pause(not_null animation) { 188 | if (_active.contains(animation)) { 189 | _pendingPause.emplace(animation); 190 | } else if (_paused.contains(animation)) { 191 | _pendingUnpause.remove(animation); 192 | } else if (const auto i = _pendingToStart.find(animation); i != end(_pendingToStart)) { 193 | i->second.paused = true; 194 | } else { 195 | _pausedBeforeStart.emplace(animation); 196 | } 197 | } 198 | 199 | void MultiPlayer::unpause(not_null animation) { 200 | if (const auto i = _paused.find(animation); i != end(_paused)) { 201 | if (_active.empty()) { 202 | unpauseFirst(animation, i->second.state); 203 | _paused.erase(i); 204 | } else { 205 | _pendingUnpause.emplace(animation); 206 | } 207 | } else if (_pendingPause.contains(animation)) { 208 | _pendingPause.remove(animation); 209 | } else { 210 | const auto i = _pendingToStart.find(animation); 211 | if (i != end(_pendingToStart)) { 212 | i->second.paused = false; 213 | } else { 214 | _pausedBeforeStart.remove(animation); 215 | } 216 | } 217 | } 218 | 219 | void MultiPlayer::unpauseFirst( 220 | not_null animation, 221 | not_null state) { 222 | Expects(_lastSyncTime != kTimeUnknown); 223 | 224 | _active.emplace(animation, state); 225 | 226 | const auto now = crl::now(); 227 | addTimelineDelay(now - _lastSyncTime); 228 | _lastSyncTime = now; 229 | 230 | markFrameShown(); 231 | } 232 | 233 | void MultiPlayer::pauseAndSaveState(not_null animation) { 234 | Expects(_lastSyncTime != kTimeUnknown); 235 | 236 | const auto i = _active.find(animation); 237 | Assert(i != end(_active)); 238 | _paused.emplace( 239 | animation, 240 | PausedInfo{ i->second, _lastSyncTime, _delay }); 241 | _active.erase(i); 242 | } 243 | 244 | void MultiPlayer::unpauseAndKeepUp(not_null animation) { 245 | Expects(_lastSyncTime != kTimeUnknown); 246 | 247 | const auto i = _paused.find(animation); 248 | Assert(i != end(_paused)); 249 | const auto state = i->second.state; 250 | const auto frameIndexAtPaused = countFrameIndex( 251 | state, 252 | i->second.pauseTime, 253 | i->second.pauseDelay); 254 | const auto frameIndexNow = countFrameIndex( 255 | state, 256 | _lastSyncTime, 257 | _delay); 258 | state->addTimelineDelay( 259 | (_delay - i->second.pauseDelay), 260 | frameIndexNow - frameIndexAtPaused); 261 | _active.emplace(animation, state); 262 | _paused.erase(i); 263 | } 264 | 265 | void MultiPlayer::failed(not_null animation, Error error) { 266 | //_updates.fire({ animation, error }); 267 | } 268 | 269 | rpl::producer MultiPlayer::updates() const { 270 | return _updates.events(); 271 | } 272 | 273 | void MultiPlayer::checkStep() { 274 | if (_active.empty() || _nextFrameTime == kFrameDisplayTimeAlreadyDone) { 275 | return; 276 | } else if (_nextFrameTime != kTimeUnknown) { 277 | checkNextFrameRender(); 278 | } else { 279 | checkNextFrameAvailability(); 280 | } 281 | } 282 | 283 | void MultiPlayer::checkNextFrameAvailability() { 284 | Expects(_nextFrameTime == kTimeUnknown); 285 | 286 | auto next = kTimeUnknown; 287 | for (const auto &[animation, state] : _active) { 288 | const auto time = state->nextFrameDisplayTime(); 289 | if (time == kTimeUnknown) { 290 | for (const auto &[animation, state] : _active) { 291 | if (state->nextFrameDisplayTime() != kTimeUnknown) { 292 | break; 293 | } 294 | } 295 | return; 296 | } else if (time == kFrameDisplayTimeAlreadyDone) { 297 | continue; 298 | } 299 | if (next == kTimeUnknown || next > time) { 300 | next = time; 301 | } 302 | } 303 | if (next == kTimeUnknown) { 304 | return; 305 | } 306 | _nextFrameTime = next; 307 | checkNextFrameRender(); 308 | } 309 | 310 | void MultiPlayer::checkNextFrameRender() { 311 | Expects(_nextFrameTime != kTimeUnknown); 312 | 313 | const auto now = crl::now(); 314 | if (now < _nextFrameTime) { 315 | if (!_timer.isActive()) { 316 | _timer.callOnce(_nextFrameTime - now); 317 | } 318 | } else { 319 | _timer.cancel(); 320 | 321 | markFrameDisplayed(now); 322 | addTimelineDelay(now - _nextFrameTime); 323 | _lastSyncTime = now; 324 | _nextFrameTime = kFrameDisplayTimeAlreadyDone; 325 | processPending(); 326 | _updates.fire({}); 327 | } 328 | } 329 | 330 | void MultiPlayer::updateFrameRequest( 331 | not_null animation, 332 | const FrameRequest &request) { 333 | const auto state = [&]() -> Lottie::SharedState* { 334 | if (const auto i = _active.find(animation); i != end(_active)) { 335 | return i->second; 336 | } else if (const auto j = _paused.find(animation); 337 | j != end(_paused)) { 338 | return j->second.state; 339 | } else if (const auto k = _pendingToStart.find(animation); 340 | k != end(_pendingToStart)) { 341 | return nullptr; 342 | } 343 | Unexpected("Animation in MultiPlayer::updateFrameRequest."); 344 | }(); 345 | if (state) { 346 | _renderer->updateFrameRequest(state, request); 347 | } 348 | } 349 | 350 | void MultiPlayer::markFrameDisplayed(crl::time now) { 351 | Expects(!_active.empty()); 352 | 353 | for (const auto &[animation, state] : _active) { 354 | const auto time = state->nextFrameDisplayTime(); 355 | Assert(time != kTimeUnknown); 356 | if (time == kFrameDisplayTimeAlreadyDone) { 357 | continue; 358 | } else if (now >= time) { 359 | state->markFrameDisplayed(now); 360 | } 361 | } 362 | } 363 | 364 | void MultiPlayer::addTimelineDelay(crl::time delayed) { 365 | Expects(!_active.empty()); 366 | 367 | for (const auto &[animation, state] : _active) { 368 | state->addTimelineDelay(delayed); 369 | } 370 | _delay += delayed; 371 | } 372 | 373 | bool MultiPlayer::markFrameShown() { 374 | if (_nextFrameTime == kFrameDisplayTimeAlreadyDone) { 375 | _nextFrameTime = kTimeUnknown; 376 | } 377 | auto count = 0; 378 | for (const auto &[animation, state] : _active) { 379 | if (state->markFrameShown()) { 380 | ++count; 381 | } 382 | } 383 | if (count) { 384 | _renderer->frameShown(); 385 | return true; 386 | } 387 | return false; 388 | } 389 | 390 | } // namespace Lottie 391 | -------------------------------------------------------------------------------- /lottie/lottie_multi_player.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "lottie/lottie_player.h" 10 | #include "base/timer.h" 11 | #include "base/algorithm.h" 12 | #include "base/flat_set.h" 13 | #include "base/flat_map.h" 14 | 15 | #include 16 | 17 | namespace Lottie { 18 | 19 | class Animation; 20 | class FrameRenderer; 21 | 22 | struct MultiUpdate { 23 | //std::variant< 24 | // std::pair, 25 | // DisplayMultiFrameRequest, 26 | // std::pair> data; 27 | }; 28 | 29 | class MultiPlayer final : public Player { 30 | public: 31 | MultiPlayer( 32 | Quality quality = Quality::Default, 33 | std::shared_ptr renderer = nullptr); 34 | ~MultiPlayer(); 35 | 36 | void start( 37 | not_null animation, 38 | std::unique_ptr state) override; 39 | void failed(not_null animation, Error error) override; 40 | void updateFrameRequest( 41 | not_null animation, 42 | const FrameRequest &request) override; 43 | bool markFrameShown() override; 44 | void checkStep() override; 45 | 46 | not_null append( 47 | const QByteArray &content, 48 | const FrameRequest &request); 49 | not_null append( 50 | FnMut)> get, // Main thread. 51 | FnMut put, // Unknown thread. 52 | const QByteArray &content, 53 | const FrameRequest &request); 54 | 55 | rpl::producer updates() const; 56 | 57 | void remove(not_null animation); 58 | 59 | void pause(not_null animation); 60 | void unpause(not_null animation); 61 | 62 | private: 63 | struct PausedInfo { 64 | not_null state; 65 | crl::time pauseTime = kTimeUnknown; 66 | crl::time pauseDelay = kTimeUnknown; 67 | }; 68 | struct StartingInfo { 69 | std::unique_ptr state; 70 | bool paused = false; 71 | }; 72 | 73 | void addNewToActive( 74 | not_null animation, 75 | StartingInfo &&info); 76 | [[nodiscard]] int countFrameIndex( 77 | not_null state, 78 | crl::time time, 79 | crl::time delay) const; 80 | void startAtRightTime(std::unique_ptr state); 81 | void processPending(); 82 | void markFrameDisplayed(crl::time now); 83 | void addTimelineDelay(crl::time delayed); 84 | void checkNextFrameAvailability(); 85 | void checkNextFrameRender(); 86 | void unpauseFirst( 87 | not_null animation, 88 | not_null state); 89 | void pauseAndSaveState(not_null animation); 90 | void unpauseAndKeepUp(not_null animation); 91 | void removeNow(not_null animation); 92 | 93 | Quality _quality = Quality::Default; 94 | base::Timer _timer; 95 | const std::shared_ptr _renderer; 96 | std::vector> _animations; 97 | base::flat_map, not_null> _active; 98 | base::flat_map, PausedInfo> _paused; 99 | base::flat_set> _pendingPause; 100 | base::flat_set> _pendingUnpause; 101 | base::flat_set> _pausedBeforeStart; 102 | base::flat_set> _pendingRemove; 103 | base::flat_map, StartingInfo> _pendingToStart; 104 | crl::time _started = kTimeUnknown; 105 | crl::time _lastSyncTime = kTimeUnknown; 106 | crl::time _delay = 0; 107 | crl::time _nextFrameTime = kTimeUnknown; 108 | rpl::event_stream _updates; 109 | rpl::lifetime _lifetime; 110 | 111 | }; 112 | 113 | } // namespace Lottie 114 | -------------------------------------------------------------------------------- /lottie/lottie_player.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "lottie/lottie_common.h" 10 | #include "base/weak_ptr.h" 11 | 12 | #include 13 | 14 | namespace Lottie { 15 | 16 | class SharedState; 17 | 18 | class Player : public base::has_weak_ptr { 19 | public: 20 | virtual void start( 21 | not_null animation, 22 | std::unique_ptr state) = 0; 23 | virtual void failed(not_null animation, Error error) = 0; 24 | virtual void updateFrameRequest( 25 | not_null animation, 26 | const FrameRequest &request) = 0; 27 | virtual bool markFrameShown() = 0; 28 | virtual void checkStep() = 0; 29 | 30 | virtual ~Player() = default; 31 | 32 | }; 33 | 34 | } // namespace Lottie 35 | -------------------------------------------------------------------------------- /lottie/lottie_single_player.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #include "lottie/lottie_single_player.h" 8 | 9 | #include "lottie/details/lottie_frame_renderer.h" 10 | #include "lottie/details/lottie_frame_provider_shared.h" 11 | #include "lottie/details/lottie_frame_provider_direct.h" 12 | 13 | #ifdef LOTTIE_USE_CACHE 14 | #include "lottie/details/lottie_frame_provider_cached_multi.h" 15 | #endif // LOTTIE_USE_CACHE 16 | 17 | #include 18 | 19 | namespace Lottie { 20 | 21 | SinglePlayer::SinglePlayer( 22 | const QByteArray &content, 23 | const FrameRequest &request, 24 | Quality quality, 25 | const ColorReplacements *replacements, 26 | std::shared_ptr renderer) 27 | : _timer([=] { checkNextFrameRender(); }) 28 | , _renderer(renderer ? renderer : FrameRenderer::Instance()) 29 | , _animation(this, content, request, quality, replacements) { 30 | } 31 | 32 | SinglePlayer::SinglePlayer( 33 | FnMut)> get, // Main thread. 34 | FnMut put, // Unknown thread. 35 | const QByteArray &content, 36 | const FrameRequest &request, 37 | Quality quality, 38 | const ColorReplacements *replacements, 39 | std::shared_ptr renderer) 40 | : _timer([=] { checkNextFrameRender(); }) 41 | , _renderer(renderer ? renderer : FrameRenderer::Instance()) 42 | , _animation( 43 | this, 44 | std::move(get), 45 | std::move(put), 46 | content, 47 | request, 48 | quality, 49 | replacements) { 50 | } 51 | 52 | SinglePlayer::SinglePlayer( 53 | int keysCount, 54 | FnMut)> get, 55 | FnMut put, 56 | const QByteArray &content, 57 | const FrameRequest &request, 58 | Quality quality, 59 | const ColorReplacements *replacements, 60 | std::shared_ptr renderer) 61 | : _timer([=] { checkNextFrameRender(); }) 62 | , _renderer(renderer ? renderer : FrameRenderer::Instance()) 63 | , _animation( 64 | this, 65 | keysCount, 66 | std::move(get), 67 | std::move(put), 68 | content, 69 | request, 70 | quality, 71 | replacements) { 72 | } 73 | 74 | SinglePlayer::~SinglePlayer() { 75 | if (_state) { 76 | _renderer->remove(_state); 77 | } 78 | } 79 | 80 | std::shared_ptr SinglePlayer::SharedProvider( 81 | int keysCount, 82 | FnMut)> get, 83 | FnMut put, 84 | const QByteArray &content, 85 | const FrameRequest &request, 86 | Quality quality, 87 | const ColorReplacements *replacements) { 88 | auto factory = [=, get = std::move(get), put = std::move(put)]( 89 | FnMut)> done) mutable { 90 | #ifdef LOTTIE_USE_CACHE 91 | struct State { 92 | std::atomic left = 0; 93 | std::vector caches; 94 | FnMut put; 95 | FnMut)> done; 96 | }; 97 | const auto state = std::make_shared(); 98 | state->left = keysCount; 99 | state->put = std::move(put); 100 | state->done = std::move(done); 101 | state->caches.resize(keysCount); 102 | for (auto i = 0; i != keysCount; ++i) { 103 | get(i, [=](QByteArray &&cached) { 104 | state->caches[i] = std::move(cached); 105 | if (--state->left) { 106 | return; 107 | } 108 | crl::async([=, done = std::move(state->done)]() mutable { 109 | if (const auto error = ContentError(content)) { 110 | done(nullptr); 111 | return; 112 | } 113 | auto provider = std::make_unique( 114 | content, 115 | std::move(state->put), 116 | std::move(state->caches), 117 | request, 118 | quality, 119 | replacements); 120 | done(provider->valid() ? std::move(provider) : nullptr); 121 | }); 122 | }); 123 | } 124 | #else // LOTTIE_USE_CACHE 125 | crl::async([=, done = std::move(done)]() mutable { 126 | if (const auto error = ContentError(content)) { 127 | done(nullptr); 128 | return; 129 | } 130 | auto provider = std::make_unique(quality); 131 | done(provider->load(content, replacements) 132 | ? std::move(provider) 133 | : nullptr); 134 | }); 135 | #endif // LOTTIE_USE_CACHE 136 | }; 137 | return std::make_shared(std::move(factory)); 138 | } 139 | 140 | SinglePlayer::SinglePlayer( 141 | std::shared_ptr provider, 142 | const FrameRequest &request, 143 | std::shared_ptr renderer) 144 | : _timer([=] { checkNextFrameRender(); }) 145 | , _renderer(renderer ? renderer : FrameRenderer::Instance()) 146 | , _animation(this, std::move(provider), request) { 147 | } 148 | 149 | void SinglePlayer::start( 150 | not_null animation, 151 | std::unique_ptr state) { 152 | Expects(animation == &_animation); 153 | 154 | _state = state.get(); 155 | auto information = state->information(); 156 | state->start(this, crl::now()); 157 | const auto request = state->frameForPaint()->request; 158 | _renderer->append(std::move(state), request); 159 | 160 | crl::on_main_update_requests( 161 | ) | rpl::start_with_next([=] { 162 | checkStep(); 163 | }, _lifetime); 164 | 165 | // This may destroy the player. 166 | _updates.fire({ std::move(information) }); 167 | } 168 | 169 | void SinglePlayer::failed(not_null animation, Error error) { 170 | Expects(animation == &_animation); 171 | 172 | _updates.fire_error(std::move(error)); 173 | } 174 | 175 | rpl::producer SinglePlayer::updates() const { 176 | return _updates.events(); 177 | } 178 | 179 | bool SinglePlayer::ready() const { 180 | return _animation.ready(); 181 | } 182 | 183 | QImage SinglePlayer::frame() const { 184 | return _animation.frame(); 185 | } 186 | 187 | QImage SinglePlayer::frame(const FrameRequest &request) const { 188 | return _animation.frame(request); 189 | } 190 | 191 | Animation::FrameInfo SinglePlayer::frameInfo( 192 | const FrameRequest &request) const { 193 | return _animation.frameInfo(request); 194 | } 195 | 196 | int SinglePlayer::frameIndex() const { 197 | return _animation.frameIndex(); 198 | } 199 | 200 | int SinglePlayer::framesCount() const { 201 | return _animation.framesCount(); 202 | } 203 | 204 | Information SinglePlayer::information() const { 205 | return _animation.information(); 206 | } 207 | 208 | void SinglePlayer::checkStep() { 209 | if (_nextFrameTime == kFrameDisplayTimeAlreadyDone) { 210 | return; 211 | } else if (_nextFrameTime != kTimeUnknown) { 212 | checkNextFrameRender(); 213 | } else { 214 | checkNextFrameAvailability(); 215 | } 216 | } 217 | 218 | void SinglePlayer::checkNextFrameAvailability() { 219 | Expects(_state != nullptr); 220 | Expects(_nextFrameTime == kTimeUnknown); 221 | 222 | _nextFrameTime = _state->nextFrameDisplayTime(); 223 | Assert(_nextFrameTime != kFrameDisplayTimeAlreadyDone); 224 | if (_nextFrameTime != kTimeUnknown) { 225 | checkNextFrameRender(); 226 | } 227 | } 228 | 229 | void SinglePlayer::checkNextFrameRender() { 230 | Expects(_nextFrameTime != kTimeUnknown); 231 | 232 | const auto now = crl::now(); 233 | if (now < _nextFrameTime) { 234 | if (!_timer.isActive()) { 235 | _timer.callOnce(_nextFrameTime - now); 236 | } 237 | } else { 238 | _timer.cancel(); 239 | renderFrame(now); 240 | } 241 | } 242 | 243 | void SinglePlayer::renderFrame(crl::time now) { 244 | _state->markFrameDisplayed(now); 245 | _state->addTimelineDelay(now - _nextFrameTime); 246 | 247 | _nextFrameTime = kFrameDisplayTimeAlreadyDone; 248 | _updates.fire({ DisplayFrameRequest() }); 249 | } 250 | 251 | void SinglePlayer::updateFrameRequest( 252 | not_null animation, 253 | const FrameRequest &request) { 254 | Expects(animation == &_animation); 255 | Expects(_state != nullptr); 256 | 257 | _renderer->updateFrameRequest(_state, request); 258 | } 259 | 260 | bool SinglePlayer::markFrameShown() { 261 | Expects(_state != nullptr); 262 | 263 | if (_nextFrameTime == kFrameDisplayTimeAlreadyDone) { 264 | _nextFrameTime = kTimeUnknown; 265 | } 266 | if (_state->markFrameShown()) { 267 | _renderer->frameShown(); 268 | return true; 269 | } 270 | return false; 271 | } 272 | 273 | } // namespace Lottie 274 | -------------------------------------------------------------------------------- /lottie/lottie_single_player.h: -------------------------------------------------------------------------------- 1 | // This file is part of Desktop App Toolkit, 2 | // a set of libraries for developing nice desktop applications. 3 | // 4 | // For license and copyright information please follow this link: 5 | // https://github.com/desktop-app/legal/blob/master/LEGAL 6 | // 7 | #pragma once 8 | 9 | #include "lottie/lottie_player.h" 10 | #include "lottie/lottie_animation.h" 11 | #include "base/timer.h" 12 | 13 | #include 14 | 15 | namespace Lottie { 16 | 17 | class FrameRenderer; 18 | class FrameProvider; 19 | 20 | struct DisplayFrameRequest { 21 | }; 22 | 23 | struct Update { 24 | std::variant< 25 | Information, 26 | DisplayFrameRequest> data; 27 | }; 28 | 29 | class SinglePlayer final : public Player { 30 | public: 31 | SinglePlayer( 32 | const QByteArray &content, 33 | const FrameRequest &request, 34 | Quality quality = Quality::Default, 35 | const ColorReplacements *replacements = nullptr, 36 | std::shared_ptr renderer = nullptr); 37 | SinglePlayer( 38 | FnMut)> get, // Main thread. 39 | FnMut put, // Unknown thread. 40 | const QByteArray &content, 41 | const FrameRequest &request, 42 | Quality quality = Quality::Default, 43 | const ColorReplacements *replacements = nullptr, 44 | std::shared_ptr renderer = nullptr); 45 | SinglePlayer( // Multi-cache version. 46 | int keysCount, 47 | FnMut)> get, 48 | FnMut put, 49 | const QByteArray &content, 50 | const FrameRequest &request, 51 | Quality quality = Quality::Default, 52 | const ColorReplacements *replacements = nullptr, 53 | std::shared_ptr renderer = nullptr); 54 | ~SinglePlayer(); 55 | 56 | [[nodiscard]] static std::shared_ptr SharedProvider( 57 | int keysCount, 58 | FnMut)> get, 59 | FnMut put, 60 | const QByteArray &content, 61 | const FrameRequest &request, 62 | Quality quality = Quality::Default, 63 | const ColorReplacements *replacements = nullptr); 64 | explicit SinglePlayer( 65 | std::shared_ptr provider, 66 | const FrameRequest &request, 67 | std::shared_ptr renderer = nullptr); 68 | 69 | void start( 70 | not_null animation, 71 | std::unique_ptr state) override; 72 | void failed(not_null animation, Error error) override; 73 | void updateFrameRequest( 74 | not_null animation, 75 | const FrameRequest &request) override; 76 | bool markFrameShown() override; 77 | void checkStep() override; 78 | 79 | [[nodiscard]] rpl::producer updates() const; 80 | 81 | [[nodiscard]] bool ready() const; 82 | [[nodiscard]] QImage frame() const; 83 | [[nodiscard]] QImage frame(const FrameRequest &request) const; 84 | [[nodiscard]] Animation::FrameInfo frameInfo( 85 | const FrameRequest &request) const; 86 | [[nodiscard]] int frameIndex() const; 87 | [[nodiscard]] int framesCount() const; 88 | [[nodiscard]] Information information() const; 89 | 90 | [[nodiscard]] rpl::lifetime &lifetime() { 91 | return _lifetime; 92 | } 93 | 94 | private: 95 | void checkNextFrameAvailability(); 96 | void checkNextFrameRender(); 97 | void renderFrame(crl::time now); 98 | 99 | base::Timer _timer; 100 | const std::shared_ptr _renderer; 101 | SharedState *_state = nullptr; 102 | crl::time _nextFrameTime = kTimeUnknown; 103 | rpl::event_stream _updates; 104 | 105 | rpl::lifetime _lifetime; 106 | 107 | Animation _animation; 108 | 109 | }; 110 | 111 | } // namespace Lottie 112 | -------------------------------------------------------------------------------- /lottie/lottie_wrap.h: -------------------------------------------------------------------------------- 1 | // a set of libraries for developing nice desktop applications. 2 | // 3 | // For license and copyright information please follow this link: 4 | // https://github.com/desktop-app/legal/blob/master/LEGAL 5 | // 6 | #pragma once 7 | 8 | #include "base/debug_log.h" 9 | 10 | #include 11 | 12 | #if __has_include() 13 | #include 14 | #endif 15 | 16 | namespace Lottie { 17 | 18 | inline std::unique_ptr LoadAnimationFromData( 19 | std::string jsonData, 20 | const std::string &key, 21 | const std::string &resourcePath = "", 22 | bool cachePolicy = true, 23 | const std::vector> &colorReplacements = {}, 24 | rlottie::FitzModifier fitzModifier = rlottie::FitzModifier::None) { 25 | #ifdef LOTTIE_DISABLE_RECOLORING 26 | [[maybe_unused]] static auto logged = [&] { 27 | const auto text = "Lottie recoloring is disabled by the distributor, " 28 | "expect animations with color issues."; 29 | LOG((text)); 30 | #if __has_include() 31 | g_warning(text); 32 | #endif // __has_include() 33 | return true; 34 | }(); 35 | #endif // LOTTIE_DISABLE_RECOLORING 36 | return rlottie::Animation::loadFromData( 37 | std::move(jsonData), 38 | key, 39 | resourcePath, 40 | cachePolicy 41 | #ifndef LOTTIE_DISABLE_RECOLORING 42 | ,std::move(colorReplacements), 43 | fitzModifier 44 | #endif // !LOTTIE_DISABLE_RECOLORING 45 | ); 46 | } 47 | 48 | } // namespace Lottie 49 | --------------------------------------------------------------------------------