128 lines
5.8 KiB
C++
128 lines
5.8 KiB
C++
#include "lottieicon.h"
|
|
#include "debug_ui.h"
|
|
#include <rlottie_capi.h>
|
|
#include <zlib.h>
|
|
#include <array>
|
|
#include <limits>
|
|
|
|
#ifdef RLOTTIE_HAS_WARNING_CALLBACK
|
|
static thread_local QStringList *lottieLoadWarnings = nullptr;
|
|
|
|
static void logLottieWarning(const char *message) {
|
|
if (lottieLoadWarnings) lottieLoadWarnings->append(QString::fromUtf8(message));
|
|
else GUI_WARN("LottieIcon: rlottie: %s", message);
|
|
}
|
|
#endif
|
|
|
|
/* Весь вход уже доступен: Z_BUF_ERROR/отсутствие прогресса означают повреждение, ждать больше нечего. */
|
|
static QByteArray unpackGzip(const QByteArray &data, const QString& key) {
|
|
if (data.isEmpty() || data.size() > std::numeric_limits<int>::max()) {
|
|
GUI_ERROR("LottieIcon: invalid gzip input path=%s bytes=%lld", qPrintable(key), (long long)data.size());
|
|
return {};
|
|
}
|
|
z_stream strm = {};
|
|
int ret = inflateInit2(&strm, 16 + MAX_WBITS);
|
|
if (ret != Z_OK) { GUI_ERROR("LottieIcon: gzip init failed path=%s zlib=%d", qPrintable(key), ret); return {}; }
|
|
strm.avail_in = (uInt)data.size();
|
|
strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data.constData()));
|
|
QByteArray result;
|
|
std::array<char, 16384> chunk;
|
|
bool complete = false;
|
|
for (;;) {
|
|
const auto before = strm.total_in;
|
|
strm.avail_out = chunk.size();
|
|
strm.next_out = reinterpret_cast<Bytef*>(chunk.data());
|
|
ret = inflate(&strm, Z_NO_FLUSH);
|
|
const int produced = int(chunk.size() - strm.avail_out);
|
|
if (ret != Z_OK && ret != Z_STREAM_END) {
|
|
const char* reason = ret == Z_BUF_ERROR && !strm.avail_in ? "truncated gzip" : strm.msg ? strm.msg : zError(ret);
|
|
GUI_ERROR("LottieIcon: gzip decode failed path=%s zlib=%d input=%lu/%lld output=%lld reason=%s",
|
|
qPrintable(key), ret, strm.total_in, (long long)data.size(), (long long)result.size(), reason);
|
|
break;
|
|
}
|
|
if (result.size() > LottieIcon::MaxJsonBytes - produced) {
|
|
GUI_ERROR("LottieIcon: JSON size limit exceeded path=%s limit=%d", qPrintable(key), LottieIcon::MaxJsonBytes);
|
|
break;
|
|
}
|
|
result.append(chunk.data(), produced);
|
|
if (ret == Z_STREAM_END) {
|
|
complete = !result.isEmpty() && strm.avail_in == 0;
|
|
if (!complete) GUI_ERROR("LottieIcon: empty JSON or trailing gzip data path=%s trailing=%u", qPrintable(key), strm.avail_in);
|
|
break;
|
|
}
|
|
if (strm.total_in == before && !produced) {
|
|
GUI_ERROR("LottieIcon: gzip made no progress path=%s remaining=%u", qPrintable(key), strm.avail_in);
|
|
break;
|
|
}
|
|
}
|
|
inflateEnd(&strm);
|
|
return complete ? result : QByteArray();
|
|
}
|
|
|
|
LottieIcon::LottieIcon(const QByteArray &tgsData, const QString &key) : m_key(key) {
|
|
#ifdef RLOTTIE_HAS_WARNING_CALLBACK
|
|
lottie_set_warning_callback(logLottieWarning);
|
|
#endif
|
|
if (tgsData.size() <= std::numeric_limits<int>::max()) m_tgsSize = int(tgsData.size());
|
|
QByteArray json = unpackGzip(tgsData, key);
|
|
if (json.isEmpty()) return;
|
|
const auto nul = json.indexOf('\0');
|
|
if (nul >= 0) { GUI_ERROR("LottieIcon: JSON contains NUL path=%s offset=%lld", qPrintable(key), (long long)nul); return; }
|
|
|
|
m_jsonData = json;
|
|
QByteArray keyUtf8 = key.toUtf8();
|
|
#ifdef RLOTTIE_HAS_WARNING_CALLBACK
|
|
// При старте настройки общего журнала применяются после загрузки иконок.
|
|
QStringList *previousWarnings = lottieLoadWarnings;
|
|
lottieLoadWarnings = &m_loadWarnings;
|
|
#endif
|
|
m_anim = lottie_animation_from_data(m_jsonData.constData(), keyUtf8.constData(), "");
|
|
#ifdef RLOTTIE_HAS_WARNING_CALLBACK
|
|
lottieLoadWarnings = previousWarnings;
|
|
#endif
|
|
if (!m_anim) { logWarnings(); GUI_ERROR("LottieIcon: JSON animation load failed path=%s", qPrintable(m_key)); return; }
|
|
|
|
lottie_animation_get_size(m_anim, &m_w, &m_h);
|
|
const size_t frames = lottie_animation_get_totalframe(m_anim);
|
|
if (!frames || frames > size_t(std::numeric_limits<int>::max()) || !m_w || !m_h ||
|
|
m_w > size_t(std::numeric_limits<int>::max()) || m_h > size_t(std::numeric_limits<int>::max())) {
|
|
GUI_ERROR("LottieIcon: invalid metadata path=%s frames=%zu size=%zux%zu", qPrintable(m_key), frames, m_w, m_h);
|
|
return;
|
|
}
|
|
m_totalFrames = int(frames);
|
|
m_frameRate = lottie_animation_get_framerate(m_anim);
|
|
m_valid = true;
|
|
GUI_DEBUG("LottieIcon: loaded path=%s gzip=%d json=%lld frames=%d size=%zux%zu fps=%.2f",
|
|
qPrintable(m_key), m_tgsSize, (long long)m_jsonData.size(), m_totalFrames, m_w, m_h, m_frameRate);
|
|
}
|
|
|
|
LottieIcon::~LottieIcon() {
|
|
if (m_anim) lottie_animation_destroy(m_anim);
|
|
}
|
|
|
|
void LottieIcon::logWarnings() const {
|
|
for (const QString &message : m_loadWarnings) GUI_WARN("LottieIcon: path=%s rlottie: %s", qPrintable(m_key), qPrintable(message));
|
|
}
|
|
|
|
QImage LottieIcon::renderFrame(int frameNum, QSize targetSize) const {
|
|
if (!m_valid || targetSize.width() <= 0 || targetSize.height() <= 0)
|
|
return QImage();
|
|
|
|
QImage image(targetSize, QImage::Format_ARGB32_Premultiplied);
|
|
if (image.isNull()) {
|
|
GUI_ERROR("LottieIcon: image allocation failed path=%s size=%dx%d", qPrintable(m_key), targetSize.width(), targetSize.height());
|
|
return {};
|
|
}
|
|
image.fill(Qt::transparent);
|
|
int frame = frameNum % m_totalFrames;
|
|
if (frame < 0) frame += m_totalFrames;
|
|
lottie_animation_render(m_anim, size_t(frame), reinterpret_cast<uint32_t*>(image.bits()),
|
|
size_t(image.width()), size_t(image.height()), size_t(image.bytesPerLine()));
|
|
return image;
|
|
}
|
|
|
|
void LottieIcon::advanceFrame() {
|
|
if (!m_valid) { GUI_WARN("LottieIcon: advance requested for invalid animation path=%s", qPrintable(m_key)); return; }
|
|
m_currentFrame = (m_currentFrame + 1) % m_totalFrames;
|
|
}
|