Perceptual Color

asyncimageprovider.h
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4#ifndef ASYNCIMAGEPROVIDER_H
5#define ASYNCIMAGEPROVIDER_H
6
7#include "asyncimageproviderbase.h"
8#include "asyncimagerenderthread.h"
9#include "rgbcolorspacefactory.h"
10#include <optional>
11#include <qimage.h>
12#include <qobject.h>
13#include <qvariant.h>
14
15namespace PerceptualColor
16{
17/** @internal
18 *
19 * @brief Support for image caching and asynchronous rendering.
20 *
21 * This class template is intended for images whose calculation is expensive.
22 * You need a (thread-safe) rendering function, and this class template will
23 * provide automatically thread-support and image caching.
24 *
25 * @note This class template requires a running event loop.
26 *
27 * @tparam T The data type which will be used to parameterize
28 * the image.
29 *
30 * @section asyncimageproviderfeatures Features
31 *
32 * - Asynchronous API: The image calculation is done in background
33 * thread(s). Results are communicated by means of the
34 * signal @ref interlacingPassCompleted as soon as they are available.
35 * - Optional interlacing support: The rendering function can
36 * provide a low-quality image first, and then progressively
37 * better images until the final full-quality image. Since today’s
38 * high-DPI screens have more and more pixels (4K screens, perhaps
39 * one day 8K screens?), interlacing becomes increasingly important,
40 * especially with complex image calculation. The @ref InterlacingPass
41 * helper class makes it easy to implement Adam7-like interlacing.
42 * - Cache: As the image calculation might be expensive, resulting image is
43 * cached for further usage.
44 *
45 * @section asyncimagecreate How to create an object
46 *
47 * @snippet testasyncimageprovider.cpp How to create
48 *
49 * @section asyncimageuse How to use an object
50 *
51 * The cache can be accessed with @ref getCache(). Note that the
52 * cache is <em>not</em> refreshed implicitly after changing the
53 * @ref imageParameters(); therefore the cache can be out-of-date.
54 * Use @ref refreshAsync() to request explicitly a refresh.
55 *
56 * @section asyncimagefurther Further reading
57 *
58 * @sa @ref AsyncImageRenderThread::pointerToRenderFunction.
59 *
60 * @note This class template is reentrant, but <em>not</em> thread-safe!
61 *
62 * @internal
63 *
64 * @section asyncimageinternals Internals
65 *
66 * @note <a href="https://stackoverflow.com/a/63021891">The <tt>Q_OBJECT</tt>
67 * macro and templates cannot be combined.</a> Therefore,
68 * @ref AsyncImageProviderBase serves as a base class to provide
69 * signals for @ref AsyncImageProvider.
70 *
71 * @todo Possible (or even necessary?) improvement: When
72 * a widget that uses this class becomes invisible (see
73 * @ref AbstractDiagram::actualVisibilityToggledEvent for
74 * details about the type of visibility we are talking about)
75 * it might make sense to delete the cache once the image
76 * parameters change. This might reduce memory consumption (though
77 * in the moment of changing from one tab to another, anyway
78 * both widgets on these tabs will need a cache). If this is <em>not</em>
79 * implemented, @ref AbstractDiagram::actualVisibilityToggledEvent
80 * can be removed.
81 *
82 * @todo Possible (or even necessary?) improvement: If a requested image
83 * is yet either available or in computation at another object of the same
84 * template class, that this object should not trigger a new computation,
85 * but use the yet available/running one of the other object. In practice,
86 * this might be interesting for the @ref ColorWheelImage, which is likely to
87 * be used twice within the same @ref ColorDialog at exactly the same size.
88 * This requires probably a thread-safe management of instances through
89 * static class members, to make sure that the resulting objects are
90 * (while still not thread-safe themselves) at least reentrant.
91 *
92 * @todo Possible (or even necessary?) improvement: Render an image
93 * could be split to more than one thread (if actually the current computer
94 * we are running on has more than one core) to speed up the rendering.
95 *
96 * @todo Possible (or even necessary?) improvement: For @ref ChromaHueDiagram
97 * and @ref ChromaLightnessDiagram, the image cache is quite big, because
98 * we cache both, the center of the diagram and also the surrounding
99 * @ref ColorWheelImage. Could we combine both into one single cache? But
100 * if so, wouldn’t this make problems with anti-aliasing if in future versions
101 * we do not want to preserve a distance between the color wheel and the
102 * inner content anymore? And: Would this be compatible with sharing
103 * computations between various objects of the same template class to
104 * safe computation power?
105 *
106 * @todo Possible (or even necessary?) improvement: Cancel the current
107 * rendering (if any) when new image parameters are set.
108 *
109 * @todo Possible (or even necessary?) improvement: Do not cancel rendering
110 * until the first (interlacing) result has been delivered to make sure that
111 * slowly but continuously moving slider see at least sometimes updates… (and
112 * it's more likely the current value is near to the last value than to the
113 * old value still in the buffer before the user started moving the cursor
114 * at all). The performance impact should be minimal when interlacing is
115 * used. And if no interlacing is available (though we might even decide not
116 * ever to do non-interlacing rendering), the impact should also not be
117 * catastrophic either.
118 *
119 * @todo xxx Use this class for all image providers, and not only for
120 * @ref ChromaHueImageParameters.
121 *
122 * @note It would be nice to merge @ref AsyncImageProviderBase and
123 * @ref AsyncImageProvider into one single class (that is <em>not</em> a
124 * template, but image parameters are now given in form of a QVariant).
125 * It would take @ref AsyncImageRenderThread::pointerToRenderFunction as
126 * argument in the constructor to be able to call the constructor of
127 * @ref AsyncImageRenderThread.
128 * <br/>
129 * <b>Advantage:</b>
130 * <br/>
131 * → Only one class is compiled, instead of a whole bunch of template classes.
132 * The binary will therefore be smaller.
133 * <br/>
134 * <b>Disadvantage:</b>
135 * <br/>
136 * → In the future, maybe we could add support within the template for a
137 * per-class inter-object cache, so that if two objects of the same class
138 * have the same @ref imageParameters then the rendering is done only once
139 * and the result is shared between these two instances. This would
140 * obviously be impossible if there are no longer different classes
141 * for different type of images. Or it would at least require a special
142 * solution…
143 * <br/>
144 * → Calling @ref setImageParameters would be done with a <tt>QVariant</tt>
145 * (or an <tt>std::any</tt>?), so there would be no compile-time error
146 * anymore if the data type of the parameters is wrong – but is this
147 * really a big issue in practice? */
148template<typename T>
149class AsyncImageProvider final : public AsyncImageProviderBase
150{
151 // Here is no Q_OBJECT macro because it cannot be combined with templates.
152 // See https://stackoverflow.com/a/63021891 for more information.
153
154public:
155 explicit AsyncImageProvider(QObject *parent = nullptr);
156 virtual ~AsyncImageProvider() noexcept override;
157
158 [[nodiscard]] QImage getCache() const;
159 [[nodiscard]] T imageParameters() const;
160 void refreshAsync();
161 void refreshSync();
162 void setImageParameters(const T &newImageParameters);
163
164private:
165 Q_DISABLE_COPY(AsyncImageProvider)
166
167 /** @internal @brief Only for unit tests. */
168 friend class TestAsyncImageProvider;
169
170 void processInterlacingPassResult(const QImage &deliveredImage);
171
172 /** @brief The image cache. */
173 QImage m_cache;
174 /** @brief Internal storage for the image parameters.
175 *
176 * @sa @ref imageParameters()
177 * @sa @ref setImageParameters() */
178 T m_imageParameters;
179 /** @brief Information about deliverd images of the last rendering
180 * request.
181 *
182 * Is <tt>true</tt> if the last rendering request has yet
183 * delivered at least <em>one</em> image, regardless of the
184 * @ref AsyncImageRenderCallback::InterlacingState of the
185 * delivered image. Is <tt>false</tt> otherwise. */
186 bool m_lastRenderingRequestHasYetDeliveredAnImage = false;
187 /** @brief The parameters of the last rendering that has been started
188 * (if any). */
189 std::optional<T> m_lastRenderingRequestImageParameters;
190 /** @brief Provides a render thread. */
191 AsyncImageRenderThread m_renderThread;
192};
193
194/** @brief Constructor
195 * @param parent The object’s parent object. This parameter will be passed
196 * to the base class’s constructor. */
197template<typename T>
198AsyncImageProvider<T>::AsyncImageProvider(QObject *parent)
199 : AsyncImageProviderBase(parent)
200 , m_renderThread(&T::render)
201{
202 // Calling qRegisterMetaType is safe even if a given type has yet
203 // been registered before.
204 qRegisterMetaType<T>();
205 connect( //
206 &m_renderThread, //
207 &AsyncImageRenderThread::interlacingPassCompleted, //
208 this, //
209 &AsyncImageProvider<T>::processInterlacingPassResult);
210}
211
212/** @brief Destructor */
213template<typename T>
214AsyncImageProvider<T>::~AsyncImageProvider() noexcept
215{
216}
217
218/** @brief Provides the content of the cache.
219 *
220 * @returns The content of the cache. Note that a cached image might
221 * be out-of-date. The cache might also be empty, which is represented
222 * by a null image. */
223template<typename T>
224QImage AsyncImageProvider<T>::getCache() const
225{
226 // m_cache is supposed to be a null image if the cache is empty.
227 return m_cache;
228}
229
230/** @brief Setter for the image parameters.
231 *
232 * @param newImageParameters The new image parameters.
233 *
234 * @note This function does <em>not</em> trigger a new image calculation.
235 * Only @ref refreshAsync() can trigger a new image calculation.
236 *
237 * @sa @ref imageParameters()
238 *
239 * @internal
240 *
241 * @sa @ref m_imageParameters */
242// NOTE This cannot be a Q_PROPERTY as its type depends on the template
243// parameter, and Q_PROPERTY is based on Q_OBJECT which cannot be used
244// within templates.
245template<typename T>
246void AsyncImageProvider<T>::setImageParameters(const T &newImageParameters)
247{
248 m_imageParameters = newImageParameters;
249}
250
251/** @brief Getter for the image parameters.
252 *
253 * @returns The current image parameters.
254 *
255 * @sa @ref setImageParameters()
256 *
257 * @internal
258 *
259 * @sa @ref m_imageParameters */
260// NOTE This cannot be a Q_PROPERTY as its type depends on the template
261// parameter, and Q_PROPERTY is based on Q_OBJECT which cannot be used
262// within templates. */
263template<typename T>
264T AsyncImageProvider<T>::imageParameters() const
265{
266 return m_imageParameters;
267}
268
269/** @brief Receives and processes newly rendered images that are
270 * delivered from the background render process.
271 *
272 * @param deliveredImage The image (either interlaced or full-quality)
273 *
274 * @post The new image will be put into the cache and the signal
275 * @ref interlacingPassCompleted() is emitted.
276 *
277 * This function is meant to be called by the background render process to
278 * deliver more data. It <em>must</em> be called after each interlacing pass
279 * exactly one time. (If the background process does not support interlacing,
280 * it is called only once when the image rendering is done.)
281 *
282 * @note Like the whole class template, this function is not thread-safe.
283 * You <em>must</em> call it from the thread within this object lives. It is
284 * not declared as slot either (because templates and <em>Q_OBJECT</em> are
285 * incompatible). To call it from a background thread, you can however use
286 * the functor-based <tt>Qt::connect()</tt> syntax to connect to this function
287 * as long as the connection type is not direct, but queued. */
288template<typename T>
289void AsyncImageProvider<T>::processInterlacingPassResult(const QImage &deliveredImage)
290{
291 m_cache = deliveredImage;
292 Q_EMIT interlacingPassCompleted();
293}
294
295/** @brief Asynchronously triggers a refresh of the image cache (if
296 * necessary). */
297template<typename T>
298void AsyncImageProvider<T>::refreshAsync()
299{
300 if (imageParameters() == m_lastRenderingRequestImageParameters) {
301 return;
302 }
303 m_renderThread.startRenderingAsync(QVariant::fromValue(imageParameters()));
304 m_lastRenderingRequestImageParameters = imageParameters();
305}
306
307/** @brief Synchronously refreshes the image cache (if necessary). */
308template<typename T>
309void AsyncImageProvider<T>::refreshSync()
310{
311 refreshAsync();
312 m_renderThread.waitForIdle();
313}
314
315} // namespace PerceptualColor
316
317#endif // ASYNCIMAGEPROVIDER_H
The namespace of this library.
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:36 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.