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 "qquickimagebase_p.h" |
5 | #include "qquickimagebase_p_p.h" |
6 | |
7 | #include <QtGui/qguiapplication.h> |
8 | #include <QtGui/qscreen.h> |
9 | #include <QtGui/qicon.h> |
10 | |
11 | #include <QtQml/qqmlinfo.h> |
12 | #include <QtQml/qqmlfile.h> |
13 | #include <QtQml/qqmlabstracturlinterceptor.h> |
14 | |
15 | |
16 | QT_BEGIN_NAMESPACE |
17 | |
18 | bool isScalableImageFormat(const QUrl &url) |
19 | { |
20 | if (url.scheme() == QLatin1String("image" )) |
21 | return true; |
22 | |
23 | const QString stringUrl = url.path(options: QUrl::PrettyDecoded); |
24 | return stringUrl.endsWith(s: QLatin1String("svg" )) |
25 | || stringUrl.endsWith(s: QLatin1String("svgz" )) |
26 | || stringUrl.endsWith(s: QLatin1String("pdf" )); |
27 | } |
28 | |
29 | // This function gives derived classes the chance set the devicePixelRatio |
30 | // if they're not happy with our implementation of it. |
31 | bool QQuickImageBasePrivate::updateDevicePixelRatio(qreal targetDevicePixelRatio) |
32 | { |
33 | // QQuickImageProvider and SVG and PDF can generate a high resolution image when |
34 | // sourceSize is set. If sourceSize is not set then the provider default size will |
35 | // be used, as usual. |
36 | const bool setDevicePixelRatio = isScalableImageFormat(url); |
37 | |
38 | if (setDevicePixelRatio) |
39 | devicePixelRatio = targetDevicePixelRatio; |
40 | |
41 | return setDevicePixelRatio; |
42 | } |
43 | |
44 | void QQuickImageBasePrivate::setStatus(QQuickImageBase::Status value) |
45 | { |
46 | Q_Q(QQuickImageBase); |
47 | |
48 | if (status == value) |
49 | return; |
50 | |
51 | status = value; |
52 | emit q->statusChanged(status); |
53 | } |
54 | |
55 | void QQuickImageBasePrivate::setProgress(qreal value) |
56 | { |
57 | Q_Q(QQuickImageBase); |
58 | |
59 | if (qFuzzyCompare(p1: progress, p2: value)) |
60 | return; |
61 | |
62 | progress = value; |
63 | emit q->progressChanged(progress); |
64 | } |
65 | |
66 | QQuickImageBase::QQuickImageBase(QQuickItem *parent) |
67 | : QQuickImplicitSizeItem(*(new QQuickImageBasePrivate), parent) |
68 | { |
69 | setFlag(flag: ItemHasContents); |
70 | } |
71 | |
72 | QQuickImageBase::QQuickImageBase(QQuickImageBasePrivate &dd, QQuickItem *parent) |
73 | : QQuickImplicitSizeItem(dd, parent) |
74 | { |
75 | setFlag(flag: ItemHasContents); |
76 | } |
77 | |
78 | QQuickImageBase::~QQuickImageBase() |
79 | { |
80 | } |
81 | |
82 | QQuickImageBase::Status QQuickImageBase::status() const |
83 | { |
84 | Q_D(const QQuickImageBase); |
85 | return d->status; |
86 | } |
87 | |
88 | qreal QQuickImageBase::progress() const |
89 | { |
90 | Q_D(const QQuickImageBase); |
91 | return d->progress; |
92 | } |
93 | |
94 | bool QQuickImageBase::asynchronous() const |
95 | { |
96 | Q_D(const QQuickImageBase); |
97 | return d->async; |
98 | } |
99 | |
100 | void QQuickImageBase::setAsynchronous(bool async) |
101 | { |
102 | Q_D(QQuickImageBase); |
103 | if (d->async != async) { |
104 | d->async = async; |
105 | emit asynchronousChanged(); |
106 | } |
107 | } |
108 | |
109 | QUrl QQuickImageBase::source() const |
110 | { |
111 | Q_D(const QQuickImageBase); |
112 | return d->url; |
113 | } |
114 | |
115 | void QQuickImageBase::setSource(const QUrl &url) |
116 | { |
117 | Q_D(QQuickImageBase); |
118 | |
119 | if (url == d->url) |
120 | return; |
121 | |
122 | d->url = url; |
123 | emit sourceChanged(d->url); |
124 | |
125 | if (isComponentComplete()) |
126 | load(); |
127 | } |
128 | |
129 | void QQuickImageBase::setSourceSize(const QSize& size) |
130 | { |
131 | Q_D(QQuickImageBase); |
132 | if (d->sourcesize == size) |
133 | return; |
134 | |
135 | d->sourcesize = size; |
136 | emit sourceSizeChanged(); |
137 | if (isComponentComplete()) |
138 | load(); |
139 | } |
140 | |
141 | QSize QQuickImageBase::sourceSize() const |
142 | { |
143 | Q_D(const QQuickImageBase); |
144 | |
145 | int width = d->sourcesize.width(); |
146 | int height = d->sourcesize.height(); |
147 | return QSize(width != -1 ? width : d->pix.width(), height != -1 ? height : d->pix.height()); |
148 | } |
149 | |
150 | void QQuickImageBase::resetSourceSize() |
151 | { |
152 | setSourceSize(QSize()); |
153 | } |
154 | |
155 | QRectF QQuickImageBase::sourceClipRect() const |
156 | { |
157 | Q_D(const QQuickImageBase); |
158 | return d->sourceClipRect; |
159 | } |
160 | |
161 | void QQuickImageBase::setSourceClipRect(const QRectF &r) |
162 | { |
163 | Q_D(QQuickImageBase); |
164 | if (d->sourceClipRect == r) |
165 | return; |
166 | |
167 | d->sourceClipRect = r; |
168 | emit sourceClipRectChanged(); |
169 | if (isComponentComplete()) |
170 | load(); |
171 | } |
172 | |
173 | void QQuickImageBase::resetSourceClipRect() |
174 | { |
175 | setSourceClipRect(QRect()); |
176 | } |
177 | |
178 | bool QQuickImageBase::cache() const |
179 | { |
180 | Q_D(const QQuickImageBase); |
181 | return d->cache; |
182 | } |
183 | |
184 | void QQuickImageBase::setCache(bool cache) |
185 | { |
186 | Q_D(QQuickImageBase); |
187 | if (d->cache == cache) |
188 | return; |
189 | |
190 | d->cache = cache; |
191 | emit cacheChanged(); |
192 | if (isComponentComplete()) |
193 | load(); |
194 | } |
195 | |
196 | QImage QQuickImageBase::image() const |
197 | { |
198 | Q_D(const QQuickImageBase); |
199 | return d->pix.image(); |
200 | } |
201 | |
202 | void QQuickImageBase::setMirror(bool mirror) |
203 | { |
204 | Q_D(QQuickImageBase); |
205 | if (mirror == d->mirrorHorizontally) |
206 | return; |
207 | |
208 | d->mirrorHorizontally = mirror; |
209 | |
210 | if (isComponentComplete()) |
211 | update(); |
212 | |
213 | emit mirrorChanged(); |
214 | } |
215 | |
216 | bool QQuickImageBase::mirror() const |
217 | { |
218 | Q_D(const QQuickImageBase); |
219 | return d->mirrorHorizontally; |
220 | } |
221 | |
222 | void QQuickImageBase::setMirrorVertically(bool mirror) |
223 | { |
224 | Q_D(QQuickImageBase); |
225 | if (mirror == d->mirrorVertically) |
226 | return; |
227 | |
228 | d->mirrorVertically = mirror; |
229 | |
230 | if (isComponentComplete()) |
231 | update(); |
232 | |
233 | emit mirrorVerticallyChanged(); |
234 | } |
235 | |
236 | bool QQuickImageBase::mirrorVertically() const |
237 | { |
238 | Q_D(const QQuickImageBase); |
239 | return d->mirrorVertically; |
240 | } |
241 | |
242 | void QQuickImageBase::setCurrentFrame(int frame) |
243 | { |
244 | Q_D(QQuickImageBase); |
245 | if (frame == d->currentFrame || frame < 0 || (isComponentComplete() && frame >= d->pix.frameCount())) |
246 | return; |
247 | |
248 | d->currentFrame = frame; |
249 | |
250 | if (isComponentComplete()) { |
251 | if (frame > 0) |
252 | d->cache = false; |
253 | load(); |
254 | update(); |
255 | } |
256 | |
257 | emit currentFrameChanged(); |
258 | } |
259 | |
260 | int QQuickImageBase::currentFrame() const |
261 | { |
262 | Q_D(const QQuickImageBase); |
263 | return d->currentFrame; |
264 | } |
265 | |
266 | int QQuickImageBase::frameCount() const |
267 | { |
268 | Q_D(const QQuickImageBase); |
269 | return d->frameCount; |
270 | } |
271 | |
272 | void QQuickImageBase::loadEmptyUrl() |
273 | { |
274 | Q_D(QQuickImageBase); |
275 | d->pix.clear(this); |
276 | d->setProgress(0); |
277 | d->setStatus(Null); |
278 | setImplicitSize(0, 0); // also called in QQuickImageBase::pixmapChange, but not QQuickImage/QQuickBorderImage overrides |
279 | pixmapChange(); // This calls update() in QQuickBorderImage and QQuickImage, not in QQuickImageBase... |
280 | |
281 | if (sourceSize() != d->oldSourceSize) { |
282 | d->oldSourceSize = sourceSize(); |
283 | emit sourceSizeChanged(); |
284 | } |
285 | if (autoTransform() != d->oldAutoTransform) { |
286 | d->oldAutoTransform = autoTransform(); |
287 | emitAutoTransformBaseChanged(); |
288 | } |
289 | update(); // .. but double updating should be harmless |
290 | } |
291 | |
292 | void QQuickImageBase::loadPixmap(const QUrl &url, LoadPixmapOptions loadOptions) |
293 | { |
294 | Q_D(QQuickImageBase); |
295 | QQuickPixmap::Options options; |
296 | if (d->async) |
297 | options |= QQuickPixmap::Asynchronous; |
298 | if (d->cache) |
299 | options |= QQuickPixmap::Cache; |
300 | d->pix.clear(this); |
301 | QUrl loadUrl = url; |
302 | const QQmlContext *context = qmlContext(this); |
303 | if (context) |
304 | loadUrl = context->resolvedUrl(url); |
305 | |
306 | if (loadOptions & HandleDPR) { |
307 | const qreal targetDevicePixelRatio = (window() ? window()->effectiveDevicePixelRatio() : qApp->devicePixelRatio()); |
308 | d->devicePixelRatio = 1.0; |
309 | bool updatedDevicePixelRatio = false; |
310 | if (d->sourcesize.isValid() |
311 | || (isScalableImageFormat(url: d->url) && d->url.scheme() != QLatin1String("image" ))) { |
312 | updatedDevicePixelRatio = d->updateDevicePixelRatio(targetDevicePixelRatio); |
313 | } |
314 | |
315 | if (!updatedDevicePixelRatio) { |
316 | // (possible) local file: loadUrl and d->devicePixelRatio will be modified if |
317 | // an "@2x" file is found. |
318 | resolve2xLocalFile(url: context ? context->resolvedUrl(d->url) : d->url, |
319 | targetDevicePixelRatio, sourceUrl: &loadUrl, sourceDevicePixelRatio: &d->devicePixelRatio); |
320 | } |
321 | } |
322 | |
323 | d->status = Null; // reset status, no emit |
324 | |
325 | d->pix.load(qmlEngine(this), |
326 | loadUrl, |
327 | requestRegion: d->sourceClipRect.toRect(), |
328 | requestSize: (loadOptions & HandleDPR) ? d->sourcesize * d->devicePixelRatio : QSize(), |
329 | options, |
330 | providerOptions: (loadOptions & UseProviderOptions) ? d->providerOptions : QQuickImageProviderOptions(), |
331 | frame: d->currentFrame, frameCount: d->frameCount, |
332 | devicePixelRatio: d->devicePixelRatio); |
333 | |
334 | if (d->pix.isLoading()) { |
335 | d->setProgress(0); |
336 | d->setStatus(Loading); |
337 | |
338 | static int thisRequestProgress = -1; |
339 | static int thisRequestFinished = -1; |
340 | if (thisRequestProgress == -1) { |
341 | thisRequestProgress = |
342 | QQuickImageBase::staticMetaObject.indexOfSlot(slot: "requestProgress(qint64,qint64)" ); |
343 | thisRequestFinished = |
344 | QQuickImageBase::staticMetaObject.indexOfSlot(slot: "requestFinished()" ); |
345 | } |
346 | |
347 | d->pix.connectFinished(this, thisRequestFinished); |
348 | d->pix.connectDownloadProgress(this, thisRequestProgress); |
349 | update(); //pixmap may have invalidated texture, updatePaintNode needs to be called before the next repaint |
350 | } else { |
351 | requestFinished(); |
352 | } |
353 | } |
354 | |
355 | void QQuickImageBase::load() |
356 | { |
357 | Q_D(QQuickImageBase); |
358 | |
359 | if (d->url.isEmpty()) { |
360 | loadEmptyUrl(); |
361 | update(); |
362 | } else { |
363 | loadPixmap(url: d->url, loadOptions: LoadPixmapOptions(HandleDPR | UseProviderOptions)); |
364 | } |
365 | } |
366 | |
367 | void QQuickImageBase::requestFinished() |
368 | { |
369 | Q_D(QQuickImageBase); |
370 | |
371 | if (d->pix.isError()) { |
372 | qmlWarning(me: this) << d->pix.error(); |
373 | d->pix.clear(this); |
374 | d->setStatus(Error); |
375 | d->setProgress(0); |
376 | } else { |
377 | d->setStatus(Ready); |
378 | d->setProgress(1); |
379 | } |
380 | pixmapChange(); |
381 | |
382 | if (sourceSize() != d->oldSourceSize) { |
383 | d->oldSourceSize = sourceSize(); |
384 | emit sourceSizeChanged(); |
385 | } |
386 | if (autoTransform() != d->oldAutoTransform) { |
387 | d->oldAutoTransform = autoTransform(); |
388 | emitAutoTransformBaseChanged(); |
389 | } |
390 | if (d->frameCount != d->pix.frameCount()) { |
391 | d->frameCount = d->pix.frameCount(); |
392 | emit frameCountChanged(); |
393 | } |
394 | if (d->colorSpace != d->pix.colorSpace()) { |
395 | d->colorSpace = d->pix.colorSpace(); |
396 | emit colorSpaceChanged(); |
397 | } |
398 | |
399 | update(); |
400 | } |
401 | |
402 | void QQuickImageBase::requestProgress(qint64 received, qint64 total) |
403 | { |
404 | Q_D(QQuickImageBase); |
405 | if (d->status == Loading && total > 0) |
406 | d->setProgress(qreal(received) / total); |
407 | } |
408 | |
409 | void QQuickImageBase::itemChange(ItemChange change, const ItemChangeData &value) |
410 | { |
411 | Q_D(QQuickImageBase); |
412 | // If the screen DPI changed, reload image. |
413 | if (change == ItemDevicePixelRatioHasChanged && value.realValue != d->devicePixelRatio) { |
414 | const auto oldDpr = d->devicePixelRatio; |
415 | // ### how can we get here with !qmlEngine(this)? that implies |
416 | // itemChange() on an item pending deletion, which seems strange. |
417 | if (qmlEngine(this) && isComponentComplete() && d->url.isValid()) { |
418 | load(); |
419 | // not changed when loading (sourceSize might not be set) |
420 | if (d->devicePixelRatio == oldDpr) |
421 | d->updateDevicePixelRatio(targetDevicePixelRatio: value.realValue); |
422 | } |
423 | } |
424 | QQuickItem::itemChange(change, value); |
425 | } |
426 | |
427 | void QQuickImageBase::componentComplete() |
428 | { |
429 | Q_D(QQuickImageBase); |
430 | QQuickItem::componentComplete(); |
431 | if (d->url.isValid()) |
432 | load(); |
433 | } |
434 | |
435 | void QQuickImageBase::pixmapChange() |
436 | { |
437 | Q_D(QQuickImageBase); |
438 | setImplicitSize(d->pix.width() / d->devicePixelRatio, d->pix.height() / d->devicePixelRatio); |
439 | } |
440 | |
441 | void QQuickImageBase::resolve2xLocalFile(const QUrl &url, qreal targetDevicePixelRatio, QUrl *sourceUrl, qreal *sourceDevicePixelRatio) |
442 | { |
443 | Q_ASSERT(sourceUrl); |
444 | Q_ASSERT(sourceDevicePixelRatio); |
445 | |
446 | // Bail out if "@2x" image loading is disabled, don't change the source url or devicePixelRatio. |
447 | static const bool disable2xImageLoading = !qEnvironmentVariableIsEmpty(varName: "QT_HIGHDPI_DISABLE_2X_IMAGE_LOADING" ); |
448 | if (disable2xImageLoading) |
449 | return; |
450 | |
451 | const QString localFile = QQmlFile::urlToLocalFileOrQrc(url); |
452 | |
453 | // Non-local file path: @2x loading is not supported. |
454 | if (localFile.isEmpty()) |
455 | return; |
456 | |
457 | // Special case: the url in the QML source refers directly to an "@2x" file. |
458 | int atLocation = localFile.lastIndexOf(c: QLatin1Char('@')); |
459 | if (atLocation > 0 && atLocation + 3 < localFile.size()) { |
460 | if (localFile[atLocation + 1].isDigit() |
461 | && localFile[atLocation + 2] == QLatin1Char('x') |
462 | && localFile[atLocation + 3] == QLatin1Char('.')) { |
463 | *sourceDevicePixelRatio = localFile[atLocation + 1].digitValue(); |
464 | return; |
465 | } |
466 | } |
467 | |
468 | // Look for an @2x version |
469 | QString localFileX = qt_findAtNxFile(baseFileName: localFile, targetDevicePixelRatio, sourceDevicePixelRatio); |
470 | if (localFileX != localFile) |
471 | *sourceUrl = QUrl::fromLocalFile(localfile: localFileX); |
472 | } |
473 | |
474 | bool QQuickImageBase::autoTransform() const |
475 | { |
476 | Q_D(const QQuickImageBase); |
477 | if (d->providerOptions.autoTransform() == QQuickImageProviderOptions::UsePluginDefaultTransform) |
478 | return d->pix.autoTransform() == QQuickImageProviderOptions::ApplyTransform; |
479 | return d->providerOptions.autoTransform() == QQuickImageProviderOptions::ApplyTransform; |
480 | } |
481 | |
482 | void QQuickImageBase::setAutoTransform(bool transform) |
483 | { |
484 | Q_D(QQuickImageBase); |
485 | if (d->providerOptions.autoTransform() != QQuickImageProviderOptions::UsePluginDefaultTransform && |
486 | transform == (d->providerOptions.autoTransform() == QQuickImageProviderOptions::ApplyTransform)) |
487 | return; |
488 | d->providerOptions.setAutoTransform(transform ? QQuickImageProviderOptions::ApplyTransform : QQuickImageProviderOptions::DoNotApplyTransform); |
489 | emitAutoTransformBaseChanged(); |
490 | } |
491 | |
492 | QColorSpace QQuickImageBase::colorSpace() const |
493 | { |
494 | Q_D(const QQuickImageBase); |
495 | return d->colorSpace; |
496 | } |
497 | |
498 | void QQuickImageBase::setColorSpace(const QColorSpace &colorSpace) |
499 | { |
500 | Q_D(QQuickImageBase); |
501 | if (d->colorSpace == colorSpace) |
502 | return; |
503 | d->colorSpace = colorSpace; |
504 | d->providerOptions.setTargetColorSpace(colorSpace); |
505 | emit colorSpaceChanged(); |
506 | } |
507 | |
508 | QT_END_NAMESPACE |
509 | |
510 | #include "moc_qquickimagebase_p.cpp" |
511 | |