Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qsvgcsshandler.cpp
Go to the documentation of this file.
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
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
13
14namespace {
15
21
22bool fillPropertyEasing(const QList<CssKeyFrameValue> &keyFrames, QSvgAbstractAnimatedProperty *prop)
23{
24 for (const CssKeyFrameValue &keyFrameValue : keyFrames) {
25 QStringView timingFunctionStr = QSvgCssHandler::parseDecltoString(keyFrameValue.timingFunction);
26 QSvgEasingInterfacePtr easing;
27 if (timingFunctionStr == QStringLiteral("linear")) {
28 easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::Linear);
29 } else if (timingFunctionStr == QStringLiteral("ease-in")) {
30 easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::EaseIn);
31 } else if (timingFunctionStr == QStringLiteral("ease-out")) {
32 easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::EaseOut);
33 } else if (timingFunctionStr == QStringLiteral("ease-in-out")) {
34 easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::EaseInOut);
35 } else if (timingFunctionStr == QStringLiteral("step-end")) {
36 easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::Steps,
37 QSvgCssValues::StepValues{quint32(1),
38 QSvgCssValues::StepPosition::End});
39 } else if (timingFunctionStr == QStringLiteral("step-start")) {
40 easing = QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction::Steps,
41 QSvgCssValues::StepValues{quint32(1),
42 QSvgCssValues::StepPosition::Start});
43 }
44
45 prop->appendEasing(std::move(easing));
46 }
47
48
49
50 return true;
51}
52
53bool fillColorProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyColor *prop)
54{
55 for (const CssKeyFrameValue &keyFrame : keyFrames) {
56 if (keyFrame.values.size() != 1)
57 return false;
58
59 QString colorStr = keyFrame.values.first().toString();
60 QColor color = QColor::fromString(colorStr);
61 prop->appendColor(color);
62 prop->appendKeyFrame(keyFrame.keyFrame);
63 }
64
65 return true;
66}
67
68bool fillOpacityProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyFloat *prop)
69{
70 for (const CssKeyFrameValue &keyFrame : keyFrames) {
71 if (keyFrame.values.size() != 1)
72 return false;
73
74 QString opacity = keyFrame.values.first().toString();
75 prop->appendValue(opacity.toDouble());
76 prop->appendKeyFrame(keyFrame.keyFrame);
77 }
78
79 return true;
80}
81
82bool validateTransform(QList<QList<QSvgAnimatedPropertyTransform::TransformComponent>> &keyFrameComponents) {
83
84 if (keyFrameComponents.size() < 2)
85 return false;
86
87 qsizetype maxIndex = 0;
88 qsizetype maxSize = 0;
89 for (int i = 1; i < keyFrameComponents.size(); i++) {
90 auto &listA = keyFrameComponents[i - 1];
91 auto &listB = keyFrameComponents[i];
92 for (int j = 0; j < qMin(listA.size(), listB.size()); j++) {
93 auto typeA = listA.at(j).type;
94 auto typeB = listB.at(j).type;
95 // TODO: Handle type mismatch as mentioned in CSS Transform Module specs.
96 if (typeA != typeB)
97 return false;
98 }
99
100 if (listA.size() > maxSize) {
101 maxIndex = i - 1;
102 maxSize = listA.size();
103 }
104
105 if (listB.size() > maxSize) {
106 maxIndex = i;
107 maxSize = listB.size();
108 }
109 }
110
111 const auto &longList = keyFrameComponents.at(maxIndex);
112 // pad the shorter list with identical transforms set to default values.
113 for (auto &list : keyFrameComponents) {
114 qsizetype size = list.size();
115
116 for (int j = size; j < maxSize; j++) {
117 QSvgAnimatedPropertyTransform::TransformComponent comp = longList.value(j);
118 switch (comp.type) {
119 case QSvgAnimatedPropertyTransform::TransformComponent::Translate:
120 case QSvgAnimatedPropertyTransform::TransformComponent::Skew:
121 comp.values[0] = 0;
122 comp.values[1] = 0;
123 break;
124 case QSvgAnimatedPropertyTransform::TransformComponent::Rotate:
125 comp.values[0] = 0;
126 comp.values[1] = 0;
127 comp.values[2] = 0;
128 break;
129 case QSvgAnimatedPropertyTransform::TransformComponent::Scale:
130 comp.values[0] = 1;
131 comp.values[1] = 1;
132 break;
133 default:
134 break;
135 }
136 list.append(comp);
137 }
138 }
139
140 return true;
141}
142
143bool fillTransformProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyTransform *prop)
144{
145 // Stores each key frame's list of components
146 QList<QList<QSvgAnimatedPropertyTransform::TransformComponent>> keyFramesComponents;
147
148 for (const CssKeyFrameValue &keyFrame : keyFrames) {
149 QList<QSvgAnimatedPropertyTransform::TransformComponent> components;
150 for (const QCss::Value &val : keyFrame.values) {
151 if (val.type == QCss::Value::Function) {
152 const QStringList lst = val.variant.toStringList();
153 const QStringView transformType = lst.value(0);
154 const QList<QStringView> args =
155 QStringView{ lst.value(1) }.split(QStringLiteral(","), Qt::SkipEmptyParts);
156 if (transformType == QStringLiteral("scale")) {
157 QSvgAnimatedPropertyTransform::TransformComponent component;
158 qreal scale0 = QSvgUtils::toDouble(args.value(0).trimmed());
159 qreal scale1 = QSvgUtils::toDouble(args.value(1).trimmed());
160 component.type = QSvgAnimatedPropertyTransform::TransformComponent::Scale;
161 component.values.append(scale0);
162 component.values.append(scale1);
163 components.append(component);
164 } else if (transformType == QStringLiteral("translate")) {
165 QSvgAnimatedPropertyTransform::TransformComponent component;
166 QSvgUtils::LengthType type;
167 qreal translate0 = QSvgUtils::parseLength(args.value(0), &type);
168 translate0 = QSvgUtils::convertToPixels(translate0, false, type);
169 qreal translate1 = QSvgUtils::parseLength(args.value(1), &type);
170 translate1 = QSvgUtils::convertToPixels(translate1, false, type);
171 component.type = QSvgAnimatedPropertyTransform::TransformComponent::Translate;
172 component.values.append(translate0);
173 component.values.append(translate1);
174 components.append(component);
175 } else if (transformType == QStringLiteral("rotate")) {
176 QSvgAnimatedPropertyTransform::TransformComponent component;
177 qreal rotationAngle = QSvgUtils::parseAngle(args.value(0)).value_or(0);
178 component.type = QSvgAnimatedPropertyTransform::TransformComponent::Rotate;
179 component.values.append(rotationAngle);
180 component.values.append(0);
181 component.values.append(0);
182 components.append(component);
183 } else if (transformType == QStringLiteral("skew")) {
184 QSvgAnimatedPropertyTransform::TransformComponent component;
185 qreal skew0 = QSvgUtils::parseAngle(args.value(0)).value_or(0);
186 qreal skew1 = QSvgUtils::parseAngle(args.value(1)).value_or(0);
187 component.type = QSvgAnimatedPropertyTransform::TransformComponent::Skew;
188 component.values.append(skew0);
189 component.values.append(skew1);
190 components.append(component);
191 } else if (transformType == QStringLiteral("matrix")) {
192 QSvgAnimatedPropertyTransform::TransformComponent component1, component2, component3;
193 QSvgUtils::LengthType type;
194 qreal translate0 = QSvgUtils::parseLength(args.value(4), &type);
195 translate0 = QSvgUtils::convertToPixels(translate0, false, type);
196 qreal translate1 = QSvgUtils::parseLength(args.value(5), &type);
197 translate1 = QSvgUtils::convertToPixels(translate1, false, type);
198 qreal scale0 = QSvgUtils::toDouble(args.value(0).trimmed());
199 qreal scale1 = QSvgUtils::toDouble(args.value(3).trimmed());
200 qreal skew0 = QSvgUtils::toDouble((args.value(1).trimmed()));
201 qreal skew1 = QSvgUtils::toDouble((args.value(2).trimmed()));
202 component1.type = QSvgAnimatedPropertyTransform::TransformComponent::Translate;
203 component1.values.append(translate0);
204 component1.values.append(translate1);
205 component2.type = QSvgAnimatedPropertyTransform::TransformComponent::Scale;
206 component2.values.append(scale0);
207 component2.values.append(scale1);
208 component3.type = QSvgAnimatedPropertyTransform::TransformComponent::Skew;
209 component3.values.append(skew0);
210 component3.values.append(skew1);
211 components.append(component1);
212 components.append(component2);
213 components.append(component3);
214 }
215 }
216 }
217 keyFramesComponents.append(components);
218 prop->appendKeyFrame(keyFrame.keyFrame);
219 }
220
221 if (!validateTransform(keyFramesComponents))
222 return false;
223
224 for (const auto &comp : std::as_const(keyFramesComponents)) {
225 prop->appendComponents(comp);
226 }
227 prop->setTransformCount(keyFramesComponents.first().size());
228
229 return true;
230}
231
232bool fillOffsetDistanceProperty(const QList<CssKeyFrameValue> &keyFrames, QSvgAnimatedPropertyFloat *prop)
233{
234 for (const CssKeyFrameValue &keyFrame : keyFrames) {
235 if (keyFrame.values.size() != 1)
236 return false;
237
238 QString offsetDistance = keyFrame.values.first().toString();
239
240 bool ok = false;
241 qreal distance = offsetDistance.toDouble(&ok);
242 if (!ok)
243 return false;
244
245 QCss::Value::Type type = keyFrame.values.first().type;
246 if (type != QCss::Value::Percentage && !qFuzzyCompare(distance, 0.))
247 return false;
248 distance /= 100;
249 prop->appendValue(distance);
250 prop->appendKeyFrame(keyFrame.keyFrame);
251 }
252
253 return true;
254}
255
256}
257
258QSvgCssHandler::QSvgCssHandler()
259 : m_selector(new QSvgStyleSelector)
260{
261
262}
263
264QSvgCssHandler::~QSvgCssHandler()
265{
266 delete m_selector;
267 m_selector = nullptr;
268}
269
270QSvgCssAnimation *QSvgCssHandler::createAnimation(QStringView name)
271{
272 if (!m_animations.contains(name))
273 return nullptr;
274
275 QCss::AnimationRule animationRule = m_animations[name];
276 QHash<QString, QSvgAbstractAnimatedProperty*> animatedProperies;
277 QSvgCssAnimation *animation = new QSvgCssAnimation;
278
279
280 // Css Parser returns a list of all properties for each key frame. Here,
281 // we store the key frames and values for each property for easier parsing.
282 QHash<QString, QList<CssKeyFrameValue>> keyFrameValues;
283 for (const auto &ruleSet : std::as_const(animationRule.ruleSets)) {
284 for (const QCss::Declaration &decl : ruleSet.declarations) {
285 QCss::Value timingFunction;
286 CssKeyFrameValue keyFrameValue = {ruleSet.keyFrame, decl.d->values, ruleSet.timingFunction};
287 QList<CssKeyFrameValue> &value = keyFrameValues[decl.d->property];
288 value.append(keyFrameValue);
289 }
290 }
291
292 for (auto it = keyFrameValues.begin(); it != keyFrameValues.end(); it++) {
293 QStringView property = it.key();
294 const QList<CssKeyFrameValue> &keyFrames = it.value();
295 auto *prop = QSvgAbstractAnimatedProperty::createAnimatedProperty(property.toString());
296 if (!prop)
297 continue;
298
299 bool result = false;
300 if (property == QLatin1StringView("fill") || property == QLatin1StringView("stroke"))
301 result = fillColorProperty(keyFrames, static_cast<QSvgAnimatedPropertyColor*>(prop));
302 else if (property == QLatin1StringView("transform"))
303 result = fillTransformProperty(keyFrames, static_cast<QSvgAnimatedPropertyTransform*>(prop));
304 else if (property == QLatin1StringView("fill-opacity") || property == QLatin1StringView("stroke-opacity")
305 || property == QLatin1StringView("opacity"))
306 result = fillOpacityProperty(keyFrames, static_cast<QSvgAnimatedPropertyFloat*>(prop));
307 else if (property == QLatin1StringView("offset-distance"))
308 result = fillOffsetDistanceProperty(keyFrames, static_cast<QSvgAnimatedPropertyFloat*>(prop));
309
310 result &= fillPropertyEasing(keyFrames, prop);
311 if (!result) {
312 delete prop;
313 continue;
314 }
315
316 animatedProperies[property] = prop;
317 }
318
319 for (auto it = animatedProperies.begin(); it != animatedProperies.end(); it++)
320 animation->appendProperty(it.value());
321
322 return animation;
323}
324
325QSvgCssEasingPtr QSvgCssHandler::createEasing(QSvgCssValues::EasingFunction easingFunction,
326 const QSvgCssValues::EasingValues &values)
327{
328 QSvgCssEasingPtr easing;
329
330 switch (easingFunction) {
331 case QSvgCssValues::EasingFunction::Ease:
332 case QSvgCssValues::EasingFunction::EaseIn:
333 case QSvgCssValues::EasingFunction::EaseOut:
334 case QSvgCssValues::EasingFunction::EaseInOut:
335 case QSvgCssValues::EasingFunction::Linear:
336 easing = createEasingFromKeyword(easingFunction);
337 break;
338 case QSvgCssValues::EasingFunction::Steps:
339 easing = createStepsEasing(std::get<QSvgCssValues::StepValues>(values));
340 break;
341 default:
342 easing = createEasingFromKeyword(QSvgCssValues::EasingFunction::Ease);
343 break;
344 }
345
346 return easing;
347}
348
349void QSvgCssHandler::collectAnimations(const QCss::StyleSheet &sheet)
350{
351 auto sortFunction = [](QCss::AnimationRule::AnimationRuleSet r1, QCss::AnimationRule::AnimationRuleSet r2) {
352 return r1.keyFrame < r2.keyFrame;
353 };
354
355 QList<QCss::AnimationRule> animationRules = sheet.animationRules;
356 for (QCss::AnimationRule &rule : animationRules) {
357 std::sort(rule.ruleSets.begin(), rule.ruleSets.end(), sortFunction);
358 m_animations[rule.animName] = rule;
359 }
360}
361
362void QSvgCssHandler::parseStyleSheet(const QStringView str)
363{
364 QString css = str.toString();
365 QCss::StyleSheet sheet;
366 QCss::Parser(css).parse(&sheet);
367 m_selector->styleSheets.append(sheet);
368
369 collectAnimations(sheet);
370}
371
372QString QSvgCssHandler::parseDecltoString(const QCss::Declaration &decl)
373{
374 if (decl.d->property.isEmpty())
375 return QString();
376
377 QString valueStr;
378 const int valCount = decl.d->values.size();
379 for (int i = 0; i < valCount; ++i) {
380 QCss::Value val = decl.d->values.at(i);
381 switch (val.type) {
382 case QCss::Value::TermOperatorComma:
383 valueStr += QLatin1Char(';');
384 break;
385 case QCss::Value::Uri:
386 {
387 QString temp = val.toString();
388 temp.prepend(QLatin1String("url("));
389 temp.append(QLatin1Char(')'));
390 valueStr += temp;
391 break;
392 }
393 case QCss::Value::Function:
394 {
395 QStringList lst = val.variant.toStringList();
396 valueStr.append(lst.at(0));
397 valueStr.append(QLatin1Char('('));
398 for (int i = 1; i < lst.size(); ++i) {
399 valueStr.append(lst.at(i));
400 if ((i +1) < lst.size())
401 valueStr.append(QLatin1Char(','));
402 }
403 valueStr.append(QLatin1Char(')'));
404 break;
405 }
406 case QCss::Value::KnownIdentifier:
407 switch (val.variant.toInt()) {
408 case QCss::Value_None:
409 valueStr += QLatin1String("none");
410 break;
411 case QCss::Value_Auto:
412 valueStr += QLatin1String("auto");
413 break;
414 default:
415 valueStr += val.toString();
416 break;
417 }
418 break;
419 case QCss::Value::Percentage:
420 valueStr += val.toString() + QLatin1Char('%');
421 break;
422 default:
423 valueStr += val.toString();
424 break;
425 }
426
427 if (i + 1 < valCount)
428 valueStr += QLatin1Char(' ');
429 }
430
431 return valueStr;
432}
433
434void QSvgCssHandler::parseCSStoXMLAttrs(const QList<QCss::Declaration> &declarations, QXmlStreamAttributes &attributes) const
435{
436 for (int i = 0; i < declarations.size(); ++i) {
437 const QCss::Declaration &decl = declarations.at(i);
438 QString valueStr = parseDecltoString(decl);
439 if (!valueStr.isEmpty())
440 attributes.append(QString(), decl.d->property, valueStr);
441 }
442}
443
444void QSvgCssHandler::parseCSStoXMLAttrs(const QString &css, QXmlStreamAttributes &attributes) const
445{
446 // preprocess (for unicode escapes), tokenize and remove comments
447 QCss::Parser parser(css);
448 QString key;
449
450 while (parser.hasNext()) {
451 parser.skipSpace();
452
453 if (!parser.hasNext())
454 break;
455 parser.next();
456
457 QString name;
458 QString value;
459
460 if (parser.hasEscapeSequences) {
461 key = parser.lexem();
462 name = key;
463 } else {
464 const QCss::Symbol &sym = parser.symbol();
465 name = sym.text.mid(sym.start, sym.len);
466 }
467
468 parser.skipSpace();
469 if (!parser.test(QCss::COLON))
470 break;
471
472 parser.skipSpace();
473 if (!parser.hasNext())
474 break;
475
476 const int firstSymbol = parser.index;
477 int symbolCount = 0;
478 do {
479 parser.next();
480 ++symbolCount;
481 } while (parser.hasNext() && !parser.test(QCss::SEMICOLON));
482
483 bool canExtractValueByRef = !parser.hasEscapeSequences;
484 if (canExtractValueByRef) {
485 int len = parser.symbols.at(firstSymbol).len;
486 for (int i = firstSymbol + 1; i < firstSymbol + symbolCount; ++i) {
487 len += parser.symbols.at(i).len;
488
489 if (parser.symbols.at(i - 1).start + parser.symbols.at(i - 1).len
490 != parser.symbols.at(i).start) {
491 canExtractValueByRef = false;
492 break;
493 }
494 }
495 if (canExtractValueByRef) {
496 const QCss::Symbol &sym = parser.symbols.at(firstSymbol);
497 value = sym.text.mid(sym.start, len);
498 }
499 }
500 if (!canExtractValueByRef) {
501
502 for (int i = firstSymbol; i < parser.index - 1; ++i)
503 value += parser.symbols.at(i).lexem();
504 }
505
506 attributes.append(QString(), name, value);
507
508 parser.skipSpace();
509 }
510}
511
512void QSvgCssHandler::styleLookup(QSvgNode *node, QXmlStreamAttributes &attributes) const
513{
514 QCss::StyleSelector::NodePtr cssNode;
515 cssNode.ptr = node;
516 QList<QCss::Declaration> decls = m_selector->declarationsForNode(cssNode);
517
518 parseCSStoXMLAttrs(decls, attributes);
519}
520
521QSvgCssEasingPtr QSvgCssHandler::createEasingFromKeyword(QSvgCssValues::EasingFunction easingFunction)
522{
523 constexpr QPointF easeC1(0.25, 0.1);
524 constexpr QPointF easeC2(0.25, 1);
525 constexpr QPointF easeInC1(0.42, 0);
526 constexpr QPointF easeInC2(1, 1);
527 constexpr QPointF easeOutC1(0, 0);
528 constexpr QPointF easeOutC2(0.58, 1);
529 constexpr QPointF linearC1(0, 0);
530 constexpr QPointF linearC2(1, 1);
531
532 QSvgCssEasingPtr easing;
533
534 switch (easingFunction) {
535 case QSvgCssValues::EasingFunction::Ease:
536 easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, easeC1, easeC2);
537 break;
538 case QSvgCssValues::EasingFunction::EaseIn:
539 easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, easeInC1, easeInC2);
540 break;
541 case QSvgCssValues::EasingFunction::EaseOut:
542 easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, easeOutC1, easeOutC2);
543 break;
544 case QSvgCssValues::EasingFunction::EaseInOut:
545 easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, easeInC1, easeOutC2);
546 break;
547 case QSvgCssValues::EasingFunction::Linear:
548 easing = std::make_unique<QSvgCssCubicBezierEasing>(easingFunction, linearC1, linearC2);
549 break;
550 default:
551 Q_UNREACHABLE();
552 break;
553 }
554
555 return easing;
556}
557
558QSvgCssEasingPtr QSvgCssHandler::createStepsEasing(const QSvgCssValues::StepValues &values)
559{
560 quint32 steps = values.steps;
561 QSvgCssValues::StepPosition position = values.stepPosition;
562
563 return std::make_unique<QSvgCssStepsEasing>(steps, position);
564}
565
566QT_END_NAMESPACE
Combined button and popup list for selecting options.
bool fillTransformProperty(const QList< CssKeyFrameValue > &keyFrames, QSvgAnimatedPropertyTransform *prop)
bool fillOffsetDistanceProperty(const QList< CssKeyFrameValue > &keyFrames, QSvgAnimatedPropertyFloat *prop)
bool fillOpacityProperty(const QList< CssKeyFrameValue > &keyFrames, QSvgAnimatedPropertyFloat *prop)
bool fillPropertyEasing(const QList< CssKeyFrameValue > &keyFrames, QSvgAbstractAnimatedProperty *prop)
bool fillColorProperty(const QList< CssKeyFrameValue > &keyFrames, QSvgAnimatedPropertyColor *prop)
bool validateTransform(QList< QList< QSvgAnimatedPropertyTransform::TransformComponent > > &keyFrameComponents)