1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
5#include "qlottieanimation_p.h"
6
7#include <QQuickPaintedItem>
8#include <QJsonDocument>
9#include <QJsonObject>
10#include <QJsonArray>
11#include <QJsonValue>
12#include <QFile>
13#include <QPointF>
14#include <QPainter>
15#include <QImage>
16#include <QTimer>
17#include <QMetaObject>
18#include <QLoggingCategory>
19#include <QThread>
20#include <QQmlContext>
21#include <QQmlFile>
22#include <math.h>
23
24#include <QtLottie/private/qlottiebase_p.h>
25#include <QtLottie/private/qlottielayer_p.h>
26#include <QtLottie/private/qlottieconstants_p.h>
27
28
29#include <QtLottie/private/qbatchrenderer_p.h>
30#include <QtLottie/private/qlottierasterrenderer_p.h>
31
32using namespace Qt::StringLiterals;
33
34QT_BEGIN_NAMESPACE
35
36/*!
37 \qmltype LottieAnimation
38 \inqmlmodule Qt.labs.lottieqt
39 \since 5.13
40 \inherits Item
41 \brief A Lottie player for Qt.
42
43 The LottieAnimation type shows Lottie format files.
44
45 LottieAnimation is used to load and render Lottie files exported
46 from Adobe After Effects. Currently, only subset of the full Lottie
47 specification is supported. Most notable deviations are:
48
49 \list
50 \li Only Shape layer supported
51 \li Only integer frame-mode of a timeline supported
52 (real frame numbers and time are rounded to the nearest integer)
53 \li Expressions are not supported
54 \endlist
55
56 For the full list of devations, please see see the \l{Limitations}
57 section.
58
59 \section1 Example Usage
60
61 The following example shows a simple usage of the LottieAnimation type
62
63 \qml
64 LottieAnimation {
65 loops: 2
66 quality: LottieAnimation.MediumQuality
67 source: "animation.json"
68 autoPlay: false
69 onStatusChanged: {
70 if (status === LottieAnimation.Ready) {
71 // any acvities needed before
72 // playing starts go here
73 gotoAndPlay(startFrame);
74 }
75 }
76 onFinished: {
77 console.log("Finished playing")
78 }
79 }
80 \endqml
81
82 \note Changing width or height of the element does not change the size
83 of the animation within. Also, it is not possible to align the the content
84 inside of a \c LottieAnimation element. To achieve this, position the
85 animation inside e.g. an \c Item.
86
87 \section1 Rendering Performance
88
89 Internally, the rendered frame data is cached to improve performance. You
90 can control the memory usage by setting the QLOTTIE_RENDER_CACHE_SIZE
91 environment variable (default value is 2).
92
93 You can monitor the rendering performance by turning on two logging categories:
94
95 \list
96 \li \c qt.lottieqt.lottie.render - Provides information how the animation
97 is rendered
98 \li \c qt.lottieqt.lottie.render.thread - Provides information how the
99 rendering process proceeds.
100 \endlist
101
102 Specifically, you can monitor does the frame cache gets constantly full, or
103 does the rendering process have to wait for frames to become ready. The
104 first case implies that the animation is too complex, and the rendering
105 cannot keep up the pace. Try making the animation simpler, or optimize
106 the QML scene.
107*/
108
109/*!
110 \qmlproperty bool LottieAnimation::autoPlay
111
112 Defines whether the player will start playing animation automatically after
113 the animation file has been loaded.
114
115 The default value is \c true.
116*/
117
118/*!
119 \qmlproperty int LottieAnimation::loops
120
121 This property holds the number of loops the player will repeat.
122 The value \c LottieAnimation.Infinite means that the the player repeats
123 the animation continuously.
124
125 The default value is \c 1.
126*/
127
128/*!
129 \qmlsignal LottieAnimation::finished()
130
131 This signal is emitted when the player has finished playing. In case of
132 looping, the signal is emitted when the last loop has been finished.
133*/
134
135QLottieAnimation::QLottieAnimation(QQuickItem *parent)
136 : QQuickPaintedItem(parent)
137{
138 m_frameAdvance = new QTimer(this);
139 m_frameAdvance->setInterval(1000 / m_frameRate);
140 m_frameAdvance->setSingleShot(false);
141 connect (sender: m_frameAdvance, signal: &QTimer::timeout, context: this, slot: &QLottieAnimation::renderNextFrame);
142
143 m_frameRenderThread = QBatchRenderer::instance();
144
145 qRegisterMetaType<QLottieAnimation*>();
146
147 setAntialiasing(m_quality == HighQuality);
148}
149
150QLottieAnimation::~QLottieAnimation()
151{
152 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "deregisterAnimator", Q_ARG(QLottieAnimation*, this));
153}
154
155void QLottieAnimation::componentComplete()
156{
157 QQuickPaintedItem::componentComplete();
158
159 if (m_source.isValid())
160 load();
161}
162
163void QLottieAnimation::paint(QPainter *painter)
164{
165 QLottieBase* lottieTree = m_frameRenderThread->getFrame(animator: this, frameNumber: m_currentFrame);
166
167 if (!lottieTree) {
168 qCDebug(lcLottieQtLottieRender) << "QLottieAnimation::paint: Got empty element tree."
169 "Cannot draw (Animator:" << static_cast<void*>(this) << ")";
170 return;
171 }
172
173 QLottieRasterRenderer renderer(painter);
174
175 qCDebug(lcLottieQtLottieRender) << static_cast<void*>(this) << "Start to paint frame" << m_currentFrame;
176
177 for (QLottieBase *elem : lottieTree->children()) {
178 if (elem->active(frame: m_currentFrame))
179 elem->render(renderer);
180 else
181 qCDebug(lcLottieQtLottieRender) << "Element '" << elem->name() << "' inactive. No need to paint";
182 }
183
184 if (m_frameAdvance->isActive()) {
185 m_frameRenderThread->frameRendered(animator: this, frameNumber: m_currentFrame);
186 m_currentFrame += m_direction;
187
188 if (m_currentFrame < m_startFrame || m_currentFrame > m_endFrame) {
189 m_currentLoop += (m_loops > 0 ? 1 : 0);
190 }
191
192 if ((m_loops - m_currentLoop) != 0) {
193 m_currentFrame = m_currentFrame < m_startFrame ? m_endFrame :
194 m_currentFrame > m_endFrame ? m_startFrame : m_currentFrame;
195 }
196 }
197}
198
199/*!
200 \qmlproperty enumeration LottieAnimation::status
201
202 This property holds the current status of the LottieAnimation element.
203
204 \value LottieAnimation.Null
205 An initial value that is used when the source is not defined
206 (Default)
207
208 \value LottieAnimation.Loading
209 The player is loading a Lottie file
210
211 \value LottieAnimation.Ready
212 Loading has finished successfully and the player is ready to play
213 the animation
214
215 \value LottieAnimation.Error
216 An error occurred while loading the animation
217
218 For example, you could implement \c onStatusChanged signal
219 handler to monitor progress of loading an animation as follows:
220
221 \qml
222 LottieAnimation {
223 source: "animation.json"
224 autoPlay: false
225 onStatusChanged: {
226 if (status === LottieAnimation.Ready)
227 start();
228 }
229 \endqml
230*/
231QLottieAnimation::Status QLottieAnimation::status() const
232{
233 return m_status;
234}
235
236void QLottieAnimation::setStatus(QLottieAnimation::Status status)
237{
238 if (Q_UNLIKELY(m_status == status))
239 return;
240
241 m_status = status;
242 emit statusChanged();
243}
244
245/*!
246 \qmlproperty url LottieAnimation::source
247
248 The source of the Lottie asset that LottieAnimation plays.
249
250 LottieAnimation can handle any URL scheme supported by Qt.
251 The URL may be absolute, or relative to the URL of the component.
252
253 Setting the source property starts loading the animation asynchronously.
254 To monitor progress of loading, connect to the \l status change signal.
255*/
256QUrl QLottieAnimation::source() const
257{
258 return m_source;
259}
260
261void QLottieAnimation::setSource(const QUrl &source)
262{
263 if (m_source != source) {
264 m_source = source;
265 emit sourceChanged();
266
267 if (isComponentComplete())
268 load();
269 }
270}
271
272/*!
273 \qmlproperty int LottieAnimation::startFrame
274 \readonly
275
276 Frame number of the start of the animation. The value
277 is available after the animation has been loaded and
278 ready to play.
279*/
280int QLottieAnimation::startFrame() const
281{
282 return m_startFrame;
283}
284
285void QLottieAnimation::setStartFrame(int startFrame)
286{
287 if (Q_UNLIKELY(m_startFrame == startFrame))
288 return;
289
290 m_startFrame = startFrame;
291 emit startFrameChanged();
292}
293
294/*!
295 \qmlproperty int LottieAnimation::endFrame
296 \readonly
297
298 Frame number of the end of the animation. The value
299 is available after the animation has been loaded and
300 ready to play.
301*/
302int QLottieAnimation::endFrame() const
303{
304 return m_endFrame;
305}
306
307void QLottieAnimation::setEndFrame(int endFrame)
308{
309 if (Q_UNLIKELY(m_endFrame == endFrame))
310 return;
311
312 m_endFrame = endFrame;
313 emit endFrameChanged();
314}
315
316int QLottieAnimation::currentFrame() const
317{
318 return m_currentFrame;
319}
320
321QVersionNumber QLottieAnimation::version() const
322{
323 return m_version;
324}
325
326/*!
327 \qmlproperty int LottieAnimation::frameRate
328
329 This property holds the frame rate value of the Lottie animation.
330
331 \c frameRate changes after the asset has been loaded. Changing the
332 frame rate does not have effect before that, as the value defined in the
333 asset overrides the value. To change the frame rate, you can write:
334
335 \qml
336 LottieAnimation {
337 source: "animation.json"
338 onStatusChanged: {
339 if (status === LottieAnimation.Ready)
340 frameRate = 60;
341 }
342 \endqml
343*/
344int QLottieAnimation::frameRate() const
345{
346 return m_frameRate;
347}
348
349void QLottieAnimation::setFrameRate(int frameRate)
350{
351 if (Q_UNLIKELY(m_frameRate == frameRate || frameRate <= 0))
352 return;
353
354 m_frameRate = frameRate;
355 emit frameRateChanged();
356
357 m_frameAdvance->setInterval(1000 / m_frameRate);
358}
359
360void QLottieAnimation::resetFrameRate()
361{
362 setFrameRate(m_animFrameRate);
363}
364
365/*!
366 \qmlproperty enumeration LottieAnimation::quality
367
368 Speficies the rendering quality of the lottie player.
369 If \c LowQuality is selected the rendering will happen into a frame
370 buffer object, whereas with other options, the rendering will be done
371 onto \c QImage (which in turn will be rendered on the screen).
372
373 \value LottieAnimation.LowQuality
374 Antialiasing or a smooth pixmap transformation algorithm are not
375 used
376
377 \value LottieAnimation.MediumQuality
378 Smooth pixmap transformation algorithm is used but no antialiasing
379 (Default)
380
381 \value LottieAnimation.HighQuality
382 Antialiasing and a smooth pixmap tranformation algorithm are both
383 used
384*/
385QLottieAnimation::Quality QLottieAnimation::quality() const
386{
387 return m_quality;
388}
389
390void QLottieAnimation::setQuality(QLottieAnimation::Quality quality)
391{
392 if (m_quality != quality) {
393 m_quality = quality;
394 if (quality == LowQuality)
395 setRenderTarget(QQuickPaintedItem::FramebufferObject);
396 else
397 setRenderTarget(QQuickPaintedItem::Image);
398 setSmooth(quality != LowQuality);
399 setAntialiasing(quality == HighQuality);
400 emit qualityChanged();
401 }
402}
403
404void QLottieAnimation::reset()
405{
406 m_currentFrame = m_direction > 0 ? m_startFrame : m_endFrame;
407 m_currentLoop = 0;
408 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "gotoFrame",
409 Q_ARG(QLottieAnimation*, this),
410 Q_ARG(int, m_currentFrame));
411}
412
413/*!
414 \qmlmethod void LottieAnimation::start()
415
416 Starts playing the animation from the beginning.
417*/
418void QLottieAnimation::start()
419{
420 reset();
421 m_frameAdvance->start();
422}
423
424/*!
425 \qmlmethod void LottieAnimation::play()
426
427 Starts or continues playing from the current position.
428*/
429void QLottieAnimation::play()
430{
431 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "gotoFrame",
432 Q_ARG(QLottieAnimation*, this),
433 Q_ARG(int, m_currentFrame));
434 m_frameAdvance->start();
435}
436
437/*!
438 \qmlmethod void LottieAnimation::pause()
439
440 Pauses the playback.
441*/
442void QLottieAnimation::pause()
443{
444 m_frameAdvance->stop();
445 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "gotoFrame",
446 Q_ARG(QLottieAnimation*, this),
447 Q_ARG(int, m_currentFrame));
448}
449
450/*!
451 \qmlmethod void LottieAnimation::togglePause()
452
453 Toggles the status of player between playing and paused states.
454*/
455void QLottieAnimation::togglePause()
456{
457 if (m_frameAdvance->isActive()) {
458 pause();
459 } else {
460 play();
461 }
462}
463
464/*!
465 \qmlmethod void LottieAnimation::stop()
466
467 Stops the playback and returns to startFrame.
468*/
469void QLottieAnimation::stop()
470{
471 m_frameAdvance->stop();
472 reset();
473 renderNextFrame();
474}
475
476/*!
477 \qmlmethod void LottieAnimation::gotoAndPlay(int frame)
478
479 Plays the asset from the given \a frame.
480*/
481void QLottieAnimation::gotoAndPlay(int frame)
482{
483 gotoFrame(frame);
484 m_currentLoop = 0;
485 m_frameAdvance->start();
486}
487
488/*!
489 \qmlmethod bool LottieAnimation::gotoAndPlay(string frameMarker)
490
491 Plays the asset from the frame that has a marker with the given \a frameMarker.
492 Returns \c true if the frameMarker was found, \c false otherwise.
493*/
494bool QLottieAnimation::gotoAndPlay(const QString &frameMarker)
495{
496 if (m_markers.contains(key: frameMarker)) {
497 gotoAndPlay(frame: m_markers.value(key: frameMarker));
498 return true;
499 } else
500 return false;
501}
502
503/*!
504 \qmlmethod void LottieAnimation::gotoAndStop(int frame)
505
506 Moves the playhead to the given \a frame and stops.
507*/
508void QLottieAnimation::gotoAndStop(int frame)
509{
510 m_frameAdvance->stop();
511 gotoFrame(frame);
512 renderNextFrame();
513}
514
515/*!
516 \qmlmethod bool LottieAnimation::gotoAndStop(string frameMarker)
517
518 Moves the playhead to the given marker and stops.
519 Returns \c true if \a frameMarker was found, \c false otherwise.
520*/
521bool QLottieAnimation::gotoAndStop(const QString &frameMarker)
522{
523 if (m_markers.contains(key: frameMarker)) {
524 gotoAndStop(frame: m_markers.value(key: frameMarker));
525 return true;
526 } else
527 return false;
528}
529
530void QLottieAnimation::gotoFrame(int frame)
531{
532 m_currentFrame = qMax(a: m_startFrame, b: qMin(a: frame, b: m_endFrame));
533 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "gotoFrame",
534 Q_ARG(QLottieAnimation*, this),
535 Q_ARG(int, m_currentFrame));
536}
537
538/*!
539 \qmlmethod double LottieAnimation::getDuration(bool inFrames)
540
541 Returns the duration of the currently playing asset.
542
543 If a given \a inFrames is \c true, the return value is the duration in
544 number of frames. Otherwise, returns the duration in seconds.
545*/
546double QLottieAnimation::getDuration(bool inFrames)
547{
548 return (m_endFrame - m_startFrame) /
549 static_cast<double>(inFrames ? 1 : m_frameRate);
550}
551
552/*!
553 \qmlproperty enumeration LottieAnimation::direction
554
555 This property holds the direction of rendering.
556
557 \value LottieAnimation.Forward
558 Forward direction (Default)
559
560 \value LottieAnimation.Reverse
561 Reverse direction
562*/
563QLottieAnimation::Direction QLottieAnimation::direction() const
564{
565 return static_cast<Direction>(m_direction);
566}
567
568void QLottieAnimation::setDirection(QLottieAnimation::Direction direction)
569{
570 if (Q_UNLIKELY(static_cast<Direction>(m_direction) == direction))
571 return;
572
573 m_direction = direction;
574 m_currentLoop = 0;
575 emit directionChanged();
576
577 m_frameRenderThread->gotoFrame(animator: this, frame: m_currentFrame);
578}
579
580void QLottieAnimation::load()
581{
582 setStatus(Loading);
583
584 const QQmlContext *context = qmlContext(this);
585 const QUrl loadUrl = context ? context->resolvedUrl(m_source) : m_source;
586 m_file.reset(other: new QQmlFile(qmlEngine(this), loadUrl));
587 if (m_file->isLoading())
588 m_file->connectFinished(this, SLOT(loadFinished()));
589 else
590 loadFinished();
591}
592
593void QLottieAnimation::loadFinished()
594{
595 if (Q_UNLIKELY(m_file->isError())) {
596 m_file.reset();
597 setStatus(Error);
598 return;
599 }
600
601 Q_ASSERT(m_file->isReady());
602 const QByteArray json = m_file->dataByteArray();
603 m_file.reset();
604
605 if (Q_UNLIKELY(parse(json) == -1)) {
606 setStatus(Error);
607 return;
608 }
609
610 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "registerAnimator", Q_ARG(QLottieAnimation*, this));
611
612 if (m_autoPlay)
613 start();
614
615 m_frameRenderThread->start();
616
617 setStatus(Ready);
618}
619
620QByteArray QLottieAnimation::jsonSource() const
621{
622 return m_jsonSource;
623}
624
625void QLottieAnimation::renderNextFrame()
626{
627 if (m_currentFrame >= m_startFrame && m_currentFrame <= m_endFrame) {
628 if (m_frameRenderThread->getFrame(animator: this, frameNumber: m_currentFrame)) {
629 update();
630 } else if (!m_waitForFrameConn) {
631 qCDebug(lcLottieQtLottieRender) << static_cast<void*>(this)
632 << "Frame cache was empty for frame" << m_currentFrame;
633 m_waitForFrameConn = connect(sender: m_frameRenderThread, signal: &QBatchRenderer::frameReady,
634 context: this, slot: [this](QLottieAnimation *target, int frameNumber) {
635 if (target != this)
636 return;
637 qCDebug(lcLottieQtLottieRender) << static_cast<void*>(this)
638 << "Frame ready" << frameNumber;
639 disconnect(m_waitForFrameConn);
640 update();
641 });
642 }
643 } else if (m_loops == m_currentLoop) {
644 if ( m_loops != Infinite)
645 m_frameAdvance->stop();
646 emit finished();
647 }
648}
649
650int QLottieAnimation::parse(const QByteArray &jsonSource)
651{
652 m_jsonSource = jsonSource;
653
654 QJsonParseError error;
655 QJsonDocument doc = QJsonDocument::fromJson(json: m_jsonSource, error: &error);
656 if (Q_UNLIKELY(error.error != QJsonParseError::NoError)) {
657 qCWarning(lcLottieQtLottieParser)
658 << "JSON parse error:" << error.errorString();
659 return -1;
660 }
661
662 QJsonObject rootObj = doc.object();
663 if (Q_UNLIKELY(rootObj.empty()))
664 return -1;
665
666 m_version = QVersionNumber::fromString(string: rootObj.value(key: "v"_L1).toString());
667
668 int startFrame = rootObj.value(key: QLatin1String("ip")).toVariant().toInt();
669 int endFrame = rootObj.value(key: QLatin1String("op")).toVariant().toInt();
670 m_animFrameRate = rootObj.value(key: QLatin1String("fr")).toVariant().toInt();
671 m_animWidth = rootObj.value(key: QLatin1String("w")).toVariant().toReal();
672 m_animHeight = rootObj.value(key: QLatin1String("h")).toVariant().toReal();
673
674 QJsonArray markerArr = rootObj.value(key: QLatin1String("markers")).toArray();
675 QJsonArray::const_iterator markerIt = markerArr.constBegin();
676 while (markerIt != markerArr.constEnd()) {
677 QString marker = (*markerIt).toObject().value(key: QLatin1String("cm")).toString();
678 int frame = (*markerIt).toObject().value(key: QLatin1String("tm")).toInt();
679 m_markers.insert(key: marker, value: frame);
680
681 if ((*markerIt).toObject().value(key: QLatin1String("dr")).toInt())
682 qCInfo(lcLottieQtLottieParser)
683 << "property 'dr' not support in a marker";
684 ++markerIt;
685 }
686
687 if (rootObj.value(key: QLatin1String("chars")).toArray().count())
688 qCInfo(lcLottieQtLottieParser) << "chars not supported";
689
690 setWidth(m_animWidth);
691 setHeight(m_animHeight);
692 setStartFrame(startFrame);
693 setEndFrame(endFrame);
694 setFrameRate(m_animFrameRate);
695
696 return 0;
697}
698
699QT_END_NAMESPACE
700

source code of qtlottie/src/qml/qlottieanimation.cpp