roundimage.cpp 8.47 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*****************************************************************************
 * roundimage.cpp: Custom widgets
 ****************************************************************************
 * Copyright (C) 2021 the VideoLAN team
 *
 * Authors: Prince Gupta <guptaprince8832@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/


#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "roundimage.hpp"

30
31
#include <qhashfunctions.h>

Prince Gupta's avatar
Prince Gupta committed
32
#include <QBuffer>
33
#include <QCache>
34
#include <QImage>
Prince Gupta's avatar
Prince Gupta committed
35
#include <QImageReader>
36
#include <QPainter>
37
38
39
40
41
42
43
44
45
46
47
48
#include <QPainterPath>
#include <QQuickWindow>
#include <QGuiApplication>

#ifdef QT_NETWORK_LIB
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#endif

namespace
{
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
    struct ImageCacheKey
    {
        QUrl url;
        QSize size;
        qreal radius;
    };

    bool operator ==(const ImageCacheKey &lhs, const ImageCacheKey &rhs)
    {
        return lhs.radius == rhs.radius && lhs.size == rhs.size && lhs.url == rhs.url;
    }

    uint qHash(const ImageCacheKey &key, uint seed)
    {
        QtPrivate::QHashCombine hash;
        seed = hash(seed, key.url);
        seed = hash(seed, key.size.width());
        seed = hash(seed, key.size.height());
        seed = hash(seed, key.radius);
        return seed;
    }

    // images are cached (result of RoundImageGenerator) with the cost calculated from QImage::sizeInBytes
    QCache<ImageCacheKey, QImage> imageCache(2 * 1024 * 1024); // 2 MiB

74
75
76
77
78
79
80
81
    QString getPath(const QUrl &url)
    {
        QString path = url.isLocalFile() ? url.toLocalFile() : url.toString();
        if (path.startsWith("qrc:///"))
            path.replace(0, strlen("qrc:///"), ":/");
        return path;
    }

Prince Gupta's avatar
Prince Gupta committed
82
    std::unique_ptr<QIODevice> getReadable(const QUrl &url)
83
84
85
86
87
88
89
90
91
92
    {
#ifdef QT_NETWORK_LIB
        if (url.scheme() == "http" || url.scheme() == "https")
        {
            QNetworkAccessManager networkMgr;
            networkMgr.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
            auto reply = networkMgr.get(QNetworkRequest(url));
            QEventLoop loop;
            QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
            loop.exec();
Prince Gupta's avatar
Prince Gupta committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

            class DataOwningBuffer : public QBuffer
            {
            public:
                DataOwningBuffer(const QByteArray &data) : m_data {data}
                {
                    setBuffer(&m_data);
                }

                ~DataOwningBuffer()
                {
                    close();
                    setBuffer(nullptr);
                }

            private:
                QByteArray m_data;
            };

            auto file = std::make_unique<DataOwningBuffer>(reply->readAll());
            file->open(QIODevice::ReadOnly);
            return file;
115
116
117
        }
#endif

Prince Gupta's avatar
Prince Gupta committed
118
119
120
        auto file = std::make_unique<QFile>(getPath(url));
        file->open(QIODevice::ReadOnly);
        return file;
121
122
123
124
125
126
127
128
129
130
131
132
133
134
    }
}

RoundImage::RoundImage(QQuickItem *parent) : QQuickPaintedItem {parent}
{
    if (window() || qGuiApp)
        setDPR(window() ? window()->devicePixelRatio() : qGuiApp->devicePixelRatio());

    connect(this, &QQuickItem::heightChanged, this, &RoundImage::regenerateRoundImage);
    connect(this, &QQuickItem::widthChanged, this, &RoundImage::regenerateRoundImage);
}

void RoundImage::paint(QPainter *painter)
{
135
136
    if (m_roundImage.isNull())
        return;
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
    painter->drawImage(QPointF {0., 0.}, m_roundImage, m_roundImage.rect());
}

void RoundImage::classBegin()
{
    QQuickPaintedItem::classBegin();

    m_isComponentComplete = false;
}

void RoundImage::componentComplete()
{
    QQuickPaintedItem::componentComplete();

    Q_ASSERT(!m_isComponentComplete); // classBegin is not called?
    m_isComponentComplete = true;
    if (!m_source.isEmpty())
154
155
156
        regenerateRoundImage();
    else
        m_roundImage = {};
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
}

QUrl RoundImage::source() const
{
    return m_source;
}

qreal RoundImage::radius() const
{
    return m_radius;
}

