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 | #include "qsvgiconengine.h" |
4 | |
5 | #ifndef QT_NO_SVGRENDERER |
6 | |
7 | #include "qpainter.h" |
8 | #include "qpixmap.h" |
9 | #include "qsvgrenderer.h" |
10 | #include "qpixmapcache.h" |
11 | #include "qfileinfo.h" |
12 | #if QT_CONFIG(mimetype) |
13 | #include <qmimedatabase.h> |
14 | #include <qmimetype.h> |
15 | #endif |
16 | #include <QAtomicInt> |
17 | #include "qdebug.h" |
18 | #include <private/qguiapplication_p.h> |
19 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | class QSvgIconEnginePrivate : public QSharedData |
23 | { |
24 | public: |
25 | QSvgIconEnginePrivate() |
26 | : svgBuffers(0), addedPixmaps(0) |
27 | { stepSerialNum(); } |
28 | |
29 | ~QSvgIconEnginePrivate() |
30 | { delete addedPixmaps; delete svgBuffers; } |
31 | |
32 | static int hashKey(QIcon::Mode mode, QIcon::State state) |
33 | { return (((mode)<<4)|state); } |
34 | |
35 | QString pmcKey(const QSize &size, QIcon::Mode mode, QIcon::State state) |
36 | { return QLatin1String("$qt_svgicon_" ) |
37 | + QString::number(serialNum, base: 16).append(c: QLatin1Char('_')) |
38 | + QString::number((((((qint64(size.width()) << 11) | size.height()) << 11) | mode) << 4) | state, base: 16); } |
39 | |
40 | void stepSerialNum() |
41 | { serialNum = lastSerialNum.fetchAndAddRelaxed(valueToAdd: 1); } |
42 | |
43 | bool tryLoad(QSvgRenderer *renderer, QIcon::Mode mode, QIcon::State state); |
44 | QIcon::Mode loadDataForModeAndState(QSvgRenderer *renderer, QIcon::Mode mode, QIcon::State state); |
45 | |
46 | QHash<int, QString> svgFiles; |
47 | QHash<int, QByteArray> *svgBuffers; |
48 | QHash<int, QPixmap> *addedPixmaps; |
49 | int serialNum; |
50 | static QAtomicInt lastSerialNum; |
51 | }; |
52 | |
53 | QAtomicInt QSvgIconEnginePrivate::lastSerialNum; |
54 | |
55 | QSvgIconEngine::QSvgIconEngine() |
56 | : d(new QSvgIconEnginePrivate) |
57 | { |
58 | } |
59 | |
60 | QSvgIconEngine::QSvgIconEngine(const QSvgIconEngine &other) |
61 | : QIconEngine(other), d(new QSvgIconEnginePrivate) |
62 | { |
63 | d->svgFiles = other.d->svgFiles; |
64 | if (other.d->svgBuffers) |
65 | d->svgBuffers = new QHash<int, QByteArray>(*other.d->svgBuffers); |
66 | if (other.d->addedPixmaps) |
67 | d->addedPixmaps = new QHash<int, QPixmap>(*other.d->addedPixmaps); |
68 | } |
69 | |
70 | |
71 | QSvgIconEngine::~QSvgIconEngine() |
72 | { |
73 | } |
74 | |
75 | |
76 | QSize QSvgIconEngine::actualSize(const QSize &size, QIcon::Mode mode, |
77 | QIcon::State state) |
78 | { |
79 | if (d->addedPixmaps) { |
80 | QPixmap pm = d->addedPixmaps->value(key: d->hashKey(mode, state)); |
81 | if (!pm.isNull() && pm.size() == size) |
82 | return size; |
83 | } |
84 | |
85 | QPixmap pm = pixmap(size, mode, state); |
86 | if (pm.isNull()) |
87 | return QSize(); |
88 | return pm.size(); |
89 | } |
90 | |
91 | static QByteArray maybeUncompress(const QByteArray &ba) |
92 | { |
93 | #ifndef QT_NO_COMPRESS |
94 | return qUncompress(data: ba); |
95 | #else |
96 | return ba; |
97 | #endif |
98 | } |
99 | |
100 | bool QSvgIconEnginePrivate::tryLoad(QSvgRenderer *renderer, QIcon::Mode mode, QIcon::State state) |
101 | { |
102 | if (svgBuffers) { |
103 | QByteArray buf = svgBuffers->value(key: hashKey(mode, state)); |
104 | if (!buf.isEmpty()) { |
105 | buf = maybeUncompress(ba: buf); |
106 | renderer->load(contents: buf); |
107 | return true; |
108 | } |
109 | } |
110 | QString svgFile = svgFiles.value(key: hashKey(mode, state)); |
111 | if (!svgFile.isEmpty()) { |
112 | renderer->load(filename: svgFile); |
113 | return true; |
114 | } |
115 | return false; |
116 | } |
117 | |
118 | QIcon::Mode QSvgIconEnginePrivate::loadDataForModeAndState(QSvgRenderer *renderer, QIcon::Mode mode, QIcon::State state) |
119 | { |
120 | if (tryLoad(renderer, mode, state)) |
121 | return mode; |
122 | |
123 | const QIcon::State oppositeState = (state == QIcon::On) ? QIcon::Off : QIcon::On; |
124 | if (mode == QIcon::Disabled || mode == QIcon::Selected) { |
125 | const QIcon::Mode oppositeMode = (mode == QIcon::Disabled) ? QIcon::Selected : QIcon::Disabled; |
126 | if (tryLoad(renderer, mode: QIcon::Normal, state)) |
127 | return QIcon::Normal; |
128 | if (tryLoad(renderer, mode: QIcon::Active, state)) |
129 | return QIcon::Active; |
130 | if (tryLoad(renderer, mode, state: oppositeState)) |
131 | return mode; |
132 | if (tryLoad(renderer, mode: QIcon::Normal, state: oppositeState)) |
133 | return QIcon::Normal; |
134 | if (tryLoad(renderer, mode: QIcon::Active, state: oppositeState)) |
135 | return QIcon::Active; |
136 | if (tryLoad(renderer, mode: oppositeMode, state)) |
137 | return oppositeMode; |
138 | if (tryLoad(renderer, mode: oppositeMode, state: oppositeState)) |
139 | return oppositeMode; |
140 | } else { |
141 | const QIcon::Mode oppositeMode = (mode == QIcon::Normal) ? QIcon::Active : QIcon::Normal; |
142 | if (tryLoad(renderer, mode: oppositeMode, state)) |
143 | return oppositeMode; |
144 | if (tryLoad(renderer, mode, state: oppositeState)) |
145 | return mode; |
146 | if (tryLoad(renderer, mode: oppositeMode, state: oppositeState)) |
147 | return oppositeMode; |
148 | if (tryLoad(renderer, mode: QIcon::Disabled, state)) |
149 | return QIcon::Disabled; |
150 | if (tryLoad(renderer, mode: QIcon::Selected, state)) |
151 | return QIcon::Selected; |
152 | if (tryLoad(renderer, mode: QIcon::Disabled, state: oppositeState)) |
153 | return QIcon::Disabled; |
154 | if (tryLoad(renderer, mode: QIcon::Selected, state: oppositeState)) |
155 | return QIcon::Selected; |
156 | } |
157 | return QIcon::Normal; |
158 | } |
159 | |
160 | QPixmap QSvgIconEngine::pixmap(const QSize &size, QIcon::Mode mode, |
161 | QIcon::State state) |
162 | { |
163 | QPixmap pm; |
164 | |
165 | QString pmckey(d->pmcKey(size, mode, state)); |
166 | if (QPixmapCache::find(key: pmckey, pixmap: &pm)) |
167 | return pm; |
168 | |
169 | if (d->addedPixmaps) { |
170 | pm = d->addedPixmaps->value(key: d->hashKey(mode, state)); |
171 | if (!pm.isNull() && pm.size() == size) |
172 | return pm; |
173 | } |
174 | |
175 | QSvgRenderer renderer; |
176 | const QIcon::Mode loadmode = d->loadDataForModeAndState(renderer: &renderer, mode, state); |
177 | if (!renderer.isValid()) |
178 | return pm; |
179 | |
180 | QSize actualSize = renderer.defaultSize(); |
181 | if (!actualSize.isNull()) |
182 | actualSize.scale(s: size, mode: Qt::KeepAspectRatio); |
183 | |
184 | if (actualSize.isEmpty()) |
185 | return QPixmap(); |
186 | |
187 | QImage img(actualSize, QImage::Format_ARGB32_Premultiplied); |
188 | img.fill(pixel: 0x00000000); |
189 | QPainter p(&img); |
190 | renderer.render(p: &p); |
191 | p.end(); |
192 | pm = QPixmap::fromImage(image: img); |
193 | if (qobject_cast<QGuiApplication *>(object: QCoreApplication::instance())) { |
194 | if (loadmode != mode && mode != QIcon::Normal) { |
195 | const QPixmap generated = QGuiApplicationPrivate::instance()->applyQIconStyleHelper(mode, basePixmap: pm); |
196 | if (!generated.isNull()) |
197 | pm = generated; |
198 | } |
199 | } |
200 | |
201 | if (!pm.isNull()) |
202 | QPixmapCache::insert(key: pmckey, pixmap: pm); |
203 | |
204 | return pm; |
205 | } |
206 | |
207 | |
208 | void QSvgIconEngine::addPixmap(const QPixmap &pixmap, QIcon::Mode mode, |
209 | QIcon::State state) |
210 | { |
211 | if (!d->addedPixmaps) |
212 | d->addedPixmaps = new QHash<int, QPixmap>; |
213 | d->stepSerialNum(); |
214 | d->addedPixmaps->insert(key: d->hashKey(mode, state), value: pixmap); |
215 | } |
216 | |
217 | enum FileType { OtherFile, SvgFile, CompressedSvgFile }; |
218 | |
219 | static FileType fileType(const QFileInfo &fi) |
220 | { |
221 | const QString &abs = fi.absoluteFilePath(); |
222 | if (abs.endsWith(s: QLatin1String(".svg" ), cs: Qt::CaseInsensitive)) |
223 | return SvgFile; |
224 | if (abs.endsWith(s: QLatin1String(".svgz" ), cs: Qt::CaseInsensitive) |
225 | || abs.endsWith(s: QLatin1String(".svg.gz" ), cs: Qt::CaseInsensitive)) { |
226 | return CompressedSvgFile; |
227 | } |
228 | #if QT_CONFIG(mimetype) |
229 | const QString &mimeTypeName = QMimeDatabase().mimeTypeForFile(fileInfo: fi).name(); |
230 | if (mimeTypeName == QLatin1String("image/svg+xml" )) |
231 | return SvgFile; |
232 | if (mimeTypeName == QLatin1String("image/svg+xml-compressed" )) |
233 | return CompressedSvgFile; |
234 | #endif |
235 | return OtherFile; |
236 | } |
237 | |
238 | void QSvgIconEngine::addFile(const QString &fileName, const QSize &, |
239 | QIcon::Mode mode, QIcon::State state) |
240 | { |
241 | if (!fileName.isEmpty()) { |
242 | const QFileInfo fi(fileName); |
243 | const QString abs = fi.absoluteFilePath(); |
244 | const FileType type = fileType(fi); |
245 | #ifndef QT_NO_COMPRESS |
246 | if (type == SvgFile || type == CompressedSvgFile) { |
247 | #else |
248 | if (type == SvgFile) { |
249 | #endif |
250 | QSvgRenderer renderer(abs); |
251 | if (renderer.isValid()) { |
252 | d->stepSerialNum(); |
253 | d->svgFiles.insert(key: d->hashKey(mode, state), value: abs); |
254 | } |
255 | } else if (type == OtherFile) { |
256 | QPixmap pm(abs); |
257 | if (!pm.isNull()) |
258 | addPixmap(pixmap: pm, mode, state); |
259 | } |
260 | } |
261 | } |
262 | |
263 | void QSvgIconEngine::paint(QPainter *painter, const QRect &rect, |
264 | QIcon::Mode mode, QIcon::State state) |
265 | { |
266 | QSize pixmapSize = rect.size(); |
267 | if (painter->device()) |
268 | pixmapSize *= painter->device()->devicePixelRatio(); |
269 | painter->drawPixmap(r: rect, pm: pixmap(size: pixmapSize, mode, state)); |
270 | } |
271 | |
272 | QString QSvgIconEngine::key() const |
273 | { |
274 | return QLatin1String("svg" ); |
275 | } |
276 | |
277 | QIconEngine *QSvgIconEngine::clone() const |
278 | { |
279 | return new QSvgIconEngine(*this); |
280 | } |
281 | |
282 | |
283 | bool QSvgIconEngine::read(QDataStream &in) |
284 | { |
285 | d = new QSvgIconEnginePrivate; |
286 | d->svgBuffers = new QHash<int, QByteArray>; |
287 | |
288 | if (in.version() >= QDataStream::Qt_4_4) { |
289 | int isCompressed; |
290 | QHash<int, QString> fileNames; // For memoryoptimization later |
291 | in >> fileNames >> isCompressed >> *d->svgBuffers; |
292 | #ifndef QT_NO_COMPRESS |
293 | if (!isCompressed) { |
294 | for (auto it = d->svgBuffers->begin(), end = d->svgBuffers->end(); it != end; ++it) |
295 | it.value() = qCompress(data: it.value()); |
296 | } |
297 | #else |
298 | if (isCompressed) { |
299 | qWarning("QSvgIconEngine: Can not decompress SVG data" ); |
300 | d->svgBuffers->clear(); |
301 | } |
302 | #endif |
303 | int hasAddedPixmaps; |
304 | in >> hasAddedPixmaps; |
305 | if (hasAddedPixmaps) { |
306 | d->addedPixmaps = new QHash<int, QPixmap>; |
307 | in >> *d->addedPixmaps; |
308 | } |
309 | } |
310 | else { |
311 | QPixmap pixmap; |
312 | QByteArray data; |
313 | uint mode; |
314 | uint state; |
315 | int num_entries; |
316 | |
317 | in >> data; |
318 | if (!data.isEmpty()) { |
319 | #ifndef QT_NO_COMPRESS |
320 | data = qUncompress(data); |
321 | #endif |
322 | if (!data.isEmpty()) |
323 | d->svgBuffers->insert(key: d->hashKey(mode: QIcon::Normal, state: QIcon::Off), value: data); |
324 | } |
325 | in >> num_entries; |
326 | for (int i=0; i<num_entries; ++i) { |
327 | if (in.atEnd()) |
328 | return false; |
329 | in >> pixmap; |
330 | in >> mode; |
331 | in >> state; |
332 | // The pm list written by 4.3 is buggy and/or useless, so ignore. |
333 | //addPixmap(pixmap, QIcon::Mode(mode), QIcon::State(state)); |
334 | } |
335 | } |
336 | |
337 | return true; |
338 | } |
339 | |
340 | |
341 | bool QSvgIconEngine::write(QDataStream &out) const |
342 | { |
343 | if (out.version() >= QDataStream::Qt_4_4) { |
344 | int isCompressed = 0; |
345 | #ifndef QT_NO_COMPRESS |
346 | isCompressed = 1; |
347 | #endif |
348 | QHash<int, QByteArray> svgBuffers; |
349 | if (d->svgBuffers) |
350 | svgBuffers = *d->svgBuffers; |
351 | for (auto it = d->svgFiles.cbegin(), end = d->svgFiles.cend(); it != end; ++it) { |
352 | QByteArray buf; |
353 | QFile f(it.value()); |
354 | if (f.open(flags: QIODevice::ReadOnly)) |
355 | buf = f.readAll(); |
356 | #ifndef QT_NO_COMPRESS |
357 | buf = qCompress(data: buf); |
358 | #endif |
359 | svgBuffers.insert(key: it.key(), value: buf); |
360 | } |
361 | out << d->svgFiles << isCompressed << svgBuffers; |
362 | if (d->addedPixmaps) |
363 | out << (int)1 << *d->addedPixmaps; |
364 | else |
365 | out << (int)0; |
366 | } |
367 | else { |
368 | QByteArray buf; |
369 | if (d->svgBuffers) |
370 | buf = d->svgBuffers->value(key: d->hashKey(mode: QIcon::Normal, state: QIcon::Off)); |
371 | if (buf.isEmpty()) { |
372 | QString svgFile = d->svgFiles.value(key: d->hashKey(mode: QIcon::Normal, state: QIcon::Off)); |
373 | if (!svgFile.isEmpty()) { |
374 | QFile f(svgFile); |
375 | if (f.open(flags: QIODevice::ReadOnly)) |
376 | buf = f.readAll(); |
377 | } |
378 | } |
379 | #ifndef QT_NO_COMPRESS |
380 | buf = qCompress(data: buf); |
381 | #endif |
382 | out << buf; |
383 | // 4.3 has buggy handling of added pixmaps, so don't write any |
384 | out << (int)0; |
385 | } |
386 | return true; |
387 | } |
388 | |
389 | void QSvgIconEngine::virtual_hook(int id, void *data) |
390 | { |
391 | if (id == QIconEngine::IsNullHook) { |
392 | *reinterpret_cast<bool*>(data) = d->svgFiles.isEmpty() && !d->addedPixmaps && (!d->svgBuffers || d->svgBuffers->isEmpty()); |
393 | } |
394 | QIconEngine::virtual_hook(id, data); |
395 | } |
396 | |
397 | QT_END_NAMESPACE |
398 | |
399 | #endif // QT_NO_SVGRENDERER |
400 | |