1// Copyright (C) 2024 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
4#include "qquickqmlgenerator_p.h"
5#include "qquicknodeinfo_p.h"
6#include "utils_p.h"
7
8#include <private/qsgcurveprocessor_p.h>
9#include <private/qquickshape_p.h>
10#include <private/qquadpath_p.h>
11#include <private/qquickitem_p.h>
12#include <private/qquickimagebase_p_p.h>
13#include <private/qquicktext_p.h>
14#include <private/qquicktranslate_p.h>
15#include <private/qquickimage_p.h>
16
17#include <QtCore/qloggingcategory.h>
18#include <QtCore/qdir.h>
19
20QT_BEGIN_NAMESPACE
21
22QQuickQmlGenerator::QQuickQmlGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, const QString &outFileName)
23 : QQuickGenerator(fileName, flags)
24 , outputFileName(outFileName)
25{
26 m_result.open(openMode: QIODevice::ReadWrite);
27}
28
29QQuickQmlGenerator::~QQuickQmlGenerator()
30{
31}
32
33bool QQuickQmlGenerator::save()
34{
35 bool res = true;
36 if (!outputFileName.isEmpty()) {
37 QFileInfo fileInfo(outputFileName);
38 QDir dir(fileInfo.absolutePath());
39 if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
40 qCWarning(lcQuickVectorImage) << "Failed to create path" << dir.absolutePath();
41 res = false;
42 } else {
43 QFile outFile(outputFileName);
44 if (outFile.open(flags: QIODevice::WriteOnly)) {
45 outFile.write(data: m_result.data());
46 outFile.close();
47 } else {
48 qCWarning(lcQuickVectorImage) << "Failed to write to file" << outFile.fileName();
49 res = false;
50 }
51 }
52 }
53
54 if (lcQuickVectorImage().isDebugEnabled())
55 qCDebug(lcQuickVectorImage).noquote() << m_result.data().left(n: 300);
56
57 return res;
58}
59
60void QQuickQmlGenerator::setShapeTypeName(const QString &name)
61{
62 m_shapeTypeName = name.toLatin1();
63}
64
65QString QQuickQmlGenerator::shapeTypeName() const
66{
67 return QString::fromLatin1(ba: m_shapeTypeName);
68}
69
70void QQuickQmlGenerator::setCommentString(const QString commentString)
71{
72 m_commentString = commentString;
73}
74
75QString QQuickQmlGenerator::commentString() const
76{
77 return m_commentString;
78}
79
80QString QQuickQmlGenerator::generateNodeBase(const NodeInfo &info)
81{
82 auto layerIdString = [](int layerId) {
83 return QStringLiteral("_qt_layer%1").arg(a: layerId);
84 };
85
86 if (!info.nodeId.isEmpty())
87 stream() << "objectName: \"" << info.nodeId << "\"";
88
89 static int counter = 0;
90 QString idString;
91 if (info.layerNum >= 0)
92 idString = layerIdString(info.layerNum);
93 else
94 idString = QStringLiteral("_qt_node%1").arg(a: counter++);
95 stream() << "id: " << idString;
96
97 if (!info.isDefaultOpacity)
98 stream() << "opacity: " << info.opacity.defaultValue().toReal();
99
100 const bool hasTransformReference = (info.transformReferenceLayerNum >= 0);
101 const bool hasTransform = info.transform.isAnimated() || !info.isDefaultTransform || hasTransformReference;
102 if (hasTransform) {
103 stream() << "transform: TransformGroup {";
104 m_indentLevel++;
105 stream() << "id: " << idString << "_transform_base_group";
106
107 if (info.transform.isAnimated()) {
108 for (int groupIndex = 0; groupIndex < info.transform.animationGroupCount(); ++groupIndex) {
109 stream() << "TransformGroup {";
110 m_indentLevel++;
111 stream() << "id: " << idString << "_transform_group_" << groupIndex;
112
113 int animationStart = info.transform.animationGroup(index: groupIndex);
114 int nextAnimationStart = groupIndex + 1 < info.transform.animationGroupCount()
115 ? info.transform.animationGroup(index: groupIndex + 1)
116 : info.transform.animationCount();
117
118 for (int i = nextAnimationStart - 1; i >= animationStart; --i) {
119 const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(index: i);
120 if (animation.frames.isEmpty())
121 continue;
122
123 const QVariantList &parameters = animation.frames.first().value<QVariantList>();
124 switch (animation.subtype) {
125 case QTransform::TxTranslate:
126 if (animation.isConstant()) {
127 const QPointF translation = parameters.value(i: 0).value<QPointF>();
128 if (!translation.isNull())
129 stream() << "Translate { x: " << translation.x() << "; y: " << translation.y() << " }";
130 } else {
131 stream() << "Translate { id: " << idString << "_transform_" << groupIndex << "_" << i << " }";
132 }
133 break;
134 case QTransform::TxScale:
135 if (animation.isConstant()) {
136 const QPointF scale = parameters.value(i: 0).value<QPointF>();
137 if (scale != QPointF(1, 1))
138 stream() << "Scale { xScale: " << scale.x() << "; yScale: " << scale.y() << " }";
139 } else {
140 stream() << "Scale { id: " << idString << "_transform_" << groupIndex << "_" << i << "}";
141 }
142 break;
143 case QTransform::TxRotate:
144 if (animation.isConstant()) {
145 const QPointF center = parameters.value(i: 0).value<QPointF>();
146 const qreal angle = parameters.value(i: 1).toReal();
147 if (!qFuzzyIsNull(d: angle))
148 stream() << "Rotation { angle: " << angle << "; origin.x: " << center.x() << "; origin.y: " << center.y() << " }"; //### center relative to what?
149 } else {
150 stream() << "Rotation { id: " << idString << "_transform_" << groupIndex << "_" << i << " }";
151 }
152 break;
153 case QTransform::TxShear:
154 if (animation.isConstant()) {
155 const QPointF skew = parameters.value(i: 0).value<QPointF>();
156 if (!skew.isNull())
157 stream() << "Shear { xAngle: " << skew.x() << "; yAngle: " << skew.y() << " }";
158 } else {
159 stream() << "Shear { id: " << idString << "_transform_" << groupIndex << "_" << i << " }";
160 }
161 break;
162 default:
163 Q_UNREACHABLE();
164 }
165 }
166
167 m_indentLevel--;
168 stream() << "}";
169 }
170 }
171
172 if (!info.isDefaultTransform) {
173 QTransform xf = info.transform.defaultValue().value<QTransform>();
174 if (xf.type() <= QTransform::TxTranslate) {
175 stream() << "Translate { x: " << xf.dx() << "; y: " << xf.dy() << "}";
176 } else {
177 stream() << "Matrix4x4 { matrix: ";
178 generateTransform(xf);
179 stream(flags: SameLine) << "}";
180 }
181 }
182
183 if (hasTransformReference) {
184 stream() << "Matrix4x4 { matrix: " << layerIdString(info.transformReferenceLayerNum)
185 << ".transformMatrix }";
186 }
187
188 m_indentLevel--;
189 stream() << "}";
190
191 generateAnimateTransform(targetName: idString, info);
192 }
193
194 if (info.opacity.isAnimated())
195 generatePropertyAnimation(property: info.opacity, targetName: idString, QStringLiteral("opacity"));
196
197 generatePropertyAnimation(property: info.visibility, targetName: idString, QStringLiteral("visible"));
198
199 return idString;
200}
201
202bool QQuickQmlGenerator::generateDefsNode(const NodeInfo &info)
203{
204 Q_UNUSED(info)
205
206 return false;
207}
208
209void QQuickQmlGenerator::generateImageNode(const ImageNodeInfo &info)
210{
211 if (!isNodeVisible(info))
212 return;
213
214 const QFileInfo outputFileInfo(outputFileName);
215 const QDir outputDir(outputFileInfo.absolutePath());
216
217 QString filePath;
218
219 if (!m_retainFilePaths || info.externalFileReference.isEmpty()) {
220 filePath = m_assetFileDirectory;
221 if (filePath.isEmpty())
222 filePath = outputDir.absolutePath();
223
224 if (!filePath.isEmpty() && !filePath.endsWith(c: u'/'))
225 filePath += u'/';
226
227 QDir fileDir(filePath);
228 if (!fileDir.exists()) {
229 if (!fileDir.mkpath(QStringLiteral(".")))
230 qCWarning(lcQuickVectorImage) << "Failed to create image resource directory:" << filePath;
231 }
232
233 filePath += QStringLiteral("%1%2.png").arg(a: m_assetFilePrefix.isEmpty()
234 ? QStringLiteral("svg_asset_")
235 : m_assetFilePrefix)
236 .arg(a: info.image.cacheKey());
237
238 if (!info.image.save(fileName: filePath))
239 qCWarning(lcQuickVectorImage) << "Unabled to save image resource" << filePath;
240 qCDebug(lcQuickVectorImage) << "Saving copy of IMAGE" << filePath;
241 } else {
242 filePath = info.externalFileReference;
243 }
244
245 const QFileInfo assetFileInfo(filePath);
246
247 stream() << "Image {";
248
249 m_indentLevel++;
250 generateNodeBase(info);
251 stream() << "x: " << info.rect.x();
252 stream() << "y: " << info.rect.y();
253 stream() << "width: " << info.rect.width();
254 stream() << "height: " << info.rect.height();
255 stream() << "source: \"" << m_urlPrefix << outputDir.relativeFilePath(fileName: assetFileInfo.absoluteFilePath()) <<"\"";
256
257 m_indentLevel--;
258
259 stream() << "}";
260}
261
262void QQuickQmlGenerator::generatePath(const PathNodeInfo &info, const QRectF &overrideBoundingRect)
263{
264 if (!isNodeVisible(info))
265 return;
266
267 if (m_inShapeItemLevel > 0) {
268 if (!info.isDefaultTransform)
269 qWarning() << "Skipped transform for node" << info.nodeId << "type" << info.typeName << "(this is not supposed to happen)";
270 optimizePaths(info, overrideBoundingRect);
271 } else {
272 m_inShapeItemLevel++;
273 stream() << shapeName() << " {";
274
275 m_indentLevel++;
276 generateNodeBase(info);
277
278 if (m_flags.testFlag(flag: QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer))
279 stream() << "preferredRendererType: Shape.CurveRenderer";
280 optimizePaths(info, overrideBoundingRect);
281 //qCDebug(lcQuickVectorGraphics) << *node->qpath();
282 m_indentLevel--;
283 stream() << "}";
284 m_inShapeItemLevel--;
285 }
286}
287
288void QQuickQmlGenerator::generateGradient(const QGradient *grad)
289{
290 if (grad->type() == QGradient::LinearGradient) {
291 auto *linGrad = static_cast<const QLinearGradient *>(grad);
292 stream() << "fillGradient: LinearGradient {";
293 m_indentLevel++;
294
295 QRectF gradRect(linGrad->start(), linGrad->finalStop());
296
297 stream() << "x1: " << gradRect.left();
298 stream() << "y1: " << gradRect.top();
299 stream() << "x2: " << gradRect.right();
300 stream() << "y2: " << gradRect.bottom();
301 for (auto &stop : linGrad->stops())
302 stream() << "GradientStop { position: " << QString::number(stop.first, format: 'g', precision: 7)
303 << "; color: \"" << stop.second.name(format: QColor::HexArgb) << "\" }";
304 m_indentLevel--;
305 stream() << "}";
306 } else if (grad->type() == QGradient::RadialGradient) {
307 auto *radGrad = static_cast<const QRadialGradient*>(grad);
308 stream() << "fillGradient: RadialGradient {";
309 m_indentLevel++;
310
311 stream() << "centerX: " << radGrad->center().x();
312 stream() << "centerY: " << radGrad->center().y();
313 stream() << "centerRadius: " << radGrad->radius();
314 stream() << "focalX:" << radGrad->focalPoint().x();
315 stream() << "focalY:" << radGrad->focalPoint().y();
316 for (auto &stop : radGrad->stops())
317 stream() << "GradientStop { position: " << QString::number(stop.first, format: 'g', precision: 7)
318 << "; color: \"" << stop.second.name(format: QColor::HexArgb) << "\" }";
319 m_indentLevel--;
320 stream() << "}";
321 }
322}
323
324void QQuickQmlGenerator::generateAnimationBindings()
325{
326 QString prefix;
327 if (Q_UNLIKELY(!isRuntimeGenerator()))
328 prefix = QStringLiteral(".animations");
329
330 stream() << "loops: " << m_topLevelIdString << prefix << ".loops";
331 stream() << "paused: " << m_topLevelIdString << prefix << ".paused";
332 stream() << "running: true";
333
334 // We need to reset the animation when the loop count changes
335 stream() << "onLoopsChanged: { if (running) { restart() } }";
336}
337
338void QQuickQmlGenerator::generateEasing(const QQuickAnimatedProperty::PropertyAnimation &animation, int time)
339{
340 if (animation.easingPerFrame.contains(key: time)) {
341 QBezier bezier = animation.easingPerFrame.value(key: time);
342 QPointF c1 = bezier.pt2();
343 QPointF c2 = bezier.pt3();
344
345 bool isLinear = (c1 == c1.transposed() && c2 == c2.transposed());
346 if (!isLinear) {
347 int nextIdx = m_easings.size();
348 QString &id = m_easings[{c1.x(), c1.y(), c2.x(), c2.y()}];
349 if (id.isNull())
350 id = QString(QLatin1String("easing_%1")).arg(a: nextIdx, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0'));
351 stream() << "easing: " << m_topLevelIdString << "." << id;
352 }
353 }
354}
355
356void QQuickQmlGenerator::generatePropertyAnimation(const QQuickAnimatedProperty &property,
357 const QString &targetName,
358 const QString &propertyName,
359 AnimationType animationType)
360{
361 if (!property.isAnimated())
362 return;
363
364 QString mainAnimationId = targetName
365 + QStringLiteral("_")
366 + propertyName
367 + QStringLiteral("_animation");
368 mainAnimationId.replace(before: QLatin1Char('.'), after: QLatin1Char('_'));
369
370 QString prefix;
371 if (Q_UNLIKELY(!isRuntimeGenerator()))
372 prefix = QStringLiteral(".animations");
373
374 stream() << "Connections { target: " << m_topLevelIdString << prefix << "; function onRestart() {" << mainAnimationId << ".restart() } }";
375
376 stream() << "ParallelAnimation {";
377 m_indentLevel++;
378
379 stream() << "id: " << mainAnimationId;
380
381 generateAnimationBindings();
382
383 for (int i = 0; i < property.animationCount(); ++i) {
384 const QQuickAnimatedProperty::PropertyAnimation &animation = property.animation(index: i);
385
386 stream() << "SequentialAnimation {";
387 m_indentLevel++;
388
389 const int startOffset = animation.startOffset;
390 if (startOffset > 0)
391 stream() << "PauseAnimation { duration: " << startOffset << " }";
392
393 stream() << "SequentialAnimation {";
394 m_indentLevel++;
395
396 const int repeatCount = animation.repeatCount;
397 if (repeatCount < 0)
398 stream() << "loops: Animation.Infinite";
399 else
400 stream() << "loops: " << repeatCount;
401
402 int previousTime = 0;
403 QVariant previousValue;
404 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
405 const int time = it.key();
406 const int frameTime = time - previousTime;
407 const QVariant &value = it.value();
408
409 if (previousValue.isValid() && previousValue == value) {
410 if (frameTime > 0)
411 stream() << "PauseAnimation { duration: " << frameTime << " }";
412 } else if (animationType == AnimationType::Auto && value.typeId() == QMetaType::Bool) {
413 // We special case bools, with PauseAnimation and then a setter at the end
414 if (frameTime > 0)
415 stream() << "PauseAnimation { duration: " << frameTime << " }";
416 stream() << "ScriptAction {";
417 m_indentLevel++;
418
419 stream() << "script:" << targetName << "." << propertyName << " = " << value.toString();
420
421 m_indentLevel--;
422 stream() << "}";
423 } else {
424 generateAnimatedPropertySetter(targetName,
425 propertyName,
426 value,
427 animation,
428 time: frameTime,
429 frameTime: time,
430 animationType);
431 }
432
433 previousTime = time;
434 previousValue = value;
435 }
436
437 if (!(animation.flags & QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd)) {
438 stream() << "ScriptAction {";
439 m_indentLevel++;
440 stream() << "script: ";
441
442 switch (animationType) {
443 case AnimationType::Auto:
444 stream(flags: SameLine) << targetName << "." << propertyName << " = ";
445 break;
446 case AnimationType::ColorOpacity:
447 stream(flags: SameLine) << targetName << "." << propertyName << ".a = ";
448 break;
449 };
450
451 QVariant value = property.defaultValue();
452 if (value.typeId() == QMetaType::QColor)
453 stream(flags: SameLine) << "\"" << value.toString() << "\"";
454 else
455 stream(flags: SameLine) << value.toReal();
456
457 m_indentLevel--;
458 stream() << "}";
459 }
460
461 m_indentLevel--;
462 stream() << "}";
463
464 m_indentLevel--;
465 stream() << "}";
466 }
467
468 m_indentLevel--;
469 stream() << "}";
470}
471
472void QQuickQmlGenerator::generateTransform(const QTransform &xf)
473{
474 if (xf.isAffine()) {
475 stream(flags: SameLine) << "PlanarTransform.fromAffineMatrix("
476 << xf.m11() << ", " << xf.m12() << ", "
477 << xf.m21() << ", " << xf.m22() << ", "
478 << xf.dx() << ", " << xf.dy() << ")";
479 } else {
480 QMatrix4x4 m(xf);
481 stream(flags: SameLine) << "Qt.matrix4x4(";
482 m_indentLevel += 3;
483 const auto *data = m.data();
484 for (int i = 0; i < 4; i++) {
485 stream() << data[i] << ", " << data[i+4] << ", " << data[i+8] << ", " << data[i+12];
486 if (i < 3)
487 stream(flags: SameLine) << ", ";
488 }
489 stream(flags: SameLine) << ")";
490 m_indentLevel -= 3;
491 }
492}
493
494void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainterPath *painterPath, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect)
495{
496 Q_UNUSED(pathSelector)
497 Q_ASSERT(painterPath || quadPath);
498
499 static int counter = 0;
500
501 const QColor strokeColor = info.strokeStyle.color.defaultValue().value<QColor>();
502 const bool noPen = strokeColor == QColorConstants::Transparent
503 && !info.strokeStyle.color.isAnimated()
504 && !info.strokeStyle.opacity.isAnimated();
505 if (pathSelector == QQuickVectorImageGenerator::StrokePath && noPen)
506 return;
507
508 const QColor fillColor = info.fillColor.defaultValue().value<QColor>();
509 const bool noFill = info.grad.type() == QGradient::NoGradient
510 && fillColor == QColorConstants::Transparent
511 && !info.fillColor.isAnimated()
512 && !info.fillOpacity.isAnimated();
513 if (pathSelector == QQuickVectorImageGenerator::FillPath && noFill)
514 return;
515
516 if (noPen && noFill)
517 return;
518
519 auto fillRule = QQuickShapePath::FillRule(painterPath ? painterPath->fillRule() : quadPath->fillRule());
520 stream() << "ShapePath {";
521 m_indentLevel++;
522
523 const QString shapePathId = QStringLiteral("_qt_shapePath_%1").arg(a: counter);
524 stream() << "id: " << shapePathId;
525
526 if (!info.nodeId.isEmpty()) {
527 switch (pathSelector) {
528 case QQuickVectorImageGenerator::FillPath:
529 stream() << "objectName: \"svg_fill_path:" << info.nodeId << "\"";
530 break;
531 case QQuickVectorImageGenerator::StrokePath:
532 stream() << "objectName: \"svg_stroke_path:" << info.nodeId << "\"";
533 break;
534 case QQuickVectorImageGenerator::FillAndStroke:
535 stream() << "objectName: \"svg_path:" << info.nodeId << "\"";
536 break;
537 }
538 }
539
540 if (noPen || !(pathSelector & QQuickVectorImageGenerator::StrokePath)) {
541 stream() << "strokeColor: \"transparent\"";
542 } else {
543 stream() << "strokeColor: \"" << strokeColor.name(format: QColor::HexArgb) << "\"";
544 stream() << "strokeWidth: " << info.strokeStyle.width;
545 stream() << "capStyle: " << QQuickVectorImageGenerator::Utils::strokeCapStyleString(strokeCapStyle: info.strokeStyle.lineCapStyle);
546 stream() << "joinStyle: " << QQuickVectorImageGenerator::Utils::strokeJoinStyleString(strokeJoinStyle: info.strokeStyle.lineJoinStyle);
547 stream() << "miterLimit: " << info.strokeStyle.miterLimit;
548 if (info.strokeStyle.dashArray.length() != 0) {
549 stream() << "strokeStyle: " << "ShapePath.DashLine";
550 stream() << "dashPattern: " << QQuickVectorImageGenerator::Utils::listString(list: info.strokeStyle.dashArray);
551 stream() << "dashOffset: " << info.strokeStyle.dashOffset;
552 }
553 }
554
555 QTransform fillTransform = info.fillTransform;
556 if (!(pathSelector & QQuickVectorImageGenerator::FillPath)) {
557 stream() << "fillColor: \"transparent\"";
558 } else if (info.grad.type() != QGradient::NoGradient) {
559 generateGradient(grad: &info.grad);
560 if (info.grad.coordinateMode() == QGradient::ObjectMode) {
561 QTransform objectToUserSpace;
562 objectToUserSpace.translate(dx: boundingRect.x(), dy: boundingRect.y());
563 objectToUserSpace.scale(sx: boundingRect.width(), sy: boundingRect.height());
564 fillTransform *= objectToUserSpace;
565 }
566 } else {
567 stream() << "fillColor: \"" << fillColor.name(format: QColor::HexArgb) << "\"";
568 }
569
570 if (!fillTransform.isIdentity()) {
571 const QTransform &xf = fillTransform;
572 stream() << "fillTransform: ";
573 if (info.fillTransform.type() == QTransform::TxTranslate)
574 stream(flags: SameLine) << "PlanarTransform.fromTranslate(" << xf.dx() << ", " << xf.dy() << ")";
575 else if (info.fillTransform.type() == QTransform::TxScale && !xf.dx() && !xf.dy())
576 stream(flags: SameLine) << "PlanarTransform.fromScale(" << xf.m11() << ", " << xf.m22() << ")";
577 else
578 generateTransform(xf);
579 }
580
581 if (info.trim.enabled) {
582 stream() << "trim.start: " << info.trim.start.defaultValue().toReal();
583 stream() << "trim.end: " << info.trim.end.defaultValue().toReal();
584 stream() << "trim.offset: " << info.trim.offset.defaultValue().toReal();
585
586 }
587
588 if (fillRule == QQuickShapePath::WindingFill)
589 stream() << "fillRule: ShapePath.WindingFill";
590 else
591 stream() << "fillRule: ShapePath.OddEvenFill";
592
593 QString hintStr;
594 if (quadPath)
595 hintStr = QQuickVectorImageGenerator::Utils::pathHintString(qp: *quadPath);
596 if (!hintStr.isEmpty())
597 stream() << hintStr;
598
599 QString svgPathString = painterPath ? QQuickVectorImageGenerator::Utils::toSvgString(path: *painterPath) : QQuickVectorImageGenerator::Utils::toSvgString(path: *quadPath);
600 stream() << "PathSvg { path: \"" << svgPathString << "\" }";
601
602 m_indentLevel--;
603 stream() << "}";
604
605 if (info.trim.enabled) {
606 generatePropertyAnimation(property: info.trim.start, targetName: shapePathId + QStringLiteral(".trim"), QStringLiteral("start"));
607 generatePropertyAnimation(property: info.trim.end, targetName: shapePathId + QStringLiteral(".trim"), QStringLiteral("end"));
608 generatePropertyAnimation(property: info.trim.offset, targetName: shapePathId + QStringLiteral(".trim"), QStringLiteral("offset"));
609 }
610
611 generatePropertyAnimation(property: info.strokeStyle.color, targetName: shapePathId, QStringLiteral("strokeColor"));
612 generatePropertyAnimation(property: info.strokeStyle.opacity, targetName: shapePathId, QStringLiteral("strokeColor"), animationType: AnimationType::ColorOpacity);
613 generatePropertyAnimation(property: info.fillColor, targetName: shapePathId, QStringLiteral("fillColor"));
614 generatePropertyAnimation(property: info.fillOpacity, targetName: shapePathId, QStringLiteral("fillColor"), animationType: AnimationType::ColorOpacity);
615
616 counter++;
617}
618
619void QQuickQmlGenerator::generateNode(const NodeInfo &info)
620{
621 if (!isNodeVisible(info))
622 return;
623
624 stream() << "// Missing Implementation for SVG Node: " << info.typeName;
625 stream() << "// Adding an empty Item and skipping";
626 stream() << "Item {";
627 m_indentLevel++;
628 generateNodeBase(info);
629 m_indentLevel--;
630 stream() << "}";
631}
632
633void QQuickQmlGenerator::generateTextNode(const TextNodeInfo &info)
634{
635 if (!isNodeVisible(info))
636 return;
637
638 static int counter = 0;
639 stream() << "Item {";
640 m_indentLevel++;
641 generateNodeBase(info);
642
643 if (!info.isTextArea)
644 stream() << "Item { id: textAlignItem_" << counter << "; x: " << info.position.x() << "; y: " << info.position.y() << "}";
645
646 stream() << "Text {";
647
648 m_indentLevel++;
649
650 const QString textItemId = QStringLiteral("_qt_textItem_%1").arg(a: counter);
651 stream() << "id: " << textItemId;
652
653 generatePropertyAnimation(property: info.fillColor, targetName: textItemId, QStringLiteral("color"));
654 generatePropertyAnimation(property: info.fillOpacity, targetName: textItemId, QStringLiteral("color"), animationType: AnimationType::ColorOpacity);
655 generatePropertyAnimation(property: info.strokeColor, targetName: textItemId, QStringLiteral("styleColor"));
656 generatePropertyAnimation(property: info.strokeOpacity, targetName: textItemId, QStringLiteral("styleColor"), animationType: AnimationType::ColorOpacity);
657
658 if (info.isTextArea) {
659 stream() << "x: " << info.position.x();
660 stream() << "y: " << info.position.y();
661 if (info.size.width() > 0)
662 stream() << "width: " << info.size.width();
663 if (info.size.height() > 0)
664 stream() << "height: " << info.size.height();
665 stream() << "wrapMode: Text.Wrap"; // ### WordWrap? verify with SVG standard
666 stream() << "clip: true"; //### Not exactly correct: should clip on the text level, not the pixel level
667 } else {
668 QString hAlign = QStringLiteral("left");
669 stream() << "anchors.baseline: textAlignItem_" << counter << ".top";
670 switch (info.alignment) {
671 case Qt::AlignHCenter:
672 hAlign = QStringLiteral("horizontalCenter");
673 break;
674 case Qt::AlignRight:
675 hAlign = QStringLiteral("right");
676 break;
677 default:
678 qCDebug(lcQuickVectorImage) << "Unexpected text alignment" << info.alignment;
679 Q_FALLTHROUGH();
680 case Qt::AlignLeft:
681 break;
682 }
683 stream() << "anchors." << hAlign << ": textAlignItem_" << counter << ".left";
684 }
685 counter++;
686
687 stream() << "color: \"" << info.fillColor.defaultValue().value<QColor>().name(format: QColor::HexArgb) << "\"";
688 stream() << "textFormat:" << (info.needsRichText ? "Text.RichText" : "Text.StyledText");
689
690 QString s = info.text;
691 s.replace(c: QLatin1Char('"'), after: QLatin1String("\\\""));
692 stream() << "text: \"" << s << "\"";
693 stream() << "font.family: \"" << info.font.family() << "\"";
694 if (info.font.pixelSize() > 0)
695 stream() << "font.pixelSize:" << info.font.pixelSize();
696 else if (info.font.pointSize() > 0)
697 stream() << "font.pixelSize:" << info.font.pointSizeF();
698 if (info.font.underline())
699 stream() << "font.underline: true";
700 if (info.font.weight() != QFont::Normal)
701 stream() << "font.weight: " << int(info.font.weight());
702 if (info.font.italic())
703 stream() << "font.italic: true";
704 switch (info.font.hintingPreference()) {
705 case QFont::PreferFullHinting:
706 stream() << "font.hintingPreference: Font.PreferFullHinting";
707 break;
708 case QFont::PreferVerticalHinting:
709 stream() << "font.hintingPreference: Font.PreferVerticalHinting";
710 break;
711 case QFont::PreferNoHinting:
712 stream() << "font.hintingPreference: Font.PreferNoHinting";
713 break;
714 case QFont::PreferDefaultHinting:
715 stream() << "font.hintingPreference: Font.PreferDefaultHinting";
716 break;
717 };
718
719 const QColor strokeColor = info.strokeColor.defaultValue().value<QColor>();
720 if (strokeColor != QColorConstants::Transparent || info.strokeColor.isAnimated()) {
721 stream() << "styleColor: \"" << strokeColor.name(format: QColor::HexArgb) << "\"";
722 stream() << "style: Text.Outline";
723 }
724
725 m_indentLevel--;
726 stream() << "}";
727
728 m_indentLevel--;
729 stream() << "}";
730}
731
732void QQuickQmlGenerator::generateUseNode(const UseNodeInfo &info)
733{
734 if (!isNodeVisible(info))
735 return;
736
737 if (info.stage == StructureNodeStage::Start) {
738 stream() << "Item {";
739 m_indentLevel++;
740 generateNodeBase(info);
741 stream() << "x: " << info.startPos.x();
742 stream() << "y: " << info.startPos.y();
743 } else {
744 m_indentLevel--;
745 stream() << "}";
746 }
747}
748
749void QQuickQmlGenerator::generatePathContainer(const StructureNodeInfo &info)
750{
751 Q_UNUSED(info);
752 stream() << shapeName() <<" {";
753 m_indentLevel++;
754 if (m_flags.testFlag(flag: QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer))
755 stream() << "preferredRendererType: Shape.CurveRenderer";
756 m_indentLevel--;
757
758 m_inShapeItemLevel++;
759}
760
761void QQuickQmlGenerator::generateAnimatedPropertySetter(const QString &targetName,
762 const QString &propertyName,
763 const QVariant &value,
764 const QQuickAnimatedProperty::PropertyAnimation &animation,
765 int frameTime,
766 int time,
767 AnimationType animationType)
768{
769 if (frameTime > 0) {
770 switch (animationType) {
771 case AnimationType::Auto:
772 if (value.typeId() == QMetaType::QColor)
773 stream() << "ColorAnimation {";
774 else
775 stream() << "PropertyAnimation {";
776 break;
777 case AnimationType::ColorOpacity:
778 stream() << "ColorOpacityAnimation {";
779 break;
780 };
781 m_indentLevel++;
782
783 stream() << "duration: " << frameTime;
784 stream() << "target: " << targetName;
785 stream() << "property: \"" << propertyName << "\"";
786 stream() << "to: ";
787 if (value.typeId() == QMetaType::QVector3D) {
788 const QVector3D &v = value.value<QVector3D>();
789 stream(flags: SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
790 } else if (value.typeId() == QMetaType::QColor) {
791 stream(flags: SameLine) << "\"" << value.toString() << "\"";
792 } else {
793 stream(flags: SameLine) << value.toReal();
794 }
795 generateEasing(animation, time);
796 m_indentLevel--;
797 stream() << "}";
798 } else {
799 stream() << "ScriptAction {";
800 m_indentLevel++;
801 stream() << "script:" << targetName << "." << propertyName;
802 if (animationType == AnimationType::ColorOpacity)
803 stream(flags: SameLine) << ".a";
804
805 stream(flags: SameLine) << " = ";
806 if (value.typeId() == QMetaType::QVector3D) {
807 const QVector3D &v = value.value<QVector3D>();
808 stream(flags: SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
809 } else if (value.typeId() == QMetaType::QColor) {
810 stream(flags: SameLine) << "\"" << value.toString() << "\"";
811 } else {
812 stream(flags: SameLine) << value.toReal();
813 }
814 m_indentLevel--;
815 stream() << "}";
816 }
817}
818
819void QQuickQmlGenerator::generateAnimateTransform(const QString &targetName, const NodeInfo &info)
820{
821 if (!info.transform.isAnimated())
822 return;
823
824 const QString mainAnimationId = targetName
825 + QStringLiteral("_transform_animation");
826
827 QString prefix;
828 if (Q_UNLIKELY(!isRuntimeGenerator()))
829 prefix = QStringLiteral(".animations");
830 stream() << "Connections { target: " << m_topLevelIdString << prefix << "; function onRestart() {" << mainAnimationId << ".restart() } }";
831
832 stream() << "ParallelAnimation {";
833 m_indentLevel++;
834
835 stream() << "id:" << mainAnimationId;
836
837 generateAnimationBindings();
838 for (int groupIndex = 0; groupIndex < info.transform.animationGroupCount(); ++groupIndex) {
839 int animationStart = info.transform.animationGroup(index: groupIndex);
840 int nextAnimationStart = groupIndex + 1 < info.transform.animationGroupCount()
841 ? info.transform.animationGroup(index: groupIndex + 1)
842 : info.transform.animationCount();
843
844 // The first animation in the group holds the shared properties for the whole group
845 const QQuickAnimatedProperty::PropertyAnimation &firstAnimation = info.transform.animation(index: animationStart);
846 const bool freeze = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd;
847 const bool replace = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::ReplacePreviousAnimations;
848
849 stream() << "SequentialAnimation {";
850 m_indentLevel++;
851
852 const int startOffset = firstAnimation.startOffset;
853 if (startOffset > 0)
854 stream() << "PauseAnimation { duration: " << startOffset << " }";
855
856 const int repeatCount = firstAnimation.repeatCount;
857 if (repeatCount < 0)
858 stream() << "loops: Animation.Infinite";
859 else
860 stream() << "loops: " << repeatCount;
861
862 if (replace) {
863 stream() << "ScriptAction {";
864 m_indentLevel++;
865
866 stream() << "script: " << targetName << "_transform_base_group"
867 << ".activateOverride(" << targetName << "_transform_group_" << groupIndex << ")";
868
869 m_indentLevel--;
870 stream() << "}";
871 }
872
873 stream() << "ParallelAnimation {";
874 m_indentLevel++;
875
876 for (int i = animationStart; i < nextAnimationStart; ++i) {
877 const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(index: i);
878 if (animation.isConstant())
879 continue;
880 bool hasRotationCenter = false;
881 if (animation.subtype == QTransform::TxRotate) {
882 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
883 const QPointF center = it->value<QVariantList>().value(i: 0).value<QPointF>();
884 if (!center.isNull()) {
885 hasRotationCenter = true;
886 break;
887 }
888 }
889 }
890
891 stream() << "SequentialAnimation {";
892 m_indentLevel++;
893
894 int previousTime = 0;
895 QVariantList previousParameters;
896 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
897 const int time = it.key();
898 const int frameTime = time - previousTime;
899 const QVariantList &parameters = it.value().value<QVariantList>();
900 if (parameters.isEmpty())
901 continue;
902
903 if (parameters == previousParameters) {
904 if (frameTime > 0)
905 stream() << "PauseAnimation { duration: " << frameTime << " }";
906 } else {
907 stream() << "ParallelAnimation {";
908 m_indentLevel++;
909
910 const QString propertyTargetName = targetName
911 + QStringLiteral("_transform_")
912 + QString::number(groupIndex)
913 + QStringLiteral("_")
914 + QString::number(i);
915
916 switch (animation.subtype) {
917 case QTransform::TxTranslate:
918 {
919 const QPointF translation = parameters.first().value<QPointF>();
920
921 generateAnimatedPropertySetter(targetName: propertyTargetName,
922 QStringLiteral("x"),
923 value: translation.x(),
924 animation,
925 frameTime,
926 time);
927 generateAnimatedPropertySetter(targetName: propertyTargetName,
928 QStringLiteral("y"),
929 value: translation.y(),
930 animation,
931 frameTime,
932 time);
933 break;
934 }
935 case QTransform::TxScale:
936 {
937 const QPointF scale = parameters.first().value<QPointF>();
938 generateAnimatedPropertySetter(targetName: propertyTargetName,
939 QStringLiteral("xScale"),
940 value: scale.x(),
941 animation,
942 frameTime,
943 time);
944 generateAnimatedPropertySetter(targetName: propertyTargetName,
945 QStringLiteral("yScale"),
946 value: scale.y(),
947 animation,
948 frameTime,
949 time);
950 break;
951 }
952 case QTransform::TxRotate:
953 {
954 Q_ASSERT(parameters.size() == 2);
955 const qreal angle = parameters.value(i: 1).toReal();
956 if (hasRotationCenter) {
957 const QPointF center = parameters.value(i: 0).value<QPointF>();
958 generateAnimatedPropertySetter(targetName: propertyTargetName,
959 QStringLiteral("origin"),
960 value: QVector3D(center.x(), center.y(), 0.0),
961 animation,
962 frameTime,
963 time);
964 }
965 generateAnimatedPropertySetter(targetName: propertyTargetName,
966 QStringLiteral("angle"),
967 value: angle,
968 animation,
969 frameTime,
970 time);
971 break;
972 }
973 case QTransform::TxShear:
974 {
975 const QPointF skew = parameters.first().value<QPointF>();
976
977 generateAnimatedPropertySetter(targetName: propertyTargetName,
978 QStringLiteral("xAngle"),
979 value: skew.x(),
980 animation,
981 frameTime,
982 time);
983
984 generateAnimatedPropertySetter(targetName: propertyTargetName,
985 QStringLiteral("yAngle"),
986 value: skew.y(),
987 animation,
988 frameTime,
989 time);
990 break;
991 }
992 default:
993 Q_UNREACHABLE();
994 }
995
996 m_indentLevel--;
997 stream() << "}"; // Parallel key frame animation
998 }
999
1000 previousTime = time;
1001 previousParameters = parameters;
1002 }
1003
1004 m_indentLevel--;
1005 stream() << "}"; // Parallel key frame animation
1006 }
1007
1008 m_indentLevel--;
1009 stream() << "}"; // Parallel key frame animation
1010
1011 // If the animation ever finishes, then we add an action on the end that handles itsr
1012 // freeze state
1013 if (firstAnimation.repeatCount >= 0) {
1014 stream() << "ScriptAction {";
1015 m_indentLevel++;
1016
1017 stream() << "script: {";
1018 m_indentLevel++;
1019
1020 if (!freeze) {
1021 stream() << targetName << "_transform_base_group.deactivate("
1022 << targetName << "_transform_group_" << groupIndex << ")";
1023 } else if (!replace) {
1024 stream() << targetName << "_transform_base_group.deactivateOverride("
1025 << targetName << "_transform_group_" << groupIndex << ")";
1026 }
1027
1028 m_indentLevel--;
1029 stream() << "}";
1030
1031 m_indentLevel--;
1032 stream() << "}";
1033 }
1034
1035 m_indentLevel--;
1036 stream() << "}";
1037 }
1038
1039 m_indentLevel--;
1040 stream() << "}";
1041}
1042
1043bool QQuickQmlGenerator::generateStructureNode(const StructureNodeInfo &info)
1044{
1045 if (!isNodeVisible(info))
1046 return false;
1047
1048 const bool isPathContainer = !info.forceSeparatePaths && info.isPathContainer;
1049 if (info.stage == StructureNodeStage::Start) {
1050 if (isPathContainer) {
1051 generatePathContainer(info);
1052 } else if (info.layerNum >= 0) {
1053 stream() << "LayerItem {";
1054 } else {
1055 stream() << "Item { // Structure node";
1056 }
1057
1058 if (!info.viewBox.isEmpty()) {
1059 m_indentLevel++;
1060 stream() << "transform: [";
1061 m_indentLevel++;
1062 bool translate = !qFuzzyIsNull(d: info.viewBox.x()) || !qFuzzyIsNull(d: info.viewBox.y());
1063 if (translate)
1064 stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },";
1065 stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }";
1066 m_indentLevel--;
1067 stream() << "]";
1068 m_indentLevel--;
1069 }
1070
1071 m_indentLevel++;
1072 generateNodeBase(info);
1073 } else {
1074 m_indentLevel--;
1075 stream() << "}";
1076 if (isPathContainer)
1077 m_inShapeItemLevel--;
1078 }
1079
1080 return true;
1081}
1082
1083bool QQuickQmlGenerator::generateRootNode(const StructureNodeInfo &info)
1084{
1085 const QStringList comments = m_commentString.split(sep: u'\n');
1086
1087 if (!isNodeVisible(info)) {
1088 m_indentLevel = 0;
1089
1090 if (comments.isEmpty()) {
1091 stream() << "// Generated from SVG";
1092 } else {
1093 for (const auto &comment : comments)
1094 stream() << "// " << comment;
1095 }
1096
1097 stream() << "import QtQuick";
1098 stream() << "import QtQuick.Shapes" << Qt::endl;
1099 stream() << "Item {";
1100 m_indentLevel++;
1101
1102 double w = info.size.width();
1103 double h = info.size.height();
1104 if (w > 0)
1105 stream() << "implicitWidth: " << w;
1106 if (h > 0)
1107 stream() << "implicitHeight: " << h;
1108
1109 m_indentLevel--;
1110 stream() << "}";
1111
1112 return false;
1113 }
1114
1115 if (info.stage == StructureNodeStage::Start) {
1116 m_indentLevel = 0;
1117
1118 if (comments.isEmpty())
1119 stream() << "// Generated from SVG";
1120 else
1121 for (const auto &comment : comments)
1122 stream() << "// " << comment;
1123
1124 stream() << "import QtQuick";
1125 stream() << "import QtQuick.VectorImage";
1126 stream() << "import QtQuick.VectorImage.Helpers";
1127 stream() << "import QtQuick.Shapes";
1128 for (const auto &import : std::as_const(t&: m_extraImports))
1129 stream() << "import " << import;
1130
1131 stream() << Qt::endl << "Item {";
1132 m_indentLevel++;
1133
1134 double w = info.size.width();
1135 double h = info.size.height();
1136 if (w > 0)
1137 stream() << "implicitWidth: " << w;
1138 if (h > 0)
1139 stream() << "implicitHeight: " << h;
1140
1141 if (Q_UNLIKELY(!isRuntimeGenerator())) {
1142 stream() << "component AnimationsInfo : QtObject";
1143 stream() << "{";
1144 m_indentLevel++;
1145 }
1146
1147 stream() << "property bool paused: false";
1148 stream() << "property int loops: 1";
1149 stream() << "signal restart()";
1150
1151 if (Q_UNLIKELY(!isRuntimeGenerator())) {
1152 m_indentLevel--;
1153 stream() << "}";
1154 stream() << "property AnimationsInfo animations : AnimationsInfo {}";
1155 }
1156
1157 if (!info.viewBox.isEmpty()) {
1158 stream() << "transform: [";
1159 m_indentLevel++;
1160 bool translate = !qFuzzyIsNull(d: info.viewBox.x()) || !qFuzzyIsNull(d: info.viewBox.y());
1161 if (translate)
1162 stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },";
1163 stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }";
1164 m_indentLevel--;
1165 stream() << "]";
1166 }
1167
1168 if (!info.forceSeparatePaths && info.isPathContainer) {
1169 m_topLevelIdString = QStringLiteral("__qt_toplevel");
1170 stream() << "id: " << m_topLevelIdString;
1171
1172 generatePathContainer(info);
1173 m_indentLevel++;
1174
1175 generateNodeBase(info);
1176 } else {
1177 m_topLevelIdString = generateNodeBase(info);
1178 }
1179 } else {
1180 if (m_inShapeItemLevel > 0) {
1181 m_inShapeItemLevel--;
1182 m_indentLevel--;
1183 stream() << "}";
1184 }
1185
1186 for (const auto [coords, id] : m_easings.asKeyValueRange()) {
1187 stream() << "property easingCurve " << id << ": { type: Easing.BezierSpline; bezierCurve: [ ";
1188 for (auto coord : coords)
1189 stream(flags: SameLine) << coord << ", ";
1190 stream(flags: SameLine) << "1, 1 ] }";
1191 }
1192
1193 m_indentLevel--;
1194 stream() << "}";
1195 stream().flush();
1196 }
1197
1198 return true;
1199}
1200
1201QStringView QQuickQmlGenerator::indent()
1202{
1203 static QString indentString;
1204 int indentWidth = m_indentLevel * 4;
1205 if (indentWidth > indentString.size())
1206 indentString.fill(c: QLatin1Char(' '), size: indentWidth * 2);
1207 return QStringView(indentString).first(n: indentWidth);
1208}
1209
1210QTextStream &QQuickQmlGenerator::stream(int flags)
1211{
1212 if (m_stream.device() == nullptr)
1213 m_stream.setDevice(&m_result);
1214 else if (!(flags & StreamFlags::SameLine))
1215 m_stream << Qt::endl << indent();
1216 return m_stream;
1217}
1218
1219const char *QQuickQmlGenerator::shapeName() const
1220{
1221 return m_shapeTypeName.isEmpty() ? "Shape" : m_shapeTypeName.constData();
1222}
1223
1224QT_END_NAMESPACE
1225

source code of qtdeclarative/src/quickvectorimage/generator/qquickqmlgenerator.cpp