File indexing completed on 2024-05-12 04:44:29

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 #ifndef ASYNCIMAGEPROVIDER_H
0005 #define ASYNCIMAGEPROVIDER_H
0006 
0007 #include "asyncimageproviderbase.h"
0008 #include "asyncimagerenderthread.h"
0009 #include "rgbcolorspacefactory.h"
0010 #include <optional>
0011 #include <qimage.h>
0012 #include <qobject.h>
0013 #include <qvariant.h>
0014 
0015 namespace PerceptualColor
0016 {
0017 /** @internal
0018  *
0019  * @brief Support for image caching and asynchronous rendering.
0020  *
0021  * This class template is intended for images whose calculation is expensive.
0022  * You need a (thread-safe) rendering function, and this class template will
0023  * provide automatically thread-support and image caching.
0024  *
0025  * @note This class template requires a running event loop.
0026  *
0027  * @tparam T The data type which will be used to parameterize
0028  * the image.
0029  *
0030  * @section asyncimageproviderfeatures Features
0031  *
0032  * - Asynchronous API: The image calculation is done in background
0033  *   thread(s). Results are communicated by means of the
0034  *   signal @ref interlacingPassCompleted as soon as they are available.
0035  * - Optional interlacing support: The rendering function can
0036  *   provide a low-quality image first, and then progressively
0037  *   better images until the final full-quality image. Since today’s
0038  *   high-DPI screens have more and more pixels (4K screens, perhaps
0039  *   one day 8K screens?), interlacing becomes increasingly important,
0040  *   especially with complex image calculation. The @ref InterlacingPass
0041  *   helper class makes it easy to implement  Adam7-like interlacing.
0042  * - Cache: As the image calculation might be expensive, resulting image is
0043  *   cached for further usage.
0044  *
0045  * @section asyncimagecreate How to create an object
0046  *
0047  * @snippet testasyncimageprovider.cpp How to create
0048  *
0049  * @section asyncimageuse How to use an object
0050  *
0051  * The cache can be accessed with @ref getCache(). Note that the
0052  * cache is <em>not</em> refreshed implicitly after changing the
0053  * @ref imageParameters(); therefore the cache can be out-of-date.
0054  * Use @ref refreshAsync() to request explicitly a refresh.
0055  *
0056  * @section asyncimagefurther Further reading
0057  *
0058  * @sa @ref AsyncImageRenderThread::pointerToRenderFunction.
0059  *
0060  * @note This class template is reentrant, but <em>not</em> thread-safe!
0061  *
0062  * @internal
0063  *
0064  * @section asyncimageinternals Internals
0065  *
0066  * @note <a href="https://stackoverflow.com/a/63021891">The <tt>Q_OBJECT</tt>
0067  * macro and templates cannot be combined.</a> Therefore,
0068  * @ref AsyncImageProviderBase serves as a base class to provide
0069  * signals for @ref AsyncImageProvider.
0070  *
0071  * @todo Possible (or even necessary?) improvement: When
0072  * a widget that uses this class becomes invisible (see
0073  * @ref AbstractDiagram::actualVisibilityToggledEvent for
0074  * details about the type of visibility we are talking about)
0075  * it might make sense to delete the cache once the image
0076  * parameters change. This might reduce memory consumption (though
0077  * in the moment of changing from one tab to another, anyway
0078  * both widgets on these tabs will need a cache). If this is <em>not</em>
0079  * implemented, @ref AbstractDiagram::actualVisibilityToggledEvent
0080  * can be removed.
0081  *
0082  * @todo Possible (or even necessary?) improvement: If a requested image
0083  * is yet either available or in computation at another object of the same
0084  * template class, that this object should not trigger a new computation,
0085  * but use the yet available/running one of the other object. In practice,
0086  * this might be interesting for the @ref ColorWheelImage, which is likely to
0087  * be used twice within the same @ref ColorDialog at exactly the same size.
0088  * This requires probably a thread-safe management of instances through
0089  * static class members, to make sure that the resulting objects are
0090  * (while still not thread-safe themselves) at least reentrant.
0091  *
0092  * @todo Possible (or even necessary?) improvement: Render an image
0093  * could be split to more than one thread (if actually the current computer
0094  * we are running on has more than one core) to speed up the rendering.
0095  *
0096  * @todo Possible (or even necessary?) improvement: For @ref ChromaHueDiagram
0097  * and @ref ChromaLightnessDiagram, the image cache is quite big, because
0098  * we cache both, the center of the diagram and also the surrounding
0099  * @ref ColorWheelImage. Could we combine both into one single cache? But
0100  * if so, wouldn’t this make problems with anti-aliasing if in future versions
0101  * we do not want to preserve a distance between the color wheel and the
0102  * inner content anymore? And: Would this be compatible with sharing
0103  * computations between various objects of the same template class to
0104  * safe computation power?
0105  *
0106  * @todo Possible (or even necessary?) improvement: Cancel the current
0107  * rendering (if any) when new image parameters are set.
0108  *
0109  * @todo Possible (or even necessary?) improvement: Do not cancel rendering
0110  * until the first (interlacing) result has been delivered to make sure that
0111  * slowly but continuously moving slider see at least sometimes updates… (and
0112  * it's more likely the current value is near to the last value than to the
0113  * old value still in the buffer before the user started moving the cursor
0114  * at all). The performance impact should be minimal when interlacing is
0115  * used. And if no interlacing is available (though we might even decide not
0116  * ever to do non-interlacing rendering), the impact should also not be
0117  * catastrophic either.
0118  *
0119  * @todo xxx Use this class for all image providers, and not only for
0120  * @ref ChromaHueImageParameters.
0121  *
0122  * @note It would be nice to merge @ref AsyncImageProviderBase and
0123  * @ref AsyncImageProvider into one single class (that is <em>not</em> a
0124  * template, but image parameters are now given in form of a QVariant).
0125  * It would take @ref AsyncImageRenderThread::pointerToRenderFunction as
0126  * argument in the constructor to be able to call the constructor of
0127  * @ref AsyncImageRenderThread.
0128  * <br/>
0129  * <b>Advantage:</b>
0130  * <br/>
0131  * → Only one class is compiled, instead of a whole bunch of template classes.
0132  *   The binary will therefore be smaller.
0133  * <br/>
0134  * <b>Disadvantage:</b>
0135  * <br/>
0136  * → In the future, maybe we could add support within the template for a
0137  *   per-class inter-object cache, so that if two objects of the same class
0138  *   have the same @ref imageParameters then the rendering is done only once
0139  *   and the result is shared between these two instances. This would
0140  *   obviously be impossible if there are no longer different classes
0141  *   for different type of images. Or it would at least require a special
0142  *   solution…
0143  * <br/>
0144  * → Calling @ref setImageParameters would be done with a <tt>QVariant</tt>
0145  *   (or an <tt>std::any</tt>?), so there would be no compile-time error
0146  *   anymore if the data type of the parameters is wrong – but is this
0147  *   really a big issue in practice?  */
0148 template<typename T>
0149 class AsyncImageProvider final : public AsyncImageProviderBase
0150 {
0151     // Here is no Q_OBJECT macro because it cannot be combined with templates.
0152     // See https://stackoverflow.com/a/63021891 for more information.
0153 
0154 public:
0155     explicit AsyncImageProvider(QObject *parent = nullptr);
0156     virtual ~AsyncImageProvider() noexcept override;
0157 
0158     [[nodiscard]] QImage getCache() const;
0159     [[nodiscard]] T imageParameters() const;
0160     void refreshAsync();
0161     void refreshSync();
0162     void setImageParameters(const T &newImageParameters);
0163 
0164 private:
0165     Q_DISABLE_COPY(AsyncImageProvider)
0166 
0167     /** @internal @brief Only for unit tests. */
0168     friend class TestAsyncImageProvider;
0169 
0170     void processInterlacingPassResult(const QImage &deliveredImage);
0171 
0172     /** @brief The image cache. */
0173     QImage m_cache;
0174     /** @brief Internal storage for the image parameters.
0175      *
0176      * @sa @ref imageParameters()
0177      * @sa @ref setImageParameters() */
0178     T m_imageParameters;
0179     /** @brief Information about deliverd images of the last rendering
0180      * request.
0181      *
0182      * Is <tt>true</tt> if the last rendering request has yet
0183      * delivered at least <em>one</em> image, regardless of the
0184      * @ref AsyncImageRenderCallback::InterlacingState of the
0185      * delivered image. Is <tt>false</tt> otherwise. */
0186     bool m_lastRenderingRequestHasYetDeliveredAnImage = false;
0187     /** @brief The parameters of the last rendering that has been started
0188      * (if any). */
0189     std::optional<T> m_lastRenderingRequestImageParameters;
0190     /** @brief Provides a render thread. */
0191     AsyncImageRenderThread m_renderThread;
0192 };
0193 
0194 /** @brief Constructor
0195  * @param parent The object’s parent object. This parameter will be passed
0196  * to the base class’s constructor. */
0197 template<typename T>
0198 AsyncImageProvider<T>::AsyncImageProvider(QObject *parent)
0199     : AsyncImageProviderBase(parent)
0200     , m_renderThread(&T::render)
0201 {
0202     // Calling qRegisterMetaType is safe even if a given type has yet
0203     // been registered before.
0204     qRegisterMetaType<T>();
0205     connect( //
0206         &m_renderThread, //
0207         &AsyncImageRenderThread::interlacingPassCompleted, //
0208         this, //
0209         &AsyncImageProvider<T>::processInterlacingPassResult);
0210 }
0211 
0212 /** @brief Destructor */
0213 template<typename T>
0214 AsyncImageProvider<T>::~AsyncImageProvider() noexcept
0215 {
0216 }
0217 
0218 /** @brief Provides the content of the cache.
0219  *
0220  * @returns The content of the cache. Note that a cached image might
0221  * be out-of-date. The cache might also be empty, which is represented
0222  * by a null image. */
0223 template<typename T>
0224 QImage AsyncImageProvider<T>::getCache() const
0225 {
0226     // m_cache is supposed to be a null image if the cache is empty.
0227     return m_cache;
0228 }
0229 
0230 /** @brief Setter for the image parameters.
0231  *
0232  * @param newImageParameters The new image parameters.
0233  *
0234  * @note This function does <em>not</em> trigger a new image calculation.
0235  * Only @ref refreshAsync() can trigger a new image calculation.
0236  *
0237  * @sa @ref imageParameters()
0238  *
0239  * @internal
0240  *
0241  * @sa @ref m_imageParameters */
0242 // NOTE This cannot be a Q_PROPERTY as its type depends on the template
0243 // parameter, and Q_PROPERTY is based on Q_OBJECT which cannot be used
0244 // within templates.
0245 template<typename T>
0246 void AsyncImageProvider<T>::setImageParameters(const T &newImageParameters)
0247 {
0248     m_imageParameters = newImageParameters;
0249 }
0250 
0251 /** @brief Getter for the image parameters.
0252  *
0253  * @returns The current image parameters.
0254  *
0255  * @sa @ref setImageParameters()
0256  *
0257  * @internal
0258  *
0259  * @sa @ref m_imageParameters */
0260 // NOTE This cannot be a Q_PROPERTY as its type depends on the template
0261 // parameter, and Q_PROPERTY is based on Q_OBJECT which cannot be used
0262 // within templates. */
0263 template<typename T>
0264 T AsyncImageProvider<T>::imageParameters() const
0265 {
0266     return m_imageParameters;
0267 }
0268 
0269 /** @brief Receives and processes newly rendered images that are
0270  * delivered from the background render process.
0271  *
0272  * @param deliveredImage The image (either interlaced or full-quality)
0273  *
0274  * @post The new image will be put into the cache and the signal
0275  * @ref interlacingPassCompleted() is emitted.
0276  *
0277  * This function is meant to be called by the background render process to
0278  * deliver more data. It <em>must</em> be called after each interlacing pass
0279  * exactly one time. (If the background process does not support interlacing,
0280  * it is called only once when the image rendering is done.)
0281  *
0282  * @note Like the whole class template, this function is not thread-safe.
0283  * You <em>must</em> call it from the thread within this object lives. It is
0284  * not declared as slot either (because templates and <em>Q_OBJECT</em> are
0285  * incompatible). To call it from a background thread, you can however use
0286  * the functor-based <tt>Qt::connect()</tt> syntax to connect to this function
0287  * as long as the connection type is not direct, but queued. */
0288 template<typename T>
0289 void AsyncImageProvider<T>::processInterlacingPassResult(const QImage &deliveredImage)
0290 {
0291     m_cache = deliveredImage;
0292     Q_EMIT interlacingPassCompleted();
0293 }
0294 
0295 /** @brief Asynchronously triggers a refresh of the image cache (if
0296  * necessary). */
0297 template<typename T>
0298 void AsyncImageProvider<T>::refreshAsync()
0299 {
0300     if (imageParameters() == m_lastRenderingRequestImageParameters) {
0301         return;
0302     }
0303     m_renderThread.startRenderingAsync(QVariant::fromValue(imageParameters()));
0304     m_lastRenderingRequestImageParameters = imageParameters();
0305 }
0306 
0307 /** @brief Synchronously refreshes the image cache (if necessary). */
0308 template<typename T>
0309 void AsyncImageProvider<T>::refreshSync()
0310 {
0311     refreshAsync();
0312     m_renderThread.waitForIdle();
0313 }
0314 
0315 } // namespace PerceptualColor
0316 
0317 #endif // ASYNCIMAGEPROVIDER_H