Revision control
++ // Copyright (C) 2025 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include "qsvgcsshandler_p.h"
++ #include <QtSvg/private/qsvgstyleselector_p.h>
++ #include <QtSvg/private/qsvganimatedproperty_p.h>
++ #include <QtGui/private/qmath_p.h>
++ #include <QtGui/private/qguisvg_p.h>
++ #include <QtCore/qlist.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace {
++
++ struct CssKeyFrameValue{
++ qreal keyFrame{0.0};
++ QList<QCss::Value> values;
++ QCss::Declaration timingFunction;
++ };
++
++ bool fillPropertyEasing(const QList<CssKeyFrameValue> &keyFrames, QSvgAbstractAnimatedProperty *prop)
++ {
++ for (const CssKeyFrameValue &keyFrameValue : keyFrames) {
++ QStringView timingFunctionStr = QSvgCssHandler::parseDecltoString(keyFrameValue.timingFunction);
++ QSvgEasingInterfacePtr easing;
++ if (timingFunctionStr == QStringLiteral("linear")) {
++ easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::Linear);
++ } else if (timingFunctionStr == QStringLiteral("ease-in")) {
++ easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::EaseIn);
++ } else if (timingFunctionStr == QStringLiteral("ease-out")) {
++ easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::EaseOut);
++ } else if (timingFunctionStr == QStringLiteral("ease-in-out")) {
++ easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::EaseInOut);
++ } else if (timingFunctionStr == QStringLiteral("step-end")) {
++ easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::Steps,
++ QSvgCssValues::StepValues{quint32(1),
++ QSvgCssValues::StepPosition::End});
++ } else if (timingFunctionStr == QStringLiteral("step-start")) {
++ easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::Steps,
++ QSvgCssValues::StepValues{quint32(1),
++ QSvgCssValues::StepPosition::Start});
++ }
++
++ prop->appendEasing(std::move(easing));
++ }
++
++
++
++ return true;
++ }
++
++ bool fillColorProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyColor *prop)
++ {
++ for (const CssKeyFrameValue &keyFrame : keyFrames) {
++ if (keyFrame.values.size() != 1)
++ return false;
++
++ QString colorStr = keyFrame.values.first().toString();
++ QColor color = QColor::fromString(colorStr);
++ prop->appendColor(color);
++ prop->appendKeyFrame(keyFrame.keyFrame);
++ }
++
++ return true;
++ }
++
++ bool fillOpacityProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyFloat *prop)
++ {
++ for (const CssKeyFrameValue &keyFrame : keyFrames) {
++ if (keyFrame.values.size() != 1)
++ return false;
++
++ QString opacity = keyFrame.values.first().toString();
++ prop->appendValue(opacity.toDouble());
++ prop->appendKeyFrame(keyFrame.keyFrame);
++ }
++
++ return true;
++ }
++
++ bool validateTransform(QList<QList<QSvgAnimatedPropertyTransform::TransformComponent>> &keyFrameComponents) {
++
++ if (keyFrameComponents.size() < 2)
++ return false;
++
++ qsizetype maxIndex = 0;
++ qsizetype maxSize = 0;
++ for (int i = 1; i < keyFrameComponents.size(); i++) {
++ auto &listA = keyFrameComponents[i - 1];
++ auto &listB = keyFrameComponents[i];
++ for (int j = 0; j < qMin(listA.size(), listB.size()); j++) {
++ auto typeA = listA.at(j).type;
++ auto typeB = listB.at(j).type;
++ // TODO: Handle type mismatch as mentioned in CSS Transform Module specs.
++ if (typeA != typeB)
++ return false;
++ }
++
++ if (listA.size() > maxSize) {
++ maxIndex = i - 1;
++ maxSize = listA.size();
++ }
++
++ if (listB.size() > maxSize) {
++ maxIndex = i;
++ maxSize = listB.size();
++ }
++ }
++
++ const auto &longList = keyFrameComponents.at(maxIndex);
++ // pad the shorter list with identical transforms set to default values.
++ for (auto &list : keyFrameComponents) {
++ qsizetype size = list.size();
++
++ for (int j = size; j < maxSize; j++) {
++ QSvgAnimatedPropertyTransform::TransformComponent comp = longList.value(j);
++ switch (comp.type) {
++ case QSvgAnimatedPropertyTransform::TransformComponent::Translate:
++ case QSvgAnimatedPropertyTransform::TransformComponent::Skew:
++ comp.values[0] = 0;
++ comp.values[1] = 0;
++ break;
++ case QSvgAnimatedPropertyTransform::TransformComponent::Rotate:
++ comp.values[0] = 0;
++ comp.values[1] = 0;
++ comp.values[2] = 0;
++ break;
++ case QSvgAnimatedPropertyTransform::TransformComponent::Scale:
++ comp.values[0] = 1;
++ comp.values[1] = 1;
++ break;
++ default:
++ break;
++ }
++ list.append(comp);
++ }
++ }
++
++ return true;
++ }
++
++ bool fillTransformProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyTransform *prop)
++ {
++ // Stores each key frame's list of components
++ QList<QList<QSvgAnimatedPropertyTransform::TransformComponent>> keyFramesComponents;
++
++ for (const CssKeyFrameValue &keyFrame : keyFrames) {
++ QList<QSvgAnimatedPropertyTransform::TransformComponent> components;
++ for (const QCss::Value &val : keyFrame.values) {
++ if (val.type == QCss::Value::Function) {
++ const QStringList lst = val.variant.toStringList();
++ const QStringView transformType = lst.value(0);
++ const QList<QStringView> args =
++ QStringView{ lst.value(1) }.split(QStringLiteral(","), Qt::SkipEmptyParts);
++ if (transformType == QStringLiteral("scale")) {
++ QSvgAnimatedPropertyTransform::TransformComponent component;
++ qreal scale0 = QGuiSvg::toDouble(args.value(0).trimmed());
++ qreal scale1 = QGuiSvg::toDouble(args.value(1).trimmed());
++ component.type = QSvgAnimatedPropertyTransform::TransformComponent::Scale;
++ component.values.append(scale0);
++ component.values.append(scale1);
++ components.append(component);
++ } else if (transformType == QStringLiteral("translate")) {
++ QSvgAnimatedPropertyTransform::TransformComponent component;
++ QGuiSvg::LengthType type;
++ qreal translate0 = QGuiSvg::parseLength(args.value(0), &type);
++ translate0 = QGuiSvg::convertToPixels(translate0, false, type);
++ qreal translate1 = QGuiSvg::parseLength(args.value(1), &type);
++ translate1 = QGuiSvg::convertToPixels(translate1, false, type);
++ component.type = QSvgAnimatedPropertyTransform::TransformComponent::Translate;
++ component.values.append(translate0);
++ component.values.append(translate1);
++ components.append(component);
++ } else if (transformType == QStringLiteral("rotate")) {
++ QSvgAnimatedPropertyTransform::TransformComponent component;
++ qreal rotationAngle = QGuiSvg::parseAngle(args.value(0)).value_or(0);
++ component.type = QSvgAnimatedPropertyTransform::TransformComponent::Rotate;
++ component.values.append(rotationAngle);
++ component.values.append(0);
++ component.values.append(0);
++ components.append(component);
++ } else if (transformType == QStringLiteral("skew")) {
++ QSvgAnimatedPropertyTransform::TransformComponent component;
++ qreal skew0 = QGuiSvg::parseAngle(args.value(0)).value_or(0);
++ qreal skew1 = QGuiSvg::parseAngle(args.value(1)).value_or(0);
++ component.type = QSvgAnimatedPropertyTransform::TransformComponent::Skew;
++ component.values.append(skew0);
++ component.values.append(skew1);
++ components.append(component);
++ } else if (transformType == QStringLiteral("matrix")) {
++ QSvgAnimatedPropertyTransform::TransformComponent component1, component2, component3;
++ QGuiSvg::LengthType type;
++ qreal translate0 = QGuiSvg::parseLength(args.value(4), &type);
++ translate0 = QGuiSvg::convertToPixels(translate0, false, type);
++ qreal translate1 = QGuiSvg::parseLength(args.value(5), &type);
++ translate1 = QGuiSvg::convertToPixels(translate1, false, type);
++ qreal scale0 = QGuiSvg::toDouble(args.value(0).trimmed());
++ qreal scale1 = QGuiSvg::toDouble(args.value(3).trimmed());
++ qreal skew0 = QGuiSvg::toDouble((args.value(1).trimmed()));
++ qreal skew1 = QGuiSvg::toDouble((args.value(2).trimmed()));
++ component1.type = QSvgAnimatedPropertyTransform::TransformComponent::Translate;
++ component1.values.append(translate0);
++ component1.values.append(translate1);
++ component2.type = QSvgAnimatedPropertyTransform::TransformComponent::Scale;
++ component2.values.append(scale0);
++ component2.values.append(scale1);
++ component3.type = QSvgAnimatedPropertyTransform::TransformComponent::Skew;
++ component3.values.append(skew0);
++ component3.values.append(skew1);
++ components.append(component1);
++ components.append(component2);
++ components.append(component3);
++ }
++ }
++ }
++ keyFramesComponents.append(components);
++ prop->appendKeyFrame(keyFrame.keyFrame);
++ }
++
++ if (!validateTransform(keyFramesComponents))
++ return false;
++
++ for (const auto &comp : std::as_const(keyFramesComponents)) {
++ prop->appendComponents(comp);
++ }
++ prop->setTransformCount(keyFramesComponents.first().size());
++
++ return true;
++ }
++
++ bool fillOffsetDistanceProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyFloat *prop)
++ {
++ for (const CssKeyFrameValue &keyFrame : keyFrames) {
++ if (keyFrame.values.size() != 1)
++ return false;
++
++ QString offsetDistance = keyFrame.values.first().toString();
++
++ bool ok = false;
++ qreal distance = offsetDistance.toDouble(&ok);
++ if (!ok)
++ return false;
++
++ QCss::Value::Type type = keyFrame.values.first().type;
++ if (type != QCss::Value::Percentage && !qFuzzyCompare(distance, 0.))
++ return false;
++ distance /= 100;
++ prop->appendValue(distance);
++ prop->appendKeyFrame(keyFrame.keyFrame);
++ }
++
++ return true;
++ }
++
++ }
++
++ QSvgCssHandler::QSvgCssHandler()
++ : m_selector(new QSvgStyleSelector)
++ {
++
++ }
++
++ QSvgCssHandler::~QSvgCssHandler()
++ {
++ delete m_selector;
++ m_selector = nullptr;
++ }
++
++ QSvgCssAnimation *QSvgCssHandler::createAnimation(QStringView name)
++ {
++ if (!m_animations.contains(name))
++ return nullptr;
++
++ QCss::AnimationRule animationRule = m_animations[name];
++ QHash<QString, QSvgAbstractAnimatedProperty*> animatedProperies;
++ QSvgCssAnimation *animation = new QSvgCssAnimation;
++
++
++ // Css Parser returns a list of all properties for each key frame. Here,
++ // we store the key frames and values for each property for easier parsing.
++ QHash<QString, QList<CssKeyFrameValue>> keyFrameValues;
++ for (const auto &ruleSet : std::as_const(animationRule.ruleSets)) {
++ for (const QCss::Declaration &decl : ruleSet.declarations) {
++ QCss::Value timingFunction;
++ CssKeyFrameValue keyFrameValue = {ruleSet.keyFrame, decl.d->values, ruleSet.timingFunction};
++ QList<CssKeyFrameValue> &value = keyFrameValues[decl.d->property];
++ value.append(keyFrameValue);
++ }
++ }
++
++ for (auto it = keyFrameValues.begin(); it != keyFrameValues.end(); it++) {
++ QStringView property = it.key();
++ const QList<CssKeyFrameValue> &keyFrames = it.value();
++ auto *prop = QSvgAbstractAnimatedProperty::createAnimatedProperty(property.toString());
++ if (!prop)
++ continue;
++
++ bool result = false;
++ if (property == QLatin1StringView("fill") || property == QLatin1StringView("stroke"))
++ result = fillColorProperty(keyFrames, static_cast<QSvgAnimatedPropertyColor*>(prop));
++ else if (property == QLatin1StringView("transform"))
++ result = fillTransformProperty(keyFrames, static_cast<QSvgAnimatedPropertyTransform*>(prop));
++ else if (property == QLatin1StringView("fill-opacity") || property == QLatin1StringView("stroke-opacity")
++ || property == QLatin1StringView("opacity"))
++ result = fillOpacityProperty(keyFrames, static_cast<QSvgAnimatedPropertyFloat*>(prop));
++ else if (property == QLatin1StringView("offset-distance"))
++ result = fillOffsetDistanceProperty(keyFrames, static_cast<QSvgAnimatedPropertyFloat*>(prop));
++
++ result &= fillPropertyEasing(keyFrames, prop);
++ if (!result) {
++ delete prop;
++ continue;
++ }
++
++ animatedProperies[property] = prop;
++ }
++
++ for (auto it = animatedProperies.begin(); it != animatedProperies.end(); it++)
++ animation->appendProperty(it.value());
++
++ return animation;
++ }
++
++ QSvgCssEasingPtr QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction easingFunction,
++ const QSvgCssValues::EasingValues &values)
++ {
++ QSvgCssEasingPtr easing;
++
++ switch (easingFunction) {
++ case QSvgCssValues::EasingFunction::Ease:
++ case QSvgCssValues::EasingFunction::EaseIn:
++ case QSvgCssValues::EasingFunction::EaseOut:
++ case QSvgCssValues::EasingFunction::EaseInOut:
++ case QSvgCssValues::EasingFunction::Linear:
++ easing = createEasingFromKeyword(easingFunction);
++ break;
++ case QSvgCssValues::EasingFunction::Steps:
++ easing = createStepsEasing(std::get<QSvgCssValues::StepValues>(values));
++ break;
++ default:
++ easing = createEasingFromKeyword(QSvgCssValues::EasingFunction::Ease);
++ break;
++ }
++
++ return easing;
++ }
++
++ void QSvgCssHandler::collectAnimations(const QCss::StyleSheet &sheet)
++ {
++ auto sortFunction = [](QCss::AnimationRule::AnimationRuleSet r1, QCss::AnimationRule::AnimationRuleSet r2) {
++ return r1.keyFrame < r2.keyFrame;
++ };
++
++ QList<QCss::AnimationRule> animationRules = sheet.animationRules;
++ for (QCss::AnimationRule &rule : animationRules) {
++ std::sort(rule.ruleSets.begin(), rule.ruleSets.end(), sortFunction);
++ m_animations[rule.animName] = rule;
++ }
++ }
++
++ void QSvgCssHandler::parseStyleSheet(const QStringView str)
++ {
++ QString css = str.toString();
++ QCss::StyleSheet sheet;
++ QCss::Parser(css).parse(&sheet);
++ m_selector->styleSheets.append(sheet);
++
++ collectAnimations(sheet);
++ }
++
++ QString QSvgCssHandler::parseDecltoString(const QCss::Declaration &decl)
++ {
++ if (decl.d->property.isEmpty())
++ return QString();
++
++ QString valueStr;
++ const int valCount = decl.d->values.size();
++ for (int i = 0; i < valCount; ++i) {
++ QCss::Value val = decl.d->values.at(i);
++ switch (val.type) {
++ case QCss::Value::TermOperatorComma:
++ valueStr += QLatin1Char(';');
++ break;
++ case QCss::Value::Uri:
++ {
++ QString temp = val.toString();
++ temp.prepend(QLatin1String("url("));
++ temp.append(QLatin1Char(')'));
++ valueStr += temp;
++ break;
++ }
++ case QCss::Value::Function:
++ {
++ QStringList lst = val.variant.toStringList();
++ valueStr.append(lst.at(0));
++ valueStr.append(QLatin1Char('('));
++ for (int i = 1; i < lst.size(); ++i) {
++ valueStr.append(lst.at(i));
++ if ((i +1) < lst.size())
++ valueStr.append(QLatin1Char(','));
++ }
++ valueStr.append(QLatin1Char(')'));
++ break;
++ }
++ case QCss::Value::KnownIdentifier:
++ switch (val.variant.toInt()) {
++ case QCss::Value_None:
++ valueStr += QLatin1String("none");
++ break;
++ case QCss::Value_Auto:
++ valueStr += QLatin1String("auto");
++ break;
++ default:
++ valueStr += val.toString();
++ break;
++ }
++ break;
++ case QCss::Value::Percentage:
++ valueStr += val.toString() + QLatin1Char('%');
++ break;
++ default:
++ valueStr += val.toString();
++ break;
++ }
++
++ if (i + 1 < valCount)
++ valueStr += QLatin1Char(' ');
++ }
++
++ return valueStr;
++ }
++
++ void QSvgCssHandler::parseCSStoXMLAttrs(const QList<QCss::Declaration> &declarations, QXmlStreamAttributes &attributes) const
++ {
++ for (int i = 0; i < declarations.size(); ++i) {
++ const QCss::Declaration &decl = declarations.at(i);
++ QString valueStr = parseDecltoString(decl);
++ if (!valueStr.isEmpty())
++ attributes.append(QString(), decl.d->property, valueStr);
++ }
++ }
++
++ void QSvgCssHandler::parseCSStoXMLAttrs(const QString &css, QXmlStreamAttributes &attributes) const
++ {
++ // preprocess (for unicode escapes), tokenize and remove comments
++ QCss::Parser parser(css);
++ QString key;
++
++ while (parser.hasNext()) {
++ parser.skipSpace();
++
++ if (!parser.hasNext())
++ break;
++ parser.next();
++
++ QString name;
++ QString value;
++
++ if (parser.hasEscapeSequences) {
++ key = parser.lexem();
++ name = key;
++ } else {
++ const QCss::Symbol &sym = parser.symbol();
++ name = sym.text.mid(sym.start, sym.len);
++ }
++
++ parser.skipSpace();
++ if (!parser.test(QCss::COLON))
++ break;
++
++ parser.skipSpace();
++ if (!parser.hasNext())
++ break;
++
++ const int firstSymbol = parser.index;
++ int symbolCount = 0;
++ do {
++ parser.next();
++ ++symbolCount;
++ } while (parser.hasNext() && !parser.test(QCss::SEMICOLON));
++
++ bool canExtractValueByRef = !parser.hasEscapeSequences;
++ if (canExtractValueByRef) {
++ int len = parser.symbols.at(firstSymbol).len;
++ for (int i = firstSymbol + 1; i < firstSymbol + symbolCount; ++i) {
++ len += parser.symbols.at(i).len;
++
++ if (parser.symbols.at(i - 1).start + parser.symbols.at(i - 1).len
++ != parser.symbols.at(i).start) {
++ canExtractValueByRef = false;
++ break;
++ }
++ }
++ if (canExtractValueByRef) {
++ const QCss::Symbol &sym = parser.symbols.at(firstSymbol);
++ value = sym.text.mid(sym.start, len);
++ }
++ }
++ if (!canExtractValueByRef) {
++
++ for (int i = firstSymbol; i < parser.index - 1; ++i)
++ value += parser.symbols.at(i).lexem();
++ }
++
++ attributes.append(QString(), name, value);
++
++ parser.skipSpace();
++ }
++ }
++
++ void QSvgCssHandler::styleLookup(QSvgNode *node, QXmlStreamAttributes &attributes) const
++ {
++ QCss::StyleSelector::NodePtr cssNode;
++ cssNode.ptr = node;
++ QList<QCss::Declaration> decls = m_selector->declarationsForNode(cssNode);
++
++ parseCSStoXMLAttrs(decls, attributes);
++ }
++
++ QSvgCssEasingPtr QSvgCssHandler::createEasingFromKeyword(QSvgCssValues::EasingFunction easingFunction)
++ {
++ constexpr QPointF easeC1(0.25, 0.1);
++ constexpr QPointF easeC2(0.25, 1);
++ constexpr QPointF easeInC1(0.42, 0);
++ constexpr QPointF easeInC2(1, 1);
++ constexpr QPointF easeOutC1(0, 0);
++ constexpr QPointF easeOutC2(0.58, 1);
++ constexpr QPointF linearC1(0, 0);
++ constexpr QPointF linearC2(1, 1);
++
++ QSvgCssEasingPtr easing;
++
++ switch (easingFunction) {
++ case QSvgCssValues::EasingFunction::Ease:
++ easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, easeC1, easeC2);
++ break;
++ case QSvgCssValues::EasingFunction::EaseIn:
++ easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, easeInC1, easeInC2);
++ break;
++ case QSvgCssValues::EasingFunction::EaseOut:
++ easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, easeOutC1, easeOutC2);
++ break;
++ case QSvgCssValues::EasingFunction::EaseInOut:
++ easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, easeInC1, easeOutC2);
++ break;
++ case QSvgCssValues::EasingFunction::Linear:
++ easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, linearC1, linearC2);
++ break;
++ default:
++ Q_UNREACHABLE();
++ break;
++ }
++
++ return easing;
++ }
++
++ QSvgCssEasingPtr QSvgCssHandler::createStepsEasing(const QSvgCssValues::StepValues &values)
++ {
++ quint32 steps = values.steps;
++ QSvgCssValues::StepPosition position = values.stepPosition;
++
++ return std::make_unique<QSvgCssStepsEasing>(steps, position);
++ }
++
++ QT_END_NAMESPACE