Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qlottieroot_p.h"
#include "qlottielayer_p.h"
#include "qlottieprecomplayer_p.h"
#include "qlottieprecomposition_p.h"
#include "qlottietextlayer_p.h"
#include <QJsonDocument>
#include <QJsonArray>
#include <QString>
QT_BEGIN_NAMESPACE
using namespace Qt::Literals::StringLiterals;
namespace {
void distributeChars(QLottieBase *container, const QMap<QString, QJsonObject> &chars)
{
for (QLottieBase *child : container->children()) {
if (child->type() == LOTTIE_LAYER_TEXT_IX)
static_cast<QLottieTextLayer *>(child)->setCharData(chars);
}
}
} // namespace
QLottieRoot::QLottieRoot()
{
m_type = LOTTIE_ROOT_IX;
}
QLottieRoot::QLottieRoot(const QLottieRoot &other)
: QLottieBase(other)
{
m_size = other.m_size;
m_frameRate = other.m_frameRate;
m_startFrame = other.m_startFrame;
m_endFrame = other.m_endFrame;
m_markers = other.m_markers;
}
QLottieRoot::~QLottieRoot()
{
qDeleteAll(m_precompositions);
m_precompositions.clear();
}
QLottieBase *QLottieRoot::clone() const
{
return new QLottieRoot(*this);
}
void QLottieRoot::updateProperties(int frame)
{
for (QLottiePrecomposition *precomp : m_precompositions)
precomp->updateProperties(frame);
QLottieBase::updateProperties(frame);
}
QSize QLottieRoot::layerSize() const
{
return m_size;
}
int QLottieRoot::parseSource(const QByteArray &jsonSource, const QUrl &fileSource)
{
QJsonDocument doc = QJsonDocument::fromJson(jsonSource);
QJsonObject rootObj = doc.object();
m_definition = rootObj;
if (rootObj.empty())
return -1;
QMap<QString, QJsonObject> assets;
QJsonArray jsonLayers = rootObj.value("layers"_L1).toArray();
QJsonArray jsonAssets = rootObj.value("assets"_L1).toArray();
QString name = rootObj.value("nm"_L1).toString();
if (!checkRequiredKeys(rootObj, ""_L1, { "fr"_L1, "ip"_L1, "op"_L1 }, name))
return -1;
m_frameRate = rootObj.value("fr"_L1).toVariant().toInt();
if (m_frameRate <= 0) {
qCWarning(lcLottieQtLottieParser) << "\"fr\" value of" << name << "should be greater than 0";
return -1;
}
m_startFrame = rootObj.value("ip"_L1).toVariant().toInt();
m_endFrame = rootObj.value("op"_L1).toVariant().toInt();
if (m_endFrame <= m_startFrame) {
qCWarning(lcLottieQtLottieParser) << "Invalid top level \"ip\"/\"op\" values";
return -1;
}
m_size = QSize(rootObj.value("w"_L1).toInt(-1), rootObj.value("h"_L1).toInt(-1));
m_markers.clear();
const QJsonArray jsonMarkers = rootObj.value("markers"_L1).toArray();
for (const QJsonValue &markerVal : jsonMarkers) {
const QJsonObject markerObj = markerVal.toObject();
const QString name = markerObj.value("cm"_L1).toString();
if (!name.isEmpty()) {
m_markers.append({ name, qRound(markerObj.value("tm"_L1).toDouble()),
qRound(markerObj.value("dr"_L1).toDouble()) });
}
}
QJsonArray::const_iterator jsonAssetsIt = jsonAssets.constBegin();
while (jsonAssetsIt != jsonAssets.constEnd()) {
QJsonObject jsonAsset = (*jsonAssetsIt).toObject();
jsonAsset.insert("fileSource"_L1, QJsonValue::fromVariant(fileSource));
if (!checkRequiredKeys(jsonAsset, "Asset"_L1, { "id"_L1 }))
return -1;
QString id = jsonAsset.value("id"_L1).toString();
assets.insert(id, jsonAsset);
jsonAssetsIt++;
}
QMap<QString, QJsonObject> chars;
const QJsonArray jsonChars = rootObj.value("chars"_L1).toArray();
if (!jsonChars.isEmpty()) {
// "chars" entries identify fonts by (fFamily, style), but text layers
// reference fonts by fName. fName is not always "fFamily-fStyle"
// e.g. regular Helvetica has fName="Helvetica", not "Helvetica-Regular"
// while light Helvetica has fName="Helvetica-Light" (fFamily-fStyle).
// so we build a lookup from (fFamily, style) to fName via fonts.list.
QMap<QPair<QString, QString>, QString> fontNameMap;
const QJsonArray fontList = rootObj.value("fonts"_L1).toObject().value("list"_L1).toArray();
for (const QJsonValue &fontVal : fontList) {
const QJsonObject fontObj = fontVal.toObject();
fontNameMap.insert({fontObj.value("fFamily"_L1).toString(),
fontObj.value("fStyle"_L1).toString()},
fontObj.value("fName"_L1).toString());
}
for (const QJsonValue &charVal : jsonChars) {
const QJsonObject charObj = charVal.toObject();
const QString ch = charObj.value("ch"_L1).toString();
const QString fFamily = charObj.value("fFamily"_L1).toString();
const QString style = charObj.value("style"_L1).toString();
const QString fName = fontNameMap.value({fFamily, style}, fFamily);
if (!ch.isEmpty())
chars.insert(ch + u'|' + fName, charObj);
}
}
for (const auto &[id, jsonAsset] : assets.asKeyValueRange()) {
auto *precomp = QLottiePrecomposition::construct(jsonAsset, assets);
if (precomp) {
precomp->setParent(this);
if (!chars.isEmpty())
distributeChars(precomp, chars);
m_precompositions.insert(id, precomp);
}
}
int ret = QLottieLayer::constructLayers(jsonLayers, this, assets);
if (!chars.isEmpty())
distributeChars(this, chars);
// Resolve precomp layers
QStack<QString> visitedIds;
for (QLottieBase *element : m_precompositions) {
if (!resolvePrecompLayers(element, &visitedIds)) {
qCWarning(lcLottieQtLottieParser) << "Precomp layer resolving failed [assets]";
return -1;
}
}
if (!resolvePrecompLayers(this, &visitedIds)) {
qCWarning(lcLottieQtLottieParser) << "Precomp layer resolving failed [root]";
return -1;
}
return ret;
}
void QLottieRoot::setStructureDumping(bool enabled)
{
m_structureDumping = enabled ? 1 : 0;
}
bool QLottieRoot::resolvePrecompLayers(QLottieBase *element, QStack<QString> *visitedIds)
{
QLottiePrecompLayer *precompLayer = nullptr;
const int elemType = element->type();
if (elemType == LOTTIE_LAYER_PRECOMP_IX) {
precompLayer = static_cast<QLottiePrecompLayer *>(element);
const QString refId = precompLayer->refId();
if (visitedIds->contains(refId)) {
qCWarning(lcLottieQtLottieParser) << "Cyclical precomp reference detected";
return false;
}
QLottiePrecomposition *precomp = m_precompositions.value(refId);
if (!precomp) {
qCWarning(lcLottieQtLottieParser) << "Precomp reference not found:" << refId;
return false;
}
visitedIds->push(refId);
if (precompLayer->children().size() == 0) { // Not already resolved
QLottieBase *clone = precomp->clone();
clone->setParent(precompLayer);
precompLayer->appendChild(clone);
}
}
if (precompLayer || elemType == LOTTIE_ROOT_IX || elemType == LOTTIE_PRECOMPOSITION_IX) {
for (QLottieBase *child : element->children()) {
if (!resolvePrecompLayers(child, visitedIds))
return false;
}
}
if (precompLayer)
visitedIds->pop();
return true;
}
QT_END_NAMESPACE