| 1 | // Copyright (C) 2025 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | // Qt-Security score:critical reason:data-parser |
| 4 | |
| 5 | #include "qsvgcsshandler_p.h" |
| 6 | #include <QtSvg/private/qsvgstyleselector_p.h> |
| 7 | #include <QtSvg/private/qsvganimatedproperty_p.h> |
| 8 | #include <QtSvg/private/qsvgutils_p.h> |
| 9 | #include <QtGui/private/qmath_p.h> |
| 10 | #include <QtCore/qlist.h> |
| 11 | |
| 12 | QT_BEGIN_NAMESPACE |
| 13 | |
| 14 | namespace { |
| 15 | |
| 16 | // Parses the angle from a string and convert it to degrees. |
| 17 | qreal qsvg_parseAngle(QStringView str, bool *ok = nullptr) |
| 18 | { |
| 19 | QStringView numStr = str.trimmed(); |
| 20 | |
| 21 | if (numStr.isEmpty()) { |
| 22 | if (ok) |
| 23 | *ok = false; |
| 24 | return false; |
| 25 | } |
| 26 | |
| 27 | qreal unitFactor; |
| 28 | if (numStr.endsWith(s: QLatin1String("deg" ))) { |
| 29 | numStr.chop(n: 3); |
| 30 | unitFactor = 1.0; |
| 31 | } else if (numStr.endsWith(s: QLatin1String("grad" ))) { |
| 32 | numStr.chop(n: 4); |
| 33 | // deg = grad * 0.9; |
| 34 | unitFactor = 0.9; |
| 35 | } else if (numStr.endsWith(s: QLatin1String("rad" ))) { |
| 36 | numStr.chop(n: 3); |
| 37 | unitFactor = 180.0 / Q_PI; |
| 38 | } else if (numStr.endsWith(s: QLatin1String("turn" ))) { |
| 39 | numStr.chop(n: 4); |
| 40 | // one circle = one turn |
| 41 | unitFactor = 360.0; |
| 42 | } else { |
| 43 | unitFactor = 0.0; |
| 44 | } |
| 45 | |
| 46 | return QSvgUtils::toDouble(str: numStr, ok) * unitFactor; |
| 47 | } |
| 48 | |
| 49 | struct CssKeyFrameValue{ |
| 50 | qreal keyFrame; |
| 51 | QList<QCss::Value> values; |
| 52 | }; |
| 53 | |
| 54 | bool fillColorProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyColor *prop) |
| 55 | { |
| 56 | for (CssKeyFrameValue keyFrame : keyFrames) { |
| 57 | if (keyFrame.values.size() != 1) |
| 58 | return false; |
| 59 | |
| 60 | QString colorStr = keyFrame.values.first().toString(); |
| 61 | QColor color = QColor::fromString(name: colorStr); |
| 62 | prop->appendColor(color); |
| 63 | prop->appendKeyFrame(keyFrame: keyFrame.keyFrame); |
| 64 | } |
| 65 | |
| 66 | return true; |
| 67 | } |
| 68 | |
| 69 | bool fillOpacityProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyFloat *prop) |
| 70 | { |
| 71 | for (CssKeyFrameValue keyFrame : keyFrames) { |
| 72 | if (keyFrame.values.size() != 1) |
| 73 | return false; |
| 74 | |
| 75 | QString opacity = keyFrame.values.first().toString(); |
| 76 | prop->appendValue(value: opacity.toDouble()); |
| 77 | prop->appendKeyFrame(keyFrame: keyFrame.keyFrame); |
| 78 | } |
| 79 | |
| 80 | return true; |
| 81 | } |
| 82 | |
| 83 | bool validateTransform(QList<QList<QSvgAnimatedPropertyTransform::TransformComponent>> &keyFrameComponents) { |
| 84 | |
| 85 | if (keyFrameComponents.size() < 2) |
| 86 | return false; |
| 87 | |
| 88 | qsizetype maxIndex = 0; |
| 89 | qsizetype maxSize = 0; |
| 90 | for (int i = 1; i < keyFrameComponents.size(); i++) { |
| 91 | auto &listA = keyFrameComponents[i - 1]; |
| 92 | auto &listB = keyFrameComponents[i]; |
| 93 | for (int j = 0; j < qMin(a: listA.size(), b: listB.size()); j++) { |
| 94 | auto typeA = listA.at(i: j).type; |
| 95 | auto typeB = listB.at(i: j).type; |
| 96 | // TODO: Handle type mismatch as mentioned in CSS Transform Module specs. |
| 97 | if (typeA != typeB) |
| 98 | return false; |
| 99 | } |
| 100 | |
| 101 | if (listA.size() > maxSize) { |
| 102 | maxIndex = i - 1; |
| 103 | maxSize = listA.size(); |
| 104 | } |
| 105 | |
| 106 | if (listB.size() > maxSize) { |
| 107 | maxIndex = i; |
| 108 | maxSize = listB.size(); |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | const auto &longList = keyFrameComponents.at(i: maxIndex); |
| 113 | // pad the shorter list with identical transforms set to default values. |
| 114 | for (auto &list : keyFrameComponents) { |
| 115 | qsizetype size = list.size(); |
| 116 | |
| 117 | for (int j = size; j < maxSize; j++) { |
| 118 | QSvgAnimatedPropertyTransform::TransformComponent comp = longList.value(i: j); |
| 119 | switch (comp.type) { |
| 120 | case QSvgAnimatedPropertyTransform::TransformComponent::Translate: |
| 121 | case QSvgAnimatedPropertyTransform::TransformComponent::Skew: |
| 122 | comp.values[0] = 0; |
| 123 | comp.values[1] = 0; |
| 124 | break; |
| 125 | case QSvgAnimatedPropertyTransform::TransformComponent::Rotate: |
| 126 | comp.values[0] = 0; |
| 127 | comp.values[1] = 0; |
| 128 | comp.values[2] = 0; |
| 129 | break; |
| 130 | case QSvgAnimatedPropertyTransform::TransformComponent::Scale: |
| 131 | comp.values[0] = 1; |
| 132 | comp.values[1] = 1; |
| 133 | break; |
| 134 | default: |
| 135 | break; |
| 136 | } |
| 137 | list.append(t: comp); |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | return true; |
| 142 | } |
| 143 | |
| 144 | bool fillTransformProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyTransform *prop) |
| 145 | { |
| 146 | // Stores each key frame's list of components |
| 147 | QList<QList<QSvgAnimatedPropertyTransform::TransformComponent>> keyFramesComponents; |
| 148 | |
| 149 | for (CssKeyFrameValue keyFrame : keyFrames) { |
| 150 | QList<QSvgAnimatedPropertyTransform::TransformComponent> components; |
| 151 | for (QCss::Value val : keyFrame.values) { |
| 152 | if (val.type == QCss::Value::Function) { |
| 153 | QStringList lst = val.variant.toStringList(); |
| 154 | QStringView transformType = lst.value(i: 0); |
| 155 | QStringList args = lst.value(i: 1).split(QStringLiteral("," ), behavior: Qt::SkipEmptyParts); |
| 156 | if (transformType == QStringLiteral("scale" )) { |
| 157 | QSvgAnimatedPropertyTransform::TransformComponent component; |
| 158 | qreal scale0 = QSvgUtils::toDouble(str: args.value(i: 0).trimmed()); |
| 159 | qreal scale1 = QSvgUtils::toDouble(str: args.value(i: 1).trimmed()); |
| 160 | component.type = QSvgAnimatedPropertyTransform::TransformComponent::Scale; |
| 161 | component.values.append(t: scale0); |
| 162 | component.values.append(t: scale1); |
| 163 | components.append(t: component); |
| 164 | } else if (transformType == QStringLiteral("translate" )) { |
| 165 | QSvgAnimatedPropertyTransform::TransformComponent component; |
| 166 | QSvgUtils::LengthType type; |
| 167 | qreal translate0 = QSvgUtils::parseLength(str: args.value(i: 0), type: &type); |
| 168 | translate0 = QSvgUtils::convertToPixels(len: translate0, false, type); |
| 169 | qreal translate1 = QSvgUtils::parseLength(str: args.value(i: 1), type: &type); |
| 170 | translate1 = QSvgUtils::convertToPixels(len: translate1, false, type); |
| 171 | component.type = QSvgAnimatedPropertyTransform::TransformComponent::Translate; |
| 172 | component.values.append(t: translate0); |
| 173 | component.values.append(t: translate1); |
| 174 | components.append(t: component); |
| 175 | } else if (transformType == QStringLiteral("rotate" )) { |
| 176 | QSvgAnimatedPropertyTransform::TransformComponent component; |
| 177 | qreal rotationAngle = qsvg_parseAngle(str: args.value(i: 0)); |
| 178 | component.type = QSvgAnimatedPropertyTransform::TransformComponent::Rotate; |
| 179 | component.values.append(t: rotationAngle); |
| 180 | component.values.append(t: 0); |
| 181 | component.values.append(t: 0); |
| 182 | components.append(t: component); |
| 183 | } else if (transformType == QStringLiteral("skew" )) { |
| 184 | QSvgAnimatedPropertyTransform::TransformComponent component; |
| 185 | qreal skew0 = qsvg_parseAngle(str: args.value(i: 0)); |
| 186 | qreal skew1 = qsvg_parseAngle(str: args.value(i: 1)); |
| 187 | component.type = QSvgAnimatedPropertyTransform::TransformComponent::Skew; |
| 188 | component.values.append(t: skew0); |
| 189 | component.values.append(t: skew1); |
| 190 | components.append(t: component); |
| 191 | } else if (transformType == QStringLiteral("matrix" )) { |
| 192 | QSvgAnimatedPropertyTransform::TransformComponent component1, component2, component3; |
| 193 | QSvgUtils::LengthType type; |
| 194 | qreal translate0 = QSvgUtils::parseLength(str: args.value(i: 4), type: &type); |
| 195 | translate0 = QSvgUtils::convertToPixels(len: translate0, false, type); |
| 196 | qreal translate1 = QSvgUtils::parseLength(str: args.value(i: 5), type: &type); |
| 197 | translate1 = QSvgUtils::convertToPixels(len: translate1, false, type); |
| 198 | qreal scale0 = QSvgUtils::toDouble(str: args.value(i: 0).trimmed()); |
| 199 | qreal scale1 = QSvgUtils::toDouble(str: args.value(i: 3).trimmed()); |
| 200 | qreal skew0 = QSvgUtils::toDouble(str: (args.value(i: 1).trimmed())); |
| 201 | qreal skew1 = QSvgUtils::toDouble(str: (args.value(i: 2).trimmed())); |
| 202 | component1.type = QSvgAnimatedPropertyTransform::TransformComponent::Translate; |
| 203 | component1.values.append(t: translate0); |
| 204 | component1.values.append(t: translate1); |
| 205 | component2.type = QSvgAnimatedPropertyTransform::TransformComponent::Scale; |
| 206 | component2.values.append(t: scale0); |
| 207 | component2.values.append(t: scale1); |
| 208 | component3.type = QSvgAnimatedPropertyTransform::TransformComponent::Skew; |
| 209 | component3.values.append(t: skew0); |
| 210 | component3.values.append(t: skew1); |
| 211 | components.append(t: component1); |
| 212 | components.append(t: component2); |
| 213 | components.append(t: component3); |
| 214 | } |
| 215 | } |
| 216 | } |
| 217 | keyFramesComponents.append(t: components); |
| 218 | prop->appendKeyFrame(keyFrame: keyFrame.keyFrame); |
| 219 | } |
| 220 | |
| 221 | if (!validateTransform(keyFrameComponents&: keyFramesComponents)) |
| 222 | return false; |
| 223 | |
| 224 | for (auto comp : keyFramesComponents) { |
| 225 | prop->appendComponents(components: comp); |
| 226 | } |
| 227 | prop->setTransformCount(keyFramesComponents.first().size()); |
| 228 | |
| 229 | return true; |
| 230 | } |
| 231 | |
| 232 | } |
| 233 | |
| 234 | QSvgCssHandler::QSvgCssHandler() |
| 235 | : m_selector(new QSvgStyleSelector) |
| 236 | { |
| 237 | |
| 238 | } |
| 239 | |
| 240 | QSvgCssHandler::~QSvgCssHandler() |
| 241 | { |
| 242 | delete m_selector; |
| 243 | m_selector = nullptr; |
| 244 | } |
| 245 | |
| 246 | QSvgCssAnimation *QSvgCssHandler::createAnimation(QStringView name) |
| 247 | { |
| 248 | if (!m_animations.contains(key: name)) |
| 249 | return nullptr; |
| 250 | |
| 251 | QCss::AnimationRule animationRule = m_animations[name]; |
| 252 | QHash<QString, QSvgAbstractAnimatedProperty*> animatedProperies; |
| 253 | QSvgCssAnimation *animation = new QSvgCssAnimation; |
| 254 | |
| 255 | |
| 256 | // Css Parser returns a list of all properties for each key frame. Here, |
| 257 | // we store the key frames and values for each property for easier parsing. |
| 258 | QHash<QString, QList<CssKeyFrameValue>> keyFrameValues; |
| 259 | for (const auto &ruleSet : std::as_const(t&: animationRule.ruleSets)) { |
| 260 | for (QCss::Declaration decl : ruleSet.declarations) { |
| 261 | CssKeyFrameValue keyFrameValue = {.keyFrame: ruleSet.keyFrame, .values: decl.d->values}; |
| 262 | QList<CssKeyFrameValue> &value = keyFrameValues[decl.d->property]; |
| 263 | value.append(t: keyFrameValue); |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | for (auto it = keyFrameValues.begin(); it != keyFrameValues.end(); it++) { |
| 268 | QStringView property = it.key(); |
| 269 | const QList<CssKeyFrameValue> &keyFrames = it.value(); |
| 270 | auto *prop = QSvgAbstractAnimatedProperty::createAnimatedProperty(name: property.toString()); |
| 271 | if (!prop) |
| 272 | continue; |
| 273 | |
| 274 | bool result = false; |
| 275 | if (property == QLatin1StringView("fill" ) || property == QLatin1StringView("stroke" )) |
| 276 | result = fillColorProperty(keyFrames, prop: static_cast<QSvgAnimatedPropertyColor*>(prop)); |
| 277 | else if (property == QLatin1StringView("transform" )) |
| 278 | result = fillTransformProperty(keyFrames, prop: static_cast<QSvgAnimatedPropertyTransform*>(prop)); |
| 279 | else if (property == QLatin1StringView("fill-opacity" ) || property == QLatin1StringView("stroke-opacity" ) |
| 280 | || property == QLatin1StringView("opacity" )) |
| 281 | result = fillOpacityProperty(keyFrames, prop: static_cast<QSvgAnimatedPropertyFloat*>(prop)); |
| 282 | |
| 283 | if (!result) { |
| 284 | delete prop; |
| 285 | continue; |
| 286 | } |
| 287 | |
| 288 | animatedProperies[property] = prop; |
| 289 | } |
| 290 | |
| 291 | for (auto it = animatedProperies.begin(); it != animatedProperies.end(); it++) |
| 292 | animation->appendProperty(property: it.value()); |
| 293 | |
| 294 | return animation; |
| 295 | } |
| 296 | |
| 297 | void QSvgCssHandler::collectAnimations(const QCss::StyleSheet &sheet) |
| 298 | { |
| 299 | auto sortFunction = [](QCss::AnimationRule::AnimationRuleSet r1, QCss::AnimationRule::AnimationRuleSet r2) { |
| 300 | return r1.keyFrame < r2.keyFrame; |
| 301 | }; |
| 302 | |
| 303 | QList<QCss::AnimationRule> animationRules = sheet.animationRules; |
| 304 | for (QCss::AnimationRule rule : animationRules) { |
| 305 | std::sort(first: rule.ruleSets.begin(), last: rule.ruleSets.end(), comp: sortFunction); |
| 306 | m_animations[rule.animName] = rule; |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | void QSvgCssHandler::parseStyleSheet(const QStringView str) |
| 311 | { |
| 312 | QString css = str.toString(); |
| 313 | QCss::StyleSheet sheet; |
| 314 | QCss::Parser(css).parse(styleSheet: &sheet); |
| 315 | m_selector->styleSheets.append(t: sheet); |
| 316 | |
| 317 | collectAnimations(sheet); |
| 318 | } |
| 319 | |
| 320 | void QSvgCssHandler::parseCSStoXMLAttrs(const QList<QCss::Declaration> &declarations, QXmlStreamAttributes &attributes) const |
| 321 | { |
| 322 | for (int i = 0; i < declarations.size(); ++i) { |
| 323 | const QCss::Declaration &decl = declarations.at(i); |
| 324 | if (decl.d->property.isEmpty()) |
| 325 | continue; |
| 326 | QString valueStr; |
| 327 | const int valCount = decl.d->values.size(); |
| 328 | for (int i = 0; i < valCount; ++i) { |
| 329 | QCss::Value val = decl.d->values.at(i); |
| 330 | if (val.type == QCss::Value::TermOperatorComma) { |
| 331 | valueStr += QLatin1Char(';'); |
| 332 | } else if (val.type == QCss::Value::Uri) { |
| 333 | valueStr.prepend(s: QLatin1String("url(" )); |
| 334 | valueStr.append(c: QLatin1Char(')')); |
| 335 | } else if (val.type == QCss::Value::Function) { |
| 336 | QStringList lst = val.variant.toStringList(); |
| 337 | valueStr.append(s: lst.at(i: 0)); |
| 338 | valueStr.append(c: QLatin1Char('(')); |
| 339 | for (int i = 1; i < lst.size(); ++i) { |
| 340 | valueStr.append(s: lst.at(i)); |
| 341 | if ((i +1) < lst.size()) |
| 342 | valueStr.append(c: QLatin1Char(',')); |
| 343 | } |
| 344 | valueStr.append(c: QLatin1Char(')')); |
| 345 | } else if (val.type == QCss::Value::KnownIdentifier) { |
| 346 | switch (val.variant.toInt()) { |
| 347 | case QCss::Value_None: |
| 348 | valueStr = QLatin1String("none" ); |
| 349 | break; |
| 350 | default: |
| 351 | break; |
| 352 | } |
| 353 | } else |
| 354 | valueStr += val.toString(); |
| 355 | |
| 356 | if (i + 1 < valCount) |
| 357 | valueStr += QLatin1Char(' '); |
| 358 | } |
| 359 | |
| 360 | attributes.append(namespaceUri: QString(), name: decl.d->property, value: valueStr); |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | void QSvgCssHandler::parseCSStoXMLAttrs(const QString &css, QXmlStreamAttributes &attributes) const |
| 365 | { |
| 366 | // preprocess (for unicode escapes), tokenize and remove comments |
| 367 | QCss::Parser parser(css); |
| 368 | QString key; |
| 369 | |
| 370 | while (parser.hasNext()) { |
| 371 | parser.skipSpace(); |
| 372 | |
| 373 | if (!parser.hasNext()) |
| 374 | break; |
| 375 | parser.next(); |
| 376 | |
| 377 | QString name; |
| 378 | QString value; |
| 379 | |
| 380 | if (parser.hasEscapeSequences) { |
| 381 | key = parser.lexem(); |
| 382 | name = key; |
| 383 | } else { |
| 384 | const QCss::Symbol &sym = parser.symbol(); |
| 385 | name = sym.text.mid(position: sym.start, n: sym.len); |
| 386 | } |
| 387 | |
| 388 | parser.skipSpace(); |
| 389 | if (!parser.test(t: QCss::COLON)) |
| 390 | break; |
| 391 | |
| 392 | parser.skipSpace(); |
| 393 | if (!parser.hasNext()) |
| 394 | break; |
| 395 | |
| 396 | const int firstSymbol = parser.index; |
| 397 | int symbolCount = 0; |
| 398 | do { |
| 399 | parser.next(); |
| 400 | ++symbolCount; |
| 401 | } while (parser.hasNext() && !parser.test(t: QCss::SEMICOLON)); |
| 402 | |
| 403 | bool = !parser.hasEscapeSequences; |
| 404 | if (canExtractValueByRef) { |
| 405 | int len = parser.symbols.at(i: firstSymbol).len; |
| 406 | for (int i = firstSymbol + 1; i < firstSymbol + symbolCount; ++i) { |
| 407 | len += parser.symbols.at(i).len; |
| 408 | |
| 409 | if (parser.symbols.at(i: i - 1).start + parser.symbols.at(i: i - 1).len |
| 410 | != parser.symbols.at(i).start) { |
| 411 | canExtractValueByRef = false; |
| 412 | break; |
| 413 | } |
| 414 | } |
| 415 | if (canExtractValueByRef) { |
| 416 | const QCss::Symbol &sym = parser.symbols.at(i: firstSymbol); |
| 417 | value = sym.text.mid(position: sym.start, n: len); |
| 418 | } |
| 419 | } |
| 420 | if (!canExtractValueByRef) { |
| 421 | |
| 422 | for (int i = firstSymbol; i < parser.index - 1; ++i) |
| 423 | value += parser.symbols.at(i).lexem(); |
| 424 | } |
| 425 | |
| 426 | attributes.append(namespaceUri: QString(), name, value); |
| 427 | |
| 428 | parser.skipSpace(); |
| 429 | } |
| 430 | } |
| 431 | |
| 432 | void QSvgCssHandler::styleLookup(QSvgNode *node, QXmlStreamAttributes &attributes) const |
| 433 | { |
| 434 | QCss::StyleSelector::NodePtr cssNode; |
| 435 | cssNode.ptr = node; |
| 436 | QList<QCss::Declaration> decls = m_selector->declarationsForNode(node: cssNode); |
| 437 | |
| 438 | parseCSStoXMLAttrs(declarations: decls, attributes); |
| 439 | } |
| 440 | |
| 441 | QT_END_NAMESPACE |
| 442 | |