Revision control

++ // Copyright (C) 2018 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qlottielayer_p.h"
++
++ #include <QJsonArray>
++ #include <QJsonObject>
++ #include <QJsonValue>
++ #include <QLoggingCategory>
++ #include <QtCore/QScopedValueRollback>
++ #include <QString>
++
++ #include "qlottieflatlayers_p.h"
++ #include "qlottietextlayer_p.h"
++ #include "qlottieshapelayer_p.h"
++ #include "qlottieprecomplayer_p.h"
++ #include "qlottiefilleffect_p.h"
++ #include "qlottiebasictransform_p.h"
++ #include "qlottierenderer_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::Literals::StringLiterals;
++
++ QLottieLayer::QLottieLayer(const QLottieLayer &other)
++ : QLottieBase(other)
++ {
++ m_layerIndex = other.m_layerIndex;
++ m_startFrame = other.m_startFrame;
++ m_endFrame = other.m_endFrame;
++ m_startTime = other.m_startTime;
++ m_blendMode = other.m_blendMode;
++ m_autoOrient = other.m_autoOrient;
++ m_3dLayer = other.m_3dLayer;
++ m_stretch = other.m_stretch;
++ m_hasLinkedLayer = other.m_hasLinkedLayer;
++ m_linkedLayerId = other.m_linkedLayerId;
++ m_isMatte = other.m_isMatte;
++ m_matteMode = other.m_matteMode;
++ if (other.m_layerTransform) {
++ m_layerTransform = new QLottieBasicTransform(*other.m_layerTransform);
++ m_layerTransform->setParent(this);
++ }
++ m_size = other.m_size;
++ if (other.m_effects) {
++ m_effects = new QLottieBase;
++ for (QLottieBase *effect : other.m_effects->children())
++ m_effects->appendChild(effect->clone());
++ }
++ //m_transformAtFirstFrame = other.m_transformAtFirstFrame;
++ }
++
++ QLottieLayer::~QLottieLayer()
++ {
++ if (m_layerTransform)
++ delete m_layerTransform;
++ if (m_effects)
++ delete m_effects;
++ }
++
++ QLottieBase *QLottieLayer::clone() const
++ {
++ return new QLottieLayer(*this);
++ }
++
++ QLottieLayer *QLottieLayer::construct(QJsonObject definition)
++ {
++ qCDebug(lcLottieQtLottieParser) << "QLottieLayer::parse()";
++
++ QLottieLayer *layer = nullptr;
++ int type = definition.value("ty"_L1).toInt();
++ int ret = 0;
++ switch (type) {
++ case 0:
++ qCDebug(lcLottieQtLottieParser) << "Parse precomp layer";
++ layer = new QLottiePrecompLayer();
++ ret = layer->parse(definition);
++ break;
++ case 1:
++ qCDebug(lcLottieQtLottieParser) << "Parse solid layer";
++ layer = new QLottieSolidLayer();
++ ret = layer->parse(definition);
++ break;
++ case 2:
++ qCDebug(lcLottieQtLottieParser) << "Parse image layer";
++ layer = new QLottieImageLayer();
++ ret = layer->parse(definition);
++ break;
++ case 3:
++ qCDebug(lcLottieQtLottieParser) << "Parse null layer";
++ layer = new QLottieNullLayer();
++ ret = layer->parse(definition);
++ break;
++ case 4:
++ qCDebug(lcLottieQtLottieParser) << "Parse shape layer";
++ layer = new QLottieShapeLayer();
++ ret = layer->parse(definition);
++ break;
++ case 5:
++ qCDebug(lcLottieQtLottieParser) << "Parse text layer";
++ layer = new QLottieTextLayer();
++ ret = layer->parse(definition);
++ break;
++ default:
++ qCInfo(lcLottieQtLottieParser) << "Unsupported layer type:" << type;
++ }
++
++ if (ret < 0)
++ return nullptr;
++
++ return layer;
++ }
++
++ // Create a shape layer that can be used as a matte to emulate the effect of a layer's mask
++ QLottieLayer *QLottieLayer::constructMaskLayer(QLottieLayer *layer)
++ {
++ static int newLayerIndex = 0x10000;
++
++ qCDebug(lcLottieQtLottieParser) << "Parsing mask for layer" << layer->name();
++
++ const QJsonObject &layerDef = layer->definition();
++ const QJsonArray jsonMasks = layerDef.value("masksProperties"_L1).toArray();
++ if (jsonMasks.isEmpty())
++ return nullptr;
++
++ QJsonObject maskLayerDef;
++ maskLayerDef.insert("ty"_L1, 4);
++ maskLayerDef.insert("nm"_L1, layer->name().prepend(QStringLiteral("qt_maskLayer_for_")));
++ maskLayerDef.insert("ip"_L1, layerDef.value("ip"_L1));
++ maskLayerDef.insert("op"_L1, layerDef.value("op"_L1));
++ maskLayerDef.insert("td"_L1, 1); // i.e.: this layer will be used as matte
++ maskLayerDef.insert("ks"_L1, QJsonObject{});
++ // Mirror the masked layer's transform by using Lottie's transform reference ("parent") feature
++ // Requires the masked layer to have a non-0 layer index, so it can be referenced
++ int maskedLayerIndex = layer->m_layerIndex ? layer->m_layerIndex : newLayerIndex++;
++ maskLayerDef.insert("parent"_L1, maskedLayerIndex);
++
++ QJsonObject color({ { "a"_L1, 0 }, { "k"_L1, QJsonArray({ 1, 1, 1 }) } });
++ QJsonObject shapeXf;
++ shapeXf.insert("ty"_L1, "tr"_L1);
++ QJsonArray shapes;
++ for (const QJsonValue &maskValue : jsonMasks) {
++ QJsonObject maskDef = maskValue.toObject();
++ QJsonValue shapeDef = maskDef.value("pt"_L1);
++ if (shapeDef.isUndefined())
++ continue;
++ const auto maskMode = maskDef.value("mode"_L1).toStringView("a"_L1);
++ if (maskMode == "n"_L1)
++ continue;
++ if (maskMode != "a"_L1)
++ qCInfo(lcLottieQtLottieParser) << "Unsupported mask mode" << maskMode;
++ if (maskDef.value("inv"_L1).toBool())
++ qCInfo(lcLottieQtLottieParser) << "Unsupported inverted mask";
++
++ QJsonObject shape;
++ shape["ty"_L1] = QJsonValue("sh"_L1);
++ shape["ks"_L1] = shapeDef;
++
++ QJsonArray items;
++ items.append(shape);
++ QJsonObject fill;
++ fill.insert("ty"_L1, "fl"_L1);
++ fill.insert("c"_L1, color);
++ fill.insert("o"_L1, maskDef.value("o"_L1));
++ items.append(fill);
++ items.append(shapeXf);
++
++ QJsonObject groupDef;
++ groupDef["ty"_L1] = QJsonValue("gr"_L1);
++ groupDef["it"_L1] = items;
++
++ shapes.append(groupDef);
++ }
++ if (shapes.isEmpty())
++ return nullptr;
++ maskLayerDef["shapes"_L1] = shapes;
++
++ // For debugging: qDebug() << QJsonDocument(maskLayerDef).toJson().constData();
++ QLottieLayer *res = QLottieLayer::construct(maskLayerDef);
++ if (res) {
++ layer->m_matteMode = MatteClipMode::Alpha;
++ layer->m_layerIndex = maskedLayerIndex;
++ }
++ return res;
++ }
++
++ // Take the content of a lottie layers tag and construct the corresponding layer objects
++ // Also adds them as children to given parent
++ int QLottieLayer::constructLayers(QJsonArray jsonLayers, QLottieBase *parent,
++ const QMap<QString, QJsonObject> &assets)
++ {
++ if (jsonLayers.size() == 0) {
++ qCWarning(lcLottieQtLottieParser) << "Layers are empty";
++ return -1;
++ }
++
++ int layersAdded = 0;
++ QJsonArray::const_iterator jsonLayerIt = jsonLayers.constEnd();
++ while (jsonLayerIt != jsonLayers.constBegin()) {
++ jsonLayerIt--;
++ QJsonObject jsonLayer = (*jsonLayerIt).toObject();
++ if (!jsonLayer.contains("ty"_L1)) {
++ qCWarning(lcLottieQtLottieParser) << "Layer" << jsonLayer.value("nm"_L1).toString()
++ << "is missing required key \"ty\"";
++ return -1;
++ }
++
++ if (jsonLayer.value("ty"_L1).toInt() == 2) {
++ if (!jsonLayer.contains("refId"_L1)) {
++ qCWarning(lcLottieQtLottieParser) << "Layer" << jsonLayer.value("nm"_L1).toString()
++ << "is missing required key \"refId\"";
++ return -1;
++ }
++
++ QString refId = jsonLayer.value("refId"_L1).toString();
++ jsonLayer.insert("asset"_L1, assets.value(refId));
++ }
++
++ QLottieLayer *layer = QLottieLayer::construct(jsonLayer);
++ if (layer) {
++ layer->setParent(parent);
++ // Matte layers must be rendered before the layer they apply to, even though they
++ // appear after in the list of layers. Hence, we move matte layers in front of
++ // the layer they (by default) apply to, so it will be rendered first
++ if (layer->isMatteLayer())
++ parent->insertChildBeforeLast(layer);
++ else
++ parent->appendChild(layer);
++ layersAdded++;
++
++ if (layer->type() != LOTTIE_LAYER_NULL_IX && jsonLayer.value("hasMask"_L1).toBool()) {
++ if (layer->isMatteLayer() || layer->isUsingMatteLayer()) {
++ qCInfo(lcLottieQtLottieParser) << "Ignoring mask of layer" << layer->name();
++ } else {
++ // Create a matte that implements the layer mask
++ QLottieLayer *maskLayer = constructMaskLayer(layer);
++ if (maskLayer) {
++ maskLayer->setParent(parent);
++ parent->insertChildBeforeLast(maskLayer);
++ layersAdded++;
++ }
++ }
++ }
++ }
++ }
++ return layersAdded;
++ }
++
++ bool QLottieLayer::active(int frame) const
++ {
++ return (!m_hidden && ((frame >= m_startFrame && frame <= m_endFrame) || isStructureDumping()));
++ }
++
++ int QLottieLayer::parse(const QJsonObject &definition)
++ {
++ QLottieBase::parse(definition);
++ if (m_hidden)
++ return 0;
++
++ qCDebug(lcLottieQtLottieParser) << "QLottieLayer::parse():" << m_name;
++
++ m_layerIndex = definition.value("ind"_L1).toVariant().toInt();
++ if (!checkRequiredKeys(definition, "Layer"_L1, { "ip"_L1, "op"_L1, "ks"_L1 }, m_name))
++ return -1;
++ m_startFrame = definition.value("ip"_L1).toVariant().toInt();
++ m_endFrame = definition.value("op"_L1).toVariant().toInt();
++ m_blendMode = definition.value("lottie"_L1).toVariant().toInt();
++ m_autoOrient = (definition.value("ao"_L1).toInt() == 1);
++ m_3dLayer = definition.value("ddd"_L1).toBool();
++ m_linkedLayerId = definition.value("parent"_L1).toVariant().toInt(&m_hasLinkedLayer);
++ m_isMatte = definition.value("td"_L1).toInt() == 1;
++ int matteMode = definition.value("tt"_L1).toInt(-1);
++ if (matteMode > -1 && matteMode < 5)
++ m_matteMode = static_cast<MatteClipMode>(matteMode);
++
++ QJsonObject trans = definition.value("ks"_L1).toObject();
++ m_layerTransform = new QLottieBasicTransform(this);
++ if (m_layerTransform->parse(trans) < 0)
++ return -1;
++
++ QJsonArray effects = definition.value("ef"_L1).toArray();
++ parseEffects(effects);
++
++ if (m_blendMode > 0)
++ qCInfo(lcLottieQtLottieParser)
++ << "Lottie Layer: Unsupported blend mode" << m_blendMode;
++ if (m_3dLayer)
++ qCInfo(lcLottieQtLottieParser)
++ << "Lottie Layer: is a 3D layer, but not handled";
++
++ return 0;
++ }
++
++ void QLottieLayer::updateProperties(int frame)
++ {
++ if (m_hasLinkedLayer)
++ resolveLinkedLayer();
++
++ m_isActive = active(frame);
++ if (!m_isActive)
++ return;
++
++ // Update first effects, as they are not children of the layer
++ if (m_effects) {
++ for (QLottieBase* effect : m_effects->children())
++ effect->updateProperties(frame);
++ }
++
++ if (m_layerTransform)
++ m_layerTransform->updateProperties(frame);
++
++ QLottieBase::updateProperties(frame);
++ }
++
++ void QLottieLayer::render(QLottieRenderer &renderer) const
++ {
++ if (!m_isActive)
++ return;
++
++ // Render first effects, as they affect the children
++ renderEffects(renderer);
++
++ // In case there is a linked layer, apply its transform first
++ // as it affects tranforms of this layer too
++ applyLayerTransform(renderer);
++
++ renderer.render(*this);
++
++ renderChildren(renderer);
++ }
++
++ QLottieBase *QLottieLayer::findChild(const QString &childName)
++ {
++ QLottieBase *child = nullptr;
++
++ if (m_effects)
++ child = m_effects->findChild(childName);
++
++ if (child)
++ return child;
++ else
++ return QLottieBase::findChild(childName);
++ }
++
++ QLottieLayer *QLottieLayer::resolveLinkedLayer()
++ {
++ if (m_linkedLayer)
++ return m_linkedLayer;
++
++ Q_ASSERT(parent());
++
++ for (QLottieBase *child : parent()->children()) {
++ QLottieLayer *layer = static_cast<QLottieLayer*>(child);
++ if (layer->layerId() == m_linkedLayerId) {
++ m_linkedLayer = layer;
++ break;
++ }
++ }
++ return m_linkedLayer;
++ }
++
++ QLottieLayer *QLottieLayer::linkedLayer() const
++ {
++ return m_linkedLayer;
++ }
++
++ bool QLottieLayer::isUsingMatteLayer() const
++ {
++ return m_matteMode != NoClip;
++ }
++
++ bool QLottieLayer::isMatteLayer() const
++ {
++ return m_isMatte;
++ }
++
++ QLottieLayer::MatteClipMode QLottieLayer::matteMode() const
++ {
++ return m_matteMode;
++ }
++
++ int QLottieLayer::layerId() const
++ {
++ return m_layerIndex;
++ }
++
++ QLottieBasicTransform *QLottieLayer::transform() const
++ {
++ return m_layerTransform;
++ }
++
++ void QLottieLayer::renderEffects(QLottieRenderer &renderer) const
++ {
++ if (!m_effects)
++ return;
++
++ for (QLottieBase* effect : m_effects->children()) {
++ if (effect->hidden())
++ continue;
++ effect->render(renderer);
++ }
++ }
++
++ void QLottieLayer::applyLayerTransform(QLottieRenderer &renderer) const
++ {
++ if (m_applyingLayerTransform)
++ return;
++ QScopedValueRollback<bool> recursionGuard(m_applyingLayerTransform, true);
++
++ if (!isStructureDumping()) {
++ if (QLottieLayer *ll = linkedLayer()) {
++ ll->applyLayerTransform(renderer);
++ if (m_layerTransform)
++ m_layerTransform->setHasLinkedLayerTransform(true);
++ }
++ }
++ if (m_layerTransform)
++ m_layerTransform->render(renderer);
++ }
++
++ QSize QLottieLayer::layerSize() const
++ {
++ QSize res = m_size;
++ if (!res.isValid() && parent())
++ res = parent()->layerSize();
++ return res;
++ }
++
++ const QLottieLayer *QLottieLayer::checkedCast(const QLottieBase *node)
++ {
++ const QLottieLayer *res = nullptr;
++ if (node && node->type() >= LOTTIE_LAYER_PRECOMP_IX && node->type() <= LOTTIE_LAYER_TEXT_IX)
++ res = static_cast<const QLottieLayer *>(node);
++ return res;
++ }
++
++ void QLottieLayer::parseEffects(const QJsonArray &definition, QLottieBase *effectRoot)
++ {
++ QJsonArray::const_iterator it = definition.constEnd();
++ while (it != definition.constBegin()) {
++ // Create effects container if at least one effect found
++ if (!effectRoot) {
++ Q_ASSERT(!m_effects);
++ effectRoot = new QLottieBase;
++ m_effects = effectRoot;
++ }
++ it--;
++ QJsonObject effect = (*it).toObject();
++ int type = effect.value("ty"_L1).toInt();
++ switch (type) {
++ case 0:
++ {
++ QLottieBase *slider = new QLottieBase;
++ slider->parse(effect);
++ effectRoot->appendChild(slider);
++ break;
++ }
++ case 5:
++ {
++ if (effect.value("en"_L1).toInt()) {
++ QLottieBase *group = new QLottieBase;
++ group->parse(effect);
++ effectRoot->appendChild(group);
++ parseEffects(effect.value("ef"_L1).toArray(), group);
++ }
++ break;
++ }
++ case 21:
++ {
++ QLottieFillEffect *fill = new QLottieFillEffect;
++ fill->construct(effect);
++ effectRoot->appendChild(fill);
++ break;
++ }
++ default:
++ qCInfo(lcLottieQtLottieParser)
++ << "QLottieLayer: Unsupported effect" << type;
++ }
++ }
++ }
++
++ QT_END_NAMESPACE