1 | // Copyright (C) 2016 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 "qquickanimatedimage_p.h" |
5 | #include "qquickanimatedimage_p_p.h" |
6 | |
7 | #include <QtGui/qguiapplication.h> |
8 | #include <QtQml/qqmlinfo.h> |
9 | #include <QtQml/qqmlfile.h> |
10 | #include <QtQml/qqmlengine.h> |
11 | #include <QtGui/qmovie.h> |
12 | #if QT_CONFIG(qml_network) |
13 | #include <QtNetwork/qnetworkrequest.h> |
14 | #include <QtNetwork/qnetworkreply.h> |
15 | #endif |
16 | |
17 | QT_BEGIN_NAMESPACE |
18 | |
19 | QQuickPixmap* QQuickAnimatedImagePrivate::infoForCurrentFrame(QQmlEngine *engine) |
20 | { |
21 | if (!movie) |
22 | return nullptr; |
23 | |
24 | int current = movie->currentFrameNumber(); |
25 | if (!frameMap.contains(key: current)) { |
26 | QUrl requestedUrl; |
27 | QQuickPixmap *pixmap = nullptr; |
28 | if (engine && !movie->fileName().isEmpty()) { |
29 | requestedUrl.setUrl(url: QString::fromUtf8(utf8: "quickanimatedimage://%1#%2x%3#%4" ) |
30 | .arg(a: movie->fileName()) |
31 | .arg(a: movie->scaledSize().width()) |
32 | .arg(a: movie->scaledSize().height()) |
33 | .arg(a: current)); |
34 | } |
35 | if (!requestedUrl.isEmpty()) { |
36 | if (QQuickPixmap::isCached(url: requestedUrl, requestRegion: QRect(), requestSize: QSize(), frame: 0, options: QQuickImageProviderOptions())) |
37 | pixmap = new QQuickPixmap(engine, requestedUrl); |
38 | else |
39 | pixmap = new QQuickPixmap(requestedUrl, movie->currentImage()); |
40 | } else { |
41 | pixmap = new QQuickPixmap; |
42 | pixmap->setImage(movie->currentImage()); |
43 | } |
44 | frameMap.insert(key: current, value: pixmap); |
45 | } |
46 | |
47 | return frameMap.value(key: current); |
48 | } |
49 | |
50 | void QQuickAnimatedImagePrivate::clearCache() |
51 | { |
52 | qDeleteAll(c: frameMap); |
53 | frameMap.clear(); |
54 | } |
55 | |
56 | /*! |
57 | \qmltype AnimatedImage |
58 | \instantiates QQuickAnimatedImage |
59 | \inqmlmodule QtQuick |
60 | \inherits Image |
61 | \brief Plays animations stored as a series of images. |
62 | \ingroup qtquick-visual |
63 | |
64 | The AnimatedImage type extends the features of the \l Image type, providing |
65 | a way to play animations stored as images containing a series of frames, |
66 | such as those stored in GIF files. |
67 | |
68 | Information about the current frame and total length of the animation can be |
69 | obtained using the \l currentFrame and \l frameCount properties. You can |
70 | start, pause and stop the animation by changing the values of the \l playing |
71 | and \l paused properties. |
72 | |
73 | The full list of supported formats can be determined with QMovie::supportedFormats(). |
74 | |
75 | \section1 Example Usage |
76 | |
77 | \beginfloatleft |
78 | \image animatedimageitem.gif |
79 | \endfloat |
80 | |
81 | The following QML shows how to display an animated image and obtain information |
82 | about its state, such as the current frame and total number of frames. |
83 | The result is an animated image with a simple progress indicator underneath it. |
84 | |
85 | \b Note: When animated images are cached, every frame of the animation will be cached. |
86 | |
87 | Set cache to false if you are playing a long or large animation and you |
88 | want to conserve memory. |
89 | |
90 | If the image data comes from a sequential device (e.g. a socket), |
91 | AnimatedImage can only loop if cache is set to true. |
92 | |
93 | \clearfloat |
94 | \snippet qml/animatedimage.qml document |
95 | |
96 | \sa BorderImage, Image |
97 | */ |
98 | |
99 | /*! |
100 | \qmlproperty url QtQuick::AnimatedImage::source |
101 | |
102 | This property holds the URL that refers to the source image. |
103 | |
104 | AnimatedImage can handle any image format supported by Qt, loaded |
105 | from any URL scheme supported by Qt. It is however not compatible |
106 | with QQuickImageProvider. |
107 | */ |
108 | |
109 | /*! |
110 | \qmlproperty size QtQuick::AnimatedImage::sourceSize |
111 | |
112 | This property holds the scaled width and height of the full-frame image. |
113 | |
114 | Unlike the \l {Item::}{width} and \l {Item::}{height} properties, which scale |
115 | the painting of the image, this property sets the maximum number of pixels |
116 | stored for cached frames so that large animations do not use more |
117 | memory than necessary. |
118 | |
119 | If the original size is larger than \c sourceSize, the image is scaled down. |
120 | |
121 | The natural size of the image can be restored by setting this property to |
122 | \c undefined. |
123 | |
124 | \note \e {Changing this property dynamically causes the image source to be reloaded, |
125 | potentially even from the network, if it is not in the disk cache.} |
126 | |
127 | \sa Image::sourceSize |
128 | */ |
129 | QQuickAnimatedImage::QQuickAnimatedImage(QQuickItem *parent) |
130 | : QQuickImage(*(new QQuickAnimatedImagePrivate), parent) |
131 | { |
132 | connect(sender: this, signal: &QQuickImageBase::cacheChanged, context: this, slot: &QQuickAnimatedImage::onCacheChanged); |
133 | connect(sender: this, signal: &QQuickImageBase::currentFrameChanged, context: this, slot: &QQuickAnimatedImage::frameChanged); |
134 | connect(sender: this, signal: &QQuickImageBase::currentFrameChanged, context: this, slot: &QQuickAnimatedImage::currentFrameChanged); |
135 | connect(sender: this, signal: &QQuickImageBase::frameCountChanged, context: this, slot: &QQuickAnimatedImage::frameCountChanged); |
136 | } |
137 | |
138 | QQuickAnimatedImage::~QQuickAnimatedImage() |
139 | { |
140 | Q_D(QQuickAnimatedImage); |
141 | #if QT_CONFIG(qml_network) |
142 | if (d->reply) |
143 | d->reply->deleteLater(); |
144 | #endif |
145 | delete d->movie; |
146 | d->clearCache(); |
147 | } |
148 | |
149 | /*! |
150 | \qmlproperty bool QtQuick::AnimatedImage::paused |
151 | This property holds whether the animated image is paused. |
152 | |
153 | By default, this property is false. Set it to true when you want to pause |
154 | the animation. |
155 | */ |
156 | |
157 | bool QQuickAnimatedImage::isPaused() const |
158 | { |
159 | Q_D(const QQuickAnimatedImage); |
160 | if (!d->movie) |
161 | return d->paused; |
162 | return d->movie->state()==QMovie::Paused; |
163 | } |
164 | |
165 | void QQuickAnimatedImage::setPaused(bool pause) |
166 | { |
167 | Q_D(QQuickAnimatedImage); |
168 | if (pause == d->paused) |
169 | return; |
170 | if (!d->movie) { |
171 | d->paused = pause; |
172 | emit pausedChanged(); |
173 | } else { |
174 | d->movie->setPaused(pause); |
175 | } |
176 | } |
177 | |
178 | /*! |
179 | \qmlproperty bool QtQuick::AnimatedImage::playing |
180 | This property holds whether the animated image is playing. |
181 | |
182 | By default, this property is true, meaning that the animation |
183 | will start playing immediately. |
184 | |
185 | \b Note: this property is affected by changes to the actual playing |
186 | state of AnimatedImage. If non-animated images are used, \a playing |
187 | will need to be manually set to \a true in order to animate |
188 | following images. |
189 | \qml |
190 | AnimatedImage { |
191 | onStatusChanged: playing = (status == AnimatedImage.Ready) |
192 | } |
193 | \endqml |
194 | */ |
195 | |
196 | bool QQuickAnimatedImage::isPlaying() const |
197 | { |
198 | Q_D(const QQuickAnimatedImage); |
199 | if (!d->movie) |
200 | return d->playing; |
201 | return d->movie->state()!=QMovie::NotRunning; |
202 | } |
203 | |
204 | void QQuickAnimatedImage::setPlaying(bool play) |
205 | { |
206 | Q_D(QQuickAnimatedImage); |
207 | if (play == d->playing) |
208 | return; |
209 | if (!d->movie) { |
210 | d->playing = play; |
211 | emit playingChanged(); |
212 | return; |
213 | } |
214 | if (play) |
215 | d->movie->start(); |
216 | else |
217 | d->movie->stop(); |
218 | } |
219 | |
220 | /*! |
221 | \qmlproperty int QtQuick::AnimatedImage::currentFrame |
222 | \qmlproperty int QtQuick::AnimatedImage::frameCount |
223 | |
224 | currentFrame is the frame that is currently visible. By monitoring this property |
225 | for changes, you can animate other items at the same time as the image. |
226 | |
227 | frameCount is the number of frames in the animation. For some animation formats, |
228 | frameCount is unknown and has a value of zero. |
229 | */ |
230 | int QQuickAnimatedImage::currentFrame() const |
231 | { |
232 | Q_D(const QQuickAnimatedImage); |
233 | if (!d->movie) |
234 | return d->presetCurrentFrame; |
235 | return d->movie->currentFrameNumber(); |
236 | } |
237 | |
238 | void QQuickAnimatedImage::setCurrentFrame(int frame) |
239 | { |
240 | Q_D(QQuickAnimatedImage); |
241 | if (!d->movie) { |
242 | d->presetCurrentFrame = frame; |
243 | return; |
244 | } |
245 | d->movie->jumpToFrame(frameNumber: frame); |
246 | } |
247 | |
248 | int QQuickAnimatedImage::frameCount() const |
249 | { |
250 | Q_D(const QQuickAnimatedImage); |
251 | if (!d->movie) |
252 | return 0; |
253 | return d->movie->frameCount(); |
254 | } |
255 | |
256 | /*! |
257 | \qmlproperty real QtQuick::AnimatedImage::speed |
258 | \since QtQuick 2.11 |
259 | |
260 | This property holds the speed of the animation. |
261 | |
262 | The speed is measured in percentage of the original animated image speed. |
263 | The default speed is 1.0 (original speed). |
264 | */ |
265 | qreal QQuickAnimatedImage::speed() const |
266 | { |
267 | Q_D(const QQuickAnimatedImage); |
268 | return d->speed; |
269 | } |
270 | |
271 | void QQuickAnimatedImage::setSpeed(qreal speed) |
272 | { |
273 | Q_D(QQuickAnimatedImage); |
274 | if (d->speed != speed) { |
275 | d->speed = speed; |
276 | if (d->movie) |
277 | d->movie->setSpeed(qRound(d: speed * 100.0)); |
278 | emit speedChanged(); |
279 | } |
280 | } |
281 | |
282 | void QQuickAnimatedImage::setSource(const QUrl &url) |
283 | { |
284 | Q_D(QQuickAnimatedImage); |
285 | if (url == d->url) |
286 | return; |
287 | |
288 | #if QT_CONFIG(qml_network) |
289 | if (d->reply) { |
290 | d->reply->deleteLater(); |
291 | d->reply = nullptr; |
292 | } |
293 | #endif |
294 | |
295 | d->setImage(QImage()); |
296 | d->oldPlaying = isPlaying(); |
297 | d->setMovie(nullptr); |
298 | d->url = url; |
299 | emit sourceChanged(d->url); |
300 | |
301 | if (isComponentComplete()) |
302 | load(); |
303 | } |
304 | |
305 | void QQuickAnimatedImage::load() |
306 | { |
307 | Q_D(QQuickAnimatedImage); |
308 | |
309 | if (d->url.isEmpty()) { |
310 | d->setProgress(0); |
311 | |
312 | d->setImage(QImage()); |
313 | if (sourceSize() != d->oldSourceSize) { |
314 | d->oldSourceSize = sourceSize(); |
315 | emit sourceSizeChanged(); |
316 | } |
317 | |
318 | d->setStatus(Null); |
319 | if (isPlaying() != d->oldPlaying) |
320 | emit playingChanged(); |
321 | } else { |
322 | const qreal targetDevicePixelRatio = (window() ? window()->effectiveDevicePixelRatio() : qApp->devicePixelRatio()); |
323 | d->devicePixelRatio = 1.0; |
324 | |
325 | const auto context = qmlContext(this); |
326 | QUrl loadUrl = context ? context->resolvedUrl(d->url) : d->url; |
327 | const QUrl resolvedUrl = loadUrl; |
328 | resolve2xLocalFile(url: resolvedUrl, targetDevicePixelRatio, sourceUrl: &loadUrl, sourceDevicePixelRatio: &d->devicePixelRatio); |
329 | QString lf = QQmlFile::urlToLocalFileOrQrc(loadUrl); |
330 | |
331 | d->status = Null; // reset status, no emit |
332 | |
333 | if (!lf.isEmpty()) { |
334 | d->setMovie(new QMovie(lf)); |
335 | movieRequestFinished(); |
336 | } else { |
337 | #if QT_CONFIG(qml_network) |
338 | d->setStatus(Loading); |
339 | d->setProgress(0); |
340 | QNetworkRequest req(d->url); |
341 | req.setAttribute(code: QNetworkRequest::HttpPipeliningAllowedAttribute, value: true); |
342 | |
343 | d->reply = qmlEngine(this)->networkAccessManager()->get(request: req); |
344 | connect(sender: d->reply, signal: &QNetworkReply::finished, context: this, slot: &QQuickAnimatedImage::movieRequestFinished); |
345 | connect(sender: d->reply, SIGNAL(downloadProgress(qint64,qint64)), receiver: this, SLOT(requestProgress(qint64,qint64))); |
346 | #endif |
347 | } |
348 | } |
349 | } |
350 | |
351 | #define ANIMATEDIMAGE_MAXIMUM_REDIRECT_RECURSION 16 |
352 | |
353 | void QQuickAnimatedImage::movieRequestFinished() |
354 | { |
355 | Q_D(QQuickAnimatedImage); |
356 | |
357 | #if QT_CONFIG(qml_network) |
358 | if (d->reply) { |
359 | d->redirectCount++; |
360 | if (d->redirectCount < ANIMATEDIMAGE_MAXIMUM_REDIRECT_RECURSION) { |
361 | QVariant redirect = d->reply->attribute(code: QNetworkRequest::RedirectionTargetAttribute); |
362 | if (redirect.isValid()) { |
363 | QUrl url = d->reply->url().resolved(relative: redirect.toUrl()); |
364 | d->reply->deleteLater(); |
365 | setSource(url); |
366 | return; |
367 | } |
368 | } |
369 | |
370 | d->redirectCount=0; |
371 | d->setMovie(new QMovie(d->reply)); |
372 | } |
373 | #endif |
374 | |
375 | if (!d->movie || !d->movie->isValid()) { |
376 | const QQmlContext *context = qmlContext(this); |
377 | qmlWarning(me: this) << "Error Reading Animated Image File " |
378 | << (context ? context->resolvedUrl(d->url) : d->url).toString(); |
379 | d->setMovie(nullptr); |
380 | |
381 | d->setImage(QImage()); |
382 | if (sourceSize() != d->oldSourceSize) { |
383 | d->oldSourceSize = sourceSize(); |
384 | emit sourceSizeChanged(); |
385 | } |
386 | |
387 | d->setProgress(0); |
388 | d->setStatus(Error); |
389 | |
390 | if (isPlaying() != d->oldPlaying) |
391 | emit playingChanged(); |
392 | return; |
393 | } |
394 | |
395 | connect(sender: d->movie, signal: &QMovie::stateChanged, context: this, slot: &QQuickAnimatedImage::playingStatusChanged); |
396 | connect(sender: d->movie, signal: &QMovie::frameChanged, context: this, slot: &QQuickAnimatedImage::movieUpdate); |
397 | if (d->cache) |
398 | d->movie->setCacheMode(QMovie::CacheAll); |
399 | d->movie->setSpeed(qRound(d: d->speed * 100.0)); |
400 | |
401 | d->setProgress(1); |
402 | |
403 | bool pausedAtStart = d->paused; |
404 | if (d->movie && d->playing) |
405 | d->movie->start(); |
406 | if (d->movie && pausedAtStart) |
407 | d->movie->setPaused(true); |
408 | if (d->movie && (d->paused || !d->playing)) { |
409 | d->movie->jumpToFrame(frameNumber: d->presetCurrentFrame); |
410 | d->presetCurrentFrame = 0; |
411 | } |
412 | |
413 | QQuickPixmap *pixmap = d->infoForCurrentFrame(engine: qmlEngine(this)); |
414 | if (pixmap) { |
415 | d->setPixmap(*pixmap); |
416 | if (sourceSize() != d->oldSourceSize) { |
417 | d->oldSourceSize = sourceSize(); |
418 | emit sourceSizeChanged(); |
419 | } |
420 | } |
421 | |
422 | d->setStatus(Ready); |
423 | |
424 | if (isPlaying() != d->oldPlaying) |
425 | emit playingChanged(); |
426 | } |
427 | |
428 | void QQuickAnimatedImage::movieUpdate() |
429 | { |
430 | Q_D(QQuickAnimatedImage); |
431 | |
432 | if (!d->cache) |
433 | d->clearCache(); |
434 | |
435 | if (d->movie) { |
436 | d->setPixmap(*d->infoForCurrentFrame(engine: qmlEngine(this))); |
437 | emit QQuickImageBase::currentFrameChanged(); |
438 | } |
439 | } |
440 | |
441 | void QQuickAnimatedImage::playingStatusChanged() |
442 | { |
443 | Q_D(QQuickAnimatedImage); |
444 | |
445 | if ((d->movie->state() != QMovie::NotRunning) != d->playing) { |
446 | d->playing = (d->movie->state() != QMovie::NotRunning); |
447 | emit playingChanged(); |
448 | } |
449 | if ((d->movie->state() == QMovie::Paused) != d->paused) { |
450 | d->paused = (d->movie->state() == QMovie::Paused); |
451 | emit pausedChanged(); |
452 | } |
453 | } |
454 | |
455 | void QQuickAnimatedImage::onCacheChanged() |
456 | { |
457 | Q_D(QQuickAnimatedImage); |
458 | if (!cache()) { |
459 | d->clearCache(); |
460 | if (d->movie) |
461 | d->movie->setCacheMode(QMovie::CacheNone); |
462 | } else { |
463 | if (d->movie) |
464 | d->movie->setCacheMode(QMovie::CacheAll); |
465 | } |
466 | } |
467 | |
468 | void QQuickAnimatedImage::componentComplete() |
469 | { |
470 | QQuickItem::componentComplete(); // NOT QQuickImage |
471 | load(); |
472 | } |
473 | |
474 | void QQuickAnimatedImagePrivate::setMovie(QMovie *m) |
475 | { |
476 | if (movie == m) |
477 | return; |
478 | |
479 | Q_Q(QQuickAnimatedImage); |
480 | const int oldFrameCount = q->frameCount(); |
481 | |
482 | if (movie) { |
483 | movie->disconnect(); |
484 | movie->deleteLater(); |
485 | } |
486 | |
487 | movie = m; |
488 | clearCache(); |
489 | |
490 | if (movie) |
491 | movie->setScaledSize(sourcesize); |
492 | |
493 | if (oldFrameCount != q->frameCount()) |
494 | emit q->frameCountChanged(); |
495 | } |
496 | |
497 | QT_END_NAMESPACE |
498 | |
499 | #include "moc_qquickanimatedimage_p.cpp" |
500 | |