void RoundImage::setSource(QUrl source)
{
    if (m_source == source)
        return;

    m_source = source;
    emit sourceChanged(m_source);
176
    regenerateRoundImage();
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
}

void RoundImage::setRadius(qreal radius)
{
    if (m_radius == radius)
        return;

    m_radius = radius;
    emit radiusChanged(m_radius);
    regenerateRoundImage();
}

void RoundImage::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
{
    if (change == QQuickItem::ItemDevicePixelRatioHasChanged)
        setDPR(value.realValue);

    QQuickPaintedItem::itemChange(change, value);
}

void RoundImage::setDPR(const qreal value)
{
    if (m_dpr == value)
        return;

    m_dpr = value;
203
    regenerateRoundImage();
204
205
206
207
}

void RoundImage::regenerateRoundImage()
{
208
    if (!m_isComponentComplete || m_enqueuedGeneration)
209
210
211
212
213
214
215
216
217
218
        return;

    // use Qt::QueuedConnection to delay generation, so that dependent properties
    // subsequent updates can be merged, f.e when VLCStyle.scale changes
    m_enqueuedGeneration = true;

    QMetaObject::invokeMethod(this, [this] ()
    {
        m_enqueuedGeneration = false;

219
220
221
222
223
224
225
226
227
228
229
230
        const qreal scaleWidth = this->width() * m_dpr;
        const qreal scaledHeight = this->height() * m_dpr;
        const qreal scaledRadius = this->radius() * m_dpr;

        const ImageCacheKey key {source(), QSizeF {scaleWidth, scaledHeight}.toSize(), scaledRadius};
        if (auto image = imageCache.object(key)) // should only by called in mainthread
        {
            m_roundImage = *image;
            update();
            return;
        }

231
232
        // Image is generated in size factor of `m_dpr` to avoid scaling artefacts when
        // generated image is set with device pixel ratio
233
234
        m_roundImageGenerator.reset(new RoundImageGenerator(m_source, scaleWidth, scaledHeight, scaledRadius));
        connect(m_roundImageGenerator.get(), &BaseAsyncTask::result, this, [this, key]()
235
236
237
238
239
        {
            m_roundImage = m_roundImageGenerator->takeResult();
            m_roundImage.setDevicePixelRatio(m_dpr);
            m_roundImageGenerator.reset();

240
241
242
            if (!m_roundImage.isNull())
                imageCache.insert(key, new QImage(m_roundImage), m_roundImage.sizeInBytes());

243
244
245
246
247
248
249
            update();
        });

        m_roundImageGenerator->start(*QThreadPool::globalInstance());
    }, Qt::QueuedConnection);
}

250
251
252
253
254
RoundImage::RoundImageGenerator::RoundImageGenerator(const QUrl &source, qreal width, qreal height, qreal radius)
    : source(source)
    , width(width)
    , height(height)
    , radius(radius)
255
256
257
258
259
{
}

QImage RoundImage::RoundImageGenerator::execute()
{
260
    if (width <= 0 || height <= 0)
261
262
        return {};

Prince Gupta's avatar
Prince Gupta committed
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
    auto file = getReadable(source);
    if (!file || !file->isOpen())
        return {};

    QImageReader sourceReader(file.get());

    // do PreserveAspectCrop
    const QSizeF size {width, height};
    QSizeF defaultSize = sourceReader.size();
    if (!defaultSize.isValid())
        defaultSize = size;

    const qreal ratio = std::max(size.width() / defaultSize.width(), size.height() / defaultSize.height());
    const QSizeF targetSize = defaultSize * ratio;
    const QPointF alignedCenteredTopLeft {(size.width() - targetSize.width()) / 2., (size.height() - targetSize.height()) / 2.};
    sourceReader.setScaledSize(targetSize.toSize());

280
    QImage target(width, height, QImage::Format_ARGB32);
281
282
283
284
285
286
287
288
289
290
291
    if (target.isNull())
        return target;

    target.fill(Qt::transparent);

    QPainter painter;
    painter.begin(&target);
    painter.setRenderHint(QPainter::Antialiasing, true);
    painter.setRenderHint(QPainter::SmoothPixmapTransform, true);

    QPainterPath path;
292
    path.addRoundedRect(0, 0, width, height, radius, radius);
293
294
    painter.setClipPath(path);

Prince Gupta's avatar
Prince Gupta committed
295
    painter.drawImage({alignedCenteredTopLeft, targetSize}, sourceReader.read());
296
297
298
299
    painter.end();

    return target;
}